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 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 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 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 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 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 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 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 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 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 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 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
1870fn 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
1881fn 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 push_global_message_ok(ctx);
1958 Ok(true)
1959 }
1960 constants::elm_value::GLOBAL_MSG_BLOCK | constants::elm_value::GLOBAL_MSG_PP_BLOCK => {
1961 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 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 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 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 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 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 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 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 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 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 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 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 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}