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::{constants, forms, CommandContext, Value};
8
9use crate::runtime::forms::{
10    cgtable, counter, database, editbox, file, frame_action, frame_action_ch, g00buf, input,
11    int_event, int_list, key, keylist, mask, math, mouse, object_event, script, stage, steam,
12    str_list, syscom, system, timewait,
13};
14
15fn canonical_global_form_id(ctx: &CommandContext, form_id: u32) -> u32 {
16    let ids = &ctx.ids;
17    if constants::is_stage_global_form(form_id, ids.form_global_stage) {
18        return constants::global_form::STAGE_ALT;
19    }
20    if constants::matches_form_id(form_id, ids.form_global_mov, constants::global_form::MOV) {
21        return constants::global_form::MOV;
22    }
23    if constants::matches_form_id(form_id, ids.form_global_bgm, constants::global_form::BGM) {
24        return constants::global_form::BGM;
25    }
26    if constants::matches_form_id(
27        form_id,
28        ids.form_global_bgm_table,
29        constants::global_form::BGMTABLE,
30    ) {
31        return constants::global_form::BGMTABLE;
32    }
33    if constants::matches_form_id(form_id, ids.form_global_pcm, constants::global_form::PCM) {
34        return constants::global_form::PCM;
35    }
36    if constants::matches_form_id(
37        form_id,
38        ids.form_global_pcmch,
39        constants::global_form::PCMCH,
40    ) {
41        return constants::global_form::PCMCH;
42    }
43    if constants::matches_form_id(form_id, ids.form_global_se, constants::global_form::SE) {
44        return constants::global_form::SE;
45    }
46    if constants::matches_form_id(
47        form_id,
48        ids.form_global_pcm_event,
49        constants::global_form::PCMEVENT,
50    ) {
51        return constants::global_form::PCMEVENT;
52    }
53    if constants::matches_form_id(
54        form_id,
55        ids.form_global_excall,
56        constants::global_form::EXCALL,
57    ) {
58        return constants::global_form::EXCALL;
59    }
60    if constants::matches_form_id(
61        form_id,
62        ids.form_global_screen,
63        constants::global_form::SCREEN,
64    ) {
65        return constants::global_form::SCREEN;
66    }
67    if constants::matches_form_id(
68        form_id,
69        ids.form_global_msgbk,
70        constants::global_form::MSGBK,
71    ) {
72        return constants::global_form::MSGBK;
73    }
74    if constants::matches_form_id(
75        form_id,
76        ids.form_global_koe_st,
77        constants::global_form::KOE_ST,
78    ) {
79        return constants::global_form::KOE_ST;
80    }
81    if constants::matches_form_id(form_id, ids.form_global_key, constants::global_form::KEY) {
82        return constants::global_form::KEY;
83    }
84    if constants::matches_form_id(
85        form_id,
86        ids.form_global_frame_action,
87        constants::global_form::FRAME_ACTION,
88    ) {
89        return constants::global_form::FRAME_ACTION;
90    }
91    if form_id == constants::global_form::TIMEWAIT {
92        return constants::global_form::TIMEWAIT;
93    }
94    if form_id == constants::global_form::TIMEWAIT_KEY {
95        return constants::global_form::TIMEWAIT_KEY;
96    }
97    if form_id == constants::global_form::COUNTER {
98        return constants::global_form::COUNTER;
99    }
100    form_id
101}
102
103fn named_i64(args: &[Value], id: i32) -> Option<i64> {
104    args.iter().find_map(|v| match v {
105        Value::NamedArg { id: got, value } if *got == id => value.as_i64(),
106        _ => None,
107    })
108}
109
110fn positional_i64(args: &[Value], idx: usize) -> Option<i64> {
111    args.iter()
112        .filter(|v| !matches!(v, Value::NamedArg { .. }))
113        .filter_map(Value::as_i64)
114        .nth(idx)
115}
116
117fn global_stage_alias_to_index(form_id: i32) -> Option<i64> {
118    let form_id = form_id as u32;
119    if form_id == constants::global_form::BACK {
120        Some(0)
121    } else if form_id == constants::global_form::FRONT {
122        Some(1)
123    } else if form_id == constants::global_form::NEXT {
124        Some(2)
125    } else {
126        None
127    }
128}
129
130fn mwnd_ref_from_value(v: &Value) -> Option<(i64, usize)> {
131    match v.unwrap_named() {
132        Value::Int(n) if *n >= 0 => Some((1, *n as usize)),
133        Value::Element(chain) => {
134            let stage = chain
135                .first()
136                .and_then(|head| global_stage_alias_to_index(*head))
137                .unwrap_or(1);
138            let no = chain.windows(2).find_map(|w| {
139                (w[0] == forms::codes::ELM_ARRAY && w[1] >= 0).then_some(w[1] as usize)
140            })?;
141            Some((stage, no))
142        }
143        _ => None,
144    }
145}
146
147fn mwnd_no_from_value(v: &Value) -> Option<usize> {
148    mwnd_ref_from_value(v).map(|(_, no)| no)
149}
150
151const TNM_STAGE_FRONT_SELBTN: i64 = 1;
152const TNM_SEL_ITEM_TYPE_OFF: i64 = 0;
153const TNM_SEL_ITEM_TYPE_ON: i64 = 1;
154const TNM_SEL_ITEM_TYPE_READ: i64 = 2;
155
156fn parse_selbtn_choices(
157    args: &[Value],
158) -> (i64, Vec<crate::runtime::globals::BtnSelectChoiceState>) {
159    let mut template_no = 0i64;
160    let mut start = 0usize;
161    if args.first().is_some_and(|v| v.named_id().is_none() && v.as_i64().is_some()) {
162        template_no = args.first().and_then(Value::as_i64).unwrap_or(0);
163        start = 1;
164    }
165
166    let mut out = Vec::new();
167    let mut last: Option<usize> = None;
168    let mut arg_no = 0i32;
169    for v in args.iter().skip(start).filter(|v| v.named_id().is_none()).map(Value::unwrap_named) {
170        if let Some(s) = v.as_str() {
171            out.push(crate::runtime::globals::BtnSelectChoiceState {
172                text: s.to_string(),
173                item_type: TNM_SEL_ITEM_TYPE_ON,
174                color: -1,
175                pos: (0, 0),
176                size: (0, 0),
177            });
178            last = Some(out.len() - 1);
179            arg_no = 0;
180        } else if let Some(n) = v.as_i64() {
181            if let Some(i) = last {
182                match arg_no {
183                    1 => out[i].item_type = n,
184                    2 => out[i].color = n,
185                    _ => {}
186                }
187            }
188        }
189        arg_no += 1;
190    }
191    (template_no, out)
192}
193
194fn selbtn_text_extent(text: &str, tmpl: &crate::runtime::tables::SelBtnTemplate) -> (i64, i64) {
195    let font_px = tmpl.moji_size.max(1);
196    let mut width = 0i64;
197    for ch in text.chars() {
198        let advance = if ch.is_ascii() || matches!(ch as u32, 0xFF61..=0xFF9F) {
199            (font_px + tmpl.moji_space.0) / 2
200        } else {
201            font_px + tmpl.moji_space.0
202        };
203        width = width.saturating_add(advance.max(1));
204    }
205    if !text.is_empty() {
206        width = width.saturating_sub(tmpl.moji_space.0);
207    }
208    (width.max(font_px), font_px)
209}
210
211fn load_selbtn_image_id(
212    ctx: &mut CommandContext,
213    file_name: &str,
214    patno: u32,
215) -> Option<crate::image_manager::ImageId> {
216    if file_name.is_empty() {
217        return None;
218    }
219    match ctx.images.load_g00(file_name, patno) {
220        Ok(id) => Some(id),
221        Err(_) => ctx.images.load_bg_frame(file_name, patno as usize).ok(),
222    }
223}
224
225fn selbtn_template_item_size(
226    ctx: &mut CommandContext,
227    choices: &[crate::runtime::globals::BtnSelectChoiceState],
228    tmpl: &crate::runtime::tables::SelBtnTemplate,
229) -> (i64, i64) {
230    if let Some(img_id) = load_selbtn_image_id(ctx, &tmpl.base_file, 0) {
231        if let Some(img) = ctx.images.get(img_id) {
232            return (img.width as i64, img.height as i64);
233        }
234    }
235    choices
236        .first()
237        .map(|choice| {
238            let (tw, th) = selbtn_text_extent(&choice.text, tmpl);
239            (
240                tw.saturating_add(tmpl.moji_pos.0.max(0)).max(1),
241                th.saturating_add(tmpl.moji_pos.1.max(0)).max(1),
242            )
243        })
244        .unwrap_or((tmpl.moji_size.max(1), tmpl.moji_size.max(1)))
245}
246
247fn layout_selbtn_choices(
248    choices: &mut [crate::runtime::globals::BtnSelectChoiceState],
249    tmpl: &crate::runtime::tables::SelBtnTemplate,
250    item_size: (i64, i64),
251) {
252    let rep_pos = if tmpl.rep_pos == (0, 0) {
253        (0, item_size.1.max(tmpl.moji_size.max(1)).max(1))
254    } else {
255        tmpl.rep_pos
256    };
257
258    let mut offset = (0i64, 0i64);
259    let mut max_offset = (0i64, 0i64);
260    let mut org_offset_x = 0i64;
261    let mut y_cnt = 0i64;
262    for choice in choices.iter_mut() {
263        if choice.item_type != TNM_SEL_ITEM_TYPE_OFF {
264            choice.pos = offset;
265            choice.size = item_size;
266            offset.0 = offset.0.saturating_add(rep_pos.0);
267            offset.1 = offset.1.saturating_add(rep_pos.1);
268            max_offset.0 = max_offset.0.max(offset.0);
269            max_offset.1 = max_offset.1.max(offset.1);
270            y_cnt += 1;
271            if tmpl.max_y_cnt > 0 && y_cnt >= tmpl.max_y_cnt {
272                offset.0 = org_offset_x.saturating_add(tmpl.line_width);
273                offset.1 = 0;
274                org_offset_x = offset.0;
275                y_cnt = 0;
276            }
277        }
278    }
279
280    let total_x = max_offset.0.saturating_sub(rep_pos.0).saturating_add(item_size.0);
281    let total_y = max_offset.1.saturating_sub(rep_pos.1).saturating_add(item_size.1);
282    let align_x = match tmpl.x_align {
283        1 => -total_x / 2,
284        2 => -total_x,
285        _ => 0,
286    };
287    let align_y = match tmpl.y_align {
288        1 => -total_y / 2,
289        2 => -total_y,
290        _ => 0,
291    };
292    for choice in choices.iter_mut() {
293        if choice.item_type != TNM_SEL_ITEM_TYPE_OFF {
294            choice.pos.0 = choice.pos.0.saturating_add(align_x).saturating_add(tmpl.base_pos.0);
295            choice.pos.1 = choice.pos.1.saturating_add(align_y).saturating_add(tmpl.base_pos.1);
296        }
297    }
298}
299
300fn selbtn_table_color(
301    tables: &crate::runtime::tables::AssetTables,
302    color_no: i64,
303    fallback: (u8, u8, u8),
304) -> (u8, u8, u8) {
305    if color_no < 0 {
306        return fallback;
307    }
308    tables
309        .color_table
310        .get(color_no as usize)
311        .copied()
312        .unwrap_or(fallback)
313}
314
315fn hide_selbtn_object_backing(ctx: &mut CommandContext, obj: &crate::runtime::globals::ObjectState) {
316    match obj.backend {
317        crate::runtime::globals::ObjectBackend::Rect { layer_id, sprite_id, .. }
318        | crate::runtime::globals::ObjectBackend::String { layer_id, sprite_id, .. }
319        | crate::runtime::globals::ObjectBackend::Movie { layer_id, sprite_id, .. } => {
320            if let Some(layer) = ctx.layers.layer_mut(layer_id) {
321                if let Some(sprite) = layer.sprite_mut(sprite_id) {
322                    sprite.visible = false;
323                    sprite.image_id = None;
324                }
325            }
326        }
327        crate::runtime::globals::ObjectBackend::Number { layer_id, ref sprite_ids }
328        | crate::runtime::globals::ObjectBackend::Weather { layer_id, ref sprite_ids } => {
329            if let Some(layer) = ctx.layers.layer_mut(layer_id) {
330                for &sprite_id in sprite_ids {
331                    if let Some(sprite) = layer.sprite_mut(sprite_id) {
332                        sprite.visible = false;
333                        sprite.image_id = None;
334                    }
335                }
336            }
337        }
338        _ => {}
339    }
340    for child in &obj.runtime.child_objects {
341        hide_selbtn_object_backing(ctx, child);
342    }
343}
344
345fn clear_existing_stage_btnselitems(ctx: &mut CommandContext) {
346    let old_items = {
347        let Some(st) = ctx.globals.stage_forms.get(&ctx.ids.form_global_stage) else {
348            return;
349        };
350        st.btnselitem_lists
351            .get(&TNM_STAGE_FRONT_SELBTN)
352            .cloned()
353            .unwrap_or_default()
354    };
355    for item in &old_items {
356        for obj in item.generated_objects.iter().chain(item.object_list.iter()) {
357            hide_selbtn_object_backing(ctx, obj);
358        }
359    }
360}
361
362fn make_selbtn_image_object(
363    ctx: &mut CommandContext,
364    file_name: &str,
365    patno: u32,
366    width: i64,
367    height: i64,
368    layer_rep: i64,
369    item_type: i64,
370    _selected: bool,
371) -> Option<crate::runtime::globals::ObjectState> {
372    let img_id = load_selbtn_image_id(ctx, file_name, patno)?;
373    let (img_w, img_h) = ctx
374        .images
375        .get(img_id)
376        .map(|img| (img.width.max(1), img.height.max(1)))
377        .unwrap_or((width.max(1) as u32, height.max(1) as u32));
378    let layer_id = ctx.layers.create_layer();
379    let sprite_id = ctx.layers.layer_mut(layer_id).map(|layer| layer.create_sprite())?;
380    if let Some(layer) = ctx.layers.layer_mut(layer_id) {
381        if let Some(sprite) = layer.sprite_mut(sprite_id) {
382            sprite.fit = crate::layer::SpriteFit::PixelRect;
383            sprite.size_mode = crate::layer::SpriteSizeMode::Intrinsic;
384            sprite.visible = item_type == TNM_SEL_ITEM_TYPE_ON || item_type == TNM_SEL_ITEM_TYPE_READ;
385            sprite.x = 0;
386            sprite.y = 0;
387            sprite.image_id = Some(img_id);
388            sprite.tr = 255;
389        }
390    }
391
392    let mut obj = crate::runtime::globals::ObjectState::default();
393    obj.used = true;
394    obj.backend = crate::runtime::globals::ObjectBackend::Rect {
395        layer_id,
396        sprite_id,
397        width: img_w,
398        height: img_h,
399    };
400    obj.object_type = 2;
401    obj.file_name = Some(file_name.to_string());
402    obj.base.disp = if item_type == TNM_SEL_ITEM_TYPE_ON || item_type == TNM_SEL_ITEM_TYPE_READ { 1 } else { 0 };
403    obj.base.x = 0;
404    obj.base.y = 0;
405    obj.base.patno = patno as i64;
406    obj.base.layer = layer_rep;
407    if ctx.ids.obj_disp != 0 {
408        obj.set_int_prop(&ctx.ids, ctx.ids.obj_disp, obj.base.disp);
409    }
410    if ctx.ids.obj_x != 0 {
411        obj.set_int_prop(&ctx.ids, ctx.ids.obj_x, 0);
412    }
413    if ctx.ids.obj_y != 0 {
414        obj.set_int_prop(&ctx.ids, ctx.ids.obj_y, 0);
415    }
416    if ctx.ids.obj_patno != 0 {
417        obj.set_int_prop(&ctx.ids, ctx.ids.obj_patno, patno as i64);
418    }
419    if ctx.ids.obj_layer != 0 {
420        obj.set_int_prop(&ctx.ids, ctx.ids.obj_layer, layer_rep);
421    }
422
423
424    Some(obj)
425}
426
427fn make_selbtn_text_object(
428    ctx: &mut CommandContext,
429    choice: &crate::runtime::globals::BtnSelectChoiceState,
430    tmpl: &crate::runtime::tables::SelBtnTemplate,
431    color_no: i64,
432    selected: bool,
433) -> Option<crate::runtime::globals::ObjectState> {
434    if !(choice.item_type == TNM_SEL_ITEM_TYPE_ON || choice.item_type == TNM_SEL_ITEM_TYPE_READ) || choice.text.is_empty() {
435        return None;
436    }
437    if !ctx.font_cache.is_loaded() {
438        let _ = ctx.font_cache.load_for_project(&ctx.project_dir);
439    }
440    let (tw, th) = selbtn_text_extent(&choice.text, tmpl);
441    let width = tw.max(1) as u32;
442    let height = th.max(tmpl.moji_size.max(1)) as u32;
443    let effective_color_no = if selected && choice.item_type == TNM_SEL_ITEM_TYPE_ON && tmpl.moji_hit_color >= 0 {
444        tmpl.moji_hit_color
445    } else {
446        color_no
447    };
448    let color = selbtn_table_color(&ctx.tables, effective_color_no, (255, 255, 255));
449    let shadow_color = selbtn_table_color(&ctx.tables, ctx.tables.mwnd_render.shadow_color, (0, 0, 0));
450    let fuchi_color = selbtn_table_color(&ctx.tables, ctx.tables.mwnd_render.fuchi_color, (0, 0, 0));
451    let style = crate::text_render::TextStyle {
452        color,
453        shadow_color,
454        fuchi_color,
455        shadow: ctx.tables.font_defaults.shadow == 1 || ctx.tables.font_defaults.shadow == 3,
456        fuchi: ctx.tables.font_defaults.shadow == 2 || ctx.tables.font_defaults.shadow == 3,
457        bold: ctx.tables.font_defaults.futoku != 0,
458    };
459    let img_id = ctx.font_cache.render_mwnd_text_styled(
460        &mut ctx.images,
461        &choice.text,
462        tmpl.moji_size.max(1) as f32,
463        width,
464        height,
465        Some(tmpl.moji_space),
466        style,
467    );
468    let layer_id = ctx.layers.create_layer();
469    let sprite_id = ctx.layers.layer_mut(layer_id).map(|layer| layer.create_sprite())?;
470    if let Some(layer) = ctx.layers.layer_mut(layer_id) {
471        if let Some(sprite) = layer.sprite_mut(sprite_id) {
472            sprite.fit = crate::layer::SpriteFit::PixelRect;
473            sprite.size_mode = if img_id.is_some() {
474                crate::layer::SpriteSizeMode::Intrinsic
475            } else {
476                crate::layer::SpriteSizeMode::Explicit { width, height }
477            };
478            sprite.visible = true;
479            sprite.x = tmpl.moji_pos.0 as i32;
480            sprite.y = tmpl.moji_pos.1 as i32;
481            sprite.image_id = img_id;
482            sprite.tr = 255;
483        }
484    }
485    let mut obj = crate::runtime::globals::ObjectState::default();
486    obj.used = true;
487    obj.backend = crate::runtime::globals::ObjectBackend::String {
488        layer_id,
489        sprite_id,
490        image_id: img_id,
491        width,
492        height,
493    };
494    obj.object_type = 3;
495    obj.string_value = Some(choice.text.clone());
496    obj.string_param.moji_size = tmpl.moji_size;
497    obj.string_param.moji_space_x = tmpl.moji_space.0;
498    obj.string_param.moji_space_y = tmpl.moji_space.1;
499    obj.string_param.moji_cnt = tmpl.moji_cnt;
500    obj.string_param.moji_color = effective_color_no;
501    obj.string_param.shadow_color = ctx.tables.mwnd_render.shadow_color;
502    obj.string_param.fuchi_color = ctx.tables.mwnd_render.fuchi_color;
503    obj.base.disp = 1;
504    obj.base.x = tmpl.moji_pos.0;
505    obj.base.y = tmpl.moji_pos.1;
506    obj.base.layer = ctx.tables.mwnd_render.moji_layer_rep;
507    if ctx.ids.obj_disp != 0 {
508        obj.set_int_prop(&ctx.ids, ctx.ids.obj_disp, 1);
509    }
510    if ctx.ids.obj_x != 0 {
511        obj.set_int_prop(&ctx.ids, ctx.ids.obj_x, tmpl.moji_pos.0);
512    }
513    if ctx.ids.obj_y != 0 {
514        obj.set_int_prop(&ctx.ids, ctx.ids.obj_y, tmpl.moji_pos.1);
515    }
516    if ctx.ids.obj_layer != 0 {
517        obj.set_int_prop(&ctx.ids, ctx.ids.obj_layer, ctx.tables.mwnd_render.moji_layer_rep);
518    }
519    Some(obj)
520}
521
522fn prepare_stage_btnselitems(ctx: &mut CommandContext) {
523    clear_existing_stage_btnselitems(ctx);
524    let template_no = ctx.globals.selbtn.template_no.max(0) as usize;
525    let tmpl = ctx
526        .tables
527        .sel_btn_templates
528        .get(template_no)
529        .cloned()
530        .unwrap_or_default();
531    let choices_for_size = ctx.globals.selbtn.choices.clone();
532    let item_size = selbtn_template_item_size(ctx, &choices_for_size, &tmpl);
533    layout_selbtn_choices(&mut ctx.globals.selbtn.choices, &tmpl, item_size);
534    let choices_snapshot = ctx.globals.selbtn.choices.clone();
535    let cursor = ctx.globals.selbtn.cursor;
536    let waku_layer_rep = ctx.tables.mwnd_render.waku_layer_rep;
537    let filter_layer_rep = ctx.tables.mwnd_render.filter_layer_rep;
538    let mut prepared = Vec::with_capacity(choices_snapshot.len());
539    for (idx, choice) in choices_snapshot.iter().enumerate() {
540        let mut item = crate::runtime::globals::BtnSelItemState::default();
541        item.text = choice.text.clone();
542        item.item_type = choice.item_type;
543        item.color = if choice.color >= 0 { choice.color } else { tmpl.moji_color };
544        item.pos = choice.pos;
545        item.size = choice.size;
546        item.visible = choice.item_type == TNM_SEL_ITEM_TYPE_ON || choice.item_type == TNM_SEL_ITEM_TYPE_READ;
547        item.selected = idx == cursor;
548        item.button_action_no = tmpl.btn_action_no;
549        item.button_state = if choice.item_type == TNM_SEL_ITEM_TYPE_READ {
550            4
551        } else if item.selected && choice.item_type == TNM_SEL_ITEM_TYPE_ON {
552            1
553        } else {
554            0
555        };
556
557        if let Some(obj) = make_selbtn_image_object(
558            ctx,
559            &tmpl.base_file,
560            0,
561            item.size.0,
562            item.size.1,
563            waku_layer_rep,
564            choice.item_type,
565            item.selected,
566        ) {
567            item.generated_objects.push(obj);
568        }
569        if let Some(obj) = make_selbtn_image_object(
570            ctx,
571            &tmpl.filter_file,
572            0,
573            item.size.0,
574            item.size.1,
575            filter_layer_rep,
576            choice.item_type,
577            item.selected,
578        ) {
579            item.generated_objects.push(obj);
580        }
581        if let Some(obj) = make_selbtn_text_object(ctx, choice, &tmpl, item.color, item.selected) {
582            item.generated_objects.push(obj);
583        }
584        prepared.push(item);
585    }
586    let st = ctx
587        .globals
588        .stage_forms
589        .entry(ctx.ids.form_global_stage)
590        .or_default();
591    st.btnselitem_lists
592        .insert(TNM_STAGE_FRONT_SELBTN, prepared);
593}
594
595fn first_selectable_selbtn_choice(choices: &[crate::runtime::globals::BtnSelectChoiceState]) -> usize {
596    choices
597        .iter()
598        .position(|choice| choice.item_type == TNM_SEL_ITEM_TYPE_ON)
599        .unwrap_or(0)
600}
601
602fn dispatch_selbtn_command(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
603    let op = form_id as i32;
604    let ready = op == constants::elm_value::GLOBAL_SELBTN_READY
605        || op == constants::elm_value::GLOBAL_SELBTN_CANCEL_READY;
606    let start_now = op == constants::elm_value::GLOBAL_SELBTN
607        || op == constants::elm_value::GLOBAL_SELBTN_CANCEL
608        || op == constants::elm_value::GLOBAL_SELBTN_START;
609    if !ready && !start_now {
610        return Ok(false);
611    }
612
613    if op != constants::elm_value::GLOBAL_SELBTN_START {
614        let (template_no, choices) = parse_selbtn_choices(args);
615        let capture_flag = named_i64(args, 1).unwrap_or(0) != 0;
616        let sel_start_call_scn = args
617            .iter()
618            .find(|v| v.named_id() == Some(2))
619            .and_then(Value::as_str)
620            .unwrap_or("")
621            .to_string();
622        let sel_start_call_z_no = named_i64(args, 3).unwrap_or(0);
623        ctx.globals.selbtn.template_no = template_no;
624        ctx.globals.selbtn.choices = choices;
625        ctx.globals.selbtn.cursor = first_selectable_selbtn_choice(&ctx.globals.selbtn.choices);
626        ctx.globals.selbtn.cancel_enable = op == constants::elm_value::GLOBAL_SELBTN_CANCEL
627            || op == constants::elm_value::GLOBAL_SELBTN_CANCEL_READY;
628        ctx.globals.selbtn.capture_flag = if ready { false } else { capture_flag };
629        ctx.globals.selbtn.sel_start_call_scn = if ready {
630            String::new()
631        } else {
632            sel_start_call_scn
633        };
634        ctx.globals.selbtn.sel_start_call_z_no = if ready { 0 } else { sel_start_call_z_no };
635        ctx.globals.selbtn.result = 0;
636        prepare_stage_btnselitems(ctx);
637    }
638
639    if start_now {
640        ctx.globals.selbtn.started = true;
641        ctx.globals.selbtn.sync_type = named_i64(args, 4).unwrap_or(0);
642        ctx.globals.selbtn.read_flag_scene_no = ctx.current_scene_no.unwrap_or(-1);
643        ctx.globals.selbtn.read_flag_flag_no = -1;
644        ctx.request_read_flag_no_for_selbtn();
645        ctx.wait.wait_key();
646    }
647    Ok(true)
648}
649
650fn global_koe_state_key(_ctx: &CommandContext) -> u32 {
651    constants::fm::GLOBAL as u32
652}
653
654fn remember_global_koe(ctx: &mut CommandContext, koe_no: i64, chara_no: i64, is_ex: bool) {
655    let key = global_koe_state_key(ctx);
656    let props = ctx.globals.int_props.entry(key).or_default();
657    props.insert(constants::elm_value::GLOBAL_KOE_CHECK_GET_KOE_NO, koe_no);
658    props.insert(
659        constants::elm_value::GLOBAL_KOE_CHECK_GET_CHARA_NO,
660        chara_no,
661    );
662    props.insert(
663        constants::elm_value::GLOBAL_KOE_CHECK_IS_EX_KOE,
664        if is_ex { 1 } else { 0 },
665    );
666}
667
668fn remembered_global_koe(ctx: &CommandContext, op: i32) -> i64 {
669    let key = global_koe_state_key(ctx);
670    ctx.globals
671        .int_props
672        .get(&key)
673        .and_then(|m| m.get(&op).copied())
674        .unwrap_or(0)
675}
676
677fn dispatch_global_koe_command(
678    ctx: &mut CommandContext,
679    form_id: u32,
680    args: &[Value],
681) -> Result<bool> {
682    let op = form_id as i32;
683    let ret_form: Option<i64> = crate::runtime::forms::prop_access::current_vm_meta(ctx).1;
684    match op {
685        constants::elm_value::GLOBAL_KOE | constants::elm_value::GLOBAL_EXKOE => {
686            ctx.request_read_flag_no();
687            let is_ex = op == constants::elm_value::GLOBAL_EXKOE;
688            let koe_no = if is_ex {
689                named_i64(args, 0).or_else(|| positional_i64(args, 0))
690            } else {
691                positional_i64(args, 0)
692            }
693            .unwrap_or(0);
694            let chara_no = if is_ex {
695                named_i64(args, 1).or_else(|| positional_i64(args, 1))
696            } else {
697                positional_i64(args, 1)
698            }
699            .unwrap_or(0);
700            remember_global_koe(ctx, koe_no, chara_no, is_ex);
701            if let Err(err) = {
702                let (koe, audio) = (&mut ctx.koe, &mut ctx.audio);
703                koe.play_koe_no(audio, koe_no)
704            } {
705                eprintln!("[SG_AUDIO] koe.play failed koe_no={koe_no}: {err:#}");
706            }
707            if is_ex && named_i64(args, 2).unwrap_or(0) != 0 {
708                let key_skip = named_i64(args, 3).unwrap_or(0) != 0;
709                ctx.wait
710                    .wait_audio(crate::runtime::wait::AudioWait::KoeAny, key_skip);
711            }
712            if ret_form.unwrap_or(0) != 0 {
713                ctx.push(Value::Int(0));
714            }
715            Ok(true)
716        }
717        constants::elm_value::GLOBAL_KOE_PLAY_WAIT
718        | constants::elm_value::GLOBAL_KOE_PLAY_WAIT_KEY
719        | constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT
720        | constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY => {
721            ctx.request_read_flag_no();
722            let is_ex = op == constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT
723                || op == constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY;
724            let koe_no = if is_ex {
725                named_i64(args, 0).or_else(|| positional_i64(args, 0))
726            } else {
727                positional_i64(args, 0)
728            }
729            .unwrap_or(0);
730            let chara_no = if is_ex {
731                named_i64(args, 1).or_else(|| positional_i64(args, 1))
732            } else {
733                positional_i64(args, 1)
734            }
735            .unwrap_or(0);
736            remember_global_koe(ctx, koe_no, chara_no, is_ex);
737            if let Err(err) = {
738                let (koe, audio) = (&mut ctx.koe, &mut ctx.audio);
739                koe.play_koe_no(audio, koe_no)
740            } {
741                eprintln!("[SG_AUDIO] koe.play_wait failed koe_no={koe_no}: {err:#}");
742            }
743            let key_skip = op == constants::elm_value::GLOBAL_KOE_PLAY_WAIT_KEY
744                || op == constants::elm_value::GLOBAL_EXKOE_PLAY_WAIT_KEY;
745            ctx.wait
746                .wait_audio(crate::runtime::wait::AudioWait::KoeAny, key_skip);
747            if ret_form.unwrap_or(0) != 0 {
748                ctx.push(Value::Int(0));
749            }
750            Ok(true)
751        }
752        constants::elm_value::GLOBAL_KOE_STOP => {
753            let fade = args.get(0).and_then(Value::as_i64);
754            let _ = ctx.koe.stop(fade);
755            Ok(true)
756        }
757        constants::elm_value::GLOBAL_KOE_WAIT | constants::elm_value::GLOBAL_KOE_WAIT_KEY => {
758            let key_skip = op == constants::elm_value::GLOBAL_KOE_WAIT_KEY;
759            ctx.wait
760                .wait_audio(crate::runtime::wait::AudioWait::KoeAny, key_skip);
761            if ret_form.unwrap_or(0) != 0 {
762                ctx.push(Value::Int(0));
763            }
764            Ok(true)
765        }
766        constants::elm_value::GLOBAL_KOE_CHECK => {
767            let playing = ctx.koe.is_playing_any();
768            ctx.push(Value::Int(if playing { 1 } else { 0 }));
769            Ok(true)
770        }
771        constants::elm_value::GLOBAL_KOE_CHECK_GET_KOE_NO
772        | constants::elm_value::GLOBAL_KOE_CHECK_GET_CHARA_NO
773        | constants::elm_value::GLOBAL_KOE_CHECK_IS_EX_KOE => {
774            ctx.push(Value::Int(remembered_global_koe(ctx, op)));
775            Ok(true)
776        }
777        constants::elm_value::GLOBAL_KOE_SET_VOLUME => {
778            let vol = args
779                .get(0)
780                .and_then(Value::as_i64)
781                .unwrap_or(255)
782                .clamp(0, 255) as u8;
783            let fade = args.get(1).and_then(Value::as_i64).unwrap_or(0);
784            let _ = ctx.koe.set_volume_raw_fade(&mut ctx.audio, vol, fade);
785            Ok(true)
786        }
787        constants::elm_value::GLOBAL_KOE_SET_VOLUME_MAX => {
788            let fade = args.get(0).and_then(Value::as_i64).unwrap_or(0);
789            let _ = ctx.koe.set_volume_raw_fade(&mut ctx.audio, 255, fade);
790            Ok(true)
791        }
792        constants::elm_value::GLOBAL_KOE_SET_VOLUME_MIN => {
793            let fade = args.get(0).and_then(Value::as_i64).unwrap_or(0);
794            let _ = ctx.koe.set_volume_raw_fade(&mut ctx.audio, 0, fade);
795            Ok(true)
796        }
797        constants::elm_value::GLOBAL_KOE_GET_VOLUME => {
798            ctx.push(Value::Int(ctx.koe.volume_raw() as i64));
799            Ok(true)
800        }
801        _ => Ok(false),
802    }
803}
804
805fn parse_i32_value(v: &Value) -> Option<i32> {
806    v.unwrap_named()
807        .as_i64()
808        .and_then(|n| i32::try_from(n).ok())
809}
810
811fn parse_bool_value(v: &Value) -> Option<bool> {
812    parse_i32_value(v).map(|n| n != 0)
813}
814
815fn parse_list_i32_value(v: &Value) -> Vec<i32> {
816    match v.unwrap_named() {
817        Value::List(xs) => xs
818            .iter()
819            .filter_map(|x| x.as_i64().and_then(|n| i32::try_from(n).ok()))
820            .collect(),
821        _ => Vec::new(),
822    }
823}
824
825fn dispatch_global_fog_command(
826    ctx: &mut CommandContext,
827    form_id: u32,
828    args: &[Value],
829) -> Result<bool> {
830    let op = form_id as i32;
831    let ret_form = crate::runtime::forms::prop_access::current_vm_meta(ctx)
832        .1
833        .unwrap_or(0);
834
835    if op == constants::elm_value::GLOBAL___FOG_NAME {
836        match ret_form {
837            rf if rf == constants::fm::STR as i64 => {
838                ctx.push(Value::Str(ctx.globals.fog_global.name.clone()));
839            }
840            _ => {
841                let name = args
842                    .first()
843                    .and_then(|v| v.unwrap_named().as_str())
844                    .unwrap_or("");
845                ctx.globals.fog_global = Default::default();
846                if !name.is_empty() {
847                    match ctx.images.load_g00(name, 0) {
848                        Ok(id) => {
849                            ctx.globals.fog_global.enabled = true;
850                            ctx.globals.fog_global.name = name.to_string();
851                            ctx.globals.fog_global.texture_image_id = Some(id);
852                        }
853                        Err(e) => {
854                            log::error!(
855                                "GLOBAL.__FOG_NAME failed to load fog texture '{name}': {e}"
856                            );
857                        }
858                    }
859                }
860            }
861        }
862        return Ok(true);
863    }
864
865    if op == constants::elm_value::GLOBAL___FOG_X {
866        if ret_form != 0 {
867            ctx.push(Value::Int(ctx.globals.fog_global.x_event.get_value() as i64));
868        } else {
869            let x = args
870                .first()
871                .and_then(|v| v.unwrap_named().as_i64())
872                .unwrap_or(0) as i32;
873            ctx.globals.fog_global.set_x(x);
874        }
875        return Ok(true);
876    }
877
878    if op == constants::elm_value::GLOBAL___FOG_NEAR {
879        if ret_form != 0 {
880            ctx.push(Value::Int(ctx.globals.fog_global.near as i64));
881        } else {
882            ctx.globals.fog_global.near = args
883                .first()
884                .and_then(|v| v.unwrap_named().as_i64())
885                .unwrap_or(0) as f32;
886        }
887        return Ok(true);
888    }
889
890    if op == constants::elm_value::GLOBAL___FOG_FAR {
891        if ret_form != 0 {
892            ctx.push(Value::Int(ctx.globals.fog_global.far as i64));
893        } else {
894            ctx.globals.fog_global.far = args
895                .first()
896                .and_then(|v| v.unwrap_named().as_i64())
897                .unwrap_or(0) as f32;
898        }
899        return Ok(true);
900    }
901
902    if op != constants::elm_value::GLOBAL___FOG_X_EVE {
903        return Ok(false);
904    }
905
906    let Some((chain_pos, chain)) =
907        crate::runtime::forms::prop_access::parse_element_chain_ctx(ctx, form_id, args)
908            .map(|(i, ch)| (i, ch.to_vec()))
909    else {
910        return Ok(true);
911    };
912    if chain.len() < 2 {
913        return Ok(true);
914    }
915    let params = &args[..chain_pos];
916    match chain[1] {
917        int_event_op::SET | int_event_op::SET_REAL => {
918            let value = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
919            let total_time = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
920            let delay_time = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
921            let speed_type = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
922            let real_flag = if chain[1] == int_event_op::SET_REAL {
923                1
924            } else {
925                0
926            };
927            ctx.globals
928                .fog_global
929                .x_event
930                .set_event(value, total_time, delay_time, speed_type, real_flag);
931        }
932        int_event_op::LOOP | int_event_op::LOOP_REAL => {
933            let start_value = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
934            let end_value = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
935            let loop_time = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
936            let delay_time = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
937            let speed_type = params.get(4).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
938            let real_flag = if chain[1] == int_event_op::LOOP_REAL {
939                1
940            } else {
941                0
942            };
943            ctx.globals.fog_global.x_event.loop_event(
944                start_value,
945                end_value,
946                loop_time,
947                delay_time,
948                speed_type,
949                real_flag,
950            );
951        }
952        int_event_op::TURN | int_event_op::TURN_REAL => {
953            let start_value = params.first().and_then(|v| v.as_i64()).unwrap_or(0) as i32;
954            let end_value = params.get(1).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
955            let loop_time = params.get(2).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
956            let delay_time = params.get(3).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
957            let speed_type = params.get(4).and_then(|v| v.as_i64()).unwrap_or(0) as i32;
958            let real_flag = if chain[1] == int_event_op::TURN_REAL {
959                1
960            } else {
961                0
962            };
963            ctx.globals.fog_global.x_event.turn_event(
964                start_value,
965                end_value,
966                loop_time,
967                delay_time,
968                speed_type,
969                real_flag,
970            );
971        }
972        int_event_op::END => ctx.globals.fog_global.x_event.end_event(),
973        int_event_op::WAIT => ctx.wait.wait_fog_x_event(false, false),
974        int_event_op::WAIT_KEY => ctx.wait.wait_fog_x_event(true, true),
975        int_event_op::CHECK => {
976            ctx.push(Value::Int(
977                if ctx.globals.fog_global.x_event.check_event() {
978                    1
979                } else {
980                    0
981                },
982            ));
983        }
984        _ => {}
985    }
986    Ok(true)
987}
988
989fn resolve_wipe_mask_path(project_dir: &Path, raw: &str) -> Option<PathBuf> {
990    if raw.is_empty() {
991        return None;
992    }
993    let norm = raw.replace('\\', "/");
994    let p = Path::new(&norm);
995    if p.is_absolute() && p.is_file() {
996        return Some(p.to_path_buf());
997    }
998    let mut candidates = Vec::new();
999    candidates.push(project_dir.join(&norm));
1000    candidates.push(project_dir.join("dat").join(&norm));
1001    if p.extension().is_none() {
1002        for ext in ["png", "bmp", "jpg"] {
1003            candidates.push(project_dir.join(format!("{}.{}", norm, ext)));
1004            candidates.push(project_dir.join("dat").join(format!("{}.{}", norm, ext)));
1005        }
1006    }
1007    candidates.into_iter().find(|c| c.is_file())
1008}
1009
1010fn dispatch_global_wipe_command(
1011    ctx: &mut CommandContext,
1012    form_id: u32,
1013    args: &[Value],
1014) -> Result<bool> {
1015    let op = form_id as i32;
1016    let is_mask = matches!(
1017        op,
1018        constants::elm_value::GLOBAL_MASK_WIPE | constants::elm_value::GLOBAL_MASK_WIPE_ALL
1019    );
1020    let is_all = matches!(
1021        op,
1022        constants::elm_value::GLOBAL_WIPE_ALL | constants::elm_value::GLOBAL_MASK_WIPE_ALL
1023    );
1024
1025    if op == constants::elm_value::GLOBAL_WIPE_END {
1026        ctx.globals.finish_wipe();
1027        return Ok(true);
1028    }
1029    if op == constants::elm_value::GLOBAL_WAIT_WIPE {
1030        let key_wait_mode = args
1031            .iter()
1032            .find_map(|v| match v {
1033                Value::NamedArg { id, value } if *id == 0 => parse_i32_value(value),
1034                _ => None,
1035            })
1036            .unwrap_or(-1);
1037        let key_skip = match key_wait_mode {
1038            0 => false,
1039            1 => true,
1040            _ => {
1041                ctx.globals
1042                    .syscom
1043                    .config_int
1044                    .get(&197)
1045                    .copied()
1046                    .unwrap_or(0)
1047                    != 0
1048            }
1049        };
1050        ctx.wait.wait_wipe(key_skip);
1051        return Ok(true);
1052    }
1053    if op == constants::elm_value::GLOBAL_CHECK_WIPE {
1054        ctx.push(Value::Int(if ctx.globals.wipe_done() { 0 } else { 1 }));
1055        return Ok(true);
1056    }
1057
1058    if !matches!(
1059        op,
1060        constants::elm_value::GLOBAL_WIPE
1061            | constants::elm_value::GLOBAL_WIPE_ALL
1062            | constants::elm_value::GLOBAL_MASK_WIPE
1063            | constants::elm_value::GLOBAL_MASK_WIPE_ALL
1064    ) {
1065        return Ok(false);
1066    }
1067
1068    let mut positional: Vec<&Value> = Vec::new();
1069    let mut named: Vec<(i32, &Value)> = Vec::new();
1070    for a in args {
1071        match a {
1072            Value::NamedArg { id, value } => named.push((*id, value.as_ref())),
1073            _ => positional.push(a),
1074        }
1075    }
1076
1077    let mut mask_file: Option<String> = None;
1078    let mut wipe_type: i32 = 0;
1079    let mut wipe_time: i32 = 500;
1080    let mut speed_mode: i32 = 0;
1081    let mut start_time: i32 = 0;
1082    let mut option: Vec<i32> = Vec::new();
1083    let mut begin_order: i32 = 0;
1084    let mut end_order: i32 = if is_all { i32::MAX } else { 0 };
1085    let mut begin_layer: i32 = i32::MIN;
1086    let mut end_layer: i32 = i32::MAX;
1087    let mut wait_flag = true;
1088    let mut key_wait_mode: i32 = -1;
1089    let mut with_low_order: i32 = 0;
1090
1091    if is_mask {
1092        mask_file = positional
1093            .get(0)
1094            .and_then(|v| v.unwrap_named().as_str())
1095            .map(str::to_string);
1096        if let Some(v) = positional.get(1).and_then(|v| parse_i32_value(v)) {
1097            wipe_type = v;
1098        }
1099        if let Some(v) = positional.get(2).and_then(|v| parse_i32_value(v)) {
1100            wipe_time = v;
1101        }
1102        if let Some(v) = positional.get(3).and_then(|v| parse_i32_value(v)) {
1103            speed_mode = v;
1104        }
1105        if let Some(v) = positional.get(4) {
1106            option = parse_list_i32_value(v);
1107        }
1108    } else {
1109        if let Some(v) = positional.get(0).and_then(|v| parse_i32_value(v)) {
1110            wipe_type = v;
1111        }
1112        if let Some(v) = positional.get(1).and_then(|v| parse_i32_value(v)) {
1113            wipe_time = v;
1114        }
1115        if let Some(v) = positional.get(2).and_then(|v| parse_i32_value(v)) {
1116            speed_mode = v;
1117        }
1118        if let Some(v) = positional.get(3) {
1119            option = parse_list_i32_value(v);
1120        }
1121    }
1122
1123    for (id, v) in named {
1124        match id {
1125            0 => {
1126                if let Some(x) = parse_i32_value(v) {
1127                    wipe_type = x;
1128                }
1129            }
1130            1 => {
1131                if let Some(x) = parse_i32_value(v) {
1132                    wipe_time = x;
1133                }
1134            }
1135            2 => {
1136                if let Some(x) = parse_i32_value(v) {
1137                    speed_mode = x;
1138                }
1139            }
1140            3 => option = parse_list_i32_value(v),
1141            4 => {
1142                if let Some(x) = parse_i32_value(v) {
1143                    begin_order = x;
1144                }
1145            }
1146            5 => {
1147                if let Some(x) = parse_i32_value(v) {
1148                    end_order = x;
1149                }
1150            }
1151            6 => {
1152                if let Some(x) = parse_i32_value(v) {
1153                    begin_layer = x;
1154                }
1155            }
1156            7 => {
1157                if let Some(x) = parse_i32_value(v) {
1158                    end_layer = x;
1159                }
1160            }
1161            8 => {
1162                if let Some(x) = parse_bool_value(v) {
1163                    wait_flag = x;
1164                }
1165            }
1166            9 => {
1167                if let Some(x) = parse_i32_value(v) {
1168                    key_wait_mode = x;
1169                }
1170            }
1171            10 => {
1172                if let Some(x) = parse_i32_value(v) {
1173                    with_low_order = x;
1174                }
1175            }
1176            11 => {
1177                if let Some(x) = parse_i32_value(v) {
1178                    start_time = x;
1179                }
1180            }
1181            _ => {}
1182        }
1183    }
1184    if is_all {
1185        end_order = i32::MAX;
1186    }
1187
1188    let mask_image_id = mask_file.as_ref().and_then(|f| {
1189        resolve_wipe_mask_path(&ctx.project_dir, f).and_then(|p| ctx.images.load_file(&p, 0).ok())
1190    });
1191
1192    stage::apply_stage_wipe(ctx, begin_order, end_order, begin_layer, end_layer);
1193    ctx.globals.start_wipe(WipeState::new(
1194        mask_file,
1195        mask_image_id,
1196        wipe_type,
1197        wipe_time,
1198        start_time,
1199        speed_mode,
1200        option,
1201        begin_order,
1202        end_order,
1203        begin_layer,
1204        end_layer,
1205        wait_flag,
1206        key_wait_mode,
1207        with_low_order,
1208    ));
1209
1210    if wait_flag {
1211        let key_skip = match key_wait_mode {
1212            0 => false,
1213            1 => true,
1214            _ => {
1215                ctx.globals
1216                    .syscom
1217                    .config_int
1218                    .get(&197)
1219                    .copied()
1220                    .unwrap_or(0)
1221                    != 0
1222            }
1223        };
1224        ctx.wait.wait_wipe(key_skip);
1225    }
1226    Ok(true)
1227}
1228
1229fn dispatch_capture_command(
1230    ctx: &mut CommandContext,
1231    form_id: u32,
1232    args: &[Value],
1233) -> Result<bool> {
1234    match form_id as i32 {
1235        constants::elm_value::GLOBAL_CAPTURE => {
1236            let img = ctx.capture_frame_rgba();
1237            ctx.globals.capture_image = Some(img.clone());
1238            crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture(ctx);
1239            ctx.push(Value::Int(0));
1240            Ok(true)
1241        }
1242        constants::elm_value::GLOBAL_CAPTURE_FREE => {
1243            ctx.globals.capture_image = None;
1244            ctx.globals.save_thumb_capture_image = None;
1245            ctx.push(Value::Int(0));
1246            Ok(true)
1247        }
1248        constants::elm_value::GLOBAL_CAPTURE_FROM_FILE => {
1249            let Some(file) = args.get(0).and_then(|v| v.as_str()) else {
1250                panic!("GLOBAL.CAPTURE_FROM_FILE requires file name");
1251            };
1252            let Some(path) =
1253                stage::resolve_capture_file_path(&ctx.project_dir, &ctx.globals.append_dir, file)
1254            else {
1255                panic!("GLOBAL.CAPTURE_FROM_FILE cannot resolve file: {file}");
1256            };
1257            let img_id = ctx.images.load_file(&path, 0).unwrap_or_else(|e| {
1258                panic!(
1259                    "GLOBAL.CAPTURE_FROM_FILE failed to load {}: {e}",
1260                    path.display()
1261                )
1262            });
1263            let img = ctx
1264                .images
1265                .get(img_id)
1266                .map(|img| img.as_ref().clone())
1267                .unwrap_or_else(|| {
1268                    panic!(
1269                        "GLOBAL.CAPTURE_FROM_FILE image disappeared: {}",
1270                        path.display()
1271                    )
1272                });
1273            crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture_from_image(ctx, &img);
1274            ctx.globals.capture_image = Some(img);
1275            ctx.push(Value::Int(0));
1276            Ok(true)
1277        }
1278        constants::elm_value::GLOBAL_CAPTURE_FOR_OBJECT => {
1279            let has_range = named_i64(args, 0).is_some() || named_i64(args, 1).is_some();
1280            let img = if has_range {
1281                let end_order = named_i64(args, 0).unwrap_or(i32::MAX as i64 / 1024);
1282                let end_layer = named_i64(args, 1).unwrap_or(1023);
1283                ctx.capture_frame_rgba_until(end_order, end_layer)
1284            } else {
1285                ctx.capture_frame_rgba()
1286            };
1287            ctx.globals.capture_for_object_image = Some(img);
1288            ctx.push(Value::Int(0));
1289            Ok(true)
1290        }
1291        constants::elm_value::GLOBAL_CAPTURE_FOR_OBJECT_FREE => {
1292            ctx.globals.capture_for_object_image = None;
1293            ctx.push(Value::Int(0));
1294            Ok(true)
1295        }
1296        constants::elm_value::GLOBAL_CAPTURE_FOR_LOCAL_SAVE => {
1297            let end_order = named_i64(args, 0).unwrap_or(i32::MAX as i64 / 1024);
1298            let end_layer = named_i64(args, 1).unwrap_or(1023);
1299            let width = named_i64(args, 3).unwrap_or(ctx.screen_w as i64).max(1) as u32;
1300            let height = named_i64(args, 4).unwrap_or(ctx.screen_h as i64).max(1) as u32;
1301            let img = ctx.capture_frame_rgba_until(end_order, end_layer);
1302            let resized = crate::runtime::forms::syscom::resize_capture_rgba_nearest(
1303                &img,
1304                width,
1305                height,
1306            );
1307            ctx.globals.capture_for_object_image = Some(resized);
1308            ctx.push(Value::Int(1));
1309            Ok(true)
1310        }
1311        constants::elm_value::GLOBAL_CAPTURE_FOR_TWEET => {
1312            let img = ctx.capture_frame_rgba();
1313            ctx.globals.capture_image = Some(img);
1314            ctx.push(Value::Int(0));
1315            Ok(true)
1316        }
1317        constants::elm_value::GLOBAL_CAPTURE_FREE_FOR_TWEET => {
1318            ctx.globals.capture_image = None;
1319            ctx.push(Value::Int(0));
1320            Ok(true)
1321        }
1322        _ => Ok(false),
1323    }
1324}
1325
1326fn push_global_message_ok(ctx: &mut CommandContext) {
1327    let ret_form = crate::runtime::forms::prop_access::current_vm_meta(ctx)
1328        .1
1329        .unwrap_or(0);
1330    if ret_form == 0 {
1331        return;
1332    }
1333    if ret_form == constants::fm::STR as i64 {
1334        ctx.push(Value::Str(String::new()));
1335    } else {
1336        ctx.push(Value::Int(0));
1337    }
1338}
1339
1340fn global_message_arg_str(args: &[Value]) -> Option<&str> {
1341    args.iter().rev().find_map(|v| v.unwrap_named().as_str())
1342}
1343
1344fn dispatch_global_message_command(
1345    ctx: &mut CommandContext,
1346    form_id: u32,
1347    args: &[Value],
1348) -> Result<bool> {
1349    match form_id as i32 {
1350        constants::elm_value::GLOBAL_MESSAGE_BOX => {
1351            let text = global_message_arg_str(args).unwrap_or("").to_string();
1352            ctx.request_system_messagebox_no_return(
1353                17,
1354                false,
1355                text,
1356                vec![crate::runtime::globals::SystemMessageBoxButton {
1357                    label: "OK".to_string(),
1358                    value: 0,
1359                }],
1360            );
1361            Ok(true)
1362        }
1363        constants::elm_value::GLOBAL_GET_LAST_SEL_MSG => {
1364            ctx.push(Value::Str(ctx.globals.syscom.system_extra_str_value.clone()));
1365            Ok(true)
1366        }
1367        constants::elm_value::GLOBAL_OPEN
1368        | constants::elm_value::GLOBAL_OPEN_WAIT
1369        | constants::elm_value::GLOBAL_OPEN_NOWAIT => {
1370            ctx.ui.show_message_bg(true);
1371            push_global_message_ok(ctx);
1372            Ok(true)
1373        }
1374        constants::elm_value::GLOBAL_CLOSE
1375        | constants::elm_value::GLOBAL_CLOSE_WAIT
1376        | constants::elm_value::GLOBAL_CLOSE_NOWAIT => {
1377            ctx.ui.show_message_bg(false);
1378            push_global_message_ok(ctx);
1379            Ok(true)
1380        }
1381        constants::elm_value::GLOBAL_END_CLOSE => {
1382            // C++ GLOBAL.END_CLOSE dispatches MWND.END_CLOSE for the current
1383            // message window. If the stage/current-MWND route above did not
1384            // handle it, this fallback must not perform CLOSE semantics.
1385            push_global_message_ok(ctx);
1386            Ok(true)
1387        }
1388        constants::elm_value::GLOBAL_MSG_BLOCK | constants::elm_value::GLOBAL_MSG_PP_BLOCK => {
1389            // Message block commands update/forward message state only. They must not
1390            // create a script-proc boundary; WAIT_MSG / PP / R / PAGE are the commands
1391            // that actually stop the running script.
1392            push_global_message_ok(ctx);
1393            Ok(true)
1394        }
1395        constants::elm_value::GLOBAL_CLEAR => {
1396            ctx.ui.clear_message();
1397            ctx.ui.clear_name();
1398            push_global_message_ok(ctx);
1399            Ok(true)
1400        }
1401        constants::elm_value::GLOBAL_CLEAR_MSGBK => {
1402            ctx.ui.clear_message();
1403            let form_id = ctx.ids.form_global_msgbk;
1404            if form_id != 0 {
1405                ctx.globals.msgbk_forms.entry(form_id).or_default().clear();
1406            }
1407            push_global_message_ok(ctx);
1408            Ok(true)
1409        }
1410        constants::elm_value::GLOBAL_PRINT => {
1411            ctx.request_read_flag_no();
1412            if let Some(s) = global_message_arg_str(args) {
1413                if !s.is_empty() {
1414                    syscom::append_current_save_message(ctx, s);
1415                    ctx.ui.show_message_bg(true);
1416                    ctx.ui.append_message(s);
1417                }
1418            }
1419            push_global_message_ok(ctx);
1420            Ok(true)
1421        }
1422        constants::elm_value::GLOBAL_NL | constants::elm_value::GLOBAL_NLI => {
1423            ctx.ui.append_linebreak();
1424            push_global_message_ok(ctx);
1425            Ok(true)
1426        }
1427        constants::elm_value::GLOBAL_WAIT_MSG | constants::elm_value::GLOBAL_PP => {
1428            ctx.ui.begin_wait_message();
1429            ctx.wait.wait_key();
1430            ctx.request_message_wait_proc_boundary();
1431            push_global_message_ok(ctx);
1432            Ok(true)
1433        }
1434        constants::elm_value::GLOBAL_R | constants::elm_value::GLOBAL_PAGE => {
1435            if (form_id as i32) == constants::elm_value::GLOBAL_PAGE {
1436                ctx.ui.begin_wait_page_message();
1437            } else {
1438                ctx.ui.begin_wait_message();
1439            }
1440            ctx.ui.request_clear_message_on_wait_end();
1441            ctx.wait.wait_key();
1442            ctx.request_message_wait_proc_boundary();
1443            push_global_message_ok(ctx);
1444            Ok(true)
1445        }
1446        constants::elm_value::GLOBAL_SET_NAMAE => {
1447            let name = global_message_arg_str(args).unwrap_or("");
1448            if !stage::cd_name_current_mwnd(ctx, name) {
1449                ctx.ui.set_name(name.to_string());
1450            }
1451            push_global_message_ok(ctx);
1452            Ok(true)
1453        }
1454        constants::elm_value::GLOBAL_CLEAR_FACE
1455        | constants::elm_value::GLOBAL_SET_FACE
1456        | constants::elm_value::GLOBAL_SIZE
1457        | constants::elm_value::GLOBAL_COLOR
1458        | constants::elm_value::GLOBAL_RUBY
1459        | constants::elm_value::GLOBAL_MSGBTN
1460        | constants::elm_value::GLOBAL_MULTI_MSG
1461        | constants::elm_value::GLOBAL_NEXT_MSG
1462        | constants::elm_value::GLOBAL_START_SLIDE_MSG
1463        | constants::elm_value::GLOBAL_END_SLIDE_MSG
1464        | constants::elm_value::GLOBAL_INDENT
1465        | constants::elm_value::GLOBAL_CLEAR_INDENT
1466        | constants::elm_value::GLOBAL_REP_POS
1467        | constants::elm_value::GLOBAL_SET_WAKU => {
1468            push_global_message_ok(ctx);
1469            Ok(true)
1470        }
1471        _ => Ok(false),
1472    }
1473}
1474
1475pub fn dispatch_global_form(
1476    ctx: &mut CommandContext,
1477    form_id: u32,
1478    args: &[Value],
1479) -> Result<bool> {
1480    let form_id = canonical_global_form_id(ctx, form_id);
1481
1482    if dispatch_global_wipe_command(ctx, form_id, args)? {
1483        return Ok(true);
1484    }
1485    if dispatch_capture_command(ctx, form_id, args)? {
1486        return Ok(true);
1487    }
1488    if dispatch_global_fog_command(ctx, form_id, args)? {
1489        return Ok(true);
1490    }
1491    if dispatch_selbtn_command(ctx, form_id, args)? {
1492        return Ok(true);
1493    }
1494    if stage::dispatch_current_mwnd_global_op(ctx, form_id as i32, args) {
1495        return Ok(true);
1496    }
1497    if dispatch_global_koe_command(ctx, form_id, args)? {
1498        return Ok(true);
1499    }
1500    if dispatch_global_message_command(ctx, form_id, args)? {
1501        return Ok(true);
1502    }
1503
1504    // Same-version testcase still uses compact startup aliases that bypass the
1505    // canonical global-form ids. Keep them routed to their original handlers.
1506    if form_id == 24 {
1507        return keylist::dispatch(ctx, args);
1508    }
1509    if form_id == 40 {
1510        return counter::dispatch(ctx, form_id, args);
1511    }
1512    if form_id == 63 {
1513        if syscom::dispatch(ctx, form_id, args)? {
1514            return Ok(true);
1515        }
1516    }
1517    if form_id == 64 {
1518        if script::dispatch(ctx, form_id, args)? {
1519            return Ok(true);
1520        }
1521    }
1522    if form_id == 46 {
1523        return mouse::dispatch(ctx, args);
1524    }
1525    if form_id == 86 {
1526        if input::dispatch(ctx, form_id, args)? {
1527            return Ok(true);
1528        }
1529    }
1530    if form_id == 92 {
1531        if system::dispatch(ctx, form_id, args)? {
1532            return Ok(true);
1533        }
1534    }
1535    if form_id == constants::elm_value::GLOBAL_DISP as u32 {
1536        ctx.wait.wait_next_frame(ctx.globals.render_frame);
1537        ctx.request_disp_proc_boundary();
1538        return Ok(true);
1539    }
1540    if form_id == constants::elm_value::GLOBAL_FRAME as u32 {
1541        ctx.wait.wait_next_frame(ctx.globals.render_frame);
1542        ctx.request_proc_boundary(crate::runtime::ProcKind::Frame);
1543        return Ok(true);
1544    }
1545    if form_id == constants::elm_value::GLOBAL_SET_MWND as u32
1546        || form_id == constants::elm_value::GLOBAL_SET_SEL_MWND as u32
1547    {
1548        let next = args.iter().find_map(mwnd_ref_from_value);
1549        if form_id == constants::elm_value::GLOBAL_SET_SEL_MWND as u32 {
1550            if let Some((stage, no)) = next {
1551                ctx.globals.current_sel_mwnd_stage_idx = stage;
1552                ctx.globals.current_sel_mwnd_no = Some(no);
1553            }
1554        } else if let Some((stage, no)) = next {
1555            ctx.globals.current_mwnd_stage_idx = stage;
1556            ctx.globals.current_mwnd_no = Some(no);
1557            ctx.globals.last_mwnd_stage_idx = stage;
1558            ctx.globals.last_mwnd_no = Some(no);
1559        }
1560        return Ok(true);
1561    }
1562    if form_id == constants::elm_value::GLOBAL_GET_MWND as u32
1563        || form_id == constants::elm_value::GLOBAL_GET_SEL_MWND as u32
1564    {
1565        let no = if form_id == constants::elm_value::GLOBAL_GET_SEL_MWND as u32 {
1566            ctx.globals.current_sel_mwnd_no
1567        } else {
1568            ctx.globals.current_mwnd_no
1569        };
1570        // C++ returns -1 only when no current MWND element resolves.
1571        ctx.push(Value::Int(no.map(|n| n as i64).unwrap_or(-1)));
1572        return Ok(true);
1573    }
1574    if form_id == constants::elm_value::GLOBAL_SET_TITLE as u32 {
1575        // Original engine forwards this to the OS window caption.  The winit
1576        // shell owns the actual window, so keep VM semantics as a successful
1577        // side-effect command here.
1578        return Ok(true);
1579    }
1580
1581    if form_id == constants::global_form::STAGE_ALT {
1582        return stage::dispatch(ctx, args);
1583    }
1584    if form_id == constants::global_form::BGM {
1585        return forms::bgm::dispatch(ctx, args);
1586    }
1587    if form_id == constants::global_form::BGMTABLE {
1588        return forms::bgm_table::dispatch(ctx, args);
1589    }
1590    if form_id == constants::global_form::MOV {
1591        return forms::mov::dispatch(ctx, args);
1592    }
1593    if form_id == constants::global_form::PCM {
1594        return forms::pcm::dispatch(ctx, args);
1595    }
1596    if form_id == constants::global_form::PCMCH {
1597        return forms::pcmch::dispatch(ctx, form_id, args);
1598    }
1599    if form_id == constants::global_form::SE {
1600        return forms::se::dispatch(ctx, args);
1601    }
1602    if form_id == constants::global_form::PCMEVENT {
1603        return forms::pcmevent::dispatch(ctx, args);
1604    }
1605    if form_id == constants::global_form::EXCALL {
1606        return forms::excall::dispatch(ctx, args);
1607    }
1608    if form_id == constants::global_form::KOE_ST {
1609        return forms::koe_st::dispatch(ctx, args);
1610    }
1611    if form_id == ctx.ids.form_global_input {
1612        return input::dispatch(ctx, form_id, args);
1613    }
1614    if form_id == ctx.ids.form_global_mouse {
1615        return mouse::dispatch(ctx, args);
1616    }
1617    if form_id == ctx.ids.form_global_keylist {
1618        return keylist::dispatch(ctx, args);
1619    }
1620    if form_id == constants::global_form::KEY {
1621        return key::dispatch(ctx, args);
1622    }
1623    if form_id == constants::global_form::SCREEN {
1624        return forms::screen::dispatch(ctx, args);
1625    }
1626    if form_id == constants::global_form::MSGBK {
1627        return forms::msgbk::dispatch(ctx, args);
1628    }
1629    if ctx.ids.form_global_math != 0 && form_id == ctx.ids.form_global_math {
1630        return math::dispatch(ctx, form_id, args);
1631    }
1632    if ctx.ids.form_global_cgtable != 0 && form_id == ctx.ids.form_global_cgtable {
1633        return cgtable::dispatch(ctx, form_id, args);
1634    }
1635    if ctx.ids.form_global_database != 0 && form_id == ctx.ids.form_global_database {
1636        return database::dispatch(ctx, form_id, args);
1637    }
1638    if ctx.ids.form_global_g00buf != 0 && form_id == ctx.ids.form_global_g00buf {
1639        return g00buf::dispatch(ctx, form_id, args);
1640    }
1641    if ctx.ids.form_global_mask != 0 && form_id == ctx.ids.form_global_mask {
1642        return mask::dispatch(ctx, form_id, args);
1643    }
1644    if ctx.ids.form_global_editbox != 0 && form_id == ctx.ids.form_global_editbox {
1645        return editbox::dispatch(ctx, form_id, args);
1646    }
1647    if ctx.ids.form_global_file != 0 && form_id == ctx.ids.form_global_file {
1648        return file::dispatch(ctx, form_id, args);
1649    }
1650    if ctx.ids.form_global_steam != 0 && form_id == ctx.ids.form_global_steam {
1651        return steam::dispatch(ctx, form_id, args);
1652    }
1653    if ctx.ids.form_global_syscom != 0 && form_id == ctx.ids.form_global_syscom {
1654        return syscom::dispatch(ctx, form_id, args);
1655    }
1656    if ctx.ids.form_global_script != 0 && form_id == ctx.ids.form_global_script {
1657        return script::dispatch(ctx, form_id, args);
1658    }
1659    if ctx.ids.form_global_system != 0 && form_id == ctx.ids.form_global_system {
1660        return system::dispatch(ctx, form_id, args);
1661    }
1662    if form_id == constants::global_form::FRAME_ACTION {
1663        return frame_action::dispatch(ctx, form_id, args);
1664    }
1665    if ctx.ids.form_global_frame_action_ch != 0 && form_id == ctx.ids.form_global_frame_action_ch {
1666        return frame_action_ch::dispatch(ctx, form_id, args);
1667    }
1668
1669    match form_id {
1670        constants::global_form::BGM => forms::bgm::dispatch(ctx, args),
1671        constants::global_form::BGMTABLE => forms::bgm_table::dispatch(ctx, args),
1672        constants::global_form::MOV => forms::mov::dispatch(ctx, args),
1673        constants::global_form::PCM => forms::pcm::dispatch(ctx, args),
1674        constants::global_form::PCMCH => forms::pcmch::dispatch(ctx, form_id, args),
1675        constants::global_form::SE => forms::se::dispatch(ctx, args),
1676        constants::global_form::PCMEVENT => forms::pcmevent::dispatch(ctx, args),
1677        constants::global_form::EXCALL => forms::excall::dispatch(ctx, args),
1678        constants::global_form::KOE_ST => forms::koe_st::dispatch(ctx, args),
1679        constants::global_form::SCREEN => forms::screen::dispatch(ctx, args),
1680        constants::global_form::MSGBK => forms::msgbk::dispatch(ctx, args),
1681        constants::global_form::KEY => key::dispatch(ctx, args),
1682        _ => {
1683            // TIMEWAIT/TIMEWAIT_KEY are statement-like forms that block execution.
1684            if form_id == constants::global_form::TIMEWAIT {
1685                return timewait::dispatch(ctx, false, args);
1686            }
1687            if form_id == constants::global_form::TIMEWAIT_KEY {
1688                return timewait::dispatch(ctx, true, args);
1689            }
1690
1691            if form_id as i32 == constants::fm::INTEVENT
1692                || form_id as i32 == constants::fm::INTEVENTLIST
1693            {
1694                return int_event::dispatch(ctx, form_id, args);
1695            }
1696
1697            if form_id as i32 == constants::fm::OBJECTEVENT {
1698                return object_event::dispatch(ctx, args);
1699            }
1700            if form_id as i32 == crate::runtime::forms::codes::FM_OBJECTEVENTLIST {
1701                return object_event::dispatch_list(ctx, args);
1702            }
1703
1704            if constants::global_form::INT_LIST_FORMS.contains(&form_id) {
1705                return int_list::dispatch(ctx, form_id, args);
1706            }
1707            if constants::global_form::STR_LIST_FORMS.contains(&form_id) {
1708                return str_list::dispatch(ctx, form_id, args);
1709            }
1710
1711            if form_id == constants::global_form::COUNTER {
1712                return counter::dispatch(ctx, form_id, args);
1713            }
1714
1715            if form_id == constants::global_form::FRAME_ACTION {
1716                return int_list::dispatch(ctx, form_id, args);
1717            }
1718
1719            Ok(false)
1720        }
1721    }
1722}