Skip to main content

siglus_scene_vm/runtime/forms/
excall.rs

1use anyhow::{Result, bail};
2
3use crate::runtime::globals::{ObjectFrameActionState, PendingFrameActionFinish};
4use crate::runtime::{CommandContext, Value, VmCallMeta};
5
6use super::codes::{self, excall_op};
7use super::{counter, frame_action, frame_action_ch, int_list, script, stage};
8
9const EXCALL_LOCAL_NS_XOR: u32 = 0x4000;
10
11fn excall_form_key(ctx: &CommandContext) -> u32 {
12    if ctx.ids.form_global_excall != 0 {
13        ctx.ids.form_global_excall
14    } else {
15        super::codes::FORM_GLOBAL_EXCALL
16    }
17}
18
19fn stage_form_key(ctx: &CommandContext) -> u32 {
20    if ctx.ids.form_global_stage != 0 {
21        ctx.ids.form_global_stage
22    } else {
23        super::codes::FORM_GLOBAL_STAGE
24    }
25}
26
27fn excall_stage_form_key(ctx: &CommandContext, selector: i32) -> u32 {
28    let base = stage_form_key(ctx);
29    if selector == 0 {
30        base
31    } else {
32        base ^ EXCALL_LOCAL_NS_XOR
33    }
34}
35
36fn synth_form_key(base: u32, selector: i32, op: i32) -> u32 {
37    (base << 8) ^ (((selector as u32) & 0x0f) << 4) ^ (op as u32 & 0x0f)
38}
39
40type ParsedExcall = (
41    usize,
42    Vec<i32>,
43    i32,
44    i32,
45    Vec<Value>,
46    Option<i64>,
47    Option<i64>,
48);
49
50fn parse_call(ctx: &CommandContext, args: &[Value]) -> Option<ParsedExcall> {
51    let form_id = excall_form_key(ctx);
52    let (chain_pos, chain) = super::prop_access::parse_element_chain_ctx(ctx, form_id, args)?;
53    let (selector, op_pos) = if chain.len() >= 3
54        && chain[1] == crate::runtime::forms::codes::ELM_ARRAY
55        && (chain[2] == 0 || chain[2] == 1)
56    {
57        (chain[2], 3usize)
58    } else {
59        (1i32, 1usize)
60    };
61    let op = chain
62        .get(op_pos)
63        .copied()
64        .or_else(|| args.first().and_then(|v| v.as_i64()).map(|v| v as i32))?;
65    let params = super::prop_access::script_args(args, chain_pos.min(args.len()));
66    let (meta_al_id, meta_ret_form) = crate::runtime::forms::prop_access::current_vm_meta(ctx);
67    let al_id = meta_al_id;
68    let ret_form = meta_ret_form;
69    // Own the nested element chain and script parameters before returning.
70    // parse_element_chain_ctx() may borrow the chain from ctx.vm_call; keeping
71    // that borrow alive across dispatch would prevent child handlers from
72    // mutably borrowing CommandContext.
73    Some((
74        chain_pos,
75        chain.to_vec(),
76        selector,
77        op,
78        params.to_vec(),
79        al_id,
80        ret_form,
81    ))
82}
83
84fn local_flag_count(ctx: &CommandContext) -> usize {
85    ctx.tables
86        .gameexe
87        .as_ref()
88        .and_then(|cfg| {
89            cfg.get_usize("#FLAG.CNT")
90                .or_else(|| cfg.get_usize("FLAG.CNT"))
91        })
92        .unwrap_or(1000)
93        .min(10000)
94}
95
96fn counter_count(ctx: &CommandContext) -> usize {
97    ctx.tables
98        .gameexe
99        .as_ref()
100        .and_then(|cfg| {
101            cfg.get_usize("#COUNTER.CNT")
102                .or_else(|| cfg.get_usize("COUNTER.CNT"))
103        })
104        .unwrap_or(16)
105        .min(256)
106}
107
108fn frame_action_ch_count(ctx: &CommandContext) -> usize {
109    ctx.tables
110        .gameexe
111        .as_ref()
112        .and_then(|cfg| {
113            cfg.get_usize("#FRAME_ACTION_CH.CNT")
114                .or_else(|| cfg.get_usize("FRAME_ACTION_CH.CNT"))
115        })
116        .unwrap_or(4)
117        .min(256)
118}
119
120fn excall_local_f_key(ctx: &CommandContext) -> u32 {
121    synth_form_key(excall_form_key(ctx), 1, excall_op::OP_7)
122}
123
124fn queue_frame_action_finish(
125    ctx: &mut CommandContext,
126    fa: &ObjectFrameActionState,
127    frame_action_chain: Vec<i32>,
128) {
129    if fa.cmd_name.is_empty() {
130        return;
131    }
132    ctx.globals
133        .pending_frame_action_finishes
134        .push(PendingFrameActionFinish {
135            frame_action_chain,
136            object_chain: None,
137            snapshot: fa.clone(),
138            reinit_after_finish: false,
139            scn_name: fa.scn_name.clone(),
140            cmd_name: fa.cmd_name.clone(),
141            end_time: fa.end_time,
142            args: fa.args.clone(),
143        });
144}
145
146fn begin_free(ctx: &mut CommandContext) {
147    let targets = tick_targets(ctx);
148
149    // C_elm_excall::finish(): main frame action, channel list, then stage list.
150    if let Some(fa) = ctx
151        .globals
152        .frame_actions
153        .get(&targets.frame_action_id)
154        .cloned()
155    {
156        queue_frame_action_finish(ctx, &fa, vec![targets.frame_action_id as i32]);
157    }
158    let ch_snapshot = ctx
159        .globals
160        .frame_action_lists
161        .get(&targets.frame_action_ch_list_id)
162        .cloned()
163        .unwrap_or_default();
164    for (idx, fa) in ch_snapshot.iter().enumerate() {
165        queue_frame_action_finish(
166            ctx,
167            fa,
168            vec![
169                targets.frame_action_ch_list_id as i32,
170                crate::runtime::forms::codes::ELM_ARRAY,
171                idx as i32,
172            ],
173        );
174    }
175    stage::queue_stage_form_finishes(ctx, targets.stage_form_id);
176
177    // Keep EXCALL ready until the VM drains the synchronous-equivalent finish
178    // callbacks. C++ clears child storage only after finish() returns.
179    ctx.excall_state.pending_free = true;
180}
181
182pub(crate) fn finalize_pending_free(ctx: &mut CommandContext) {
183    if !ctx.excall_state.pending_free {
184        return;
185    }
186    let targets = tick_targets(ctx);
187    let f_key = excall_local_f_key(ctx);
188    ctx.globals.int_lists.remove(&f_key);
189    ctx.globals.counter_lists.remove(&targets.counter_list_id);
190    ctx.globals
191        .frame_action_lists
192        .remove(&targets.frame_action_ch_list_id);
193    stage::free_stage_form(ctx, targets.stage_form_id);
194    ctx.excall_state.ready = false;
195    ctx.excall_state.pending_free = false;
196    // C_elm_excall::free() clears m_font_name but leaves the POD overrides.
197    ctx.excall_state.font_name.clear();
198}
199
200pub(crate) fn tick_targets(ctx: &CommandContext) -> crate::runtime::globals::ExcallTickTargets {
201    let form_key = excall_form_key(ctx);
202    crate::runtime::globals::ExcallTickTargets {
203        counter_list_id: synth_form_key(form_key, 1, excall_op::OP_6),
204        frame_action_id: synth_form_key(form_key, 1, excall_op::OP_9),
205        frame_action_ch_list_id: synth_form_key(form_key, 1, excall_op::OP_10),
206        stage_form_id: excall_stage_form_key(ctx, 1),
207    }
208}
209
210fn push_default(ctx: &mut CommandContext, ret_form: Option<i64>) {
211    if ret_form == Some(codes::FM_STR as i64) || ret_form == Some(codes::FM_STRREF as i64) {
212        ctx.push(Value::Str(String::new()));
213    } else {
214        ctx.push(Value::Int(0));
215    }
216}
217
218fn child_requires_ready(op: i32) -> bool {
219    matches!(
220        op,
221        excall_op::OP_0
222            | excall_op::OP_1
223            | excall_op::OP_2
224            | excall_op::OP_3
225            | excall_op::OP_6
226            | excall_op::OP_7
227            | excall_op::OP_9
228            | excall_op::OP_10
229            | excall_op::OP_13
230    )
231}
232
233fn translated_call_element(form_id: u32, chain_tail: &[i32]) -> Vec<i32> {
234    let mut chain = Vec::with_capacity(1 + chain_tail.len());
235    chain.push(form_id as i32);
236    chain.extend_from_slice(chain_tail);
237    chain
238}
239
240/// Map EXCALL child selectors to the concrete stage index used by
241/// C_elm_excall::m_stage_list.  The EXCALL element ids are *not* stage
242/// indices: def_element_Siglus.h / cmd_call.cpp define FRONT=1, BACK=2,
243/// NEXT=3, while TNM_STAGE_BACK/FRONT/NEXT are 0/1/2.
244fn excall_stage_index(op: i32) -> Option<i32> {
245    match op {
246        excall_op::FRONT => Some(1),
247        excall_op::BACK => Some(0),
248        excall_op::NEXT => Some(2),
249        _ => None,
250    }
251}
252
253fn translated_stage_element(
254    stage_form_id: u32,
255    stage_idx: Option<i32>,
256    chain_tail: &[i32],
257) -> Vec<i32> {
258    let mut chain = Vec::new();
259    chain.push(stage_form_id as i32);
260    if let Some(idx) = stage_idx {
261        chain.push(crate::runtime::forms::codes::ELM_ARRAY);
262        chain.push(idx);
263    }
264    chain.extend_from_slice(chain_tail);
265    chain
266}
267
268fn with_forwarded_vm_call<F>(ctx: &mut CommandContext, element: Vec<i32>, f: F) -> Result<bool>
269where
270    F: FnOnce(&mut CommandContext) -> Result<bool>,
271{
272    // C++ passes elm_begin + 1/+2 directly to each EXCALL child handler. In
273    // Rust, child form parsers read ctx.vm_call, so install the equivalent
274    // advanced element chain for the duration of the nested dispatch.
275    let saved = ctx.vm_call.clone();
276    let (al_id, ret_form) = saved
277        .as_ref()
278        .map(|m| (m.al_id, m.ret_form))
279        .unwrap_or((0, 0));
280    ctx.vm_call = Some(VmCallMeta {
281        element,
282        al_id,
283        ret_form,
284    });
285    let result = f(ctx);
286    ctx.vm_call = saved;
287    result
288}
289
290pub fn dispatch(ctx: &mut CommandContext, args: &[Value]) -> Result<bool> {
291    let Some((_chain_pos, chain, selector, op, params, _al_id, ret_form)) = parse_call(ctx, args)
292    else {
293        if args.is_empty() {
294            bail!("EXCALL form expects at least one argument (op id)");
295        }
296        return Ok(false);
297    };
298
299    let op_pos =
300        if chain.len() >= 3 && chain[1] == codes::ELM_ARRAY && (chain[2] == 0 || chain[2] == 1) {
301            3usize
302        } else {
303            1usize
304        };
305    let tail = chain.get(op_pos + 1..).unwrap_or(&[]);
306    let form_key = excall_form_key(ctx);
307
308    match op {
309        excall_op::OP_4 => {
310            if selector != 1 {
311                log::error!("EXCALL.ALLOC requires EXCALL[1], selector={}", selector);
312                push_default(ctx, ret_form);
313                return Ok(true);
314            }
315            let targets = tick_targets(ctx);
316            let f_key = excall_local_f_key(ctx);
317            let f_count = local_flag_count(ctx);
318            let counter_count = counter_count(ctx);
319            let frame_action_ch_count = frame_action_ch_count(ctx);
320            ctx.globals
321                .int_lists
322                .entry(f_key)
323                .or_default()
324                .resize(f_count, 0);
325            ctx.globals
326                .counter_lists
327                .entry(targets.counter_list_id)
328                .or_default()
329                .resize_with(counter_count, Default::default);
330            ctx.globals
331                .frame_actions
332                .entry(targets.frame_action_id)
333                .or_default();
334            ctx.globals
335                .frame_action_lists
336                .entry(targets.frame_action_ch_list_id)
337                .or_default()
338                .resize_with(frame_action_ch_count, ObjectFrameActionState::default);
339            stage::ensure_stage_form_allocated(ctx, targets.stage_form_id);
340            ctx.excall_state.ready = true;
341            push_default(ctx, ret_form);
342            return Ok(true);
343        }
344        excall_op::OP_5 => {
345            if selector != 1 {
346                log::error!("EXCALL.FREE requires EXCALL[1], selector={}", selector);
347                push_default(ctx, ret_form);
348                return Ok(true);
349            }
350            begin_free(ctx);
351            push_default(ctx, ret_form);
352            return Ok(true);
353        }
354        excall_op::OP_8 => {
355            ctx.push(Value::Int(if selector == 1 && ctx.excall_state.ready {
356                1
357            } else {
358                0
359            }));
360            return Ok(true);
361        }
362        excall_op::OP_12 => {
363            ctx.push(Value::Int(if ctx.excall_state.ex_call_flag {
364                1
365            } else {
366                0
367            }));
368            return Ok(true);
369        }
370        _ => {}
371    }
372
373    if selector == 1 && child_requires_ready(op) && !ctx.excall_state.ready {
374        log::error!(
375            "EXCALL child accessed before ALLOC: op={} chain={:?}",
376            op,
377            chain
378        );
379        push_default(ctx, ret_form);
380        return Ok(true);
381    }
382
383    match op {
384        excall_op::OP_0 => {
385            let element =
386                translated_stage_element(excall_stage_form_key(ctx, selector), None, tail);
387            with_forwarded_vm_call(ctx, element, |ctx| stage::dispatch(ctx, &params))
388        }
389        excall_op::FRONT | excall_op::BACK | excall_op::NEXT => {
390            let stage_idx = excall_stage_index(op)
391                .expect("FRONT/BACK/NEXT must map to a concrete EXCALL stage");
392            let element = translated_stage_element(
393                excall_stage_form_key(ctx, selector),
394                Some(stage_idx),
395                tail,
396            );
397            with_forwarded_vm_call(ctx, element, |ctx| stage::dispatch(ctx, &params))
398        }
399        excall_op::OP_6 => {
400            let key = synth_form_key(form_key, selector, op);
401            let element = translated_call_element(key, tail);
402            with_forwarded_vm_call(ctx, element, |ctx| counter::dispatch(ctx, key, &params))
403        }
404        excall_op::OP_7 => {
405            let key = if selector == 0 {
406                codes::ELM_GLOBAL_F as u32
407            } else {
408                excall_local_f_key(ctx)
409            };
410            let element = translated_call_element(key, tail);
411            with_forwarded_vm_call(ctx, element, |ctx| int_list::dispatch(ctx, key, &params))
412        }
413        excall_op::OP_9 => {
414            let key = synth_form_key(form_key, selector, op);
415            let element = translated_call_element(key, tail);
416            with_forwarded_vm_call(ctx, element, |ctx| {
417                frame_action::dispatch(ctx, key, &params)
418            })
419        }
420        excall_op::OP_10 => {
421            let key = synth_form_key(form_key, selector, op);
422            let element = translated_call_element(key, tail);
423            with_forwarded_vm_call(ctx, element, |ctx| {
424                frame_action_ch::dispatch(ctx, key, &params)
425            })
426        }
427        excall_op::OP_13 => {
428            let Some(script_op) = tail.first().copied() else {
429                push_default(ctx, ret_form);
430                return Ok(true);
431            };
432            if script::dispatch_excall(ctx, script_op, &params)? {
433                Ok(true)
434            } else {
435                log::error!(
436                    "unsupported EXCALL.SCRIPT operation: selector={} op={}",
437                    selector,
438                    script_op
439                );
440                Ok(false)
441            }
442        }
443        _ => {
444            log::error!(
445                "unsupported EXCALL operation: selector={} op={}",
446                selector,
447                op
448            );
449            push_default(ctx, ret_form);
450            Ok(true)
451        }
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn excall_stage_selectors_match_original_stage_indices() {
461        // Original cmd_call.cpp:
462        //   ELM_EXCALL_FRONT -> TNM_STAGE_FRONT (1)
463        //   ELM_EXCALL_BACK  -> TNM_STAGE_BACK  (0)
464        //   ELM_EXCALL_NEXT  -> TNM_STAGE_NEXT  (2)
465        assert_eq!(excall_stage_index(excall_op::FRONT), Some(1));
466        assert_eq!(excall_stage_index(excall_op::BACK), Some(0));
467        assert_eq!(excall_stage_index(excall_op::NEXT), Some(2));
468        assert_eq!(excall_stage_index(excall_op::STAGE), None);
469    }
470}