Skip to main content

siglus_scene_vm/runtime/forms/
global.rs

1use anyhow::Result;
2
3use std::path::{Path, PathBuf};
4
5use crate::runtime::forms::codes::int_event_op;
6use crate::runtime::globals::WipeState;
7use crate::runtime::{CommandContext, Value, constants, forms};
8
9use crate::runtime::forms::{
10    cgtable, counter, database, editbox, file, frame_action, frame_action_ch, g00buf, input,
11    int_event, int_list, joypad, key, keylist, mask, math, mouse, object_event, script, stage,
12    steam, str_list, syscom, system, timewait,
13};
14
15fn normal_stage_form_id(ctx: &CommandContext) -> u32 {
16    if ctx.ids.form_global_stage != 0 {
17        ctx.ids.form_global_stage
18    } else {
19        crate::runtime::forms::codes::FORM_GLOBAL_STAGE
20    }
21}
22
23fn canonical_global_form_id(ctx: &CommandContext, form_id: u32) -> u32 {
24    let ids = &ctx.ids;
25    if constants::is_stage_global_form(form_id, ids.form_global_stage) {
26        return constants::global_form::STAGE_ALT;
27    }
28    if constants::matches_form_id(form_id, ids.form_global_mov, constants::global_form::MOV) {
29        return constants::global_form::MOV;
30    }
31    if constants::matches_form_id(form_id, ids.form_global_bgm, constants::global_form::BGM) {
32        return constants::global_form::BGM;
33    }
34    if constants::matches_form_id(
35        form_id,
36        ids.form_global_bgm_table,
37        constants::global_form::BGMTABLE,
38    ) {
39        return constants::global_form::BGMTABLE;
40    }
41    if constants::matches_form_id(form_id, ids.form_global_pcm, constants::global_form::PCM) {
42        return constants::global_form::PCM;
43    }
44    if constants::matches_form_id(
45        form_id,
46        ids.form_global_pcmch,
47        constants::global_form::PCMCH,
48    ) {
49        return constants::global_form::PCMCH;
50    }
51    if constants::matches_form_id(form_id, ids.form_global_se, constants::global_form::SE) {
52        return constants::global_form::SE;
53    }
54    if constants::matches_form_id(
55        form_id,
56        ids.form_global_pcm_event,
57        constants::global_form::PCMEVENT,
58    ) {
59        return constants::global_form::PCMEVENT;
60    }
61    if constants::matches_form_id(
62        form_id,
63        ids.form_global_excall,
64        constants::global_form::EXCALL,
65    ) {
66        return constants::global_form::EXCALL;
67    }
68    if constants::matches_form_id(
69        form_id,
70        ids.form_global_screen,
71        constants::global_form::SCREEN,
72    ) {
73        return constants::global_form::SCREEN;
74    }
75    if constants::matches_form_id(
76        form_id,
77        ids.form_global_msgbk,
78        constants::global_form::MSGBK,
79    ) {
80        return constants::global_form::MSGBK;
81    }
82    if constants::matches_form_id(
83        form_id,
84        ids.form_global_koe_st,
85        constants::global_form::KOE_ST,
86    ) {
87        return constants::global_form::KOE_ST;
88    }
89    if constants::matches_form_id(form_id, ids.form_global_key, constants::global_form::KEY) {
90        return constants::global_form::KEY;
91    }
92    if constants::matches_form_id(
93        form_id,
94        ids.form_global_frame_action,
95        constants::global_form::FRAME_ACTION,
96    ) {
97        return constants::global_form::FRAME_ACTION;
98    }
99    if form_id == constants::global_form::TIMEWAIT {
100        return constants::global_form::TIMEWAIT;
101    }
102    if form_id == constants::global_form::TIMEWAIT_KEY {
103        return constants::global_form::TIMEWAIT_KEY;
104    }
105    if form_id == constants::global_form::COUNTER {
106        return constants::global_form::COUNTER;
107    }
108    form_id
109}
110
111fn named_i64(args: &[Value], id: i32) -> Option<i64> {
112    args.iter().find_map(|v| match v {
113        Value::NamedArg { id: got, value } if *got == id => value.as_i64(),
114        _ => None,
115    })
116}
117
118fn positional_i64(args: &[Value], idx: usize) -> Option<i64> {
119    args.iter()
120        .filter(|v| !matches!(v, Value::NamedArg { .. }))
121        .filter_map(Value::as_i64)
122        .nth(idx)
123}
124
125fn global_stage_alias_to_index(form_id: i32) -> Option<i64> {
126    let form_id = form_id as u32;
127    if form_id == constants::global_form::BACK {
128        Some(0)
129    } else if form_id == constants::global_form::FRONT {
130        Some(1)
131    } else if form_id == constants::global_form::NEXT {
132        Some(2)
133    } else {
134        None
135    }
136}
137
138fn is_element_array(code: i32) -> bool {
139    code == forms::codes::ELM_ARRAY || code == -1
140}
141
142fn mwnd_ref_from_element(chain: &[i32]) -> Option<(i64, usize)> {
143    if chain.len() >= 4
144        && let Some(stage) = chain
145            .first()
146            .and_then(|head| global_stage_alias_to_index(*head))
147        && chain[1] == forms::codes::ELM_STAGE_MWND
148        && is_element_array(chain[2])
149        && chain[3] >= 0
150    {
151        return Some((stage, chain[3] as usize));
152    }
153
154    // GLOBAL.STAGE[stage].MWND[index].  Do not scan for the first ELM_ARRAY:
155    // the stage index and MWND index are independent array dimensions.
156    if chain.len() >= 6
157        && chain[0] == forms::codes::ELM_GLOBAL_STAGE
158        && is_element_array(chain[1])
159        && chain[2] >= 0
160        && chain[3] == forms::codes::ELM_STAGE_MWND
161        && is_element_array(chain[4])
162        && chain[5] >= 0
163    {
164        return Some((chain[2] as i64, chain[5] as usize));
165    }
166
167    None
168}
169
170fn front_mwnd_element(no: usize) -> Vec<i32> {
171    vec![
172        forms::codes::ELM_GLOBAL_FRONT,
173        forms::codes::ELM_STAGE_MWND,
174        forms::codes::ELM_ARRAY,
175        no as i32,
176    ]
177}
178
179fn mwnd_ref_from_value(v: &Value) -> Option<(i64, usize)> {
180    match v.unwrap_named() {
181        Value::Int(n @ 0..) => Some((1, *n as usize)),
182        Value::Element(chain) => mwnd_ref_from_element(chain),
183        _ => None,
184    }
185}
186
187fn mwnd_no_from_value(v: &Value) -> Option<usize> {
188    mwnd_ref_from_value(v).map(|(_, no)| no)
189}
190
191const TNM_STAGE_FRONT_SELBTN: i64 = 1;
192const TNM_SEL_ITEM_TYPE_OFF: i64 = 0;
193const TNM_SEL_ITEM_TYPE_ON: i64 = 1;
194const TNM_SEL_ITEM_TYPE_READ: i64 = 2;
195
196fn parse_selbtn_choices(
197    args: &[Value],
198) -> (i64, Vec<crate::runtime::globals::BtnSelectChoiceState>) {
199    let mut template_no = 0i64;
200    let mut start = 0usize;
201    if args
202        .first()
203        .is_some_and(|v| v.named_id().is_none() && v.as_i64().is_some())
204    {
205        template_no = args.first().and_then(Value::as_i64).unwrap_or(0);
206        start = 1;
207    }
208
209    let mut out = Vec::new();
210    let mut last: Option<usize> = None;
211    let mut arg_no = 0i32;
212    for v in args
213        .iter()
214        .skip(start)
215        .filter(|v| v.named_id().is_none())
216        .map(Value::unwrap_named)
217    {
218        if let Some(s) = v.as_str() {
219            out.push(crate::runtime::globals::BtnSelectChoiceState {
220                text: s.to_string(),
221                item_type: TNM_SEL_ITEM_TYPE_ON,
222                color: -1,
223                pos: (0, 0),
224                size: (0, 0),
225                ..Default::default()
226            });
227            last = Some(out.len() - 1);
228            arg_no = 0;
229        } else if let Some(n) = v.as_i64()
230            && let Some(i) = last
231        {
232            match arg_no {
233                1 => out[i].item_type = n,
234                2 => out[i].color = n,
235                _ => {}
236            }
237        }
238        arg_no += 1;
239    }
240    (template_no, out)
241}
242
243fn selbtn_text_extent(
244    text: &str,
245    tmpl: &crate::runtime::tables::SelBtnTemplate,
246    vertical: bool,
247) -> (i64, i64) {
248    let font_px = tmpl.moji_size.max(1);
249    let mut advance_sum = 0i64;
250    for ch in text.chars() {
251        let advance = if ch.is_ascii() || matches!(ch as u32, 0xFF61..=0xFF9F) {
252            (font_px + tmpl.moji_space.0) / 2
253        } else {
254            font_px + tmpl.moji_space.0
255        };
256        advance_sum = advance_sum.saturating_add(advance.max(1));
257    }
258    if !text.is_empty() {
259        advance_sum = advance_sum.saturating_sub(tmpl.moji_space.0);
260    }
261    if vertical {
262        (font_px, advance_sum.max(font_px))
263    } else {
264        (advance_sum.max(font_px), font_px)
265    }
266}
267
268fn load_selbtn_image_id(
269    ctx: &mut CommandContext,
270    file_name: &str,
271    patno: u32,
272) -> Option<crate::image_manager::ImageHandle> {
273    if file_name.is_empty() {
274        return None;
275    }
276    match ctx.images.load_g00(file_name, patno) {
277        Ok(id) => Some(id),
278        Err(_) => ctx.images.load_bg_frame(file_name, patno as usize).ok(),
279    }
280}
281
282fn selbtn_template_item_size(
283    ctx: &mut CommandContext,
284    choices: &[crate::runtime::globals::BtnSelectChoiceState],
285    tmpl: &crate::runtime::tables::SelBtnTemplate,
286) -> (i64, i64) {
287    if let Some(img_id) = load_selbtn_image_id(ctx, &tmpl.base_file, 0)
288        && let Some(img) = ctx.images.get(&img_id)
289    {
290        return (img.width as i64, img.height as i64);
291    }
292    choices
293        .first()
294        .map(|choice| {
295            let (tw, th) =
296                selbtn_text_extent(&choice.text, tmpl, ctx.tables.mwnd_render.vertical_writing);
297            (
298                tw.saturating_add(tmpl.moji_pos.0.max(0)).max(1),
299                th.saturating_add(tmpl.moji_pos.1.max(0)).max(1),
300            )
301        })
302        .unwrap_or((tmpl.moji_size.max(1), tmpl.moji_size.max(1)))
303}
304
305fn selbtn_loaded_item_size(
306    ctx: &mut CommandContext,
307    choice: &crate::runtime::globals::BtnSelectChoiceState,
308    fallback: (i64, i64),
309) -> (i64, i64) {
310    if let Some(img_id) = load_selbtn_image_id(ctx, &choice.base_file, 0)
311        && let Some(img) = ctx.images.get(&img_id)
312    {
313        return (img.width as i64, img.height as i64);
314    }
315
316    let mut max_x = 0i64;
317    let mut max_y = 0i64;
318    for glyph in &choice.glyphs {
319        if glyph.appeared {
320            max_x = max_x.max(glyph.x.saturating_add(glyph.size.max(1)));
321            max_y = max_y.max(glyph.y.saturating_add(glyph.size.max(1)));
322        }
323    }
324    if max_x > 0 || max_y > 0 {
325        (max_x.max(1), max_y.max(1))
326    } else {
327        fallback
328    }
329}
330
331fn layout_selbtn_choices(
332    choices: &mut [crate::runtime::globals::BtnSelectChoiceState],
333    tmpl: &crate::runtime::tables::SelBtnTemplate,
334    item_size: (i64, i64),
335) {
336    let rep_pos = if tmpl.rep_pos == (0, 0) {
337        (0, item_size.1.max(tmpl.moji_size.max(1)).max(1))
338    } else {
339        tmpl.rep_pos
340    };
341
342    let mut offset = (0i64, 0i64);
343    let mut max_offset = (0i64, 0i64);
344    let mut org_offset_x = 0i64;
345    let mut y_cnt = 0i64;
346    for choice in choices.iter_mut() {
347        if choice.item_type != TNM_SEL_ITEM_TYPE_OFF {
348            choice.pos = offset;
349            choice.size = item_size;
350            offset.0 = offset.0.saturating_add(rep_pos.0);
351            offset.1 = offset.1.saturating_add(rep_pos.1);
352            max_offset.0 = max_offset.0.max(offset.0);
353            max_offset.1 = max_offset.1.max(offset.1);
354            y_cnt += 1;
355            if tmpl.max_y_cnt > 0 && y_cnt >= tmpl.max_y_cnt {
356                offset.0 = org_offset_x.saturating_add(tmpl.line_width);
357                offset.1 = 0;
358                org_offset_x = offset.0;
359                y_cnt = 0;
360            }
361        }
362    }
363
364    let total_x = max_offset
365        .0
366        .saturating_sub(rep_pos.0)
367        .saturating_add(item_size.0);
368    let total_y = max_offset
369        .1
370        .saturating_sub(rep_pos.1)
371        .saturating_add(item_size.1);
372    let align_x = match tmpl.x_align {
373        1 => -total_x / 2,
374        2 => -total_x,
375        _ => 0,
376    };
377    let align_y = match tmpl.y_align {
378        1 => -total_y / 2,
379        2 => -total_y,
380        _ => 0,
381    };
382    for choice in choices.iter_mut() {
383        if choice.item_type != TNM_SEL_ITEM_TYPE_OFF {
384            choice.pos.0 = choice
385                .pos
386                .0
387                .saturating_add(align_x)
388                .saturating_add(tmpl.base_pos.0);
389            choice.pos.1 = choice
390                .pos
391                .1
392                .saturating_add(align_y)
393                .saturating_add(tmpl.base_pos.1);
394        }
395    }
396}
397
398fn selbtn_table_color(
399    tables: &crate::runtime::tables::AssetTables,
400    color_no: i64,
401    fallback: (u8, u8, u8),
402) -> (u8, u8, u8) {
403    if color_no < 0 {
404        return fallback;
405    }
406    tables
407        .color_table
408        .get(color_no as usize)
409        .copied()
410        .unwrap_or(fallback)
411}
412
413fn hide_selbtn_object_backing(
414    ctx: &mut CommandContext,
415    obj: &crate::runtime::globals::ObjectState,
416) {
417    match obj.backend {
418        crate::runtime::globals::ObjectBackend::String {
419            layer_id,
420            shadow_sprite_id,
421            fuchi_sprite_id,
422            sprite_id,
423            ref glyphs,
424            ..
425        } => {
426            if let Some(layer) = ctx.layers.layer_mut(layer_id) {
427                if glyphs.is_empty() {
428                    for sid in [shadow_sprite_id, fuchi_sprite_id, sprite_id] {
429                        if let Some(sprite) = layer.sprite_mut(sid) {
430                            sprite.visible = false;
431                            sprite.image_id = None;
432                        }
433                    }
434                } else {
435                    for glyph in glyphs {
436                        for sid in [
437                            glyph.shadow_sprite_id,
438                            glyph.fuchi_sprite_id,
439                            glyph.body_sprite_id,
440                        ] {
441                            if let Some(sprite) = layer.sprite_mut(sid) {
442                                sprite.visible = false;
443                                sprite.image_id = None;
444                            }
445                        }
446                    }
447                }
448            }
449        }
450        crate::runtime::globals::ObjectBackend::Rect {
451            layer_id,
452            sprite_id,
453            ..
454        }
455        | crate::runtime::globals::ObjectBackend::Movie {
456            layer_id,
457            sprite_id,
458            ..
459        } => {
460            if let Some(layer) = ctx.layers.layer_mut(layer_id)
461                && let Some(sprite) = layer.sprite_mut(sprite_id)
462            {
463                sprite.visible = false;
464                sprite.image_id = None;
465            }
466        }
467        crate::runtime::globals::ObjectBackend::Number {
468            layer_id,
469            ref sprite_ids,
470        }
471        | crate::runtime::globals::ObjectBackend::Weather {
472            layer_id,
473            ref sprite_ids,
474        } => {
475            if let Some(layer) = ctx.layers.layer_mut(layer_id) {
476                for &sprite_id in sprite_ids {
477                    if let Some(sprite) = layer.sprite_mut(sprite_id) {
478                        sprite.visible = false;
479                        sprite.image_id = None;
480                    }
481                }
482            }
483        }
484        _ => {}
485    }
486    for child in &obj.runtime.child_objects {
487        hide_selbtn_object_backing(ctx, child);
488    }
489}
490
491fn clear_existing_stage_btnselitems(ctx: &mut CommandContext, stage_idx: i64) {
492    let old_items = {
493        let Some(st) = ctx.globals.stage_forms.get(&normal_stage_form_id(ctx)) else {
494            return;
495        };
496        st.btnselitem_lists
497            .get(&stage_idx)
498            .cloned()
499            .unwrap_or_default()
500    };
501    for item in &old_items {
502        for obj in item.generated_objects.iter().chain(item.object_list.iter()) {
503            hide_selbtn_object_backing(ctx, obj);
504        }
505    }
506}
507
508fn make_selbtn_image_object(
509    ctx: &mut CommandContext,
510    file_name: &str,
511    patno: u32,
512    width: i64,
513    height: i64,
514    layer_rep: i64,
515    item_type: i64,
516    _selected: bool,
517) -> Option<crate::runtime::globals::ObjectState> {
518    let img_id = load_selbtn_image_id(ctx, file_name, patno)?;
519    let (img_w, img_h) = ctx
520        .images
521        .get(&img_id)
522        .map(|img| (img.width.max(1), img.height.max(1)))
523        .unwrap_or((width.max(1) as u32, height.max(1) as u32));
524    let layer_id = ctx.layers.create_layer();
525    let sprite_id = ctx
526        .layers
527        .layer_mut(layer_id)
528        .map(|layer| layer.create_sprite())?;
529    if let Some(layer) = ctx.layers.layer_mut(layer_id)
530        && let Some(sprite) = layer.sprite_mut(sprite_id)
531    {
532        sprite.fit = crate::layer::SpriteFit::PixelRect;
533        sprite.size_mode = crate::layer::SpriteSizeMode::Intrinsic;
534        sprite.visible = item_type == TNM_SEL_ITEM_TYPE_ON || item_type == TNM_SEL_ITEM_TYPE_READ;
535        sprite.x = 0;
536        sprite.y = 0;
537        sprite.image_id = Some(img_id);
538        sprite.tr = 255;
539    }
540
541    let mut obj = crate::runtime::globals::ObjectState {
542        used: true,
543        backend: crate::runtime::globals::ObjectBackend::Rect {
544            layer_id,
545            sprite_id,
546            width: img_w,
547            height: img_h,
548        },
549        object_type: 2,
550        file_name: Some(file_name.to_string()),
551        ..Default::default()
552    };
553
554    obj.base.disp = if item_type == TNM_SEL_ITEM_TYPE_ON || item_type == TNM_SEL_ITEM_TYPE_READ {
555        1
556    } else {
557        0
558    };
559    obj.base.x = 0;
560    obj.base.y = 0;
561    obj.base.patno = patno as i64;
562    obj.base.layer = layer_rep;
563    if ctx.ids.obj_disp != 0 {
564        obj.set_int_prop(&ctx.ids, ctx.ids.obj_disp, obj.base.disp);
565    }
566    if ctx.ids.obj_x != 0 {
567        obj.set_int_prop(&ctx.ids, ctx.ids.obj_x, 0);
568    }
569    if ctx.ids.obj_y != 0 {
570        obj.set_int_prop(&ctx.ids, ctx.ids.obj_y, 0);
571    }
572    if ctx.ids.obj_patno != 0 {
573        obj.set_int_prop(&ctx.ids, ctx.ids.obj_patno, patno as i64);
574    }
575    if ctx.ids.obj_layer != 0 {
576        obj.set_int_prop(&ctx.ids, ctx.ids.obj_layer, layer_rep);
577    }
578
579    Some(obj)
580}
581
582fn make_selbtn_text_object(
583    ctx: &mut CommandContext,
584    choice: &crate::runtime::globals::BtnSelectChoiceState,
585    tmpl: &crate::runtime::tables::SelBtnTemplate,
586    color_no: i64,
587    _selected: bool,
588) -> Option<crate::runtime::globals::ObjectState> {
589    if !(choice.item_type == TNM_SEL_ITEM_TYPE_ON || choice.item_type == TNM_SEL_ITEM_TYPE_READ)
590        || (choice.text.is_empty() && choice.glyphs.is_empty())
591    {
592        return None;
593    }
594
595    let font_name = ctx.effective_font_name().to_string();
596    let _ = ctx
597        .font_cache
598        .load_for_project_named(&ctx.project_dir, &font_name);
599
600    // An original save contains C_elm_mwnd_moji entries with their final
601    // item-local position and size.  Reusing those values is important: the
602    // source text may already have expanded name tokens and may contain mixed
603    // half/full-width glyphs whose layout cannot be reconstructed from the
604    // saved string alone.  Fresh script selections still use the template
605    // layout path below.
606    let loaded_glyphs = !choice.glyphs.is_empty();
607    let mut placements: Vec<(usize, char, i64, i64, i64)> = Vec::new();
608    let (text_x, text_y, nominal_width, nominal_height) = if loaded_glyphs {
609        placements.extend(
610            choice
611                .glyphs
612                .iter()
613                .enumerate()
614                .filter(|(_, glyph)| glyph.moji_type == 0 && glyph.appeared)
615                .map(|(index, glyph)| (index, glyph.ch, glyph.x, glyph.y, glyph.size.max(1))),
616        );
617        (0, 0, 1, 1)
618    } else {
619        let font_px = tmpl.moji_size.max(1);
620        let mut cursor_x = 0i64;
621        for (index, ch) in choice.text.chars().enumerate() {
622            placements.push((index, ch, cursor_x, 0, font_px));
623            let advance = if crate::text_render::is_hankaku(ch) {
624                (font_px + tmpl.moji_space.0) / 2
625            } else {
626                font_px + tmpl.moji_space.0
627            };
628            cursor_x = cursor_x.saturating_add(advance.max(1));
629        }
630        let total_x = cursor_x.saturating_sub(tmpl.moji_space.0).max(font_px);
631        let total_y = font_px;
632        let align_x = match tmpl.moji_x_align {
633            1 => -total_x / 2,
634            2 => -total_x,
635            _ => 0,
636        };
637        let align_y = match tmpl.moji_y_align {
638            1 => -total_y / 2,
639            2 => -total_y,
640            _ => 0,
641        };
642        (
643            tmpl.moji_pos.0.saturating_add(align_x),
644            tmpl.moji_pos.1.saturating_add(align_y),
645            total_x,
646            total_y,
647        )
648    };
649
650    if placements.is_empty() {
651        return None;
652    }
653
654    let effective_color_no = color_no;
655    let color = selbtn_table_color(&ctx.tables, effective_color_no, (255, 255, 255));
656    let shadow_color =
657        selbtn_table_color(&ctx.tables, ctx.tables.mwnd_render.shadow_color, (0, 0, 0));
658    let fuchi_color =
659        selbtn_table_color(&ctx.tables, ctx.tables.mwnd_render.fuchi_color, (0, 0, 0));
660    let shadow_mode = ctx.effective_font_shadow_mode();
661    let (shadow, fuchi) = crate::text_render::font_shadow_mode_flags(shadow_mode);
662    let style = crate::text_render::TextStyle {
663        color,
664        shadow_color,
665        fuchi_color,
666        shadow_mode,
667        shadow,
668        fuchi,
669        bold: ctx.effective_font_bold(),
670    };
671
672    let layer_id = ctx.layers.create_layer();
673    let mut glyphs = Vec::with_capacity(placements.len());
674    let mut bounds_min_x = i64::MAX;
675    let mut bounds_min_y = i64::MAX;
676    let mut bounds_max_x = i64::MIN;
677    let mut bounds_max_y = i64::MIN;
678    for (glyph_index, ch, glyph_x, glyph_y, glyph_size) in placements {
679        let glyph = crate::text_render::PositionedTextGlyph {
680            ch,
681            x: 0,
682            y: 0,
683            size: glyph_size as f32,
684            vertical: ctx.tables.mwnd_render.vertical_writing,
685            style,
686        };
687        let shadow_render = if style.shadow {
688            ctx.font_cache.render_single_glyph_layer_into(
689                &mut ctx.images,
690                None,
691                glyph,
692                crate::text_render::TextSpriteLayer::Shadow,
693            )
694        } else {
695            None
696        };
697        let fuchi_render = if style.fuchi {
698            ctx.font_cache.render_single_glyph_layer_into(
699                &mut ctx.images,
700                None,
701                glyph,
702                crate::text_render::TextSpriteLayer::Fuchi,
703            )
704        } else {
705            None
706        };
707        let body_render = ctx.font_cache.render_single_glyph_layer_into(
708            &mut ctx.images,
709            None,
710            glyph,
711            crate::text_render::TextSpriteLayer::Body,
712        );
713
714        let (shadow_sprite_id, fuchi_sprite_id, body_sprite_id) = {
715            let layer = ctx.layers.layer_mut(layer_id)?;
716            (
717                layer.create_sprite(),
718                layer.create_sprite(),
719                layer.create_sprite(),
720            )
721        };
722
723        let shadow_local_x =
724            glyph_x.saturating_add(shadow_render.as_ref().map_or(0, |r| r.offset_x as i64));
725        let shadow_local_y =
726            glyph_y.saturating_add(shadow_render.as_ref().map_or(0, |r| r.offset_y as i64));
727        let fuchi_local_x =
728            glyph_x.saturating_add(fuchi_render.as_ref().map_or(0, |r| r.offset_x as i64));
729        let fuchi_local_y =
730            glyph_y.saturating_add(fuchi_render.as_ref().map_or(0, |r| r.offset_y as i64));
731        let body_local_x =
732            glyph_x.saturating_add(body_render.as_ref().map_or(0, |r| r.offset_x as i64));
733        let body_local_y =
734            glyph_y.saturating_add(body_render.as_ref().map_or(0, |r| r.offset_y as i64));
735
736        for (render, local_x, local_y) in [
737            (shadow_render.as_ref(), shadow_local_x, shadow_local_y),
738            (fuchi_render.as_ref(), fuchi_local_x, fuchi_local_y),
739            (body_render.as_ref(), body_local_x, body_local_y),
740        ] {
741            if let Some(render) = render
742                && let Some(image) = ctx.images.get(&render.image)
743            {
744                bounds_min_x = bounds_min_x.min(local_x);
745                bounds_min_y = bounds_min_y.min(local_y);
746                bounds_max_x = bounds_max_x.max(local_x.saturating_add(image.width as i64));
747                bounds_max_y = bounds_max_y.max(local_y.saturating_add(image.height as i64));
748            }
749        }
750
751        if let Some(layer) = ctx.layers.layer_mut(layer_id) {
752            for (sid, render, local_x, local_y) in [
753                (
754                    shadow_sprite_id,
755                    shadow_render.as_ref(),
756                    shadow_local_x,
757                    shadow_local_y,
758                ),
759                (
760                    fuchi_sprite_id,
761                    fuchi_render.as_ref(),
762                    fuchi_local_x,
763                    fuchi_local_y,
764                ),
765                (
766                    body_sprite_id,
767                    body_render.as_ref(),
768                    body_local_x,
769                    body_local_y,
770                ),
771            ] {
772                if let Some(sprite) = layer.sprite_mut(sid) {
773                    sprite.fit = crate::layer::SpriteFit::PixelRect;
774                    sprite.size_mode = crate::layer::SpriteSizeMode::Intrinsic;
775                    sprite.visible = render.is_some();
776                    sprite.x = text_x
777                        .saturating_add(local_x)
778                        .clamp(i32::MIN as i64, i32::MAX as i64)
779                        as i32;
780                    sprite.y = text_y
781                        .saturating_add(local_y)
782                        .clamp(i32::MIN as i64, i32::MAX as i64)
783                        as i32;
784                    sprite.image_id = render.map(|r| r.image.clone());
785                    sprite.tr = 255;
786                }
787            }
788        }
789
790        glyphs.push(crate::runtime::globals::StringGlyphBackend {
791            glyph_index,
792            shadow_local_x: shadow_local_x.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
793            shadow_local_y: shadow_local_y.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
794            fuchi_local_x: fuchi_local_x.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
795            fuchi_local_y: fuchi_local_y.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
796            body_local_x: body_local_x.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
797            body_local_y: body_local_y.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
798            shadow_sprite_id,
799            fuchi_sprite_id,
800            body_sprite_id,
801            shadow_image_id: shadow_render.map(|r| r.image),
802            fuchi_image_id: fuchi_render.map(|r| r.image),
803            body_image_id: body_render.map(|r| r.image),
804        });
805    }
806
807    let first = glyphs.first()?;
808    let width = if bounds_min_x <= bounds_max_x {
809        bounds_max_x.saturating_sub(bounds_min_x).max(1)
810    } else {
811        nominal_width.max(1)
812    }
813    .min(u32::MAX as i64) as u32;
814    let height = if bounds_min_y <= bounds_max_y {
815        bounds_max_y.saturating_sub(bounds_min_y).max(1)
816    } else {
817        nominal_height.max(1)
818    }
819    .min(u32::MAX as i64) as u32;
820    let mut obj = crate::runtime::globals::ObjectState {
821        used: true,
822        backend: crate::runtime::globals::ObjectBackend::String {
823            layer_id,
824            shadow_sprite_id: first.shadow_sprite_id,
825            fuchi_sprite_id: first.fuchi_sprite_id,
826            sprite_id: first.body_sprite_id,
827            shadow_image_id: first.shadow_image_id.clone(),
828            fuchi_image_id: first.fuchi_image_id.clone(),
829            image_id: first.body_image_id.clone(),
830            glyphs,
831            mwnd_layer_reps: true,
832            width,
833            height,
834        },
835        object_type: 3,
836        string_value: Some(choice.text.clone()),
837        ..Default::default()
838    };
839
840    obj.string_param.moji_size = tmpl.moji_size;
841    obj.string_param.moji_space_x = tmpl.moji_space.0;
842    obj.string_param.moji_space_y = tmpl.moji_space.1;
843    obj.string_param.moji_cnt = tmpl.moji_cnt;
844    obj.string_param.moji_color = effective_color_no;
845    obj.string_param.shadow_color = ctx.tables.mwnd_render.shadow_color;
846    obj.string_param.fuchi_color = ctx.tables.mwnd_render.fuchi_color;
847    obj.base.disp = 1;
848    obj.base.x = text_x;
849    obj.base.y = text_y;
850    obj.base.layer = 0;
851    if ctx.ids.obj_disp != 0 {
852        obj.set_int_prop(&ctx.ids, ctx.ids.obj_disp, 1);
853    }
854    if ctx.ids.obj_x != 0 {
855        obj.set_int_prop(&ctx.ids, ctx.ids.obj_x, text_x);
856    }
857    if ctx.ids.obj_y != 0 {
858        obj.set_int_prop(&ctx.ids, ctx.ids.obj_y, text_y);
859    }
860    if ctx.ids.obj_layer != 0 {
861        obj.set_int_prop(&ctx.ids, ctx.ids.obj_layer, 0);
862    }
863    Some(obj)
864}
865
866fn prepare_stage_btnselitems_from_state(
867    ctx: &mut CommandContext,
868    stage_idx: i64,
869    state: &mut crate::runtime::globals::BtnSelectRuntimeState,
870) {
871    clear_existing_stage_btnselitems(ctx, stage_idx);
872    let template_no = state.template_no.max(0) as usize;
873    let tmpl = ctx
874        .tables
875        .sel_btn_templates
876        .get(template_no)
877        .cloned()
878        .unwrap_or_default();
879    let choices_for_size = state.choices.clone();
880    let item_size = selbtn_template_item_size(ctx, &choices_for_size, &tmpl);
881    if state.saved_cur_param.is_some() {
882        // C_elm_btn_select_item::load restores m_pos directly.  Its transient
883        // m_item_size is not serialized.  Prefer the item's saved base file,
884        // then fall back to its restored glyph bounds/template size.
885        let loaded_choices = state.choices.clone();
886        let loaded_sizes: Vec<(i64, i64)> = loaded_choices
887            .iter()
888            .map(|choice| selbtn_loaded_item_size(ctx, choice, item_size))
889            .collect();
890        for (choice, loaded_size) in state.choices.iter_mut().zip(loaded_sizes) {
891            if choice.size == (0, 0) {
892                choice.size = loaded_size;
893            }
894        }
895    } else {
896        layout_selbtn_choices(&mut state.choices, &tmpl, item_size);
897    }
898    let choices_snapshot = state.choices.clone();
899    let cursor = state.cursor;
900    // C_elm_btn_select::frame draws only while the selection is appearing or
901    // while a close animation is active.  Decide animation runs while
902    // m_appear_flag remains set and therefore needs no independent clause.
903    let show_items = state.appear_flag || state.close_anime_type > 0;
904    let waku_layer_rep = ctx.tables.mwnd_render.waku_layer_rep;
905    let filter_layer_rep = ctx.tables.mwnd_render.filter_layer_rep;
906    let mut prepared = Vec::with_capacity(choices_snapshot.len());
907    for (idx, choice) in choices_snapshot.iter().enumerate() {
908        let mut item = crate::runtime::globals::BtnSelItemState::default();
909        item.text = choice.text.clone();
910        item.item_type = choice.item_type;
911        item.color = if choice.color >= 0 {
912            choice.color
913        } else {
914            tmpl.moji_color
915        };
916        item.pos = choice.pos;
917        item.size = choice.size;
918        item.visible = show_items
919            && (choice.item_type == TNM_SEL_ITEM_TYPE_ON
920                || choice.item_type == TNM_SEL_ITEM_TYPE_READ);
921        item.selected = idx == cursor;
922        item.button_action_no = state
923            .saved_cur_param
924            .map(|param| param[20])
925            .unwrap_or(tmpl.btn_action_no);
926        item.animation_offset = (0, 0);
927        item.animation_tr = Some(255);
928        item.button_state = if choice.item_type == TNM_SEL_ITEM_TYPE_READ {
929            4
930        } else if item.selected && choice.item_type == TNM_SEL_ITEM_TYPE_ON {
931            1
932        } else {
933            0
934        };
935
936        let loaded = state.saved_cur_param.is_some();
937        let base_file = if loaded || !choice.base_file.is_empty() {
938            choice.base_file.as_str()
939        } else {
940            tmpl.base_file.as_str()
941        };
942        let filter_file = if loaded || !choice.filter_file.is_empty() {
943            choice.filter_file.as_str()
944        } else {
945            tmpl.filter_file.as_str()
946        };
947
948        // Keep fixed component slots (base, filter, text).  The visual pass
949        // indexes these slots exactly like C_elm_btn_select_item::frame; using
950        // placeholders prevents a missing base/filter file from shifting text
951        // into the wrong render path.
952        item.generated_objects.push(
953            make_selbtn_image_object(
954                ctx,
955                base_file,
956                0,
957                item.size.0,
958                item.size.1,
959                waku_layer_rep,
960                choice.item_type,
961                item.selected,
962            )
963            .unwrap_or_default(),
964        );
965        item.generated_objects.push(
966            make_selbtn_image_object(
967                ctx,
968                filter_file,
969                0,
970                item.size.0,
971                item.size.1,
972                filter_layer_rep,
973                choice.item_type,
974                item.selected,
975            )
976            .unwrap_or_default(),
977        );
978        if let Some(obj) = make_selbtn_text_object(ctx, choice, &tmpl, item.color, item.selected) {
979            item.generated_objects.push(obj);
980        }
981        prepared.push(item);
982    }
983    let st = ctx
984        .globals
985        .stage_forms
986        .entry(normal_stage_form_id(ctx))
987        .or_default();
988    st.btnselitem_lists.insert(stage_idx, prepared);
989}
990
991pub(crate) fn prepare_stage_btnselitems(ctx: &mut CommandContext) {
992    let mut state = std::mem::take(&mut ctx.globals.selbtn);
993    prepare_stage_btnselitems_from_state(ctx, TNM_STAGE_FRONT_SELBTN, &mut state);
994    ctx.globals.selbtn = state.clone();
995    ctx.globals
996        .stage_forms
997        .entry(normal_stage_form_id(ctx))
998        .or_default()
999        .btn_select_states
1000        .insert(TNM_STAGE_FRONT_SELBTN, state);
1001}
1002
1003pub(crate) fn prepare_saved_stage_btnselitems(
1004    ctx: &mut CommandContext,
1005    stage_idx: i64,
1006    mut state: crate::runtime::globals::BtnSelectRuntimeState,
1007) -> crate::runtime::globals::BtnSelectRuntimeState {
1008    prepare_stage_btnselitems_from_state(ctx, stage_idx, &mut state);
1009    ctx.globals
1010        .stage_forms
1011        .entry(normal_stage_form_id(ctx))
1012        .or_default()
1013        .btn_select_states
1014        .insert(stage_idx, state.clone());
1015    state
1016}
1017
1018fn first_selectable_selbtn_choice(
1019    choices: &[crate::runtime::globals::BtnSelectChoiceState],
1020) -> usize {
1021    choices
1022        .iter()
1023        .position(|choice| choice.item_type == TNM_SEL_ITEM_TYPE_ON)
1024        .unwrap_or(0)
1025}
1026
1027fn dispatch_selbtn_command(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
1028    let op = form_id as i32;
1029    let ready = op == constants::elm_value::GLOBAL_SELBTN_READY
1030        || op == constants::elm_value::GLOBAL_SELBTN_CANCEL_READY;
1031    let start_now = op == constants::elm_value::GLOBAL_SELBTN
1032        || op == constants::elm_value::GLOBAL_SELBTN_CANCEL
1033        || op == constants::elm_value::GLOBAL_SELBTN_START;
1034    if !ready && !start_now {
1035        return Ok(false);
1036    }
1037
1038    if op != constants::elm_value::GLOBAL_SELBTN_START {
1039        let (template_no, choices) = parse_selbtn_choices(args);
1040        let capture_flag = named_i64(args, 1).unwrap_or(0) != 0;
1041        let sel_start_call_scn = args
1042            .iter()
1043            .find(|v| v.named_id() == Some(2))
1044            .and_then(Value::as_str)
1045            .unwrap_or("")
1046            .to_string();
1047        let sel_start_call_z_no = named_i64(args, 3).unwrap_or(0);
1048        ctx.globals.selbtn.template_no = template_no;
1049        ctx.globals.selbtn.saved_cur_param = None;
1050        ctx.globals.selbtn.choices = choices;
1051        ctx.globals.selbtn.cursor = first_selectable_selbtn_choice(&ctx.globals.selbtn.choices);
1052        ctx.globals.selbtn.pressed_index = None;
1053        ctx.globals.selbtn.pressed_inside = false;
1054        ctx.globals.selbtn.cancel_enable = op == constants::elm_value::GLOBAL_SELBTN_CANCEL
1055            || op == constants::elm_value::GLOBAL_SELBTN_CANCEL_READY;
1056        ctx.globals.selbtn.capture_flag = if ready { false } else { capture_flag };
1057        ctx.globals.selbtn.sel_start_call_scn = if ready {
1058            String::new()
1059        } else {
1060            sel_start_call_scn
1061        };
1062        ctx.globals.selbtn.sel_start_call_z_no = if ready { 0 } else { sel_start_call_z_no };
1063        ctx.globals.selbtn.result = 0;
1064        prepare_stage_btnselitems(ctx);
1065    }
1066
1067    if start_now {
1068        let stage_form_id = normal_stage_form_id(ctx);
1069        let read_flag_scene_no = ctx.current_scene_no.unwrap_or(-1);
1070        let template_no = ctx.globals.selbtn.template_no.max(0) as usize;
1071        let tmpl = ctx
1072            .tables
1073            .sel_btn_templates
1074            .get(template_no)
1075            .cloned()
1076            .unwrap_or_default();
1077        let sync_type = if op == constants::elm_value::GLOBAL_SELBTN_START {
1078            0
1079        } else {
1080            // C_elm_btn_select::is_processing() recognizes only 0..=2.
1081            // An out-of-range value therefore cannot keep the original
1082            // command blocked.
1083            named_i64(args, 4).unwrap_or(0).clamp(0, 2)
1084        };
1085        {
1086            let selbtn = &mut ctx.globals.selbtn;
1087            selbtn.started = true;
1088            selbtn.appear_flag = true;
1089            selbtn.sync_type = sync_type;
1090            selbtn.open_anime_type = tmpl.open_anime_type.max(0);
1091            selbtn.open_anime_time = tmpl.open_anime_time.max(0);
1092            selbtn.open_anime_cur_time = 0;
1093            selbtn.close_anime_type = 0;
1094            selbtn.close_anime_time = tmpl.close_anime_time.max(0);
1095            selbtn.close_anime_cur_time = 0;
1096            selbtn.decide_anime_type = 0;
1097            selbtn.decide_anime_time = tmpl.decide_anime_time.max(0);
1098            selbtn.decide_anime_cur_time = 0;
1099            selbtn.decide_sel_no = -1;
1100            selbtn.pressed_index = None;
1101            selbtn.pressed_inside = false;
1102            selbtn.processing_flag_0 = true;
1103            selbtn.processing_flag_1 = true;
1104            selbtn.processing_flag_2 = true;
1105            selbtn.capture_now_flag = false;
1106            selbtn.result_delivered = false;
1107            selbtn.result = 0;
1108            selbtn.read_flag_scene_no = read_flag_scene_no;
1109            selbtn.read_flag_flag_no = -1;
1110        }
1111        if let Some(items) = ctx
1112            .globals
1113            .stage_forms
1114            .get_mut(&stage_form_id)
1115            .and_then(|stage| stage.btnselitem_lists.get_mut(&TNM_STAGE_FRONT_SELBTN))
1116        {
1117            for item in items {
1118                item.visible = item.item_type == TNM_SEL_ITEM_TYPE_ON
1119                    || item.item_type == TNM_SEL_ITEM_TYPE_READ;
1120            }
1121        }
1122        ctx.request_read_flag_no_for_selbtn();
1123        // C++ pushes TNM_PROC_TYPE_SEL_BTN here. It is released only by
1124        // C_elm_btn_select::is_processing(), not by generic key/message waits.
1125        ctx.wait.wait_selbtn();
1126    }
1127    Ok(true)
1128}
1129
1130fn global_koe_state_key(_ctx: &CommandContext) -> u32 {
1131    constants::fm::GLOBAL as u32
1132}
1133
1134pub(crate) fn remember_global_koe(
1135    ctx: &mut CommandContext,
1136    koe_no: i64,
1137    chara_no: i64,
1138    is_ex: bool,
1139) {
1140    // C_tnm_local_data_pod tracks only the voice attached to the current
1141    // message.  EXKOE updates C_elm_koe playback metadata, but must not
1142    // replace the message voice used by SYSCOM.REPLAY_KOE or local saves.
1143    ctx.globals.sound_routing.koe_chara_no = chara_no;
1144    ctx.globals.sound_routing.koe_ex_flag = is_ex;
1145    if !is_ex {
1146        ctx.globals.script.cur_koe_no = koe_no;
1147        ctx.globals.script.cur_chr_no = chara_no;
1148        ctx.globals.syscom.replay_koe = Some((koe_no, chara_no));
1149        // eng_message.cpp sets this when the current message establishes its
1150        // voice.  It is cleared when the message is finalized/cleared.
1151        ctx.globals.sound_routing.bgmfade2_need_flag = true;
1152    }
1153    let key = global_koe_state_key(ctx);
1154    let props = ctx.globals.int_props.entry(key).or_default();
1155    props.insert(constants::elm_value::GLOBAL_KOE_CHECK_GET_KOE_NO, koe_no);
1156    props.insert(
1157        constants::elm_value::GLOBAL_KOE_CHECK_GET_CHARA_NO,
1158        chara_no,
1159    );
1160    props.insert(
1161        constants::elm_value::GLOBAL_KOE_CHECK_IS_EX_KOE,
1162        if is_ex { 1 } else { 0 },
1163    );
1164}
1165
1166fn remembered_global_koe(ctx: &CommandContext, op: i32) -> i64 {
1167    let key = global_koe_state_key(ctx);
1168    ctx.globals
1169        .int_props
1170        .get(&key)
1171        .and_then(|m| m.get(&op).copied())
1172        .unwrap_or(0)
1173}
1174
1175fn dispatch_global_koe_command(
1176    ctx: &mut CommandContext,
1177    form_id: u32,
1178    args: &[Value],
1179) -> Result<bool> {
1180    let op = form_id as i32;
1181    let ret_form: Option<i64> = crate::runtime::forms::prop_access::current_vm_meta(ctx).1;
1182    match op {
1183        constants::elm_value::GLOBAL_KOE | constants::elm_value::GLOBAL_EXKOE => {
1184            let is_ex = op == constants::elm_value::GLOBAL_EXKOE;
1185            if !is_ex {
1186                ctx.request_read_flag_no();
1187            }
1188            let koe_no = if is_ex {
1189                named_i64(args, 0).or_else(|| positional_i64(args, 0))
1190            } else {
1191                positional_i64(args, 0)
1192            }
1193            .unwrap_or(0);
1194            let chara_no = if is_ex {
1195                named_i64(args, 1).or_else(|| positional_i64(args, 1))
1196            } else {
1197                positional_i64(args, 1)
1198            }
1199            .unwrap_or(-1);
1200            remember_global_koe(ctx, koe_no, chara_no, is_ex);
1201            let append_dir = ctx.globals.append_dir.clone();
1202            let jitan_rate =
1203                ctx.koe_jitan_rate(is_ex.then(|| named_i64(args, 4).unwrap_or(0) != 0), false);
1204            if let Err(err) = {
1205                let (koe, audio) = (&mut ctx.koe, &mut ctx.audio);
1206                koe.play_koe_no_with_rate(audio, koe_no, &append_dir, jitan_rate)
1207            } {
1208                eprintln!("[SG_AUDIO] koe.play failed koe_no={koe_no}: {err:#}");
1209            }
1210            let ex_wait = is_ex && named_i64(args, 2).unwrap_or(0) != 0;
1211            if ex_wait {
1212                let key_skip = named_i64(args, 3).unwrap_or(0) != 0;
1213                ctx.wait.wait_audio_with_return(
1214                    crate::runtime::wait::AudioWait::KoeAny,
1215                    key_skip,
1216                    ret_form.unwrap_or(0) != 0,
1217                );
1218            } else if ret_form.unwrap_or(0) != 0 {
1219                ctx.push(Value::Int(0));
1220            }
1221            Ok(true)
1222        }
1223        constants::elm_value::GLOBAL_KOE_PLAY_WAIT
1224        | constants::elm_value::GLOBAL_KOE_PLAY_WAIT_KEY
1225        | constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT
1226        | constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY => {
1227            let is_ex = op == constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT
1228                || op == constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY;
1229            if !is_ex {
1230                ctx.request_read_flag_no();
1231            }
1232            let koe_no = if is_ex {
1233                named_i64(args, 0).or_else(|| positional_i64(args, 0))
1234            } else {
1235                positional_i64(args, 0)
1236            }
1237            .unwrap_or(0);
1238            let chara_no = if is_ex {
1239                named_i64(args, 1).or_else(|| positional_i64(args, 1))
1240            } else {
1241                positional_i64(args, 1)
1242            }
1243            .unwrap_or(-1);
1244            remember_global_koe(ctx, koe_no, chara_no, is_ex);
1245            let append_dir = ctx.globals.append_dir.clone();
1246            let jitan_rate =
1247                ctx.koe_jitan_rate(is_ex.then(|| named_i64(args, 4).unwrap_or(0) != 0), false);
1248            if let Err(err) = {
1249                let (koe, audio) = (&mut ctx.koe, &mut ctx.audio);
1250                koe.play_koe_no_with_rate(audio, koe_no, &append_dir, jitan_rate)
1251            } {
1252                eprintln!("[SG_AUDIO] koe.play_wait failed koe_no={koe_no}: {err:#}");
1253            }
1254            let key_skip = op == constants::elm_value::GLOBAL_KOE_PLAY_WAIT_KEY
1255                || op == constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY;
1256            ctx.wait.wait_audio_with_return(
1257                crate::runtime::wait::AudioWait::KoeAny,
1258                key_skip,
1259                ret_form.unwrap_or(0) != 0,
1260            );
1261            Ok(true)
1262        }
1263        constants::elm_value::GLOBAL_KOE_STOP => {
1264            let fade = args.first().and_then(Value::as_i64);
1265            let _ = ctx.koe.stop(fade);
1266            Ok(true)
1267        }
1268        constants::elm_value::GLOBAL_KOE_WAIT | constants::elm_value::GLOBAL_KOE_WAIT_KEY => {
1269            let key_skip = op == constants::elm_value::GLOBAL_KOE_WAIT_KEY;
1270            ctx.wait.wait_audio_with_return(
1271                crate::runtime::wait::AudioWait::KoeAny,
1272                key_skip,
1273                ret_form.unwrap_or(0) != 0,
1274            );
1275            Ok(true)
1276        }
1277        constants::elm_value::GLOBAL_KOE_CHECK => {
1278            let playing = ctx.koe.is_playing_any();
1279            ctx.push(Value::Int(if playing { 1 } else { 0 }));
1280            Ok(true)
1281        }
1282        constants::elm_value::GLOBAL_KOE_CHECK_GET_KOE_NO
1283        | constants::elm_value::GLOBAL_KOE_CHECK_GET_CHARA_NO
1284        | constants::elm_value::GLOBAL_KOE_CHECK_IS_EX_KOE => {
1285            ctx.push(Value::Int(remembered_global_koe(ctx, op)));
1286            Ok(true)
1287        }
1288        constants::elm_value::GLOBAL_KOE_SET_VOLUME => {
1289            let vol = args
1290                .first()
1291                .and_then(Value::as_i64)
1292                .unwrap_or(255)
1293                .clamp(0, 255) as u8;
1294            let fade = args.get(1).and_then(Value::as_i64).unwrap_or(0);
1295            let _ = ctx.koe.set_volume_raw_fade(&mut ctx.audio, vol, fade);
1296            Ok(true)
1297        }
1298        constants::elm_value::GLOBAL_KOE_SET_VOLUME_MAX => {
1299            let fade = args.first().and_then(Value::as_i64).unwrap_or(0);
1300            let _ = ctx.koe.set_volume_raw_fade(&mut ctx.audio, 255, fade);
1301            Ok(true)
1302        }
1303        constants::elm_value::GLOBAL_KOE_SET_VOLUME_MIN => {
1304            let fade = args.first().and_then(Value::as_i64).unwrap_or(0);
1305            let _ = ctx.koe.set_volume_raw_fade(&mut ctx.audio, 0, fade);
1306            Ok(true)
1307        }
1308        constants::elm_value::GLOBAL_KOE_GET_VOLUME => {
1309            ctx.push(Value::Int(ctx.koe.volume_raw() as i64));
1310            Ok(true)
1311        }
1312        _ => Ok(false),
1313    }
1314}
1315
1316fn parse_i32_value(v: &Value) -> Option<i32> {
1317    v.unwrap_named()
1318        .as_i64()
1319        .and_then(|n| i32::try_from(n).ok())
1320}
1321
1322fn parse_bool_value(v: &Value) -> Option<bool> {
1323    parse_i32_value(v).map(|n| n != 0)
1324}
1325
1326fn parse_list_i32_value(v: &Value) -> Vec<i32> {
1327    match v.unwrap_named() {
1328        Value::List(xs) => xs
1329            .iter()
1330            .filter_map(|x| x.as_i64().and_then(|n| i32::try_from(n).ok()))
1331            .collect(),
1332        _ => Vec::new(),
1333    }
1334}
1335
1336fn dispatch_global_fog_command(
1337    ctx: &mut CommandContext,
1338    form_id: u32,
1339    args: &[Value],
1340) -> Result<bool> {
1341    let op = form_id as i32;
1342    let ret_form = crate::runtime::forms::prop_access::current_vm_meta(ctx)
1343        .1
1344        .unwrap_or(0);
1345
1346    if op == constants::elm_value::GLOBAL___FOG_NAME {
1347        match ret_form {
1348            rf if rf == constants::fm::STR as i64 => {
1349                ctx.push(Value::Str(ctx.globals.fog_global.name.clone()));
1350            }
1351            _ => {
1352                let name = args
1353                    .first()
1354                    .and_then(|v| v.unwrap_named().as_str())
1355                    .unwrap_or("");
1356                ctx.globals.fog_global = Default::default();
1357                if !name.is_empty() {
1358                    match ctx.images.load_g00(name, 0) {
1359                        Ok(id) => {
1360                            ctx.globals.fog_global.enabled = true;
1361                            ctx.globals.fog_global.name = name.to_string();
1362                            ctx.globals.fog_global.texture_image_id = Some(id);
1363                        }
1364                        Err(e) => {
1365                            log::error!(
1366                                "GLOBAL.__FOG_NAME failed to load fog texture '{name}': {e}"
1367                            );
1368                        }
1369                    }
1370                }
1371            }
1372        }
1373        return Ok(true);
1374    }
1375
1376    if op == constants::elm_value::GLOBAL___FOG_X {
1377        if ret_form != 0 {
1378            ctx.push(Value::Int(ctx.globals.fog_global.x_event.get_value() as i64));
1379        } else {
1380            let x = args
1381                .first()
1382                .and_then(|v| v.unwrap_named().as_i64())
1383                .unwrap_or(0) as i32;
1384            ctx.globals.fog_global.set_x(x);
1385        }
1386        return Ok(true);
1387    }
1388
1389    if op == constants::elm_value::GLOBAL___FOG_NEAR {
1390        if ret_form != 0 {
1391            ctx.push(Value::Int(ctx.globals.fog_global.near as i64));
1392        } else {
1393            ctx.globals.fog_global.near = args
1394                .first()
1395                .and_then(|v| v.unwrap_named().as_i64())
1396                .unwrap_or(0) as f32;
1397        }
1398        return Ok(true);
1399    }
1400
1401    if op == constants::elm_value::GLOBAL___FOG_FAR {
1402        if ret_form != 0 {
1403            ctx.push(Value::Int(ctx.globals.fog_global.far as i64));
1404        } else {
1405            ctx.globals.fog_global.far = args
1406                .first()
1407                .and_then(|v| v.unwrap_named().as_i64())
1408                .unwrap_or(0) as f32;
1409        }
1410        return Ok(true);
1411    }
1412
1413    if op != constants::elm_value::GLOBAL___FOG_X_EVE {
1414        return Ok(false);
1415    }
1416
1417    let Some((chain_pos, chain)) =
1418        crate::runtime::forms::prop_access::parse_element_chain_ctx(ctx, form_id, args)
1419            .map(|(i, ch)| (i, ch.to_vec()))
1420    else {
1421        return Ok(true);
1422    };
1423    if chain.len() < 2 {
1424        return Ok(true);
1425    }
1426    let params = &args[..chain_pos];
1427    match chain[1] {
1428        int_event_op::SET | int_event_op::SET_REAL => {
1429            let value = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1430            let total_time = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1431            let delay_time = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1432            let speed_type = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1433            let real_flag = if chain[1] == int_event_op::SET_REAL {
1434                1
1435            } else {
1436                0
1437            };
1438            ctx.globals
1439                .fog_global
1440                .x_event
1441                .set_event(value, total_time, delay_time, speed_type, real_flag);
1442        }
1443        int_event_op::LOOP | int_event_op::LOOP_REAL => {
1444            let start_value = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1445            let end_value = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1446            let loop_time = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1447            let delay_time = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1448            let speed_type = params.get(4).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1449            let real_flag = if chain[1] == int_event_op::LOOP_REAL {
1450                1
1451            } else {
1452                0
1453            };
1454            ctx.globals.fog_global.x_event.loop_event(
1455                start_value,
1456                end_value,
1457                loop_time,
1458                delay_time,
1459                speed_type,
1460                real_flag,
1461            );
1462        }
1463        int_event_op::TURN | int_event_op::TURN_REAL => {
1464            let start_value = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1465            let end_value = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1466            let loop_time = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1467            let delay_time = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1468            let speed_type = params.get(4).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
1469            let real_flag = if chain[1] == int_event_op::TURN_REAL {
1470                1
1471            } else {
1472                0
1473            };
1474            ctx.globals.fog_global.x_event.turn_event(
1475                start_value,
1476                end_value,
1477                loop_time,
1478                delay_time,
1479                speed_type,
1480                real_flag,
1481            );
1482        }
1483        int_event_op::END => ctx.globals.fog_global.x_event.end_event(),
1484        int_event_op::WAIT => ctx.wait.wait_fog_x_event(false, false),
1485        int_event_op::WAIT_KEY => ctx.wait.wait_fog_x_event(true, true),
1486        int_event_op::CHECK => {
1487            ctx.push(Value::Int(
1488                if ctx.globals.fog_global.x_event.check_event() {
1489                    1
1490                } else {
1491                    0
1492                },
1493            ));
1494        }
1495        _ => {}
1496    }
1497    Ok(true)
1498}
1499
1500fn resolve_wipe_mask_path(project_dir: &Path, raw: &str) -> Option<PathBuf> {
1501    if raw.is_empty() {
1502        return None;
1503    }
1504    let norm = raw.replace('\\', "/");
1505    let p = Path::new(&norm);
1506    if p.is_absolute()
1507        && let Some(path) = crate::resource::resolve_game_file(p).ok().flatten()
1508    {
1509        return Some(path);
1510    }
1511    let mut candidates = Vec::new();
1512    candidates.push(project_dir.join(&norm));
1513    candidates.push(project_dir.join("dat").join(&norm));
1514    if p.extension().is_none() {
1515        for ext in ["png", "bmp", "jpg"] {
1516            candidates.push(project_dir.join(format!("{}.{}", norm, ext)));
1517            candidates.push(project_dir.join("dat").join(format!("{}.{}", norm, ext)));
1518        }
1519    }
1520    candidates
1521        .into_iter()
1522        .find_map(|c| crate::resource::resolve_game_file(&c).ok().flatten())
1523}
1524
1525fn dispatch_global_wipe_command(
1526    ctx: &mut CommandContext,
1527    form_id: u32,
1528    args: &[Value],
1529) -> Result<bool> {
1530    let op = form_id as i32;
1531    let is_mask = matches!(
1532        op,
1533        constants::elm_value::GLOBAL_MASK_WIPE | constants::elm_value::GLOBAL_MASK_WIPE_ALL
1534    );
1535    let is_all = matches!(
1536        op,
1537        constants::elm_value::GLOBAL_WIPE_ALL | constants::elm_value::GLOBAL_MASK_WIPE_ALL
1538    );
1539
1540    if op == constants::elm_value::GLOBAL_WIPE_END {
1541        ctx.finish_wipe_runtime();
1542        return Ok(true);
1543    }
1544    if op == constants::elm_value::GLOBAL_WAIT_WIPE {
1545        let key_wait_mode = args
1546            .iter()
1547            .find_map(|v| match v {
1548                Value::NamedArg { id: 0, value } => parse_i32_value(value),
1549                _ => None,
1550            })
1551            .unwrap_or(-1);
1552        let key_skip = match key_wait_mode {
1553            0 => false,
1554            1 => true,
1555            _ => ctx.globals.syscom.original_config.skip_wipe_anime_flag,
1556        };
1557        ctx.wait.wait_wipe_with_return(key_skip, true);
1558        return Ok(true);
1559    }
1560    if op == constants::elm_value::GLOBAL_CHECK_WIPE {
1561        ctx.push(Value::Int(if ctx.globals.wipe_done() { 0 } else { 1 }));
1562        return Ok(true);
1563    }
1564
1565    if !matches!(
1566        op,
1567        constants::elm_value::GLOBAL_WIPE
1568            | constants::elm_value::GLOBAL_WIPE_ALL
1569            | constants::elm_value::GLOBAL_MASK_WIPE
1570            | constants::elm_value::GLOBAL_MASK_WIPE_ALL
1571    ) {
1572        return Ok(false);
1573    }
1574
1575    let mut positional: Vec<&Value> = Vec::new();
1576    let mut named: Vec<(i32, &Value)> = Vec::new();
1577    for a in args {
1578        match a {
1579            Value::NamedArg { id, value } => named.push((*id, value.as_ref())),
1580            _ => positional.push(a),
1581        }
1582    }
1583
1584    let mut mask_file: Option<String> = None;
1585    let mut wipe_type: i32 = 0;
1586    let mut wipe_time: i32 = 500;
1587    let mut speed_mode: i32 = 0;
1588    let mut start_time: i32 = 0;
1589    let mut option: Vec<i32> = Vec::new();
1590    let mut begin_order: i32 = 0;
1591    let mut end_order: i32 = if is_all { i32::MAX } else { 0 };
1592    let mut begin_layer: i32 = i32::MIN;
1593    let mut end_layer: i32 = i32::MAX;
1594    let mut wait_flag = true;
1595    let mut key_wait_mode: i32 = -1;
1596    let mut with_low_order: i32 = 0;
1597
1598    if is_mask {
1599        mask_file = positional
1600            .first()
1601            .and_then(|v| v.unwrap_named().as_str())
1602            .map(str::to_string);
1603        if let Some(v) = positional.get(1).and_then(|v| parse_i32_value(v)) {
1604            wipe_type = v;
1605        }
1606        if let Some(v) = positional.get(2).and_then(|v| parse_i32_value(v)) {
1607            wipe_time = v;
1608        }
1609        if let Some(v) = positional.get(3).and_then(|v| parse_i32_value(v)) {
1610            speed_mode = v;
1611        }
1612        if let Some(v) = positional.get(4) {
1613            option = parse_list_i32_value(v);
1614        }
1615    } else {
1616        if let Some(v) = positional.first().and_then(|v| parse_i32_value(v)) {
1617            wipe_type = v;
1618        }
1619        if let Some(v) = positional.get(1).and_then(|v| parse_i32_value(v)) {
1620            wipe_time = v;
1621        }
1622        if let Some(v) = positional.get(2).and_then(|v| parse_i32_value(v)) {
1623            speed_mode = v;
1624        }
1625        if let Some(v) = positional.get(3) {
1626            option = parse_list_i32_value(v);
1627        }
1628    }
1629
1630    for (id, v) in named {
1631        match id {
1632            0 => {
1633                if let Some(x) = parse_i32_value(v) {
1634                    wipe_type = x;
1635                }
1636            }
1637            1 => {
1638                if let Some(x) = parse_i32_value(v) {
1639                    wipe_time = x;
1640                }
1641            }
1642            2 => {
1643                if let Some(x) = parse_i32_value(v) {
1644                    speed_mode = x;
1645                }
1646            }
1647            3 => option = parse_list_i32_value(v),
1648            4 => {
1649                if let Some(x) = parse_i32_value(v) {
1650                    begin_order = x;
1651                }
1652            }
1653            5 => {
1654                if let Some(x) = parse_i32_value(v) {
1655                    end_order = x;
1656                }
1657            }
1658            6 => {
1659                if let Some(x) = parse_i32_value(v) {
1660                    begin_layer = x;
1661                }
1662            }
1663            7 => {
1664                if let Some(x) = parse_i32_value(v) {
1665                    end_layer = x;
1666                }
1667            }
1668            8 => {
1669                if let Some(x) = parse_bool_value(v) {
1670                    wait_flag = x;
1671                }
1672            }
1673            9 => {
1674                if let Some(x) = parse_i32_value(v) {
1675                    key_wait_mode = x;
1676                }
1677            }
1678            10 => {
1679                if let Some(x) = parse_i32_value(v) {
1680                    with_low_order = x;
1681                }
1682            }
1683            11 => {
1684                if let Some(x) = parse_i32_value(v) {
1685                    start_time = x;
1686                }
1687            }
1688            _ => {}
1689        }
1690    }
1691    if is_all {
1692        end_order = i32::MAX;
1693    }
1694
1695    // `C_tnm_wipe::start()` begins with `end()`.  A new wipe must therefore
1696    // tear down the previous wipe's NEXT stage before repopulating NEXT from
1697    // the current FRONT/BACK stages.  Overwriting `GlobalState::wipe` alone
1698    // leaves the old cloned objects and IntEvents alive.
1699    if ctx.globals.wipe.is_some() {
1700        ctx.finish_wipe_runtime();
1701    }
1702
1703    let mask_image_id = mask_file.as_ref().and_then(|f| {
1704        resolve_wipe_mask_path(&ctx.project_dir, f).and_then(|p| ctx.images.load_file(&p, 0).ok())
1705    });
1706
1707    let stage_form_id =
1708        stage::apply_stage_wipe(ctx, begin_order, end_order, begin_layer, end_layer);
1709    ctx.globals.start_wipe(WipeState::new(
1710        stage_form_id,
1711        mask_file,
1712        mask_image_id,
1713        wipe_type,
1714        wipe_time,
1715        start_time,
1716        speed_mode,
1717        option,
1718        begin_order,
1719        end_order,
1720        begin_layer,
1721        end_layer,
1722        wait_flag,
1723        key_wait_mode,
1724        with_low_order,
1725    ));
1726
1727    if wait_flag {
1728        let key_skip = match key_wait_mode {
1729            0 => false,
1730            1 => true,
1731            _ => ctx.globals.syscom.original_config.skip_wipe_anime_flag,
1732        };
1733        ctx.wait.wait_wipe(key_skip);
1734    }
1735    Ok(true)
1736}
1737
1738fn dispatch_capture_command(
1739    ctx: &mut CommandContext,
1740    form_id: u32,
1741    args: &[Value],
1742) -> Result<bool> {
1743    match form_id as i32 {
1744        constants::elm_value::GLOBAL_CAPTURE => {
1745            crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture_with_priority(
1746                ctx,
1747                crate::runtime::forms::syscom::CAPTURE_PRIOR_CAPTURE,
1748            );
1749            ctx.push(Value::Int(0));
1750            Ok(true)
1751        }
1752        constants::elm_value::GLOBAL_CAPTURE_FREE => {
1753            crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
1754                ctx,
1755                crate::runtime::forms::syscom::CAPTURE_PRIOR_CAPTURE,
1756            );
1757            ctx.push(Value::Int(0));
1758            Ok(true)
1759        }
1760        constants::elm_value::GLOBAL_CAPTURE_FROM_FILE => {
1761            let Some(file) = args.first().and_then(|v| v.as_str()) else {
1762                panic!("GLOBAL.CAPTURE_FROM_FILE requires file name");
1763            };
1764            let Some(path) =
1765                stage::resolve_capture_file_path(&ctx.project_dir, &ctx.globals.append_dir, file)
1766            else {
1767                panic!("GLOBAL.CAPTURE_FROM_FILE cannot resolve file: {file}");
1768            };
1769            let img_id = ctx.images.load_file(&path, 0).unwrap_or_else(|e| {
1770                panic!(
1771                    "GLOBAL.CAPTURE_FROM_FILE failed to load {}: {e}",
1772                    path.display()
1773                )
1774            });
1775            let img = ctx
1776                .images
1777                .get(&img_id)
1778                .map(|img| img.as_ref().clone())
1779                .unwrap_or_else(|| {
1780                    panic!(
1781                        "GLOBAL.CAPTURE_FROM_FILE image disappeared: {}",
1782                        path.display()
1783                    )
1784                });
1785            crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture_from_image(ctx, &img);
1786            ctx.push(Value::Int(0));
1787            Ok(true)
1788        }
1789        constants::elm_value::GLOBAL_CAPTURE_FOR_OBJECT => {
1790            let has_range = named_i64(args, 0).is_some() || named_i64(args, 1).is_some();
1791            let img = if has_range {
1792                let end_order = named_i64(args, 0).unwrap_or(i32::MAX as i64 / 1024);
1793                let end_layer = named_i64(args, 1).unwrap_or(1023);
1794                ctx.capture_frame_rgba_until(end_order, end_layer)?
1795            } else {
1796                ctx.capture_frame_rgba()?
1797            };
1798            ctx.globals.capture_for_object_image = Some(img);
1799            ctx.push(Value::Int(0));
1800            Ok(true)
1801        }
1802        constants::elm_value::GLOBAL_CAPTURE_FOR_OBJECT_FREE => {
1803            ctx.globals.capture_for_object_image = None;
1804            ctx.push(Value::Int(0));
1805            Ok(true)
1806        }
1807        constants::elm_value::GLOBAL_CAPTURE_FOR_LOCAL_SAVE => {
1808            let end_order = named_i64(args, 0).unwrap_or(i32::MAX as i64 / 1024);
1809            let end_layer = named_i64(args, 1).unwrap_or(1023);
1810            let width = named_i64(args, 3).unwrap_or(ctx.screen_w as i64).max(1) as u32;
1811            let height = named_i64(args, 4).unwrap_or(ctx.screen_h as i64).max(1) as u32;
1812            let img = ctx.capture_frame_rgba_until(end_order, end_layer)?;
1813            let capture_time =
1814                crate::runtime::forms::syscom::capture_for_local_save(ctx, &img, width, height);
1815            ctx.push(Value::Int(capture_time));
1816            Ok(true)
1817        }
1818        constants::elm_value::GLOBAL_CAPTURE_FOR_TWEET => {
1819            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1820            {
1821                // C++ tnm_syscom_create_capture_for_tweet() only raises
1822                // TNM_CAPTURE_TYPE_TWEET and pushes DISP.  The texture itself
1823                // is produced while that DISP is rendered, not at command
1824                // dispatch time.
1825                ctx.globals.capture_for_tweet_pending = true;
1826                ctx.request_disp_proc_boundary();
1827            }
1828            #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
1829            {
1830                log::error!("GLOBAL.CAPTURE_FOR_TWEET is not implemented on this platform");
1831            }
1832            ctx.push(Value::Int(0));
1833            Ok(true)
1834        }
1835        constants::elm_value::GLOBAL_CAPTURE_FREE_FOR_TWEET => {
1836            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1837            {
1838                ctx.globals.capture_image = None;
1839                ctx.globals.capture_for_tweet_pending = false;
1840            }
1841            #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
1842            {
1843                log::error!("GLOBAL.CAPTURE_FREE_FOR_TWEET is not implemented on this platform");
1844            }
1845            ctx.push(Value::Int(0));
1846            Ok(true)
1847        }
1848        _ => Ok(false),
1849    }
1850}
1851
1852fn push_global_message_ok(ctx: &mut CommandContext) {
1853    let ret_form = crate::runtime::forms::prop_access::current_vm_meta(ctx)
1854        .1
1855        .unwrap_or(0);
1856    if ret_form == 0 {
1857        return;
1858    }
1859    if ret_form == constants::fm::STR as i64 {
1860        ctx.push(Value::Str(String::new()));
1861    } else {
1862        ctx.push(Value::Int(0));
1863    }
1864}
1865
1866fn global_message_arg_str(args: &[Value]) -> Option<&str> {
1867    args.iter().rev().find_map(|v| v.unwrap_named().as_str())
1868}
1869
1870/// Map a full-width romaji letter (A..Z, U+FF21..U+FF3A) to 0..25.
1871/// Mirrors `get_zenkaku_alpha_no()` in the original `elm_flag.cpp`.
1872fn zenkaku_alpha_index(ch: char) -> Option<i32> {
1873    let code = ch as u32;
1874    if (0xFF21..=0xFF3A).contains(&code) {
1875        Some((code - 0xFF21) as i32)
1876    } else {
1877        None
1878    }
1879}
1880
1881/// Resolve the `GLOBAL.NAMAE` string reference (`ELM_GLOBAL_NAMAE`).
1882///
1883/// Original `C_elm_flag::get_flag_by_name()`: the first character selects the
1884/// list (* = global namae, % = local namae), followed by one or two full-width
1885/// letters (A..Z or AA..ZZ). The resulting element reference addresses the
1886/// corresponding slot of the 26 + 26 * 26 entry namae str-list.
1887fn namae_flag_target(name: &str) -> Option<(i32, i32)> {
1888    let mut chars = name.chars();
1889    let list_form = match chars.next()? {
1890        '\u{FF0A}' /* * */ => forms::codes::ELM_GLOBAL_NAMAE_GLOBAL,
1891        '\u{FF05}' /* % */ => forms::codes::ELM_GLOBAL_NAMAE_LOCAL,
1892        _ => return None,
1893    };
1894    let first = zenkaku_alpha_index(chars.next()?)?;
1895    let rest: Vec<char> = chars.collect();
1896    let index = match rest.as_slice() {
1897        [] => first,
1898        [second] => first * 26 + 26 + zenkaku_alpha_index(*second)?,
1899        _ => return None,
1900    };
1901    Some((list_form, index))
1902}
1903
1904fn namae_flag_element(ctx: &CommandContext, name: &str) -> Option<Vec<i32>> {
1905    let (list_form, index) = namae_flag_target(name)?;
1906    let elm_array = if ctx.ids.elm_array != 0 {
1907        ctx.ids.elm_array
1908    } else {
1909        forms::codes::ELM_ARRAY
1910    };
1911    Some(vec![list_form, elm_array, index])
1912}
1913
1914fn dispatch_global_message_command(
1915    ctx: &mut CommandContext,
1916    form_id: u32,
1917    args: &[Value],
1918) -> Result<bool> {
1919    match form_id as i32 {
1920        constants::elm_value::GLOBAL_MESSAGE_BOX => {
1921            let text = global_message_arg_str(args).unwrap_or("").to_string();
1922            ctx.request_system_messagebox_no_return(
1923                17,
1924                false,
1925                text,
1926                vec![crate::runtime::globals::SystemMessageBoxButton {
1927                    label: "OK".to_string(),
1928                    value: 0,
1929                }],
1930            );
1931            Ok(true)
1932        }
1933        constants::elm_value::GLOBAL_GET_LAST_SEL_MSG => {
1934            ctx.push(Value::Str(
1935                ctx.globals.syscom.system_extra_str_value.clone(),
1936            ));
1937            Ok(true)
1938        }
1939        constants::elm_value::GLOBAL_OPEN
1940        | constants::elm_value::GLOBAL_OPEN_WAIT
1941        | constants::elm_value::GLOBAL_OPEN_NOWAIT => {
1942            ctx.ui.show_message_bg(true);
1943            push_global_message_ok(ctx);
1944            Ok(true)
1945        }
1946        constants::elm_value::GLOBAL_CLOSE
1947        | constants::elm_value::GLOBAL_CLOSE_WAIT
1948        | constants::elm_value::GLOBAL_CLOSE_NOWAIT => {
1949            ctx.ui.show_message_bg(false);
1950            push_global_message_ok(ctx);
1951            Ok(true)
1952        }
1953        constants::elm_value::GLOBAL_END_CLOSE => {
1954            // C++ GLOBAL.END_CLOSE dispatches MWND.END_CLOSE for the current
1955            // message window. If the stage/current-MWND route above did not
1956            // handle it, this fallback must not perform CLOSE semantics.
1957            push_global_message_ok(ctx);
1958            Ok(true)
1959        }
1960        constants::elm_value::GLOBAL_MSG_BLOCK | constants::elm_value::GLOBAL_MSG_PP_BLOCK => {
1961            // Message block commands update/forward message state only. They must not
1962            // create a script-proc boundary; WAIT_MSG / PP / R / PAGE are the commands
1963            // that actually stop the running script.
1964            push_global_message_ok(ctx);
1965            Ok(true)
1966        }
1967        constants::elm_value::GLOBAL_CLEAR => {
1968            ctx.ui.clear_message();
1969            ctx.ui.clear_name();
1970            push_global_message_ok(ctx);
1971            Ok(true)
1972        }
1973        constants::elm_value::GLOBAL_CLEAR_MSGBK => {
1974            ctx.ui.clear_message();
1975            let form_id = ctx.ids.form_global_msgbk;
1976            if form_id != 0 {
1977                ctx.globals.msgbk_forms.entry(form_id).or_default().clear();
1978            }
1979            push_global_message_ok(ctx);
1980            Ok(true)
1981        }
1982        constants::elm_value::GLOBAL_INSERT_MSGBK_IMG => {
1983            let mut positional = args.iter().filter(|v| !matches!(v, Value::NamedArg { .. }));
1984            let file_name = positional
1985                .next()
1986                .and_then(Value::as_str)
1987                .unwrap_or("")
1988                .to_string();
1989            let x = positional.next().and_then(Value::as_i64).unwrap_or(0) as i32;
1990
1991            // Original cmd_global.cpp forwards GLOBAL.INSERT_MSGBK_IMG to
1992            // tnm_msg_back_add_pct(file_name, x, 0).  The command has two
1993            // overloads: (str) and (str, int); Y is always zero.
1994            let msgbk_form_id = ctx.ids.form_global_msgbk;
1995            if msgbk_form_id != 0 {
1996                ctx.globals
1997                    .msgbk_forms
1998                    .entry(msgbk_form_id)
1999                    .or_default()
2000                    .add_pct(&file_name, x, 0);
2001            }
2002            push_global_message_ok(ctx);
2003            Ok(true)
2004        }
2005        constants::elm_value::GLOBAL_PRINT => {
2006            ctx.request_read_flag_no();
2007            if let Some(s) = global_message_arg_str(args)
2008                && !s.is_empty()
2009            {
2010                syscom::append_current_save_message(ctx, s);
2011                ctx.ui.show_message_bg(true);
2012                ctx.ui.append_message(s);
2013            }
2014            push_global_message_ok(ctx);
2015            Ok(true)
2016        }
2017        constants::elm_value::GLOBAL_NL | constants::elm_value::GLOBAL_NLI => {
2018            ctx.ui.append_linebreak();
2019            push_global_message_ok(ctx);
2020            Ok(true)
2021        }
2022        constants::elm_value::GLOBAL_WAIT_MSG | constants::elm_value::GLOBAL_PP => {
2023            ctx.ui.begin_wait_message();
2024            ctx.wait.wait_key();
2025            ctx.request_message_wait_proc_boundary();
2026            push_global_message_ok(ctx);
2027            Ok(true)
2028        }
2029        constants::elm_value::GLOBAL_R | constants::elm_value::GLOBAL_PAGE => {
2030            if (form_id as i32) == constants::elm_value::GLOBAL_PAGE {
2031                ctx.ui.begin_wait_page_message();
2032            } else {
2033                ctx.ui.begin_wait_message();
2034            }
2035            ctx.ui.request_clear_message_on_wait_end();
2036            ctx.wait.wait_key();
2037            ctx.request_message_wait_proc_boundary();
2038            push_global_message_ok(ctx);
2039            Ok(true)
2040        }
2041        constants::elm_value::GLOBAL_SET_NAMAE => {
2042            let name = global_message_arg_str(args).unwrap_or("");
2043            if !stage::cd_name_current_mwnd(ctx, name) {
2044                ctx.ui.set_name(name.to_string());
2045            }
2046            push_global_message_ok(ctx);
2047            Ok(true)
2048        }
2049        constants::elm_value::GLOBAL_CLEAR_FACE
2050        | constants::elm_value::GLOBAL_SET_FACE
2051        | constants::elm_value::GLOBAL_SIZE
2052        | constants::elm_value::GLOBAL_COLOR
2053        | constants::elm_value::GLOBAL_RUBY
2054        | constants::elm_value::GLOBAL_MSGBTN
2055        | constants::elm_value::GLOBAL_MULTI_MSG
2056        | constants::elm_value::GLOBAL_NEXT_MSG
2057        | constants::elm_value::GLOBAL_START_SLIDE_MSG
2058        | constants::elm_value::GLOBAL_END_SLIDE_MSG
2059        | constants::elm_value::GLOBAL_INDENT
2060        | constants::elm_value::GLOBAL_CLEAR_INDENT
2061        | constants::elm_value::GLOBAL_REP_POS
2062        | constants::elm_value::GLOBAL_SET_WAKU => {
2063            push_global_message_ok(ctx);
2064            Ok(true)
2065        }
2066        _ => Ok(false),
2067    }
2068}
2069
2070pub fn dispatch_global_form(
2071    ctx: &mut CommandContext,
2072    form_id: u32,
2073    args: &[Value],
2074) -> Result<bool> {
2075    let form_id = canonical_global_form_id(ctx, form_id);
2076
2077    // Original cmd_global.cpp dispatches root-level global commands directly
2078    // by their element code. GLOBAL.NOP is therefore encoded as the one-item
2079    // command chain [ELM_GLOBAL_NOP] (60), not as [FM_GLOBAL, NOP].
2080    // It intentionally performs no work but must still report the command as
2081    // handled so execution continues to the next bytecode instruction.
2082    if form_id == constants::elm_value::GLOBAL_NOP as u32 {
2083        return Ok(true);
2084    }
2085
2086    if form_id == constants::elm_value::GLOBAL_OWARI as u32 {
2087        use crate::runtime::globals::{SyscomPendingProc, SyscomPendingProcKind};
2088
2089        // cmd_global.cpp: tnm_syscom_end_game(false, false, false).
2090        ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
2091            kind: SyscomPendingProcKind::EndGame,
2092            warning: false,
2093            se_play: false,
2094            fade_out: false,
2095            leave_msgbk: false,
2096            save_id: 0,
2097        });
2098        ctx.globals.syscom.menu_open = false;
2099        // Let the host save persistent state and exit before the next instruction.
2100        ctx.request_proc_boundary(crate::runtime::ProcKind::Script);
2101        return Ok(true);
2102    }
2103
2104    if form_id == constants::elm_value::GLOBAL_GET_SCENE_NAME as u32 {
2105        ctx.stack.push(Value::Str(
2106            ctx.current_scene_name.clone().unwrap_or_default(),
2107        ));
2108        return Ok(true);
2109    }
2110    if form_id == constants::elm_value::GLOBAL_GET_LINE_NO as u32 {
2111        ctx.stack.push(Value::Int(ctx.current_line_no));
2112        return Ok(true);
2113    }
2114    if form_id == constants::elm_value::GLOBAL_NAMAE as u32 {
2115        // cmd_global.cpp: `ELM_GLOBAL_NAMAE` returns a strref (FM_STRREF) to the
2116        // requested name flag so scripts can read/assign the stored name.
2117        let name = global_message_arg_str(args).unwrap_or("");
2118        match namae_flag_element(ctx, name) {
2119            Some(element) => ctx.stack.push(Value::Element(element)),
2120            None => {
2121                log::error!("GLOBAL.NAMAE: invalid name flag {name:?}");
2122                ctx.stack.push(Value::Element(Vec::new()));
2123            }
2124        }
2125        return Ok(true);
2126    }
2127    if form_id == constants::elm_value::GLOBAL_RETURNMENU as u32 {
2128        use crate::runtime::globals::{SyscomPendingProc, SyscomPendingProcKind};
2129
2130        // Original cmd_global.cpp has three overloads with deliberately different
2131        // control flow:
2132        //   returnmenu()             -> tnm_syscom_return_to_menu(false,false,true,false)
2133        //   returnmenu(scene)        -> tnm_syscom_restart_from_scene(scene, 0)
2134        //   returnmenu(scene, z_no)  -> tnm_syscom_restart_from_scene(scene, z_no)
2135        // The scene overloads therefore use the SCENESTART warning/save path and
2136        // must not be treated as a temporary MENU_SCENE override.
2137        let target = args.first().and_then(Value::as_str).map(|scene| {
2138            (
2139                scene.to_string(),
2140                args.get(1).and_then(Value::as_i64).unwrap_or(0) as i32,
2141            )
2142        });
2143        if let Some(target) = target {
2144            ctx.pending_scene_restart = Some(target);
2145            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
2146                kind: SyscomPendingProcKind::RestartScene,
2147                warning: true,
2148                se_play: false,
2149                fade_out: false,
2150                leave_msgbk: false,
2151                save_id: 0,
2152            });
2153        } else {
2154            ctx.pending_scene_restart = None;
2155            ctx.globals.syscom.pending_proc = Some(SyscomPendingProc {
2156                kind: SyscomPendingProcKind::ReturnToMenu,
2157                warning: false,
2158                se_play: false,
2159                fade_out: true,
2160                leave_msgbk: false,
2161                save_id: 0,
2162            });
2163        }
2164        ctx.globals.syscom.menu_open = false;
2165        // Return control to the host before executing another script instruction.
2166        ctx.request_proc_boundary(crate::runtime::ProcKind::Script);
2167        return Ok(true);
2168    }
2169
2170    if dispatch_global_wipe_command(ctx, form_id, args)? {
2171        return Ok(true);
2172    }
2173    if dispatch_capture_command(ctx, form_id, args)? {
2174        return Ok(true);
2175    }
2176    if dispatch_global_fog_command(ctx, form_id, args)? {
2177        return Ok(true);
2178    }
2179    if dispatch_selbtn_command(ctx, form_id, args)? {
2180        return Ok(true);
2181    }
2182    if stage::dispatch_current_mwnd_global_op(ctx, form_id as i32, args) {
2183        return Ok(true);
2184    }
2185    if dispatch_global_koe_command(ctx, form_id, args)? {
2186        return Ok(true);
2187    }
2188    if dispatch_global_message_command(ctx, form_id, args)? {
2189        return Ok(true);
2190    }
2191
2192    // Same-version testcase still uses compact startup aliases that bypass the
2193    // canonical global-form ids. Keep them routed to their original handlers.
2194    if form_id == 24 {
2195        return keylist::dispatch(ctx, args);
2196    }
2197    if form_id == 40 {
2198        return counter::dispatch(ctx, form_id, args);
2199    }
2200    if form_id == 63 && syscom::dispatch(ctx, form_id, args)? {
2201        return Ok(true);
2202    }
2203    if form_id == 64 && script::dispatch(ctx, form_id, args)? {
2204        return Ok(true);
2205    }
2206    if form_id == 46 {
2207        return mouse::dispatch(ctx, args);
2208    }
2209    if form_id == 86 && input::dispatch(ctx, form_id, args)? {
2210        return Ok(true);
2211    }
2212    if form_id == 92 && system::dispatch(ctx, form_id, args)? {
2213        return Ok(true);
2214    }
2215    if form_id == constants::elm_value::GLOBAL_DISP as u32 {
2216        ctx.wait.wait_next_frame(ctx.globals.render_frame);
2217        ctx.request_disp_proc_boundary();
2218        return Ok(true);
2219    }
2220    if form_id == constants::elm_value::GLOBAL_FRAME as u32 {
2221        ctx.wait.wait_next_frame(ctx.globals.render_frame);
2222        ctx.request_proc_boundary(crate::runtime::ProcKind::Frame);
2223        return Ok(true);
2224    }
2225    if form_id == constants::elm_value::GLOBAL_SET_MWND as u32
2226        || form_id == constants::elm_value::GLOBAL_SET_SEL_MWND as u32
2227    {
2228        // C++ dispatches the two overloads by al_id: al_id=0 copies the full
2229        // S_element verbatim, while al_id=1 constructs FRONT.MWND[int].
2230        let al_id = ctx.vm_call.as_ref().map(|m| m.al_id);
2231        let arg = args.first().map(Value::unwrap_named);
2232        let element = match (al_id, arg) {
2233            (Some(0), Some(Value::Element(chain))) => Some(chain.clone()),
2234            (Some(1), Some(Value::Int(no @ 0..))) => Some(front_mwnd_element(*no as usize)),
2235            _ => None,
2236        };
2237
2238        if let Some(element) = element
2239            && let Some((stage, no)) = mwnd_ref_from_element(&element)
2240        {
2241            if form_id == constants::elm_value::GLOBAL_SET_SEL_MWND as u32 {
2242                ctx.globals.current_sel_mwnd_element = element;
2243                ctx.globals.current_sel_mwnd_stage_idx = stage;
2244                ctx.globals.current_sel_mwnd_no = Some(no);
2245            } else {
2246                ctx.globals.current_mwnd_element = element;
2247                ctx.globals.current_mwnd_stage_idx = stage;
2248                ctx.globals.current_mwnd_no = Some(no);
2249            }
2250        }
2251        return Ok(true);
2252    }
2253    if form_id == constants::elm_value::GLOBAL_GET_MWND as u32
2254        || form_id == constants::elm_value::GLOBAL_GET_SEL_MWND as u32
2255    {
2256        let no = if form_id == constants::elm_value::GLOBAL_GET_SEL_MWND as u32 {
2257            ctx.globals.current_sel_mwnd_no
2258        } else {
2259            ctx.globals.current_mwnd_no
2260        };
2261        // C++ returns -1 only when no current MWND element resolves.
2262        ctx.push(Value::Int(no.map(|n| n as i64).unwrap_or(-1)));
2263        return Ok(true);
2264    }
2265    if form_id == constants::elm_value::GLOBAL_SET_TITLE as u32 {
2266        // cmd_global.cpp stores Gp_local->scene_title. This is script/save
2267        // state, even when the host does not update the OS window caption.
2268        ctx.globals.syscom.current_save_scene_title = args
2269            .first()
2270            .and_then(Value::as_str)
2271            .unwrap_or_default()
2272            .to_string();
2273        return Ok(true);
2274    }
2275    if form_id == constants::elm_value::GLOBAL_GET_TITLE as u32 {
2276        ctx.push(Value::Str(
2277            ctx.globals.syscom.current_save_scene_title.clone(),
2278        ));
2279        return Ok(true);
2280    }
2281
2282    if form_id == constants::global_form::STAGE_ALT {
2283        return stage::dispatch(ctx, args);
2284    }
2285    if form_id == constants::global_form::BGM {
2286        return forms::bgm::dispatch(ctx, args);
2287    }
2288    if form_id == constants::global_form::BGMTABLE {
2289        return forms::bgm_table::dispatch(ctx, args);
2290    }
2291    if form_id == constants::global_form::MOV {
2292        return forms::mov::dispatch(ctx, args);
2293    }
2294    if form_id == constants::global_form::PCM {
2295        return forms::pcm::dispatch(ctx, args);
2296    }
2297    if form_id == constants::global_form::PCMCH {
2298        return forms::pcmch::dispatch(ctx, form_id, args);
2299    }
2300    if form_id == constants::global_form::SE {
2301        return forms::se::dispatch(ctx, args);
2302    }
2303    if form_id == constants::global_form::PCMEVENT {
2304        return forms::pcmevent::dispatch(ctx, args);
2305    }
2306    if form_id == constants::global_form::EXCALL {
2307        return forms::excall::dispatch(ctx, args);
2308    }
2309    if form_id == constants::global_form::KOE_ST {
2310        return forms::koe_st::dispatch(ctx, args);
2311    }
2312    if form_id == ctx.ids.form_global_input {
2313        return input::dispatch(ctx, form_id, args);
2314    }
2315    if form_id == ctx.ids.form_global_mouse {
2316        return mouse::dispatch(ctx, args);
2317    }
2318    if form_id == ctx.ids.form_global_keylist {
2319        return keylist::dispatch(ctx, args);
2320    }
2321    if form_id == constants::global_form::KEY {
2322        return key::dispatch(ctx, args);
2323    }
2324    if form_id == constants::global_form::JOYPAD {
2325        return joypad::dispatch(ctx, args);
2326    }
2327    if form_id == constants::global_form::SCREEN {
2328        return forms::screen::dispatch(ctx, args);
2329    }
2330    if form_id == constants::global_form::MSGBK {
2331        return forms::msgbk::dispatch(ctx, args);
2332    }
2333    if ctx.ids.form_global_math != 0 && form_id == ctx.ids.form_global_math {
2334        return math::dispatch(ctx, form_id, args);
2335    }
2336    if ctx.ids.form_global_cgtable != 0 && form_id == ctx.ids.form_global_cgtable {
2337        return cgtable::dispatch(ctx, form_id, args);
2338    }
2339    if ctx.ids.form_global_database != 0 && form_id == ctx.ids.form_global_database {
2340        return database::dispatch(ctx, form_id, args);
2341    }
2342    if ctx.ids.form_global_g00buf != 0 && form_id == ctx.ids.form_global_g00buf {
2343        return g00buf::dispatch(ctx, form_id, args);
2344    }
2345    if ctx.ids.form_global_mask != 0 && form_id == ctx.ids.form_global_mask {
2346        return mask::dispatch(ctx, form_id, args);
2347    }
2348    if ctx.ids.form_global_editbox != 0 && form_id == ctx.ids.form_global_editbox {
2349        return editbox::dispatch(ctx, form_id, args);
2350    }
2351    if ctx.ids.form_global_file != 0 && form_id == ctx.ids.form_global_file {
2352        return file::dispatch(ctx, form_id, args);
2353    }
2354    if ctx.ids.form_global_steam != 0 && form_id == ctx.ids.form_global_steam {
2355        return steam::dispatch(ctx, form_id, args);
2356    }
2357    if ctx.ids.form_global_syscom != 0 && form_id == ctx.ids.form_global_syscom {
2358        return syscom::dispatch(ctx, form_id, args);
2359    }
2360    if ctx.ids.form_global_script != 0 && form_id == ctx.ids.form_global_script {
2361        return script::dispatch(ctx, form_id, args);
2362    }
2363    if ctx.ids.form_global_system != 0 && form_id == ctx.ids.form_global_system {
2364        return system::dispatch(ctx, form_id, args);
2365    }
2366    if form_id == constants::global_form::FRAME_ACTION {
2367        return frame_action::dispatch(ctx, form_id, args);
2368    }
2369    if ctx.ids.form_global_frame_action_ch != 0 && form_id == ctx.ids.form_global_frame_action_ch {
2370        return frame_action_ch::dispatch(ctx, form_id, args);
2371    }
2372
2373    match form_id {
2374        constants::global_form::BGM => forms::bgm::dispatch(ctx, args),
2375        constants::global_form::BGMTABLE => forms::bgm_table::dispatch(ctx, args),
2376        constants::global_form::MOV => forms::mov::dispatch(ctx, args),
2377        constants::global_form::PCM => forms::pcm::dispatch(ctx, args),
2378        constants::global_form::PCMCH => forms::pcmch::dispatch(ctx, form_id, args),
2379        constants::global_form::SE => forms::se::dispatch(ctx, args),
2380        constants::global_form::PCMEVENT => forms::pcmevent::dispatch(ctx, args),
2381        constants::global_form::EXCALL => forms::excall::dispatch(ctx, args),
2382        constants::global_form::KOE_ST => forms::koe_st::dispatch(ctx, args),
2383        constants::global_form::SCREEN => forms::screen::dispatch(ctx, args),
2384        constants::global_form::MSGBK => forms::msgbk::dispatch(ctx, args),
2385        constants::global_form::KEY => key::dispatch(ctx, args),
2386        constants::global_form::JOYPAD => joypad::dispatch(ctx, args),
2387        _ => {
2388            // TIMEWAIT/TIMEWAIT_KEY are statement-like forms that block execution.
2389            if form_id == constants::global_form::TIMEWAIT {
2390                return timewait::dispatch(ctx, false, args);
2391            }
2392            if form_id == constants::global_form::TIMEWAIT_KEY {
2393                return timewait::dispatch(ctx, true, args);
2394            }
2395
2396            if form_id as i32 == constants::fm::INTEVENT
2397                || form_id as i32 == constants::fm::INTEVENTLIST
2398            {
2399                return int_event::dispatch(ctx, form_id, args);
2400            }
2401
2402            if form_id as i32 == constants::fm::OBJECTEVENT {
2403                return object_event::dispatch(ctx, args);
2404            }
2405            if form_id as i32 == crate::runtime::forms::codes::FM_OBJECTEVENTLIST {
2406                return object_event::dispatch_list(ctx, args);
2407            }
2408
2409            if constants::global_form::INT_LIST_FORMS.contains(&form_id) {
2410                return int_list::dispatch(ctx, form_id, args);
2411            }
2412            if constants::global_form::STR_LIST_FORMS.contains(&form_id) {
2413                return str_list::dispatch(ctx, form_id, args);
2414            }
2415
2416            if form_id == constants::global_form::COUNTER {
2417                return counter::dispatch(ctx, form_id, args);
2418            }
2419
2420            if form_id == constants::global_form::FRAME_ACTION {
2421                return int_list::dispatch(ctx, form_id, args);
2422            }
2423
2424            Ok(false)
2425        }
2426    }
2427}
2428
2429#[cfg(test)]
2430mod koe_wait_return_tests {
2431    use super::*;
2432    use crate::runtime::VmCallMeta;
2433    use std::path::PathBuf;
2434
2435    fn set_call(ctx: &mut CommandContext, op: i32, ret_form: i64) {
2436        ctx.vm_call = Some(VmCallMeta {
2437            element: vec![op],
2438            al_id: 0,
2439            ret_form,
2440        });
2441    }
2442
2443    fn named(id: i32, value: i64) -> Value {
2444        Value::NamedArg {
2445            id,
2446            value: Box::new(Value::Int(value)),
2447        }
2448    }
2449
2450    #[test]
2451    fn global_nop_root_element_is_handled_without_side_effects() {
2452        let mut ctx = CommandContext::new(PathBuf::from("."));
2453        ctx.vm_call = Some(VmCallMeta {
2454            element: vec![constants::elm_value::GLOBAL_NOP],
2455            al_id: 0,
2456            ret_form: 0,
2457        });
2458
2459        assert!(
2460            dispatch_global_form(&mut ctx, constants::elm_value::GLOBAL_NOP as u32, &[],).unwrap()
2461        );
2462        assert!(ctx.stack.is_empty());
2463        assert!(!ctx.wait_poll());
2464    }
2465
2466    #[test]
2467    fn mwnd_element_decoder_distinguishes_stage_index_from_mwnd_index() {
2468        let canonical = vec![
2469            forms::codes::ELM_GLOBAL_STAGE,
2470            forms::codes::ELM_ARRAY,
2471            2,
2472            forms::codes::ELM_STAGE_MWND,
2473            forms::codes::ELM_ARRAY,
2474            7,
2475        ];
2476        assert_eq!(mwnd_ref_from_element(&canonical), Some((2, 7)));
2477
2478        let alias = vec![
2479            forms::codes::ELM_GLOBAL_FRONT,
2480            forms::codes::ELM_STAGE_MWND,
2481            forms::codes::ELM_ARRAY,
2482            7,
2483        ];
2484        assert_eq!(mwnd_ref_from_element(&alias), Some((1, 7)));
2485    }
2486
2487    #[test]
2488    fn set_mwnd_element_overload_preserves_complete_element_and_does_not_touch_last() {
2489        let mut ctx = CommandContext::new(PathBuf::from("."));
2490        let element = vec![
2491            forms::codes::ELM_GLOBAL_STAGE,
2492            forms::codes::ELM_ARRAY,
2493            2,
2494            forms::codes::ELM_STAGE_MWND,
2495            forms::codes::ELM_ARRAY,
2496            7,
2497        ];
2498        ctx.vm_call = Some(VmCallMeta {
2499            element: vec![constants::elm_value::GLOBAL_SET_MWND],
2500            al_id: 0,
2501            ret_form: 0,
2502        });
2503
2504        assert!(
2505            dispatch_global_form(
2506                &mut ctx,
2507                constants::elm_value::GLOBAL_SET_MWND as u32,
2508                &[Value::Element(element.clone())],
2509            )
2510            .unwrap()
2511        );
2512        assert_eq!(ctx.globals.current_mwnd_element, element);
2513        assert_eq!(ctx.globals.current_mwnd_stage_idx, 2);
2514        assert_eq!(ctx.globals.current_mwnd_no, Some(7));
2515        assert!(ctx.globals.last_mwnd_element.is_empty());
2516        assert_eq!(ctx.globals.last_mwnd_no, None);
2517    }
2518
2519    #[test]
2520    fn set_mwnd_integer_overload_constructs_front_mwnd() {
2521        let mut ctx = CommandContext::new(PathBuf::from("."));
2522        ctx.vm_call = Some(VmCallMeta {
2523            element: vec![constants::elm_value::GLOBAL_SET_MWND],
2524            al_id: 1,
2525            ret_form: 0,
2526        });
2527
2528        assert!(
2529            dispatch_global_form(
2530                &mut ctx,
2531                constants::elm_value::GLOBAL_SET_MWND as u32,
2532                &[Value::Int(6)],
2533            )
2534            .unwrap()
2535        );
2536        assert_eq!(
2537            ctx.globals.current_mwnd_element,
2538            vec![
2539                forms::codes::ELM_GLOBAL_FRONT,
2540                forms::codes::ELM_STAGE_MWND,
2541                forms::codes::ELM_ARRAY,
2542                6,
2543            ]
2544        );
2545        assert_eq!(ctx.globals.current_mwnd_stage_idx, 1);
2546        assert_eq!(ctx.globals.current_mwnd_no, Some(6));
2547    }
2548
2549    #[test]
2550    fn exkoe_named_wait_defers_its_integer_result_until_wait_completion() {
2551        let mut ctx = CommandContext::new(PathBuf::from("."));
2552        let op = constants::elm_value::GLOBAL_EXKOE;
2553        set_call(&mut ctx, op, 10);
2554        let args = vec![named(0, -1), named(2, 1), named(3, 1)];
2555
2556        assert!(dispatch_global_koe_command(&mut ctx, op as u32, &args).unwrap());
2557        assert!(ctx.wait.audio.is_some());
2558        assert!(
2559            ctx.stack.is_empty(),
2560            "EXKOE(wait=1) must not push the result before the wait proc finishes"
2561        );
2562    }
2563
2564    #[test]
2565    fn exkoe_without_wait_returns_zero_immediately() {
2566        let mut ctx = CommandContext::new(PathBuf::from("."));
2567        let op = constants::elm_value::GLOBAL_EXKOE;
2568        set_call(&mut ctx, op, 10);
2569        let args = vec![named(0, -1), named(2, 0), named(3, 1)];
2570
2571        assert!(dispatch_global_koe_command(&mut ctx, op as u32, &args).unwrap());
2572        assert!(ctx.wait.audio.is_none());
2573        assert_eq!(ctx.stack.pop().and_then(|v| v.as_i64()), Some(0));
2574    }
2575
2576    #[test]
2577    fn exkoe_play_wait_key_defers_return_value() {
2578        let mut ctx = CommandContext::new(PathBuf::from("."));
2579        let op = constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY;
2580        set_call(&mut ctx, op, 10);
2581
2582        assert!(dispatch_global_koe_command(&mut ctx, op as u32, &[Value::Int(-1)]).unwrap());
2583        assert!(ctx.wait.audio.is_some());
2584        assert!(ctx.stack.is_empty());
2585    }
2586
2587    #[test]
2588    fn koe_wait_key_defers_return_value() {
2589        let mut ctx = CommandContext::new(PathBuf::from("."));
2590        let op = constants::elm_value::GLOBAL_KOE_WAIT_KEY;
2591        set_call(&mut ctx, op, 10);
2592
2593        assert!(dispatch_global_koe_command(&mut ctx, op as u32, &[]).unwrap());
2594        assert!(ctx.wait.audio.is_some());
2595        assert!(ctx.stack.is_empty());
2596    }
2597}
2598
2599#[cfg(test)]
2600mod namae_flag_tests {
2601    use super::{forms, namae_flag_target, zenkaku_alpha_index};
2602
2603    #[test]
2604    fn fullwidth_alpha_index_covers_a_to_z() {
2605        assert_eq!(zenkaku_alpha_index('A'), Some(0));
2606        assert_eq!(zenkaku_alpha_index('Z'), Some(25));
2607        assert_eq!(zenkaku_alpha_index('A'), None);
2608        assert_eq!(zenkaku_alpha_index('1'), None);
2609    }
2610
2611    #[test]
2612    fn single_letter_names() {
2613        assert_eq!(
2614            namae_flag_target("*A"),
2615            Some((forms::codes::ELM_GLOBAL_NAMAE_GLOBAL, 0))
2616        );
2617        assert_eq!(
2618            namae_flag_target("*Z"),
2619            Some((forms::codes::ELM_GLOBAL_NAMAE_GLOBAL, 25))
2620        );
2621        assert_eq!(
2622            namae_flag_target("%A"),
2623            Some((forms::codes::ELM_GLOBAL_NAMAE_LOCAL, 0))
2624        );
2625    }
2626
2627    #[test]
2628    fn double_letter_names() {
2629        assert_eq!(
2630            namae_flag_target("*AA"),
2631            Some((forms::codes::ELM_GLOBAL_NAMAE_GLOBAL, 26))
2632        );
2633        assert_eq!(
2634            namae_flag_target("%ZZ"),
2635            Some((forms::codes::ELM_GLOBAL_NAMAE_LOCAL, 26 + 25 * 26 + 25))
2636        );
2637        assert_eq!(
2638            namae_flag_target("*BA"),
2639            Some((forms::codes::ELM_GLOBAL_NAMAE_GLOBAL, 26 + 26))
2640        );
2641    }
2642
2643    #[test]
2644    fn invalid_names_are_rejected() {
2645        assert_eq!(namae_flag_target(""), None);
2646        assert_eq!(namae_flag_target("A"), None);
2647        assert_eq!(namae_flag_target("*"), None);
2648        assert_eq!(namae_flag_target("*1"), None);
2649        assert_eq!(namae_flag_target("*AAA"), None);
2650        assert_eq!(namae_flag_target("MA"), None);
2651    }
2652}