Skip to main content

siglus_scene_vm/runtime/
globals.rs

1/// The runtime's tables (objects, stage lists, properties) are keyed by
2/// small integers and looked up many times per object operation; SipHash
3/// was about a tenth of the VM's frame time on consoles.
4pub type HashMap<K, V> = std::collections::HashMap<K, V, rustc_hash::FxBuildHasher>;
5pub type HashSet<K> = std::collections::HashSet<K, rustc_hash::FxBuildHasher>;
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9use crate::assets::RgbaImage;
10use crate::platform_time::{Duration, Instant};
11use crate::runtime::gan::GanState;
12use crate::runtime::int_event::IntEvent;
13
14use crate::image_manager::ImageHandle;
15use crate::layer::{LayerId, SpriteId};
16
17/// Screen wipe transition state.
18///
19/// This models the timing and script-visible behavior of the original `Gp_wipe`
20/// subsystem. Rendering is handled elsewhere; here we only track parameters and
21/// completion.
22#[derive(Debug, Clone)]
23pub struct WipeState {
24    /// Stage-form namespace whose NEXT stage was populated for this wipe.
25    ///
26    /// Normal scene stages use `FORM_GLOBAL_STAGE`; EXCALL stages use the
27    /// corresponding local namespace (`FORM_GLOBAL_STAGE ^ 0x4000`).  The
28    /// original `C_tnm_wipe::end()` reinitializes only the NEXT stage that
29    /// belongs to the active wipe range, so the Rust runtime must retain this
30    /// target until the wipe is ended.
31    pub stage_form_id: u32,
32    pub mask_file: Option<String>,
33    pub mask_image_id: Option<ImageHandle>,
34    pub wipe_type: i32,
35    pub wipe_time_ms: i32,
36    pub speed_mode: i32,
37    pub start_time_ms: i32,
38    pub option: Vec<i32>,
39
40    pub begin_order: i32,
41    pub end_order: i32,
42    pub begin_layer: i32,
43    pub end_layer: i32,
44
45    pub wait_flag: bool,
46    pub key_wait_mode: i32,
47    pub with_low_order: i32,
48
49    /// C++ mask generators consume the process RNG once at wipe creation.
50    pub random_seed: u32,
51
52    /// Original C_tnm_wipe frame state.  Time is advanced explicitly from
53    /// `local_wipe_time_past`; wall-clock time must never advance a paused wipe.
54    step: i32,
55    cur_time_ms: i64,
56    progress_value: f32,
57    done: bool,
58}
59
60impl WipeState {
61    #[allow(clippy::too_many_arguments)]
62    pub fn new(
63        stage_form_id: u32,
64        mask_file: Option<String>,
65        mask_image_id: Option<ImageHandle>,
66        wipe_type: i32,
67        wipe_time_ms: i32,
68        start_time_ms: i32,
69        speed_mode: i32,
70        option: Vec<i32>,
71        begin_order: i32,
72        end_order: i32,
73        begin_layer: i32,
74        end_layer: i32,
75        wait_flag: bool,
76        key_wait_mode: i32,
77        with_low_order: i32,
78    ) -> Self {
79        static NEXT_WIPE_SEED: AtomicU32 = AtomicU32::new(0x6d2b_79f5);
80        let seq = NEXT_WIPE_SEED.fetch_add(0x9e37_79b9, Ordering::Relaxed);
81        let random_seed = seq
82            ^ (wipe_type as u32).rotate_left(7)
83            ^ (wipe_time_ms as u32).rotate_left(17)
84            ^ (start_time_ms as u32).rotate_left(25);
85
86        Self {
87            stage_form_id,
88            mask_file,
89            mask_image_id,
90            wipe_type,
91            wipe_time_ms,
92            speed_mode,
93            start_time_ms,
94            option,
95            begin_order,
96            end_order,
97            begin_layer,
98            end_layer,
99            wait_flag,
100            key_wait_mode,
101            with_low_order,
102            random_seed,
103            step: 0,
104            cur_time_ms: 0,
105            progress_value: 0.0,
106            done: false,
107        }
108    }
109
110    /// Mirrors C_tnm_wipe::update_time() followed by C_tnm_wipe::frame().
111    /// The first two frame steps intentionally discard accumulated time before
112    /// installing `start_time_ms`, preventing a delayed first presentation from
113    /// making the wipe jump forward.
114    pub fn advance(&mut self, past_time_ms: i32) {
115        if self.done {
116            return;
117        }
118        self.cur_time_ms = self.cur_time_ms.saturating_add(past_time_ms.max(0) as i64);
119
120        if self.step == 0 {
121            self.step = 1;
122            return;
123        }
124        if self.step == 1 {
125            self.cur_time_ms = self.start_time_ms as i64;
126            self.step = 2;
127        }
128
129        let end = self.wipe_time_ms as f64;
130        let cur = self.cur_time_ms as f64;
131        let raw = if self.wipe_time_ms <= 0 {
132            1.0
133        } else {
134            match self.speed_mode {
135                1 => (cur * cur) / (end * end),
136                2 => 1.0 - ((cur - end) * (cur - end)) / (end * end),
137                _ => cur / end,
138            }
139        };
140        self.progress_value = raw.clamp(0.0, 1.0) as f32;
141        if self.cur_time_ms >= self.wipe_time_ms as i64 {
142            self.progress_value = 1.0;
143            self.done = true;
144        }
145    }
146
147    pub fn is_done(&self) -> bool {
148        self.done
149    }
150
151    pub fn progress(&self) -> f32 {
152        self.progress_value
153    }
154
155    #[allow(dead_code)]
156    pub fn remaining_ms(&self) -> u64 {
157        if self.done {
158            0
159        } else {
160            (self.wipe_time_ms as i64 - self.cur_time_ms).max(0) as u64
161        }
162    }
163}
164
165#[derive(Debug, Clone)]
166pub struct ScriptRuntimeState {
167    // C_tnm_local_data_pod fields which are not exposed by the SCRIPT form but
168    // still participate in original local-save compatibility.
169    pub cur_koe_no: i64,
170    pub cur_chr_no: i64,
171    pub cur_read_flag_scn_no: i64,
172    pub cur_read_flag_flag_no: i64,
173    pub msg_back_save_cntr: i64,
174    pub multi_msg_mode: bool,
175
176    pub dont_set_save_point: bool,
177    pub skip_disable: bool,
178    pub ctrl_disable: bool,
179    pub not_stop_skip_by_click: bool,
180    pub not_skip_msg_by_click: bool,
181    pub skip_unread_message: bool,
182
183    pub auto_mode_flag: bool,
184    pub auto_mode_moji_wait: i64,
185    pub auto_mode_min_wait: i64,
186    pub auto_mode_moji_cnt: i64,
187
188    pub mouse_cursor_hide_onoff: i64,
189    pub mouse_cursor_hide_time: i64,
190
191    pub msg_speed: i64,
192    pub msg_nowait: bool,
193    pub async_msg_mode: bool,
194    pub async_msg_mode_once: bool,
195
196    pub hide_mwnd_disable: bool,
197    pub msg_back_disable: bool,
198    pub msg_back_off: bool,
199    pub msg_back_disp_off: bool,
200
201    pub cursor_disp_off: bool,
202    pub cursor_move_by_key_disable: bool,
203    pub cursor_runtime_visible: bool,
204    pub key_disable: HashSet<u8>,
205
206    pub mwnd_anime_off_flag: bool,
207    pub mwnd_anime_on_flag: bool,
208    pub mwnd_disp_off_flag: bool,
209
210    pub koe_dont_stop_on_flag: bool,
211    pub koe_dont_stop_off_flag: bool,
212
213    pub shortcut_disable: bool,
214    pub quake_stop_flag: bool,
215    pub emote_mouth_stop_flag: bool,
216    pub bgmfade_flag: bool,
217    pub wait_display_vsync_off_flag: bool,
218    pub skip_trigger: bool,
219    pub ignore_r_flag: bool,
220    pub cursor_no: i64,
221
222    pub time_stop_flag: bool,
223    pub counter_time_stop_flag: bool,
224    pub frame_action_time_stop_flag: bool,
225    pub stage_time_stop_flag: bool,
226
227    // SiglusEngine 1.1.141.2 joypad-mode override.  -1 means use the
228    // Gameexe configuration, 0 forces joypad mode off, and 1 forces it allowed.
229    pub joypad_mode_override: i64,
230
231    pub font_name: String,
232    pub font_bold: i64,
233    pub font_shadow: i64,
234}
235
236impl Default for ScriptRuntimeState {
237    fn default() -> Self {
238        Self {
239            cur_koe_no: -1,
240            cur_chr_no: -1,
241            cur_read_flag_scn_no: -1,
242            cur_read_flag_flag_no: -1,
243            msg_back_save_cntr: 0,
244            multi_msg_mode: false,
245            dont_set_save_point: false,
246            skip_disable: false,
247            ctrl_disable: false,
248            not_stop_skip_by_click: false,
249            not_skip_msg_by_click: false,
250            skip_unread_message: false,
251            auto_mode_flag: false,
252            auto_mode_moji_wait: -1,
253            auto_mode_min_wait: -1,
254            auto_mode_moji_cnt: 0,
255            mouse_cursor_hide_onoff: -1,
256            mouse_cursor_hide_time: -1,
257            msg_speed: -1,
258            msg_nowait: false,
259            async_msg_mode: false,
260            async_msg_mode_once: false,
261            hide_mwnd_disable: false,
262            msg_back_disable: false,
263            msg_back_off: false,
264            msg_back_disp_off: false,
265            cursor_disp_off: false,
266            cursor_move_by_key_disable: false,
267            cursor_runtime_visible: true,
268            key_disable: HashSet::default(),
269            mwnd_anime_off_flag: false,
270            mwnd_anime_on_flag: false,
271            mwnd_disp_off_flag: false,
272            koe_dont_stop_on_flag: false,
273            koe_dont_stop_off_flag: false,
274            shortcut_disable: false,
275            quake_stop_flag: false,
276            emote_mouth_stop_flag: false,
277            bgmfade_flag: false,
278            wait_display_vsync_off_flag: false,
279            skip_trigger: false,
280            ignore_r_flag: false,
281            cursor_no: -1,
282            time_stop_flag: false,
283            counter_time_stop_flag: false,
284            frame_action_time_stop_flag: false,
285            stage_time_stop_flag: false,
286            joypad_mode_override: -1,
287            font_name: String::new(),
288            font_bold: -1,
289            font_shadow: -1,
290        }
291    }
292}
293
294#[derive(Debug, Clone, Default)]
295pub struct SystemMessageBoxRecord {
296    pub kind: i32,
297    pub text: String,
298    pub debug_only: bool,
299}
300
301#[derive(Debug, Clone)]
302pub struct SystemMessageBoxButton {
303    pub label: String,
304    pub value: i64,
305}
306
307#[derive(Debug, Clone)]
308pub struct SystemMessageBoxModalState {
309    pub request_id: u64,
310    pub kind: i32,
311    pub text: String,
312    pub debug_only: bool,
313    pub buttons: Vec<SystemMessageBoxButton>,
314    pub cursor: usize,
315    pub native_pending: bool,
316    pub complete_wait_with_value: bool,
317}
318
319impl SystemMessageBoxModalState {
320    pub fn selected_value(&self) -> i64 {
321        self.buttons
322            .get(self.cursor.min(self.buttons.len().saturating_sub(1)))
323            .map(|b| b.value)
324            .unwrap_or(0)
325    }
326
327    pub fn cancel_value(&self) -> i64 {
328        self.buttons
329            .last()
330            .map(|b| b.value)
331            .unwrap_or_else(|| self.selected_value())
332    }
333}
334
335#[derive(Debug, Clone)]
336pub struct SystemRuntimeState {
337    pub active_flag: bool,
338    pub debug_flag: bool,
339    pub language_code: String,
340    pub debug_logs: Vec<String>,
341    pub dummy_checks: HashSet<String>,
342    pub bench_dialogs: Vec<String>,
343    pub messagebox_history: Vec<SystemMessageBoxRecord>,
344    pub messagebox_response_queue: Vec<i64>,
345    pub messagebox_modal: Option<SystemMessageBoxModalState>,
346    pub messagebox_modal_result: Option<i64>,
347    pub chihaya_display_adapter_name: String,
348}
349
350impl Default for SystemRuntimeState {
351    fn default() -> Self {
352        Self {
353            active_flag: true,
354            debug_flag: false,
355            language_code: std::env::var("SIGLUS_LANGUAGE").unwrap_or_else(|_| "JP".to_string()),
356            debug_logs: Vec::new(),
357            dummy_checks: HashSet::default(),
358            bench_dialogs: Vec::new(),
359            messagebox_history: Vec::new(),
360            messagebox_response_queue: Vec::new(),
361            messagebox_modal: None,
362            messagebox_modal_result: None,
363            chihaya_display_adapter_name: String::new(),
364        }
365    }
366}
367
368#[derive(Debug, Clone, Copy, Default)]
369pub struct ToggleFeatureState {
370    pub onoff: bool,
371    pub enable: bool,
372    pub exist: bool,
373}
374
375impl ToggleFeatureState {
376    pub fn check_enabled(&self) -> i64 {
377        if self.enable && self.exist { 1 } else { 0 }
378    }
379}
380
381#[derive(Debug, Clone, Copy, Default)]
382pub struct ValueFeatureState {
383    pub value: i64,
384    pub enable: bool,
385    pub exist: bool,
386}
387
388impl ValueFeatureState {
389    pub fn check_enabled(&self) -> i64 {
390        if self.enable && self.exist { 1 } else { 0 }
391    }
392}
393
394#[derive(Debug, Clone, Default)]
395pub struct SaveSlotState {
396    /// Mirrors C_tnm_save_cache's three states:
397    /// false = not queried yet; true+exist = positive header cache;
398    /// true+!exist = negative (file missing/invalid) cache.
399    pub header_cache_valid: bool,
400    pub exist: bool,
401    pub year: i64,
402    pub month: i64,
403    pub day: i64,
404    pub weekday: i64,
405    pub hour: i64,
406    pub minute: i64,
407    pub second: i64,
408    pub millisecond: i64,
409    pub title: String,
410    pub message: String,
411    pub full_message: String,
412    pub comment: String,
413    /// Header-only fields that are not directly exposed by the script API but
414    /// must survive SET_SAVE_COMMENT / SET_SAVE_VALUE unchanged.
415    pub comment2: String,
416    pub packed_data_size: usize,
417    pub append_dir: String,
418    pub append_name: String,
419    pub values: HashMap<i32, i64>,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum SyscomPendingProcKind {
424    EndGame,
425    ReturnToSel,
426    ReturnToMenu,
427    RestartScene,
428    Save,
429    Load,
430    QuickSave,
431    QuickLoad,
432    BacklogLoad,
433    MsgBack,
434    OpenSyscomMenu,
435    OpenSave,
436    OpenLoad,
437    OpenConfig,
438    /// A built-in settings dialog; never re-enter the game's CONFIG_SCENE.
439    OpenConfigDialog,
440}
441
442#[derive(Debug, Clone)]
443pub struct SyscomPendingProc {
444    pub kind: SyscomPendingProcKind,
445    pub warning: bool,
446    pub se_play: bool,
447    pub fade_out: bool,
448    pub leave_msgbk: bool,
449    pub save_id: i64,
450}
451
452/// Cross-platform replacement for the native Syscom dialogs used by the
453/// original Windows build when a project does not provide SAVE_SCENE,
454/// LOAD_SCENE, CONFIG_SCENE, or CANCEL_SCENE.  The dialog itself is rendered
455/// by the engine and receives input through the normal winit path, so the same
456/// state machine is used on desktop, mobile, and WebAssembly hosts.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub enum SyscomFallbackDialogKind {
459    SystemMenu,
460    SaveMenu,
461    LoadMenu,
462    ConfigRoot,
463    ConfigWindow,
464    ConfigVolumeRoot,
465    ConfigVolume(usize),
466    ConfigMessage,
467    ConfigAuto,
468    ConfigFont,
469    ConfigOther,
470    Notice,
471}
472
473#[derive(Debug, Clone)]
474pub struct SyscomFallbackDialogState {
475    pub kind: SyscomFallbackDialogKind,
476    pub page: usize,
477    /// A modal result belongs to this state only after `awaiting_result` is
478    /// set.  This prevents an unrelated SYSTEM.MESSAGEBOX result from being
479    /// consumed by the Syscom fallback state machine.
480    pub awaiting_result: bool,
481    pub return_kind: Option<SyscomFallbackDialogKind>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub struct ConfigChrKoeState {
486    pub onoff: bool,
487    pub volume: i64,
488}
489
490impl Default for ConfigChrKoeState {
491    fn default() -> Self {
492        Self {
493            onoff: true,
494            volume: 255,
495        }
496    }
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct OriginalConfigRuntimeState {
501    pub screen_size_mode: i64,
502    pub screen_size_mode_window: i64,
503    pub screen_size_scale: (i64, i64),
504    pub screen_size_free: (i64, i64),
505    pub fullscreen_change_resolution: bool,
506    pub fullscreen_display_cnt: i64,
507    pub fullscreen_display_no: i64,
508    pub fullscreen_resolution_cnt: i64,
509    pub fullscreen_resolution_no: i64,
510    pub fullscreen_resolution: (i64, i64),
511    pub fullscreen_mode: i64,
512    pub fullscreen_scale: (i64, i64),
513    pub fullscreen_scale_sync_switch: bool,
514    pub fullscreen_move: (i64, i64),
515    pub all_sound_user_volume: i64,
516    pub sound_user_volume: [i64; 32],
517    pub play_all_sound_check: bool,
518    pub play_sound_check: [bool; 32],
519    pub bgmfade_volume: i64,
520    pub bgmfade_use_check: bool,
521    pub filter_color_argb: u32,
522    pub font_proportional: bool,
523    pub font_name: String,
524    pub font_shadow: i64,
525    pub font_futoku: bool,
526    pub message_speed: i64,
527    pub message_speed_nowait: bool,
528    pub auto_mode_onoff: bool,
529    pub auto_mode_moji_wait: i64,
530    pub auto_mode_min_wait: i64,
531    pub mouse_cursor_hide_onoff: bool,
532    pub mouse_cursor_hide_time: i64,
533    pub jitan_normal_onoff: bool,
534    pub jitan_auto_mode_onoff: bool,
535    pub jitan_msgbk_onoff: bool,
536    pub jitan_speed: i64,
537    pub koe_mode: i64,
538    pub chrkoe: Vec<ConfigChrKoeState>,
539    pub message_chrcolor_flag: bool,
540    pub object_disp_flag: Vec<bool>,
541    pub global_extra_switch_flag: Vec<bool>,
542    pub global_extra_mode_flag: Vec<i64>,
543    pub sleep_flag: bool,
544    pub no_wipe_anime_flag: bool,
545    pub skip_wipe_anime_flag: bool,
546    pub no_mwnd_anime_flag: bool,
547    pub wheel_next_message_flag: bool,
548    pub koe_dont_stop_flag: bool,
549    pub skip_unread_message_flag: bool,
550    pub saveload_alert_flag: bool,
551    pub saveload_dblclick_flag: bool,
552    pub ss_path: String,
553    pub editor_path: String,
554    pub koe_path: String,
555    pub koe_tool_path: String,
556    // config.sav v1.4 additions from SiglusEngine 1.1.141.x.
557    pub joypad_swap_ab: bool,
558    pub joypad_swap_xy: bool,
559}
560
561impl Default for OriginalConfigRuntimeState {
562    fn default() -> Self {
563        Self {
564            screen_size_mode: 0,
565            screen_size_mode_window: 0,
566            screen_size_scale: (100, 100),
567            screen_size_free: (0, 0),
568            fullscreen_change_resolution: false,
569            fullscreen_display_cnt: 0,
570            fullscreen_display_no: 0,
571            fullscreen_resolution_cnt: 0,
572            fullscreen_resolution_no: 0,
573            fullscreen_resolution: (0, 0),
574            fullscreen_mode: 0,
575            fullscreen_scale: (100, 100),
576            fullscreen_scale_sync_switch: true,
577            fullscreen_move: (0, 0),
578            all_sound_user_volume: 255,
579            sound_user_volume: [255; 32],
580            play_all_sound_check: true,
581            play_sound_check: [true; 32],
582            bgmfade_volume: 192,
583            bgmfade_use_check: true,
584            filter_color_argb: 0x8000_0000,
585            font_proportional: false,
586            font_name: "MS ゴシック".to_string(),
587            font_shadow: 2,
588            font_futoku: false,
589            message_speed: 20,
590            message_speed_nowait: false,
591            auto_mode_onoff: false,
592            auto_mode_moji_wait: 70,
593            auto_mode_min_wait: 300,
594            mouse_cursor_hide_onoff: false,
595            mouse_cursor_hide_time: 5000,
596            jitan_normal_onoff: false,
597            jitan_auto_mode_onoff: false,
598            jitan_msgbk_onoff: false,
599            jitan_speed: 100,
600            koe_mode: 0,
601            chrkoe: vec![ConfigChrKoeState::default(); 64],
602            message_chrcolor_flag: true,
603            object_disp_flag: vec![true; 4],
604            global_extra_switch_flag: vec![true; 4],
605            global_extra_mode_flag: vec![0; 4],
606            sleep_flag: false,
607            no_wipe_anime_flag: false,
608            skip_wipe_anime_flag: true,
609            no_mwnd_anime_flag: false,
610            wheel_next_message_flag: true,
611            koe_dont_stop_flag: false,
612            skip_unread_message_flag: false,
613            saveload_alert_flag: true,
614            saveload_dblclick_flag: false,
615            ss_path: String::new(),
616            editor_path: String::new(),
617            koe_path: String::new(),
618            koe_tool_path: String::new(),
619            joypad_swap_ab: false,
620            joypad_swap_xy: false,
621        }
622    }
623}
624
625#[derive(Debug, Clone)]
626pub struct SyscomRuntimeState {
627    pub syscom_menu_disable: bool,
628    pub menu_open: bool,
629    pub menu_kind: Option<i32>,
630    pub menu_result: Option<i64>,
631    pub menu_cursor: usize,
632    pub font_list: Vec<String>,
633    pub mwnd_btn_disable_all: bool,
634    pub mwnd_btn_touch_disable: bool,
635    pub mwnd_btn_disable: HashMap<i64, bool>,
636    pub read_skip: ToggleFeatureState,
637    pub unread_skip: ToggleFeatureState,
638    pub auto_skip: ToggleFeatureState,
639    pub auto_mode: ToggleFeatureState,
640    pub hide_mwnd: ToggleFeatureState,
641    pub local_extra_switch: ToggleFeatureState,
642    pub local_extra_mode: ValueFeatureState,
643    pub local_extra_switches: [ToggleFeatureState; 4],
644    pub local_extra_modes: [ValueFeatureState; 4],
645    pub msg_back: ToggleFeatureState,
646    pub msg_back_open: bool,
647    pub msg_back_view_pos: usize,
648    pub msg_back_scroll_pos: i32,
649    pub msg_back_slider_pos: i32,
650    pub msg_back_target_no: isize,
651    pub msg_back_mouse_target_no: isize,
652    pub msg_back_msg_total_height: i32,
653    pub msg_back_proc_initialized: bool,
654    pub msg_back_slider_dragging: bool,
655    pub msg_back_slider_drag_start_mouse: i32,
656    pub msg_back_slider_drag_start_pos: i32,
657    pub msg_back_content_dragging: bool,
658    pub msg_back_content_drag_start_mouse: i32,
659    pub msg_back_content_drag_start_scroll_pos: i32,
660    pub return_to_sel: ToggleFeatureState,
661    pub config_feature: ToggleFeatureState,
662    pub manual_feature: ToggleFeatureState,
663    pub version_feature: ToggleFeatureState,
664    pub return_to_menu: ToggleFeatureState,
665    pub end_game: ToggleFeatureState,
666    pub cancel_feature: ToggleFeatureState,
667    pub save_feature: ToggleFeatureState,
668    pub load_feature: ToggleFeatureState,
669    pub replay_koe: Option<(i64, i64)>,
670    pub current_save_scene_title: String,
671    pub current_save_message: String,
672    pub current_save_full_message: String,
673    pub total_play_time: i64,
674    pub save_slots: Vec<SaveSlotState>,
675    pub quick_save_slots: Vec<SaveSlotState>,
676    pub inner_save_exists: bool,
677    pub inner_save_streams: Vec<Vec<u8>>,
678    pub sel_save_stock_stream: Vec<u8>,
679    pub sel_save_ids: Vec<[u16; 7]>,
680    pub end_save_exists: bool,
681    pub last_menu_call: i32,
682    pub system_extra_int_value: i64,
683    pub system_extra_str_value: String,
684    pub config_int: HashMap<i32, i64>,
685    pub config_str: HashMap<i32, String>,
686    pub original_config: OriginalConfigRuntimeState,
687    pub chrkoe_look_flags: HashMap<String, bool>,
688    pub capture_buffer: Option<RgbaImage>,
689    pub capture_size: Option<(u32, u32)>,
690    pub return_scene_once: Option<(String, i64)>,
691    pub pending_proc: Option<SyscomPendingProc>,
692    pub msg_back_load_tid: i64,
693    pub fallback_dialog: Option<SyscomFallbackDialogState>,
694    pub fallback_origin: Option<SyscomFallbackDialogKind>,
695}
696
697impl Default for SyscomRuntimeState {
698    fn default() -> Self {
699        Self {
700            syscom_menu_disable: false,
701            menu_open: false,
702            menu_kind: None,
703            menu_result: None,
704            menu_cursor: 0,
705            font_list: Vec::new(),
706            mwnd_btn_disable_all: false,
707            mwnd_btn_touch_disable: false,
708            mwnd_btn_disable: HashMap::default(),
709            read_skip: ToggleFeatureState {
710                onoff: false,
711                enable: true,
712                exist: true,
713            },
714            unread_skip: ToggleFeatureState {
715                onoff: false,
716                enable: true,
717                exist: true,
718            },
719            auto_skip: ToggleFeatureState {
720                onoff: false,
721                enable: true,
722                exist: true,
723            },
724            auto_mode: ToggleFeatureState {
725                onoff: false,
726                enable: true,
727                exist: true,
728            },
729            hide_mwnd: ToggleFeatureState {
730                onoff: false,
731                enable: true,
732                exist: true,
733            },
734            local_extra_switch: ToggleFeatureState {
735                onoff: false,
736                enable: true,
737                exist: true,
738            },
739            local_extra_mode: ValueFeatureState {
740                value: 0,
741                enable: true,
742                exist: true,
743            },
744            local_extra_switches: [ToggleFeatureState {
745                onoff: false,
746                enable: true,
747                exist: true,
748            }; 4],
749            local_extra_modes: [ValueFeatureState {
750                value: 0,
751                enable: true,
752                exist: true,
753            }; 4],
754            msg_back: ToggleFeatureState {
755                onoff: false,
756                enable: true,
757                exist: true,
758            },
759            msg_back_open: false,
760            msg_back_view_pos: 0,
761            msg_back_scroll_pos: 0,
762            msg_back_slider_pos: 0,
763            msg_back_target_no: -1,
764            msg_back_mouse_target_no: -1,
765            msg_back_msg_total_height: 0,
766            msg_back_proc_initialized: false,
767            msg_back_slider_dragging: false,
768            msg_back_slider_drag_start_mouse: 0,
769            msg_back_slider_drag_start_pos: 0,
770            msg_back_content_dragging: false,
771            msg_back_content_drag_start_mouse: 0,
772            msg_back_content_drag_start_scroll_pos: 0,
773            return_to_sel: ToggleFeatureState {
774                onoff: false,
775                enable: true,
776                exist: true,
777            },
778            config_feature: ToggleFeatureState {
779                onoff: false,
780                enable: true,
781                exist: true,
782            },
783            manual_feature: ToggleFeatureState {
784                onoff: false,
785                enable: true,
786                exist: true,
787            },
788            version_feature: ToggleFeatureState {
789                onoff: false,
790                enable: true,
791                exist: true,
792            },
793            return_to_menu: ToggleFeatureState {
794                onoff: false,
795                enable: true,
796                exist: true,
797            },
798            end_game: ToggleFeatureState {
799                onoff: false,
800                enable: true,
801                exist: true,
802            },
803            cancel_feature: ToggleFeatureState {
804                onoff: false,
805                enable: true,
806                exist: true,
807            },
808            save_feature: ToggleFeatureState {
809                onoff: false,
810                enable: true,
811                exist: true,
812            },
813            load_feature: ToggleFeatureState {
814                onoff: false,
815                enable: true,
816                exist: true,
817            },
818            replay_koe: None,
819            current_save_scene_title: String::new(),
820            current_save_message: String::new(),
821            current_save_full_message: String::new(),
822            total_play_time: 0,
823            save_slots: Vec::new(),
824            quick_save_slots: Vec::new(),
825            inner_save_exists: false,
826            inner_save_streams: Vec::new(),
827            sel_save_stock_stream: Vec::new(),
828            sel_save_ids: Vec::new(),
829            end_save_exists: false,
830            last_menu_call: 0,
831            system_extra_int_value: 0,
832            system_extra_str_value: String::new(),
833            config_int: HashMap::default(),
834            config_str: HashMap::default(),
835            original_config: OriginalConfigRuntimeState::default(),
836            chrkoe_look_flags: HashMap::default(),
837            capture_buffer: None,
838            capture_size: None,
839            return_scene_once: None,
840            pending_proc: None,
841            msg_back_load_tid: 0,
842            fallback_dialog: None,
843            fallback_origin: None,
844        }
845    }
846}
847/// Global mutable state used by various "global element" (form) handlers.
848///
849/// This crate keeps these structures generic on purpose: many Siglus
850/// "global elements" are simple lists, counters, etc.
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852pub enum LightType {
853    None = -1,
854    Directional = 0,
855    Point = 1,
856    Spot = 2,
857    ShadowMapSpot = 3,
858}
859
860pub const WORLD_LIGHT_MAX: usize = 128;
861pub const OBJ_DIRECTIONAL_LIGHT_MAX: usize = 4;
862pub const OBJ_POINT_LIGHT_MAX: usize = 4;
863pub const OBJ_SPOT_LIGHT_MAX: usize = 4;
864
865#[derive(Debug, Clone)]
866pub struct LightState {
867    pub id: i32,
868    pub kind: LightType,
869    pub diffuse: [f32; 4],
870    pub ambient: [f32; 4],
871    pub specular: [f32; 4],
872    pub pos: [f32; 3],
873    pub dir: [f32; 3],
874    pub attenuation0: f32,
875    pub attenuation1: f32,
876    pub attenuation2: f32,
877    pub range: f32,
878    pub theta_deg: f32,
879    pub phi_deg: f32,
880    pub falloff: f32,
881}
882
883impl LightState {
884    pub fn directional(id: i32, dir: [f32; 3]) -> Self {
885        Self {
886            id,
887            kind: LightType::Directional,
888            diffuse: [1.0, 1.0, 1.0, 1.0],
889            ambient: [0.18, 0.18, 0.18, 1.0],
890            specular: [0.0, 0.0, 0.0, 1.0],
891            pos: [0.0, 0.0, 0.0],
892            dir,
893            attenuation0: 1.0,
894            attenuation1: 0.0,
895            attenuation2: 0.0,
896            range: 5000.0,
897            theta_deg: 20.0,
898            phi_deg: 40.0,
899            falloff: 1.0,
900        }
901    }
902}
903
904impl Default for LightState {
905    fn default() -> Self {
906        Self::directional(0, [0.0, 0.0, -1.0])
907    }
908}
909
910#[derive(Debug, Clone)]
911pub struct FogGlobalState {
912    pub enabled: bool,
913    pub name: String,
914    pub near: f32,
915    pub far: f32,
916    pub color: [f32; 4],
917    pub scroll_x: f32,
918    pub x_event: IntEvent,
919    pub texture_image_id: Option<ImageHandle>,
920}
921
922impl Default for FogGlobalState {
923    fn default() -> Self {
924        Self {
925            enabled: false,
926            name: String::new(),
927            near: 0.0,
928            far: 0.0,
929            color: [0.62, 0.62, 0.62, 1.0],
930            scroll_x: 0.0,
931            x_event: IntEvent::new(0),
932            texture_image_id: None,
933        }
934    }
935}
936
937impl FogGlobalState {
938    pub fn set_x(&mut self, x: i32) {
939        self.x_event.set_value(x);
940        self.x_event.frame();
941        self.scroll_x = self.x_event.get_total_value() as f32;
942    }
943
944    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
945        self.x_event.update_time(past_game_time, past_real_time);
946    }
947
948    pub fn frame(&mut self) {
949        self.x_event.frame();
950        self.scroll_x = self.x_event.get_total_value() as f32;
951    }
952}
953
954/// Original `C_elm_pcmch` parameters persisted in local saves.
955///
956/// Playback handles alone cannot reconstruct the source kind (PCM/BGM/KOE/SE)
957/// or the volume routing flags, so keep the exact command-side state alongside
958/// the modern audio backend.
959#[derive(Debug, Clone)]
960pub struct PcmChPersistentState {
961    pub pcm_name: String,
962    pub bgm_name: String,
963    pub koe_no: i64,
964    pub se_no: i64,
965    pub volume_type: i64,
966    pub chara_no: i64,
967    pub volume: i64,
968    pub delay_time: i64,
969    pub fade_in_time: i64,
970    pub loop_flag: bool,
971    pub bgm_fade_target_flag: bool,
972    pub bgm_fade2_target_flag: bool,
973    pub bgm_fade_source_flag: bool,
974    pub ready_flag: bool,
975}
976
977impl Default for PcmChPersistentState {
978    fn default() -> Self {
979        Self {
980            pcm_name: String::new(),
981            bgm_name: String::new(),
982            koe_no: -1,
983            se_no: -1,
984            volume_type: 2, // TNM_VOLUME_TYPE_PCM
985            chara_no: -1,
986            volume: 255,
987            delay_time: 0,
988            fade_in_time: 0,
989            loop_flag: false,
990            bgm_fade_target_flag: false,
991            bgm_fade2_target_flag: false,
992            bgm_fade_source_flag: false,
993            ready_flag: false,
994        }
995    }
996}
997
998#[derive(Debug, Clone)]
999pub struct SoundRoutingState {
1000    /// Current C_elm_koe playback metadata.  This is independent from the
1001    /// message-local `cur_koe_no`: EXKOE must affect the actual buffer volume
1002    /// without replacing the voice attached to message history/replay.
1003    pub koe_chara_no: i64,
1004    pub koe_ex_flag: bool,
1005    /// Original global guard used when restoring BGMFADE2 after a message
1006    /// without an attached voice.
1007    pub bgmfade2_need_flag: bool,
1008    pub bgmfade_flag: bool,
1009    pub bgmfade_cur_time: i64,
1010    pub bgmfade_start_value: i64,
1011    pub bgmfade_total_volume: i64,
1012    pub bgmfade2_flag: bool,
1013    pub bgmfade2_cur_time: i64,
1014    pub bgmfade2_start_value: i64,
1015    pub bgmfade2_total_volume: i64,
1016    /// Cached `#CHRKOE.i` character-number lists parsed from the immutable
1017    /// Gameexe table. Re-parsing these entries on every routing update was the
1018    /// dominant per-frame allocation hotspot on mobile.
1019    pub chrkoe_chara_lists_cache: Arc<Vec<Vec<i64>>>,
1020}
1021
1022impl Default for SoundRoutingState {
1023    fn default() -> Self {
1024        Self {
1025            koe_chara_no: -1,
1026            koe_ex_flag: false,
1027            bgmfade2_need_flag: false,
1028            bgmfade_flag: false,
1029            bgmfade_cur_time: 0,
1030            bgmfade_start_value: 255,
1031            bgmfade_total_volume: 255,
1032            bgmfade2_flag: false,
1033            bgmfade2_cur_time: 0,
1034            bgmfade2_start_value: 255,
1035            bgmfade2_total_volume: 255,
1036            chrkoe_chara_lists_cache: Arc::new(Vec::new()),
1037        }
1038    }
1039}
1040
1041#[derive(Debug, Clone)]
1042pub struct GlobalState {
1043    /// Generic int-list storage keyed by the global form ID.
1044    pub int_lists: HashMap<u32, Vec<i64>>,
1045    /// Generic string-list storage keyed by the global form ID.
1046    pub str_lists: HashMap<u32, Vec<String>>,
1047    /// Counter-list storage keyed by the global form ID.
1048    pub counter_lists: HashMap<u32, Vec<Counter>>,
1049    /// PCM-event lists keyed by the global form ID.
1050    pub pcm_event_lists: HashMap<u32, Vec<PcmEventState>>,
1051    /// Exact C_elm_pcmch save parameters, indexed by PCM channel.
1052    pub pcmch_persistent: Vec<PcmChPersistentState>,
1053    /// Dynamic category/chara/BGM-fade routing state from ifc_sound.cpp.
1054    pub sound_routing: SoundRoutingState,
1055    /// Original `Gp_read_flag[scene][flag]` backing store.  Rows are keyed by
1056    /// current Scene.pck scene number and contain one byte per lexer read flag.
1057    pub read_flags: HashMap<i64, Vec<u8>>,
1058
1059    /// Generic integer-event roots keyed by the form ID.
1060    pub int_event_roots: HashMap<u32, IntEvent>,
1061    /// Generic integer-event lists keyed by the form ID.
1062    pub int_event_lists: HashMap<u32, Vec<IntEvent>>,
1063
1064    /// Generic int properties keyed by (form_id -> op_id).
1065    pub int_props: HashMap<u32, HashMap<i32, i64>>,
1066    /// Generic string properties keyed by (form_id -> op_id).
1067    pub str_props: HashMap<u32, HashMap<i32, String>>,
1068
1069    /// CGTABLE global disable flag.
1070    pub cg_table_off: bool,
1071
1072    /// DATABASE global disable flag.
1073    pub database_off: bool,
1074
1075    /// G00BUF slots. Each slot stores an ImageHandle loaded from the `g00/` directory.
1076    pub g00buf: Vec<Option<ImageHandle>>,
1077    /// Original C_elm_g00_buf persists file names, not texture handles.
1078    pub g00buf_names: Vec<Option<String>>,
1079
1080    /// RNG state for MATH.RAND (xorshift32). 0 means "uninitialized".
1081    pub rng_state: u32,
1082
1083    /// Mask subsystem state keyed by the (guessed or mapped) form id.
1084    pub mask_lists: HashMap<u32, MaskListState>,
1085    /// EditBox subsystem state keyed by the (guessed or mapped) form id.
1086    pub editbox_lists: HashMap<u32, EditBoxListState>,
1087    /// Currently focused editbox (form_id, index).
1088    pub focused_editbox: Option<(u32, usize)>,
1089    /// Cross-platform fallback clipboard used when the host does not expose an OS clipboard.
1090    pub editbox_clipboard: String,
1091    /// Display-mode transition counter used by editbox frame visibility.
1092    pub change_display_mode_proc_cnt: i32,
1093
1094    /// Global frame-action roots keyed by the owning form id.
1095    pub frame_actions: HashMap<u32, ObjectFrameActionState>,
1096    /// Global frame-action channel lists keyed by the owning form id.
1097    pub frame_action_lists: HashMap<u32, Vec<ObjectFrameActionState>>,
1098    /// Finish callbacks queued by FRAMEACTION.START/START_REAL/END.
1099    pub pending_frame_action_finishes: Vec<PendingFrameActionFinish>,
1100    /// Button decided actions queued from C_elm_object::button_event semantics.
1101    pub pending_button_actions: Vec<PendingButtonAction>,
1102
1103    /// Stage UI subsystem state keyed by the stage form ID.
1104    /// Boxed: `with_stage_state` takes a state out of the map around each
1105    /// stage operation, which moved the whole struct twice per op.
1106    pub stage_forms: HashMap<u32, Box<StageFormState>>,
1107    /// Currently focused stage group selection (form_id, stage_idx, group_idx).
1108    pub focused_stage_group: Option<(u32, i64, usize)>,
1109    /// Currently focused message-window selection (form_id, stage_idx, mwnd_idx).
1110    pub focused_stage_mwnd: Option<(u32, i64, usize)>,
1111    /// Complete S_element handles used by GLOBAL.SET_MWND/SET_SEL_MWND and local save.
1112    /// The stage/no fields below are derived caches for the Rust stage backend; the
1113    /// element itself is authoritative, matching C_tnm_local_data.
1114    pub current_mwnd_element: Vec<i32>,
1115    pub current_sel_mwnd_element: Vec<i32>,
1116    pub last_mwnd_element: Vec<i32>,
1117    pub current_mwnd_no: Option<usize>,
1118    pub current_mwnd_stage_idx: i64,
1119    pub current_sel_mwnd_no: Option<usize>,
1120    pub current_sel_mwnd_stage_idx: i64,
1121    pub last_mwnd_no: Option<usize>,
1122    pub last_mwnd_stage_idx: i64,
1123
1124    /// Original C_tnm_timer local fields saved by C_tnm_eng::save_local().
1125    pub local_real_time: i64,
1126    pub local_game_time: i64,
1127    pub local_wipe_time: i64,
1128
1129    /// Original extend-enable local flag lists H/I/J.
1130    pub local_flag_h: Vec<i64>,
1131    pub local_flag_i: Vec<i64>,
1132    pub local_flag_j: Vec<i64>,
1133    /// GLOBAL.SELBTN button-selection runtime state.
1134    pub selbtn: BtnSelectRuntimeState,
1135    /// Last object target touched by stage/object dispatch. Compact object-only chains in scene bytecode
1136    /// use this as the ambient current-object context when they omit the object index.
1137    pub current_stage_object: Option<(i64, usize)>,
1138    pub current_object_chain: Option<Vec<i32>>,
1139
1140    /// Screen subsystem state keyed by the screen form ID.
1141    pub screen_forms: HashMap<u32, ScreenFormState>,
1142
1143    /// Message backlog (MSGBK) subsystem state keyed by the form ID.
1144    pub msgbk_forms: HashMap<u32, MsgBackState>,
1145
1146    /// Script/global runtime state translated from the original the original implementation command handlers.
1147    pub script: ScriptRuntimeState,
1148
1149    /// System helper runtime state.
1150    pub system: SystemRuntimeState,
1151
1152    /// System-command runtime state.
1153    pub syscom: SyscomRuntimeState,
1154    /// Active GLOBAL.MOV direct movie player.
1155    pub mov: GlobalMovieState,
1156
1157    /// Capture buffer reserved for the optional Tweet integration.
1158    ///
1159    /// This must never be used by OBJECT.CREATE_CAPTURE or save thumbnails.
1160    pub capture_image: Option<RgbaImage>,
1161    /// A CAPTURE_FOR_TWEET request is materialized only at the following DISP
1162    /// boundary, matching the original TNM_CAPTURE_TYPE_TWEET flow.
1163    pub capture_for_tweet_pending: bool,
1164    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1165    pub twitter: crate::runtime::twitter::TwitterState,
1166    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1167    pub twitter_dialog_request: Option<crate::runtime::twitter::TwitterDialogRequest>,
1168    /// Capture buffer used exclusively by OBJECT.CREATE_CAPTURE.
1169    pub capture_for_object_image: Option<RgbaImage>,
1170    /// Save thumbnail capture prepared before entering the save UI.
1171    pub save_thumb_capture_image: Option<RgbaImage>,
1172    /// C++ `TNM_CAPTURE_PRIOR_*` value currently owning the save-thumbnail capture.
1173    pub save_thumb_capture_prior: i32,
1174
1175    /// Currently selected append directory used by original file resolution helpers.
1176    pub append_dir: String,
1177    /// Display name for the currently selected append directory.
1178    pub append_name: String,
1179
1180    /// BGM table listened flags keyed by registered name.
1181    pub bgm_table_listened: HashMap<String, bool>,
1182    /// BGM table flags indexed by original BGM registration number.
1183    pub bgm_table_flags: Vec<bool>,
1184    /// Default flag applied to names not seen yet via BGMTABLE.SET_ALL_FLAG.
1185    pub bgm_table_all_flag: bool,
1186
1187    /// Active wipe transition (WIPE / MASK_WIPE).
1188    pub wipe: Option<WipeState>,
1189
1190    /// Global light manager keyed by original engine light id.
1191    pub lights: HashMap<i32, LightState>,
1192    /// Global fog state.
1193    pub fog_global: FogGlobalState,
1194
1195    /// Monotonic frame counter used by render effects.
1196    pub render_frame: u64,
1197}
1198
1199impl Default for GlobalState {
1200    fn default() -> Self {
1201        Self {
1202            int_lists: HashMap::default(),
1203            str_lists: HashMap::default(),
1204            counter_lists: HashMap::default(),
1205            pcm_event_lists: HashMap::default(),
1206            pcmch_persistent: Vec::new(),
1207            sound_routing: SoundRoutingState::default(),
1208            read_flags: HashMap::default(),
1209            int_event_roots: HashMap::default(),
1210            int_event_lists: HashMap::default(),
1211            int_props: HashMap::default(),
1212            str_props: HashMap::default(),
1213            cg_table_off: false,
1214            database_off: false,
1215            g00buf: Vec::new(),
1216            g00buf_names: Vec::new(),
1217            rng_state: 0,
1218            mask_lists: HashMap::default(),
1219            editbox_lists: HashMap::default(),
1220            focused_editbox: None,
1221            editbox_clipboard: String::new(),
1222            change_display_mode_proc_cnt: 0,
1223
1224            frame_actions: HashMap::default(),
1225            frame_action_lists: HashMap::default(),
1226            pending_frame_action_finishes: Vec::new(),
1227            pending_button_actions: Vec::new(),
1228            stage_forms: HashMap::default(),
1229            focused_stage_group: None,
1230            focused_stage_mwnd: None,
1231            current_mwnd_element: vec![
1232                crate::runtime::forms::codes::ELM_GLOBAL_FRONT,
1233                crate::runtime::forms::codes::ELM_STAGE_MWND,
1234                crate::runtime::forms::codes::ELM_ARRAY,
1235                0,
1236            ],
1237            current_sel_mwnd_element: vec![
1238                crate::runtime::forms::codes::ELM_GLOBAL_FRONT,
1239                crate::runtime::forms::codes::ELM_STAGE_MWND,
1240                crate::runtime::forms::codes::ELM_ARRAY,
1241                1,
1242            ],
1243            last_mwnd_element: Vec::new(),
1244            current_mwnd_no: Some(0),
1245            current_mwnd_stage_idx: 1,
1246            current_sel_mwnd_no: Some(1),
1247            current_sel_mwnd_stage_idx: 1,
1248            last_mwnd_no: None,
1249            last_mwnd_stage_idx: 1,
1250            local_real_time: 0,
1251            local_game_time: 0,
1252            local_wipe_time: 0,
1253            local_flag_h: Vec::new(),
1254            local_flag_i: Vec::new(),
1255            local_flag_j: Vec::new(),
1256            selbtn: BtnSelectRuntimeState::default(),
1257            current_stage_object: None,
1258            current_object_chain: None,
1259
1260            screen_forms: HashMap::default(),
1261            msgbk_forms: HashMap::default(),
1262
1263            script: ScriptRuntimeState::default(),
1264            system: SystemRuntimeState::default(),
1265            syscom: SyscomRuntimeState::default(),
1266            mov: GlobalMovieState::default(),
1267            capture_image: None,
1268            capture_for_tweet_pending: false,
1269            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1270            twitter: crate::runtime::twitter::TwitterState::default(),
1271            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
1272            twitter_dialog_request: None,
1273            capture_for_object_image: None,
1274            save_thumb_capture_image: None,
1275            save_thumb_capture_prior: 0,
1276            append_dir: String::new(),
1277            append_name: String::new(),
1278
1279            bgm_table_listened: HashMap::default(),
1280            bgm_table_flags: Vec::new(),
1281            bgm_table_all_flag: false,
1282
1283            wipe: None,
1284            lights: HashMap::default(),
1285            fog_global: FogGlobalState::default(),
1286            render_frame: 0,
1287        }
1288    }
1289}
1290
1291#[derive(Debug, Clone)]
1292pub struct BtnSelectChoiceState {
1293    /// Per-item template number saved by C_elm_btn_select_item.  Script-created
1294    /// items normally inherit the parent selection template.
1295    pub template_no: i64,
1296    pub base_file: String,
1297    pub filter_file: String,
1298    pub text: String,
1299    pub item_type: i64,
1300    pub color: i64,
1301    /// Runtime absolute screen position.  The original save stream stores this
1302    /// relative to S_tnm_btn_select_param::base_pos.
1303    pub pos: (i64, i64),
1304    pub size: (i64, i64),
1305    pub glyphs: Vec<MwndGlyphState>,
1306}
1307
1308impl Default for BtnSelectChoiceState {
1309    fn default() -> Self {
1310        Self {
1311            template_no: -1,
1312            base_file: String::new(),
1313            filter_file: String::new(),
1314            text: String::new(),
1315            item_type: 1,
1316            color: -1,
1317            pos: (0, 0),
1318            size: (0, 0),
1319            glyphs: Vec::new(),
1320        }
1321    }
1322}
1323
1324#[derive(Debug, Clone)]
1325pub struct BtnSelectRuntimeState {
1326    pub template_no: i64,
1327    /// Exact serialized S_tnm_btn_select_param.  This is retained separately
1328    /// because C++ restores m_cur but deliberately leaves all animation work
1329    /// members (including m_sync_type) at their reinitialized values.
1330    pub saved_cur_param: Option<[i64; 28]>,
1331    pub choices: Vec<BtnSelectChoiceState>,
1332    pub cursor: usize,
1333    /// Mouse button currently captured by the selection manager.  The C++
1334    /// button manager enters PUSH on mouse-down and only decides on a matching
1335    /// mouse-up; it does not decide immediately on the down event.
1336    pub pressed_index: Option<usize>,
1337    pub pressed_inside: bool,
1338    pub cancel_enable: bool,
1339    pub capture_flag: bool,
1340    /// True only while the original button manager accepts input.
1341    pub started: bool,
1342    pub result: i64,
1343    pub sync_type: i64,
1344    pub read_flag_scene_no: i64,
1345    pub read_flag_flag_no: i64,
1346    pub sel_start_call_scn: String,
1347    pub sel_start_call_z_no: i64,
1348
1349    // C_elm_btn_select animation and process state.  These are kept separate
1350    // from `started`: the original can return from SELBTN at sync points 1/2
1351    // while the close animation is still being drawn.
1352    pub appear_flag: bool,
1353    pub open_anime_type: i64,
1354    pub open_anime_time: i64,
1355    pub open_anime_cur_time: i64,
1356    pub close_anime_type: i64,
1357    pub close_anime_time: i64,
1358    pub close_anime_cur_time: i64,
1359    pub decide_anime_type: i64,
1360    pub decide_anime_time: i64,
1361    pub decide_anime_cur_time: i64,
1362    pub decide_sel_no: i64,
1363    pub processing_flag_0: bool,
1364    pub processing_flag_1: bool,
1365    pub processing_flag_2: bool,
1366    pub capture_now_flag: bool,
1367    pub result_delivered: bool,
1368}
1369
1370impl Default for BtnSelectRuntimeState {
1371    fn default() -> Self {
1372        Self {
1373            template_no: -1,
1374            saved_cur_param: None,
1375            choices: Vec::new(),
1376            cursor: 0,
1377            pressed_index: None,
1378            pressed_inside: false,
1379            cancel_enable: false,
1380            capture_flag: false,
1381            started: false,
1382            result: 0,
1383            sync_type: 0,
1384            read_flag_scene_no: -1,
1385            read_flag_flag_no: -1,
1386            sel_start_call_scn: String::new(),
1387            sel_start_call_z_no: -1,
1388            appear_flag: false,
1389            open_anime_type: 0,
1390            open_anime_time: 0,
1391            open_anime_cur_time: 0,
1392            close_anime_type: 0,
1393            close_anime_time: 0,
1394            close_anime_cur_time: 0,
1395            decide_anime_type: 0,
1396            decide_anime_time: 0,
1397            decide_anime_cur_time: 0,
1398            decide_sel_no: -1,
1399            processing_flag_0: false,
1400            processing_flag_1: false,
1401            processing_flag_2: false,
1402            capture_now_flag: false,
1403            result_delivered: false,
1404        }
1405    }
1406}
1407
1408#[derive(Debug, Clone, Default)]
1409pub struct PendingFrameActionFinish {
1410    pub frame_action_chain: Vec<i32>,
1411    pub object_chain: Option<Vec<i32>>,
1412    /// Full pre-reinit snapshot. START/START_REAL install their replacement
1413    /// before the deferred finish callback is drained, so the old action must
1414    /// be restored temporarily while its finish action is running.
1415    pub snapshot: ObjectFrameActionState,
1416    /// END is reinit(true): after the finish callback, reinit(false) must run
1417    /// against the callback-visible live state. START/START_REAL instead restore
1418    /// the replacement state installed by the outer set_param().
1419    pub reinit_after_finish: bool,
1420    pub scn_name: String,
1421    pub cmd_name: String,
1422    pub end_time: i64,
1423    pub args: Vec<crate::runtime::Value>,
1424}
1425
1426#[derive(Debug, Clone)]
1427pub enum PendingButtonActionKind {
1428    UserCall {
1429        scn_name: String,
1430        cmd_name: String,
1431        z_no: i64,
1432    },
1433    Syscom {
1434        sys_type: i64,
1435        sys_type_opt: i64,
1436        mode: i64,
1437    },
1438}
1439
1440impl Default for PendingButtonActionKind {
1441    fn default() -> Self {
1442        Self::UserCall {
1443            scn_name: String::new(),
1444            cmd_name: String::new(),
1445            z_no: -1,
1446        }
1447    }
1448}
1449
1450#[derive(Debug, Clone, Default)]
1451pub struct PendingButtonAction {
1452    pub kind: PendingButtonActionKind,
1453}
1454
1455/// Runtime state for the GLOBAL.MOV player.
1456///
1457/// Original Siglus MOV is not a stage OBJECT; it is a full-screen/direct movie
1458/// player. The WGPU port renders it through a dedicated LayerManager sprite so
1459/// MOV.PLAY/WAIT/STOP still produce visible frames when stage objects are hidden.
1460#[derive(Debug, Clone, Default)]
1461pub struct GlobalMovieState {
1462    pub file_name: Option<String>,
1463    pub playing: bool,
1464    pub key_skip_flag: bool,
1465    pub timer_ms: u64,
1466    pub total_ms: Option<u64>,
1467    pub x: i32,
1468    pub y: i32,
1469    pub width: u32,
1470    pub height: u32,
1471    pub layer_id: Option<LayerId>,
1472    pub sprite_id: Option<SpriteId>,
1473    pub image_id: Option<ImageHandle>,
1474    pub last_frame_idx: Option<usize>,
1475    pub audio_id: Option<u64>,
1476    pub audio_start_attempted: bool,
1477}
1478
1479impl GlobalMovieState {
1480    pub fn start(
1481        &mut self,
1482        file_name: String,
1483        x: i32,
1484        y: i32,
1485        width: u32,
1486        height: u32,
1487        total_ms: Option<u64>,
1488        key_skip_flag: bool,
1489    ) {
1490        self.file_name = Some(file_name);
1491        self.playing = true;
1492        self.key_skip_flag = key_skip_flag;
1493        self.timer_ms = 0;
1494        self.total_ms = total_ms;
1495        self.x = x;
1496        self.y = y;
1497        self.width = width;
1498        self.height = height;
1499        self.last_frame_idx = None;
1500        self.audio_id = None;
1501        self.audio_start_attempted = false;
1502    }
1503
1504    pub fn stop(&mut self) {
1505        self.file_name = None;
1506        self.playing = false;
1507        self.key_skip_flag = false;
1508        self.timer_ms = 0;
1509        self.total_ms = None;
1510        self.last_frame_idx = None;
1511        self.audio_id = None;
1512        self.audio_start_attempted = false;
1513    }
1514
1515    pub fn tick(&mut self, past_real_time: i32) {
1516        if !self.playing || self.audio_id.is_some() {
1517            // Once movie audio starts, its hardware playback position is the
1518            // master clock. The renderer synchronizes timer_ms from Kira.
1519            return;
1520        }
1521        let add = past_real_time.max(0) as u64;
1522        if add == 0 {
1523            return;
1524        }
1525        self.timer_ms = self.timer_ms.saturating_add(add);
1526        if let Some(total) = self.total_ms
1527            && total > 0
1528            && self.timer_ms >= total
1529        {
1530            self.timer_ms = total;
1531            self.playing = false;
1532        }
1533    }
1534}
1535
1536#[derive(Debug, Clone, Copy, Default)]
1537pub struct Counter {
1538    cur_time: i64,
1539    is_running: bool,
1540    real_flag: bool,
1541    frame_mode: bool,
1542    frame_loop_flag: bool,
1543    frame_start_value: i64,
1544    frame_end_value: i64,
1545    frame_time: i64,
1546}
1547
1548impl Counter {
1549    fn limit(min: i64, value: i64, max: i64) -> i64 {
1550        if value < min {
1551            min
1552        } else if value > max {
1553            max
1554        } else {
1555            value
1556        }
1557    }
1558
1559    pub fn reinit(&mut self) {
1560        *self = Self::default();
1561    }
1562
1563    pub fn reset(&mut self) {
1564        self.is_running = false;
1565        self.real_flag = false;
1566        self.frame_mode = false;
1567        self.cur_time = 0;
1568    }
1569
1570    pub fn set_count(&mut self, value: i64) {
1571        if self.frame_mode {
1572            if self.frame_end_value == self.frame_start_value {
1573                self.cur_time = 0;
1574                return;
1575            }
1576
1577            let denom = self.frame_end_value - self.frame_start_value;
1578            let frame_time = self.frame_time;
1579            let mut cur_time = (value - self.frame_start_value) * frame_time / denom;
1580            if self.frame_loop_flag {
1581                cur_time = Self::limit(0, cur_time, frame_time - 1);
1582            } else {
1583                cur_time = Self::limit(0, cur_time, frame_time);
1584            }
1585            self.cur_time = cur_time;
1586        } else {
1587            self.cur_time = value;
1588        }
1589    }
1590
1591    pub fn start(&mut self) {
1592        self.is_running = true;
1593        self.real_flag = false;
1594        self.frame_mode = false;
1595        self.cur_time = 0;
1596    }
1597
1598    pub fn start_real(&mut self) {
1599        self.is_running = true;
1600        self.real_flag = true;
1601        self.frame_mode = false;
1602        self.cur_time = 0;
1603    }
1604
1605    pub fn start_frame(&mut self, from: i64, to: i64, frame_time: i64) {
1606        self.is_running = true;
1607        self.real_flag = false;
1608        self.frame_mode = true;
1609        self.frame_loop_flag = false;
1610        self.frame_start_value = from;
1611        self.frame_end_value = to;
1612        self.frame_time = frame_time;
1613        self.cur_time = 0;
1614    }
1615
1616    pub fn start_frame_real(&mut self, from: i64, to: i64, frame_time: i64) {
1617        self.is_running = true;
1618        self.real_flag = true;
1619        self.frame_mode = true;
1620        self.frame_loop_flag = false;
1621        self.frame_start_value = from;
1622        self.frame_end_value = to;
1623        self.frame_time = frame_time;
1624        self.cur_time = 0;
1625    }
1626
1627    pub fn start_frame_loop(&mut self, from: i64, to: i64, frame_time: i64) {
1628        self.is_running = true;
1629        self.real_flag = false;
1630        self.frame_mode = true;
1631        self.frame_loop_flag = true;
1632        self.frame_start_value = from;
1633        self.frame_end_value = to;
1634        self.frame_time = frame_time;
1635        self.cur_time = 0;
1636    }
1637
1638    pub fn start_frame_loop_real(&mut self, from: i64, to: i64, frame_time: i64) {
1639        self.is_running = true;
1640        self.real_flag = true;
1641        self.frame_mode = true;
1642        self.frame_loop_flag = true;
1643        self.frame_start_value = from;
1644        self.frame_end_value = to;
1645        self.frame_time = frame_time;
1646        self.cur_time = 0;
1647    }
1648
1649    pub fn stop(&mut self) {
1650        self.is_running = false;
1651    }
1652
1653    pub fn resume(&mut self) {
1654        self.is_running = true;
1655    }
1656
1657    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
1658        if self.is_running {
1659            let add = if self.real_flag {
1660                past_real_time
1661            } else {
1662                past_game_time
1663            };
1664            self.cur_time = self.cur_time.saturating_add(add as i64);
1665        }
1666
1667        if self.frame_mode && !self.frame_loop_flag && self.cur_time >= self.frame_time {
1668            self.is_running = false;
1669        }
1670    }
1671
1672    pub fn get_count(&self) -> i64 {
1673        if self.frame_mode {
1674            if self.frame_time <= 0 {
1675                return self.frame_end_value;
1676            }
1677            if self.frame_start_value == self.frame_end_value {
1678                return self.frame_end_value;
1679            }
1680
1681            let span = self.frame_end_value - self.frame_start_value;
1682            let mut value = span * self.cur_time / self.frame_time;
1683            if self.frame_loop_flag {
1684                value %= span;
1685                value += self.frame_start_value;
1686            } else {
1687                value += self.frame_start_value;
1688                if self.frame_start_value > self.frame_end_value {
1689                    value = Self::limit(self.frame_end_value, value, self.frame_start_value);
1690                } else {
1691                    value = Self::limit(self.frame_start_value, value, self.frame_end_value);
1692                }
1693            }
1694            value
1695        } else {
1696            self.cur_time
1697        }
1698    }
1699
1700    pub fn get_count_with_frame(&self, _current_frame: i64) -> i64 {
1701        self.get_count()
1702    }
1703
1704    pub fn is_running(&self) -> bool {
1705        self.is_running
1706    }
1707
1708    pub(crate) fn save_parts(&self) -> (bool, bool, bool, bool, i64, i64, i64, i64) {
1709        (
1710            self.is_running,
1711            self.real_flag,
1712            self.frame_mode,
1713            self.frame_loop_flag,
1714            self.frame_start_value,
1715            self.frame_end_value,
1716            self.frame_time,
1717            self.cur_time,
1718        )
1719    }
1720
1721    pub(crate) fn from_save_parts(
1722        is_running: bool,
1723        real_flag: bool,
1724        frame_mode: bool,
1725        frame_loop_flag: bool,
1726        frame_start_value: i64,
1727        frame_end_value: i64,
1728        frame_time: i64,
1729        cur_time: i64,
1730    ) -> Self {
1731        Self {
1732            cur_time,
1733            is_running,
1734            real_flag,
1735            frame_mode,
1736            frame_loop_flag,
1737            frame_start_value,
1738            frame_end_value,
1739            frame_time,
1740        }
1741    }
1742}
1743
1744#[derive(Debug, Clone, Default)]
1745pub struct PcmEventLine {
1746    pub file_name: String,
1747    pub probability: i32,
1748    pub min_time: i32,
1749    pub max_time: i32,
1750}
1751
1752pub const PCM_EVENT_TYPE_NONE: i32 = -1;
1753pub const PCM_EVENT_TYPE_ONESHOT: i32 = 0;
1754pub const PCM_EVENT_TYPE_LOOP: i32 = 1;
1755pub const PCM_EVENT_TYPE_RANDOM: i32 = 2;
1756
1757#[derive(Debug, Clone)]
1758pub struct PcmEventState {
1759    pub event_type: i32,
1760    pub pcm_buf_no: i32,
1761    pub volume_type: i32,
1762    pub chara_no: i32,
1763    pub bgm_fade_target_flag: bool,
1764    pub bgm_fade2_target_flag: bool,
1765    pub bgm_fade_source_flag: bool,
1766    pub real_flag: bool,
1767    pub time_type: bool,
1768    pub lines: Vec<PcmEventLine>,
1769
1770    // C_elm_pcm_event working members. They deliberately are not serialized:
1771    // LOOP/RANDOM are restarted from the beginning after loading, while
1772    // ONESHOT is not restored by the original save format.
1773    pub cur_time: i64,
1774    pub cur_line_no: i32,
1775    pub next_time: i64,
1776    pub last_line_no: i32,
1777}
1778
1779impl Default for PcmEventState {
1780    fn default() -> Self {
1781        Self {
1782            event_type: PCM_EVENT_TYPE_NONE,
1783            pcm_buf_no: -1,
1784            volume_type: 2, // TNM_VOLUME_TYPE_PCM
1785            chara_no: -1,
1786            bgm_fade_target_flag: false,
1787            bgm_fade2_target_flag: false,
1788            bgm_fade_source_flag: false,
1789            real_flag: false,
1790            time_type: false,
1791            lines: Vec::new(),
1792            cur_time: 0,
1793            cur_line_no: -1,
1794            next_time: 0,
1795            last_line_no: -1,
1796        }
1797    }
1798}
1799
1800impl PcmEventState {
1801    pub fn reinit(&mut self) {
1802        *self = Self::default();
1803    }
1804
1805    pub fn is_active(&self) -> bool {
1806        self.event_type != PCM_EVENT_TYPE_NONE
1807    }
1808
1809    pub fn start(
1810        &mut self,
1811        event_type: i32,
1812        pcm_buf_no: i32,
1813        volume_type: i32,
1814        chara_no: i32,
1815        bgm_fade_target_flag: bool,
1816        bgm_fade2_target_flag: bool,
1817        bgm_fade_source_flag: bool,
1818        real_flag: bool,
1819        time_type: bool,
1820    ) {
1821        self.event_type = event_type;
1822        self.pcm_buf_no = pcm_buf_no;
1823        self.volume_type = volume_type;
1824        self.chara_no = chara_no;
1825        self.bgm_fade_target_flag = bgm_fade_target_flag;
1826        self.bgm_fade2_target_flag = bgm_fade2_target_flag;
1827        self.bgm_fade_source_flag = bgm_fade_source_flag;
1828        self.real_flag = real_flag;
1829        self.time_type = time_type;
1830        self.cur_time = 0;
1831        self.cur_line_no = -1;
1832        self.next_time = 0;
1833        self.last_line_no = -1;
1834    }
1835}
1836
1837/// Mask state.
1838#[derive(Debug, Clone)]
1839pub struct MaskState {
1840    pub name: Option<String>,
1841    pub x_event: IntEvent,
1842    pub y_event: IntEvent,
1843    pub extra_int: HashMap<i32, i32>,
1844    pub script_events: HashMap<i32, IntEvent>,
1845}
1846
1847impl Default for MaskState {
1848    fn default() -> Self {
1849        Self::new()
1850    }
1851}
1852
1853impl MaskState {
1854    pub fn new() -> Self {
1855        Self {
1856            name: None,
1857            x_event: IntEvent::new(0),
1858            y_event: IntEvent::new(0),
1859            extra_int: HashMap::default(),
1860            script_events: HashMap::default(),
1861        }
1862    }
1863
1864    pub fn reinit(&mut self) {
1865        self.name = None;
1866        self.x_event.reinit();
1867        self.y_event.reinit();
1868        self.extra_int.clear();
1869        self.script_events.clear();
1870    }
1871}
1872
1873#[derive(Debug, Clone)]
1874pub struct MaskListState {
1875    pub masks: Vec<MaskState>,
1876}
1877
1878pub const EDITBOX_ACTION_NOT_DECIDED: i32 = 0;
1879pub const EDITBOX_ACTION_DECIDED: i32 = 1;
1880pub const EDITBOX_ACTION_CANCELED: i32 = -1;
1881
1882const EDITBOX_UNDO_LIMIT: usize = 64;
1883const EDITBOX_TEXT_PADDING_X: i32 = 4;
1884
1885#[derive(Debug, Clone, PartialEq, Eq)]
1886struct EditBoxUndoSnapshot {
1887    text: String,
1888    cursor_pos: usize,
1889    selection_anchor: Option<usize>,
1890}
1891
1892#[derive(Debug, Clone)]
1893pub struct EditBoxState {
1894    pub created: bool,
1895    pub visible: bool,
1896    pub text: String,
1897    /// UTF-8 byte index. It is always normalized to a character boundary.
1898    pub cursor_pos: usize,
1899    /// The fixed end of the selection; `cursor_pos` is the active end.
1900    pub selection_anchor: Option<usize>,
1901    /// Current winit IME preedit string. It is not part of `GET_TEXT` until committed.
1902    pub composition_text: String,
1903    /// Selected/caret byte range inside `composition_text`, as supplied by winit.
1904    pub composition_cursor: Option<(usize, usize)>,
1905    /// Committed-text range replaced by the current composition.
1906    pub composition_range: Option<(usize, usize)>,
1907    /// Winit sends an empty Preedit immediately before Commit. Keep the
1908    /// replacement range alive until the next event tells us commit or cancel.
1909    pub composition_clear_pending: bool,
1910    /// Horizontal ES_AUTOHSCROLL-equivalent offset in logical pixels.
1911    pub scroll_x_px: i32,
1912    /// True while the left pointer owns selection capture for this editbox.
1913    pub mouse_selecting: bool,
1914    pub action_flag: i32,
1915    pub moji_size: i32,
1916    pub rect_x: i32,
1917    pub rect_y: i32,
1918    pub rect_w: i32,
1919    pub rect_h: i32,
1920    pub design_screen_w: i32,
1921    pub design_screen_h: i32,
1922    pub window_x: i32,
1923    pub window_y: i32,
1924    pub window_w: i32,
1925    pub window_h: i32,
1926    pub window_moji_size: i32,
1927    undo_stack: Vec<EditBoxUndoSnapshot>,
1928    redo_stack: Vec<EditBoxUndoSnapshot>,
1929}
1930
1931impl Default for EditBoxState {
1932    fn default() -> Self {
1933        Self {
1934            created: false,
1935            visible: false,
1936            text: String::new(),
1937            cursor_pos: 0,
1938            selection_anchor: None,
1939            composition_text: String::new(),
1940            composition_cursor: None,
1941            composition_range: None,
1942            composition_clear_pending: false,
1943            scroll_x_px: 0,
1944            mouse_selecting: false,
1945            action_flag: EDITBOX_ACTION_NOT_DECIDED,
1946            moji_size: 0,
1947            rect_x: 0,
1948            rect_y: 0,
1949            rect_w: 0,
1950            rect_h: 0,
1951            design_screen_w: 0,
1952            design_screen_h: 0,
1953            window_x: 0,
1954            window_y: 0,
1955            window_w: 0,
1956            window_h: 0,
1957            window_moji_size: 0,
1958            undo_stack: Vec::new(),
1959            redo_stack: Vec::new(),
1960        }
1961    }
1962}
1963
1964impl EditBoxState {
1965    pub fn create_like(
1966        &mut self,
1967        x: i32,
1968        y: i32,
1969        w: i32,
1970        h: i32,
1971        moji_size: i32,
1972        design_screen_w: i32,
1973        design_screen_h: i32,
1974    ) {
1975        self.created = true;
1976        self.visible = false;
1977        self.text.clear();
1978        self.cursor_pos = 0;
1979        self.selection_anchor = None;
1980        self.clear_composition();
1981        self.scroll_x_px = 0;
1982        self.mouse_selecting = false;
1983        self.undo_stack.clear();
1984        self.redo_stack.clear();
1985        self.action_flag = EDITBOX_ACTION_NOT_DECIDED;
1986        self.rect_x = x;
1987        self.rect_y = y;
1988        self.rect_w = w;
1989        self.rect_h = h;
1990        self.moji_size = moji_size;
1991        self.design_screen_w = design_screen_w.max(1);
1992        self.design_screen_h = design_screen_h.max(1);
1993        self.window_x = 0;
1994        self.window_y = 0;
1995        self.window_w = 0;
1996        self.window_h = 0;
1997        self.window_moji_size = 0;
1998    }
1999
2000    pub fn destroy_like(&mut self) {
2001        *self = Self::default();
2002    }
2003
2004    pub fn set_text_like(&mut self, text: String) {
2005        self.text = text;
2006        self.cursor_pos = self.text.len();
2007        self.selection_anchor = None;
2008        self.clear_composition();
2009        self.scroll_x_px = 0;
2010        self.mouse_selecting = false;
2011        self.undo_stack.clear();
2012        self.redo_stack.clear();
2013        self.ensure_caret_visible();
2014    }
2015
2016    pub fn insert_text_at_cursor(&mut self, text: &str) {
2017        self.commit_text(text);
2018    }
2019
2020    pub fn commit_text(&mut self, text: &str) {
2021        let filtered: String = text.chars().filter(|ch| !ch.is_control()).collect();
2022        if filtered.is_empty() {
2023            self.cancel_composition();
2024            return;
2025        }
2026        let range = self
2027            .composition_range
2028            .take()
2029            .or_else(|| self.selection_range())
2030            .unwrap_or_else(|| {
2031                let p = self.normalized_cursor();
2032                (p, p)
2033            });
2034        self.push_undo_snapshot();
2035        self.replace_range_without_undo(range, &filtered);
2036        self.selection_anchor = None;
2037        self.clear_composition();
2038        self.ensure_caret_visible();
2039    }
2040
2041    pub fn set_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) {
2042        if text.is_empty() {
2043            if self.composition_range.is_some() {
2044                self.composition_text.clear();
2045                self.composition_cursor = None;
2046                self.composition_clear_pending = true;
2047                self.ensure_caret_visible();
2048            }
2049            return;
2050        }
2051        if self.composition_clear_pending {
2052            self.cancel_composition();
2053        }
2054        if self.composition_range.is_none() {
2055            let range = self.selection_range().unwrap_or_else(|| {
2056                let p = self.normalized_cursor();
2057                (p, p)
2058            });
2059            self.composition_range = Some(range);
2060            self.cursor_pos = range.0;
2061            self.selection_anchor = None;
2062        }
2063        self.composition_text.clear();
2064        self.composition_text.push_str(text);
2065        self.composition_clear_pending = false;
2066        self.composition_cursor = cursor.map(|(a, b)| {
2067            (
2068                Self::normalize_boundary(&self.composition_text, a),
2069                Self::normalize_boundary(&self.composition_text, b),
2070            )
2071        });
2072        self.ensure_caret_visible();
2073    }
2074
2075    pub fn cancel_composition(&mut self) {
2076        let had_composition = self.composition_range.is_some() || !self.composition_text.is_empty();
2077        if !had_composition {
2078            return;
2079        }
2080        if let Some((start, _)) = self.composition_range {
2081            self.cursor_pos = Self::normalize_boundary(&self.text, start);
2082        }
2083        self.clear_composition();
2084        self.selection_anchor = None;
2085        self.ensure_caret_visible();
2086    }
2087
2088    pub fn clear_composition(&mut self) {
2089        self.composition_text.clear();
2090        self.composition_cursor = None;
2091        self.composition_range = None;
2092        self.composition_clear_pending = false;
2093    }
2094
2095    pub fn cancel_pending_composition_clear(&mut self) {
2096        if self.composition_clear_pending {
2097            self.cancel_composition();
2098        }
2099    }
2100
2101    pub fn is_composing(&self) -> bool {
2102        self.composition_range.is_some()
2103    }
2104
2105    pub fn selection_range(&self) -> Option<(usize, usize)> {
2106        let cursor = self.normalized_cursor();
2107        let anchor = self
2108            .selection_anchor
2109            .map(|p| Self::normalize_boundary(&self.text, p))?;
2110        if anchor == cursor {
2111            None
2112        } else if anchor < cursor {
2113            Some((anchor, cursor))
2114        } else {
2115            Some((cursor, anchor))
2116        }
2117    }
2118
2119    pub fn selected_text(&self) -> Option<String> {
2120        let (start, end) = self.selection_range()?;
2121        Some(self.text[start..end].to_string())
2122    }
2123
2124    pub fn select_all(&mut self) {
2125        self.cancel_composition();
2126        self.selection_anchor = Some(0);
2127        self.cursor_pos = self.text.len();
2128        self.ensure_caret_visible();
2129    }
2130
2131    pub fn cut_selection(&mut self) -> Option<String> {
2132        let selected = self.selected_text()?;
2133        self.delete_selection();
2134        Some(selected)
2135    }
2136
2137    pub fn backspace_like(&mut self) {
2138        self.delete_backward(false);
2139    }
2140
2141    pub fn delete_backward(&mut self, by_word: bool) {
2142        if self.is_composing() {
2143            return;
2144        }
2145        if self.delete_selection() {
2146            return;
2147        }
2148        let cursor = self.normalized_cursor();
2149        if cursor == 0 {
2150            return;
2151        }
2152        let start = if by_word {
2153            self.word_left_boundary(cursor)
2154        } else {
2155            Self::previous_boundary(&self.text, cursor)
2156        };
2157        self.push_undo_snapshot();
2158        self.replace_range_without_undo((start, cursor), "");
2159        self.ensure_caret_visible();
2160    }
2161
2162    pub fn delete_forward(&mut self, by_word: bool) {
2163        if self.is_composing() {
2164            return;
2165        }
2166        if self.delete_selection() {
2167            return;
2168        }
2169        let cursor = self.normalized_cursor();
2170        if cursor >= self.text.len() {
2171            return;
2172        }
2173        let end = if by_word {
2174            self.word_right_boundary(cursor)
2175        } else {
2176            Self::next_boundary(&self.text, cursor)
2177        };
2178        self.push_undo_snapshot();
2179        self.replace_range_without_undo((cursor, end), "");
2180        self.ensure_caret_visible();
2181    }
2182
2183    pub fn move_cursor_left(&mut self, extend: bool, by_word: bool) {
2184        if self.is_composing() {
2185            return;
2186        }
2187        if !extend && let Some((start, _)) = self.selection_range() {
2188            self.cursor_pos = start;
2189            self.selection_anchor = None;
2190            self.ensure_caret_visible();
2191            return;
2192        }
2193        let old = self.normalized_cursor();
2194        let next = if by_word {
2195            self.word_left_boundary(old)
2196        } else {
2197            Self::previous_boundary(&self.text, old)
2198        };
2199        self.set_cursor_with_selection(old, next, extend);
2200    }
2201
2202    pub fn move_cursor_right(&mut self, extend: bool, by_word: bool) {
2203        if self.is_composing() {
2204            return;
2205        }
2206        if !extend && let Some((_, end)) = self.selection_range() {
2207            self.cursor_pos = end;
2208            self.selection_anchor = None;
2209            self.ensure_caret_visible();
2210            return;
2211        }
2212        let old = self.normalized_cursor();
2213        let next = if by_word {
2214            self.word_right_boundary(old)
2215        } else {
2216            Self::next_boundary(&self.text, old)
2217        };
2218        self.set_cursor_with_selection(old, next, extend);
2219    }
2220
2221    pub fn move_cursor_home(&mut self, extend: bool) {
2222        if self.is_composing() {
2223            return;
2224        }
2225        let old = self.normalized_cursor();
2226        self.set_cursor_with_selection(old, 0, extend);
2227    }
2228
2229    pub fn move_cursor_end(&mut self, extend: bool) {
2230        if self.is_composing() {
2231            return;
2232        }
2233        let old = self.normalized_cursor();
2234        self.set_cursor_with_selection(old, self.text.len(), extend);
2235    }
2236
2237    pub fn set_cursor_from_window_x(&mut self, window_x: i32, extend: bool) {
2238        self.cancel_composition();
2239        let target_x = window_x
2240            .saturating_sub(self.window_x)
2241            .saturating_sub(EDITBOX_TEXT_PADDING_X)
2242            .saturating_add(self.scroll_x_px)
2243            .max(0);
2244        let mut x = 0i32;
2245        let mut target = self.text.len();
2246        for (idx, ch) in self.text.char_indices() {
2247            let width = crate::text_render::editbox_cell_width_px(ch, self.font_px());
2248            if target_x < x.saturating_add((width + 1) / 2) {
2249                target = idx;
2250                break;
2251            }
2252            x = x.saturating_add(width);
2253            target = idx + ch.len_utf8();
2254        }
2255        let old = self.normalized_cursor();
2256        self.set_cursor_with_selection(old, target, extend);
2257    }
2258
2259    pub fn caret_window_x(&self) -> i32 {
2260        let content_x = self.display_caret_content_x();
2261        self.window_x
2262            .saturating_add(EDITBOX_TEXT_PADDING_X)
2263            .saturating_add(content_x.saturating_sub(self.scroll_x_px))
2264    }
2265
2266    pub fn ensure_caret_visible_for_focus(&mut self) {
2267        self.ensure_caret_visible();
2268    }
2269
2270    pub fn undo(&mut self) {
2271        self.cancel_composition();
2272        let Some(snapshot) = self.undo_stack.pop() else {
2273            return;
2274        };
2275        let current = self.snapshot();
2276        self.redo_stack.push(current);
2277        self.restore_snapshot(snapshot);
2278    }
2279
2280    pub fn redo(&mut self) {
2281        self.cancel_composition();
2282        let Some(snapshot) = self.redo_stack.pop() else {
2283            return;
2284        };
2285        let current = self.snapshot();
2286        self.undo_stack.push(current);
2287        self.restore_snapshot(snapshot);
2288    }
2289
2290    pub fn update_rect(&mut self, screen_w: i32, screen_h: i32) {
2291        let base_w = self.design_screen_w.max(1);
2292        let base_h = self.design_screen_h.max(1);
2293        let sw = screen_w.max(1);
2294        let sh = screen_h.max(1);
2295        self.window_x = self.rect_x.saturating_mul(sw) / base_w;
2296        self.window_y = self.rect_y.saturating_mul(sh) / base_h;
2297        self.window_w = self.rect_w.saturating_mul(sw) / base_w;
2298        self.window_h = self.rect_h.saturating_mul(sh) / base_h;
2299        self.window_moji_size = self.moji_size.saturating_mul(sh) / base_h;
2300        self.ensure_caret_visible();
2301    }
2302
2303    pub fn frame(&mut self, display_mode_change_proc_cnt: i32) {
2304        self.visible = self.created && display_mode_change_proc_cnt == 0;
2305        if !self.visible {
2306            self.mouse_selecting = false;
2307            self.clear_composition();
2308        }
2309    }
2310
2311    pub fn clear_input(&mut self) {
2312        self.action_flag = EDITBOX_ACTION_NOT_DECIDED;
2313    }
2314
2315    pub fn is_decided(&self) -> bool {
2316        self.action_flag == EDITBOX_ACTION_DECIDED
2317    }
2318
2319    pub fn is_canceled(&self) -> bool {
2320        self.action_flag == EDITBOX_ACTION_CANCELED
2321    }
2322
2323    pub fn contains_point(&self, x: i32, y: i32) -> bool {
2324        self.created
2325            && self.visible
2326            && self.window_w > 0
2327            && self.window_h > 0
2328            && x >= self.window_x
2329            && y >= self.window_y
2330            && x < self.window_x.saturating_add(self.window_w)
2331            && y < self.window_y.saturating_add(self.window_h)
2332    }
2333
2334    fn font_px(&self) -> i32 {
2335        self.window_moji_size.max(1)
2336    }
2337
2338    fn normalized_cursor(&self) -> usize {
2339        Self::normalize_boundary(&self.text, self.cursor_pos)
2340    }
2341
2342    fn normalize_boundary(text: &str, pos: usize) -> usize {
2343        let mut pos = pos.min(text.len());
2344        while pos > 0 && !text.is_char_boundary(pos) {
2345            pos -= 1;
2346        }
2347        pos
2348    }
2349
2350    fn previous_boundary(text: &str, pos: usize) -> usize {
2351        let pos = Self::normalize_boundary(text, pos);
2352        text[..pos]
2353            .char_indices()
2354            .next_back()
2355            .map(|(idx, _)| idx)
2356            .unwrap_or(0)
2357    }
2358
2359    fn next_boundary(text: &str, pos: usize) -> usize {
2360        let pos = Self::normalize_boundary(text, pos);
2361        text[pos..]
2362            .chars()
2363            .next()
2364            .map(|ch| pos + ch.len_utf8())
2365            .unwrap_or(text.len())
2366    }
2367
2368    fn word_class(ch: char) -> u8 {
2369        if ch.is_whitespace() {
2370            0
2371        } else if ch.is_alphanumeric() || ch == '_' {
2372            1
2373        } else {
2374            2
2375        }
2376    }
2377
2378    fn word_left_boundary(&self, pos: usize) -> usize {
2379        let mut cursor = Self::normalize_boundary(&self.text, pos);
2380        while cursor > 0 {
2381            let prev = Self::previous_boundary(&self.text, cursor);
2382            let ch = self.text[prev..cursor].chars().next().unwrap_or(' ');
2383            if Self::word_class(ch) != 0 {
2384                break;
2385            }
2386            cursor = prev;
2387        }
2388        let Some(class) = (cursor > 0).then(|| {
2389            let prev = Self::previous_boundary(&self.text, cursor);
2390            let ch = self.text[prev..cursor].chars().next().unwrap_or(' ');
2391            Self::word_class(ch)
2392        }) else {
2393            return 0;
2394        };
2395        while cursor > 0 {
2396            let prev = Self::previous_boundary(&self.text, cursor);
2397            let ch = self.text[prev..cursor].chars().next().unwrap_or(' ');
2398            if Self::word_class(ch) != class {
2399                break;
2400            }
2401            cursor = prev;
2402        }
2403        cursor
2404    }
2405
2406    fn word_right_boundary(&self, pos: usize) -> usize {
2407        let mut cursor = Self::normalize_boundary(&self.text, pos);
2408        if cursor >= self.text.len() {
2409            return self.text.len();
2410        }
2411        let class = self.text[cursor..]
2412            .chars()
2413            .next()
2414            .map(Self::word_class)
2415            .unwrap_or(0);
2416        while cursor < self.text.len() {
2417            let next = Self::next_boundary(&self.text, cursor);
2418            let ch = self.text[cursor..next].chars().next().unwrap_or(' ');
2419            if Self::word_class(ch) != class {
2420                break;
2421            }
2422            cursor = next;
2423        }
2424        while cursor < self.text.len() {
2425            let next = Self::next_boundary(&self.text, cursor);
2426            let ch = self.text[cursor..next].chars().next().unwrap_or(' ');
2427            if Self::word_class(ch) != 0 {
2428                break;
2429            }
2430            cursor = next;
2431        }
2432        cursor
2433    }
2434
2435    fn set_cursor_with_selection(&mut self, old: usize, next: usize, extend: bool) {
2436        if extend {
2437            if self.selection_anchor.is_none() {
2438                self.selection_anchor = Some(old);
2439            }
2440        } else {
2441            self.selection_anchor = None;
2442        }
2443        self.cursor_pos = Self::normalize_boundary(&self.text, next);
2444        if self.selection_anchor == Some(self.cursor_pos) {
2445            self.selection_anchor = None;
2446        }
2447        self.ensure_caret_visible();
2448    }
2449
2450    fn delete_selection(&mut self) -> bool {
2451        let Some(range) = self.selection_range() else {
2452            return false;
2453        };
2454        self.push_undo_snapshot();
2455        self.replace_range_without_undo(range, "");
2456        self.selection_anchor = None;
2457        self.ensure_caret_visible();
2458        true
2459    }
2460
2461    fn replace_range_without_undo(&mut self, range: (usize, usize), replacement: &str) {
2462        let start = Self::normalize_boundary(&self.text, range.0.min(range.1));
2463        let end = Self::normalize_boundary(&self.text, range.0.max(range.1));
2464        self.text.replace_range(start..end, replacement);
2465        self.cursor_pos = start.saturating_add(replacement.len()).min(self.text.len());
2466        self.cursor_pos = Self::normalize_boundary(&self.text, self.cursor_pos);
2467    }
2468
2469    fn snapshot(&self) -> EditBoxUndoSnapshot {
2470        EditBoxUndoSnapshot {
2471            text: self.text.clone(),
2472            cursor_pos: self.normalized_cursor(),
2473            selection_anchor: self
2474                .selection_anchor
2475                .map(|p| Self::normalize_boundary(&self.text, p)),
2476        }
2477    }
2478
2479    fn restore_snapshot(&mut self, snapshot: EditBoxUndoSnapshot) {
2480        self.text = snapshot.text;
2481        self.cursor_pos = Self::normalize_boundary(&self.text, snapshot.cursor_pos);
2482        self.selection_anchor = snapshot
2483            .selection_anchor
2484            .map(|p| Self::normalize_boundary(&self.text, p));
2485        self.clear_composition();
2486        self.ensure_caret_visible();
2487    }
2488
2489    fn push_undo_snapshot(&mut self) {
2490        let snapshot = self.snapshot();
2491        if self.undo_stack.last() == Some(&snapshot) {
2492            return;
2493        }
2494        if self.undo_stack.len() >= EDITBOX_UNDO_LIMIT {
2495            self.undo_stack.remove(0);
2496        }
2497        self.undo_stack.push(snapshot);
2498        self.redo_stack.clear();
2499    }
2500
2501    fn display_width_before(&self, byte_pos: usize) -> i32 {
2502        let pos = Self::normalize_boundary(&self.text, byte_pos);
2503        self.text[..pos].chars().fold(0i32, |sum, ch| {
2504            sum.saturating_add(crate::text_render::editbox_cell_width_px(
2505                ch,
2506                self.font_px(),
2507            ))
2508        })
2509    }
2510
2511    fn display_caret_content_x(&self) -> i32 {
2512        if let Some((start, _end)) = self.composition_range {
2513            let mut x = self.display_width_before(start);
2514            let comp_cursor = self
2515                .composition_cursor
2516                .map(|(_, end)| Self::normalize_boundary(&self.composition_text, end))
2517                .unwrap_or(self.composition_text.len());
2518            for ch in self.composition_text[..comp_cursor].chars() {
2519                x = x.saturating_add(crate::text_render::editbox_cell_width_px(
2520                    ch,
2521                    self.font_px(),
2522                ));
2523            }
2524            x
2525        } else {
2526            self.display_width_before(self.normalized_cursor())
2527        }
2528    }
2529
2530    fn display_total_width(&self) -> i32 {
2531        if let Some((start, end)) = self.composition_range {
2532            let start = Self::normalize_boundary(&self.text, start);
2533            let end = Self::normalize_boundary(&self.text, end);
2534            let mut width = self.display_width_before(start);
2535            for ch in self.composition_text.chars() {
2536                width = width.saturating_add(crate::text_render::editbox_cell_width_px(
2537                    ch,
2538                    self.font_px(),
2539                ));
2540            }
2541            for ch in self.text[end..].chars() {
2542                width = width.saturating_add(crate::text_render::editbox_cell_width_px(
2543                    ch,
2544                    self.font_px(),
2545                ));
2546            }
2547            width
2548        } else {
2549            self.display_width_before(self.text.len())
2550        }
2551    }
2552
2553    fn ensure_caret_visible(&mut self) {
2554        let available = self
2555            .window_w
2556            .saturating_sub(EDITBOX_TEXT_PADDING_X.saturating_mul(2))
2557            .max(1);
2558        let caret = self.display_caret_content_x();
2559        if caret < self.scroll_x_px {
2560            self.scroll_x_px = caret;
2561        } else if caret > self.scroll_x_px.saturating_add(available) {
2562            self.scroll_x_px = caret.saturating_sub(available);
2563        }
2564        let max_scroll = self.display_total_width().saturating_sub(available).max(0);
2565        self.scroll_x_px = self.scroll_x_px.clamp(0, max_scroll);
2566    }
2567}
2568
2569#[derive(Debug, Clone)]
2570pub struct EditBoxListState {
2571    pub boxes: Vec<EditBoxState>,
2572}
2573
2574impl EditBoxListState {
2575    pub fn new(cnt: usize) -> Self {
2576        Self {
2577            boxes: vec![EditBoxState::default(); cnt],
2578        }
2579    }
2580
2581    pub fn ensure_size(&mut self, cnt: usize) {
2582        if self.boxes.len() < cnt {
2583            self.boxes
2584                .extend((0..(cnt - self.boxes.len())).map(|_| EditBoxState::default()));
2585        } else if self.boxes.len() > cnt {
2586            self.boxes.truncate(cnt);
2587        }
2588    }
2589}
2590
2591#[cfg(test)]
2592mod editbox_state_tests {
2593    use super::EditBoxState;
2594
2595    fn editbox_with_text(text: &str) -> EditBoxState {
2596        let mut editbox = EditBoxState::default();
2597        editbox.create_like(0, 0, 320, 32, 20, 1280, 720);
2598        editbox.update_rect(1280, 720);
2599        editbox.frame(0);
2600        editbox.set_text_like(text.to_string());
2601        editbox
2602    }
2603
2604    #[test]
2605    fn unicode_backspace_and_delete_keep_utf8_boundaries() {
2606        let mut editbox = editbox_with_text("A猫😀B");
2607        editbox.move_cursor_left(false, false);
2608        editbox.delete_backward(false);
2609        assert_eq!(editbox.text, "A猫B");
2610        assert!(editbox.text.is_char_boundary(editbox.cursor_pos));
2611
2612        editbox.move_cursor_home(false);
2613        editbox.move_cursor_right(false, false);
2614        editbox.delete_forward(false);
2615        assert_eq!(editbox.text, "AB");
2616        assert!(editbox.text.is_char_boundary(editbox.cursor_pos));
2617    }
2618
2619    #[test]
2620    fn committed_text_replaces_active_selection() {
2621        let mut editbox = editbox_with_text("abcdef");
2622        editbox.move_cursor_home(false);
2623        editbox.move_cursor_right(false, false);
2624        editbox.move_cursor_right(true, false);
2625        editbox.move_cursor_right(true, false);
2626        assert_eq!(editbox.selection_range(), Some((1, 3)));
2627
2628        editbox.commit_text("猫");
2629        assert_eq!(editbox.text, "a猫def");
2630        assert_eq!(editbox.selection_range(), None);
2631        assert_eq!(editbox.cursor_pos, "a猫".len());
2632    }
2633
2634    #[test]
2635    fn ime_preedit_is_transient_until_commit() {
2636        let mut editbox = editbox_with_text("abcd");
2637        editbox.move_cursor_home(false);
2638        editbox.move_cursor_right(false, false);
2639        editbox.move_cursor_right(true, false);
2640        editbox.move_cursor_right(true, false);
2641
2642        editbox.set_ime_preedit("日本", Some((3, 6)));
2643        assert_eq!(editbox.text, "abcd");
2644        assert_eq!(editbox.composition_range, Some((1, 3)));
2645        assert!(editbox.is_composing());
2646
2647        // Winit clears Preedit immediately before delivering Commit. The
2648        // original replacement range must survive that synthetic clear.
2649        editbox.set_ime_preedit("", None);
2650        assert!(editbox.composition_clear_pending);
2651        assert_eq!(editbox.composition_range, Some((1, 3)));
2652
2653        editbox.commit_text("日本");
2654        assert_eq!(editbox.text, "a日本d");
2655        assert!(!editbox.is_composing());
2656        assert_eq!(editbox.cursor_pos, "a日本".len());
2657    }
2658
2659    #[test]
2660    fn canceled_empty_preedit_does_not_replace_later_input() {
2661        let mut editbox = editbox_with_text("abcd");
2662        editbox.move_cursor_home(false);
2663        editbox.move_cursor_right(false, false);
2664        editbox.move_cursor_right(true, false);
2665        editbox.set_ime_preedit("x", Some((1, 1)));
2666        editbox.set_ime_preedit("", None);
2667        editbox.cancel_pending_composition_clear();
2668        editbox.commit_text("Z");
2669        assert_eq!(editbox.text, "aZbcd");
2670    }
2671
2672    #[test]
2673    fn undo_and_redo_restore_text_cursor_and_selection() {
2674        let mut editbox = editbox_with_text("one");
2675        editbox.commit_text(" two");
2676        assert_eq!(editbox.text, "one two");
2677
2678        editbox.undo();
2679        assert_eq!(editbox.text, "one");
2680        assert_eq!(editbox.cursor_pos, 3);
2681
2682        editbox.redo();
2683        assert_eq!(editbox.text, "one two");
2684        assert_eq!(editbox.cursor_pos, 7);
2685    }
2686
2687    #[test]
2688    fn mouse_drag_keeps_the_initial_selection_anchor() {
2689        let mut editbox = editbox_with_text("abcdef");
2690        editbox.set_cursor_from_window_x(editbox.window_x + 4, false);
2691        let anchor = editbox.cursor_pos;
2692        editbox.set_cursor_from_window_x(editbox.window_x + 80, true);
2693        assert_eq!(editbox.selection_anchor, Some(anchor));
2694        editbox.set_cursor_from_window_x(editbox.window_x + 120, true);
2695        assert_eq!(editbox.selection_anchor, Some(anchor));
2696        assert!(editbox.selection_range().is_some());
2697    }
2698}
2699
2700// -----------------------------------------------------------------------------
2701// Stage/MWND/Group state
2702// -----------------------------------------------------------------------------
2703
2704#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2705pub enum WorldListOpKind {
2706    GetSize,
2707    Create,
2708    Destroy,
2709    Unknown,
2710}
2711
2712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2713pub enum WorldOpKind {
2714    Init,
2715    GetNo,
2716    CameraEyeX,
2717    CameraEyeY,
2718    CameraEyeZ,
2719    CameraPintX,
2720    CameraPintY,
2721    CameraPintZ,
2722    CameraUpX,
2723    CameraUpY,
2724    CameraUpZ,
2725    CameraEyeXEve,
2726    CameraEyeYEve,
2727    CameraEyeZEve,
2728    CameraPintXEve,
2729    CameraPintYEve,
2730    CameraPintZEve,
2731    CameraUpXEve,
2732    CameraUpYEve,
2733    CameraUpZEve,
2734    CameraViewAngle,
2735    SetCameraEye,
2736    CalcCameraEye,
2737    SetCameraPint,
2738    CalcCameraPint,
2739    SetCameraUp,
2740    Mono,
2741    SetCameraEveXzRotate,
2742    Order,
2743    Layer,
2744    WipeCopy,
2745    WipeErase,
2746    Unknown,
2747}
2748
2749#[derive(Debug, Clone, Copy)]
2750pub struct WorldRotateEvent {
2751    pub loop_type: i32,
2752    pub cur_time: i32,
2753    pub end_time: i32,
2754    pub delay_time: i32,
2755    pub speed_type: i32,
2756    pub start_x: i32,
2757    pub start_z: i32,
2758    pub end_x: i32,
2759    pub end_z: i32,
2760}
2761
2762impl Default for WorldRotateEvent {
2763    fn default() -> Self {
2764        Self::new()
2765    }
2766}
2767
2768impl WorldRotateEvent {
2769    pub fn new() -> Self {
2770        Self {
2771            loop_type: -1,
2772            cur_time: 0,
2773            end_time: 0,
2774            delay_time: 0,
2775            speed_type: 0,
2776            start_x: 0,
2777            start_z: 0,
2778            end_x: 0,
2779            end_z: 0,
2780        }
2781    }
2782
2783    pub fn is_active(&self) -> bool {
2784        self.loop_type != -1
2785    }
2786}
2787
2788#[derive(Debug, Clone)]
2789pub struct WorldState {
2790    pub world_no: i32,
2791    pub mode: i32,
2792    pub camera_eye_x: IntEvent,
2793    pub camera_eye_y: IntEvent,
2794    pub camera_eye_z: IntEvent,
2795    pub camera_pint_x: IntEvent,
2796    pub camera_pint_y: IntEvent,
2797    pub camera_pint_z: IntEvent,
2798    pub camera_up_x: IntEvent,
2799    pub camera_up_y: IntEvent,
2800    pub camera_up_z: IntEvent,
2801    pub camera_view_angle: i32,
2802    pub mono: i32,
2803    pub order: i32,
2804    pub layer: i32,
2805    pub wipe_copy: i32,
2806    pub wipe_erase: i32,
2807    pub camera_eye_xz_eve: WorldRotateEvent,
2808    pub script_events: HashMap<i32, IntEvent>,
2809    pub extra_int: HashMap<i32, i64>,
2810    pub extra_str: HashMap<i32, String>,
2811}
2812
2813impl WorldState {
2814    pub fn new(world_no: i32) -> Self {
2815        let mut out = Self {
2816            world_no,
2817            mode: 1,
2818            camera_eye_x: IntEvent::new(0),
2819            camera_eye_y: IntEvent::new(0),
2820            camera_eye_z: IntEvent::new(-1000),
2821            camera_pint_x: IntEvent::new(0),
2822            camera_pint_y: IntEvent::new(0),
2823            camera_pint_z: IntEvent::new(0),
2824            camera_up_x: IntEvent::new(0),
2825            camera_up_y: IntEvent::new(1),
2826            camera_up_z: IntEvent::new(0),
2827            camera_view_angle: 450,
2828            mono: 0,
2829            order: 0,
2830            layer: 0,
2831            wipe_copy: 0,
2832            wipe_erase: 0,
2833            camera_eye_xz_eve: WorldRotateEvent::new(),
2834            script_events: HashMap::default(),
2835            extra_int: HashMap::default(),
2836            extra_str: HashMap::default(),
2837        };
2838        out.reinit();
2839        out
2840    }
2841
2842    pub fn reinit(&mut self) {
2843        self.mode = 1;
2844        self.camera_eye_x = IntEvent::new(0);
2845        self.camera_eye_y = IntEvent::new(0);
2846        self.camera_eye_z = IntEvent::new(-1000);
2847        self.camera_pint_x = IntEvent::new(0);
2848        self.camera_pint_y = IntEvent::new(0);
2849        self.camera_pint_z = IntEvent::new(0);
2850        self.camera_up_x = IntEvent::new(0);
2851        self.camera_up_y = IntEvent::new(1);
2852        self.camera_up_z = IntEvent::new(0);
2853        self.camera_view_angle = 450;
2854        self.mono = 0;
2855        self.order = 0;
2856        self.layer = 0;
2857        self.wipe_copy = 0;
2858        self.wipe_erase = 0;
2859        self.camera_eye_xz_eve = WorldRotateEvent::new();
2860    }
2861
2862    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
2863        self.camera_eye_x
2864            .update_time(past_game_time, past_real_time);
2865        self.camera_eye_y
2866            .update_time(past_game_time, past_real_time);
2867        self.camera_eye_z
2868            .update_time(past_game_time, past_real_time);
2869        self.camera_pint_x
2870            .update_time(past_game_time, past_real_time);
2871        self.camera_pint_y
2872            .update_time(past_game_time, past_real_time);
2873        self.camera_pint_z
2874            .update_time(past_game_time, past_real_time);
2875        self.camera_up_x.update_time(past_game_time, past_real_time);
2876        self.camera_up_y.update_time(past_game_time, past_real_time);
2877        self.camera_up_z.update_time(past_game_time, past_real_time);
2878        if self.camera_eye_xz_eve.is_active() {
2879            self.camera_eye_xz_eve.cur_time = self
2880                .camera_eye_xz_eve
2881                .cur_time
2882                .saturating_add(past_game_time);
2883        }
2884    }
2885
2886    pub fn frame(&mut self) {
2887        self.camera_eye_x.frame();
2888        self.camera_eye_y.frame();
2889        self.camera_eye_z.frame();
2890        self.camera_pint_x.frame();
2891        self.camera_pint_y.frame();
2892        self.camera_pint_z.frame();
2893        self.camera_up_x.frame();
2894        self.camera_up_y.frame();
2895        self.camera_up_z.frame();
2896
2897        if self.camera_eye_xz_eve.is_active() {
2898            self.frame_xz_rotate();
2899        }
2900    }
2901
2902    fn frame_xz_rotate(&mut self) {
2903        let mut cur_time = self.camera_eye_xz_eve.cur_time - self.camera_eye_xz_eve.delay_time;
2904        let end_time = self.camera_eye_xz_eve.end_time;
2905
2906        if self.camera_eye_xz_eve.loop_type == 0 && cur_time - end_time >= 0 {
2907            self.camera_eye_xz_eve.loop_type = -1;
2908            return;
2909        }
2910
2911        if cur_time <= 0 {
2912            self.camera_eye_x.cur_value = self.camera_eye_x.start_value;
2913            self.camera_eye_z.cur_value = self.camera_eye_z.start_value;
2914            return;
2915        }
2916
2917        if end_time <= 0 {
2918            return;
2919        }
2920
2921        if self.camera_eye_xz_eve.loop_type == 1 {
2922            cur_time %= end_time;
2923        }
2924        if self.camera_eye_xz_eve.loop_type == 2 {
2925            cur_time %= end_time * 2;
2926            if cur_time - end_time > 0 {
2927                cur_time = end_time - (cur_time - end_time);
2928            }
2929        }
2930
2931        match self.camera_eye_xz_eve.speed_type {
2932            1 => {
2933                cur_time = (cur_time as f64 * cur_time as f64 / end_time as f64) as i32;
2934            }
2935            2 => {
2936                let ct = (cur_time - end_time) as f64;
2937                let et = end_time as f64;
2938                cur_time = (-ct * ct / et + et) as i32;
2939            }
2940            _ => {}
2941        }
2942
2943        let px = self.camera_pint_x.get_total_value() as f64;
2944        let pz = self.camera_pint_z.get_total_value() as f64;
2945        let sx = self.camera_eye_x.start_value as f64;
2946        let sz = self.camera_eye_z.start_value as f64;
2947        let ex = self.camera_eye_x.end_value as f64;
2948        let ez = self.camera_eye_z.end_value as f64;
2949
2950        let sdx = sx - px;
2951        let sdz = sz - pz;
2952        let edx = ex - px;
2953        let edz = ez - pz;
2954
2955        let s_len = (sdx * sdx + sdz * sdz).sqrt();
2956        let e_len = (edx * edx + edz * edz).sqrt();
2957        let t_len = linear(cur_time, s_len, end_time, e_len);
2958
2959        let mut s_theta = sdz.atan2(sdx);
2960        let mut e_theta = edz.atan2(edx);
2961        if (s_theta - e_theta).abs() > std::f64::consts::PI {
2962            if e_theta < 0.0 {
2963                e_theta += std::f64::consts::PI * 2.0;
2964            } else {
2965                e_theta -= std::f64::consts::PI * 2.0;
2966            }
2967        }
2968        let t_theta = linear(cur_time, s_theta, end_time, e_theta);
2969
2970        let tmp_x = t_len * t_theta.cos() + px;
2971        let tmp_z = t_len * t_theta.sin() + pz;
2972
2973        self.camera_eye_x.cur_value = tmp_x as i32;
2974        self.camera_eye_z.cur_value = tmp_z as i32;
2975    }
2976}
2977
2978fn linear(cur: i32, start_value: f64, end_time: i32, end_value: f64) -> f64 {
2979    if end_time <= 0 {
2980        return end_value;
2981    }
2982    let t = cur as f64 / end_time as f64;
2983    start_value + (end_value - start_value) * t
2984}
2985
2986#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2987pub enum ObjectListOpKind {
2988    GetSize,
2989    Resize,
2990    Unknown,
2991}
2992
2993#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2994pub enum ObjectOpKind {
2995    Init,
2996    Free,
2997    InitParam,
2998    CreatePct,
2999    CreateRect,
3000    CreateString,
3001    /// CREATE_COPY_FROM
3002    CreateCopyFrom,
3003    /// SET_POS (2 or 3 ints)
3004    SetPos,
3005    /// SET_CENTER (2 or 3 ints)
3006    SetCenter,
3007    /// SET_SCALE (2 or 3 ints)
3008    SetScale,
3009    /// SET_ROTATE (2 or 3 ints)
3010    SetRotate,
3011    /// SET_CLIP (4 ints)
3012    SetClip,
3013    /// SET_SRC_CLIP (4 ints)
3014    SetSrcClip,
3015    /// CLEAR_BUTTON
3016    ClearButton,
3017    /// SET_BUTTON (1..4 ints, al_id=0..2)
3018    SetButton,
3019    /// SET_BUTTON_GROUP (int or element)
3020    SetButtonGroup,
3021    /// Int-list sub-element (X_REP/Y_REP/Z_REP/TR_REP/F, etc.).
3022    RepIntList,
3023    Unknown,
3024}
3025
3026#[derive(Debug, Clone, PartialEq, Eq)]
3027pub struct StringGlyphBackend {
3028    /// Original glyph index in the parsed string/mwnd item.
3029    pub glyph_index: usize,
3030    /// Layer-local texture origins relative to OBJECT.X/Y or the selection
3031    /// item origin.  Bearings and outline/shadow padding differ per layer.
3032    pub shadow_local_x: i32,
3033    pub shadow_local_y: i32,
3034    pub fuchi_local_x: i32,
3035    pub fuchi_local_y: i32,
3036    pub body_local_x: i32,
3037    pub body_local_y: i32,
3038    pub shadow_sprite_id: SpriteId,
3039    pub fuchi_sprite_id: SpriteId,
3040    pub body_sprite_id: SpriteId,
3041    pub shadow_image_id: Option<ImageHandle>,
3042    pub fuchi_image_id: Option<ImageHandle>,
3043    pub body_image_id: Option<ImageHandle>,
3044}
3045
3046#[derive(Debug, Clone, PartialEq, Eq, Default)]
3047pub enum ObjectBackend {
3048    #[default]
3049    None,
3050    /// Uses the engine's GfxRuntime object pipeline.
3051    Gfx,
3052    /// Rectangle backed by a standalone LayerManager sprite.
3053    Rect {
3054        layer_id: LayerId,
3055        sprite_id: SpriteId,
3056        width: u32,
3057        height: u32,
3058    },
3059    /// STRING object backend: original shadow/fuchi/body sprite triplet.
3060    String {
3061        layer_id: LayerId,
3062        shadow_sprite_id: SpriteId,
3063        fuchi_sprite_id: SpriteId,
3064        sprite_id: SpriteId,
3065        shadow_image_id: Option<ImageHandle>,
3066        fuchi_image_id: Option<ImageHandle>,
3067        image_id: Option<ImageHandle>,
3068        /// Per-glyph shadow/fuchi/body sprites.  The three scalar sprite fields
3069        /// above alias the first entry for compatibility with older helper paths;
3070        /// when this list is non-empty it is authoritative.
3071        glyphs: Vec<StringGlyphBackend>,
3072        /// MWND glyphs use the configured shadow/fuchi/body relative layers.
3073        /// Standalone OBJECT.STRING sprites retain the object's own sorter.
3074        mwnd_layer_reps: bool,
3075        width: u32,
3076        height: u32,
3077    },
3078    /// NUMBER object backend: a fixed sprite list (16) with per-digit sprites.
3079    Number {
3080        layer_id: LayerId,
3081        sprite_ids: Vec<SpriteId>,
3082    },
3083    /// WEATHER object backend: sprite list owned by the weather object runtime.
3084    Weather {
3085        layer_id: LayerId,
3086        sprite_ids: Vec<SpriteId>,
3087    },
3088    /// MOVIE object backend: a single sprite updated with video frames.
3089    Movie {
3090        layer_id: LayerId,
3091        sprite_id: SpriteId,
3092        image_id: Option<ImageHandle>,
3093        width: u32,
3094        height: u32,
3095    },
3096}
3097
3098pub const OBJECT_NESTED_SLOT_KEY: i32 = i32::MIN + 1;
3099
3100/// An `ObjectBaseState` integer field, named for the op tables.
3101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3102pub enum BaseIntField {
3103    Alpha,
3104    AlphaBlend,
3105    AlphaTest,
3106    Blend,
3107    ClickDisable,
3108    ClipUse,
3109    Culling,
3110    Disp,
3111    FogUse,
3112    Layer,
3113    LightNo,
3114    MaskNo,
3115    Order,
3116    SrcClipUse,
3117    TonecurveNo,
3118    WipeCopy,
3119    WipeErase,
3120    World,
3121}
3122
3123impl ObjectBaseState {
3124    fn int_field(&self, field: BaseIntField) -> i64 {
3125        match field {
3126            BaseIntField::Alpha => self.alpha,
3127            BaseIntField::AlphaBlend => self.alpha_blend,
3128            BaseIntField::AlphaTest => self.alpha_test,
3129            BaseIntField::Blend => self.blend,
3130            BaseIntField::ClickDisable => self.click_disable,
3131            BaseIntField::ClipUse => self.clip_use,
3132            BaseIntField::Culling => self.culling,
3133            BaseIntField::Disp => self.disp,
3134            BaseIntField::FogUse => self.fog_use,
3135            BaseIntField::Layer => self.layer,
3136            BaseIntField::LightNo => self.light_no,
3137            BaseIntField::MaskNo => self.mask_no,
3138            BaseIntField::Order => self.order,
3139            BaseIntField::SrcClipUse => self.src_clip_use,
3140            BaseIntField::TonecurveNo => self.tonecurve_no,
3141            BaseIntField::WipeCopy => self.wipe_copy,
3142            BaseIntField::WipeErase => self.wipe_erase,
3143            BaseIntField::World => self.world,
3144        }
3145    }
3146}
3147
3148/// Where an OBJECT integer property's value lives (`ObjectState::fixed_int_prop`).
3149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3150pub enum FixedIntProp {
3151    Base(BaseIntField),
3152    /// The total value of the property's event.
3153    EventTotal(ObjectEventTarget),
3154}
3155
3156/// `ObjectOpTables::fixed_int_props`' entries, in the order of the
3157/// comparison chain they replace (first match wins).
3158fn fixed_int_prop_pairs(
3159    ids: &crate::runtime::constants::RuntimeConstants,
3160) -> Vec<(i32, FixedIntProp)> {
3161    vec![
3162        (ids.obj_disp, FixedIntProp::Base(BaseIntField::Disp)),
3163        (
3164            ids.obj_wipe_copy,
3165            FixedIntProp::Base(BaseIntField::WipeCopy),
3166        ),
3167        (
3168            ids.obj_wipe_erase,
3169            FixedIntProp::Base(BaseIntField::WipeErase),
3170        ),
3171        (
3172            ids.obj_click_disable,
3173            FixedIntProp::Base(BaseIntField::ClickDisable),
3174        ),
3175        (
3176            ids.obj_patno,
3177            FixedIntProp::EventTotal(ObjectEventTarget::Patno),
3178        ),
3179        (ids.obj_world, FixedIntProp::Base(BaseIntField::World)),
3180        (ids.obj_order, FixedIntProp::Base(BaseIntField::Order)),
3181        (ids.obj_layer, FixedIntProp::Base(BaseIntField::Layer)),
3182        (ids.obj_x, FixedIntProp::EventTotal(ObjectEventTarget::X)),
3183        (ids.obj_y, FixedIntProp::EventTotal(ObjectEventTarget::Y)),
3184        (ids.obj_z, FixedIntProp::EventTotal(ObjectEventTarget::Z)),
3185        (
3186            ids.obj_center_x,
3187            FixedIntProp::EventTotal(ObjectEventTarget::CenterX),
3188        ),
3189        (
3190            ids.obj_center_y,
3191            FixedIntProp::EventTotal(ObjectEventTarget::CenterY),
3192        ),
3193        (
3194            ids.obj_center_z,
3195            FixedIntProp::EventTotal(ObjectEventTarget::CenterZ),
3196        ),
3197        (
3198            ids.obj_center_rep_x,
3199            FixedIntProp::EventTotal(ObjectEventTarget::CenterRepX),
3200        ),
3201        (
3202            ids.obj_center_rep_y,
3203            FixedIntProp::EventTotal(ObjectEventTarget::CenterRepY),
3204        ),
3205        (
3206            ids.obj_center_rep_z,
3207            FixedIntProp::EventTotal(ObjectEventTarget::CenterRepZ),
3208        ),
3209        (
3210            ids.obj_scale_x,
3211            FixedIntProp::EventTotal(ObjectEventTarget::ScaleX),
3212        ),
3213        (
3214            ids.obj_scale_y,
3215            FixedIntProp::EventTotal(ObjectEventTarget::ScaleY),
3216        ),
3217        (
3218            ids.obj_scale_z,
3219            FixedIntProp::EventTotal(ObjectEventTarget::ScaleZ),
3220        ),
3221        (
3222            ids.obj_rotate_x,
3223            FixedIntProp::EventTotal(ObjectEventTarget::RotateX),
3224        ),
3225        (
3226            ids.obj_rotate_y,
3227            FixedIntProp::EventTotal(ObjectEventTarget::RotateY),
3228        ),
3229        (
3230            ids.obj_rotate_z,
3231            FixedIntProp::EventTotal(ObjectEventTarget::RotateZ),
3232        ),
3233        (ids.obj_clip_use, FixedIntProp::Base(BaseIntField::ClipUse)),
3234        (
3235            ids.obj_clip_left,
3236            FixedIntProp::EventTotal(ObjectEventTarget::ClipLeft),
3237        ),
3238        (
3239            ids.obj_clip_top,
3240            FixedIntProp::EventTotal(ObjectEventTarget::ClipTop),
3241        ),
3242        (
3243            ids.obj_clip_right,
3244            FixedIntProp::EventTotal(ObjectEventTarget::ClipRight),
3245        ),
3246        (
3247            ids.obj_clip_bottom,
3248            FixedIntProp::EventTotal(ObjectEventTarget::ClipBottom),
3249        ),
3250        (
3251            ids.obj_src_clip_use,
3252            FixedIntProp::Base(BaseIntField::SrcClipUse),
3253        ),
3254        (
3255            ids.obj_src_clip_left,
3256            FixedIntProp::EventTotal(ObjectEventTarget::SrcClipLeft),
3257        ),
3258        (
3259            ids.obj_src_clip_top,
3260            FixedIntProp::EventTotal(ObjectEventTarget::SrcClipTop),
3261        ),
3262        (
3263            ids.obj_src_clip_right,
3264            FixedIntProp::EventTotal(ObjectEventTarget::SrcClipRight),
3265        ),
3266        (
3267            ids.obj_src_clip_bottom,
3268            FixedIntProp::EventTotal(ObjectEventTarget::SrcClipBottom),
3269        ),
3270        (ids.obj_alpha, FixedIntProp::Base(BaseIntField::Alpha)),
3271        (ids.obj_tr, FixedIntProp::EventTotal(ObjectEventTarget::Tr)),
3272        (
3273            ids.obj_mono,
3274            FixedIntProp::EventTotal(ObjectEventTarget::Mono),
3275        ),
3276        (
3277            ids.obj_reverse,
3278            FixedIntProp::EventTotal(ObjectEventTarget::Reverse),
3279        ),
3280        (
3281            ids.obj_bright,
3282            FixedIntProp::EventTotal(ObjectEventTarget::Bright),
3283        ),
3284        (
3285            ids.obj_dark,
3286            FixedIntProp::EventTotal(ObjectEventTarget::Dark),
3287        ),
3288        (
3289            ids.obj_color_r,
3290            FixedIntProp::EventTotal(ObjectEventTarget::ColorR),
3291        ),
3292        (
3293            ids.obj_color_g,
3294            FixedIntProp::EventTotal(ObjectEventTarget::ColorG),
3295        ),
3296        (
3297            ids.obj_color_b,
3298            FixedIntProp::EventTotal(ObjectEventTarget::ColorB),
3299        ),
3300        (
3301            ids.obj_color_rate,
3302            FixedIntProp::EventTotal(ObjectEventTarget::ColorRate),
3303        ),
3304        (
3305            ids.obj_color_add_r,
3306            FixedIntProp::EventTotal(ObjectEventTarget::ColorAddR),
3307        ),
3308        (
3309            ids.obj_color_add_g,
3310            FixedIntProp::EventTotal(ObjectEventTarget::ColorAddG),
3311        ),
3312        (
3313            ids.obj_color_add_b,
3314            FixedIntProp::EventTotal(ObjectEventTarget::ColorAddB),
3315        ),
3316        (ids.obj_mask_no, FixedIntProp::Base(BaseIntField::MaskNo)),
3317        (
3318            ids.obj_tonecurve_no,
3319            FixedIntProp::Base(BaseIntField::TonecurveNo),
3320        ),
3321        (ids.obj_light_no, FixedIntProp::Base(BaseIntField::LightNo)),
3322        (ids.obj_fog_use, FixedIntProp::Base(BaseIntField::FogUse)),
3323        (ids.obj_culling, FixedIntProp::Base(BaseIntField::Culling)),
3324        (
3325            ids.obj_alpha_test,
3326            FixedIntProp::Base(BaseIntField::AlphaTest),
3327        ),
3328        (
3329            ids.obj_alpha_blend,
3330            FixedIntProp::Base(BaseIntField::AlphaBlend),
3331        ),
3332        (ids.obj_blend, FixedIntProp::Base(BaseIntField::Blend)),
3333    ]
3334}
3335
3336/// Which object event an OBJECT property op drives, looked up by op code.
3337/// These ran as chains of about fifty comparisons on every object property
3338/// read and write (a tenth of the VM's frame time in particle scripts).
3339#[derive(Debug, Default)]
3340pub struct ObjectOpTables {
3341    /// `*_EVE` ops: the event itself.
3342    events: HashMap<i32, ObjectEventTarget>,
3343    /// Plain property ops whose value an event also holds.
3344    props: HashMap<i32, ObjectEventTarget>,
3345    /// Integer properties read directly (`ObjectState::fixed_int_prop`).
3346    fixed_int_props: HashMap<i32, FixedIntProp>,
3347}
3348
3349impl ObjectOpTables {
3350    pub fn new(ids: &crate::runtime::constants::RuntimeConstants) -> Self {
3351        // First match wins, as in the comparison chains these replace; a
3352        // zero id is unused.
3353        fn table(pairs: &[(i32, ObjectEventTarget)]) -> HashMap<i32, ObjectEventTarget> {
3354            let mut map = HashMap::default();
3355            for &(id, target) in pairs {
3356                if id != 0 {
3357                    map.entry(id).or_insert(target);
3358                }
3359            }
3360            map
3361        }
3362        let mut fixed_int_props = HashMap::default();
3363        for (index, (id, prop)) in fixed_int_prop_pairs(ids).into_iter().enumerate() {
3364            // The chain compared DISP without a zero check.
3365            if id != 0 || index == 0 {
3366                fixed_int_props.entry(id).or_insert(prop);
3367            }
3368        }
3369        Self {
3370            fixed_int_props,
3371            events: table(&[
3372                (ids.obj_x_eve, ObjectEventTarget::X),
3373                (ids.obj_y_eve, ObjectEventTarget::Y),
3374                (ids.obj_x_rep_eve, ObjectEventTarget::XRep),
3375                (ids.obj_y_rep_eve, ObjectEventTarget::YRep),
3376                (ids.obj_z_rep_eve, ObjectEventTarget::ZRep),
3377                (ids.obj_tr_eve, ObjectEventTarget::Tr),
3378                (ids.obj_tr_rep_eve, ObjectEventTarget::TrRep),
3379                (ids.obj_patno_eve, ObjectEventTarget::Patno),
3380                (ids.obj_z_eve, ObjectEventTarget::Z),
3381                (ids.obj_center_x_eve, ObjectEventTarget::CenterX),
3382                (ids.obj_center_y_eve, ObjectEventTarget::CenterY),
3383                (ids.obj_center_z_eve, ObjectEventTarget::CenterZ),
3384                (ids.obj_center_rep_x_eve, ObjectEventTarget::CenterRepX),
3385                (ids.obj_center_rep_y_eve, ObjectEventTarget::CenterRepY),
3386                (ids.obj_center_rep_z_eve, ObjectEventTarget::CenterRepZ),
3387                (ids.obj_scale_x_eve, ObjectEventTarget::ScaleX),
3388                (ids.obj_scale_y_eve, ObjectEventTarget::ScaleY),
3389                (ids.obj_scale_z_eve, ObjectEventTarget::ScaleZ),
3390                (ids.obj_rotate_x_eve, ObjectEventTarget::RotateX),
3391                (ids.obj_rotate_y_eve, ObjectEventTarget::RotateY),
3392                (ids.obj_rotate_z_eve, ObjectEventTarget::RotateZ),
3393                (ids.obj_clip_left_eve, ObjectEventTarget::ClipLeft),
3394                (ids.obj_clip_top_eve, ObjectEventTarget::ClipTop),
3395                (ids.obj_clip_right_eve, ObjectEventTarget::ClipRight),
3396                (ids.obj_clip_bottom_eve, ObjectEventTarget::ClipBottom),
3397                (ids.obj_src_clip_left_eve, ObjectEventTarget::SrcClipLeft),
3398                (ids.obj_src_clip_top_eve, ObjectEventTarget::SrcClipTop),
3399                (ids.obj_src_clip_right_eve, ObjectEventTarget::SrcClipRight),
3400                (
3401                    ids.obj_src_clip_bottom_eve,
3402                    ObjectEventTarget::SrcClipBottom,
3403                ),
3404                (ids.obj_mono_eve, ObjectEventTarget::Mono),
3405                (ids.obj_reverse_eve, ObjectEventTarget::Reverse),
3406                (ids.obj_bright_eve, ObjectEventTarget::Bright),
3407                (ids.obj_dark_eve, ObjectEventTarget::Dark),
3408                (ids.obj_color_rate_eve, ObjectEventTarget::ColorRate),
3409                (ids.obj_color_add_r_eve, ObjectEventTarget::ColorAddR),
3410                (ids.obj_color_add_g_eve, ObjectEventTarget::ColorAddG),
3411                (ids.obj_color_add_b_eve, ObjectEventTarget::ColorAddB),
3412                (ids.obj_color_r_eve, ObjectEventTarget::ColorR),
3413                (ids.obj_color_g_eve, ObjectEventTarget::ColorG),
3414                (ids.obj_color_b_eve, ObjectEventTarget::ColorB),
3415            ]),
3416            props: table(&[
3417                (ids.obj_patno, ObjectEventTarget::Patno),
3418                (ids.obj_x, ObjectEventTarget::X),
3419                (ids.obj_y, ObjectEventTarget::Y),
3420                (ids.obj_z, ObjectEventTarget::Z),
3421                (ids.obj_center_x, ObjectEventTarget::CenterX),
3422                (ids.obj_center_y, ObjectEventTarget::CenterY),
3423                (ids.obj_center_z, ObjectEventTarget::CenterZ),
3424                (ids.obj_center_rep_x, ObjectEventTarget::CenterRepX),
3425                (ids.obj_center_rep_y, ObjectEventTarget::CenterRepY),
3426                (ids.obj_center_rep_z, ObjectEventTarget::CenterRepZ),
3427                (ids.obj_scale_x, ObjectEventTarget::ScaleX),
3428                (ids.obj_scale_y, ObjectEventTarget::ScaleY),
3429                (ids.obj_scale_z, ObjectEventTarget::ScaleZ),
3430                (ids.obj_rotate_x, ObjectEventTarget::RotateX),
3431                (ids.obj_rotate_y, ObjectEventTarget::RotateY),
3432                (ids.obj_rotate_z, ObjectEventTarget::RotateZ),
3433                (ids.obj_clip_left, ObjectEventTarget::ClipLeft),
3434                (ids.obj_clip_top, ObjectEventTarget::ClipTop),
3435                (ids.obj_clip_right, ObjectEventTarget::ClipRight),
3436                (ids.obj_clip_bottom, ObjectEventTarget::ClipBottom),
3437                (ids.obj_src_clip_left, ObjectEventTarget::SrcClipLeft),
3438                (ids.obj_src_clip_top, ObjectEventTarget::SrcClipTop),
3439                (ids.obj_src_clip_right, ObjectEventTarget::SrcClipRight),
3440                (ids.obj_src_clip_bottom, ObjectEventTarget::SrcClipBottom),
3441                (ids.obj_tr, ObjectEventTarget::Tr),
3442                (ids.obj_mono, ObjectEventTarget::Mono),
3443                (ids.obj_reverse, ObjectEventTarget::Reverse),
3444                (ids.obj_bright, ObjectEventTarget::Bright),
3445                (ids.obj_dark, ObjectEventTarget::Dark),
3446                (ids.obj_color_r, ObjectEventTarget::ColorR),
3447                (ids.obj_color_g, ObjectEventTarget::ColorG),
3448                (ids.obj_color_b, ObjectEventTarget::ColorB),
3449                (ids.obj_color_rate, ObjectEventTarget::ColorRate),
3450                (ids.obj_color_add_r, ObjectEventTarget::ColorAddR),
3451                (ids.obj_color_add_g, ObjectEventTarget::ColorAddG),
3452                (ids.obj_color_add_b, ObjectEventTarget::ColorAddB),
3453            ]),
3454        }
3455    }
3456
3457    pub fn fixed_int_prop(&self, op: i32) -> Option<FixedIntProp> {
3458        self.fixed_int_props.get(&op).copied()
3459    }
3460
3461    pub fn event_target(&self, op: i32) -> ObjectEventTarget {
3462        self.events
3463            .get(&op)
3464            .copied()
3465            .unwrap_or(ObjectEventTarget::Unknown)
3466    }
3467
3468    /// The event backing a property op, else the event op's own target.
3469    pub fn prop_target(&self, op: i32) -> ObjectEventTarget {
3470        match self.props.get(&op) {
3471            Some(&target) => target,
3472            None => self.event_target(op),
3473        }
3474    }
3475}
3476
3477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3478pub enum ObjectEventTarget {
3479    X,
3480    Y,
3481    XRep,
3482    YRep,
3483    ZRep,
3484    Alpha,
3485    Patno,
3486    Order,
3487    Layer,
3488    Z,
3489    CenterX,
3490    CenterY,
3491    CenterZ,
3492    CenterRepX,
3493    CenterRepY,
3494    CenterRepZ,
3495    ScaleX,
3496    ScaleY,
3497    ScaleZ,
3498    RotateX,
3499    RotateY,
3500    RotateZ,
3501    TrRep,
3502    ClipLeft,
3503    ClipTop,
3504    ClipRight,
3505    ClipBottom,
3506    SrcClipLeft,
3507    SrcClipTop,
3508    SrcClipRight,
3509    SrcClipBottom,
3510    Tr,
3511    Mono,
3512    Reverse,
3513    Bright,
3514    Dark,
3515    ColorRate,
3516    ColorAddR,
3517    ColorAddG,
3518    ColorAddB,
3519    ColorR,
3520    ColorG,
3521    ColorB,
3522    Unknown,
3523}
3524
3525#[derive(Debug, Clone)]
3526pub struct ObjectButtonState {
3527    pub enabled: bool,
3528    pub button_no: i64,
3529    pub group_no: i64,
3530    /// Additional cut offset applied after OBJECT.PATNO for button rendering.
3531    ///
3532    /// Original C_elm_object::frame()/create_trp() submits
3533    /// `obp.pat_no + button.cut_no`, and button action templates add their
3534    /// `rep_pat_no` on top of that submitted cut.
3535    pub cut_no: i64,
3536    /// Optional override derived from SET_BUTTON_GROUP(element).
3537    pub group_idx_override: Option<usize>,
3538    pub action_no: i64,
3539    pub se_no: i64,
3540    pub sys_type: i64,
3541    pub sys_type_opt: i64,
3542    pub mode: i64,
3543    pub push_keep: bool,
3544    pub alpha_test: bool,
3545    /// Button state constants: 0=normal, 1=hit, 2=push, 3=select, 4=disable.
3546    pub state: i64,
3547    pub hit: bool,
3548    pub pushed: bool,
3549
3550    // Decided action (set_button_decided_action)
3551    pub decided_action_scn_name: String,
3552    pub decided_action_cmd_name: String,
3553    pub decided_action_z_no: i64,
3554    /// Previous-frame hit flag used to model *_this_frame button transitions.
3555    pub last_hit: bool,
3556    /// Previous-frame pushed flag used to model *_this_frame button transitions.
3557    pub last_pushed: bool,
3558}
3559
3560impl Default for ObjectButtonState {
3561    fn default() -> Self {
3562        Self {
3563            enabled: false,
3564            button_no: 0,
3565            group_no: -1,
3566            cut_no: 0,
3567            group_idx_override: None,
3568            action_no: -1,
3569            se_no: -1,
3570            sys_type: 0,
3571            sys_type_opt: 0,
3572            mode: 0,
3573            push_keep: false,
3574            alpha_test: false,
3575            state: 0,
3576            hit: false,
3577            pushed: false,
3578            decided_action_scn_name: String::new(),
3579            decided_action_cmd_name: String::new(),
3580            decided_action_z_no: -1,
3581            last_hit: false,
3582            last_pushed: false,
3583        }
3584    }
3585}
3586
3587impl ObjectButtonState {
3588    pub fn clear(&mut self) {
3589        *self = Self::default();
3590    }
3591
3592    pub fn group_idx(&self) -> Option<usize> {
3593        if !self.enabled {
3594            return None;
3595        }
3596        if let Some(i) = self.group_idx_override {
3597            return Some(i);
3598        }
3599        if self.group_no < 0 {
3600            return None;
3601        }
3602        Some(self.group_no as usize)
3603    }
3604
3605    pub fn is_disabled(&self) -> bool {
3606        self.enabled && self.state == 4
3607    }
3608}
3609
3610#[derive(Debug, Clone)]
3611pub struct ObjectStringParam {
3612    pub moji_size: i64,
3613    pub moji_space_x: i64,
3614    pub moji_space_y: i64,
3615    pub moji_cnt: i64,
3616    pub moji_color: i64,
3617    pub shadow_color: i64,
3618    pub fuchi_color: i64,
3619    /// -1: auto/default in original engine
3620    pub shadow_mode: i64,
3621}
3622
3623impl Default for ObjectStringParam {
3624    fn default() -> Self {
3625        Self {
3626            moji_size: 12,
3627            moji_space_x: 0,
3628            moji_space_y: 0,
3629            moji_cnt: 0,
3630            moji_color: 0,
3631            shadow_color: 1,
3632            fuchi_color: 1,
3633            shadow_mode: -1,
3634        }
3635    }
3636}
3637
3638#[derive(Debug, Default, Clone)]
3639pub struct ObjectNumberParam {
3640    pub keta_max: i64,
3641    pub disp_zero: i64,
3642    pub disp_sign: i64,
3643    pub tumeru_sign: i64,
3644    pub space_mod: i64,
3645    pub space: i64,
3646}
3647
3648#[derive(Debug, Default, Clone)]
3649pub struct ObjectWeatherParam {
3650    /// TNM_OBJECT_WEATHER_TYPE_* (0=none, 1=type A, 2=type B)
3651    pub weather_type: i64,
3652    pub cnt: i64,
3653    pub pat_mode: i64,
3654    pub pat_no_00: i64,
3655    pub pat_no_01: i64,
3656    pub pat_time: i64,
3657    pub move_time_x: i64,
3658    pub move_time_y: i64,
3659    pub sin_time_x: i64,
3660    pub sin_power_x: i64,
3661    pub sin_time_y: i64,
3662    pub sin_power_y: i64,
3663    pub center_x: i64,
3664    pub center_y: i64,
3665    pub appear_range: i64,
3666    pub move_time: i64,
3667    pub center_rotate: i64,
3668    pub zoom_min: i64,
3669    pub zoom_max: i64,
3670    pub scale_x: i64,
3671    pub scale_y: i64,
3672    pub active_time: i64,
3673    pub real_time_flag: bool,
3674}
3675
3676#[derive(Debug, Default, Clone)]
3677pub struct ObjectWeatherWorkSub {
3678    pub state: i64,
3679    pub state_cur_time: i64,
3680    pub state_time_len: i64,
3681    pub move_start_pos_x: i64,
3682    pub move_start_pos_y: i64,
3683    pub move_start_distance: i64,
3684    pub move_start_degree: i64,
3685    pub move_time_x: i64,
3686    pub move_time_y: i64,
3687    pub move_cur_time: i64,
3688    pub sin_time_x: i64,
3689    pub sin_time_y: i64,
3690    pub sin_power_x: i64,
3691    pub sin_power_y: i64,
3692    pub sin_cur_time: i64,
3693    pub center_rotate: i64,
3694    pub zoom_min: i64,
3695    pub zoom_max: i64,
3696    pub scale_x: i64,
3697    pub scale_y: i64,
3698    pub active_time_len: i64,
3699    pub real_time_flag: bool,
3700    pub restruct_flag: bool,
3701}
3702
3703#[derive(Debug, Clone)]
3704pub struct ObjectWeatherWorkState {
3705    pub cnt_max: usize,
3706    pub sub: Vec<ObjectWeatherWorkSub>,
3707    rand_seed: u32,
3708}
3709
3710impl Default for ObjectWeatherWorkState {
3711    fn default() -> Self {
3712        Self {
3713            cnt_max: 0,
3714            sub: Vec::new(),
3715            rand_seed: 0x1234_abcd,
3716        }
3717    }
3718}
3719
3720impl ObjectWeatherWorkState {
3721    fn next_rand(&mut self) -> i64 {
3722        self.rand_seed = self.rand_seed.wrapping_mul(1103515245).wrapping_add(12345);
3723        ((self.rand_seed >> 16) & 0x7fff) as i64
3724    }
3725
3726    pub fn rand_mod(&mut self, modulo: i64) -> i64 {
3727        if modulo <= 0 {
3728            0
3729        } else {
3730            self.next_rand() % modulo
3731        }
3732    }
3733}
3734
3735#[derive(Debug, Clone)]
3736pub struct ObjectMovieState {
3737    pub loop_flag: bool,
3738    pub auto_free_flag: bool,
3739    pub real_time_flag: bool,
3740    pub pause_flag: bool,
3741
3742    /// Current playback timer in milliseconds (the original implementation: m_omv_timer).
3743    pub timer_ms: u64,
3744    /// Total movie time in milliseconds if known.
3745    pub total_ms: Option<u64>,
3746    /// Intrinsic movie texture size.  The original engine creates the dynamic
3747    /// OMV texture during restruct_movie(), so GET_SIZE_X/Y can query this
3748    /// immediately after CREATE_MOVIE, before the first decoded frame arrives.
3749    pub width: u32,
3750    pub height: u32,
3751
3752    pub playing: bool,
3753    pub last_tick: Option<Instant>,
3754    pub last_frame_idx: Option<usize>,
3755    pub audio_id: Option<u64>,
3756    pub audio_started_once: bool,
3757    // OBJECT.OMV mirrors the original single D3DUSAGE_DYNAMIC texture. Slot 0
3758    // owns that stable ImageHandle for the movie lifetime; slot 1 is retained only
3759    // for snapshot/backward state layout compatibility and is no longer used
3760    // for per-frame ping-pong.
3761    pub frame_image_ids: [Option<ImageHandle>; 2],
3762    pub frame_image_cursor: usize,
3763    pub just_finished: bool,
3764    pub just_looped: bool,
3765    pub seeked: bool,
3766}
3767
3768impl Default for ObjectMovieState {
3769    fn default() -> Self {
3770        Self {
3771            loop_flag: false,
3772            auto_free_flag: true,
3773            real_time_flag: true,
3774            pause_flag: false,
3775            timer_ms: 0,
3776            total_ms: None,
3777            width: 0,
3778            height: 0,
3779            playing: false,
3780            last_tick: None,
3781            last_frame_idx: None,
3782            audio_id: None,
3783            audio_started_once: false,
3784            frame_image_ids: [None, None],
3785            frame_image_cursor: 0,
3786            just_finished: false,
3787            just_looped: false,
3788            seeked: false,
3789        }
3790    }
3791}
3792
3793impl ObjectMovieState {
3794    pub fn reset(&mut self) {
3795        *self = Self::default();
3796    }
3797
3798    pub fn start(
3799        &mut self,
3800        total_ms: Option<u64>,
3801        loop_flag: bool,
3802        auto_free_flag: bool,
3803        real_time_flag: bool,
3804        ready_only: bool,
3805    ) {
3806        self.total_ms = total_ms;
3807        self.loop_flag = loop_flag;
3808        self.auto_free_flag = auto_free_flag;
3809        self.real_time_flag = real_time_flag;
3810        self.pause_flag = ready_only;
3811        self.timer_ms = 0;
3812        self.playing = !ready_only;
3813        self.last_tick = Some(Instant::now());
3814        self.last_frame_idx = None;
3815        self.audio_id = None;
3816        self.audio_started_once = false;
3817        self.frame_image_ids = [None, None];
3818        self.frame_image_cursor = 0;
3819        self.just_finished = false;
3820        self.just_looped = false;
3821        self.seeked = false;
3822    }
3823
3824    pub fn tick(&mut self, past_game_time: i32, past_real_time: i32) {
3825        self.just_finished = false;
3826        self.just_looped = false;
3827        if !self.playing || self.pause_flag || self.audio_id.is_some() {
3828            // The audio device is the master clock while a movie soundtrack is
3829            // active. Advancing a second wall/game clock here causes drift and
3830            // lets video reach EOS before the sound handle does.
3831            return;
3832        }
3833        let add = if self.real_time_flag {
3834            past_real_time.max(0) as u64
3835        } else {
3836            past_game_time.max(0) as u64
3837        };
3838        if add == 0 {
3839            return;
3840        }
3841        self.timer_ms = self.timer_ms.saturating_add(add);
3842        if let Some(total) = self.total_ms
3843            && total > 0
3844            && self.timer_ms >= total
3845        {
3846            if self.loop_flag {
3847                self.timer_ms %= total;
3848                self.just_looped = true;
3849            } else {
3850                self.playing = false;
3851                self.just_finished = true;
3852            }
3853        }
3854    }
3855
3856    pub fn seek(&mut self, time_ms: u64) {
3857        // C_elm_object::seek_movie stores m_omv_timer verbatim. Loop wrapping
3858        // happens later from the movie update path, not in SEEK_MOVIE itself.
3859        self.timer_ms = time_ms;
3860        self.last_tick = Some(Instant::now());
3861        self.last_frame_idx = None;
3862        self.audio_started_once = false;
3863        self.seeked = true;
3864    }
3865
3866    pub fn get_seek_time(&self) -> u64 {
3867        if let Some(total) = self.total_ms
3868            && total > 0
3869        {
3870            return self.timer_ms % total;
3871        }
3872        0
3873    }
3874
3875    pub fn check_movie(&self) -> bool {
3876        self.playing
3877    }
3878}
3879
3880#[derive(Debug, Clone)]
3881pub struct ObjectEmoteParam {
3882    pub width: i64,
3883    pub height: i64,
3884    pub file_name: Option<String>,
3885    pub timeline_names: [String; 8],
3886    pub timeline_options: [i64; 8],
3887    pub koe_chara_no: i64,
3888    pub koe_mouth_volume: i64,
3889    pub rep_x: i64,
3890    pub rep_y: i64,
3891    pub runtime: Option<crate::emote::SiglusEmoteRuntime>,
3892}
3893
3894impl Default for ObjectEmoteParam {
3895    fn default() -> Self {
3896        Self {
3897            width: 0,
3898            height: 0,
3899            file_name: None,
3900            timeline_names: std::array::from_fn(|_| String::new()),
3901            timeline_options: [0; 8],
3902            koe_chara_no: -1,
3903            koe_mouth_volume: 0,
3904            rep_x: 0,
3905            rep_y: 0,
3906            runtime: None,
3907        }
3908    }
3909}
3910
3911impl ObjectEmoteParam {
3912    pub fn clear_runtime(&mut self) {
3913        self.runtime = None;
3914    }
3915
3916    pub fn tick(&mut self, past_game_time: i32) {
3917        if let Some(runtime) = self.runtime.as_mut()
3918            && let Err(err) = runtime.progress_ms(past_game_time)
3919        {
3920            log::error!("Emote Progress failed: {err:#}");
3921        }
3922    }
3923
3924    pub fn is_animating(&self) -> bool {
3925        if let Some(runtime) = self.runtime.as_ref() {
3926            return runtime.is_animating();
3927        }
3928        false
3929    }
3930
3931    pub fn clone_player_for_object(&mut self) {
3932        if let Some(runtime) = self.runtime.as_ref() {
3933            self.runtime = Some(runtime.clone_for_object());
3934        }
3935    }
3936}
3937
3938#[derive(Debug, Clone, Default)]
3939pub struct ObjectFrameActionState {
3940    pub scn_name: String,
3941    pub cmd_name: String,
3942    pub counter: Counter,
3943    pub end_time: i64,
3944    pub real_time_flag: bool,
3945    pub end_flag: bool,
3946    pub args: Vec<crate::runtime::Value>,
3947}
3948
3949impl ObjectFrameActionState {
3950    /// C_elm_frame_action::reinit(false): clear the active action and counter,
3951    /// but deliberately preserve m_end_time.
3952    pub fn reinit_without_finish(&mut self) {
3953        let end_time = self.end_time;
3954        *self = Self::default();
3955        self.end_time = end_time;
3956    }
3957}
3958
3959#[derive(Debug, Clone)]
3960pub struct ObjectBaseState {
3961    pub wipe_copy: i64,
3962    pub wipe_erase: i64,
3963    pub click_disable: i64,
3964    pub disp: i64,
3965    pub patno: i64,
3966    pub world: i64,
3967    pub order: i64,
3968    pub layer: i64,
3969    pub x: i64,
3970    pub y: i64,
3971    pub z: i64,
3972    pub center_x: i64,
3973    pub center_y: i64,
3974    pub center_z: i64,
3975    pub center_rep_x: i64,
3976    pub center_rep_y: i64,
3977    pub center_rep_z: i64,
3978    pub scale_x: i64,
3979    pub scale_y: i64,
3980    pub scale_z: i64,
3981    pub rotate_x: i64,
3982    pub rotate_y: i64,
3983    pub rotate_z: i64,
3984    pub clip_use: i64,
3985    pub clip_left: i64,
3986    pub clip_top: i64,
3987    pub clip_right: i64,
3988    pub clip_bottom: i64,
3989    pub src_clip_use: i64,
3990    pub src_clip_left: i64,
3991    pub src_clip_top: i64,
3992    pub src_clip_right: i64,
3993    pub src_clip_bottom: i64,
3994    pub alpha: i64,
3995    pub tr: i64,
3996    pub mono: i64,
3997    pub reverse: i64,
3998    pub bright: i64,
3999    pub dark: i64,
4000    pub color_r: i64,
4001    pub color_g: i64,
4002    pub color_b: i64,
4003    pub color_rate: i64,
4004    pub color_add_r: i64,
4005    pub color_add_g: i64,
4006    pub color_add_b: i64,
4007    pub mask_no: i64,
4008    pub tonecurve_no: i64,
4009    pub light_no: i64,
4010    pub fog_use: i64,
4011    pub culling: i64,
4012    pub alpha_test: i64,
4013    pub alpha_blend: i64,
4014    pub blend: i64,
4015    pub child_sort_type: i64,
4016    pub no_event_hint: bool,
4017}
4018
4019impl Default for ObjectBaseState {
4020    fn default() -> Self {
4021        Self {
4022            wipe_copy: 0,
4023            wipe_erase: 0,
4024            click_disable: 0,
4025            disp: 0,
4026            patno: 0,
4027            world: -1,
4028            order: 0,
4029            layer: 0,
4030            x: 0,
4031            y: 0,
4032            z: 0,
4033            center_x: 0,
4034            center_y: 0,
4035            center_z: 0,
4036            center_rep_x: 0,
4037            center_rep_y: 0,
4038            center_rep_z: 0,
4039            scale_x: 1000,
4040            scale_y: 1000,
4041            scale_z: 1000,
4042            rotate_x: 0,
4043            rotate_y: 0,
4044            rotate_z: 0,
4045            clip_use: 0,
4046            clip_left: 0,
4047            clip_top: 0,
4048            clip_right: 0,
4049            clip_bottom: 0,
4050            src_clip_use: 0,
4051            src_clip_left: 0,
4052            src_clip_top: 0,
4053            src_clip_right: 0,
4054            src_clip_bottom: 0,
4055            alpha: 255,
4056            tr: 255,
4057            mono: 0,
4058            reverse: 0,
4059            bright: 0,
4060            dark: 0,
4061            color_r: 0,
4062            color_g: 0,
4063            color_b: 0,
4064            color_rate: 0,
4065            color_add_r: 0,
4066            color_add_g: 0,
4067            color_add_b: 0,
4068            mask_no: -1,
4069            tonecurve_no: -1,
4070            light_no: -1,
4071            fog_use: 0,
4072            culling: 0,
4073            alpha_test: 1,
4074            alpha_blend: 1,
4075            blend: 0,
4076            child_sort_type: 0,
4077            no_event_hint: false,
4078        }
4079    }
4080}
4081
4082#[derive(Debug, Clone)]
4083pub struct ObjectPropEvents {
4084    pub patno: IntEvent,
4085    pub x: IntEvent,
4086    pub y: IntEvent,
4087    pub z: IntEvent,
4088    pub center_x: IntEvent,
4089    pub center_y: IntEvent,
4090    pub center_z: IntEvent,
4091    pub center_rep_x: IntEvent,
4092    pub center_rep_y: IntEvent,
4093    pub center_rep_z: IntEvent,
4094    pub scale_x: IntEvent,
4095    pub scale_y: IntEvent,
4096    pub scale_z: IntEvent,
4097    pub rotate_x: IntEvent,
4098    pub rotate_y: IntEvent,
4099    pub rotate_z: IntEvent,
4100    pub clip_left: IntEvent,
4101    pub clip_top: IntEvent,
4102    pub clip_right: IntEvent,
4103    pub clip_bottom: IntEvent,
4104    pub src_clip_left: IntEvent,
4105    pub src_clip_top: IntEvent,
4106    pub src_clip_right: IntEvent,
4107    pub src_clip_bottom: IntEvent,
4108    pub tr: IntEvent,
4109    pub mono: IntEvent,
4110    pub reverse: IntEvent,
4111    pub bright: IntEvent,
4112    pub dark: IntEvent,
4113    pub color_r: IntEvent,
4114    pub color_g: IntEvent,
4115    pub color_b: IntEvent,
4116    pub color_rate: IntEvent,
4117    pub color_add_r: IntEvent,
4118    pub color_add_g: IntEvent,
4119    pub color_add_b: IntEvent,
4120}
4121
4122impl Default for ObjectPropEvents {
4123    fn default() -> Self {
4124        Self {
4125            patno: IntEvent::new(0),
4126            x: IntEvent::new(0),
4127            y: IntEvent::new(0),
4128            z: IntEvent::new(0),
4129            center_x: IntEvent::new(0),
4130            center_y: IntEvent::new(0),
4131            center_z: IntEvent::new(0),
4132            center_rep_x: IntEvent::new(0),
4133            center_rep_y: IntEvent::new(0),
4134            center_rep_z: IntEvent::new(0),
4135            scale_x: IntEvent::new(1000),
4136            scale_y: IntEvent::new(1000),
4137            scale_z: IntEvent::new(1000),
4138            rotate_x: IntEvent::new(0),
4139            rotate_y: IntEvent::new(0),
4140            rotate_z: IntEvent::new(0),
4141            clip_left: IntEvent::new(0),
4142            clip_top: IntEvent::new(0),
4143            clip_right: IntEvent::new(0),
4144            clip_bottom: IntEvent::new(0),
4145            src_clip_left: IntEvent::new(0),
4146            src_clip_top: IntEvent::new(0),
4147            src_clip_right: IntEvent::new(0),
4148            src_clip_bottom: IntEvent::new(0),
4149            tr: IntEvent::new(255),
4150            mono: IntEvent::new(0),
4151            reverse: IntEvent::new(0),
4152            bright: IntEvent::new(0),
4153            dark: IntEvent::new(0),
4154            color_r: IntEvent::new(0),
4155            color_g: IntEvent::new(0),
4156            color_b: IntEvent::new(0),
4157            color_rate: IntEvent::new(0),
4158            color_add_r: IntEvent::new(0),
4159            color_add_g: IntEvent::new(0),
4160            color_add_b: IntEvent::new(0),
4161        }
4162    }
4163}
4164
4165impl ObjectPropEvents {
4166    pub fn clear(&mut self) {
4167        self.patno.reinit();
4168        self.x.reinit();
4169        self.y.reinit();
4170        self.z.reinit();
4171        self.center_x.reinit();
4172        self.center_y.reinit();
4173        self.center_z.reinit();
4174        self.center_rep_x.reinit();
4175        self.center_rep_y.reinit();
4176        self.center_rep_z.reinit();
4177        self.scale_x.reinit();
4178        self.scale_y.reinit();
4179        self.scale_z.reinit();
4180        self.rotate_x.reinit();
4181        self.rotate_y.reinit();
4182        self.rotate_z.reinit();
4183        self.clip_left.reinit();
4184        self.clip_top.reinit();
4185        self.clip_right.reinit();
4186        self.clip_bottom.reinit();
4187        self.src_clip_left.reinit();
4188        self.src_clip_top.reinit();
4189        self.src_clip_right.reinit();
4190        self.src_clip_bottom.reinit();
4191        self.tr.reinit();
4192        self.mono.reinit();
4193        self.reverse.reinit();
4194        self.bright.reinit();
4195        self.dark.reinit();
4196        self.color_r.reinit();
4197        self.color_g.reinit();
4198        self.color_b.reinit();
4199        self.color_rate.reinit();
4200        self.color_add_r.reinit();
4201        self.color_add_g.reinit();
4202        self.color_add_b.reinit();
4203    }
4204
4205    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
4206        self.patno.update_time(past_game_time, past_real_time);
4207        self.x.update_time(past_game_time, past_real_time);
4208        self.y.update_time(past_game_time, past_real_time);
4209        self.z.update_time(past_game_time, past_real_time);
4210        self.center_x.update_time(past_game_time, past_real_time);
4211        self.center_y.update_time(past_game_time, past_real_time);
4212        self.center_z.update_time(past_game_time, past_real_time);
4213        self.center_rep_x
4214            .update_time(past_game_time, past_real_time);
4215        self.center_rep_y
4216            .update_time(past_game_time, past_real_time);
4217        self.center_rep_z
4218            .update_time(past_game_time, past_real_time);
4219        self.scale_x.update_time(past_game_time, past_real_time);
4220        self.scale_y.update_time(past_game_time, past_real_time);
4221        self.scale_z.update_time(past_game_time, past_real_time);
4222        self.rotate_x.update_time(past_game_time, past_real_time);
4223        self.rotate_y.update_time(past_game_time, past_real_time);
4224        self.rotate_z.update_time(past_game_time, past_real_time);
4225        self.clip_left.update_time(past_game_time, past_real_time);
4226        self.clip_top.update_time(past_game_time, past_real_time);
4227        self.clip_right.update_time(past_game_time, past_real_time);
4228        self.clip_bottom.update_time(past_game_time, past_real_time);
4229        self.src_clip_left
4230            .update_time(past_game_time, past_real_time);
4231        self.src_clip_top
4232            .update_time(past_game_time, past_real_time);
4233        self.src_clip_right
4234            .update_time(past_game_time, past_real_time);
4235        self.src_clip_bottom
4236            .update_time(past_game_time, past_real_time);
4237        self.tr.update_time(past_game_time, past_real_time);
4238        self.mono.update_time(past_game_time, past_real_time);
4239        self.reverse.update_time(past_game_time, past_real_time);
4240        self.bright.update_time(past_game_time, past_real_time);
4241        self.dark.update_time(past_game_time, past_real_time);
4242        self.color_r.update_time(past_game_time, past_real_time);
4243        self.color_g.update_time(past_game_time, past_real_time);
4244        self.color_b.update_time(past_game_time, past_real_time);
4245        self.color_rate.update_time(past_game_time, past_real_time);
4246        self.color_add_r.update_time(past_game_time, past_real_time);
4247        self.color_add_g.update_time(past_game_time, past_real_time);
4248        self.color_add_b.update_time(past_game_time, past_real_time);
4249    }
4250
4251    pub fn frame(&mut self) {
4252        self.patno.frame();
4253        self.x.frame();
4254        self.y.frame();
4255        self.z.frame();
4256        self.center_x.frame();
4257        self.center_y.frame();
4258        self.center_z.frame();
4259        self.center_rep_x.frame();
4260        self.center_rep_y.frame();
4261        self.center_rep_z.frame();
4262        self.scale_x.frame();
4263        self.scale_y.frame();
4264        self.scale_z.frame();
4265        self.rotate_x.frame();
4266        self.rotate_y.frame();
4267        self.rotate_z.frame();
4268        self.clip_left.frame();
4269        self.clip_top.frame();
4270        self.clip_right.frame();
4271        self.clip_bottom.frame();
4272        self.src_clip_left.frame();
4273        self.src_clip_top.frame();
4274        self.src_clip_right.frame();
4275        self.src_clip_bottom.frame();
4276        self.tr.frame();
4277        self.mono.frame();
4278        self.reverse.frame();
4279        self.bright.frame();
4280        self.dark.frame();
4281        self.color_r.frame();
4282        self.color_g.frame();
4283        self.color_b.frame();
4284        self.color_rate.frame();
4285        self.color_add_r.frame();
4286        self.color_add_g.frame();
4287        self.color_add_b.frame();
4288    }
4289
4290    pub fn tick(&mut self, delta: i32) {
4291        self.update_time(delta, delta);
4292        self.frame();
4293    }
4294
4295    pub fn any_active(&self) -> bool {
4296        self.patno.check_event()
4297            || self.x.check_event()
4298            || self.y.check_event()
4299            || self.z.check_event()
4300            || self.center_x.check_event()
4301            || self.center_y.check_event()
4302            || self.center_z.check_event()
4303            || self.center_rep_x.check_event()
4304            || self.center_rep_y.check_event()
4305            || self.center_rep_z.check_event()
4306            || self.scale_x.check_event()
4307            || self.scale_y.check_event()
4308            || self.scale_z.check_event()
4309            || self.rotate_x.check_event()
4310            || self.rotate_y.check_event()
4311            || self.rotate_z.check_event()
4312            || self.clip_left.check_event()
4313            || self.clip_top.check_event()
4314            || self.clip_right.check_event()
4315            || self.clip_bottom.check_event()
4316            || self.src_clip_left.check_event()
4317            || self.src_clip_top.check_event()
4318            || self.src_clip_right.check_event()
4319            || self.src_clip_bottom.check_event()
4320            || self.tr.check_event()
4321            || self.mono.check_event()
4322            || self.reverse.check_event()
4323            || self.bright.check_event()
4324            || self.dark.check_event()
4325            || self.color_r.check_event()
4326            || self.color_g.check_event()
4327            || self.color_b.check_event()
4328            || self.color_rate.check_event()
4329            || self.color_add_r.check_event()
4330            || self.color_add_g.check_event()
4331            || self.color_add_b.check_event()
4332    }
4333
4334    pub fn end_all(&mut self) {
4335        self.patno.end_event();
4336        self.x.end_event();
4337        self.y.end_event();
4338        self.z.end_event();
4339        self.center_x.end_event();
4340        self.center_y.end_event();
4341        self.center_z.end_event();
4342        self.center_rep_x.end_event();
4343        self.center_rep_y.end_event();
4344        self.center_rep_z.end_event();
4345        self.scale_x.end_event();
4346        self.scale_y.end_event();
4347        self.scale_z.end_event();
4348        self.rotate_x.end_event();
4349        self.rotate_y.end_event();
4350        self.rotate_z.end_event();
4351        self.clip_left.end_event();
4352        self.clip_top.end_event();
4353        self.clip_right.end_event();
4354        self.clip_bottom.end_event();
4355        self.src_clip_left.end_event();
4356        self.src_clip_top.end_event();
4357        self.src_clip_right.end_event();
4358        self.src_clip_bottom.end_event();
4359        self.tr.end_event();
4360        self.mono.end_event();
4361        self.reverse.end_event();
4362        self.bright.end_event();
4363        self.dark.end_event();
4364        self.color_r.end_event();
4365        self.color_g.end_event();
4366        self.color_b.end_event();
4367        self.color_rate.end_event();
4368        self.color_add_r.end_event();
4369        self.color_add_g.end_event();
4370        self.color_add_b.end_event();
4371    }
4372
4373    pub fn get(&self, target: ObjectEventTarget) -> Option<&IntEvent> {
4374        match target {
4375            ObjectEventTarget::Patno => Some(&self.patno),
4376            ObjectEventTarget::X => Some(&self.x),
4377            ObjectEventTarget::Y => Some(&self.y),
4378            ObjectEventTarget::Z => Some(&self.z),
4379            ObjectEventTarget::CenterX => Some(&self.center_x),
4380            ObjectEventTarget::CenterY => Some(&self.center_y),
4381            ObjectEventTarget::CenterZ => Some(&self.center_z),
4382            ObjectEventTarget::CenterRepX => Some(&self.center_rep_x),
4383            ObjectEventTarget::CenterRepY => Some(&self.center_rep_y),
4384            ObjectEventTarget::CenterRepZ => Some(&self.center_rep_z),
4385            ObjectEventTarget::ScaleX => Some(&self.scale_x),
4386            ObjectEventTarget::ScaleY => Some(&self.scale_y),
4387            ObjectEventTarget::ScaleZ => Some(&self.scale_z),
4388            ObjectEventTarget::RotateX => Some(&self.rotate_x),
4389            ObjectEventTarget::RotateY => Some(&self.rotate_y),
4390            ObjectEventTarget::RotateZ => Some(&self.rotate_z),
4391            ObjectEventTarget::ClipLeft => Some(&self.clip_left),
4392            ObjectEventTarget::ClipTop => Some(&self.clip_top),
4393            ObjectEventTarget::ClipRight => Some(&self.clip_right),
4394            ObjectEventTarget::ClipBottom => Some(&self.clip_bottom),
4395            ObjectEventTarget::SrcClipLeft => Some(&self.src_clip_left),
4396            ObjectEventTarget::SrcClipTop => Some(&self.src_clip_top),
4397            ObjectEventTarget::SrcClipRight => Some(&self.src_clip_right),
4398            ObjectEventTarget::SrcClipBottom => Some(&self.src_clip_bottom),
4399            ObjectEventTarget::Tr => Some(&self.tr),
4400            ObjectEventTarget::Mono => Some(&self.mono),
4401            ObjectEventTarget::Reverse => Some(&self.reverse),
4402            ObjectEventTarget::Bright => Some(&self.bright),
4403            ObjectEventTarget::Dark => Some(&self.dark),
4404            ObjectEventTarget::ColorR => Some(&self.color_r),
4405            ObjectEventTarget::ColorG => Some(&self.color_g),
4406            ObjectEventTarget::ColorB => Some(&self.color_b),
4407            ObjectEventTarget::ColorRate => Some(&self.color_rate),
4408            ObjectEventTarget::ColorAddR => Some(&self.color_add_r),
4409            ObjectEventTarget::ColorAddG => Some(&self.color_add_g),
4410            ObjectEventTarget::ColorAddB => Some(&self.color_add_b),
4411            ObjectEventTarget::XRep
4412            | ObjectEventTarget::YRep
4413            | ObjectEventTarget::ZRep
4414            | ObjectEventTarget::TrRep
4415            | ObjectEventTarget::Alpha
4416            | ObjectEventTarget::Order
4417            | ObjectEventTarget::Layer
4418            | ObjectEventTarget::Unknown => None,
4419        }
4420    }
4421
4422    pub fn get_mut(&mut self, target: ObjectEventTarget) -> Option<&mut IntEvent> {
4423        match target {
4424            ObjectEventTarget::Patno => Some(&mut self.patno),
4425            ObjectEventTarget::X => Some(&mut self.x),
4426            ObjectEventTarget::Y => Some(&mut self.y),
4427            ObjectEventTarget::Z => Some(&mut self.z),
4428            ObjectEventTarget::CenterX => Some(&mut self.center_x),
4429            ObjectEventTarget::CenterY => Some(&mut self.center_y),
4430            ObjectEventTarget::CenterZ => Some(&mut self.center_z),
4431            ObjectEventTarget::CenterRepX => Some(&mut self.center_rep_x),
4432            ObjectEventTarget::CenterRepY => Some(&mut self.center_rep_y),
4433            ObjectEventTarget::CenterRepZ => Some(&mut self.center_rep_z),
4434            ObjectEventTarget::ScaleX => Some(&mut self.scale_x),
4435            ObjectEventTarget::ScaleY => Some(&mut self.scale_y),
4436            ObjectEventTarget::ScaleZ => Some(&mut self.scale_z),
4437            ObjectEventTarget::RotateX => Some(&mut self.rotate_x),
4438            ObjectEventTarget::RotateY => Some(&mut self.rotate_y),
4439            ObjectEventTarget::RotateZ => Some(&mut self.rotate_z),
4440            ObjectEventTarget::ClipLeft => Some(&mut self.clip_left),
4441            ObjectEventTarget::ClipTop => Some(&mut self.clip_top),
4442            ObjectEventTarget::ClipRight => Some(&mut self.clip_right),
4443            ObjectEventTarget::ClipBottom => Some(&mut self.clip_bottom),
4444            ObjectEventTarget::SrcClipLeft => Some(&mut self.src_clip_left),
4445            ObjectEventTarget::SrcClipTop => Some(&mut self.src_clip_top),
4446            ObjectEventTarget::SrcClipRight => Some(&mut self.src_clip_right),
4447            ObjectEventTarget::SrcClipBottom => Some(&mut self.src_clip_bottom),
4448            ObjectEventTarget::Tr => Some(&mut self.tr),
4449            ObjectEventTarget::Mono => Some(&mut self.mono),
4450            ObjectEventTarget::Reverse => Some(&mut self.reverse),
4451            ObjectEventTarget::Bright => Some(&mut self.bright),
4452            ObjectEventTarget::Dark => Some(&mut self.dark),
4453            ObjectEventTarget::ColorR => Some(&mut self.color_r),
4454            ObjectEventTarget::ColorG => Some(&mut self.color_g),
4455            ObjectEventTarget::ColorB => Some(&mut self.color_b),
4456            ObjectEventTarget::ColorRate => Some(&mut self.color_rate),
4457            ObjectEventTarget::ColorAddR => Some(&mut self.color_add_r),
4458            ObjectEventTarget::ColorAddG => Some(&mut self.color_add_g),
4459            ObjectEventTarget::ColorAddB => Some(&mut self.color_add_b),
4460            ObjectEventTarget::XRep
4461            | ObjectEventTarget::YRep
4462            | ObjectEventTarget::ZRep
4463            | ObjectEventTarget::TrRep
4464            | ObjectEventTarget::Alpha
4465            | ObjectEventTarget::Order
4466            | ObjectEventTarget::Layer
4467            | ObjectEventTarget::Unknown => None,
4468        }
4469    }
4470}
4471
4472#[derive(Debug, Default, Clone)]
4473pub struct ObjectPropEventLists {
4474    pub x_rep: Vec<IntEvent>,
4475    pub y_rep: Vec<IntEvent>,
4476    pub z_rep: Vec<IntEvent>,
4477    pub tr_rep: Vec<IntEvent>,
4478}
4479
4480impl ObjectPropEventLists {
4481    pub fn clear(&mut self) {
4482        self.x_rep.clear();
4483        self.y_rep.clear();
4484        self.z_rep.clear();
4485        self.tr_rep.clear();
4486    }
4487
4488    pub fn update_time(&mut self, past_game_time: i32, past_real_time: i32) {
4489        for ev in &mut self.x_rep {
4490            ev.update_time(past_game_time, past_real_time);
4491        }
4492        for ev in &mut self.y_rep {
4493            ev.update_time(past_game_time, past_real_time);
4494        }
4495        for ev in &mut self.z_rep {
4496            ev.update_time(past_game_time, past_real_time);
4497        }
4498        for ev in &mut self.tr_rep {
4499            ev.update_time(past_game_time, past_real_time);
4500        }
4501    }
4502
4503    pub fn frame(&mut self) {
4504        for ev in &mut self.x_rep {
4505            ev.frame();
4506        }
4507        for ev in &mut self.y_rep {
4508            ev.frame();
4509        }
4510        for ev in &mut self.z_rep {
4511            ev.frame();
4512        }
4513        for ev in &mut self.tr_rep {
4514            ev.frame();
4515        }
4516    }
4517
4518    pub fn tick(&mut self, delta: i32) {
4519        self.update_time(delta, delta);
4520        self.frame();
4521    }
4522
4523    pub fn any_active(&self) -> bool {
4524        self.x_rep.iter().any(|e| e.check_event())
4525            || self.y_rep.iter().any(|e| e.check_event())
4526            || self.z_rep.iter().any(|e| e.check_event())
4527            || self.tr_rep.iter().any(|e| e.check_event())
4528    }
4529
4530    pub fn end_all(&mut self) {
4531        for ev in &mut self.x_rep {
4532            ev.end_event();
4533        }
4534        for ev in &mut self.y_rep {
4535            ev.end_event();
4536        }
4537        for ev in &mut self.z_rep {
4538            ev.end_event();
4539        }
4540        for ev in &mut self.tr_rep {
4541            ev.end_event();
4542        }
4543    }
4544}
4545
4546#[derive(Debug, Clone, Default)]
4547pub struct ObjectPropLists {
4548    pub x_rep: Vec<i64>,
4549    pub y_rep: Vec<i64>,
4550    pub z_rep: Vec<i64>,
4551    pub tr_rep: Vec<i64>,
4552    pub f: Vec<i64>,
4553}
4554
4555impl ObjectPropLists {
4556    pub fn clear(&mut self) {
4557        self.x_rep.clear();
4558        self.y_rep.clear();
4559        self.z_rep.clear();
4560        self.tr_rep.clear();
4561        // Original C++ initializes OBJECT.F as an extendable INTLIST with
4562        // default size 0 and calls m_op.f.reinit() from C_elm_object::init_param().
4563        // Reinitializing therefore restores size 0 rather than preserving the
4564        // previous allocation and only zeroing its contents.
4565        self.f.clear();
4566    }
4567}
4568
4569#[cfg(test)]
4570mod object_prop_lists_tests {
4571    use super::ObjectPropLists;
4572
4573    #[test]
4574    fn clear_restores_object_f_default_size_zero() {
4575        let mut lists = ObjectPropLists {
4576            x_rep: vec![1, 2],
4577            y_rep: vec![3],
4578            z_rep: vec![4],
4579            tr_rep: vec![5],
4580            f: vec![10, 20, 30, 40, 50, 60, 70],
4581        };
4582
4583        lists.clear();
4584
4585        assert!(lists.x_rep.is_empty());
4586        assert!(lists.y_rep.is_empty());
4587        assert!(lists.z_rep.is_empty());
4588        assert!(lists.tr_rep.is_empty());
4589        assert!(lists.f.is_empty());
4590    }
4591}
4592
4593#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
4594pub struct ObjectRectParam {
4595    pub left: i64,
4596    pub top: i64,
4597    pub right: i64,
4598    pub bottom: i64,
4599    /// Packed C++ `C_argb` value: 0xAARRGGBB.
4600    pub color_argb: i64,
4601}
4602
4603#[derive(Debug, Default, Clone)]
4604pub struct ObjectRuntimeState {
4605    /// NUMBER digit offsets relative to the object; None marks unused slots.
4606    pub number_sprite_offsets: Vec<Option<i32>>,
4607    pub explicit_int_props: HashSet<i32>,
4608    pub explicit_str_props: HashSet<i32>,
4609    pub prop_events: ObjectPropEvents,
4610    pub prop_event_lists: ObjectPropEventLists,
4611    pub prop_lists: ObjectPropLists,
4612    pub child_objects: Vec<ObjectState>,
4613}
4614
4615#[derive(Debug, Default, Clone)]
4616pub struct ObjectState {
4617    /// Port-internal runtime-content marker.
4618    ///
4619    /// This is NOT C++ `C_elm_object::m_op_def.use_flag`.  The original
4620    /// `use_flag` is immutable for the lifetime of an object-list slot and is
4621    /// stored by this port in `StageFormState::object_slot_use` for top-level
4622    /// STAGE objects.  Do not use this field to emulate `C_elm_object::is_use()`.
4623    pub used: bool,
4624    pub backend: ObjectBackend,
4625    pub file_name: Option<String>,
4626    pub string_value: Option<String>,
4627
4628    /// TNM_OBJECT_TYPE_* (0=none, 1=rect, 2=pct, 3=string, 4=weather, 5=number, ...).
4629    pub object_type: i64,
4630
4631    /// For RECT objects.
4632    pub rect_param: ObjectRectParam,
4633
4634    /// For NUMBER objects, stores the current number value.
4635    pub number_value: i64,
4636
4637    /// For STRING objects.
4638    pub string_param: ObjectStringParam,
4639
4640    /// For NUMBER objects.
4641    pub number_param: ObjectNumberParam,
4642
4643    /// For WEATHER objects (type A/B).
4644    pub weather_param: ObjectWeatherParam,
4645    pub weather_work: ObjectWeatherWorkState,
4646
4647    /// For SAVE_THUMB / THUMB objects.
4648    pub thumb_save_no: i64,
4649
4650    /// For MOVIE objects.
4651    pub movie: ObjectMovieState,
4652
4653    /// For E-mote objects.
4654    pub emote: ObjectEmoteParam,
4655
4656    /// Last loaded GAN file.
4657    pub gan_file: Option<String>,
4658    /// GAN runtime state.
4659    pub gan: GanState,
4660
4661    /// OBJECT.FRAME_ACTION state.
4662    pub frame_action: ObjectFrameActionState,
4663    /// OBJECT.FRAME_ACTION_CH state.
4664    pub frame_action_ch: Vec<ObjectFrameActionState>,
4665
4666    pub base: ObjectBaseState,
4667
4668    pub button: ObjectButtonState,
4669
4670    pub runtime: ObjectRuntimeState,
4671
4672    pub mesh_animation_state: crate::mesh3d::MeshAnimationState,
4673    /// Optional backend-only slot override for top-level objects whose logical
4674    /// list ownership is not represented by GfxRuntime (currently EXCALL).
4675    /// This is deliberately separate from nested_runtime_slot: many VM/render
4676    /// paths use nested_runtime_slot as the semantic marker for CHILD/MWND tree
4677    /// objects.
4678    pub backend_runtime_slot: Option<usize>,
4679    pub nested_runtime_slot: Option<usize>,
4680}
4681
4682fn normalize_object_int_prop(
4683    ids: &crate::runtime::constants::RuntimeConstants,
4684    op: i32,
4685    value: i64,
4686) -> i64 {
4687    let byte_range = [
4688        ids.obj_tr,
4689        ids.obj_mono,
4690        ids.obj_reverse,
4691        ids.obj_bright,
4692        ids.obj_dark,
4693        ids.obj_color_r,
4694        ids.obj_color_g,
4695        ids.obj_color_b,
4696        ids.obj_color_rate,
4697        ids.obj_color_add_r,
4698        ids.obj_color_add_g,
4699        ids.obj_color_add_b,
4700    ];
4701    if byte_range.iter().any(|&id| id != 0 && op == id) {
4702        return value.clamp(0, 255);
4703    }
4704
4705    if op == ids.obj_disp {
4706        return if value != 0 { 1 } else { 0 };
4707    }
4708
4709    let boolean = [
4710        ids.obj_clip_use,
4711        ids.obj_src_clip_use,
4712        ids.obj_fog_use,
4713        ids.obj_culling,
4714        ids.obj_alpha_test,
4715        ids.obj_alpha_blend,
4716        ids.obj_wipe_copy,
4717        ids.obj_wipe_erase,
4718        ids.obj_click_disable,
4719    ];
4720    if boolean.iter().any(|&id| id != 0 && op == id) {
4721        return if value != 0 { 1 } else { 0 };
4722    }
4723
4724    value
4725}
4726
4727impl ObjectState {
4728    /// Mirrors C_elm_object::copy for the type-specific Emote resources across
4729    /// the complete CHILD tree. Each copied Emote object receives a cloned
4730    /// player and a fresh render-target identity before renderer backends are
4731    /// rebuilt for the destination tree.
4732    pub fn clone_emote_players_for_object_tree(&mut self) {
4733        if self.object_type == 12 {
4734            self.emote.clone_player_for_object();
4735        }
4736        for child in &mut self.runtime.child_objects {
4737            child.clone_emote_players_for_object_tree();
4738        }
4739    }
4740
4741    pub fn contains_emote_in_object_tree(&self) -> bool {
4742        self.object_type == 12
4743            || self
4744                .runtime
4745                .child_objects
4746                .iter()
4747                .any(ObjectState::contains_emote_in_object_tree)
4748    }
4749
4750    fn sync_event_backed_prop_value(
4751        &mut self,
4752        ids: &crate::runtime::constants::RuntimeConstants,
4753        op: i32,
4754        value: i64,
4755    ) {
4756        let target = ids.object_op_tables().prop_target(op);
4757
4758        let Some(ev) = self.runtime.prop_events.get_mut(target) else {
4759            return;
4760        };
4761        ev.set_value(value as i32);
4762        if !ev.check_event() {
4763            ev.cur_value = value as i32;
4764        }
4765    }
4766
4767    /// Reset type-specific parameters (mirrors C_elm_object::init_type(true)).
4768    ///
4769    /// Important: this does NOT clear button/groups/events (those are part of init_param/reinit in the original implementation).
4770    pub fn init_type_like(&mut self) {
4771        self.backend = ObjectBackend::None;
4772        self.file_name = None;
4773        self.string_value = None;
4774        self.object_type = 0;
4775
4776        self.rect_param = ObjectRectParam::default();
4777        self.number_value = 0;
4778        self.string_param = ObjectStringParam::default();
4779        self.number_param = ObjectNumberParam::default();
4780        self.weather_param = ObjectWeatherParam::default();
4781        self.weather_work = ObjectWeatherWorkState::default();
4782        self.thumb_save_no = -1;
4783
4784        self.movie.reset();
4785        self.emote = ObjectEmoteParam::default();
4786
4787        self.gan_file = None;
4788        self.gan.reset();
4789        self.mesh_animation_state = crate::mesh3d::MeshAnimationState::default();
4790    }
4791
4792    fn weather_rand_percent(&mut self, base: i64, span: i64) -> i64 {
4793        base + self.weather_work.rand_mod(span)
4794    }
4795
4796    fn setup_weather_sub(&mut self, idx: usize, init_state: i64, screen_w: i64, screen_h: i64) {
4797        if idx >= self.weather_work.sub.len() {
4798            return;
4799        }
4800        let param = self.weather_param.clone();
4801        let screen_w = screen_w.max(1);
4802        let screen_h = screen_h.max(1);
4803        let mut sub = ObjectWeatherWorkSub {
4804            state: init_state,
4805            ..Default::default()
4806        };
4807
4808        if param.weather_type == 1 {
4809            sub.move_start_pos_x = self.weather_work.rand_mod(screen_w);
4810            sub.move_start_pos_y = self.weather_work.rand_mod(screen_h);
4811            sub.move_time_x = param.move_time_x * self.weather_rand_percent(90, 20) / 100;
4812            sub.move_time_y = param.move_time_y * self.weather_rand_percent(90, 20) / 100;
4813            sub.move_cur_time = self.weather_work.next_rand();
4814            sub.sin_time_x = param.sin_time_x * self.weather_rand_percent(90, 20) / 100;
4815            sub.sin_time_y = param.sin_time_y * self.weather_rand_percent(90, 20) / 100;
4816            sub.sin_power_x = param.sin_power_x * self.weather_rand_percent(90, 20) / 100;
4817            sub.sin_power_y = param.sin_power_y * self.weather_rand_percent(90, 20) / 100;
4818            sub.sin_cur_time = self.weather_work.next_rand();
4819            sub.scale_x = param.scale_x;
4820            sub.scale_y = param.scale_y;
4821            sub.active_time_len = param.active_time * self.weather_rand_percent(80, 40) / 100;
4822            sub.state_time_len = if init_state == 0 {
4823                self.weather_work.rand_mod(3000)
4824            } else {
4825                sub.active_time_len
4826            };
4827            sub.real_time_flag = param.real_time_flag;
4828        } else if param.weather_type == 2 {
4829            let max_distance_x = if param.center_x > screen_w / 2 {
4830                param.center_x
4831            } else {
4832                screen_w - param.center_x
4833            };
4834            let max_distance_y = if param.center_y > screen_h / 2 {
4835                param.center_y
4836            } else {
4837                screen_h - param.center_y
4838            };
4839            let max_distance =
4840                (((max_distance_x * max_distance_x + max_distance_y * max_distance_y) as f64)
4841                    .sqrt()) as i64;
4842            sub.move_start_distance =
4843                self.weather_work.rand_mod(max_distance.max(1)) * param.appear_range / 100;
4844            sub.move_start_degree = self.weather_work.rand_mod(3600);
4845            sub.move_time_x = param.move_time_x.abs() * self.weather_rand_percent(80, 40) / 100;
4846            sub.move_time_y = param.move_time_y.abs() * self.weather_rand_percent(80, 40) / 100;
4847            sub.sin_time_x = param.sin_time_x * self.weather_rand_percent(90, 20) / 100;
4848            sub.sin_time_y = param.sin_time_y * self.weather_rand_percent(90, 20) / 100;
4849            sub.sin_power_x = param.sin_power_x * self.weather_rand_percent(90, 20) / 100;
4850            sub.sin_power_y = param.sin_power_y * self.weather_rand_percent(90, 20) / 100;
4851            sub.sin_cur_time = self.weather_work.next_rand();
4852            sub.center_rotate = param.center_rotate * self.weather_rand_percent(90, 20) / 100;
4853            sub.zoom_min = param.zoom_min;
4854            sub.zoom_max = param.zoom_max;
4855            sub.scale_x = param.scale_x;
4856            sub.scale_y = param.scale_y;
4857            sub.active_time_len = param.move_time_x.abs() * max_distance / 1000 - 1000;
4858            sub.state_time_len = if init_state == 0 {
4859                self.weather_work
4860                    .rand_mod((1000 + sub.active_time_len + 1000).max(1))
4861            } else {
4862                sub.active_time_len
4863            };
4864            sub.move_cur_time = if sub.state_time_len == 0 {
4865                0
4866            } else {
4867                self.weather_work.rand_mod(sub.state_time_len.abs().max(1))
4868            };
4869            sub.real_time_flag = param.real_time_flag;
4870        }
4871        sub.restruct_flag = false;
4872        self.weather_work.sub[idx] = sub;
4873    }
4874
4875    pub fn weather_sprite_count(&self) -> usize {
4876        match self.weather_param.weather_type {
4877            1 => self.weather_work.cnt_max.saturating_mul(4),
4878            2 => self.weather_work.cnt_max,
4879            _ => 0,
4880        }
4881    }
4882
4883    pub fn restruct_weather_work(&mut self, screen_w: i64, screen_h: i64) {
4884        if self.object_type != 4 {
4885            return;
4886        }
4887        for sub in &mut self.weather_work.sub {
4888            sub.restruct_flag = true;
4889        }
4890        let cnt = self.weather_param.cnt.max(0) as usize;
4891        let old_cnt = self.weather_work.sub.len();
4892        if cnt > old_cnt {
4893            self.weather_work
4894                .sub
4895                .resize_with(cnt, ObjectWeatherWorkSub::default);
4896            for idx in old_cnt..cnt {
4897                self.setup_weather_sub(idx, 0, screen_w, screen_h);
4898            }
4899        }
4900        if cnt > self.weather_work.cnt_max {
4901            self.weather_work.cnt_max = cnt;
4902        }
4903    }
4904
4905    pub fn init_param_like(&mut self) {
4906        self.base = ObjectBaseState::default();
4907        self.button.clear();
4908        self.runtime.explicit_int_props.clear();
4909        self.runtime.explicit_str_props.clear();
4910        self.runtime.prop_events.clear();
4911        self.runtime.prop_lists.clear();
4912        self.runtime.prop_event_lists.clear();
4913        self.frame_action = ObjectFrameActionState::default();
4914        self.frame_action_ch.clear();
4915        self.gan_file = None;
4916        self.gan.reset();
4917    }
4918
4919    pub fn clear_runtime_only(&mut self) {
4920        self.runtime.explicit_int_props.clear();
4921        self.runtime.explicit_str_props.clear();
4922        self.runtime.prop_events.clear();
4923        self.runtime.prop_lists.clear();
4924        self.runtime.prop_event_lists.clear();
4925        self.frame_action = ObjectFrameActionState::default();
4926        self.frame_action_ch.clear();
4927    }
4928
4929    pub fn set_int_prop(
4930        &mut self,
4931        ids: &crate::runtime::constants::RuntimeConstants,
4932        op: i32,
4933        value: i64,
4934    ) {
4935        let value = normalize_object_int_prop(ids, op, value);
4936        self.runtime.explicit_int_props.insert(op);
4937        let ok =
4938            self.sync_fixed_int_prop(ids, op, value) || self.sync_special_int_prop(ids, op, value);
4939        assert!(ok, "unknown object int property op {}", op);
4940    }
4941
4942    pub fn set_int_prop_from_event_frame(
4943        &mut self,
4944        ids: &crate::runtime::constants::RuntimeConstants,
4945        op: i32,
4946        value: i64,
4947    ) {
4948        self.runtime.explicit_int_props.insert(op);
4949        macro_rules! set_if {
4950            ($id:expr, $field:ident) => {
4951                if $id != 0 && op == $id {
4952                    self.base.$field = value;
4953                    return;
4954                }
4955            };
4956        }
4957        set_if!(ids.obj_patno, patno);
4958        set_if!(ids.obj_x, x);
4959        set_if!(ids.obj_y, y);
4960        set_if!(ids.obj_z, z);
4961        set_if!(ids.obj_center_x, center_x);
4962        set_if!(ids.obj_center_y, center_y);
4963        set_if!(ids.obj_center_z, center_z);
4964        set_if!(ids.obj_center_rep_x, center_rep_x);
4965        set_if!(ids.obj_center_rep_y, center_rep_y);
4966        set_if!(ids.obj_center_rep_z, center_rep_z);
4967        set_if!(ids.obj_scale_x, scale_x);
4968        set_if!(ids.obj_scale_y, scale_y);
4969        set_if!(ids.obj_scale_z, scale_z);
4970        set_if!(ids.obj_rotate_x, rotate_x);
4971        set_if!(ids.obj_rotate_y, rotate_y);
4972        set_if!(ids.obj_rotate_z, rotate_z);
4973        set_if!(ids.obj_clip_left, clip_left);
4974        set_if!(ids.obj_clip_top, clip_top);
4975        set_if!(ids.obj_clip_right, clip_right);
4976        set_if!(ids.obj_clip_bottom, clip_bottom);
4977        set_if!(ids.obj_src_clip_left, src_clip_left);
4978        set_if!(ids.obj_src_clip_top, src_clip_top);
4979        set_if!(ids.obj_src_clip_right, src_clip_right);
4980        set_if!(ids.obj_src_clip_bottom, src_clip_bottom);
4981        set_if!(ids.obj_alpha, alpha);
4982        set_if!(ids.obj_tr, tr);
4983        set_if!(ids.obj_mono, mono);
4984        set_if!(ids.obj_reverse, reverse);
4985        set_if!(ids.obj_bright, bright);
4986        set_if!(ids.obj_dark, dark);
4987        set_if!(ids.obj_color_r, color_r);
4988        set_if!(ids.obj_color_g, color_g);
4989        set_if!(ids.obj_color_b, color_b);
4990        set_if!(ids.obj_color_rate, color_rate);
4991        set_if!(ids.obj_color_add_r, color_add_r);
4992        set_if!(ids.obj_color_add_g, color_add_g);
4993        set_if!(ids.obj_color_add_b, color_add_b);
4994        panic!("unknown object event-backed int property op {}", op);
4995    }
4996
4997    pub fn has_int_prop(&self, op: i32) -> bool {
4998        self.runtime.explicit_int_props.contains(&op)
4999    }
5000
5001    pub fn remove_int_prop(&mut self, op: i32) {
5002        self.runtime.explicit_int_props.remove(&op);
5003    }
5004
5005    pub fn set_str_prop(
5006        &mut self,
5007        ids: &crate::runtime::constants::RuntimeConstants,
5008        op: i32,
5009        value: String,
5010    ) {
5011        self.runtime.explicit_str_props.insert(op);
5012        let ok = self.sync_special_str_prop(ids, op, value);
5013        assert!(ok, "unknown object string property op {}", op);
5014    }
5015
5016    pub fn lookup_str_prop(
5017        &self,
5018        ids: &crate::runtime::constants::RuntimeConstants,
5019        op: i32,
5020    ) -> Option<String> {
5021        self.special_str_prop(ids, op)
5022    }
5023
5024    pub fn has_str_prop(&self, op: i32) -> bool {
5025        self.runtime.explicit_str_props.contains(&op)
5026    }
5027
5028    pub fn remove_str_prop(&mut self, ids: &crate::runtime::constants::RuntimeConstants, op: i32) {
5029        self.runtime.explicit_str_props.remove(&op);
5030        if ids.obj_mesh_anim_clip_name != 0 && op == ids.obj_mesh_anim_clip_name {
5031            let mut next = self.mesh_animation_state.clone();
5032            next.clip_name = None;
5033            self.set_mesh_animation_state(next);
5034        } else if ids.obj_mesh_anim_blend_clip_name != 0 && op == ids.obj_mesh_anim_blend_clip_name
5035        {
5036            let mut next = self.mesh_animation_state.clone();
5037            next.blend_clip_name = None;
5038            self.set_mesh_animation_state(next);
5039        }
5040    }
5041
5042    pub fn lookup_int_prop(
5043        &self,
5044        ids: &crate::runtime::constants::RuntimeConstants,
5045        op: i32,
5046    ) -> Option<i64> {
5047        self.fixed_int_prop(ids, op)
5048            .or_else(|| self.special_int_prop(ids, op))
5049    }
5050
5051    pub fn get_int_prop(&self, ids: &crate::runtime::constants::RuntimeConstants, op: i32) -> i64 {
5052        self.lookup_int_prop(ids, op).unwrap_or(0)
5053    }
5054
5055    pub fn runtime_slot_or(&self, fallback: usize) -> usize {
5056        self.nested_runtime_slot
5057            .or(self.backend_runtime_slot)
5058            .unwrap_or(fallback)
5059    }
5060
5061    pub fn ensure_runtime_slot(&mut self, next_slot: &mut usize) -> usize {
5062        if let Some(slot) = self.nested_runtime_slot {
5063            return slot;
5064        }
5065        let slot = *next_slot;
5066        *next_slot += 1;
5067        self.nested_runtime_slot = Some(slot);
5068        slot
5069    }
5070
5071    pub fn int_list_by_op<'a>(
5072        &'a self,
5073        ids: &crate::runtime::constants::RuntimeConstants,
5074        op: i32,
5075    ) -> Option<&'a Vec<i64>> {
5076        if ids.obj_f != 0 && op == ids.obj_f {
5077            Some(&self.runtime.prop_lists.f)
5078        } else {
5079            None
5080        }
5081    }
5082
5083    pub fn int_list_by_op_mut<'a>(
5084        &'a mut self,
5085        ids: &crate::runtime::constants::RuntimeConstants,
5086        op: i32,
5087    ) -> Option<&'a mut Vec<i64>> {
5088        if ids.obj_f != 0 && op == ids.obj_f {
5089            Some(&mut self.runtime.prop_lists.f)
5090        } else {
5091            None
5092        }
5093    }
5094
5095    pub fn rep_int_event_list_by_rep_op<'a>(
5096        &'a self,
5097        ids: &crate::runtime::constants::RuntimeConstants,
5098        op: i32,
5099    ) -> Option<&'a Vec<IntEvent>> {
5100        if ids.obj_x_rep != 0 && op == ids.obj_x_rep {
5101            Some(&self.runtime.prop_event_lists.x_rep)
5102        } else if ids.obj_y_rep != 0 && op == ids.obj_y_rep {
5103            Some(&self.runtime.prop_event_lists.y_rep)
5104        } else if ids.obj_z_rep != 0 && op == ids.obj_z_rep {
5105            Some(&self.runtime.prop_event_lists.z_rep)
5106        } else if ids.obj_tr_rep != 0 && op == ids.obj_tr_rep {
5107            Some(&self.runtime.prop_event_lists.tr_rep)
5108        } else {
5109            None
5110        }
5111    }
5112
5113    pub fn rep_int_event_list_by_rep_op_mut<'a>(
5114        &'a mut self,
5115        ids: &crate::runtime::constants::RuntimeConstants,
5116        op: i32,
5117    ) -> Option<&'a mut Vec<IntEvent>> {
5118        if ids.obj_x_rep != 0 && op == ids.obj_x_rep {
5119            Some(&mut self.runtime.prop_event_lists.x_rep)
5120        } else if ids.obj_y_rep != 0 && op == ids.obj_y_rep {
5121            Some(&mut self.runtime.prop_event_lists.y_rep)
5122        } else if ids.obj_z_rep != 0 && op == ids.obj_z_rep {
5123            Some(&mut self.runtime.prop_event_lists.z_rep)
5124        } else if ids.obj_tr_rep != 0 && op == ids.obj_tr_rep {
5125            Some(&mut self.runtime.prop_event_lists.tr_rep)
5126        } else {
5127            None
5128        }
5129    }
5130
5131    pub fn int_event_by_op<'a>(
5132        &'a self,
5133        ids: &crate::runtime::constants::RuntimeConstants,
5134        op: i32,
5135    ) -> Option<&'a IntEvent> {
5136        self.runtime.prop_events.get(self.event_target(ids, op))
5137    }
5138
5139    pub fn int_event_by_op_mut<'a>(
5140        &'a mut self,
5141        ids: &crate::runtime::constants::RuntimeConstants,
5142        op: i32,
5143    ) -> Option<&'a mut IntEvent> {
5144        let target = self.event_target(ids, op);
5145        self.runtime.prop_events.get_mut(target)
5146    }
5147
5148    pub fn int_event_list_by_op<'a>(
5149        &'a self,
5150        ids: &crate::runtime::constants::RuntimeConstants,
5151        op: i32,
5152    ) -> Option<&'a Vec<IntEvent>> {
5153        if ids.obj_x_rep_eve != 0 && op == ids.obj_x_rep_eve {
5154            Some(&self.runtime.prop_event_lists.x_rep)
5155        } else if ids.obj_y_rep_eve != 0 && op == ids.obj_y_rep_eve {
5156            Some(&self.runtime.prop_event_lists.y_rep)
5157        } else if ids.obj_z_rep_eve != 0 && op == ids.obj_z_rep_eve {
5158            Some(&self.runtime.prop_event_lists.z_rep)
5159        } else if ids.obj_tr_rep_eve != 0 && op == ids.obj_tr_rep_eve {
5160            Some(&self.runtime.prop_event_lists.tr_rep)
5161        } else {
5162            None
5163        }
5164    }
5165
5166    pub fn int_event_list_by_op_mut<'a>(
5167        &'a mut self,
5168        ids: &crate::runtime::constants::RuntimeConstants,
5169        op: i32,
5170    ) -> Option<&'a mut Vec<IntEvent>> {
5171        if ids.obj_x_rep_eve != 0 && op == ids.obj_x_rep_eve {
5172            Some(&mut self.runtime.prop_event_lists.x_rep)
5173        } else if ids.obj_y_rep_eve != 0 && op == ids.obj_y_rep_eve {
5174            Some(&mut self.runtime.prop_event_lists.y_rep)
5175        } else if ids.obj_z_rep_eve != 0 && op == ids.obj_z_rep_eve {
5176            Some(&mut self.runtime.prop_event_lists.z_rep)
5177        } else if ids.obj_tr_rep_eve != 0 && op == ids.obj_tr_rep_eve {
5178            Some(&mut self.runtime.prop_event_lists.tr_rep)
5179        } else {
5180            None
5181        }
5182    }
5183
5184    fn sync_fixed_int_prop(
5185        &mut self,
5186        ids: &crate::runtime::constants::RuntimeConstants,
5187        op: i32,
5188        value: i64,
5189    ) -> bool {
5190        macro_rules! set_if {
5191            ($id:expr, $field:ident) => {
5192                if $id != 0 && op == $id {
5193                    self.base.$field = value;
5194                    self.sync_event_backed_prop_value(ids, op, value);
5195                    return true;
5196                }
5197            };
5198        }
5199        if op == ids.obj_disp {
5200            self.base.disp = value;
5201            return true;
5202        }
5203        set_if!(ids.obj_wipe_copy, wipe_copy);
5204        set_if!(ids.obj_wipe_erase, wipe_erase);
5205        set_if!(ids.obj_click_disable, click_disable);
5206        set_if!(ids.obj_patno, patno);
5207        set_if!(ids.obj_world, world);
5208        set_if!(ids.obj_order, order);
5209        set_if!(ids.obj_layer, layer);
5210        set_if!(ids.obj_x, x);
5211        set_if!(ids.obj_y, y);
5212        set_if!(ids.obj_z, z);
5213        set_if!(ids.obj_center_x, center_x);
5214        set_if!(ids.obj_center_y, center_y);
5215        set_if!(ids.obj_center_z, center_z);
5216        set_if!(ids.obj_center_rep_x, center_rep_x);
5217        set_if!(ids.obj_center_rep_y, center_rep_y);
5218        set_if!(ids.obj_center_rep_z, center_rep_z);
5219        set_if!(ids.obj_scale_x, scale_x);
5220        set_if!(ids.obj_scale_y, scale_y);
5221        set_if!(ids.obj_scale_z, scale_z);
5222        set_if!(ids.obj_rotate_x, rotate_x);
5223        set_if!(ids.obj_rotate_y, rotate_y);
5224        set_if!(ids.obj_rotate_z, rotate_z);
5225        set_if!(ids.obj_clip_use, clip_use);
5226        set_if!(ids.obj_clip_left, clip_left);
5227        set_if!(ids.obj_clip_top, clip_top);
5228        set_if!(ids.obj_clip_right, clip_right);
5229        set_if!(ids.obj_clip_bottom, clip_bottom);
5230        set_if!(ids.obj_src_clip_use, src_clip_use);
5231        set_if!(ids.obj_src_clip_left, src_clip_left);
5232        set_if!(ids.obj_src_clip_top, src_clip_top);
5233        set_if!(ids.obj_src_clip_right, src_clip_right);
5234        set_if!(ids.obj_src_clip_bottom, src_clip_bottom);
5235        set_if!(ids.obj_alpha, alpha);
5236        set_if!(ids.obj_tr, tr);
5237        set_if!(ids.obj_mono, mono);
5238        set_if!(ids.obj_reverse, reverse);
5239        set_if!(ids.obj_bright, bright);
5240        set_if!(ids.obj_dark, dark);
5241        set_if!(ids.obj_color_r, color_r);
5242        set_if!(ids.obj_color_g, color_g);
5243        set_if!(ids.obj_color_b, color_b);
5244        set_if!(ids.obj_color_rate, color_rate);
5245        set_if!(ids.obj_color_add_r, color_add_r);
5246        set_if!(ids.obj_color_add_g, color_add_g);
5247        set_if!(ids.obj_color_add_b, color_add_b);
5248        set_if!(ids.obj_mask_no, mask_no);
5249        set_if!(ids.obj_tonecurve_no, tonecurve_no);
5250        set_if!(ids.obj_light_no, light_no);
5251        set_if!(ids.obj_fog_use, fog_use);
5252        set_if!(ids.obj_culling, culling);
5253        set_if!(ids.obj_alpha_test, alpha_test);
5254        set_if!(ids.obj_alpha_blend, alpha_blend);
5255        set_if!(ids.obj_blend, blend);
5256        false
5257    }
5258
5259    fn sync_special_int_prop(
5260        &mut self,
5261        ids: &crate::runtime::constants::RuntimeConstants,
5262        op: i32,
5263        value: i64,
5264    ) -> bool {
5265        if op == OBJECT_NESTED_SLOT_KEY {
5266            self.nested_runtime_slot = (value >= 0).then_some(value as usize);
5267            return true;
5268        }
5269        if ids.obj_x_rep != 0 && op == ids.obj_x_rep {
5270            if self.runtime.prop_event_lists.x_rep.is_empty() {
5271                self.runtime.prop_event_lists.x_rep.push(IntEvent::new(0));
5272            }
5273            self.runtime.prop_event_lists.x_rep[0].set_value(value as i32);
5274            return true;
5275        }
5276        if ids.obj_y_rep != 0 && op == ids.obj_y_rep {
5277            if self.runtime.prop_event_lists.y_rep.is_empty() {
5278                self.runtime.prop_event_lists.y_rep.push(IntEvent::new(0));
5279            }
5280            self.runtime.prop_event_lists.y_rep[0].set_value(value as i32);
5281            return true;
5282        }
5283        if ids.obj_z_rep != 0 && op == ids.obj_z_rep {
5284            if self.runtime.prop_event_lists.z_rep.is_empty() {
5285                self.runtime.prop_event_lists.z_rep.push(IntEvent::new(0));
5286            }
5287            self.runtime.prop_event_lists.z_rep[0].set_value(value as i32);
5288            return true;
5289        }
5290        if ids.obj_tr_rep != 0 && op == ids.obj_tr_rep {
5291            if self.runtime.prop_event_lists.tr_rep.is_empty() {
5292                self.runtime
5293                    .prop_event_lists
5294                    .tr_rep
5295                    .push(IntEvent::new(255));
5296            }
5297            self.runtime.prop_event_lists.tr_rep[0].set_value(value as i32);
5298            return true;
5299        }
5300        if ids.obj_mesh_anim_clip != 0 && op == ids.obj_mesh_anim_clip {
5301            let mut next = self.mesh_animation_state.clone();
5302            next.change_animation_clip(None, (value >= 0).then_some(value as usize));
5303            self.set_mesh_animation_state(next);
5304            return true;
5305        }
5306        if ids.obj_mesh_anim_rate != 0 && op == ids.obj_mesh_anim_rate {
5307            let mut next = self.mesh_animation_state.clone();
5308            next.rate = (value as f32) / 1000.0;
5309            self.set_mesh_animation_state(next);
5310            return true;
5311        }
5312        if ids.obj_mesh_anim_time_offset != 0 && op == ids.obj_mesh_anim_time_offset {
5313            let mut next = self.mesh_animation_state.clone();
5314            next.time_offset_sec = (value as f32) / 1000.0;
5315            self.set_mesh_animation_state(next);
5316            return true;
5317        }
5318        if ids.obj_mesh_anim_pause != 0 && op == ids.obj_mesh_anim_pause {
5319            let mut next = self.mesh_animation_state.clone();
5320            next.paused = value != 0;
5321            next.is_anim = !next.paused;
5322            self.set_mesh_animation_state(next);
5323            return true;
5324        }
5325        if ids.obj_mesh_anim_hold_time != 0 && op == ids.obj_mesh_anim_hold_time {
5326            let mut next = self.mesh_animation_state.clone();
5327            next.hold_time_sec = ((value as f32) / 1000.0).max(0.0);
5328            next.time_sec = if next.rate > 0.0 {
5329                next.hold_time_sec / next.rate.max(0.000_001)
5330            } else {
5331                0.0
5332            };
5333            self.set_mesh_animation_state(next);
5334            return true;
5335        }
5336        if ids.obj_mesh_anim_shift_time != 0 && op == ids.obj_mesh_anim_shift_time {
5337            let mut next = self.mesh_animation_state.clone();
5338            next.set_anim_shift_time_sec(((value as f32) / 1000.0).max(0.0));
5339            self.set_mesh_animation_state(next);
5340            return true;
5341        }
5342        if ids.obj_mesh_anim_loop != 0 && op == ids.obj_mesh_anim_loop {
5343            let mut next = self.mesh_animation_state.clone();
5344            next.looped = value != 0;
5345            self.set_mesh_animation_state(next);
5346            return true;
5347        }
5348        if ids.obj_mesh_anim_blend_clip != 0 && op == ids.obj_mesh_anim_blend_clip {
5349            let mut next = self.mesh_animation_state.clone();
5350            next.blend_clip_index = (value >= 0).then_some(value as usize);
5351            next.blend_clip_name = None;
5352            self.set_mesh_animation_state(next);
5353            return true;
5354        }
5355        if ids.obj_mesh_anim_blend_weight != 0 && op == ids.obj_mesh_anim_blend_weight {
5356            let mut next = self.mesh_animation_state.clone();
5357            next.blend_weight = ((value as f32) / 1000.0).clamp(0.0, 1.0);
5358            self.set_mesh_animation_state(next);
5359            return true;
5360        }
5361        false
5362    }
5363
5364    fn special_int_prop(
5365        &self,
5366        ids: &crate::runtime::constants::RuntimeConstants,
5367        op: i32,
5368    ) -> Option<i64> {
5369        if op == OBJECT_NESTED_SLOT_KEY {
5370            return self.nested_runtime_slot.map(|v| v as i64);
5371        }
5372        if ids.obj_x_rep != 0 && op == ids.obj_x_rep {
5373            return self
5374                .runtime
5375                .prop_event_lists
5376                .x_rep
5377                .first()
5378                .map(|ev| ev.get_value() as i64);
5379        }
5380        if ids.obj_y_rep != 0 && op == ids.obj_y_rep {
5381            return self
5382                .runtime
5383                .prop_event_lists
5384                .y_rep
5385                .first()
5386                .map(|ev| ev.get_value() as i64);
5387        }
5388        if ids.obj_z_rep != 0 && op == ids.obj_z_rep {
5389            return self
5390                .runtime
5391                .prop_event_lists
5392                .z_rep
5393                .first()
5394                .map(|ev| ev.get_value() as i64);
5395        }
5396        if ids.obj_tr_rep != 0 && op == ids.obj_tr_rep {
5397            return self
5398                .runtime
5399                .prop_event_lists
5400                .tr_rep
5401                .first()
5402                .map(|ev| ev.get_value() as i64);
5403        }
5404        if ids.obj_mesh_anim_clip != 0 && op == ids.obj_mesh_anim_clip {
5405            return Some(
5406                self.mesh_animation_state
5407                    .clip_index
5408                    .map(|v| v as i64)
5409                    .unwrap_or(-1),
5410            );
5411        }
5412        if ids.obj_mesh_anim_rate != 0 && op == ids.obj_mesh_anim_rate {
5413            return Some((self.mesh_animation_state.rate * 1000.0).round() as i64);
5414        }
5415        if ids.obj_mesh_anim_time_offset != 0 && op == ids.obj_mesh_anim_time_offset {
5416            return Some((self.mesh_animation_state.time_offset_sec * 1000.0).round() as i64);
5417        }
5418        if ids.obj_mesh_anim_pause != 0 && op == ids.obj_mesh_anim_pause {
5419            return Some(if self.mesh_animation_state.paused {
5420                1
5421            } else {
5422                0
5423            });
5424        }
5425        if ids.obj_mesh_anim_hold_time != 0 && op == ids.obj_mesh_anim_hold_time {
5426            return Some((self.mesh_animation_state.hold_time_sec * 1000.0).round() as i64);
5427        }
5428        if ids.obj_mesh_anim_shift_time != 0 && op == ids.obj_mesh_anim_shift_time {
5429            return Some((self.mesh_animation_state.anim_shift_time_sec * 1000.0).round() as i64);
5430        }
5431        if ids.obj_mesh_anim_loop != 0 && op == ids.obj_mesh_anim_loop {
5432            return Some(if self.mesh_animation_state.looped {
5433                1
5434            } else {
5435                0
5436            });
5437        }
5438        if ids.obj_mesh_anim_blend_clip != 0 && op == ids.obj_mesh_anim_blend_clip {
5439            return Some(
5440                self.mesh_animation_state
5441                    .blend_clip_index
5442                    .map(|v| v as i64)
5443                    .unwrap_or(-1),
5444            );
5445        }
5446        if ids.obj_mesh_anim_blend_weight != 0 && op == ids.obj_mesh_anim_blend_weight {
5447            return Some((self.mesh_animation_state.blend_weight * 1000.0).round() as i64);
5448        }
5449        None
5450    }
5451
5452    fn sync_special_str_prop(
5453        &mut self,
5454        ids: &crate::runtime::constants::RuntimeConstants,
5455        op: i32,
5456        value: String,
5457    ) -> bool {
5458        if ids.obj_mesh_anim_clip_name != 0 && op == ids.obj_mesh_anim_clip_name {
5459            let mut next = self.mesh_animation_state.clone();
5460            next.change_animation_clip(Some(value), None);
5461            self.set_mesh_animation_state(next);
5462            return true;
5463        }
5464        if ids.obj_mesh_anim_blend_clip_name != 0 && op == ids.obj_mesh_anim_blend_clip_name {
5465            let mut next = self.mesh_animation_state.clone();
5466            next.blend_clip_name = Some(value);
5467            next.blend_clip_index = None;
5468            self.set_mesh_animation_state(next);
5469            return true;
5470        }
5471        false
5472    }
5473
5474    fn special_str_prop(
5475        &self,
5476        ids: &crate::runtime::constants::RuntimeConstants,
5477        op: i32,
5478    ) -> Option<String> {
5479        if ids.obj_mesh_anim_clip_name != 0 && op == ids.obj_mesh_anim_clip_name {
5480            return self.mesh_animation_state.clip_name.clone();
5481        }
5482        if ids.obj_mesh_anim_blend_clip_name != 0 && op == ids.obj_mesh_anim_blend_clip_name {
5483            return self.mesh_animation_state.blend_clip_name.clone();
5484        }
5485        None
5486    }
5487
5488    fn fixed_int_prop(
5489        &self,
5490        ids: &crate::runtime::constants::RuntimeConstants,
5491        op: i32,
5492    ) -> Option<i64> {
5493        match ids.object_op_tables().fixed_int_prop(op)? {
5494            FixedIntProp::Base(field) => Some(self.base.int_field(field)),
5495            FixedIntProp::EventTotal(target) => self
5496                .runtime
5497                .prop_events
5498                .get(target)
5499                .map(|ev| ev.get_total_value() as i64),
5500        }
5501    }
5502
5503    pub fn set_mesh_animation_state(&mut self, next: crate::mesh3d::MeshAnimationState) {
5504        self.apply_mesh_animation_state(next, None);
5505    }
5506
5507    fn apply_mesh_animation_state(
5508        &mut self,
5509        next: crate::mesh3d::MeshAnimationState,
5510        explicit_hold_override: Option<f32>,
5511    ) {
5512        let prev = self.mesh_animation_state.clone();
5513        let mut merged = next.sanitized();
5514        let clip_changed =
5515            prev.clip_name != merged.clip_name || prev.clip_index != merged.clip_index;
5516        let pause_enter = !prev.paused && merged.paused;
5517        let pause_exit = prev.paused && !merged.paused;
5518        let prev_base = prev.current_sample_base_sec();
5519
5520        merged.anim_track_no = prev.anim_track_no;
5521        merged.is_anim = !merged.paused;
5522        merged.time_sec = prev.time_sec.max(0.0);
5523        merged.hold_time_sec = prev.hold_time_sec.max(0.0);
5524        merged.prev_clip_name = prev.prev_clip_name.clone();
5525        merged.prev_clip_index = prev.prev_clip_index;
5526        merged.prev_time_sec = prev.prev_time_sec.max(0.0);
5527        merged.prev_time_offset_sec = prev.prev_time_offset_sec.max(0.0);
5528        merged.prev_rate = prev.prev_rate.max(0.0);
5529        merged.transition_elapsed_sec = prev.transition_elapsed_sec.max(0.0);
5530
5531        if clip_changed {
5532            merged.change_animation_clip(merged.clip_name.clone(), merged.clip_index);
5533        }
5534
5535        if let Some(hold_sec) = explicit_hold_override {
5536            let hold_sec = hold_sec.max(0.0);
5537            merged.hold_time_sec = hold_sec;
5538            merged.time_sec = if merged.rate > 0.0 {
5539                hold_sec / merged.rate.max(0.000_001)
5540            } else {
5541                0.0
5542            };
5543        } else if pause_enter {
5544            merged.hold_time_sec = prev_base;
5545        } else if pause_exit {
5546            merged.time_sec = if merged.rate > 0.0 {
5547                prev.hold_time_sec.max(0.0) / merged.rate.max(0.000_001)
5548            } else {
5549                prev.time_sec.max(0.0)
5550            };
5551        } else if merged.paused {
5552            merged.hold_time_sec = prev.hold_time_sec.max(0.0);
5553        } else if !clip_changed && (prev.rate - merged.rate).abs() > 0.000_001 {
5554            merged.time_sec = if merged.rate > 0.0 {
5555                prev_base / merged.rate.max(0.000_001)
5556            } else {
5557                prev.time_sec.max(0.0)
5558            };
5559        }
5560
5561        self.mesh_animation_state = merged.sanitized();
5562    }
5563
5564    pub fn sync_mesh_animation_state_from_props(
5565        &mut self,
5566        ids: &super::constants::RuntimeConstants,
5567    ) {
5568        let int_prop = |id: i32, default: i64| -> i64 {
5569            if id != 0 {
5570                self.lookup_int_prop(ids, id).unwrap_or(default)
5571            } else {
5572                default
5573            }
5574        };
5575        let str_prop = |id: i32| -> Option<String> {
5576            if id != 0 {
5577                self.lookup_str_prop(ids, id)
5578            } else {
5579                None
5580            }
5581        };
5582        let explicit_hold =
5583            ids.obj_mesh_anim_hold_time != 0 && self.has_int_prop(ids.obj_mesh_anim_hold_time);
5584        let requested_hold_sec = (int_prop(ids.obj_mesh_anim_hold_time, 0) as f32) / 1000.0;
5585        let requested_shift_sec = (int_prop(
5586            ids.obj_mesh_anim_shift_time,
5587            (self.mesh_animation_state.anim_shift_time_sec * 1000.0).round() as i64,
5588        ) as f32)
5589            / 1000.0;
5590        let next = crate::mesh3d::MeshAnimationState {
5591            clip_name: str_prop(ids.obj_mesh_anim_clip_name),
5592            clip_index: (int_prop(ids.obj_mesh_anim_clip, -1) >= 0).then_some(int_prop(
5593                ids.obj_mesh_anim_clip,
5594                -1,
5595            )
5596                as usize),
5597            blend_clip_name: str_prop(ids.obj_mesh_anim_blend_clip_name),
5598            blend_clip_index: (int_prop(ids.obj_mesh_anim_blend_clip, -1) >= 0)
5599                .then_some(int_prop(ids.obj_mesh_anim_blend_clip, -1) as usize),
5600            blend_weight: ((int_prop(ids.obj_mesh_anim_blend_weight, 0) as f32) / 1000.0)
5601                .clamp(0.0, 1.0),
5602            time_sec: self.mesh_animation_state.time_sec,
5603            rate: (int_prop(ids.obj_mesh_anim_rate, 1000) as f32) / 1000.0,
5604            time_offset_sec: (int_prop(ids.obj_mesh_anim_time_offset, 0) as f32) / 1000.0,
5605            hold_time_sec: if explicit_hold {
5606                requested_hold_sec
5607            } else {
5608                self.mesh_animation_state.hold_time_sec
5609            },
5610            paused: int_prop(ids.obj_mesh_anim_pause, 0) != 0,
5611            looped: int_prop(ids.obj_mesh_anim_loop, 1) != 0,
5612            anim_track_no: self.mesh_animation_state.anim_track_no,
5613            anim_shift_time_sec: requested_shift_sec.max(0.0),
5614            is_anim: !(int_prop(ids.obj_mesh_anim_pause, 0) != 0),
5615            prev_clip_name: self.mesh_animation_state.prev_clip_name.clone(),
5616            prev_clip_index: self.mesh_animation_state.prev_clip_index,
5617            prev_time_sec: self.mesh_animation_state.prev_time_sec,
5618            prev_time_offset_sec: self.mesh_animation_state.prev_time_offset_sec,
5619            prev_rate: self.mesh_animation_state.prev_rate,
5620            transition_elapsed_sec: self.mesh_animation_state.transition_elapsed_sec,
5621        };
5622        self.apply_mesh_animation_state(next, explicit_hold.then_some(requested_hold_sec));
5623    }
5624
5625    pub fn uses_mesh_animation_bridge_op(
5626        ids: &super::constants::RuntimeConstants,
5627        op: i32,
5628    ) -> bool {
5629        [
5630            ids.obj_mesh_anim_clip,
5631            ids.obj_mesh_anim_clip_name,
5632            ids.obj_mesh_anim_rate,
5633            ids.obj_mesh_anim_time_offset,
5634            ids.obj_mesh_anim_pause,
5635            ids.obj_mesh_anim_hold_time,
5636            ids.obj_mesh_anim_shift_time,
5637            ids.obj_mesh_anim_loop,
5638            ids.obj_mesh_anim_blend_clip,
5639            ids.obj_mesh_anim_blend_clip_name,
5640            ids.obj_mesh_anim_blend_weight,
5641        ]
5642        .into_iter()
5643        .any(|id| id != 0 && op == id)
5644    }
5645
5646    pub fn tick(&mut self, past_game_time: i32, past_real_time: i32) {
5647        let delta = past_game_time.max(0);
5648        self.runtime
5649            .prop_events
5650            .update_time(past_game_time, past_real_time);
5651        self.runtime.prop_events.frame();
5652        self.runtime
5653            .prop_event_lists
5654            .update_time(past_game_time, past_real_time);
5655        self.runtime.prop_event_lists.frame();
5656        self.frame_action
5657            .counter
5658            .update_time(past_game_time, past_real_time);
5659        for fa in &mut self.frame_action_ch {
5660            fa.counter.update_time(past_game_time, past_real_time);
5661        }
5662        for child in &mut self.runtime.child_objects {
5663            child.tick(past_game_time, past_real_time);
5664        }
5665        if self.object_type == 12 {
5666            self.emote.tick(past_game_time);
5667        }
5668        self.movie.tick(past_game_time, past_real_time);
5669        self.gan.update_time(past_game_time, past_real_time);
5670        if matches!(self.object_type, 6 | 7) {
5671            self.mesh_animation_state.advance_controller_frames(delta);
5672        }
5673
5674        if self.object_type == 9 && self.movie.just_finished && !self.movie.auto_free_flag {
5675            self.movie.pause_flag = true;
5676        }
5677    }
5678
5679    pub fn update_weather_time(
5680        &mut self,
5681        past_game_time: i32,
5682        past_real_time: i32,
5683        screen_w: i64,
5684        screen_h: i64,
5685    ) {
5686        if self.object_type != 4 || !matches!(self.weather_param.weather_type, 1 | 2) {
5687            return;
5688        }
5689        let cnt = self.weather_param.cnt.max(0) as usize;
5690        let cnt_max = self.weather_work.cnt_max.min(self.weather_work.sub.len());
5691        for idx in 0..cnt_max {
5692            let mut setup_after_sleep = false;
5693            {
5694                let sub = &mut self.weather_work.sub[idx];
5695                if idx >= cnt && sub.state == 0 {
5696                    continue;
5697                }
5698                let past_time = if sub.real_time_flag {
5699                    past_real_time.max(0) as i64
5700                } else {
5701                    past_game_time.max(0) as i64
5702                };
5703                sub.state_cur_time = sub.state_cur_time.saturating_add(past_time);
5704                sub.move_cur_time = sub.move_cur_time.saturating_add(past_time);
5705                sub.sin_cur_time = sub.sin_cur_time.saturating_add(past_time);
5706
5707                if (idx >= cnt || sub.restruct_flag)
5708                    && sub.state == 2
5709                    && sub.state_time_len - sub.state_cur_time >= 3000
5710                {
5711                    sub.state_cur_time = sub.state_time_len.saturating_sub(1500);
5712                }
5713
5714                while sub.state_cur_time - sub.state_time_len > 0 {
5715                    let amari_time = sub.state_cur_time - sub.state_time_len;
5716                    sub.state = (sub.state + 1) % 4;
5717                    if sub.state == 0 {
5718                        if idx >= cnt {
5719                            break;
5720                        }
5721                        setup_after_sleep = true;
5722                        break;
5723                    }
5724                    if sub.state == 1 {
5725                        sub.move_cur_time = amari_time;
5726                    }
5727                    sub.state_time_len = match sub.state {
5728                        1 => 1000,
5729                        2 => sub.active_time_len,
5730                        3 => 1000,
5731                        _ => sub.state_time_len,
5732                    };
5733                    sub.state_cur_time = amari_time;
5734                }
5735            }
5736            if setup_after_sleep {
5737                self.setup_weather_sub(idx, 1, screen_w, screen_h);
5738            }
5739        }
5740    }
5741
5742    pub fn any_event_active(&self) -> bool {
5743        self.runtime.prop_events.any_active() || self.runtime.prop_event_lists.any_active()
5744    }
5745
5746    pub fn end_all_events(&mut self) {
5747        self.runtime.prop_events.end_all();
5748        self.runtime.prop_event_lists.end_all();
5749    }
5750
5751    pub fn event_target(
5752        &self,
5753        ids: &super::constants::RuntimeConstants,
5754        op: i32,
5755    ) -> ObjectEventTarget {
5756        ids.object_op_tables().event_target(op)
5757    }
5758}
5759
5760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5761pub enum GroupListOpKind {
5762    Alloc,
5763    Free,
5764    Unknown,
5765}
5766
5767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5768pub enum GroupOpKind {
5769    Sel,
5770    SelCancel,
5771    Init,
5772    Start,
5773    StartCancel,
5774    End,
5775    GetHitNo,
5776    GetPushedNo,
5777    GetDecidedNo,
5778    GetResult,
5779    GetResultButtonNo,
5780    Order,
5781    Layer,
5782    CancelPriority,
5783    Unknown,
5784}
5785
5786#[derive(Debug, Clone)]
5787pub struct GroupState {
5788    pub wait_flag: bool,
5789    pub cancel_flag: bool,
5790    pub cancel_se_no: i64,
5791    /// Original C_elm_group_param::doing flag.
5792    pub started: bool,
5793    /// Original C_elm_group_param::pause_flag. A paused group remains started
5794    /// but does not participate in button hit testing.
5795    pub pause_flag: bool,
5796    /// Original C_elm_group_param::target_object S_element.
5797    pub target_object: Vec<i32>,
5798
5799    pub hit_button_no: i64,
5800    pub pushed_button_no: i64,
5801    pub decided_button_no: i64,
5802    pub hit_runtime_slot: Option<usize>,
5803    pub pushed_runtime_slot: Option<usize>,
5804    /// Runtime-only marker for C_elm_mwnd_msg character buttons. These do not
5805    /// own an OBJECT runtime slot but participate in the same group state.
5806    pub hit_message_button: bool,
5807    pub pushed_message_button: bool,
5808    pub message_button_se_no: i64,
5809
5810    pub result: i64,
5811    pub result_button_no: i64,
5812
5813    pub order: i64,
5814    pub layer: i64,
5815    pub cancel_priority: i64,
5816    pub props: HashMap<i32, i64>,
5817    pub aux_str_props: HashMap<i32, String>,
5818}
5819
5820impl Default for GroupState {
5821    fn default() -> Self {
5822        let mut state = Self {
5823            wait_flag: false,
5824            cancel_flag: false,
5825            cancel_se_no: -1,
5826            started: false,
5827            pause_flag: false,
5828            target_object: Vec::new(),
5829            hit_button_no: -1,
5830            pushed_button_no: -1,
5831            decided_button_no: TNM_GROUP_NOT_DECIDED,
5832            hit_runtime_slot: None,
5833            pushed_runtime_slot: None,
5834            hit_message_button: false,
5835            pushed_message_button: false,
5836            message_button_se_no: -1,
5837            result: TNM_GROUP_RESULT_NONE,
5838            result_button_no: 0,
5839            order: 0,
5840            layer: 0,
5841            cancel_priority: 0,
5842            props: HashMap::default(),
5843            aux_str_props: HashMap::default(),
5844        };
5845        state.reinit();
5846        state
5847    }
5848}
5849
5850pub const TNM_GROUP_NOT_DECIDED: i64 = -2;
5851pub const TNM_GROUP_CANCELED: i64 = -1;
5852pub const TNM_GROUP_RESULT_DECIDED: i64 = 1;
5853pub const TNM_GROUP_RESULT_NONE: i64 = 0;
5854pub const TNM_GROUP_RESULT_CANCELLED: i64 = -1;
5855
5856impl GroupState {
5857    pub fn reinit(&mut self) {
5858        self.order = 0;
5859        self.layer = 0;
5860        self.cancel_priority = 0;
5861        self.cancel_se_no = -1;
5862        self.decided_button_no = TNM_GROUP_NOT_DECIDED;
5863        self.result = TNM_GROUP_RESULT_NONE;
5864        self.result_button_no = 0;
5865        self.started = false;
5866        self.pause_flag = false;
5867        self.wait_flag = false;
5868        self.cancel_flag = false;
5869        self.hit_button_no = -1;
5870        self.pushed_button_no = -1;
5871        self.hit_runtime_slot = None;
5872        self.pushed_runtime_slot = None;
5873        self.hit_message_button = false;
5874        self.pushed_message_button = false;
5875        self.message_button_se_no = -1;
5876    }
5877
5878    pub fn init_sel(&mut self) {
5879        self.cancel_priority = 0;
5880        self.cancel_se_no = -1;
5881        self.decided_button_no = TNM_GROUP_NOT_DECIDED;
5882        self.result = TNM_GROUP_RESULT_NONE;
5883        self.result_button_no = 0;
5884        self.started = false;
5885        self.pause_flag = false;
5886        self.wait_flag = false;
5887        self.cancel_flag = false;
5888        self.hit_button_no = -1;
5889        self.pushed_button_no = -1;
5890        self.hit_runtime_slot = None;
5891        self.pushed_runtime_slot = None;
5892        self.hit_message_button = false;
5893        self.pushed_message_button = false;
5894        self.message_button_se_no = -1;
5895    }
5896
5897    pub fn start(&mut self) {
5898        self.started = true;
5899        self.decided_button_no = TNM_GROUP_NOT_DECIDED;
5900    }
5901
5902    pub fn is_doing(&self) -> bool {
5903        self.started && !self.pause_flag
5904    }
5905
5906    pub fn end(&mut self) {
5907        self.started = false;
5908        self.decided_button_no = TNM_GROUP_NOT_DECIDED;
5909    }
5910
5911    pub fn decide(&mut self, button_no: i64) -> bool {
5912        if !self.started {
5913            return false;
5914        }
5915        self.started = false;
5916        self.decided_button_no = button_no;
5917        self.result = TNM_GROUP_RESULT_DECIDED;
5918        self.result_button_no = button_no;
5919        self.hit_button_no = -1;
5920        self.pushed_button_no = -1;
5921        self.hit_runtime_slot = None;
5922        self.pushed_runtime_slot = None;
5923        self.hit_message_button = false;
5924        self.pushed_message_button = false;
5925        self.message_button_se_no = -1;
5926        true
5927    }
5928
5929    pub fn cancel(&mut self) -> Option<i64> {
5930        if !self.started {
5931            return None;
5932        }
5933        let hit_button_no = self.hit_button_no;
5934        self.started = false;
5935        self.decided_button_no = TNM_GROUP_CANCELED;
5936        self.result = TNM_GROUP_RESULT_CANCELLED;
5937        self.result_button_no = hit_button_no;
5938        self.hit_button_no = -1;
5939        self.pushed_button_no = -1;
5940        self.hit_runtime_slot = None;
5941        self.pushed_runtime_slot = None;
5942        self.hit_message_button = false;
5943        self.pushed_message_button = false;
5944        self.message_button_se_no = -1;
5945        Some(hit_button_no)
5946    }
5947}
5948
5949#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5950pub enum MwndListOpKind {
5951    CloseAll,
5952    CloseAllWait,
5953    CloseAllNowait,
5954    Unknown,
5955}
5956
5957#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5958pub enum MwndOpKind {
5959    MsgBlock,
5960    OpenWait,
5961    OpenNowait,
5962    CloseWait,
5963    CloseNowait,
5964    EndClose,
5965    CheckOpen,
5966    Clear,
5967    NovelClear,
5968    /// Append text to the current message buffer.
5969    Print,
5970    /// NL: line break without preserving indent.
5971    NewLineNoIndent,
5972    /// NLI: line break with indent path preserved.
5973    NewLineIndent,
5974    /// Wait for input while in message mode.
5975    WaitMsg,
5976    /// PP: wait for text completion, then wait for key.
5977    Pp,
5978    /// R: wait for text completion, then clear-ready + key wait.
5979    R,
5980    /// PAGE: wait for text completion, then page-clear + key wait.
5981    PageWait,
5982
5983    SetName,
5984    ClearName,
5985    GetName,
5986    NextMsg,
5987    MultiMsg,
5988    Ruby,
5989    Koe,
5990    KoePlayWait,
5991    KoePlayWaitKey,
5992    Layer,
5993    World,
5994    SetMojiSize,
5995    SetMojiColor,
5996    SetIndent,
5997    ClearIndent,
5998    StartSlideMsg,
5999    EndSlideMsg,
6000    SlideMsg,
6001    InitOpenAnimeType,
6002    InitOpenAnimeTime,
6003    InitCloseAnimeType,
6004    InitCloseAnimeTime,
6005    SetOpenAnimeType,
6006    SetOpenAnimeTime,
6007    SetCloseAnimeType,
6008    SetCloseAnimeTime,
6009    GetOpenAnimeType,
6010    GetOpenAnimeTime,
6011    GetCloseAnimeType,
6012    GetCloseAnimeTime,
6013    GetDefaultOpenAnimeType,
6014    GetDefaultOpenAnimeTime,
6015    GetDefaultCloseAnimeType,
6016    GetDefaultCloseAnimeTime,
6017    Sel,
6018    SelCancel,
6019    SelMsg,
6020    SelMsgCancel,
6021
6022    /// (bool new_line_flag) -> bool
6023    AddMsgCheck,
6024    /// (string) -> (string overflow_msg)
6025    AddMsg,
6026
6027    SetWaku,
6028    InitWakuFile,
6029    SetWakuFile,
6030    GetWakuFile,
6031    InitFilterFile,
6032    SetFilterFile,
6033    GetFilterFile,
6034
6035    ClearFace,
6036    SetFace,
6037    SetRepPos,
6038    MsgBtn,
6039    InitWindowPos,
6040    InitWindowSize,
6041    SetWindowPos,
6042    SetWindowSize,
6043    GetWindowPosX,
6044    GetWindowPosY,
6045    GetWindowSizeX,
6046    GetWindowSizeY,
6047    InitWindowMojiCnt,
6048    SetWindowMojiCnt,
6049    GetWindowMojiCntX,
6050    GetWindowMojiCntY,
6051    Unknown,
6052}
6053
6054#[derive(Debug, Clone, PartialEq, Eq)]
6055pub struct MwndMessageButtonState {
6056    pub btn_no: i64,
6057    pub group_no: i64,
6058    pub action_no: i64,
6059    pub se_no: i64,
6060}
6061
6062#[derive(Debug, Clone, PartialEq, Eq)]
6063pub struct MwndGlyphState {
6064    /// C_tnm_moji::type: 0 normal, 1 emoji A, 2 emoji B.
6065    pub moji_type: i32,
6066    /// C_tnm_moji::code. For normal text this is the UTF-16 code unit.
6067    pub code: i32,
6068    pub ch: char,
6069    pub x: i64,
6070    pub y: i64,
6071    pub size: i64,
6072    pub moji_color_no: i64,
6073    pub shadow_color_no: i64,
6074    pub fuchi_color_no: i64,
6075    pub shadow: bool,
6076    pub fuchi: bool,
6077    pub bold: bool,
6078    /// Number of revealed body glyphs required before this glyph becomes visible.
6079    pub reveal_index: usize,
6080    pub ruby: bool,
6081    /// C_elm_mwnd_moji::m_appeared_flag.
6082    pub appeared: bool,
6083    /// Message-button metadata registered for this glyph.
6084    pub message_button: Option<MwndMessageButtonState>,
6085}
6086
6087impl Default for MwndGlyphState {
6088    fn default() -> Self {
6089        Self {
6090            moji_type: 0,
6091            code: 0,
6092            ch: '\0',
6093            x: 0,
6094            y: 0,
6095            size: 0,
6096            moji_color_no: 0,
6097            shadow_color_no: 0,
6098            fuchi_color_no: -1,
6099            shadow: true,
6100            fuchi: false,
6101            bold: false,
6102            reveal_index: 1,
6103            ruby: false,
6104            appeared: false,
6105            message_button: None,
6106        }
6107    }
6108}
6109
6110#[derive(Debug, Clone, Default, PartialEq, Eq)]
6111pub struct MwndRubyPendingState {
6112    pub text: String,
6113    pub start_pos: Option<(i64, i64)>,
6114}
6115
6116/// One completed C_elm_mwnd_msg entry retained by MULTI_MSG/NEXT_MSG.
6117/// The active page remains in MwndState so existing form handlers can mutate it
6118/// without an additional indirection; completed pages are immutable snapshots.
6119#[derive(Debug, Clone, Default, PartialEq, Eq)]
6120pub struct MwndMessagePageState {
6121    pub msg_text: String,
6122    pub glyphs: Vec<MwndGlyphState>,
6123    pub disp_moji_cnt: i64,
6124    pub hide_moji_cnt: i64,
6125    pub cur_msg_type: i64,
6126    pub cur_msg_type_decided: bool,
6127    pub ruby_start_pos: (i64, i64),
6128    pub ruby_start_ready: bool,
6129    pub cursor_pos: (i64, i64),
6130    pub moji_rep_pos: (i64, i64),
6131    pub indent_pos: i64,
6132    pub indent_moji: Option<char>,
6133    pub indent_count: i64,
6134    pub line_head: bool,
6135    pub ruby_pending: Option<MwndRubyPendingState>,
6136    pub moji_size: Option<i64>,
6137    pub moji_color: Option<i64>,
6138    pub shadow_color: Option<i64>,
6139    pub fuchi_color: Option<i64>,
6140    pub chara_moji_color: Option<i64>,
6141    pub chara_shadow_color: Option<i64>,
6142    pub chara_fuchi_color: Option<i64>,
6143    pub msgbtn: Option<(i64, i64, i64, i64)>,
6144}
6145
6146#[derive(Debug, Default, Clone)]
6147pub struct MwndSelectionChoice {
6148    pub text: String,
6149    pub kind: i64,
6150    pub color: i64,
6151    /// Original C_elm_mwnd_select_item placement and measured extent.
6152    pub pos: (i64, i64),
6153    pub size: (i64, i64),
6154    /// Original per-character records retained for save/load round trips.
6155    pub glyphs: Vec<MwndGlyphState>,
6156}
6157
6158#[derive(Debug, Default, Clone)]
6159pub struct MwndSelectionState {
6160    pub choices: Vec<MwndSelectionChoice>,
6161    /// C_elm_mwnd_select::m_disp_item_cnt. This is not the keyboard cursor.
6162    pub disp_item_count: usize,
6163    pub cursor: usize,
6164    pub cancel_enable: bool,
6165    pub close_mwnd: bool,
6166    /// Conservative runtime result: selected entry index (1-based), 0 for none, -1 for cancel.
6167    pub result: i64,
6168}
6169
6170#[derive(Debug, Default, Clone)]
6171pub struct BtnSelItemState {
6172    pub generated_objects: Vec<ObjectState>,
6173    pub object_list: Vec<ObjectState>,
6174    pub strict: bool,
6175    pub text: String,
6176    pub item_type: i64,
6177    pub color: i64,
6178    pub pos: (i64, i64),
6179    pub size: (i64, i64),
6180    pub visible: bool,
6181    pub selected: bool,
6182    pub button_action_no: i64,
6183    pub button_state: i64,
6184    /// Per-item correction produced by C_elm_btn_select::frame.
6185    pub animation_offset: (i64, i64),
6186    /// None means the normal 255 parent TR used by non-selection BTNSELITEMs.
6187    pub animation_tr: Option<i64>,
6188}
6189
6190#[derive(Debug, Default, Clone)]
6191pub struct MwndState {
6192    pub initialized_from_gameexe: bool,
6193    pub open: bool,
6194    pub name_text: String,
6195    /// Original name-window per-character records retained by the save stream.
6196    pub name_glyphs: Vec<MwndGlyphState>,
6197    pub msg_text: String,
6198    pub msg_waku_no: Option<i64>,
6199    pub waku_file: String,
6200    pub filter_file: String,
6201    pub filter_margin: Option<(i64, i64, i64, i64)>,
6202    pub filter_color: Option<(u8, u8, u8, u8)>,
6203    pub filter_config_color: bool,
6204    pub filter_config_tr: bool,
6205    pub waku_extend_type: i64,
6206    pub icon_no: i64,
6207    pub page_icon_no: i64,
6208    pub key_icon_appear: bool,
6209    pub key_icon_mode: i64,
6210    pub key_icon_pos: Option<(i64, i64)>,
6211    pub icon_pos_type: i64,
6212    pub icon_pos_base: i64,
6213    pub icon_pos: Option<(i64, i64, i64)>,
6214    /// Per-button WAKU template placement: (pos_base, x, y).
6215    pub waku_button_layout: Vec<(i64, i64, i64)>,
6216    /// Per-face WAKU template placement.
6217    pub waku_face_pos: Vec<(i64, i64)>,
6218    pub face_file: String,
6219    pub face_no: i64,
6220    pub rep_pos: Option<(i64, i64)>,
6221    pub msgbtn: Option<(i64, i64, i64, i64)>,
6222    pub window_pos: Option<(i64, i64)>,
6223    pub window_size: Option<(i64, i64)>,
6224    pub message_pos: Option<(i64, i64)>,
6225    pub message_margin: Option<(i64, i64, i64, i64)>,
6226    pub window_moji_cnt: Option<(i64, i64)>,
6227    pub moji_space: Option<(i64, i64)>,
6228    pub mwnd_extend_type: i64,
6229    pub multi_msg: bool,
6230    pub vertical_writing: bool,
6231    /// Completed message entries created by NEXT_MSG. C++ keeps all entries in
6232    /// m_msg_list and draws every one of them.
6233    pub message_pages: Vec<MwndMessagePageState>,
6234    /// Original C_elm_mwnd_msg keeps one fully styled and positioned record per glyph.
6235    pub glyphs: Vec<MwndGlyphState>,
6236    /// Original m_disp_moji_cnt and m_hide_moji_cnt.
6237    pub disp_moji_cnt: i64,
6238    pub hide_moji_cnt: i64,
6239    pub cur_msg_type: i64,
6240    pub cur_msg_type_decided: bool,
6241    pub ruby_start_pos: (i64, i64),
6242    pub ruby_start_ready: bool,
6243    pub cursor_pos: (i64, i64),
6244    pub moji_rep_pos: (i64, i64),
6245    pub indent_pos: i64,
6246    pub indent_moji: Option<char>,
6247    pub indent_count: i64,
6248    pub line_head: bool,
6249    pub ruby_pending: Option<MwndRubyPendingState>,
6250    pub ruby_size: i64,
6251    pub ruby_space: i64,
6252    pub default_moji_size: i64,
6253    pub default_moji_color: i64,
6254    pub default_shadow_color: i64,
6255    pub default_fuchi_color: i64,
6256    pub default_name_moji_color: i64,
6257    pub default_name_shadow_color: i64,
6258    pub default_name_fuchi_color: i64,
6259    pub koe: Option<(i64, i64)>,
6260    /// C++ C_elm_mwnd::get_sorter() uses an order/layer pair.
6261    /// There is no public MWND.ORDER script element in the recovered headers;
6262    /// this order is initialized from the engine MWND render defaults and is
6263    /// kept as runtime state so wipe/render code does not read a global table
6264    /// in place of the per-MWND sorter.
6265    pub order: i64,
6266    pub layer: i64,
6267    pub world: i64,
6268    pub moji_size: Option<i64>,
6269    pub moji_color: Option<i64>,
6270    pub shadow_color: Option<i64>,
6271    pub fuchi_color: Option<i64>,
6272    pub chara_color_mod: Option<i64>,
6273    pub chara_moji_color: Option<i64>,
6274    pub chara_shadow_color: Option<i64>,
6275    pub chara_fuchi_color: Option<i64>,
6276    pub name_moji_color: Option<i64>,
6277    pub name_shadow_color: Option<i64>,
6278    pub name_fuchi_color: Option<i64>,
6279    pub indent: bool,
6280    pub slide_msg: bool,
6281    pub slide_time: i64,
6282    pub open_anime_type: i64,
6283    pub open_anime_time: i64,
6284    pub close_anime_type: i64,
6285    pub close_anime_time: i64,
6286    pub selection: Option<MwndSelectionState>,
6287    /// Runtime-only equivalent of C_elm_mwnd::m_read_flag_stock_list.
6288    /// Entries are committed to GlobalState::read_flags at the same message
6289    /// boundaries as the original engine and are intentionally not serialized
6290    /// in the MWND save stream.
6291    pub read_flag_stock: Vec<(i64, i64)>,
6292
6293    // C_elm_mwnd persistent work variables needed by the original save stream.
6294    pub novel_mode: i64,
6295    pub name_disp_mode: i64,
6296    pub name_bracket: i64,
6297    pub name_extend_type: i64,
6298    pub name_window_align: i64,
6299    pub name_window_pos: (i64, i64),
6300    pub name_window_size: (i64, i64),
6301    pub name_window_rect: (i64, i64, i64, i64),
6302    pub name_message_pos: (i64, i64),
6303    pub name_message_pos_rep: (i64, i64),
6304    pub name_message_margin: (i64, i64, i64, i64),
6305    pub overflow_check_size: i64,
6306    pub face_hide_name: i64,
6307    pub time: i64,
6308    pub auto_proc_ready: bool,
6309    pub window_appear: bool,
6310    pub name_appear: bool,
6311    pub auto_mode_end_moji_cnt: i64,
6312    pub target_msg_no: i64,
6313    pub koe_play_flag: bool,
6314    pub open_anime_start_time: i64,
6315    pub close_anime_start_time: i64,
6316
6317    pub text_dirty: bool,
6318    pub clear_ready: bool,
6319    pub msg_block_started: bool,
6320
6321    pub button_list: Vec<ObjectState>,
6322    pub button_list_strict: bool,
6323    pub face_list: Vec<ObjectState>,
6324    pub face_list_strict: bool,
6325    pub object_list: Vec<ObjectState>,
6326    pub object_list_strict: bool,
6327    pub props: HashMap<i32, i64>,
6328    pub aux_str_props: HashMap<i32, String>,
6329}
6330
6331#[derive(Debug, Default, Clone)]
6332pub struct StageFormState {
6333    /// Backend object-slot namespace for this stage form. The original engine
6334    /// owns separate C_elm_stage_list instances for normal and EXCALL scenes;
6335    /// the Rust GfxRuntime is shared, so EXCALL must not reuse normal slots.
6336    pub backend_slot_base: usize,
6337    /// C++ C_elm_stage_list::init creates BACK/FRONT/NEXT sub stages eagerly.
6338    pub initialized_from_gameexe: bool,
6339    /// Group list storage per stage index.
6340    pub group_lists: HashMap<i64, Vec<GroupState>>,
6341    /// BTNSELITEM list storage per stage index.
6342    pub btnselitem_lists: HashMap<i64, Vec<BtnSelItemState>>,
6343    /// Persistent C_elm_stage::m_btn_sel state per stage.  The global SELBTN
6344    /// command operates on FRONT, but stage wipe copies the whole selector to
6345    /// NEXT/FRONT/BACK and the original local save serializes BACK and FRONT
6346    /// independently.
6347    pub btn_select_states: HashMap<i64, BtnSelectRuntimeState>,
6348    /// MWND list storage per stage index.
6349    pub mwnd_lists: HashMap<i64, Vec<MwndState>>,
6350    /// World list storage per stage index.
6351    pub world_lists: HashMap<i64, Vec<WorldState>>,
6352    /// Effect list storage per stage index. Mirrors C_elm_stage::m_effect_list.
6353    pub effect_lists: HashMap<i64, Vec<ScreenEffectState>>,
6354    /// Quake list storage per stage index. Mirrors C_elm_stage::m_quake_list.
6355    pub quake_lists: HashMap<i64, Vec<ScreenQuakeState>>,
6356    // --- OBJECT / OBJECTLIST ---
6357    /// Per-stage object state (string objects, rect objects, nested child objects, etc.).
6358    pub object_lists: HashMap<i64, Vec<ObjectState>>,
6359    /// Fixed per-slot C++ `C_elm_object::m_op_def.use_flag` values.
6360    ///
6361    /// These belong to the destination object-list slot itself.  They are
6362    /// initialized by `C_elm_object_list::_init()` and are never changed by
6363    /// OBJECT.INIT, OBJECT.FREE, `C_elm_object::copy()`, or STAGE.WIPE.
6364    /// `ObjectState::used` is deliberately not this flag.
6365    pub object_slot_use: HashMap<i64, Vec<bool>>,
6366    /// Whether this stage's object list should enforce its current size (enabled after RESIZE).
6367    pub object_list_strict: HashMap<i64, bool>,
6368    /// Rectangle-object layer per stage (created lazily).
6369    pub rect_layers: HashMap<i64, LayerId>,
6370
6371    /// Stable slot assignment for embedded object lists and nested child objects.
6372    pub embedded_object_slots: HashMap<String, usize>,
6373    /// Reverse membership index used by every-frame object scans.  The string
6374    /// map above preserves stable semantic keys; this map makes the hot-path
6375    /// "is this top-level slot actually an embedded backing slot?" query O(1).
6376    pub embedded_object_slots_by_stage: HashMap<i64, HashSet<usize>>,
6377    pub next_embedded_object_slot: HashMap<i64, usize>,
6378    pub next_nested_object_slot: HashMap<i64, usize>,
6379}
6380
6381// -----------------------------------------------------------------------------
6382// Screen (GLOBAL.SCREEN) state
6383// -----------------------------------------------------------------------------
6384
6385pub fn normalize_screen_effect_scalar(
6386    ids: &crate::runtime::constants::RuntimeConstants,
6387    op: i32,
6388    value: i32,
6389) -> i32 {
6390    let byte_range = [
6391        ids.effect_mono,
6392        ids.effect_mono_eve,
6393        ids.effect_reverse,
6394        ids.effect_reverse_eve,
6395        ids.effect_bright,
6396        ids.effect_bright_eve,
6397        ids.effect_dark,
6398        ids.effect_dark_eve,
6399        ids.effect_color_r,
6400        ids.effect_color_r_eve,
6401        ids.effect_color_g,
6402        ids.effect_color_g_eve,
6403        ids.effect_color_b,
6404        ids.effect_color_b_eve,
6405        ids.effect_color_rate,
6406        ids.effect_color_rate_eve,
6407        ids.effect_color_add_r,
6408        ids.effect_color_add_r_eve,
6409        ids.effect_color_add_g,
6410        ids.effect_color_add_g_eve,
6411        ids.effect_color_add_b,
6412        ids.effect_color_add_b_eve,
6413    ];
6414    if byte_range.iter().any(|&id| id != 0 && op == id) {
6415        value.clamp(0, 255)
6416    } else {
6417        value
6418    }
6419}
6420
6421#[derive(Debug, Clone)]
6422pub struct ScreenEffectState {
6423    pub x: IntEvent,
6424    pub y: IntEvent,
6425    pub z: IntEvent,
6426    pub mono: IntEvent,
6427    pub reverse: IntEvent,
6428    pub bright: IntEvent,
6429    pub dark: IntEvent,
6430    pub color_r: IntEvent,
6431    pub color_g: IntEvent,
6432    pub color_b: IntEvent,
6433    pub color_rate: IntEvent,
6434    pub color_add_r: IntEvent,
6435    pub color_add_g: IntEvent,
6436    pub color_add_b: IntEvent,
6437    pub begin_order: i32,
6438    pub begin_layer: i32,
6439    pub end_order: i32,
6440    pub end_layer: i32,
6441    pub wipe_copy: i32,
6442    pub wipe_erase: i32,
6443}
6444
6445impl Default for ScreenEffectState {
6446    fn default() -> Self {
6447        Self {
6448            x: IntEvent::new(0),
6449            y: IntEvent::new(0),
6450            z: IntEvent::new(0),
6451            mono: IntEvent::new(0),
6452            reverse: IntEvent::new(0),
6453            bright: IntEvent::new(0),
6454            dark: IntEvent::new(0),
6455            color_r: IntEvent::new(0),
6456            color_g: IntEvent::new(0),
6457            color_b: IntEvent::new(0),
6458            color_rate: IntEvent::new(0),
6459            color_add_r: IntEvent::new(0),
6460            color_add_g: IntEvent::new(0),
6461            color_add_b: IntEvent::new(0),
6462            begin_order: 0,
6463            begin_layer: i32::MIN,
6464            end_order: 0,
6465            end_layer: i32::MAX,
6466            wipe_copy: 0,
6467            wipe_erase: 0,
6468        }
6469    }
6470}
6471impl ScreenEffectState {
6472    pub fn int_event_by_op(
6473        &self,
6474        ids: &crate::runtime::constants::RuntimeConstants,
6475        op: i32,
6476    ) -> Option<&IntEvent> {
6477        match op {
6478            s if s == ids.effect_x || s == ids.effect_x_eve => Some(&self.x),
6479            s if s == ids.effect_y || s == ids.effect_y_eve => Some(&self.y),
6480            s if s == ids.effect_z || s == ids.effect_z_eve => Some(&self.z),
6481            s if s == ids.effect_mono || s == ids.effect_mono_eve => Some(&self.mono),
6482            s if s == ids.effect_reverse || s == ids.effect_reverse_eve => Some(&self.reverse),
6483            s if s == ids.effect_bright || s == ids.effect_bright_eve => Some(&self.bright),
6484            s if s == ids.effect_dark || s == ids.effect_dark_eve => Some(&self.dark),
6485            s if s == ids.effect_color_r || s == ids.effect_color_r_eve => Some(&self.color_r),
6486            s if s == ids.effect_color_g || s == ids.effect_color_g_eve => Some(&self.color_g),
6487            s if s == ids.effect_color_b || s == ids.effect_color_b_eve => Some(&self.color_b),
6488            s if s == ids.effect_color_rate || s == ids.effect_color_rate_eve => {
6489                Some(&self.color_rate)
6490            }
6491            s if s == ids.effect_color_add_r || s == ids.effect_color_add_r_eve => {
6492                Some(&self.color_add_r)
6493            }
6494            s if s == ids.effect_color_add_g || s == ids.effect_color_add_g_eve => {
6495                Some(&self.color_add_g)
6496            }
6497            s if s == ids.effect_color_add_b || s == ids.effect_color_add_b_eve => {
6498                Some(&self.color_add_b)
6499            }
6500            _ => None,
6501        }
6502    }
6503
6504    pub fn int_event_by_op_mut(
6505        &mut self,
6506        ids: &crate::runtime::constants::RuntimeConstants,
6507        op: i32,
6508    ) -> Option<&mut IntEvent> {
6509        match op {
6510            s if s == ids.effect_x || s == ids.effect_x_eve => Some(&mut self.x),
6511            s if s == ids.effect_y || s == ids.effect_y_eve => Some(&mut self.y),
6512            s if s == ids.effect_z || s == ids.effect_z_eve => Some(&mut self.z),
6513            s if s == ids.effect_mono || s == ids.effect_mono_eve => Some(&mut self.mono),
6514            s if s == ids.effect_reverse || s == ids.effect_reverse_eve => Some(&mut self.reverse),
6515            s if s == ids.effect_bright || s == ids.effect_bright_eve => Some(&mut self.bright),
6516            s if s == ids.effect_dark || s == ids.effect_dark_eve => Some(&mut self.dark),
6517            s if s == ids.effect_color_r || s == ids.effect_color_r_eve => Some(&mut self.color_r),
6518            s if s == ids.effect_color_g || s == ids.effect_color_g_eve => Some(&mut self.color_g),
6519            s if s == ids.effect_color_b || s == ids.effect_color_b_eve => Some(&mut self.color_b),
6520            s if s == ids.effect_color_rate || s == ids.effect_color_rate_eve => {
6521                Some(&mut self.color_rate)
6522            }
6523            s if s == ids.effect_color_add_r || s == ids.effect_color_add_r_eve => {
6524                Some(&mut self.color_add_r)
6525            }
6526            s if s == ids.effect_color_add_g || s == ids.effect_color_add_g_eve => {
6527                Some(&mut self.color_add_g)
6528            }
6529            s if s == ids.effect_color_add_b || s == ids.effect_color_add_b_eve => {
6530                Some(&mut self.color_add_b)
6531            }
6532            _ => None,
6533        }
6534    }
6535
6536    pub fn reinit(&mut self) {
6537        *self = Self::default();
6538    }
6539
6540    pub fn tick(&mut self, delta: i32) {
6541        self.x.tick(delta);
6542        self.y.tick(delta);
6543        self.z.tick(delta);
6544        self.mono.tick(delta);
6545        self.reverse.tick(delta);
6546        self.bright.tick(delta);
6547        self.dark.tick(delta);
6548        self.color_r.tick(delta);
6549        self.color_g.tick(delta);
6550        self.color_b.tick(delta);
6551        self.color_rate.tick(delta);
6552        self.color_add_r.tick(delta);
6553        self.color_add_g.tick(delta);
6554        self.color_add_b.tick(delta);
6555    }
6556}
6557
6558#[derive(Debug, Clone, Copy, Default, PartialEq)]
6559pub struct ScreenQuakeTransform {
6560    pub x: i32,
6561    pub y: i32,
6562    pub scale: f32,
6563    pub rotate_degrees: f32,
6564    pub center_x: i32,
6565    pub center_y: i32,
6566}
6567
6568#[derive(Debug, Clone)]
6569pub struct ScreenQuakeState {
6570    pub quake_type: i32,
6571    pub vec: i32,
6572    pub power: i32,
6573    pub cur_time: i32,
6574    pub total_time: i32,
6575    pub ending: bool,
6576    pub end_cur_time: i32,
6577    pub end_total_time: i32,
6578    pub cnt: i32,
6579    pub end_cnt: i32,
6580    pub center_x: i32,
6581    pub center_y: i32,
6582    pub begin_order: i32,
6583    pub end_order: i32,
6584}
6585
6586impl Default for ScreenQuakeState {
6587    fn default() -> Self {
6588        Self {
6589            quake_type: -1,
6590            vec: 0,
6591            power: 0,
6592            cur_time: 0,
6593            total_time: 0,
6594            ending: false,
6595            end_cur_time: 0,
6596            end_total_time: 0,
6597            cnt: 0,
6598            end_cnt: 0,
6599            center_x: 0,
6600            center_y: 0,
6601            begin_order: 0,
6602            end_order: 0,
6603        }
6604    }
6605}
6606
6607fn speed_up_limit_i32(now: i32, start: i32, start_value: i32, end: i32, end_value: i32) -> i32 {
6608    if start == end {
6609        return end_value;
6610    }
6611    let lo = start.min(end) as f64;
6612    let hi = start.max(end) as f64;
6613    let t = (now as f64).clamp(lo, hi);
6614    let start = start as f64;
6615    let end = end as f64;
6616    let start_value = start_value as f64;
6617    let end_value = end_value as f64;
6618    (((t - start) * (t - start) * (end_value - start_value) / ((end - start) * (end - start)))
6619        + start_value) as i32
6620}
6621
6622fn speed_down_limit_i32(now: i32, start: i32, start_value: i32, end: i32, end_value: i32) -> i32 {
6623    if start == end {
6624        return end_value;
6625    }
6626    let lo = start.min(end) as f64;
6627    let hi = start.max(end) as f64;
6628    let t = (now as f64).clamp(lo, hi);
6629    let start = start as f64;
6630    let end = end as f64;
6631    let start_value = start_value as f64;
6632    let end_value = end_value as f64;
6633    (-(t - end) * (t - end) * (end_value - start_value) / ((end - start) * (end - start))
6634        + end_value) as i32
6635}
6636
6637fn linear_limit_f64(now: i32, start: i32, start_value: f64, end: i32, end_value: f64) -> f64 {
6638    if start == end {
6639        return end_value;
6640    }
6641    if now <= start.min(end) {
6642        return if start < end { start_value } else { end_value };
6643    }
6644    if now >= start.max(end) {
6645        return if start < end { end_value } else { start_value };
6646    }
6647    (end_value - start_value) * (now - start) as f64 / (end - start) as f64 + start_value
6648}
6649
6650impl ScreenQuakeState {
6651    pub fn reinit(&mut self) {
6652        *self = Self::default();
6653    }
6654
6655    pub fn start_kind(&mut self, quake_type: i32, time_ms: i64, cnt: i32, end_cnt: i32) {
6656        self.quake_type = quake_type;
6657        self.cur_time = 0;
6658        self.total_time = time_ms.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
6659        self.cnt = cnt;
6660        self.end_cnt = end_cnt;
6661        self.ending = false;
6662        self.end_cur_time = 0;
6663        self.end_total_time = 0;
6664        if self.total_time <= 0 || !(0..=3).contains(&self.quake_type) {
6665            self.reinit();
6666        }
6667    }
6668
6669    pub fn end_ms(&mut self, time_ms: i64) {
6670        let time_ms = time_ms.clamp(0, i32::MAX as i64) as i32;
6671        if time_ms == 0 {
6672            self.reinit();
6673            return;
6674        }
6675        if self.quake_type < 0 {
6676            return;
6677        }
6678        self.ending = true;
6679        self.end_cur_time = 0;
6680        self.end_total_time = time_ms;
6681    }
6682
6683    pub fn check_value(&self) -> i32 {
6684        if self.quake_type < 0 {
6685            0
6686        } else if self.ending {
6687            2
6688        } else {
6689            1
6690        }
6691    }
6692
6693    pub fn is_active(&self) -> bool {
6694        self.quake_type >= 0 && self.total_time > 0
6695    }
6696
6697    pub fn is_infinite(&self) -> bool {
6698        self.is_active() && self.cnt == 0 && self.end_cnt == 0 && !self.ending
6699    }
6700
6701    pub fn remaining_ms(&self) -> Option<u64> {
6702        if !self.is_active() {
6703            return Some(0);
6704        }
6705        if self.ending {
6706            return Some(self.end_total_time.saturating_sub(self.end_cur_time).max(0) as u64);
6707        }
6708        if self.cnt == 0 && self.end_cnt == 0 {
6709            return None;
6710        }
6711        Some(
6712            self.total_time
6713                .saturating_mul(self.cnt.saturating_add(self.end_cnt))
6714                .saturating_sub(self.cur_time)
6715                .max(0) as u64,
6716        )
6717    }
6718
6719    pub fn tick(&mut self, delta_ms: i32) {
6720        if !self.is_active() {
6721            return;
6722        }
6723        let delta_ms = delta_ms.max(0);
6724        self.cur_time = self.cur_time.saturating_add(delta_ms);
6725        if self.ending {
6726            self.end_cur_time = self.end_cur_time.saturating_add(delta_ms);
6727        }
6728        let loop_forever = self.cnt == 0 && self.end_cnt == 0;
6729        if (!loop_forever
6730            && self.cur_time
6731                >= self
6732                    .total_time
6733                    .saturating_mul(self.cnt.saturating_add(self.end_cnt)))
6734            || (self.ending && self.end_cur_time >= self.end_total_time)
6735        {
6736            self.reinit();
6737        }
6738    }
6739
6740    pub fn transform(&self) -> ScreenQuakeTransform {
6741        const SCALE_UNIT: i32 = 1000;
6742        if !self.is_active() {
6743            return ScreenQuakeTransform {
6744                scale: 1.0,
6745                ..ScreenQuakeTransform::default()
6746            };
6747        }
6748        let quarter = (self.total_time / 4).max(1);
6749        let jump = self.cur_time.rem_euclid(self.total_time.max(1));
6750        let (x_sign, y_sign) = match self.vec {
6751            0 => (0, -1),
6752            1 => (1, -1),
6753            2 => (1, 0),
6754            3 => (1, 1),
6755            4 => (0, 1),
6756            5 => (-1, 1),
6757            6 => (-1, 0),
6758            7 => (-1, -1),
6759            _ => (0, 0),
6760        };
6761        let mut x = 0i32;
6762        let mut y = 0i32;
6763        let mut scale = SCALE_UNIT;
6764        let mut rotate = 0i32;
6765        match self.quake_type {
6766            0 => {
6767                let value = if jump < self.total_time / 4 {
6768                    speed_up_limit_i32(jump, 0, 0, quarter, self.power / 2)
6769                } else if jump < self.total_time / 2 {
6770                    speed_down_limit_i32(
6771                        jump - self.total_time / 4,
6772                        0,
6773                        self.power / 2,
6774                        quarter,
6775                        self.power,
6776                    )
6777                } else if jump < self.total_time * 3 / 4 {
6778                    speed_up_limit_i32(
6779                        jump - self.total_time / 2,
6780                        0,
6781                        self.power,
6782                        quarter,
6783                        self.power / 2,
6784                    )
6785                } else {
6786                    speed_down_limit_i32(
6787                        jump - self.total_time * 3 / 4,
6788                        0,
6789                        self.power / 2,
6790                        quarter,
6791                        0,
6792                    )
6793                };
6794                x = value.saturating_mul(x_sign);
6795                y = value.saturating_mul(y_sign);
6796            }
6797            1 => {
6798                let value = if jump < self.total_time / 4 {
6799                    speed_down_limit_i32(jump, 0, 0, quarter, self.power / 2)
6800                } else if jump < self.total_time / 2 {
6801                    speed_up_limit_i32(jump - self.total_time / 4, 0, self.power / 2, quarter, 0)
6802                } else if jump < self.total_time * 3 / 4 {
6803                    speed_down_limit_i32(jump - self.total_time / 2, 0, 0, quarter, -self.power / 2)
6804                } else {
6805                    speed_up_limit_i32(
6806                        jump - self.total_time * 3 / 4,
6807                        0,
6808                        -self.power / 2,
6809                        quarter,
6810                        0,
6811                    )
6812                };
6813                x = value.saturating_mul(x_sign);
6814                y = value.saturating_mul(y_sign);
6815            }
6816            2 => {
6817                let power = self.power.clamp(0, 255);
6818                let max_scale = 256 * SCALE_UNIT / (256 - power);
6819                let half_scale = (max_scale - SCALE_UNIT) / 2 + SCALE_UNIT;
6820                scale = if jump < self.total_time / 4 {
6821                    speed_up_limit_i32(jump, 0, SCALE_UNIT, quarter, half_scale)
6822                } else if jump < self.total_time / 2 {
6823                    speed_down_limit_i32(
6824                        jump - self.total_time / 4,
6825                        0,
6826                        half_scale,
6827                        quarter,
6828                        max_scale,
6829                    )
6830                } else if jump < self.total_time * 3 / 4 {
6831                    speed_up_limit_i32(
6832                        jump - self.total_time / 2,
6833                        0,
6834                        max_scale,
6835                        quarter,
6836                        half_scale,
6837                    )
6838                } else {
6839                    speed_down_limit_i32(
6840                        jump - self.total_time * 3 / 4,
6841                        0,
6842                        half_scale,
6843                        quarter,
6844                        SCALE_UNIT,
6845                    )
6846                };
6847            }
6848            // TNM_QUAKE_TYPE_ROTATE exists in the C++ enum/save structure,
6849            // but the original frame routine has no rotate branch. Imported
6850            // state therefore remains active without altering the render transform.
6851            3 => {}
6852            _ => {}
6853        }
6854
6855        let loop_forever = self.cnt == 0 && self.end_cnt == 0;
6856        if !loop_forever && self.cur_time >= self.total_time.saturating_mul(self.cnt) {
6857            let fade = linear_limit_f64(
6858                self.cur_time,
6859                self.total_time.saturating_mul(self.cnt),
6860                1.0,
6861                self.total_time
6862                    .saturating_mul(self.cnt.saturating_add(self.end_cnt)),
6863                0.0,
6864            );
6865            x = (x as f64 * fade) as i32;
6866            y = (y as f64 * fade) as i32;
6867            scale = ((scale - SCALE_UNIT) as f64 * fade + SCALE_UNIT as f64) as i32;
6868            rotate = (rotate as f64 * fade) as i32;
6869        }
6870        if self.ending && self.end_total_time > 0 {
6871            let fade = linear_limit_f64(self.end_cur_time, 0, 1.0, self.end_total_time, 0.0);
6872            x = (x as f64 * fade) as i32;
6873            y = (y as f64 * fade) as i32;
6874            scale = ((scale - SCALE_UNIT) as f64 * fade + SCALE_UNIT as f64) as i32;
6875            rotate = (rotate as f64 * fade) as i32;
6876        }
6877
6878        ScreenQuakeTransform {
6879            x,
6880            y,
6881            scale: scale as f32 / SCALE_UNIT as f32,
6882            rotate_degrees: rotate as f32 / 1000.0,
6883            center_x: self.center_x,
6884            center_y: self.center_y,
6885        }
6886    }
6887}
6888
6889#[derive(Debug, Clone)]
6890pub struct ScreenShakeState {
6891    pub shake_no: i32,
6892    pub cur_time: i32,
6893    pub cur_x: i32,
6894    pub cur_y: i32,
6895}
6896
6897impl ScreenShakeState {
6898    pub fn init() -> Self {
6899        Self {
6900            shake_no: -1,
6901            cur_time: 0,
6902            cur_x: 0,
6903            cur_y: 0,
6904        }
6905    }
6906
6907    pub fn start(&mut self, shake_no: i64, table_count: usize) -> bool {
6908        if shake_no < 0 || shake_no as usize >= table_count {
6909            return false;
6910        }
6911        self.shake_no = shake_no as i32;
6912        self.cur_time = 0;
6913        self.cur_x = 0;
6914        self.cur_y = 0;
6915        true
6916    }
6917
6918    pub fn end(&mut self) {
6919        *self = Self::init();
6920    }
6921
6922    pub fn tick(&mut self, delta_ms: i32, templates: &[Vec<crate::runtime::tables::ShakeStep>]) {
6923        if self.shake_no < 0 || self.shake_no as usize >= templates.len() {
6924            self.end();
6925            return;
6926        }
6927        self.cur_time = self.cur_time.saturating_add(delta_ms.max(0));
6928        let mut total = 0i32;
6929        let mut found = None;
6930        for step in &templates[self.shake_no as usize] {
6931            let period_time = self.cur_time.saturating_sub(total);
6932            if period_time < step.time_ms {
6933                found = Some((step.x, step.y));
6934                break;
6935            }
6936            total = total.saturating_add(step.time_ms.max(0));
6937        }
6938        if let Some((x, y)) = found {
6939            self.cur_x = x;
6940            self.cur_y = y;
6941        } else {
6942            self.end();
6943        }
6944    }
6945
6946    pub fn is_active(&self) -> bool {
6947        self.shake_no >= 0
6948    }
6949}
6950
6951impl Default for ScreenShakeState {
6952    fn default() -> Self {
6953        Self::init()
6954    }
6955}
6956
6957#[derive(Debug, Default, Clone)]
6958pub struct ScreenFormState {
6959    pub effect_list: Vec<ScreenEffectState>,
6960    pub quake_list: Vec<ScreenQuakeState>,
6961    pub shake: ScreenShakeState,
6962}
6963
6964impl ScreenFormState {
6965    pub fn ensure_effect_len(&mut self, n: usize) {
6966        if self.effect_list.len() < n {
6967            self.effect_list
6968                .extend((0..(n - self.effect_list.len())).map(|_| ScreenEffectState::default()));
6969        } else if self.effect_list.len() > n {
6970            self.effect_list.truncate(n);
6971        }
6972    }
6973
6974    pub fn ensure_quake_len(&mut self, n: usize) {
6975        if self.quake_list.len() < n {
6976            self.quake_list
6977                .extend((0..(n - self.quake_list.len())).map(|_| ScreenQuakeState::default()));
6978        } else if self.quake_list.len() > n {
6979            self.quake_list.truncate(n);
6980        }
6981    }
6982
6983    pub fn tick(&mut self, delta: i32, shake_templates: &[Vec<crate::runtime::tables::ShakeStep>]) {
6984        for effect in &mut self.effect_list {
6985            effect.tick(delta);
6986        }
6987        for quake in &mut self.quake_list {
6988            quake.tick(delta);
6989        }
6990        self.shake.tick(delta, shake_templates);
6991    }
6992}
6993
6994// -----------------------------------------------------------------------------
6995// Message backlog (GLOBAL.MSGBK) state
6996// -----------------------------------------------------------------------------
6997
6998#[derive(Debug, Default, Clone)]
6999pub struct MsgBackEntry {
7000    pub pct_flag: bool,
7001    pub msg_str: String,
7002    pub original_name: String,
7003    pub disp_name: String,
7004    pub pct_pos_x: i32,
7005    pub pct_pos_y: i32,
7006    pub koe_no_list: Vec<i64>,
7007    pub chr_no_list: Vec<i64>,
7008    pub koe_play_no: i64,
7009    pub debug_msg: String,
7010    pub scn_no: i64,
7011    pub line_no: i64,
7012    pub save_id: i64,
7013    pub save_id_check_flag: bool,
7014}
7015
7016#[derive(Debug, Clone)]
7017pub struct MsgBackState {
7018    pub history: Vec<MsgBackEntry>,
7019    pub history_cnt_max: usize,
7020    pub history_cnt: usize,
7021    pub history_start_pos: usize,
7022    pub history_insert_pos: usize,
7023    pub history_last_pos: usize,
7024    pub new_msg_flag: bool,
7025}
7026
7027impl Default for MsgBackState {
7028    fn default() -> Self {
7029        let history_cnt_max = 256usize;
7030        Self {
7031            history: vec![
7032                MsgBackEntry {
7033                    scn_no: -1,
7034                    line_no: -1,
7035                    ..MsgBackEntry::default()
7036                };
7037                history_cnt_max
7038            ],
7039            history_cnt_max,
7040            history_cnt: 0,
7041            history_start_pos: 0,
7042            history_insert_pos: 0,
7043            history_last_pos: 0,
7044            new_msg_flag: true,
7045        }
7046    }
7047}
7048
7049impl MsgBackState {
7050    fn reset_entry(entry: &mut MsgBackEntry) {
7051        *entry = MsgBackEntry {
7052            scn_no: -1,
7053            line_no: -1,
7054            ..MsgBackEntry::default()
7055        };
7056    }
7057
7058    fn ensure_capacity(&mut self) {
7059        if self.history_cnt_max == 0 {
7060            self.history_cnt_max = 256;
7061        }
7062        if self.history.len() != self.history_cnt_max {
7063            self.history
7064                .resize_with(self.history_cnt_max, || MsgBackEntry {
7065                    scn_no: -1,
7066                    line_no: -1,
7067                    ..MsgBackEntry::default()
7068                });
7069        }
7070        self.history_insert_pos %= self.history_cnt_max;
7071        self.history_start_pos %= self.history_cnt_max;
7072        self.history_last_pos %= self.history_cnt_max;
7073    }
7074
7075    pub fn set_history_cnt_max(&mut self, max_count: usize) {
7076        let max_count = max_count.max(1);
7077        if max_count == self.history_cnt_max {
7078            self.ensure_capacity();
7079            return;
7080        }
7081        let mut ordered = self
7082            .ordered_history_indices()
7083            .into_iter()
7084            .filter_map(|idx| self.history.get(idx).cloned())
7085            .collect::<Vec<_>>();
7086        if ordered.len() > max_count {
7087            let drop_count = ordered.len() - max_count;
7088            ordered.drain(0..drop_count);
7089        }
7090        self.history_cnt_max = max_count;
7091        self.history = vec![
7092            MsgBackEntry {
7093                scn_no: -1,
7094                line_no: -1,
7095                ..MsgBackEntry::default()
7096            };
7097            max_count
7098        ];
7099        self.history_cnt = ordered.len();
7100        self.history_start_pos = 0;
7101        for (i, entry) in ordered.into_iter().enumerate() {
7102            self.history[i] = entry;
7103        }
7104        self.history_insert_pos = self.history_cnt % self.history_cnt_max;
7105        self.history_last_pos = self
7106            .history_cnt
7107            .saturating_sub(1)
7108            .min(self.history_cnt_max - 1);
7109        self.new_msg_flag = true;
7110    }
7111
7112    fn ready_msg(&mut self) -> &mut MsgBackEntry {
7113        self.ensure_capacity();
7114        if self.new_msg_flag {
7115            if self.history_cnt < self.history_cnt_max {
7116                self.history_cnt += 1;
7117            } else {
7118                self.history_start_pos = (self.history_start_pos + 1) % self.history_cnt_max;
7119            }
7120            Self::reset_entry(&mut self.history[self.history_insert_pos]);
7121            self.new_msg_flag = false;
7122        }
7123        &mut self.history[self.history_insert_pos]
7124    }
7125
7126    pub fn clear(&mut self) {
7127        self.ensure_capacity();
7128        for entry in &mut self.history {
7129            Self::reset_entry(entry);
7130        }
7131        self.history_cnt = 0;
7132        self.history_start_pos = 0;
7133        self.history_insert_pos = 0;
7134        self.history_last_pos = 0;
7135        self.new_msg_flag = true;
7136    }
7137
7138    pub fn ordered_history_indices(&self) -> Vec<usize> {
7139        if self.history_cnt_max == 0 || self.history_cnt == 0 {
7140            return Vec::new();
7141        }
7142        (0..self.history_cnt)
7143            .map(|i| (self.history_start_pos + i) % self.history_cnt_max)
7144            .collect()
7145    }
7146
7147    pub fn next(&mut self) {
7148        self.ensure_capacity();
7149        if self.new_msg_flag {
7150            return;
7151        }
7152        let Some(cur) = self.history.get(self.history_insert_pos) else {
7153            self.new_msg_flag = true;
7154            return;
7155        };
7156        if !cur.pct_flag && cur.msg_str.is_empty() {
7157            return;
7158        }
7159        self.history_insert_pos = (self.history_insert_pos + 1) % self.history_cnt_max;
7160        self.new_msg_flag = true;
7161    }
7162
7163    pub fn add_koe(&mut self, koe_no: i64, chara_no: i64, scn_no: i64, line_no: i64) -> bool {
7164        if koe_no < 0 {
7165            return true;
7166        }
7167        let insert_pos = self.history_insert_pos;
7168        let entry = self.ready_msg();
7169        entry.koe_no_list.push(koe_no);
7170        entry.chr_no_list.push(chara_no);
7171        entry.scn_no = scn_no;
7172        entry.line_no = line_no;
7173        self.history_last_pos = insert_pos;
7174        true
7175    }
7176
7177    pub fn add_name(
7178        &mut self,
7179        original_name: &str,
7180        disp_name: &str,
7181        scn_no: i64,
7182        line_no: i64,
7183    ) -> bool {
7184        if disp_name.is_empty() {
7185            return true;
7186        }
7187        let insert_pos = self.history_insert_pos;
7188        let entry = self.ready_msg();
7189        entry.original_name.clear();
7190        entry.original_name.push_str(original_name);
7191        entry.disp_name.clear();
7192        entry.disp_name.push_str(disp_name);
7193        entry.scn_no = scn_no;
7194        entry.line_no = line_no;
7195        self.history_last_pos = insert_pos;
7196        true
7197    }
7198
7199    pub fn add_msg(&mut self, msg: &str, debug_msg: &str, scn_no: i64, line_no: i64) -> bool {
7200        if msg.is_empty() {
7201            return true;
7202        }
7203        let insert_pos = self.history_insert_pos;
7204        let entry = self.ready_msg();
7205        entry.msg_str.push_str(msg);
7206        entry.debug_msg.clear();
7207        entry.debug_msg.push_str(debug_msg);
7208        entry.scn_no = scn_no;
7209        entry.line_no = line_no;
7210        self.history_last_pos = insert_pos;
7211        true
7212    }
7213
7214    pub fn add_new_line_indent(&mut self, scn_no: i64, line_no: i64) -> bool {
7215        let insert_pos = self.history_insert_pos;
7216        let entry = self.ready_msg();
7217        entry.msg_str.push('\n');
7218        entry.scn_no = scn_no;
7219        entry.line_no = line_no;
7220        self.history_last_pos = insert_pos;
7221        true
7222    }
7223
7224    pub fn add_new_line_no_indent(&mut self, scn_no: i64, line_no: i64) -> bool {
7225        let insert_pos = self.history_insert_pos;
7226        let entry = self.ready_msg();
7227        entry.msg_str.push('\u{0007}');
7228        entry.scn_no = scn_no;
7229        entry.line_no = line_no;
7230        self.history_last_pos = insert_pos;
7231        true
7232    }
7233
7234    pub fn add_pct(&mut self, file_name: &str, x: i32, y: i32) -> bool {
7235        if file_name.is_empty() {
7236            return false;
7237        }
7238        self.next();
7239        let insert_pos = self.history_insert_pos;
7240        let entry = self.ready_msg();
7241        entry.pct_flag = true;
7242        entry.pct_pos_x = x;
7243        entry.pct_pos_y = y;
7244        entry.msg_str.clear();
7245        entry.msg_str.push_str(file_name);
7246        self.history_last_pos = insert_pos;
7247        self.next();
7248        true
7249    }
7250
7251    pub fn current_entry(&self) -> Option<&MsgBackEntry> {
7252        self.history.get(self.history_insert_pos)
7253    }
7254}
7255
7256impl StageFormState {
7257    /// Return the fixed C++ `C_elm_object::is_use()` value for a top-level
7258    /// STAGE object slot.  Stage forms are initialized before script access, so
7259    /// the fallback is only for legacy/incomplete snapshots.
7260    #[inline(always)]
7261    pub fn object_slot_is_used(&self, stage_idx: i64, slot: usize) -> bool {
7262        self.object_slot_use
7263            .get(&stage_idx)
7264            .and_then(|flags| flags.get(slot))
7265            .copied()
7266            .unwrap_or(true)
7267    }
7268
7269    pub fn ensure_group_list(&mut self, stage_idx: i64, cnt: usize) {
7270        let entry = self.group_lists.entry(stage_idx).or_default();
7271        if entry.len() < cnt {
7272            entry.extend((0..(cnt - entry.len())).map(|_| GroupState::default()));
7273        } else if entry.len() > cnt {
7274            entry.truncate(cnt);
7275        }
7276    }
7277
7278    pub fn clear_group_list(&mut self, stage_idx: i64) {
7279        self.group_lists.insert(stage_idx, Vec::new());
7280    }
7281
7282    pub fn ensure_mwnd_list(&mut self, stage_idx: i64, cnt: usize) {
7283        let entry = self.mwnd_lists.entry(stage_idx).or_default();
7284        if entry.len() < cnt {
7285            entry.extend((0..(cnt - entry.len())).map(|_| MwndState::default()));
7286        } else if entry.len() > cnt {
7287            entry.truncate(cnt);
7288        }
7289    }
7290
7291    pub fn ensure_effect_list(&mut self, stage_idx: i64, cnt: usize) {
7292        let entry = self.effect_lists.entry(stage_idx).or_default();
7293        if entry.len() < cnt {
7294            entry.extend((0..(cnt - entry.len())).map(|_| ScreenEffectState::default()));
7295        } else if entry.len() > cnt {
7296            entry.truncate(cnt);
7297        }
7298    }
7299
7300    pub fn ensure_quake_list(&mut self, stage_idx: i64, cnt: usize) {
7301        let entry = self.quake_lists.entry(stage_idx).or_default();
7302        if entry.len() < cnt {
7303            entry.extend((0..(cnt - entry.len())).map(|_| ScreenQuakeState::default()));
7304        } else if entry.len() > cnt {
7305            entry.truncate(cnt);
7306        }
7307    }
7308
7309    pub fn close_all_mwnd(&mut self, stage_idx: i64) {
7310        if let Some(list) = self.mwnd_lists.get_mut(&stage_idx) {
7311            for (idx, m) in list.iter_mut().enumerate() {
7312                let old_open = m.open;
7313                m.open = false;
7314                if env_is_set!("SG_DEBUG") {
7315                    eprintln!(
7316                        "[SG_DEBUG][MWND_STATE_TRACE] scene=<runtime> scene_no=- line=- reason=STAGE_CLOSE_ALL_MWND stage={} mwnd={} old_open={} new_open={} buttons={} faces={} objects={} waku={} filter={} pos={:?} size={:?} open_anim=({}, {}) close_anim=({}, {}) selection={} msg_len={} name_len={}",
7317                        stage_idx,
7318                        idx,
7319                        old_open,
7320                        m.open,
7321                        m.button_list.len(),
7322                        m.face_list.len(),
7323                        m.object_list.len(),
7324                        if m.waku_file.is_empty() {
7325                            "-"
7326                        } else {
7327                            m.waku_file.as_str()
7328                        },
7329                        if m.filter_file.is_empty() {
7330                            "-"
7331                        } else {
7332                            m.filter_file.as_str()
7333                        },
7334                        m.window_pos,
7335                        m.window_size,
7336                        m.open_anime_type,
7337                        m.open_anime_time,
7338                        m.close_anime_type,
7339                        m.close_anime_time,
7340                        m.selection.is_some(),
7341                        m.msg_text.len(),
7342                        m.name_text.len(),
7343                    );
7344                }
7345            }
7346        }
7347    }
7348
7349    pub fn ensure_object_list(&mut self, stage_idx: i64, cnt: usize) {
7350        let entry = self.object_lists.entry(stage_idx).or_default();
7351        if entry.len() < cnt {
7352            entry.extend((0..(cnt - entry.len())).map(|_| ObjectState::default()));
7353        } else if entry.len() > cnt {
7354            entry.truncate(cnt);
7355        }
7356
7357        let slot_use = self.object_slot_use.entry(stage_idx).or_default();
7358        if slot_use.len() < cnt {
7359            slot_use.extend((0..(cnt - slot_use.len())).map(|_| true));
7360        } else if slot_use.len() > cnt {
7361            slot_use.truncate(cnt);
7362        }
7363    }
7364
7365    pub fn set_object_list_len_strict(&mut self, stage_idx: i64, cnt: usize) {
7366        self.ensure_object_list(stage_idx, cnt);
7367        self.object_list_strict.insert(stage_idx, true);
7368    }
7369
7370    pub fn object_list_len(&self, stage_idx: i64) -> usize {
7371        self.object_lists
7372            .get(&stage_idx)
7373            .map(|v| v.len())
7374            .unwrap_or(0)
7375    }
7376
7377    #[inline(always)]
7378    pub fn is_embedded_object_slot(&self, stage_idx: i64, slot: usize) -> bool {
7379        self.embedded_object_slots_by_stage
7380            .get(&stage_idx)
7381            .is_some_and(|slots| slots.contains(&slot))
7382    }
7383
7384    pub fn register_embedded_object_slot(&mut self, stage_idx: i64, key: String, slot: usize) {
7385        self.embedded_object_slots.insert(key, slot);
7386        self.embedded_object_slots_by_stage
7387            .entry(stage_idx)
7388            .or_default()
7389            .insert(slot);
7390    }
7391
7392    pub fn clear_embedded_object_slots_for_stage(&mut self, stage_idx: i64) {
7393        let prefix = format!("{stage_idx}:");
7394        self.embedded_object_slots
7395            .retain(|key, _| !key.starts_with(&prefix));
7396        self.embedded_object_slots_by_stage.remove(&stage_idx);
7397    }
7398}
7399
7400impl MaskListState {
7401    pub fn new(mask_cnt: usize) -> Self {
7402        let mut masks = Vec::with_capacity(mask_cnt);
7403        for _ in 0..mask_cnt {
7404            masks.push(MaskState::new());
7405        }
7406        Self { masks }
7407    }
7408
7409    pub fn ensure_size(&mut self, mask_cnt: usize) {
7410        if self.masks.len() < mask_cnt {
7411            self.masks.reserve(mask_cnt - self.masks.len());
7412            while self.masks.len() < mask_cnt {
7413                self.masks.push(MaskState::new());
7414            }
7415        } else if self.masks.len() > mask_cnt {
7416            self.masks.truncate(mask_cnt);
7417        }
7418    }
7419
7420    pub fn tick_frame(&mut self, delta: i32) {
7421        for m in &mut self.masks {
7422            m.x_event.tick(delta);
7423            m.y_event.tick(delta);
7424            for ev in m.script_events.values_mut() {
7425                ev.tick(delta);
7426            }
7427        }
7428    }
7429}
7430
7431#[derive(Debug, Clone, Copy)]
7432pub struct ExcallTickTargets {
7433    pub counter_list_id: u32,
7434    pub frame_action_id: u32,
7435    pub frame_action_ch_list_id: u32,
7436    pub stage_form_id: u32,
7437}
7438
7439impl GlobalState {
7440    pub fn ensure_read_flag_count(&mut self, scene_no: i64, flag_count: usize) {
7441        if scene_no < 0 {
7442            return;
7443        }
7444        let row = self.read_flags.entry(scene_no).or_default();
7445        row.resize(flag_count, 0);
7446    }
7447
7448    pub fn set_read_flag(&mut self, scene_no: i64, flag_no: i64) -> bool {
7449        if scene_no < 0 || flag_no < 0 {
7450            return false;
7451        }
7452        let Ok(flag_no) = usize::try_from(flag_no) else {
7453            return false;
7454        };
7455        let row = self.read_flags.entry(scene_no).or_default();
7456        if row.len() <= flag_no {
7457            // The number is supplied by the current scene lexer immediately
7458            // after the command.  Grow lazily so a valid flag can be committed
7459            // even before the full Scene.pck table has been initialized.
7460            row.resize(flag_no + 1, 0);
7461        }
7462        row[flag_no] = 1;
7463        true
7464    }
7465
7466    pub fn read_flag(&self, scene_no: i64, flag_no: i64) -> bool {
7467        if scene_no < 0 || flag_no < 0 {
7468            return false;
7469        }
7470        self.read_flags
7471            .get(&scene_no)
7472            .and_then(|row| row.get(flag_no as usize))
7473            .copied()
7474            .unwrap_or(0)
7475            != 0
7476    }
7477
7478    pub fn start_wipe(&mut self, w: WipeState) {
7479        self.wipe = Some(w);
7480    }
7481
7482    pub fn finish_wipe(&mut self) {
7483        self.wipe = None;
7484    }
7485
7486    pub fn wipe_done(&self) -> bool {
7487        self.wipe.as_ref().map(|w| w.is_done()).unwrap_or(true)
7488    }
7489
7490    pub fn tick_frame(
7491        &mut self,
7492        past_game_time: i32,
7493        past_real_time: i32,
7494        shake_templates: &[Vec<crate::runtime::tables::ShakeStep>],
7495        excall: Option<ExcallTickTargets>,
7496    ) {
7497        // C++ eng_frame.cpp has two time domains here: ordinary/local elements
7498        // obey the local time-stop flags, while an allocated EXCALL runtime
7499        // advances independently. System scenes (Config/Save/Load) depend on
7500        // that distinction while the gameplay scene below them is frozen.
7501        let wipe_active = self.wipe.is_some();
7502        self.render_frame = self.render_frame.wrapping_add(1);
7503        self.local_real_time = self
7504            .local_real_time
7505            .saturating_add(past_real_time.max(0) as i64);
7506        self.local_game_time = self
7507            .local_game_time
7508            .saturating_add(past_game_time.max(0) as i64);
7509        let past_wipe_time = past_game_time.max(0);
7510        self.local_wipe_time = self.local_wipe_time.saturating_add(past_wipe_time as i64);
7511        if let Some(wipe) = self.wipe.as_mut() {
7512            wipe.advance(past_wipe_time);
7513        }
7514        // A completed wipe is torn down by CommandContext::tick_frame(), which
7515        // also performs the original stage[NEXT].reinit(false) semantics.
7516        if self.change_display_mode_proc_cnt > 0 {
7517            self.change_display_mode_proc_cnt -= 1;
7518        }
7519        self.mov.tick(past_real_time);
7520
7521        let excall_counter_id = excall.map(|v| v.counter_list_id);
7522        let excall_frame_action_id = excall.map(|v| v.frame_action_id);
7523        let excall_frame_action_ch_list_id = excall.map(|v| v.frame_action_ch_list_id);
7524        let excall_stage_form_id = excall.map(|v| v.stage_form_id);
7525
7526        // Ordinary/local elements. C++ skips this block while
7527        // m_local.pod.time_stop_flag is set, then still runs the EXCALL block.
7528        if !self.script.time_stop_flag {
7529            self.fog_global.update_time(past_game_time, past_real_time);
7530            self.fog_global.frame();
7531
7532            if !self.script.counter_time_stop_flag {
7533                let mut counter_ids: Vec<u32> = self.counter_lists.keys().copied().collect();
7534                counter_ids.sort_unstable();
7535                for counter_id in counter_ids {
7536                    if Some(counter_id) == excall_counter_id {
7537                        continue;
7538                    }
7539                    let Some(counters) = self.counter_lists.get_mut(&counter_id) else {
7540                        continue;
7541                    };
7542                    for counter in counters {
7543                        counter.update_time(past_game_time, past_real_time);
7544                    }
7545                }
7546            }
7547
7548            if !self.script.frame_action_time_stop_flag {
7549                let mut frame_action_ids: Vec<u32> = self.frame_actions.keys().copied().collect();
7550                frame_action_ids.sort_unstable();
7551                for frame_action_id in frame_action_ids {
7552                    if Some(frame_action_id) == excall_frame_action_id {
7553                        continue;
7554                    }
7555                    let Some(fa) = self.frame_actions.get_mut(&frame_action_id) else {
7556                        continue;
7557                    };
7558                    fa.counter.update_time(past_game_time, past_real_time);
7559                }
7560                let mut frame_action_list_ids: Vec<u32> =
7561                    self.frame_action_lists.keys().copied().collect();
7562                frame_action_list_ids.sort_unstable();
7563                for frame_action_list_id in frame_action_list_ids {
7564                    if Some(frame_action_list_id) == excall_frame_action_ch_list_id {
7565                        continue;
7566                    }
7567                    let Some(list) = self.frame_action_lists.get_mut(&frame_action_list_id) else {
7568                        continue;
7569                    };
7570                    for fa in list {
7571                        fa.counter.update_time(past_game_time, past_real_time);
7572                    }
7573                }
7574            }
7575
7576            let mut mask_list_ids: Vec<u32> = self.mask_lists.keys().copied().collect();
7577            mask_list_ids.sort_unstable();
7578            for mask_list_id in mask_list_ids {
7579                let Some(ml) = self.mask_lists.get_mut(&mask_list_id) else {
7580                    continue;
7581                };
7582                ml.tick_frame(past_game_time.max(0));
7583            }
7584
7585            let mut screen_form_ids: Vec<u32> = self.screen_forms.keys().copied().collect();
7586            screen_form_ids.sort_unstable();
7587            for screen_form_id in screen_form_ids {
7588                let Some(sc) = self.screen_forms.get_mut(&screen_form_id) else {
7589                    continue;
7590                };
7591                sc.tick(past_game_time.max(0), shake_templates);
7592            }
7593
7594            let mut int_event_root_ids: Vec<u32> = self.int_event_roots.keys().copied().collect();
7595            int_event_root_ids.sort_unstable();
7596            for int_event_root_id in int_event_root_ids {
7597                let Some(ev) = self.int_event_roots.get_mut(&int_event_root_id) else {
7598                    continue;
7599                };
7600                ev.update_time(past_game_time, past_real_time);
7601                ev.frame();
7602            }
7603            let mut int_event_list_ids: Vec<u32> = self.int_event_lists.keys().copied().collect();
7604            int_event_list_ids.sort_unstable();
7605            for int_event_list_id in int_event_list_ids {
7606                let Some(events) = self.int_event_lists.get_mut(&int_event_list_id) else {
7607                    continue;
7608                };
7609                for ev in events {
7610                    ev.update_time(past_game_time, past_real_time);
7611                    ev.frame();
7612                }
7613            }
7614
7615            if !self.script.stage_time_stop_flag {
7616                let mut stage_form_ids: Vec<u32> = self.stage_forms.keys().copied().collect();
7617                stage_form_ids.sort_unstable();
7618                for stage_form_id in stage_form_ids {
7619                    if Some(stage_form_id) == excall_stage_form_id {
7620                        continue;
7621                    }
7622                    self.tick_stage_form(
7623                        stage_form_id,
7624                        past_game_time,
7625                        past_real_time,
7626                        wipe_active,
7627                    );
7628                }
7629            }
7630        }
7631
7632        // Original eng_frame.cpp updates EXCALL whenever m_excall.is_ready(),
7633        // regardless of local time_stop/counter_time_stop/frame_action_time_stop/
7634        // stage_time_stop. This is essential for Config animations over a
7635        // deliberately frozen gameplay scene.
7636        if let Some(excall) = excall {
7637            if let Some(counters) = self.counter_lists.get_mut(&excall.counter_list_id) {
7638                for counter in counters {
7639                    counter.update_time(past_game_time, past_real_time);
7640                }
7641            }
7642            if let Some(fa) = self.frame_actions.get_mut(&excall.frame_action_id) {
7643                fa.counter.update_time(past_game_time, past_real_time);
7644            }
7645            if let Some(list) = self
7646                .frame_action_lists
7647                .get_mut(&excall.frame_action_ch_list_id)
7648            {
7649                for fa in list {
7650                    fa.counter.update_time(past_game_time, past_real_time);
7651                }
7652            }
7653            self.tick_stage_form(
7654                excall.stage_form_id,
7655                past_game_time,
7656                past_real_time,
7657                wipe_active,
7658            );
7659        }
7660    }
7661
7662    fn tick_stage_form(
7663        &mut self,
7664        stage_form_id: u32,
7665        past_game_time: i32,
7666        past_real_time: i32,
7667        wipe_active: bool,
7668    ) {
7669        let Some(st) = self.stage_forms.get_mut(&stage_form_id) else {
7670            return;
7671        };
7672
7673        let mut object_stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
7674        object_stage_ids.sort_unstable();
7675        for object_stage_id in object_stage_ids {
7676            if object_stage_id == 2 && !wipe_active {
7677                continue;
7678            }
7679            let embedded_slots = st.embedded_object_slots_by_stage.get(&object_stage_id);
7680            let Some(objs) = st.object_lists.get_mut(&object_stage_id) else {
7681                continue;
7682            };
7683            for (obj_idx, obj) in objs.iter_mut().enumerate() {
7684                if embedded_slots.is_some_and(|slots| slots.contains(&obj_idx)) {
7685                    continue;
7686                }
7687                obj.tick(past_game_time, past_real_time);
7688            }
7689        }
7690
7691        let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
7692        mwnd_stage_ids.sort_unstable();
7693        for mwnd_stage_id in mwnd_stage_ids {
7694            if mwnd_stage_id == 2 && !wipe_active {
7695                continue;
7696            }
7697            let Some(mwnds) = st.mwnd_lists.get_mut(&mwnd_stage_id) else {
7698                continue;
7699            };
7700            for mwnd in mwnds {
7701                for obj in &mut mwnd.button_list {
7702                    obj.tick(past_game_time, past_real_time);
7703                }
7704                for obj in &mut mwnd.face_list {
7705                    obj.tick(past_game_time, past_real_time);
7706                }
7707                for obj in &mut mwnd.object_list {
7708                    obj.tick(past_game_time, past_real_time);
7709                }
7710            }
7711        }
7712
7713        let mut world_stage_ids: Vec<i64> = st.world_lists.keys().copied().collect();
7714        world_stage_ids.sort_unstable();
7715        for world_stage_id in world_stage_ids {
7716            if world_stage_id == 2 && !wipe_active {
7717                continue;
7718            }
7719            let Some(worlds) = st.world_lists.get_mut(&world_stage_id) else {
7720                continue;
7721            };
7722            for w in worlds {
7723                w.update_time(past_game_time, past_real_time);
7724                w.frame();
7725            }
7726        }
7727
7728        let mut effect_stage_ids: Vec<i64> = st.effect_lists.keys().copied().collect();
7729        effect_stage_ids.sort_unstable();
7730        for effect_stage_id in effect_stage_ids {
7731            if effect_stage_id == 2 && !wipe_active {
7732                continue;
7733            }
7734            let Some(effects) = st.effect_lists.get_mut(&effect_stage_id) else {
7735                continue;
7736            };
7737            for effect in effects {
7738                effect.tick(past_game_time.max(0));
7739            }
7740        }
7741
7742        let mut quake_stage_ids: Vec<i64> = st.quake_lists.keys().copied().collect();
7743        quake_stage_ids.sort_unstable();
7744        for quake_stage_id in quake_stage_ids {
7745            if quake_stage_id == 2 && !wipe_active {
7746                continue;
7747            }
7748            let Some(quakes) = st.quake_lists.get_mut(&quake_stage_id) else {
7749                continue;
7750            };
7751            for quake in quakes {
7752                quake.tick(past_game_time.max(0));
7753            }
7754        }
7755    }
7756}
7757
7758#[cfg(test)]
7759mod wipe_stage_tick_tests {
7760    use super::{ExcallTickTargets, GlobalState, StageFormState, WipeState, WorldState};
7761
7762    const TEST_STAGE_FORM_ID: u32 = 49;
7763    const NEXT_STAGE: i64 = 2;
7764
7765    fn next_world_event_time(globals: &GlobalState) -> i32 {
7766        globals.stage_forms[&TEST_STAGE_FORM_ID].world_lists[&NEXT_STAGE][0]
7767            .camera_eye_x
7768            .cur_time
7769    }
7770
7771    #[test]
7772    fn next_stage_events_advance_only_while_wipe_is_active() {
7773        let mut globals = GlobalState::default();
7774        let mut stage = StageFormState::default();
7775        let mut world = WorldState::new(0);
7776        world.camera_eye_x.set_event(100, 1_000, 0, 0, 0);
7777        stage.world_lists.insert(NEXT_STAGE, vec![world]);
7778        globals
7779            .stage_forms
7780            .insert(TEST_STAGE_FORM_ID, Box::new(stage));
7781
7782        globals.tick_frame(10, 10, &[], None);
7783        assert_eq!(next_world_event_time(&globals), 0);
7784
7785        globals.start_wipe(WipeState::new(
7786            TEST_STAGE_FORM_ID,
7787            None,
7788            None,
7789            0,
7790            1_000,
7791            0,
7792            0,
7793            Vec::new(),
7794            i32::MIN,
7795            i32::MAX,
7796            i32::MIN,
7797            i32::MAX,
7798            false,
7799            0,
7800            0,
7801        ));
7802        globals.tick_frame(10, 10, &[], None);
7803        assert_eq!(next_world_event_time(&globals), 10);
7804
7805        globals.finish_wipe();
7806        globals.tick_frame(10, 10, &[], None);
7807        assert_eq!(next_world_event_time(&globals), 10);
7808    }
7809
7810    #[test]
7811    fn main_time_stop_freezes_stage_but_not_wipe_clock() {
7812        let mut globals = GlobalState::default();
7813        let mut stage = StageFormState::default();
7814        let mut world = WorldState::new(0);
7815        world.camera_eye_x.set_event(100, 1_000, 0, 0, 0);
7816        stage.world_lists.insert(NEXT_STAGE, vec![world]);
7817        globals
7818            .stage_forms
7819            .insert(TEST_STAGE_FORM_ID, Box::new(stage));
7820        globals.start_wipe(WipeState::new(
7821            TEST_STAGE_FORM_ID,
7822            None,
7823            None,
7824            0,
7825            1_000,
7826            0,
7827            0,
7828            Vec::new(),
7829            i32::MIN,
7830            i32::MAX,
7831            i32::MIN,
7832            i32::MAX,
7833            false,
7834            0,
7835            0,
7836        ));
7837        globals.script.time_stop_flag = true;
7838
7839        globals.tick_frame(10, 10, &[], None);
7840
7841        assert_eq!(next_world_event_time(&globals), 0);
7842        assert_eq!(globals.local_wipe_time, 10);
7843    }
7844
7845    #[test]
7846    fn stage_time_stop_freezes_stage_events_during_wipe() {
7847        let mut globals = GlobalState::default();
7848        let mut stage = StageFormState::default();
7849        let mut world = WorldState::new(0);
7850        world.camera_eye_x.set_event(100, 1_000, 0, 0, 0);
7851        stage.world_lists.insert(NEXT_STAGE, vec![world]);
7852        globals
7853            .stage_forms
7854            .insert(TEST_STAGE_FORM_ID, Box::new(stage));
7855        globals.start_wipe(WipeState::new(
7856            TEST_STAGE_FORM_ID,
7857            None,
7858            None,
7859            0,
7860            1_000,
7861            0,
7862            0,
7863            Vec::new(),
7864            i32::MIN,
7865            i32::MAX,
7866            i32::MIN,
7867            i32::MAX,
7868            false,
7869            0,
7870            0,
7871        ));
7872        globals.script.stage_time_stop_flag = true;
7873
7874        globals.tick_frame(10, 10, &[], None);
7875
7876        assert_eq!(next_world_event_time(&globals), 0);
7877    }
7878
7879    #[test]
7880    fn excall_stage_advances_while_local_time_is_stopped() {
7881        const FRONT_STAGE: i64 = 1;
7882        const EXCALL_STAGE_FORM_ID: u32 = TEST_STAGE_FORM_ID ^ 0x4000;
7883
7884        let mut globals = GlobalState::default();
7885
7886        let mut normal_stage = StageFormState::default();
7887        let mut normal_world = WorldState::new(0);
7888        normal_world.camera_eye_x.set_event(100, 1_000, 0, 0, 0);
7889        normal_stage
7890            .world_lists
7891            .insert(FRONT_STAGE, vec![normal_world]);
7892        globals
7893            .stage_forms
7894            .insert(TEST_STAGE_FORM_ID, Box::new(normal_stage));
7895
7896        let mut excall_stage = StageFormState::default();
7897        let mut excall_world = WorldState::new(0);
7898        excall_world.camera_eye_x.set_event(100, 1_000, 0, 0, 0);
7899        excall_stage
7900            .world_lists
7901            .insert(FRONT_STAGE, vec![excall_world]);
7902        globals
7903            .stage_forms
7904            .insert(EXCALL_STAGE_FORM_ID, Box::new(excall_stage));
7905
7906        globals.script.time_stop_flag = true;
7907        globals.tick_frame(
7908            10,
7909            10,
7910            &[],
7911            Some(ExcallTickTargets {
7912                counter_list_id: 0x7000_0001,
7913                frame_action_id: 0x7000_0002,
7914                frame_action_ch_list_id: 0x7000_0003,
7915                stage_form_id: EXCALL_STAGE_FORM_ID,
7916            }),
7917        );
7918
7919        assert_eq!(
7920            globals.stage_forms[&TEST_STAGE_FORM_ID].world_lists[&FRONT_STAGE][0]
7921                .camera_eye_x
7922                .cur_time,
7923            0
7924        );
7925        assert_eq!(
7926            globals.stage_forms[&EXCALL_STAGE_FORM_ID].world_lists[&FRONT_STAGE][0]
7927                .camera_eye_x
7928                .cur_time,
7929            10
7930        );
7931    }
7932}