Skip to main content

siglus_scene_vm/
vm.rs

1//! Scene VM
2
3mod close;
4mod early_save;
5mod short_save;
6use crate::original_save::NativeLocalLayout;
7mod indexed_save;
8
9use anyhow::{Result, anyhow, bail};
10use std::collections::BTreeMap;
11use std::fmt::Write as _;
12use std::sync::Arc;
13
14use crate::elm_code;
15use crate::runtime::forms::codes;
16use crate::runtime::globals::{
17    ObjectFrameActionState, PendingButtonAction, PendingButtonActionKind, PendingFrameActionFinish,
18};
19use crate::runtime::{
20    self, CommandContext, RuntimeLoadRequest, RuntimeSaveKind, RuntimeSaveRequest, Value, constants,
21};
22use crate::scene_stream::SceneStream;
23use siglus_assets::scene_pck::{ScenePck, ScenePckDecodeOptions};
24
25// VM tracing is diagnostic-only. Keep the disabled path to a single cached
26// branch: the message expression is evaluated only when the trace filter
27// matches, so format!/Debug formatting does not pollute the interpreter hot
28// path.
29macro_rules! vm_trace {
30    ($vm:expr, $pc:expr, $msg:expr $(,)?) => {{
31        // The flag is tested inline: this runs for every opcode and push.
32        if $vm.vm_trace_config.enabled && $vm.vm_trace_matches() {
33            $vm.vm_trace_emit($pc, $msg);
34        }
35    }};
36}
37
38// SG_* diagnostic traces are intentionally lazy as well.  A disabled trace must
39// not allocate Strings, snapshot stacks, or format Debug values in the VM hot
40// path.  Keep all formatting behind the cached SG_DEBUG branch.
41macro_rules! sg_omv_trace {
42    ($vm:expr, $($arg:tt)*) => {{
43        if $vm.sg_debug_enabled() {
44            $vm.sg_omv_trace_emit(format_args!($($arg)*));
45        }
46    }};
47}
48
49const CD_NONE: u8 = constants::cd::NONE;
50const CD_NL: u8 = constants::cd::NL;
51const CD_PUSH: u8 = constants::cd::PUSH;
52const CD_POP: u8 = constants::cd::POP;
53const CD_COPY: u8 = constants::cd::COPY;
54const CD_PROPERTY: u8 = constants::cd::PROPERTY;
55const CD_COPY_ELM: u8 = constants::cd::COPY_ELM;
56const CD_DEC_PROP: u8 = constants::cd::DEC_PROP;
57const CD_ELM_POINT: u8 = constants::cd::ELM_POINT;
58const CD_ARG: u8 = constants::cd::ARG;
59
60const CD_GOTO: u8 = constants::cd::GOTO;
61const CD_GOTO_TRUE: u8 = constants::cd::GOTO_TRUE;
62const CD_GOTO_FALSE: u8 = constants::cd::GOTO_FALSE;
63const CD_GOSUB: u8 = constants::cd::GOSUB;
64const CD_GOSUBSTR: u8 = constants::cd::GOSUBSTR;
65const CD_RETURN: u8 = constants::cd::RETURN;
66const CD_EOF: u8 = constants::cd::EOF;
67
68const CD_ASSIGN: u8 = constants::cd::ASSIGN;
69const CD_OPERATE_1: u8 = constants::cd::OPERATE_1;
70const CD_OPERATE_2: u8 = constants::cd::OPERATE_2;
71
72const CD_COMMAND: u8 = constants::cd::COMMAND;
73const CD_TEXT: u8 = constants::cd::TEXT;
74const CD_NAME: u8 = constants::cd::NAME;
75const CD_SEL_BLOCK_START: u8 = constants::cd::SEL_BLOCK_START;
76const CD_SEL_BLOCK_END: u8 = constants::cd::SEL_BLOCK_END;
77
78const OP_PLUS: u8 = constants::op::PLUS;
79const OP_MINUS: u8 = constants::op::MINUS;
80const OP_MULTIPLE: u8 = constants::op::MULTIPLE;
81const OP_DIVIDE: u8 = constants::op::DIVIDE;
82const OP_AMARI: u8 = constants::op::AMARI;
83
84const OP_EQUAL: u8 = constants::op::EQUAL;
85const OP_NOT_EQUAL: u8 = constants::op::NOT_EQUAL;
86const OP_GREATER: u8 = constants::op::GREATER;
87const OP_GREATER_EQUAL: u8 = constants::op::GREATER_EQUAL;
88const OP_LESS: u8 = constants::op::LESS;
89const OP_LESS_EQUAL: u8 = constants::op::LESS_EQUAL;
90
91const OP_LOGICAL_AND: u8 = constants::op::LOGICAL_AND;
92const OP_LOGICAL_OR: u8 = constants::op::LOGICAL_OR;
93
94const OP_TILDE: u8 = constants::op::TILDE;
95const OP_AND: u8 = constants::op::AND;
96const OP_OR: u8 = constants::op::OR;
97const OP_HAT: u8 = constants::op::HAT;
98const OP_SL: u8 = constants::op::SL;
99const OP_SR: u8 = constants::op::SR;
100const OP_SR3: u8 = constants::op::SR3;
101
102// Early Siglus element tables exposed GLOBAL.WORLD at element slot 47.  Later
103// tables moved WORLD under STAGE and reused global slot 47 for MSGBTN.  Retail
104// Rewrite-era Scene.pck files still contain the old [47, ARRAY, world, ...]
105// chains, so they must be recognized structurally rather than by the current
106// element name.
107const LEGACY_GLOBAL_WORLD_ELEMENT: i32 = 47;
108
109// C++ initializes cur_call.L / cur_call.K from Gameexe CALL_FLAG.CNT (default 50).
110
111// -----------------------------------------------------------------------------
112// VM configuration (form codes are game-specific, so keep them injectable)
113// -----------------------------------------------------------------------------
114
115#[derive(Debug, Clone, Copy)]
116pub struct VmConfig {
117    pub fm_void: i32,
118    pub fm_int: i32,
119    pub fm_str: i32,
120    pub fm_label: i32,
121    pub fm_list: i32,
122    pub fm_intlist: i32,
123    pub fm_strlist: i32,
124    pub max_steps: u64,
125}
126
127impl VmConfig {
128    pub fn from_env() -> Self {
129        fn env_u64(key: &str, default: u64) -> u64 {
130            std::env::var(key)
131                .ok()
132                .and_then(|v| v.parse::<u64>().ok())
133                .unwrap_or(default)
134        }
135
136        Self {
137            fm_void: constants::fm::VOID,
138            fm_int: constants::fm::INT,
139            fm_str: constants::fm::STR,
140            fm_label: constants::fm::LABEL,
141            fm_list: constants::fm::LIST,
142            fm_intlist: constants::fm::INTLIST,
143            fm_strlist: constants::fm::STRLIST,
144            max_steps: env_u64("SIGLUS_VM_MAX_STEPS", 0),
145        }
146    }
147}
148
149#[derive(Debug, Clone)]
150struct VmTraceConfig {
151    enabled: bool,
152    scene: Option<String>,
153    pc_range: Option<(usize, usize)>,
154    commands_enabled: bool,
155}
156
157impl VmTraceConfig {
158    fn from_env() -> Self {
159        let enabled = env_is_set!("SIGLUS_TRACE_VM");
160        let scene = std::env::var("SIGLUS_TRACE_VM_SCENE")
161            .ok()
162            .filter(|value| !value.is_empty());
163        let pc_range = std::env::var("SIGLUS_TRACE_VM_PC").ok().and_then(|range| {
164            let (start, end) = range.split_once("..")?;
165            let parse = |value: &str| {
166                usize::from_str_radix(value.trim_start_matches("0x"), 16)
167                    .or_else(|_| value.parse::<usize>())
168                    .ok()
169            };
170            Some((parse(start)?, parse(end)?))
171        });
172
173        Self {
174            enabled,
175            scene,
176            pc_range,
177            commands_enabled: env_is_set!("SIGLUS_TRACE_VM_COMMANDS"),
178        }
179    }
180}
181
182#[derive(Debug, Clone, Copy)]
183struct VmRuntimeOptions {
184    inline_user_cmd_max_steps: u64,
185    frame_action_max_steps: u64,
186    trace_unknown_forms: bool,
187    proc_flow_trace: bool,
188    sg_debug: bool,
189    syscom_proc_trace: bool,
190    tick_trace: bool,
191    frame_action_trace: bool,
192    title_chain_trace: bool,
193    save_load_trace: bool,
194    trace_call_return_pc: bool,
195    trace_frame_action_call: bool,
196}
197
198impl VmRuntimeOptions {
199    fn from_env() -> Self {
200        fn env_u64(key: &str) -> u64 {
201            std::env::var(key)
202                .ok()
203                .and_then(|value| value.parse::<u64>().ok())
204                .unwrap_or(0)
205        }
206
207        let sg_debug = env_is_set!("SG_DEBUG");
208        Self {
209            inline_user_cmd_max_steps: env_u64("SIGLUS_INLINE_USER_CMD_MAX_STEPS"),
210            frame_action_max_steps: env_u64("SIGLUS_FRAME_ACTION_MAX_STEPS"),
211            trace_unknown_forms: env_is_set!("SIGLUS_TRACE_UNKNOWN_FORMS"),
212            proc_flow_trace: env_is_set!("SG_PROC_FLOW_TRACE"),
213            sg_debug,
214            syscom_proc_trace: sg_debug || env_is_set!("SG_SYSCOM_PROC_TRACE"),
215            tick_trace: env_is_set!("SG_TICK_TRACE"),
216            frame_action_trace: env_is_set!("SG_FRAME_ACTION_TRACE"),
217            title_chain_trace: env_is_set!("SG_TITLE_CHAIN_TRACE"),
218            save_load_trace: env_is_set!("SG_SAVELOAD_TRACE"),
219            trace_call_return_pc: env_is_set!("SIGLUS_TRACE_CALL_RETURN_PC"),
220            trace_frame_action_call: env_is_set!("SIGLUS_TRACE_FRAME_ACTION_CALL"),
221        }
222    }
223}
224
225#[derive(Debug, Clone, Copy)]
226struct InlineExecCheckpoint {
227    scene_no: Option<usize>,
228    pc: usize,
229    line_no: i32,
230    ctx_line_no: i64,
231    halted: bool,
232    int_len: usize,
233    str_len: usize,
234    element_point_len: usize,
235    call_depth: usize,
236    gosub_depth: usize,
237    scene_depth: usize,
238    caller_return: Option<(usize, i32)>,
239}
240
241#[derive(Debug, Clone)]
242struct CallProp {
243    scn_no: i32,
244    prop_id: i32,
245    form: i32,
246    decl_size: usize,
247    element: Vec<i32>,
248    value: CallPropValue,
249}
250
251#[derive(Debug, Clone)]
252enum CallPropValue {
253    Int(i32),
254    Str(String),
255    Element(Vec<i32>),
256    IntList(Vec<i32>),
257    StrList(Vec<String>),
258}
259
260#[derive(Debug, Clone)]
261struct CallFrame {
262    /// Original E_tnm_call_type on the callee frame: 0 NONE, 1 GOSUB, 2 FARCALL, 3 USER_CMD.
263    call_type: i32,
264    return_pc: usize,
265    // C_elm_call::m_call_save identifies the lexer state owned by this frame.
266    // It is also the only scene-boundary metadata available in the original
267    // save format.
268    return_scene_no: Option<usize>,
269    return_scene_name: Option<String>,
270    return_line_no: i32,
271    ret_form: i32,
272    /// Rust-only per-callee continuation for inline user-command/frame-action
273    /// execution. Original C++ saves do not serialize this field; loaded frames
274    /// keep it as None and use the caller-frame continuation.
275    return_override: Option<(usize, i32)>,
276    excall_proc: bool,
277    frame_action_proc: bool,
278    arg_cnt: usize,
279    delayed_ret_form: Option<i32>,
280    user_props: Vec<CallProp>,
281    int_args: Vec<i32>,
282    str_args: Vec<String>,
283}
284
285#[derive(Debug, Clone)]
286struct UserPropCell {
287    form: i32,
288    int_value: i32,
289    str_value: String,
290    element: Vec<i32>,
291    int_list: Vec<i32>,
292    str_list: Vec<String>,
293    list_items: Vec<UserPropCell>,
294}
295
296impl UserPropCell {
297    fn new(form: i32, element: Vec<i32>) -> Self {
298        Self {
299            form,
300            int_value: 0,
301            str_value: String::new(),
302            element,
303            int_list: Vec::new(),
304            str_list: Vec::new(),
305            list_items: Vec::new(),
306        }
307    }
308}
309
310#[derive(Debug)]
311struct SceneExecFrame<'a> {
312    // The original engine keeps a single VM stack/call-list across scene calls.
313    // Only lexer state and scene-local property selection change.  Keep the
314    // caller stream by move (not clone) and leave all VM stacks resident.
315    stream: SceneStream<'a>,
316    user_cmd_names: Arc<std::collections::HashMap<u32, String>>,
317    call_cmd_names: Arc<std::collections::HashMap<u32, String>>,
318    current_scene_no: Option<usize>,
319    current_scene_name: Option<String>,
320    current_line_no: i32,
321    /// Call-stack depth after the cross-scene callee frame is pushed.
322    /// A RETURN at this exact depth is the scene boundary return.
323    call_depth: usize,
324}
325
326#[derive(Debug, Clone)]
327struct ResolvedUserCommand {
328    encoded_no: usize,
329    name: String,
330    target_scene_no: usize,
331    target_offset: usize,
332    include_command: bool,
333}
334
335fn siglus_name_eq(lhs: &str, rhs: &str) -> bool {
336    lhs.eq_ignore_ascii_case(rhs)
337}
338
339fn find_named_index(names: &std::collections::HashMap<u32, String>, target: &str) -> Option<usize> {
340    names.iter().find_map(|(no, name)| {
341        if siglus_name_eq(name, target) {
342            Some(*no as usize)
343        } else {
344            None
345        }
346    })
347}
348
349fn resolve_named_user_command_number(
350    include_names: &std::collections::HashMap<u32, String>,
351    local_names: &std::collections::HashMap<u32, String>,
352    include_count: usize,
353    target: &str,
354) -> Option<(usize, bool)> {
355    if let Some(no) = find_named_index(include_names, target) {
356        return Some((no, true));
357    }
358    find_named_index(local_names, target).map(|no| (include_count + no, false))
359}
360
361#[derive(Debug, Clone)]
362struct RuntimeDiskSnapshot {
363    scene_name: String,
364    scene_no: i32,
365    line_no: i32,
366    pc: i32,
367    int_stack: Vec<i32>,
368    str_stack: Vec<String>,
369    element_points: Vec<usize>,
370    call_stack: Vec<CallFrame>,
371}
372
373fn resize_i64_vec(mut v: Vec<i64>, n: usize) -> Vec<i64> {
374    v.resize(n, 0);
375    v
376}
377
378fn resize_string_vec(mut v: Vec<String>, n: usize) -> Vec<String> {
379    v.resize_with(n, String::new);
380    v
381}
382
383#[derive(Clone)]
384struct VmResumePoint<'a> {
385    stream: SceneStream<'a>,
386    user_cmd_names: Arc<std::collections::HashMap<u32, String>>,
387    call_cmd_names: Arc<std::collections::HashMap<u32, String>>,
388    int_stack: Vec<i32>,
389    str_stack: Vec<String>,
390    element_points: Vec<usize>,
391    call_stack: Vec<CallFrame>,
392    gosub_return_stack: Vec<(usize, i32)>,
393    user_props: BTreeMap<u16, UserPropCell>,
394    scene_user_props: BTreeMap<usize, BTreeMap<u16, UserPropCell>>,
395    current_scene_no: Option<usize>,
396    current_scene_name: Option<String>,
397    current_line_no: i32,
398    globals: runtime::globals::GlobalState,
399}
400
401pub struct SceneVm<'a> {
402    pub cfg: VmConfig,
403    vm_trace_config: VmTraceConfig,
404    runtime_options: VmRuntimeOptions,
405    call_flag_count: usize,
406    stream: SceneStream<'a>,
407
408    pub ctx: CommandContext,
409
410    // Stack model: separate int/str stacks plus element point list.
411    int_stack: Vec<i32>,
412    str_stack: Vec<String>,
413    element_points: Vec<usize>,
414
415    call_stack: Vec<CallFrame>,
416    call_frame_pool: Vec<CallFrame>,
417    gosub_return_stack: Vec<(usize, i32)>,
418    user_props: BTreeMap<u16, UserPropCell>,
419    // Original C++ keeps one scene-property list per scene in
420    // Gp_user_scn_prop_list[scene_no]. Only the active scene is projected into
421    // user_props; inactive scene-local values remain resident here.
422    scene_user_props: BTreeMap<usize, BTreeMap<u16, UserPropCell>>,
423    scene_stack: Vec<SceneExecFrame<'a>>,
424    save_point: Option<VmResumePoint<'a>>,
425    sel_point_stack: Vec<VmResumePoint<'a>>,
426    current_scene_no: Option<usize>,
427    current_scene_name: Option<String>,
428    current_line_no: i32,
429
430    pub unknown_opcodes: BTreeMap<u8, u64>,
431    pub unknown_forms: BTreeMap<i32, u64>,
432    // Warn-once dedup for form command chains the runtime cannot dispatch yet.
433    // A hard failure here would freeze the host tick loop with no visible
434    // symptom, so unhandled chains are counted and skipped instead.
435    steps: u64,
436    halted: bool,
437
438    // When a command triggers a VM wait (movie wait-key etc.), its return value is produced when the wait completes.
439    delayed_ret_form: Option<i32>,
440    script_input_synced_this_frame: bool,
441    yield_safe_after_step: bool,
442
443    user_cmd_names: Arc<std::collections::HashMap<u32, String>>,
444    call_cmd_names: Arc<std::collections::HashMap<u32, String>>,
445
446    // C++ keeps the lexer / scene package resident. Do not reload and rebuild
447    // Scene.pck for frame-action callbacks or scene-local user command calls.
448    scene_pck_cache: Option<ScenePck>,
449    scene_pck_append_dir: Option<String>,
450    scene_stream_cache: BTreeMap<usize, SceneStream<'a>>,
451    scene_name_resolve_cache: std::collections::HashMap<String, Option<usize>>,
452    user_cmd_resolve_cache:
453        std::collections::HashMap<usize, std::collections::HashMap<String, ResolvedUserCommand>>,
454}
455
456#[derive(Debug, Clone, Copy, Default)]
457pub struct SceneMemoryStats {
458    pub scene_pck_bytes: usize,
459    pub cached_scene_streams: usize,
460}
461
462#[derive(Debug, Clone)]
463struct FrameActionWork {
464    stage_idx: i64,
465    obj_idx: usize,
466    ch_idx: Option<usize>,
467    global_form_id: Option<u32>,
468    object_chain: Option<Vec<i32>>,
469    frame_action_chain: Option<Vec<i32>>,
470    scn_name: String,
471    cmd_name: String,
472    args: Vec<Value>,
473    count: i64,
474    end_time: i64,
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478enum FrameActionObjectRoot {
479    Stage {
480        obj_idx: usize,
481        child_pos: usize,
482    },
483    Mwnd {
484        mwnd_idx: usize,
485        selector: i32,
486        obj_idx: usize,
487        child_pos: usize,
488    },
489    BtnSelItem {
490        item_idx: usize,
491        obj_idx: usize,
492        child_pos: usize,
493    },
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497struct FrameActionObjectLocator {
498    raw_form_id: i32,
499    stage_idx: i64,
500    root: FrameActionObjectRoot,
501}
502
503fn frame_action_stage_alias_to_index(code: i32) -> Option<i64> {
504    if code == crate::runtime::forms::codes::ELM_GLOBAL_BACK {
505        Some(0)
506    } else if code == crate::runtime::forms::codes::ELM_GLOBAL_FRONT {
507        Some(1)
508    } else if code == crate::runtime::forms::codes::ELM_GLOBAL_NEXT {
509        Some(2)
510    } else {
511        None
512    }
513}
514
515fn frame_action_array_marker(code: i32, elm_array: i32) -> bool {
516    code == elm_array || code == crate::runtime::forms::codes::ELM_ARRAY || code == -1
517}
518
519fn parse_frame_action_object_locator(
520    chain: &[i32],
521    elm_array: i32,
522) -> Option<FrameActionObjectLocator> {
523    let raw_form_id = *chain.first()?;
524    let (stage_idx, pos) = if let Some(stage_idx) = frame_action_stage_alias_to_index(raw_form_id) {
525        (stage_idx, 1usize)
526    } else {
527        if chain.len() < 4 || !frame_action_array_marker(chain[1], elm_array) {
528            return None;
529        }
530        (chain[2] as i64, 3usize)
531    };
532
533    let list_op = *chain.get(pos)?;
534    if !frame_action_array_marker(*chain.get(pos + 1)?, elm_array) {
535        return None;
536    }
537    let first_idx = (*chain.get(pos + 2)?).max(0) as usize;
538
539    let root = if list_op == crate::runtime::forms::codes::elm_value::STAGE_OBJECT {
540        FrameActionObjectRoot::Stage {
541            obj_idx: first_idx,
542            child_pos: pos + 3,
543        }
544    } else if list_op == crate::runtime::forms::codes::elm_value::STAGE_MWND {
545        let selector = *chain.get(pos + 3)?;
546        if !matches!(
547            selector,
548            crate::runtime::forms::codes::elm_value::MWND_OBJECT
549                | crate::runtime::forms::codes::elm_value::MWND_BUTTON
550                | crate::runtime::forms::codes::elm_value::MWND_FACE
551        ) || !frame_action_array_marker(*chain.get(pos + 4)?, elm_array)
552        {
553            return None;
554        }
555        FrameActionObjectRoot::Mwnd {
556            mwnd_idx: first_idx,
557            selector,
558            obj_idx: (*chain.get(pos + 5)?).max(0) as usize,
559            child_pos: pos + 6,
560        }
561    } else if list_op == crate::runtime::forms::codes::elm_value::STAGE_BTNSELITEM {
562        if *chain.get(pos + 3)? != crate::runtime::forms::codes::elm_value::BTNSELITEM_OBJECT
563            || !frame_action_array_marker(*chain.get(pos + 4)?, elm_array)
564        {
565            return None;
566        }
567        FrameActionObjectRoot::BtnSelItem {
568            item_idx: first_idx,
569            obj_idx: (*chain.get(pos + 5)?).max(0) as usize,
570            child_pos: pos + 6,
571        }
572    } else {
573        return None;
574    };
575
576    Some(FrameActionObjectLocator {
577        raw_form_id,
578        stage_idx,
579        root,
580    })
581}
582
583fn frame_action_locator_object_idx(locator: FrameActionObjectLocator) -> usize {
584    match locator.root {
585        FrameActionObjectRoot::Stage { obj_idx, .. }
586        | FrameActionObjectRoot::Mwnd { obj_idx, .. }
587        | FrameActionObjectRoot::BtnSelItem { obj_idx, .. } => obj_idx,
588    }
589}
590
591#[cfg(test)]
592mod frame_action_locator_tests {
593    use super::*;
594
595    #[test]
596    fn front_object_alias_and_canonical_stage_locate_the_same_object() {
597        let alias = [
598            crate::runtime::forms::codes::ELM_GLOBAL_FRONT,
599            crate::runtime::forms::codes::elm_value::STAGE_OBJECT,
600            -1,
601            115,
602        ];
603        let canonical = [
604            crate::runtime::forms::codes::ELM_GLOBAL_STAGE,
605            -1,
606            1,
607            crate::runtime::forms::codes::elm_value::STAGE_OBJECT,
608            -1,
609            115,
610        ];
611        let a = parse_frame_action_object_locator(&alias, -1).unwrap();
612        let b = parse_frame_action_object_locator(&canonical, -1).unwrap();
613        assert_eq!(a.stage_idx, b.stage_idx);
614        assert_eq!(frame_action_locator_object_idx(a), 115);
615        assert_eq!(frame_action_locator_object_idx(b), 115);
616    }
617
618    #[test]
619    fn front_mwnd_button_alias_keeps_both_array_indices() {
620        let alias = [
621            crate::runtime::forms::codes::ELM_GLOBAL_FRONT,
622            crate::runtime::forms::codes::elm_value::STAGE_MWND,
623            -1,
624            0,
625            crate::runtime::forms::codes::elm_value::MWND_BUTTON,
626            -1,
627            9,
628        ];
629        let canonical = [
630            crate::runtime::forms::codes::ELM_GLOBAL_STAGE,
631            -1,
632            1,
633            crate::runtime::forms::codes::elm_value::STAGE_MWND,
634            -1,
635            0,
636            crate::runtime::forms::codes::elm_value::MWND_BUTTON,
637            -1,
638            9,
639        ];
640        let a = parse_frame_action_object_locator(&alias, -1).unwrap();
641        let b = parse_frame_action_object_locator(&canonical, -1).unwrap();
642        assert_eq!(a.stage_idx, 1);
643        assert_eq!(b.stage_idx, 1);
644        assert_eq!(frame_action_locator_object_idx(a), 9);
645        assert_eq!(frame_action_locator_object_idx(b), 9);
646        assert!(matches!(
647            a.root,
648            FrameActionObjectRoot::Mwnd { mwnd_idx: 0, selector, obj_idx: 9, .. }
649                if selector == crate::runtime::forms::codes::elm_value::MWND_BUTTON
650        ));
651    }
652
653    #[test]
654    fn btnselitem_object_child_locator_starts_children_after_object_index() {
655        let chain = [
656            crate::runtime::forms::codes::ELM_GLOBAL_FRONT,
657            crate::runtime::forms::codes::elm_value::STAGE_BTNSELITEM,
658            -1,
659            2,
660            crate::runtime::forms::codes::elm_value::BTNSELITEM_OBJECT,
661            -1,
662            3,
663            crate::runtime::forms::codes::elm_value::OBJECT_CHILD,
664            -1,
665            4,
666        ];
667        let locator = parse_frame_action_object_locator(&chain, -1).unwrap();
668        assert!(matches!(
669            locator.root,
670            FrameActionObjectRoot::BtnSelItem {
671                item_idx: 2,
672                obj_idx: 3,
673                child_pos: 7
674            }
675        ));
676    }
677}
678
679struct SavedSound {
680    bgm_regist_name: String,
681    bgm_volume: u8,
682    bgm_delay_time: i64,
683    bgm_loop_flag: bool,
684    bgm_pause_flag: bool,
685    koe_volume: u8,
686    pcm_volume: u8,
687    pcmch: Vec<runtime::globals::PcmChPersistentState>,
688    se_volume: u8,
689    mov_file_name: String,
690}
691
692impl<'a> SceneVm<'a> {
693    /// HUD-only accounting. Scene streams share ScenePck's Arc buffer and do
694    /// not own duplicate scene bytecode after the Arc-backed stream fix.
695    pub fn debug_scene_memory_stats(&self) -> SceneMemoryStats {
696        SceneMemoryStats {
697            scene_pck_bytes: self.scene_pck_cache.as_ref().map_or(0, |pck| pck.buf.len()),
698            cached_scene_streams: self.scene_stream_cache.len(),
699        }
700    }
701
702    fn trace_unknown_form(&mut self, form_code: i32, site: &str) {
703        *self.unknown_forms.entry(form_code).or_insert(0) += 1;
704        if self.runtime_options.trace_unknown_forms {
705            eprintln!(
706                "[vm unknown form] site={} form={} pc=0x{:x}",
707                site,
708                form_code,
709                self.stream.get_prg_cntr()
710            );
711        }
712    }
713
714    fn configured_call_flag_count(ctx: &CommandContext) -> usize {
715        ctx.tables
716            .gameexe
717            .as_ref()
718            .and_then(|cfg| {
719                cfg.get_usize("#CALL_FLAG.CNT")
720                    .or_else(|| cfg.get_usize("CALL_FLAG.CNT"))
721            })
722            .unwrap_or(50)
723            .min(256)
724    }
725
726    fn blank_call_int_args(count: usize) -> Vec<i32> {
727        vec![0; count]
728    }
729
730    fn blank_call_str_args(count: usize) -> Vec<String> {
731        vec![String::new(); count]
732    }
733
734    fn make_call_frame(
735        &self,
736        ret_form: i32,
737        excall_proc: bool,
738        frame_action_proc: bool,
739        arg_cnt: usize,
740        scratch_args: Option<(Vec<i32>, Vec<String>)>,
741    ) -> CallFrame {
742        let (int_args, str_args) = scratch_args.unwrap_or_else(|| {
743            (
744                Self::blank_call_int_args(self.call_flag_count),
745                Self::blank_call_str_args(self.call_flag_count),
746            )
747        });
748        CallFrame {
749            call_type: 0,
750            return_pc: 0,
751            return_scene_no: None,
752            return_scene_name: None,
753            return_line_no: -1,
754            ret_form,
755            return_override: None,
756            excall_proc,
757            frame_action_proc,
758            arg_cnt,
759            delayed_ret_form: None,
760            user_props: Vec::new(),
761            int_args,
762            str_args,
763        }
764    }
765
766    fn take_call_frame(
767        &mut self,
768        ret_form: i32,
769        excall_proc: bool,
770        frame_action_proc: bool,
771        arg_cnt: usize,
772        scratch_args: Option<(Vec<i32>, Vec<String>)>,
773    ) -> CallFrame {
774        let Some(mut frame) = self.call_frame_pool.pop() else {
775            return self.make_call_frame(
776                ret_form,
777                excall_proc,
778                frame_action_proc,
779                arg_cnt,
780                scratch_args,
781            );
782        };
783
784        frame.call_type = 0;
785        frame.return_pc = 0;
786        frame.return_scene_no = None;
787        frame.return_scene_name = None;
788        frame.return_line_no = -1;
789        frame.ret_form = ret_form;
790        frame.return_override = None;
791        frame.excall_proc = excall_proc;
792        frame.frame_action_proc = frame_action_proc;
793        frame.arg_cnt = arg_cnt;
794        frame.delayed_ret_form = None;
795        frame.user_props.clear();
796
797        if let Some((int_args, str_args)) = scratch_args {
798            frame.int_args = int_args;
799            frame.str_args = str_args;
800        } else {
801            frame.int_args.resize(self.call_flag_count, 0);
802            frame.int_args.fill(0);
803            frame
804                .str_args
805                .resize_with(self.call_flag_count, String::new);
806            for value in &mut frame.str_args {
807                value.clear();
808            }
809        }
810        frame
811    }
812
813    fn recycle_call_frame(&mut self, mut frame: CallFrame) {
814        frame.user_props.clear();
815        frame.return_override = None;
816        frame.delayed_ret_form = None;
817        // C_elm_call_list::sub_call() keeps the slot allocated and add_call()
818        // reinitializes it on the next call. Keep the Rust frame allocated too
819        // instead of reallocating CALL.L / CALL.K on every callback.
820        self.call_frame_pool.push(frame);
821    }
822
823    fn shared_user_prop_count(&self) -> usize {
824        self.scene_pck_cache
825            .as_ref()
826            .map(|pck| pck.inc_props.len())
827            .unwrap_or_else(|| self.stream.header.scn_prop_cnt.max(0) as usize)
828    }
829
830    fn stash_current_scene_user_props(&mut self) {
831        let Some(scene_no) = self.current_scene_no else {
832            return;
833        };
834        let shared_count = self.shared_user_prop_count();
835        let locals = if shared_count > u16::MAX as usize {
836            BTreeMap::new()
837        } else {
838            self.user_props.split_off(&(shared_count as u16))
839        };
840        // Gp_user_scn_prop_list[scene_no] owns the scene-local cells in the
841        // original engine. Move them out of the active projection instead of
842        // cloning the complete property tree on every FARCALL/frame action.
843        self.scene_user_props.insert(scene_no, locals);
844    }
845
846    fn activate_scene_user_prop_scope(&mut self, scene_no: usize) {
847        let shared_count = self.shared_user_prop_count();
848        // Callers always stash before switching scenes. Keep this defensive
849        // trim so a malformed transition cannot expose the previous scene's
850        // local properties under the target scene.
851        if shared_count <= u16::MAX as usize {
852            let _ = self.user_props.split_off(&(shared_count as u16));
853        }
854        if let Some(mut locals) = self.scene_user_props.remove(&scene_no) {
855            self.user_props.append(&mut locals);
856        }
857    }
858
859    fn enter_cross_scene_user_prop_scope(&mut self, target_scene_no: usize) {
860        self.stash_current_scene_user_props();
861        self.activate_scene_user_prop_scope(target_scene_no);
862    }
863
864    fn restore_cross_scene_user_prop_scope(&mut self, caller_scene_no: Option<usize>) {
865        // current_scene_no still identifies the target here. Store its locals,
866        // then reactivate the caller's resident locals. Shared include
867        // properties never leave self.user_props and therefore need no clone.
868        self.stash_current_scene_user_props();
869        if let Some(scene_no) = caller_scene_no {
870            self.activate_scene_user_prop_scope(scene_no);
871        }
872    }
873
874    fn inline_exec_checkpoint(&self) -> InlineExecCheckpoint {
875        InlineExecCheckpoint {
876            scene_no: self.current_scene_no,
877            pc: self.stream.get_prg_cntr(),
878            line_no: self.current_line_no,
879            ctx_line_no: self.ctx.current_line_no,
880            halted: self.halted,
881            int_len: self.int_stack.len(),
882            str_len: self.str_stack.len(),
883            element_point_len: self.element_points.len(),
884            call_depth: self.call_stack.len(),
885            gosub_depth: self.gosub_return_stack.len(),
886            scene_depth: self.scene_stack.len(),
887            caller_return: self
888                .call_stack
889                .last()
890                .map(|frame| (frame.return_pc, frame.ret_form)),
891        }
892    }
893
894    fn restore_inline_exec_checkpoint(&mut self, checkpoint: InlineExecCheckpoint) -> Result<()> {
895        if self.current_scene_no != checkpoint.scene_no {
896            return Ok(());
897        }
898        if self.int_stack.len() < checkpoint.int_len
899            || self.str_stack.len() < checkpoint.str_len
900            || self.element_points.len() < checkpoint.element_point_len
901            || self.call_stack.len() < checkpoint.call_depth
902            || self.gosub_return_stack.len() < checkpoint.gosub_depth
903            || self.scene_stack.len() < checkpoint.scene_depth
904        {
905            bail!(
906                "inline user command corrupted caller execution state: scene={:?} int={}/{} str={}/{} elm={}/{} call={}/{} gosub={}/{} scene_stack={}/{}",
907                checkpoint.scene_no,
908                self.int_stack.len(),
909                checkpoint.int_len,
910                self.str_stack.len(),
911                checkpoint.str_len,
912                self.element_points.len(),
913                checkpoint.element_point_len,
914                self.call_stack.len(),
915                checkpoint.call_depth,
916                self.gosub_return_stack.len(),
917                checkpoint.gosub_depth,
918                self.scene_stack.len(),
919                checkpoint.scene_depth,
920            );
921        }
922
923        self.int_stack.truncate(checkpoint.int_len);
924        self.str_stack.truncate(checkpoint.str_len);
925        self.element_points.truncate(checkpoint.element_point_len);
926        while self.call_stack.len() > checkpoint.call_depth {
927            if let Some(frame) = self.call_stack.pop() {
928                self.recycle_call_frame(frame);
929            }
930        }
931        self.gosub_return_stack.truncate(checkpoint.gosub_depth);
932        self.scene_stack.truncate(checkpoint.scene_depth);
933        if let (Some((return_pc, ret_form)), Some(caller)) =
934            (checkpoint.caller_return, self.call_stack.last_mut())
935        {
936            caller.return_pc = return_pc;
937            caller.ret_form = ret_form;
938        }
939        self.current_line_no = checkpoint.line_no;
940        self.ctx.current_line_no = checkpoint.ctx_line_no;
941        self.halted = checkpoint.halted;
942        self.stream.set_prg_cntr(checkpoint.pc)?;
943        Ok(())
944    }
945
946    pub fn new(stream: SceneStream<'a>, ctx: CommandContext) -> Self {
947        let cfg = VmConfig::from_env();
948        let vm_trace_config = VmTraceConfig::from_env();
949        let runtime_options = VmRuntimeOptions::from_env();
950        let call_flag_count = Self::configured_call_flag_count(&ctx);
951        let user_cmd_names = stream.scn_cmd_name_map.clone();
952        let base_call = CallFrame {
953            call_type: 0,
954            return_pc: 0,
955            return_scene_no: None,
956            return_scene_name: None,
957            return_line_no: -1,
958            ret_form: cfg.fm_void,
959            return_override: None,
960            excall_proc: false,
961            frame_action_proc: false,
962            arg_cnt: 0,
963            delayed_ret_form: None,
964            user_props: Vec::new(),
965            int_args: Self::blank_call_int_args(call_flag_count),
966            str_args: Self::blank_call_str_args(call_flag_count),
967        };
968        Self {
969            cfg,
970            vm_trace_config,
971            runtime_options,
972            call_flag_count,
973            stream,
974            ctx,
975            int_stack: Vec::new(),
976            str_stack: Vec::new(),
977            element_points: Vec::new(),
978            call_stack: vec![base_call],
979            call_frame_pool: Vec::new(),
980            gosub_return_stack: Vec::new(),
981            user_props: BTreeMap::new(),
982            scene_user_props: BTreeMap::new(),
983            scene_stack: Vec::new(),
984            save_point: None,
985            sel_point_stack: Vec::new(),
986            current_scene_no: None,
987            current_scene_name: None,
988            current_line_no: -1,
989            unknown_opcodes: BTreeMap::new(),
990            unknown_forms: BTreeMap::new(),
991
992            steps: 0,
993            halted: false,
994            delayed_ret_form: None,
995            script_input_synced_this_frame: false,
996            yield_safe_after_step: false,
997            user_cmd_names,
998            call_cmd_names: Arc::default(),
999            scene_pck_cache: None,
1000            scene_pck_append_dir: None,
1001            scene_stream_cache: BTreeMap::new(),
1002            scene_name_resolve_cache: std::collections::HashMap::new(),
1003            user_cmd_resolve_cache: std::collections::HashMap::new(),
1004        }
1005    }
1006
1007    pub fn with_config(cfg: VmConfig, stream: SceneStream<'a>, ctx: CommandContext) -> Self {
1008        let vm_trace_config = VmTraceConfig::from_env();
1009        let runtime_options = VmRuntimeOptions::from_env();
1010        let call_flag_count = Self::configured_call_flag_count(&ctx);
1011        let user_cmd_names = stream.scn_cmd_name_map.clone();
1012        let base_call = CallFrame {
1013            call_type: 0,
1014            return_pc: 0,
1015            return_scene_no: None,
1016            return_scene_name: None,
1017            return_line_no: -1,
1018            ret_form: cfg.fm_void,
1019            return_override: None,
1020            excall_proc: false,
1021            frame_action_proc: false,
1022            arg_cnt: 0,
1023            delayed_ret_form: None,
1024            user_props: Vec::new(),
1025            int_args: Self::blank_call_int_args(call_flag_count),
1026            str_args: Self::blank_call_str_args(call_flag_count),
1027        };
1028        Self {
1029            cfg,
1030            vm_trace_config,
1031            runtime_options,
1032            call_flag_count,
1033            stream,
1034            ctx,
1035            int_stack: Vec::new(),
1036            str_stack: Vec::new(),
1037            element_points: Vec::new(),
1038            call_stack: vec![base_call],
1039            call_frame_pool: Vec::new(),
1040            gosub_return_stack: Vec::new(),
1041            user_props: BTreeMap::new(),
1042            scene_user_props: BTreeMap::new(),
1043            scene_stack: Vec::new(),
1044            save_point: None,
1045            sel_point_stack: Vec::new(),
1046            current_scene_no: None,
1047            current_scene_name: None,
1048            current_line_no: -1,
1049            unknown_opcodes: BTreeMap::new(),
1050            unknown_forms: BTreeMap::new(),
1051
1052            steps: 0,
1053            halted: false,
1054            delayed_ret_form: None,
1055            script_input_synced_this_frame: false,
1056            yield_safe_after_step: false,
1057            user_cmd_names,
1058            call_cmd_names: Arc::default(),
1059            scene_pck_cache: None,
1060            scene_pck_append_dir: None,
1061            scene_stream_cache: BTreeMap::new(),
1062            scene_name_resolve_cache: std::collections::HashMap::new(),
1063            user_cmd_resolve_cache: std::collections::HashMap::new(),
1064        }
1065    }
1066
1067    pub fn is_blocked(&mut self) -> bool {
1068        self.ctx.wait_poll()
1069    }
1070
1071    pub fn is_halted(&self) -> bool {
1072        self.halted
1073    }
1074
1075    pub fn proc_generation(&self) -> u64 {
1076        self.ctx.proc_generation()
1077    }
1078
1079    pub fn last_proc_kind(&self) -> runtime::ProcKind {
1080        self.ctx.last_proc_kind()
1081    }
1082
1083    pub fn current_scene_name(&self) -> Option<&str> {
1084        self.current_scene_name.as_deref()
1085    }
1086
1087    pub fn current_line_no(&self) -> i32 {
1088        self.current_line_no
1089    }
1090
1091    pub fn current_scene_no(&self) -> Option<usize> {
1092        self.current_scene_no
1093    }
1094
1095    pub fn take_runtime_load_completed(&mut self) -> bool {
1096        self.ctx.take_runtime_load_completed()
1097    }
1098
1099    pub fn call_syscom_configured_scene(&mut self, key: &str) -> Result<bool> {
1100        // Match the original C++ Gp_ini fields: SAVE_SCENE, LOAD_SCENE and
1101        // CONFIG_SCENE store both scene name and z label number.  GameexeConfig
1102        // get_unquoted() returns only the first item, so using it here loses the
1103        // z value and incorrectly calls sys10_sc00,0.  Rewrite's Gameexe has:
1104        //   #SAVE_SCENE   = "sys10_sc00",02
1105        //   #LOAD_SCENE   = "sys10_sc00",03
1106        //   #CONFIG_SCENE = "sys10_sc00",04
1107        // The original calls tnm_scene_proc_farcall(name, z, FM_VOID, true, false).
1108        let entry = self.ctx.tables.gameexe.as_ref().and_then(|cfg| {
1109            cfg.get_entry(key)
1110                .or_else(|| cfg.get_entry(&format!("#{key}")))
1111        });
1112        let Some(entry) = entry else {
1113            if self.runtime_options.proc_flow_trace {
1114                eprintln!(
1115                    "[SG_PROC_FLOW] syscom_config_scene key={} raw=<missing> scene={:?} line={} pending_proc={:?}",
1116                    key,
1117                    self.current_scene_name.as_deref(),
1118                    self.current_line_no,
1119                    self.ctx.globals.syscom.pending_proc
1120                );
1121            }
1122            return Ok(false);
1123        };
1124
1125        let scene_name = entry
1126            .item_unquoted(0)
1127            .map(|s| s.trim().trim_matches('\"').trim().to_string())
1128            .unwrap_or_default();
1129        let z_no = entry
1130            .item_unquoted(1)
1131            .and_then(|s| s.trim().parse::<i32>().ok())
1132            .unwrap_or(0);
1133        let raw = format!("{scene_name},{z_no}");
1134
1135        if scene_name.is_empty() {
1136            if self.runtime_options.proc_flow_trace {
1137                eprintln!(
1138                    "[SG_PROC_FLOW] syscom_config_scene key={} raw={:?} target=<empty> scene={:?} line={}",
1139                    key,
1140                    raw,
1141                    self.current_scene_name.as_deref(),
1142                    self.current_line_no
1143                );
1144            }
1145            return Ok(false);
1146        }
1147
1148        if self.runtime_options.proc_flow_trace {
1149            eprintln!(
1150                "[SG_PROC_FLOW] syscom_config_scene key={} raw={:?} target={} z={} before_scene={:?} line={} scene_stack={} call_depth={}",
1151                key,
1152                raw,
1153                scene_name,
1154                z_no,
1155                self.current_scene_name.as_deref(),
1156                self.current_line_no,
1157                self.scene_stack.len(),
1158                self.call_stack.len()
1159            );
1160        }
1161        self.call_syscom_scene(&scene_name, z_no)?;
1162        if self.runtime_options.proc_flow_trace {
1163            eprintln!(
1164                "[SG_PROC_FLOW] syscom_config_scene entered key={} now_scene={:?} line={} scene_stack={} call_depth={}",
1165                key,
1166                self.current_scene_name.as_deref(),
1167                self.current_line_no,
1168                self.scene_stack.len(),
1169                self.call_stack.len()
1170            );
1171        }
1172        Ok(true)
1173    }
1174
1175    /// Enter a game-owned system menu. The host must suspend the caller's wait
1176    /// and service the script-proc push/pop requests, as for CONFIG_SCENE.
1177    fn call_syscom_scene(&mut self, scene_name: &str, z_no: i32) -> Result<()> {
1178        self.farcall_scene_name_ex(scene_name, z_no, self.cfg.fm_void, true, &[])
1179    }
1180
1181    #[inline(always)]
1182    fn vm_trace_matches(&self) -> bool {
1183        let config = &self.vm_trace_config;
1184        if !config.enabled {
1185            return false;
1186        }
1187        if let Some(filter) = config.scene.as_deref()
1188            && self.current_scene_name.as_deref() != Some(filter)
1189        {
1190            return false;
1191        }
1192        if let Some((start, end)) = config.pc_range {
1193            let pc = self.stream.get_prg_cntr();
1194            if pc < start || pc > end {
1195                return false;
1196            }
1197        }
1198        true
1199    }
1200
1201    fn vm_trace_stack_summary(&self) -> String {
1202        let mut out = String::new();
1203        let int_tail_start = self.int_stack.len().saturating_sub(8);
1204        let int_tail = &self.int_stack[int_tail_start..];
1205        let _ = write!(
1206            &mut out,
1207            "call_depth={} int_len={} str_len={} elm_points={:?} int_tail={:?}",
1208            self.call_stack.len(),
1209            self.int_stack.len(),
1210            self.str_stack.len(),
1211            self.element_points,
1212            int_tail
1213        );
1214        if let Some(last) = self.str_stack.last() {
1215            let preview = if last.chars().count() > 48 {
1216                let mut tmp = last.chars().take(48).collect::<String>();
1217                tmp.push('…');
1218                tmp
1219            } else {
1220                last.clone()
1221            };
1222            let _ = write!(&mut out, " str_top={:?}", preview);
1223        }
1224        out
1225    }
1226
1227    fn vm_trace_emit(&self, pc: Option<usize>, msg: impl std::fmt::Display) {
1228        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1229        let scene_no = self
1230            .current_scene_no
1231            .map(|v| v.to_string())
1232            .unwrap_or_else(|| "-".to_string());
1233        let pc_text = pc
1234            .map(|v| format!("0x{v:x}"))
1235            .unwrap_or_else(|| "-".to_string());
1236        eprintln!(
1237            "[SG_VM_TRACE] scene={} scene_no={} line={} pc={} {} | {}",
1238            scene,
1239            scene_no,
1240            self.current_line_no,
1241            pc_text,
1242            msg,
1243            self.vm_trace_stack_summary()
1244        );
1245    }
1246
1247    fn vm_opcode_name(opcode: u8) -> &'static str {
1248        match opcode {
1249            CD_NONE => "NONE",
1250            CD_NL => "NL",
1251            CD_PUSH => "PUSH",
1252            CD_POP => "POP",
1253            CD_COPY => "COPY",
1254            CD_PROPERTY => "PROPERTY",
1255            CD_COPY_ELM => "COPY_ELM",
1256            CD_DEC_PROP => "DEC_PROP",
1257            CD_ELM_POINT => "ELM_POINT",
1258            CD_ARG => "ARG",
1259            CD_GOTO => "GOTO",
1260            CD_GOTO_TRUE => "GOTO_TRUE",
1261            CD_GOTO_FALSE => "GOTO_FALSE",
1262            CD_GOSUB => "GOSUB",
1263            CD_GOSUBSTR => "GOSUBSTR",
1264            CD_RETURN => "RETURN",
1265            CD_EOF => "EOF",
1266            CD_ASSIGN => "ASSIGN",
1267            CD_OPERATE_1 => "OPERATE_1",
1268            CD_OPERATE_2 => "OPERATE_2",
1269            CD_COMMAND => "COMMAND",
1270            CD_TEXT => "TEXT",
1271            CD_NAME => "NAME",
1272            CD_SEL_BLOCK_START => "SEL_BLOCK_START",
1273            CD_SEL_BLOCK_END => "SEL_BLOCK_END",
1274            _ => "UNKNOWN",
1275        }
1276    }
1277
1278    #[inline(always)]
1279    fn vm_trace_opcode(&self, pc: usize, opcode: u8, phase: &str) {
1280        vm_trace!(
1281            self,
1282            Some(pc),
1283            format!(
1284                "{} opcode={}({:#04x})",
1285                phase,
1286                Self::vm_opcode_name(opcode),
1287                opcode
1288            )
1289        );
1290    }
1291    #[inline(always)]
1292    fn sg_debug_enabled(&self) -> bool {
1293        self.runtime_options.sg_debug
1294    }
1295
1296    fn sg_cgm_coord_trace_emit(&self, msg: impl std::fmt::Display) {
1297        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1298        let scene_no = self
1299            .current_scene_no
1300            .map(|v| v.to_string())
1301            .unwrap_or_else(|| "-".to_string());
1302        eprintln!(
1303            "[SG_DEBUG][CGM_COORD_TRACE][VM] scene={} scene_no={} line={} pc=0x{:x} {}",
1304            scene,
1305            scene_no,
1306            self.current_line_no,
1307            self.stream.get_prg_cntr(),
1308            msg
1309        );
1310    }
1311
1312    fn trace_cgm_coord_assign(&self, elm: &[i32], rhs: &Value) {
1313        if !self.sg_debug_enabled() || elm.len() < 3 {
1314            return;
1315        }
1316        let array_op = if self.ctx.ids.elm_array != 0 {
1317            self.ctx.ids.elm_array
1318        } else {
1319            crate::runtime::forms::codes::ELM_ARRAY
1320        };
1321        if elm[1] != array_op {
1322            return;
1323        }
1324        let head = elm[0] as u32;
1325        let idx = elm[2];
1326        if head == crate::runtime::forms::codes::elm_value::GLOBAL_B as u32 {
1327            let interesting = (100..=129).contains(&idx)
1328                || (140..=169).contains(&idx)
1329                || (180..=209).contains(&idx);
1330            if interesting {
1331                self.sg_cgm_coord_trace_emit(format_args!("global B[{}] <- {:?}", idx, rhs));
1332            }
1333        } else if head == crate::runtime::forms::codes::elm_value::GLOBAL_S as u32
1334            && (1120..=1139).contains(&idx)
1335        {
1336            self.sg_cgm_coord_trace_emit(format_args!("global S[{}] <- {:?}", idx, rhs));
1337        }
1338    }
1339
1340    fn cf_branch_trace_interesting_line(&self) -> bool {
1341        if !self.sg_debug_enabled() || self.current_scene_name.as_deref() != Some("sys10_cf01") {
1342            return false;
1343        }
1344        matches!(self.current_line_no, 700..=730 | 870..=895)
1345    }
1346
1347    fn cf_condition_trace_interesting_line(&self) -> bool {
1348        if !self.sg_debug_enabled() {
1349            return false;
1350        }
1351        matches!(self.current_scene_name.as_deref(), Some("sys10_cf01"))
1352            && matches!(self.current_line_no, 700..=730 | 870..=895)
1353    }
1354
1355    fn cf_condition_trace_prop_name(prop_id: u16) -> Option<&'static str> {
1356        match prop_id {
1357            14 => Some("ip_mx"),
1358            15 => Some("ip_my"),
1359            16 => Some("ip_wheel"),
1360            18 => Some("ip_bl_is"),
1361            19 => Some("ip_br_is"),
1362            20 => Some("ip_bl_on"),
1363            21 => Some("ip_br_on"),
1364            22 => Some("ip_key_enable_enter"),
1365            23 => Some("ip_key_enable_esc"),
1366            24 => Some("ip_key_is_enter"),
1367            25 => Some("ip_key_is_esc"),
1368            26 => Some("ip_key_on_enter"),
1369            27 => Some("ip_key_on_esc"),
1370            39 => Some("cntr_now"),
1371            40 => Some("cntr_exit"),
1372            41 => Some("skip_flag"),
1373            _ => None,
1374        }
1375    }
1376
1377    fn cf_condition_trace_value_summary(
1378        &self,
1379        cell: &UserPropCell,
1380        array_idx: Option<usize>,
1381    ) -> String {
1382        if let Some(idx) = array_idx {
1383            if cell.form == self.cfg.fm_intlist {
1384                return format!(
1385                    "intlist[{}]={}",
1386                    idx,
1387                    cell.int_list.get(idx).copied().unwrap_or(0)
1388                );
1389            }
1390            if cell.form == self.cfg.fm_strlist {
1391                return format!(
1392                    "strlist[{}]={:?}",
1393                    idx,
1394                    cell.str_list.get(idx).cloned().unwrap_or_default()
1395                );
1396            }
1397            if let Some(slot) = cell.list_items.get(idx) {
1398                return format!(
1399                    "list[{}] form={} int={} str={:?} int_list_len={} str_list_len={} items={}",
1400                    idx,
1401                    slot.form,
1402                    slot.int_value,
1403                    slot.str_value,
1404                    slot.int_list.len(),
1405                    slot.str_list.len(),
1406                    slot.list_items.len()
1407                );
1408            }
1409            return format!(
1410                "array[{}] <missing> form={} int_list_len={} str_list_len={} items={}",
1411                idx,
1412                cell.form,
1413                cell.int_list.len(),
1414                cell.str_list.len(),
1415                cell.list_items.len()
1416            );
1417        }
1418        if cell.form == self.cfg.fm_int {
1419            return format!("int={}", cell.int_value);
1420        }
1421        if cell.form == self.cfg.fm_str {
1422            return format!("str={:?}", cell.str_value);
1423        }
1424        if cell.form == self.cfg.fm_intlist {
1425            let preview = cell.int_list.iter().take(20).copied().collect::<Vec<_>>();
1426            return format!("intlist len={} head={:?}", cell.int_list.len(), preview);
1427        }
1428        if cell.form == self.cfg.fm_strlist {
1429            let preview = cell.str_list.iter().take(6).cloned().collect::<Vec<_>>();
1430            return format!("strlist len={} head={:?}", cell.str_list.len(), preview);
1431        }
1432        format!(
1433            "form={} int={} str={:?} int_list_len={} str_list_len={} items={}",
1434            cell.form,
1435            cell.int_value,
1436            cell.str_value,
1437            cell.int_list.len(),
1438            cell.str_list.len(),
1439            cell.list_items.len()
1440        )
1441    }
1442
1443    fn sg_cf_condition_trace(&self, pc: usize, msg: impl AsRef<str>) {
1444        if !self.sg_debug_enabled() {
1445            return;
1446        }
1447        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1448        let scene_no = self
1449            .current_scene_no
1450            .map(|v| v.to_string())
1451            .unwrap_or_else(|| "-".to_string());
1452        let int_tail_start = self.int_stack.len().saturating_sub(12);
1453        let int_tail = &self.int_stack[int_tail_start..];
1454        eprintln!(
1455            "[SG_DEBUG][CF_CONDITION_TRACE] scene={} scene_no={} line={} pc=0x{:x} {} | int_tail={:?}",
1456            scene,
1457            scene_no,
1458            self.current_line_no,
1459            pc,
1460            msg.as_ref(),
1461            int_tail
1462        );
1463    }
1464
1465    fn trace_cf_condition_user_prop_read(
1466        &self,
1467        pc: usize,
1468        prop_id: u16,
1469        array_idx: Option<usize>,
1470        cell: &UserPropCell,
1471        elm: &[i32],
1472    ) {
1473        if !self.cf_condition_trace_interesting_line() {
1474            return;
1475        }
1476        let Some(name) = Self::cf_condition_trace_prop_name(prop_id) else {
1477            return;
1478        };
1479        self.sg_cf_condition_trace(
1480            pc,
1481            format!(
1482                "kind=USER_PROP_READ prop={}({}) array={:?} value={} elm={:?}",
1483                prop_id,
1484                name,
1485                array_idx,
1486                self.cf_condition_trace_value_summary(cell, array_idx),
1487                elm
1488            ),
1489        );
1490    }
1491
1492    fn trace_cf_condition_user_prop_assign(
1493        &self,
1494        pc: usize,
1495        prop_id: u16,
1496        array_idx: Option<usize>,
1497        old: Option<&UserPropCell>,
1498        new: Option<&UserPropCell>,
1499        rhs: &Value,
1500        elm: &[i32],
1501    ) {
1502        if !self.cf_condition_trace_interesting_line() {
1503            return;
1504        }
1505        let Some(name) = Self::cf_condition_trace_prop_name(prop_id) else {
1506            return;
1507        };
1508        let old_summary = old
1509            .map(|cell| self.cf_condition_trace_value_summary(cell, array_idx))
1510            .unwrap_or_else(|| "<default/missing>".to_string());
1511        let new_summary = new
1512            .map(|cell| self.cf_condition_trace_value_summary(cell, array_idx))
1513            .unwrap_or_else(|| "<missing>".to_string());
1514        self.sg_cf_condition_trace(
1515            pc,
1516            format!(
1517                "kind=USER_PROP_ASSIGN prop={}({}) array={:?} old={} new={} rhs={:?} elm={:?}",
1518                prop_id, name, array_idx, old_summary, new_summary, rhs, elm
1519            ),
1520        );
1521    }
1522
1523    fn cf_condition_op_name(opr: u8) -> &'static str {
1524        match opr {
1525            OP_PLUS => "+",
1526            OP_MINUS => "-",
1527            OP_MULTIPLE => "*",
1528            OP_DIVIDE => "/",
1529            OP_AMARI => "%",
1530            OP_EQUAL => "==",
1531            OP_NOT_EQUAL => "!=",
1532            OP_GREATER => ">",
1533            OP_GREATER_EQUAL => ">=",
1534            OP_LESS => "<",
1535            OP_LESS_EQUAL => "<=",
1536            OP_LOGICAL_AND => "&&",
1537            OP_LOGICAL_OR => "||",
1538            OP_TILDE => "~",
1539            OP_AND => "&",
1540            OP_OR => "|",
1541            OP_HAT => "^",
1542            OP_SL => "<<",
1543            OP_SR => ">>",
1544            OP_SR3 => ">>>",
1545            _ => "?",
1546        }
1547    }
1548
1549    fn cf_branch_trace_stack_snapshot(&self) -> String {
1550        let int_tail_start = self.int_stack.len().saturating_sub(16);
1551        let int_tail = &self.int_stack[int_tail_start..];
1552        let str_tail_start = self.str_stack.len().saturating_sub(4);
1553        let str_tail = &self.str_stack[str_tail_start..];
1554        let (cur_l, cur_s, arg_cnt) = if let Some(frame) = self.call_stack.last() {
1555            let l_take = frame.int_args.len().min(16);
1556            let s_take = frame.str_args.len().min(6);
1557            (
1558                format!("{:?}", &frame.int_args[..l_take]),
1559                format!("{:?}", &frame.str_args[..s_take]),
1560                frame.arg_cnt,
1561            )
1562        } else {
1563            ("[]".to_string(), "[]".to_string(), 0)
1564        };
1565        format!(
1566            "int_len={} int_tail={:?} str_len={} str_tail={:?} elm_points={:?} call_depth={} arg_cnt={} cur_call_l0_15={} cur_call_s0_5={}",
1567            self.int_stack.len(),
1568            int_tail,
1569            self.str_stack.len(),
1570            str_tail,
1571            self.element_points,
1572            self.call_stack.len(),
1573            arg_cnt,
1574            cur_l,
1575            cur_s,
1576        )
1577    }
1578
1579    fn sg_cf_branch_trace_emit(&self, pc: usize, msg: impl std::fmt::Display) {
1580        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1581        let scene_no = self
1582            .current_scene_no
1583            .map(|v| v.to_string())
1584            .unwrap_or_else(|| "-".to_string());
1585        eprintln!(
1586            "[SG_DEBUG][CF_BRANCH_TRACE] scene={} scene_no={} line={} pc=0x{:x} {} | {}",
1587            scene,
1588            scene_no,
1589            self.current_line_no,
1590            pc,
1591            msg,
1592            self.cf_branch_trace_stack_snapshot(),
1593        );
1594    }
1595
1596    fn trace_cf_branch_goto(
1597        &self,
1598        pc: usize,
1599        opcode_name: &str,
1600        label_no: i32,
1601        cond: i32,
1602        taken: bool,
1603        before_tail: &[i32],
1604    ) {
1605        if self.cf_branch_trace_interesting_line() {
1606            self.sg_cf_branch_trace_emit(
1607                pc,
1608                format_args!(
1609                    "kind=GOTO opcode={} label={} cond={} taken={} before_int_tail={:?}",
1610                    opcode_name, label_no, cond, taken, before_tail
1611                ),
1612            );
1613        }
1614    }
1615
1616    fn trace_cf_branch_farcall(
1617        &self,
1618        pc: usize,
1619        scene_name: &str,
1620        z_no: i32,
1621        ret_form: i32,
1622        ex_call_proc: bool,
1623        scratch_source_args: &[Value],
1624    ) {
1625        if !self.sg_debug_enabled()
1626            || !(self.current_scene_name.as_deref() == Some("sys10_cf01")
1627                && matches!(self.current_line_no, 700..=730 | 870..=895)
1628                && matches!(scene_name, "sys10_sm00" | "sys10_cf00")
1629                && matches!(z_no, 14 | 15))
1630        {
1631            return;
1632        }
1633        let args_dbg = scratch_source_args
1634            .iter()
1635            .map(|v| format!("{v:?}"))
1636            .collect::<Vec<_>>()
1637            .join(", ");
1638        self.sg_cf_branch_trace_emit(
1639            pc,
1640            format_args!(
1641                "kind=FARCALL target={} z={} ret_form={} ex_call_proc={} argc={} args=[{}]",
1642                scene_name,
1643                z_no,
1644                ret_form,
1645                ex_call_proc,
1646                scratch_source_args.len(),
1647                args_dbg
1648            ),
1649        );
1650    }
1651
1652    fn sg_omv_trace_emit(&self, msg: impl std::fmt::Display) {
1653        let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
1654        let scene_no = self
1655            .current_scene_no
1656            .map(|v| v.to_string())
1657            .unwrap_or_else(|| "-".to_string());
1658        eprintln!(
1659            "[SG_DEBUG][OMV_TRACE] scene={} scene_no={} line={} pc=0x{:x} {}",
1660            scene,
1661            scene_no,
1662            self.current_line_no,
1663            self.stream.get_prg_cntr(),
1664            msg
1665        );
1666    }
1667
1668    fn sg_omv_trace_command(
1669        &self,
1670        phase: &str,
1671        elm: &[i32],
1672        form_id: i32,
1673        op_id: i32,
1674        al_id: i32,
1675        ret_form: i32,
1676        args: &[Value],
1677    ) {
1678        if !self.sg_debug_enabled() {
1679            return;
1680        }
1681
1682        let label = if form_id == crate::runtime::forms::codes::elm_value::GLOBAL_JUMP
1683            || (form_id == crate::runtime::forms::codes::FM_GLOBAL
1684                && op_id == crate::runtime::forms::codes::elm_value::GLOBAL_JUMP)
1685        {
1686            Some("GLOBAL.JUMP")
1687        } else if form_id == crate::runtime::forms::codes::elm_value::GLOBAL_FARCALL
1688            || (form_id == crate::runtime::forms::codes::FM_GLOBAL
1689                && op_id == crate::runtime::forms::codes::elm_value::GLOBAL_FARCALL)
1690        {
1691            Some("GLOBAL.FARCALL")
1692        } else if (form_id as u32 == constants::global_form::SYSCOM
1693            || form_id == constants::fm::SYSCOM)
1694            && op_id == crate::runtime::forms::codes::elm_value::SYSCOM_CALL_EX
1695        {
1696            Some("SYSCOM.CALL_EX")
1697        } else if form_id as u32 == constants::global_form::MOV || form_id == constants::fm::MOV {
1698            Some("MOV")
1699        } else if form_id == constants::fm::OBJECT
1700            && matches!(
1701                op_id,
1702                crate::runtime::forms::codes::object_op::CREATE_MOVIE
1703                    | crate::runtime::forms::codes::object_op::CREATE_MOVIE_LOOP
1704                    | crate::runtime::forms::codes::object_op::CREATE_MOVIE_WAIT
1705                    | crate::runtime::forms::codes::object_op::CREATE_MOVIE_WAIT_KEY
1706            )
1707        {
1708            Some("OBJECT.CREATE_MOVIE")
1709        } else {
1710            None
1711        };
1712        let Some(label) = label else {
1713            return;
1714        };
1715
1716        let args_dbg = args
1717            .iter()
1718            .take(8)
1719            .map(|v| format!("{v:?}"))
1720            .collect::<Vec<_>>()
1721            .join(", ");
1722        self.sg_omv_trace_emit(format_args!(
1723            "{} {} form={} op={} al_id={} ret_form={} elm={:?} argc={} args=[{}]",
1724            phase,
1725            label,
1726            form_id,
1727            op_id,
1728            al_id,
1729            ret_form,
1730            elm,
1731            args.len(),
1732            args_dbg
1733        ));
1734    }
1735
1736    fn vm_scn_cmd_context(&self, pc: usize) -> String {
1737        let cnt = self.stream.header.scn_cmd_cnt.max(0) as usize;
1738        let mut prev: Option<(usize, usize)> = None;
1739        let mut next: Option<(usize, usize)> = None;
1740        for cmd_no in 0..cnt {
1741            let Ok(off) = self.stream.scn_cmd_offset(cmd_no) else {
1742                continue;
1743            };
1744            if off <= pc {
1745                prev = Some(match prev {
1746                    Some(cur) if cur.1 > off => cur,
1747                    _ => (cmd_no, off),
1748                });
1749            }
1750            if off > pc {
1751                next = Some(match next {
1752                    Some(cur) if cur.1 < off => cur,
1753                    _ => (cmd_no, off),
1754                });
1755            }
1756        }
1757
1758        let mut out = String::new();
1759        if let Some((cmd_no, off)) = prev {
1760            let name = self
1761                .stream
1762                .scn_cmd_name_map
1763                .get(&(cmd_no as u32))
1764                .map(String::as_str)
1765                .unwrap_or("<unnamed>");
1766            let _ = write!(
1767                &mut out,
1768                "prev_scn_cmd=#{}:{}@0x{:x} delta={} ",
1769                cmd_no,
1770                name,
1771                off,
1772                pc.saturating_sub(off)
1773            );
1774        } else {
1775            let _ = write!(&mut out, "prev_scn_cmd=<none> ");
1776        }
1777        if let Some((cmd_no, off)) = next {
1778            let name = self
1779                .stream
1780                .scn_cmd_name_map
1781                .get(&(cmd_no as u32))
1782                .map(String::as_str)
1783                .unwrap_or("<unnamed>");
1784            let _ = write!(
1785                &mut out,
1786                "next_scn_cmd=#{}:{}@0x{:x} distance={}",
1787                cmd_no,
1788                name,
1789                off,
1790                off.saturating_sub(pc)
1791            );
1792        } else {
1793            let _ = write!(&mut out, "next_scn_cmd=<none>");
1794        }
1795        out
1796    }
1797
1798    pub fn take_script_proc_request(&mut self) -> bool {
1799        let requested = self.ctx.excall_state.script_proc_requested;
1800        self.ctx.excall_state.script_proc_requested = false;
1801        requested
1802    }
1803
1804    pub fn take_script_proc_pop_request(&mut self) -> bool {
1805        let requested = self.ctx.excall_state.script_proc_pop_requested;
1806        self.ctx.excall_state.script_proc_pop_requested = false;
1807        requested
1808    }
1809
1810    fn mark_excall_script_proc_requested(&mut self) {
1811        self.halted = false;
1812        self.ctx.excall_state.ex_call_flag = true;
1813        self.ctx.excall_state.script_proc_requested = true;
1814        // Original tnm_scene_proc_farcall(..., ex_call=true) pushes a
1815        // TNM_PROC_TYPE_SCRIPT proc immediately.  Make that process-stack
1816        // transition visible to the host at this exact instruction boundary.
1817        self.ctx.request_proc_boundary(runtime::ProcKind::Script);
1818    }
1819
1820    fn mark_excall_script_proc_pop_requested(&mut self) {
1821        self.ctx.excall_state.ex_call_flag = false;
1822        self.ctx.excall_state.script_proc_pop_requested = true;
1823        self.ctx.input.clear_all();
1824        // Original tnm_scene_proc_return() pops the EXCALL SCRIPT proc before
1825        // resuming the caller.  If Rust keeps executing here, caller script can
1826        // create a new wait while the menu EXCALL is still on FlowState; the
1827        // later wait restore then overwrites that new wait and leaves repeated
1828        // SAVE/LOAD/CONFIG entry out of sync.
1829        self.ctx.request_proc_boundary(runtime::ProcKind::Script);
1830    }
1831
1832    fn push_call_arg_value(&mut self, arg: &Value) {
1833        match arg {
1834            Value::NamedArg { value, .. } => self.push_call_arg_value(value),
1835            Value::Int(n) => self.push_int(*n as i32),
1836            Value::Str(s) => self.push_str(s.clone()),
1837            Value::Element(elm) => self.push_element(elm.clone()),
1838            Value::List(items) => {
1839                for item in items {
1840                    self.push_call_arg_value(item);
1841                }
1842            }
1843        }
1844    }
1845
1846    fn run_user_cmd_inline_at_offset(
1847        &mut self,
1848        cmd_name: &str,
1849        offset: usize,
1850        return_pc: usize,
1851        end_offset: Option<usize>,
1852        _expected_return_pc: Option<usize>,
1853        ret_form: i32,
1854        call_args: &[Value],
1855        frame_action_proc: bool,
1856    ) -> Result<bool> {
1857        let checkpoint = self.inline_exec_checkpoint();
1858        let base_depth = checkpoint.call_depth;
1859
1860        if let Some(caller) = self.call_stack.last_mut() {
1861            if self.runtime_options.trace_call_return_pc {
1862                eprintln!(
1863                    "[SG_CALL_PC] inline set cmd={} depth={} saved_pc=0x{:x} return_pc=0x{:x} old=0x{:x}",
1864                    cmd_name, base_depth, checkpoint.pc, return_pc, caller.return_pc
1865                );
1866            }
1867            caller.return_pc = return_pc;
1868            caller.return_scene_no = self.current_scene_no;
1869            caller.return_scene_name = self.current_scene_name.clone();
1870            caller.return_line_no = self.current_line_no;
1871            caller.ret_form = ret_form;
1872        }
1873        for arg in call_args {
1874            self.push_call_arg_value(arg);
1875        }
1876        let mut call_frame = self.take_call_frame(
1877            self.cfg.fm_void,
1878            false,
1879            frame_action_proc,
1880            call_args.len(),
1881            None,
1882        );
1883        call_frame.call_type = 3;
1884        call_frame.return_override = Some((return_pc, ret_form));
1885        self.call_stack.push(call_frame);
1886        self.stream.set_prg_cntr(offset)?;
1887
1888        if self.runtime_options.trace_frame_action_call {
1889            eprintln!(
1890                "[SG_FRAME_ACTION_CALL] run cmd={} scene={:?} offset=0x{:x} return_pc=0x{:x} args={:?}",
1891                cmd_name, self.current_scene_no, offset, return_pc, call_args
1892            );
1893        }
1894
1895        let max_steps = self.runtime_options.inline_user_cmd_max_steps;
1896        let mut steps: u64 = 0;
1897        let mut run_error = None;
1898        loop {
1899            if let Some(end) = end_offset
1900                && self.stream.get_prg_cntr() >= end
1901            {
1902                break;
1903            }
1904            let wait_generation_before_step = self.ctx.wait.block_generation();
1905            let proc_generation_before_step = self.ctx.proc_generation();
1906            let running = match self.step_inner(false) {
1907                Ok(v) => v,
1908                Err(e) => {
1909                    run_error = Some(e);
1910                    break;
1911                }
1912            };
1913            if self.halted || !running {
1914                break;
1915            }
1916            if self.ctx.proc_generation() != proc_generation_before_step {
1917                break;
1918            }
1919            if self.ctx.wait.block_generation() != wait_generation_before_step
1920                && self.ctx.wait_poll()
1921            {
1922                break;
1923            }
1924            if self.call_stack.len() == base_depth {
1925                break;
1926            }
1927            steps = steps.saturating_add(1);
1928            if max_steps > 0 && steps >= max_steps {
1929                run_error = Some(anyhow!(
1930                    "inline user command exceeded SIGLUS_INLINE_USER_CMD_MAX_STEPS: cmd={}",
1931                    cmd_name
1932                ));
1933                break;
1934            }
1935        }
1936
1937        let captured_inline_return = if ret_form == self.cfg.fm_int || ret_form == self.cfg.fm_label
1938        {
1939            if self.int_stack.len() > checkpoint.int_len {
1940                self.int_stack.last().copied().map(|v| Value::Int(v as i64))
1941            } else {
1942                None
1943            }
1944        } else if ret_form == self.cfg.fm_str {
1945            if self.str_stack.len() > checkpoint.str_len {
1946                self.str_stack.last().cloned().map(Value::Str)
1947            } else {
1948                None
1949            }
1950        } else {
1951            None
1952        };
1953
1954        if self.current_scene_no == checkpoint.scene_no {
1955            self.restore_inline_exec_checkpoint(checkpoint)?;
1956            if self.runtime_options.trace_call_return_pc
1957                && let Some(caller) = self.call_stack.last()
1958            {
1959                eprintln!(
1960                    "[SG_CALL_PC] inline restore cmd={} depth={} return_pc=0x{:x}",
1961                    cmd_name, base_depth, caller.return_pc
1962                );
1963            }
1964        }
1965        if let Some(v) = captured_inline_return {
1966            self.ctx.stack.push(v);
1967        }
1968
1969        if let Some(e) = run_error {
1970            return Err(e);
1971        }
1972
1973        Ok(true)
1974    }
1975
1976    pub fn install_initial_scene_pck(&mut self, pack: ScenePck, append_dir: String) {
1977        self.scene_pck_cache = Some(pack);
1978        self.scene_pck_append_dir = Some(append_dir);
1979    }
1980
1981    fn ensure_scene_pck_cache(&mut self) -> Result<()> {
1982        let active_append = self.ctx.globals.append_dir.clone();
1983        let append_changed = self
1984            .scene_pck_append_dir
1985            .as_deref()
1986            .map(|cached| !cached.eq_ignore_ascii_case(&active_append))
1987            .unwrap_or(true);
1988        if self.scene_pck_cache.is_some() && !append_changed {
1989            return Ok(());
1990        }
1991
1992        // Original `tnm_reload_scene_pck()` replaces the lexer package when the
1993        // active append changes.  All scene-number/name caches belong to that
1994        // package and must be discarded together.
1995        self.scene_pck_cache = None;
1996        self.scene_stream_cache.clear();
1997        self.scene_name_resolve_cache.clear();
1998        self.user_cmd_resolve_cache.clear();
1999
2000        let scene_pck_path =
2001            crate::resource::find_scene_pck_path_for_append(&self.ctx.project_dir, &active_append)?;
2002
2003        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
2004        {
2005            let bytes = crate::resource::read_file_bytes(&scene_pck_path)?;
2006            let key_cfg = ["key.toml", "Key.toml"].iter().find_map(|name| {
2007                let p = self.ctx.project_dir.join(name);
2008                if !crate::resource::wasm_path_is_file(&p) {
2009                    return None;
2010                }
2011                let text = crate::resource::read_file_to_string(&p).ok()?;
2012                siglus_assets::key_toml::parse_key_toml(&text).ok()
2013            });
2014            let exe = key_cfg
2015                .as_ref()
2016                .and_then(|cfg| cfg.exe_key16)
2017                .map(|v| v.to_vec());
2018            let string_encryption_override = key_cfg
2019                .map(|cfg| cfg.override_string_encryption)
2020                .unwrap_or_default();
2021            let opt = ScenePckDecodeOptions {
2022                exe_angou_element: exe,
2023                easy_angou_code: Some(siglus_assets::keys::SCENE_KEY.to_vec()),
2024                string_encryption_override,
2025            };
2026            self.scene_pck_cache = Some(ScenePck::load_lazy_from_bytes(bytes, &opt)?);
2027        }
2028
2029        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
2030        {
2031            let opt = crate::resource::load_scene_pck_decode_options(&self.ctx.project_dir)?;
2032            self.scene_pck_cache = Some(ScenePck::load_lazy(&scene_pck_path, &opt)?);
2033        }
2034
2035        self.ctx.install_scene_metadata(
2036            &active_append,
2037            self.scene_pck_cache
2038                .as_ref()
2039                .expect("scene pck cache initialized"),
2040        )?;
2041        self.scene_pck_append_dir = Some(active_append);
2042        Ok(())
2043    }
2044
2045    fn ensure_scene_stream_cached(&mut self, scene_no: usize) -> Result<()> {
2046        self.ensure_scene_pck_cache()?;
2047        if !self.scene_stream_cache.contains_key(&scene_no) {
2048            let (owner, range, string_codec) = {
2049                let pck = self
2050                    .scene_pck_cache
2051                    .as_ref()
2052                    .expect("scene pck cache initialized");
2053                let (owner, range) = pck.scn_data_shared(scene_no)?;
2054                (owner, range, pck.string_codec)
2055            };
2056            let stream =
2057                SceneStream::new_shared_range_with_string_codec(owner, range, string_codec)?;
2058            self.scene_stream_cache.insert(scene_no, stream);
2059        }
2060        Ok(())
2061    }
2062
2063    fn cached_scene_stream(&mut self, scene_no: usize) -> Result<SceneStream<'a>> {
2064        self.ensure_scene_stream_cached(scene_no)?;
2065        Ok(self
2066            .scene_stream_cache
2067            .get(&scene_no)
2068            .expect("scene stream cached")
2069            .clone())
2070    }
2071
2072    fn find_scene_no_by_name(pck: &ScenePck, name: &str) -> Option<usize> {
2073        pck.scn_name_map.iter().find_map(|(scene_name, scene_no)| {
2074            if siglus_name_eq(scene_name, name) {
2075                Some(*scene_no)
2076            } else {
2077                None
2078            }
2079        })
2080    }
2081
2082    fn requested_user_command_scene_no(&mut self, scn_name: Option<&str>) -> Result<Option<usize>> {
2083        let Some(name) = scn_name.filter(|name| !name.is_empty()) else {
2084            return Ok(self.current_scene_no);
2085        };
2086        if let Some(scene_no) = self.scene_name_resolve_cache.get(name) {
2087            return Ok(*scene_no);
2088        }
2089        self.ensure_scene_pck_cache()?;
2090        let pck = self
2091            .scene_pck_cache
2092            .as_ref()
2093            .ok_or_else(|| anyhow!("scene pck cache is not initialized"))?;
2094        let scene_no = Self::find_scene_no_by_name(pck, name);
2095        self.scene_name_resolve_cache
2096            .insert(name.to_string(), scene_no);
2097        Ok(scene_no)
2098    }
2099
2100    fn resolve_user_command_by_name(
2101        &mut self,
2102        requested_scene_no: usize,
2103        cmd_name: &str,
2104    ) -> Result<Option<ResolvedUserCommand>> {
2105        if let Some(cached) = self
2106            .user_cmd_resolve_cache
2107            .get(&requested_scene_no)
2108            .and_then(|commands| commands.get(cmd_name))
2109        {
2110            return Ok(Some(cached.clone()));
2111        }
2112
2113        self.ensure_scene_pck_cache()?;
2114        if self.current_scene_no != Some(requested_scene_no) {
2115            self.ensure_scene_stream_cached(requested_scene_no)?;
2116        }
2117
2118        let resolved = {
2119            let pck = self
2120                .scene_pck_cache
2121                .as_ref()
2122                .ok_or_else(|| anyhow!("scene pck cache is not initialized"))?;
2123            let local_names = if self.current_scene_no == Some(requested_scene_no) {
2124                &self.user_cmd_names
2125            } else {
2126                &self
2127                    .scene_stream_cache
2128                    .get(&requested_scene_no)
2129                    .expect("scene stream cached")
2130                    .scn_cmd_name_map
2131            };
2132            let inc_cmd_cnt = pck.inc_cmds.len();
2133            let Some((encoded_no, include_command)) = resolve_named_user_command_number(
2134                &pck.inc_cmd_name_map,
2135                local_names,
2136                inc_cmd_cnt,
2137                cmd_name,
2138            ) else {
2139                return Ok(None);
2140            };
2141
2142            if include_command {
2143                let target = pck.inc_cmds.get(encoded_no).copied().ok_or_else(|| {
2144                    anyhow!(
2145                        "include user command {} is missing from Scene.pck inc_cmds",
2146                        encoded_no
2147                    )
2148                })?;
2149                let canonical_name = pck
2150                    .inc_cmd_name_map
2151                    .get(&(encoded_no as u32))
2152                    .cloned()
2153                    .unwrap_or_else(|| cmd_name.to_string());
2154                if target.scn_no < 0 || target.offset < 0 {
2155                    bail!(
2156                        "invalid include user command target: cmd_no={} name={} scn_no={} offset={}",
2157                        encoded_no,
2158                        canonical_name,
2159                        target.scn_no,
2160                        target.offset
2161                    );
2162                }
2163                ResolvedUserCommand {
2164                    encoded_no,
2165                    name: canonical_name,
2166                    target_scene_no: target.scn_no as usize,
2167                    target_offset: target.offset as usize,
2168                    include_command: true,
2169                }
2170            } else {
2171                let local_cmd_no = encoded_no - inc_cmd_cnt;
2172                let target_offset = if self.current_scene_no == Some(requested_scene_no) {
2173                    self.stream.scn_cmd_offset(local_cmd_no)?
2174                } else {
2175                    self.scene_stream_cache
2176                        .get(&requested_scene_no)
2177                        .expect("scene stream cached")
2178                        .scn_cmd_offset(local_cmd_no)?
2179                };
2180                let name = local_names
2181                    .get(&(local_cmd_no as u32))
2182                    .cloned()
2183                    .unwrap_or_else(|| cmd_name.to_string());
2184                ResolvedUserCommand {
2185                    encoded_no,
2186                    name,
2187                    target_scene_no: requested_scene_no,
2188                    target_offset,
2189                    include_command: false,
2190                }
2191            }
2192        };
2193
2194        self.user_cmd_resolve_cache
2195            .entry(requested_scene_no)
2196            .or_default()
2197            .insert(cmd_name.to_string(), resolved.clone());
2198        Ok(Some(resolved))
2199    }
2200
2201    fn resolve_user_command_by_id(
2202        &mut self,
2203        requested_scene_no: usize,
2204        cmd_no: usize,
2205    ) -> Result<ResolvedUserCommand> {
2206        self.ensure_scene_pck_cache()?;
2207        let (inc_cmd_cnt, inc_target, inc_name) = {
2208            let pck = self
2209                .scene_pck_cache
2210                .as_ref()
2211                .ok_or_else(|| anyhow!("scene pck cache is not initialized"))?;
2212            let inc_target = pck.inc_cmds.get(cmd_no).copied();
2213            let inc_name = pck.inc_cmd_name_map.get(&(cmd_no as u32)).cloned();
2214            (pck.inc_cmds.len(), inc_target, inc_name)
2215        };
2216
2217        if cmd_no < inc_cmd_cnt {
2218            let target = inc_target.ok_or_else(|| {
2219                anyhow!(
2220                    "include user command {} is missing from Scene.pck inc_cmds",
2221                    cmd_no
2222                )
2223            })?;
2224            if target.scn_no < 0 || target.offset < 0 {
2225                bail!(
2226                    "invalid include user command target: cmd_no={} name={} scn_no={} offset={}",
2227                    cmd_no,
2228                    inc_name.as_deref().unwrap_or("<unknown>"),
2229                    target.scn_no,
2230                    target.offset
2231                );
2232            }
2233            return Ok(ResolvedUserCommand {
2234                encoded_no: cmd_no,
2235                name: inc_name.unwrap_or_else(|| format!("<include-command-{cmd_no}>")),
2236                target_scene_no: target.scn_no as usize,
2237                target_offset: target.offset as usize,
2238                include_command: true,
2239            });
2240        }
2241
2242        let local_cmd_no = cmd_no - inc_cmd_cnt;
2243        let (target_offset, name) = if self.current_scene_no == Some(requested_scene_no) {
2244            let target_offset = self.stream.scn_cmd_offset(local_cmd_no)?;
2245            let name = self
2246                .user_cmd_names
2247                .get(&(local_cmd_no as u32))
2248                .cloned()
2249                .unwrap_or_else(|| format!("<scene-command-{local_cmd_no}>"));
2250            (target_offset, name)
2251        } else {
2252            let target_stream = self.cached_scene_stream(requested_scene_no)?;
2253            let target_offset = target_stream.scn_cmd_offset(local_cmd_no)?;
2254            let name = target_stream
2255                .scn_cmd_name_map
2256                .get(&(local_cmd_no as u32))
2257                .cloned()
2258                .unwrap_or_else(|| format!("<scene-command-{local_cmd_no}>"));
2259            (target_offset, name)
2260        };
2261
2262        Ok(ResolvedUserCommand {
2263            encoded_no: cmd_no,
2264            name,
2265            target_scene_no: requested_scene_no,
2266            target_offset,
2267            include_command: false,
2268        })
2269    }
2270
2271    fn enter_resolved_user_command(
2272        &mut self,
2273        command: &ResolvedUserCommand,
2274        ret_form: i32,
2275        call_args: &[Value],
2276        excall_proc: bool,
2277        frame_action_proc: bool,
2278    ) -> Result<bool> {
2279        if self.current_scene_no == Some(command.target_scene_no) {
2280            self.enter_current_scene_user_cmd_proc_at_offset(
2281                command.target_offset,
2282                ret_form,
2283                call_args,
2284                excall_proc,
2285                frame_action_proc,
2286            )
2287        } else {
2288            self.enter_scene_user_cmd_at_scene_offset_ex(
2289                command.target_scene_no,
2290                command.target_offset,
2291                call_args,
2292                ret_form,
2293                excall_proc,
2294                frame_action_proc,
2295            )
2296        }
2297    }
2298
2299    fn run_scene_user_cmd_inline_at_cached_scene_offset(
2300        &mut self,
2301        target_scene_no: usize,
2302        cmd_name: &str,
2303        target_offset: usize,
2304        call_args: &[Value],
2305        ret_form: i32,
2306        preserve_return_pc: bool,
2307        frame_action_proc: bool,
2308    ) -> Result<bool> {
2309        let target_stream = self.cached_scene_stream(target_scene_no)?;
2310        if target_offset > target_stream.scn.len() {
2311            bail!(
2312                "scene_pck: user command offset out of bounds: cmd={} scn_no={} offset=0x{:x} scn_len=0x{:x}",
2313                cmd_name,
2314                target_scene_no,
2315                target_offset,
2316                target_stream.scn.len()
2317            );
2318        }
2319        let (target_call_cmd_names, target_scene_name) = {
2320            let pck = self
2321                .scene_pck_cache
2322                .as_ref()
2323                .expect("scene pck cache initialized");
2324            (
2325                pck.inc_cmd_name_map.clone(),
2326                pck.find_scene_name(target_scene_no).map(ToOwned::to_owned),
2327            )
2328        };
2329
2330        let saved_stream = std::mem::replace(&mut self.stream, target_stream);
2331        let target_user_cmd_names = self.stream.scn_cmd_name_map.clone();
2332        let saved_user_cmd_names =
2333            std::mem::replace(&mut self.user_cmd_names, target_user_cmd_names);
2334        let saved_call_cmd_names =
2335            std::mem::replace(&mut self.call_cmd_names, target_call_cmd_names);
2336        let saved_current_scene_no = self.current_scene_no;
2337        let saved_current_scene_name = self.current_scene_name.clone();
2338        let saved_current_line_no = self.current_line_no;
2339        let saved_ctx_scene_no = self.ctx.current_scene_no;
2340        let saved_ctx_scene_name = self.ctx.current_scene_name.clone();
2341        let saved_ctx_line_no = self.ctx.current_line_no;
2342        let saved_halted = self.halted;
2343        self.enter_cross_scene_user_prop_scope(target_scene_no);
2344
2345        self.current_scene_no = Some(target_scene_no);
2346        self.current_scene_name = target_scene_name;
2347        self.current_line_no = -1;
2348        self.ctx.current_scene_no = Some(target_scene_no as i64);
2349        self.ctx.current_scene_name = self.current_scene_name.clone();
2350        self.ctx.current_line_no = -1;
2351
2352        let target_return_pc = if preserve_return_pc {
2353            saved_stream.get_prg_cntr()
2354        } else {
2355            self.stream.scn.len()
2356        };
2357        let result = self.run_user_cmd_inline_at_offset(
2358            cmd_name,
2359            target_offset,
2360            target_return_pc,
2361            None,
2362            None,
2363            ret_form,
2364            call_args,
2365            frame_action_proc,
2366        );
2367
2368        // Save the target scene's locals while current_scene_no still
2369        // identifies it, then reactivate the caller's resident scene scope.
2370        self.restore_cross_scene_user_prop_scope(saved_current_scene_no);
2371        self.stream = saved_stream;
2372        self.user_cmd_names = saved_user_cmd_names;
2373        self.call_cmd_names = saved_call_cmd_names;
2374        self.current_scene_no = saved_current_scene_no;
2375        self.current_scene_name = saved_current_scene_name;
2376        self.current_line_no = saved_current_line_no;
2377        self.ctx.current_scene_no = saved_ctx_scene_no;
2378        self.ctx.current_scene_name = saved_ctx_scene_name;
2379        self.ctx.current_line_no = saved_ctx_line_no;
2380        self.halted = saved_halted;
2381        result
2382    }
2383
2384    fn run_scene_user_cmd_inline(
2385        &mut self,
2386        scn_name: Option<&str>,
2387        cmd_name: &str,
2388        call_args: &[Value],
2389        ret_form: i32,
2390        frame_action_proc: bool,
2391    ) -> Result<bool> {
2392        if frame_action_proc {
2393            return self.run_scene_user_cmd_frame_action_proc(scn_name, cmd_name, call_args);
2394        }
2395
2396        let Some(requested_scene_no) = self.requested_user_command_scene_no(scn_name)? else {
2397            return Ok(false);
2398        };
2399        let Some(command) = self.resolve_user_command_by_name(requested_scene_no, cmd_name)? else {
2400            if self.runtime_options.trace_frame_action_call {
2401                eprintln!(
2402                    "[SG_FRAME_ACTION_CALL] user command not found: requested_scene={} scn_name={:?} cmd={}",
2403                    requested_scene_no, scn_name, cmd_name
2404                );
2405            }
2406            return Ok(false);
2407        };
2408
2409        if self.current_scene_no == Some(command.target_scene_no) {
2410            let return_pc = self.stream.get_prg_cntr();
2411            return self.run_user_cmd_inline_at_offset(
2412                &command.name,
2413                command.target_offset,
2414                return_pc,
2415                None,
2416                Some(return_pc),
2417                ret_form,
2418                call_args,
2419                false,
2420            );
2421        }
2422
2423        self.run_scene_user_cmd_inline_at_cached_scene_offset(
2424            command.target_scene_no,
2425            &command.name,
2426            command.target_offset,
2427            call_args,
2428            ret_form,
2429            false,
2430            false,
2431        )
2432    }
2433
2434    fn run_scene_user_cmd_frame_action_proc(
2435        &mut self,
2436        scn_name: Option<&str>,
2437        cmd_name: &str,
2438        call_args: &[Value],
2439    ) -> Result<bool> {
2440        let checkpoint = self.inline_exec_checkpoint();
2441        let saved_scene_no = checkpoint.scene_no;
2442        // Frame-action callbacks execute through the same interpreter stacks as
2443        // the suspended caller. If the callback consumes an operand that the
2444        // caller had already pushed for an unfinished expression, restoring only
2445        // stack lengths cannot reconstruct that value. Keep an exact snapshot
2446        // and restore it only after a normal callback RETURN.
2447        let saved_int_stack = self.int_stack.clone();
2448        let saved_str_stack = self.str_stack.clone();
2449        let saved_element_points = self.element_points.clone();
2450        let saved_scene_stack_len = checkpoint.scene_depth;
2451        let saved_call_depth = checkpoint.call_depth;
2452
2453        let Some(requested_scene_no) = self.requested_user_command_scene_no(scn_name)? else {
2454            return Ok(false);
2455        };
2456        let Some(command) = self.resolve_user_command_by_name(requested_scene_no, cmd_name)? else {
2457            if self.runtime_options.trace_frame_action_call {
2458                eprintln!(
2459                    "[SG_FRAME_ACTION_CALL] user command not found: requested_scene={} scn_name={:?} cmd={}",
2460                    requested_scene_no, scn_name, cmd_name
2461                );
2462            }
2463            return Ok(false);
2464        };
2465        // Frame actions are an independent nested SCRIPT proc in the original
2466        // engine and continue after the main scenario proc has returned.
2467        // Preserve the caller's halted bit in saved_exec, but do not let it
2468        // abort this callback after its first opcode.
2469        self.halted = false;
2470        self.enter_resolved_user_command(&command, self.cfg.fm_void, call_args, false, true)?;
2471
2472        if self.runtime_options.trace_frame_action_call {
2473            eprintln!(
2474                "[SG_FRAME_ACTION_CALL] proc enter cmd={} scene={:?} depth={} args={:?}",
2475                cmd_name,
2476                self.current_scene_no,
2477                self.call_stack.len(),
2478                call_args
2479            );
2480        }
2481
2482        let mut completed_by_return = false;
2483        let mut stopped_at_proc_boundary = false;
2484        let mut stopped_at_wait_boundary = false;
2485        let mut run_error = None;
2486        let max_steps = self.runtime_options.frame_action_max_steps;
2487        let mut steps: u64 = 0;
2488        loop {
2489            let wait_generation_before_step = self.ctx.wait.block_generation();
2490            let proc_generation_before_step = self.ctx.proc_generation();
2491            let running = match self.step_inner(false) {
2492                Ok(v) => v,
2493                Err(e) => {
2494                    run_error = Some(e);
2495                    break;
2496                }
2497            };
2498            if self.current_scene_no == saved_scene_no
2499                && self.scene_stack.len() == saved_scene_stack_len
2500                && self.call_stack.len() == saved_call_depth
2501            {
2502                completed_by_return = true;
2503                break;
2504            }
2505            if self.halted || !running {
2506                break;
2507            }
2508            if self.ctx.proc_generation() != proc_generation_before_step {
2509                stopped_at_proc_boundary = true;
2510                break;
2511            }
2512            if self.ctx.wait.block_generation() != wait_generation_before_step
2513                && self.ctx.wait_poll()
2514            {
2515                stopped_at_wait_boundary = true;
2516                break;
2517            }
2518            steps = steps.saturating_add(1);
2519            if max_steps > 0 && steps >= max_steps {
2520                run_error = Some(anyhow!(
2521                    "frame_action user command exceeded SIGLUS_FRAME_ACTION_MAX_STEPS: cmd={} scene={:?}",
2522                    cmd_name,
2523                    scn_name
2524                ));
2525                break;
2526            }
2527        }
2528
2529        // Original Siglus enters frame-action user commands through the normal
2530        // call stack (tnm_scene_proc_call_user_cmd + recursive tnm_proc_script).
2531        // The caller VM stacks are shared, not deep-copied.  For the synchronous
2532        // Rust frame phase we only need to discard any unfinished temporary
2533        // frames/stack tail when execution stops at a wait/proc/error boundary.
2534        // Normal callback writes to globals / user properties remain visible,
2535        // matching the original engine.
2536        let restore_callback_lexer = self.current_scene_no == saved_scene_no;
2537        if restore_callback_lexer {
2538            if self.runtime_options.trace_frame_action_call {
2539                eprintln!(
2540                    "[SG_FRAME_ACTION_CALL] proc exit cmd={} scene={:?} completed={} proc_boundary={} wait_boundary={} error={} restoring caller execution checkpoint",
2541                    cmd_name,
2542                    scn_name,
2543                    completed_by_return,
2544                    stopped_at_proc_boundary,
2545                    stopped_at_wait_boundary,
2546                    run_error.is_some()
2547                );
2548            }
2549            if completed_by_return {
2550                self.int_stack = saved_int_stack;
2551                self.str_stack = saved_str_stack;
2552                self.element_points = saved_element_points;
2553            }
2554            self.restore_inline_exec_checkpoint(checkpoint)?;
2555        }
2556
2557        if let Some(e) = run_error {
2558            return Err(e);
2559        }
2560
2561        Ok(true)
2562    }
2563
2564    fn enter_current_scene_user_cmd_proc_at_offset(
2565        &mut self,
2566        offset: usize,
2567        ret_form: i32,
2568        call_args: &[Value],
2569        excall_proc: bool,
2570        frame_action_proc: bool,
2571    ) -> Result<bool> {
2572        let return_pc = self.stream.get_prg_cntr();
2573        let depth = self.call_stack.len();
2574        let Some(caller) = self.call_stack.last_mut() else {
2575            return Ok(false);
2576        };
2577        if self.runtime_options.trace_call_return_pc {
2578            eprintln!(
2579                "[SG_CALL_PC] proc-call set depth={} offset=0x{:x} return_pc=0x{:x} old=0x{:x} frame_action={}",
2580                depth, offset, return_pc, caller.return_pc, frame_action_proc
2581            );
2582        }
2583        caller.return_pc = return_pc;
2584        caller.return_scene_no = self.current_scene_no;
2585        caller.return_scene_name = self.current_scene_name.clone();
2586        caller.return_line_no = self.current_line_no;
2587        caller.ret_form = ret_form;
2588        for arg in call_args {
2589            self.push_call_arg_value(arg);
2590        }
2591        let mut call_frame = self.take_call_frame(
2592            self.cfg.fm_void,
2593            excall_proc,
2594            frame_action_proc,
2595            call_args.len(),
2596            None,
2597        );
2598        call_frame.call_type = 3;
2599        call_frame.return_override = Some((return_pc, ret_form));
2600        self.call_stack.push(call_frame);
2601        self.stream.set_prg_cntr(offset)?;
2602        if excall_proc {
2603            self.mark_excall_script_proc_requested();
2604        }
2605        Ok(true)
2606    }
2607
2608    fn enter_scene_user_cmd_at_scene_offset_ex(
2609        &mut self,
2610        target_scene_no: usize,
2611        target_offset: usize,
2612        call_args: &[Value],
2613        ret_form: i32,
2614        ex_call_proc: bool,
2615        frame_action_proc: bool,
2616    ) -> Result<bool> {
2617        let target_stream = self.cached_scene_stream(target_scene_no)?;
2618        if target_offset > target_stream.scn.len() {
2619            bail!(
2620                "scene_pck: user command offset out of bounds: scn_no={} offset=0x{:x} scn_len=0x{:x}",
2621                target_scene_no,
2622                target_offset,
2623                target_stream.scn.len()
2624            );
2625        }
2626
2627        // C++ tnm_scene_proc_call_user_cmd() stores the caller lexer position
2628        // on the current C_elm_call and then add_call()s one callee.  VM value
2629        // stacks and the call list themselves stay shared across scenes.
2630        let return_pc = self.stream.get_prg_cntr();
2631        let depth = self.call_stack.len();
2632        let Some(caller) = self.call_stack.last_mut() else {
2633            return Ok(false);
2634        };
2635        if self.runtime_options.trace_call_return_pc {
2636            eprintln!(
2637                "[SG_CALL_PC] cross-scene user-cmd set depth={} target_scene={} offset=0x{:x} return_pc=0x{:x} old=0x{:x} frame_action={}",
2638                depth,
2639                target_scene_no,
2640                target_offset,
2641                return_pc,
2642                caller.return_pc,
2643                frame_action_proc
2644            );
2645        }
2646        caller.return_pc = return_pc;
2647        caller.return_scene_no = self.current_scene_no;
2648        caller.return_scene_name = self.current_scene_name.clone();
2649        caller.return_line_no = self.current_line_no;
2650        caller.ret_form = ret_form;
2651
2652        let target_user_cmd_names = target_stream.scn_cmd_name_map.clone();
2653        let (target_call_cmd_names, target_scene_name) = {
2654            let pck = self
2655                .scene_pck_cache
2656                .as_ref()
2657                .expect("scene pck cache initialized");
2658            (
2659                pck.inc_cmd_name_map.clone(),
2660                pck.find_scene_name(target_scene_no).map(ToOwned::to_owned),
2661            )
2662        };
2663
2664        let saved_stream = std::mem::replace(&mut self.stream, target_stream);
2665        let saved_user_cmd_names =
2666            std::mem::replace(&mut self.user_cmd_names, target_user_cmd_names);
2667        let saved_call_cmd_names =
2668            std::mem::replace(&mut self.call_cmd_names, target_call_cmd_names);
2669        let saved_current_scene_no = self.current_scene_no;
2670        let saved_current_scene_name = self.current_scene_name.clone();
2671        let saved_current_line_no = self.current_line_no;
2672
2673        self.enter_cross_scene_user_prop_scope(target_scene_no);
2674        self.current_scene_no = Some(target_scene_no);
2675        self.current_scene_name = target_scene_name;
2676        self.current_line_no = -1;
2677        self.ctx.current_scene_no = Some(target_scene_no as i64);
2678        self.ctx.current_scene_name = self.current_scene_name.clone();
2679        self.ctx.current_line_no = -1;
2680
2681        for arg in call_args {
2682            self.push_call_arg_value(arg);
2683        }
2684        let mut call_frame = self.take_call_frame(
2685            self.cfg.fm_void,
2686            ex_call_proc,
2687            frame_action_proc,
2688            call_args.len(),
2689            None,
2690        );
2691        call_frame.call_type = 3;
2692        self.call_stack.push(call_frame);
2693        self.scene_stack.push(SceneExecFrame {
2694            stream: saved_stream,
2695            user_cmd_names: saved_user_cmd_names,
2696            call_cmd_names: saved_call_cmd_names,
2697            current_scene_no: saved_current_scene_no,
2698            current_scene_name: saved_current_scene_name,
2699            current_line_no: saved_current_line_no,
2700            call_depth: self.call_stack.len(),
2701        });
2702        self.stream.set_prg_cntr(target_offset)?;
2703        if ex_call_proc {
2704            self.mark_excall_script_proc_requested();
2705        }
2706        Ok(true)
2707    }
2708
2709    fn enter_scene_user_cmd_call(
2710        &mut self,
2711        scn_name: Option<&str>,
2712        cmd_name: &str,
2713        call_args: &[Value],
2714    ) -> Result<bool> {
2715        let Some(requested_scene_no) = self.requested_user_command_scene_no(scn_name)? else {
2716            return Ok(false);
2717        };
2718        let Some(command) = self.resolve_user_command_by_name(requested_scene_no, cmd_name)? else {
2719            if self.runtime_options.sg_debug {
2720                eprintln!(
2721                    "[SG_DEBUG][BUTTON] user command not found for ex-call: requested_scene={} scn_name={:?} cmd={}",
2722                    requested_scene_no, scn_name, cmd_name
2723                );
2724            }
2725            return Ok(false);
2726        };
2727
2728        if self.runtime_options.sg_debug {
2729            eprintln!(
2730                "[SG_DEBUG][BUTTON] enter user command requested_scene={} target_scene={} cmd={} encoded_no={} include={} offset=0x{:x}",
2731                requested_scene_no,
2732                command.target_scene_no,
2733                command.name.as_str(),
2734                command.encoded_no,
2735                command.include_command,
2736                command.target_offset
2737            );
2738        }
2739        self.enter_resolved_user_command(&command, self.cfg.fm_void, call_args, true, false)
2740    }
2741
2742    fn enter_current_scene_user_cmd_at_offset(
2743        &mut self,
2744        offset: usize,
2745        call_args: &[Value],
2746    ) -> Result<bool> {
2747        self.enter_current_scene_user_cmd_proc_at_offset(
2748            offset,
2749            self.cfg.fm_void,
2750            call_args,
2751            true,
2752            false,
2753        )
2754    }
2755
2756    fn enter_scene_user_cmd_at_scene_offset(
2757        &mut self,
2758        target_scene_no: usize,
2759        target_offset: usize,
2760        call_args: &[Value],
2761    ) -> Result<bool> {
2762        self.enter_scene_user_cmd_at_scene_offset_ex(
2763            target_scene_no,
2764            target_offset,
2765            call_args,
2766            self.cfg.fm_void,
2767            true,
2768            false,
2769        )
2770    }
2771
2772    fn run_current_scene_user_cmd_inline(
2773        &mut self,
2774        cmd_name: &str,
2775        call_args: &[Value],
2776    ) -> Result<bool> {
2777        self.run_scene_user_cmd_inline(None, cmd_name, call_args, self.cfg.fm_void, false)
2778    }
2779
2780    fn run_scene_user_cmd_inline_at_scene_offset(
2781        &mut self,
2782        pck: &ScenePck,
2783        target_scene_no: usize,
2784        cmd_name: &str,
2785        target_offset: usize,
2786        call_args: &[Value],
2787        preserve_return_pc: bool,
2788        frame_action_proc: bool,
2789    ) -> Result<bool> {
2790        let (owner, range) = pck.scn_data_shared(target_scene_no)?;
2791        let target_stream: SceneStream<'a> =
2792            SceneStream::new_shared_range_with_string_codec(owner, range, pck.string_codec)?;
2793        if target_offset > target_stream.scn.len() {
2794            bail!(
2795                "scene_pck: user command offset out of bounds: cmd={} scn_no={} offset=0x{:x} scn_len=0x{:x}",
2796                cmd_name,
2797                target_scene_no,
2798                target_offset,
2799                target_stream.scn.len()
2800            );
2801        }
2802
2803        let saved_stream = std::mem::replace(&mut self.stream, target_stream);
2804        let target_user_cmd_names = self.stream.scn_cmd_name_map.clone();
2805        let target_call_cmd_names = pck.inc_cmd_name_map.clone();
2806        let saved_user_cmd_names =
2807            std::mem::replace(&mut self.user_cmd_names, target_user_cmd_names);
2808        let saved_call_cmd_names =
2809            std::mem::replace(&mut self.call_cmd_names, target_call_cmd_names);
2810        let saved_current_scene_no = self.current_scene_no;
2811        let saved_current_scene_name = self.current_scene_name.clone();
2812        let saved_current_line_no = self.current_line_no;
2813        let saved_ctx_scene_no = self.ctx.current_scene_no;
2814        let saved_ctx_scene_name = self.ctx.current_scene_name.clone();
2815        let saved_ctx_line_no = self.ctx.current_line_no;
2816        let saved_halted = self.halted;
2817        self.enter_cross_scene_user_prop_scope(target_scene_no);
2818
2819        self.current_scene_no = Some(target_scene_no);
2820        self.current_scene_name = pck.find_scene_name(target_scene_no).map(ToOwned::to_owned);
2821        self.current_line_no = -1;
2822        self.ctx.current_scene_no = Some(target_scene_no as i64);
2823        self.ctx.current_scene_name = self.current_scene_name.clone();
2824        self.ctx.current_line_no = -1;
2825
2826        let target_return_pc = if preserve_return_pc {
2827            saved_stream.get_prg_cntr()
2828        } else {
2829            self.stream.scn.len()
2830        };
2831        let result = self.run_user_cmd_inline_at_offset(
2832            cmd_name,
2833            target_offset,
2834            target_return_pc,
2835            None,
2836            None,
2837            self.cfg.fm_void,
2838            call_args,
2839            frame_action_proc,
2840        );
2841
2842        // Save the target scene's locals while current_scene_no still
2843        // identifies it, then reactivate the caller's resident scene scope.
2844        self.restore_cross_scene_user_prop_scope(saved_current_scene_no);
2845        self.stream = saved_stream;
2846        self.user_cmd_names = saved_user_cmd_names;
2847        self.call_cmd_names = saved_call_cmd_names;
2848        self.current_scene_no = saved_current_scene_no;
2849        self.current_scene_name = saved_current_scene_name;
2850        self.current_line_no = saved_current_line_no;
2851        self.ctx.current_scene_no = saved_ctx_scene_no;
2852        self.ctx.current_scene_name = saved_ctx_scene_name;
2853        self.ctx.current_line_no = saved_ctx_line_no;
2854        self.halted = saved_halted;
2855        result
2856    }
2857
2858    fn collect_object_frame_action_work_recursive(
2859        obj: &crate::runtime::globals::ObjectState,
2860        stage_idx: i64,
2861        obj_idx: usize,
2862        // Extended and truncated in place while descending: most objects
2863        // (particles) have no frame action, and a copy per child per frame
2864        // added up.
2865        object_chain: &mut Vec<i32>,
2866        out: &mut Vec<FrameActionWork>,
2867    ) {
2868        let fa = &obj.frame_action;
2869        if !fa.cmd_name.is_empty() {
2870            let mut frame_action_chain = object_chain.clone();
2871            frame_action_chain.push(crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION);
2872            out.push(FrameActionWork {
2873                stage_idx,
2874                obj_idx,
2875                ch_idx: None,
2876                global_form_id: None,
2877                object_chain: Some(object_chain.clone()),
2878                frame_action_chain: Some(frame_action_chain),
2879                scn_name: fa.scn_name.clone(),
2880                cmd_name: fa.cmd_name.clone(),
2881                args: fa.args.clone(),
2882                count: fa.counter.get_count(),
2883                end_time: fa.end_time,
2884            });
2885        }
2886        for (ch_idx, ch) in obj.frame_action_ch.iter().enumerate() {
2887            if !ch.cmd_name.is_empty() {
2888                let mut frame_action_chain = object_chain.clone();
2889                frame_action_chain
2890                    .push(crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION_CH);
2891                frame_action_chain.push(crate::runtime::forms::codes::ELM_ARRAY);
2892                frame_action_chain.push(ch_idx as i32);
2893                out.push(FrameActionWork {
2894                    stage_idx,
2895                    obj_idx,
2896                    ch_idx: Some(ch_idx),
2897                    global_form_id: None,
2898                    object_chain: Some(object_chain.clone()),
2899                    frame_action_chain: Some(frame_action_chain),
2900                    scn_name: ch.scn_name.clone(),
2901                    cmd_name: ch.cmd_name.clone(),
2902                    args: ch.args.clone(),
2903                    count: ch.counter.get_count(),
2904                    end_time: ch.end_time,
2905                });
2906            }
2907        }
2908        for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
2909            // CHILD object lists are initialized with use_ini=false in C++;
2910            // every allocated child slot therefore has use_flag=true even when
2911            // its current type is NONE.  Recurse over the list itself instead
2912            // of treating ObjectState::used as C_elm_object::is_use().
2913            let depth = object_chain.len();
2914            object_chain.extend([
2915                crate::runtime::forms::codes::elm_value::OBJECT_CHILD,
2916                crate::runtime::forms::codes::ELM_ARRAY,
2917                child_idx as i32,
2918            ]);
2919            Self::collect_object_frame_action_work_recursive(
2920                child,
2921                stage_idx,
2922                child_idx,
2923                object_chain,
2924                out,
2925            );
2926            object_chain.truncate(depth);
2927        }
2928    }
2929
2930    fn object_child_from_chain_mut<'b>(
2931        mut obj: &'b mut crate::runtime::globals::ObjectState,
2932        object_chain: &[i32],
2933        mut pos: usize,
2934        elm_array: i32,
2935    ) -> Option<&'b mut crate::runtime::globals::ObjectState> {
2936        while pos + 2 < object_chain.len() {
2937            let op = object_chain[pos];
2938            if op != crate::runtime::forms::codes::elm_value::OBJECT_CHILD {
2939                break;
2940            }
2941            if object_chain[pos + 1] != elm_array
2942                && object_chain[pos + 1] != crate::runtime::forms::codes::ELM_ARRAY
2943            {
2944                return None;
2945            }
2946            let child_idx = object_chain[pos + 2].max(0) as usize;
2947            obj = obj.runtime.child_objects.get_mut(child_idx)?;
2948            pos += 3;
2949        }
2950        Some(obj)
2951    }
2952
2953    fn with_frame_action_mut<R>(
2954        &mut self,
2955        item: &FrameActionWork,
2956        f: impl FnOnce(&mut crate::runtime::globals::ObjectFrameActionState) -> R,
2957    ) -> Option<R> {
2958        if item.object_chain.is_none() {
2959            let form_id = item.global_form_id?;
2960            if let Some(idx) = item.ch_idx {
2961                let list = self.ctx.globals.frame_action_lists.get_mut(&form_id)?;
2962                return list.get_mut(idx).map(f);
2963            }
2964            return self.ctx.globals.frame_actions.get_mut(&form_id).map(f);
2965        }
2966
2967        let chain = item.object_chain.as_deref()?;
2968        let elm_array = if self.ctx.ids.elm_array != 0 {
2969            self.ctx.ids.elm_array
2970        } else {
2971            crate::runtime::forms::codes::ELM_ARRAY
2972        };
2973        let locator = parse_frame_action_object_locator(chain, elm_array)?;
2974        let form_id =
2975            crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, locator.raw_form_id);
2976        let st = self.ctx.globals.stage_forms.get_mut(&form_id)?;
2977        let obj = match locator.root {
2978            FrameActionObjectRoot::Stage { obj_idx, child_pos } => {
2979                let objects = st.object_lists.get_mut(&locator.stage_idx)?;
2980                let obj = objects.get_mut(obj_idx)?;
2981                Self::object_child_from_chain_mut(obj, chain, child_pos, elm_array)?
2982            }
2983            FrameActionObjectRoot::Mwnd {
2984                mwnd_idx,
2985                selector,
2986                obj_idx,
2987                child_pos,
2988            } => {
2989                let mwnds = st.mwnd_lists.get_mut(&locator.stage_idx)?;
2990                let mwnd = mwnds.get_mut(mwnd_idx)?;
2991                let obj = if selector == crate::runtime::forms::codes::elm_value::MWND_BUTTON {
2992                    mwnd.button_list.get_mut(obj_idx)?
2993                } else if selector == crate::runtime::forms::codes::elm_value::MWND_FACE {
2994                    mwnd.face_list.get_mut(obj_idx)?
2995                } else {
2996                    mwnd.object_list.get_mut(obj_idx)?
2997                };
2998                Self::object_child_from_chain_mut(obj, chain, child_pos, elm_array)?
2999            }
3000            FrameActionObjectRoot::BtnSelItem {
3001                item_idx,
3002                obj_idx,
3003                child_pos,
3004            } => {
3005                let items = st.btnselitem_lists.get_mut(&locator.stage_idx)?;
3006                let item = items.get_mut(item_idx)?;
3007                let obj = item.object_list.get_mut(obj_idx)?;
3008                Self::object_child_from_chain_mut(obj, chain, child_pos, elm_array)?
3009            }
3010        };
3011
3012        if let Some(idx) = item.ch_idx {
3013            obj.frame_action_ch.get_mut(idx).map(f)
3014        } else {
3015            Some(f(&mut obj.frame_action))
3016        }
3017    }
3018
3019    fn begin_frame_action_finish(
3020        &mut self,
3021        item: &FrameActionWork,
3022    ) -> Option<(String, String, Vec<Value>)> {
3023        self.with_frame_action_mut(item, |fa| {
3024            if fa.cmd_name.is_empty() || fa.end_time < 0 {
3025                return None;
3026            }
3027            if fa.counter.get_count() < fa.end_time {
3028                return None;
3029            }
3030
3031            // C_elm_frame_action::frame() checks the live action after the per-frame
3032            // do_action() callback. If that callback replaced itself, finish the
3033            // replacement that is now installed, not the snapshot collected earlier.
3034            let scn_name = fa.scn_name.clone();
3035            let cmd_name = fa.cmd_name.clone();
3036            let args = fa.args.clone();
3037            fa.counter.set_count(fa.end_time);
3038            fa.scn_name.clear();
3039            fa.cmd_name.clear();
3040            fa.end_flag = true;
3041            Some((scn_name, cmd_name, args))
3042        })?
3043    }
3044
3045    fn end_frame_action_finish(&mut self, item: &FrameActionWork) {
3046        let _ = self.with_frame_action_mut(item, |fa| {
3047            // frame() calls reinit(true), so after finish() returns it performs
3048            // reinit(false). Any action started by the finish callback is cleared;
3049            // m_end_time is the one field reinit intentionally preserves.
3050            fa.reinit_without_finish();
3051        });
3052    }
3053
3054    fn make_frame_action_call_args(
3055        frame_action_chain: Option<&Vec<i32>>,
3056        object_chain: Option<&Vec<i32>>,
3057        args: &[Value],
3058    ) -> Vec<Value> {
3059        let mut call_args = Vec::with_capacity(args.len() + 2);
3060        if let Some(frame_action_chain) = frame_action_chain {
3061            call_args.push(Value::Element(frame_action_chain.clone()));
3062        }
3063        if let Some(object_chain) = object_chain {
3064            call_args.push(Value::Element(object_chain.clone()));
3065        }
3066        call_args.extend(args.iter().cloned());
3067        call_args
3068    }
3069
3070    fn runtime_slot_from_object_children(
3071        mut obj: &mut crate::runtime::globals::ObjectState,
3072        fallback_slot: usize,
3073        chain: &[i32],
3074        mut pos: usize,
3075        elm_array: i32,
3076        next_slot: &mut usize,
3077    ) -> usize {
3078        let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
3079        let mut slot = obj.runtime_slot_or(fallback_slot);
3080        while pos + 2 < chain.len() {
3081            if chain[pos] == object_child
3082                && (chain[pos + 1] == elm_array
3083                    || chain[pos + 1] == crate::runtime::forms::codes::ELM_ARRAY)
3084            {
3085                let child_idx = chain[pos + 2].max(0) as usize;
3086                if obj.runtime.child_objects.len() <= child_idx {
3087                    obj.runtime
3088                        .child_objects
3089                        .resize_with(child_idx + 1, crate::runtime::globals::ObjectState::default);
3090                }
3091                let child = &mut obj.runtime.child_objects[child_idx];
3092                slot = child.ensure_runtime_slot(next_slot);
3093                obj = child;
3094                pos += 3;
3095            } else {
3096                pos += 1;
3097            }
3098        }
3099        slot
3100    }
3101
3102    fn runtime_slot_from_object_chain(&mut self, fallback_obj_idx: usize, chain: &[i32]) -> usize {
3103        let elm_array = if self.ctx.ids.elm_array != 0 {
3104            self.ctx.ids.elm_array
3105        } else {
3106            crate::runtime::forms::codes::ELM_ARRAY
3107        };
3108        let Some(locator) = parse_frame_action_object_locator(chain, elm_array) else {
3109            return fallback_obj_idx;
3110        };
3111        let stage_form =
3112            crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, locator.raw_form_id);
3113        let Some(st) = self.ctx.globals.stage_forms.get_mut(&stage_form) else {
3114            return fallback_obj_idx;
3115        };
3116        let nested_slot_base = st.backend_slot_base + 100_000;
3117        let next_slot = st
3118            .next_nested_object_slot
3119            .entry(locator.stage_idx)
3120            .or_insert(nested_slot_base);
3121
3122        match locator.root {
3123            FrameActionObjectRoot::Stage { obj_idx, child_pos } => {
3124                let Some(list) = st.object_lists.get_mut(&locator.stage_idx) else {
3125                    return obj_idx;
3126                };
3127                let Some(obj) = list.get_mut(obj_idx) else {
3128                    return obj_idx;
3129                };
3130                Self::runtime_slot_from_object_children(
3131                    obj, obj_idx, chain, child_pos, elm_array, next_slot,
3132                )
3133            }
3134            FrameActionObjectRoot::Mwnd {
3135                mwnd_idx,
3136                selector,
3137                obj_idx,
3138                child_pos,
3139            } => {
3140                let Some(mwnds) = st.mwnd_lists.get_mut(&locator.stage_idx) else {
3141                    return obj_idx;
3142                };
3143                let Some(mwnd) = mwnds.get_mut(mwnd_idx) else {
3144                    return obj_idx;
3145                };
3146                let obj = if selector == crate::runtime::forms::codes::elm_value::MWND_BUTTON {
3147                    mwnd.button_list.get_mut(obj_idx)
3148                } else if selector == crate::runtime::forms::codes::elm_value::MWND_FACE {
3149                    mwnd.face_list.get_mut(obj_idx)
3150                } else {
3151                    mwnd.object_list.get_mut(obj_idx)
3152                };
3153                let Some(obj) = obj else {
3154                    return obj_idx;
3155                };
3156                Self::runtime_slot_from_object_children(
3157                    obj, obj_idx, chain, child_pos, elm_array, next_slot,
3158                )
3159            }
3160            FrameActionObjectRoot::BtnSelItem {
3161                item_idx,
3162                obj_idx,
3163                child_pos,
3164            } => {
3165                let Some(items) = st.btnselitem_lists.get_mut(&locator.stage_idx) else {
3166                    return obj_idx;
3167                };
3168                let Some(item) = items.get_mut(item_idx) else {
3169                    return obj_idx;
3170                };
3171                let Some(obj) = item.object_list.get_mut(obj_idx) else {
3172                    return obj_idx;
3173                };
3174                Self::runtime_slot_from_object_children(
3175                    obj, obj_idx, chain, child_pos, elm_array, next_slot,
3176                )
3177            }
3178        }
3179    }
3180
3181    fn set_frame_action_current_object(
3182        &mut self,
3183        item: &FrameActionWork,
3184    ) -> (Option<(i64, usize)>, Option<Vec<i32>>) {
3185        let prev_target = self.ctx.globals.current_stage_object;
3186        let prev_chain = self.ctx.globals.current_object_chain.clone();
3187        if let Some(chain) = item.object_chain.clone() {
3188            let elm_array = if self.ctx.ids.elm_array != 0 {
3189                self.ctx.ids.elm_array
3190            } else {
3191                crate::runtime::forms::codes::ELM_ARRAY
3192            };
3193            if let Some(locator) = parse_frame_action_object_locator(&chain, elm_array) {
3194                let top_idx = frame_action_locator_object_idx(locator);
3195                let runtime_slot = self.runtime_slot_from_object_chain(top_idx, &chain);
3196                self.ctx.globals.current_stage_object = Some((locator.stage_idx, runtime_slot));
3197                self.ctx.globals.current_object_chain = Some(chain);
3198            } else {
3199                self.ctx.globals.current_stage_object = None;
3200                self.ctx.globals.current_object_chain = None;
3201            }
3202        } else {
3203            self.ctx.globals.current_stage_object = None;
3204            self.ctx.globals.current_object_chain = None;
3205        }
3206        (prev_target, prev_chain)
3207    }
3208
3209    fn restore_frame_action_current_object(
3210        &mut self,
3211        prev_target: Option<(i64, usize)>,
3212        prev_chain: Option<Vec<i32>>,
3213    ) {
3214        self.ctx.globals.current_stage_object = prev_target;
3215        self.ctx.globals.current_object_chain = prev_chain;
3216    }
3217
3218    fn frame_action_work_from_pending_finish(
3219        &self,
3220        pending: &PendingFrameActionFinish,
3221    ) -> FrameActionWork {
3222        let object_chain = pending.object_chain.clone();
3223        let elm_array = if self.ctx.ids.elm_array != 0 {
3224            self.ctx.ids.elm_array
3225        } else {
3226            crate::runtime::forms::codes::ELM_ARRAY
3227        };
3228        let (stage_idx, obj_idx) = object_chain
3229            .as_deref()
3230            .and_then(|chain| parse_frame_action_object_locator(chain, elm_array))
3231            .map(|locator| (locator.stage_idx, frame_action_locator_object_idx(locator)))
3232            .unwrap_or((-1, usize::MAX));
3233
3234        let ch_idx = if let Some(chain) = object_chain.as_ref() {
3235            let base = chain.len();
3236            if pending.frame_action_chain.len() >= base + 3
3237                && pending.frame_action_chain[base]
3238                    == crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION_CH
3239                && pending.frame_action_chain[base + 1] == crate::runtime::forms::codes::ELM_ARRAY
3240            {
3241                Some(pending.frame_action_chain[base + 2].max(0) as usize)
3242            } else {
3243                None
3244            }
3245        } else if pending.frame_action_chain.len() >= 3
3246            && pending.frame_action_chain[1] == crate::runtime::forms::codes::ELM_ARRAY
3247        {
3248            Some(pending.frame_action_chain[2].max(0) as usize)
3249        } else {
3250            None
3251        };
3252
3253        let global_form_id = if object_chain.is_none() {
3254            pending
3255                .frame_action_chain
3256                .first()
3257                .copied()
3258                .map(|v| v as u32)
3259        } else {
3260            None
3261        };
3262
3263        FrameActionWork {
3264            stage_idx,
3265            obj_idx,
3266            ch_idx,
3267            global_form_id,
3268            object_chain,
3269            frame_action_chain: Some(pending.frame_action_chain.clone()),
3270            scn_name: pending.scn_name.clone(),
3271            cmd_name: pending.cmd_name.clone(),
3272            args: pending.args.clone(),
3273            count: pending.end_time,
3274            end_time: pending.end_time,
3275        }
3276    }
3277
3278    fn run_pending_frame_action_finish(&mut self, pending: PendingFrameActionFinish) -> Result<()> {
3279        if pending.cmd_name.is_empty() {
3280            return Ok(());
3281        }
3282        let item = self.frame_action_work_from_pending_finish(&pending);
3283        let final_count = if pending.end_time == -1 {
3284            0
3285        } else {
3286            pending.end_time
3287        };
3288
3289        // START/START_REAL/END have already modified the live slot by the time the
3290        // deferred callback is drained. Temporarily restore the complete old state
3291        // and put it into C_elm_frame_action::finish()'s callback-visible shape.
3292        // After the callback, restore the outer state: this reproduces the C++
3293        // reinit(true) ordering without letting the old finish overwrite a new START.
3294        let outer_state = self.with_frame_action_mut(&item, |fa| {
3295            let outer_state = fa.clone();
3296            let mut finishing = pending.snapshot.clone();
3297            finishing.scn_name.clear();
3298            finishing.cmd_name.clear();
3299            finishing.counter.set_count(final_count);
3300            finishing.end_flag = true;
3301            *fa = finishing;
3302            outer_state
3303        });
3304
3305        let call_args = Self::make_frame_action_call_args(
3306            item.frame_action_chain.as_ref(),
3307            item.object_chain.as_ref(),
3308            &pending.args,
3309        );
3310        let (prev_target, prev_chain) = self.set_frame_action_current_object(&item);
3311        let result = self.run_scene_user_cmd_inline(
3312            Some(&pending.scn_name),
3313            &pending.cmd_name,
3314            &call_args,
3315            self.cfg.fm_void,
3316            true,
3317        );
3318        self.restore_frame_action_current_object(prev_target, prev_chain);
3319
3320        if pending.reinit_after_finish {
3321            let _ = self.with_frame_action_mut(&item, |fa| {
3322                fa.reinit_without_finish();
3323            });
3324        } else if let Some(outer_state) = outer_state {
3325            let _ = self.with_frame_action_mut(&item, |fa| {
3326                *fa = outer_state;
3327            });
3328        }
3329        if let Err(e) = result {
3330            self.ctx.unknown.record_note(&format!(
3331                "frame_action.finish.failed:{}:{}:{e}",
3332                pending.scn_name, pending.cmd_name
3333            ));
3334        }
3335        Ok(())
3336    }
3337
3338    fn run_pending_button_action(&mut self, action: PendingButtonAction) -> Result<()> {
3339        // Original tona3/Siglus copies runtime input into script input before
3340        // frame_main_proc(), while object button actions are decided later from
3341        // the element/frame pass.  A scene or user command entered by a button
3342        // action must therefore not observe the same mouse down/up stock that
3343        // decided the button.  Clear edge stocks here while preserving held
3344        // state and mouse position.
3345        self.ctx.input.use_current();
3346        self.script_input_synced_this_frame = false;
3347
3348        match action.kind {
3349            PendingButtonActionKind::UserCall {
3350                scn_name,
3351                cmd_name,
3352                z_no,
3353            } => {
3354                if scn_name.is_empty() {
3355                    return Ok(());
3356                }
3357                if self.runtime_options.sg_debug {
3358                    eprintln!(
3359                        "[SG_DEBUG][BUTTON] run action scene={} cmd={} z_no={}",
3360                        scn_name, cmd_name, z_no
3361                    );
3362                }
3363                if !cmd_name.is_empty() {
3364                    let _ = self.enter_scene_user_cmd_call(Some(&scn_name), &cmd_name, &[])?;
3365                } else if z_no >= 0 {
3366                    self.farcall_scene_name_ex(
3367                        &scn_name,
3368                        z_no as i32,
3369                        self.cfg.fm_void,
3370                        true,
3371                        &[],
3372                    )?;
3373                }
3374            }
3375            PendingButtonActionKind::Syscom {
3376                sys_type,
3377                sys_type_opt,
3378                mode,
3379            } => {
3380                self.run_pending_button_syscom_action(sys_type, sys_type_opt, mode)?;
3381            }
3382        }
3383        Ok(())
3384    }
3385
3386    #[inline(always)]
3387    fn syscom_proc_trace_enabled(&self) -> bool {
3388        self.runtime_options.syscom_proc_trace
3389    }
3390
3391    fn syscom_trace_state(&self) -> String {
3392        let st = &self.ctx.globals.syscom;
3393        let msgbk_form = self.ctx.ids.form_global_msgbk;
3394        let msgbk_count = self
3395            .ctx
3396            .globals
3397            .msgbk_forms
3398            .get(&msgbk_form)
3399            .map(|m| m.history.len())
3400            .unwrap_or(0);
3401        let msgbk_visible_count = self
3402            .ctx
3403            .globals
3404            .msgbk_forms
3405            .get(&msgbk_form)
3406            .map(|m| {
3407                m.history
3408                    .iter()
3409                    .filter(|entry| {
3410                        entry.pct_flag
3411                            || !entry.msg_str.is_empty()
3412                            || !entry.disp_name.is_empty()
3413                            || !entry.original_name.is_empty()
3414                            || !entry.koe_no_list.is_empty()
3415                    })
3416                    .count()
3417            })
3418            .unwrap_or(0);
3419        format!(
3420            "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={}",
3421            st.read_skip.onoff,
3422            st.auto_mode.onoff,
3423            st.hide_mwnd.onoff,
3424            st.msg_back_open,
3425            st.msg_back.check_enabled(),
3426            st.pending_proc,
3427            msgbk_form,
3428            msgbk_count,
3429            msgbk_visible_count,
3430            self.ctx.ui.message_waiting(),
3431            self.ctx.ui.message_visible_chars(),
3432            self.ctx.ui.message_wait_message_len(),
3433            self.ctx.ui.message_text().unwrap_or("").chars().count()
3434        )
3435    }
3436
3437    fn syscom_button_op(sys_type: i64) -> Option<(i32, &'static str)> {
3438        use crate::runtime::forms::codes::syscom_op;
3439        Some(match sys_type {
3440            1 => (syscom_op::CALL_SAVE_MENU, "CALL_SAVE_MENU"),
3441            2 => (syscom_op::CALL_LOAD_MENU, "CALL_LOAD_MENU"),
3442            3 => (
3443                syscom_op::SET_READ_SKIP_ONOFF_FLAG,
3444                "SET_READ_SKIP_ONOFF_FLAG",
3445            ),
3446            4 => (
3447                syscom_op::SET_AUTO_MODE_ONOFF_FLAG,
3448                "SET_AUTO_MODE_ONOFF_FLAG",
3449            ),
3450            5 => (syscom_op::RETURN_TO_SEL, "RETURN_TO_SEL"),
3451            6 => (
3452                syscom_op::SET_HIDE_MWND_ONOFF_FLAG,
3453                "SET_HIDE_MWND_ONOFF_FLAG",
3454            ),
3455            7 => (syscom_op::OPEN_MSG_BACK, "OPEN_MSG_BACK"),
3456            8 => (syscom_op::REPLAY_KOE, "REPLAY_KOE"),
3457            9 => (syscom_op::QUICK_SAVE, "QUICK_SAVE"),
3458            10 => (syscom_op::QUICK_LOAD, "QUICK_LOAD"),
3459            11 => (syscom_op::CALL_CONFIG_MENU, "CALL_CONFIG_MENU"),
3460            12 => (
3461                syscom_op::SET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG,
3462                "SET_LOCAL_EXTRA_SWITCH_ONOFF_FLAG",
3463            ),
3464            13 => (
3465                syscom_op::SET_LOCAL_EXTRA_MODE_VALUE,
3466                "SET_LOCAL_EXTRA_MODE_VALUE",
3467            ),
3468            14 => (
3469                syscom_op::SET_GLOBAL_EXTRA_SWITCH_ONOFF,
3470                "SET_GLOBAL_EXTRA_SWITCH_ONOFF",
3471            ),
3472            15 => (
3473                syscom_op::SET_GLOBAL_EXTRA_MODE_VALUE,
3474                "SET_GLOBAL_EXTRA_MODE_VALUE",
3475            ),
3476            _ => return None,
3477        })
3478    }
3479
3480    fn dispatch_syscom_button_op(&mut self, op: i32, params: &[Value]) -> Result<bool> {
3481        // In this VM, form dispatch is rooted at the GLOBAL.SYSCOM element id
3482        // (normally 63), not at the FM_SYSCOM type id (1600).  A normal script
3483        // call reaches syscom.rs as the element chain [GLOBAL_SYSCOM, op].
3484        // Button actions must use the same root or global::dispatch_form()
3485        // never reaches syscom::dispatch(), and the trace shows handled=false.
3486        let form_id = if self.ctx.ids.form_global_syscom != 0 {
3487            self.ctx.ids.form_global_syscom as i32
3488        } else {
3489            constants::global_form::SYSCOM as i32
3490        };
3491
3492        let saved_call = self.ctx.vm_call.take();
3493        let saved_stack_len = self.ctx.stack.len();
3494        self.ctx.vm_call = Some(runtime::VmCallMeta {
3495            element: vec![form_id, op],
3496            al_id: 0,
3497            ret_form: self.cfg.fm_void as i64,
3498        });
3499
3500        let result = runtime::dispatch_form_code(&mut self.ctx, form_id as u32, params);
3501
3502        self.ctx.vm_call = saved_call;
3503        self.ctx.stack.truncate(saved_stack_len);
3504        result
3505    }
3506
3507    fn run_pending_button_syscom_action(
3508        &mut self,
3509        sys_type: i64,
3510        sys_type_opt: i64,
3511        mode: i64,
3512    ) -> Result<()> {
3513        let trace = self.syscom_proc_trace_enabled();
3514        let Some((op, op_name)) = Self::syscom_button_op(sys_type) else {
3515            if trace {
3516                eprintln!(
3517                    "[SYSCOM_PROC_TRACE] button sys_type={} sys_opt={} mode={} resolved=UNKNOWN before {}",
3518                    sys_type,
3519                    sys_type_opt,
3520                    mode,
3521                    self.syscom_trace_state()
3522                );
3523            }
3524            return Ok(());
3525        };
3526
3527        if matches!(sys_type, 1 | 9) {
3528            crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture(&mut self.ctx);
3529        }
3530
3531        // This follows C_elm_object::check_button_action() in the original engine:
3532        // system buttons call the SYSCOM operation directly.  The form dispatcher
3533        // expects CommandContext::vm_call to carry the current element chain, so
3534        // constructing Value::Element in the argument list is not enough here.
3535        // Button actions also ignore command return values, so any values pushed
3536        // by the generic SYSCOM command handler are discarded after dispatch.
3537        let params: Vec<Value> = match sys_type {
3538            1 | 2 => Vec::new(),
3539            3 | 4 => vec![Value::Int(if mode == 0 { 1 } else { 0 })],
3540            6 => vec![Value::Int(1)],
3541            5 => vec![Value::Int(1), Value::Int(1), Value::Int(1)],
3542            7 | 8 | 11 => Vec::new(),
3543            9 => vec![Value::Int(sys_type_opt), Value::Int(1), Value::Int(1)],
3544            10 => vec![
3545                Value::Int(sys_type_opt),
3546                Value::Int(1),
3547                Value::Int(1),
3548                Value::Int(1),
3549            ],
3550            12 | 14 => vec![
3551                Value::Int(sys_type_opt),
3552                Value::Int(if mode == 0 { 1 } else { 0 }),
3553            ],
3554            13 | 15 => vec![Value::Int(sys_type_opt), Value::Int(mode + 1)],
3555            _ => Vec::new(),
3556        };
3557        if trace {
3558            eprintln!(
3559                "[SYSCOM_PROC_TRACE] button sys_type={} sys_opt={} mode={} resolved={}({}) params={:?} before {}",
3560                sys_type,
3561                sys_type_opt,
3562                mode,
3563                op_name,
3564                op,
3565                params,
3566                self.syscom_trace_state()
3567            );
3568        }
3569        let dispatch_result = self.dispatch_syscom_button_op(op, &params);
3570        if trace {
3571            let status = match dispatch_result.as_ref() {
3572                Ok(handled) => format!("ok handled={}", handled),
3573                Err(err) => format!("err={}", err),
3574            };
3575            eprintln!(
3576                "[SYSCOM_PROC_TRACE] after resolved={}({}) status={} {}",
3577                op_name,
3578                op,
3579                status,
3580                self.syscom_trace_state()
3581            );
3582        }
3583        dispatch_result?;
3584        Ok(())
3585    }
3586
3587    fn drain_pending_button_actions(&mut self) -> Result<()> {
3588        let mut budget = 64usize;
3589        while !self.ctx.globals.pending_button_actions.is_empty() {
3590            if budget == 0 {
3591                bail!("button action queue did not drain");
3592            }
3593            budget -= 1;
3594            let pending = std::mem::take(&mut self.ctx.globals.pending_button_actions);
3595            for action in pending {
3596                self.run_pending_button_action(action)?;
3597            }
3598        }
3599        Ok(())
3600    }
3601
3602    pub fn process_pending_button_actions(&mut self) -> Result<()> {
3603        self.drain_pending_button_actions()
3604    }
3605
3606    fn drain_pending_frame_action_finishes(&mut self) -> Result<()> {
3607        let mut budget = 64usize;
3608        while !self.ctx.globals.pending_frame_action_finishes.is_empty() {
3609            if budget == 0 {
3610                bail!("frame action finish queue did not drain");
3611            }
3612            budget -= 1;
3613            let pending = std::mem::take(&mut self.ctx.globals.pending_frame_action_finishes);
3614            for item in pending {
3615                self.run_pending_frame_action_finish(item)?;
3616            }
3617        }
3618        // C_elm_excall::free() performs finish callbacks synchronously and only
3619        // then releases F/counters/frame-action channels/stages. Runtime form
3620        // dispatch cannot recurse into the VM directly, so EXCALL.FREE defers
3621        // this final release until the finish queue has reached the same point.
3622        crate::runtime::forms::excall::finalize_pending_free(&mut self.ctx);
3623        Ok(())
3624    }
3625
3626    pub fn tick_frame(&mut self) -> Result<()> {
3627        let trace = self.runtime_options.tick_trace || self.runtime_options.frame_action_trace;
3628        if trace {
3629            eprintln!(
3630                "[SG_TICK_TRACE] tick_frame start blocked={} halted={} scene={:?}",
3631                self.is_blocked(),
3632                self.halted,
3633                self.current_scene_name()
3634            );
3635        }
3636        self.drain_pending_button_actions()?;
3637        if self.ctx.globals.syscom.pending_proc.is_some() {
3638            if trace || self.runtime_options.sg_debug {
3639                eprintln!(
3640                    "[SG_DEBUG][SYSCOM_PROC] stop frame tick before frame actions pending_proc={:?}",
3641                    self.ctx.globals.syscom.pending_proc
3642                );
3643            }
3644            return Ok(());
3645        }
3646        if self.ctx.globals.script.frame_action_time_stop_flag && trace {
3647            eprintln!(
3648                "[SG_TICK_TRACE] frame_action_time_stop_flag set; executing callbacks with frozen frame-action time"
3649            );
3650        }
3651
3652        // C++ C_tnm_eng::frame() advances element time before frame_action_proc().
3653        // FRAME_ACTION callbacks read the already advanced counter value, then
3654        // C_elm_frame_action::frame() performs the end check later in the same
3655        // frame.  Keep the same order here; otherwise callbacks such as the MWND
3656        // rotating circle animation repeatedly observe the previous count.
3657        self.ctx.tick_frame();
3658
3659        if let Some(result) = self.ctx.take_pending_sel_point_result() {
3660            // Original decide(): push result, then tnm_set_sel_point().  Keep
3661            // the live command return path untouched while storing a resume
3662            // point whose int stack has the same observable shape.
3663            self.int_stack.push(result);
3664            let point = self.make_resume_point();
3665            let _ = self.int_stack.pop();
3666            self.sel_point_stack.clear();
3667            self.sel_point_stack.push(point);
3668        }
3669
3670        let mut work: Vec<FrameActionWork> = Vec::new();
3671        let mut global_form_ids: Vec<u32> =
3672            self.ctx.globals.frame_actions.keys().copied().collect();
3673        global_form_ids.sort_unstable();
3674        for form_id in global_form_ids {
3675            let Some(fa) = self.ctx.globals.frame_actions.get(&form_id) else {
3676                continue;
3677            };
3678            if !fa.cmd_name.is_empty() {
3679                work.push(FrameActionWork {
3680                    stage_idx: -1,
3681                    obj_idx: usize::MAX,
3682                    ch_idx: None,
3683                    global_form_id: Some(form_id),
3684                    object_chain: None,
3685                    frame_action_chain: Some(vec![form_id as i32]),
3686                    scn_name: fa.scn_name.clone(),
3687                    cmd_name: fa.cmd_name.clone(),
3688                    args: fa.args.clone(),
3689                    count: fa.counter.get_count(),
3690                    end_time: fa.end_time,
3691                });
3692            }
3693        }
3694        let mut global_list_form_ids: Vec<u32> = self
3695            .ctx
3696            .globals
3697            .frame_action_lists
3698            .keys()
3699            .copied()
3700            .collect();
3701        global_list_form_ids.sort_unstable();
3702        for form_id in global_list_form_ids {
3703            let Some(list) = self.ctx.globals.frame_action_lists.get(&form_id) else {
3704                continue;
3705            };
3706            for (idx, fa) in list.iter().enumerate() {
3707                if !fa.cmd_name.is_empty() {
3708                    work.push(FrameActionWork {
3709                        stage_idx: -1,
3710                        obj_idx: usize::MAX,
3711                        ch_idx: Some(idx),
3712                        global_form_id: Some(form_id),
3713                        object_chain: None,
3714                        frame_action_chain: Some(vec![
3715                            form_id as i32,
3716                            crate::runtime::forms::codes::ELM_ARRAY,
3717                            idx as i32,
3718                        ]),
3719                        scn_name: fa.scn_name.clone(),
3720                        cmd_name: fa.cmd_name.clone(),
3721                        args: fa.args.clone(),
3722                        count: fa.counter.get_count(),
3723                        end_time: fa.end_time,
3724                    });
3725                }
3726            }
3727        }
3728        let mut stage_form_ids: Vec<u32> = self.ctx.globals.stage_forms.keys().copied().collect();
3729        stage_form_ids.sort_unstable();
3730        for form_id in stage_form_ids {
3731            let Some(st) = self.ctx.globals.stage_forms.get(&form_id) else {
3732                continue;
3733            };
3734            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
3735            stage_ids.sort_unstable();
3736            for stage_idx in stage_ids {
3737                let Some(objs) = st.object_lists.get(&stage_idx) else {
3738                    continue;
3739                };
3740                for (obj_idx, obj) in objs.iter().enumerate() {
3741                    if st.is_embedded_object_slot(stage_idx, obj_idx)
3742                        || !st.object_slot_is_used(stage_idx, obj_idx)
3743                    {
3744                        continue;
3745                    }
3746                    let mut object_chain = vec![
3747                        form_id as i32,
3748                        self.ctx.ids.elm_array,
3749                        stage_idx as i32,
3750                        self.ctx.ids.stage_elm_object,
3751                        self.ctx.ids.elm_array,
3752                        obj_idx as i32,
3753                    ];
3754                    Self::collect_object_frame_action_work_recursive(
3755                        obj,
3756                        stage_idx,
3757                        obj_idx,
3758                        &mut object_chain,
3759                        &mut work,
3760                    );
3761                }
3762            }
3763
3764            let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
3765            mwnd_stage_ids.sort_unstable();
3766            for stage_idx in mwnd_stage_ids {
3767                let Some(mwnds) = st.mwnd_lists.get(&stage_idx) else {
3768                    continue;
3769                };
3770                for (mwnd_idx, mwnd) in mwnds.iter().enumerate() {
3771                    for (obj_idx, obj) in mwnd.button_list.iter().enumerate() {
3772                        let mut object_chain = vec![
3773                            form_id as i32,
3774                            self.ctx.ids.elm_array,
3775                            stage_idx as i32,
3776                            crate::runtime::forms::codes::elm_value::STAGE_MWND,
3777                            self.ctx.ids.elm_array,
3778                            mwnd_idx as i32,
3779                            crate::runtime::forms::codes::elm_value::MWND_BUTTON,
3780                            self.ctx.ids.elm_array,
3781                            obj_idx as i32,
3782                        ];
3783                        Self::collect_object_frame_action_work_recursive(
3784                            obj,
3785                            stage_idx,
3786                            obj_idx,
3787                            &mut object_chain,
3788                            &mut work,
3789                        );
3790                    }
3791                    for (obj_idx, obj) in mwnd.face_list.iter().enumerate() {
3792                        let mut object_chain = vec![
3793                            form_id as i32,
3794                            self.ctx.ids.elm_array,
3795                            stage_idx as i32,
3796                            crate::runtime::forms::codes::elm_value::STAGE_MWND,
3797                            self.ctx.ids.elm_array,
3798                            mwnd_idx as i32,
3799                            crate::runtime::forms::codes::elm_value::MWND_FACE,
3800                            self.ctx.ids.elm_array,
3801                            obj_idx as i32,
3802                        ];
3803                        Self::collect_object_frame_action_work_recursive(
3804                            obj,
3805                            stage_idx,
3806                            obj_idx,
3807                            &mut object_chain,
3808                            &mut work,
3809                        );
3810                    }
3811                    for (obj_idx, obj) in mwnd.object_list.iter().enumerate() {
3812                        let mut object_chain = vec![
3813                            form_id as i32,
3814                            self.ctx.ids.elm_array,
3815                            stage_idx as i32,
3816                            crate::runtime::forms::codes::elm_value::STAGE_MWND,
3817                            self.ctx.ids.elm_array,
3818                            mwnd_idx as i32,
3819                            crate::runtime::forms::codes::elm_value::MWND_OBJECT,
3820                            self.ctx.ids.elm_array,
3821                            obj_idx as i32,
3822                        ];
3823                        Self::collect_object_frame_action_work_recursive(
3824                            obj,
3825                            stage_idx,
3826                            obj_idx,
3827                            &mut object_chain,
3828                            &mut work,
3829                        );
3830                    }
3831                }
3832            }
3833        }
3834        if trace {
3835            eprintln!("[SG_TICK_TRACE] frame_action work items={}", work.len());
3836        }
3837        // C++ drives FRAME_ACTION from the engine proc loop independently of the
3838        // current script call depth. Do not stall object callbacks while a user
3839        // command/excall is active; MWND child setup relies on these callbacks.
3840        for item in work {
3841            if trace {
3842                eprintln!(
3843                    "[SG_TICK_TRACE] invoke stage={} obj={} ch={:?} global={:?} cmd={} count={} end_time={} args={:?}",
3844                    item.stage_idx,
3845                    item.obj_idx,
3846                    item.ch_idx,
3847                    item.global_form_id,
3848                    item.cmd_name,
3849                    item.count,
3850                    item.end_time,
3851                    item.args
3852                );
3853            }
3854            // Original order is C_tnm_eng::frame_action_proc() do_action() first,
3855            // then C_elm_frame_action::frame() checks for finish later in the frame.
3856            // This matters for end_time == 0 and for callbacks that replace their own
3857            // frame action.
3858            let call_args = Self::make_frame_action_call_args(
3859                item.frame_action_chain.as_ref(),
3860                item.object_chain.as_ref(),
3861                &item.args,
3862            );
3863            let (prev_target, prev_chain) = self.set_frame_action_current_object(&item);
3864            if let Err(e) = self.run_scene_user_cmd_inline(
3865                Some(&item.scn_name),
3866                &item.cmd_name,
3867                &call_args,
3868                self.cfg.fm_void,
3869                true,
3870            ) {
3871                self.ctx.unknown.record_note(&format!(
3872                    "frame_action.call.failed:{}:{}:{e}",
3873                    item.scn_name, item.cmd_name
3874                ));
3875            }
3876            self.restore_frame_action_current_object(prev_target, prev_chain);
3877
3878            if let Some((finish_scn_name, finish_cmd_name, finish_args)) =
3879                self.begin_frame_action_finish(&item)
3880            {
3881                if trace {
3882                    eprintln!(
3883                        "[SG_TICK_TRACE] finish stage={} obj={} ch={:?} global={:?} cmd={} args={:?}",
3884                        item.stage_idx,
3885                        item.obj_idx,
3886                        item.ch_idx,
3887                        item.global_form_id,
3888                        finish_cmd_name,
3889                        finish_args
3890                    );
3891                }
3892                let finish_call_args = Self::make_frame_action_call_args(
3893                    item.frame_action_chain.as_ref(),
3894                    item.object_chain.as_ref(),
3895                    &finish_args,
3896                );
3897                let (prev_target, prev_chain) = self.set_frame_action_current_object(&item);
3898                if let Err(e) = self.run_scene_user_cmd_inline(
3899                    Some(&finish_scn_name),
3900                    &finish_cmd_name,
3901                    &finish_call_args,
3902                    self.cfg.fm_void,
3903                    true,
3904                ) {
3905                    self.ctx.unknown.record_note(&format!(
3906                        "frame_action.finish_call.failed:{}:{}:{e}",
3907                        finish_scn_name, finish_cmd_name
3908                    ));
3909                }
3910                self.restore_frame_action_current_object(prev_target, prev_chain);
3911                self.end_frame_action_finish(&item);
3912            }
3913        }
3914        if trace {
3915            eprintln!(
3916                "[SG_TICK_TRACE] after frame-action callbacks blocked={} halted={}",
3917                self.is_blocked(),
3918                self.halted
3919            );
3920        }
3921        self.script_input_synced_this_frame = false;
3922        Ok(())
3923    }
3924
3925    pub fn restart_scene_name(&mut self, scene_name: &str, z_no: i32) -> Result<()> {
3926        // C++ restart paths call tnm_finish_local()/tnm_reinit_local() before
3927        // resolving and entering the target scene.  Keep the same ordering so
3928        // local teardown cannot depend on the new scene having loaded already.
3929        self.ctx.reset_for_scene_restart();
3930        let (stream, scene_no) = self.load_scene_stream(scene_name, z_no)?;
3931        self.stream = stream;
3932        self.int_stack.clear();
3933        self.str_stack.clear();
3934        self.element_points.clear();
3935        self.call_stack.clear();
3936        self.call_stack.push(self.scene_base_call());
3937        self.gosub_return_stack.clear();
3938        self.user_props.clear();
3939        self.scene_user_props.clear();
3940        self.scene_stack.clear();
3941        self.save_point = None;
3942        self.ctx.local_save_snapshot = None;
3943        self.sel_point_stack.clear();
3944        self.current_scene_no = Some(scene_no);
3945        self.current_scene_name = Some(scene_name.to_string());
3946        self.current_line_no = -1;
3947        self.ctx.current_scene_no = Some(scene_no as i64);
3948        self.ctx.current_scene_name = Some(scene_name.to_string());
3949        self.ctx.current_line_no = -1;
3950        self.halted = false;
3951        self.delayed_ret_form = None;
3952        Ok(())
3953    }
3954
3955    fn make_resume_point(&self) -> VmResumePoint<'a> {
3956        VmResumePoint {
3957            stream: self.stream.clone(),
3958            user_cmd_names: self.user_cmd_names.clone(),
3959            call_cmd_names: self.call_cmd_names.clone(),
3960            int_stack: self.int_stack.clone(),
3961            str_stack: self.str_stack.clone(),
3962            element_points: self.element_points.clone(),
3963            call_stack: self.call_stack.clone(),
3964            gosub_return_stack: self.gosub_return_stack.clone(),
3965            user_props: self.user_props.clone(),
3966            scene_user_props: self.scene_user_props.clone(),
3967            current_scene_no: self.current_scene_no,
3968            current_scene_name: self.current_scene_name.clone(),
3969            current_line_no: self.current_line_no,
3970            globals: self.ctx.globals.clone(),
3971        }
3972    }
3973
3974    fn restore_resume_point(&mut self, point: VmResumePoint<'a>) {
3975        self.stream = point.stream;
3976        self.user_cmd_names = point.user_cmd_names;
3977        self.call_cmd_names = point.call_cmd_names;
3978        self.int_stack = point.int_stack;
3979        self.str_stack = point.str_stack;
3980        self.element_points = point.element_points;
3981        self.call_stack = point.call_stack;
3982        self.gosub_return_stack = point.gosub_return_stack;
3983        self.user_props = point.user_props;
3984        self.scene_user_props = point.scene_user_props;
3985        self.current_scene_no = point.current_scene_no;
3986        self.current_scene_name = point.current_scene_name;
3987        self.current_line_no = point.current_line_no;
3988        let mut restored_globals = point.globals;
3989        restored_globals.syscom.pending_proc = None;
3990        restored_globals.syscom.menu_open = false;
3991        restored_globals.syscom.menu_kind = None;
3992        restored_globals.syscom.fallback_dialog = None;
3993        restored_globals.syscom.fallback_origin = None;
3994        restored_globals.syscom.msg_back_open = false;
3995        self.ctx.globals = restored_globals;
3996        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
3997        self.ctx.current_scene_name = self.current_scene_name.clone();
3998        self.ctx.current_line_no = self.current_line_no as i64;
3999        self.ctx.wait = runtime::wait::VmWait::default();
4000        self.ctx.stack.clear();
4001        self.halted = false;
4002        self.delayed_ret_form = None;
4003    }
4004
4005    pub fn has_sel_point(&self) -> bool {
4006        !self.sel_point_stack.is_empty()
4007    }
4008
4009    pub fn restore_last_sel_point(&mut self) -> bool {
4010        let Some(point) = self.sel_point_stack.last().cloned() else {
4011            return false;
4012        };
4013        self.restore_resume_point(point);
4014        true
4015    }
4016
4017    pub fn step(&mut self) -> Result<bool> {
4018        match self.step_inner(true) {
4019            Ok(running) => Ok(running),
4020            Err(err) => {
4021                // A decoded instruction may already have consumed operands before
4022                // reporting an error.  Continuing from that partial PC would make
4023                // the following frame interpret an operand byte as a fresh opcode.
4024                // The original engine treats such script errors as fatal, so stop
4025                // the VM at the first real failure instead of cascading into
4026                // CD_NONE / invalid element-point errors.
4027                self.halted = true;
4028                Err(err)
4029            }
4030        }
4031    }
4032
4033    /// Reset the infinite-loop guard for one C++-style frame_main_proc pass.
4034    /// This is not a scheduling quota; it only preserves SIGLUS_VM_MAX_STEPS
4035    /// as a hard error if a script never reaches a proc/wait boundary.
4036    pub fn begin_script_proc_pump(&mut self) {
4037        self.steps = 0;
4038        self.ctx.begin_frame_main_proc_pass();
4039    }
4040
4041    /// Execute one standalone SCRIPT proc pass. Direct callers get a fresh
4042    /// infinite-loop guard, while the winit shell uses run_script_proc_continue()
4043    /// inside its C++-style frame_main_proc loop.
4044    pub fn run_script_proc(&mut self) -> Result<bool> {
4045        self.begin_script_proc_pump();
4046        self.run_script_proc_continue()
4047    }
4048
4049    /// Execute the current SCRIPT proc the same way the original engine's
4050    /// `tnm_proc_script()` does: keep stepping while the current proc is SCRIPT,
4051    /// and return only when a command changes the proc, enters a wait, returns,
4052    /// or stops the VM. There is no per-frame instruction quota here.
4053    pub fn run_script_proc_continue(&mut self) -> Result<bool> {
4054        if self.halted {
4055            return Ok(false);
4056        }
4057        // A completed MESSAGE_KEY_WAIT can itself request the DISP boundary
4058        // used by the original skip-rate limiter. Detect that boundary even
4059        // though wait_poll() has just made the VM unblocked.
4060        let proc_generation_before_wait = self.ctx.proc_generation();
4061        if self.is_blocked() {
4062            return Ok(true);
4063        }
4064        if self.ctx.proc_generation() != proc_generation_before_wait {
4065            return Ok(true);
4066        }
4067
4068        if !self.script_input_synced_this_frame {
4069            self.ctx.sync_script_input_from_runtime();
4070            self.ctx.begin_input_frame();
4071            self.ctx.input.next_frame();
4072            self.script_input_synced_this_frame = true;
4073        }
4074
4075        loop {
4076            let proc_generation_before = self.ctx.proc_generation();
4077            let running = match self.step_inner(true) {
4078                Ok(running) => running,
4079                Err(err) => {
4080                    // Do not retry a half-consumed instruction on the next redraw.
4081                    // Retrying from its operand field is what produced the later
4082                    // CD_NONE and invalid-element-point cascade.
4083                    self.halted = true;
4084                    return Err(err);
4085                }
4086            };
4087            if !running || self.halted {
4088                return Ok(running);
4089            }
4090
4091            if self.is_blocked() {
4092                return Ok(true);
4093            }
4094            if self.ctx.proc_generation() != proc_generation_before {
4095                return Ok(true);
4096            }
4097        }
4098    }
4099
4100    #[allow(dead_code)]
4101    fn step_inner(&mut self, respect_wait: bool) -> Result<bool> {
4102        self.yield_safe_after_step = false;
4103        if self.halted {
4104            return Ok(false);
4105        }
4106
4107        // Normal scene execution is blocked by WAIT / WAIT_KEY.
4108        // Frame-action inline callbacks bypass this outer wait guard so the
4109        // frame phase can run object callbacks while the main script is waiting.
4110        if respect_wait {
4111            let blocked = self.ctx.wait_poll();
4112            if blocked {
4113                return Ok(true);
4114            }
4115        }
4116
4117        // If the main script yielded a delayed return, materialize it only when
4118        // the normal script pump resumes. Frame-action callbacks intentionally
4119        // bypass the outer wait guard, so letting them take this value would
4120        // resume the blocked statement before the C++ wait proc has produced
4121        // its return value.
4122        if respect_wait {
4123            let delayed = self
4124                .call_stack
4125                .last_mut()
4126                .and_then(|frame| frame.delayed_ret_form.take());
4127            if let Some(rf) = delayed {
4128                if self.ctx.stack.is_empty() {
4129                    self.push_default_for_ret(rf);
4130                } else {
4131                    self.take_ctx_return(rf)?;
4132                }
4133            }
4134        }
4135
4136        if self.cfg.max_steps > 0 && self.steps >= self.cfg.max_steps {
4137            let scene = self.current_scene_name.as_deref().unwrap_or("<none>");
4138            let scene_no = self
4139                .current_scene_no
4140                .map(|v| v.to_string())
4141                .unwrap_or_else(|| "-".to_string());
4142            bail!(
4143                "VM reached SIGLUS_VM_MAX_STEPS={} (possible infinite loop) scene={} scene_no={} line={} pc=0x{:x}",
4144                self.cfg.max_steps,
4145                scene,
4146                scene_no,
4147                self.current_line_no,
4148                self.stream.get_prg_cntr()
4149            );
4150        }
4151        self.steps += 1;
4152
4153        let pc_before = self.stream.get_prg_cntr();
4154        let opcode = match self.stream.pop_u8() {
4155            Ok(v) => v,
4156            Err(_) => {
4157                if self.at_cross_scene_return_boundary() && self.return_from_scene(Vec::new())? {
4158                    return Ok(true);
4159                }
4160                self.halted = true;
4161                return Ok(false);
4162            }
4163        };
4164
4165        self.vm_trace_opcode(pc_before, opcode, "before");
4166
4167        match opcode {
4168            CD_NL => {
4169                let line_no = self.stream.pop_i32()?;
4170                self.current_line_no = line_no;
4171                self.ctx.current_line_no = line_no as i64;
4172                self.yield_safe_after_step = true;
4173                // Compact element continuation is statement-local, but some title/menu code
4174                // keeps a base object element alive across `NL` boundaries and continues to use
4175                // compact child/property syntax on the following line. So we only drop the
4176                // continuation context when there is no live element on the VM stack anymore.
4177                if self.element_points.is_empty() {
4178                    self.ctx.globals.current_object_chain = None;
4179                    self.ctx.globals.current_stage_object = None;
4180                }
4181            }
4182
4183            CD_PUSH => {
4184                let form_code = self.stream.pop_i32()?;
4185                self.exec_push(form_code)?;
4186            }
4187            CD_POP => {
4188                let form_code = self.stream.pop_i32()?;
4189                self.exec_pop(form_code)?;
4190            }
4191            CD_COPY => {
4192                let form_code = self.stream.pop_i32()?;
4193                self.exec_copy(form_code)?;
4194            }
4195
4196            CD_ELM_POINT => {
4197                self.element_points.push(self.int_stack.len());
4198                vm_trace!(
4199                    self,
4200                    None,
4201                    format!("ELM_POINT push start={} ", self.int_stack.len()),
4202                );
4203            }
4204            CD_COPY_ELM => {
4205                self.exec_copy_element()?;
4206            }
4207
4208            CD_PROPERTY => {
4209                let elm = self.pop_element()?;
4210                vm_trace!(self, None, format!("CD_PROPERTY elm={:?}", elm));
4211                self.exec_property(elm)?;
4212            }
4213            CD_DEC_PROP => {
4214                let form_code = self.stream.pop_i32()?;
4215                let prop_id = self.stream.pop_i32()?;
4216
4217                let size = if form_code == self.cfg.fm_intlist || form_code == self.cfg.fm_strlist {
4218                    self.pop_int()?.max(0) as usize
4219                } else {
4220                    0usize
4221                };
4222                let prop_element = vec![constants::elm::create(
4223                    constants::elm::OWNER_CALL_PROP,
4224                    0,
4225                    prop_id,
4226                )];
4227                let value = if form_code == self.cfg.fm_int {
4228                    CallPropValue::Int(0)
4229                } else if form_code == self.cfg.fm_str {
4230                    CallPropValue::Str(String::new())
4231                } else if form_code == self.cfg.fm_intlist {
4232                    CallPropValue::IntList(vec![0; size])
4233                } else if form_code == self.cfg.fm_strlist {
4234                    CallPropValue::StrList(vec![String::new(); size])
4235                } else {
4236                    CallPropValue::Element(prop_element.clone())
4237                };
4238
4239                let frame = self
4240                    .call_stack
4241                    .last_mut()
4242                    .ok_or_else(|| anyhow!("call stack underflow"))?;
4243                frame.user_props.push(CallProp {
4244                    scn_no: self.current_scene_no.unwrap_or(0) as i32,
4245                    prop_id,
4246                    form: form_code,
4247                    decl_size: size,
4248                    element: prop_element,
4249                    value,
4250                });
4251            }
4252            CD_ARG => {
4253                // Expand stack arguments into the current call's declared properties
4254                // (tnm_expand_arg_into_call_flag).
4255                let (frame_action_proc, actual_arg_cnt, forms): (bool, usize, Vec<i32>) = {
4256                    let frame = self
4257                        .call_stack
4258                        .last()
4259                        .ok_or_else(|| anyhow!("call stack underflow"))?;
4260                    (
4261                        frame.frame_action_proc,
4262                        frame.arg_cnt,
4263                        frame.user_props.iter().map(|p| p.form).collect(),
4264                    )
4265                };
4266
4267                if frame_action_proc {
4268                    if forms.first().copied() != Some(crate::runtime::forms::codes::FM_FRAMEACTION)
4269                    {
4270                        bail!("frame_action CD_ARG requires first argument to be FM_FRAMEACTION");
4271                    }
4272                    if actual_arg_cnt != forms.len() {
4273                        bail!(
4274                            "frame_action CD_ARG argument count mismatch: declared={} actual={}",
4275                            forms.len(),
4276                            actual_arg_cnt
4277                        );
4278                    }
4279                }
4280
4281                // Pop values in reverse order to match the original stack layout.
4282                let mut values: Vec<CallPropValue> = Vec::with_capacity(forms.len());
4283                for &form in forms.iter().rev() {
4284                    let v = if form == self.cfg.fm_int {
4285                        CallPropValue::Int(self.pop_int()?)
4286                    } else if form == self.cfg.fm_str {
4287                        CallPropValue::Str(self.pop_str()?)
4288                    } else {
4289                        CallPropValue::Element(self.pop_element()?)
4290                    };
4291                    values.push(v);
4292                }
4293                values.reverse();
4294
4295                {
4296                    let frame = self
4297                        .call_stack
4298                        .last_mut()
4299                        .ok_or_else(|| anyhow!("call stack underflow"))?;
4300                    for (prop, v) in frame.user_props.iter_mut().zip(values) {
4301                        match (&v, prop.form) {
4302                            (CallPropValue::Int(_), f) if f == self.cfg.fm_int => {
4303                                prop.value = v;
4304                            }
4305                            (CallPropValue::Str(_), f) if f == self.cfg.fm_str => {
4306                                prop.value = v;
4307                            }
4308                            (CallPropValue::Element(e), _) => {
4309                                // C++ tnm_expand_arg_into_call_flag() writes all non-int/str
4310                                // arguments into user_prop_list[i].element directly.
4311                                prop.element = e.clone();
4312                                if matches!(
4313                                    prop.form,
4314                                    crate::runtime::forms::codes::FM_INTREF
4315                                        | crate::runtime::forms::codes::FM_STRREF
4316                                        | crate::runtime::forms::codes::FM_INTLISTREF
4317                                        | crate::runtime::forms::codes::FM_STRLISTREF
4318                                        | crate::runtime::forms::codes::FM_LIST
4319                                ) {
4320                                    prop.value = CallPropValue::Element(e.clone());
4321                                }
4322                            }
4323                            _ => {
4324                                prop.value = v;
4325                            }
4326                        }
4327                    }
4328                }
4329                vm_trace!(
4330                    self,
4331                    Some(pc_before),
4332                    format!(
4333                        "ARG expanded frame={:?}",
4334                        self.call_stack.last().map(|frame| {
4335                            format!(
4336                                "ret_form={} arg_cnt={} props={:?} L0_8={:?} K0_4={:?}",
4337                                frame.ret_form,
4338                                frame.arg_cnt,
4339                                frame.user_props,
4340                                &frame.int_args[..frame.int_args.len().min(8)],
4341                                &frame.str_args[..frame.str_args.len().min(4)]
4342                            )
4343                        })
4344                    )
4345                );
4346            }
4347
4348            CD_GOTO => {
4349                let label_no = self.stream.pop_i32()?;
4350                sg_omv_trace!(self, "GOTO label={} taken=true", label_no);
4351                self.stream.jump_to_label(label_no.max(0) as usize)?;
4352            }
4353            CD_GOTO_TRUE => {
4354                let label_no = self.stream.pop_i32()?;
4355                let trace_cf_branch = self.cf_branch_trace_interesting_line();
4356                let before_tail = if trace_cf_branch {
4357                    let start = self.int_stack.len().saturating_sub(16);
4358                    Some(self.int_stack[start..].to_vec())
4359                } else {
4360                    None
4361                };
4362                let cond = self.pop_int()?;
4363                let taken = cond != 0;
4364                sg_omv_trace!(
4365                    self,
4366                    "GOTO_TRUE label={} cond={} taken={}",
4367                    label_no,
4368                    cond,
4369                    taken
4370                );
4371                if let Some(before_tail) = before_tail.as_deref() {
4372                    self.trace_cf_branch_goto(
4373                        pc_before,
4374                        "GOTO_TRUE",
4375                        label_no,
4376                        cond,
4377                        taken,
4378                        before_tail,
4379                    );
4380                }
4381                if taken {
4382                    self.stream.jump_to_label(label_no.max(0) as usize)?;
4383                }
4384            }
4385            CD_GOTO_FALSE => {
4386                let label_no = self.stream.pop_i32()?;
4387                let trace_cf_branch = self.cf_branch_trace_interesting_line();
4388                let before_tail = if trace_cf_branch {
4389                    let start = self.int_stack.len().saturating_sub(16);
4390                    Some(self.int_stack[start..].to_vec())
4391                } else {
4392                    None
4393                };
4394                let cond = self.pop_int()?;
4395                let taken = cond == 0;
4396                sg_omv_trace!(
4397                    self,
4398                    "GOTO_FALSE label={} cond={} taken={}",
4399                    label_no,
4400                    cond,
4401                    taken
4402                );
4403                if let Some(before_tail) = before_tail.as_deref() {
4404                    self.trace_cf_branch_goto(
4405                        pc_before,
4406                        "GOTO_FALSE",
4407                        label_no,
4408                        cond,
4409                        taken,
4410                        before_tail,
4411                    );
4412                }
4413                if taken {
4414                    self.stream.jump_to_label(label_no.max(0) as usize)?;
4415                }
4416            }
4417            CD_GOSUB => {
4418                let label_no = self.stream.pop_i32()?;
4419                let _args = self.pop_arg_list()?;
4420                let return_pc = self.stream.get_prg_cntr();
4421                vm_trace!(
4422                    self,
4423                    Some(pc_before),
4424                    format!("GOSUB label={} return_pc=0x{return_pc:x}", label_no),
4425                );
4426
4427                // Save return info on the caller frame .
4428                let caller = self
4429                    .call_stack
4430                    .last_mut()
4431                    .ok_or_else(|| anyhow!("call stack underflow"))?;
4432                caller.return_pc = return_pc;
4433                caller.return_scene_no = self.current_scene_no;
4434                caller.return_scene_name = self.current_scene_name.clone();
4435                caller.return_line_no = self.current_line_no;
4436                caller.ret_form = self.cfg.fm_int;
4437                self.gosub_return_stack.push((return_pc, self.cfg.fm_int));
4438
4439                // Enter callee context.
4440                let scratch_args = self.call_scratch_from_args(&_args);
4441                let mut callee = self.take_call_frame(
4442                    self.cfg.fm_void,
4443                    false,
4444                    false,
4445                    _args.len(),
4446                    Some(scratch_args),
4447                );
4448                callee.call_type = 1;
4449                callee.return_override = Some((return_pc, self.cfg.fm_int));
4450                self.call_stack.push(callee);
4451
4452                self.stream.jump_to_label(label_no.max(0) as usize)?;
4453            }
4454            CD_GOSUBSTR => {
4455                let label_no = self.stream.pop_i32()?;
4456                let _args = self.pop_arg_list()?;
4457                let return_pc = self.stream.get_prg_cntr();
4458                vm_trace!(
4459                    self,
4460                    Some(pc_before),
4461                    format!("GOSUBSTR label={} return_pc=0x{return_pc:x}", label_no),
4462                );
4463
4464                let caller = self
4465                    .call_stack
4466                    .last_mut()
4467                    .ok_or_else(|| anyhow!("call stack underflow"))?;
4468                caller.return_pc = return_pc;
4469                caller.return_scene_no = self.current_scene_no;
4470                caller.return_scene_name = self.current_scene_name.clone();
4471                caller.return_line_no = self.current_line_no;
4472                caller.ret_form = self.cfg.fm_str;
4473                self.gosub_return_stack.push((return_pc, self.cfg.fm_str));
4474
4475                let scratch_args = self.call_scratch_from_args(&_args);
4476                let mut callee = self.take_call_frame(
4477                    self.cfg.fm_void,
4478                    false,
4479                    false,
4480                    _args.len(),
4481                    Some(scratch_args),
4482                );
4483                callee.call_type = 1;
4484                callee.return_override = Some((return_pc, self.cfg.fm_str));
4485                self.call_stack.push(callee);
4486
4487                self.stream.jump_to_label(label_no.max(0) as usize)?;
4488            }
4489            CD_RETURN => {
4490                let args = self.pop_arg_list()?;
4491                if self.vm_trace_matches() {
4492                    if let Some(frame) = self.call_stack.last() {
4493                        self.vm_trace_emit(
4494                            Some(pc_before),
4495                            format_args!(
4496                                "RETURN decoded argc={} args={:?} call_depth={} scene_stack={} frame=ret_form={} arg_cnt={} props={:?} L0_8={:?} K0_4={:?}",
4497                                args.len(),
4498                                args,
4499                                self.call_stack.len(),
4500                                self.scene_stack.len(),
4501                                frame.ret_form,
4502                                frame.arg_cnt,
4503                                frame.user_props,
4504                                &frame.int_args[..frame.int_args.len().min(8)],
4505                                &frame.str_args[..frame.str_args.len().min(4)],
4506                            ),
4507                        );
4508                    } else {
4509                        self.vm_trace_emit(
4510                            Some(pc_before),
4511                            format_args!(
4512                                "RETURN decoded argc={} args={:?} call_depth={} scene_stack={} frame=<none>",
4513                                args.len(),
4514                                args,
4515                                self.call_stack.len(),
4516                                self.scene_stack.len(),
4517                            ),
4518                        );
4519                    }
4520                }
4521                sg_omv_trace!(
4522                    self,
4523                    "RETURN argc={} args={:?} call_depth={} scene_stack={}",
4524                    args.len(),
4525                    args,
4526                    self.call_stack.len(),
4527                    self.scene_stack.len()
4528                );
4529                if self.at_cross_scene_return_boundary() {
4530                    if self.return_from_scene(args)? {
4531                        return Ok(true);
4532                    }
4533                    self.halted = true;
4534                    return Ok(false);
4535                }
4536                if self.call_stack.len() == 1 {
4537                    self.halted = true;
4538                    return Ok(false);
4539                }
4540                if self.exec_return(args)? {
4541                    return Ok(false);
4542                }
4543            }
4544
4545            CD_ASSIGN => {
4546                let left_form = self.stream.pop_i32()?;
4547                let right_form = self.stream.pop_i32()?;
4548                let al_id = self.stream.pop_i32()?;
4549                let rhs = self.pop_value_for_form(right_form)?;
4550                let elm = self.pop_element()?;
4551                vm_trace!(
4552                    self,
4553                    Some(pc_before),
4554                    format!(
4555                        "ASSIGN decoded left_form={} right_form={} al_id={} elm={:?} rhs={:?}",
4556                        left_form, right_form, al_id, elm, rhs
4557                    ),
4558                );
4559                self.exec_assign(elm, al_id, rhs)?;
4560                if self.vm_trace_matches() {
4561                    if let Some(frame) = self.call_stack.last() {
4562                        self.vm_trace_emit(
4563                            Some(pc_before),
4564                            format_args!(
4565                                "ASSIGN applied frame=ret_form={} arg_cnt={} props={:?} L0_8={:?} K0_4={:?}",
4566                                frame.ret_form,
4567                                frame.arg_cnt,
4568                                frame.user_props,
4569                                &frame.int_args[..frame.int_args.len().min(8)],
4570                                &frame.str_args[..frame.str_args.len().min(4)],
4571                            ),
4572                        );
4573                    } else {
4574                        self.vm_trace_emit(Some(pc_before), "ASSIGN applied frame=<none>");
4575                    }
4576                }
4577            }
4578
4579            CD_OPERATE_1 => {
4580                let form_code = self.stream.pop_i32()?;
4581                let opr = self.stream.pop_u8()?;
4582                self.exec_operate_1(form_code, opr)?;
4583            }
4584            CD_OPERATE_2 => {
4585                let form_l = self.stream.pop_i32()?;
4586                let form_r = self.stream.pop_i32()?;
4587                let opr = self.stream.pop_u8()?;
4588                self.exec_operate_2(form_l, form_r, opr)?;
4589            }
4590            CD_COMMAND => {
4591                // CD_COMMAND reads: arg_list_id, arg_list, element, named_arg_cnt, named_arg_ids..., ret_form
4592                let arg_list_id = self.stream.pop_i32()?;
4593                let mut args = self.pop_arg_list()?;
4594                let elm = self.pop_element()?;
4595
4596                let named_arg_cnt = self.stream.pop_i32()?;
4597                if named_arg_cnt < 0 {
4598                    bail!("negative named_arg_cnt={named_arg_cnt}");
4599                }
4600
4601                let mut named_ids: Vec<i32> = Vec::with_capacity(named_arg_cnt as usize);
4602                for _ in 0..(named_arg_cnt as usize) {
4603                    named_ids.push(self.stream.pop_i32()?);
4604                }
4605
4606                if !named_ids.is_empty() {
4607                    let n = named_ids.len().min(args.len());
4608                    for a in 0..n {
4609                        let idx = args.len() - 1 - a;
4610                        let id = named_ids[a];
4611                        let v = std::mem::replace(&mut args[idx], crate::runtime::Value::Int(0));
4612                        args[idx] = crate::runtime::Value::NamedArg {
4613                            id,
4614                            value: Box::new(v),
4615                        };
4616                    }
4617                }
4618
4619                let ret_form = self.stream.pop_i32()?;
4620                if let Some(raw_head) = elm.first().copied() {
4621                    let form_id = self.canonical_runtime_form_id(raw_head as u32) as i32;
4622                    let op_id = if elm.len() >= 2 { elm[1] } else { arg_list_id };
4623                    self.sg_omv_trace_command(
4624                        "CD_COMMAND",
4625                        &elm,
4626                        form_id,
4627                        op_id,
4628                        arg_list_id,
4629                        ret_form,
4630                        &args,
4631                    );
4632                }
4633                let _ = self.ctx.take_read_flag_no_request();
4634                let block_generation = self.ctx.wait.block_generation();
4635                let proc_generation = self.ctx.proc_generation();
4636                self.exec_command(elm, arg_list_id, ret_form, &mut args)?;
4637                self.drain_runtime_save_load_requests()?;
4638                if self.ctx.take_read_flag_no_request() {
4639                    // SiglusCompiler's command read-flag ABI changed over
4640                    // time, and old scenes can mix both forms in one scene:
4641                    // KOE may continue directly with CD_POP while SELBTN later
4642                    // in the same bytecode already carries a trailing i32.
4643                    // SceneStream resolves the exact command boundaries by
4644                    // parsing the whole scene against the monotonically
4645                    // numbered read_flag_list. Never decide this from the local
4646                    // bytes alone: CD_POP/FM_VOID starts 03 00 00 00 00 and can
4647                    // collide with a real read_flag_no=3 on the same line.
4648                    if self.stream.has_resolved_command_read_flag_layout() {
4649                        if let Some(expected) = self.stream.command_read_flag_no_at_current_pc() {
4650                            let read_flag_no = self.stream.pop_i32()?;
4651                            if read_flag_no != expected {
4652                                bail!(
4653                                    "scene read-flag layout mismatch at pc=0x{:x}: expected {}, got {}",
4654                                    self.stream.get_prg_cntr().saturating_sub(4),
4655                                    expected,
4656                                    read_flag_no
4657                                );
4658                            }
4659                            self.ctx.submit_read_flag_no(read_flag_no);
4660                        } else {
4661                            self.ctx.discard_read_flag_no_request();
4662                        }
4663                    } else {
4664                        // Do not regress scenes whose historical layout cannot
4665                        // be proven by the static scan. Preserve the previous
4666                        // modern-engine behavior rather than guessing legacy.
4667                        let read_flag_no = self.stream.pop_i32()?;
4668                        self.ctx.submit_read_flag_no(read_flag_no);
4669                    }
4670                }
4671                if self.ctx.proc_generation() != proc_generation {
4672                    return Ok(true);
4673                }
4674                if respect_wait
4675                    && self.ctx.wait.block_generation() != block_generation
4676                    && self.ctx.wait_poll()
4677                {
4678                    return Ok(true);
4679                }
4680            }
4681            CD_TEXT => {
4682                let rf_flag_no = self.stream.pop_i32()?;
4683                let text = self.pop_str()?;
4684                if !crate::runtime::forms::stage::cd_text_current_mwnd(
4685                    &mut self.ctx,
4686                    &text,
4687                    rf_flag_no as i64,
4688                ) {
4689                    self.ctx.ui.set_message(text);
4690                }
4691                self.drain_runtime_save_load_requests()?;
4692            }
4693            CD_NAME => {
4694                let name = self.pop_str()?;
4695                if !crate::runtime::forms::stage::cd_name_current_mwnd(&mut self.ctx, &name) {
4696                    self.ctx.ui.set_name(name);
4697                }
4698                self.drain_runtime_save_load_requests()?;
4699            }
4700            CD_SEL_BLOCK_START => {
4701                // Selection blocks are handled by higher-level UI commands.
4702                // Keep a marker to avoid breaking control flow.
4703            }
4704            CD_SEL_BLOCK_END => {
4705                // The original VM leaves a result on the int stack for certain selection constructs.
4706                // Default to 0 (first choice) if scripts expect a value.
4707                self.push_int(0);
4708            }
4709
4710            CD_EOF => {
4711                if self.at_cross_scene_return_boundary() && self.return_from_scene(Vec::new())? {
4712                    return Ok(true);
4713                }
4714                self.halted = true;
4715                return Ok(false);
4716            }
4717
4718            CD_NONE => {
4719                // In the original engine this is treated as a fatal script error.
4720                // Stop execution and record it.
4721                *self.unknown_opcodes.entry(opcode).or_insert(0) += 1;
4722                let scn_cmd_context = self.vm_scn_cmd_context(pc_before);
4723                eprintln!(
4724                    "VM hit CD_NONE scene={} line={} pc=0x{:x} {} bytes={:02x?}; stopping",
4725                    self.current_scene_name.as_deref().unwrap_or("<none>"),
4726                    self.current_line_no,
4727                    pc_before,
4728                    scn_cmd_context,
4729                    &self.stream.scn
4730                        [pc_before.saturating_sub(8)..self.stream.scn.len().min(pc_before + 16)]
4731                );
4732                self.halted = true;
4733                return Ok(false);
4734            }
4735
4736            other => {
4737                *self.unknown_opcodes.entry(other).or_insert(0) += 1;
4738                println!(
4739                    "VM unknown opcode=0x{other:02x} at pc=0x{:x}; stopping",
4740                    pc_before
4741                );
4742                self.halted = true;
4743                return Ok(false);
4744            }
4745        }
4746
4747        Ok(true)
4748    }
4749
4750    pub fn run(&mut self) -> Result<()> {
4751        while self.step()? {}
4752        Ok(())
4753    }
4754
4755    // ---------------------------------------------------------------------
4756    // Stack helpers
4757    // ---------------------------------------------------------------------
4758
4759    fn push_int(&mut self, v: i32) {
4760        self.int_stack.push(v);
4761        vm_trace!(self, None, format!("push_int {}", v));
4762    }
4763
4764    fn pop_int(&mut self) -> Result<i32> {
4765        match self.int_stack.pop() {
4766            Some(v) => {
4767                vm_trace!(self, None, format!("pop_int -> {}", v));
4768                Ok(v)
4769            }
4770            None => {
4771                vm_trace!(self, None, "pop_int underflow");
4772                Err(anyhow!(
4773                    "int stack underflow: scene={} scene_no={} line={} pc=0x{:x}",
4774                    self.current_scene_name.as_deref().unwrap_or("<none>"),
4775                    self.current_scene_no
4776                        .map(|v| v.to_string())
4777                        .unwrap_or_else(|| "-".to_string()),
4778                    self.current_line_no,
4779                    self.stream.get_prg_cntr()
4780                ))
4781            }
4782        }
4783    }
4784
4785    fn peek_int(&self) -> Result<i32> {
4786        self.int_stack
4787            .last()
4788            .copied()
4789            .ok_or_else(|| anyhow!("int stack underflow"))
4790    }
4791
4792    fn push_str(&mut self, s: String) {
4793        self.str_stack.push(s);
4794        vm_trace!(self, None, {
4795            let value = self.str_stack.last().expect("string was just pushed");
4796            let preview = if value.chars().count() > 48 {
4797                let mut preview = value.chars().take(48).collect::<String>();
4798                preview.push('…');
4799                preview
4800            } else {
4801                value.clone()
4802            };
4803            format!("push_str {:?}", preview)
4804        });
4805    }
4806
4807    fn pop_str(&mut self) -> Result<String> {
4808        match self.str_stack.pop() {
4809            Some(v) => {
4810                vm_trace!(
4811                    self,
4812                    None,
4813                    format!(
4814                        "pop_str -> {:?}",
4815                        if v.chars().count() > 48 {
4816                            let mut preview = v.chars().take(48).collect::<String>();
4817                            preview.push('…');
4818                            preview
4819                        } else {
4820                            v.clone()
4821                        }
4822                    )
4823                );
4824                Ok(v)
4825            }
4826            None => {
4827                vm_trace!(self, None, "pop_str underflow");
4828                Err(anyhow!(
4829                    "str stack underflow: scene={} scene_no={} line={} pc=0x{:x}",
4830                    self.current_scene_name.as_deref().unwrap_or("<none>"),
4831                    self.current_scene_no
4832                        .map(|v| v.to_string())
4833                        .unwrap_or_else(|| "-".to_string()),
4834                    self.current_line_no,
4835                    self.stream.get_prg_cntr()
4836                ))
4837            }
4838        }
4839    }
4840
4841    fn peek_str(&self) -> Result<String> {
4842        self.str_stack
4843            .last()
4844            .cloned()
4845            .ok_or_else(|| anyhow!("str stack underflow"))
4846    }
4847
4848    fn push_element(&mut self, elm: Vec<i32>) {
4849        self.element_points.push(self.int_stack.len());
4850        self.int_stack.extend_from_slice(&elm);
4851        vm_trace!(self, None, format!("push_element {:?}", elm));
4852    }
4853
4854    fn pop_element(&mut self) -> Result<Vec<i32>> {
4855        let start = match self.element_points.pop() {
4856            Some(v) => v,
4857            None => {
4858                vm_trace!(self, None, "pop_element underflow (missing ELM_POINT)");
4859                return Err(anyhow!("element stack underflow (missing ELM_POINT)"));
4860            }
4861        };
4862        if start > self.int_stack.len() {
4863            vm_trace!(
4864                self,
4865                None,
4866                format!(
4867                    "pop_element invalid start={} len={}",
4868                    start,
4869                    self.int_stack.len()
4870                ),
4871            );
4872            bail!(
4873                "invalid element point start={start} len={}",
4874                self.int_stack.len()
4875            );
4876        }
4877        let elm = self.ctx.int_vec_pool.take_copy(&self.int_stack[start..]);
4878        self.int_stack.truncate(start);
4879        vm_trace!(self, None, format!("pop_element -> {:?}", elm));
4880        Ok(elm)
4881    }
4882
4883    fn extract_array_index(&self, elm: &[i32]) -> Option<usize> {
4884        if elm.len() >= 3 && elm[1] == self.ctx.ids.elm_array {
4885            let idx = elm[2];
4886            if idx >= 0 {
4887                return Some(idx as usize);
4888            }
4889        }
4890        None
4891    }
4892
4893    fn user_prop_decl(&self, prop_id: u16) -> Option<(i32, usize)> {
4894        let prop_idx = prop_id as usize;
4895        if let Some(pck) = self.scene_pck_cache.as_ref() {
4896            if prop_idx < pck.inc_props.len() {
4897                let decl = &pck.inc_props[prop_idx];
4898                return Some((decl.form, decl.size.max(0) as usize));
4899            }
4900        } else if prop_idx < self.stream.header.scn_prop_cnt.max(0) as usize {
4901            // Some VM/unit-test entry points construct a SceneVm directly from
4902            // one scene chunk and do not eagerly load Scene.pck metadata.  In
4903            // that mode the bytecode still numbers the shared/inc properties
4904            // from zero, and this scene chunk carries the matching declarations
4905            // in its own prop table.  Treat them as authoritative until the
4906            // pack cache is available; otherwise scalar input/menu variables
4907            // are mis-created as generic lists and title clicks never latch.
4908            let off =
4909                (self.stream.header.scn_prop_list_ofs.max(0) as usize).checked_add(prop_idx * 8)?;
4910            if off + 8 <= self.stream.chunk.len() {
4911                let form = i32::from_le_bytes(self.stream.chunk[off..off + 4].try_into().unwrap());
4912                let size =
4913                    i32::from_le_bytes(self.stream.chunk[off + 4..off + 8].try_into().unwrap());
4914                return Some((form, size.max(0) as usize));
4915            }
4916        }
4917
4918        let local_idx = prop_idx.saturating_sub(
4919            self.scene_pck_cache
4920                .as_ref()
4921                .map(|pck| pck.inc_props.len())
4922                .unwrap_or(0),
4923        );
4924        let list_ofs = self.stream.header.scn_prop_list_ofs.max(0) as usize;
4925        let cnt = self.stream.header.scn_prop_cnt.max(0) as usize;
4926        if local_idx < cnt {
4927            let off = list_ofs.checked_add(local_idx * 8)?;
4928            if off + 8 <= self.stream.chunk.len() {
4929                let form = i32::from_le_bytes(self.stream.chunk[off..off + 4].try_into().unwrap());
4930                let size =
4931                    i32::from_le_bytes(self.stream.chunk[off + 4..off + 8].try_into().unwrap());
4932                return Some((form, size.max(0) as usize));
4933            }
4934        }
4935        None
4936    }
4937
4938    fn default_user_prop_element(&self, prop_id: u16, _form: i32) -> Vec<i32> {
4939        let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
4940        vec![head]
4941    }
4942
4943    fn default_user_prop_slot_element(&self, prop_id: u16, idx: usize) -> Vec<i32> {
4944        vec![
4945            constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32),
4946            self.ctx.ids.elm_array,
4947            idx as i32,
4948        ]
4949    }
4950
4951    fn default_user_prop_cell(&self, prop_id: u16) -> UserPropCell {
4952        let (form, size) = self
4953            .user_prop_decl(prop_id)
4954            .unwrap_or((self.cfg.fm_list, 0));
4955        let mut cell = UserPropCell::new(form, self.default_user_prop_element(prop_id, form));
4956        if form == self.cfg.fm_intlist {
4957            cell.int_list = vec![0; size];
4958        } else if form == self.cfg.fm_strlist {
4959            cell.str_list = vec![String::new(); size];
4960        } else if form == self.cfg.fm_list && size > 0 {
4961            let mut items = Vec::with_capacity(size);
4962            for i in 0..size {
4963                let mut slot = UserPropCell::new(
4964                    self.cfg.fm_list,
4965                    self.default_user_prop_slot_element(prop_id, i),
4966                );
4967                slot.form = self.cfg.fm_list;
4968                items.push(slot);
4969            }
4970            cell.list_items = items;
4971        }
4972        cell
4973    }
4974
4975    fn user_prop_cell_from_value(
4976        &self,
4977        rhs: Value,
4978        declared_form: i32,
4979        element: Vec<i32>,
4980        prop_id: Option<u16>,
4981    ) -> UserPropCell {
4982        let mut cell = UserPropCell::new(declared_form, element.clone());
4983        match rhs {
4984            Value::NamedArg { value, .. } => {
4985                return self.user_prop_cell_from_value(*value, declared_form, element, prop_id);
4986            }
4987            Value::Int(n) => {
4988                cell.form = self.cfg.fm_int;
4989                cell.int_value = n as i32;
4990            }
4991            Value::Str(s) => {
4992                cell.form = self.cfg.fm_str;
4993                cell.str_value = s;
4994            }
4995            Value::Element(e) => {
4996                cell.form = declared_form;
4997                cell.element = e;
4998            }
4999            Value::List(items) => {
5000                if declared_form == self.cfg.fm_intlist {
5001                    cell.form = self.cfg.fm_intlist;
5002                    cell.int_list = items
5003                        .into_iter()
5004                        .map(|item| item.as_i64().unwrap_or(0) as i32)
5005                        .collect();
5006                } else if declared_form == self.cfg.fm_strlist {
5007                    cell.form = self.cfg.fm_strlist;
5008                    cell.str_list = items
5009                        .into_iter()
5010                        .map(|item| item.as_str().unwrap_or("").to_string())
5011                        .collect();
5012                } else {
5013                    cell.form = self.cfg.fm_list;
5014                    let mut out = Vec::with_capacity(items.len());
5015                    for (idx, item) in items.into_iter().enumerate() {
5016                        let slot_element = if let Some(pid) = prop_id {
5017                            self.default_user_prop_slot_element(pid, idx)
5018                        } else {
5019                            vec![]
5020                        };
5021                        out.push(self.user_prop_cell_from_value(
5022                            item,
5023                            self.cfg.fm_list,
5024                            slot_element,
5025                            prop_id,
5026                        ));
5027                    }
5028                    cell.list_items = out;
5029                }
5030            }
5031        }
5032        cell
5033    }
5034
5035    fn consume_array_sub_signed<'b>(&self, sub: &'b [i32]) -> Option<(i32, &'b [i32])> {
5036        if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5037            Some((sub[1], &sub[2..]))
5038        } else {
5039            None
5040        }
5041    }
5042
5043    fn consume_array_sub<'b>(&self, sub: &'b [i32]) -> Option<(usize, &'b [i32])> {
5044        let (index, rest) = self.consume_array_sub_signed(sub)?;
5045        usize::try_from(index).ok().map(|index| (index, rest))
5046    }
5047
5048    fn intlist_bit_get(values: &[i32], bit: i32, index: usize) -> i32 {
5049        let word = values
5050            .get(index / (32 / bit as usize))
5051            .copied()
5052            .unwrap_or(0) as u32;
5053        let shift = (index % (32 / bit as usize)) * bit as usize;
5054        let mask = ((1u32 << bit) - 1) << shift;
5055        ((word & mask) >> shift) as i32
5056    }
5057
5058    fn intlist_dispatch_read(
5059        &mut self,
5060        values: &[i32],
5061        sub: &[i32],
5062        fallback_element: &[i32],
5063    ) -> Result<()> {
5064        use crate::runtime::forms::codes::{
5065            ELM_ARRAY, ELM_INTLIST_BIT, ELM_INTLIST_BIT2, ELM_INTLIST_BIT4, ELM_INTLIST_BIT8,
5066            ELM_INTLIST_BIT16, ELM_INTLIST_GET_SIZE,
5067        };
5068        let sub = if sub.len() == 1 && self.call_array_marker(sub[0]) {
5069            &[][..]
5070        } else {
5071            sub
5072        };
5073        if sub.is_empty() {
5074            self.push_element(fallback_element.to_vec());
5075            return Ok(());
5076        }
5077        if let Some((idx, rest)) = self.consume_array_sub_signed(sub) {
5078            if !rest.is_empty() {
5079                bail!("unsupported chained intlist array access {:?}", sub);
5080            }
5081            let value = usize::try_from(idx)
5082                .ok()
5083                .and_then(|idx| values.get(idx).copied())
5084                .unwrap_or(0);
5085            self.push_int(value);
5086            return Ok(());
5087        }
5088        match sub[0] {
5089            ELM_INTLIST_GET_SIZE => {
5090                self.push_int(values.len() as i32);
5091            }
5092            ELM_INTLIST_BIT | ELM_INTLIST_BIT2 | ELM_INTLIST_BIT4 | ELM_INTLIST_BIT8
5093            | ELM_INTLIST_BIT16 => {
5094                let bit = match sub[0] {
5095                    ELM_INTLIST_BIT => 1,
5096                    ELM_INTLIST_BIT2 => 2,
5097                    ELM_INTLIST_BIT4 => 4,
5098                    ELM_INTLIST_BIT8 => 8,
5099                    _ => 16,
5100                };
5101                if let Some((idx, rest)) = self.consume_array_sub_signed(&sub[1..]) {
5102                    if !rest.is_empty() {
5103                        bail!("unsupported chained intlist bit access {:?}", sub);
5104                    }
5105                    let value = usize::try_from(idx)
5106                        .ok()
5107                        .map(|idx| Self::intlist_bit_get(values, bit, idx))
5108                        .unwrap_or(0);
5109                    self.push_int(value);
5110                } else {
5111                    let mut chained = fallback_element.to_vec();
5112                    chained.extend_from_slice(sub);
5113                    self.push_element(chained);
5114                }
5115            }
5116            _ => self.push_element(fallback_element.to_vec()),
5117        }
5118        Ok(())
5119    }
5120
5121    fn push_user_prop_cell_result(
5122        &mut self,
5123        cell: &UserPropCell,
5124        sub: &[i32],
5125        full_elm: &[i32],
5126    ) -> Result<()> {
5127        use crate::runtime::forms::codes::{
5128            ELM_STRLIST_GET_SIZE, FM_INTLISTREF, FM_INTREF, FM_STRLISTREF, FM_STRREF,
5129        };
5130
5131        let sub = if sub.len() == 1 && self.call_array_marker(sub[0]) {
5132            &[][..]
5133        } else {
5134            sub
5135        };
5136        if cell.form == self.cfg.fm_int && sub.is_empty() {
5137            self.push_int(cell.int_value);
5138            return Ok(());
5139        }
5140        if cell.form == self.cfg.fm_str {
5141            if sub.is_empty() {
5142                self.push_str(cell.str_value.clone());
5143            } else {
5144                self.call_prop_eval_str_op(&cell.str_value, sub[0], &[], 0)?;
5145            }
5146            return Ok(());
5147        }
5148        if cell.form == self.cfg.fm_intlist {
5149            return self.intlist_dispatch_read(&cell.int_list, sub, &cell.element);
5150        }
5151        if cell.form == self.cfg.fm_strlist {
5152            if sub.is_empty() {
5153                self.push_element(cell.element.clone());
5154            } else if let Some((idx, rest)) = self.consume_array_sub_signed(sub) {
5155                let cur = usize::try_from(idx)
5156                    .ok()
5157                    .and_then(|idx| cell.str_list.get(idx).cloned())
5158                    .unwrap_or_default();
5159                if rest.is_empty() {
5160                    self.push_str(cur);
5161                } else {
5162                    self.call_prop_eval_str_op(&cur, rest[0], &[], 0)?;
5163                }
5164            } else if sub[0] == ELM_STRLIST_GET_SIZE {
5165                self.push_int(cell.str_list.len() as i32);
5166            } else {
5167                self.push_element(cell.element.clone());
5168            }
5169            return Ok(());
5170        }
5171        if matches!(
5172            cell.form,
5173            FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF
5174        ) {
5175            self.push_element(cell.element.clone());
5176            return Ok(());
5177        }
5178        if let Some((idx, rest)) = self.consume_array_sub(sub) {
5179            let slot = if let Some(slot) = cell.list_items.get(idx) {
5180                slot.clone()
5181            } else {
5182                let mut tmp = UserPropCell::new(
5183                    self.cfg.fm_list,
5184                    self.default_user_prop_slot_element(elm_code::code(full_elm[0]), idx),
5185                );
5186                tmp.form = self.cfg.fm_list;
5187                tmp
5188            };
5189            return self.push_user_prop_cell_result(&slot, rest, full_elm);
5190        }
5191        self.push_element(cell.element.clone());
5192        Ok(())
5193    }
5194
5195    fn default_value_like(&self, v: &Value) -> Value {
5196        match v {
5197            Value::NamedArg { value, .. } => self.default_value_like(value),
5198            Value::Int(_) => Value::Int(0),
5199            Value::Str(_) => Value::Str(String::new()),
5200            Value::Element(_) => Value::Element(Vec::new()),
5201            Value::List(_) => Value::List(Vec::new()),
5202        }
5203    }
5204    fn call_scratch_from_args(&self, args: &[Value]) -> (Vec<i32>, Vec<String>) {
5205        let mut int_args = Self::blank_call_int_args(self.call_flag_count);
5206        let mut str_args = Self::blank_call_str_args(self.call_flag_count);
5207        let mut int_pos = 0usize;
5208        let mut str_pos = 0usize;
5209        for v in args {
5210            match v {
5211                Value::NamedArg { value, .. } => match value.as_ref() {
5212                    Value::Int(n) => {
5213                        if int_pos < int_args.len() {
5214                            int_args[int_pos] = *n as i32;
5215                            int_pos += 1;
5216                        }
5217                    }
5218                    Value::Str(s) if str_pos < str_args.len() => {
5219                        str_args[str_pos] = s.clone();
5220                        str_pos += 1;
5221                    }
5222                    _ => {}
5223                },
5224                Value::Int(n) => {
5225                    if int_pos < int_args.len() {
5226                        int_args[int_pos] = *n as i32;
5227                        int_pos += 1;
5228                    }
5229                }
5230                Value::Str(s) if str_pos < str_args.len() => {
5231                    str_args[str_pos] = s.clone();
5232                    str_pos += 1;
5233                }
5234                _ => {}
5235            }
5236        }
5237        (int_args, str_args)
5238    }
5239
5240    fn call_array_marker(&self, code: i32) -> bool {
5241        let mapped = self.ctx.ids.elm_array;
5242        code == crate::runtime::forms::codes::ELM_ARRAY || (mapped >= 0 && code == mapped)
5243    }
5244
5245    fn resolve_call_frame_index(&self, idx: i32) -> Option<usize> {
5246        if idx < 0 {
5247            return None;
5248        }
5249        let depth = self.call_stack.len();
5250        let rev = idx as usize;
5251        if rev >= depth {
5252            return None;
5253        }
5254        Some(depth - 1 - rev)
5255    }
5256
5257    fn current_call_frame_index(&self) -> Option<usize> {
5258        if self.call_stack.is_empty() {
5259            None
5260        } else {
5261            Some(self.call_stack.len() - 1)
5262        }
5263    }
5264
5265    fn find_call_prop_index_in_frame(&self, frame_idx: usize, call_prop_id: i32) -> Option<usize> {
5266        // C++ tnm_command_proc_call_prop() indexes the current call's
5267        // user_prop_list directly with the CALL_PROP code value:
5268        //   p_cur_call->user_prop_list[call_prop_id]
5269        // The stored C_elm_user_call_prop::prop_id is the declared property id
5270        // and is not the lookup key for CALL_PROP bytecode.
5271        if call_prop_id < 0 {
5272            return None;
5273        }
5274        let frame = self.call_stack.get(frame_idx)?;
5275        let idx = call_prop_id as usize;
5276        if idx < frame.user_props.len() {
5277            Some(idx)
5278        } else {
5279            None
5280        }
5281    }
5282
5283    fn call_prop_element(prop_id: i32) -> Vec<i32> {
5284        vec![constants::elm::create(
5285            constants::elm::OWNER_CALL_PROP,
5286            0,
5287            prop_id,
5288        )]
5289    }
5290
5291    fn call_prop_value_from_rhs(&self, rhs: &Value) -> (i32, CallPropValue) {
5292        match rhs {
5293            Value::NamedArg { value, .. } => self.call_prop_value_from_rhs(value),
5294            Value::Int(n) => (self.cfg.fm_int, CallPropValue::Int(*n as i32)),
5295            Value::Str(s) => (self.cfg.fm_str, CallPropValue::Str(s.clone())),
5296            Value::Element(e) => (self.cfg.fm_list, CallPropValue::Element(e.clone())),
5297            Value::List(_) => (self.cfg.fm_list, CallPropValue::Element(Vec::new())),
5298        }
5299    }
5300
5301    fn ensure_call_prop_index_for_assign(
5302        &mut self,
5303        frame_idx: usize,
5304        call_prop_id: i32,
5305        rhs: &Value,
5306    ) -> Result<usize> {
5307        if let Some(idx) = self.find_call_prop_index_in_frame(frame_idx, call_prop_id) {
5308            return Ok(idx);
5309        }
5310        if call_prop_id < 0 {
5311            bail!("negative CALL_PROP id {}", call_prop_id);
5312        }
5313
5314        // The original engine expects CALL_PROP ids to be dense list indexes
5315        // created by CD_DEC_PROP. If Rust reaches an assignment before a slot
5316        // exists, keep the same indexed layout rather than appending a slot with
5317        // a matching prop_id, because later CALL_PROP[0] must address slot 0.
5318        let (form, value) = self.call_prop_value_from_rhs(rhs);
5319        let target_idx = call_prop_id as usize;
5320        let frame = self
5321            .call_stack
5322            .get_mut(frame_idx)
5323            .ok_or_else(|| anyhow!("call stack frame out of range"))?;
5324        while frame.user_props.len() <= target_idx {
5325            let idx = frame.user_props.len() as i32;
5326            frame.user_props.push(CallProp {
5327                scn_no: self.current_scene_no.unwrap_or(0) as i32,
5328                prop_id: idx,
5329                form: self.cfg.fm_list,
5330                decl_size: 0,
5331                element: Self::call_prop_element(idx),
5332                value: CallPropValue::Element(Self::call_prop_element(idx)),
5333            });
5334        }
5335        let prop = frame
5336            .user_props
5337            .get_mut(target_idx)
5338            .ok_or_else(|| anyhow!("CALL_PROP slot allocation failed"))?;
5339        prop.form = form;
5340        prop.element = Self::call_prop_element(call_prop_id);
5341        prop.value = value;
5342        Ok(target_idx)
5343    }
5344
5345    fn is_direct_value_form(&self, form: i32) -> bool {
5346        form == self.cfg.fm_int
5347            || form == self.cfg.fm_str
5348            || form == self.cfg.fm_intlist
5349            || form == self.cfg.fm_strlist
5350    }
5351
5352    fn call_prop_effective_element(&self, prop: &CallProp) -> Vec<i32> {
5353        match &prop.value {
5354            CallPropValue::Element(e) if !e.is_empty() => e.clone(),
5355            _ => prop.element.clone(),
5356        }
5357    }
5358
5359    /// Reads a CALL_PROP without copying it: references continue into the
5360    /// composed element, plain ints are pushed. False for the other forms
5361    /// (`push_call_prop_result` handles them). Frame-action scripts read
5362    /// these props thousands of times a frame.
5363    fn push_call_prop_fast(&mut self, frame: usize, prop_idx: usize, sub: &[i32]) -> Result<bool> {
5364        let prop = &self.call_stack[frame].user_props[prop_idx];
5365        if let Some(composed) = self.compose_call_prop_tail(prop, sub) {
5366            self.exec_property(composed)?;
5367            return Ok(true);
5368        }
5369        use crate::runtime::forms::codes::{
5370            FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR, FM_STRLIST, FM_STRLISTREF,
5371            FM_STRREF,
5372        };
5373        let plain = sub.is_empty() || (sub.len() == 1 && self.call_array_marker(sub[0]));
5374        match (prop.form, &prop.value) {
5375            (FM_INT, CallPropValue::Int(n)) if plain => {
5376                let n = *n;
5377                self.push_int(n);
5378            }
5379            (FM_STR, CallPropValue::Str(text)) if plain => {
5380                let text = text.clone();
5381                self.push_str(text);
5382            }
5383            // The catch-all arm of `push_call_prop_result`: objects and other
5384            // element-valued props push their element.
5385            (
5386                FM_INT | FM_STR | FM_INTLIST | FM_STRLIST | FM_INTREF | FM_STRREF | FM_INTLISTREF
5387                | FM_STRLISTREF,
5388                _,
5389            ) => return Ok(false),
5390            _ => {
5391                // As `push_element`, copying straight from the call frame.
5392                self.element_points.push(self.int_stack.len());
5393                self.int_stack
5394                    .extend_from_slice(&self.call_stack[frame].user_props[prop_idx].element);
5395            }
5396        }
5397        Ok(true)
5398    }
5399
5400    fn compose_call_prop_tail(&self, prop: &CallProp, sub: &[i32]) -> Option<Vec<i32>> {
5401        // A lone ELM_ARRAY after a reference is the compiler/runtime marker used
5402        // while dereferencing the property itself, not an indexed access.  The
5403        // real array form always carries the evaluated index as the next word.
5404        // Do not turn a reference such as GLOBAL.D into the malformed [D, ARRAY]
5405        // chain; let push_call_prop_result() return the referenced element first.
5406        if sub.is_empty()
5407            || (sub.len() == 1 && self.call_array_marker(sub[0]))
5408            || self.is_direct_value_form(prop.form)
5409        {
5410            return None;
5411        }
5412        let mut element = self.call_prop_effective_element(prop);
5413        if element.is_empty() {
5414            return None;
5415        }
5416        element.extend_from_slice(sub);
5417        Some(element)
5418    }
5419
5420    fn compose_user_prop_tail(
5421        &self,
5422        prop_id: u16,
5423        cell: &UserPropCell,
5424        sub: &[i32],
5425    ) -> Option<Vec<i32>> {
5426        // Keep the same marker-only reference semantics as CALL_PROP.  A bare
5427        // ELM_ARRAY is not a concrete array access because it has no index.
5428        if sub.is_empty()
5429            || (sub.len() == 1 && self.call_array_marker(sub[0]))
5430            || self.is_direct_value_form(cell.form)
5431        {
5432            return None;
5433        }
5434        if let Some((idx, rest)) = self.consume_array_sub(sub) {
5435            let slot = cell.list_items.get(idx)?;
5436            let default_slot = self.default_user_prop_slot_element(prop_id, idx);
5437            if slot.element.is_empty() || slot.element == default_slot {
5438                return None;
5439            }
5440            if rest.is_empty() || self.is_direct_value_form(slot.form) {
5441                return Some(slot.element.clone());
5442            }
5443            let mut element = slot.element.clone();
5444            element.extend_from_slice(rest);
5445            return Some(element);
5446        }
5447
5448        let default_root = self.default_user_prop_element(prop_id, cell.form);
5449        if cell.element.is_empty() || cell.element == default_root {
5450            return None;
5451        }
5452        let mut element = cell.element.clone();
5453        element.extend_from_slice(sub);
5454        Some(element)
5455    }
5456
5457    fn push_call_prop_result(
5458        &mut self,
5459        prop: &CallProp,
5460        sub: &[i32],
5461        full_elm: &[i32],
5462    ) -> Result<()> {
5463        use crate::runtime::forms::codes::{
5464            ELM_STRLIST_GET_SIZE, FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR, FM_STRLIST,
5465            FM_STRLISTREF, FM_STRREF,
5466        };
5467
5468        let sub = if sub.len() == 1 && self.call_array_marker(sub[0]) {
5469            &[][..]
5470        } else {
5471            sub
5472        };
5473        match (prop.form, sub) {
5474            (FM_INT, []) => {
5475                if let CallPropValue::Int(n) = &prop.value {
5476                    self.push_int(*n);
5477                } else {
5478                    bail!("CALL_PROP int storage mismatch for {:?}", full_elm);
5479                }
5480            }
5481            (FM_STR, []) => {
5482                if let CallPropValue::Str(s) = &prop.value {
5483                    self.push_str(s.clone());
5484                } else {
5485                    bail!("CALL_PROP str storage mismatch for {:?}", full_elm);
5486                }
5487            }
5488            (FM_STR, _) => {
5489                if let CallPropValue::Str(s) = &prop.value {
5490                    self.call_prop_eval_str_op(s, sub[0], &[], 0)?;
5491                } else {
5492                    bail!("CALL_PROP str storage mismatch for {:?}", full_elm);
5493                }
5494            }
5495            (FM_INTLIST, _) => {
5496                if let CallPropValue::IntList(v) = &prop.value {
5497                    self.intlist_dispatch_read(v, sub, &prop.element)?;
5498                } else {
5499                    bail!("CALL_PROP intlist storage mismatch for {:?}", full_elm);
5500                }
5501            }
5502            (FM_STRLIST, _) => {
5503                if let CallPropValue::StrList(v) = &prop.value {
5504                    if sub.is_empty() {
5505                        self.push_element(prop.element.clone());
5506                    } else if let Some((idx, rest)) = self.consume_array_sub_signed(sub) {
5507                        let current = usize::try_from(idx)
5508                            .ok()
5509                            .and_then(|idx| v.get(idx).cloned())
5510                            .unwrap_or_default();
5511                        if rest.is_empty() {
5512                            self.push_str(current);
5513                        } else {
5514                            self.call_prop_eval_str_op(&current, rest[0], &[], 0)?;
5515                        }
5516                    } else if sub[0] == ELM_STRLIST_GET_SIZE {
5517                        self.push_int(v.len() as i32);
5518                    } else {
5519                        self.push_element(prop.element.clone());
5520                    }
5521                } else {
5522                    bail!("CALL_PROP strlist storage mismatch for {:?}", full_elm);
5523                }
5524            }
5525            (FM_INTREF | FM_STRREF, []) => {
5526                // C++ tnm_command_proc_prop() does not read the scalar here.
5527                // For every *_REF form it pushes the referenced element back to
5528                // the element stack. SiglusCompiler emits another CD_PROPERTY
5529                // when the expression actually needs a value, so resolving the
5530                // target recursively at this first step consumes one
5531                // dereference too early and corrupts the following stack shape.
5532                let target = self.call_prop_effective_element(prop);
5533                let default_target = Self::call_prop_element(prop.prop_id);
5534                if target.is_empty() || target == full_elm || target == default_target {
5535                    bail!(
5536                        "unbound scalar CALL_PROP reference form={} target={:?} access={:?}",
5537                        prop.form,
5538                        target,
5539                        full_elm
5540                    );
5541                }
5542                self.push_element(target);
5543            }
5544            (FM_INTLISTREF | FM_STRLISTREF, _) => {
5545                // Lists remain element-valued after dereference; their ARRAY and
5546                // list-command suffixes are dispatched through the target chain.
5547                self.push_element(self.call_prop_effective_element(prop));
5548            }
5549            (FM_INTREF | FM_STRREF, _) => {
5550                // A non-empty suffix should normally have been composed by
5551                // compose_call_prop_tail(). Keep a precise failure here rather
5552                // than silently returning the reference itself as a scalar.
5553                bail!(
5554                    "unsupported scalar CALL_PROP reference suffix form={} sub={:?} access={:?}",
5555                    prop.form,
5556                    sub,
5557                    full_elm
5558                );
5559            }
5560            _ => {
5561                self.push_element(prop.element.clone());
5562            }
5563        }
5564        Ok(())
5565    }
5566
5567    fn call_prop_eval_str_op(
5568        &mut self,
5569        current: &str,
5570        op: i32,
5571        params: &[Value],
5572        al_id: i32,
5573    ) -> Result<()> {
5574        use crate::runtime::forms::codes::str_op;
5575        match op {
5576            str_op::UPPER => self.push_str(crate::runtime::string_semantics::ascii_upper(current)),
5577            str_op::LOWER => self.push_str(crate::runtime::string_semantics::ascii_lower(current)),
5578            str_op::CNT => {
5579                self.push_int(crate::runtime::string_semantics::utf16_len(current) as i32)
5580            }
5581            str_op::LEN => {
5582                self.push_int(crate::runtime::string_semantics::display_width(current) as i32)
5583            }
5584            str_op::LEFT => {
5585                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5586                self.push_str(crate::runtime::string_semantics::utf16_left(current, len));
5587            }
5588            str_op::LEFT_LEN => {
5589                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5590                self.push_str(crate::runtime::string_semantics::left_by_display_width(
5591                    current, len,
5592                ));
5593            }
5594            str_op::RIGHT => {
5595                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5596                self.push_str(crate::runtime::string_semantics::utf16_right(current, len));
5597            }
5598            str_op::RIGHT_LEN => {
5599                let len = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5600                self.push_str(crate::runtime::string_semantics::right_by_display_width(
5601                    current, len,
5602                ));
5603            }
5604            str_op::MID => {
5605                let start = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5606                if al_id == 0 || params.len() <= 1 {
5607                    self.push_str(crate::runtime::string_semantics::utf16_slice(
5608                        current, start, None,
5609                    ));
5610                } else {
5611                    let len = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5612                    self.push_str(crate::runtime::string_semantics::utf16_slice(
5613                        current,
5614                        start,
5615                        Some(len),
5616                    ));
5617                }
5618            }
5619            str_op::MID_LEN => {
5620                let start = params.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5621                let len = if al_id == 0 || params.len() <= 1 {
5622                    None
5623                } else {
5624                    Some(params.get(1).and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize)
5625                };
5626                self.push_str(crate::runtime::string_semantics::mid_by_display_width(
5627                    current, start, len,
5628                ));
5629            }
5630            str_op::SEARCH => {
5631                let needle = params.first().and_then(|v| v.as_str()).unwrap_or("");
5632                self.push_int(
5633                    crate::runtime::string_semantics::search_ascii_case_insensitive(
5634                        current, needle,
5635                    )
5636                    .map(|v| v as i32)
5637                    .unwrap_or(-1),
5638                );
5639            }
5640            str_op::SEARCH_LAST => {
5641                let needle = params.first().and_then(|v| v.as_str()).unwrap_or("");
5642                self.push_int(
5643                    crate::runtime::string_semantics::rsearch_ascii_case_insensitive(
5644                        current, needle,
5645                    )
5646                    .map(|v| v as i32)
5647                    .unwrap_or(-1),
5648                );
5649            }
5650            str_op::GET_CODE => {
5651                let pos = params.first().and_then(|v| v.as_i64()).unwrap_or(0);
5652                self.push_int(
5653                    usize::try_from(pos)
5654                        .ok()
5655                        .and_then(|pos| {
5656                            crate::runtime::string_semantics::utf16_code_unit(current, pos)
5657                        })
5658                        .map(i32::from)
5659                        .unwrap_or(-1),
5660                );
5661            }
5662            str_op::TONUM => self.push_int(current.parse::<i32>().unwrap_or(0)),
5663            _ => bail!("unsupported CALL_PROP string op {}", op),
5664        }
5665        Ok(())
5666    }
5667
5668    fn intlist_assignment_subscript(sub: &[i32]) -> Option<(i32, i32)> {
5669        use crate::runtime::forms::codes::{
5670            ELM_ARRAY, ELM_INTLIST_BIT, ELM_INTLIST_BIT2, ELM_INTLIST_BIT4, ELM_INTLIST_BIT8,
5671            ELM_INTLIST_BIT16,
5672        };
5673
5674        if sub.len() == 2 && sub[0] == ELM_ARRAY {
5675            return Some((32, sub[1]));
5676        }
5677        if sub.len() != 3 || sub[1] != ELM_ARRAY {
5678            return None;
5679        }
5680        let bit = match sub[0] {
5681            ELM_INTLIST_BIT => 1,
5682            ELM_INTLIST_BIT2 => 2,
5683            ELM_INTLIST_BIT4 => 4,
5684            ELM_INTLIST_BIT8 => 8,
5685            ELM_INTLIST_BIT16 => 16,
5686            _ => return None,
5687        };
5688        Some((bit, sub[2]))
5689    }
5690
5691    fn assign_call_prop_result(prop: &mut CallProp, sub: &[i32], rhs: Value) -> Result<()> {
5692        use crate::runtime::forms::codes::{
5693            ELM_ARRAY, FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR, FM_STRLIST,
5694            FM_STRLISTREF, FM_STRREF,
5695        };
5696
5697        match (prop.form, sub) {
5698            (FM_INT, []) => match rhs {
5699                Value::Int(n) => {
5700                    prop.value = CallPropValue::Int(n as i32);
5701                }
5702                _ => bail!("unsupported CALL_PROP int assign sub={:?}", sub),
5703            },
5704            (FM_STR, []) => match rhs {
5705                Value::Str(s) => {
5706                    prop.value = CallPropValue::Str(s);
5707                }
5708                _ => bail!("unsupported CALL_PROP str assign sub={:?}", sub),
5709            },
5710            (FM_INTLIST, _) => {
5711                let Some((bit, index)) = Self::intlist_assignment_subscript(sub) else {
5712                    bail!("unsupported CALL_PROP intlist assign sub={:?}", sub);
5713                };
5714                let Value::Int(n) = rhs else {
5715                    bail!("unsupported CALL_PROP intlist assign sub={:?}", sub);
5716                };
5717                let mut dst =
5718                    match std::mem::replace(&mut prop.value, CallPropValue::IntList(Vec::new())) {
5719                        CallPropValue::IntList(v) => v,
5720                        other => {
5721                            prop.value = other;
5722                            bail!("CALL_PROP intlist storage mismatch");
5723                        }
5724                    };
5725                if bit == 32 && index >= 0 {
5726                    let idx = index as usize;
5727                    if dst.len() <= idx {
5728                        dst.resize(idx + 1, 0);
5729                    }
5730                }
5731                Self::set_user_int_list_value(&mut dst, bit, index, n as i32);
5732                prop.value = CallPropValue::IntList(dst);
5733            }
5734            (FM_STRLIST, [ELM_ARRAY, _, ..]) => match rhs {
5735                Value::Str(s) => {
5736                    let idx = sub[1].max(0) as usize;
5737                    let mut dst = match std::mem::replace(
5738                        &mut prop.value,
5739                        CallPropValue::StrList(Vec::new()),
5740                    ) {
5741                        CallPropValue::StrList(v) => v,
5742                        other => {
5743                            prop.value = other;
5744                            bail!("CALL_PROP strlist storage mismatch");
5745                        }
5746                    };
5747                    if dst.len() <= idx {
5748                        dst.resize_with(idx + 1, String::new);
5749                    }
5750                    dst[idx] = s;
5751                    prop.value = CallPropValue::StrList(dst);
5752                }
5753                _ => bail!("unsupported CALL_PROP strlist assign sub={:?}", sub),
5754            },
5755            (FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF, _) => match rhs {
5756                Value::Element(e) => {
5757                    prop.element = e.clone();
5758                    prop.value = CallPropValue::Element(e);
5759                }
5760                _ => bail!("unsupported CALL_PROP ref assign sub={:?}", sub),
5761            },
5762            _ => bail!(
5763                "unsupported call prop assign form={} sub={:?}",
5764                prop.form,
5765                sub
5766            ),
5767        }
5768        Ok(())
5769    }
5770
5771    fn set_user_int_list_value(values: &mut [i32], bit: i32, index: i32, value: i32) {
5772        if index < 0 || !matches!(bit, 1 | 2 | 4 | 8 | 16 | 32) {
5773            return;
5774        }
5775        let index = index as usize;
5776        if bit == 32 {
5777            if let Some(slot) = values.get_mut(index) {
5778                *slot = value;
5779            }
5780            return;
5781        }
5782        let per_word = 32usize / bit as usize;
5783        let Some(word) = values.get_mut(index / per_word) else {
5784            return;
5785        };
5786        let shift = (index % per_word) * bit as usize;
5787        let mask = ((1u32 << bit) - 1) << shift;
5788        let encoded = ((value as u32) << shift) & mask;
5789        *word = (((*word as u32) & !mask) | encoded) as i32;
5790    }
5791
5792    fn get_user_int_list_value(values: &[i32], bit: i32, index: i32) -> Option<i32> {
5793        if index < 0 || !matches!(bit, 1 | 2 | 4 | 8 | 16 | 32) {
5794            return None;
5795        }
5796        let index = index as usize;
5797        if bit == 32 {
5798            return values.get(index).copied();
5799        }
5800        let per_word = 32usize / bit as usize;
5801        let word = *values.get(index / per_word)? as u32;
5802        let shift = (index % per_word) * bit as usize;
5803        Some(((word >> shift) & ((1u32 << bit) - 1)) as i32)
5804    }
5805
5806    fn exec_user_prop_list_command(
5807        &mut self,
5808        prop_id: u16,
5809        sub: &[i32],
5810        al_id: i32,
5811        ret_form: i32,
5812        args: &[Value],
5813    ) -> Result<bool> {
5814        use crate::runtime::forms::codes::{
5815            ELM_INTLIST_BIT, ELM_INTLIST_BIT2, ELM_INTLIST_BIT4, ELM_INTLIST_BIT8,
5816            ELM_INTLIST_BIT16, ELM_INTLIST_CLEAR, ELM_INTLIST_GET_SIZE, ELM_INTLIST_INIT,
5817            ELM_INTLIST_RESIZE, ELM_INTLIST_SETS, ELM_STRLIST_GET_SIZE, ELM_STRLIST_INIT,
5818            ELM_STRLIST_RESIZE,
5819        };
5820
5821        if sub.is_empty() {
5822            return Ok(false);
5823        }
5824
5825        let (decl_form, decl_size) = self
5826            .user_prop_decl(prop_id)
5827            .unwrap_or((self.cfg.fm_list, 0));
5828        let mut cell = self
5829            .user_props
5830            .remove(&prop_id)
5831            .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
5832        let form = cell.form;
5833        let mut handled = false;
5834
5835        if form == self.cfg.fm_intlist {
5836            let mut bit = 32;
5837            let mut op = sub;
5838            if let Some(first) = op.first().copied() {
5839                bit = match first {
5840                    ELM_INTLIST_BIT => 1,
5841                    ELM_INTLIST_BIT2 => 2,
5842                    ELM_INTLIST_BIT4 => 4,
5843                    ELM_INTLIST_BIT8 => 8,
5844                    ELM_INTLIST_BIT16 => 16,
5845                    _ => 32,
5846                };
5847                if bit != 32 {
5848                    op = &op[1..];
5849                }
5850            }
5851
5852            if op.len() >= 2 && self.call_array_marker(op[0]) {
5853                let index = op[1];
5854                if al_id == 0 {
5855                    if let Some(value) = Self::get_user_int_list_value(&cell.int_list, bit, index) {
5856                        self.push_int(value);
5857                    } else {
5858                        self.push_default_for_ret(ret_form);
5859                    }
5860                } else {
5861                    let value = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
5862                    Self::set_user_int_list_value(&mut cell.int_list, bit, index, value);
5863                    self.push_default_for_ret(ret_form);
5864                }
5865                handled = true;
5866            } else if op.len() == 1 {
5867                match op[0] {
5868                    ELM_INTLIST_INIT => {
5869                        cell.form = decl_form;
5870                        cell.element = self.default_user_prop_element(prop_id, decl_form);
5871                        cell.int_list.clear();
5872                        cell.int_list.resize(decl_size, 0);
5873                        self.push_default_for_ret(ret_form);
5874                        handled = true;
5875                    }
5876                    ELM_INTLIST_RESIZE => {
5877                        let new_len =
5878                            args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5879                        cell.int_list.resize(new_len, 0);
5880                        self.push_default_for_ret(ret_form);
5881                        handled = true;
5882                    }
5883                    ELM_INTLIST_GET_SIZE => {
5884                        let multiplier = 32 / bit;
5885                        self.push_int((cell.int_list.len() * multiplier as usize) as i32);
5886                        handled = true;
5887                    }
5888                    ELM_INTLIST_CLEAR => {
5889                        let start = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
5890                        let end =
5891                            args.get(1).and_then(|v| v.as_i64()).unwrap_or(start as i64) as i32;
5892                        let value = if al_id == 0 {
5893                            0
5894                        } else {
5895                            args.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32
5896                        };
5897                        if start <= end {
5898                            for index in start..=end {
5899                                Self::set_user_int_list_value(
5900                                    &mut cell.int_list,
5901                                    bit,
5902                                    index,
5903                                    value,
5904                                );
5905                            }
5906                        }
5907                        self.push_default_for_ret(ret_form);
5908                        handled = true;
5909                    }
5910                    ELM_INTLIST_SETS => {
5911                        let mut index = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
5912                        for value in args.iter().skip(1) {
5913                            Self::set_user_int_list_value(
5914                                &mut cell.int_list,
5915                                bit,
5916                                index,
5917                                value.as_i64().unwrap_or(0) as i32,
5918                            );
5919                            index = index.saturating_add(1);
5920                        }
5921                        self.push_default_for_ret(ret_form);
5922                        handled = true;
5923                    }
5924                    _ => {}
5925                }
5926            }
5927        } else if form == self.cfg.fm_strlist {
5928            if sub.len() >= 2 && self.call_array_marker(sub[0]) {
5929                let index = sub[1];
5930                let value = usize::try_from(index)
5931                    .ok()
5932                    .and_then(|index| cell.str_list.get(index).cloned());
5933                if sub.len() == 2 {
5934                    if al_id == 0 {
5935                        if let Some(value) = value {
5936                            self.push_str(value);
5937                        } else {
5938                            self.push_default_for_ret(ret_form);
5939                        }
5940                    } else if let Ok(index) = usize::try_from(index) {
5941                        if let Some(slot) = cell.str_list.get_mut(index) {
5942                            *slot = args
5943                                .first()
5944                                .and_then(|v| v.as_str())
5945                                .unwrap_or("")
5946                                .to_string();
5947                        }
5948                        self.push_default_for_ret(ret_form);
5949                    } else {
5950                        self.push_default_for_ret(ret_form);
5951                    }
5952                } else if let Some(value) = value {
5953                    self.call_prop_eval_str_op(&value, sub[2], args, al_id)?;
5954                } else {
5955                    self.push_default_for_ret(ret_form);
5956                }
5957                handled = true;
5958            } else if sub.len() == 1 {
5959                match sub[0] {
5960                    ELM_STRLIST_INIT => {
5961                        cell.form = decl_form;
5962                        cell.element = self.default_user_prop_element(prop_id, decl_form);
5963                        cell.str_list.clear();
5964                        cell.str_list.resize_with(decl_size, String::new);
5965                        self.push_default_for_ret(ret_form);
5966                        handled = true;
5967                    }
5968                    ELM_STRLIST_RESIZE => {
5969                        let new_len =
5970                            args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
5971                        cell.str_list.resize_with(new_len, String::new);
5972                        self.push_default_for_ret(ret_form);
5973                        handled = true;
5974                    }
5975                    ELM_STRLIST_GET_SIZE => {
5976                        self.push_int(cell.str_list.len() as i32);
5977                        handled = true;
5978                    }
5979                    _ => {}
5980                }
5981            }
5982        }
5983
5984        self.user_props.insert(prop_id, cell);
5985        Ok(handled)
5986    }
5987
5988    fn exec_call_prop_command(
5989        &mut self,
5990        frame_idx: usize,
5991        prop_idx: usize,
5992        sub: &[i32],
5993        al_id: i32,
5994        ret_form: i32,
5995        args: &[Value],
5996    ) -> Result<()> {
5997        use crate::runtime::forms::codes::{
5998            ELM_ARRAY, ELM_INTLIST_BIT, ELM_INTLIST_BIT2, ELM_INTLIST_BIT4, ELM_INTLIST_BIT8,
5999            ELM_INTLIST_BIT16, ELM_INTLIST_CLEAR, ELM_INTLIST_GET_SIZE, ELM_INTLIST_INIT,
6000            ELM_INTLIST_RESIZE, ELM_INTLIST_SETS, ELM_STRLIST_GET_SIZE, ELM_STRLIST_INIT,
6001            ELM_STRLIST_RESIZE, FM_INT, FM_INTLIST, FM_INTLISTREF, FM_INTREF, FM_STR, FM_STRLIST,
6002            FM_STRLISTREF, FM_STRREF,
6003        };
6004
6005        let (form, decl_size, mut value, mut element) = {
6006            let prop = self
6007                .call_stack
6008                .get(frame_idx)
6009                .and_then(|f| f.user_props.get(prop_idx))
6010                .ok_or_else(|| anyhow!("call prop frame/index out of range"))?;
6011            (
6012                prop.form,
6013                prop.decl_size,
6014                prop.value.clone(),
6015                prop.element.clone(),
6016            )
6017        };
6018
6019        let mut write_back = false;
6020
6021        // Keep command-side CALL_PROP forwarding identical to property/assign
6022        // forwarding.  In particular, a marker-only `[ELM_ARRAY]` suffix is not
6023        // an indexed access: the original tnm_command_proc_prop() simply pushes
6024        // the referenced element for every *_REF form and does not dispatch an
6025        // indexless array chain.  The old duplicated composition below bypassed
6026        // compose_call_prop_tail() and produced targets such as [D, ELM_ARRAY].
6027        if let Some(composed) =
6028            self.compose_call_prop_tail(&self.call_stack[frame_idx].user_props[prop_idx], sub)
6029        {
6030            let mut owned_args = args.to_vec();
6031            self.exec_command(composed, al_id, ret_form, &mut owned_args)?;
6032            return Ok(());
6033        }
6034
6035        match form {
6036            FM_INT => {
6037                if sub.is_empty() {
6038                    if al_id == 0 {
6039                        match &value {
6040                            CallPropValue::Int(n) => self.push_int(*n),
6041                            _ => bail!("CALL_PROP int storage mismatch"),
6042                        }
6043                    } else {
6044                        let rhs = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
6045                        value = CallPropValue::Int(rhs);
6046                        write_back = true;
6047                        self.push_default_for_ret(ret_form);
6048                    }
6049                } else {
6050                    self.push_element(element.clone());
6051                }
6052            }
6053            FM_STR => {
6054                let current = match &value {
6055                    CallPropValue::Str(s) => s.clone(),
6056                    _ => bail!("CALL_PROP str storage mismatch"),
6057                };
6058                if sub.is_empty() {
6059                    if al_id == 0 {
6060                        self.push_str(current);
6061                    } else {
6062                        let rhs = args
6063                            .first()
6064                            .and_then(|v| v.as_str())
6065                            .unwrap_or("")
6066                            .to_string();
6067                        value = CallPropValue::Str(rhs);
6068                        write_back = true;
6069                        self.push_default_for_ret(ret_form);
6070                    }
6071                } else {
6072                    self.call_prop_eval_str_op(&current, sub[0], args, al_id)?;
6073                }
6074            }
6075            FM_INTLIST => {
6076                let mut list = match value {
6077                    CallPropValue::IntList(v) => v,
6078                    _ => bail!("CALL_PROP intlist storage mismatch"),
6079                };
6080                if sub.is_empty() {
6081                    self.push_element(element.clone());
6082                } else if sub.len() >= 2 && sub[0] == ELM_ARRAY {
6083                    let idx = sub[1].max(0) as usize;
6084                    if al_id == 0 {
6085                        self.push_int(list.get(idx).copied().unwrap_or(0));
6086                    } else {
6087                        let rhs = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
6088                        if list.len() <= idx {
6089                            list.resize(idx + 1, 0);
6090                        }
6091                        list[idx] = rhs;
6092                        write_back = true;
6093                        self.push_default_for_ret(ret_form);
6094                    }
6095                } else {
6096                    match sub[0] {
6097                        ELM_INTLIST_BIT | ELM_INTLIST_BIT2 | ELM_INTLIST_BIT4
6098                        | ELM_INTLIST_BIT8 | ELM_INTLIST_BIT16 => {
6099                            self.push_element(element.clone());
6100                        }
6101                        ELM_INTLIST_INIT => {
6102                            list.clear();
6103                            list.resize(decl_size, 0);
6104                            write_back = true;
6105                            self.push_default_for_ret(ret_form);
6106                        }
6107                        ELM_INTLIST_RESIZE => {
6108                            let new_len =
6109                                args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
6110                            list.resize(new_len, 0);
6111                            write_back = true;
6112                            self.push_default_for_ret(ret_form);
6113                        }
6114                        ELM_INTLIST_GET_SIZE => self.push_int(list.len() as i32),
6115                        ELM_INTLIST_CLEAR => {
6116                            let start =
6117                                args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
6118                            let end =
6119                                args.get(1).and_then(|v| v.as_i64()).unwrap_or(-1).max(-1) as isize;
6120                            let clear_value = if al_id == 0 {
6121                                0
6122                            } else {
6123                                args.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32
6124                            };
6125                            if !list.is_empty() && end >= 0 {
6126                                let end = usize::min(end as usize, list.len().saturating_sub(1));
6127                                for i in start..=end {
6128                                    if i < list.len() {
6129                                        list[i] = clear_value;
6130                                    }
6131                                }
6132                            }
6133                            write_back = true;
6134                            self.push_default_for_ret(ret_form);
6135                        }
6136                        ELM_INTLIST_SETS => {
6137                            let start =
6138                                args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
6139                            for (off, v) in args.iter().skip(1).enumerate() {
6140                                let idx = start + off;
6141                                if list.len() <= idx {
6142                                    list.resize(idx + 1, 0);
6143                                }
6144                                list[idx] = v.as_i64().unwrap_or(0) as i32;
6145                            }
6146                            write_back = true;
6147                            self.push_default_for_ret(ret_form);
6148                        }
6149                        _ => bail!("unsupported CALL_PROP intlist op {:?}", sub),
6150                    }
6151                }
6152                value = CallPropValue::IntList(list);
6153            }
6154            FM_STRLIST => {
6155                let mut list = match value {
6156                    CallPropValue::StrList(v) => v,
6157                    _ => bail!("CALL_PROP strlist storage mismatch"),
6158                };
6159                if sub.is_empty() {
6160                    self.push_element(element.clone());
6161                } else if sub.len() >= 2 && sub[0] == ELM_ARRAY {
6162                    let idx = sub[1].max(0) as usize;
6163                    if list.len() <= idx {
6164                        list.resize_with(idx + 1, String::new);
6165                    }
6166                    if sub.len() == 2 {
6167                        if al_id == 0 {
6168                            self.push_str(list[idx].clone());
6169                        } else {
6170                            let rhs = args
6171                                .first()
6172                                .and_then(|v| v.as_str())
6173                                .unwrap_or("")
6174                                .to_string();
6175                            list[idx] = rhs;
6176                            write_back = true;
6177                            self.push_default_for_ret(ret_form);
6178                        }
6179                    } else {
6180                        let current = list[idx].clone();
6181                        self.call_prop_eval_str_op(&current, sub[2], args, al_id)?;
6182                    }
6183                } else {
6184                    match sub[0] {
6185                        ELM_STRLIST_INIT => {
6186                            list.clear();
6187                            list.resize_with(decl_size, String::new);
6188                            write_back = true;
6189                            self.push_default_for_ret(ret_form);
6190                        }
6191                        ELM_STRLIST_RESIZE => {
6192                            let new_len =
6193                                args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
6194                            list.resize_with(new_len, String::new);
6195                            write_back = true;
6196                            self.push_default_for_ret(ret_form);
6197                        }
6198                        ELM_STRLIST_GET_SIZE => self.push_int(list.len() as i32),
6199                        _ => bail!("unsupported CALL_PROP strlist op {:?}", sub),
6200                    }
6201                }
6202                value = CallPropValue::StrList(list);
6203            }
6204            FM_INTREF | FM_STRREF | FM_INTLISTREF | FM_STRLISTREF => {
6205                if sub.is_empty() {
6206                    if al_id == 0 {
6207                        if let CallPropValue::Element(e) = &value {
6208                            if e.is_empty() {
6209                                self.push_element(element.clone());
6210                            } else {
6211                                self.push_element(e.clone());
6212                            }
6213                        } else {
6214                            self.push_element(element.clone());
6215                        }
6216                    } else {
6217                        let rhs = args.first().cloned().unwrap_or(Value::Element(Vec::new()));
6218                        match rhs {
6219                            Value::Element(e) => {
6220                                element = e.clone();
6221                                value = CallPropValue::Element(e);
6222                                write_back = true;
6223                            }
6224                            _ => bail!("CALL_PROP ref assign requires element"),
6225                        }
6226                        self.push_default_for_ret(ret_form);
6227                    }
6228                } else if let CallPropValue::Element(e) = &value {
6229                    if e.is_empty() {
6230                        self.push_element(element.clone());
6231                    } else {
6232                        self.push_element(e.clone());
6233                    }
6234                } else {
6235                    self.push_element(element.clone());
6236                }
6237            }
6238            _ => {
6239                if !sub.is_empty() || al_id == 0 {
6240                    self.push_element(element.clone());
6241                } else {
6242                    bail!("unsupported CALL_PROP form {}", form);
6243                }
6244            }
6245        }
6246
6247        if write_back {
6248            let prop = self
6249                .call_stack
6250                .get_mut(frame_idx)
6251                .and_then(|f| f.user_props.get_mut(prop_idx))
6252                .ok_or_else(|| anyhow!("call prop frame/index out of range"))?;
6253            prop.value = value;
6254            prop.element = element;
6255        }
6256        Ok(())
6257    }
6258
6259    fn exec_call_property(&mut self, elm: &[i32]) -> Result<bool> {
6260        vm_trace!(self, None, format!("exec_call_property elm={:?}", elm));
6261        use crate::runtime::forms::codes::{
6262            ELM_CALL_K, ELM_CALL_L, ELM_GLOBAL_CUR_CALL, ELM_INTLIST_GET_SIZE,
6263            ELM_STRLIST_GET_SIZE, FM_CALL, FM_CALLLIST,
6264        };
6265
6266        if elm.is_empty() {
6267            return Ok(false);
6268        }
6269        let head = elm[0];
6270        if head != FM_CALL && head != FM_CALLLIST && head != ELM_GLOBAL_CUR_CALL {
6271            return Ok(false);
6272        }
6273
6274        let current_idx = self
6275            .current_call_frame_index()
6276            .ok_or_else(|| anyhow!("call stack underflow"))?;
6277
6278        let tail: &[i32] = if head == FM_CALLLIST {
6279            if elm.len() < 3 || !self.call_array_marker(elm[1]) {
6280                bail!("malformed CALLLIST access: {:?}", elm);
6281            }
6282            self.resolve_call_frame_index(elm[2])
6283                .ok_or_else(|| anyhow!("CALLLIST index out of range: {}", elm[2]))?;
6284            &elm[3..]
6285        } else {
6286            &elm[1..]
6287        };
6288
6289        if tail.is_empty() {
6290            self.push_element(elm.to_vec());
6291            return Ok(true);
6292        }
6293
6294        match tail[0] {
6295            ELM_CALL_L => {
6296                let sub = &tail[1..];
6297                if sub.is_empty() {
6298                    self.push_element(elm.to_vec());
6299                } else if sub.len() >= 2 && self.call_array_marker(sub[0]) {
6300                    let idx = usize::try_from(sub[1]).ok();
6301                    let v = idx
6302                        .and_then(|idx| self.call_stack[current_idx].int_args.get(idx))
6303                        .copied()
6304                        .unwrap_or(0);
6305                    self.push_int(v);
6306                } else if sub[0] == ELM_INTLIST_GET_SIZE {
6307                    self.push_int(self.call_stack[current_idx].int_args.len() as i32);
6308                } else {
6309                    self.push_element(elm[..elm.len() - sub.len()].to_vec());
6310                }
6311                return Ok(true);
6312            }
6313            ELM_CALL_K => {
6314                let sub = &tail[1..];
6315                if sub.is_empty() {
6316                    self.push_element(elm.to_vec());
6317                } else if sub.len() >= 2 && self.call_array_marker(sub[0]) {
6318                    let idx = usize::try_from(sub[1]).ok();
6319                    let v = idx
6320                        .and_then(|idx| self.call_stack[current_idx].str_args.get(idx))
6321                        .cloned()
6322                        .unwrap_or_default();
6323                    if sub.len() == 2 {
6324                        self.push_str(v);
6325                    } else {
6326                        self.call_prop_eval_str_op(&v, sub[2], &[], 0)?;
6327                    }
6328                } else if sub[0] == ELM_STRLIST_GET_SIZE {
6329                    self.push_int(self.call_stack[current_idx].str_args.len() as i32);
6330                } else {
6331                    self.push_element(elm[..elm.len() - sub.len()].to_vec());
6332                }
6333                return Ok(true);
6334            }
6335            _ => {}
6336        }
6337
6338        if elm_code::owner(tail[0]) != elm_code::ELM_OWNER_CALL_PROP {
6339            bail!("invalid CALL property owner for {:?}", elm);
6340        }
6341
6342        let call_prop_id = elm_code::code(tail[0]) as i32;
6343        let prop_idx = self
6344            .find_call_prop_index_in_frame(current_idx, call_prop_id)
6345            .ok_or_else(|| anyhow!("missing CALL_PROP id={} for {:?}", call_prop_id, elm))?;
6346        let sub = &tail[1..];
6347        if self.push_call_prop_fast(current_idx, prop_idx, sub)? {
6348            return Ok(true);
6349        }
6350        let prop = self.call_stack[current_idx].user_props[prop_idx].clone();
6351        self.push_call_prop_result(&prop, sub, elm)?;
6352        Ok(true)
6353    }
6354
6355    fn exec_call_assign(&mut self, elm: &[i32], al_id: i32, rhs: Value) -> Result<bool> {
6356        use crate::runtime::forms::codes::{
6357            ELM_CALL_K, ELM_CALL_L, ELM_GLOBAL_CUR_CALL, FM_CALL, FM_CALLLIST,
6358        };
6359
6360        if elm.is_empty() {
6361            return Ok(false);
6362        }
6363        let head = elm[0];
6364        if head != FM_CALL && head != FM_CALLLIST && head != ELM_GLOBAL_CUR_CALL {
6365            return Ok(false);
6366        }
6367
6368        let current_idx = match self.current_call_frame_index() {
6369            Some(v) => v,
6370            None => return Ok(true),
6371        };
6372
6373        let tail: &[i32] = if head == FM_CALLLIST {
6374            if elm.len() < 3 || !self.call_array_marker(elm[1]) {
6375                bail!("malformed CALLLIST assign: {:?}", elm);
6376            }
6377            self.resolve_call_frame_index(elm[2])
6378                .ok_or_else(|| anyhow!("CALLLIST index out of range: {}", elm[2]))?;
6379            &elm[3..]
6380        } else {
6381            &elm[1..]
6382        };
6383
6384        if tail.is_empty() {
6385            return Ok(true);
6386        }
6387
6388        match tail[0] {
6389            ELM_CALL_L => {
6390                let sub = &tail[1..];
6391                if sub.len() >= 2
6392                    && self.call_array_marker(sub[0])
6393                    && let (Ok(idx), Value::Int(n)) = (usize::try_from(sub[1]), rhs)
6394                {
6395                    let len = self.call_stack[current_idx].int_args.len();
6396                    let old = self.call_stack[current_idx].int_args.get(idx).copied();
6397                    vm_trace!(
6398                        self,
6399                        None,
6400                        format!(
6401                            "CALL.L assign frame={} idx={} len={} old={:?} new={}",
6402                            current_idx, idx, len, old, n
6403                        ),
6404                    );
6405                    if let Some(slot) = self.call_stack[current_idx].int_args.get_mut(idx) {
6406                        *slot = n as i32;
6407                    }
6408                }
6409                return Ok(true);
6410            }
6411            ELM_CALL_K => {
6412                let sub = &tail[1..];
6413                if sub.len() >= 2
6414                    && self.call_array_marker(sub[0])
6415                    && let (Ok(idx), Value::Str(s)) = (usize::try_from(sub[1]), rhs)
6416                {
6417                    let len = self.call_stack[current_idx].str_args.len();
6418                    let old = self.call_stack[current_idx].str_args.get(idx).cloned();
6419                    vm_trace!(
6420                        self,
6421                        None,
6422                        format!(
6423                            "CALL.K assign frame={} idx={} len={} old={:?} new={:?}",
6424                            current_idx, idx, len, old, s
6425                        ),
6426                    );
6427                    if let Some(slot) = self.call_stack[current_idx].str_args.get_mut(idx) {
6428                        *slot = s;
6429                    }
6430                }
6431                return Ok(true);
6432            }
6433            _ => {}
6434        }
6435
6436        if elm_code::owner(tail[0]) != elm_code::ELM_OWNER_CALL_PROP {
6437            bail!("invalid CALL assign owner for {:?}", elm);
6438        }
6439        let call_prop_id = elm_code::code(tail[0]) as i32;
6440        let sub = &tail[1..];
6441        let prop_idx = self.ensure_call_prop_index_for_assign(current_idx, call_prop_id, &rhs)?;
6442        let prop_for_compose = self.call_stack[current_idx].user_props[prop_idx].clone();
6443        if let Some(composed) = self.compose_call_prop_tail(&prop_for_compose, sub) {
6444            self.exec_assign(composed, al_id, rhs)?;
6445            return Ok(true);
6446        }
6447
6448        if sub.is_empty()
6449            && matches!(
6450                prop_for_compose.form,
6451                crate::runtime::forms::codes::FM_INTREF | crate::runtime::forms::codes::FM_STRREF
6452            )
6453        {
6454            // Assignment to a scalar ref writes through to the referenced
6455            // element. Rebinding the CALL_PROP itself would lose the caller's
6456            // variable and contradict the compiler's STRREF/INTREF assignment
6457            // encoding (left form remains *REF, right form is scalar).
6458            let target = self.call_prop_effective_element(&prop_for_compose);
6459            let default_target = Self::call_prop_element(prop_for_compose.prop_id);
6460            if target.is_empty() || target == elm || target == default_target {
6461                bail!(
6462                    "unbound scalar CALL_PROP assignment form={} target={:?} access={:?}",
6463                    prop_for_compose.form,
6464                    target,
6465                    elm
6466                );
6467            }
6468            self.exec_assign(target, al_id, rhs)?;
6469            return Ok(true);
6470        }
6471
6472        let frame = &mut self.call_stack[current_idx];
6473        let prop = frame
6474            .user_props
6475            .get_mut(prop_idx)
6476            .ok_or_else(|| anyhow!("missing CALL_PROP slot assign id={}", call_prop_id))?;
6477        Self::assign_call_prop_result(prop, sub, rhs)?;
6478        Ok(true)
6479    }
6480
6481    fn exec_call_command(
6482        &mut self,
6483        elm: &[i32],
6484        al_id: i32,
6485        ret_form: i32,
6486        args: &[Value],
6487    ) -> Result<bool> {
6488        use crate::runtime::forms::codes::{
6489            ELM_CALL_K, ELM_CALL_L, ELM_GLOBAL_CUR_CALL, ELM_INTLIST_CLEAR, ELM_INTLIST_GET_SIZE,
6490            ELM_INTLIST_INIT, ELM_INTLIST_RESIZE, ELM_INTLIST_SETS, ELM_STRLIST_GET_SIZE,
6491            ELM_STRLIST_INIT, ELM_STRLIST_RESIZE, FM_CALL, FM_CALLLIST,
6492        };
6493
6494        if elm.is_empty() {
6495            return Ok(false);
6496        }
6497        let head = elm[0];
6498        if head != FM_CALL && head != FM_CALLLIST && head != ELM_GLOBAL_CUR_CALL {
6499            return Ok(false);
6500        }
6501
6502        let current_idx = match self.current_call_frame_index() {
6503            Some(v) => v,
6504            None => {
6505                self.push_default_for_ret(ret_form);
6506                return Ok(true);
6507            }
6508        };
6509
6510        let tail: &[i32] = if head == FM_CALLLIST {
6511            if elm.len() < 3 || !self.call_array_marker(elm[1]) {
6512                self.push_default_for_ret(ret_form);
6513                return Ok(true);
6514            }
6515            let Some(_selected_idx) = self.resolve_call_frame_index(elm[2]) else {
6516                self.push_default_for_ret(ret_form);
6517                return Ok(true);
6518            };
6519            &elm[3..]
6520        } else {
6521            &elm[1..]
6522        };
6523
6524        if tail.is_empty() {
6525            self.push_default_for_ret(ret_form);
6526            return Ok(true);
6527        }
6528
6529        match tail[0] {
6530            ELM_CALL_L => {
6531                let sub = &tail[1..];
6532                if sub.is_empty() {
6533                    self.push_default_for_ret(ret_form);
6534                    return Ok(true);
6535                }
6536                if sub.len() >= 2 && self.call_array_marker(sub[0]) {
6537                    let idx = usize::try_from(sub[1]).ok();
6538                    if al_id == 1 {
6539                        let rhs = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
6540                        if let Some(slot) =
6541                            idx.and_then(|idx| self.call_stack[current_idx].int_args.get_mut(idx))
6542                        {
6543                            *slot = rhs;
6544                        }
6545                        self.push_default_for_ret(ret_form);
6546                    } else {
6547                        let value = idx
6548                            .and_then(|idx| self.call_stack[current_idx].int_args.get(idx))
6549                            .copied()
6550                            .unwrap_or(0);
6551                        self.push_int(value);
6552                    }
6553                    return Ok(true);
6554                }
6555                match sub[0] {
6556                    ELM_INTLIST_INIT => {
6557                        let values = Self::blank_call_int_args(self.call_flag_count);
6558                        self.call_stack[current_idx].int_args = values;
6559                    }
6560                    ELM_INTLIST_RESIZE => {
6561                        let new_len =
6562                            args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
6563                        self.call_stack[current_idx].int_args.resize(new_len, 0);
6564                    }
6565                    ELM_INTLIST_GET_SIZE => {
6566                        self.push_int(self.call_stack[current_idx].int_args.len() as i32);
6567                    }
6568                    ELM_INTLIST_CLEAR => {
6569                        let start = args.first().and_then(|v| v.as_i64()).unwrap_or(0);
6570                        let end = args.get(1).and_then(|v| v.as_i64()).unwrap_or(start);
6571                        let value = if al_id == 0 {
6572                            0
6573                        } else {
6574                            args.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32
6575                        };
6576                        if start <= end {
6577                            let frame = &mut self.call_stack[current_idx];
6578                            for index in start..=end {
6579                                if let Ok(index) = usize::try_from(index)
6580                                    && let Some(slot) = frame.int_args.get_mut(index)
6581                                {
6582                                    *slot = value;
6583                                }
6584                            }
6585                        }
6586                    }
6587                    ELM_INTLIST_SETS => {
6588                        let start = args.first().and_then(|v| v.as_i64()).unwrap_or(0);
6589                        let frame = &mut self.call_stack[current_idx];
6590                        for (offset, value) in args.iter().skip(1).enumerate() {
6591                            let Some(index) = start.checked_add(offset as i64) else {
6592                                break;
6593                            };
6594                            if let Ok(index) = usize::try_from(index)
6595                                && let Some(slot) = frame.int_args.get_mut(index)
6596                            {
6597                                *slot = value.as_i64().unwrap_or(0) as i32;
6598                            }
6599                        }
6600                    }
6601                    _ => self.push_default_for_ret(ret_form),
6602                }
6603                Ok(true)
6604            }
6605            ELM_CALL_K => {
6606                let sub = &tail[1..];
6607                if sub.is_empty() {
6608                    self.push_default_for_ret(ret_form);
6609                    return Ok(true);
6610                }
6611                if sub.len() >= 2 && self.call_array_marker(sub[0]) {
6612                    let idx = usize::try_from(sub[1]).ok();
6613                    if sub.len() == 2 {
6614                        if al_id == 1 {
6615                            let rhs = args
6616                                .first()
6617                                .and_then(|v| v.as_str())
6618                                .unwrap_or("")
6619                                .to_string();
6620                            if let Some(slot) = idx
6621                                .and_then(|idx| self.call_stack[current_idx].str_args.get_mut(idx))
6622                            {
6623                                *slot = rhs;
6624                            }
6625                            self.push_default_for_ret(ret_form);
6626                        } else {
6627                            let value = idx
6628                                .and_then(|idx| self.call_stack[current_idx].str_args.get(idx))
6629                                .cloned()
6630                                .unwrap_or_default();
6631                            self.push_str(value);
6632                        }
6633                    } else {
6634                        let value = idx
6635                            .and_then(|idx| self.call_stack[current_idx].str_args.get(idx))
6636                            .cloned()
6637                            .unwrap_or_default();
6638                        self.call_prop_eval_str_op(&value, sub[2], args, al_id)?;
6639                    }
6640                    return Ok(true);
6641                }
6642                match sub[0] {
6643                    ELM_STRLIST_INIT => {
6644                        let values = Self::blank_call_str_args(self.call_flag_count);
6645                        self.call_stack[current_idx].str_args = values;
6646                    }
6647                    ELM_STRLIST_RESIZE => {
6648                        let new_len =
6649                            args.first().and_then(|v| v.as_i64()).unwrap_or(0).max(0) as usize;
6650                        self.call_stack[current_idx]
6651                            .str_args
6652                            .resize_with(new_len, String::new);
6653                    }
6654                    ELM_STRLIST_GET_SIZE => {
6655                        self.push_int(self.call_stack[current_idx].str_args.len() as i32);
6656                    }
6657                    _ => self.push_default_for_ret(ret_form),
6658                }
6659                Ok(true)
6660            }
6661            _ => {
6662                if elm_code::owner(tail[0]) == elm_code::ELM_OWNER_CALL_PROP {
6663                    let call_prop_id = elm_code::code(tail[0]) as i32;
6664                    let prop_idx = self
6665                        .find_call_prop_index_in_frame(current_idx, call_prop_id)
6666                        .ok_or_else(|| {
6667                            anyhow!(
6668                                "missing CALL_PROP command id={} for {:?}",
6669                                call_prop_id,
6670                                elm
6671                            )
6672                        })?;
6673                    self.exec_call_prop_command(
6674                        current_idx,
6675                        prop_idx,
6676                        &tail[1..],
6677                        al_id,
6678                        ret_form,
6679                        args,
6680                    )?;
6681                    return Ok(true);
6682                }
6683                bail!("unsupported CALL command chain {:?}", elm);
6684            }
6685        }
6686    }
6687
6688    fn push_property_value(&mut self, v: Value, array_idx: Option<usize>) {
6689        match v {
6690            Value::NamedArg { value, .. } => self.push_property_value(*value, array_idx),
6691            Value::Int(n) => self.push_int(n as i32),
6692            Value::Str(s) => self.push_str(s),
6693            Value::Element(elm) => self.push_element(elm),
6694            Value::List(items) => {
6695                if let Some(i) = array_idx {
6696                    if let Some(item) = items.get(i).cloned() {
6697                        self.push_property_value(item, None);
6698                    } else {
6699                        self.push_int(0);
6700                    }
6701                } else {
6702                    panic!("raw Value::List used as property result; expected runtime ref");
6703                }
6704            }
6705        }
6706    }
6707
6708    fn assign_user_prop(&mut self, prop_id: u16, array_idx: Option<usize>, rhs: Value) {
6709        let decl = self
6710            .user_prop_decl(prop_id)
6711            .unwrap_or((self.cfg.fm_list, 0));
6712        if let Some(i) = array_idx {
6713            let slot_element = self.default_user_prop_slot_element(prop_id, i);
6714            let default_entry = self.default_user_prop_cell(prop_id);
6715            let existing_form = self
6716                .user_props
6717                .get(&prop_id)
6718                .map(|e| e.form)
6719                .unwrap_or(default_entry.form);
6720            if existing_form == self.cfg.fm_intlist {
6721                let entry = self.user_props.entry(prop_id).or_insert(default_entry);
6722                if let Some(slot) = entry.int_list.get_mut(i) {
6723                    *slot = rhs.as_i64().unwrap_or(0) as i32;
6724                }
6725                return;
6726            }
6727            if existing_form == self.cfg.fm_strlist {
6728                let entry = self.user_props.entry(prop_id).or_insert(default_entry);
6729                if let Some(slot) = entry.str_list.get_mut(i) {
6730                    *slot = rhs.as_str().unwrap_or("").to_string();
6731                }
6732                return;
6733            }
6734            let list_form = self.cfg.fm_list;
6735            let new_slot =
6736                self.user_prop_cell_from_value(rhs, list_form, slot_element, Some(prop_id));
6737            let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
6738            let elm_array = self.ctx.ids.elm_array;
6739            let entry = self.user_props.entry(prop_id).or_insert(default_entry);
6740            if entry.list_items.len() <= i {
6741                let cur = entry.list_items.len();
6742                entry
6743                    .list_items
6744                    .resize_with(i + 1, || UserPropCell::new(list_form, Vec::new()));
6745                for idx in cur..entry.list_items.len() {
6746                    entry.list_items[idx].form = list_form;
6747                    entry.list_items[idx].element = vec![head, elm_array, idx as i32];
6748                }
6749            }
6750            entry.list_items[i] = new_slot;
6751        } else {
6752            let element = self.default_user_prop_element(prop_id, decl.0);
6753            let cell = self.user_prop_cell_from_value(rhs, decl.0, element, Some(prop_id));
6754            self.user_props.insert(prop_id, cell);
6755        }
6756    }
6757
6758    fn exec_copy_element(&mut self) -> Result<()> {
6759        let start = match self.element_points.last().copied() {
6760            Some(v) => v,
6761            None => {
6762                vm_trace!(self, None, "COPY_ELM missing prior ELM_POINT");
6763                return Err(anyhow!("COPY_ELM without a prior ELM_POINT"));
6764            }
6765        };
6766        if start > self.int_stack.len() {
6767            vm_trace!(
6768                self,
6769                None,
6770                format!(
6771                    "COPY_ELM invalid start={} len={}",
6772                    start,
6773                    self.int_stack.len()
6774                ),
6775            );
6776            bail!(
6777                "invalid element point start={start} len={}",
6778                self.int_stack.len()
6779            );
6780        }
6781        let end = self.int_stack.len();
6782        if self.sg_mwnd_object_trace_enabled()
6783            && Self::sg_mwnd_chain_interesting(&self.int_stack[start..])
6784        {
6785            self.sg_mwnd_object_trace_emit(format_args!(
6786                "COPY_ELM slice={:?} before_current_chain={:?} before_current_stage_object={:?}",
6787                &self.int_stack[start..],
6788                self.ctx.globals.current_object_chain,
6789                self.ctx.globals.current_stage_object
6790            ));
6791        }
6792        self.element_points.push(end);
6793        self.int_stack.extend_from_within(start..end);
6794        vm_trace!(
6795            self,
6796            None,
6797            format!("COPY_ELM copied {:?}", &self.int_stack[end..])
6798        );
6799        Ok(())
6800    }
6801
6802    fn pop_value_for_form(&mut self, form_code: i32) -> Result<Value> {
6803        if form_code == self.cfg.fm_void {
6804            return Ok(Value::Int(0));
6805        }
6806        if form_code == self.cfg.fm_int {
6807            return Ok(Value::Int(self.pop_int()? as i64));
6808        }
6809        if form_code == self.cfg.fm_str {
6810            return Ok(Value::Str(self.pop_str()?));
6811        }
6812        if form_code == self.cfg.fm_label {
6813            return Ok(Value::Int(self.pop_int()? as i64));
6814        }
6815        if form_code == self.cfg.fm_list {
6816            let nested = self.pop_arg_list()?;
6817            return Ok(Value::List(nested));
6818        }
6819
6820        // Unknown form: treat as element.
6821        self.trace_unknown_form(form_code, "pop_value_for_form");
6822        Ok(Value::Element(self.pop_element()?))
6823    }
6824
6825    fn pop_arg_list(&mut self) -> Result<Vec<Value>> {
6826        let arg_cnt_i32 = self.stream.pop_i32()?;
6827        if arg_cnt_i32 < 0 {
6828            bail!("negative arg_cnt={arg_cnt_i32}");
6829        }
6830        let arg_cnt = arg_cnt_i32 as usize;
6831        let mut out: Vec<Value> = vec![Value::Int(0); arg_cnt];
6832
6833        // The original fills from the end (stack pop order).
6834        for i in (0..arg_cnt).rev() {
6835            let form_code = self.stream.pop_i32()?;
6836            let v = self.pop_value_for_form(form_code)?;
6837            out[i] = v;
6838        }
6839        Ok(out)
6840    }
6841
6842    fn exec_push(&mut self, form_code: i32) -> Result<()> {
6843        if form_code == self.cfg.fm_void {
6844            return Ok(());
6845        }
6846        if form_code == self.cfg.fm_int {
6847            let v = self.stream.pop_i32()?;
6848            self.push_int(v);
6849            return Ok(());
6850        }
6851        if form_code == self.cfg.fm_str {
6852            let s = self.stream.pop_str()?;
6853            self.push_str(s);
6854            return Ok(());
6855        }
6856
6857        // Other forms are not pushed by CD_PUSH in the fork.
6858        self.trace_unknown_form(form_code, "exec_push");
6859        Ok(())
6860    }
6861
6862    fn exec_pop(&mut self, form_code: i32) -> Result<()> {
6863        if form_code == self.cfg.fm_void {
6864            return Ok(());
6865        }
6866        if form_code == self.cfg.fm_int {
6867            let _ = self.pop_int()?;
6868            return Ok(());
6869        }
6870        if form_code == self.cfg.fm_str {
6871            let _ = self.pop_str()?;
6872            return Ok(());
6873        }
6874
6875        self.trace_unknown_form(form_code, "exec_pop");
6876        Ok(())
6877    }
6878
6879    fn exec_copy(&mut self, form_code: i32) -> Result<()> {
6880        if form_code == self.cfg.fm_void {
6881            return Ok(());
6882        }
6883        if form_code == self.cfg.fm_int {
6884            let v = self.peek_int()?;
6885            self.push_int(v);
6886            return Ok(());
6887        }
6888        if form_code == self.cfg.fm_str {
6889            let s = self.peek_str()?;
6890            self.push_str(s);
6891            return Ok(());
6892        }
6893
6894        // Original CD_COPY only handles scalar INT/STR forms.
6895        self.trace_unknown_form(form_code, "exec_copy");
6896        Ok(())
6897    }
6898
6899    // ---------------------------------------------------------------------
6900    // Command/Property dispatch bridging
6901    // ---------------------------------------------------------------------
6902
6903    fn legacy_global_world_stage_chain(&self, elm: &[i32], stage_idx: i64) -> Option<Vec<i32>> {
6904        if elm.first().copied() != Some(LEGACY_GLOBAL_WORLD_ELEMENT) || elm.len() < 2 {
6905            return None;
6906        }
6907
6908        let elm_array = if self.ctx.ids.elm_array != 0 {
6909            self.ctx.ids.elm_array
6910        } else {
6911            crate::runtime::forms::codes::ELM_ARRAY
6912        };
6913        let is_array =
6914            |value: i32| value == elm_array || value == crate::runtime::forms::codes::ELM_ARRAY;
6915        let stage_form = if self.ctx.ids.form_global_stage != 0 {
6916            self.ctx.ids.form_global_stage as i32
6917        } else {
6918            crate::runtime::forms::codes::FORM_GLOBAL_STAGE as i32
6919        };
6920        let stage_world = if self.ctx.ids.stage_elm_world != 0 {
6921            self.ctx.ids.stage_elm_world
6922        } else {
6923            crate::runtime::forms::codes::STAGE_ELM_WORLD
6924        };
6925
6926        if is_array(elm[1]) {
6927            let world_idx = *elm.get(2)?;
6928            if world_idx < 0 {
6929                return None;
6930            }
6931            let mut out = vec![
6932                stage_form,
6933                elm_array,
6934                stage_idx as i32,
6935                stage_world,
6936                elm_array,
6937                world_idx,
6938            ];
6939            out.extend_from_slice(&elm[3..]);
6940            return Some(out);
6941        }
6942
6943        if elm.len() == 2
6944            && matches!(
6945                elm[1],
6946                crate::runtime::forms::codes::elm_value::WORLDLIST_CREATE_WORLD
6947                    | crate::runtime::forms::codes::elm_value::WORLDLIST_DESTROY_WORLD
6948            )
6949        {
6950            return Some(vec![
6951                stage_form,
6952                elm_array,
6953                stage_idx as i32,
6954                stage_world,
6955                elm[1],
6956            ]);
6957        }
6958
6959        None
6960    }
6961
6962    fn sync_legacy_global_worlds_from_front(&mut self) {
6963        const BACK_STAGE: i64 = 0;
6964        const FRONT_STAGE: i64 = 1;
6965
6966        let stage_form = if self.ctx.ids.form_global_stage != 0 {
6967            self.ctx.ids.form_global_stage
6968        } else {
6969            crate::runtime::forms::codes::FORM_GLOBAL_STAGE
6970        };
6971        let Some(stage) = self.ctx.globals.stage_forms.get_mut(&stage_form) else {
6972            return;
6973        };
6974        let Some(front_worlds) = stage.world_lists.get(&FRONT_STAGE).cloned() else {
6975            return;
6976        };
6977
6978        // Early native saves contain one global world list before BACK/FRONT.
6979        // Mirror that representation here: script-side GLOBAL.WORLD mutations
6980        // update the FRONT copy used for rendering and the BACK copy used by
6981        // stage transitions. NEXT remains transient and is populated by the
6982        // normal wipe/stage-copy path.
6983        stage.world_lists.insert(BACK_STAGE, front_worlds);
6984    }
6985
6986    fn canonical_runtime_form_id(&self, form_id: u32) -> u32 {
6987        let ids = &self.ctx.ids;
6988
6989        // EXCALL forwards its private STAGE as (global STAGE ^ 0x4000).
6990        // Dispatch it through the normal STAGE handler while keeping the raw
6991        // owner in ctx.vm_call.element[0], which stage::dispatch uses to pick
6992        // the correct storage container.
6993        if crate::runtime::forms::stage::is_stage_form_id(&self.ctx, form_id as i32) {
6994            return constants::global_form::STAGE_ALT;
6995        }
6996        if constants::matches_form_id(form_id, ids.form_global_mov, constants::global_form::MOV) {
6997            return constants::global_form::MOV;
6998        }
6999        if constants::matches_form_id(form_id, ids.form_global_bgm, constants::global_form::BGM) {
7000            return constants::global_form::BGM;
7001        }
7002        if constants::matches_form_id(
7003            form_id,
7004            ids.form_global_bgm_table,
7005            constants::global_form::BGMTABLE,
7006        ) {
7007            return constants::global_form::BGMTABLE;
7008        }
7009        if constants::matches_form_id(form_id, ids.form_global_math, constants::global_form::MATH) {
7010            return constants::global_form::MATH;
7011        }
7012        if constants::matches_form_id(form_id, ids.form_global_pcm, constants::global_form::PCM) {
7013            return constants::global_form::PCM;
7014        }
7015        if constants::matches_form_id(
7016            form_id,
7017            ids.form_global_pcmch,
7018            constants::global_form::PCMCH,
7019        ) {
7020            return constants::global_form::PCMCH;
7021        }
7022        if constants::matches_form_id(form_id, ids.form_global_se, constants::global_form::SE) {
7023            return constants::global_form::SE;
7024        }
7025        if constants::matches_form_id(
7026            form_id,
7027            ids.form_global_pcm_event,
7028            constants::global_form::PCMEVENT,
7029        ) {
7030            return constants::global_form::PCMEVENT;
7031        }
7032        if constants::matches_form_id(
7033            form_id,
7034            ids.form_global_excall,
7035            constants::global_form::EXCALL,
7036        ) {
7037            return constants::global_form::EXCALL;
7038        }
7039        if constants::matches_form_id(
7040            form_id,
7041            ids.form_global_screen,
7042            constants::global_form::SCREEN,
7043        ) {
7044            return constants::global_form::SCREEN;
7045        }
7046        if constants::matches_form_id(
7047            form_id,
7048            ids.form_global_msgbk,
7049            constants::global_form::MSGBK,
7050        ) {
7051            return constants::global_form::MSGBK;
7052        }
7053        if constants::matches_form_id(
7054            form_id,
7055            ids.form_global_koe_st,
7056            constants::global_form::KOE_ST,
7057        ) {
7058            return constants::global_form::KOE_ST;
7059        }
7060        if constants::matches_form_id(form_id, ids.form_global_key, constants::global_form::KEY) {
7061            return constants::global_form::KEY;
7062        }
7063        if form_id == constants::global_form::COUNTER {
7064            return constants::global_form::COUNTER;
7065        }
7066        if constants::matches_form_id(
7067            form_id,
7068            ids.form_global_frame_action,
7069            constants::global_form::FRAME_ACTION,
7070        ) {
7071            return constants::global_form::FRAME_ACTION;
7072        }
7073        if form_id == constants::global_form::TIMEWAIT {
7074            return constants::global_form::TIMEWAIT;
7075        }
7076        if form_id == constants::global_form::TIMEWAIT_KEY {
7077            return constants::global_form::TIMEWAIT_KEY;
7078        }
7079
7080        form_id
7081    }
7082
7083    #[inline(always)]
7084    fn sg_mwnd_object_trace_enabled(&self) -> bool {
7085        self.runtime_options.sg_debug
7086    }
7087
7088    fn sg_mwnd_object_trace_emit(&self, msg: impl std::fmt::Display) {
7089        eprintln!("[SG_DEBUG][MWND_OBJECT_TRACE][VM] {}", msg);
7090    }
7091
7092    fn sg_mwnd_chain_interesting(elm: &[i32]) -> bool {
7093        elm.iter().any(|v| {
7094            *v == crate::runtime::forms::codes::STAGE_ELM_MWND
7095                || *v == crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM
7096                || *v == crate::runtime::forms::codes::elm_value::MWND_OBJECT
7097                || *v == crate::runtime::forms::codes::elm_value::MWND_BUTTON
7098                || *v == crate::runtime::forms::codes::elm_value::MWND_FACE
7099                || *v == crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT
7100                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CHILD
7101                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CREATE
7102                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CREATE_RECT
7103                || *v == crate::runtime::forms::codes::elm_value::OBJECT_CREATE_STRING
7104                || *v == crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION
7105                || *v == crate::runtime::forms::codes::elm_value::OBJECT_FRAME_ACTION_CH
7106        })
7107    }
7108
7109    fn is_global_indexed_list_head(&self, head: i32) -> bool {
7110        if head < 0 {
7111            return false;
7112        }
7113        let head = head as u32;
7114        crate::runtime::constants::global_form::INT_LIST_FORMS.contains(&head)
7115            || crate::runtime::constants::global_form::STR_LIST_FORMS.contains(&head)
7116    }
7117
7118    fn is_global_indexed_list_chain(&self, elm: &[i32]) -> bool {
7119        if elm.len() < 3 || !self.is_global_indexed_list_head(elm[0]) {
7120            return false;
7121        }
7122        elm[1] == self.ctx.ids.elm_array || elm[1] == crate::runtime::forms::codes::ELM_ARRAY
7123    }
7124
7125    fn current_object_chain_has_child_index(&self, child_idx: i32) -> bool {
7126        if child_idx < 0 {
7127            return false;
7128        }
7129        let Some(chain) = self.ctx.globals.current_object_chain.as_ref() else {
7130            return false;
7131        };
7132        let Some(raw_stage_form) = chain.first().copied() else {
7133            return false;
7134        };
7135        let stage_form = raw_stage_form;
7136        if !crate::runtime::forms::stage::is_stage_form_id(&self.ctx, stage_form) {
7137            return false;
7138        }
7139        let stage_form = crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, stage_form);
7140        let elm_array = if self.ctx.ids.elm_array != 0 {
7141            self.ctx.ids.elm_array
7142        } else {
7143            crate::runtime::forms::codes::ELM_ARRAY
7144        };
7145        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
7146            self.ctx.ids.stage_elm_object
7147        } else {
7148            crate::runtime::forms::codes::STAGE_ELM_OBJECT
7149        };
7150        if chain.len() < 6
7151            || crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, chain[0])
7152                != stage_form
7153            || chain[1] != elm_array
7154            || chain[2] < 0
7155        {
7156            return false;
7157        }
7158
7159        let stage_idx = chain[2] as i64;
7160        let Some(stage_state) = self.ctx.globals.stage_forms.get(&stage_form) else {
7161            return false;
7162        };
7163
7164        fn descend_child_chain<'a>(
7165            mut obj: &'a crate::runtime::globals::ObjectState,
7166            chain: &[i32],
7167            mut pos: usize,
7168            elm_array: i32,
7169        ) -> Option<&'a crate::runtime::globals::ObjectState> {
7170            let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
7171            while pos + 2 < chain.len() {
7172                if chain[pos] == object_child && chain[pos + 1] == elm_array && chain[pos + 2] >= 0
7173                {
7174                    let idx = chain[pos + 2] as usize;
7175                    obj = obj.runtime.child_objects.get(idx)?;
7176                    pos += 3;
7177                } else {
7178                    break;
7179                }
7180            }
7181            Some(obj)
7182        }
7183
7184        let current_obj = (|| -> Option<&crate::runtime::globals::ObjectState> {
7185            if chain[3] == stage_object {
7186                if chain[4] != elm_array || chain[5] < 0 {
7187                    return None;
7188                }
7189                let top_idx = chain[5] as usize;
7190                let list = stage_state.object_lists.get(&stage_idx)?;
7191                let obj = list.get(top_idx)?;
7192                descend_child_chain(obj, chain, 6, elm_array)
7193            } else if chain[3] == crate::runtime::forms::codes::STAGE_ELM_MWND {
7194                if chain.len() < 9
7195                    || chain[4] != elm_array
7196                    || chain[5] < 0
7197                    || chain[7] != elm_array
7198                    || chain[8] < 0
7199                {
7200                    return None;
7201                }
7202                let mwnd_idx = chain[5] as usize;
7203                let selector = chain[6];
7204                let obj_idx = chain[8] as usize;
7205                let mwnds = stage_state.mwnd_lists.get(&stage_idx)?;
7206                let mwnd = mwnds.get(mwnd_idx)?;
7207                let list = if selector == constants::MWND_BUTTON {
7208                    &mwnd.button_list
7209                } else if selector == constants::MWND_FACE {
7210                    &mwnd.face_list
7211                } else if selector == constants::MWND_OBJECT {
7212                    &mwnd.object_list
7213                } else {
7214                    return None;
7215                };
7216                let obj = list.get(obj_idx)?;
7217                descend_child_chain(obj, chain, 9, elm_array)
7218            } else if chain[3] == crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM {
7219                if chain.len() < 9
7220                    || chain[4] != elm_array
7221                    || chain[5] < 0
7222                    || chain[7] != elm_array
7223                    || chain[8] < 0
7224                {
7225                    return None;
7226                }
7227                if chain[6] != crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT {
7228                    return None;
7229                }
7230                let item_idx = chain[5] as usize;
7231                let obj_idx = chain[8] as usize;
7232                let items = stage_state.btnselitem_lists.get(&stage_idx)?;
7233                let item = items.get(item_idx)?;
7234                let obj = item.object_list.get(obj_idx)?;
7235                descend_child_chain(obj, chain, 9, elm_array)
7236            } else {
7237                None
7238            }
7239        })();
7240
7241        let Some(current_obj) = current_obj else {
7242            return false;
7243        };
7244        (child_idx as usize) < current_obj.runtime.child_objects.len()
7245    }
7246
7247    fn object_array_property_op(&self, op: i32) -> bool {
7248        let ids = &self.ctx.ids;
7249        op == crate::runtime::forms::codes::elm_value::OBJECT_CHILD
7250            || (ids.obj_x_rep != 0 && op == ids.obj_x_rep)
7251            || (ids.obj_y_rep != 0 && op == ids.obj_y_rep)
7252            || (ids.obj_z_rep != 0 && op == ids.obj_z_rep)
7253            || (ids.obj_tr_rep != 0 && op == ids.obj_tr_rep)
7254            || (ids.obj_f != 0 && op == ids.obj_f)
7255            || (ids.obj_frame_action_ch != 0 && op == ids.obj_frame_action_ch)
7256    }
7257
7258    fn is_current_object_child_tail(&self, elm: &[i32]) -> bool {
7259        if elm.len() < 2 {
7260            return false;
7261        }
7262        if elm[0] < 0 {
7263            return false;
7264        }
7265        if elm[1] != self.ctx.ids.elm_array && elm[1] != crate::runtime::forms::codes::ELM_ARRAY {
7266            return false;
7267        }
7268        if self.object_array_property_op(elm[0]) {
7269            return false;
7270        }
7271        if elm.len() == 2 {
7272            return true;
7273        }
7274        if self.object_array_property_op(elm[2]) {
7275            return elm[2] == crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
7276        }
7277        elm[2] == self.ctx.ids.elm_array
7278            || elm[2] == crate::runtime::forms::codes::ELM_ARRAY
7279            || elm[2] == crate::runtime::forms::codes::ELM_UP
7280            || self.compact_object_op_allowed(elm[2])
7281    }
7282
7283    fn global_indexed_list_must_dispatch_direct(&self, elm: &[i32]) -> bool {
7284        // cmd_global.cpp resolves explicit A..G/X/Z/S/M list roots before
7285        // object dispatch. Ambient children cannot change B[43], for example,
7286        // into child 26's property 43 when a gallery creates a large container.
7287        self.is_global_indexed_list_chain(elm)
7288    }
7289
7290    fn dispatch_global_indexed_list_property_direct(&mut self, elm: &[i32]) -> Result<bool> {
7291        if !self.global_indexed_list_must_dispatch_direct(elm) {
7292            return Ok(false);
7293        }
7294        let form_id = elm[0] as u32;
7295        self.ctx.vm_call = Some(runtime::VmCallMeta {
7296            element: elm.to_vec(),
7297            al_id: 0,
7298            ret_form: self.cfg.fm_int as i64,
7299        });
7300        if !runtime::dispatch_form_code(&mut self.ctx, form_id, &[])? {
7301            self.ctx.vm_call = None;
7302            bail!("unhandled global indexed-list property chain {:?}", elm);
7303        }
7304        self.ctx.vm_call = None;
7305        if let Some(v) = self.ctx.pop() {
7306            self.push_return_value_raw(v);
7307        } else {
7308            bail!("global indexed-list property returned no value: {:?}", elm);
7309        }
7310        Ok(true)
7311    }
7312
7313    fn dispatch_global_indexed_list_assign_direct(
7314        &mut self,
7315        elm: &[i32],
7316        al_id: i32,
7317        rhs: Value,
7318    ) -> Result<bool> {
7319        if !self.global_indexed_list_must_dispatch_direct(elm) {
7320            return Ok(false);
7321        }
7322        let form_id = elm[0] as u32;
7323        let args: Vec<Value> = vec![rhs];
7324        self.ctx.vm_call = Some(runtime::VmCallMeta {
7325            element: elm.to_vec(),
7326            al_id: al_id as i64,
7327            ret_form: 0,
7328        });
7329        if !runtime::dispatch_form_code(&mut self.ctx, form_id, &args)? {
7330            self.ctx.vm_call = None;
7331            bail!("unhandled global indexed-list assignment chain {:?}", elm);
7332        }
7333        self.ctx.vm_call = None;
7334        self.ctx.stack.clear();
7335        self.drain_pending_frame_action_finishes()?;
7336        Ok(true)
7337    }
7338
7339    fn dispatch_global_indexed_list_command_direct(
7340        &mut self,
7341        elm: &[i32],
7342        al_id: i32,
7343        ret_form: i32,
7344        args: &mut [Value],
7345    ) -> Result<bool> {
7346        if !self.global_indexed_list_must_dispatch_direct(elm) {
7347            return Ok(false);
7348        }
7349        let form_id = elm[0] as u32;
7350        self.ctx.vm_call = Some(runtime::VmCallMeta {
7351            element: elm.to_vec(),
7352            al_id: al_id as i64,
7353            ret_form: ret_form as i64,
7354        });
7355        if !runtime::dispatch_form_code(&mut self.ctx, form_id, args)? {
7356            self.ctx.vm_call = None;
7357            bail!("unhandled global indexed-list command chain {:?}", elm);
7358        }
7359        self.ctx.vm_call = None;
7360        self.drain_pending_frame_action_finishes()?;
7361        if ret_form != self.cfg.fm_void {
7362            self.take_ctx_return(ret_form)?;
7363        } else {
7364            self.ctx.stack.clear();
7365        }
7366        Ok(true)
7367    }
7368
7369    fn try_parent_slot_property(&mut self, elm: &[i32]) -> bool {
7370        if elm.len() != 3 || elm[1] != self.ctx.ids.elm_array || elm[2] <= 0 {
7371            return false;
7372        }
7373        // Siglus object-child shorthand reuses the same compact `[slot, ELM_ARRAY, parent]`
7374        // shape as generic parent-slot access. When we are already inside an object chain,
7375        // prefer the object child interpretation so title/menu patno updates like
7376        // `front.object[0].[29] = ...` continue to drive the actual child objects instead
7377        // of disappearing into the generic parent-form property bags.
7378        if self.ctx.globals.current_object_chain.is_some() && self.compact_object_op_allowed(elm[0])
7379        {
7380            return false;
7381        }
7382        let parent_form = elm[2] as u32;
7383        let slot = elm[0];
7384        let ret_form = self
7385            .ctx
7386            .vm_call
7387            .as_ref()
7388            .map(|m| m.ret_form)
7389            .unwrap_or(self.cfg.fm_int as i64);
7390        if ret_form == self.cfg.fm_str as i64 {
7391            let value = self
7392                .ctx
7393                .globals
7394                .str_props
7395                .get(&parent_form)
7396                .and_then(|m| m.get(&slot))
7397                .cloned()
7398                .unwrap_or_default();
7399            self.push_str(value);
7400        } else if let Some(value) = self
7401            .ctx
7402            .globals
7403            .str_props
7404            .get(&parent_form)
7405            .and_then(|m| m.get(&slot))
7406            .cloned()
7407        {
7408            self.push_str(value);
7409        } else {
7410            let value = self
7411                .ctx
7412                .globals
7413                .int_props
7414                .get(&parent_form)
7415                .and_then(|m| m.get(&slot).copied())
7416                .unwrap_or(0);
7417            self.push_int(value as i32);
7418        }
7419        true
7420    }
7421
7422    fn compact_object_op_allowed(&self, op: i32) -> bool {
7423        (0..=187).contains(&op)
7424    }
7425
7426    fn compact_object_op_allowed_for_element(
7427        &self,
7428        elm: &[i32],
7429        allow_ambiguous_single_token_object_op: bool,
7430    ) -> bool {
7431        let Some(op) = elm.first().copied() else {
7432            return false;
7433        };
7434        if elm.len() == 1 && !allow_ambiguous_single_token_object_op {
7435            return false;
7436        }
7437        self.compact_object_op_allowed(op)
7438    }
7439
7440    fn current_object_has_child_index(&self, child_idx: i32) -> bool {
7441        if self.current_object_chain_has_child_index(child_idx) {
7442            return true;
7443        }
7444        if child_idx < 0 {
7445            return false;
7446        }
7447        let Some((stage_idx, obj_idx)) = self.ctx.globals.current_stage_object else {
7448            return false;
7449        };
7450        let stage_form = self
7451            .ctx
7452            .globals
7453            .current_object_chain
7454            .as_ref()
7455            .and_then(|chain| chain.first().copied())
7456            .filter(|form| crate::runtime::forms::stage::is_stage_form_id(&self.ctx, *form))
7457            .map(|form| crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, form))
7458            .unwrap_or_else(|| crate::runtime::forms::stage::current_stage_form_id(&self.ctx));
7459        let Some(stage_state) = self.ctx.globals.stage_forms.get(&stage_form) else {
7460            return false;
7461        };
7462        let Some(list) = stage_state.object_lists.get(&stage_idx) else {
7463            return false;
7464        };
7465        let Some(obj) = list.get(obj_idx) else {
7466            return false;
7467        };
7468        (child_idx as usize) < obj.runtime.child_objects.len()
7469    }
7470
7471    fn try_compact_object_chain(
7472        &self,
7473        elm: &[i32],
7474        allow_ambiguous_single_token_object_op: bool,
7475    ) -> Option<Vec<i32>> {
7476        if elm.is_empty() {
7477            return None;
7478        }
7479
7480        let op = elm[0];
7481        if !self.compact_object_op_allowed_for_element(elm, allow_ambiguous_single_token_object_op)
7482        {
7483            return None;
7484        }
7485
7486        let elm_array = if self.ctx.ids.elm_array != 0 {
7487            self.ctx.ids.elm_array
7488        } else {
7489            crate::runtime::forms::codes::ELM_ARRAY
7490        };
7491        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
7492            self.ctx.ids.stage_elm_object
7493        } else {
7494            crate::runtime::forms::codes::STAGE_ELM_OBJECT
7495        };
7496
7497        let looks_like_absolute_stage_alias_object = elm.len() >= 4
7498            && constants::is_stage_global_form(elm[0] as u32, self.ctx.ids.form_global_stage)
7499            && elm[1] == stage_object
7500            && (elm[2] == elm_array || elm[2] == crate::runtime::forms::codes::ELM_ARRAY);
7501
7502        if looks_like_absolute_stage_alias_object {
7503            return None;
7504        }
7505
7506        // EXCALL.STAGE[index] starts with [65, 0, ARRAY, index], which also
7507        // matches the compact OBJECT.Z_EVE layout. Preserve the explicit
7508        // EXCALL chain so menu objects and button groups use its private stage.
7509        if elm.len() >= 4
7510            && constants::matches_form_id(
7511                elm[0] as u32,
7512                self.ctx.ids.form_global_excall,
7513                constants::global_form::EXCALL,
7514            )
7515            && elm[1] == crate::runtime::forms::codes::ELM_EXCALL_STAGE
7516            && (elm[2] == elm_array || elm[2] == crate::runtime::forms::codes::ELM_ARRAY)
7517        {
7518            return None;
7519        }
7520
7521        // Original command dispatch receives the complete element chain.  The
7522        // only compact form we keep for an ambient object context is the
7523        // explicit child shorthand used after an already-resolved OBJECT.  Do
7524        // not append arbitrary OBJECT op ids to current_object_chain here:
7525        // many unrelated forms share the same numeric element values.
7526        if let Some(prefix) = &self.ctx.globals.current_object_chain
7527            && self.is_current_object_child_tail(elm)
7528            && self.current_object_has_child_index(elm[0])
7529        {
7530            let mut synthetic = prefix.clone();
7531            synthetic.push(crate::runtime::forms::codes::elm_value::OBJECT_CHILD);
7532            synthetic.push(elm_array);
7533            synthetic.push(elm[0]);
7534            if elm.len() > 2 {
7535                if elm[2] == crate::runtime::forms::codes::elm_value::OBJECT_CHILD {
7536                    synthetic.extend_from_slice(&elm[3..]);
7537                } else {
7538                    synthetic.extend_from_slice(&elm[2..]);
7539                }
7540            }
7541            if self.sg_mwnd_object_trace_enabled()
7542                && (Self::sg_mwnd_chain_interesting(elm)
7543                    || Self::sg_mwnd_chain_interesting(&synthetic))
7544            {
7545                eprintln!(
7546                    "[SG_DEBUG][MWND_OBJECT_TRACE][VM] try_compact child-shorthand elm={:?} prefix={:?} synthetic={:?}",
7547                    elm, prefix, synthetic
7548                );
7549            }
7550            return Some(synthetic);
7551        }
7552
7553        // Explicit compact absolute form: [object_op, stage_no, ARRAY, obj_no, ...]
7554        // This still carries both the stage and object index, so it is not an
7555        // ambient-context guess.
7556        if elm.len() >= 4
7557            && elm[1] >= 0
7558            && (elm[2] == elm_array || elm[2] == crate::runtime::forms::codes::ELM_ARRAY)
7559            && elm[3] >= 0
7560        {
7561            let stage_idx = elm[1];
7562            if !(0..3).contains(&stage_idx) {
7563                return None;
7564            }
7565            let stage_form = crate::runtime::forms::stage::current_stage_form_id(&self.ctx) as i32;
7566            let mut synthetic = vec![
7567                stage_form,
7568                elm_array,
7569                stage_idx,
7570                stage_object,
7571                elm_array,
7572                elm[3],
7573                op,
7574            ];
7575            if elm.len() > 4 {
7576                synthetic.extend_from_slice(&elm[4..]);
7577            }
7578            if self.sg_mwnd_object_trace_enabled()
7579                && (Self::sg_mwnd_chain_interesting(elm)
7580                    || Self::sg_mwnd_chain_interesting(&synthetic))
7581            {
7582                eprintln!(
7583                    "[SG_DEBUG][MWND_OBJECT_TRACE][VM] try_compact absolute elm={:?} synthetic={:?}",
7584                    elm, synthetic
7585                );
7586            }
7587            return Some(synthetic);
7588        }
7589
7590        None
7591    }
7592
7593    fn try_parent_slot_assign(&mut self, elm: &[i32], rhs: &Value) -> bool {
7594        if elm.len() != 3 || elm[1] != self.ctx.ids.elm_array || elm[2] <= 0 {
7595            return false;
7596        }
7597        // See try_parent_slot_property(): inside object chains this compact syntax is used for
7598        // object child operations, not generic parent-form slots.
7599        if self.ctx.globals.current_object_chain.is_some() && self.compact_object_op_allowed(elm[0])
7600        {
7601            return false;
7602        }
7603        let parent_form = elm[2] as u32;
7604        let slot = elm[0];
7605        match rhs {
7606            Value::Str(s) => {
7607                self.ctx
7608                    .globals
7609                    .str_props
7610                    .entry(parent_form)
7611                    .or_default()
7612                    .insert(slot, s.clone());
7613            }
7614            Value::Int(n) => {
7615                self.ctx
7616                    .globals
7617                    .int_props
7618                    .entry(parent_form)
7619                    .or_default()
7620                    .insert(slot, *n);
7621            }
7622            Value::NamedArg { value, .. } => return self.try_parent_slot_assign(elm, value),
7623            _ => return false,
7624        }
7625        true
7626    }
7627
7628    fn exec_property(&mut self, mut elm: Vec<i32>) -> Result<()> {
7629        if self.runtime_options.title_chain_trace
7630            && self.current_scene_name.as_deref() == Some("sys10_tt01")
7631            && matches!(elm.first().copied(), Some(83 | 84 | 24 | 25))
7632        {
7633            eprintln!(
7634                "[SG_TITLE_CHAIN_TRACE] line={} elm={:?} current_object_chain={:?} current_stage_object={:?}",
7635                self.current_line_no,
7636                elm,
7637                self.ctx.globals.current_object_chain,
7638                self.ctx.globals.current_stage_object
7639            );
7640        }
7641        vm_trace!(self, None, format!("exec_property enter elm={:?}", elm));
7642        if elm.is_empty() {
7643            self.push_int(0);
7644            return Ok(());
7645        }
7646        // Call-local properties (declared by CD_DEC_PROP / populated by CD_ARG).
7647        if self.exec_call_property(&elm)? {
7648            vm_trace!(
7649                self,
7650                None,
7651                format!("exec_property handled by call-property elm={:?}", elm),
7652            );
7653            return Ok(());
7654        }
7655
7656        let head = elm[0];
7657        let head_owner = elm_code::owner(head);
7658        if head_owner == elm_code::ELM_OWNER_CALL_PROP {
7659            let current_idx = self
7660                .current_call_frame_index()
7661                .ok_or_else(|| anyhow!("call stack underflow"))?;
7662            let call_prop_id = elm_code::code(head) as i32;
7663            let prop_idx = self
7664                .find_call_prop_index_in_frame(current_idx, call_prop_id)
7665                .ok_or_else(|| {
7666                    anyhow!("missing direct CALL_PROP id={} for {:?}", call_prop_id, elm)
7667                })?;
7668            if self.push_call_prop_fast(current_idx, prop_idx, &elm[1..])? {
7669                vm_trace!(
7670                    self,
7671                    None,
7672                    format!("exec_property direct CALL_PROP fast elm={:?}", elm),
7673                );
7674                return Ok(());
7675            }
7676            let prop = self.call_stack[current_idx].user_props[prop_idx].clone();
7677            self.push_call_prop_result(&prop, &elm[1..], &elm)?;
7678            vm_trace!(
7679                self,
7680                None,
7681                format!("exec_property direct CALL_PROP elm={:?}", elm),
7682            );
7683            return Ok(());
7684        }
7685
7686        if head_owner == elm_code::ELM_OWNER_USER_PROP {
7687            let prop_id = elm_code::code(head);
7688            let cell = self
7689                .user_props
7690                .get(&prop_id)
7691                .cloned()
7692                .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
7693            let array_idx = self.extract_array_index(&elm);
7694            self.trace_cf_condition_user_prop_read(
7695                self.stream.get_prg_cntr(),
7696                prop_id,
7697                array_idx,
7698                &cell,
7699                &elm,
7700            );
7701            self.push_user_prop_cell_result(&cell, &elm[1..], &elm)?;
7702            vm_trace!(
7703                self,
7704                None,
7705                format!("exec_property direct USER_PROP elm={:?}", elm),
7706            );
7707            return Ok(());
7708        }
7709
7710        if head_owner != elm_code::ELM_OWNER_FORM {
7711            bail!(
7712                "unsupported property owner {} for element {:?}",
7713                head_owner,
7714                elm
7715            );
7716        }
7717
7718        if let Some(synthetic) = self.legacy_global_world_stage_chain(&elm, 1) {
7719            self.exec_property(synthetic)?;
7720            return Ok(());
7721        }
7722
7723        if self.dispatch_global_indexed_list_property_direct(&elm)? {
7724            vm_trace!(
7725                self,
7726                None,
7727                format!("exec_property handled by global indexed-list elm={:?}", elm)
7728            );
7729            return Ok(());
7730        }
7731
7732        if self.try_parent_slot_property(&elm) {
7733            vm_trace!(
7734                self,
7735                None,
7736                format!("exec_property handled by parent-slot elm={:?}", elm),
7737            );
7738            return Ok(());
7739        }
7740        if let Some(synthetic) = self.try_compact_object_chain(&elm, false) {
7741            vm_trace!(
7742                self,
7743                None,
7744                format!(
7745                    "exec_property compact-object elm={:?} synthetic={:?}",
7746                    elm, synthetic
7747                ),
7748            );
7749            self.ctx.vm_call = Some(runtime::VmCallMeta {
7750                element: synthetic.clone(),
7751                al_id: 0,
7752                ret_form: self.cfg.fm_int as i64,
7753            });
7754            let form_id = self.canonical_runtime_form_id(synthetic[0] as u32);
7755            if !runtime::dispatch_form_code(&mut self.ctx, form_id, &[])? {
7756                self.ctx.vm_call = None;
7757                bail!(
7758                    "unhandled compact object property chain {:?} -> {:?}",
7759                    elm,
7760                    synthetic
7761                );
7762            }
7763            self.ctx.vm_call = None;
7764            self.update_compact_context_from_object_dispatch_chain(&synthetic);
7765            if let Some(v) = self.ctx.pop() {
7766                self.push_return_value_raw(v);
7767            } else {
7768                bail!("compact object property chain returned no value: {:?}", elm);
7769            }
7770            return Ok(());
7771        }
7772
7773        let form_id = self.canonical_runtime_form_id(head as u32);
7774        let args: Vec<Value> = Vec::new();
7775        vm_trace!(
7776            self,
7777            None,
7778            format!("exec_property dispatch form_id={} elm={:?}", form_id, elm),
7779        );
7780        // The element moves into the call metadata and comes back after the
7781        // dispatch (a dispatch error leaves the metadata as it was).
7782        self.ctx.vm_call = Some(runtime::VmCallMeta {
7783            element: elm,
7784            al_id: 0,
7785            ret_form: self.cfg.fm_int as i64,
7786        });
7787        let handled = runtime::dispatch_form_code(&mut self.ctx, form_id, &args)?;
7788        let elm = self
7789            .ctx
7790            .vm_call
7791            .take()
7792            .map(|meta| meta.element)
7793            .unwrap_or_default();
7794        if !handled {
7795            bail!("unhandled form property chain {:?}", elm);
7796        }
7797
7798        if let Some(v) = self.ctx.pop() {
7799            self.push_return_value_raw(v);
7800        } else {
7801            bail!("property chain returned no value: {:?}", elm);
7802        }
7803        self.ctx.int_vec_pool.give(elm);
7804
7805        Ok(())
7806    }
7807
7808    fn exec_assign(&mut self, elm: Vec<i32>, al_id: i32, rhs: Value) -> Result<()> {
7809        if elm.is_empty() {
7810            return Ok(());
7811        }
7812
7813        self.trace_cgm_coord_assign(&elm, &rhs);
7814
7815        // Call-local property assignment.
7816        if self.exec_call_assign(&elm, al_id, rhs.clone())? {
7817            return Ok(());
7818        }
7819
7820        let head = elm[0];
7821        let head_owner = elm_code::owner(head);
7822        if head_owner == elm_code::ELM_OWNER_CALL_PROP {
7823            let current_idx = self
7824                .current_call_frame_index()
7825                .ok_or_else(|| anyhow!("call stack underflow"))?;
7826            let call_prop_id = elm_code::code(head) as i32;
7827            let prop_idx = self
7828                .find_call_prop_index_in_frame(current_idx, call_prop_id)
7829                .ok_or_else(|| {
7830                    anyhow!(
7831                        "missing direct CALL_PROP assign id={} for {:?}",
7832                        call_prop_id,
7833                        elm
7834                    )
7835                })?;
7836            let prop_for_compose = self.call_stack[current_idx].user_props[prop_idx].clone();
7837            if let Some(composed) = self.compose_call_prop_tail(&prop_for_compose, &elm[1..]) {
7838                self.exec_assign(composed, al_id, rhs)?;
7839                return Ok(());
7840            }
7841            let frame = self
7842                .call_stack
7843                .get_mut(current_idx)
7844                .ok_or_else(|| anyhow!("call stack underflow"))?;
7845            let prop = frame.user_props.get_mut(prop_idx).ok_or_else(|| {
7846                anyhow!("missing direct CALL_PROP slot assign id={}", call_prop_id)
7847            })?;
7848            Self::assign_call_prop_result(prop, &elm[1..], rhs)?;
7849            return Ok(());
7850        }
7851
7852        if head_owner == elm_code::ELM_OWNER_USER_PROP {
7853            let prop_id = elm_code::code(head);
7854            if elm.len() >= 3 && self.call_array_marker(elm[1]) && elm[2] < 0 {
7855                return Ok(());
7856            }
7857            let old_cell = self.user_props.get(&prop_id).cloned();
7858
7859            // Original `tnm_command_proc_prop()` preserves the declared list
7860            // object and forwards the complete element tail to
7861            // `tnm_command_proc_int_list()` / `tnm_command_proc_str_list()`.
7862            // In particular, packed lvalues such as BIT16[ARRAY,index] are
7863            // writes into the existing INTLIST, not assignments that change
7864            // the user property's form.
7865            if self.exec_user_prop_list_command(
7866                prop_id,
7867                &elm[1..],
7868                al_id,
7869                self.cfg.fm_void,
7870                std::slice::from_ref(&rhs),
7871            )? {
7872                let new_cell = self.user_props.get(&prop_id);
7873                let array_idx = Self::intlist_assignment_subscript(&elm[1..])
7874                    .and_then(|(_, index)| usize::try_from(index).ok());
7875                self.trace_cf_condition_user_prop_assign(
7876                    self.stream.get_prg_cntr(),
7877                    prop_id,
7878                    array_idx,
7879                    old_cell.as_ref(),
7880                    new_cell,
7881                    &rhs,
7882                    &elm,
7883                );
7884                return Ok(());
7885            }
7886
7887            let array_idx = self.extract_array_index(&elm);
7888            self.assign_user_prop(prop_id, array_idx, rhs.clone());
7889            let new_cell = self.user_props.get(&prop_id);
7890            self.trace_cf_condition_user_prop_assign(
7891                self.stream.get_prg_cntr(),
7892                prop_id,
7893                array_idx,
7894                old_cell.as_ref(),
7895                new_cell,
7896                &rhs,
7897                &elm,
7898            );
7899            return Ok(());
7900        }
7901
7902        if head_owner != elm_code::ELM_OWNER_FORM {
7903            bail!(
7904                "unsupported assignment owner {} for element {:?}",
7905                head_owner,
7906                elm
7907            );
7908        }
7909
7910        if let Some(synthetic) = self.legacy_global_world_stage_chain(&elm, 1) {
7911            self.exec_assign(synthetic, al_id, rhs)?;
7912            self.sync_legacy_global_worlds_from_front();
7913            return Ok(());
7914        }
7915
7916        if self.dispatch_global_indexed_list_assign_direct(&elm, al_id, rhs.clone())? {
7917            return Ok(());
7918        }
7919
7920        if self.try_parent_slot_assign(&elm, &rhs) {
7921            return Ok(());
7922        }
7923        if let Some(synthetic) = self.try_compact_object_chain(&elm, true) {
7924            vm_trace!(
7925                self,
7926                None,
7927                format!(
7928                    "exec_assign compact-object elm={:?} synthetic={:?} al_id={} rhs={:?}",
7929                    elm, synthetic, al_id, rhs
7930                ),
7931            );
7932            let args: Vec<Value> = vec![rhs];
7933            self.ctx.vm_call = Some(runtime::VmCallMeta {
7934                element: synthetic.clone(),
7935                al_id: al_id as i64,
7936                ret_form: 0,
7937            });
7938            let form_id = self.canonical_runtime_form_id(synthetic[0] as u32);
7939            if !runtime::dispatch_form_code(&mut self.ctx, form_id, &args)? {
7940                self.ctx.vm_call = None;
7941                bail!(
7942                    "unhandled compact object assignment chain {:?} -> {:?}",
7943                    elm,
7944                    synthetic
7945                );
7946            }
7947            self.ctx.vm_call = None;
7948            self.update_compact_context_from_object_dispatch_chain(&synthetic);
7949            self.ctx.stack.clear();
7950            self.drain_pending_frame_action_finishes()?;
7951            return Ok(());
7952        }
7953
7954        let form_id = self.canonical_runtime_form_id(head as u32);
7955        vm_trace!(
7956            self,
7957            None,
7958            format!(
7959                "exec_assign dispatch form_id={} elm={:?} al_id={} rhs={:?}",
7960                form_id, elm, al_id, rhs
7961            ),
7962        );
7963        let args: Vec<Value> = vec![rhs];
7964        if self.vm_trace_config.commands_enabled {
7965            eprintln!(
7966                "[vm form assign] form={} al_id={} elm={:?} rhs={:?}",
7967                form_id,
7968                al_id,
7969                elm,
7970                args.first()
7971            );
7972        }
7973        self.ctx.vm_call = Some(runtime::VmCallMeta {
7974            element: elm,
7975            al_id: al_id as i64,
7976            ret_form: 0,
7977        });
7978
7979        let handled = runtime::dispatch_form_code(&mut self.ctx, form_id, &args)?;
7980        let elm = self
7981            .ctx
7982            .vm_call
7983            .take()
7984            .map(|meta| meta.element)
7985            .unwrap_or_default();
7986        if !handled {
7987            bail!("unhandled form assignment chain {:?}", elm);
7988        }
7989        self.ctx.int_vec_pool.give(elm);
7990        self.ctx.stack.clear();
7991        self.drain_pending_frame_action_finishes()?;
7992        Ok(())
7993    }
7994
7995    fn dispatch_owner_command(
7996        &mut self,
7997        owner: u8,
7998        raw_head: i32,
7999        ret_form: i32,
8000        args: &[Value],
8001    ) -> Result<bool> {
8002        let cmd_no = elm_code::code(raw_head) as usize;
8003
8004        if owner == elm_code::ELM_OWNER_CALL_CMD {
8005            // The original headers describe ELM_OWNER_CALL_CMD as a call command
8006            // that does not exist, and cmd_global.cpp does not dispatch it. Do not
8007            // reinterpret malformed bytecode as a command name and guess BG/CHR/
8008            // FADE behavior.
8009            let name = self
8010                .call_cmd_names
8011                .get(&(cmd_no as u32))
8012                .map(String::as_str)
8013                .unwrap_or("<unknown>");
8014            bail!(
8015                "invalid CALL_CMD owner: raw_head={} cmd_no={} name={}",
8016                raw_head,
8017                cmd_no,
8018                name
8019            );
8020        }
8021
8022        if owner != elm_code::ELM_OWNER_USER_CMD {
8023            return Ok(false);
8024        }
8025
8026        // C++ tnm_command_proc_user_cmd() passes the encoded user-command ID to
8027        // tnm_scene_proc_call_user_cmd(). C_tnm_scene_lexer::jump_to_user_cmd()
8028        // resolves pack-level include commands through Scene.pck.inc_cmds and
8029        // resolves only later IDs through the current scene's local command table.
8030        let requested_scene_no = self
8031            .current_scene_no
8032            .ok_or_else(|| anyhow!("USER_CMD executed without a current scene"))?;
8033        let command = self.resolve_user_command_by_id(requested_scene_no, cmd_no)?;
8034        vm_trace!(
8035            self,
8036            None,
8037            format!(
8038                "USER_CMD decoded name={} target_scene={} offset=0x{:x} include={} ret_form={} args={:?}",
8039                command.name.as_str(),
8040                command.target_scene_no,
8041                command.target_offset,
8042                command.include_command,
8043                ret_form,
8044                args
8045            ),
8046        );
8047        sg_omv_trace!(
8048            self,
8049            "USER_CMD enter name={} raw_head={} cmd_no={} target_scene={} offset=0x{:x} include={} ret_form={} argc={} current_scene={:?} current_pc=0x{:x}",
8050            command.name.as_str(),
8051            raw_head,
8052            command.encoded_no,
8053            command.target_scene_no,
8054            command.target_offset,
8055            command.include_command,
8056            ret_form,
8057            args.len(),
8058            self.current_scene_no,
8059            self.stream.get_prg_cntr()
8060        );
8061        self.enter_resolved_user_command(&command, ret_form, args, false, false)
8062    }
8063
8064    fn command_consumes_read_flag_no(&self, elm: &[i32]) -> bool {
8065        fn global_consumes(op: i32) -> bool {
8066            matches!(
8067                op,
8068                crate::runtime::forms::codes::elm_value::GLOBAL_PRINT
8069                    | crate::runtime::forms::codes::elm_value::GLOBAL_SEL
8070                    | crate::runtime::forms::codes::elm_value::GLOBAL_SEL_CANCEL
8071                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELMSG
8072                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELMSG_CANCEL
8073                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELBTN
8074                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELBTN_CANCEL
8075                    | crate::runtime::forms::codes::elm_value::GLOBAL_SELBTN_START
8076                    | crate::runtime::forms::codes::elm_value::GLOBAL_KOE
8077                    | crate::runtime::forms::codes::elm_value::GLOBAL_KOE_PLAY_WAIT
8078                    | crate::runtime::forms::codes::elm_value::GLOBAL_KOE_PLAY_WAIT_KEY
8079            )
8080        }
8081
8082        fn mwnd_consumes(op: i32) -> bool {
8083            matches!(
8084                op,
8085                crate::runtime::forms::codes::elm_value::MWND_PRINT
8086                    | crate::runtime::forms::codes::elm_value::MWND_SEL
8087                    | crate::runtime::forms::codes::elm_value::MWND_SEL_CANCEL
8088                    | crate::runtime::forms::codes::elm_value::MWND_SELMSG
8089                    | crate::runtime::forms::codes::elm_value::MWND_SELMSG_CANCEL
8090                    | crate::runtime::forms::codes::elm_value::MWND_KOE
8091                    | crate::runtime::forms::codes::elm_value::MWND_KOE_PLAY_WAIT
8092                    | crate::runtime::forms::codes::elm_value::MWND_KOE_PLAY_WAIT_KEY
8093            )
8094        }
8095
8096        // C++ consumes the read-flag integer inside the concrete command
8097        // handler after CD_COMMAND has read the command metadata. The Rust VM
8098        // has to make the same decision from the actual element chain. Do not
8099        // assume the chain is exactly [FORM, OP]: commands can arrive through
8100        // object, mwnd, and global aliases, so scan every adjacent form/op pair.
8101        for pair in elm.windows(2) {
8102            let form_id = self.canonical_runtime_form_id(pair[0] as u32) as i32;
8103            let op = pair[1];
8104            if form_id == crate::runtime::forms::codes::FM_GLOBAL && global_consumes(op) {
8105                return true;
8106            }
8107            if form_id == crate::runtime::forms::codes::FM_MWND && mwnd_consumes(op) {
8108                return true;
8109            }
8110        }
8111
8112        false
8113    }
8114
8115    fn exec_command(
8116        &mut self,
8117        elm: Vec<i32>,
8118        al_id: i32,
8119        ret_form: i32,
8120        args: &mut Vec<Value>,
8121    ) -> Result<()> {
8122        let result = self.exec_command_element(&elm, al_id, ret_form, args);
8123        self.ctx.int_vec_pool.give(elm);
8124        result
8125    }
8126
8127    fn exec_command_element(
8128        &mut self,
8129        elm: &[i32],
8130        al_id: i32,
8131        ret_form: i32,
8132        args: &mut Vec<Value>,
8133    ) -> Result<()> {
8134        if elm.is_empty() {
8135            self.push_default_for_ret(ret_form);
8136            return Ok(());
8137        }
8138
8139        if self.exec_call_command(&elm, al_id, ret_form, args)? {
8140            return Ok(());
8141        }
8142
8143        let raw_head = elm[0];
8144        let owner = elm_code::owner(raw_head);
8145
8146        if owner == elm_code::ELM_OWNER_CALL_PROP {
8147            let current_idx = self
8148                .current_call_frame_index()
8149                .ok_or_else(|| anyhow!("call stack underflow"))?;
8150            let call_prop_id = elm_code::code(raw_head) as i32;
8151            let prop_idx = self
8152                .find_call_prop_index_in_frame(current_idx, call_prop_id)
8153                .ok_or_else(|| {
8154                    anyhow!(
8155                        "missing direct CALL_PROP command id={} for {:?}",
8156                        call_prop_id,
8157                        elm
8158                    )
8159                })?;
8160            self.exec_call_prop_command(current_idx, prop_idx, &elm[1..], al_id, ret_form, args)?;
8161            return Ok(());
8162        }
8163
8164        if owner == elm_code::ELM_OWNER_USER_PROP {
8165            let prop_id = elm_code::code(raw_head);
8166            if self.exec_user_prop_list_command(prop_id, &elm[1..], al_id, ret_form, args)? {
8167                return Ok(());
8168            }
8169            let cell = self
8170                .user_props
8171                .get(&prop_id)
8172                .cloned()
8173                .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
8174            if let Some(composed) = self.compose_user_prop_tail(prop_id, &cell, &elm[1..]) {
8175                self.exec_command(composed, al_id, ret_form, args)?;
8176                return Ok(());
8177            }
8178            if cell.form == self.cfg.fm_str && elm.len() == 2 {
8179                self.call_prop_eval_str_op(&cell.str_value, elm[1], args, al_id)?;
8180                return Ok(());
8181            }
8182            self.push_default_for_ret(ret_form);
8183            return Ok(());
8184        }
8185
8186        match owner {
8187            elm_code::ELM_OWNER_FORM => {
8188                if let Some(synthetic) = self.legacy_global_world_stage_chain(&elm, 1) {
8189                    self.exec_command(synthetic, al_id, ret_form, args)?;
8190                    self.sync_legacy_global_worlds_from_front();
8191                    return Ok(());
8192                }
8193                if self.dispatch_global_indexed_list_command_direct(&elm, al_id, ret_form, args)? {
8194                    return Ok(());
8195                }
8196                if let Some(synthetic) = self.try_compact_object_chain(&elm, true) {
8197                    vm_trace!(
8198                        self,
8199                        None,
8200                        format!(
8201                            "exec_command compact-object elm={:?} synthetic={:?} al_id={} ret_form={} args={:?}",
8202                            elm, synthetic, al_id, ret_form, args
8203                        ),
8204                    );
8205                    if self.sg_mwnd_object_trace_enabled()
8206                        && (Self::sg_mwnd_chain_interesting(&elm)
8207                            || Self::sg_mwnd_chain_interesting(&synthetic))
8208                    {
8209                        self.sg_mwnd_object_trace_emit(format_args!(
8210                            "exec_command compact elm={:?} synthetic={:?} al_id={} ret_form={} args={:?} current_chain={:?} current_stage_object={:?}",
8211                            elm,
8212                            synthetic,
8213                            al_id,
8214                            ret_form,
8215                            args,
8216                            self.ctx.globals.current_object_chain,
8217                            self.ctx.globals.current_stage_object
8218                        ));
8219                    }
8220                    self.ctx.vm_call = Some(runtime::VmCallMeta {
8221                        element: synthetic.clone(),
8222                        al_id: al_id as i64,
8223                        ret_form: ret_form as i64,
8224                    });
8225                    let form_id = self.canonical_runtime_form_id(synthetic[0] as u32) as i32;
8226                    let op_id = if synthetic.len() >= 2 {
8227                        synthetic[1]
8228                    } else {
8229                        al_id
8230                    };
8231                    self.sg_omv_trace_command(
8232                        "compact", &synthetic, form_id, op_id, al_id, ret_form, args,
8233                    );
8234                    if !runtime::dispatch_form_code(&mut self.ctx, form_id as u32, args)? {
8235                        self.ctx.vm_call = None;
8236                        bail!(
8237                            "unhandled compact object command chain {:?} -> {:?}",
8238                            elm,
8239                            synthetic
8240                        );
8241                    }
8242                    self.ctx.vm_call = None;
8243                    self.update_compact_context_from_object_dispatch_chain(&synthetic);
8244                    self.drain_pending_frame_action_finishes()?;
8245                    if ret_form != self.cfg.fm_void {
8246                        if !self.ctx.stack.is_empty() {
8247                            self.take_ctx_return(ret_form)?;
8248                            return Ok(());
8249                        }
8250                        if self.ctx.wait_poll() {
8251                            if let Some(frame) = self.call_stack.last_mut() {
8252                                frame.delayed_ret_form = Some(ret_form);
8253                            } else {
8254                                self.delayed_ret_form = Some(ret_form);
8255                            }
8256                            return Ok(());
8257                        }
8258                    }
8259                    self.take_ctx_return(ret_form)?;
8260                    return Ok(());
8261                }
8262
8263                let form_id = self.canonical_runtime_form_id(raw_head as u32) as i32;
8264                if self.exec_builtin_global_control(form_id, ret_form, args)? {
8265                    if ret_form != self.cfg.fm_void {
8266                        self.take_ctx_return(ret_form)?;
8267                    } else {
8268                        self.ctx.stack.clear();
8269                    }
8270                    return Ok(());
8271                }
8272                if self.exec_syscom_save_value_intlistref(&elm, form_id, ret_form, args)? {
8273                    return Ok(());
8274                }
8275                if self.exec_mouse_get_pos(&elm, form_id, args)? {
8276                    return Ok(());
8277                }
8278                if self.exec_builtin_scene_form(&elm, form_id, al_id, ret_form, args)? {
8279                    return Ok(());
8280                }
8281
8282                let op_id = if elm.len() >= 2 { elm[1] } else { al_id };
8283                self.ctx.vm_call = Some(runtime::VmCallMeta {
8284                    element: self.ctx.int_vec_pool.take_copy(elm),
8285                    al_id: al_id as i64,
8286                    ret_form: ret_form as i64,
8287                });
8288
8289                if self.vm_trace_config.commands_enabled {
8290                    let elm_tail = elm
8291                        .iter()
8292                        .map(|v| v.to_string())
8293                        .collect::<Vec<_>>()
8294                        .join(",");
8295                    let args_dbg = args
8296                        .iter()
8297                        .map(|v| format!("{v:?}"))
8298                        .collect::<Vec<_>>()
8299                        .join(", ");
8300                    eprintln!(
8301                        "[vm form cmd] form={} op={} argc={} ret_form={} al_id={} elm=[{}] args=[{}]",
8302                        form_id,
8303                        op_id,
8304                        args.len(),
8305                        ret_form,
8306                        al_id,
8307                        elm_tail,
8308                        args_dbg
8309                    );
8310                }
8311
8312                self.sg_omv_trace_command("dispatch", &elm, form_id, op_id, al_id, ret_form, args);
8313
8314                let handled = runtime::dispatch_form_code(&mut self.ctx, form_id as u32, args);
8315                if let Some(call) = self.ctx.vm_call.take() {
8316                    self.ctx.int_vec_pool.give(call.element);
8317                }
8318                if !handled? {
8319                    bail!("unhandled form command chain {:?}", elm);
8320                }
8321                self.drain_pending_frame_action_finishes()?;
8322            }
8323            elm_code::ELM_OWNER_USER_CMD | elm_code::ELM_OWNER_CALL_CMD => {
8324                if self.vm_trace_config.commands_enabled {
8325                    let cmd_no = elm_code::code(raw_head);
8326                    let elm_tail = elm
8327                        .iter()
8328                        .map(|v| v.to_string())
8329                        .collect::<Vec<_>>()
8330                        .join(",");
8331                    let args_dbg = args
8332                        .iter()
8333                        .map(|v| format!("{v:?}"))
8334                        .collect::<Vec<_>>()
8335                        .join(", ");
8336                    eprintln!(
8337                        "[vm owner cmd] owner={} cmd_no={} argc={} ret_form={} al_id={} elm=[{}] args=[{}]",
8338                        owner,
8339                        cmd_no,
8340                        args.len(),
8341                        ret_form,
8342                        al_id,
8343                        elm_tail,
8344                        args_dbg
8345                    );
8346                }
8347
8348                if !self.dispatch_owner_command(owner, raw_head, ret_form, args)? {
8349                    bail!("unhandled owner command chain {:?}", elm);
8350                }
8351                if owner == elm_code::ELM_OWNER_USER_CMD {
8352                    // USER_CMD has transferred control to the callee. Its return
8353                    // value will be materialized by CD_RETURN when that callee
8354                    // finishes, so CD_COMMAND must not consume ctx.stack now.
8355                    return Ok(());
8356                }
8357            }
8358            _ => {
8359                bail!("unsupported command owner {} for element {:?}", owner, elm);
8360            }
8361        }
8362
8363        if ret_form != self.cfg.fm_void {
8364            if !self.ctx.stack.is_empty() {
8365                self.take_ctx_return(ret_form)?;
8366                return Ok(());
8367            }
8368            if self.ctx.wait_poll() {
8369                if let Some(frame) = self.call_stack.last_mut() {
8370                    frame.delayed_ret_form = Some(ret_form);
8371                } else {
8372                    self.delayed_ret_form = Some(ret_form);
8373                }
8374                return Ok(());
8375            }
8376        }
8377
8378        self.take_ctx_return(ret_form)?;
8379        Ok(())
8380    }
8381
8382    fn save_kind_to_original(kind: RuntimeSaveKind) -> Option<crate::original_save::SaveKind> {
8383        match kind {
8384            RuntimeSaveKind::Normal => Some(crate::original_save::SaveKind::Normal),
8385            RuntimeSaveKind::Quick => Some(crate::original_save::SaveKind::Quick),
8386            RuntimeSaveKind::End => Some(crate::original_save::SaveKind::End),
8387            RuntimeSaveKind::Inner => None,
8388        }
8389    }
8390
8391    fn configured_runtime_save_count(&self, quick: bool) -> usize {
8392        let keys: [&str; 2] = if quick {
8393            ["#QUICK_SAVE.CNT", "QUICK_SAVE.CNT"]
8394        } else {
8395            ["#SAVE.CNT", "SAVE.CNT"]
8396        };
8397        let default_count = if quick { 3 } else { 10 };
8398        self.ctx
8399            .tables
8400            .gameexe
8401            .as_ref()
8402            .and_then(|cfg| keys.iter().find_map(|key| cfg.get_usize(key)))
8403            .unwrap_or(default_count)
8404            .min(10000)
8405    }
8406
8407    fn runtime_save_file_path(
8408        &self,
8409        kind: RuntimeSaveKind,
8410        index: usize,
8411    ) -> Option<std::path::PathBuf> {
8412        let save_kind = Self::save_kind_to_original(kind)?;
8413        let save_cnt = self.configured_runtime_save_count(false);
8414        let quick_cnt = self.configured_runtime_save_count(true);
8415        Some(crate::original_save::save_file_path_with_counts(
8416            &self.ctx.project_dir,
8417            save_cnt,
8418            quick_cnt,
8419            save_kind,
8420            index,
8421        ))
8422    }
8423
8424    fn stamp_slot_with_local_time(slot: &mut crate::runtime::globals::SaveSlotState) {
8425        let now = crate::platform_time::local_time_fields();
8426        slot.exist = true;
8427        slot.year = now.year as i64;
8428        slot.month = now.month as i64;
8429        slot.day = now.day as i64;
8430        // SYSTEMTIME.wDayOfWeek uses 0..6 with Sunday = 0.
8431        slot.weekday = now.weekday_sunday0 as i64;
8432        slot.hour = now.hour as i64;
8433        slot.minute = now.minute as i64;
8434        slot.second = now.second as i64;
8435        slot.millisecond = now.millisecond as i64;
8436    }
8437
8438    /// Build the slot record that ends up in the save file header and in the in-memory
8439    /// `save_slots` / `quick_save_slots` tables. Mirrors C++ `tnm_save_local_on_file`:
8440    /// timestamps come from `GetLocalTime` (i.e. "now"), while the textual fields
8441    /// (title / message / full_message / append_dir / append_name) come from the
8442    /// engine's m_local_save snapshot.
8443    ///
8444    /// Inner-save still pulls textual fields from live runtime state because the
8445    /// inner-save path here is the only consumer that doesn't go through SAVEPOINT.
8446    fn ensure_runtime_slot_for_save(
8447        &mut self,
8448        req: RuntimeSaveRequest,
8449    ) -> crate::runtime::globals::SaveSlotState {
8450        let mut slot = crate::runtime::globals::SaveSlotState::default();
8451        Self::stamp_slot_with_local_time(&mut slot);
8452        if let Some(snapshot) = self.ctx.local_save_snapshot.as_ref() {
8453            slot.title = snapshot.save_scene_title.clone();
8454            slot.message = snapshot.save_msg.clone();
8455            slot.full_message = if snapshot.save_full_msg.is_empty() {
8456                snapshot.save_msg.clone()
8457            } else {
8458                snapshot.save_full_msg.clone()
8459            };
8460            slot.append_dir = snapshot.append_dir.clone();
8461            slot.append_name = snapshot.append_name.clone();
8462        } else {
8463            // No snapshot exists (e.g. inner save before any SAVEPOINT). Fall back to
8464            // live runtime values so inner-save still records something meaningful.
8465            slot.title = self.ctx.globals.syscom.current_save_scene_title.clone();
8466            slot.message = self.ctx.globals.syscom.current_save_message.clone();
8467            slot.full_message = if self.ctx.globals.syscom.current_save_full_message.is_empty() {
8468                self.ctx.globals.syscom.current_save_message.clone()
8469            } else {
8470                self.ctx.globals.syscom.current_save_full_message.clone()
8471            };
8472            slot.append_dir = self.ctx.globals.append_dir.clone();
8473            slot.append_name = self.ctx.globals.append_name.clone();
8474        }
8475
8476        match req.kind {
8477            RuntimeSaveKind::Normal => {
8478                if self.ctx.globals.syscom.save_slots.len() <= req.index {
8479                    self.ctx
8480                        .globals
8481                        .syscom
8482                        .save_slots
8483                        .resize_with(req.index + 1, Default::default);
8484                }
8485                self.ctx.globals.syscom.save_slots[req.index] = slot.clone();
8486            }
8487            RuntimeSaveKind::Quick => {
8488                if self.ctx.globals.syscom.quick_save_slots.len() <= req.index {
8489                    self.ctx
8490                        .globals
8491                        .syscom
8492                        .quick_save_slots
8493                        .resize_with(req.index + 1, Default::default);
8494                }
8495                self.ctx.globals.syscom.quick_save_slots[req.index] = slot.clone();
8496            }
8497            RuntimeSaveKind::End | RuntimeSaveKind::Inner => {}
8498        }
8499        slot
8500    }
8501
8502    fn local_flag_count(&self) -> usize {
8503        if let Some(configured) = self.ctx.tables.gameexe.as_ref().and_then(|cfg| {
8504            cfg.get_usize("#FLAG.CNT")
8505                .or_else(|| cfg.get_usize("FLAG.CNT"))
8506        }) {
8507            return configured.min(10000);
8508        }
8509
8510        [
8511            codes::ELM_GLOBAL_A,
8512            codes::ELM_GLOBAL_B,
8513            codes::ELM_GLOBAL_C,
8514            codes::ELM_GLOBAL_D,
8515            codes::ELM_GLOBAL_E,
8516            codes::ELM_GLOBAL_F,
8517            codes::ELM_GLOBAL_X,
8518        ]
8519        .into_iter()
8520        .map(|elm| self.int_list_by_element(elm).len())
8521        .chain(std::iter::once(
8522            self.str_list_by_element(codes::ELM_GLOBAL_S).len(),
8523        ))
8524        .max()
8525        .unwrap_or(1000)
8526        .clamp(1000, 10000)
8527    }
8528
8529    fn mwnd_waku_btn_count(&self) -> usize {
8530        self.ctx
8531            .tables
8532            .gameexe
8533            .as_ref()
8534            .and_then(|cfg| {
8535                cfg.get_usize("#WAKU.BTN.CNT")
8536                    .or_else(|| cfg.get_usize("WAKU.BTN.CNT"))
8537            })
8538            .unwrap_or(8)
8539            .min(256)
8540    }
8541
8542    fn int_list_by_element(&self, elm: i32) -> &[i64] {
8543        self.ctx
8544            .globals
8545            .int_lists
8546            .get(&(elm as u32))
8547            .map(Vec::as_slice)
8548            .unwrap_or(&[])
8549    }
8550
8551    fn str_list_by_element(&self, elm: i32) -> &[String] {
8552        self.ctx
8553            .globals
8554            .str_lists
8555            .get(&(elm as u32))
8556            .map(Vec::as_slice)
8557            .unwrap_or(&[])
8558    }
8559
8560    fn build_cpp_local_data_pod(&self) -> Vec<u8> {
8561        let mut out = Vec::with_capacity(356);
8562        let script = &self.ctx.globals.script;
8563        let syscom = &self.ctx.globals.syscom;
8564        let push_i32 = |out: &mut Vec<u8>, v: i64| out.extend_from_slice(&(v as i32).to_le_bytes());
8565        let push_bool = |out: &mut Vec<u8>, v: bool| out.push(if v { 1 } else { 0 });
8566
8567        push_i32(&mut out, script.cur_koe_no);
8568        push_i32(&mut out, script.cur_chr_no);
8569        push_i32(&mut out, script.cur_read_flag_scn_no);
8570        push_i32(&mut out, script.cur_read_flag_flag_no);
8571        push_i32(&mut out, script.cursor_no);
8572
8573        push_bool(&mut out, syscom.syscom_menu_disable);
8574        push_bool(&mut out, script.hide_mwnd_disable);
8575        push_bool(&mut out, script.msg_back_disable);
8576        push_bool(&mut out, script.shortcut_disable);
8577
8578        push_bool(&mut out, script.skip_disable);
8579        push_bool(&mut out, script.ctrl_disable);
8580        push_bool(&mut out, script.not_stop_skip_by_click);
8581        push_bool(&mut out, script.not_skip_msg_by_click);
8582        push_bool(&mut out, script.skip_unread_message);
8583        push_bool(&mut out, script.auto_mode_flag);
8584        while out.len() % 4 != 0 {
8585            out.push(0);
8586        }
8587        push_i32(&mut out, script.auto_mode_moji_wait);
8588        push_i32(&mut out, script.auto_mode_min_wait);
8589        push_i32(&mut out, script.auto_mode_moji_cnt);
8590        push_i32(&mut out, script.mouse_cursor_hide_onoff);
8591        push_i32(&mut out, script.mouse_cursor_hide_time);
8592        push_i32(&mut out, script.msg_back_save_cntr);
8593
8594        push_i32(&mut out, script.msg_speed);
8595        push_bool(&mut out, script.msg_nowait);
8596        push_bool(&mut out, script.async_msg_mode);
8597        push_bool(&mut out, script.async_msg_mode_once);
8598        push_bool(&mut out, script.multi_msg_mode);
8599        push_bool(&mut out, script.skip_trigger);
8600        push_bool(&mut out, script.koe_dont_stop_on_flag);
8601        push_bool(&mut out, script.koe_dont_stop_off_flag);
8602
8603        push_bool(&mut out, syscom.mwnd_btn_disable_all);
8604        push_bool(&mut out, syscom.mwnd_btn_touch_disable);
8605        push_bool(&mut out, script.mwnd_anime_on_flag);
8606        push_bool(&mut out, script.mwnd_anime_off_flag);
8607        push_bool(&mut out, script.mwnd_disp_off_flag);
8608
8609        push_bool(&mut out, script.msg_back_off);
8610        push_bool(&mut out, script.msg_back_disp_off);
8611        while out.len() % 4 != 0 {
8612            out.push(0);
8613        }
8614        push_i32(&mut out, script.font_bold);
8615        push_i32(&mut out, script.font_shadow);
8616
8617        push_bool(&mut out, script.cursor_disp_off);
8618        push_bool(&mut out, script.cursor_move_by_key_disable);
8619        for key in 0u16..=255u16 {
8620            push_bool(&mut out, script.key_disable.contains(&(key as u8)));
8621        }
8622
8623        push_bool(&mut out, script.quake_stop_flag);
8624        push_bool(&mut out, script.emote_mouth_stop_flag);
8625        push_bool(&mut out, self.ctx.globals.cg_table_off);
8626        push_bool(&mut out, script.bgmfade_flag);
8627        push_bool(&mut out, script.dont_set_save_point);
8628        push_bool(&mut out, script.ignore_r_flag);
8629        push_bool(&mut out, script.wait_display_vsync_off_flag);
8630
8631        push_bool(&mut out, script.time_stop_flag);
8632        push_bool(&mut out, script.counter_time_stop_flag);
8633        push_bool(&mut out, script.frame_action_time_stop_flag);
8634        push_bool(&mut out, script.stage_time_stop_flag);
8635        while out.len() % 4 != 0 {
8636            out.push(0);
8637        }
8638        debug_assert_eq!(out.len(), 356);
8639        out
8640    }
8641
8642    fn read_cpp_local_data_pod(
8643        &mut self,
8644        rd: &mut crate::original_save::OriginalStreamReader<'_>,
8645        has_font: bool,
8646    ) -> Result<()> {
8647        if rd.layout.uses_early_records() {
8648            return self.read_early_local_data_pod(rd);
8649        }
8650        let script = &mut self.ctx.globals.script;
8651
8652        script.cur_koe_no = rd.i32()? as i64;
8653        script.cur_chr_no = rd.i32()? as i64;
8654        script.cur_read_flag_scn_no = rd.i32()? as i64;
8655        script.cur_read_flag_flag_no = rd.i32()? as i64;
8656        script.cursor_no = rd.i32()? as i64;
8657
8658        self.ctx.globals.syscom.syscom_menu_disable = rd.bool()?;
8659        script.hide_mwnd_disable = rd.bool()?;
8660        script.msg_back_disable = rd.bool()?;
8661        script.shortcut_disable = rd.bool()?;
8662
8663        script.skip_disable = rd.bool()?;
8664        script.ctrl_disable = rd.bool()?;
8665        script.not_stop_skip_by_click = rd.bool()?;
8666        script.not_skip_msg_by_click = rd.bool()?;
8667        script.skip_unread_message = rd.bool()?;
8668        script.auto_mode_flag = rd.bool()?;
8669        rd.skip(2)?;
8670        script.auto_mode_moji_wait = rd.i32()? as i64;
8671        script.auto_mode_min_wait = rd.i32()? as i64;
8672        script.auto_mode_moji_cnt = rd.i32()? as i64;
8673        script.mouse_cursor_hide_onoff = rd.i32()? as i64;
8674        script.mouse_cursor_hide_time = rd.i32()? as i64;
8675        script.msg_back_save_cntr = rd.i32()? as i64;
8676
8677        script.msg_speed = rd.i32()? as i64;
8678        script.msg_nowait = rd.bool()?;
8679        script.async_msg_mode = rd.bool()?;
8680        script.async_msg_mode_once = rd.bool()?;
8681        script.multi_msg_mode = rd.bool()?;
8682        script.skip_trigger = rd.bool()?;
8683        script.koe_dont_stop_on_flag = rd.bool()?;
8684        script.koe_dont_stop_off_flag = rd.bool()?;
8685
8686        self.ctx.globals.syscom.mwnd_btn_disable_all = rd.bool()?;
8687        self.ctx.globals.syscom.mwnd_btn_touch_disable = rd.bool()?;
8688        script.mwnd_anime_on_flag = rd.bool()?;
8689        script.mwnd_anime_off_flag = rd.bool()?;
8690        script.mwnd_disp_off_flag = rd.bool()?;
8691
8692        script.msg_back_off = rd.bool()?;
8693        script.msg_back_disp_off = rd.bool()?;
8694        if has_font {
8695            rd.skip(2)?;
8696            script.font_bold = rd.i32()? as i64;
8697            script.font_shadow = rd.i32()? as i64;
8698        } else {
8699            script.font_bold = -1;
8700            script.font_shadow = -1;
8701        }
8702
8703        script.cursor_disp_off = rd.bool()?;
8704        script.cursor_runtime_visible = !script.cursor_disp_off;
8705        script.cursor_move_by_key_disable = rd.bool()?;
8706        script.key_disable.clear();
8707        for key in 0u16..=255u16 {
8708            if rd.bool()? {
8709                script.key_disable.insert(key as u8);
8710            }
8711        }
8712
8713        script.quake_stop_flag = rd.bool()?;
8714        script.emote_mouth_stop_flag = rd.bool()?;
8715        self.ctx.globals.cg_table_off = rd.bool()?;
8716        script.bgmfade_flag = rd.bool()?;
8717        script.dont_set_save_point = rd.bool()?;
8718        script.ignore_r_flag = rd.bool()?;
8719        script.wait_display_vsync_off_flag = rd.bool()?;
8720
8721        script.time_stop_flag = rd.bool()?;
8722        script.counter_time_stop_flag = rd.bool()?;
8723        script.frame_action_time_stop_flag = rd.bool()?;
8724        script.stage_time_stop_flag = rd.bool()?;
8725        rd.skip(if has_font { 3 } else { 1 })?;
8726
8727        self.ctx.globals.syscom.replay_koe = if script.cur_koe_no >= 0 {
8728            Some((script.cur_koe_no, script.cur_chr_no))
8729        } else {
8730            None
8731        };
8732        Ok(())
8733    }
8734
8735    fn write_cpp_syscom_menu(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8736        let base = w.position();
8737        let s = &self.ctx.globals.syscom;
8738        let push_ex =
8739            |w: &mut crate::original_save::OriginalStreamWriter, exist: bool, enable: bool| {
8740                w.push_bool(exist);
8741                w.push_bool(enable);
8742            };
8743        push_ex(w, s.read_skip.exist, s.read_skip.enable);
8744        push_ex(w, s.unread_skip.exist, s.unread_skip.enable);
8745        push_ex(w, s.auto_skip.exist, s.auto_skip.enable);
8746        push_ex(w, s.auto_mode.exist, s.auto_mode.enable);
8747        push_ex(w, s.hide_mwnd.exist, s.hide_mwnd.enable);
8748        push_ex(w, s.msg_back.exist, s.msg_back.enable);
8749        push_ex(w, s.save_feature.exist, s.save_feature.enable);
8750        push_ex(w, s.load_feature.exist, s.load_feature.enable);
8751        push_ex(w, s.return_to_sel.exist, s.return_to_sel.enable);
8752        push_ex(w, s.config_feature.exist, s.config_feature.enable);
8753        push_ex(w, s.manual_feature.exist, s.manual_feature.enable);
8754        push_ex(w, s.version_feature.exist, s.version_feature.enable);
8755        push_ex(w, s.return_to_menu.exist, s.return_to_menu.enable);
8756        push_ex(w, s.end_game.exist, s.end_game.enable);
8757        push_ex(w, s.cancel_feature.exist, s.cancel_feature.enable);
8758        for i in 0..4 {
8759            let sw = s.local_extra_switches.get(i).copied().unwrap_or(if i == 0 {
8760                s.local_extra_switch
8761            } else {
8762                runtime::globals::ToggleFeatureState::default()
8763            });
8764            w.push_bool(sw.exist);
8765            w.push_bool(sw.enable);
8766            w.push_bool(sw.onoff);
8767        }
8768        while !(w.position() - base).is_multiple_of(4) {
8769            w.push_bool(false);
8770        }
8771        for i in 0..4 {
8772            let mode = s.local_extra_modes.get(i).copied().unwrap_or(if i == 0 {
8773                s.local_extra_mode
8774            } else {
8775                runtime::globals::ValueFeatureState::default()
8776            });
8777            w.push_bool(mode.exist);
8778            w.push_bool(mode.enable);
8779            w.push_padding(2);
8780            w.push_i32(mode.value as i32);
8781        }
8782        debug_assert_eq!(w.position() - base, 76);
8783    }
8784
8785    fn read_cpp_syscom_menu(
8786        &mut self,
8787        rd: &mut crate::original_save::OriginalStreamReader<'_>,
8788    ) -> Result<()> {
8789        fn read_ex(
8790            rd: &mut crate::original_save::OriginalStreamReader<'_>,
8791            state: &mut runtime::globals::ToggleFeatureState,
8792        ) -> Result<()> {
8793            state.exist = rd.bool()?;
8794            state.enable = rd.bool()?;
8795            Ok(())
8796        }
8797
8798        let s = &mut self.ctx.globals.syscom;
8799        read_ex(rd, &mut s.read_skip)?;
8800        read_ex(rd, &mut s.unread_skip)?;
8801        read_ex(rd, &mut s.auto_skip)?;
8802        read_ex(rd, &mut s.auto_mode)?;
8803        read_ex(rd, &mut s.hide_mwnd)?;
8804        read_ex(rd, &mut s.msg_back)?;
8805        read_ex(rd, &mut s.save_feature)?;
8806        read_ex(rd, &mut s.load_feature)?;
8807        read_ex(rd, &mut s.return_to_sel)?;
8808        read_ex(rd, &mut s.config_feature)?;
8809        read_ex(rd, &mut s.manual_feature)?;
8810        read_ex(rd, &mut s.version_feature)?;
8811        read_ex(rd, &mut s.return_to_menu)?;
8812        read_ex(rd, &mut s.end_game)?;
8813        read_ex(rd, &mut s.cancel_feature)?;
8814
8815        for sw in &mut s.local_extra_switches {
8816            sw.exist = rd.bool()?;
8817            sw.enable = rd.bool()?;
8818            sw.onoff = rd.bool()?;
8819        }
8820        rd.skip(2)?;
8821        for mode in &mut s.local_extra_modes {
8822            mode.exist = rd.bool()?;
8823            mode.enable = rd.bool()?;
8824            rd.skip(2)?;
8825            mode.value = rd.i32()? as i64;
8826        }
8827        s.local_extra_switch = s.local_extra_switches[0];
8828        s.local_extra_mode = s.local_extra_modes[0];
8829        Ok(())
8830    }
8831
8832    fn write_empty_counter_param(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8833        w.push_bool(false);
8834        w.push_bool(false);
8835        w.push_bool(false);
8836        w.push_bool(false);
8837        w.push_i32(0);
8838        w.push_i32(0);
8839        w.push_i32(0);
8840        w.push_i32(0);
8841    }
8842
8843    fn write_empty_frame_action(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8844        w.push_i32(0);
8845        w.push_str("");
8846        w.push_str("");
8847        w.push_i32(0);
8848        self.write_empty_counter_param(w);
8849    }
8850
8851    fn write_empty_btn_select(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8852        w.push_i32(0);
8853        w.push_padding(112);
8854        w.push_bool(false);
8855        w.push_bool(false);
8856        w.push_bool(false);
8857        w.push_bool(false);
8858        w.push_bool(false);
8859        w.push_bool(false);
8860        w.push_str("");
8861        w.push_i32(0);
8862        w.push_i32(0);
8863    }
8864
8865    fn write_empty_stage(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8866        w.push_empty_fixed_array();
8867        w.push_empty_fixed_array();
8868        w.push_empty_fixed_array();
8869        self.write_empty_btn_select(w);
8870        w.push_empty_fixed_array();
8871        w.push_empty_fixed_array();
8872        w.push_empty_fixed_array();
8873    }
8874
8875    fn write_empty_screen(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8876        w.push_empty_fixed_array();
8877        w.push_padding(16);
8878        w.push_empty_fixed_array();
8879    }
8880
8881    fn write_empty_sound(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8882        w.push_str("");
8883        w.push_i32(0);
8884        w.push_i32(0);
8885        w.push_bool(false);
8886        w.push_bool(false);
8887        w.push_i32(0);
8888        w.push_i32(0);
8889        w.push_empty_fixed_array();
8890        w.push_i32(0);
8891        w.push_str("");
8892    }
8893
8894    fn write_empty_msg_back(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8895        w.push_i32(0);
8896        w.push_i32(0);
8897        w.push_i32(0);
8898        w.push_i32(0);
8899        w.push_bool(false);
8900    }
8901
8902    fn write_cpp_prop(
8903        &self,
8904        w: &mut crate::original_save::OriginalStreamWriter,
8905        prop_id: i32,
8906        cell: &UserPropCell,
8907    ) {
8908        w.push_i32(prop_id);
8909        w.push_i32(cell.form);
8910        w.push_i32(cell.int_value);
8911        w.push_str(&cell.str_value);
8912        w.push_element(&cell.element);
8913        w.push_extend_items(&cell.list_items, |w, item| self.write_cpp_prop(w, 0, item));
8914        w.push_i32(cell.list_items.len() as i32);
8915        if cell.form == self.cfg.fm_intlist {
8916            let vals: Vec<i64> = cell.int_list.iter().map(|v| *v as i64).collect();
8917            w.push_extend_i32_list(&vals);
8918        } else if cell.form == self.cfg.fm_strlist {
8919            w.push_extend_str_list(&cell.str_list);
8920        }
8921    }
8922
8923    fn read_cpp_prop(
8924        &self,
8925        rd: &mut crate::original_save::OriginalStreamReader<'_>,
8926    ) -> Result<(i32, UserPropCell)> {
8927        let prop_id = rd.i32()?;
8928        let form = rd.i32()?;
8929        let int_value = rd.i32()?;
8930        let str_value = rd.string()?;
8931        let element = rd.element()?;
8932        let list_items = rd.extend_items(|rd| {
8933            let (_id, cell) = self.read_cpp_prop(rd)?;
8934            Ok(cell)
8935        })?;
8936        let _exp_cnt = rd.i32()?;
8937        let mut cell = UserPropCell::new(form, element);
8938        cell.int_value = int_value;
8939        cell.str_value = str_value;
8940        cell.list_items = list_items;
8941        if form == self.cfg.fm_intlist {
8942            cell.int_list = rd
8943                .extend_i32_list()?
8944                .into_iter()
8945                .map(|v| v as i32)
8946                .collect();
8947        } else if form == self.cfg.fm_strlist {
8948            cell.str_list = rd.extend_items(|rd| rd.string())?;
8949        }
8950        Ok((prop_id, cell))
8951    }
8952
8953    fn write_cpp_inc_prop_list(&self, w: &mut crate::original_save::OriginalStreamWriter) {
8954        let shared = self.shared_user_prop_count();
8955        let props: Vec<(i32, UserPropCell)> = (0..shared)
8956            .map(|idx| {
8957                let prop_id = idx as u16;
8958                let cell = self
8959                    .user_props
8960                    .get(&prop_id)
8961                    .cloned()
8962                    .unwrap_or_else(|| self.default_user_prop_cell(prop_id));
8963                (idx as i32, cell)
8964            })
8965            .collect();
8966        w.push_fixed_items(&props, |w, (id, cell)| self.write_cpp_prop(w, *id, cell));
8967    }
8968
8969    fn read_cpp_inc_prop_list(
8970        &mut self,
8971        rd: &mut crate::original_save::OriginalStreamReader<'_>,
8972    ) -> Result<()> {
8973        let props = rd.fixed_items(|rd| self.read_cpp_prop(rd))?;
8974        for (idx, (_stored_id, cell)) in props.into_iter().enumerate() {
8975            self.user_props.insert(idx as u16, cell);
8976        }
8977        Ok(())
8978    }
8979
8980    fn write_cpp_current_scene_prop_lists(
8981        &self,
8982        w: &mut crate::original_save::OriginalStreamWriter,
8983    ) {
8984        let shared = self.shared_user_prop_count();
8985        let mut props: Vec<(i32, UserPropCell)> = Vec::new();
8986        let scene_prop_cnt = self.stream.header.scn_prop_cnt.max(0) as usize;
8987        for idx in 0..scene_prop_cnt {
8988            let prop_id = (shared + idx) as u16;
8989            if let Some(cell) = self.user_props.get(&prop_id).cloned() {
8990                props.push((idx as i32, cell));
8991            } else if let Some((_, _)) = self.user_prop_decl(prop_id) {
8992                props.push((idx as i32, self.default_user_prop_cell(prop_id)));
8993            }
8994        }
8995        if props.is_empty() {
8996            w.push_i32(0);
8997            return;
8998        }
8999        w.push_i32(1);
9000        w.push_str(self.current_scene_name.as_deref().unwrap_or(""));
9001        w.push_fixed_items(&props, |w, (id, cell)| self.write_cpp_prop(w, *id, cell));
9002    }
9003
9004    fn read_cpp_scene_prop_lists(
9005        &mut self,
9006        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9007        current_scene_name: &str,
9008    ) -> Result<()> {
9009        let shared = self.shared_user_prop_count();
9010        if rd.layout.is_indexed() {
9011            let lists = rd.fixed_items(|rd| rd.fixed_items(|rd| self.read_cpp_prop(rd)))?;
9012            let scene_no = self.ctx.lookup_scene_no(current_scene_name).unwrap_or(-1);
9013            if let Some(props) = lists.into_iter().nth(scene_no as usize) {
9014                for (idx, (_, cell)) in props.into_iter().enumerate() {
9015                    self.user_props.insert((shared + idx) as u16, cell);
9016                }
9017            }
9018            return Ok(());
9019        }
9020        let scene_prop_cnt = rd.i32()?.max(0) as usize;
9021        for _ in 0..scene_prop_cnt {
9022            let scene_name = rd.string()?;
9023            let props = rd.fixed_items(|rd| self.read_cpp_prop(rd))?;
9024            if scene_name == current_scene_name {
9025                for (idx, (_stored_id, cell)) in props.into_iter().enumerate() {
9026                    self.user_props.insert((shared + idx) as u16, cell);
9027                }
9028            }
9029        }
9030        Ok(())
9031    }
9032
9033    fn write_cpp_call_prop(
9034        &self,
9035        w: &mut crate::original_save::OriginalStreamWriter,
9036        prop: &CallProp,
9037    ) {
9038        w.push_i32(prop.scn_no);
9039        w.push_i32(prop.prop_id);
9040        let mut cell = UserPropCell::new(prop.form, prop.element.clone());
9041        match &prop.value {
9042            CallPropValue::Int(v) => cell.int_value = *v,
9043            CallPropValue::Str(v) => cell.str_value = v.clone(),
9044            CallPropValue::Element(v) => cell.element = v.clone(),
9045            CallPropValue::IntList(v) => cell.int_list = v.clone(),
9046            CallPropValue::StrList(v) => cell.str_list = v.clone(),
9047        }
9048        self.write_cpp_prop(w, prop.prop_id, &cell);
9049    }
9050
9051    fn read_cpp_call_prop(
9052        &self,
9053        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9054    ) -> Result<CallProp> {
9055        let scn_no = rd.i32()?;
9056        let declared_prop_id = rd.i32()?;
9057        let (_stored_id, cell) = self.read_cpp_prop(rd)?;
9058        let value = if cell.form == self.cfg.fm_int {
9059            CallPropValue::Int(cell.int_value)
9060        } else if cell.form == self.cfg.fm_str {
9061            CallPropValue::Str(cell.str_value.clone())
9062        } else if cell.form == self.cfg.fm_intlist {
9063            CallPropValue::IntList(cell.int_list.clone())
9064        } else if cell.form == self.cfg.fm_strlist {
9065            CallPropValue::StrList(cell.str_list.clone())
9066        } else {
9067            CallPropValue::Element(cell.element.clone())
9068        };
9069        Ok(CallProp {
9070            scn_no,
9071            prop_id: declared_prop_id,
9072            form: cell.form,
9073            decl_size: cell
9074                .int_list
9075                .len()
9076                .max(cell.str_list.len())
9077                .max(cell.list_items.len()),
9078            element: cell.element,
9079            value,
9080        })
9081    }
9082
9083    fn write_cpp_call_frame(
9084        &self,
9085        w: &mut crate::original_save::OriginalStreamWriter,
9086        frame: &CallFrame,
9087    ) {
9088        let l: Vec<i64> = frame.int_args.iter().map(|v| *v as i64).collect();
9089        w.push_extend_i32_list(&l);
9090        w.push_extend_str_list(&frame.str_args);
9091        w.push_extend_items(&frame.user_props, |w, prop| {
9092            self.write_cpp_call_prop(w, prop)
9093        });
9094        w.push_i32(frame.call_type);
9095        w.push_i32(frame.ret_form);
9096        w.push_str(frame.return_scene_name.as_deref().unwrap_or(""));
9097        w.push_i32(frame.return_line_no);
9098        w.push_i32(frame.return_pc as i32);
9099    }
9100
9101    fn flattened_call_stack_for_save(&self) -> Vec<CallFrame> {
9102        // C++ saves C_elm_call_list verbatim. The caller lexer position is
9103        // captured when a call is entered (tnm_save_call), so the save writer
9104        // must not infer or rewrite scene ownership from Rust's scene_stack.
9105        self.call_stack.clone()
9106    }
9107
9108    fn read_cpp_call_frame(
9109        &self,
9110        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9111    ) -> Result<CallFrame> {
9112        let int_args: Vec<i32> = rd
9113            .extend_i32_list()?
9114            .into_iter()
9115            .map(|v| v as i32)
9116            .collect();
9117        let str_args: Vec<String> = rd.extend_items(|rd| rd.string())?;
9118        let user_props = rd.extend_items(|rd| self.read_cpp_call_prop(rd))?;
9119        let call_type = rd.i32()?;
9120        let ret_form = rd.i32()?;
9121        let (scene_name, line_no, return_pc) = if rd.layout.is_indexed() {
9122            let data = rd.len_bytes()?;
9123            anyhow::ensure!(
9124                data.len() == 12,
9125                "invalid indexed call save length {}",
9126                data.len()
9127            );
9128            let mut saved = crate::original_save::OriginalStreamReader::new(&data);
9129            let scene_no = saved.i32()?;
9130            let scene_name = self.indexed_scene_name(scene_no)?;
9131            (scene_name, saved.i32()?, saved.i32()?.max(0) as usize)
9132        } else {
9133            (rd.string()?, rd.i32()?, rd.i32()?.max(0) as usize)
9134        };
9135        Ok(CallFrame {
9136            call_type,
9137            return_pc,
9138            return_scene_no: None,
9139            return_scene_name: (!scene_name.is_empty()).then_some(scene_name),
9140            return_line_no: line_no,
9141            ret_form,
9142            return_override: None,
9143            excall_proc: false,
9144            // C_elm_call::save() does not persist excall_flag or
9145            // frame_action_flag. Do not reinterpret USER_CMD as frame_action.
9146            frame_action_proc: false,
9147            arg_cnt: 0,
9148            delayed_ret_form: None,
9149            user_props,
9150            int_args,
9151            str_args,
9152        })
9153    }
9154
9155    fn save_i32(v: i64) -> i32 {
9156        v.clamp(i32::MIN as i64, i32::MAX as i64) as i32
9157    }
9158
9159    fn write_cpp_counter_param(
9160        &self,
9161        w: &mut crate::original_save::OriginalStreamWriter,
9162        c: &runtime::globals::Counter,
9163    ) {
9164        let (
9165            is_running,
9166            real_flag,
9167            frame_mode,
9168            frame_loop_flag,
9169            frame_start_value,
9170            frame_end_value,
9171            frame_time,
9172            cur_time,
9173        ) = c.save_parts();
9174        w.push_bool(is_running);
9175        w.push_bool(real_flag);
9176        w.push_bool(frame_mode);
9177        w.push_bool(frame_loop_flag);
9178        w.push_i32(Self::save_i32(frame_start_value));
9179        w.push_i32(Self::save_i32(frame_end_value));
9180        w.push_i32(Self::save_i32(frame_time));
9181        w.push_i32(Self::save_i32(cur_time));
9182    }
9183
9184    fn read_cpp_counter_param(
9185        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9186    ) -> Result<runtime::globals::Counter> {
9187        let is_running = rd.bool()?;
9188        let real_flag = rd.bool()?;
9189        let frame_mode = rd.bool()?;
9190        let frame_loop_flag = rd.bool()?;
9191        let frame_start_value = rd.i32()? as i64;
9192        let frame_end_value = rd.i32()? as i64;
9193        let frame_time = rd.i32()? as i64;
9194        let cur_time = rd.i32()? as i64;
9195        Ok(runtime::globals::Counter::from_save_parts(
9196            is_running,
9197            real_flag,
9198            frame_mode,
9199            frame_loop_flag,
9200            frame_start_value,
9201            frame_end_value,
9202            frame_time,
9203            cur_time,
9204        ))
9205    }
9206
9207    fn write_cpp_value_prop(w: &mut crate::original_save::OriginalStreamWriter, value: &Value) {
9208        use crate::runtime::forms::codes;
9209        w.push_i32(0);
9210        match value {
9211            Value::Str(s) => {
9212                w.push_i32(codes::FM_STR);
9213                w.push_i32(0);
9214                w.push_str(s);
9215            }
9216            Value::Int(v) => {
9217                w.push_i32(codes::FM_INT);
9218                w.push_i32(Self::save_i32(*v));
9219                w.push_str("");
9220            }
9221            _ => {
9222                w.push_i32(codes::FM_INT);
9223                w.push_i32(0);
9224                w.push_str("");
9225            }
9226        }
9227        w.push_empty_element();
9228        w.push_i32(0);
9229        w.push_i32(0);
9230    }
9231
9232    fn read_cpp_value_prop(
9233        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9234    ) -> Result<Value> {
9235        use crate::runtime::forms::codes;
9236        let _id = rd.i32()?;
9237        let form = rd.i32()?;
9238        let int_value = rd.i32()?;
9239        let str_value = rd.string()?;
9240        let _element = rd.element()?;
9241        let _exp_list: Vec<Value> = rd.extend_items(|rd| Self::read_cpp_value_prop(rd))?;
9242        let _exp_cnt = rd.i32()?;
9243        if form == codes::FM_INTLIST {
9244            let _ = rd.extend_i32_list()?;
9245        } else if form == codes::FM_STRLIST {
9246            let _ = rd.extend_items(|rd| rd.string())?;
9247        }
9248        if form == codes::FM_STR {
9249            Ok(Value::Str(str_value))
9250        } else {
9251            Ok(Value::Int(int_value as i64))
9252        }
9253    }
9254
9255    fn write_cpp_frame_action(
9256        &self,
9257        w: &mut crate::original_save::OriginalStreamWriter,
9258        fa: &runtime::globals::ObjectFrameActionState,
9259    ) {
9260        w.push_i32(Self::save_i32(fa.end_time));
9261        w.push_str(&fa.scn_name);
9262        w.push_str(&fa.cmd_name);
9263        w.push_extend_items(&fa.args, Self::write_cpp_value_prop);
9264        self.write_cpp_counter_param(w, &fa.counter);
9265    }
9266
9267    fn read_cpp_frame_action(
9268        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9269    ) -> Result<runtime::globals::ObjectFrameActionState> {
9270        let end_time = rd.i32()? as i64;
9271        let scn_name = rd.string()?;
9272        let cmd_name = rd.string()?;
9273        let args = rd.extend_items(|rd| Self::read_cpp_value_prop(rd))?;
9274        let counter = Self::read_cpp_counter_param(rd)?;
9275        Ok(runtime::globals::ObjectFrameActionState {
9276            scn_name,
9277            cmd_name,
9278            counter,
9279            end_time,
9280            real_time_flag: false,
9281            end_flag: false,
9282            args,
9283        })
9284    }
9285
9286    fn write_cpp_int_event_raw(
9287        w: &mut crate::original_save::OriginalStreamWriter,
9288        e: &runtime::int_event::IntEvent,
9289    ) {
9290        w.push_i32(e.def_value);
9291        w.push_i32(e.value);
9292        w.push_i32(e.cur_time);
9293        w.push_i32(e.end_time);
9294        w.push_i32(e.delay_time);
9295        w.push_i32(e.start_value);
9296        w.push_i32(e.cur_value);
9297        w.push_i32(e.end_value);
9298        w.push_i32(e.loop_type);
9299        w.push_i32(e.speed_type);
9300        w.push_i32(e.real_flag);
9301    }
9302
9303    fn read_cpp_int_event_raw(
9304        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9305    ) -> Result<runtime::int_event::IntEvent> {
9306        let def_value = rd.i32()?;
9307        Ok(runtime::int_event::IntEvent {
9308            def_value,
9309            value: rd.i32()?,
9310            cur_time: rd.i32()?,
9311            end_time: rd.i32()?,
9312            delay_time: rd.i32()?,
9313            start_value: rd.i32()?,
9314            cur_value: rd.i32()?,
9315            end_value: rd.i32()?,
9316            loop_type: rd.i32()?,
9317            speed_type: rd.i32()?,
9318            real_flag: rd.i32()?,
9319        })
9320    }
9321
9322    fn write_cpp_save_event(
9323        w: &mut crate::original_save::OriginalStreamWriter,
9324        e: &runtime::int_event::IntEvent,
9325    ) {
9326        w.push_i32(e.loop_type);
9327        if e.loop_type != -1 {
9328            Self::write_cpp_int_event_raw(w, e);
9329        } else {
9330            w.push_i32(e.value);
9331        }
9332    }
9333
9334    fn read_cpp_save_event(
9335        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9336        def_value: i32,
9337    ) -> Result<runtime::int_event::IntEvent> {
9338        let loop_type = rd.i32()?;
9339        if loop_type != -1 {
9340            let mut e = Self::read_cpp_int_event_raw(rd)?;
9341            e.loop_type = loop_type;
9342            Ok(e)
9343        } else {
9344            let mut e = runtime::int_event::IntEvent::new(def_value);
9345            e.loop_type = -1;
9346            e.value = rd.i32()?;
9347            e.cur_value = e.value;
9348            Ok(e)
9349        }
9350    }
9351
9352    fn write_cpp_int_event_extend_list(
9353        &self,
9354        w: &mut crate::original_save::OriginalStreamWriter,
9355        values: &[runtime::int_event::IntEvent],
9356    ) {
9357        w.push_extend_items(values, Self::write_cpp_int_event_raw);
9358    }
9359
9360    fn read_cpp_int_event_extend_list(
9361        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9362    ) -> Result<Vec<runtime::int_event::IntEvent>> {
9363        rd.extend_items(|rd| Self::read_cpp_int_event_raw(rd))
9364    }
9365
9366    fn write_cpp_group(
9367        &self,
9368        w: &mut crate::original_save::OriginalStreamWriter,
9369        g: &runtime::globals::GroupState,
9370    ) {
9371        w.push_i32(Self::save_i32(g.order));
9372        w.push_i32(Self::save_i32(g.layer));
9373        w.push_i32(Self::save_i32(g.cancel_priority));
9374        w.push_i32(Self::save_i32(g.cancel_se_no));
9375        w.push_i32(Self::save_i32(g.decided_button_no));
9376        w.push_i32(Self::save_i32(g.result));
9377        w.push_i32(Self::save_i32(g.result_button_no));
9378        w.push_bool(g.started);
9379        w.push_bool(g.pause_flag);
9380        w.push_bool(g.wait_flag);
9381        w.push_bool(g.cancel_flag);
9382        w.push_element(&g.target_object);
9383    }
9384
9385    fn read_cpp_group(
9386        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9387    ) -> Result<runtime::globals::GroupState> {
9388        let mut g = runtime::globals::GroupState {
9389            order: rd.i32()? as i64,
9390            layer: rd.i32()? as i64,
9391            cancel_priority: rd.i32()? as i64,
9392            cancel_se_no: rd.i32()? as i64,
9393            decided_button_no: rd.i32()? as i64,
9394            ..Default::default()
9395        };
9396
9397        if !rd.layout.is_indexed() {
9398            g.result = rd.i32()? as i64;
9399            g.result_button_no = rd.i32()? as i64;
9400        }
9401        g.started = rd.bool()?;
9402        g.pause_flag = rd.bool()?;
9403        g.wait_flag = rd.bool()?;
9404        g.cancel_flag = rd.bool()?;
9405        g.target_object = rd.element()?;
9406        Ok(g)
9407    }
9408
9409    fn write_cpp_object(
9410        &self,
9411        w: &mut crate::original_save::OriginalStreamWriter,
9412        obj: &runtime::globals::ObjectState,
9413    ) {
9414        let b = &obj.base;
9415        let ev = &obj.runtime.prop_events;
9416        w.push_i32(Self::save_i32(obj.object_type));
9417        w.push_i32(Self::save_i32(b.wipe_copy));
9418        w.push_i32(Self::save_i32(b.wipe_erase));
9419        w.push_i32(Self::save_i32(b.click_disable));
9420        // C_elm_object_param_filter: C_rect + C_argb.
9421        w.push_i32(Self::save_i32(obj.rect_param.left));
9422        w.push_i32(Self::save_i32(obj.rect_param.top));
9423        w.push_i32(Self::save_i32(obj.rect_param.right));
9424        w.push_i32(Self::save_i32(obj.rect_param.bottom));
9425        w.push_i32(Self::save_i32(obj.rect_param.color_argb));
9426        // C_elm_object_param_string.
9427        w.push_i32(Self::save_i32(obj.string_param.moji_size));
9428        w.push_i32(Self::save_i32(obj.string_param.moji_space_x));
9429        w.push_i32(Self::save_i32(obj.string_param.moji_space_y));
9430        w.push_i32(Self::save_i32(obj.string_param.moji_cnt));
9431        w.push_i32(Self::save_i32(obj.string_param.moji_color));
9432        w.push_i32(Self::save_i32(obj.string_param.shadow_color));
9433        w.push_i32(Self::save_i32(obj.string_param.fuchi_color));
9434        w.push_i32(Self::save_i32(obj.string_param.shadow_mode));
9435        // C_elm_object_param_number.
9436        w.push_i32(Self::save_i32(obj.number_value));
9437        w.push_i32(Self::save_i32(obj.number_param.keta_max));
9438        w.push_i32(Self::save_i32(obj.number_param.disp_zero));
9439        w.push_i32(Self::save_i32(obj.number_param.disp_sign));
9440        w.push_i32(Self::save_i32(obj.number_param.tumeru_sign));
9441        w.push_i32(Self::save_i32(obj.number_param.space_mod));
9442        w.push_i32(Self::save_i32(obj.number_param.space));
9443        if obj.object_type == 4 {
9444            // C_tnm_save_stream::save(TYPE) writes the complete MSVC struct
9445            // byte-for-byte. C_elm_object_param_weather is 21 i32 fields, one
9446            // bool, then three bytes of tail padding (88 bytes total).
9447            let wp = &obj.weather_param;
9448            for v in [
9449                wp.weather_type,
9450                wp.cnt,
9451                wp.pat_mode,
9452                wp.pat_no_00,
9453                wp.pat_no_01,
9454                wp.pat_time,
9455                wp.move_time_x,
9456                wp.move_time_y,
9457                wp.sin_time_x,
9458                wp.sin_time_y,
9459                wp.sin_power_x,
9460                wp.sin_power_y,
9461                wp.center_x,
9462                wp.center_y,
9463                wp.center_rotate,
9464                wp.appear_range,
9465                wp.zoom_min,
9466                wp.zoom_max,
9467                wp.scale_x,
9468                wp.scale_y,
9469                wp.active_time,
9470            ] {
9471                w.push_i32(Self::save_i32(v));
9472            }
9473            w.push_bool(wp.real_time_flag);
9474            w.push_padding(3);
9475        }
9476        w.push_i32(Self::save_i32(obj.thumb_save_no));
9477        w.push_bool(obj.movie.loop_flag);
9478        w.push_bool(obj.movie.auto_free_flag);
9479        w.push_bool(obj.movie.real_time_flag);
9480        w.push_bool(obj.movie.pause_flag);
9481        if obj.object_type == 12 {
9482            w.push_i32(Self::save_i32(obj.emote.width));
9483            w.push_i32(Self::save_i32(obj.emote.height));
9484            for option in obj.emote.timeline_options {
9485                w.push_i32(Self::save_i32(option));
9486            }
9487            w.push_i32(Self::save_i32(obj.emote.koe_chara_no));
9488            w.push_i32(Self::save_i32(obj.emote.koe_mouth_volume));
9489            w.push_i32(Self::save_i32(obj.emote.rep_x));
9490            w.push_i32(Self::save_i32(obj.emote.rep_y));
9491        }
9492        if obj.button.enabled {
9493            w.push_i32(1);
9494            w.push_i32(Self::save_i32(obj.button.sys_type));
9495            w.push_i32(Self::save_i32(obj.button.sys_type_opt));
9496            w.push_i32(Self::save_i32(obj.button.action_no));
9497            w.push_i32(Self::save_i32(obj.button.se_no));
9498            w.push_i32(Self::save_i32(obj.button.button_no));
9499            w.push_empty_element();
9500            w.push_i32(if obj.button.push_keep { 1 } else { 0 });
9501            w.push_i32(Self::save_i32(obj.button.state));
9502            w.push_i32(Self::save_i32(obj.button.mode));
9503            w.push_i32(Self::save_i32(obj.button.cut_no));
9504            w.push_i32(-1);
9505            w.push_i32(-1);
9506            w.push_i32(Self::save_i32(obj.button.decided_action_z_no));
9507            w.push_i32(0);
9508            w.push_i32(if obj.button.alpha_test { 1 } else { 0 });
9509        } else {
9510            w.push_i32(0);
9511        }
9512        w.push_i32(Self::save_i32(b.disp));
9513        // Original saves obp.pat_no as a whole C_elm_int_event (raw 44-byte
9514        // struct), not a plain int. A plain int here shifted every following
9515        // field by 40 bytes for each object, which is what crashed the
9516        // original engine on our saves and corrupted our own stage restore.
9517        {
9518            let mut pat_ev = ev.patno.clone();
9519            if pat_ev.loop_type == -1 {
9520                pat_ev.def_value = b.patno as i32;
9521                pat_ev.value = b.patno as i32;
9522                pat_ev.cur_value = b.patno as i32;
9523            }
9524            Self::write_cpp_int_event_raw(w, &pat_ev);
9525        }
9526        w.push_i32(Self::save_i32(b.order));
9527        w.push_i32(Self::save_i32(b.layer));
9528        w.push_i32(Self::save_i32(b.world));
9529        w.push_i32(Self::save_i32(b.child_sort_type));
9530        for e in [
9531            &ev.x,
9532            &ev.y,
9533            &ev.z,
9534            &ev.center_x,
9535            &ev.center_y,
9536            &ev.center_z,
9537            &ev.center_rep_x,
9538            &ev.center_rep_y,
9539            &ev.center_rep_z,
9540            &ev.scale_x,
9541            &ev.scale_y,
9542            &ev.scale_z,
9543            &ev.rotate_x,
9544            &ev.rotate_y,
9545            &ev.rotate_z,
9546        ] {
9547            Self::write_cpp_save_event(w, e);
9548        }
9549        w.push_i32(Self::save_i32(b.clip_use));
9550        for e in [&ev.clip_left, &ev.clip_top, &ev.clip_right, &ev.clip_bottom] {
9551            Self::write_cpp_save_event(w, e);
9552        }
9553        w.push_i32(Self::save_i32(b.src_clip_use));
9554        for e in [
9555            &ev.src_clip_left,
9556            &ev.src_clip_top,
9557            &ev.src_clip_right,
9558            &ev.src_clip_bottom,
9559            &ev.tr,
9560            &ev.mono,
9561            &ev.reverse,
9562            &ev.bright,
9563            &ev.dark,
9564            &ev.color_r,
9565            &ev.color_g,
9566            &ev.color_b,
9567            &ev.color_rate,
9568            &ev.color_add_r,
9569            &ev.color_add_g,
9570            &ev.color_add_b,
9571        ] {
9572            Self::write_cpp_save_event(w, e);
9573        }
9574        for v in [
9575            b.mask_no,
9576            b.tonecurve_no,
9577            b.light_no,
9578            b.fog_use,
9579            b.culling,
9580            b.alpha_test,
9581            b.alpha_blend,
9582            b.blend,
9583            0,
9584        ] {
9585            w.push_i32(Self::save_i32(v));
9586        }
9587        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.x_rep);
9588        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.y_rep);
9589        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.z_rep);
9590        self.write_cpp_int_event_extend_list(w, &obj.runtime.prop_event_lists.tr_rep);
9591        w.push_extend_i32_list(&obj.runtime.prop_lists.f);
9592        w.push_str(obj.file_name.as_deref().unwrap_or(""));
9593        w.push_str(obj.string_value.as_deref().unwrap_or(""));
9594        w.push_str(&obj.button.decided_action_scn_name);
9595        w.push_str(&obj.button.decided_action_cmd_name);
9596        if obj.object_type == 12 {
9597            for timeline in &obj.emote.timeline_names {
9598                w.push_str(timeline);
9599            }
9600        }
9601        self.write_cpp_frame_action(w, &obj.frame_action);
9602        w.push_extend_items(&obj.frame_action_ch, |w, fa| {
9603            self.write_cpp_frame_action(w, fa)
9604        });
9605        w.push_str(obj.gan_file.as_deref().unwrap_or(""));
9606        w.push_extend_items(&obj.runtime.child_objects, |w, child| {
9607            self.write_cpp_object(w, child)
9608        });
9609    }
9610
9611    fn read_cpp_object(
9612        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9613    ) -> Result<runtime::globals::ObjectState> {
9614        match rd.layout {
9615            NativeLocalLayout::ShortElements => return Self::read_short_object(rd),
9616            NativeLocalLayout::Early | NativeLocalLayout::Indexed => {
9617                return Self::read_early_object(rd);
9618            }
9619            NativeLocalLayout::Legacy | NativeLocalLayout::Current => {}
9620        }
9621        let mut obj = runtime::globals::ObjectState {
9622            object_type: rd.i32()? as i64,
9623            ..Default::default()
9624        };
9625
9626        obj.base.wipe_copy = rd.i32()? as i64;
9627        obj.base.wipe_erase = rd.i32()? as i64;
9628        obj.base.click_disable = rd.i32()? as i64;
9629        obj.rect_param.left = rd.i32()? as i64;
9630        obj.rect_param.top = rd.i32()? as i64;
9631        obj.rect_param.right = rd.i32()? as i64;
9632        obj.rect_param.bottom = rd.i32()? as i64;
9633        obj.rect_param.color_argb = rd.i32()? as i64;
9634        obj.string_param.moji_size = rd.i32()? as i64;
9635        obj.string_param.moji_space_x = rd.i32()? as i64;
9636        obj.string_param.moji_space_y = rd.i32()? as i64;
9637        obj.string_param.moji_cnt = rd.i32()? as i64;
9638        obj.string_param.moji_color = rd.i32()? as i64;
9639        obj.string_param.shadow_color = rd.i32()? as i64;
9640        obj.string_param.fuchi_color = rd.i32()? as i64;
9641        obj.string_param.shadow_mode = rd.i32()? as i64;
9642        obj.number_value = rd.i32()? as i64;
9643        obj.number_param.keta_max = rd.i32()? as i64;
9644        obj.number_param.disp_zero = rd.i32()? as i64;
9645        obj.number_param.disp_sign = rd.i32()? as i64;
9646        obj.number_param.tumeru_sign = rd.i32()? as i64;
9647        obj.number_param.space_mod = rd.i32()? as i64;
9648        obj.number_param.space = rd.i32()? as i64;
9649        if obj.object_type == 4 {
9650            obj.weather_param.weather_type = rd.i32()? as i64;
9651            obj.weather_param.cnt = rd.i32()? as i64;
9652            obj.weather_param.pat_mode = rd.i32()? as i64;
9653            obj.weather_param.pat_no_00 = rd.i32()? as i64;
9654            obj.weather_param.pat_no_01 = rd.i32()? as i64;
9655            obj.weather_param.pat_time = rd.i32()? as i64;
9656            obj.weather_param.move_time_x = rd.i32()? as i64;
9657            obj.weather_param.move_time_y = rd.i32()? as i64;
9658            obj.weather_param.sin_time_x = rd.i32()? as i64;
9659            obj.weather_param.sin_time_y = rd.i32()? as i64;
9660            obj.weather_param.sin_power_x = rd.i32()? as i64;
9661            obj.weather_param.sin_power_y = rd.i32()? as i64;
9662            obj.weather_param.center_x = rd.i32()? as i64;
9663            obj.weather_param.center_y = rd.i32()? as i64;
9664            obj.weather_param.center_rotate = rd.i32()? as i64;
9665            obj.weather_param.appear_range = rd.i32()? as i64;
9666            obj.weather_param.zoom_min = rd.i32()? as i64;
9667            obj.weather_param.zoom_max = rd.i32()? as i64;
9668            obj.weather_param.scale_x = rd.i32()? as i64;
9669            obj.weather_param.scale_y = rd.i32()? as i64;
9670            obj.weather_param.active_time = rd.i32()? as i64;
9671            obj.weather_param.real_time_flag = rd.bool()?;
9672            // MSVC pads the trailing bool to the struct's 4-byte alignment.
9673            rd.skip(3)?;
9674            // `move_time` is a Rust convenience alias for TYPE_B; it is not an
9675            // additional C++ serialized field.
9676            obj.weather_param.move_time = obj.weather_param.move_time_x;
9677        }
9678        obj.thumb_save_no = rd.i32()? as i64;
9679        obj.movie.loop_flag = rd.bool()?;
9680        obj.movie.auto_free_flag = rd.bool()?;
9681        obj.movie.real_time_flag = rd.bool()?;
9682        obj.movie.pause_flag = rd.bool()?;
9683        if obj.object_type == 12 {
9684            obj.emote.width = rd.i32()? as i64;
9685            obj.emote.height = rd.i32()? as i64;
9686            for option in &mut obj.emote.timeline_options {
9687                *option = rd.i32()? as i64;
9688            }
9689            obj.emote.koe_chara_no = rd.i32()? as i64;
9690            obj.emote.koe_mouth_volume = rd.i32()? as i64;
9691            obj.emote.rep_x = rd.i32()? as i64;
9692            obj.emote.rep_y = rd.i32()? as i64;
9693        }
9694        let button_exist = rd.i32()? != 0;
9695        if button_exist {
9696            obj.button.enabled = true;
9697            obj.button.sys_type = rd.i32()? as i64;
9698            obj.button.sys_type_opt = rd.i32()? as i64;
9699            obj.button.action_no = rd.i32()? as i64;
9700            obj.button.se_no = rd.i32()? as i64;
9701            obj.button.button_no = rd.i32()? as i64;
9702            rd.skip_element()?;
9703            obj.button.push_keep = rd.i32()? != 0;
9704            obj.button.state = rd.i32()? as i64;
9705            obj.button.mode = rd.i32()? as i64;
9706            obj.button.cut_no = rd.i32()? as i64;
9707            let _ = rd.i32()?;
9708            let _ = rd.i32()?;
9709            obj.button.decided_action_z_no = rd.i32()? as i64;
9710            let _ = rd.i32()?;
9711            obj.button.alpha_test = rd.i32()? != 0;
9712        }
9713        obj.base.disp = rd.i32()? as i64;
9714        // Mirror of the writer: original pat_no is a full C_elm_int_event.
9715        obj.runtime.prop_events.patno = Self::read_cpp_int_event_raw(rd)?;
9716        if obj.runtime.prop_events.patno.loop_type == -1 {
9717            obj.base.patno = obj.runtime.prop_events.patno.value as i64;
9718        }
9719        obj.base.order = rd.i32()? as i64;
9720        obj.base.layer = rd.i32()? as i64;
9721        obj.base.world = rd.i32()? as i64;
9722        obj.base.child_sort_type = rd.i32()? as i64;
9723        obj.runtime.prop_events.x = Self::read_cpp_save_event(rd, 0)?;
9724        obj.runtime.prop_events.y = Self::read_cpp_save_event(rd, 0)?;
9725        obj.runtime.prop_events.z = Self::read_cpp_save_event(rd, 0)?;
9726        obj.runtime.prop_events.center_x = Self::read_cpp_save_event(rd, 0)?;
9727        obj.runtime.prop_events.center_y = Self::read_cpp_save_event(rd, 0)?;
9728        obj.runtime.prop_events.center_z = Self::read_cpp_save_event(rd, 0)?;
9729        obj.runtime.prop_events.center_rep_x = Self::read_cpp_save_event(rd, 0)?;
9730        obj.runtime.prop_events.center_rep_y = Self::read_cpp_save_event(rd, 0)?;
9731        obj.runtime.prop_events.center_rep_z = Self::read_cpp_save_event(rd, 0)?;
9732        obj.runtime.prop_events.scale_x = Self::read_cpp_save_event(rd, 1000)?;
9733        obj.runtime.prop_events.scale_y = Self::read_cpp_save_event(rd, 1000)?;
9734        obj.runtime.prop_events.scale_z = Self::read_cpp_save_event(rd, 1000)?;
9735        obj.runtime.prop_events.rotate_x = Self::read_cpp_save_event(rd, 0)?;
9736        obj.runtime.prop_events.rotate_y = Self::read_cpp_save_event(rd, 0)?;
9737        obj.runtime.prop_events.rotate_z = Self::read_cpp_save_event(rd, 0)?;
9738        obj.base.clip_use = rd.i32()? as i64;
9739        obj.runtime.prop_events.clip_left = Self::read_cpp_save_event(rd, 0)?;
9740        obj.runtime.prop_events.clip_top = Self::read_cpp_save_event(rd, 0)?;
9741        obj.runtime.prop_events.clip_right = Self::read_cpp_save_event(rd, 0)?;
9742        obj.runtime.prop_events.clip_bottom = Self::read_cpp_save_event(rd, 0)?;
9743        obj.base.src_clip_use = rd.i32()? as i64;
9744        obj.runtime.prop_events.src_clip_left = Self::read_cpp_save_event(rd, 0)?;
9745        obj.runtime.prop_events.src_clip_top = Self::read_cpp_save_event(rd, 0)?;
9746        obj.runtime.prop_events.src_clip_right = Self::read_cpp_save_event(rd, 0)?;
9747        obj.runtime.prop_events.src_clip_bottom = Self::read_cpp_save_event(rd, 0)?;
9748        obj.runtime.prop_events.tr = Self::read_cpp_save_event(rd, 255)?;
9749        obj.runtime.prop_events.mono = Self::read_cpp_save_event(rd, 0)?;
9750        obj.runtime.prop_events.reverse = Self::read_cpp_save_event(rd, 0)?;
9751        obj.runtime.prop_events.bright = Self::read_cpp_save_event(rd, 0)?;
9752        obj.runtime.prop_events.dark = Self::read_cpp_save_event(rd, 0)?;
9753        obj.runtime.prop_events.color_r = Self::read_cpp_save_event(rd, 0)?;
9754        obj.runtime.prop_events.color_g = Self::read_cpp_save_event(rd, 0)?;
9755        obj.runtime.prop_events.color_b = Self::read_cpp_save_event(rd, 0)?;
9756        obj.runtime.prop_events.color_rate = Self::read_cpp_save_event(rd, 0)?;
9757        obj.runtime.prop_events.color_add_r = Self::read_cpp_save_event(rd, 0)?;
9758        obj.runtime.prop_events.color_add_g = Self::read_cpp_save_event(rd, 0)?;
9759        obj.runtime.prop_events.color_add_b = Self::read_cpp_save_event(rd, 0)?;
9760        obj.base.mask_no = rd.i32()? as i64;
9761        obj.base.tonecurve_no = rd.i32()? as i64;
9762        obj.base.light_no = rd.i32()? as i64;
9763        obj.base.fog_use = rd.i32()? as i64;
9764        obj.base.culling = rd.i32()? as i64;
9765        obj.base.alpha_test = rd.i32()? as i64;
9766        obj.base.alpha_blend = rd.i32()? as i64;
9767        obj.base.blend = rd.i32()? as i64;
9768        let _flags = rd.i32()?;
9769        obj.runtime.prop_event_lists.x_rep = Self::read_cpp_int_event_extend_list(rd)?;
9770        obj.runtime.prop_event_lists.y_rep = Self::read_cpp_int_event_extend_list(rd)?;
9771        obj.runtime.prop_event_lists.z_rep = Self::read_cpp_int_event_extend_list(rd)?;
9772        obj.runtime.prop_event_lists.tr_rep = Self::read_cpp_int_event_extend_list(rd)?;
9773        obj.runtime.prop_lists.f = rd.extend_i32_list()?;
9774        let file_name = rd.string()?;
9775        obj.file_name = if file_name.is_empty() {
9776            None
9777        } else {
9778            Some(file_name)
9779        };
9780        let string_value = rd.string()?;
9781        obj.string_value = if string_value.is_empty() {
9782            None
9783        } else {
9784            Some(string_value)
9785        };
9786        obj.button.decided_action_scn_name = rd.string()?;
9787        obj.button.decided_action_cmd_name = rd.string()?;
9788        if obj.object_type == 12 {
9789            for timeline in &mut obj.emote.timeline_names {
9790                *timeline = rd.string()?;
9791            }
9792        }
9793        obj.frame_action = Self::read_cpp_frame_action(rd)?;
9794        obj.frame_action_ch = rd.extend_items(|rd| Self::read_cpp_frame_action(rd))?;
9795        let gan_file = rd.string()?;
9796        obj.gan_file = if gan_file.is_empty() {
9797            None
9798        } else {
9799            Some(gan_file)
9800        };
9801        if rd.layout != NativeLocalLayout::Current {
9802            // C_tnm_gan::save follows the name with three ints and seven
9803            // bools, even for an object with no animation.
9804            obj.gan.read_original_work(rd)?;
9805        }
9806        obj.runtime.child_objects = rd.extend_items(|rd| Self::read_cpp_object(rd))?;
9807        obj.used = obj.object_type != 0 || obj.file_name.is_some() || obj.string_value.is_some();
9808        Ok(obj)
9809    }
9810
9811    fn write_cpp_mwnd_glyph(
9812        w: &mut crate::original_save::OriginalStreamWriter,
9813        glyph: &runtime::globals::MwndGlyphState,
9814    ) {
9815        // C_tnm_moji is eight consecutive 32-bit integers.
9816        w.push_i32(glyph.moji_type);
9817        w.push_i32(glyph.code);
9818        w.push_i32(Self::save_i32(glyph.size));
9819        w.push_i32(Self::save_i32(glyph.moji_color_no));
9820        w.push_i32(Self::save_i32(glyph.shadow_color_no));
9821        w.push_i32(Self::save_i32(glyph.fuchi_color_no));
9822        w.push_i32(Self::save_i32(glyph.x));
9823        w.push_i32(Self::save_i32(glyph.y));
9824        w.push_bool(glyph.appeared);
9825        w.push_bool(glyph.ruby);
9826    }
9827
9828    fn read_cpp_mwnd_glyph(
9829        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9830    ) -> Result<runtime::globals::MwndGlyphState> {
9831        if rd.layout.uses_early_records() {
9832            return Self::read_early_mwnd_glyph(rd);
9833        }
9834        let moji_type = rd.i32()?;
9835        let code = rd.i32()?;
9836        let size = rd.i32()? as i64;
9837        let moji_color_no = rd.i32()? as i64;
9838        let shadow_color_no = rd.i32()? as i64;
9839        let fuchi_color_no = rd.i32()? as i64;
9840        let x = rd.i32()? as i64;
9841        let y = rd.i32()? as i64;
9842        let appeared = rd.bool()?;
9843        let ruby = rd.bool()?;
9844        let ch = if moji_type == 0 {
9845            char::from_u32(code as u32).unwrap_or('\u{fffd}')
9846        } else {
9847            '\0'
9848        };
9849        Ok(runtime::globals::MwndGlyphState {
9850            moji_type,
9851            code,
9852            ch,
9853            x,
9854            y,
9855            size,
9856            moji_color_no,
9857            shadow_color_no,
9858            fuchi_color_no,
9859            shadow: shadow_color_no >= 0,
9860            fuchi: fuchi_color_no >= 0,
9861            bold: false,
9862            reveal_index: 1,
9863            ruby,
9864            appeared,
9865            message_button: None,
9866        })
9867    }
9868
9869    fn active_mwnd_message_page(
9870        m: &runtime::globals::MwndState,
9871    ) -> runtime::globals::MwndMessagePageState {
9872        runtime::globals::MwndMessagePageState {
9873            msg_text: m.msg_text.clone(),
9874            glyphs: m.glyphs.clone(),
9875            disp_moji_cnt: m.disp_moji_cnt,
9876            hide_moji_cnt: m.hide_moji_cnt,
9877            cur_msg_type: m.cur_msg_type,
9878            cur_msg_type_decided: m.cur_msg_type_decided,
9879            ruby_start_pos: m.ruby_start_pos,
9880            ruby_start_ready: m.ruby_start_ready,
9881            cursor_pos: m.cursor_pos,
9882            moji_rep_pos: m.moji_rep_pos,
9883            indent_pos: m.indent_pos,
9884            indent_moji: m.indent_moji,
9885            indent_count: m.indent_count,
9886            line_head: m.line_head,
9887            ruby_pending: m.ruby_pending.clone(),
9888            moji_size: m.moji_size,
9889            moji_color: m.moji_color,
9890            shadow_color: m.shadow_color,
9891            fuchi_color: m.fuchi_color,
9892            chara_moji_color: m.chara_moji_color,
9893            chara_shadow_color: m.chara_shadow_color,
9894            chara_fuchi_color: m.chara_fuchi_color,
9895            msgbtn: m.msgbtn,
9896        }
9897    }
9898
9899    fn write_cpp_mwnd_message(
9900        w: &mut crate::original_save::OriginalStreamWriter,
9901        m: &runtime::globals::MwndState,
9902        page: &runtime::globals::MwndMessagePageState,
9903    ) {
9904        // C_elm_mwnd_msg::PARAM (21 consecutive i32 fields).
9905        let (cnt_x, cnt_y) = m.window_moji_cnt.unwrap_or((0, 0));
9906        let (pos_x, pos_y) = m.message_pos.unwrap_or((0, 0));
9907        let (space_x, space_y) = m.moji_space.unwrap_or((-1, 10));
9908        let (talk_l, talk_t, talk_r, talk_b) = m.message_margin.unwrap_or((0, 0, 0, 0));
9909        for value in [
9910            cnt_x,
9911            cnt_y,
9912            pos_x,
9913            pos_y,
9914            page.moji_rep_pos.0,
9915            page.moji_rep_pos.1,
9916            page.moji_size.unwrap_or(m.default_moji_size),
9917            space_x,
9918            space_y,
9919            page.moji_color.unwrap_or(-1),
9920            page.shadow_color.unwrap_or(-1),
9921            page.fuchi_color.unwrap_or(-1),
9922            m.ruby_size,
9923            m.ruby_space,
9924            m.default_moji_size,
9925            0,
9926            m.name_bracket,
9927            talk_l,
9928            talk_t,
9929            talk_r,
9930            talk_b,
9931        ] {
9932            w.push_i32(Self::save_i32(value));
9933        }
9934        w.push_i32(Self::save_i32(page.chara_moji_color.unwrap_or(-1)));
9935        w.push_i32(Self::save_i32(page.chara_shadow_color.unwrap_or(-1)));
9936        w.push_i32(Self::save_i32(page.chara_fuchi_color.unwrap_or(-1)));
9937        w.push_i32(Self::save_i32(page.indent_pos));
9938        w.push_u16(page.indent_moji.unwrap_or('\0') as u32 as u16);
9939        w.push_i32(Self::save_i32(page.indent_count));
9940        w.push_i32(Self::save_i32(page.cur_msg_type));
9941        w.push_i32(Self::save_i32(page.ruby_start_pos.0));
9942        w.push_i32(Self::save_i32(page.ruby_start_pos.1));
9943        w.push_i32(Self::save_i32(page.disp_moji_cnt));
9944        w.push_i32(Self::save_i32(page.hide_moji_cnt));
9945        w.push_str(&page.msg_text);
9946        w.push_str(
9947            page.ruby_pending
9948                .as_ref()
9949                .map(|ruby| ruby.text.as_str())
9950                .unwrap_or(""),
9951        );
9952        let (btn_no, group_no, action_no, se_no) = page.msgbtn.unwrap_or((0, 0, 0, 0));
9953        w.push_i32(Self::save_i32(btn_no));
9954        w.push_i32(Self::save_i32(group_no));
9955        w.push_i32(Self::save_i32(action_no));
9956        w.push_i32(Self::save_i32(se_no));
9957        w.push_bool(page.cur_msg_type_decided);
9958        w.push_bool(page.line_head);
9959        w.push_bool(page.ruby_start_ready);
9960        w.push_bool(page.msgbtn.is_some());
9961        w.push_extend_items(&page.glyphs, |w, glyph| {
9962            Self::write_cpp_mwnd_glyph(w, glyph)
9963        });
9964    }
9965
9966    fn read_cpp_mwnd_message(
9967        rd: &mut crate::original_save::OriginalStreamReader<'_>,
9968        m: &mut runtime::globals::MwndState,
9969    ) -> Result<runtime::globals::MwndMessagePageState> {
9970        if rd.layout.uses_early_records() {
9971            return Self::read_early_mwnd_message(rd, m);
9972        }
9973        let cnt_x = rd.i32()? as i64;
9974        let cnt_y = rd.i32()? as i64;
9975        let pos_x = rd.i32()? as i64;
9976        let pos_y = rd.i32()? as i64;
9977        let rep_x = rd.i32()? as i64;
9978        let rep_y = rd.i32()? as i64;
9979        let moji_size = rd.i32()? as i64;
9980        let space_x = rd.i32()? as i64;
9981        let space_y = rd.i32()? as i64;
9982        let moji_color = rd.i32()? as i64;
9983        let shadow_color = rd.i32()? as i64;
9984        let fuchi_color = rd.i32()? as i64;
9985        let ruby_size = rd.i32()? as i64;
9986        let ruby_space = rd.i32()? as i64;
9987        let _name_moji_size = rd.i32()?;
9988        let _name_newline = rd.i32()?;
9989        let _name_bracket = rd.i32()?;
9990        let talk_l = rd.i32()? as i64;
9991        let talk_t = rd.i32()? as i64;
9992        let talk_r = rd.i32()? as i64;
9993        let talk_b = rd.i32()? as i64;
9994        m.window_moji_cnt = Some((cnt_x, cnt_y));
9995        m.message_pos = Some((pos_x, pos_y));
9996        m.moji_space = Some((space_x, space_y));
9997        m.message_margin = Some((talk_l, talk_t, talk_r, talk_b));
9998        m.ruby_size = ruby_size;
9999        m.ruby_space = ruby_space;
10000
10001        let chara_moji = rd.i32()? as i64;
10002        let chara_shadow = rd.i32()? as i64;
10003        let chara_fuchi = rd.i32()? as i64;
10004        let indent_pos = rd.i32()? as i64;
10005        let indent_u16 = rd.u16()?;
10006        let indent_count = rd.i32()? as i64;
10007        let cur_msg_type = rd.i32()? as i64;
10008        let ruby_x = rd.i32()? as i64;
10009        let ruby_y = rd.i32()? as i64;
10010        let disp_moji_cnt = rd.i32()? as i64;
10011        let hide_moji_cnt = rd.i32()? as i64;
10012        let debug_msg = rd.string()?;
10013        let ruby = rd.string()?;
10014        let btn_no = rd.i32()? as i64;
10015        let group_no = rd.i32()? as i64;
10016        let action_no = rd.i32()? as i64;
10017        let se_no = rd.i32()? as i64;
10018        let cur_msg_type_decided = rd.bool()?;
10019        let line_head = rd.bool()?;
10020        let ruby_start_ready = rd.bool()?;
10021        let button_flag = rd.bool()?;
10022        let mut glyphs = rd.extend_items(Self::read_cpp_mwnd_glyph)?;
10023        let mut body_index = 0usize;
10024        for glyph in &mut glyphs {
10025            if !glyph.ruby {
10026                body_index += 1;
10027            }
10028            glyph.reveal_index = body_index.max(1);
10029            if button_flag {
10030                glyph.message_button = Some(runtime::globals::MwndMessageButtonState {
10031                    btn_no,
10032                    group_no,
10033                    action_no,
10034                    se_no,
10035                });
10036            }
10037        }
10038        let msg_text = if !debug_msg.is_empty() {
10039            debug_msg
10040        } else {
10041            let units: Vec<u16> = glyphs
10042                .iter()
10043                .filter(|glyph| glyph.moji_type == 0 && !glyph.ruby)
10044                .map(|glyph| glyph.code as u16)
10045                .collect();
10046            String::from_utf16_lossy(&units)
10047        };
10048        Ok(runtime::globals::MwndMessagePageState {
10049            msg_text,
10050            glyphs,
10051            disp_moji_cnt,
10052            hide_moji_cnt,
10053            cur_msg_type,
10054            cur_msg_type_decided,
10055            ruby_start_pos: (ruby_x, ruby_y),
10056            ruby_start_ready,
10057            cursor_pos: (pos_x, pos_y),
10058            moji_rep_pos: (rep_x, rep_y),
10059            indent_pos,
10060            indent_moji: (indent_u16 != 0)
10061                .then(|| char::from_u32(indent_u16 as u32).unwrap_or('\u{fffd}')),
10062            indent_count,
10063            line_head,
10064            ruby_pending: (!ruby.is_empty()).then_some(runtime::globals::MwndRubyPendingState {
10065                text: ruby,
10066                start_pos: Some((ruby_x, ruby_y)),
10067            }),
10068            moji_size: Some(moji_size),
10069            moji_color: (moji_color >= 0).then_some(moji_color),
10070            shadow_color: (shadow_color >= 0).then_some(shadow_color),
10071            fuchi_color: (fuchi_color >= 0).then_some(fuchi_color),
10072            chara_moji_color: (chara_moji >= 0).then_some(chara_moji),
10073            chara_shadow_color: (chara_shadow >= 0).then_some(chara_shadow),
10074            chara_fuchi_color: (chara_fuchi >= 0).then_some(chara_fuchi),
10075            msgbtn: button_flag.then_some((btn_no, group_no, action_no, se_no)),
10076        })
10077    }
10078
10079    fn write_cpp_mwnd_waku(
10080        &self,
10081        w: &mut crate::original_save::OriginalStreamWriter,
10082        m: &runtime::globals::MwndState,
10083        name_waku: bool,
10084    ) {
10085        w.push_i32(Self::save_i32(m.msg_waku_no.unwrap_or(0)));
10086        w.push_str(if name_waku { "" } else { &m.waku_file });
10087        w.push_str(if name_waku { "" } else { &m.filter_file });
10088        let margin = if name_waku {
10089            (0, 0, 0, 0)
10090        } else {
10091            m.filter_margin.unwrap_or((0, 0, 0, 0))
10092        };
10093        for value in [margin.0, margin.1, margin.2, margin.3] {
10094            w.push_i32(Self::save_i32(value));
10095        }
10096        let (a, r, g, b) = if name_waku {
10097            (0, 0, 0, 0)
10098        } else {
10099            m.filter_color.unwrap_or((0, 0, 0, 0))
10100        };
10101        w.push_raw(&[b, g, r, a]);
10102        w.push_bool(!name_waku && m.filter_config_color);
10103        w.push_bool(!name_waku && m.filter_config_tr);
10104        // MSVC rounds STATE (22 bytes) up to four-byte alignment.
10105        w.push_padding(2);
10106        w.push_i32(Self::save_i32(m.msg_waku_no.unwrap_or(0)));
10107        w.push_i32(Self::save_i32(if name_waku { 0 } else { m.key_icon_mode }));
10108        let icon_pos = if name_waku { None } else { m.key_icon_pos };
10109        w.push_i32(Self::save_i32(icon_pos.map(|p| p.0).unwrap_or(0)));
10110        w.push_i32(Self::save_i32(icon_pos.map(|p| p.1).unwrap_or(0)));
10111        let faces: &[runtime::globals::ObjectState] = if name_waku { &[] } else { &m.face_list };
10112        let objects: &[runtime::globals::ObjectState] =
10113            if name_waku { &[] } else { &m.object_list };
10114        w.push_fixed_items(faces, |w, object| self.write_cpp_object(w, object));
10115        w.push_fixed_items(objects, |w, object| self.write_cpp_object(w, object));
10116    }
10117
10118    fn read_cpp_mwnd_waku(
10119        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10120        m: &mut runtime::globals::MwndState,
10121        name_waku: bool,
10122    ) -> Result<()> {
10123        let template_no = rd.i32()? as i64;
10124        let waku_file = rd.string()?;
10125        let filter_file = rd.string()?;
10126        let margin = (
10127            rd.i32()? as i64,
10128            rd.i32()? as i64,
10129            rd.i32()? as i64,
10130            rd.i32()? as i64,
10131        );
10132        let color = rd.take_raw(4)?;
10133        let filter_config_color = rd.bool()?;
10134        let filter_config_tr = rd.bool()?;
10135        rd.skip(2)?;
10136        let _key_template = rd.i32()?;
10137        let key_mode = rd.i32()? as i64;
10138        let key_x = rd.i32()? as i64;
10139        let key_y = rd.i32()? as i64;
10140        let faces = rd.fixed_items(Self::read_cpp_object)?;
10141        let objects = rd.fixed_items(Self::read_cpp_object)?;
10142        if !name_waku {
10143            m.msg_waku_no = Some(template_no);
10144            m.waku_file = waku_file;
10145            m.filter_file = filter_file;
10146            m.filter_margin = Some(margin);
10147            m.filter_color = Some((color[3], color[2], color[1], color[0]));
10148            m.filter_config_color = filter_config_color;
10149            m.filter_config_tr = filter_config_tr;
10150            m.key_icon_mode = key_mode;
10151            m.key_icon_pos = Some((key_x, key_y));
10152            m.face_list = faces;
10153            m.object_list = objects;
10154        }
10155        Ok(())
10156    }
10157
10158    fn write_cpp_mwnd_name(
10159        w: &mut crate::original_save::OriginalStreamWriter,
10160        m: &runtime::globals::MwndState,
10161    ) {
10162        w.push_i32(0);
10163        let (space_x, space_y) = m.moji_space.unwrap_or((-1, 10));
10164        let (pos_x, pos_y) = m.name_message_pos;
10165        for value in [
10166            pos_x,
10167            pos_y,
10168            m.default_moji_size,
10169            space_x,
10170            space_y,
10171            m.name_text.chars().count() as i64,
10172            m.name_window_align,
10173            m.name_moji_color.unwrap_or(-1),
10174            m.name_shadow_color.unwrap_or(-1),
10175            m.name_fuchi_color.unwrap_or(-1),
10176        ] {
10177            w.push_i32(Self::save_i32(value));
10178        }
10179        w.push_str(&m.name_text);
10180        for value in [
10181            m.name_window_rect.0,
10182            m.name_window_rect.1,
10183            m.name_window_rect.2,
10184            m.name_window_rect.3,
10185        ] {
10186            w.push_i32(Self::save_i32(value));
10187        }
10188        w.push_extend_items(&m.name_glyphs, |w, glyph| {
10189            Self::write_cpp_mwnd_glyph(w, glyph)
10190        });
10191    }
10192
10193    fn read_cpp_mwnd_name(
10194        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10195        m: &mut runtime::globals::MwndState,
10196    ) -> Result<()> {
10197        if rd.layout.uses_early_records() {
10198            return Self::read_early_mwnd_name(rd, m);
10199        }
10200        let _template_no = rd.i32()?;
10201        let pos_x = rd.i32()? as i64;
10202        let pos_y = rd.i32()? as i64;
10203        let _size = rd.i32()?;
10204        let _space_x = rd.i32()?;
10205        let _space_y = rd.i32()?;
10206        let _cnt = rd.i32()?;
10207        m.name_window_align = rd.i32()? as i64;
10208        let name_moji_color = rd.i32()? as i64;
10209        let name_shadow_color = rd.i32()? as i64;
10210        let name_fuchi_color = rd.i32()? as i64;
10211        m.name_moji_color = (name_moji_color >= 0).then_some(name_moji_color);
10212        m.name_shadow_color = (name_shadow_color >= 0).then_some(name_shadow_color);
10213        m.name_fuchi_color = (name_fuchi_color >= 0).then_some(name_fuchi_color);
10214        m.name_text = rd.string()?;
10215        m.name_message_pos = (pos_x, pos_y);
10216        m.name_window_rect = (
10217            rd.i32()? as i64,
10218            rd.i32()? as i64,
10219            rd.i32()? as i64,
10220            rd.i32()? as i64,
10221        );
10222        m.name_glyphs = rd.extend_items(Self::read_cpp_mwnd_glyph)?;
10223        Ok(())
10224    }
10225
10226    fn write_cpp_mwnd_selection(
10227        w: &mut crate::original_save::OriginalStreamWriter,
10228        m: &runtime::globals::MwndState,
10229    ) {
10230        w.push_i32(0);
10231        let (cnt_x, cnt_y) = m.window_moji_cnt.unwrap_or((0, 0));
10232        let (pos_x, pos_y) = m.message_pos.unwrap_or((0, 0));
10233        let (space_x, space_y) = m.moji_space.unwrap_or((-1, 10));
10234        for value in [
10235            cnt_x,
10236            cnt_y,
10237            pos_x,
10238            pos_y,
10239            m.default_moji_size,
10240            space_x,
10241            space_y,
10242            m.default_moji_color,
10243            m.default_shadow_color,
10244            m.default_fuchi_color,
10245        ] {
10246            w.push_i32(Self::save_i32(value));
10247        }
10248        let selection = m.selection.as_ref();
10249        w.push_i32(
10250            selection
10251                .map(|sel| sel.disp_item_count.min(i32::MAX as usize) as i32)
10252                .unwrap_or(0),
10253        );
10254        w.push_bool(selection.is_some_and(|sel| sel.cancel_enable));
10255        let choices: &[runtime::globals::MwndSelectionChoice] =
10256            selection.map(|sel| sel.choices.as_slice()).unwrap_or(&[]);
10257        w.push_u32(choices.len().min(u32::MAX as usize) as u32);
10258        for choice in choices {
10259            w.push_i32(Self::save_i32(choice.kind));
10260            w.push_i32(Self::save_i32(choice.pos.0));
10261            w.push_i32(Self::save_i32(choice.pos.1));
10262            w.push_str(&choice.text);
10263            let fallback_width = choice.text.chars().count() as i64 * m.default_moji_size;
10264            w.push_i32(Self::save_i32(if choice.size.0 != 0 {
10265                choice.size.0
10266            } else {
10267                fallback_width
10268            }));
10269            w.push_i32(Self::save_i32(if choice.size.1 != 0 {
10270                choice.size.1
10271            } else {
10272                m.default_moji_size
10273            }));
10274            w.push_extend_items(&choice.glyphs, |w, glyph| {
10275                Self::write_cpp_mwnd_glyph(w, glyph)
10276            });
10277        }
10278    }
10279
10280    fn read_cpp_mwnd_selection(
10281        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10282        m: &mut runtime::globals::MwndState,
10283    ) -> Result<()> {
10284        if rd.layout.uses_early_records() {
10285            return Self::read_early_mwnd_selection(rd, m);
10286        }
10287        let _template = rd.i32()?;
10288        for _ in 0..10 {
10289            let _ = rd.i32()?;
10290        }
10291        let disp_item_count = rd.i32()?.max(0) as usize;
10292        let cancel_enable = rd.bool()?;
10293        let count = rd.i32()?.max(0) as usize;
10294        let mut choices = Vec::with_capacity(count);
10295        for _ in 0..count {
10296            let kind = rd.i32()? as i64;
10297            let x = rd.i32()? as i64;
10298            let y = rd.i32()? as i64;
10299            let text = rd.string()?;
10300            let sx = rd.i32()? as i64;
10301            let sy = rd.i32()? as i64;
10302            let glyphs = rd.extend_items(Self::read_cpp_mwnd_glyph)?;
10303            let color = glyphs
10304                .first()
10305                .map(|glyph| glyph.moji_color_no)
10306                .unwrap_or(m.default_moji_color);
10307            choices.push(runtime::globals::MwndSelectionChoice {
10308                text,
10309                kind,
10310                color,
10311                pos: (x, y),
10312                size: (sx, sy),
10313                glyphs,
10314            });
10315        }
10316        if !choices.is_empty() || cancel_enable {
10317            m.selection = Some(runtime::globals::MwndSelectionState {
10318                choices,
10319                disp_item_count,
10320                cursor: 0,
10321                cancel_enable,
10322                close_mwnd: false,
10323                result: 0,
10324            });
10325        }
10326        Ok(())
10327    }
10328
10329    fn write_cpp_mwnd(
10330        &self,
10331        w: &mut crate::original_save::OriginalStreamWriter,
10332        m: &runtime::globals::MwndState,
10333    ) {
10334        // C_elm_mwnd::PARAM (43 consecutive i32 fields).
10335        let (window_x, window_y) = m.window_pos.unwrap_or((0, 0));
10336        let (window_w, window_h) = m.window_size.unwrap_or((0, 0));
10337        let (msg_x, msg_y) = m.message_pos.unwrap_or((0, 0));
10338        let (margin_l, margin_t, margin_r, margin_b) = m.message_margin.unwrap_or((0, 0, 0, 0));
10339        let (cnt_x, cnt_y) = m.window_moji_cnt.unwrap_or((0, 0));
10340        for value in [
10341            m.order,
10342            m.layer,
10343            m.world,
10344            m.novel_mode,
10345            m.mwnd_extend_type,
10346            window_x,
10347            window_y,
10348            window_w,
10349            window_h,
10350            msg_x,
10351            msg_y,
10352            margin_l,
10353            margin_t,
10354            margin_r,
10355            margin_b,
10356            cnt_x,
10357            cnt_y,
10358            m.name_disp_mode,
10359            m.name_bracket,
10360            m.name_extend_type,
10361            m.name_window_align,
10362            m.name_window_pos.0,
10363            m.name_window_pos.1,
10364            m.name_window_size.0,
10365            m.name_window_size.1,
10366            m.name_window_rect.0,
10367            m.name_window_rect.1,
10368            m.name_window_rect.2,
10369            m.name_window_rect.3,
10370            m.name_message_pos.0,
10371            m.name_message_pos.1,
10372            m.name_message_pos_rep.0,
10373            m.name_message_pos_rep.1,
10374            m.name_message_margin.0,
10375            m.name_message_margin.1,
10376            m.name_message_margin.2,
10377            m.name_message_margin.3,
10378            m.overflow_check_size,
10379            m.face_hide_name,
10380            m.open_anime_type,
10381            m.open_anime_time,
10382            m.close_anime_type,
10383            m.close_anime_time,
10384        ] {
10385            w.push_i32(Self::save_i32(value));
10386        }
10387        w.push_i32(Self::save_i32(m.time));
10388        w.push_bool(m.msg_block_started);
10389        w.push_bool(m.auto_proc_ready);
10390        w.push_bool(m.window_appear || m.open);
10391        w.push_bool(m.name_appear || !m.name_text.is_empty());
10392        w.push_bool(m.clear_ready);
10393        w.push_i32(Self::save_i32(m.auto_mode_end_moji_cnt));
10394        w.push_i32(Self::save_i32(m.target_msg_no));
10395        w.push_bool(m.slide_msg);
10396        w.push_i32(Self::save_i32(m.slide_time));
10397        w.push_i32(m.koe.map(|value| Self::save_i32(value.0)).unwrap_or(-1));
10398        w.push_bool(m.koe_play_flag || m.koe.is_some());
10399        w.push_i32(Self::save_i32(m.open_anime_type));
10400        w.push_i32(Self::save_i32(m.open_anime_time));
10401        w.push_i32(Self::save_i32(m.open_anime_start_time));
10402        w.push_i32(Self::save_i32(m.close_anime_type));
10403        w.push_i32(Self::save_i32(m.close_anime_time));
10404        w.push_i32(Self::save_i32(m.close_anime_start_time));
10405
10406        w.push_i32((m.message_pages.len() + 1).min(i32::MAX as usize) as i32);
10407        for page in &m.message_pages {
10408            Self::write_cpp_mwnd_message(w, m, page);
10409        }
10410        let active = Self::active_mwnd_message_page(m);
10411        Self::write_cpp_mwnd_message(w, m, &active);
10412        self.write_cpp_mwnd_waku(w, m, false);
10413        Self::write_cpp_mwnd_name(w, m);
10414        self.write_cpp_mwnd_waku(w, m, true);
10415        Self::write_cpp_mwnd_selection(w, m);
10416    }
10417
10418    fn read_cpp_mwnd(
10419        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10420    ) -> Result<runtime::globals::MwndState> {
10421        let mut m = runtime::globals::MwndState::default();
10422        m.order = rd.i32()? as i64;
10423        m.layer = rd.i32()? as i64;
10424        m.world = rd.i32()? as i64;
10425        m.novel_mode = rd.i32()? as i64;
10426        m.mwnd_extend_type = rd.i32()? as i64;
10427        m.window_pos = Some((rd.i32()? as i64, rd.i32()? as i64));
10428        m.window_size = Some((rd.i32()? as i64, rd.i32()? as i64));
10429        m.message_pos = Some((rd.i32()? as i64, rd.i32()? as i64));
10430        m.message_margin = Some((
10431            rd.i32()? as i64,
10432            rd.i32()? as i64,
10433            rd.i32()? as i64,
10434            rd.i32()? as i64,
10435        ));
10436        m.window_moji_cnt = Some((rd.i32()? as i64, rd.i32()? as i64));
10437        m.name_disp_mode = rd.i32()? as i64;
10438        m.name_bracket = rd.i32()? as i64;
10439        m.name_extend_type = rd.i32()? as i64;
10440        m.name_window_align = rd.i32()? as i64;
10441        m.name_window_pos = (rd.i32()? as i64, rd.i32()? as i64);
10442        m.name_window_size = (rd.i32()? as i64, rd.i32()? as i64);
10443        m.name_window_rect = (
10444            rd.i32()? as i64,
10445            rd.i32()? as i64,
10446            rd.i32()? as i64,
10447            rd.i32()? as i64,
10448        );
10449        m.name_message_pos = (rd.i32()? as i64, rd.i32()? as i64);
10450        m.name_message_pos_rep = (rd.i32()? as i64, rd.i32()? as i64);
10451        m.name_message_margin = (
10452            rd.i32()? as i64,
10453            rd.i32()? as i64,
10454            rd.i32()? as i64,
10455            rd.i32()? as i64,
10456        );
10457        m.overflow_check_size = rd.i32()? as i64;
10458        m.face_hide_name = rd.i32()? as i64;
10459        m.open_anime_type = rd.i32()? as i64;
10460        m.open_anime_time = rd.i32()? as i64;
10461        m.close_anime_type = rd.i32()? as i64;
10462        m.close_anime_time = rd.i32()? as i64;
10463        m.time = rd.i32()? as i64;
10464        m.msg_block_started = rd.bool()?;
10465        m.auto_proc_ready = rd.bool()?;
10466        m.window_appear = rd.bool()?;
10467        m.open = m.window_appear;
10468        m.name_appear = rd.bool()?;
10469        m.clear_ready = rd.bool()?;
10470        m.auto_mode_end_moji_cnt = rd.i32()? as i64;
10471        m.target_msg_no = rd.i32()? as i64;
10472        m.slide_msg = rd.bool()?;
10473        m.slide_time = rd.i32()? as i64;
10474        let koe_no = rd.i32()? as i64;
10475        m.koe_play_flag = rd.bool()?;
10476        if m.koe_play_flag {
10477            m.koe = Some((koe_no, 0));
10478        }
10479        m.open_anime_type = rd.i32()? as i64;
10480        m.open_anime_time = rd.i32()? as i64;
10481        m.open_anime_start_time = rd.i32()? as i64;
10482        m.close_anime_type = rd.i32()? as i64;
10483        m.close_anime_time = rd.i32()? as i64;
10484        m.close_anime_start_time = rd.i32()? as i64;
10485
10486        let message_count = rd.i32()?.max(0) as usize;
10487        let mut pages = Vec::with_capacity(message_count.max(1));
10488        for _ in 0..message_count {
10489            pages.push(Self::read_cpp_mwnd_message(rd, &mut m)?);
10490        }
10491        let active_index = m
10492            .target_msg_no
10493            .clamp(0, pages.len().saturating_sub(1) as i64) as usize;
10494        if let Some(active) = pages.get(active_index).cloned() {
10495            m.message_pages = pages[..active_index].to_vec();
10496            m.msg_text = active.msg_text;
10497            m.glyphs = active.glyphs;
10498            m.disp_moji_cnt = active.disp_moji_cnt;
10499            m.hide_moji_cnt = active.hide_moji_cnt;
10500            m.cur_msg_type = active.cur_msg_type;
10501            m.cur_msg_type_decided = active.cur_msg_type_decided;
10502            m.ruby_start_pos = active.ruby_start_pos;
10503            m.ruby_start_ready = active.ruby_start_ready;
10504            m.cursor_pos = active.cursor_pos;
10505            m.moji_rep_pos = active.moji_rep_pos;
10506            m.indent_pos = active.indent_pos;
10507            m.indent_moji = active.indent_moji;
10508            m.indent_count = active.indent_count;
10509            m.line_head = active.line_head;
10510            m.ruby_pending = active.ruby_pending;
10511            m.moji_size = active.moji_size;
10512            m.moji_color = active.moji_color;
10513            m.shadow_color = active.shadow_color;
10514            m.fuchi_color = active.fuchi_color;
10515            m.chara_moji_color = active.chara_moji_color;
10516            m.chara_shadow_color = active.chara_shadow_color;
10517            m.chara_fuchi_color = active.chara_fuchi_color;
10518            m.msgbtn = active.msgbtn;
10519        }
10520        let mut reveal_index = 0usize;
10521        for page in &mut m.message_pages {
10522            for glyph in &mut page.glyphs {
10523                if !glyph.ruby {
10524                    reveal_index += 1;
10525                }
10526                glyph.reveal_index = reveal_index.max(1);
10527            }
10528        }
10529        for glyph in &mut m.glyphs {
10530            if !glyph.ruby {
10531                reveal_index += 1;
10532            }
10533            glyph.reveal_index = reveal_index.max(1);
10534        }
10535        Self::read_cpp_mwnd_waku(rd, &mut m, false)?;
10536        Self::read_cpp_mwnd_name(rd, &mut m)?;
10537        Self::read_cpp_mwnd_waku(rd, &mut m, true)?;
10538        Self::read_cpp_mwnd_selection(rd, &mut m)?;
10539        Ok(m)
10540    }
10541
10542    fn write_cpp_world(
10543        &self,
10544        w: &mut crate::original_save::OriginalStreamWriter,
10545        world: &runtime::globals::WorldState,
10546    ) {
10547        w.push_i32(world.mode);
10548        for e in [
10549            &world.camera_eye_x,
10550            &world.camera_eye_y,
10551            &world.camera_eye_z,
10552            &world.camera_pint_x,
10553            &world.camera_pint_y,
10554            &world.camera_pint_z,
10555            &world.camera_up_x,
10556            &world.camera_up_y,
10557            &world.camera_up_z,
10558        ] {
10559            Self::write_cpp_int_event_raw(w, e);
10560        }
10561        for v in [
10562            world.camera_view_angle,
10563            world.mono,
10564            world.order,
10565            world.layer,
10566            world.wipe_copy,
10567            world.wipe_erase,
10568        ] {
10569            w.push_i32(v);
10570        }
10571    }
10572
10573    fn read_cpp_world(
10574        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10575        world_no: i32,
10576    ) -> Result<runtime::globals::WorldState> {
10577        match rd.layout {
10578            NativeLocalLayout::ShortElements => return Self::read_short_world(rd, world_no),
10579            NativeLocalLayout::Early | NativeLocalLayout::Indexed => {
10580                return Self::read_early_world(rd, world_no);
10581            }
10582            NativeLocalLayout::Legacy | NativeLocalLayout::Current => {}
10583        }
10584        let mut world = runtime::globals::WorldState::new(world_no);
10585        world.mode = rd.i32()?;
10586        world.camera_eye_x = Self::read_cpp_int_event_raw(rd)?;
10587        world.camera_eye_y = Self::read_cpp_int_event_raw(rd)?;
10588        world.camera_eye_z = Self::read_cpp_int_event_raw(rd)?;
10589        world.camera_pint_x = Self::read_cpp_int_event_raw(rd)?;
10590        world.camera_pint_y = Self::read_cpp_int_event_raw(rd)?;
10591        world.camera_pint_z = Self::read_cpp_int_event_raw(rd)?;
10592        world.camera_up_x = Self::read_cpp_int_event_raw(rd)?;
10593        world.camera_up_y = Self::read_cpp_int_event_raw(rd)?;
10594        world.camera_up_z = Self::read_cpp_int_event_raw(rd)?;
10595        world.camera_view_angle = rd.i32()?;
10596        world.mono = rd.i32()?;
10597        world.order = rd.i32()?;
10598        world.layer = rd.i32()?;
10599        world.wipe_copy = rd.i32()?;
10600        world.wipe_erase = rd.i32()?;
10601        Ok(world)
10602    }
10603
10604    fn write_cpp_effect(
10605        &self,
10606        w: &mut crate::original_save::OriginalStreamWriter,
10607        e: &runtime::globals::ScreenEffectState,
10608    ) {
10609        for ev in [
10610            &e.x,
10611            &e.y,
10612            &e.z,
10613            &e.mono,
10614            &e.reverse,
10615            &e.bright,
10616            &e.dark,
10617            &e.color_r,
10618            &e.color_g,
10619            &e.color_b,
10620            &e.color_rate,
10621            &e.color_add_r,
10622            &e.color_add_g,
10623            &e.color_add_b,
10624        ] {
10625            Self::write_cpp_int_event_raw(w, ev);
10626        }
10627        for v in [
10628            e.begin_order,
10629            e.end_order,
10630            e.begin_layer,
10631            e.end_layer,
10632            e.wipe_copy,
10633            e.wipe_erase,
10634        ] {
10635            w.push_i32(v);
10636        }
10637    }
10638
10639    fn read_cpp_effect(
10640        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10641    ) -> Result<runtime::globals::ScreenEffectState> {
10642        if rd.layout == NativeLocalLayout::ShortElements {
10643            return Self::read_short_effect(rd);
10644        }
10645        let mut e = runtime::globals::ScreenEffectState {
10646            x: Self::read_cpp_int_event_raw(rd)?,
10647            y: Self::read_cpp_int_event_raw(rd)?,
10648            z: Self::read_cpp_int_event_raw(rd)?,
10649            mono: Self::read_cpp_int_event_raw(rd)?,
10650            reverse: Self::read_cpp_int_event_raw(rd)?,
10651            bright: Self::read_cpp_int_event_raw(rd)?,
10652            dark: Self::read_cpp_int_event_raw(rd)?,
10653            color_r: Self::read_cpp_int_event_raw(rd)?,
10654            color_g: Self::read_cpp_int_event_raw(rd)?,
10655            color_b: Self::read_cpp_int_event_raw(rd)?,
10656            color_rate: Self::read_cpp_int_event_raw(rd)?,
10657            color_add_r: Self::read_cpp_int_event_raw(rd)?,
10658            color_add_g: Self::read_cpp_int_event_raw(rd)?,
10659            color_add_b: Self::read_cpp_int_event_raw(rd)?,
10660            begin_order: rd.i32()?,
10661            end_order: rd.i32()?,
10662            begin_layer: rd.i32()?,
10663            end_layer: rd.i32()?,
10664            wipe_copy: rd.i32()?,
10665            wipe_erase: rd.i32()?,
10666        };
10667
10668        Ok(e)
10669    }
10670
10671    fn write_cpp_quake(
10672        &self,
10673        w: &mut crate::original_save::OriginalStreamWriter,
10674        q: &runtime::globals::ScreenQuakeState,
10675    ) {
10676        w.push_i32(q.quake_type);
10677        w.push_i32(q.vec);
10678        w.push_i32(q.power);
10679        w.push_i32(q.cur_time);
10680        w.push_i32(q.total_time);
10681        w.push_i32(if q.ending { 1 } else { 0 });
10682        w.push_i32(q.end_cur_time);
10683        w.push_i32(q.end_total_time);
10684        w.push_i32(q.cnt);
10685        w.push_i32(q.end_cnt);
10686        w.push_i32(q.center_x);
10687        w.push_i32(q.center_y);
10688        w.push_i32(q.begin_order);
10689        w.push_i32(q.end_order);
10690    }
10691
10692    fn read_cpp_quake(
10693        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10694    ) -> Result<runtime::globals::ScreenQuakeState> {
10695        let mut q = runtime::globals::ScreenQuakeState {
10696            quake_type: rd.i32()?,
10697            vec: rd.i32()?,
10698            power: rd.i32()?,
10699            cur_time: rd.i32()?,
10700            total_time: rd.i32()?,
10701            ending: rd.i32()? != 0,
10702            end_cur_time: rd.i32()?,
10703            end_total_time: rd.i32()?,
10704            cnt: rd.i32()?,
10705            end_cnt: rd.i32()?,
10706            center_x: rd.i32()?,
10707            center_y: rd.i32()?,
10708            begin_order: rd.i32()?,
10709            end_order: rd.i32()?,
10710        };
10711
10712        Ok(q)
10713    }
10714
10715    fn cpp_btn_select_param(&self, state: &runtime::globals::BtnSelectRuntimeState) -> [i64; 28] {
10716        if let Some(saved) = state.saved_cur_param {
10717            return saved;
10718        }
10719
10720        // C_elm_btn_select::init initializes an empty m_def/m_cur with only
10721        // btn_action_no set to -1.  set_template() then replaces every field
10722        // below from Gameexe.ini and copies m_def into m_cur.
10723        let mut param = [0i64; 28];
10724        param[20] = -1;
10725        let Some(tmpl) = (state.template_no >= 0)
10726            .then(|| {
10727                self.ctx
10728                    .tables
10729                    .sel_btn_templates
10730                    .get(state.template_no as usize)
10731            })
10732            .flatten()
10733        else {
10734            return param;
10735        };
10736        param = [
10737            tmpl.base_pos.0,
10738            tmpl.base_pos.1,
10739            tmpl.rep_pos.0,
10740            tmpl.rep_pos.1,
10741            tmpl.x_align,
10742            tmpl.y_align,
10743            tmpl.max_y_cnt,
10744            tmpl.line_width,
10745            tmpl.moji_cnt,
10746            tmpl.moji_pos.0,
10747            tmpl.moji_pos.1,
10748            tmpl.moji_size,
10749            tmpl.moji_space.0,
10750            tmpl.moji_space.1,
10751            tmpl.moji_x_align,
10752            tmpl.moji_y_align,
10753            tmpl.moji_color,
10754            tmpl.moji_hit_color,
10755            self.ctx.tables.mwnd_render.shadow_color,
10756            self.ctx.tables.mwnd_render.fuchi_color,
10757            tmpl.btn_action_no,
10758            tmpl.open_anime_type,
10759            tmpl.open_anime_time,
10760            tmpl.close_anime_type,
10761            tmpl.close_anime_time,
10762            tmpl.decide_anime_type,
10763            tmpl.decide_anime_time,
10764            state.sync_type,
10765        ];
10766        param
10767    }
10768
10769    fn synthesize_cpp_btn_select_glyphs(
10770        text: &str,
10771        param: &[i64; 28],
10772        color: i64,
10773        appeared: bool,
10774    ) -> Vec<runtime::globals::MwndGlyphState> {
10775        let size = param[11];
10776        let space_x = param[12];
10777        let mut x = 0i64;
10778        let mut glyphs = Vec::with_capacity(text.chars().count());
10779        for ch in text.chars() {
10780            glyphs.push(runtime::globals::MwndGlyphState {
10781                moji_type: 0,
10782                code: ch as u32 as i32,
10783                ch,
10784                x: param[9].saturating_add(x),
10785                y: param[10],
10786                size,
10787                moji_color_no: color,
10788                shadow_color_no: param[18],
10789                fuchi_color_no: param[19],
10790                shadow: param[18] >= 0,
10791                fuchi: param[19] >= 0,
10792                bold: false,
10793                reveal_index: glyphs.len().saturating_add(1),
10794                ruby: false,
10795                appeared,
10796                message_button: None,
10797            });
10798            let full_advance = size.saturating_add(space_x);
10799            let half_width = ch.is_ascii() || matches!(ch as u32, 0xff61..=0xff9f);
10800            x = x.saturating_add(if half_width {
10801                full_advance / 2
10802            } else {
10803                full_advance
10804            });
10805        }
10806
10807        // C_elm_btn_select_item::set_msg removes one trailing spacing unit
10808        // before applying horizontal/vertical alignment to every glyph.
10809        let total_x = if glyphs.is_empty() {
10810            0
10811        } else {
10812            x.saturating_sub(space_x)
10813        };
10814        let rep_x = match param[14] {
10815            1 => -total_x / 2,
10816            2 => -total_x,
10817            _ => 0,
10818        };
10819        let rep_y = match param[15] {
10820            1 => -size / 2,
10821            2 => -size,
10822            _ => 0,
10823        };
10824        for glyph in &mut glyphs {
10825            glyph.x = glyph.x.saturating_add(rep_x);
10826            glyph.y = glyph.y.saturating_add(rep_y);
10827        }
10828        glyphs
10829    }
10830
10831    fn write_cpp_btn_select(
10832        &self,
10833        w: &mut crate::original_save::OriginalStreamWriter,
10834        state: Option<&runtime::globals::BtnSelectRuntimeState>,
10835    ) {
10836        let empty = runtime::globals::BtnSelectRuntimeState::default();
10837        let state = state.unwrap_or(&empty);
10838        let param = self.cpp_btn_select_param(state);
10839        w.push_i32(Self::save_i32(state.template_no));
10840        for value in param {
10841            w.push_i32(Self::save_i32(value));
10842        }
10843        w.push_bool(state.appear_flag);
10844        w.push_bool(state.processing_flag_0);
10845        w.push_bool(state.processing_flag_1);
10846        w.push_bool(state.processing_flag_2);
10847        w.push_bool(state.cancel_enable);
10848        w.push_bool(state.capture_flag);
10849        w.push_str(&state.sel_start_call_scn);
10850        w.push_i32(Self::save_i32(state.sel_start_call_z_no));
10851
10852        let loaded = state.saved_cur_param.is_some();
10853        w.push_extend_items(&state.choices, |w, choice| {
10854            let item_template_no = if choice.template_no >= 0 {
10855                choice.template_no
10856            } else {
10857                state.template_no
10858            };
10859            let item_template = (item_template_no >= 0)
10860                .then(|| {
10861                    self.ctx
10862                        .tables
10863                        .sel_btn_templates
10864                        .get(item_template_no as usize)
10865                })
10866                .flatten();
10867            let base_file = if loaded || !choice.base_file.is_empty() {
10868                choice.base_file.as_str()
10869            } else {
10870                item_template.map(|t| t.base_file.as_str()).unwrap_or("")
10871            };
10872            let filter_file = if loaded || !choice.filter_file.is_empty() {
10873                choice.filter_file.as_str()
10874            } else {
10875                item_template.map(|t| t.filter_file.as_str()).unwrap_or("")
10876            };
10877            let color = if choice.color >= 0 {
10878                choice.color
10879            } else {
10880                param[16]
10881            };
10882            let relative_x = choice.pos.0.saturating_sub(param[0]);
10883            let relative_y = choice.pos.1.saturating_sub(param[1]);
10884            let generated;
10885            let glyphs = if choice.glyphs.is_empty() {
10886                generated = Self::synthesize_cpp_btn_select_glyphs(
10887                    &choice.text,
10888                    &param,
10889                    color,
10890                    state.appear_flag
10891                        || state.processing_flag_0
10892                        || state.processing_flag_1
10893                        || state.processing_flag_2,
10894                );
10895                generated.as_slice()
10896            } else {
10897                choice.glyphs.as_slice()
10898            };
10899
10900            w.push_i32(Self::save_i32(item_template_no));
10901            w.push_str(base_file);
10902            w.push_str(filter_file);
10903            w.push_str(&choice.text);
10904            w.push_i32(Self::save_i32(choice.item_type));
10905            w.push_i32(Self::save_i32(color));
10906            w.push_i32(Self::save_i32(relative_x));
10907            w.push_i32(Self::save_i32(relative_y));
10908            w.push_extend_items(glyphs, Self::write_cpp_mwnd_glyph);
10909        });
10910    }
10911
10912    fn read_cpp_btn_select(
10913        rd: &mut crate::original_save::OriginalStreamReader<'_>,
10914    ) -> Result<runtime::globals::BtnSelectRuntimeState> {
10915        if rd.layout.uses_early_records() {
10916            return Self::read_early_btn_select(rd);
10917        }
10918        let template_no = rd.i32()? as i64;
10919        let mut param = [0i64; 28];
10920        for value in &mut param {
10921            *value = rd.i32()? as i64;
10922        }
10923        let appear_flag = rd.bool()?;
10924        let processing_flag_0 = rd.bool()?;
10925        let processing_flag_1 = rd.bool()?;
10926        let processing_flag_2 = rd.bool()?;
10927        let cancel_enable = rd.bool()?;
10928        let capture_flag = rd.bool()?;
10929        let sel_start_call_scn = rd.string()?;
10930        let sel_start_call_z_no = rd.i32()? as i64;
10931        let choices = rd.extend_items(|rd| {
10932            let item_template_no = rd.i32()? as i64;
10933            let base_file = rd.string()?;
10934            let filter_file = rd.string()?;
10935            let text = rd.string()?;
10936            let item_type = rd.i32()? as i64;
10937            let color = rd.i32()? as i64;
10938            let relative_x = rd.i32()? as i64;
10939            let relative_y = rd.i32()? as i64;
10940            let glyphs = rd.extend_items(Self::read_cpp_mwnd_glyph)?;
10941            Ok(runtime::globals::BtnSelectChoiceState {
10942                template_no: item_template_no,
10943                base_file,
10944                filter_file,
10945                text,
10946                item_type,
10947                color,
10948                pos: (
10949                    param[0].saturating_add(relative_x),
10950                    param[1].saturating_add(relative_y),
10951                ),
10952                size: (0, 0),
10953                glyphs,
10954            })
10955        })?;
10956        let cursor = choices
10957            .iter()
10958            .position(|choice| choice.item_type == 1)
10959            .unwrap_or(0);
10960
10961        Ok(runtime::globals::BtnSelectRuntimeState {
10962            template_no,
10963            saved_cur_param: Some(param),
10964            choices,
10965            cursor,
10966            pressed_index: None,
10967            pressed_inside: false,
10968            cancel_enable,
10969            capture_flag,
10970            // load() calls restruct_template() after init_work_variable().  The
10971            // saved processing flags are restored, but m_sync_type and every
10972            // animation work member remain zero exactly as in the C++ object.
10973            started: processing_flag_0,
10974            result: 0,
10975            sync_type: 0,
10976            read_flag_scene_no: -1,
10977            read_flag_flag_no: -1,
10978            sel_start_call_scn,
10979            sel_start_call_z_no,
10980            appear_flag,
10981            open_anime_type: 0,
10982            open_anime_time: 0,
10983            open_anime_cur_time: 0,
10984            close_anime_type: 0,
10985            close_anime_time: 0,
10986            close_anime_cur_time: 0,
10987            decide_anime_type: 0,
10988            decide_anime_time: 0,
10989            decide_anime_cur_time: 0,
10990            decide_sel_no: -1,
10991            processing_flag_0,
10992            processing_flag_1,
10993            processing_flag_2,
10994            capture_now_flag: false,
10995            result_delivered: false,
10996        })
10997    }
10998
10999    fn write_cpp_stage(&self, w: &mut crate::original_save::OriginalStreamWriter, stage_idx: i64) {
11000        let form_id = if self.ctx.ids.form_global_stage != 0 {
11001            self.ctx.ids.form_global_stage
11002        } else {
11003            runtime::forms::codes::FORM_GLOBAL_STAGE
11004        };
11005        let st = self.ctx.globals.stage_forms.get(&form_id);
11006        let empty_groups: Vec<runtime::globals::GroupState> = Vec::new();
11007        let empty_objects: Vec<runtime::globals::ObjectState> = Vec::new();
11008        let empty_mwnds: Vec<runtime::globals::MwndState> = Vec::new();
11009        let empty_worlds: Vec<runtime::globals::WorldState> = Vec::new();
11010        let empty_effects: Vec<runtime::globals::ScreenEffectState> = Vec::new();
11011        let empty_quakes: Vec<runtime::globals::ScreenQuakeState> = Vec::new();
11012        let groups = st
11013            .and_then(|s| s.group_lists.get(&stage_idx))
11014            .unwrap_or(&empty_groups);
11015        let objects = st
11016            .and_then(|s| s.object_lists.get(&stage_idx))
11017            .unwrap_or(&empty_objects);
11018        let mwnds = st
11019            .and_then(|s| s.mwnd_lists.get(&stage_idx))
11020            .unwrap_or(&empty_mwnds);
11021        let worlds = st
11022            .and_then(|s| s.world_lists.get(&stage_idx))
11023            .unwrap_or(&empty_worlds);
11024        let effects = st
11025            .and_then(|s| s.effect_lists.get(&stage_idx))
11026            .unwrap_or(&empty_effects);
11027        let quakes = st
11028            .and_then(|s| s.quake_lists.get(&stage_idx))
11029            .unwrap_or(&empty_quakes);
11030        w.push_fixed_items(groups, |w, g| self.write_cpp_group(w, g));
11031        w.push_fixed_items(objects, |w, obj| self.write_cpp_object(w, obj));
11032        w.push_fixed_items(mwnds, |w, m| self.write_cpp_mwnd(w, m));
11033        let btn_select = if stage_idx == 1 {
11034            Some(&self.ctx.globals.selbtn)
11035        } else {
11036            st.and_then(|stage| stage.btn_select_states.get(&stage_idx))
11037        };
11038        self.write_cpp_btn_select(w, btn_select);
11039        w.push_fixed_items(worlds, |w, world| self.write_cpp_world(w, world));
11040        w.push_fixed_items(effects, |w, e| self.write_cpp_effect(w, e));
11041        w.push_fixed_items(quakes, |w, q| self.write_cpp_quake(w, q));
11042    }
11043
11044    fn read_cpp_stage(
11045        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11046        stage_idx: i64,
11047    ) -> Result<(
11048        runtime::globals::StageFormState,
11049        runtime::globals::BtnSelectRuntimeState,
11050    )> {
11051        if rd.layout.uses_early_records() {
11052            return Self::read_early_stage(rd, stage_idx);
11053        }
11054        let mut st = runtime::globals::StageFormState {
11055            initialized_from_gameexe: true,
11056            ..Default::default()
11057        };
11058
11059        st.group_lists
11060            .insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_group(rd))?);
11061        st.object_lists
11062            .insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_object(rd))?);
11063        st.mwnd_lists
11064            .insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_mwnd(rd))?);
11065        let btn_select = Self::read_cpp_btn_select(rd)?;
11066        st.btn_select_states.insert(stage_idx, btn_select.clone());
11067        let mut world_no = 0i32;
11068        let worlds = rd.fixed_items(|rd| {
11069            let w = Self::read_cpp_world(rd, world_no);
11070            world_no += 1;
11071            w
11072        })?;
11073        st.world_lists.insert(stage_idx, worlds);
11074        st.effect_lists
11075            .insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_effect(rd))?);
11076        st.quake_lists
11077            .insert(stage_idx, rd.fixed_items(|rd| Self::read_cpp_quake(rd))?);
11078        Ok((st, btn_select))
11079    }
11080
11081    fn write_cpp_screen(&self, w: &mut crate::original_save::OriginalStreamWriter) {
11082        let form_id = self.ctx.ids.form_global_screen;
11083        let screen = self
11084            .ctx
11085            .globals
11086            .screen_forms
11087            .get(&form_id)
11088            .cloned()
11089            .unwrap_or_default();
11090        w.push_fixed_items(&screen.effect_list, |w, e| self.write_cpp_effect(w, e));
11091        w.push_i32(screen.shake.shake_no);
11092        w.push_i32(screen.shake.cur_time);
11093        w.push_i32(screen.shake.cur_x);
11094        w.push_i32(screen.shake.cur_y);
11095        w.push_fixed_items(&screen.quake_list, |w, q| self.write_cpp_quake(w, q));
11096    }
11097
11098    fn read_cpp_screen(
11099        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11100    ) -> Result<runtime::globals::ScreenFormState> {
11101        let effect_list = rd.fixed_items(|rd| Self::read_cpp_effect(rd))?;
11102        let mut shake = runtime::globals::ScreenShakeState {
11103            shake_no: rd.i32()?,
11104            cur_time: rd.i32()?,
11105            cur_x: rd.i32()?,
11106            cur_y: rd.i32()?,
11107        };
11108
11109        let quake_list = rd.fixed_items(|rd| Self::read_cpp_quake(rd))?;
11110        Ok(runtime::globals::ScreenFormState {
11111            effect_list,
11112            quake_list,
11113            shake,
11114        })
11115    }
11116
11117    const ORIGINAL_PCMCH_DEFAULT_CNT: usize = 16;
11118    const ORIGINAL_PCMCH_MAX_CNT: usize = 256;
11119
11120    fn original_pcmch_count(&self) -> usize {
11121        self.ctx
11122            .tables
11123            .gameexe
11124            .as_ref()
11125            .and_then(|cfg| {
11126                cfg.get_usize("#PCMCH.CNT")
11127                    .or_else(|| cfg.get_usize("PCMCH.CNT"))
11128            })
11129            .unwrap_or(Self::ORIGINAL_PCMCH_DEFAULT_CNT)
11130            .min(Self::ORIGINAL_PCMCH_MAX_CNT)
11131    }
11132
11133    fn write_cpp_pcmch(&self, w: &mut crate::original_save::OriginalStreamWriter, ch: usize) {
11134        let state = self
11135            .ctx
11136            .globals
11137            .pcmch_persistent
11138            .get(ch)
11139            .cloned()
11140            .unwrap_or_default();
11141        w.push_str(&state.pcm_name);
11142        w.push_str(&state.bgm_name);
11143        w.push_i32(Self::save_i32(state.koe_no));
11144        w.push_i32(Self::save_i32(state.se_no));
11145        w.push_i32(Self::save_i32(state.volume_type));
11146        w.push_i32(Self::save_i32(state.chara_no));
11147        w.push_i32(self.ctx.pcm.slot_volume_raw(ch) as i32);
11148        w.push_i32(Self::save_i32(self.ctx.pcm.slot_resume_delay_ms(ch)));
11149        w.push_bool(state.loop_flag);
11150        w.push_bool(state.bgm_fade_target_flag);
11151        w.push_bool(state.bgm_fade2_target_flag);
11152        w.push_bool(state.bgm_fade_source_flag);
11153        w.push_bool(state.ready_flag);
11154    }
11155
11156    fn read_cpp_pcmch(
11157        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11158    ) -> Result<runtime::globals::PcmChPersistentState> {
11159        Ok(runtime::globals::PcmChPersistentState {
11160            pcm_name: rd.string()?,
11161            bgm_name: rd.string()?,
11162            koe_no: rd.i32()? as i64,
11163            se_no: rd.i32()? as i64,
11164            volume_type: rd.i32()? as i64,
11165            chara_no: rd.i32()? as i64,
11166            volume: rd.i32()? as i64,
11167            delay_time: rd.i32()? as i64,
11168            fade_in_time: 0,
11169            loop_flag: rd.bool()?,
11170            bgm_fade_target_flag: rd.bool()?,
11171            bgm_fade2_target_flag: rd.bool()?,
11172            bgm_fade_source_flag: rd.bool()?,
11173            ready_flag: rd.bool()?,
11174        })
11175    }
11176
11177    fn write_cpp_sound(&self, w: &mut crate::original_save::OriginalStreamWriter) {
11178        // Exact C_elm_sound::save order: BGM, KOE, PCM, PCMCHLIST, SE, MOV.
11179        w.push_str(self.ctx.bgm.current_name().unwrap_or(""));
11180        w.push_i32(self.ctx.bgm.volume_raw() as i32);
11181        w.push_i32(Self::save_i32(self.ctx.bgm.save_delay_time_ms()));
11182        w.push_bool(self.ctx.bgm.save_loop_flag());
11183        w.push_bool(self.ctx.bgm.save_pause_flag());
11184        w.push_i32(self.ctx.koe.volume_raw() as i32);
11185        w.push_i32(self.ctx.pcm.volume_raw() as i32);
11186        let pcmch_count = self.original_pcmch_count();
11187        let channels: Vec<usize> = (0..pcmch_count).collect();
11188        w.push_fixed_items(&channels, |w, ch| self.write_cpp_pcmch(w, *ch));
11189        w.push_i32(self.ctx.se.volume_raw() as i32);
11190        w.push_str(self.ctx.globals.mov.file_name.as_deref().unwrap_or(""));
11191    }
11192
11193    fn read_current_sound(
11194        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11195    ) -> Result<SavedSound> {
11196        let bgm_regist_name = rd.string()?;
11197        let bgm_volume = rd.i32()?.clamp(0, 255) as u8;
11198        let bgm_delay_time = rd.i32()?.max(0) as i64;
11199        let bgm_loop_flag = rd.bool()?;
11200        let bgm_pause_flag = rd.bool()?;
11201        let koe_volume = rd.i32()?.clamp(0, 255) as u8;
11202        let pcm_volume = rd.i32()?.clamp(0, 255) as u8;
11203        let pcmch = rd.fixed_items(|rd| Self::read_cpp_pcmch(rd))?;
11204        let se_volume = rd.i32()?.clamp(0, 255) as u8;
11205        let mov_file_name = rd.string()?;
11206        Ok(SavedSound {
11207            bgm_regist_name,
11208            bgm_volume,
11209            bgm_delay_time,
11210            bgm_loop_flag,
11211            bgm_pause_flag,
11212            koe_volume,
11213            pcm_volume,
11214            pcmch,
11215            se_volume,
11216            mov_file_name,
11217        })
11218    }
11219
11220    fn read_cpp_sound(
11221        &mut self,
11222        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11223    ) -> Result<()> {
11224        // The original local-load path reinitializes C_elm_sound before these
11225        // records are applied.  Stop every live Rust backend first so a saved
11226        // silent/one-shot state cannot leave pre-load playback running.
11227        let _ = self.ctx.bgm.stop();
11228        let _ = self.ctx.koe.stop(None);
11229        let _ = self.ctx.pcm.stop_all(None);
11230        let _ = self.ctx.se.stop(None);
11231        self.ctx.movie.stop();
11232
11233        let SavedSound {
11234            bgm_regist_name,
11235            bgm_volume,
11236            bgm_delay_time,
11237            bgm_loop_flag,
11238            bgm_pause_flag,
11239            koe_volume,
11240            pcm_volume,
11241            pcmch,
11242            se_volume,
11243            mov_file_name,
11244        } = match rd.layout {
11245            NativeLocalLayout::ShortElements => Self::read_short_sound(rd)?,
11246            _ => Self::read_current_sound(rd)?,
11247        };
11248
11249        {
11250            let (bgm, audio) = (&mut self.ctx.bgm, &mut self.ctx.audio);
11251            bgm.set_volume_raw(audio, bgm_volume)?;
11252            if !bgm_regist_name.is_empty()
11253                && (bgm_loop_flag || bgm_pause_flag || bgm_delay_time > 0)
11254            {
11255                // C_elm_bgm::load uses delay only as the restore predicate and
11256                // calls play(..., ready_flag, 0); preserve that behavior.
11257                if let Err(err) = bgm.play_name_script(
11258                    audio,
11259                    &bgm_regist_name,
11260                    bgm_loop_flag,
11261                    0,
11262                    0,
11263                    -1,
11264                    bgm_pause_flag,
11265                    0,
11266                ) {
11267                    log::error!("failed to restore BGM {:?}: {err:#}", bgm_regist_name);
11268                }
11269            }
11270        }
11271        {
11272            let (koe, audio) = (&mut self.ctx.koe, &mut self.ctx.audio);
11273            koe.set_volume_raw(audio, koe_volume)?;
11274        }
11275        {
11276            let (pcm, audio) = (&mut self.ctx.pcm, &mut self.ctx.audio);
11277            pcm.set_volume_raw(audio, pcm_volume)?;
11278        }
11279        {
11280            let (se, audio) = (&mut self.ctx.se, &mut self.ctx.audio);
11281            se.set_volume_raw(audio, se_volume)?;
11282        }
11283
11284        self.ctx.globals.pcmch_persistent.clear();
11285        for (ch, state) in pcmch.into_iter().enumerate() {
11286            if let Err(err) =
11287                crate::runtime::forms::pcmch::restore_persistent_channel(&mut self.ctx, ch, state)
11288            {
11289                log::error!("failed to restore PCMCH[{ch}]: {err:#}");
11290            }
11291        }
11292
11293        if mov_file_name.is_empty() {
11294            self.ctx.globals.mov.file_name = None;
11295            self.ctx.movie.stop();
11296        } else {
11297            self.ctx.globals.mov.file_name = Some(mov_file_name.clone());
11298            self.ctx.globals.mov.playing = false;
11299            if let Err(err) = self.ctx.movie.prepare(&mov_file_name) {
11300                log::error!(
11301                    "failed to restructure saved MOV {:?}: {err:#}",
11302                    mov_file_name
11303                );
11304            }
11305        }
11306        Ok(())
11307    }
11308
11309    fn write_cpp_pcm_event(
11310        &self,
11311        w: &mut crate::original_save::OriginalStreamWriter,
11312        ev: &runtime::globals::PcmEventState,
11313    ) {
11314        let ty = ev.event_type;
11315        w.push_i32(ty);
11316        if ty == runtime::globals::PCM_EVENT_TYPE_LOOP
11317            || ty == runtime::globals::PCM_EVENT_TYPE_RANDOM
11318        {
11319            w.push_i32(ev.pcm_buf_no);
11320            w.push_i32(ev.volume_type);
11321            w.push_i32(ev.chara_no);
11322            w.push_bool(ev.bgm_fade_target_flag);
11323            w.push_bool(ev.bgm_fade2_target_flag);
11324            w.push_bool(ev.bgm_fade_source_flag);
11325            w.push_bool(ev.real_flag);
11326            w.push_bool(ev.time_type);
11327            w.push_extend_items(&ev.lines, |w, line| {
11328                w.push_str(&line.file_name);
11329                w.push_i32(line.min_time);
11330                w.push_i32(line.max_time);
11331                w.push_i32(line.probability);
11332            });
11333        }
11334    }
11335
11336    fn read_cpp_pcm_event(
11337        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11338    ) -> Result<runtime::globals::PcmEventState> {
11339        let ty = rd.i32()?;
11340        let mut ev = runtime::globals::PcmEventState::default();
11341        if ty == runtime::globals::PCM_EVENT_TYPE_LOOP
11342            || ty == runtime::globals::PCM_EVENT_TYPE_RANDOM
11343        {
11344            let pcm_buf_no = rd.i32()?;
11345            let volume_type = rd.i32()?;
11346            let chara_no = rd.i32()?;
11347            let bgm_fade_target_flag = rd.bool()?;
11348            let bgm_fade2_target_flag = rd.bool()?;
11349            let bgm_fade_source_flag = rd.bool()?;
11350            let real_flag = rd.bool()?;
11351            let time_type = rd.bool()?;
11352            ev.lines = rd.extend_items(|rd| {
11353                Ok(runtime::globals::PcmEventLine {
11354                    file_name: rd.string()?,
11355                    min_time: rd.i32()?,
11356                    max_time: rd.i32()?,
11357                    probability: rd.i32()?,
11358                })
11359            })?;
11360            // C_elm_pcm_event::load restarts LOOP/RANDOM and intentionally
11361            // discards all scheduler working values. ONESHOT is not restored.
11362            ev.start(
11363                ty,
11364                pcm_buf_no,
11365                volume_type,
11366                chara_no,
11367                bgm_fade_target_flag,
11368                bgm_fade2_target_flag,
11369                bgm_fade_source_flag,
11370                real_flag,
11371                time_type,
11372            );
11373        }
11374        Ok(ev)
11375    }
11376
11377    fn write_cpp_editbox(
11378        &self,
11379        w: &mut crate::original_save::OriginalStreamWriter,
11380        e: &runtime::globals::EditBoxState,
11381    ) {
11382        w.push_bool(e.created);
11383        w.push_i32(e.rect_x);
11384        w.push_i32(e.rect_y);
11385        w.push_i32(e.rect_w);
11386        w.push_i32(e.rect_h);
11387        w.push_i32(e.moji_size);
11388    }
11389
11390    fn read_cpp_editbox(
11391        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11392    ) -> Result<runtime::globals::EditBoxState> {
11393        let mut e = runtime::globals::EditBoxState::default();
11394        e.created = rd.bool()?;
11395        e.rect_x = rd.i32()?;
11396        e.rect_y = rd.i32()?;
11397        e.rect_w = rd.i32()?;
11398        e.rect_h = rd.i32()?;
11399        e.moji_size = rd.i32()?;
11400        e.text.clear();
11401        Ok(e)
11402    }
11403
11404    fn write_cpp_msg_back(&self, w: &mut crate::original_save::OriginalStreamWriter) {
11405        let msgbk = self
11406            .ctx
11407            .globals
11408            .msgbk_forms
11409            .values()
11410            .next()
11411            .cloned()
11412            .unwrap_or_default();
11413        w.push_i32(msgbk.history_cnt as i32);
11414        let count = msgbk.history_cnt.min(msgbk.history.len());
11415        for entry in msgbk.history.iter().take(count) {
11416            w.push_bool(entry.pct_flag);
11417            w.push_str(&entry.msg_str);
11418            w.push_str(&entry.original_name);
11419            w.push_str(&entry.disp_name);
11420            w.push_i32(entry.pct_pos_x);
11421            w.push_i32(entry.pct_pos_y);
11422            w.push_extend_i32_list(&entry.koe_no_list);
11423            w.push_extend_i32_list(&entry.chr_no_list);
11424            w.push_i32(Self::save_i32(entry.koe_play_no));
11425            w.push_str(&entry.debug_msg);
11426            w.push_i32(Self::save_i32(entry.scn_no));
11427            w.push_i32(Self::save_i32(entry.line_no));
11428            w.push_tid_zero();
11429            w.push_bool(entry.save_id_check_flag);
11430        }
11431        w.push_i32(msgbk.history_start_pos as i32);
11432        w.push_i32(msgbk.history_last_pos as i32);
11433        w.push_i32(msgbk.history_insert_pos as i32);
11434        w.push_i32(if msgbk.new_msg_flag { 1 } else { 0 });
11435    }
11436
11437    fn read_cpp_msg_back(
11438        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11439    ) -> Result<runtime::globals::MsgBackState> {
11440        if rd.layout == NativeLocalLayout::ShortElements {
11441            return Self::read_short_msg_back(rd);
11442        }
11443        if rd.layout.uses_early_records() {
11444            return Self::read_early_msg_back(rd);
11445        }
11446        let cnt = rd.i32()?.max(0) as usize;
11447        let mut st = runtime::globals::MsgBackState::default();
11448        st.history.clear();
11449        for _ in 0..cnt {
11450            let mut entry = runtime::globals::MsgBackEntry {
11451                pct_flag: rd.bool()?,
11452                msg_str: rd.string()?,
11453                original_name: rd.string()?,
11454                disp_name: rd.string()?,
11455                pct_pos_x: rd.i32()?,
11456                pct_pos_y: rd.i32()?,
11457                koe_no_list: rd.extend_i32_list()?,
11458                chr_no_list: rd.extend_i32_list()?,
11459                koe_play_no: rd.i32()? as i64,
11460                debug_msg: rd.string()?,
11461                scn_no: rd.i32()? as i64,
11462                line_no: rd.i32()? as i64,
11463                ..Default::default()
11464            };
11465
11466            rd.skip(14)?;
11467            if rd.layout == NativeLocalLayout::Current {
11468                entry.save_id_check_flag = rd.bool()?;
11469            }
11470            st.history.push(entry);
11471        }
11472        st.history_cnt = cnt;
11473        st.history_cnt_max = cnt.max(256);
11474        st.history_start_pos = rd.i32()?.max(0) as usize;
11475        st.history_last_pos = rd.i32()?.max(0) as usize;
11476        st.history_insert_pos = rd.i32()?.max(0) as usize;
11477        st.new_msg_flag = rd.i32()? != 0;
11478        if st.history.len() < st.history_cnt_max {
11479            st.history
11480                .resize_with(st.history_cnt_max, runtime::globals::MsgBackEntry::default);
11481        }
11482        Ok(st)
11483    }
11484
11485    fn read_saved_stage(
11486        &mut self,
11487        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11488        stage_idx: i64,
11489    ) -> Result<(
11490        runtime::globals::StageFormState,
11491        runtime::globals::BtnSelectRuntimeState,
11492    )> {
11493        match rd.layout {
11494            NativeLocalLayout::ShortElements => self.read_short_stage(rd, stage_idx),
11495            _ => Self::read_cpp_stage(rd, stage_idx),
11496        }
11497    }
11498
11499    fn parse_cpp_tail_state(
11500        &mut self,
11501        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11502        current_scene_name: &str,
11503    ) -> Result<Vec<CallFrame>> {
11504        self.read_cpp_inc_prop_list(rd)?;
11505        self.read_cpp_scene_prop_lists(rd, current_scene_name)?;
11506
11507        let counter_list = rd.fixed_items(|rd| Self::read_cpp_counter_param(rd))?;
11508        if !counter_list.is_empty() {
11509            self.ctx.globals.counter_lists.insert(
11510                crate::runtime::forms::codes::FORM_GLOBAL_COUNTER,
11511                counter_list,
11512            );
11513        }
11514
11515        let frame_action = Self::read_cpp_frame_action(rd)?;
11516        self.ctx
11517            .globals
11518            .frame_actions
11519            .insert(self.ctx.ids.form_global_frame_action, frame_action);
11520
11521        let frame_action_ch = rd.fixed_items(|rd| Self::read_cpp_frame_action(rd))?;
11522        if !frame_action_ch.is_empty() {
11523            self.ctx
11524                .globals
11525                .frame_action_lists
11526                .insert(self.ctx.ids.form_global_frame_action_ch, frame_action_ch);
11527        }
11528
11529        let g00buf_files = rd.fixed_items(|rd| rd.string())?;
11530        self.ctx.globals.g00buf.clear();
11531        self.ctx.globals.g00buf_names.clear();
11532        self.ctx.globals.g00buf.resize(g00buf_files.len(), None);
11533        self.ctx
11534            .globals
11535            .g00buf_names
11536            .resize(g00buf_files.len(), None);
11537        for (idx, name) in g00buf_files.into_iter().enumerate() {
11538            if !name.is_empty() {
11539                self.ctx.globals.g00buf_names[idx] = Some(name.clone());
11540                if let Ok(img_id) = self.ctx.images.load_g00(&name, 0) {
11541                    self.ctx.globals.g00buf[idx] = Some(img_id);
11542                }
11543            }
11544        }
11545
11546        let masks = if rd.layout == NativeLocalLayout::ShortElements {
11547            Vec::new()
11548        } else {
11549            rd.fixed_items(|rd| {
11550                let x_event = Self::read_cpp_int_event_raw(rd)?;
11551                let y_event = Self::read_cpp_int_event_raw(rd)?;
11552                let name = rd.string()?;
11553                Ok(runtime::globals::MaskState {
11554                    name: if name.is_empty() { None } else { Some(name) },
11555                    x_event,
11556                    y_event,
11557                    extra_int: Default::default(),
11558                    script_events: Default::default(),
11559                })
11560            })?
11561        };
11562        if !masks.is_empty() {
11563            self.ctx.globals.mask_lists.insert(
11564                self.ctx.ids.form_global_mask,
11565                runtime::globals::MaskListState { masks },
11566            );
11567        }
11568
11569        let mut st = runtime::globals::StageFormState::default();
11570        // Early engines saved one global world list before both stages.
11571        if rd.layout.uses_early_records() {
11572            let mut world_no = 0;
11573            let worlds = rd.fixed_items(|rd| {
11574                let world = Self::read_cpp_world(rd, world_no);
11575                world_no += 1;
11576                world
11577            })?;
11578            st.world_lists.insert(0, worlds.clone());
11579            st.world_lists.insert(1, worlds);
11580        }
11581        let (back, back_btn_select) = self.read_saved_stage(rd, 0)?;
11582        let (front, front_btn_select) = self.read_saved_stage(rd, 1)?;
11583        st.initialized_from_gameexe = true;
11584        st.group_lists.extend(back.group_lists);
11585        st.object_lists.extend(back.object_lists);
11586        st.mwnd_lists.extend(back.mwnd_lists);
11587        st.btn_select_states.extend(back.btn_select_states);
11588        st.world_lists.extend(back.world_lists);
11589        st.effect_lists.extend(back.effect_lists);
11590        st.quake_lists.extend(back.quake_lists);
11591        st.group_lists.extend(front.group_lists);
11592        st.object_lists.extend(front.object_lists);
11593        st.mwnd_lists.extend(front.mwnd_lists);
11594        st.btn_select_states.extend(front.btn_select_states);
11595        st.world_lists.extend(front.world_lists);
11596        st.effect_lists.extend(front.effect_lists);
11597        st.quake_lists.extend(front.quake_lists);
11598        let normal_stage_form_id = if self.ctx.ids.form_global_stage != 0 {
11599            self.ctx.ids.form_global_stage
11600        } else {
11601            runtime::forms::codes::FORM_GLOBAL_STAGE
11602        };
11603        self.ctx
11604            .globals
11605            .stage_forms
11606            .insert(normal_stage_form_id, Box::new(st));
11607        if !back_btn_select.choices.is_empty() {
11608            runtime::forms::global::prepare_saved_stage_btnselitems(
11609                &mut self.ctx,
11610                0,
11611                back_btn_select,
11612            );
11613        }
11614        self.ctx.globals.selbtn = front_btn_select;
11615        if !self.ctx.globals.selbtn.choices.is_empty() {
11616            runtime::forms::global::prepare_stage_btnselitems(&mut self.ctx);
11617        }
11618
11619        let screen = Self::read_cpp_screen(rd)?;
11620        self.ctx
11621            .globals
11622            .screen_forms
11623            .insert(self.ctx.ids.form_global_screen, screen);
11624
11625        self.read_cpp_sound(rd)?;
11626
11627        let pcm_events = rd.fixed_items(|rd| Self::read_cpp_pcm_event(rd))?;
11628        if !pcm_events.is_empty() {
11629            self.ctx
11630                .globals
11631                .pcm_event_lists
11632                .insert(self.ctx.ids.form_global_pcm_event, pcm_events);
11633        }
11634
11635        let mut editboxes = rd.fixed_items(|rd| Self::read_cpp_editbox(rd))?;
11636        if !editboxes.is_empty() {
11637            let screen_w = self.ctx.screen_w as i32;
11638            let screen_h = self.ctx.screen_h as i32;
11639            let display_mode_change_proc_cnt = self.ctx.globals.change_display_mode_proc_cnt;
11640            for eb in &mut editboxes {
11641                eb.design_screen_w = screen_w.max(1);
11642                eb.design_screen_h = screen_h.max(1);
11643                eb.update_rect(screen_w, screen_h);
11644                eb.frame(display_mode_change_proc_cnt);
11645            }
11646            let focused_idx = editboxes.iter().rposition(|eb| eb.created);
11647            let form_id = self.ctx.ids.form_global_editbox;
11648            self.ctx.globals.editbox_lists.insert(
11649                form_id,
11650                runtime::globals::EditBoxListState { boxes: editboxes },
11651            );
11652            self.ctx
11653                .set_focused_editbox(focused_idx.map(|idx| (form_id, idx)));
11654        }
11655
11656        let call_cnt = rd.i32()?.max(0) as usize;
11657        let mut call_stack = Vec::with_capacity(call_cnt.max(1));
11658        for _ in 0..call_cnt {
11659            call_stack.push(self.read_cpp_call_frame(rd)?);
11660        }
11661        if rd.layout == NativeLocalLayout::ShortElements {
11662            self.normalize_short_call_returns(&mut call_stack);
11663        }
11664        if call_stack.is_empty() {
11665            call_stack.push(self.scene_base_call());
11666        }
11667
11668        let msg_back = Self::read_cpp_msg_back(rd)?;
11669        self.ctx
11670            .globals
11671            .msgbk_forms
11672            .insert(self.ctx.ids.form_global_msgbk, msg_back);
11673
11674        // The oldest stream ends at the backlog. Selection/inner snapshots
11675        // were added to later local formats.
11676        if rd.layout == NativeLocalLayout::ShortElements {
11677            anyhow::ensure!(
11678                rd.remaining().is_empty(),
11679                "unexpected trailing short-element save data"
11680            );
11681            self.ctx.globals.syscom.sel_save_stock_stream.clear();
11682            self.ctx.globals.syscom.inner_save_streams.clear();
11683            self.ctx.globals.syscom.inner_save_exists = false;
11684            self.ctx.globals.syscom.sel_save_ids.clear();
11685            return Ok(call_stack);
11686        }
11687
11688        self.ctx.globals.syscom.sel_save_stock_stream = rd.len_bytes()?;
11689        let inner_cnt = rd.i32()?.max(0) as usize;
11690        self.ctx.globals.syscom.inner_save_streams.clear();
11691        for _ in 0..inner_cnt {
11692            self.ctx
11693                .globals
11694                .syscom
11695                .inner_save_streams
11696                .push(rd.len_bytes()?);
11697        }
11698        self.ctx.globals.syscom.inner_save_exists = self
11699            .ctx
11700            .globals
11701            .syscom
11702            .inner_save_streams
11703            .iter()
11704            .any(|s| !s.is_empty());
11705        let sel_save_cnt = if rd.layout.is_indexed() {
11706            0
11707        } else {
11708            rd.i32()?.max(0) as usize
11709        };
11710        self.ctx.globals.syscom.sel_save_ids.clear();
11711        for _ in 0..sel_save_cnt {
11712            self.ctx.globals.syscom.sel_save_ids.push(rd.tid()?);
11713        }
11714        Ok(call_stack)
11715    }
11716
11717    fn restore_saved_scene_stack(
11718        &mut self,
11719        mut frames: Vec<CallFrame>,
11720        current_scene_name: &str,
11721    ) -> Result<Vec<CallFrame>> {
11722        // C++ does not save a separate cross-scene stack.  Each caller frame's
11723        // C_elm_call::m_call_save stores the lexer scene/line/pc captured by
11724        // tnm_save_call(), while the following callee frame stores call_type.
11725        // Rebuild only boundaries proven by those bytes; never infer a
11726        // dispatcher scene or synthesize z labels for a particular game.
11727        self.scene_stack.clear();
11728        if self.save_load_trace_enabled() {
11729            for (index, frame) in frames.iter().enumerate() {
11730                eprintln!(
11731                    "[SG_SAVELOAD_TRACE][CALL] index={index} type={} ret_form={} scene={:?} line={} pc=0x{:x}",
11732                    frame.call_type,
11733                    frame.ret_form,
11734                    frame.return_scene_name,
11735                    frame.return_line_no,
11736                    frame.return_pc,
11737                );
11738            }
11739        }
11740        if frames.is_empty() {
11741            return Ok(vec![self.scene_base_call()]);
11742        }
11743
11744        // Scene numbers are runtime package indices and are not serialized in
11745        // C_elm_call. Resolve them from the saved scene names in the active
11746        // Scene.pck after load/reload.
11747        for frame in &mut frames {
11748            frame.return_scene_no = frame.return_scene_name.as_deref().and_then(|name| {
11749                self.scene_pck_cache
11750                    .as_ref()
11751                    .and_then(|cache| cache.find_scene_no(name))
11752            });
11753        }
11754
11755        for callee_idx in 1..frames.len() {
11756            let call_type = frames[callee_idx].call_type;
11757            if call_type != 2 && call_type != 3 {
11758                continue;
11759            }
11760
11761            let Some(caller_scene_name) = frames[callee_idx - 1]
11762                .return_scene_name
11763                .as_deref()
11764                .filter(|name| !name.is_empty())
11765            else {
11766                // A legacy/non-original Rust save may not contain caller lexer
11767                // metadata. There is no C++-justified way to recover it.
11768                continue;
11769            };
11770
11771            let callee_scene_name = if callee_idx + 1 == frames.len() {
11772                Some(current_scene_name)
11773            } else {
11774                frames[callee_idx]
11775                    .return_scene_name
11776                    .as_deref()
11777                    .filter(|name| !name.is_empty())
11778            };
11779            let Some(callee_scene_name) = callee_scene_name else {
11780                continue;
11781            };
11782
11783            // USER_CMD can target the current scene. Only a real scene change
11784            // needs a SceneExecFrame; same-scene calls are restored solely by
11785            // the shared C_elm_call list.
11786            if caller_scene_name.eq_ignore_ascii_case(callee_scene_name) {
11787                continue;
11788            }
11789
11790            let Some(caller_scene_no) = self
11791                .scene_pck_cache
11792                .as_ref()
11793                .and_then(|cache| cache.find_scene_no(caller_scene_name))
11794            else {
11795                log::warn!(
11796                    "[SG_SAVELOAD] saved caller scene not found in active Scene.pck: {}",
11797                    caller_scene_name
11798                );
11799                continue;
11800            };
11801
11802            let mut stream = self.cached_scene_stream(caller_scene_no)?;
11803            stream.set_prg_cntr(frames[callee_idx - 1].return_pc)?;
11804            let user_cmd_names = stream.scn_cmd_name_map.clone();
11805            let call_cmd_names = self
11806                .scene_pck_cache
11807                .as_ref()
11808                .expect("scene pck cache initialized")
11809                .inc_cmd_name_map
11810                .clone();
11811
11812            self.scene_stack.push(SceneExecFrame {
11813                stream,
11814                user_cmd_names,
11815                call_cmd_names,
11816                current_scene_no: Some(caller_scene_no),
11817                current_scene_name: Some(caller_scene_name.to_string()),
11818                current_line_no: frames[callee_idx - 1].return_line_no,
11819                // At call entry C_elm_call_list::add_call() creates callee_idx,
11820                // so Rust's shared call stack length at this boundary is
11821                // callee_idx + 1 (base frame included).
11822                call_depth: callee_idx + 1,
11823            });
11824        }
11825
11826        Ok(frames)
11827    }
11828
11829    fn write_cpp_proc_record(
11830        w: &mut crate::original_save::OriginalStreamWriter,
11831        proc_type: i32,
11832        element: &[i32],
11833        option: i32,
11834    ) {
11835        w.push_i32(proc_type);
11836        w.push_element(element);
11837        w.push_i32(0);
11838        w.push_i32(0);
11839        w.push_bool(false);
11840        w.push_bool(false);
11841        w.push_bool(false);
11842        w.push_i32(option);
11843    }
11844
11845    fn write_cpp_runtime_proc_stack(&self, w: &mut crate::original_save::OriginalStreamWriter) {
11846        // Do not fabricate C_tnm_proc states.  Until the real C++ proc element,
11847        // arg_list, return_value_flag and option are mirrored from runtime state,
11848        // only the script proc can be represented safely; other transient waits are
11849        // saved as NONE rather than writing a guessed proc_type.
11850        let proc_type = if matches!(self.ctx.last_proc_kind(), runtime::ProcKind::Script) {
11851            1
11852        } else {
11853            0
11854        };
11855        Self::write_cpp_proc_record(w, proc_type, &[], 0);
11856        w.push_i32(0);
11857    }
11858
11859    fn read_cpp_proc_record(
11860        &self,
11861        rd: &mut crate::original_save::OriginalStreamReader<'_>,
11862    ) -> Result<()> {
11863        let _proc_type = rd.i32()?;
11864        let _element = rd.element()?;
11865        let _arg_list_id = rd.i32()?;
11866        let _arg_list: Vec<()> = rd.extend_items(|rd| {
11867            let _ = self.read_cpp_prop(rd)?;
11868            Ok(())
11869        })?;
11870        rd.skip(rd.layout.proc_trailer_bytes())?;
11871        Ok(())
11872    }
11873
11874    fn decode_cpp_mwnd_element(elm: &[i32]) -> Option<(i64, usize)> {
11875        let is_array = |v: i32| v == crate::runtime::forms::codes::ELM_ARRAY || v == -1;
11876        if elm.len() >= 4 {
11877            let stage_idx = if elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_BACK {
11878                Some(0)
11879            } else if elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_FRONT {
11880                Some(1)
11881            } else if elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_NEXT {
11882                Some(2)
11883            } else {
11884                None
11885            };
11886            if let Some(stage_idx) = stage_idx
11887                && elm[1] == crate::runtime::forms::codes::ELM_STAGE_MWND
11888                && is_array(elm[2])
11889                && elm[3] >= 0
11890            {
11891                return Some((stage_idx, elm[3] as usize));
11892            }
11893        }
11894        if elm.len() >= 6
11895            && elm[0] == crate::runtime::forms::codes::ELM_GLOBAL_STAGE
11896            && is_array(elm[1])
11897            && elm[2] >= 0
11898            && elm[3] == crate::runtime::forms::codes::ELM_STAGE_MWND
11899            && is_array(elm[4])
11900            && elm[5] >= 0
11901        {
11902            return Some((elm[2] as i64, elm[5] as usize));
11903        }
11904        None
11905    }
11906
11907    fn apply_saved_current_mwnd_elements(
11908        &mut self,
11909        cur_mwnd: &[i32],
11910        cur_sel_mwnd: &[i32],
11911        last_mwnd: &[i32],
11912    ) {
11913        self.ctx.globals.current_mwnd_element = cur_mwnd.to_vec();
11914        self.ctx.globals.current_sel_mwnd_element = cur_sel_mwnd.to_vec();
11915        self.ctx.globals.last_mwnd_element = last_mwnd.to_vec();
11916        self.ctx.globals.current_mwnd_no = None;
11917        self.ctx.globals.current_sel_mwnd_no = None;
11918        self.ctx.globals.last_mwnd_no = None;
11919
11920        if let Some((stage, no)) = Self::decode_cpp_mwnd_element(cur_mwnd) {
11921            self.ctx.globals.current_mwnd_stage_idx = stage;
11922            self.ctx.globals.current_mwnd_no = Some(no);
11923        }
11924        if let Some((stage, no)) = Self::decode_cpp_mwnd_element(cur_sel_mwnd) {
11925            self.ctx.globals.current_sel_mwnd_stage_idx = stage;
11926            self.ctx.globals.current_sel_mwnd_no = Some(no);
11927        }
11928        if let Some((stage, no)) = Self::decode_cpp_mwnd_element(last_mwnd) {
11929            self.ctx.globals.last_mwnd_stage_idx = stage;
11930            self.ctx.globals.last_mwnd_no = Some(no);
11931        }
11932    }
11933
11934    fn current_local_save_id(&self) -> [u16; 7] {
11935        let now = crate::platform_time::local_time_fields();
11936        [
11937            now.year.clamp(0, u16::MAX as i32) as u16,
11938            now.month as u16,
11939            now.day as u16,
11940            now.hour as u16,
11941            now.minute as u16,
11942            now.second as u16,
11943            now.millisecond as u16,
11944        ]
11945    }
11946
11947    /// Mirror of C++ `C_tnm_eng::save_local()`. Captures the engine snapshot into
11948    /// `ctx.local_save_snapshot` so subsequent SAVE / QUICK_SAVE / END_SAVE invocations
11949    /// write the savepoint-time state, not whatever transient menu state happens to be
11950    /// live when the user picks a slot.
11951    fn build_local_save_snapshot(&mut self) {
11952        let local_stream = self.build_original_local_stream();
11953        let local_ex_stream = self.build_original_local_ex_stream();
11954        let snapshot = crate::runtime::LocalSaveSnapshot {
11955            save_id: self.current_local_save_id(),
11956            append_dir: self.ctx.globals.append_dir.clone(),
11957            append_name: self.ctx.globals.append_name.clone(),
11958            save_scene_title: self.ctx.globals.syscom.current_save_scene_title.clone(),
11959            save_msg: String::new(),
11960            save_full_msg: self.ctx.globals.syscom.current_save_full_message.clone(),
11961            local_stream,
11962            local_ex_stream,
11963            sel_saves: self
11964                .ctx
11965                .local_save_snapshot
11966                .as_ref()
11967                .map(|s| s.sel_saves.clone())
11968                .unwrap_or_default(),
11969        };
11970        self.ctx.local_save_snapshot = Some(snapshot);
11971    }
11972
11973    fn build_original_local_stream(&self) -> Vec<u8> {
11974        let mut w = crate::original_save::OriginalStreamWriter::new();
11975        let scene_name = self.current_scene_name.as_deref().unwrap_or("");
11976        let flag_cnt = self.local_flag_count();
11977        use crate::runtime::forms::codes;
11978
11979        w.push_str(scene_name);
11980        w.push_i32(self.current_line_no);
11981        w.push_i32(self.stream.get_prg_cntr() as i32);
11982
11983        self.write_cpp_runtime_proc_stack(&mut w);
11984        w.push_element(&self.ctx.globals.current_mwnd_element);
11985        w.push_element(&self.ctx.globals.current_sel_mwnd_element);
11986        w.push_element(&self.ctx.globals.last_mwnd_element);
11987        w.push_str(&self.ctx.globals.syscom.current_save_scene_title);
11988        let current_full_message = if self.ctx.globals.syscom.current_save_full_message.is_empty() {
11989            self.ctx.globals.syscom.current_save_message.as_str()
11990        } else {
11991            self.ctx.globals.syscom.current_save_full_message.as_str()
11992        };
11993        w.push_str(current_full_message);
11994
11995        let btn_cnt = self.mwnd_waku_btn_count();
11996        for idx in 0..btn_cnt {
11997            w.push_bool(
11998                self.ctx
11999                    .globals
12000                    .syscom
12001                    .mwnd_btn_disable
12002                    .get(&(idx as i64))
12003                    .copied()
12004                    .unwrap_or(false),
12005            );
12006        }
12007        w.push_str(&self.ctx.globals.script.font_name);
12008        w.push_raw(&self.build_cpp_local_data_pod());
12009
12010        w.push_i32(self.int_stack.len() as i32);
12011        for v in &self.int_stack {
12012            w.push_i32(*v);
12013        }
12014        w.push_i32(self.str_stack.len() as i32);
12015        for s in &self.str_stack {
12016            w.push_str(s);
12017        }
12018        w.push_i32(self.element_points.len() as i32);
12019        for p in &self.element_points {
12020            w.push_i32(*p as i32);
12021        }
12022
12023        w.push_i32(
12024            self.ctx
12025                .globals
12026                .local_real_time
12027                .clamp(i32::MIN as i64, i32::MAX as i64) as i32,
12028        );
12029        w.push_i32(
12030            self.ctx
12031                .globals
12032                .local_game_time
12033                .clamp(i32::MIN as i64, i32::MAX as i64) as i32,
12034        );
12035        w.push_i32(
12036            self.ctx
12037                .globals
12038                .local_wipe_time
12039                .clamp(i32::MIN as i64, i32::MAX as i64) as i32,
12040        );
12041        self.write_cpp_syscom_menu(&mut w);
12042
12043        let fog = &self.ctx.globals.fog_global;
12044        w.push_str(&fog.name);
12045        Self::write_cpp_int_event_raw(&mut w, &fog.x_event);
12046        w.push_i32(fog.near as i32);
12047        w.push_i32(fog.far as i32);
12048
12049        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_A), flag_cnt);
12050        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_B), flag_cnt);
12051        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_C), flag_cnt);
12052        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_D), flag_cnt);
12053        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_E), flag_cnt);
12054        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_F), flag_cnt);
12055        w.push_fixed_i32_list(self.int_list_by_element(codes::ELM_GLOBAL_X), flag_cnt);
12056        w.push_fixed_str_list(self.str_list_by_element(codes::ELM_GLOBAL_S), flag_cnt);
12057        w.push_extend_i32_list(&self.ctx.globals.local_flag_h);
12058        w.push_extend_i32_list(&self.ctx.globals.local_flag_i);
12059        w.push_extend_i32_list(&self.ctx.globals.local_flag_j);
12060        w.push_fixed_str_list(
12061            self.str_list_by_element(codes::ELM_GLOBAL_NAMAE_LOCAL),
12062            26 + 26 * 26,
12063        );
12064
12065        self.write_cpp_inc_prop_list(&mut w);
12066        self.write_cpp_current_scene_prop_lists(&mut w);
12067
12068        let counter_list = self
12069            .ctx
12070            .globals
12071            .counter_lists
12072            .values()
12073            .next()
12074            .cloned()
12075            .unwrap_or_default();
12076        w.push_fixed_items(&counter_list, |w, c| self.write_cpp_counter_param(w, c));
12077
12078        let frame_action = self
12079            .ctx
12080            .globals
12081            .frame_actions
12082            .values()
12083            .next()
12084            .cloned()
12085            .unwrap_or_default();
12086        self.write_cpp_frame_action(&mut w, &frame_action);
12087
12088        let frame_action_ch = self
12089            .ctx
12090            .globals
12091            .frame_action_lists
12092            .values()
12093            .next()
12094            .cloned()
12095            .unwrap_or_default();
12096        w.push_fixed_items(&frame_action_ch, |w, fa| self.write_cpp_frame_action(w, fa));
12097
12098        // Original C_elm_g00_buf::save writes the file name for each slot.
12099        w.push_fixed_items(&self.ctx.globals.g00buf_names, |w, name| {
12100            w.push_str(name.as_deref().unwrap_or(""))
12101        });
12102
12103        let mask_list = self
12104            .ctx
12105            .globals
12106            .mask_lists
12107            .values()
12108            .next()
12109            .map(|m| m.masks.clone())
12110            .unwrap_or_default();
12111        w.push_fixed_items(&mask_list, |w, m| {
12112            Self::write_cpp_int_event_raw(w, &m.x_event);
12113            Self::write_cpp_int_event_raw(w, &m.y_event);
12114            w.push_str(m.name.as_deref().unwrap_or(""));
12115        });
12116
12117        self.write_cpp_stage(&mut w, 0);
12118        self.write_cpp_stage(&mut w, 1);
12119        self.write_cpp_screen(&mut w);
12120        self.write_cpp_sound(&mut w);
12121
12122        let pcm_events = self
12123            .ctx
12124            .globals
12125            .pcm_event_lists
12126            .values()
12127            .next()
12128            .cloned()
12129            .unwrap_or_default();
12130        w.push_fixed_items(&pcm_events, |w, ev| self.write_cpp_pcm_event(w, ev));
12131
12132        let editboxes = self
12133            .ctx
12134            .globals
12135            .editbox_lists
12136            .values()
12137            .next()
12138            .map(|e| e.boxes.clone())
12139            .unwrap_or_default();
12140        w.push_fixed_items(&editboxes, |w, e| self.write_cpp_editbox(w, e));
12141
12142        let saved_call_stack = self.flattened_call_stack_for_save();
12143        w.push_i32(saved_call_stack.len() as i32);
12144        for frame in &saved_call_stack {
12145            self.write_cpp_call_frame(&mut w, frame);
12146        }
12147        self.write_cpp_msg_back(&mut w);
12148
12149        w.push_len_bytes(&self.ctx.globals.syscom.sel_save_stock_stream);
12150        w.push_i32(self.ctx.globals.syscom.inner_save_streams.len() as i32);
12151        for stream in &self.ctx.globals.syscom.inner_save_streams {
12152            w.push_len_bytes(stream);
12153        }
12154        w.push_i32(self.ctx.globals.syscom.sel_save_ids.len() as i32);
12155        for tid in &self.ctx.globals.syscom.sel_save_ids {
12156            w.push_tid(tid);
12157        }
12158        w.into_inner()
12159    }
12160
12161    fn build_original_local_ex_stream(&self) -> Vec<u8> {
12162        let mut w = crate::original_save::OriginalStreamWriter::new();
12163        let s = &self.ctx.globals.syscom;
12164        for i in 0..4 {
12165            let sw = s.local_extra_switches.get(i).copied().unwrap_or(if i == 0 {
12166                s.local_extra_switch
12167            } else {
12168                runtime::globals::ToggleFeatureState::default()
12169            });
12170            w.push_bool(sw.exist);
12171            w.push_bool(sw.enable);
12172            w.push_bool(sw.onoff);
12173        }
12174        for i in 0..4 {
12175            let mode = s.local_extra_modes.get(i).copied().unwrap_or(if i == 0 {
12176                s.local_extra_mode
12177            } else {
12178                runtime::globals::ValueFeatureState::default()
12179            });
12180            w.push_bool(mode.exist);
12181            w.push_bool(mode.enable);
12182            w.push_padding(2);
12183            w.push_i32(mode.value as i32);
12184        }
12185        let out = w.into_inner();
12186        debug_assert_eq!(out.len(), 44);
12187        out
12188    }
12189
12190    fn parse_original_local_ex_stream(&mut self, local_ex_stream: &[u8]) -> Result<()> {
12191        if local_ex_stream.len() < 44 {
12192            return Ok(());
12193        }
12194        let mut rd = crate::original_save::OriginalStreamReader::new(local_ex_stream);
12195        for i in 0..4 {
12196            self.ctx.globals.syscom.local_extra_switches[i].exist = rd.bool()?;
12197            self.ctx.globals.syscom.local_extra_switches[i].enable = rd.bool()?;
12198            self.ctx.globals.syscom.local_extra_switches[i].onoff = rd.bool()?;
12199        }
12200        for i in 0..4 {
12201            self.ctx.globals.syscom.local_extra_modes[i].exist = rd.bool()?;
12202            self.ctx.globals.syscom.local_extra_modes[i].enable = rd.bool()?;
12203            rd.skip(2)?;
12204            self.ctx.globals.syscom.local_extra_modes[i].value = rd.i32()? as i64;
12205        }
12206        self.ctx.globals.syscom.local_extra_switch =
12207            self.ctx.globals.syscom.local_extra_switches[0];
12208        self.ctx.globals.syscom.local_extra_mode = self.ctx.globals.syscom.local_extra_modes[0];
12209        Ok(())
12210    }
12211
12212    fn parse_original_local_stream(&mut self, local_stream: &[u8]) -> Result<RuntimeDiskSnapshot> {
12213        let mut rd = crate::original_save::OriginalStreamReader::new(local_stream);
12214        let flag_cnt = self.local_flag_count();
12215        use crate::runtime::forms::codes;
12216
12217        rd.layout = self
12218            .detect_indexed_local_layout(local_stream)
12219            .unwrap_or_default();
12220        let scene_name = if rd.layout.is_indexed() {
12221            self.indexed_scene_name(rd.i32()?)?
12222        } else {
12223            rd.string()?
12224        };
12225        let line_no = rd.i32()?;
12226        let pc = rd.i32()?;
12227
12228        self.read_cpp_proc_record(&mut rd)?;
12229        let proc_stack_cnt = rd.i32()?.max(0) as usize;
12230        for _ in 0..proc_stack_cnt {
12231            self.read_cpp_proc_record(&mut rd)?;
12232        }
12233        let cur_mwnd = rd.element()?;
12234        let cur_sel_mwnd = rd.element()?;
12235        let last_mwnd = rd.element()?;
12236        self.apply_saved_current_mwnd_elements(&cur_mwnd, &cur_sel_mwnd, &last_mwnd);
12237        self.ctx.globals.syscom.current_save_message.clear();
12238        if rd.layout.is_indexed() {
12239            match rd.layout {
12240                NativeLocalLayout::ShortElements => self.read_short_local_settings(&mut rd)?,
12241                NativeLocalLayout::Indexed => self.read_indexed_local_settings(&mut rd)?,
12242                _ => unreachable!(),
12243            }
12244        } else {
12245            self.ctx.globals.syscom.current_save_scene_title = rd.string()?;
12246            let btn_cnt = self.mwnd_waku_btn_count();
12247            rd.detect_early_local_layout(btn_cnt);
12248            self.ctx.globals.syscom.current_save_full_message = if rd.layout.uses_early_records() {
12249                String::new()
12250            } else {
12251                rd.string()?
12252            };
12253            self.ctx.globals.syscom.mwnd_btn_disable.clear();
12254            for idx in 0..btn_cnt {
12255                if rd.bool()? {
12256                    self.ctx
12257                        .globals
12258                        .syscom
12259                        .mwnd_btn_disable
12260                        .insert(idx as i64, true);
12261                }
12262            }
12263            rd.detect_local_layout()?;
12264            let has_font = rd.layout == NativeLocalLayout::Current;
12265            self.ctx.globals.script.font_name = if has_font {
12266                rd.string()?
12267            } else {
12268                String::new()
12269            };
12270            self.read_cpp_local_data_pod(&mut rd, has_font)?;
12271        }
12272
12273        let int_cnt = rd.i32()?.max(0) as usize;
12274        let mut int_stack = Vec::with_capacity(int_cnt);
12275        for _ in 0..int_cnt {
12276            int_stack.push(rd.i32()?);
12277        }
12278        let str_cnt = rd.i32()?.max(0) as usize;
12279        let mut str_stack = Vec::with_capacity(str_cnt);
12280        for _ in 0..str_cnt {
12281            str_stack.push(rd.string()?);
12282        }
12283        let ep_cnt = rd.i32()?.max(0) as usize;
12284        let mut element_points = Vec::with_capacity(ep_cnt);
12285        for _ in 0..ep_cnt {
12286            element_points.push(rd.i32()?.max(0) as usize);
12287        }
12288
12289        self.ctx.globals.local_real_time = rd.i32()? as i64;
12290        self.ctx.globals.local_game_time = rd.i32()? as i64;
12291        self.ctx.globals.local_wipe_time = rd.i32()? as i64;
12292        self.read_cpp_syscom_menu(&mut rd)?;
12293
12294        if rd.layout != NativeLocalLayout::ShortElements {
12295            let fog_name = rd.string()?;
12296            let fog_x = Self::read_cpp_int_event_raw(&mut rd)?;
12297            let fog_near = rd.i32()?;
12298            let fog_far = rd.i32()?;
12299            self.ctx.globals.fog_global.name = fog_name;
12300            self.ctx.globals.fog_global.enabled = !self.ctx.globals.fog_global.name.is_empty();
12301            self.ctx.globals.fog_global.texture_image_id = None;
12302            if self.ctx.globals.fog_global.enabled {
12303                match self
12304                    .ctx
12305                    .images
12306                    .load_g00(&self.ctx.globals.fog_global.name, 0)
12307                {
12308                    Ok(id) => self.ctx.globals.fog_global.texture_image_id = Some(id),
12309                    Err(e) => log::error!(
12310                        "load_local fog texture '{}' failed: {e}",
12311                        self.ctx.globals.fog_global.name
12312                    ),
12313                }
12314            }
12315            self.ctx.globals.fog_global.x_event = fog_x;
12316            self.ctx.globals.fog_global.scroll_x =
12317                self.ctx.globals.fog_global.x_event.get_total_value() as f32;
12318            self.ctx.globals.fog_global.near = fog_near as f32;
12319            self.ctx.globals.fog_global.far = fog_far as f32;
12320        } else {
12321            self.ctx.globals.fog_global = Default::default();
12322        }
12323
12324        let a = rd.fixed_i32_list()?;
12325        let b = rd.fixed_i32_list()?;
12326        let c = rd.fixed_i32_list()?;
12327        let d = rd.fixed_i32_list()?;
12328        let e = rd.fixed_i32_list()?;
12329        let f = rd.fixed_i32_list()?;
12330        let x = if rd.layout == NativeLocalLayout::ShortElements {
12331            Vec::new()
12332        } else {
12333            rd.fixed_i32_list()?
12334        };
12335        let s = rd.fixed_str_list()?;
12336        let h = rd.extend_i32_list()?;
12337        let i = rd.extend_i32_list()?;
12338        let j = rd.extend_i32_list()?;
12339        let namae_local = rd.fixed_str_list()?;
12340        self.ctx
12341            .globals
12342            .int_lists
12343            .insert(codes::ELM_GLOBAL_A as u32, resize_i64_vec(a, flag_cnt));
12344        self.ctx
12345            .globals
12346            .int_lists
12347            .insert(codes::ELM_GLOBAL_B as u32, resize_i64_vec(b, flag_cnt));
12348        self.ctx
12349            .globals
12350            .int_lists
12351            .insert(codes::ELM_GLOBAL_C as u32, resize_i64_vec(c, flag_cnt));
12352        self.ctx
12353            .globals
12354            .int_lists
12355            .insert(codes::ELM_GLOBAL_D as u32, resize_i64_vec(d, flag_cnt));
12356        self.ctx
12357            .globals
12358            .int_lists
12359            .insert(codes::ELM_GLOBAL_E as u32, resize_i64_vec(e, flag_cnt));
12360        self.ctx
12361            .globals
12362            .int_lists
12363            .insert(codes::ELM_GLOBAL_F as u32, resize_i64_vec(f, flag_cnt));
12364        self.ctx
12365            .globals
12366            .int_lists
12367            .insert(codes::ELM_GLOBAL_X as u32, resize_i64_vec(x, flag_cnt));
12368        self.ctx
12369            .globals
12370            .str_lists
12371            .insert(codes::ELM_GLOBAL_S as u32, resize_string_vec(s, flag_cnt));
12372        self.ctx.globals.local_flag_h = h;
12373        self.ctx.globals.local_flag_i = i;
12374        self.ctx.globals.local_flag_j = j;
12375        self.ctx.globals.str_lists.insert(
12376            codes::ELM_GLOBAL_NAMAE_LOCAL as u32,
12377            resize_string_vec(namae_local, 26 + 26 * 26),
12378        );
12379
12380        let call_stack = self.parse_cpp_tail_state(&mut rd, &scene_name)?;
12381
12382        Ok(RuntimeDiskSnapshot {
12383            scene_name,
12384            scene_no: -1,
12385            line_no,
12386            pc,
12387            int_stack,
12388            str_stack,
12389            element_points,
12390            call_stack,
12391        })
12392    }
12393
12394    #[inline(always)]
12395    fn save_load_trace_enabled(&self) -> bool {
12396        self.runtime_options.save_load_trace
12397    }
12398
12399    fn perform_runtime_save_request(&mut self, req: RuntimeSaveRequest) -> Result<()> {
12400        if req.kind == RuntimeSaveKind::Inner {
12401            // C++ `tnm_saveload_proc_create_inner_save` copies the current
12402            // `m_local_save` into the inner-save slot. It must not reserialize the
12403            // live runtime (which may be the save/load menu).
12404            let Some(snapshot) = self.ctx.local_save_snapshot.as_ref() else {
12405                log::error!(
12406                    "[SG_SAVELOAD] inner save dropped idx={}: no local_save snapshot",
12407                    req.index
12408                );
12409                return Ok(());
12410            };
12411            if self.save_load_trace_enabled() {
12412                eprintln!("[SG_SAVELOAD_TRACE][VM] save inner idx={}", req.index);
12413            }
12414            if self.ctx.globals.syscom.inner_save_streams.len() <= req.index {
12415                self.ctx
12416                    .globals
12417                    .syscom
12418                    .inner_save_streams
12419                    .resize_with(req.index + 1, Vec::new);
12420            }
12421            self.ctx.globals.syscom.inner_save_streams[req.index] = snapshot.local_stream.clone();
12422            self.ctx.globals.syscom.inner_save_exists = true;
12423            return Ok(());
12424        }
12425
12426        // Normal / quick / end save mirror C++ `tnm_save_local_on_file`: bail out when
12427        // there is no snapshot (equivalent to `m_local_save.save_stream.empty()`).
12428        // Without this, picking a slot in the save menu would otherwise serialize the
12429        // menu itself - the bug we're fixing.
12430        if self.ctx.local_save_snapshot.is_none() {
12431            log::error!(
12432                "[SG_SAVELOAD] save dropped (kind={:?} idx={}): no local_save snapshot. \
12433                 SAVEPOINT has not fired in the current message block - either the script \
12434                 set dont_set_save_point or auto-SAVEPOINT wasn't reached yet. No file written.",
12435                req.kind,
12436                req.index
12437            );
12438            if req.kind == RuntimeSaveKind::Normal {
12439                crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
12440                    &mut self.ctx,
12441                    crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
12442                );
12443            }
12444            return Ok(());
12445        }
12446
12447        // Refresh local_ex_stream from the live runtime; mirrors C++ `save_local_ex()`
12448        // being called inside `tnm_save_local_on_file` right before writing.
12449        let refreshed_ex = self.build_original_local_ex_stream();
12450        if let Some(snapshot) = self.ctx.local_save_snapshot.as_mut() {
12451            snapshot.local_ex_stream = refreshed_ex;
12452        }
12453
12454        let slot = self.ensure_runtime_slot_for_save(req);
12455        let Some(path) = self.runtime_save_file_path(req.kind, req.index) else {
12456            if req.kind == RuntimeSaveKind::Normal {
12457                crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
12458                    &mut self.ctx,
12459                    crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
12460                );
12461            }
12462            return Ok(());
12463        };
12464        if self.save_load_trace_enabled() {
12465            eprintln!(
12466                "[SG_SAVELOAD_TRACE][VM] save begin kind={:?} idx={} path={} file_exists_before={}",
12467                req.kind,
12468                req.index,
12469                path.display(),
12470                crate::resource::game_file_exists(&path)
12471            );
12472        }
12473        let snapshot = self
12474            .ctx
12475            .local_save_snapshot
12476            .as_ref()
12477            .expect("snapshot presence checked above");
12478        let env = crate::original_save::OriginalLocalSaveEnvelope {
12479            save_id: snapshot.save_id,
12480            append_dir: snapshot.append_dir.clone(),
12481            append_name: snapshot.append_name.clone(),
12482            title: snapshot.save_scene_title.clone(),
12483            message: snapshot.save_msg.clone(),
12484            full_message: snapshot.save_full_msg.clone(),
12485            local_stream: snapshot.local_stream.clone(),
12486            local_ex_stream: snapshot.local_ex_stream.clone(),
12487            sel_saves: snapshot.sel_saves.clone(),
12488        };
12489        if let Err(err) = crate::original_save::write_local_save_file(&path, &slot, &env) {
12490            if req.kind == RuntimeSaveKind::Normal {
12491                crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
12492                    &mut self.ctx,
12493                    crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
12494                );
12495            }
12496            return Err(err);
12497        }
12498        crate::runtime::forms::syscom::write_global_save(&self.ctx);
12499        if self.save_load_trace_enabled() {
12500            eprintln!(
12501                "[SG_SAVELOAD_TRACE][VM] save written kind={:?} idx={} path={} bytes={}",
12502                req.kind,
12503                req.index,
12504                path.display(),
12505                crate::resource::game_file_len(&path).unwrap_or(0)
12506            );
12507        }
12508        // tnm_save_local_on_file() clears C_tnm_save_cache before writing and
12509        // leaves that slot uncached.  Do not immediately repopulate the Rust
12510        // header cache from the file; the next metadata query must reload it.
12511        match req.kind {
12512            RuntimeSaveKind::Normal => {
12513                if self.ctx.globals.syscom.save_slots.len() <= req.index {
12514                    self.ctx
12515                        .globals
12516                        .syscom
12517                        .save_slots
12518                        .resize_with(req.index + 1, Default::default);
12519                }
12520                self.ctx.globals.syscom.save_slots[req.index].header_cache_valid = false;
12521            }
12522            RuntimeSaveKind::Quick => {
12523                if self.ctx.globals.syscom.quick_save_slots.len() <= req.index {
12524                    self.ctx
12525                        .globals
12526                        .syscom
12527                        .quick_save_slots
12528                        .resize_with(req.index + 1, Default::default);
12529                }
12530                self.ctx.globals.syscom.quick_save_slots[req.index].header_cache_valid = false;
12531            }
12532            RuntimeSaveKind::End => {
12533                self.ctx.globals.syscom.end_save_exists = true;
12534            }
12535            RuntimeSaveKind::Inner => {}
12536        }
12537        if let Some(save_kind) = Self::save_kind_to_original(req.kind) {
12538            let save_no = crate::original_save::original_save_no(
12539                self.configured_runtime_save_count(false),
12540                self.configured_runtime_save_count(true),
12541                save_kind,
12542                req.index,
12543            );
12544            if self.save_load_trace_enabled() {
12545                eprintln!(
12546                    "[SG_SAVELOAD_TRACE][VM] save thumb write kind={:?} idx={} original_save_no={}",
12547                    req.kind, req.index, save_no
12548                );
12549            }
12550            crate::runtime::forms::syscom::write_runtime_slot_thumb(&mut self.ctx, save_no);
12551        }
12552        if req.kind == RuntimeSaveKind::Normal {
12553            crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
12554                &mut self.ctx,
12555                crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
12556            );
12557        }
12558        Ok(())
12559    }
12560
12561    fn perform_runtime_load_request(&mut self, req: RuntimeLoadRequest) -> Result<()> {
12562        if self.save_load_trace_enabled() {
12563            eprintln!(
12564                "[SG_SAVELOAD_TRACE][VM] load begin kind={:?} idx={}",
12565                req.kind, req.index
12566            );
12567        }
12568        struct LoadedEnvelopeMeta {
12569            save_id: [u16; 7],
12570            append_dir: String,
12571            append_name: String,
12572            title: String,
12573            message: String,
12574            full_message: String,
12575            sel_saves: Vec<crate::original_save::OriginalLocalSaveEnvelope>,
12576        }
12577        let (local_stream, local_ex_stream, loaded_meta) = if req.kind == RuntimeSaveKind::Inner {
12578            let Some(stream) = self
12579                .ctx
12580                .globals
12581                .syscom
12582                .inner_save_streams
12583                .get(req.index)
12584                .cloned()
12585            else {
12586                return Ok(());
12587            };
12588            (stream, Vec::new(), None)
12589        } else {
12590            let Some(path) = self.runtime_save_file_path(req.kind, req.index) else {
12591                return Ok(());
12592            };
12593            if self.save_load_trace_enabled() {
12594                eprintln!(
12595                    "[SG_SAVELOAD_TRACE][VM] load read kind={:?} idx={} path={} file_exists={}",
12596                    req.kind,
12597                    req.index,
12598                    path.display(),
12599                    crate::resource::game_file_exists(&path)
12600                );
12601            }
12602            let (_header, env) = crate::original_save::read_local_save_file(&path)?;
12603            let meta = LoadedEnvelopeMeta {
12604                save_id: env.save_id,
12605                append_dir: env.append_dir.clone(),
12606                append_name: env.append_name.clone(),
12607                title: env.title.clone(),
12608                message: env.message.clone(),
12609                full_message: env.full_message.clone(),
12610                sel_saves: env.sel_saves.clone(),
12611            };
12612            (env.local_stream, env.local_ex_stream, Some(meta))
12613        };
12614        if let Some(meta) = loaded_meta.as_ref() {
12615            self.ctx
12616                .set_active_append(meta.append_dir.clone(), meta.append_name.clone());
12617        }
12618        // VM-side equivalent of C++ `tnm_finish_local`: drop excall frames, sel
12619        // points, and the stale save point. The loaded scene re-establishes its
12620        // own context; without this, when the loaded scene eventually issues a
12621        // RETURN we'd pop back into the orphaned save/load menu excall frame.
12622        self.scene_stack.clear();
12623        self.scene_user_props.clear();
12624        self.sel_point_stack.clear();
12625        self.save_point = None;
12626        self.ctx.local_save_snapshot = None;
12627        self.ctx.begin_runtime_load_apply();
12628        let snapshot = self.parse_original_local_stream(&local_stream)?;
12629        self.parse_original_local_ex_stream(&local_ex_stream)?;
12630        // Mirror C++ `tnm_load_local_on_file` + tail of `load_local`: re-populate
12631        // `m_local_save` so the loaded scene can SAVE without first taking another
12632        // SAVEPOINT. C++ clears save_msg and copies save_full_msg = cur_full_message
12633        // after load_local; do the same here.
12634        if let Some(meta) = loaded_meta {
12635            self.ctx.local_save_snapshot = Some(crate::runtime::LocalSaveSnapshot {
12636                save_id: meta.save_id,
12637                append_dir: meta.append_dir,
12638                append_name: meta.append_name,
12639                save_scene_title: meta.title,
12640                save_msg: String::new(),
12641                save_full_msg: self.ctx.globals.syscom.current_save_full_message.clone(),
12642                local_stream: local_stream.clone(),
12643                local_ex_stream: local_ex_stream.clone(),
12644                sel_saves: meta.sel_saves,
12645            });
12646            // The header text from the loaded file (which represents the last
12647            // append'd-message state) takes precedence over what's left in
12648            // current_save_message after parse, so subsequent saves echo what the
12649            // user actually saw last.
12650            let snap = self.ctx.local_save_snapshot.as_ref().unwrap();
12651            self.ctx.globals.syscom.current_save_scene_title = snap.save_scene_title.clone();
12652        }
12653        if snapshot.scene_name.is_empty() {
12654            log::error!(
12655                "[SG_SAVELOAD] aborting load (kind={:?} idx={}): saved snapshot has empty scene_name. \
12656                 This save file is unusable; please delete it.",
12657                req.kind,
12658                req.index
12659            );
12660            return Ok(());
12661        }
12662        let (mut stream, scene_no) = self.load_scene_stream(&snapshot.scene_name, 0)?;
12663        stream.set_prg_cntr(snapshot.pc.max(0) as usize)?;
12664        let active_call_stack =
12665            self.restore_saved_scene_stack(snapshot.call_stack, &snapshot.scene_name)?;
12666        self.stream = stream;
12667        self.int_stack = snapshot.int_stack;
12668        self.str_stack = snapshot.str_stack;
12669        self.element_points = snapshot.element_points;
12670        self.call_stack = active_call_stack;
12671        if self.call_stack.is_empty() {
12672            self.call_stack.push(self.scene_base_call());
12673        }
12674        self.gosub_return_stack.clear();
12675        self.current_scene_no = if snapshot.scene_no >= 0 {
12676            Some(snapshot.scene_no as usize)
12677        } else {
12678            Some(scene_no)
12679        };
12680        self.current_scene_name = Some(snapshot.scene_name);
12681        self.current_line_no = snapshot.line_no;
12682        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
12683        self.ctx.current_scene_name = self.current_scene_name.clone();
12684        self.ctx.current_line_no = self.current_line_no as i64;
12685        self.ctx.wait = runtime::wait::VmWait::default();
12686        self.halted = false;
12687        self.delayed_ret_form = None;
12688        // C++ `C_elm_stage::load` / `C_elm_mwnd::load` end by calling each
12689        // object's `restruct_type()` to rebuild type-specific runtime resources.
12690        // Rust's sprite, image, movie, weather and mesh handles are not stored in
12691        // the save stream, so rebuild them recursively before resuming the script.
12692        self.restore_runtime_bindings_after_load();
12693        self.ctx.mark_runtime_load_completed();
12694        Ok(())
12695    }
12696
12697    /// Rebuild every loaded object's type-specific runtime backend, including
12698    /// top-level stage objects, message-window embedded objects and descendants.
12699    /// This is the Rust equivalent of the `restruct_type()` tail in
12700    /// `C_elm_object::load`; CAPTURE and EMOTE retain the original engine's
12701    /// non-reconstructible/unsupported behavior.
12702    fn restore_runtime_bindings_after_load(&mut self) {
12703        // C++ C_elm_object::load() always finishes with restruct_type().
12704        // Remove each form from globals while rebuilding so the backend helper
12705        // can mutably access the complete CommandContext without aliasing the map.
12706        let form_ids: Vec<u32> = self.ctx.globals.stage_forms.keys().copied().collect();
12707        for form_id in form_ids {
12708            let Some(mut stage_form) = self.ctx.globals.stage_forms.remove(&form_id) else {
12709                continue;
12710            };
12711            crate::runtime::forms::stage::restore_stage_form_backends_after_load(
12712                &mut self.ctx,
12713                &mut stage_form,
12714            );
12715            self.ctx.globals.stage_forms.insert(form_id, stage_form);
12716        }
12717    }
12718
12719    fn drain_runtime_save_load_requests(&mut self) -> Result<()> {
12720        // Auto SAVEPOINT must fire before any pending save in the same command
12721        // batch, so a SAVE issued from the script's first frame after a message
12722        // block start still has a snapshot to write.
12723        if self.ctx.take_pending_auto_savepoint() {
12724            self.build_local_save_snapshot();
12725            // The form handler has already appended the first text by this
12726            // deferred boundary. C++ takes its snapshot before that append;
12727            // retain the new block's summary instead of discarding it here.
12728            if let Some(snapshot) = self.ctx.local_save_snapshot.as_mut() {
12729                snapshot.save_msg = self.ctx.globals.syscom.current_save_message.clone();
12730            }
12731        }
12732        if let Some(req) = self.ctx.take_runtime_save_request() {
12733            self.perform_runtime_save_request(req)?;
12734        }
12735        if let Some(req) = self.ctx.take_runtime_load_request() {
12736            self.perform_runtime_load_request(req)?;
12737        }
12738        Ok(())
12739    }
12740
12741    fn exec_return(&mut self, args: Vec<Value>) -> Result<bool> {
12742        // Pop callee frame.
12743        let Some(callee) = self.call_stack.pop() else {
12744            return Ok(false);
12745        };
12746
12747        // Return info is stored on the caller frame .
12748        let caller = match self.call_stack.last_mut() {
12749            Some(f) => f,
12750            None => {
12751                // No caller: treat as end.
12752                return Ok(false);
12753            }
12754        };
12755
12756        // Original C++ persists the continuation on the caller frame. Rust also
12757        // has out-of-band inline user-command/frame-action execution that can
12758        // temporarily reuse those caller fields while another call is suspended.
12759        // Calls created by those Rust paths therefore keep a per-callee copy;
12760        // frames loaded from original saves have no override and continue to use
12761        // the original caller-frame representation.
12762        let (return_pc, ret_form) = callee
12763            .return_override
12764            .unwrap_or((caller.return_pc, caller.ret_form));
12765        if self.runtime_options.trace_call_return_pc {
12766            eprintln!(
12767                "[SG_CALL_PC] return depth={} pc=0x{:x} ret_form={} override={:?} args={:?}",
12768                self.call_stack.len() + 1,
12769                return_pc,
12770                ret_form,
12771                callee.return_override,
12772                args
12773            );
12774        }
12775        self.stream.set_prg_cntr(return_pc)?;
12776
12777        match ret_form {
12778            f if f == self.cfg.fm_int => {
12779                let v = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
12780                self.push_int(v);
12781            }
12782            f if f == self.cfg.fm_str => {
12783                let s = args
12784                    .first()
12785                    .and_then(|v| v.as_str().map(|s| s.to_string()))
12786                    .unwrap_or_default();
12787                self.push_str(s);
12788            }
12789            _ => {
12790                // Ignore.
12791            }
12792        }
12793
12794        let frame_action_proc = callee.frame_action_proc;
12795        let excall_proc = callee.excall_proc;
12796        if excall_proc {
12797            self.mark_excall_script_proc_pop_requested();
12798        }
12799        self.recycle_call_frame(callee);
12800
12801        Ok(frame_action_proc)
12802    }
12803
12804    fn scene_base_call(&self) -> CallFrame {
12805        self.make_call_frame(self.cfg.fm_void, false, false, 0, None)
12806    }
12807
12808    fn load_scene_stream(
12809        &mut self,
12810        scene_name: &str,
12811        z_no: i32,
12812    ) -> Result<(SceneStream<'a>, usize)> {
12813        self.ensure_scene_pck_cache()?;
12814        let scene_no = self
12815            .scene_pck_cache
12816            .as_ref()
12817            .expect("scene pck cache initialized")
12818            .find_scene_no(scene_name)
12819            .ok_or_else(|| anyhow!("scene not found: {}", scene_name))?;
12820        let mut stream = self.cached_scene_stream(scene_no)?;
12821        sg_omv_trace!(
12822            self,
12823            "load_scene_stream resolved target={} scene_no={} z={} initial_pc=0x{:x} scn_len=0x{:x}",
12824            scene_name,
12825            scene_no,
12826            z_no,
12827            stream.get_prg_cntr(),
12828            stream.scn.len()
12829        );
12830        self.call_cmd_names = self
12831            .scene_pck_cache
12832            .as_ref()
12833            .expect("scene pck cache initialized")
12834            .inc_cmd_name_map
12835            .clone();
12836        self.user_cmd_names = stream.scn_cmd_name_map.clone();
12837        match stream.jump_to_z_label(z_no.max(0) as usize) {
12838            Ok(()) => {
12839                sg_omv_trace!(
12840                    self,
12841                    "load_scene_stream entered target={} scene_no={} z={} target_pc=0x{:x} user_cmd_cnt={} call_cmd_cnt={}",
12842                    scene_name,
12843                    scene_no,
12844                    z_no,
12845                    stream.get_prg_cntr(),
12846                    stream.scn_cmd_name_map.len(),
12847                    self.call_cmd_names.len()
12848                );
12849            }
12850            Err(e) => {
12851                sg_omv_trace!(
12852                    self,
12853                    "load_scene_stream failed target={} scene_no={} z={} error={}",
12854                    scene_name,
12855                    scene_no,
12856                    z_no,
12857                    e
12858                );
12859                return Err(e);
12860            }
12861        }
12862        Ok((stream, scene_no))
12863    }
12864
12865    fn jump_to_scene_name(&mut self, scene_name: &str, z_no: i32) -> Result<()> {
12866        sg_omv_trace!(self, "scene_jump target={} z={}", scene_name, z_no);
12867        let (stream, scene_no) = self.load_scene_stream(scene_name, z_no)?;
12868        self.stash_current_scene_user_props();
12869        self.stream = stream;
12870        self.current_scene_no = Some(scene_no);
12871        self.activate_scene_user_prop_scope(scene_no);
12872        self.current_scene_name = Some(scene_name.to_string());
12873        self.current_line_no = -1;
12874        self.ctx.current_scene_no = Some(scene_no as i64);
12875        self.ctx.current_scene_name = Some(scene_name.to_string());
12876        self.ctx.current_line_no = -1;
12877        sg_omv_trace!(
12878            self,
12879            "scene_jump_entered target={} scene_no={} z={} pc=0x{:x}",
12880            scene_name,
12881            scene_no,
12882            z_no,
12883            self.stream.get_prg_cntr()
12884        );
12885        Ok(())
12886    }
12887
12888    fn farcall_scene_name_ex(
12889        &mut self,
12890        scene_name: &str,
12891        z_no: i32,
12892        ret_form: i32,
12893        ex_call_proc: bool,
12894        scratch_source_args: &[Value],
12895    ) -> Result<()> {
12896        sg_omv_trace!(
12897            self,
12898            "scene_farcall target={} z={} ret_form={} ex_call_proc={} scratch_argc={}",
12899            scene_name,
12900            z_no,
12901            ret_form,
12902            ex_call_proc,
12903            scratch_source_args.len()
12904        );
12905        self.trace_cf_branch_farcall(
12906            self.stream.get_prg_cntr(),
12907            scene_name,
12908            z_no,
12909            ret_form,
12910            ex_call_proc,
12911            scratch_source_args,
12912        );
12913        if self.sg_debug_enabled()
12914            && ((scene_name == "sys20_adv00" && matches!(z_no, 10 | 13 | 17))
12915                || (scene_name == "sys20_adv01" && z_no == 0))
12916        {
12917            let args_dbg = scratch_source_args
12918                .iter()
12919                .map(|v| format!("{v:?}"))
12920                .collect::<Vec<_>>()
12921                .join(", ");
12922            self.sg_cgm_coord_trace_emit(format_args!(
12923                "farcall target={} z={} ret_form={} ex_call_proc={} argc={} args=[{}]",
12924                scene_name,
12925                z_no,
12926                ret_form,
12927                ex_call_proc,
12928                scratch_source_args.len(),
12929                args_dbg
12930            ));
12931        }
12932
12933        self.ensure_scene_pck_cache()?;
12934        let scene_no = self
12935            .scene_pck_cache
12936            .as_ref()
12937            .expect("scene pck cache initialized")
12938            .find_scene_no(scene_name)
12939            .ok_or_else(|| anyhow!("scene not found: {}", scene_name))?;
12940        let mut target_stream = self.cached_scene_stream(scene_no)?;
12941        target_stream.jump_to_z_label(z_no.max(0) as usize)?;
12942
12943        let return_pc = self.stream.get_prg_cntr();
12944        let depth = self.call_stack.len();
12945        let caller = self
12946            .call_stack
12947            .last_mut()
12948            .ok_or_else(|| anyhow!("call stack underflow entering FARCALL"))?;
12949        if self.runtime_options.trace_call_return_pc {
12950            eprintln!(
12951                "[SG_CALL_PC] cross-scene farcall set depth={} target_scene={} z={} return_pc=0x{:x} old=0x{:x}",
12952                depth, scene_no, z_no, return_pc, caller.return_pc
12953            );
12954        }
12955        caller.return_pc = return_pc;
12956        caller.return_scene_no = self.current_scene_no;
12957        caller.return_scene_name = self.current_scene_name.clone();
12958        caller.return_line_no = self.current_line_no;
12959        caller.ret_form = ret_form;
12960
12961        let target_user_cmd_names = target_stream.scn_cmd_name_map.clone();
12962        let target_call_cmd_names = self
12963            .scene_pck_cache
12964            .as_ref()
12965            .expect("scene pck cache initialized")
12966            .inc_cmd_name_map
12967            .clone();
12968        let saved_stream = std::mem::replace(&mut self.stream, target_stream);
12969        let saved_user_cmd_names =
12970            std::mem::replace(&mut self.user_cmd_names, target_user_cmd_names);
12971        let saved_call_cmd_names =
12972            std::mem::replace(&mut self.call_cmd_names, target_call_cmd_names);
12973        let saved_current_scene_no = self.current_scene_no;
12974        let saved_current_scene_name = self.current_scene_name.clone();
12975        let saved_current_line_no = self.current_line_no;
12976
12977        self.enter_cross_scene_user_prop_scope(scene_no);
12978        self.current_scene_no = Some(scene_no);
12979        self.current_scene_name = Some(scene_name.to_string());
12980        self.current_line_no = -1;
12981        self.ctx.current_scene_no = Some(scene_no as i64);
12982        self.ctx.current_scene_name = Some(scene_name.to_string());
12983        self.ctx.current_line_no = -1;
12984
12985        let scratch_args = self.call_scratch_from_args(scratch_source_args);
12986        let mut call_frame = self.take_call_frame(
12987            self.cfg.fm_void,
12988            ex_call_proc,
12989            false,
12990            scratch_source_args.len(),
12991            Some(scratch_args),
12992        );
12993        call_frame.call_type = 2;
12994        self.call_stack.push(call_frame);
12995        self.scene_stack.push(SceneExecFrame {
12996            stream: saved_stream,
12997            user_cmd_names: saved_user_cmd_names,
12998            call_cmd_names: saved_call_cmd_names,
12999            current_scene_no: saved_current_scene_no,
13000            current_scene_name: saved_current_scene_name,
13001            current_line_no: saved_current_line_no,
13002            call_depth: self.call_stack.len(),
13003        });
13004
13005        sg_omv_trace!(
13006            self,
13007            "scene_farcall_entered target={} scene_no={} z={} pc=0x{:x} call_depth={} scene_stack={}",
13008            scene_name,
13009            scene_no,
13010            z_no,
13011            self.stream.get_prg_cntr(),
13012            self.call_stack.len(),
13013            self.scene_stack.len()
13014        );
13015        if ex_call_proc {
13016            self.mark_excall_script_proc_requested();
13017        }
13018        Ok(())
13019    }
13020
13021    #[inline(always)]
13022    fn at_cross_scene_return_boundary(&self) -> bool {
13023        self.scene_stack
13024            .last()
13025            .is_some_and(|saved| saved.call_depth == self.call_stack.len())
13026    }
13027
13028    fn return_from_scene(&mut self, args: Vec<Value>) -> Result<bool> {
13029        let Some(saved) = self.scene_stack.pop() else {
13030            return Ok(false);
13031        };
13032        if saved.call_depth != self.call_stack.len() {
13033            let expected = saved.call_depth;
13034            self.scene_stack.push(saved);
13035            bail!(
13036                "cross-scene RETURN at wrong call depth: current={} expected={}",
13037                self.call_stack.len(),
13038                expected
13039            );
13040        }
13041
13042        let callee = self
13043            .call_stack
13044            .pop()
13045            .ok_or_else(|| anyhow!("call stack underflow returning from scene"))?;
13046        let (return_pc, ret_form) = self
13047            .call_stack
13048            .last()
13049            .map(|caller| (caller.return_pc, caller.ret_form))
13050            .ok_or_else(|| anyhow!("caller frame missing returning from scene"))?;
13051
13052        sg_omv_trace!(
13053            self,
13054            "scene_return restore_scene={:?} restore_line={} ret_form={} args={:?}",
13055            saved.current_scene_name,
13056            saved.current_line_no,
13057            ret_form,
13058            args
13059        );
13060
13061        // Save target-scene locals and reactivate caller locals before changing
13062        // current_scene_no. Shared include properties stay in-place.
13063        self.restore_cross_scene_user_prop_scope(saved.current_scene_no);
13064        self.stream = saved.stream;
13065        self.user_cmd_names = saved.user_cmd_names;
13066        self.call_cmd_names = saved.call_cmd_names;
13067        self.current_scene_no = saved.current_scene_no;
13068        self.current_scene_name = saved.current_scene_name;
13069        self.current_line_no = saved.current_line_no;
13070        self.ctx.current_scene_no = self.current_scene_no.map(|v| v as i64);
13071        self.ctx.current_scene_name = self.current_scene_name.clone();
13072        self.ctx.current_line_no = self.current_line_no as i64;
13073        self.stream.set_prg_cntr(return_pc)?;
13074
13075        match ret_form {
13076            f if f == self.cfg.fm_int || f == self.cfg.fm_label => {
13077                let v = args.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
13078                self.push_int(v);
13079            }
13080            f if f == self.cfg.fm_str => {
13081                let s = args
13082                    .first()
13083                    .and_then(|v| v.as_str().map(|s| s.to_string()))
13084                    .unwrap_or_default();
13085                self.push_str(s);
13086            }
13087            _ => {}
13088        }
13089
13090        let was_excall_proc = callee.excall_proc;
13091        let was_frame_action_proc = callee.frame_action_proc;
13092        self.recycle_call_frame(callee);
13093        if was_excall_proc {
13094            self.mark_excall_script_proc_pop_requested();
13095        }
13096        if self.cf_branch_trace_interesting_line() {
13097            self.sg_cf_branch_trace_emit(
13098                self.stream.get_prg_cntr(),
13099                format_args!("kind=RETURN_RESTORED ret_form={} args={:?}", ret_form, args),
13100            );
13101        }
13102        sg_omv_trace!(
13103            self,
13104            "scene_return_restored scene={:?} scene_no={:?} line={} pc=0x{:x} call_depth={} scene_stack={} frame_action={}",
13105            self.current_scene_name,
13106            self.current_scene_no,
13107            self.current_line_no,
13108            self.stream.get_prg_cntr(),
13109            self.call_stack.len(),
13110            self.scene_stack.len(),
13111            was_frame_action_proc
13112        );
13113        Ok(true)
13114    }
13115
13116    /// Match `C_elm_call_list::set_call_cnt()` for the script-visible
13117    /// GLOBAL call-stack control commands. The C++ engine never grows the
13118    /// stack through SET_CALL_STACK_CNT/DEL_CALL_STACK; it only discards
13119    /// logical call entries and leaves the current lexer position untouched.
13120    ///
13121    /// `scene_stack` is Rust-only state used to represent the caller lexer
13122    /// snapshots which C++ stores in `C_elm_call::m_call_save`. Any boundary
13123    /// whose callee frame has been discarded must therefore be discarded too,
13124    /// otherwise a later RETURN could resurrect a caller that no longer exists
13125    /// in the original call list.
13126    fn shrink_call_stack_to(&mut self, dst_cnt: usize) {
13127        let dst_cnt = dst_cnt.max(1).min(self.call_stack.len());
13128        while self.call_stack.len() > dst_cnt {
13129            if let Some(frame) = self.call_stack.pop() {
13130                self.recycle_call_frame(frame);
13131            }
13132        }
13133        while self
13134            .scene_stack
13135            .last()
13136            .is_some_and(|saved| saved.call_depth > dst_cnt)
13137        {
13138            self.scene_stack.pop();
13139        }
13140    }
13141
13142    fn exec_builtin_global_control(
13143        &mut self,
13144        form_id: i32,
13145        ret_form: i32,
13146        args: &[Value],
13147    ) -> Result<bool> {
13148        match form_id {
13149            constants::elm_value::GLOBAL_INIT_CALL_STACK => {
13150                // eng_scene.cpp::tnm_scene_init_call_stack():
13151                //     Gp_call_list->set_call_cnt(1);
13152                self.shrink_call_stack_to(1);
13153                Ok(true)
13154            }
13155            constants::elm_value::GLOBAL_DEL_CALL_STACK => {
13156                // tnm_scene_del_call_stack(): ignore non-positive counts and
13157                // never reduce the logical stack below its base entry.
13158                let del_cnt = args.first().and_then(Value::as_i64).unwrap_or(0);
13159                if del_cnt > 0 {
13160                    let del_cnt = usize::try_from(del_cnt).unwrap_or(usize::MAX);
13161                    let dst_cnt = self.call_stack.len().saturating_sub(del_cnt).max(1);
13162                    self.shrink_call_stack_to(dst_cnt);
13163                }
13164                Ok(true)
13165            }
13166            constants::elm_value::GLOBAL_SET_CALL_STACK_CNT => {
13167                // tnm_scene_set_call_stack_cnt() is deliberately shrink-only.
13168                // Values below one and requests to grow the stack are no-ops.
13169                let dst_cnt = args.first().and_then(Value::as_i64).unwrap_or(0);
13170                if dst_cnt >= 1
13171                    && let Ok(dst_cnt) = usize::try_from(dst_cnt)
13172                    && dst_cnt < self.call_stack.len()
13173                {
13174                    self.shrink_call_stack_to(dst_cnt);
13175                }
13176                Ok(true)
13177            }
13178            constants::elm_value::GLOBAL_GET_CALL_STACK_CNT => {
13179                // C++ pushes the current logical C_elm_call_list count. Rust's
13180                // call_stack includes the same mandatory base entry, so the
13181                // lengths are directly equivalent.
13182                self.ctx
13183                    .stack
13184                    .push(Value::Int(self.call_stack.len() as i64));
13185                Ok(true)
13186            }
13187            constants::elm_value::GLOBAL_SAVEPOINT => {
13188                // C++ `ELM_GLOBAL_SAVEPOINT` temporarily pushes 1 before
13189                // `tnm_set_save_point()` and then replaces it with return 0.
13190                // A later load resumes from the saved stream with that 1 still
13191                // on the int stack, allowing scripts to distinguish "loaded from
13192                // this SAVEPOINT" from normal forward execution.
13193                self.int_stack.push(1);
13194                self.save_point = Some(self.make_resume_point());
13195                self.build_local_save_snapshot();
13196                let _ = self.int_stack.pop();
13197                if ret_form != self.cfg.fm_void {
13198                    self.ctx.stack.push(Value::Int(0));
13199                }
13200                Ok(true)
13201            }
13202            constants::elm_value::GLOBAL_CLEAR_SAVEPOINT => {
13203                self.save_point = None;
13204                self.ctx.local_save_snapshot = None;
13205                Ok(true)
13206            }
13207            constants::elm_value::GLOBAL_CHECK_SAVEPOINT => {
13208                let has = self
13209                    .ctx
13210                    .local_save_snapshot
13211                    .as_ref()
13212                    .map(|s| !s.local_stream.is_empty())
13213                    .unwrap_or(false);
13214                self.ctx.stack.push(Value::Int(if has { 1 } else { 0 }));
13215                Ok(true)
13216            }
13217            constants::elm_value::GLOBAL_SELPOINT => {
13218                let point = self.make_resume_point();
13219                self.sel_point_stack.clear();
13220                self.sel_point_stack.push(point);
13221                Ok(true)
13222            }
13223            constants::elm_value::GLOBAL_CLEAR_SELPOINT => {
13224                self.sel_point_stack.clear();
13225                Ok(true)
13226            }
13227            constants::elm_value::GLOBAL_CHECK_SELPOINT => {
13228                self.ctx
13229                    .stack
13230                    .push(Value::Int(if self.has_sel_point() { 1 } else { 0 }));
13231                Ok(true)
13232            }
13233            constants::elm_value::GLOBAL_STACK_SELPOINT => {
13234                let point = self.make_resume_point();
13235                self.sel_point_stack.push(point);
13236                Ok(true)
13237            }
13238            constants::elm_value::GLOBAL_DROP_SELPOINT => {
13239                let _ = self.sel_point_stack.pop();
13240                Ok(true)
13241            }
13242            _ => Ok(false),
13243        }
13244    }
13245
13246    fn exec_mouse_get_pos(&mut self, elm: &[i32], form_id: i32, args: &[Value]) -> Result<bool> {
13247        if form_id != self.ctx.ids.form_global_mouse as i32
13248            && form_id != constants::global_form::MOUSE as i32
13249            && form_id != constants::fm::MOUSE
13250        {
13251            return Ok(false);
13252        }
13253        if elm.get(1).copied() != Some(self.ctx.ids.mouse_op_get_pos) {
13254            return Ok(false);
13255        }
13256
13257        // cmd_input.cpp uses tnm_command_proc_set_int for both INTREFs.
13258        // These can name VM-owned user/call properties, which the runtime's
13259        // generic property maps cannot write (notably title-menu coordinates).
13260        let pos = [self.ctx.script_input.mouse_x, self.ctx.script_input.mouse_y];
13261        for (arg, value) in args.iter().zip(pos) {
13262            if let Value::Element(target) = arg.unwrap_named() {
13263                self.exec_assign(target.clone(), 1, Value::Int(i64::from(value)))?;
13264            }
13265        }
13266        Ok(true)
13267    }
13268
13269    fn exec_syscom_save_value_intlistref(
13270        &mut self,
13271        elm: &[i32],
13272        form_id: i32,
13273        ret_form: i32,
13274        args: &[Value],
13275    ) -> Result<bool> {
13276        use crate::runtime::forms::codes::{ELM_ARRAY, FM_SYSCOM, FORM_GLOBAL_SYSCOM, elm_value};
13277
13278        if form_id != FORM_GLOBAL_SYSCOM as i32 && form_id != FM_SYSCOM {
13279            return Ok(false);
13280        }
13281        let Some(op) = elm.get(1).copied() else {
13282            return Ok(false);
13283        };
13284        let (quick, write) = match op {
13285            elm_value::SYSCOM_GET_SAVE_VALUE => (false, false),
13286            elm_value::SYSCOM_GET_QUICK_SAVE_VALUE => (true, false),
13287            elm_value::SYSCOM_SET_SAVE_VALUE => (false, true),
13288            elm_value::SYSCOM_SET_QUICK_SAVE_VALUE => (true, true),
13289            _ => return Ok(false),
13290        };
13291
13292        // All four commands are FM_VOID in def_element_Siglus.h. The second
13293        // parameter is an actual INTLISTREF; C++ resolves the complete S_element
13294        // with tnm_get_element_ptr(), so this cannot be reduced to chain[0].
13295        if ret_form != self.cfg.fm_void {
13296            return Ok(false);
13297        }
13298        let raw_save_no = args.first().and_then(Value::as_i64).unwrap_or(-1);
13299        let Some(Value::Element(base_chain)) = args.get(1).map(Value::unwrap_named) else {
13300            return Ok(true);
13301        };
13302        if base_chain.is_empty() {
13303            return Ok(true);
13304        }
13305        let flag_index = args.get(2).and_then(Value::as_i64).unwrap_or(0);
13306        let flag_cnt_raw = args.get(3).and_then(Value::as_i64).unwrap_or(0);
13307        if flag_cnt_raw <= 0 {
13308            return Ok(true);
13309        }
13310        let flag_cnt = usize::try_from(flag_cnt_raw)
13311            .unwrap_or(usize::MAX)
13312            .min(crate::original_save::SAVE_FLAG_MAX_CNT);
13313
13314        if !write {
13315            let Some(values) = crate::runtime::forms::syscom::read_save_flag_values(
13316                &mut self.ctx,
13317                quick,
13318                raw_save_no,
13319                flag_cnt,
13320            ) else {
13321                return Ok(true);
13322            };
13323            for (i, value) in values.into_iter().enumerate() {
13324                let Some(index) = flag_index.checked_add(i as i64) else {
13325                    break;
13326                };
13327                if index < 0 {
13328                    continue;
13329                }
13330                let Ok(index) = i32::try_from(index) else {
13331                    continue;
13332                };
13333                let mut target = base_chain.clone();
13334                target.push(ELM_ARRAY);
13335                target.push(index);
13336                self.exec_assign(target, 1, Value::Int(value))?;
13337            }
13338        } else {
13339            let mut values = Vec::with_capacity(flag_cnt);
13340            for i in 0..flag_cnt {
13341                let Some(index) = flag_index.checked_add(i as i64) else {
13342                    break;
13343                };
13344                if index < 0 {
13345                    values.push(0);
13346                    continue;
13347                }
13348                let Ok(index) = i32::try_from(index) else {
13349                    values.push(0);
13350                    continue;
13351                };
13352                let mut source = base_chain.clone();
13353                source.push(ELM_ARRAY);
13354                source.push(index);
13355                self.exec_property(source)?;
13356                values.push(i64::from(self.pop_int()?));
13357            }
13358            let _ = crate::runtime::forms::syscom::write_save_flag_values(
13359                &mut self.ctx,
13360                quick,
13361                raw_save_no,
13362                &values,
13363            );
13364        }
13365
13366        // The original command pushes nothing for FM_VOID.
13367        self.ctx.stack.clear();
13368        Ok(true)
13369    }
13370
13371    fn exec_builtin_scene_form(
13372        &mut self,
13373        elm: &[i32],
13374        form_id: i32,
13375        al_id: i32,
13376        ret_form: i32,
13377        args: &[Value],
13378    ) -> Result<bool> {
13379        const FORM_GLOBAL_JUMP: i32 = crate::runtime::forms::codes::elm_value::GLOBAL_JUMP;
13380        const FORM_GLOBAL_FARCALL: i32 = crate::runtime::forms::codes::elm_value::GLOBAL_FARCALL;
13381        const FORM_GLOBAL_SYSCOM: i32 = crate::runtime::forms::codes::FORM_GLOBAL_SYSCOM as i32;
13382        const FORM_SYSCOM: i32 = crate::runtime::forms::codes::FM_SYSCOM;
13383        const ELM_SYSCOM_CALL_EX: i32 = crate::runtime::forms::codes::elm_value::SYSCOM_CALL_EX;
13384        if (form_id == FORM_GLOBAL_SYSCOM || form_id == FORM_SYSCOM)
13385            && elm.get(1).copied() == Some(ELM_SYSCOM_CALL_EX)
13386        {
13387            self.sg_omv_trace_command(
13388                "builtin",
13389                elm,
13390                form_id,
13391                ELM_SYSCOM_CALL_EX,
13392                al_id,
13393                self.cfg.fm_void,
13394                args,
13395            );
13396            let scene_name = args.first().and_then(|v| v.as_str()).unwrap_or("");
13397            let z_no = if al_id == 1 {
13398                args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32
13399            } else {
13400                0
13401            };
13402            let scratch_args = if al_id == 1 && args.len() > 2 {
13403                &args[2..]
13404            } else {
13405                &[]
13406            };
13407            self.farcall_scene_name_ex(scene_name, z_no, self.cfg.fm_void, true, scratch_args)?;
13408            self.ctx.stack.clear();
13409            return Ok(true);
13410        }
13411        if form_id == FORM_GLOBAL_JUMP {
13412            self.sg_omv_trace_command("builtin", &[], form_id, form_id, al_id, ret_form, args);
13413            let scene_name = args.first().and_then(|v| v.as_str()).unwrap_or("");
13414            let z_no = if al_id >= 1 {
13415                args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32
13416            } else {
13417                0
13418            };
13419            if !scene_name.is_empty() {
13420                self.jump_to_scene_name(scene_name, z_no)?;
13421            }
13422            return Ok(true);
13423        }
13424        if form_id == FORM_GLOBAL_FARCALL {
13425            self.sg_omv_trace_command("builtin", &[], form_id, form_id, al_id, ret_form, args);
13426            let scene_name = args.first().and_then(|v| v.as_str()).unwrap_or("");
13427            let z_no = if al_id >= 1 {
13428                args.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32
13429            } else {
13430                0
13431            };
13432            if !scene_name.is_empty() {
13433                self.farcall_scene_name_ex(
13434                    scene_name,
13435                    z_no,
13436                    self.cfg.fm_int,
13437                    false,
13438                    if al_id >= 1 && args.len() > 2 {
13439                        &args[2..]
13440                    } else {
13441                        &[]
13442                    },
13443                )?;
13444            } else {
13445                self.push_default_for_ret(self.cfg.fm_int);
13446            }
13447            return Ok(true);
13448        }
13449        Ok(false)
13450    }
13451
13452    fn take_ctx_return(&mut self, ret_form: i32) -> Result<()> {
13453        if ret_form == self.cfg.fm_void {
13454            self.ctx.stack.clear();
13455            return Ok(());
13456        }
13457
13458        let v = self.ctx.pop();
13459        match ret_form {
13460            f if f == self.cfg.fm_int || f == self.cfg.fm_label => match v {
13461                Some(Value::Int(n)) => self.push_int(n as i32),
13462                Some(Value::NamedArg { value, .. }) => match *value {
13463                    Value::Int(n) => self.push_int(n as i32),
13464                    _ => bail!("non-int ctx return for form {}", ret_form),
13465                },
13466                Some(_) => bail!("non-int ctx return for form {}", ret_form),
13467                None => bail!(
13468                    "missing ctx return int for form {}: scene={} scene_no={} line={} pc=0x{:x} vm_call={:?}",
13469                    ret_form,
13470                    self.current_scene_name.as_deref().unwrap_or("<none>"),
13471                    self.current_scene_no
13472                        .map(|v| v.to_string())
13473                        .unwrap_or_else(|| "-".to_string()),
13474                    self.current_line_no,
13475                    self.stream.get_prg_cntr(),
13476                    self.ctx.vm_call
13477                ),
13478            },
13479            f if f == self.cfg.fm_str => match v {
13480                Some(Value::Str(s)) => self.push_str(s),
13481                Some(Value::NamedArg { value, .. }) => match *value {
13482                    Value::Str(s) => self.push_str(s),
13483                    _ => bail!("non-str ctx return for form {}", ret_form),
13484                },
13485                Some(_) => bail!("non-str ctx return for form {}", ret_form),
13486                None => bail!("missing ctx return str for form {}", ret_form),
13487            },
13488            f if f == self.cfg.fm_list => match v {
13489                Some(Value::Element(elm)) => self.push_element(elm),
13490                Some(Value::NamedArg { value, .. }) => match *value {
13491                    Value::Element(elm) => self.push_element(elm),
13492                    _ => bail!("non-element ctx return for FM_LIST"),
13493                },
13494                Some(Value::List(_)) => {
13495                    bail!("FM_LIST ctx return used raw Value::List; expected element reference")
13496                }
13497                Some(_) => bail!("non-element ctx return for FM_LIST"),
13498                None => bail!("missing ctx return element for FM_LIST"),
13499            },
13500            _ => match v {
13501                Some(Value::Element(elm)) => self.push_element(elm),
13502                Some(Value::NamedArg { value, .. }) => match *value {
13503                    Value::Element(elm) => self.push_element(elm),
13504                    _ => bail!("non-element ctx return for form {}", ret_form),
13505                },
13506                Some(_) => bail!("non-element ctx return for form {}", ret_form),
13507                None => bail!("missing ctx return element for form {}", ret_form),
13508            },
13509        }
13510        Ok(())
13511    }
13512
13513    fn push_default_for_ret(&mut self, ret_form: i32) {
13514        if ret_form == self.cfg.fm_int || ret_form == self.cfg.fm_label {
13515            self.push_int(0);
13516        } else if ret_form == self.cfg.fm_str {
13517            self.push_str(String::new());
13518        }
13519    }
13520
13521    fn update_compact_context_from_element(&mut self, elm: &[i32]) {
13522        let Some(raw_stage_form) = elm.first().copied() else {
13523            return;
13524        };
13525        if !crate::runtime::forms::stage::is_stage_form_id(&self.ctx, raw_stage_form) {
13526            return;
13527        }
13528        let stage_form =
13529            crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, raw_stage_form) as i32;
13530        let elm_array = if self.ctx.ids.elm_array != 0 {
13531            self.ctx.ids.elm_array
13532        } else {
13533            crate::runtime::forms::codes::ELM_ARRAY
13534        };
13535        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
13536            self.ctx.ids.stage_elm_object
13537        } else {
13538            crate::runtime::forms::codes::STAGE_ELM_OBJECT
13539        };
13540        let stage_mwnd = crate::runtime::forms::codes::STAGE_ELM_MWND;
13541        let stage_btnselitem = crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM;
13542
13543        fn is_array_token(token: i32, elm_array: i32) -> bool {
13544            token == elm_array || token == crate::runtime::forms::codes::ELM_ARRAY
13545        }
13546
13547        fn object_chain_tail_is_plain_object_ref(
13548            elm: &[i32],
13549            mut pos: usize,
13550            elm_array: i32,
13551        ) -> bool {
13552            let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
13553            while pos + 2 < elm.len()
13554                && elm[pos] == object_child
13555                && is_array_token(elm[pos + 1], elm_array)
13556            {
13557                if elm[pos + 2] < 0 {
13558                    return false;
13559                }
13560                pos += 3;
13561            }
13562            pos == elm.len()
13563        }
13564
13565        let resolved = if elm.len() >= 6
13566            && crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, elm[0]) as i32
13567                == stage_form
13568            && is_array_token(elm[1], elm_array)
13569            && elm[2] >= 0
13570            && elm[3] == stage_object
13571            && is_array_token(elm[4], elm_array)
13572            && elm[5] >= 0
13573            && object_chain_tail_is_plain_object_ref(elm, 6, elm_array)
13574        {
13575            Some((elm[2] as i64, elm[5] as usize))
13576        } else if elm.len() >= 9
13577            && crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, elm[0]) as i32
13578                == stage_form
13579            && is_array_token(elm[1], elm_array)
13580            && elm[2] >= 0
13581            && is_array_token(elm[4], elm_array)
13582            && elm[5] >= 0
13583            && ((elm[3] == stage_mwnd
13584                && matches!(
13585                    elm[6],
13586                    crate::runtime::forms::codes::elm_value::MWND_OBJECT
13587                        | crate::runtime::forms::codes::elm_value::MWND_BUTTON
13588                        | crate::runtime::forms::codes::elm_value::MWND_FACE
13589                ))
13590                || (elm[3] == stage_btnselitem
13591                    && elm[6] == crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT))
13592            && is_array_token(elm[7], elm_array)
13593            && elm[8] >= 0
13594            && object_chain_tail_is_plain_object_ref(elm, 9, elm_array)
13595        {
13596            Some((elm[2] as i64, elm[8] as usize))
13597        } else {
13598            None
13599        };
13600
13601        let Some((stage_idx, fallback_obj_idx)) = resolved else {
13602            return;
13603        };
13604        let runtime_slot = self.runtime_slot_from_object_chain(fallback_obj_idx, elm);
13605        let prev_chain = self.ctx.globals.current_object_chain.clone();
13606        let prev_stage_object = self.ctx.globals.current_stage_object;
13607        self.ctx.globals.current_object_chain = Some(elm.to_vec());
13608        self.ctx.globals.current_stage_object = Some((stage_idx, runtime_slot));
13609        if self.sg_mwnd_object_trace_enabled() && Self::sg_mwnd_chain_interesting(elm) {
13610            self.sg_mwnd_object_trace_emit(format_args!(
13611                "update_compact_context elm={:?} resolved_stage={} fallback_idx={} runtime_slot={} prev_chain={:?} prev_stage_object={:?}",
13612                elm,
13613                stage_idx,
13614                fallback_obj_idx,
13615                runtime_slot,
13616                prev_chain,
13617                prev_stage_object
13618            ));
13619        }
13620    }
13621
13622    fn update_compact_context_from_object_dispatch_chain(&mut self, elm: &[i32]) {
13623        let Some(raw_stage_form) = elm.first().copied() else {
13624            return;
13625        };
13626        if !crate::runtime::forms::stage::is_stage_form_id(&self.ctx, raw_stage_form) {
13627            return;
13628        }
13629        let stage_form =
13630            crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, raw_stage_form) as i32;
13631        let elm_array = if self.ctx.ids.elm_array != 0 {
13632            self.ctx.ids.elm_array
13633        } else {
13634            crate::runtime::forms::codes::ELM_ARRAY
13635        };
13636        let stage_object = if self.ctx.ids.stage_elm_object != 0 {
13637            self.ctx.ids.stage_elm_object
13638        } else {
13639            crate::runtime::forms::codes::STAGE_ELM_OBJECT
13640        };
13641        let stage_mwnd = crate::runtime::forms::codes::STAGE_ELM_MWND;
13642        let stage_btnselitem = crate::runtime::forms::codes::STAGE_ELM_BTNSELITEM;
13643        let object_child = crate::runtime::forms::codes::elm_value::OBJECT_CHILD;
13644
13645        fn is_array_token(token: i32, elm_array: i32) -> bool {
13646            token == elm_array || token == crate::runtime::forms::codes::ELM_ARRAY
13647        }
13648
13649        let mut pos = if elm.len() >= 6
13650            && crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, elm[0]) as i32
13651                == stage_form
13652            && is_array_token(elm[1], elm_array)
13653            && elm[2] >= 0
13654            && elm[3] == stage_object
13655            && is_array_token(elm[4], elm_array)
13656            && elm[5] >= 0
13657        {
13658            6usize
13659        } else if elm.len() >= 9
13660            && crate::runtime::forms::stage::stage_storage_form_id(&self.ctx, elm[0]) as i32
13661                == stage_form
13662            && is_array_token(elm[1], elm_array)
13663            && elm[2] >= 0
13664            && is_array_token(elm[4], elm_array)
13665            && elm[5] >= 0
13666            && ((elm[3] == stage_mwnd
13667                && matches!(
13668                    elm[6],
13669                    crate::runtime::forms::codes::elm_value::MWND_OBJECT
13670                        | crate::runtime::forms::codes::elm_value::MWND_BUTTON
13671                        | crate::runtime::forms::codes::elm_value::MWND_FACE
13672                ))
13673                || (elm[3] == stage_btnselitem
13674                    && elm[6] == crate::runtime::forms::codes::ELM_BTNSELITEM_OBJECT))
13675            && is_array_token(elm[7], elm_array)
13676            && elm[8] >= 0
13677        {
13678            9usize
13679        } else {
13680            return;
13681        };
13682
13683        while pos + 2 < elm.len()
13684            && elm[pos] == object_child
13685            && is_array_token(elm[pos + 1], elm_array)
13686            && elm[pos + 2] >= 0
13687        {
13688            pos += 3;
13689        }
13690
13691        let object_ref = elm[..pos].to_vec();
13692        if self.sg_mwnd_object_trace_enabled() && Self::sg_mwnd_chain_interesting(elm) {
13693            self.sg_mwnd_object_trace_emit(format_args!(
13694                "update_context_from_dispatch elm={:?} object_ref={:?} pos={}",
13695                elm, object_ref, pos
13696            ));
13697        }
13698        self.update_compact_context_from_element(&object_ref);
13699    }
13700
13701    fn push_return_value_raw(&mut self, v: Value) {
13702        match v {
13703            Value::NamedArg { value, .. } => self.push_return_value_raw(*value),
13704            Value::Int(n) => self.push_int(n as i32),
13705            Value::Str(s) => self.push_str(s),
13706            Value::Element(elm) => {
13707                self.update_compact_context_from_element(&elm);
13708                self.push_element(elm);
13709            }
13710            Value::List(_) => {
13711                panic!("raw Value::List reached push_return_value_raw; expected runtime ref");
13712            }
13713        }
13714    }
13715
13716    // ---------------------------------------------------------------------
13717    // Arithmetic / comparisons
13718    // ---------------------------------------------------------------------
13719
13720    fn exec_operate_1(&mut self, form_code: i32, opr: u8) -> Result<()> {
13721        if form_code != self.cfg.fm_int {
13722            self.trace_unknown_form(form_code, "exec_operate_1");
13723            self.push_int(0);
13724            return Ok(());
13725        }
13726
13727        let v = self.pop_int()?;
13728        let out = match opr {
13729            OP_PLUS => v,
13730            OP_MINUS => v.wrapping_neg(),
13731            OP_TILDE => !v,
13732            _ => v,
13733        };
13734        if self.cf_condition_trace_interesting_line() {
13735            self.sg_cf_condition_trace(
13736                self.stream.get_prg_cntr(),
13737                format!(
13738                    "kind=OPERATE_1 op={} in={} out={}",
13739                    Self::cf_condition_op_name(opr),
13740                    v,
13741                    out
13742                ),
13743            );
13744        }
13745        self.push_int(out);
13746        Ok(())
13747    }
13748
13749    fn exec_operate_2(&mut self, form_l: i32, form_r: i32, opr: u8) -> Result<()> {
13750        // int/int
13751        if form_l == self.cfg.fm_int && form_r == self.cfg.fm_int {
13752            let r = self.pop_int()?;
13753            let l = self.pop_int()?;
13754            let out = self.calc_int_int(l, r, opr);
13755            if self.cf_condition_trace_interesting_line() {
13756                self.sg_cf_condition_trace(
13757                    self.stream.get_prg_cntr(),
13758                    format!(
13759                        "kind=OPERATE_2 op={} left={} right={} out={}",
13760                        Self::cf_condition_op_name(opr),
13761                        l,
13762                        r,
13763                        out
13764                    ),
13765                );
13766            }
13767            self.push_int(out);
13768            return Ok(());
13769        }
13770
13771        // str/int
13772        if form_l == self.cfg.fm_str && form_r == self.cfg.fm_int {
13773            let r = self.pop_int()?;
13774            let l = self.pop_str()?;
13775            let out = self.calc_str_int(l, r, opr);
13776            self.push_str(out);
13777            return Ok(());
13778        }
13779
13780        // str/str
13781        if form_l == self.cfg.fm_str && form_r == self.cfg.fm_str {
13782            let r = self.pop_str()?;
13783            let l = self.pop_str()?;
13784            let out = self.calc_str_str(l, r, opr);
13785            match out {
13786                Value::Int(n) => self.push_int(n as i32),
13787                Value::Str(s) => self.push_str(s),
13788                _ => {
13789                    self.push_int(0);
13790                }
13791            }
13792            return Ok(());
13793        }
13794
13795        // Unknown combo.
13796        self.trace_unknown_form(form_l, "exec_operate_2.left");
13797        self.trace_unknown_form(form_r, "exec_operate_2.right");
13798        self.push_int(0);
13799        Ok(())
13800    }
13801
13802    fn calc_int_int(&mut self, l: i32, r: i32, opr: u8) -> i32 {
13803        match opr {
13804            OP_PLUS => l.wrapping_add(r),
13805            OP_MINUS => l.wrapping_sub(r),
13806            OP_MULTIPLE => l.wrapping_mul(r),
13807            OP_DIVIDE => {
13808                if r == 0 {
13809                    0
13810                } else {
13811                    l.wrapping_div(r)
13812                }
13813            }
13814            OP_AMARI => {
13815                if r == 0 {
13816                    0
13817                } else {
13818                    l.wrapping_rem(r)
13819                }
13820            }
13821
13822            OP_EQUAL => (l == r) as i32,
13823            OP_NOT_EQUAL => (l != r) as i32,
13824            OP_GREATER => (l > r) as i32,
13825            OP_GREATER_EQUAL => (l >= r) as i32,
13826            OP_LESS => (l < r) as i32,
13827            OP_LESS_EQUAL => (l <= r) as i32,
13828
13829            OP_LOGICAL_OR => ((l != 0) || (r != 0)) as i32,
13830            OP_LOGICAL_AND => ((l != 0) && (r != 0)) as i32,
13831
13832            OP_OR => l | r,
13833            OP_AND => l & r,
13834            OP_HAT => l ^ r,
13835            OP_SL => l.wrapping_shl((r as u32) & 31),
13836            OP_SR => l.wrapping_shr((r as u32) & 31),
13837            OP_SR3 => ((l as u32).wrapping_shr((r as u32) & 31)) as i32,
13838
13839            _ => 0,
13840        }
13841    }
13842
13843    fn calc_str_int(&mut self, s: String, n: i32, opr: u8) -> String {
13844        match opr {
13845            OP_MULTIPLE => {
13846                if n <= 0 {
13847                    return String::new();
13848                }
13849                let mut out = String::new();
13850                for _ in 0..(n as usize) {
13851                    out.push_str(&s);
13852                }
13853                out
13854            }
13855            _ => s,
13856        }
13857    }
13858
13859    fn calc_str_str(&mut self, l: String, r: String, opr: u8) -> Value {
13860        match opr {
13861            OP_PLUS => Value::Str(format!("{l}{r}")),
13862            OP_EQUAL | OP_NOT_EQUAL | OP_GREATER | OP_GREATER_EQUAL | OP_LESS | OP_LESS_EQUAL => {
13863                // The original engine lowercases for comparisons.
13864                let ll = l.to_lowercase();
13865                let rr = r.to_lowercase();
13866                let cmp = ll.cmp(&rr);
13867                let b = match opr {
13868                    OP_EQUAL => cmp == std::cmp::Ordering::Equal,
13869                    OP_NOT_EQUAL => cmp != std::cmp::Ordering::Equal,
13870                    OP_GREATER => cmp == std::cmp::Ordering::Greater,
13871                    OP_GREATER_EQUAL => cmp != std::cmp::Ordering::Less,
13872                    OP_LESS => cmp == std::cmp::Ordering::Less,
13873                    OP_LESS_EQUAL => cmp != std::cmp::Ordering::Greater,
13874                    _ => false,
13875                };
13876                Value::Int(b as i64)
13877            }
13878            _ => Value::Int(0),
13879        }
13880    }
13881}
13882
13883#[cfg(test)]
13884mod user_command_resolution_tests {
13885    use super::resolve_named_user_command_number;
13886    use std::collections::HashMap;
13887
13888    #[test]
13889    fn include_command_shadows_same_named_scene_command() {
13890        let include_names = HashMap::from([(2, "Bg_Change".to_string())]);
13891        let local_names = HashMap::from([(7, "bg_change".to_string())]);
13892        assert_eq!(
13893            resolve_named_user_command_number(&include_names, &local_names, 5, "BG_CHANGE"),
13894            Some((2, true))
13895        );
13896    }
13897
13898    #[test]
13899    fn scene_command_number_is_offset_by_include_count() {
13900        let include_names = HashMap::new();
13901        let local_names = HashMap::from([(7, "local_proc".to_string())]);
13902        assert_eq!(
13903            resolve_named_user_command_number(&include_names, &local_names, 5, "LOCAL_PROC"),
13904            Some((12, false))
13905        );
13906    }
13907}
13908
13909#[cfg(test)]
13910mod global_call_stack_control_tests {
13911    use super::*;
13912    use crate::scene_stream::SceneStream;
13913    use std::path::PathBuf;
13914
13915    fn empty_scene_chunk() -> Vec<u8> {
13916        const HEADER_WORDS: usize = 33;
13917        const HEADER_SIZE: i32 = (HEADER_WORDS * 4) as i32;
13918        let mut words = [0i32; HEADER_WORDS];
13919        for idx in [
13920            0usize, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31,
13921        ] {
13922            words[idx] = HEADER_SIZE;
13923        }
13924        let mut out = Vec::with_capacity(HEADER_SIZE as usize);
13925        for word in words {
13926            out.extend_from_slice(&word.to_le_bytes());
13927        }
13928        out
13929    }
13930
13931    fn vm_with_depth(depth: usize) -> SceneVm<'static> {
13932        let chunk = Box::leak(empty_scene_chunk().into_boxed_slice());
13933        let stream = SceneStream::new(chunk).expect("empty scene stream");
13934        let ctx = CommandContext::new(PathBuf::from("."));
13935        let mut vm = SceneVm::new(stream, ctx);
13936        while vm.call_stack.len() < depth {
13937            let frame = vm.take_call_frame(vm.cfg.fm_void, false, false, 0, None);
13938            vm.call_stack.push(frame);
13939        }
13940        vm
13941    }
13942
13943    #[test]
13944    fn global_call_stack_controls_match_original_shrink_only_semantics() {
13945        let mut vm = vm_with_depth(4);
13946
13947        assert!(
13948            vm.exec_builtin_global_control(
13949                constants::elm_value::GLOBAL_GET_CALL_STACK_CNT,
13950                vm.cfg.fm_int,
13951                &[],
13952            )
13953            .unwrap()
13954        );
13955        assert_eq!(vm.ctx.stack.pop().and_then(|v| v.as_i64()), Some(4));
13956
13957        assert!(
13958            vm.exec_builtin_global_control(
13959                constants::elm_value::GLOBAL_DEL_CALL_STACK,
13960                vm.cfg.fm_void,
13961                &[Value::Int(2)],
13962            )
13963            .unwrap()
13964        );
13965        assert_eq!(vm.call_stack.len(), 2);
13966
13967        // SET_CALL_STACK_CNT cannot grow the original C_elm_call_list count.
13968        assert!(
13969            vm.exec_builtin_global_control(
13970                constants::elm_value::GLOBAL_SET_CALL_STACK_CNT,
13971                vm.cfg.fm_void,
13972                &[Value::Int(4)],
13973            )
13974            .unwrap()
13975        );
13976        assert_eq!(vm.call_stack.len(), 2);
13977
13978        assert!(
13979            vm.exec_builtin_global_control(
13980                constants::elm_value::GLOBAL_SET_CALL_STACK_CNT,
13981                vm.cfg.fm_void,
13982                &[Value::Int(1)],
13983            )
13984            .unwrap()
13985        );
13986        assert_eq!(vm.call_stack.len(), 1);
13987
13988        // DEL below the mandatory base frame and INIT both stay at one.
13989        assert!(
13990            vm.exec_builtin_global_control(
13991                constants::elm_value::GLOBAL_DEL_CALL_STACK,
13992                vm.cfg.fm_void,
13993                &[Value::Int(99)],
13994            )
13995            .unwrap()
13996        );
13997        assert_eq!(vm.call_stack.len(), 1);
13998        assert!(
13999            vm.exec_builtin_global_control(
14000                constants::elm_value::GLOBAL_INIT_CALL_STACK,
14001                vm.cfg.fm_void,
14002                &[],
14003            )
14004            .unwrap()
14005        );
14006        assert_eq!(vm.call_stack.len(), 1);
14007    }
14008}
14009
14010#[cfg(test)]
14011mod call_property_reference_tests {
14012    use super::*;
14013    use crate::runtime::forms::codes::{ELM_ARRAY, ELM_GLOBAL_D, FM_INTLISTREF, FM_INTREF};
14014    use crate::scene_stream::SceneStream;
14015    use std::path::PathBuf;
14016
14017    fn empty_scene_chunk() -> Vec<u8> {
14018        const HEADER_WORDS: usize = 33;
14019        const HEADER_SIZE: i32 = (HEADER_WORDS * 4) as i32;
14020        let mut words = [0i32; HEADER_WORDS];
14021        for idx in [
14022            0usize, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31,
14023        ] {
14024            words[idx] = HEADER_SIZE;
14025        }
14026        let mut out = Vec::with_capacity(HEADER_SIZE as usize);
14027        for word in words {
14028            out.extend_from_slice(&word.to_le_bytes());
14029        }
14030        out
14031    }
14032
14033    #[test]
14034    fn marker_only_call_reference_does_not_create_an_indexless_array_chain() {
14035        let chunk = empty_scene_chunk();
14036        let stream = SceneStream::new(&chunk).expect("empty scene stream");
14037        let ctx = CommandContext::new(PathBuf::from("."));
14038        let mut vm = SceneVm::new(stream, ctx);
14039
14040        let target = vec![ELM_GLOBAL_D];
14041        let call_prop_id = 0;
14042        let call_prop_element =
14043            constants::elm::create(constants::elm::OWNER_CALL_PROP, 0, call_prop_id);
14044        vm.call_stack
14045            .last_mut()
14046            .expect("base call frame")
14047            .user_props
14048            .push(CallProp {
14049                scn_no: 0,
14050                prop_id: call_prop_id,
14051                form: FM_INTLISTREF,
14052                decl_size: 0,
14053                element: target.clone(),
14054                value: CallPropValue::Element(target.clone()),
14055            });
14056
14057        // The marker-only suffix must dereference to GLOBAL.D itself.  Before
14058        // this regression fix compose_call_prop_tail() produced [D, ELM_ARRAY],
14059        // which the INTLIST dispatcher correctly rejected because no index
14060        // follows the array marker.
14061        vm.exec_property(vec![call_prop_element, ELM_ARRAY])
14062            .expect("marker-only reference property step");
14063        assert_eq!(vm.pop_element().expect("referenced element"), target);
14064    }
14065
14066    #[test]
14067    fn indexed_call_reference_still_composes_the_array_index() {
14068        let chunk = empty_scene_chunk();
14069        let stream = SceneStream::new(&chunk).expect("empty scene stream");
14070        let ctx = CommandContext::new(PathBuf::from("."));
14071        let vm = SceneVm::new(stream, ctx);
14072
14073        let target = vec![ELM_GLOBAL_D];
14074        let prop = CallProp {
14075            scn_no: 0,
14076            prop_id: 0,
14077            form: FM_INTLISTREF,
14078            decl_size: 0,
14079            element: target.clone(),
14080            value: CallPropValue::Element(target),
14081        };
14082
14083        assert_eq!(vm.compose_call_prop_tail(&prop, &[ELM_ARRAY]), None);
14084        assert_eq!(
14085            vm.compose_call_prop_tail(&prop, &[ELM_ARRAY, 7]),
14086            Some(vec![ELM_GLOBAL_D, ELM_ARRAY, 7])
14087        );
14088    }
14089
14090    #[test]
14091    fn marker_only_call_reference_command_does_not_dispatch_indexless_intlist() {
14092        let chunk = empty_scene_chunk();
14093        let stream = SceneStream::new(&chunk).expect("empty scene stream");
14094        let ctx = CommandContext::new(PathBuf::from("."));
14095        let mut vm = SceneVm::new(stream, ctx);
14096
14097        let target = vec![ELM_GLOBAL_D];
14098        let call_prop_id = 0;
14099        vm.call_stack
14100            .last_mut()
14101            .expect("base call frame")
14102            .user_props
14103            .push(CallProp {
14104                scn_no: 0,
14105                prop_id: call_prop_id,
14106                form: FM_INTLISTREF,
14107                decl_size: 0,
14108                element: target.clone(),
14109                value: CallPropValue::Element(target.clone()),
14110            });
14111
14112        vm.exec_call_prop_command(0, 0, &[ELM_ARRAY], 0, vm.cfg.fm_void, &[])
14113            .expect("marker-only reference command step");
14114
14115        assert_eq!(vm.pop_element().expect("referenced element"), target);
14116    }
14117
14118    #[test]
14119    fn direct_call_reference_command_uses_reference_dispatch() {
14120        let chunk = empty_scene_chunk();
14121        let stream = SceneStream::new(&chunk).expect("empty scene stream");
14122        let ctx = CommandContext::new(PathBuf::from("."));
14123        let mut vm = SceneVm::new(stream, ctx);
14124
14125        let target = vec![ELM_GLOBAL_D];
14126        let call_prop_id = 0;
14127        let call_prop_element =
14128            constants::elm::create(constants::elm::OWNER_CALL_PROP, 0, call_prop_id);
14129        vm.call_stack
14130            .last_mut()
14131            .expect("base call frame")
14132            .user_props
14133            .push(CallProp {
14134                scn_no: 0,
14135                prop_id: call_prop_id,
14136                form: FM_INTLISTREF,
14137                decl_size: 0,
14138                element: target.clone(),
14139                value: CallPropValue::Element(target.clone()),
14140            });
14141
14142        vm.exec_command(
14143            vec![call_prop_element, ELM_ARRAY],
14144            0,
14145            vm.cfg.fm_void,
14146            &mut Vec::new(),
14147        )
14148        .expect("direct marker-only CALL_PROP command");
14149
14150        assert_eq!(vm.pop_element().expect("referenced element"), target);
14151    }
14152
14153    #[test]
14154    fn scalar_call_reference_is_resolved_in_two_property_steps() {
14155        let chunk = empty_scene_chunk();
14156        let stream = SceneStream::new(&chunk).expect("empty scene stream");
14157        let ctx = CommandContext::new(PathBuf::from("."));
14158        let mut vm = SceneVm::new(stream, ctx);
14159
14160        let user_prop_id = 7u16;
14161        let target = vec![constants::elm::create(
14162            constants::elm::OWNER_USER_PROP,
14163            0,
14164            user_prop_id as i32,
14165        )];
14166        let mut target_cell = UserPropCell::new(vm.cfg.fm_int, target.clone());
14167        target_cell.int_value = 42;
14168        vm.user_props.insert(user_prop_id, target_cell);
14169
14170        let call_prop_id = 0;
14171        let call_prop_element = vec![constants::elm::create(
14172            constants::elm::OWNER_CALL_PROP,
14173            0,
14174            call_prop_id,
14175        )];
14176        vm.call_stack
14177            .last_mut()
14178            .expect("base call frame")
14179            .user_props
14180            .push(CallProp {
14181                scn_no: 0,
14182                prop_id: call_prop_id,
14183                form: FM_INTREF,
14184                decl_size: 0,
14185                element: target.clone(),
14186                value: CallPropValue::Element(target.clone()),
14187            });
14188
14189        // The compiler emits CD_PROPERTY once inside bs_elm_list() to replace
14190        // CALL_PROP with its referenced element, then emits another CD_PROPERTY
14191        // when the expression needs the scalar value.
14192        vm.exec_property(call_prop_element)
14193            .expect("first reference property step");
14194        let resolved_target = vm.pop_element().expect("referenced element");
14195        assert_eq!(resolved_target, target);
14196
14197        vm.exec_property(resolved_target)
14198            .expect("second scalar property step");
14199        assert_eq!(vm.pop_int().expect("referenced int value"), 42);
14200    }
14201}
14202
14203#[cfg(test)]
14204mod command_dispatch_tests {
14205    use super::*;
14206    use crate::runtime::forms::codes::{
14207        ELM_ARRAY, ELM_INTLIST_BIT16, ELM_INTLIST_GET_SIZE, ELM_INTLIST_RESIZE,
14208        ELM_STRLIST_GET_SIZE, ELM_STRLIST_RESIZE,
14209    };
14210    use crate::scene_stream::SceneStream;
14211    use std::path::PathBuf;
14212
14213    fn empty_scene_chunk() -> Vec<u8> {
14214        const HEADER_WORDS: usize = 33;
14215        const HEADER_SIZE: i32 = (HEADER_WORDS * 4) as i32;
14216        let mut words = [0i32; HEADER_WORDS];
14217        for idx in [
14218            0usize, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31,
14219        ] {
14220            words[idx] = HEADER_SIZE;
14221        }
14222        let mut out = Vec::with_capacity(HEADER_SIZE as usize);
14223        for word in words {
14224            out.extend_from_slice(&word.to_le_bytes());
14225        }
14226        out
14227    }
14228
14229    fn test_vm() -> SceneVm<'static> {
14230        let chunk = Box::leak(empty_scene_chunk().into_boxed_slice());
14231        let stream = SceneStream::new(chunk).expect("empty scene stream");
14232        SceneVm::new(stream, CommandContext::new(PathBuf::from(".")))
14233    }
14234
14235    #[test]
14236    fn legacy_global_world_slot_routes_camera_state_to_back_and_front() {
14237        let mut vm = test_vm();
14238        let mut args = vec![Value::Int(2304), Value::Int(80), Value::Int(3840)];
14239        let fm_void = vm.cfg.fm_void;
14240        vm.exec_command(
14241            vec![
14242                LEGACY_GLOBAL_WORLD_ELEMENT,
14243                ELM_ARRAY,
14244                0,
14245                crate::runtime::forms::codes::elm_value::WORLD_SET_CAMERA_EYE,
14246            ],
14247            0,
14248            fm_void,
14249            &mut args,
14250        )
14251        .expect("legacy GLOBAL.WORLD SET_CAMERA_EYE");
14252        vm.exec_assign(
14253            vec![
14254                LEGACY_GLOBAL_WORLD_ELEMENT,
14255                ELM_ARRAY,
14256                0,
14257                crate::runtime::forms::codes::elm_value::WORLD_CAMERA_VIEW_ANGLE,
14258            ],
14259            1,
14260            Value::Int(700),
14261        )
14262        .expect("legacy GLOBAL.WORLD CAMERA_VIEW_ANGLE");
14263
14264        let stage_form = if vm.ctx.ids.form_global_stage != 0 {
14265            vm.ctx.ids.form_global_stage
14266        } else {
14267            crate::runtime::forms::codes::FORM_GLOBAL_STAGE
14268        };
14269        let stage = &vm.ctx.globals.stage_forms[&stage_form];
14270        for stage_idx in [0i64, 1] {
14271            let world = &stage.world_lists[&stage_idx][0];
14272            assert_eq!(world.camera_eye_x.get_value(), 2304);
14273            assert_eq!(world.camera_eye_y.get_value(), 80);
14274            assert_eq!(world.camera_eye_z.get_value(), 3840);
14275            assert_eq!(world.camera_view_angle, 700);
14276        }
14277
14278        vm.exec_property(vec![
14279            LEGACY_GLOBAL_WORLD_ELEMENT,
14280            ELM_ARRAY,
14281            0,
14282            crate::runtime::forms::codes::elm_value::WORLD_CAMERA_VIEW_ANGLE,
14283        ])
14284        .expect("legacy GLOBAL.WORLD property read");
14285        assert_eq!(vm.pop_int().expect("camera view angle"), 700);
14286    }
14287
14288    #[test]
14289    fn current_global_msgbtn_is_not_reinterpreted_as_legacy_world() {
14290        let vm = test_vm();
14291        assert!(
14292            vm.legacy_global_world_stage_chain(&[LEGACY_GLOBAL_WORLD_ELEMENT], 1)
14293                .is_none()
14294        );
14295    }
14296
14297    #[test]
14298    fn save_summary_survives_deferred_message_savepoint_and_page_clear() {
14299        use crate::runtime::forms::{stage, syscom};
14300        let mut vm = test_vm();
14301        vm.ctx.globals.script.async_msg_mode = true;
14302        let project =
14303            std::env::temp_dir().join(format!("siglus-save-summary-{}", std::process::id()));
14304        vm.ctx.project_dir = project.clone();
14305        for (index, text) in ["最初のページ", "次のページ"].iter().enumerate() {
14306            assert!(stage::cd_text_current_mwnd(&mut vm.ctx, text, index as i64));
14307            vm.drain_runtime_save_load_requests().unwrap();
14308            assert_eq!(vm.ctx.local_save_snapshot.as_ref().unwrap().save_msg, *text);
14309            assert_eq!(
14310                vm.ctx.local_save_snapshot.as_ref().unwrap().save_full_msg,
14311                *text
14312            );
14313            syscom::append_current_save_message(&mut vm.ctx, " 続き");
14314            vm.perform_runtime_save_request(RuntimeSaveRequest {
14315                kind: RuntimeSaveKind::Normal,
14316                index,
14317            })
14318            .unwrap();
14319            let path = vm
14320                .runtime_save_file_path(RuntimeSaveKind::Normal, index)
14321                .unwrap();
14322            let header = crate::original_save::read_header_from_path(&path).unwrap();
14323            assert_eq!(header.message, format!("{text} 続き"));
14324            assert_eq!(header.full_message, header.message);
14325            // Exercise the same header lookup used by the game's save menu.
14326            vm.exec_command(
14327                vec![
14328                    vm.ctx.ids.form_global_syscom as i32,
14329                    crate::runtime::forms::codes::elm_value::SYSCOM_GET_SAVE_MESSAGE,
14330                ],
14331                0,
14332                vm.cfg.fm_str,
14333                &mut vec![Value::Int(index as i64)],
14334            )
14335            .unwrap();
14336            assert_eq!(vm.pop_str().unwrap(), header.message);
14337            // A deferred CLEAR must reset both the short summary and full page.
14338            for form in vm.ctx.globals.stage_forms.values_mut() {
14339                for list in form.mwnd_lists.values_mut() {
14340                    for mwnd in list {
14341                        mwnd.clear_ready = true;
14342                        mwnd.msg_block_started = false;
14343                    }
14344                }
14345            }
14346        }
14347        std::fs::remove_dir_all(project).unwrap();
14348    }
14349
14350    #[test]
14351    fn save_summary_keeps_text_appended_before_deferred_snapshot() {
14352        use crate::runtime::forms::syscom::append_current_save_message;
14353        let mut vm = test_vm();
14354        vm.ctx.globals.syscom.current_save_message = "previous block".into();
14355        vm.ctx.request_auto_savepoint();
14356        append_current_save_message(&mut vm.ctx, "new block");
14357        vm.drain_runtime_save_load_requests().unwrap();
14358        assert_eq!(
14359            vm.ctx.local_save_snapshot.as_ref().unwrap().save_msg,
14360            "new block"
14361        );
14362        // Explicit SAVEPOINT still starts an empty summary, as in C++ save_local.
14363        vm.build_local_save_snapshot();
14364        assert!(
14365            vm.ctx
14366                .local_save_snapshot
14367                .as_ref()
14368                .unwrap()
14369                .save_msg
14370                .is_empty()
14371        );
14372        append_current_save_message(&mut vm.ctx, "after explicit savepoint");
14373        assert_eq!(
14374            vm.ctx.local_save_snapshot.as_ref().unwrap().save_msg,
14375            "after explicit savepoint"
14376        );
14377    }
14378
14379    #[test]
14380    fn mouse_get_pos_writes_script_variable_references() {
14381        let mut vm = test_vm();
14382        let x = vec![constants::elm::create(
14383            constants::elm::OWNER_USER_PROP,
14384            0,
14385            7,
14386        )];
14387        let y = vec![constants::elm::create(
14388            constants::elm::OWNER_USER_PROP,
14389            0,
14390            8,
14391        )];
14392        for (id, target) in [(7, &x), (8, &y)] {
14393            vm.user_props
14394                .insert(id, UserPropCell::new(vm.cfg.fm_int, target.clone()));
14395        }
14396        // The title menu tests these variables against the Start button bounds.
14397        // Use the script snapshot, which can differ from the live input state.
14398        vm.ctx.script_input.on_mouse_move(100, 380);
14399        vm.ctx.input.on_mouse_move(900, 700);
14400        vm.exec_command(
14401            vec![
14402                vm.ctx.ids.form_global_mouse as i32,
14403                vm.ctx.ids.mouse_op_get_pos,
14404            ],
14405            0,
14406            vm.cfg.fm_void,
14407            &mut vec![Value::Element(x.clone()), Value::Element(y.clone())],
14408        )
14409        .unwrap();
14410        vm.exec_property(x).unwrap();
14411        assert_eq!(vm.pop_int().unwrap(), 100);
14412        vm.exec_property(y).unwrap();
14413        assert_eq!(vm.pop_int().unwrap(), 380);
14414    }
14415
14416    #[test]
14417    fn indexed_local_settings_keep_native_field_order_and_defaults() {
14418        let mut vm = test_vm();
14419        vm.ctx.tables.gameexe = Some(crate::formats::gameexe::GameexeConfig::from_text(
14420            "#WAKU.BTN.CNT=19",
14421        ));
14422        // Native byte offsets, independent of the current POD writer. Title is
14423        // empty here, so cursor/settings start at byte 20.
14424        let mut bytes = vec![0; 342];
14425        for (offset, value) in [
14426            (0, 101i32),
14427            (4, 7),
14428            (8, 48),
14429            (12, 3),
14430            (20, 2),
14431            (56, 25),
14432            (60, 300),
14433            (64, 15),
14434        ] {
14435            bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
14436        }
14437        for offset in [24, 45, 46, 55, 73, 330, 341] {
14438            bytes[offset] = 1;
14439        }
14440        vm.ctx.globals.script.font_name = "previous font".into();
14441        vm.ctx.globals.script.time_stop_flag = true;
14442        let mut rd = crate::original_save::OriginalStreamReader::new(&bytes);
14443        vm.read_indexed_local_settings(&mut rd).unwrap();
14444        assert!(rd.remaining().is_empty());
14445        let script = &vm.ctx.globals.script;
14446        assert_eq!((script.cur_koe_no, script.cur_chr_no), (101, 7));
14447        assert_eq!(
14448            (script.cur_read_flag_scn_no, script.cur_read_flag_flag_no),
14449            (48, 3)
14450        );
14451        assert_eq!(script.cursor_no, 2);
14452        assert_eq!(
14453            (
14454                script.auto_mode_moji_wait,
14455                script.auto_mode_min_wait,
14456                script.msg_speed
14457            ),
14458            (25, 300, 15)
14459        );
14460        assert!(
14461            script.auto_mode_flag && script.cursor_disp_off && script.wait_display_vsync_off_flag
14462        );
14463        assert!(!script.cursor_runtime_visible);
14464        assert!(script.key_disable.contains(&255));
14465        assert!(script.font_name.is_empty() && !script.time_stop_flag);
14466        assert_eq!(vm.ctx.globals.syscom.mwnd_btn_disable.get(&18), Some(&true));
14467        assert!(vm.ctx.globals.syscom.mwnd_btn_touch_disable);
14468        assert_eq!(vm.ctx.globals.syscom.replay_koe, Some((101, 7)));
14469    }
14470
14471    #[test]
14472    fn indexed_local_probe_checks_absolute_boundaries_and_rejects_newer_streams() {
14473        use crate::original_save::OriginalStreamWriter;
14474        let mut vm = test_vm();
14475        vm.ctx.tables.gameexe = Some(crate::formats::gameexe::GameexeConfig::from_text(
14476            "#WAKU.BTN.CNT=19",
14477        ));
14478        let mut w = OriginalStreamWriter::new();
14479        for value in [48, 60, 1885] {
14480            w.push_i32(value);
14481        }
14482        SceneVm::write_cpp_proc_record(&mut w, 1, &[], 0);
14483        w.push_i32(0);
14484        for _ in 0..3 {
14485            w.push_empty_element();
14486        }
14487        w.push_padding(16);
14488        w.push_str("native title");
14489        w.push_padding(322);
14490        for _ in 0..3 {
14491            w.push_i32(0);
14492        }
14493        w.push_padding(12 + 76);
14494        w.push_str("");
14495        w.push_padding(44 + 8);
14496        for _ in 0..7 {
14497            w.push_fixed_i32_list(&[1, 2, 3], 3);
14498        }
14499        let mut bytes = w.into_inner();
14500        assert!(vm.detect_indexed_local_stream(&bytes));
14501        bytes.pop();
14502        assert!(!vm.detect_indexed_local_stream(&bytes));
14503        for name in ["", "a", "init", "_01load", "long_scene_name"] {
14504            vm.current_scene_name = Some(name.into());
14505            assert!(
14506                !vm.detect_indexed_local_stream(&vm.build_original_local_stream()),
14507                "{name}"
14508            );
14509        }
14510        // A mismatched layout may expose a huge argument count. Probe without
14511        // allocating the alleged argument vector.
14512        bytes[148..152].copy_from_slice(&i32::MAX.to_le_bytes());
14513        assert!(!vm.detect_indexed_local_stream(&bytes));
14514    }
14515
14516    #[test]
14517    fn short_native_records_preserve_boundaries_and_values() {
14518        use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
14519        let mut w = OriginalStreamWriter::new();
14520        for value in [7, 3, -1, 201] {
14521            w.push_i32(value);
14522        }
14523        w.push_raw(&[1, 0, 1, 0]);
14524        // The native object's fixed part is 1,676 bytes. Empty lists, four
14525        // strings, frame action, 12-byte GAN work, and children add 92 bytes.
14526        let mut object = vec![0u8; 1768];
14527        let put = |bytes: &mut [u8], offset, value: i32| {
14528            bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
14529        };
14530        put(&mut object, 0, 1); // Rectangle.
14531        put(&mut object, 12, 10);
14532        put(&mut object, 20, 300);
14533        put(&mut object, 176, 15); // Button action.
14534        put(&mut object, 184, 201);
14535        put(&mut object, 188, 3); // Group number, not a serialized element.
14536        put(&mut object, 1124, 12); // Scalar source clip left.
14537        w.push_raw(&object);
14538        w.push_i32(12345);
14539        let bytes = w.into_inner();
14540        let mut rd = OriginalStreamReader::new(&bytes);
14541        rd.layout = NativeLocalLayout::ShortElements;
14542        let group = SceneVm::read_short_group(&mut rd).unwrap();
14543        assert_eq!(
14544            (group.order, group.layer, group.decided_button_no),
14545            (7, 3, 201)
14546        );
14547        assert!(group.started && group.cancel_flag);
14548        let object = SceneVm::read_short_object(&mut rd).unwrap();
14549        assert_eq!((object.rect_param.left, object.rect_param.right), (10, 300));
14550        assert_eq!((object.button.button_no, object.button.group_no), (201, 3));
14551        assert!(object.button.enabled);
14552        assert_eq!(object.runtime.prop_events.src_clip_left.value, 12);
14553        assert_eq!(rd.i32().unwrap(), 12345);
14554        assert!(rd.remaining().is_empty());
14555    }
14556
14557    #[test]
14558    fn short_native_nested_returns_keep_the_saved_result_types() {
14559        let mut vm = test_vm();
14560        let mut root = vm.scene_base_call();
14561        root.return_scene_name = Some("caller".into());
14562        root.return_line_no = 51;
14563        let mut gosub = vm.scene_base_call();
14564        gosub.call_type = 1;
14565        gosub.ret_form = vm.cfg.fm_int;
14566        gosub.return_scene_name = Some("caller".into());
14567        gosub.return_line_no = 237;
14568        let mut farcall = vm.scene_base_call();
14569        farcall.call_type = 2;
14570        farcall.ret_form = vm.cfg.fm_str;
14571        let mut frames = vec![root, gosub, farcall];
14572        vm.normalize_short_call_returns(&mut frames);
14573        assert_eq!(frames[0].return_line_no, 51);
14574        assert_eq!(frames[1].return_line_no, 237);
14575        vm.call_stack = frames;
14576        vm.scene_stack.push(SceneExecFrame {
14577            stream: vm.stream.clone(),
14578            user_cmd_names: vm.user_cmd_names.clone(),
14579            call_cmd_names: vm.call_cmd_names.clone(),
14580            current_scene_no: None,
14581            current_scene_name: Some("caller".into()),
14582            current_line_no: 237,
14583            call_depth: 3,
14584        });
14585        assert!(
14586            vm.return_from_scene(vec![Value::Str("result".into())])
14587                .unwrap()
14588        );
14589        assert_eq!(vm.pop_str().unwrap(), "result");
14590        vm.exec_return(Vec::new()).unwrap();
14591        // The next POP after the outer GOSUB needs its default integer result.
14592        assert_eq!(vm.pop_int().unwrap(), 0);
14593        assert_eq!(vm.call_stack.len(), 1);
14594        assert!(vm.int_stack.is_empty() && vm.str_stack.is_empty());
14595    }
14596
14597    #[test]
14598    fn short_native_backlog_has_one_speaker_name() {
14599        use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
14600        let mut w = OriginalStreamWriter::new();
14601        w.push_i32(1);
14602        w.push_bool(false);
14603        w.push_str("saved dialogue");
14604        w.push_str("speaker");
14605        w.push_i32(12);
14606        w.push_i32(34);
14607        w.push_extend_i32_list(&[123]);
14608        w.push_extend_i32_list(&[4]);
14609        w.push_i32(0);
14610        w.push_str("debug dialogue");
14611        w.push_i32(30);
14612        w.push_i32(3301);
14613        for value in [0, 0, 1] {
14614            w.push_i32(value);
14615        }
14616        w.push_bool(true);
14617        let bytes = w.into_inner();
14618        let mut rd = OriginalStreamReader::new(&bytes);
14619        rd.layout = NativeLocalLayout::ShortElements;
14620        let backlog = SceneVm::read_cpp_msg_back(&mut rd).unwrap();
14621        let entry = &backlog.history[0];
14622        assert_eq!(entry.msg_str, "saved dialogue");
14623        assert_eq!(entry.original_name, "speaker");
14624        assert_eq!(entry.disp_name, "speaker");
14625        assert_eq!((entry.pct_pos_x, entry.pct_pos_y), (12, 34));
14626        assert_eq!(entry.koe_no_list, vec![123]);
14627        assert_eq!(entry.debug_msg, "debug dialogue");
14628        assert_eq!((entry.scn_no, entry.line_no), (30, 3301));
14629        assert!(backlog.new_msg_flag);
14630        assert_eq!(backlog.history_insert_pos, 1);
14631        assert!(rd.remaining().is_empty());
14632        let mut truncated = OriginalStreamReader::new(&bytes[..bytes.len() - 1]);
14633        truncated.layout = NativeLocalLayout::ShortElements;
14634        assert!(SceneVm::read_cpp_msg_back(&mut truncated).is_err());
14635    }
14636
14637    #[test]
14638    fn short_native_probe_and_settings_have_their_own_layout() {
14639        use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
14640        let mut vm = test_vm();
14641        let buttons = vm.mwnd_waku_btn_count();
14642        let mut w = OriginalStreamWriter::new();
14643        for value in [0, 20, 30] {
14644            w.push_i32(value);
14645        }
14646        w.push_padding(82); // Empty proc: 64-byte element and two bools.
14647        w.push_i32(0); // Proc stack.
14648        w.push_padding(3 * 64);
14649        let settings_offset = w.position();
14650        for value in [123, 4, 0, 77] {
14651            w.push_i32(value);
14652        }
14653        w.push_str("test title");
14654        w.push_padding(283 + buttons);
14655        let settings_end = w.position();
14656        w.push_padding(12 + 12 + 76); // Stacks, clocks, and syscom.
14657        for _ in 0..6 {
14658            w.push_fixed_i32_list(&[1, 2], 2);
14659        }
14660        let mut bytes = w.into_inner();
14661        assert_eq!(
14662            vm.detect_indexed_local_layout(&bytes),
14663            Some(NativeLocalLayout::ShortElements)
14664        );
14665        let mut rd = OriginalStreamReader::new(&bytes);
14666        rd.layout = NativeLocalLayout::ShortElements;
14667        rd.skip(settings_offset).unwrap();
14668        vm.read_short_local_settings(&mut rd).unwrap();
14669        assert_eq!(rd.remaining().len(), bytes.len() - settings_end);
14670        assert_eq!(vm.ctx.globals.script.cur_koe_no, 123);
14671        assert_eq!(vm.ctx.globals.script.cur_chr_no, 4);
14672        assert_eq!(vm.ctx.globals.syscom.current_save_scene_title, "test title");
14673        assert_eq!(vm.ctx.globals.script.auto_mode_min_wait, -1);
14674        bytes.pop();
14675        assert_eq!(vm.detect_indexed_local_layout(&bytes), None);
14676    }
14677
14678    #[test]
14679    fn short_native_object_save_flags_follow_ini_order_and_ranges() {
14680        let mut vm = test_vm();
14681        vm.ctx.tables.gameexe = Some(siglus_assets::gameexe::GameexeConfig::from_text(
14682            "#OBJECT.000-255.SAVE = 1\n#OBJECT.250-255.SAVE = 0\n#OBJECT.252.SAVE = 1\n#OBJECT.252.USE = 0\n#OBJECT.254.SAVE = 1\n",
14683        ));
14684        assert!(vm.short_object_slot_saved(249));
14685        assert!(!vm.short_object_slot_saved(250));
14686        assert!(!vm.short_object_slot_saved(252));
14687        assert!(vm.short_object_slot_saved(254));
14688        assert!(!vm.short_object_slot_saved(255));
14689    }
14690
14691    #[test]
14692    fn indexed_local_groups_and_objects_keep_record_boundaries() {
14693        let mut bytes = vec![0; 152 + 2108];
14694        bytes[0..4].copy_from_slice(&7i32.to_le_bytes());
14695        bytes[20] = 1; // Group started flag follows five integers.
14696        bytes.extend_from_slice(&12345i32.to_le_bytes());
14697        let mut rd = crate::original_save::OriginalStreamReader::new(&bytes);
14698        rd.layout = NativeLocalLayout::Indexed;
14699        let group = SceneVm::read_cpp_group(&mut rd).unwrap();
14700        assert_eq!(group.order, 7);
14701        assert!(group.started);
14702        assert_eq!(rd.remaining().len(), 2108 + 4);
14703        let object = SceneVm::read_early_object(&mut rd).unwrap();
14704        assert!(!object.used);
14705        assert_eq!(rd.i32().unwrap(), 12345);
14706        assert!(rd.remaining().is_empty());
14707    }
14708
14709    #[test]
14710    fn create_emote_dispatches_with_position_overload_and_reinitializes_on_empty_file() {
14711        use crate::runtime::forms::codes;
14712        let mut vm = test_vm();
14713        let object = vec![
14714            codes::ELM_GLOBAL_FRONT,
14715            codes::ELM_STAGE_OBJECT,
14716            ELM_ARRAY,
14717            0,
14718            codes::ELM_OBJECT_CREATE_EMOTE,
14719        ];
14720        let mut args = vec![
14721            Value::Int(640),
14722            Value::Int(720),
14723            Value::Str("missing_emote_fixture".into()),
14724            Value::Int(1),
14725            Value::Int(320),
14726            Value::Int(80),
14727            Value::NamedArg {
14728                id: 0,
14729                value: Box::new(Value::Int(12)),
14730            },
14731            Value::NamedArg {
14732                id: 1,
14733                value: Box::new(Value::Int(-8)),
14734            },
14735        ];
14736        // Exercise the real VM route with the default command table. A missing
14737        // PSB may fail to create a player, but the void command must not panic.
14738        vm.exec_command(object.clone(), 2, vm.cfg.fm_void, &mut args)
14739            .unwrap();
14740        let stage = &vm.ctx.globals.stage_forms[&vm.ctx.ids.form_global_stage];
14741        let obj = &stage.object_lists[&1][0];
14742        assert_eq!(obj.object_type, 12);
14743        assert_eq!(obj.file_name.as_deref(), Some("missing_emote_fixture"));
14744        assert_eq!((obj.emote.width, obj.emote.height), (640, 720));
14745        assert_eq!((obj.emote.rep_x, obj.emote.rep_y), (12, -8));
14746        assert_eq!(obj.get_int_prop(&vm.ctx.ids, vm.ctx.ids.obj_disp), 1);
14747        assert_eq!(obj.get_int_prop(&vm.ctx.ids, vm.ctx.ids.obj_x), 320);
14748        assert_eq!(obj.get_int_prop(&vm.ctx.ids, vm.ctx.ids.obj_y), 80);
14749        assert!(vm.ctx.stack.is_empty() && vm.int_stack.is_empty());
14750
14751        vm.exec_command(
14752            object,
14753            0,
14754            vm.cfg.fm_void,
14755            &mut vec![Value::Int(640), Value::Int(720), Value::Str(String::new())],
14756        )
14757        .unwrap();
14758        let stage = &vm.ctx.globals.stage_forms[&vm.ctx.ids.form_global_stage];
14759        assert_eq!(stage.object_lists[&1][0].object_type, 0);
14760    }
14761
14762    #[test]
14763    fn current_local_save_layout_still_restores_font_and_stacks() {
14764        let mut source = test_vm();
14765        source.current_scene_name = Some("saved_scene".to_owned());
14766        source.current_line_no = 62;
14767        source.ctx.globals.script.font_name = "Test Font".to_owned();
14768        source.ctx.globals.script.font_bold = 1;
14769        source.ctx.globals.script.font_shadow = 2;
14770        source.int_stack = vec![-1, 123];
14771        source.str_stack = vec!["saved string".to_owned()];
14772        let bytes = source.build_original_local_stream();
14773        let mut restored = test_vm();
14774        let snapshot = restored.parse_original_local_stream(&bytes).unwrap();
14775        assert_eq!(snapshot.scene_name, "saved_scene");
14776        assert_eq!(snapshot.line_no, 62);
14777        assert_eq!(snapshot.int_stack, source.int_stack);
14778        assert_eq!(snapshot.str_stack, source.str_stack);
14779        assert_eq!(restored.ctx.globals.script.font_name, "Test Font");
14780        assert_eq!(restored.ctx.globals.script.font_bold, 1);
14781        assert_eq!(restored.ctx.globals.script.font_shadow, 2);
14782    }
14783
14784    #[test]
14785    fn early_object_record_preserves_animation_and_next_record_boundary() {
14786        use crate::original_save::OriginalStreamReader;
14787        // Native fixed object POD, empty variable lists/strings, full GAN work,
14788        // and no children. This layout predates compact SAVE_EVENT records.
14789        let mut bytes = vec![0u8; 2115];
14790        for (offset, value) in [
14791            (0, 1i32),
14792            (364, 1),
14793            (372, 7), // Image, visible, pattern value.
14794            (412, 50),
14795            (416, 2),
14796            (420, -1), // Draw order, layer, world.
14797            (432, 120),
14798            (472, 200),
14799            (476, 240), // X/Y event values.
14800            (480, 30),
14801            (484, 60),
14802            (492, 100),
14803            (496, 170),
14804            (500, 240),
14805        ] {
14806            bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
14807        }
14808        bytes.extend_from_slice(&42i32.to_le_bytes());
14809        let mut rd = OriginalStreamReader::new(&bytes);
14810        rd.layout = NativeLocalLayout::Early;
14811        let obj = SceneVm::read_cpp_object(&mut rd).unwrap();
14812        assert_eq!(obj.object_type, 1);
14813        assert_eq!(obj.runtime.prop_events.patno.value, 7);
14814        assert_eq!((obj.base.order, obj.base.layer), (50, 2));
14815        assert!(!obj.button.enabled);
14816        assert_eq!(obj.runtime.prop_events.x.value, 120);
14817        assert_eq!(obj.runtime.prop_events.y.value, 240);
14818        assert_eq!(obj.runtime.prop_events.y.cur_value, 170);
14819        assert_eq!(
14820            (
14821                obj.runtime.prop_events.y.cur_time,
14822                obj.runtime.prop_events.y.end_time
14823            ),
14824            (30, 60)
14825        );
14826        assert!(obj.runtime.child_objects.is_empty());
14827        assert_eq!(rd.i32().unwrap(), 42);
14828    }
14829
14830    #[test]
14831    fn early_message_page_restores_packed_flags_and_glyphs_without_outline_fields() {
14832        use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
14833        let mut w = OriginalStreamWriter::new();
14834        for value in [
14835            24, 3, 20, 40, 0, 0, 34, -1, 16, 2, 1, 12, 2, 38, 0, 0, 0, 0, 0, 0,
14836        ] {
14837            w.push_i32(value);
14838        }
14839        for value in [3, 1, 16] {
14840            w.push_i32(value);
14841        } // Character colors, indent.
14842        w.push_raw(&('「' as u16).to_le_bytes());
14843        w.push_i32(1);
14844        w.push_i32(0);
14845        w.push_bool(true); // Type decided, then line head before ruby position.
14846        w.push_bool(false);
14847        w.push_i32(50);
14848        w.push_i32(60);
14849        w.push_bool(true);
14850        w.push_i32(1);
14851        w.push_i32(0);
14852        w.push_str("A");
14853        w.push_str("ruby");
14854        w.push_i32(1);
14855        for value in [0, 'A' as i32, 34, 2, 1, 20, 40] {
14856            w.push_i32(value);
14857        }
14858        w.push_bool(true);
14859        w.push_bool(false);
14860        w.push_i32(42);
14861        let bytes = w.into_inner();
14862        let mut rd = OriginalStreamReader::new(&bytes);
14863        rd.layout = NativeLocalLayout::Early;
14864        let mut m = runtime::globals::MwndState::default();
14865        let page = SceneVm::read_cpp_mwnd_message(&mut rd, &mut m).unwrap();
14866        assert_eq!(page.msg_text, "A");
14867        assert_eq!(page.cursor_pos, (20, 40));
14868        assert_eq!(page.indent_moji, Some('「'));
14869        assert!(page.cur_msg_type_decided && page.ruby_start_ready);
14870        assert!(!page.line_head);
14871        assert_eq!(page.ruby_start_pos, (50, 60));
14872        assert_eq!(page.glyphs[0].x, 20);
14873        assert_eq!(page.glyphs[0].y, 40);
14874        assert!(page.glyphs[0].appeared);
14875        assert!(!page.glyphs[0].fuchi);
14876        assert_eq!(page.msgbtn, None);
14877        assert_eq!(rd.i32().unwrap(), 42);
14878    }
14879
14880    #[test]
14881    fn legacy_local_pod_restores_flags_without_consuming_stack_data() {
14882        use crate::original_save::OriginalStreamReader;
14883        let mut bytes = vec![0u8; 344];
14884        for (offset, value) in [(0, 12i32), (4, 3), (52, 585), (56, -1)] {
14885            bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
14886        }
14887        bytes[67] = 1; // All message-window buttons disabled.
14888        bytes[74] = 1; // Cursor hidden, immediately after the message flags.
14889        bytes[76 + 65] = 1; // Key A disabled.
14890        bytes[332] = 1; // Quake stopped.
14891        bytes[342] = 1; // Stage clock stopped.
14892        bytes.extend_from_slice(&42i32.to_le_bytes());
14893        let mut vm = test_vm();
14894        vm.ctx.globals.script.font_bold = 1;
14895        vm.ctx.globals.script.font_shadow = 2;
14896        let mut rd = OriginalStreamReader::new(&bytes);
14897        vm.read_cpp_local_data_pod(&mut rd, false).unwrap();
14898        let script = &vm.ctx.globals.script;
14899        assert_eq!((script.cur_koe_no, script.cur_chr_no), (12, 3));
14900        assert_eq!(script.msg_back_save_cntr, 585);
14901        assert_eq!((script.font_bold, script.font_shadow), (-1, -1));
14902        assert!(vm.ctx.globals.syscom.mwnd_btn_disable_all);
14903        assert!(script.cursor_disp_off && script.key_disable.contains(&65));
14904        assert!(script.quake_stop_flag && script.stage_time_stop_flag);
14905        assert_eq!(rd.i32().unwrap(), 42);
14906    }
14907
14908    #[test]
14909    fn legacy_object_gan_work_does_not_shift_the_next_object() {
14910        use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
14911        let vm = test_vm();
14912        let mut bytes = Vec::new();
14913        for name in ["first", "second"] {
14914            let mut obj = runtime::globals::ObjectState {
14915                file_name: Some(name.to_owned()),
14916                ..Default::default()
14917            };
14918
14919            let mut w = OriginalStreamWriter::new();
14920            vm.write_cpp_object(&mut w, &obj);
14921            let mut record = w.into_inner();
14922            // Native GAN work sits before the final child-list count.
14923            let child_count = record.split_off(record.len() - 4);
14924            for value in [123i32, 2, 3] {
14925                record.extend_from_slice(&value.to_le_bytes());
14926            }
14927            record.extend_from_slice(&[1, 0, 1, 0, 1, 1, 0]);
14928            record.extend_from_slice(&child_count);
14929            bytes.extend(record);
14930        }
14931        let mut rd = OriginalStreamReader::new(&bytes);
14932        rd.layout = NativeLocalLayout::Legacy;
14933        for name in ["first", "second"] {
14934            let obj = SceneVm::read_cpp_object(&mut rd).unwrap();
14935            assert_eq!(obj.file_name.as_deref(), Some(name));
14936            assert!(obj.runtime.child_objects.is_empty());
14937        }
14938        assert!(rd.remaining().is_empty());
14939    }
14940
14941    #[test]
14942    fn legacy_backlog_entries_do_not_have_save_id_check_bytes() {
14943        use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
14944        for early in [false, true] {
14945            let mut w = OriginalStreamWriter::new();
14946            w.push_i32(2);
14947            for message in ["first", "second"] {
14948                w.push_bool(false);
14949                w.push_str(message);
14950                w.push_str("name");
14951                w.push_str("display name");
14952                for value in [0, 0, 1, 123, 1, 2, 0] {
14953                    w.push_i32(value);
14954                }
14955                w.push_str("");
14956                w.push_i32(36);
14957                w.push_i32(29);
14958                if !early {
14959                    w.push_tid_zero();
14960                }
14961            }
14962            for value in [0, 1, 2] {
14963                w.push_i32(value);
14964            }
14965            if early {
14966                w.push_bool(true);
14967            } else {
14968                w.push_i32(1);
14969            }
14970            w.push_i32(42);
14971            let bytes = w.into_inner();
14972            let mut rd = OriginalStreamReader::new(&bytes);
14973            rd.layout = if early {
14974                NativeLocalLayout::Early
14975            } else {
14976                NativeLocalLayout::Legacy
14977            };
14978            let backlog = SceneVm::read_cpp_msg_back(&mut rd).unwrap();
14979            assert_eq!(backlog.history_cnt, 2);
14980            assert_eq!(backlog.history[0].msg_str, "first");
14981            assert_eq!(backlog.history[1].msg_str, "second");
14982            assert_eq!(backlog.history[1].koe_no_list, vec![123]);
14983            assert_eq!(backlog.history[1].chr_no_list, vec![2]);
14984            assert_eq!(backlog.history_insert_pos, 2);
14985            assert!(backlog.new_msg_flag);
14986            assert_eq!(rd.i32().unwrap(), 42);
14987        }
14988    }
14989
14990    #[test]
14991    fn get_line_no_returns_current_script_line_through_command_dispatch() {
14992        let mut chunk = empty_scene_chunk();
14993        let mut code = Vec::new();
14994        for line in [29i32, 83] {
14995            code.push(CD_NL);
14996            code.extend_from_slice(&line.to_le_bytes());
14997        }
14998        chunk[8..12].copy_from_slice(&(code.len() as i32).to_le_bytes());
14999        chunk.extend_from_slice(&code);
15000        let stream = SceneStream::new(Box::leak(chunk.into_boxed_slice())).unwrap();
15001        let mut vm = SceneVm::new(stream, CommandContext::new(PathBuf::from(".")));
15002
15003        for expected in [-1, 29, 83] {
15004            if expected >= 0 {
15005                assert!(vm.step_inner(false).unwrap());
15006            }
15007            vm.exec_command(
15008                vec![constants::elm_value::GLOBAL_GET_LINE_NO],
15009                0,
15010                vm.cfg.fm_int,
15011                &mut vec![],
15012            )
15013            .unwrap();
15014            assert_eq!(vm.pop_int().unwrap(), expected);
15015            assert!(vm.int_stack.is_empty());
15016            assert!(vm.ctx.stack.is_empty());
15017        }
15018    }
15019
15020    #[test]
15021    fn excall_indexed_stage_creates_menu_objects_and_preserves_properties() {
15022        use crate::runtime::forms::{codes, excall};
15023
15024        let mut vm = test_vm();
15025        vm.exec_command(
15026            vec![codes::ELM_GLOBAL_EXCALL, codes::ELM_EXCALL_ALLOC],
15027            0,
15028            vm.cfg.fm_void,
15029            &mut vec![],
15030        )
15031        .unwrap();
15032        let stage_form = excall::tick_targets(&vm.ctx).stage_form_id;
15033        for stage_idx in 0..3 {
15034            let mut object = vec![
15035                codes::ELM_GLOBAL_EXCALL,
15036                codes::ELM_EXCALL_STAGE,
15037                ELM_ARRAY,
15038                stage_idx,
15039                codes::ELM_STAGE_OBJECT,
15040                ELM_ARRAY,
15041                10,
15042            ];
15043            object.push(codes::ELM_OBJECT_CREATE_RECT);
15044            vm.exec_command(
15045                object.clone(),
15046                0,
15047                vm.cfg.fm_void,
15048                &mut [0, 0, 100, 80, 255, 255, 255, 255, 1]
15049                    .into_iter()
15050                    .map(Value::Int)
15051                    .collect(),
15052            )
15053            .unwrap();
15054            assert!(
15055                vm.ctx.globals.stage_forms[&stage_form].object_lists[&(stage_idx as i64)][10].used
15056            );
15057
15058            *object.last_mut().unwrap() = codes::ELM_OBJECT_X;
15059            vm.exec_assign(object.clone(), 1, Value::Int(123 + stage_idx as i64))
15060                .unwrap();
15061            vm.exec_property(object).unwrap();
15062            assert_eq!(vm.pop_int().unwrap(), 123 + stage_idx);
15063        }
15064        assert!(
15065            !vm.ctx.render_list_with_effects().is_empty(),
15066            "menu objects must be drawable"
15067        );
15068    }
15069
15070    #[test]
15071    fn excall_indexed_stage_button_group_accepts_right_click_cancel() {
15072        use crate::runtime::forms::codes;
15073        use crate::runtime::input::VmMouseButton;
15074
15075        let mut vm = test_vm();
15076        vm.exec_command(
15077            vec![codes::ELM_GLOBAL_EXCALL, codes::ELM_EXCALL_ALLOC],
15078            0,
15079            vm.cfg.fm_void,
15080            &mut vec![],
15081        )
15082        .unwrap();
15083        vm.ctx.excall_state.ex_call_flag = true;
15084        let mut group = vec![
15085            codes::ELM_GLOBAL_EXCALL,
15086            codes::ELM_EXCALL_STAGE,
15087            ELM_ARRAY,
15088            1,
15089            codes::STAGE_ELM_OBJBTNGROUP,
15090            ELM_ARRAY,
15091            2,
15092            constants::GROUP_INIT,
15093        ];
15094        vm.exec_command(group.clone(), 0, vm.cfg.fm_void, &mut vec![])
15095            .unwrap();
15096        *group.last_mut().unwrap() = constants::GROUP_START_CANCEL;
15097        vm.exec_command(group.clone(), 0, vm.cfg.fm_void, &mut vec![])
15098            .unwrap();
15099        *group.last_mut().unwrap() = constants::GROUP_GET_DECIDED_NO;
15100        vm.exec_command(group.clone(), 0, vm.cfg.fm_int, &mut vec![])
15101            .unwrap();
15102        assert_eq!(vm.pop_int().unwrap(), -2);
15103        vm.ctx.on_mouse_down(VmMouseButton::Right);
15104        vm.ctx.on_mouse_up(VmMouseButton::Right);
15105        vm.exec_command(group, 0, vm.cfg.fm_int, &mut vec![])
15106            .unwrap();
15107        assert_eq!(vm.pop_int().unwrap(), -1);
15108    }
15109
15110    #[test]
15111    fn polling_menu_group_restart_preserves_held_child_button() {
15112        use crate::runtime::forms::{codes, excall};
15113        use crate::runtime::input::VmMouseButton;
15114
15115        for start_op in [constants::GROUP_START, constants::GROUP_START_CANCEL] {
15116            for release_inside in [true, false] {
15117                let mut vm = test_vm();
15118                vm.exec_command(
15119                    vec![codes::ELM_GLOBAL_EXCALL, codes::ELM_EXCALL_ALLOC],
15120                    0,
15121                    vm.cfg.fm_void,
15122                    &mut vec![],
15123                )
15124                .unwrap();
15125                vm.ctx.excall_state.ex_call_flag = true;
15126                let mut object = vec![
15127                    codes::ELM_GLOBAL_EXCALL,
15128                    codes::ELM_EXCALL_FRONT,
15129                    codes::ELM_STAGE_OBJECT,
15130                    ELM_ARRAY,
15131                    62,
15132                ];
15133                for rect in [
15134                    [0, 0, 500, 400, 255, 255, 255, 255, 1, 100, 100],
15135                    [0, 0, 200, 80, 255, 255, 255, 255, 1, 20, 30],
15136                ] {
15137                    let mut command = object.clone();
15138                    command.push(codes::ELM_OBJECT_CREATE_RECT);
15139                    vm.exec_command(
15140                        command,
15141                        2,
15142                        vm.cfg.fm_void,
15143                        &mut rect.into_iter().map(Value::Int).collect(),
15144                    )
15145                    .unwrap();
15146                    if object.len() == 5 {
15147                        object.extend([codes::ELM_OBJECT_CHILD, ELM_ARRAY, 1]);
15148                    }
15149                }
15150                object.push(codes::ELM_OBJECT_SET_BUTTON);
15151                vm.exec_command(
15152                    object,
15153                    2,
15154                    vm.cfg.fm_void,
15155                    &mut [201, 0, 15, -1].into_iter().map(Value::Int).collect(),
15156                )
15157                .unwrap();
15158                let group = vec![
15159                    codes::ELM_GLOBAL_EXCALL,
15160                    codes::ELM_EXCALL_FRONT,
15161                    codes::STAGE_ELM_OBJBTNGROUP,
15162                    ELM_ARRAY,
15163                    0,
15164                    start_op,
15165                ];
15166                vm.exec_command(group.clone(), 0, vm.cfg.fm_void, &mut vec![])
15167                    .unwrap();
15168                vm.ctx.on_mouse_move(150, 150);
15169                vm.ctx.on_mouse_down(VmMouseButton::Left);
15170                let form = excall::tick_targets(&vm.ctx).stage_form_id;
15171                assert_eq!(
15172                    vm.ctx.globals.stage_forms[&form].group_lists[&1][0].pushed_button_no,
15173                    201
15174                );
15175
15176                // Save/load menus restart selection on every polling iteration.
15177                // The physical press must outlive these selection-state resets.
15178                for _ in 0..3 {
15179                    vm.exec_command(group.clone(), 0, vm.cfg.fm_void, &mut vec![])
15180                        .unwrap();
15181                    vm.ctx.input.next_frame();
15182                    vm.ctx.tick_frame();
15183                    let mut get_pushed = group.clone();
15184                    *get_pushed.last_mut().unwrap() = constants::GROUP_GET_PUSHED_NO;
15185                    vm.exec_command(get_pushed, 0, vm.cfg.fm_int, &mut vec![])
15186                        .unwrap();
15187                    assert_eq!(vm.pop_int().unwrap(), 201);
15188                }
15189                if !release_inside {
15190                    vm.ctx.on_mouse_move(900, 600);
15191                }
15192                vm.ctx.on_mouse_up(VmMouseButton::Left);
15193                let state = &vm.ctx.globals.stage_forms[&form].group_lists[&1][0];
15194                assert_eq!(
15195                    state.decided_button_no,
15196                    if release_inside { 201 } else { -2 }
15197                );
15198                assert_eq!(state.pushed_button_no, -1);
15199            }
15200        }
15201    }
15202
15203    #[test]
15204    fn dialog_child_buttons_inherit_parent_layer_for_hover_and_click() {
15205        use crate::runtime::forms::{codes, excall};
15206        use crate::runtime::input::VmMouseButton;
15207
15208        for group_no in [-1, 8] {
15209            let mut vm = test_vm();
15210            vm.exec_command(
15211                vec![codes::ELM_GLOBAL_EXCALL, codes::ELM_EXCALL_ALLOC],
15212                0,
15213                vm.cfg.fm_void,
15214                &mut vec![],
15215            )
15216            .unwrap();
15217            vm.ctx.excall_state.ex_call_flag = true;
15218            let root = vec![
15219                codes::ELM_GLOBAL_EXCALL,
15220                codes::ELM_EXCALL_FRONT,
15221                codes::ELM_STAGE_OBJECT,
15222                ELM_ARRAY,
15223                69,
15224            ];
15225            let mut child = root.clone();
15226            child.extend([codes::ELM_OBJECT_CHILD, ELM_ARRAY, 1]);
15227            for (object, rect, button_no, layer) in [
15228                (
15229                    &root,
15230                    [0, 0, 400, 200, 255, 255, 255, 255, 1, 100, 200],
15231                    111,
15232                    300,
15233                ),
15234                (&child, [0, 0, 80, 40, 255, 255, 255, 255, 1, 20, 30], 1, 0),
15235            ] {
15236                let mut command = object.clone();
15237                command.push(codes::ELM_OBJECT_CREATE_RECT);
15238                vm.exec_command(
15239                    command,
15240                    2,
15241                    vm.cfg.fm_void,
15242                    &mut rect.into_iter().map(Value::Int).collect(),
15243                )
15244                .unwrap();
15245                let mut property = object.clone();
15246                property.push(codes::ELM_OBJECT_LAYER);
15247                vm.exec_assign(property, 1, Value::Int(layer)).unwrap();
15248                let mut command = object.clone();
15249                command.push(codes::ELM_OBJECT_SET_BUTTON);
15250                vm.exec_command(
15251                    command,
15252                    2,
15253                    vm.cfg.fm_void,
15254                    &mut [button_no, group_no, 1, -1]
15255                        .into_iter()
15256                        .map(Value::Int)
15257                        .collect(),
15258                )
15259                .unwrap();
15260            }
15261            if group_no >= 0 {
15262                vm.exec_command(
15263                    vec![
15264                        codes::ELM_GLOBAL_EXCALL,
15265                        codes::ELM_EXCALL_FRONT,
15266                        codes::STAGE_ELM_OBJBTNGROUP,
15267                        ELM_ARRAY,
15268                        group_no as i32,
15269                        constants::GROUP_START_CANCEL,
15270                    ],
15271                    0,
15272                    vm.cfg.fm_void,
15273                    &mut vec![],
15274                )
15275                .unwrap();
15276            }
15277            let form = excall::tick_targets(&vm.ctx).stage_form_id;
15278            vm.ctx.on_mouse_move(140, 245);
15279            let obj = &vm.ctx.globals.stage_forms[&form].object_lists[&1][69];
15280            assert!(
15281                obj.runtime.child_objects[1].button.hit,
15282                "child must win over the dialog background"
15283            );
15284            assert!(!obj.button.hit);
15285            vm.ctx.on_mouse_down(VmMouseButton::Left);
15286            assert!(
15287                vm.ctx.globals.stage_forms[&form].object_lists[&1][69]
15288                    .runtime
15289                    .child_objects[1]
15290                    .button
15291                    .pushed
15292            );
15293            vm.ctx.on_mouse_up(VmMouseButton::Left);
15294            if group_no >= 0 {
15295                assert_eq!(
15296                    vm.ctx.globals.stage_forms[&form].group_lists[&1][group_no as usize]
15297                        .decided_button_no,
15298                    1
15299                );
15300            }
15301        }
15302    }
15303
15304    #[test]
15305    fn local_menu_flags_are_not_shadowed_by_object_children() {
15306        use crate::runtime::forms::codes;
15307        let mut vm = test_vm();
15308        let parent = vec![
15309            codes::ELM_GLOBAL_FRONT,
15310            codes::STAGE_ELM_OBJECT,
15311            ELM_ARRAY,
15312            95,
15313        ];
15314        let mut child = parent.clone();
15315        child.extend([
15316            codes::ELM_OBJECT_CHILD,
15317            ELM_ARRAY,
15318            codes::ELM_GLOBAL_B,
15319            codes::ELM_OBJECT_CREATE_RECT,
15320        ]);
15321        vm.exec_command(
15322            child,
15323            0,
15324            vm.cfg.fm_void,
15325            &mut vec![Value::Int(16), Value::Int(16), Value::Int(255)],
15326        )
15327        .unwrap();
15328        let flag = vec![codes::ELM_GLOBAL_B, ELM_ARRAY, 43];
15329        vm.ctx
15330            .globals
15331            .int_lists
15332            .entry(codes::ELM_GLOBAL_B as u32)
15333            .or_default()
15334            .resize(1000, 0);
15335        vm.ctx
15336            .globals
15337            .int_lists
15338            .get_mut(&(codes::ELM_GLOBAL_B as u32))
15339            .unwrap()[43] = -1;
15340
15341        // The gallery reads this flag after creating a container with many
15342        // children. Neither ambient object context may reinterpret B[43].
15343        for context in [
15344            None,
15345            Some(vec![
15346                codes::FORM_GLOBAL_STAGE as i32,
15347                ELM_ARRAY,
15348                1,
15349                codes::STAGE_ELM_OBJECT,
15350                ELM_ARRAY,
15351                95,
15352            ]),
15353        ] {
15354            vm.ctx.globals.current_object_chain = context;
15355            vm.ctx.globals.current_stage_object = Some((1, 95));
15356            vm.exec_property(flag.clone()).unwrap();
15357            assert_eq!(vm.pop_int().unwrap(), -1);
15358            vm.exec_assign(flag.clone(), 1, Value::Int(7)).unwrap();
15359            assert_eq!(
15360                vm.ctx.globals.int_lists[&(codes::ELM_GLOBAL_B as u32)][43],
15361                7
15362            );
15363            vm.exec_assign(flag.clone(), 1, Value::Int(-1)).unwrap();
15364        }
15365    }
15366
15367    #[test]
15368    fn op_seen_flag_reads_and_writes_the_persistent_global_list() {
15369        use crate::runtime::forms::codes::ELM_GLOBAL_G;
15370        let mut vm = test_vm();
15371        let elm = vec![ELM_GLOBAL_G, ELM_ARRAY, 153];
15372        vm.ctx
15373            .globals
15374            .int_lists
15375            .entry(ELM_GLOBAL_G as u32)
15376            .or_default()
15377            .resize(1000, 0);
15378        vm.ctx
15379            .globals
15380            .int_lists
15381            .get_mut(&(ELM_GLOBAL_G as u32))
15382            .unwrap()[153] = 1;
15383        vm.exec_property(elm.clone()).unwrap();
15384        assert_eq!(
15385            vm.pop_int().unwrap(),
15386            1,
15387            "read the OP flag restored from global.sav"
15388        );
15389        vm.exec_assign(elm.clone(), 1, Value::Int(2)).unwrap();
15390        assert_eq!(vm.ctx.globals.int_lists[&(ELM_GLOBAL_G as u32)][153], 2);
15391        vm.ctx.reset_for_scene_restart();
15392        vm.exec_property(elm).unwrap();
15393        assert_eq!(vm.pop_int().unwrap(), 2);
15394    }
15395
15396    #[test]
15397    fn wait_wipe_without_active_transition_returns_zero_each_time() {
15398        let mut vm = test_vm();
15399        for _ in 0..2 {
15400            vm.exec_command(
15401                vec![constants::elm_value::GLOBAL_WAIT_WIPE],
15402                0,
15403                vm.cfg.fm_int,
15404                &mut vec![],
15405            )
15406            .expect("WAIT_WIPE after the transition has finished");
15407            assert_eq!(vm.pop_int().unwrap(), 0);
15408            assert!(!vm.ctx.wait_poll());
15409            assert!(vm.ctx.stack.is_empty(), "return is delivered only once");
15410        }
15411    }
15412
15413    #[test]
15414    fn wait_wipe_returns_completion_result_but_inline_wipe_wait_is_void() {
15415        for (explicit_wait, key_skip) in
15416            [(true, false), (true, true), (false, false), (false, true)]
15417        {
15418            let mut vm = test_vm();
15419            vm.exec_command(
15420                vec![constants::elm_value::GLOBAL_WIPE],
15421                0,
15422                vm.cfg.fm_void,
15423                &mut vec![],
15424            )
15425            .unwrap();
15426            if explicit_wait {
15427                vm.exec_command(
15428                    vec![constants::elm_value::GLOBAL_WAIT_WIPE],
15429                    0,
15430                    vm.cfg.fm_int,
15431                    &mut vec![Value::NamedArg {
15432                        id: 0,
15433                        value: Box::new(Value::Int(1)),
15434                    }],
15435                )
15436                .unwrap();
15437            } else {
15438                vm.ctx.wait.wait_wipe(true);
15439            }
15440            assert!(vm.ctx.wait_poll());
15441            if key_skip {
15442                assert!(vm.ctx.wait.notify_key(&mut vm.ctx.globals, &vm.ctx.ids));
15443            }
15444            vm.ctx.finish_wipe_runtime();
15445            assert!(!vm.ctx.wait_poll());
15446            if explicit_wait {
15447                assert_eq!(
15448                    vm.ctx.pop().and_then(|v| v.as_i64()),
15449                    Some(if key_skip { 1 } else { 0 })
15450                );
15451            }
15452            assert!(vm.ctx.stack.is_empty());
15453            assert!(!vm.ctx.wait_poll());
15454            assert!(vm.ctx.stack.is_empty());
15455        }
15456    }
15457
15458    #[test]
15459    fn wipe_without_arguments_starts_the_default_transition() {
15460        let mut vm = test_vm();
15461        vm.exec_command(
15462            vec![constants::elm_value::GLOBAL_WIPE],
15463            0,
15464            vm.cfg.fm_void,
15465            &mut vec![],
15466        )
15467        .unwrap();
15468        assert!(
15469            vm.ctx.globals.wipe.is_some(),
15470            "WIPE() is a real script command"
15471        );
15472        assert!(vm.ctx.wait.wipe);
15473        assert!(vm.is_blocked());
15474    }
15475
15476    #[test]
15477    fn global_title_commands_read_and_update_the_saved_scene_title() {
15478        let mut vm = test_vm();
15479        vm.exec_command(
15480            vec![constants::elm_value::GLOBAL_GET_TITLE],
15481            0,
15482            vm.cfg.fm_str,
15483            &mut vec![],
15484        )
15485        .unwrap();
15486        assert_eq!(vm.pop_str().unwrap(), "");
15487
15488        for title in ["第一章", "エピローグ", ""] {
15489            vm.exec_command(
15490                vec![constants::elm_value::GLOBAL_SET_TITLE],
15491                0,
15492                vm.cfg.fm_void,
15493                &mut vec![Value::Str(title.into())],
15494            )
15495            .unwrap();
15496            vm.exec_command(
15497                vec![constants::elm_value::GLOBAL_GET_TITLE],
15498                0,
15499                vm.cfg.fm_str,
15500                &mut vec![],
15501            )
15502            .unwrap();
15503            assert_eq!(vm.pop_str().unwrap(), title);
15504            assert!(vm.ctx.stack.is_empty());
15505            vm.build_local_save_snapshot();
15506            assert_eq!(
15507                vm.ctx
15508                    .local_save_snapshot
15509                    .as_ref()
15510                    .unwrap()
15511                    .save_scene_title,
15512                title
15513            );
15514        }
15515    }
15516
15517    #[test]
15518    fn global_get_scene_name_returns_active_scene_on_string_stack() {
15519        let mut vm = test_vm();
15520        for scene in ["_start", "menu", "frame_action_scene"] {
15521            vm.current_scene_name = Some(scene.into());
15522            vm.ctx.current_scene_name = Some(scene.into());
15523            vm.exec_command(
15524                vec![constants::elm_value::GLOBAL_GET_SCENE_NAME],
15525                0,
15526                vm.cfg.fm_str,
15527                &mut vec![],
15528            )
15529            .unwrap();
15530            assert_eq!(vm.pop_str().unwrap(), scene);
15531            assert!(vm.ctx.stack.is_empty());
15532        }
15533    }
15534
15535    #[test]
15536    fn global_owari_yields_to_host_for_exit_without_confirmation() {
15537        use crate::runtime::globals::SyscomPendingProcKind;
15538
15539        let mut vm = test_vm();
15540        vm.ctx.globals.syscom.menu_open = true;
15541        let generation = vm.ctx.proc_generation();
15542        vm.exec_command(
15543            vec![constants::elm_value::GLOBAL_OWARI],
15544            0,
15545            vm.cfg.fm_void,
15546            &mut vec![],
15547        )
15548        .expect("OWARI requests normal host shutdown");
15549        let pending = vm.ctx.globals.syscom.pending_proc.as_ref().unwrap();
15550        assert_eq!(pending.kind, SyscomPendingProcKind::EndGame);
15551        assert!(!pending.warning && !pending.se_play && !pending.fade_out);
15552        assert!(!vm.ctx.globals.syscom.menu_open);
15553        assert_ne!(vm.ctx.proc_generation(), generation);
15554        assert!(vm.ctx.stack.is_empty());
15555    }
15556
15557    #[test]
15558    fn global_returnmenu_matches_original_overload_control_flow() {
15559        use crate::runtime::globals::SyscomPendingProcKind;
15560
15561        let mut vm = test_vm();
15562        for (al_id, mut args, expected_target, expected_kind, expected_warning, expected_fade) in [
15563            (
15564                2,
15565                vec![Value::Str("menu".into()), Value::Int(7)],
15566                Some(("menu".into(), 7)),
15567                SyscomPendingProcKind::RestartScene,
15568                true,
15569                false,
15570            ),
15571            (
15572                1,
15573                vec![Value::Str("title".into())],
15574                Some(("title".into(), 0)),
15575                SyscomPendingProcKind::RestartScene,
15576                true,
15577                false,
15578            ),
15579            (
15580                0,
15581                vec![],
15582                None,
15583                SyscomPendingProcKind::ReturnToMenu,
15584                false,
15585                true,
15586            ),
15587        ] {
15588            let generation = vm.ctx.proc_generation();
15589            vm.exec_command(
15590                vec![constants::elm_value::GLOBAL_RETURNMENU],
15591                al_id,
15592                vm.cfg.fm_void,
15593                &mut args,
15594            )
15595            .unwrap();
15596            assert_ne!(vm.ctx.proc_generation(), generation);
15597            assert_eq!(vm.ctx.pending_scene_restart, expected_target);
15598            let pending = vm.ctx.globals.syscom.pending_proc.as_ref().unwrap();
15599            assert_eq!(pending.kind, expected_kind);
15600            assert_eq!(pending.warning, expected_warning);
15601            assert!(!pending.se_play);
15602            assert_eq!(pending.fade_out, expected_fade);
15603            assert!(!pending.leave_msgbk);
15604            assert!(vm.ctx.stack.is_empty());
15605            assert!(!vm.halted);
15606        }
15607        vm.ctx.pending_scene_restart = Some(("stale".into(), 7));
15608        vm.ctx.reset_for_scene_restart();
15609        assert!(vm.ctx.pending_scene_restart.is_none());
15610        assert!(vm.ctx.globals.syscom.pending_proc.is_none());
15611    }
15612
15613    #[test]
15614    fn user_string_commands_extract_map_date_and_time() {
15615        use crate::runtime::forms::codes::str_op;
15616
15617        let mut vm = test_vm();
15618        let prop_id = 119;
15619        vm.assign_user_prop(prop_id, None, Value::Str("0729a".into()));
15620        let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
15621        for (start, len, expected) in [(0, 4, "0729"), (4, 1, "a")] {
15622            vm.exec_command(
15623                vec![head, str_op::MID],
15624                1,
15625                vm.cfg.fm_str,
15626                &mut vec![Value::Int(start), Value::Int(len)],
15627            )
15628            .expect("string MID command");
15629            assert_eq!(vm.pop_str().expect("substring"), expected);
15630        }
15631        vm.exec_command(vec![head, str_op::CNT], 0, vm.cfg.fm_int, &mut vec![])
15632            .expect("string CNT command");
15633        assert_eq!(vm.pop_int().expect("string length"), 5);
15634        assert_eq!(vm.user_props[&prop_id].str_value, "0729a");
15635    }
15636
15637    #[test]
15638    fn user_prop_strlist_resize_and_get_size_are_not_silently_ignored() {
15639        let mut vm = test_vm();
15640        let prop_id = 0u16;
15641        let mut cell = UserPropCell::new(
15642            vm.cfg.fm_strlist,
15643            vm.default_user_prop_element(prop_id, vm.cfg.fm_strlist),
15644        );
15645        cell.str_list.clear();
15646        vm.user_props.insert(prop_id, cell);
15647
15648        assert!(
15649            vm.exec_user_prop_list_command(
15650                prop_id,
15651                &[ELM_STRLIST_RESIZE],
15652                0,
15653                vm.cfg.fm_void,
15654                &[Value::Int(1)],
15655            )
15656            .expect("STRLIST.RESIZE")
15657        );
15658        assert_eq!(vm.user_props[&prop_id].str_list.len(), 1);
15659
15660        assert!(vm
15661            .exec_user_prop_list_command(
15662                prop_id,
15663                &[ELM_STRLIST_GET_SIZE],
15664                0,
15665                vm.cfg.fm_int,
15666                &[],
15667            )
15668            .expect("STRLIST.GET_SIZE"));
15669        assert_eq!(vm.pop_int().expect("STRLIST size"), 1);
15670    }
15671
15672    #[test]
15673    fn user_prop_intlist_resize_and_get_size_are_not_silently_ignored() {
15674        let mut vm = test_vm();
15675        let prop_id = 1u16;
15676        let mut cell = UserPropCell::new(
15677            vm.cfg.fm_intlist,
15678            vm.default_user_prop_element(prop_id, vm.cfg.fm_intlist),
15679        );
15680        cell.int_list.clear();
15681        vm.user_props.insert(prop_id, cell);
15682
15683        assert!(
15684            vm.exec_user_prop_list_command(
15685                prop_id,
15686                &[ELM_INTLIST_RESIZE],
15687                0,
15688                vm.cfg.fm_void,
15689                &[Value::Int(1)],
15690            )
15691            .expect("INTLIST.RESIZE")
15692        );
15693        assert_eq!(vm.user_props[&prop_id].int_list.len(), 1);
15694
15695        assert!(vm
15696            .exec_user_prop_list_command(
15697                prop_id,
15698                &[ELM_INTLIST_GET_SIZE],
15699                0,
15700                vm.cfg.fm_int,
15701                &[],
15702            )
15703            .expect("INTLIST.GET_SIZE"));
15704        assert_eq!(vm.pop_int().expect("INTLIST size"), 1);
15705    }
15706
15707    #[test]
15708    fn user_prop_packed_intlist_assignment_preserves_list_storage_and_element_stack() {
15709        let mut vm = test_vm();
15710        let prop_id = 98u16;
15711        let mut cell = UserPropCell::new(
15712            vm.cfg.fm_intlist,
15713            vm.default_user_prop_element(prop_id, vm.cfg.fm_intlist),
15714        );
15715        cell.int_list = vec![0; 200];
15716        vm.user_props.insert(prop_id, cell);
15717
15718        let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
15719        let bit16 = vec![head, ELM_INTLIST_BIT16, ELM_ARRAY, 319];
15720
15721        vm.exec_assign(bit16.clone(), 1, Value::Int(0x5A3C))
15722            .expect("BIT16 assignment");
15723
15724        let cell = &vm.user_props[&prop_id];
15725        assert_eq!(cell.form, vm.cfg.fm_intlist);
15726        assert_eq!(cell.int_list.len(), 200);
15727        assert_eq!(
15728            SceneVm::get_user_int_list_value(&cell.int_list, 16, 319),
15729            Some(0x5A3C)
15730        );
15731
15732        vm.exec_property(bit16).expect("BIT16 property");
15733        assert_eq!(vm.pop_int().expect("BIT16 value"), 0x5A3C);
15734        assert!(vm.element_points.is_empty());
15735        assert!(vm.int_stack.is_empty());
15736    }
15737
15738    #[test]
15739    fn call_prop_packed_intlist_assignment_preserves_list_storage() {
15740        let vm = test_vm();
15741        let prop_id = 7;
15742        let element = vec![constants::elm::create(
15743            constants::elm::OWNER_CALL_PROP,
15744            0,
15745            prop_id,
15746        )];
15747        let mut prop = CallProp {
15748            scn_no: 0,
15749            prop_id,
15750            form: vm.cfg.fm_intlist,
15751            decl_size: 2,
15752            element,
15753            value: CallPropValue::IntList(vec![0; 2]),
15754        };
15755
15756        SceneVm::assign_call_prop_result(
15757            &mut prop,
15758            &[ELM_INTLIST_BIT16, ELM_ARRAY, 3],
15759            Value::Int(0x1234),
15760        )
15761        .expect("CALL_PROP BIT16 assignment");
15762
15763        assert_eq!(prop.form, vm.cfg.fm_intlist);
15764        let CallPropValue::IntList(values) = &prop.value else {
15765            panic!("CALL_PROP INTLIST storage changed form");
15766        };
15767        assert_eq!(values.len(), 2);
15768        assert_eq!(
15769            SceneVm::get_user_int_list_value(values, 16, 3),
15770            Some(0x1234)
15771        );
15772    }
15773
15774    #[test]
15775    fn negative_user_prop_list_index_does_not_replace_the_list_root() {
15776        let mut vm = test_vm();
15777        let prop_id = 0u16;
15778        let mut cell = UserPropCell::new(
15779            vm.cfg.fm_strlist,
15780            vm.default_user_prop_element(prop_id, vm.cfg.fm_strlist),
15781        );
15782        cell.str_list = vec!["keep".to_string()];
15783        vm.user_props.insert(prop_id, cell);
15784
15785        let head = constants::elm::create(constants::elm::OWNER_USER_PROP, 0, prop_id as i32);
15786        vm.exec_assign(
15787            vec![head, ELM_ARRAY, -1],
15788            1,
15789            Value::Str("wrong".to_string()),
15790        )
15791        .expect("negative list assignment");
15792
15793        let cell = &vm.user_props[&prop_id];
15794        assert_eq!(cell.form, vm.cfg.fm_strlist);
15795        assert_eq!(cell.str_list, vec!["keep".to_string()]);
15796    }
15797}
15798#[cfg(test)]
15799mod call_frame_save_metadata_tests {
15800    use super::*;
15801    use crate::original_save::{OriginalStreamReader, OriginalStreamWriter};
15802    use crate::scene_stream::SceneStream;
15803    use std::path::PathBuf;
15804
15805    fn empty_scene_chunk() -> Vec<u8> {
15806        const HEADER_WORDS: usize = 33;
15807        const HEADER_SIZE: i32 = (HEADER_WORDS * 4) as i32;
15808        let mut words = [0i32; HEADER_WORDS];
15809        for idx in [
15810            0usize, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31,
15811        ] {
15812            words[idx] = HEADER_SIZE;
15813        }
15814        let mut out = Vec::with_capacity(HEADER_SIZE as usize);
15815        for word in words {
15816            out.extend_from_slice(&word.to_le_bytes());
15817        }
15818        out
15819    }
15820
15821    fn test_call_frame(
15822        return_pc: usize,
15823        ret_form: i32,
15824        return_override: Option<(usize, i32)>,
15825    ) -> CallFrame {
15826        CallFrame {
15827            call_type: 3,
15828            return_pc,
15829            return_scene_no: None,
15830            return_scene_name: None,
15831            return_line_no: -1,
15832            ret_form,
15833            return_override,
15834            excall_proc: false,
15835            frame_action_proc: false,
15836            arg_cnt: 0,
15837            delayed_ret_form: None,
15838            user_props: Vec::new(),
15839            int_args: Vec::new(),
15840            str_args: Vec::new(),
15841        }
15842    }
15843
15844    #[test]
15845    fn exec_return_prefers_per_callee_continuation_for_inline_calls() {
15846        let chunk = Box::leak(empty_scene_chunk().into_boxed_slice());
15847        let stream = SceneStream::new(chunk).expect("empty scene stream");
15848        let mut vm = SceneVm::new(stream, CommandContext::new(PathBuf::from(".")));
15849        vm.call_stack.clear();
15850        vm.call_stack.push(test_call_frame(8, vm.cfg.fm_void, None));
15851        vm.call_stack.push(test_call_frame(
15852            0,
15853            vm.cfg.fm_void,
15854            Some((12, vm.cfg.fm_int)),
15855        ));
15856
15857        assert!(
15858            !vm.exec_return(vec![Value::Int(77)])
15859                .expect("return from inline call")
15860        );
15861        assert_eq!(vm.stream.get_prg_cntr(), 12);
15862        assert_eq!(vm.pop_int().expect("inline return value"), 77);
15863    }
15864
15865    #[test]
15866    fn exec_return_falls_back_to_original_caller_metadata_for_saved_frames() {
15867        let chunk = Box::leak(empty_scene_chunk().into_boxed_slice());
15868        let stream = SceneStream::new(chunk).expect("empty scene stream");
15869        let mut vm = SceneVm::new(stream, CommandContext::new(PathBuf::from(".")));
15870        vm.call_stack.clear();
15871        vm.call_stack.push(test_call_frame(16, vm.cfg.fm_int, None));
15872        vm.call_stack.push(test_call_frame(0, vm.cfg.fm_void, None));
15873
15874        assert!(
15875            !vm.exec_return(vec![Value::Int(91)])
15876                .expect("return from saved-style call")
15877        );
15878        assert_eq!(vm.stream.get_prg_cntr(), 16);
15879        assert_eq!(vm.pop_int().expect("saved-style return value"), 91);
15880    }
15881
15882    #[test]
15883    fn call_frame_round_trip_keeps_caller_lexer_metadata() {
15884        let chunk = Box::leak(empty_scene_chunk().into_boxed_slice());
15885        let stream = SceneStream::new(chunk).expect("empty scene stream");
15886        let vm = SceneVm::new(stream, CommandContext::new(PathBuf::from(".")));
15887        let frame = CallFrame {
15888            call_type: 2,
15889            return_pc: 0x1234,
15890            return_scene_no: Some(7),
15891            return_scene_name: Some("caller_scene".to_string()),
15892            return_line_no: 2605,
15893            ret_form: vm.cfg.fm_void,
15894            return_override: None,
15895            excall_proc: false,
15896            frame_action_proc: false,
15897            arg_cnt: 0,
15898            delayed_ret_form: None,
15899            user_props: Vec::new(),
15900            int_args: Vec::new(),
15901            str_args: Vec::new(),
15902        };
15903        let mut writer = OriginalStreamWriter::new();
15904        vm.write_cpp_call_frame(&mut writer, &frame);
15905        let bytes = writer.into_inner();
15906        let mut reader = OriginalStreamReader::new(&bytes);
15907        let restored = vm.read_cpp_call_frame(&mut reader).expect("call frame");
15908        assert_eq!(restored.call_type, 2);
15909        assert_eq!(restored.return_pc, 0x1234);
15910        assert_eq!(restored.return_scene_name.as_deref(), Some("caller_scene"));
15911        assert_eq!(restored.return_line_no, 2605);
15912    }
15913}