Skip to main content

siglus_scene_vm/runtime/
ui.rs

1//! Message-window rendering state projected from runtime MWND state.
2
3use crate::image_manager::ImageId;
4use crate::layer::{LayerId, Sprite, SpriteFit, SpriteId, SpriteSizeMode};
5use crate::runtime::globals::{EditBoxListState, ScriptRuntimeState, SyscomRuntimeState};
6use crate::text_render::{FontCache, TextStyle};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use crate::platform_time::{Duration, Instant};
10
11#[derive(Debug, Clone, Copy)]
12struct UiRect {
13    x: i32,
14    y: i32,
15    w: u32,
16    h: u32,
17}
18
19impl UiRect {
20    fn new(x: i32, y: i32, w: u32, h: u32) -> Self {
21        Self { x, y, w, h }
22    }
23}
24
25#[derive(Debug, Clone, Copy)]
26struct UiWindowAnim {
27    dx: i32,
28    dy: i32,
29    scale_x: f32,
30    scale_y: f32,
31    rotate: f32,
32    alpha: u8,
33    pivot_abs_x: f32,
34    pivot_abs_y: f32,
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct MwndWindowRenderState {
39    pub x: i32,
40    pub y: i32,
41    pub w: u32,
42    pub h: u32,
43    pub dx: i32,
44    pub dy: i32,
45    pub scale_x: f32,
46    pub scale_y: f32,
47    pub rotate: f32,
48    pub alpha: u8,
49    pub pivot_abs_x: f32,
50    pub pivot_abs_y: f32,
51}
52
53/// UI-side sprite cache for the message-window family and related overlays.
54#[derive(Debug, Default)]
55pub struct MwndWakuRuntime {
56    pub bg_sprite: Option<SpriteId>,
57    pub filter_sprite: Option<SpriteId>,
58    pub bg_image: Option<ImageId>,
59    pub filter_image: Option<ImageId>,
60    pub solid_filter_image: Option<ImageId>,
61    pub bg_file: Option<String>,
62    pub filter_file: Option<String>,
63    pub bg_size: Option<(u32, u32)>,
64    pub filter_size: Option<(u32, u32)>,
65    pub filter_margin: (i64, i64, i64, i64),
66    pub filter_color: (u8, u8, u8, u8),
67    pub filter_config_color: bool,
68    pub filter_config_tr: bool,
69}
70
71#[derive(Debug, Default)]
72pub struct MwndFaceRuntime {
73    pub sprite: Option<SpriteId>,
74    pub image: Option<ImageId>,
75    pub file: Option<String>,
76    pub no: i64,
77    pub rep_pos: Option<(i64, i64)>,
78}
79
80#[derive(Debug, Default)]
81pub struct MwndNameRuntime {
82    pub text_sprite: Option<SpriteId>,
83    pub text_image: Option<ImageId>,
84    pub text: Option<String>,
85    pub text_dirty: bool,
86}
87
88#[derive(Debug, Default)]
89pub struct MwndKeyIconRuntime {
90    pub sprite: Option<SpriteId>,
91    pub image: Option<ImageId>,
92    pub file: Option<String>,
93    pub cached_mode: i64,
94    pub cached_pat: i64,
95    pub size: Option<(u32, u32)>,
96    pub key_file: Option<String>,
97    pub key_pat_cnt: i64,
98    pub key_speed: i64,
99    pub page_file: Option<String>,
100    pub page_pat_cnt: i64,
101    pub page_speed: i64,
102    pub appear: bool,
103    pub mode: i64,
104    pub anime_start: Option<Instant>,
105    pub icon_pos_type: i64,
106    pub icon_pos_base: i64,
107    pub icon_pos: (i64, i64, i64),
108}
109
110#[derive(Debug, Default)]
111pub struct MwndMsgRuntime {
112    pub text_sprite: Option<SpriteId>,
113    pub text_image: Option<ImageId>,
114    pub text: Option<String>,
115    pub waiting: bool,
116    pub wait_started_at: Option<Instant>,
117    pub wait_message_len: usize,
118    pub reveal_start: Option<Instant>,
119    pub visible_chars: usize,
120    pub reveal_base: usize,
121    pub slide_started_at: Option<Instant>,
122    pub slide_enabled: bool,
123    pub slide_time_ms: u64,
124    pub clear_on_wait_end: bool,
125    pub text_dirty: bool,
126}
127
128#[derive(Debug, Default)]
129pub struct MwndWindowRuntime {
130    pub pos: Option<(i32, i32)>,
131    pub size: Option<(u32, u32)>,
132    pub message_pos: Option<(i32, i32)>,
133    pub message_margin: Option<(i64, i64, i64, i64)>,
134    pub moji_cnt: Option<(i64, i64)>,
135    pub moji_size: Option<i64>,
136    pub moji_space: Option<(i64, i64)>,
137    pub extend_type: i64,
138    pub moji_color: Option<i64>,
139    pub shadow_color: Option<i64>,
140    pub fuchi_color: Option<i64>,
141}
142
143#[derive(Debug, Default)]
144pub struct MwndAnimRuntime {
145    pub visible: bool,
146    pub target_visible: bool,
147    pub progress: f32,
148    pub from: f32,
149    pub to: f32,
150    pub started_at: Option<Instant>,
151    pub duration_ms: u64,
152    pub anim_type: i64,
153    pub clear_text_on_close_end: bool,
154}
155
156#[derive(Debug, Default)]
157pub struct MwndRuntime {
158    pub layer: Option<LayerId>,
159    pub projection_active: bool,
160    pub waku: MwndWakuRuntime,
161    pub face: MwndFaceRuntime,
162    pub name: MwndNameRuntime,
163    pub key_icon: MwndKeyIconRuntime,
164    pub msg: MwndMsgRuntime,
165    pub window: MwndWindowRuntime,
166    pub anim: MwndAnimRuntime,
167}
168
169#[derive(Debug, Default)]
170pub struct SysOverlayRuntime {
171    pub active: bool,
172    pub bg_sprite: Option<SpriteId>,
173    pub text_sprite: Option<SpriteId>,
174    pub bg_image: Option<ImageId>,
175    pub text_image: Option<ImageId>,
176    pub text: String,
177    pub text_dirty: bool,
178}
179
180#[derive(Debug, Clone)]
181pub struct MsgBackTextProjection {
182    pub history_index: usize,
183    pub text: String,
184    pub x: i32,
185    pub y: i32,
186    pub width: u32,
187    pub height: u32,
188    pub style: TextStyle,
189}
190
191#[derive(Debug, Clone)]
192pub struct MsgBackImageProjection {
193    pub file: Option<String>,
194    pub x: i32,
195    pub y: i32,
196}
197
198#[derive(Debug, Clone)]
199pub struct MsgBackEntryButtonProjection {
200    pub history_index: usize,
201    pub file: Option<String>,
202    pub x: i32,
203    pub y: i32,
204}
205
206#[derive(Debug, Clone)]
207pub struct MsgBackUiProjection {
208    pub window_x: i32,
209    pub window_y: i32,
210    pub window_w: u32,
211    pub window_h: u32,
212    pub disp_margin: (i64, i64, i64, i64),
213    pub msg_pos: i32,
214    pub moji_size: i64,
215    pub moji_space: Option<(i64, i64)>,
216    pub order: i32,
217    pub filter_layer_rep: i32,
218    pub waku_layer_rep: i32,
219    pub moji_layer_rep: i32,
220    pub waku_file: Option<String>,
221    pub filter_file: Option<String>,
222    pub filter_margin: (i64, i64, i64, i64),
223    /// MSGBK.FILTER_COLOR, used when no filter texture is configured.
224    pub filter_rgba: (u8, u8, u8, u8),
225    /// Runtime CONFIG.FILTER_COLOR/Gp_config->filter_color, applied to the filter sprite.
226    pub filter_config_rgba: (u8, u8, u8, u8),
227    pub text_entries: Vec<MsgBackTextProjection>,
228    pub separators: Vec<MsgBackImageProjection>,
229    pub koe_buttons: Vec<MsgBackEntryButtonProjection>,
230    pub load_buttons: Vec<MsgBackEntryButtonProjection>,
231    pub close_btn_file: Option<String>,
232    pub close_btn_pos: (i32, i32),
233    pub msg_up_btn_file: Option<String>,
234    pub msg_up_btn_pos: (i32, i32),
235    pub msg_down_btn_file: Option<String>,
236    pub msg_down_btn_pos: (i32, i32),
237    pub slider_file: Option<String>,
238    pub slider_rect: (i32, i32, i32, i32),
239    pub slider_pos: (i32, i32),
240    pub ex_btn_files: [Option<String>; 4],
241    pub ex_btn_pos: [(i32, i32); 4],
242}
243
244
245fn msg_back_packed_sorter_key(order: i32, layer: i32) -> i32 {
246    let packed = (order as i64)
247        .clamp(i32::MIN as i64 / 1024, i32::MAX as i64 / 1024)
248        .saturating_mul(1024)
249        .saturating_add((layer as i64).clamp(-1023, 1023));
250    packed as i32
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum MsgBackHitAction {
255    Close,
256    Up,
257    Down,
258    Slider,
259}
260
261#[derive(Debug, Default)]
262pub struct MsgBackButtonRuntime {
263    pub sprite: Option<SpriteId>,
264    pub image: Option<ImageId>,
265    pub cached_file: Option<String>,
266    pub size: Option<(u32, u32)>,
267    pub center: Option<(i32, i32)>,
268}
269
270#[derive(Debug, Default)]
271pub struct MsgBackTextRuntime {
272    pub sprite: Option<SpriteId>,
273    pub image: Option<ImageId>,
274}
275
276#[derive(Debug, Default)]
277pub struct MsgBackRuntime {
278    pub projection: Option<MsgBackUiProjection>,
279    pub waku_sprite: Option<SpriteId>,
280    pub filter_sprite: Option<SpriteId>,
281    pub text_sprite: Option<SpriteId>,
282    pub waku_image: Option<ImageId>,
283    pub filter_image: Option<ImageId>,
284    pub solid_filter_image: Option<ImageId>,
285    pub solid_filter_color: Option<(u8, u8, u8, u8)>,
286    pub text_image: Option<ImageId>,
287    pub cached_waku_file: Option<String>,
288    pub cached_filter_file: Option<String>,
289    pub text_dirty: bool,
290    pub text_entries: Vec<MsgBackTextRuntime>,
291    pub separators: Vec<MsgBackButtonRuntime>,
292    pub koe_buttons: Vec<MsgBackButtonRuntime>,
293    pub load_buttons: Vec<MsgBackButtonRuntime>,
294    pub close_btn: MsgBackButtonRuntime,
295    pub msg_up_btn: MsgBackButtonRuntime,
296    pub msg_down_btn: MsgBackButtonRuntime,
297    pub slider: MsgBackButtonRuntime,
298    pub ex_buttons: Vec<MsgBackButtonRuntime>,
299}
300
301#[derive(Debug, Default)]
302pub struct EditBoxOverlayEntry {
303    pub bg_sprite: Option<SpriteId>,
304    pub text_sprite: Option<SpriteId>,
305    pub text_image: Option<ImageId>,
306    pub last_text: String,
307    pub last_w: u32,
308    pub last_h: u32,
309    pub last_font_px: u32,
310    pub last_focused: bool,
311}
312
313#[derive(Debug, Default)]
314pub struct EditBoxOverlayRuntime {
315    pub layer: Option<LayerId>,
316    pub bg_image: Option<ImageId>,
317    pub focused_bg_image: Option<ImageId>,
318    pub entries: HashMap<(u32, usize), EditBoxOverlayEntry>,
319}
320
321#[derive(Debug, Default, Clone)]
322pub struct MwndProjectionState {
323    pub bg_file: Option<String>,
324    pub filter_file: Option<String>,
325    pub filter_margin: Option<(i64, i64, i64, i64)>,
326    pub filter_color: Option<(u8, u8, u8, u8)>,
327    pub filter_config_color: bool,
328    pub filter_config_tr: bool,
329    pub face_file: Option<String>,
330    pub face_no: i64,
331    pub rep_pos: Option<(i64, i64)>,
332    pub window_pos: Option<(i64, i64)>,
333    pub window_size: Option<(i64, i64)>,
334    pub message_pos: Option<(i64, i64)>,
335    pub message_margin: Option<(i64, i64, i64, i64)>,
336    pub window_moji_cnt: Option<(i64, i64)>,
337    pub moji_size: Option<i64>,
338    pub moji_space: Option<(i64, i64)>,
339    pub mwnd_extend_type: i64,
340    pub moji_color: Option<i64>,
341    pub shadow_color: Option<i64>,
342    pub fuchi_color: Option<i64>,
343    pub chara_moji_color: Option<i64>,
344    pub chara_shadow_color: Option<i64>,
345    pub chara_fuchi_color: Option<i64>,
346    pub name_moji_color: Option<i64>,
347    pub name_shadow_color: Option<i64>,
348    pub name_fuchi_color: Option<i64>,
349    pub key_icon_file: Option<String>,
350    pub key_icon_pat_cnt: i64,
351    pub key_icon_speed: i64,
352    pub page_icon_file: Option<String>,
353    pub page_icon_pat_cnt: i64,
354    pub page_icon_speed: i64,
355    pub key_icon_appear: bool,
356    pub key_icon_mode: i64,
357    pub key_icon_pos: Option<(i64, i64)>,
358    pub icon_pos_type: i64,
359    pub icon_pos_base: i64,
360    pub icon_pos: Option<(i64, i64, i64)>,
361    pub slide_enabled: bool,
362    pub slide_time: i64,
363    pub name_text: String,
364    pub msg_text: String,
365}
366
367#[derive(Debug, Default)]
368pub struct UiRuntime {
369    pub mwnd: MwndRuntime,
370    pub sys: SysOverlayRuntime,
371    pub msg_back: MsgBackRuntime,
372    pub editbox: EditBoxOverlayRuntime,
373    text_color: (u8, u8, u8),
374    shadow_color: (u8, u8, u8),
375    fuchi_color: (u8, u8, u8),
376    fuchi_enabled: bool,
377    name_text_color: (u8, u8, u8),
378    name_shadow_color: (u8, u8, u8),
379    name_fuchi_color: (u8, u8, u8),
380    name_fuchi_enabled: bool,
381    font_paths: Vec<PathBuf>,
382    font_scanned: bool,
383    font_cache: FontCache,
384}
385
386impl UiRuntime {
387    pub fn set_text_colors(&mut self, text_color: (u8, u8, u8), shadow_color: (u8, u8, u8)) {
388        self.set_text_colors_full(text_color, shadow_color, None);
389    }
390
391    pub fn set_text_colors_full(
392        &mut self,
393        text_color: (u8, u8, u8),
394        shadow_color: (u8, u8, u8),
395        fuchi_color: Option<(u8, u8, u8)>,
396    ) {
397        self.set_mwnd_text_colors_full(
398            text_color,
399            shadow_color,
400            fuchi_color,
401            text_color,
402            shadow_color,
403            fuchi_color,
404        );
405    }
406
407    pub fn set_mwnd_text_colors_full(
408        &mut self,
409        msg_text_color: (u8, u8, u8),
410        msg_shadow_color: (u8, u8, u8),
411        msg_fuchi_color: Option<(u8, u8, u8)>,
412        name_text_color: (u8, u8, u8),
413        name_shadow_color: (u8, u8, u8),
414        name_fuchi_color: Option<(u8, u8, u8)>,
415    ) {
416        self.text_color = msg_text_color;
417        self.shadow_color = msg_shadow_color;
418        self.fuchi_enabled = msg_fuchi_color.is_some();
419        if let Some(color) = msg_fuchi_color {
420            self.fuchi_color = color;
421        }
422        self.name_text_color = name_text_color;
423        self.name_shadow_color = name_shadow_color;
424        self.name_fuchi_enabled = name_fuchi_color.is_some();
425        if let Some(color) = name_fuchi_color {
426            self.name_fuchi_color = color;
427        }
428        self.mwnd.msg.text_dirty = true;
429        self.mwnd.name.text_dirty = true;
430    }
431
432    fn mwnd_message_text_style(&self, script: &ScriptRuntimeState) -> TextStyle {
433        TextStyle {
434            color: self.text_color,
435            shadow_color: self.shadow_color,
436            fuchi_color: self.fuchi_color,
437            shadow: script.font_shadow != 0,
438            fuchi: self.fuchi_enabled,
439            bold: script.font_bold != 0,
440        }
441    }
442
443    fn mwnd_name_text_style(&self, script: &ScriptRuntimeState) -> TextStyle {
444        TextStyle {
445            color: self.name_text_color,
446            shadow_color: self.name_shadow_color,
447            fuchi_color: self.name_fuchi_color,
448            shadow: script.font_shadow != 0,
449            fuchi: self.name_fuchi_enabled,
450            bold: script.font_bold != 0,
451        }
452    }
453
454    fn ensure_layer(
455        layers: &mut crate::layer::LayerManager,
456        want: &mut Option<LayerId>,
457    ) -> LayerId {
458        if let Some(id) = *want {
459            if layers.layer(id).is_some() {
460                return id;
461            }
462        }
463        let id = layers.create_layer();
464        *want = Some(id);
465        id
466    }
467
468    fn ensure_msg_bg_sprite(
469        &mut self,
470        layers: &mut crate::layer::LayerManager,
471        ui_layer: LayerId,
472    ) -> SpriteId {
473        if let Some(id) = self.mwnd.waku.bg_sprite {
474            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
475                return id;
476            }
477        }
478        let sprite_id = layers
479            .layer_mut(ui_layer)
480            .expect("ui_layer exists")
481            .create_sprite();
482        self.mwnd.waku.bg_sprite = Some(sprite_id);
483        sprite_id
484    }
485
486    fn ensure_msg_filter_sprite(
487        &mut self,
488        layers: &mut crate::layer::LayerManager,
489        ui_layer: LayerId,
490    ) -> SpriteId {
491        if let Some(id) = self.mwnd.waku.filter_sprite {
492            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
493                return id;
494            }
495        }
496        let sprite_id = layers
497            .layer_mut(ui_layer)
498            .expect("ui_layer exists")
499            .create_sprite();
500        self.mwnd.waku.filter_sprite = Some(sprite_id);
501        sprite_id
502    }
503
504    fn ensure_msg_face_sprite(
505        &mut self,
506        layers: &mut crate::layer::LayerManager,
507        ui_layer: LayerId,
508    ) -> SpriteId {
509        if let Some(id) = self.mwnd.face.sprite {
510            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
511                return id;
512            }
513        }
514        let sprite_id = layers
515            .layer_mut(ui_layer)
516            .expect("ui_layer exists")
517            .create_sprite();
518        self.mwnd.face.sprite = Some(sprite_id);
519        sprite_id
520    }
521
522    fn ensure_key_icon_sprite(
523        &mut self,
524        layers: &mut crate::layer::LayerManager,
525        ui_layer: LayerId,
526    ) -> SpriteId {
527        if let Some(id) = self.mwnd.key_icon.sprite {
528            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
529                return id;
530            }
531        }
532        let sprite_id = layers
533            .layer_mut(ui_layer)
534            .expect("ui_layer exists")
535            .create_sprite();
536        self.mwnd.key_icon.sprite = Some(sprite_id);
537        sprite_id
538    }
539
540    fn ensure_text_sprite(
541        layers: &mut crate::layer::LayerManager,
542        ui_layer: LayerId,
543        slot: &mut Option<SpriteId>,
544    ) -> SpriteId {
545        if let Some(id) = *slot {
546            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
547                return id;
548            }
549        }
550        let sprite_id = layers
551            .layer_mut(ui_layer)
552            .expect("ui_layer exists")
553            .create_sprite();
554        *slot = Some(sprite_id);
555        sprite_id
556    }
557
558    fn default_window_origin(
559        screen_w: u32,
560        screen_h: u32,
561        window_w: u32,
562        window_h: u32,
563    ) -> (i32, i32) {
564        let x = ((screen_w as i32 - window_w as i32) / 2).max(0);
565        let y = (screen_h as i32 - window_h as i32).max(0);
566        (x, y)
567    }
568
569    fn message_font_px(&self) -> u32 {
570        self.mwnd.window.moji_size.unwrap_or(26).clamp(10, 96) as u32
571    }
572
573    fn name_font_px(&self) -> u32 {
574        ((self.message_font_px() as f32) * 0.9)
575            .round()
576            .clamp(10.0, 72.0) as u32
577    }
578
579    fn base_padding(&self) -> i32 {
580        ((self.message_font_px() as f32) * 0.75)
581            .round()
582            .clamp(12.0, 32.0) as i32
583    }
584
585    fn name_band_height(&self) -> i32 {
586        if self.mwnd.name.text.as_deref().unwrap_or("").is_empty() {
587            0
588        } else {
589            (self.name_font_px() as i32 + self.base_padding() / 2).max(20)
590        }
591    }
592
593    fn estimated_text_extent(&self, text: &str, font_px: u32) -> (u32, u32) {
594        let mut max_cols = 0u32;
595        let mut lines = 0u32;
596        for line in text.split('\n') {
597            lines += 1;
598            max_cols = max_cols.max(line.chars().count() as u32);
599        }
600        if lines == 0 {
601            lines = 1;
602        }
603        let char_w = ((font_px as f32) * 0.58).round().max(1.0) as u32;
604        let line_h = ((font_px as f32) * 1.35).round().max(1.0) as u32;
605        (
606            max_cols.max(1).saturating_mul(char_w),
607            lines.saturating_mul(line_h),
608        )
609    }
610
611    fn derive_window_size(&self, fallback_w: u32, fallback_h: u32) -> (u32, u32) {
612        if let Some((ww, hh)) = self.mwnd.window.size {
613            return (ww.max(1), hh.max(1));
614        }
615        if let Some((ww, hh)) = self.mwnd.waku.bg_size {
616            return (ww.max(1), hh.max(1));
617        }
618
619        let font_px = self.message_font_px();
620        let pad = self.base_padding().max(1) as u32;
621        let name_h = self.name_band_height().max(0) as u32;
622        let msg_text = self.mwnd.msg.text.as_deref().unwrap_or("");
623        let (text_w, text_h) = self.estimated_text_extent(msg_text, font_px);
624
625        let mut width = text_w.saturating_add(pad.saturating_mul(2));
626        let mut height = text_h
627            .saturating_add(name_h)
628            .saturating_add(pad.saturating_mul(2));
629
630        if let Some((cols, rows)) = self.mwnd.window.moji_cnt {
631            let cols = cols.max(1) as u32;
632            let rows = rows.max(1) as u32;
633            let line_h = ((font_px as f32) * 1.35).round().max(1.0) as u32;
634            width = width.max(
635                cols.saturating_mul(font_px)
636                    .saturating_add(pad.saturating_mul(2)),
637            );
638            height = height.max(
639                rows.saturating_mul(line_h)
640                    .saturating_add(name_h)
641                    .saturating_add(pad.saturating_mul(2)),
642            );
643        }
644
645        if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
646            width = width.saturating_add(self.face_reserved_width(UiRect::new(
647                0,
648                0,
649                width.max(1),
650                height.max(1),
651            )) as u32);
652        }
653
654        (
655            width.clamp(1, fallback_w.max(1)),
656            height.clamp(1, fallback_h.max(1)),
657        )
658    }
659
660    fn window_rect(&self, w: u32, h: u32) -> UiRect {
661        let (ww, hh) = self.derive_window_size(w, h);
662        let (mut x, mut y) = Self::default_window_origin(w, h, ww, hh);
663        if let Some((px, py)) = self.mwnd.window.pos {
664            x = px;
665            y = py;
666        }
667        UiRect::new(x, y, ww, hh)
668    }
669
670    fn face_reserved_width(&self, rect: UiRect) -> i32 {
671        if self.mwnd.face.image.is_none() && self.mwnd.face.file.is_none() {
672            return 0;
673        }
674        let reserve = ((rect.h as f32) * 0.42).round() as i32;
675        reserve.clamp(72, 260)
676    }
677
678    fn msg_rect(&self, w: u32, h: u32) -> (i32, i32, u32, u32) {
679        let rect = self.window_rect(w, h);
680        let pad = self.base_padding();
681        let name_h = self.name_band_height();
682        if self.mwnd.window.extend_type == 1 {
683            let (l, t, r, b) = self.mwnd.window.message_margin.unwrap_or((20, 20, 20, 20));
684            let x = rect.x + l as i32;
685            let y = rect.y + t as i32;
686            let width = (rect.w as i32 - l as i32 - r as i32).max(1) as u32;
687            let height = (rect.h as i32 - t as i32 - b as i32).max(1) as u32;
688            return (x, y, width, height);
689        }
690
691        let face_pad = if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
692            self.face_reserved_width(rect) + pad / 2
693        } else {
694            0
695        };
696        let fallback_x = rect.x + pad + face_pad;
697        let fallback_y = rect.y + pad + name_h;
698        let (x, y) = if let Some((mx, my)) = self.mwnd.window.message_pos {
699            (rect.x + mx, rect.y + my)
700        } else {
701            (fallback_x, fallback_y)
702        };
703
704        if let Some((cols, rows)) = self.mwnd.window.moji_cnt {
705            let font_px = self.message_font_px() as i32;
706            let (space_x, space_y) = self.mwnd.window.moji_space.unwrap_or((-1, 10));
707            let cols = cols.max(1) as i32;
708            let rows = rows.max(1) as i32;
709            let width = (font_px * cols + space_x as i32 * (cols - 1)).max(1) as u32;
710            let height = (font_px * rows + space_y as i32 * (rows - 1)).max(font_px) as u32;
711            return (x, y, width, height);
712        }
713
714        let (l, t, r, b) = self
715            .mwnd
716            .window
717            .message_margin
718            .unwrap_or((pad as i64, pad as i64, pad as i64, pad as i64));
719        let right_pad = if self.mwnd.window.message_pos.is_some() {
720            r as i32
721        } else {
722            l as i32
723        };
724        let bottom_pad = if self.mwnd.window.message_pos.is_some() {
725            b as i32
726        } else {
727            t as i32
728        };
729        let width = (rect.x + rect.w as i32 - x - right_pad).max(1) as u32;
730        let height = (rect.y + rect.h as i32 - y - bottom_pad).max(1) as u32;
731        (x, y, width, height)
732    }
733
734    fn name_rect(&self, w: u32, h: u32) -> (i32, i32, u32, u32) {
735        let rect = self.window_rect(w, h);
736        let pad = self.base_padding();
737        let name_h = self.name_band_height().max(1);
738        let face_pad = if self.mwnd.face.file.is_some() || self.mwnd.face.image.is_some() {
739            self.face_reserved_width(rect) + pad / 2
740        } else {
741            0
742        };
743        let x = rect.x + pad + face_pad;
744        let y = rect.y + pad;
745        let width = (rect.w as i32 - pad * 2 - face_pad).max(1) as u32;
746        let height = name_h as u32;
747        (x, y, width, height)
748    }
749
750    fn face_rect(&self, w: u32, h: u32) -> UiRect {
751        let rect = self.window_rect(w, h);
752        let pad = self.base_padding();
753        let reserve_w = self.face_reserved_width(rect).max(1) as u32;
754        let max_h = (rect.h as i32 - pad * 2).max(1) as u32;
755        let fw = reserve_w;
756        let fh = reserve_w.min(max_h);
757        let (mut x, mut y) = (rect.x + pad, rect.y + rect.h as i32 - fh as i32 - pad);
758        if let Some((rx, ry)) = self.mwnd.face.rep_pos {
759            x = rect.x + rx as i32;
760            y = rect.y + ry as i32;
761        }
762        UiRect::new(x, y, fw, fh)
763    }
764
765    fn key_icon_rect(&self, w: u32, h: u32) -> Option<UiRect> {
766        let rect = self.window_rect(w, h);
767        let (iw, ih) = self.mwnd.key_icon.size?;
768        let (ix, iy, _iz) = self.mwnd.key_icon.icon_pos;
769        let x;
770        let y;
771        if self.mwnd.key_icon.icon_pos_type == 0 {
772            match self.mwnd.key_icon.icon_pos_base {
773                1 => {
774                    x = rect.x + rect.w as i32 - ix as i32 - iw as i32;
775                    y = rect.y + iy as i32;
776                }
777                2 => {
778                    x = rect.x + ix as i32;
779                    y = rect.y + rect.h as i32 - iy as i32 - ih as i32;
780                }
781                3 => {
782                    x = rect.x + rect.w as i32 - ix as i32 - iw as i32;
783                    y = rect.y + rect.h as i32 - iy as i32 - ih as i32;
784                }
785                _ => {
786                    x = rect.x + ix as i32;
787                    y = rect.y + iy as i32;
788                }
789            }
790        } else {
791            x = rect.x + ix as i32;
792            y = rect.y + iy as i32;
793        }
794        Some(UiRect::new(x, y, iw, ih))
795    }
796
797    fn message_has_text(&self) -> bool {
798        self.mwnd
799            .msg
800            .text
801            .as_deref()
802            .unwrap_or("")
803            .chars()
804            .next()
805            .is_some()
806    }
807
808    fn message_fully_revealed(&self) -> bool {
809        let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
810        total == 0 || self.mwnd.msg.visible_chars >= total
811    }
812
813    fn begin_message_window_anim(
814        &mut self,
815        target_visible: bool,
816        anime_type: i64,
817        duration_ms: u64,
818        clear_on_close: bool,
819    ) {
820        let current = self.mwnd.anim.progress;
821        self.mwnd.anim.target_visible = target_visible;
822        self.mwnd.anim.anim_type = anime_type;
823        self.mwnd.anim.clear_text_on_close_end = clear_on_close;
824        if duration_ms == 0 {
825            self.mwnd.anim.progress = if target_visible { 1.0 } else { 0.0 };
826            self.mwnd.anim.from = self.mwnd.anim.progress;
827            self.mwnd.anim.to = self.mwnd.anim.progress;
828            self.mwnd.anim.started_at = None;
829            self.mwnd.anim.duration_ms = 0;
830            self.mwnd.anim.visible = target_visible;
831            if !target_visible && clear_on_close {
832                self.mwnd.anim.clear_text_on_close_end = false;
833                self.clear_message();
834                self.clear_name();
835            }
836            return;
837        }
838        self.mwnd.anim.visible = true;
839        self.mwnd.anim.from = current;
840        self.mwnd.anim.to = if target_visible { 1.0 } else { 0.0 };
841        self.mwnd.anim.started_at = Some(Instant::now());
842        self.mwnd.anim.duration_ms = duration_ms;
843    }
844
845    fn update_message_window_anim(&mut self) {
846        let Some(start) = self.mwnd.anim.started_at else {
847            self.mwnd.anim.visible = self.mwnd.anim.progress > 0.0 && self.mwnd.anim.target_visible;
848            return;
849        };
850        let dur = self.mwnd.anim.duration_ms.max(1);
851        let t = (start.elapsed().as_secs_f32() / (dur as f32 / 1000.0)).clamp(0.0, 1.0);
852        self.mwnd.anim.progress =
853            self.mwnd.anim.from + (self.mwnd.anim.to - self.mwnd.anim.from) * t;
854        self.mwnd.anim.visible = self.mwnd.anim.progress > 0.0;
855        if t >= 1.0 {
856            self.mwnd.anim.started_at = None;
857            self.mwnd.anim.progress = self.mwnd.anim.to;
858            self.mwnd.anim.visible = self.mwnd.anim.target_visible;
859            if !self.mwnd.anim.target_visible && self.mwnd.anim.clear_text_on_close_end {
860                self.mwnd.anim.clear_text_on_close_end = false;
861                self.clear_message();
862                self.clear_name();
863            }
864        }
865    }
866
867    fn resolve_mwnd_anim_type(
868        &self,
869        anime_type: i64,
870        rect: UiRect,
871        screen_w: u32,
872        screen_h: u32,
873    ) -> i64 {
874        match anime_type {
875            6 => {
876                let up = rect.y + rect.h as i32;
877                let down = screen_h as i32 - rect.y;
878                if up <= down {
879                    2
880                } else {
881                    3
882                }
883            }
884            7 => {
885                let left = rect.x + rect.w as i32;
886                let right = screen_w as i32 - rect.x;
887                if left <= right {
888                    4
889                } else {
890                    5
891                }
892            }
893            8 => {
894                let up = rect.y + rect.h as i32;
895                let down = screen_h as i32 - rect.y;
896                let left = rect.x + rect.w as i32;
897                let right = screen_w as i32 - rect.x;
898                let (ud_ty, ud_len) = if up <= down { (2, up) } else { (3, down) };
899                let (lr_ty, lr_len) = if left <= right { (4, left) } else { (5, right) };
900                if ud_len <= lr_len {
901                    ud_ty
902                } else {
903                    lr_ty
904                }
905            }
906            _ => anime_type,
907        }
908    }
909
910    fn current_window_anim(&self, rect: UiRect, screen_w: u32, screen_h: u32) -> UiWindowAnim {
911        let p = self.mwnd.anim.progress.clamp(0.0, 1.0);
912        let ty = self.resolve_mwnd_anim_type(self.mwnd.anim.anim_type, rect, screen_w, screen_h);
913        let mut dx = 0.0f32;
914        let mut dy = 0.0f32;
915        let mut scale_x = 1.0f32;
916        let mut scale_y = 1.0f32;
917        let mut rotate_deg = 0.0f32;
918        let mut alpha = if p <= 0.0 { 0.0 } else { 255.0 };
919        let mut pivot_abs_x = rect.x as f32 + rect.w as f32 * 0.5;
920        let mut pivot_abs_y = rect.y as f32 + rect.h as f32 * 0.5;
921
922        let ease = p * p * (3.0 - 2.0 * p);
923        let fade_alpha = |t: f32| -> f32 {
924            if t <= 0.0 {
925                0.0
926            } else {
927                (255.0 * t).clamp(0.0, 255.0)
928            }
929        };
930        let slide_from = |start: f32, t: f32| -> f32 { start * (1.0 - t) };
931        let scale_from = |start: f32, t: f32| -> f32 { start + (1.0 - start) * t };
932        let resolve_anchor = |axis: char, center_code: i32| -> f32 {
933            match (axis, center_code) {
934                ('x', 0) => rect.x as f32 + rect.w as f32 * 0.5,
935                ('x', 1) => rect.x as f32,
936                ('x', 2) => rect.x as f32 + rect.w as f32,
937                ('x', 3) => -(screen_w as f32) / 16.0,
938                ('x', 4) => screen_w as f32 + (screen_w as f32) / 16.0,
939                ('y', 0) => rect.y as f32 + rect.h as f32 * 0.5,
940                ('y', 1) => rect.y as f32,
941                ('y', 2) => rect.y as f32 + rect.h as f32,
942                ('y', 3) => -(screen_h as f32) / 16.0,
943                ('y', 4) => screen_h as f32 + (screen_h as f32) / 16.0,
944                _ => 0.0,
945            }
946        };
947
948        match ty {
949            0 => {}
950            1 => {
951                alpha = fade_alpha(ease);
952            }
953            2 => {
954                dy = slide_from(-(rect.y + rect.h as i32) as f32, ease);
955                alpha = fade_alpha(ease);
956            }
957            3 => {
958                dy = slide_from((screen_h as i32 - rect.y) as f32, ease);
959                alpha = fade_alpha(ease);
960            }
961            4 => {
962                dx = slide_from(-(rect.x + rect.w as i32) as f32, ease);
963                alpha = fade_alpha(ease);
964            }
965            5 => {
966                dx = slide_from((screen_w as i32 - rect.x) as f32, ease);
967                alpha = fade_alpha(ease);
968            }
969            9..=48 => {
970                alpha = fade_alpha(ease * (224.0 / 255.0) + p * (31.0 / 255.0));
971                let (mut ud_mod, mut ud_center, mut lr_mod, mut lr_center, mut rotate_cnt) =
972                    (0, 0, 0, 0, 0);
973                match ty {
974                    9 => {
975                        ud_mod = 1;
976                        ud_center = 0;
977                        lr_mod = 1;
978                        lr_center = 0;
979                    }
980                    10 => {
981                        ud_mod = 2;
982                        ud_center = 0;
983                        lr_mod = 1;
984                        lr_center = 0;
985                    }
986                    11 => {
987                        ud_mod = 0;
988                        ud_center = 0;
989                        lr_mod = 1;
990                        lr_center = 0;
991                    }
992                    12 => {
993                        ud_mod = 1;
994                        ud_center = 0;
995                        lr_mod = 2;
996                        lr_center = 0;
997                    }
998                    13 => {
999                        ud_mod = 2;
1000                        ud_center = 0;
1001                        lr_mod = 2;
1002                        lr_center = 0;
1003                    }
1004                    14 => {
1005                        ud_mod = 0;
1006                        ud_center = 0;
1007                        lr_mod = 2;
1008                        lr_center = 0;
1009                    }
1010                    15 => {
1011                        ud_mod = 1;
1012                        ud_center = 0;
1013                        lr_mod = 0;
1014                        lr_center = 0;
1015                    }
1016                    16 => {
1017                        ud_mod = 2;
1018                        ud_center = 0;
1019                        lr_mod = 0;
1020                        lr_center = 0;
1021                    }
1022                    17 => {
1023                        ud_mod = 0;
1024                        ud_center = 0;
1025                        lr_mod = 2;
1026                        lr_center = 1;
1027                    }
1028                    18 => {
1029                        ud_mod = 0;
1030                        ud_center = 0;
1031                        lr_mod = 2;
1032                        lr_center = 2;
1033                    }
1034                    19 => {
1035                        ud_mod = 2;
1036                        ud_center = 1;
1037                        lr_mod = 0;
1038                        lr_center = 0;
1039                    }
1040                    20 => {
1041                        ud_mod = 2;
1042                        ud_center = 2;
1043                        lr_mod = 0;
1044                        lr_center = 0;
1045                    }
1046                    21 => {
1047                        ud_mod = 2;
1048                        ud_center = 1;
1049                        lr_mod = 2;
1050                        lr_center = 1;
1051                    }
1052                    22 => {
1053                        ud_mod = 2;
1054                        ud_center = 1;
1055                        lr_mod = 2;
1056                        lr_center = 2;
1057                    }
1058                    23 => {
1059                        ud_mod = 2;
1060                        ud_center = 2;
1061                        lr_mod = 2;
1062                        lr_center = 1;
1063                    }
1064                    24 => {
1065                        ud_mod = 2;
1066                        ud_center = 2;
1067                        lr_mod = 2;
1068                        lr_center = 2;
1069                    }
1070                    25 => {
1071                        ud_mod = 0;
1072                        ud_center = 0;
1073                        lr_mod = 2;
1074                        lr_center = 3;
1075                    }
1076                    26 => {
1077                        ud_mod = 0;
1078                        ud_center = 0;
1079                        lr_mod = 2;
1080                        lr_center = 4;
1081                    }
1082                    27 => {
1083                        ud_mod = 2;
1084                        ud_center = 3;
1085                        lr_mod = 0;
1086                        lr_center = 0;
1087                    }
1088                    28 => {
1089                        ud_mod = 2;
1090                        ud_center = 4;
1091                        lr_mod = 0;
1092                        lr_center = 0;
1093                    }
1094                    29 => {
1095                        ud_mod = 2;
1096                        ud_center = 0;
1097                        lr_mod = 2;
1098                        lr_center = 0;
1099                        rotate_cnt = -4;
1100                    }
1101                    30 => {
1102                        ud_mod = 2;
1103                        ud_center = 0;
1104                        lr_mod = 2;
1105                        lr_center = 0;
1106                        rotate_cnt = 4;
1107                    }
1108                    31 => {
1109                        ud_mod = 2;
1110                        ud_center = 0;
1111                        lr_mod = 2;
1112                        lr_center = 0;
1113                        rotate_cnt = -8;
1114                    }
1115                    32 => {
1116                        ud_mod = 2;
1117                        ud_center = 0;
1118                        lr_mod = 2;
1119                        lr_center = 0;
1120                        rotate_cnt = 8;
1121                    }
1122                    33 => {
1123                        ud_mod = 1;
1124                        ud_center = 0;
1125                        lr_mod = 1;
1126                        lr_center = 0;
1127                        rotate_cnt = -4;
1128                    }
1129                    34 => {
1130                        ud_mod = 1;
1131                        ud_center = 0;
1132                        lr_mod = 1;
1133                        lr_center = 0;
1134                        rotate_cnt = 4;
1135                    }
1136                    35 => {
1137                        ud_mod = 1;
1138                        ud_center = 0;
1139                        lr_mod = 1;
1140                        lr_center = 0;
1141                        rotate_cnt = -8;
1142                    }
1143                    36 => {
1144                        ud_mod = 1;
1145                        ud_center = 0;
1146                        lr_mod = 1;
1147                        lr_center = 0;
1148                        rotate_cnt = 8;
1149                    }
1150                    37 => {
1151                        ud_mod = 2;
1152                        ud_center = 0;
1153                        lr_mod = 0;
1154                        lr_center = 0;
1155                        rotate_cnt = -4;
1156                    }
1157                    38 => {
1158                        ud_mod = 2;
1159                        ud_center = 0;
1160                        lr_mod = 0;
1161                        lr_center = 0;
1162                        rotate_cnt = 4;
1163                    }
1164                    39 => {
1165                        ud_mod = 0;
1166                        ud_center = 0;
1167                        lr_mod = 2;
1168                        lr_center = 0;
1169                        rotate_cnt = -4;
1170                    }
1171                    40 => {
1172                        ud_mod = 0;
1173                        ud_center = 0;
1174                        lr_mod = 2;
1175                        lr_center = 0;
1176                        rotate_cnt = 4;
1177                    }
1178                    41 => {
1179                        ud_mod = 2;
1180                        ud_center = 0;
1181                        lr_mod = 0;
1182                        lr_center = 0;
1183                        rotate_cnt = -2;
1184                    }
1185                    42 => {
1186                        ud_mod = 2;
1187                        ud_center = 0;
1188                        lr_mod = 0;
1189                        lr_center = 0;
1190                        rotate_cnt = 2;
1191                    }
1192                    43 => {
1193                        ud_mod = 0;
1194                        ud_center = 0;
1195                        lr_mod = 2;
1196                        lr_center = 0;
1197                        rotate_cnt = -2;
1198                    }
1199                    44 => {
1200                        ud_mod = 0;
1201                        ud_center = 0;
1202                        lr_mod = 2;
1203                        lr_center = 0;
1204                        rotate_cnt = 2;
1205                    }
1206                    45 => {
1207                        ud_mod = 2;
1208                        ud_center = 0;
1209                        lr_mod = 0;
1210                        lr_center = 0;
1211                        rotate_cnt = -1;
1212                    }
1213                    46 => {
1214                        ud_mod = 2;
1215                        ud_center = 0;
1216                        lr_mod = 0;
1217                        lr_center = 0;
1218                        rotate_cnt = 1;
1219                    }
1220                    47 => {
1221                        ud_mod = 0;
1222                        ud_center = 0;
1223                        lr_mod = 2;
1224                        lr_center = 0;
1225                        rotate_cnt = -1;
1226                    }
1227                    48 => {
1228                        ud_mod = 0;
1229                        ud_center = 0;
1230                        lr_mod = 2;
1231                        lr_center = 0;
1232                        rotate_cnt = 1;
1233                    }
1234                    _ => {}
1235                }
1236                if ud_mod != 0 {
1237                    pivot_abs_y = resolve_anchor('y', ud_center);
1238                    let start = if ud_mod == 1 { 3.0 } else { 0.0 };
1239                    scale_y = scale_from(start, ease);
1240                }
1241                if lr_mod != 0 {
1242                    pivot_abs_x = resolve_anchor('x', lr_center);
1243                    let start = if lr_mod == 1 { 3.0 } else { 0.0 };
1244                    scale_x = scale_from(start, ease);
1245                }
1246                if rotate_cnt != 0 {
1247                    rotate_deg = (rotate_cnt as f32 * 90.0) * (1.0 - ease);
1248                }
1249            }
1250            99 => {
1251                dx = ((1.0 - ease) * 800.0).round();
1252            }
1253            _ => {
1254                alpha = fade_alpha(p);
1255            }
1256        }
1257
1258        UiWindowAnim {
1259            dx: dx.round() as i32,
1260            dy: dy.round() as i32,
1261            scale_x: scale_x.clamp(0.001, 8.0),
1262            scale_y: scale_y.clamp(0.001, 8.0),
1263            rotate: rotate_deg.to_radians(),
1264            alpha: alpha.round().clamp(0.0, 255.0) as u8,
1265            pivot_abs_x: pivot_abs_x + dx,
1266            pivot_abs_y: pivot_abs_y + dy,
1267        }
1268    }
1269
1270    pub fn current_mwnd_window_render_state(
1271        &self,
1272        screen_w: u32,
1273        screen_h: u32,
1274    ) -> Option<MwndWindowRenderState> {
1275        if !self.mwnd.projection_active && !self.mwnd.anim.visible {
1276            return None;
1277        }
1278        let rect = self.window_rect(screen_w, screen_h);
1279        let anim = self.current_window_anim(rect, screen_w, screen_h);
1280        Some(MwndWindowRenderState {
1281            x: rect.x,
1282            y: rect.y,
1283            w: rect.w,
1284            h: rect.h,
1285            dx: anim.dx,
1286            dy: anim.dy,
1287            scale_x: anim.scale_x,
1288            scale_y: anim.scale_y,
1289            rotate: anim.rotate,
1290            alpha: anim.alpha,
1291            pivot_abs_x: anim.pivot_abs_x,
1292            pivot_abs_y: anim.pivot_abs_y,
1293        })
1294    }
1295
1296    fn current_slide_offset_px(&self) -> i32 {
1297        if !self.mwnd.msg.slide_enabled {
1298            return 0;
1299        }
1300        let Some(start) = self.mwnd.msg.slide_started_at else {
1301            return 0;
1302        };
1303        let dur = self.mwnd.msg.slide_time_ms.max(1);
1304        let t = (start.elapsed().as_secs_f32() / (dur as f32 / 1000.0)).clamp(0.0, 1.0);
1305        ((1.0 - t) * 36.0).round() as i32
1306    }
1307
1308    /// Ensure fixed UI sprites exist and are laid out for the given screen size.
1309    pub fn sync_layout(&mut self, layers: &mut crate::layer::LayerManager, w: u32, h: u32) {
1310        if !self.mwnd.projection_active && !self.mwnd.anim.visible {
1311            return;
1312        }
1313        let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
1314        let bg_sprite = self.ensure_msg_bg_sprite(layers, ui_layer);
1315        let filter_sprite = self.ensure_msg_filter_sprite(layers, ui_layer);
1316        let face_sprite = self.ensure_msg_face_sprite(layers, ui_layer);
1317        let key_icon_sprite = self.ensure_key_icon_sprite(layers, ui_layer);
1318        let msg_text_sprite =
1319            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.msg.text_sprite);
1320        let name_text_sprite =
1321            Self::ensure_text_sprite(layers, ui_layer, &mut self.mwnd.name.text_sprite);
1322
1323        let rect = self.window_rect(w, h);
1324        let anim = self.current_window_anim(rect, w, h);
1325        let apply_anim = |s: &mut crate::layer::Sprite, base_x: i32, base_y: i32, order: i32| {
1326            s.fit = SpriteFit::PixelRect;
1327            s.x = base_x + anim.dx;
1328            s.y = base_y + anim.dy;
1329            s.order = order;
1330            s.scale_x = anim.scale_x;
1331            s.scale_y = anim.scale_y;
1332            s.rotate = anim.rotate;
1333            s.pivot_x = anim.pivot_abs_x - s.x as f32;
1334            s.pivot_y = anim.pivot_abs_y - s.y as f32;
1335        };
1336
1337        if let Some(s) = layers
1338            .layer_mut(ui_layer)
1339            .and_then(|l| l.sprite_mut(bg_sprite))
1340        {
1341            s.size_mode = SpriteSizeMode::Explicit {
1342                width: rect.w,
1343                height: rect.h,
1344            };
1345            apply_anim(s, rect.x, rect.y, 1_000_000);
1346        }
1347
1348        if let Some(s) = layers
1349            .layer_mut(ui_layer)
1350            .and_then(|l| l.sprite_mut(filter_sprite))
1351        {
1352            let (ml, mt, mr, mb) = self.mwnd.waku.filter_margin;
1353            let fx = rect.x + ml as i32;
1354            let fy = rect.y + mt as i32;
1355            if self.mwnd.waku.filter_image.is_some() {
1356                s.size_mode = SpriteSizeMode::Intrinsic;
1357            } else {
1358                let width = (rect.w as i64 - ml - mr).max(1) as u32;
1359                let height = (rect.h as i64 - mt - mb).max(1) as u32;
1360                s.size_mode = SpriteSizeMode::Explicit { width, height };
1361            }
1362            apply_anim(s, fx, fy, 1_000_005);
1363        }
1364
1365        let face_rect = self.face_rect(w, h);
1366        if let Some(s) = layers
1367            .layer_mut(ui_layer)
1368            .and_then(|l| l.sprite_mut(face_sprite))
1369        {
1370            s.size_mode = SpriteSizeMode::Explicit {
1371                width: face_rect.w,
1372                height: face_rect.h,
1373            };
1374            apply_anim(
1375                s,
1376                face_rect.x,
1377                face_rect.y + self.current_slide_offset_px() / 3,
1378                1_000_008,
1379            );
1380        }
1381
1382        let (mx, my, mw, mh) = self.msg_rect(w, h);
1383        if let Some(s) = layers
1384            .layer_mut(ui_layer)
1385            .and_then(|l| l.sprite_mut(msg_text_sprite))
1386        {
1387            s.size_mode = if self.mwnd.msg.text_image.is_some() {
1388                SpriteSizeMode::Intrinsic
1389            } else {
1390                SpriteSizeMode::Explicit {
1391                    width: mw,
1392                    height: mh,
1393                }
1394            };
1395            apply_anim(s, mx + self.current_slide_offset_px(), my, 1_000_010);
1396        }
1397
1398        let (nx, ny, nw, nh) = self.name_rect(w, h);
1399        if let Some(s) = layers
1400            .layer_mut(ui_layer)
1401            .and_then(|l| l.sprite_mut(name_text_sprite))
1402        {
1403            s.size_mode = if self.mwnd.name.text_image.is_some() {
1404                SpriteSizeMode::Intrinsic
1405            } else {
1406                SpriteSizeMode::Explicit {
1407                    width: nw,
1408                    height: nh,
1409                }
1410            };
1411            apply_anim(s, nx, ny, 1_000_020);
1412        }
1413
1414        if let Some(icon_rect) = self.key_icon_rect(w, h) {
1415            if let Some(s) = layers
1416                .layer_mut(ui_layer)
1417                .and_then(|l| l.sprite_mut(key_icon_sprite))
1418            {
1419                s.size_mode = SpriteSizeMode::Intrinsic;
1420                apply_anim(s, icon_rect.x, icon_rect.y, 1_000_030);
1421            }
1422        }
1423    }
1424
1425    /// Called once per frame to update UI and apply visibility.
1426    pub fn tick(
1427        &mut self,
1428        layers: &mut crate::layer::LayerManager,
1429        images: &mut crate::image_manager::ImageManager,
1430        project_dir: &Path,
1431        w: u32,
1432        h: u32,
1433        script: &ScriptRuntimeState,
1434        syscom: &SyscomRuntimeState,
1435        editbox_lists: &HashMap<u32, EditBoxListState>,
1436        focused_editbox: Option<(u32, usize)>,
1437    ) {
1438        self.update_message_window_anim();
1439        self.scan_font_dir(project_dir);
1440        if !self.font_cache.is_loaded() {
1441            let _ = self.font_cache.load_for_project(project_dir);
1442        }
1443        self.refresh_waku_images(images, project_dir);
1444        self.refresh_face_image(images, project_dir);
1445        self.refresh_key_icon_image(images, project_dir);
1446        self.sync_layout(layers, w, h);
1447        self.update_message_reveal(script, syscom);
1448        self.refresh_text_images(images, w, h, script);
1449        self.sync_sys_overlay(layers, images, w, h);
1450        self.sync_msg_back_ui(layers, images, project_dir);
1451        self.sync_editbox_overlay(layers, images, editbox_lists, focused_editbox);
1452
1453        let Some(ui_layer) = self.mwnd.layer else {
1454            return;
1455        };
1456        let Some(bg_sprite) = self.mwnd.waku.bg_sprite else {
1457            return;
1458        };
1459        let mwnd_hidden = script.mwnd_disp_off_flag || syscom.hide_mwnd.onoff || syscom.msg_back_open;
1460        let mwnd_visible = self.mwnd.anim.visible && !mwnd_hidden;
1461        let anim_alpha = self.current_window_anim(self.window_rect(w, h), w, h).alpha;
1462
1463        if let Some(s) = layers
1464            .layer_mut(ui_layer)
1465            .and_then(|l| l.sprite_mut(bg_sprite))
1466        {
1467            s.visible = mwnd_visible && self.mwnd.waku.bg_image.is_some();
1468            s.alpha = anim_alpha;
1469            s.image_id = self.mwnd.waku.bg_image;
1470        }
1471
1472        if let Some(sprite_id) = self.mwnd.waku.filter_sprite {
1473            if let Some(s) = layers
1474                .layer_mut(ui_layer)
1475                .and_then(|l| l.sprite_mut(sprite_id))
1476            {
1477                let image_id = self
1478                    .mwnd
1479                    .waku
1480                    .filter_image
1481                    .or(self.mwnd.waku.solid_filter_image);
1482                let visible = mwnd_visible && image_id.is_some();
1483                s.visible = visible;
1484                s.image_id = image_id;
1485                const GET_FILTER_COLOR_R: i32 = 84;
1486                const GET_FILTER_COLOR_G: i32 = 91;
1487                const GET_FILTER_COLOR_B: i32 = 92;
1488                const GET_FILTER_COLOR_A: i32 = 93;
1489                let cfg = &syscom.config_int;
1490                let (_filter_r, _filter_g, _filter_b, filter_a) = self.mwnd.waku.filter_color;
1491                let has_filter_texture = self.mwnd.waku.filter_image.is_some();
1492
1493                s.alpha = anim_alpha;
1494                s.tr = if self.mwnd.waku.filter_config_tr {
1495                    cfg.get(&GET_FILTER_COLOR_A)
1496                        .copied()
1497                        .unwrap_or(128)
1498                        .clamp(0, 255) as u8
1499                } else if has_filter_texture {
1500                    255
1501                } else {
1502                    filter_a
1503                };
1504
1505                s.color_rate = 0;
1506                s.color_r = 255;
1507                s.color_g = 255;
1508                s.color_b = 255;
1509                s.mask_mode = 0;
1510                if self.mwnd.waku.filter_config_color {
1511                    s.color_add_r = cfg
1512                        .get(&GET_FILTER_COLOR_R)
1513                        .copied()
1514                        .unwrap_or(0)
1515                        .clamp(0, 255) as u8;
1516                    s.color_add_g = cfg
1517                        .get(&GET_FILTER_COLOR_G)
1518                        .copied()
1519                        .unwrap_or(0)
1520                        .clamp(0, 255) as u8;
1521                    s.color_add_b = cfg
1522                        .get(&GET_FILTER_COLOR_B)
1523                        .copied()
1524                        .unwrap_or(0)
1525                        .clamp(0, 255) as u8;
1526                } else {
1527                    s.color_add_r = 0;
1528                    s.color_add_g = 0;
1529                    s.color_add_b = 0;
1530                }
1531            }
1532        }
1533
1534        if let Some(sprite_id) = self.mwnd.face.sprite {
1535            if let Some(s) = layers
1536                .layer_mut(ui_layer)
1537                .and_then(|l| l.sprite_mut(sprite_id))
1538            {
1539                s.visible = mwnd_visible && self.mwnd.face.image.is_some();
1540                s.image_id = self.mwnd.face.image;
1541                s.alpha = anim_alpha;
1542            }
1543        }
1544
1545        if let Some(sprite_id) = self.mwnd.msg.text_sprite {
1546            if let Some(s) = layers
1547                .layer_mut(ui_layer)
1548                .and_then(|l| l.sprite_mut(sprite_id))
1549            {
1550                s.visible = mwnd_visible && self.mwnd.msg.text_image.is_some();
1551                s.image_id = self.mwnd.msg.text_image;
1552                s.alpha = anim_alpha;
1553            }
1554        }
1555
1556        if let Some(sprite_id) = self.mwnd.name.text_sprite {
1557            if let Some(s) = layers
1558                .layer_mut(ui_layer)
1559                .and_then(|l| l.sprite_mut(sprite_id))
1560            {
1561                s.visible = mwnd_visible && self.mwnd.name.text_image.is_some();
1562                s.image_id = self.mwnd.name.text_image;
1563                s.alpha = anim_alpha;
1564            }
1565        }
1566
1567        if let Some(sprite_id) = self.mwnd.key_icon.sprite {
1568            if let Some(s) = layers
1569                .layer_mut(ui_layer)
1570                .and_then(|l| l.sprite_mut(sprite_id))
1571            {
1572                s.visible = mwnd_visible
1573                    && self.mwnd.key_icon.appear
1574                    && self.mwnd.key_icon.image.is_some();
1575                s.image_id = self.mwnd.key_icon.image;
1576                s.alpha = anim_alpha;
1577            }
1578        }
1579
1580        if let Some(sys_bg) = self.sys.bg_sprite {
1581            if let Some(s) = layers
1582                .layer_mut(ui_layer)
1583                .and_then(|l| l.sprite_mut(sys_bg))
1584            {
1585                s.visible = self.sys.active;
1586                if let Some(img) = self.sys.bg_image {
1587                    s.image_id = Some(img);
1588                }
1589            }
1590        }
1591        if let Some(sys_text) = self.sys.text_sprite {
1592            if let Some(s) = layers
1593                .layer_mut(ui_layer)
1594                .and_then(|l| l.sprite_mut(sys_text))
1595            {
1596                s.visible = self.sys.active && self.sys.text_image.is_some();
1597                s.image_id = self.sys.text_image;
1598            }
1599        }
1600    }
1601
1602    pub fn set_message_bg(&mut self, img: ImageId) {
1603        self.mwnd.projection_active = true;
1604        self.mwnd.waku.bg_image = Some(img);
1605    }
1606
1607    pub fn show_message_bg(&mut self, on: bool) {
1608        self.mwnd.anim.target_visible = on;
1609        if self.mwnd.anim.started_at.is_none() {
1610            self.mwnd.anim.visible = on;
1611            self.mwnd.anim.progress = if on { 1.0 } else { 0.0 };
1612            self.mwnd.anim.from = self.mwnd.anim.progress;
1613            self.mwnd.anim.to = self.mwnd.anim.progress;
1614        }
1615    }
1616
1617    pub fn force_message_bg_visible(&mut self, on: bool) {
1618        self.mwnd.anim.target_visible = on;
1619        self.mwnd.anim.visible = on;
1620        self.mwnd.anim.progress = if on { 1.0 } else { 0.0 };
1621        self.mwnd.anim.from = self.mwnd.anim.progress;
1622        self.mwnd.anim.to = self.mwnd.anim.progress;
1623        self.mwnd.anim.started_at = None;
1624        self.mwnd.anim.duration_ms = 0;
1625        self.mwnd.anim.anim_type = 0;
1626        self.mwnd.anim.clear_text_on_close_end = false;
1627        if !on {
1628            self.clear_message();
1629            self.clear_name();
1630        }
1631    }
1632
1633    pub fn begin_mwnd_open(&mut self, anime_type: i64, duration_ms: i64) {
1634        self.begin_message_window_anim(true, anime_type, duration_ms.max(0) as u64, false);
1635    }
1636
1637    pub fn begin_mwnd_close(&mut self, anime_type: i64, duration_ms: i64) {
1638        self.mwnd.key_icon.appear = false;
1639        self.begin_message_window_anim(false, anime_type, duration_ms.max(0) as u64, true);
1640    }
1641
1642    pub fn set_message_filter(&mut self, img: Option<ImageId>) {
1643        self.mwnd.waku.filter_image = img;
1644    }
1645
1646    pub fn apply_mwnd_projection(&mut self, proj: &MwndProjectionState) {
1647        self.mwnd.projection_active = true;
1648
1649        let bg_file = proj
1650            .bg_file
1651            .as_deref()
1652            .filter(|s| !s.is_empty())
1653            .map(str::to_string);
1654        if self.mwnd.waku.bg_file != bg_file {
1655            self.mwnd.waku.bg_file = bg_file;
1656            self.mwnd.waku.bg_image = None;
1657            self.mwnd.waku.bg_size = None;
1658        }
1659
1660        let filter_file = proj
1661            .filter_file
1662            .as_deref()
1663            .filter(|s| !s.is_empty())
1664            .map(str::to_string);
1665        if self.mwnd.waku.filter_file != filter_file {
1666            self.mwnd.waku.filter_file = filter_file;
1667            self.mwnd.waku.filter_image = None;
1668            self.mwnd.waku.filter_size = None;
1669            self.mwnd.waku.solid_filter_image = None;
1670        }
1671        self.mwnd.waku.filter_margin = proj.filter_margin.unwrap_or((0, 0, 0, 0));
1672        let next_filter_color = proj.filter_color.unwrap_or((0, 0, 255, 128));
1673        if self.mwnd.waku.filter_color != next_filter_color {
1674            self.mwnd.waku.solid_filter_image = None;
1675        }
1676        self.mwnd.waku.filter_color = next_filter_color;
1677        self.mwnd.waku.filter_config_color = proj.filter_config_color;
1678        self.mwnd.waku.filter_config_tr = proj.filter_config_tr;
1679
1680        if self.mwnd.key_icon.key_file != proj.key_icon_file
1681            || self.mwnd.key_icon.page_file != proj.page_icon_file
1682        {
1683            self.mwnd.key_icon.image = None;
1684            self.mwnd.key_icon.file = None;
1685            self.mwnd.key_icon.size = None;
1686            self.mwnd.key_icon.anime_start = None;
1687        }
1688        self.mwnd.key_icon.key_file = proj.key_icon_file.clone();
1689        self.mwnd.key_icon.key_pat_cnt = proj.key_icon_pat_cnt.max(1);
1690        self.mwnd.key_icon.key_speed = proj.key_icon_speed.max(1);
1691        self.mwnd.key_icon.page_file = proj.page_icon_file.clone();
1692        self.mwnd.key_icon.page_pat_cnt = proj.page_icon_pat_cnt.max(1);
1693        self.mwnd.key_icon.page_speed = proj.page_icon_speed.max(1);
1694        self.mwnd.key_icon.appear = proj.key_icon_appear;
1695        if self.mwnd.key_icon.mode != proj.key_icon_mode {
1696            self.mwnd.key_icon.mode = proj.key_icon_mode;
1697            self.mwnd.key_icon.anime_start = None;
1698            self.mwnd.key_icon.image = None;
1699        }
1700        self.mwnd.key_icon.icon_pos_type = proj.icon_pos_type;
1701        self.mwnd.key_icon.icon_pos_base = proj.icon_pos_base;
1702        self.mwnd.key_icon.icon_pos = if proj.icon_pos_type == 1 {
1703            proj.key_icon_pos
1704                .map(|(x, y)| (x, y, 0))
1705                .or(proj.icon_pos)
1706                .unwrap_or((0, 0, 0))
1707        } else {
1708            proj.icon_pos.unwrap_or((0, 0, 0))
1709        };
1710
1711        self.set_mwnd_window_state(
1712            proj.window_pos,
1713            proj.window_size,
1714            proj.message_pos,
1715            proj.message_margin,
1716            proj.window_moji_cnt,
1717            proj.moji_size,
1718            proj.moji_space,
1719            proj.mwnd_extend_type,
1720            proj.moji_color,
1721            proj.shadow_color,
1722            proj.fuchi_color,
1723            proj.face_file.as_deref(),
1724            proj.face_no,
1725            proj.rep_pos,
1726            proj.slide_enabled,
1727            proj.slide_time,
1728        );
1729        self.set_name(proj.name_text.clone());
1730        if proj.msg_text.is_empty() {
1731            if !(self.mwnd.msg.waiting && self.mwnd.msg.clear_on_wait_end) {
1732                self.clear_message();
1733            }
1734        } else {
1735            self.set_message(proj.msg_text.clone());
1736        }
1737    }
1738
1739    pub fn set_mwnd_window_state(
1740        &mut self,
1741        window_pos: Option<(i64, i64)>,
1742        window_size: Option<(i64, i64)>,
1743        message_pos: Option<(i64, i64)>,
1744        message_margin: Option<(i64, i64, i64, i64)>,
1745        window_moji_cnt: Option<(i64, i64)>,
1746        moji_size: Option<i64>,
1747        moji_space: Option<(i64, i64)>,
1748        mwnd_extend_type: i64,
1749        moji_color: Option<i64>,
1750        shadow_color: Option<i64>,
1751        fuchi_color: Option<i64>,
1752        face_file: Option<&str>,
1753        face_no: i64,
1754        rep_pos: Option<(i64, i64)>,
1755        slide_enabled: bool,
1756        slide_time: i64,
1757    ) {
1758        self.mwnd.window.pos = window_pos.map(|(x, y)| (x as i32, y as i32));
1759        self.mwnd.window.size = window_size.map(|(w, h)| (w.max(1) as u32, h.max(1) as u32));
1760        self.mwnd.window.message_pos = message_pos.map(|(x, y)| (x as i32, y as i32));
1761        self.mwnd.window.message_margin = message_margin;
1762        self.mwnd.window.moji_cnt = window_moji_cnt;
1763        self.mwnd.window.moji_size = moji_size;
1764        self.mwnd.window.moji_space = moji_space;
1765        self.mwnd.window.extend_type = mwnd_extend_type;
1766        self.mwnd.window.moji_color = moji_color;
1767        self.mwnd.window.shadow_color = shadow_color;
1768        self.mwnd.window.fuchi_color = fuchi_color;
1769        self.mwnd.face.rep_pos = rep_pos;
1770        self.mwnd.msg.slide_enabled = slide_enabled;
1771        self.mwnd.msg.slide_time_ms = slide_time.max(0) as u64;
1772        let new_face = face_file.filter(|s| !s.is_empty()).map(str::to_string);
1773        if self.mwnd.face.file != new_face || self.mwnd.face.no != face_no {
1774            self.mwnd.face.file = new_face;
1775            self.mwnd.face.no = face_no;
1776            self.mwnd.face.image = None;
1777        }
1778        self.mwnd.msg.text_dirty = true;
1779        self.mwnd.name.text_dirty = true;
1780    }
1781
1782    pub fn clear_mwnd_window_state(&mut self) {
1783        self.mwnd.window.pos = None;
1784        self.mwnd.window.size = None;
1785        self.mwnd.window.message_pos = None;
1786        self.mwnd.window.message_margin = None;
1787        self.mwnd.window.moji_cnt = None;
1788        self.mwnd.window.moji_size = None;
1789        self.mwnd.window.moji_space = None;
1790        self.mwnd.window.extend_type = 0;
1791        self.mwnd.window.moji_color = None;
1792        self.mwnd.window.shadow_color = None;
1793        self.mwnd.window.fuchi_color = None;
1794        self.mwnd.waku.bg_file = None;
1795        self.mwnd.waku.filter_file = None;
1796        self.mwnd.projection_active = false;
1797        self.mwnd.waku.bg_image = None;
1798        self.mwnd.waku.filter_image = None;
1799        self.mwnd.waku.solid_filter_image = None;
1800        self.mwnd.waku.bg_size = None;
1801        self.mwnd.waku.filter_size = None;
1802        self.mwnd.waku.filter_margin = (0, 0, 0, 0);
1803        self.mwnd.waku.filter_color = (0, 0, 255, 128);
1804        self.mwnd.waku.filter_config_color = true;
1805        self.mwnd.waku.filter_config_tr = true;
1806        self.mwnd.key_icon = MwndKeyIconRuntime::default();
1807        self.mwnd.face.file = None;
1808        self.mwnd.face.no = 0;
1809        self.mwnd.face.rep_pos = None;
1810        self.mwnd.face.image = None;
1811        self.mwnd.msg.slide_enabled = false;
1812        self.mwnd.msg.slide_time_ms = 0;
1813        self.mwnd.msg.slide_started_at = None;
1814        self.mwnd.anim.anim_type = 0;
1815        self.mwnd.msg.text_dirty = true;
1816        self.mwnd.name.text_dirty = true;
1817    }
1818
1819    pub fn set_message(&mut self, msg: String) {
1820        let new_text = if msg.is_empty() { None } else { Some(msg) };
1821        if self.mwnd.msg.text == new_text {
1822            return;
1823        }
1824        self.mwnd.msg.text = new_text;
1825        self.mwnd.msg.text_dirty = true;
1826        self.mwnd.msg.visible_chars = 0;
1827        self.mwnd.msg.reveal_base = 0;
1828        self.mwnd.msg.reveal_start = Some(Instant::now());
1829        if self.mwnd.msg.slide_enabled {
1830            self.mwnd.msg.slide_started_at = Some(Instant::now());
1831        }
1832    }
1833
1834    pub fn append_message(&mut self, msg: &str) {
1835        if msg.is_empty() {
1836            return;
1837        }
1838        match self.mwnd.msg.text.as_mut() {
1839            Some(s) => s.push_str(msg),
1840            None => self.mwnd.msg.text = Some(msg.to_string()),
1841        }
1842        self.mwnd.msg.text_dirty = true;
1843        self.mwnd.msg.reveal_base = self.mwnd.msg.visible_chars;
1844        self.mwnd.msg.reveal_start = Some(Instant::now());
1845        if self.mwnd.msg.slide_enabled {
1846            self.mwnd.msg.slide_started_at = Some(Instant::now());
1847        }
1848    }
1849
1850    pub fn append_linebreak(&mut self) {
1851        match self.mwnd.msg.text.as_mut() {
1852            Some(s) => s.push('\n'),
1853            None => self.mwnd.msg.text = Some("\n".to_string()),
1854        }
1855        self.mwnd.msg.text_dirty = true;
1856        self.mwnd.msg.reveal_base = self.mwnd.msg.visible_chars;
1857        self.mwnd.msg.reveal_start = Some(Instant::now());
1858        if self.mwnd.msg.slide_enabled {
1859            self.mwnd.msg.slide_started_at = Some(Instant::now());
1860        }
1861    }
1862
1863    pub fn set_name(&mut self, name: String) {
1864        let new_text = if name.is_empty() { None } else { Some(name) };
1865        if self.mwnd.name.text == new_text {
1866            return;
1867        }
1868        self.mwnd.name.text = new_text;
1869        self.mwnd.name.text_dirty = true;
1870    }
1871
1872    pub fn clear_name(&mut self) {
1873        if self.mwnd.name.text.is_none() {
1874            return;
1875        }
1876        self.mwnd.name.text = None;
1877        self.mwnd.name.text_dirty = true;
1878    }
1879
1880    pub fn clear_message(&mut self) {
1881        self.mwnd.key_icon.appear = false;
1882        if self.mwnd.msg.text.is_none() {
1883            return;
1884        }
1885        self.mwnd.msg.text = None;
1886        self.mwnd.msg.text_dirty = true;
1887        self.mwnd.msg.visible_chars = 0;
1888        self.mwnd.msg.reveal_base = 0;
1889        self.mwnd.msg.reveal_start = None;
1890        self.mwnd.msg.slide_started_at = None;
1891    }
1892
1893    pub fn begin_wait_message(&mut self) {
1894        self.begin_wait_message_with_icon_mode(0);
1895    }
1896
1897    pub fn begin_wait_page_message(&mut self) {
1898        self.begin_wait_message_with_icon_mode(1);
1899    }
1900
1901    fn begin_wait_message_with_icon_mode(&mut self, icon_mode: i64) {
1902        self.mwnd.msg.waiting = true;
1903        self.mwnd.msg.wait_started_at = Some(Instant::now());
1904        self.mwnd.msg.wait_message_len =
1905            self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
1906        self.mwnd.key_icon.appear = true;
1907        if self.mwnd.key_icon.mode != icon_mode {
1908            self.mwnd.key_icon.mode = icon_mode;
1909            self.mwnd.key_icon.anime_start = None;
1910            self.mwnd.key_icon.image = None;
1911        }
1912    }
1913
1914    pub fn reveal_message_now(&mut self) {
1915        let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
1916        if self.mwnd.msg.visible_chars != total {
1917            self.mwnd.msg.visible_chars = total;
1918            self.mwnd.msg.reveal_base = total;
1919            self.mwnd.msg.reveal_start = None;
1920            self.mwnd.msg.text_dirty = true;
1921        }
1922    }
1923
1924    pub fn message_wait_text_fully_revealed(&self) -> bool {
1925        self.message_fully_revealed()
1926    }
1927
1928    pub fn message_waiting(&self) -> bool {
1929        self.mwnd.msg.waiting
1930    }
1931
1932    pub fn message_visible_chars(&self) -> usize {
1933        self.mwnd.msg.visible_chars
1934    }
1935
1936    pub fn message_wait_message_len(&self) -> usize {
1937        self.mwnd.msg.wait_message_len
1938    }
1939
1940    pub fn needs_continuous_frame(
1941        &self,
1942        script: &ScriptRuntimeState,
1943        syscom: &SyscomRuntimeState,
1944    ) -> bool {
1945        if self.mwnd.anim.started_at.is_some() {
1946            return true;
1947        }
1948        if self.mwnd.msg.slide_started_at.is_some() {
1949            return true;
1950        }
1951        if self.mwnd.msg.reveal_start.is_some() && message_speed_ms(script, syscom).is_some() {
1952            return true;
1953        }
1954        if self.mwnd.msg.waiting && !self.message_fully_revealed() {
1955            return true;
1956        }
1957        if self.mwnd.key_icon.appear {
1958            let pat_cnt = if self.mwnd.key_icon.mode == 1 {
1959                self.mwnd.key_icon.page_pat_cnt
1960            } else {
1961                self.mwnd.key_icon.key_pat_cnt
1962            };
1963            return pat_cnt > 1;
1964        }
1965        false
1966    }
1967
1968    pub fn end_wait_message(&mut self) -> bool {
1969        self.mwnd.msg.waiting = false;
1970        self.mwnd.msg.wait_started_at = None;
1971        self.mwnd.key_icon.appear = false;
1972
1973        if self.mwnd.msg.clear_on_wait_end {
1974            self.mwnd.msg.clear_on_wait_end = false;
1975            self.clear_message();
1976            true
1977        } else {
1978            false
1979        }
1980    }
1981
1982    pub fn request_clear_message_on_wait_end(&mut self) {
1983        self.mwnd.msg.clear_on_wait_end = true;
1984    }
1985
1986    pub fn set_sys_overlay(&mut self, active: bool, text: String) {
1987        self.sys.active = active;
1988        if self.sys.text != text {
1989            self.sys.text = text;
1990            self.sys.text_dirty = true;
1991        }
1992    }
1993
1994    pub fn message_text(&self) -> Option<&str> {
1995        self.mwnd.msg.text.as_deref()
1996    }
1997
1998    pub fn name_text(&self) -> Option<&str> {
1999        self.mwnd.name.text.as_deref()
2000    }
2001
2002    pub fn auto_advance_due(
2003        &self,
2004        script: &ScriptRuntimeState,
2005        syscom: &SyscomRuntimeState,
2006    ) -> bool {
2007        if !self.mwnd.msg.waiting {
2008            return false;
2009        }
2010        if script.msg_nowait {
2011            return true;
2012        }
2013        let auto_mode = script.auto_mode_flag || syscom.auto_mode.onoff;
2014        if !auto_mode {
2015            return false;
2016        }
2017        let Some(start) = self.mwnd.msg.wait_started_at else {
2018            return false;
2019        };
2020        let (moji_wait, min_wait) = auto_mode_timing(script, syscom);
2021        let len = self.mwnd.msg.wait_message_len.max(1) as i64;
2022        let by_len = moji_wait.saturating_mul(len);
2023        let total = by_len.max(min_wait).max(0) as u64;
2024        start.elapsed() >= Duration::from_millis(total)
2025    }
2026
2027    fn update_message_reveal(&mut self, script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) {
2028        let total = self.mwnd.msg.text.as_deref().unwrap_or("").chars().count();
2029        if total == 0 {
2030            self.mwnd.msg.visible_chars = 0;
2031            self.mwnd.msg.reveal_base = 0;
2032            self.mwnd.msg.reveal_start = None;
2033            return;
2034        }
2035
2036        if script.msg_nowait {
2037            if self.mwnd.msg.visible_chars != total {
2038                self.mwnd.msg.visible_chars = total;
2039                self.mwnd.msg.text_dirty = true;
2040            }
2041            self.mwnd.msg.reveal_base = total;
2042            self.mwnd.msg.reveal_start = None;
2043            return;
2044        }
2045
2046        let Some(ms_per_char) = message_speed_ms(script, syscom) else {
2047            if self.mwnd.msg.visible_chars != total {
2048                self.mwnd.msg.visible_chars = total;
2049                self.mwnd.msg.text_dirty = true;
2050            }
2051            self.mwnd.msg.reveal_base = total;
2052            self.mwnd.msg.reveal_start = None;
2053            return;
2054        };
2055
2056        let Some(start) = self.mwnd.msg.reveal_start else {
2057            return;
2058        };
2059        let elapsed = start.elapsed().as_millis() as usize;
2060        let inc = if ms_per_char == 0 {
2061            total
2062        } else {
2063            elapsed / ms_per_char as usize
2064        };
2065        let visible = self.mwnd.msg.reveal_base.saturating_add(inc).min(total);
2066        if self.mwnd.msg.visible_chars != visible {
2067            self.mwnd.msg.visible_chars = visible;
2068            self.mwnd.msg.text_dirty = true;
2069        }
2070        if visible >= total {
2071            self.mwnd.msg.reveal_base = total;
2072            self.mwnd.msg.reveal_start = None;
2073        }
2074    }
2075
2076    fn visible_message_text(&self) -> String {
2077        let Some(msg) = self.mwnd.msg.text.as_deref() else {
2078            return String::new();
2079        };
2080        if self.mwnd.msg.visible_chars == 0 {
2081            return String::new();
2082        }
2083        msg.chars().take(self.mwnd.msg.visible_chars).collect()
2084    }
2085
2086    fn refresh_waku_images(
2087        &mut self,
2088        images: &mut crate::image_manager::ImageManager,
2089        project_dir: &Path,
2090    ) {
2091        if let Some(id) = self.mwnd.waku.bg_image {
2092            if self.mwnd.waku.bg_size.is_none() {
2093                if let Some(img) = images.get(id) {
2094                    self.mwnd.waku.bg_size = Some((img.width, img.height));
2095                }
2096            }
2097        }
2098        if self.mwnd.waku.bg_image.is_none() {
2099            if let Some(raw) = self.mwnd.waku.bg_file.as_deref() {
2100                if !raw.is_empty() {
2101                    let path = project_dir.join(raw);
2102                    if let Ok(id) = images.load_file(Path::new(raw), 0) {
2103                        self.mwnd.waku.bg_image = Some(id);
2104                    } else if let Ok(id) = images.load_file(&path, 0) {
2105                        self.mwnd.waku.bg_image = Some(id);
2106                    } else if let Ok(id) = images.load_g00(raw, 0) {
2107                        self.mwnd.waku.bg_image = Some(id);
2108                    } else if let Ok(id) = images.load_bg(raw) {
2109                        self.mwnd.waku.bg_image = Some(id);
2110                    }
2111                    if let Some(id) = self.mwnd.waku.bg_image {
2112                        if let Some(img) = images.get(id) {
2113                            self.mwnd.waku.bg_size = Some((img.width, img.height));
2114                        }
2115                    }
2116                }
2117            }
2118        }
2119
2120        if self.mwnd.waku.filter_image.is_none() {
2121            if let Some(raw) = self.mwnd.waku.filter_file.as_deref() {
2122                if !raw.is_empty() {
2123                    let path = project_dir.join(raw);
2124                    if let Ok(id) = images.load_file(Path::new(raw), 0) {
2125                        self.mwnd.waku.filter_image = Some(id);
2126                    } else if let Ok(id) = images.load_file(&path, 0) {
2127                        self.mwnd.waku.filter_image = Some(id);
2128                    } else if let Ok(id) = images.load_g00(raw, 0) {
2129                        self.mwnd.waku.filter_image = Some(id);
2130                    } else if let Ok(id) = images.load_bg(raw) {
2131                        self.mwnd.waku.filter_image = Some(id);
2132                    }
2133                    if let Some(id) = self.mwnd.waku.filter_image {
2134                        if let Some(img) = images.get(id) {
2135                            self.mwnd.waku.filter_size = Some((img.width, img.height));
2136                        }
2137                    }
2138                }
2139            }
2140        }
2141        if self.mwnd.waku.filter_image.is_none() && self.mwnd.waku.solid_filter_image.is_none() {
2142            let (r, g, b, _a) = self.mwnd.waku.filter_color;
2143            self.mwnd.waku.solid_filter_image = Some(images.solid_rgba((r, g, b, 255)));
2144        }
2145    }
2146
2147    fn refresh_face_image(
2148        &mut self,
2149        images: &mut crate::image_manager::ImageManager,
2150        project_dir: &Path,
2151    ) {
2152        let Some(raw) = self.mwnd.face.file.as_deref() else {
2153            self.mwnd.face.image = None;
2154            return;
2155        };
2156        if raw.is_empty() {
2157            self.mwnd.face.image = None;
2158            return;
2159        }
2160        if self.mwnd.face.image.is_some() {
2161            return;
2162        }
2163        let pat = self.mwnd.face.no.max(0) as u32;
2164        if let Ok(id) = images.load_g00(raw, pat) {
2165            self.mwnd.face.image = Some(id);
2166            return;
2167        }
2168        if let Ok(id) = images.load_bg(raw) {
2169            self.mwnd.face.image = Some(id);
2170            return;
2171        }
2172        let path = project_dir.join(raw);
2173        if path.exists() {
2174            if let Ok(id) = images.load_file(&path, 0) {
2175                self.mwnd.face.image = Some(id);
2176            }
2177        }
2178    }
2179
2180    fn refresh_key_icon_image(
2181        &mut self,
2182        images: &mut crate::image_manager::ImageManager,
2183        project_dir: &Path,
2184    ) {
2185        let (file, pat_cnt, speed) = if self.mwnd.key_icon.mode == 1 {
2186            (
2187                self.mwnd.key_icon.page_file.clone(),
2188                self.mwnd.key_icon.page_pat_cnt,
2189                self.mwnd.key_icon.page_speed,
2190            )
2191        } else {
2192            (
2193                self.mwnd.key_icon.key_file.clone(),
2194                self.mwnd.key_icon.key_pat_cnt,
2195                self.mwnd.key_icon.key_speed,
2196            )
2197        };
2198        let Some(raw) = file.filter(|s| !s.is_empty()) else {
2199            self.mwnd.key_icon.image = None;
2200            self.mwnd.key_icon.file = None;
2201            self.mwnd.key_icon.size = None;
2202            return;
2203        };
2204
2205        if self.mwnd.key_icon.anime_start.is_none() {
2206            self.mwnd.key_icon.anime_start = Some(Instant::now());
2207        }
2208        let elapsed_ms = self
2209            .mwnd
2210            .key_icon
2211            .anime_start
2212            .map(|t| t.elapsed().as_millis() as i64)
2213            .unwrap_or(0);
2214        let pat_cnt = pat_cnt.max(1);
2215        let speed = speed.max(1);
2216        let pat = (elapsed_ms / speed) % pat_cnt;
2217        if self.mwnd.key_icon.file.as_deref() == Some(raw.as_str())
2218            && self.mwnd.key_icon.cached_mode == self.mwnd.key_icon.mode
2219            && self.mwnd.key_icon.cached_pat == pat
2220            && self.mwnd.key_icon.image.is_some()
2221        {
2222            return;
2223        }
2224
2225        let mut loaded = None;
2226        if let Ok(id) = images.load_g00(&raw, pat.max(0) as u32) {
2227            loaded = Some(id);
2228        } else if let Ok(id) = images.load_bg_frame(&raw, pat.max(0) as usize) {
2229            loaded = Some(id);
2230        } else {
2231            let path = project_dir.join(&raw);
2232            if path.exists() {
2233                if let Ok(id) = images.load_file(&path, pat.max(0) as usize) {
2234                    loaded = Some(id);
2235                }
2236            }
2237        }
2238
2239        self.mwnd.key_icon.image = loaded;
2240        self.mwnd.key_icon.file = Some(raw);
2241        self.mwnd.key_icon.cached_mode = self.mwnd.key_icon.mode;
2242        self.mwnd.key_icon.cached_pat = pat;
2243        self.mwnd.key_icon.size =
2244            loaded.and_then(|id| images.get(id).map(|img| (img.width, img.height)));
2245    }
2246
2247    fn refresh_text_images(
2248        &mut self,
2249        images: &mut crate::image_manager::ImageManager,
2250        w: u32,
2251        h: u32,
2252        script: &ScriptRuntimeState,
2253    ) {
2254        let msg_style = self.mwnd_message_text_style(script);
2255        let name_style = self.mwnd_name_text_style(script);
2256        if self.mwnd.msg.text_dirty {
2257            let (x, y, mw, mh) = self.msg_rect(w, h);
2258            let _ = (x, y);
2259            let font_size = self.message_font_px() as f32;
2260            self.mwnd.msg.text_image = self.font_cache.render_mwnd_text_styled_into(
2261                images,
2262                self.mwnd.msg.text_image,
2263                &self.visible_message_text(),
2264                font_size,
2265                mw,
2266                mh,
2267                self.mwnd.window.moji_space,
2268                msg_style,
2269            );
2270            self.mwnd.msg.text_dirty = false;
2271        }
2272
2273        if self.mwnd.name.text_dirty {
2274            let (x, y, mw, mh) = self.name_rect(w, h);
2275            let _ = (x, y);
2276            let font_size = self.name_font_px() as f32;
2277            self.mwnd.name.text_image = self.font_cache.render_mwnd_text_styled_into(
2278                images,
2279                self.mwnd.name.text_image,
2280                self.mwnd.name.text.as_deref().unwrap_or(""),
2281                font_size,
2282                mw,
2283                mh,
2284                self.mwnd.window.moji_space,
2285                name_style,
2286            );
2287            self.mwnd.name.text_dirty = false;
2288        }
2289    }
2290
2291    fn sync_editbox_overlay(
2292        &mut self,
2293        layers: &mut crate::layer::LayerManager,
2294        images: &mut crate::image_manager::ImageManager,
2295        editbox_lists: &HashMap<u32, EditBoxListState>,
2296        focused_editbox: Option<(u32, usize)>,
2297    ) {
2298        let ui_layer = Self::ensure_layer(layers, &mut self.editbox.layer);
2299        if self.editbox.bg_image.is_none() {
2300            self.editbox.bg_image = Some(images.solid_rgba((255, 255, 255, 230)));
2301        }
2302        if self.editbox.focused_bg_image.is_none() {
2303            self.editbox.focused_bg_image = Some(images.solid_rgba((255, 255, 220, 245)));
2304        }
2305
2306        let normal_bg_image = self.editbox.bg_image;
2307        let focused_bg_image = self.editbox.focused_bg_image;
2308        let mut active_keys: Vec<(u32, usize)> = Vec::new();
2309        for (form_id, list) in editbox_lists.iter() {
2310            for (idx, eb) in list.boxes.iter().enumerate() {
2311                let key = (*form_id, idx);
2312                if !eb.created || !eb.visible || eb.window_w <= 0 || eb.window_h <= 0 {
2313                    continue;
2314                }
2315                active_keys.push(key);
2316                let focused = focused_editbox == Some(key);
2317                let entry = self.editbox.entries.entry(key).or_default();
2318                let bg_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut entry.bg_sprite);
2319                let text_sprite =
2320                    Self::ensure_text_sprite(layers, ui_layer, &mut entry.text_sprite);
2321                let w = eb.window_w.max(1) as u32;
2322                let h = eb.window_h.max(1) as u32;
2323                let font_px = eb.window_moji_size.max(12) as u32;
2324                let display_text = editbox_display_text(&eb.text, eb.cursor_pos, focused);
2325
2326                if entry.text_image.is_none()
2327                    || entry.last_text != display_text
2328                    || entry.last_w != w
2329                    || entry.last_h != h
2330                    || entry.last_font_px != font_px
2331                    || entry.last_focused != focused
2332                {
2333                    entry.text_image = self.font_cache.render_text_into(
2334                        images,
2335                        entry.text_image,
2336                        &display_text,
2337                        font_px as f32,
2338                        w.saturating_sub(8).max(1),
2339                        h.max(1),
2340                    );
2341                    entry.last_text = display_text;
2342                    entry.last_w = w;
2343                    entry.last_h = h;
2344                    entry.last_font_px = font_px;
2345                    entry.last_focused = focused;
2346                }
2347
2348                if let Some(s) = layers
2349                    .layer_mut(ui_layer)
2350                    .and_then(|l| l.sprite_mut(bg_sprite))
2351                {
2352                    s.visible = true;
2353                    s.image_id = if focused {
2354                        focused_bg_image
2355                    } else {
2356                        normal_bg_image
2357                    };
2358                    s.fit = SpriteFit::PixelRect;
2359                    s.size_mode = SpriteSizeMode::Explicit {
2360                        width: w,
2361                        height: h,
2362                    };
2363                    s.x = eb.window_x;
2364                    s.y = eb.window_y;
2365                    s.order = 1_950_000 + idx as i32 * 2;
2366                    s.alpha = 255;
2367                }
2368                if let Some(s) = layers
2369                    .layer_mut(ui_layer)
2370                    .and_then(|l| l.sprite_mut(text_sprite))
2371                {
2372                    s.visible = entry.text_image.is_some();
2373                    s.image_id = entry.text_image;
2374                    s.fit = SpriteFit::PixelRect;
2375                    if entry.text_image.is_some() {
2376                        s.size_mode = SpriteSizeMode::Intrinsic;
2377                    } else {
2378                        s.size_mode = SpriteSizeMode::Explicit {
2379                            width: w.saturating_sub(8).max(1),
2380                            height: h,
2381                        };
2382                    }
2383                    s.x = eb.window_x.saturating_add(4);
2384                    s.y = eb.window_y;
2385                    s.order = 1_950_001 + idx as i32 * 2;
2386                    s.alpha = 255;
2387                }
2388            }
2389        }
2390
2391        for (key, entry) in self.editbox.entries.iter_mut() {
2392            if active_keys.iter().any(|x| x == key) {
2393                continue;
2394            }
2395            if let Some(sprite_id) = entry.bg_sprite {
2396                if let Some(s) = layers
2397                    .layer_mut(ui_layer)
2398                    .and_then(|l| l.sprite_mut(sprite_id))
2399                {
2400                    s.visible = false;
2401                }
2402            }
2403            if let Some(sprite_id) = entry.text_sprite {
2404                if let Some(s) = layers
2405                    .layer_mut(ui_layer)
2406                    .and_then(|l| l.sprite_mut(sprite_id))
2407                {
2408                    s.visible = false;
2409                }
2410            }
2411        }
2412    }
2413
2414    pub fn set_msg_back_projection(&mut self, projection: Option<MsgBackUiProjection>) {
2415        match projection {
2416            Some(next) => {
2417                self.msg_back.projection = Some(next);
2418                self.msg_back.text_dirty = true;
2419            }
2420            None => {
2421                if self.msg_back.projection.is_some() {
2422                    self.msg_back.projection = None;
2423                    self.msg_back.text_dirty = true;
2424                }
2425            }
2426        }
2427    }
2428
2429    pub fn msg_back_slider_size(&self) -> Option<(u32, u32)> {
2430        self.msg_back.slider.size
2431    }
2432
2433    pub fn msg_back_slider_screen_pos(&self) -> Option<(i32, i32)> {
2434        let projection = self.msg_back.projection.as_ref()?;
2435        Some((
2436            projection.window_x + projection.slider_pos.0,
2437            projection.window_y + projection.slider_pos.1,
2438        ))
2439    }
2440
2441    pub fn msg_back_hit_action(&self, x: i32, y: i32) -> Option<MsgBackHitAction> {
2442        let projection = self.msg_back.projection.as_ref()?;
2443        if let Some(action) = Self::msg_back_button_hit(
2444            projection,
2445            &self.msg_back.slider,
2446            projection.slider_pos,
2447            x,
2448            y,
2449            MsgBackHitAction::Slider,
2450        ) {
2451            return Some(action);
2452        }
2453        if let Some(action) = Self::msg_back_button_hit(
2454            projection,
2455            &self.msg_back.close_btn,
2456            projection.close_btn_pos,
2457            x,
2458            y,
2459            MsgBackHitAction::Close,
2460        ) {
2461            return Some(action);
2462        }
2463        if let Some(action) = Self::msg_back_button_hit(
2464            projection,
2465            &self.msg_back.msg_up_btn,
2466            projection.msg_up_btn_pos,
2467            x,
2468            y,
2469            MsgBackHitAction::Up,
2470        ) {
2471            return Some(action);
2472        }
2473        if let Some(action) = Self::msg_back_button_hit(
2474            projection,
2475            &self.msg_back.msg_down_btn,
2476            projection.msg_down_btn_pos,
2477            x,
2478            y,
2479            MsgBackHitAction::Down,
2480        ) {
2481            return Some(action);
2482        }
2483        None
2484    }
2485
2486    fn msg_back_button_hit(
2487        projection: &MsgBackUiProjection,
2488        button: &MsgBackButtonRuntime,
2489        pos: (i32, i32),
2490        x: i32,
2491        y: i32,
2492        action: MsgBackHitAction,
2493    ) -> Option<MsgBackHitAction> {
2494        let (w, h) = button.size?;
2495        if w == 0 || h == 0 || button.image.is_none() {
2496            return None;
2497        }
2498        let (center_x, center_y) = button.center.unwrap_or((0, 0));
2499        let left = projection.window_x + pos.0 - center_x;
2500        let top = projection.window_y + pos.1 - center_y;
2501        let right = left.saturating_add(w as i32);
2502        let bottom = top.saturating_add(h as i32);
2503        (left <= x && x < right && top <= y && y < bottom).then_some(action)
2504    }
2505
2506    fn hide_msg_back_sprites(&mut self, layers: &mut crate::layer::LayerManager) {
2507        let Some(ui_layer) = self.mwnd.layer else {
2508            return;
2509        };
2510        let mut hide = |slot: Option<SpriteId>| {
2511            if let Some(sprite_id) = slot {
2512                if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2513                    s.visible = false;
2514                }
2515            }
2516        };
2517        hide(self.msg_back.waku_sprite);
2518        hide(self.msg_back.filter_sprite);
2519        hide(self.msg_back.text_sprite);
2520        for entry in &self.msg_back.text_entries {
2521            hide(entry.sprite);
2522        }
2523        for sep in &self.msg_back.separators {
2524            hide(sep.sprite);
2525        }
2526        for button in &self.msg_back.koe_buttons {
2527            hide(button.sprite);
2528        }
2529        for button in &self.msg_back.load_buttons {
2530            hide(button.sprite);
2531        }
2532        hide(self.msg_back.close_btn.sprite);
2533        hide(self.msg_back.msg_up_btn.sprite);
2534        hide(self.msg_back.msg_down_btn.sprite);
2535        hide(self.msg_back.slider.sprite);
2536        for button in &self.msg_back.ex_buttons {
2537            hide(button.sprite);
2538        }
2539    }
2540
2541    fn ensure_msg_back_button_sprite(
2542        layers: &mut crate::layer::LayerManager,
2543        ui_layer: LayerId,
2544        button: &mut MsgBackButtonRuntime,
2545    ) -> SpriteId {
2546        if let Some(id) = button.sprite {
2547            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
2548                return id;
2549            }
2550        }
2551        let sprite_id = layers
2552            .layer_mut(ui_layer)
2553            .expect("ui_layer exists")
2554            .create_sprite();
2555        button.sprite = Some(sprite_id);
2556        sprite_id
2557    }
2558
2559    fn load_msg_back_image(
2560        images: &mut crate::image_manager::ImageManager,
2561        project_dir: &Path,
2562        file: Option<&String>,
2563    ) -> Option<ImageId> {
2564        let raw = file.map(|s| s.trim()).filter(|s| !s.is_empty())?;
2565        if let Ok(id) = images.load_g00(raw, 0) {
2566            return Some(id);
2567        }
2568        if let Ok(id) = images.load_bg_frame(raw, 0) {
2569            return Some(id);
2570        }
2571        let path = project_dir.join(raw);
2572        if path.exists() {
2573            if let Ok(id) = images.load_file(&path, 0) {
2574                return Some(id);
2575            }
2576        }
2577        None
2578    }
2579
2580    fn refresh_msg_back_button_image(
2581        button: &mut MsgBackButtonRuntime,
2582        images: &mut crate::image_manager::ImageManager,
2583        project_dir: &Path,
2584        file: Option<&String>,
2585    ) {
2586        if button.cached_file.as_ref() == file {
2587            return;
2588        }
2589        button.image = Self::load_msg_back_image(images, project_dir, file);
2590        button.size = button
2591            .image
2592            .and_then(|id| images.get(id).map(|img| (img.width, img.height)));
2593        button.center = button
2594            .image
2595            .and_then(|id| images.get(id).map(|img| (img.center_x, img.center_y)));
2596        button.cached_file = file.cloned();
2597    }
2598
2599    fn apply_msg_back_pct_anchor(
2600        sprite: &mut Sprite,
2601        images: &crate::image_manager::ImageManager,
2602        image: Option<ImageId>,
2603    ) {
2604        if let Some(img) = image.and_then(|id| images.get(id)) {
2605            sprite.object_anchor = true;
2606            sprite.texture_center_x = img.center_x as f32;
2607            sprite.texture_center_y = img.center_y as f32;
2608        } else {
2609            sprite.object_anchor = false;
2610            sprite.texture_center_x = 0.0;
2611            sprite.texture_center_y = 0.0;
2612        }
2613    }
2614
2615    fn sync_msg_back_button_sprite(
2616        layers: &mut crate::layer::LayerManager,
2617        ui_layer: LayerId,
2618        images: &crate::image_manager::ImageManager,
2619        button: &mut MsgBackButtonRuntime,
2620        projection: &MsgBackUiProjection,
2621        pos: (i32, i32),
2622        order: i32,
2623    ) {
2624        let sprite_id = Self::ensure_msg_back_button_sprite(layers, ui_layer, button);
2625        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2626            s.visible = button.image.is_some();
2627            s.image_id = button.image;
2628            s.fit = SpriteFit::PixelRect;
2629            s.size_mode = SpriteSizeMode::Intrinsic;
2630            s.x = projection.window_x + pos.0;
2631            s.y = projection.window_y + pos.1;
2632            s.order = order;
2633            s.alpha = 255;
2634            s.tr = 255;
2635            s.alpha_test = true;
2636            s.alpha_blend = true;
2637            s.color_rate = 0;
2638            s.color_add_r = 0;
2639            s.color_add_g = 0;
2640            s.color_add_b = 0;
2641            s.color_r = 0;
2642            s.color_g = 0;
2643            s.color_b = 0;
2644            s.mask_mode = 0;
2645            Self::apply_msg_back_pct_anchor(s, images, button.image);
2646            s.dst_clip = None;
2647            s.src_clip = None;
2648        }
2649    }
2650
2651    fn hide_msg_back_button_sprite(
2652        layers: &mut crate::layer::LayerManager,
2653        ui_layer: LayerId,
2654        button: &MsgBackButtonRuntime,
2655    ) {
2656        if let Some(sprite_id) = button.sprite {
2657            if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2658                s.visible = false;
2659            }
2660        }
2661    }
2662
2663    fn sync_msg_back_abs_button_sprite(
2664        layers: &mut crate::layer::LayerManager,
2665        ui_layer: LayerId,
2666        images: &crate::image_manager::ImageManager,
2667        button: &mut MsgBackButtonRuntime,
2668        pos: (i32, i32),
2669        order: i32,
2670        clip: Option<crate::layer::ClipRect>,
2671    ) {
2672        let sprite_id = Self::ensure_msg_back_button_sprite(layers, ui_layer, button);
2673        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2674            s.visible = button.image.is_some();
2675            s.image_id = button.image;
2676            s.fit = SpriteFit::PixelRect;
2677            s.size_mode = SpriteSizeMode::Intrinsic;
2678            s.x = pos.0;
2679            s.y = pos.1;
2680            s.order = order;
2681            s.alpha = 255;
2682            s.tr = 255;
2683            s.alpha_test = true;
2684            s.alpha_blend = true;
2685            s.color_rate = 0;
2686            s.color_add_r = 0;
2687            s.color_add_g = 0;
2688            s.color_add_b = 0;
2689            s.color_r = 0;
2690            s.color_g = 0;
2691            s.color_b = 0;
2692            s.mask_mode = 0;
2693            Self::apply_msg_back_pct_anchor(s, images, button.image);
2694            s.dst_clip = clip;
2695            s.src_clip = None;
2696        }
2697    }
2698
2699    fn sync_msg_back_ui(
2700        &mut self,
2701        layers: &mut crate::layer::LayerManager,
2702        images: &mut crate::image_manager::ImageManager,
2703        project_dir: &Path,
2704    ) {
2705        let Some(projection) = self.msg_back.projection.clone() else {
2706            self.hide_msg_back_sprites(layers);
2707            return;
2708        };
2709        let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
2710
2711        let waku_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.waku_sprite);
2712        let filter_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.filter_sprite);
2713        let old_text_sprite = Self::ensure_text_sprite(layers, ui_layer, &mut self.msg_back.text_sprite);
2714        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(old_text_sprite)) {
2715            s.visible = false;
2716        }
2717
2718        if self.msg_back.cached_waku_file.as_ref() != projection.waku_file.as_ref() {
2719            self.msg_back.waku_image = Self::load_msg_back_image(images, project_dir, projection.waku_file.as_ref());
2720            self.msg_back.cached_waku_file = projection.waku_file.clone();
2721        }
2722        if self.msg_back.cached_filter_file.as_ref() != projection.filter_file.as_ref() {
2723            self.msg_back.filter_image = Self::load_msg_back_image(images, project_dir, projection.filter_file.as_ref());
2724            self.msg_back.cached_filter_file = projection.filter_file.clone();
2725        }
2726        if self.msg_back.solid_filter_color != Some(projection.filter_rgba) {
2727            self.msg_back.solid_filter_image = Some(images.solid_rgba(projection.filter_rgba));
2728            self.msg_back.solid_filter_color = Some(projection.filter_rgba);
2729        }
2730
2731        Self::refresh_msg_back_button_image(
2732            &mut self.msg_back.close_btn,
2733            images,
2734            project_dir,
2735            projection.close_btn_file.as_ref(),
2736        );
2737        Self::refresh_msg_back_button_image(
2738            &mut self.msg_back.msg_up_btn,
2739            images,
2740            project_dir,
2741            projection.msg_up_btn_file.as_ref(),
2742        );
2743        Self::refresh_msg_back_button_image(
2744            &mut self.msg_back.msg_down_btn,
2745            images,
2746            project_dir,
2747            projection.msg_down_btn_file.as_ref(),
2748        );
2749        Self::refresh_msg_back_button_image(
2750            &mut self.msg_back.slider,
2751            images,
2752            project_dir,
2753            projection.slider_file.as_ref(),
2754        );
2755        if self.msg_back.ex_buttons.len() < 4 {
2756            self.msg_back.ex_buttons.resize_with(4, MsgBackButtonRuntime::default);
2757        }
2758        for i in 0..4 {
2759            Self::refresh_msg_back_button_image(
2760                &mut self.msg_back.ex_buttons[i],
2761                images,
2762                project_dir,
2763                projection.ex_btn_files[i].as_ref(),
2764            );
2765        }
2766
2767        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(waku_sprite)) {
2768            s.visible = self.msg_back.waku_image.is_some();
2769            s.image_id = self.msg_back.waku_image;
2770            s.fit = SpriteFit::PixelRect;
2771            s.size_mode = if self.msg_back.waku_image.is_some() {
2772                SpriteSizeMode::Intrinsic
2773            } else {
2774                SpriteSizeMode::Explicit {
2775                    width: projection.window_w,
2776                    height: projection.window_h,
2777                }
2778            };
2779            s.x = projection.window_x;
2780            s.y = projection.window_y;
2781            s.order = msg_back_packed_sorter_key(projection.order, projection.waku_layer_rep);
2782            s.alpha = 255;
2783            s.tr = 255;
2784            s.alpha_test = true;
2785            s.alpha_blend = true;
2786            s.color_rate = 0;
2787            s.color_add_r = 0;
2788            s.color_add_g = 0;
2789            s.color_add_b = 0;
2790            s.color_r = 0;
2791            s.color_g = 0;
2792            s.color_b = 0;
2793            s.mask_mode = 0;
2794            Self::apply_msg_back_pct_anchor(s, images, self.msg_back.waku_image);
2795            s.dst_clip = None;
2796            s.src_clip = None;
2797        }
2798
2799        let filter_image = self.msg_back.filter_image.or(self.msg_back.solid_filter_image);
2800        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(filter_sprite)) {
2801            let (ml, mt, mr, mb) = projection.filter_margin;
2802            s.visible = filter_image.is_some();
2803            s.image_id = filter_image;
2804            s.fit = SpriteFit::PixelRect;
2805            if self.msg_back.filter_image.is_some() {
2806                s.size_mode = SpriteSizeMode::Intrinsic;
2807                s.x = projection.window_x;
2808                s.y = projection.window_y;
2809            } else {
2810                s.size_mode = SpriteSizeMode::Explicit {
2811                    width: (projection.window_w as i64 - ml - mr).max(1) as u32,
2812                    height: (projection.window_h as i64 - mt - mb).max(1) as u32,
2813                };
2814                s.x = projection.window_x + ml as i32;
2815                s.y = projection.window_y + mt as i32;
2816            }
2817            s.order = msg_back_packed_sorter_key(projection.order, projection.filter_layer_rep);
2818            let (cfg_r, cfg_g, cfg_b, cfg_a) = projection.filter_config_rgba;
2819            s.alpha = 255;
2820            s.tr = cfg_a;
2821            s.alpha_test = true;
2822            s.alpha_blend = true;
2823            s.color_rate = 0;
2824            s.color_add_r = cfg_r;
2825            s.color_add_g = cfg_g;
2826            s.color_add_b = cfg_b;
2827            s.color_r = 0;
2828            s.color_g = 0;
2829            s.color_b = 0;
2830            s.mask_mode = 0;
2831            if self.msg_back.filter_image.is_some() {
2832                Self::apply_msg_back_pct_anchor(s, images, self.msg_back.filter_image);
2833            } else {
2834                s.object_anchor = false;
2835                s.texture_center_x = 0.0;
2836                s.texture_center_y = 0.0;
2837            }
2838            s.dst_clip = None;
2839            s.src_clip = None;
2840        }
2841
2842        let (dl, dt, dr, db) = projection.disp_margin;
2843        let clip = crate::layer::ClipRect {
2844            left: projection.window_x + dl as i32,
2845            top: projection.window_y + dt as i32,
2846            right: projection.window_x + projection.window_w as i32 - dr as i32,
2847            bottom: projection.window_y + projection.window_h as i32 - db as i32,
2848        };
2849
2850        if self.msg_back.separators.len() < projection.separators.len() {
2851            self.msg_back.separators.resize_with(projection.separators.len(), MsgBackButtonRuntime::default);
2852        }
2853        for i in 0..projection.separators.len() {
2854            let sep = &projection.separators[i];
2855            Self::refresh_msg_back_button_image(
2856                &mut self.msg_back.separators[i],
2857                images,
2858                project_dir,
2859                sep.file.as_ref(),
2860            );
2861            Self::sync_msg_back_abs_button_sprite(
2862                layers,
2863                ui_layer,
2864                images,
2865                &mut self.msg_back.separators[i],
2866                (projection.window_x + sep.x, projection.window_y + sep.y),
2867                msg_back_packed_sorter_key(projection.order, projection.waku_layer_rep),
2868                Some(clip),
2869            );
2870        }
2871        for i in projection.separators.len()..self.msg_back.separators.len() {
2872            Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.separators[i]);
2873        }
2874
2875        if self.msg_back.text_entries.len() < projection.text_entries.len() {
2876            self.msg_back.text_entries.resize_with(projection.text_entries.len(), MsgBackTextRuntime::default);
2877        }
2878        for i in 0..projection.text_entries.len() {
2879            let entry = &projection.text_entries[i];
2880            let runtime = &mut self.msg_back.text_entries[i];
2881            let sprite_id = Self::ensure_text_sprite(layers, ui_layer, &mut runtime.sprite);
2882            let render_text = entry.text.replace('\u{0007}', "\n");
2883            runtime.image = self.font_cache.render_mwnd_text_styled_into(
2884                images,
2885                runtime.image,
2886                &render_text,
2887                projection.moji_size.max(1) as f32,
2888                entry.width.max(1),
2889                entry.height.max(1),
2890                projection.moji_space,
2891                entry.style,
2892            );
2893            if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2894                s.visible = runtime.image.is_some();
2895                s.image_id = runtime.image;
2896                s.fit = SpriteFit::PixelRect;
2897                if runtime.image.is_some() {
2898                    s.size_mode = SpriteSizeMode::Intrinsic;
2899                } else {
2900                    s.size_mode = SpriteSizeMode::Explicit {
2901                        width: entry.width.max(1),
2902                        height: entry.height.max(1),
2903                    };
2904                }
2905                s.x = projection.window_x + entry.x;
2906                s.y = projection.window_y + entry.y;
2907                s.order = msg_back_packed_sorter_key(projection.order, 0);
2908                s.alpha = 255;
2909                s.tr = 255;
2910                s.alpha_test = false;
2911                s.alpha_blend = true;
2912                s.color_rate = 0;
2913                s.color_add_r = 0;
2914                s.color_add_g = 0;
2915                s.color_add_b = 0;
2916                s.color_r = 0;
2917                s.color_g = 0;
2918                s.color_b = 0;
2919                s.mask_mode = 0;
2920                s.src_clip = None;
2921                s.dst_clip = Some(clip);
2922            }
2923        }
2924        for i in projection.text_entries.len()..self.msg_back.text_entries.len() {
2925            if let Some(sprite_id) = self.msg_back.text_entries[i].sprite {
2926                if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(sprite_id)) {
2927                    s.visible = false;
2928                }
2929            }
2930        }
2931
2932        if self.msg_back.koe_buttons.len() < projection.koe_buttons.len() {
2933            self.msg_back.koe_buttons.resize_with(projection.koe_buttons.len(), MsgBackButtonRuntime::default);
2934        }
2935        for i in 0..projection.koe_buttons.len() {
2936            let btn = &projection.koe_buttons[i];
2937            Self::refresh_msg_back_button_image(
2938                &mut self.msg_back.koe_buttons[i],
2939                images,
2940                project_dir,
2941                btn.file.as_ref(),
2942            );
2943            Self::sync_msg_back_abs_button_sprite(
2944                layers,
2945                ui_layer,
2946                images,
2947                &mut self.msg_back.koe_buttons[i],
2948                (projection.window_x + btn.x, projection.window_y + btn.y),
2949                msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2950                Some(clip),
2951            );
2952        }
2953        for i in projection.koe_buttons.len()..self.msg_back.koe_buttons.len() {
2954            Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.koe_buttons[i]);
2955        }
2956
2957        if self.msg_back.load_buttons.len() < projection.load_buttons.len() {
2958            self.msg_back.load_buttons.resize_with(projection.load_buttons.len(), MsgBackButtonRuntime::default);
2959        }
2960        for i in 0..projection.load_buttons.len() {
2961            let btn = &projection.load_buttons[i];
2962            Self::refresh_msg_back_button_image(
2963                &mut self.msg_back.load_buttons[i],
2964                images,
2965                project_dir,
2966                btn.file.as_ref(),
2967            );
2968            Self::sync_msg_back_abs_button_sprite(
2969                layers,
2970                ui_layer,
2971                images,
2972                &mut self.msg_back.load_buttons[i],
2973                (projection.window_x + btn.x, projection.window_y + btn.y),
2974                msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2975                Some(clip),
2976            );
2977        }
2978        for i in projection.load_buttons.len()..self.msg_back.load_buttons.len() {
2979            Self::hide_msg_back_button_sprite(layers, ui_layer, &self.msg_back.load_buttons[i]);
2980        }
2981
2982        Self::sync_msg_back_button_sprite(
2983            layers,
2984            ui_layer,
2985            images,
2986            &mut self.msg_back.close_btn,
2987            &projection,
2988            projection.close_btn_pos,
2989            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2990        );
2991        Self::sync_msg_back_button_sprite(
2992            layers,
2993            ui_layer,
2994            images,
2995            &mut self.msg_back.msg_up_btn,
2996            &projection,
2997            projection.msg_up_btn_pos,
2998            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
2999        );
3000        Self::sync_msg_back_button_sprite(
3001            layers,
3002            ui_layer,
3003            images,
3004            &mut self.msg_back.msg_down_btn,
3005            &projection,
3006            projection.msg_down_btn_pos,
3007            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
3008        );
3009        Self::sync_msg_back_button_sprite(
3010            layers,
3011            ui_layer,
3012            images,
3013            &mut self.msg_back.slider,
3014            &projection,
3015            projection.slider_pos,
3016            msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
3017        );
3018        for i in 0..4 {
3019            let button = &mut self.msg_back.ex_buttons[i];
3020            Self::sync_msg_back_button_sprite(
3021                layers,
3022                ui_layer,
3023                images,
3024                button,
3025                &projection,
3026                projection.ex_btn_pos[i],
3027                msg_back_packed_sorter_key(projection.order, projection.moji_layer_rep),
3028            );
3029        }
3030    }
3031
3032    fn sync_sys_overlay(
3033        &mut self,
3034        layers: &mut crate::layer::LayerManager,
3035        images: &mut crate::image_manager::ImageManager,
3036        w: u32,
3037        h: u32,
3038    ) {
3039        if !self.sys.active {
3040            if let Some(ui_layer) = self.mwnd.layer {
3041                if let Some(sprite_id) = self.sys.bg_sprite {
3042                    if let Some(s) = layers
3043                        .layer_mut(ui_layer)
3044                        .and_then(|l| l.sprite_mut(sprite_id))
3045                    {
3046                        s.visible = false;
3047                    }
3048                }
3049                if let Some(sprite_id) = self.sys.text_sprite {
3050                    if let Some(s) = layers
3051                        .layer_mut(ui_layer)
3052                        .and_then(|l| l.sprite_mut(sprite_id))
3053                    {
3054                        s.visible = false;
3055                    }
3056                }
3057            }
3058            return;
3059        }
3060        let ui_layer = Self::ensure_layer(layers, &mut self.mwnd.layer);
3061        let bg = self.ensure_sys_bg_sprite(layers, ui_layer);
3062        let text = Self::ensure_text_sprite(layers, ui_layer, &mut self.sys.text_sprite);
3063
3064        if self.sys.bg_image.is_none() {
3065            self.sys.bg_image = Some(images.solid_rgba((0, 0, 0, 180)));
3066        }
3067
3068        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(bg)) {
3069            s.visible = self.sys.active;
3070            s.image_id = self.sys.bg_image;
3071            s.fit = SpriteFit::PixelRect;
3072            s.size_mode = SpriteSizeMode::Explicit {
3073                width: w,
3074                height: h,
3075            };
3076            s.x = 0;
3077            s.y = 0;
3078            s.order = 2_000_000;
3079        }
3080
3081        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(text)) {
3082            s.visible = self.sys.active && self.sys.text_image.is_some();
3083            s.image_id = self.sys.text_image;
3084            s.fit = SpriteFit::PixelRect;
3085            s.size_mode = SpriteSizeMode::Explicit {
3086                width: w.saturating_sub(80),
3087                height: h.saturating_sub(80),
3088            };
3089            s.x = 40;
3090            s.y = 40;
3091            s.order = 2_000_010;
3092        }
3093
3094        if self.sys.text_dirty {
3095            self.sys.text_image = self.font_cache.render_text_into(
3096                images,
3097                self.sys.text_image,
3098                &self.sys.text,
3099                24.0,
3100                w.saturating_sub(80),
3101                h.saturating_sub(80),
3102            );
3103            self.sys.text_dirty = false;
3104        }
3105        if let Some(s) = layers.layer_mut(ui_layer).and_then(|l| l.sprite_mut(text)) {
3106            s.visible = self.sys.active && self.sys.text_image.is_some();
3107            s.image_id = self.sys.text_image;
3108        }
3109    }
3110
3111    fn ensure_sys_bg_sprite(
3112        &mut self,
3113        layers: &mut crate::layer::LayerManager,
3114        ui_layer: LayerId,
3115    ) -> SpriteId {
3116        if let Some(id) = self.sys.bg_sprite {
3117            if layers.layer(ui_layer).and_then(|l| l.sprite(id)).is_some() {
3118                return id;
3119            }
3120        }
3121        let sprite_id = layers
3122            .layer_mut(ui_layer)
3123            .expect("ui_layer exists")
3124            .create_sprite();
3125        self.sys.bg_sprite = Some(sprite_id);
3126        sprite_id
3127    }
3128}
3129
3130fn editbox_display_text(text: &str, cursor_pos: usize, focused: bool) -> String {
3131    if !focused {
3132        return text.to_string();
3133    }
3134    let pos = if text.is_char_boundary(cursor_pos.min(text.len())) {
3135        cursor_pos.min(text.len())
3136    } else {
3137        text.char_indices()
3138            .map(|(i, _)| i)
3139            .take_while(|i| *i < cursor_pos)
3140            .last()
3141            .unwrap_or(0)
3142    };
3143    let mut out = String::with_capacity(text.len() + 1);
3144    out.push_str(&text[..pos]);
3145    out.push('|');
3146    out.push_str(&text[pos..]);
3147    out
3148}
3149
3150fn auto_mode_timing(script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) -> (i64, i64) {
3151    const GET_AUTO_MODE_MOJI_WAIT: i32 = 254;
3152    const GET_AUTO_MODE_MIN_WAIT: i32 = 257;
3153
3154    let moji_wait = if script.auto_mode_moji_wait >= 0 {
3155        script.auto_mode_moji_wait
3156    } else {
3157        *syscom
3158            .config_int
3159            .get(&GET_AUTO_MODE_MOJI_WAIT)
3160            .unwrap_or(&-1)
3161    };
3162    let min_wait = if script.auto_mode_min_wait >= 0 {
3163        script.auto_mode_min_wait
3164    } else {
3165        *syscom.config_int.get(&GET_AUTO_MODE_MIN_WAIT).unwrap_or(&0)
3166    };
3167
3168    let moji_wait = if moji_wait >= 0 { moji_wait } else { 80 };
3169    let min_wait = if min_wait >= 0 { min_wait } else { 0 };
3170    (moji_wait, min_wait)
3171}
3172
3173fn message_speed_ms(script: &ScriptRuntimeState, syscom: &SyscomRuntimeState) -> Option<u64> {
3174    const GET_MESSAGE_SPEED: i32 = crate::runtime::constants::elm_value::SYSCOM_GET_MESSAGE_SPEED;
3175    const GET_MESSAGE_NOWAIT: i32 = crate::runtime::constants::elm_value::SYSCOM_GET_MESSAGE_NOWAIT;
3176
3177    if script.msg_nowait || *syscom.config_int.get(&GET_MESSAGE_NOWAIT).unwrap_or(&0) != 0 {
3178        return None;
3179    }
3180    let speed = if script.msg_speed >= 0 {
3181        script.msg_speed
3182    } else {
3183        *syscom.config_int.get(&GET_MESSAGE_SPEED).unwrap_or(&20)
3184    };
3185    if speed <= 0 {
3186        None
3187    } else {
3188        Some(speed as u64)
3189    }
3190}
3191
3192impl UiRuntime {
3193    fn scan_font_dir(&mut self, project_dir: &Path) {
3194        if self.font_scanned {
3195            return;
3196        }
3197        self.font_scanned = true;
3198        for dir in [project_dir.join("font"), project_dir.join("fonts")] {
3199            let Ok(entries) = std::fs::read_dir(dir) else {
3200                continue;
3201            };
3202            for entry in entries.flatten() {
3203                let path = entry.path();
3204                if !path.is_file() {
3205                    continue;
3206                }
3207                let ext = path
3208                    .extension()
3209                    .and_then(|s| s.to_str())
3210                    .unwrap_or("")
3211                    .to_ascii_lowercase();
3212                if ext == "ttf" || ext == "otf" || ext == "ttc" {
3213                    self.font_paths.push(path);
3214                }
3215            }
3216        }
3217    }
3218}