Skip to main content

siglus_scene_vm/runtime/
mod.rs

1//! Runtime scaffolding for command execution.
2//!
3//! This layer provides shared dispatch and runtime state for VM forms and
4//! named commands.
5
6pub mod commands;
7pub mod constants;
8pub mod forms;
9pub mod graphics;
10pub mod input;
11pub mod opcode;
12
13pub use opcode::OpCode;
14pub mod gan;
15pub mod game_display_info;
16pub mod game_title;
17pub mod globals;
18pub mod int_event;
19pub mod net;
20pub mod native_ui;
21pub mod tables;
22pub mod tonecurve;
23pub mod ui;
24pub mod unknown;
25pub mod wait;
26use crate::runtime::forms::codes::syscom_op;
27use crate::runtime::forms::syscom as syscom_form;
28
29use anyhow::Result;
30use std::collections::{HashMap, HashSet};
31use std::sync::atomic::{AtomicU64, Ordering};
32use std::sync::Arc;
33
34use crate::assets::RgbaImage;
35use crate::audio::{AudioHub, BgmEngine, KoeEngine, PcmEngine, SeEngine};
36use crate::image_manager::{ImageId, ImageManager};
37use crate::layer::{
38    ClipRect, LayerId, LayerManager, RenderSprite, Sprite, SpriteFit, SpriteId, SpriteRuntimeLight,
39    SpriteSizeMode,
40};
41use crate::movie::MovieManager;
42use crate::soft_render;
43use crate::text_render::{embedded_default_font_names, FontCache, TextStyle};
44use siglus_assets::scene_pck::{find_scene_pck_in_project, ScenePck, ScenePckDecodeOptions};
45use std::fs;
46use std::path::{Path, PathBuf};
47
48#[derive(Debug, Clone)]
49pub enum Value {
50    Int(i64),
51    Str(String),
52    /// An element chain as raw i32 codes (as stored on the VM int stack).
53    Element(Vec<i32>),
54    /// A nested list value (FM_LIST).
55    List(Vec<Value>),
56    /// A named argument (id -> value), used by some engine commands.
57    NamedArg {
58        id: i32,
59        value: Box<Value>,
60    },
61}
62
63impl Value {
64    pub fn as_i64(&self) -> Option<i64> {
65        match self {
66            Value::Int(v) => Some(*v),
67            Value::NamedArg { value, .. } => value.as_i64(),
68            _ => None,
69        }
70    }
71
72    pub fn named_id(&self) -> Option<i32> {
73        match self {
74            Value::NamedArg { id, .. } => Some(*id),
75            _ => None,
76        }
77    }
78
79    pub fn unwrap_named(&self) -> &Value {
80        match self {
81            Value::NamedArg { value, .. } => value.as_ref(),
82            _ => self,
83        }
84    }
85    pub fn as_str(&self) -> Option<&str> {
86        match self {
87            Value::Str(s) => Some(s.as_str()),
88            Value::NamedArg { value, .. } => value.as_str(),
89            _ => None,
90        }
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct Command {
96    pub name: String,
97    /// Optional numeric code for VM forms.
98    pub code: Option<opcode::OpCode>,
99    pub args: Vec<Value>,
100}
101
102
103#[derive(Debug, Clone)]
104struct MsgBackLayoutEntry {
105    history_index: usize,
106    text: String,
107    total_pos: i32,
108    height: i32,
109}
110
111#[derive(Debug, Clone)]
112struct MsgBackSeparatorLayout {
113    file: Option<String>,
114    total_pos: i32,
115    height: i32,
116}
117
118#[derive(Debug, Clone, Default)]
119struct MsgBackLayout {
120    entries: Vec<MsgBackLayoutEntry>,
121    separators: Vec<MsgBackSeparatorLayout>,
122    total_height: i32,
123}
124
125/// State used by EXCALL runtime helpers.
126///
127/// We intentionally keep these names offset-based instead of guessing their meaning.
128#[derive(Debug, Default, Clone)]
129pub struct ExcallCompatState {
130    pub ready: bool,
131    pub ex_call_flag: bool,
132    pub flag_204: bool,
133    pub flag_2148: bool,
134    pub script_proc_requested: bool,
135    pub script_proc_pop_requested: bool,
136}
137
138/// Optional external handler for numeric forms.
139///
140/// The project can keep game-specific implementations (e.g. SCREEN/MSGBK)
141/// outside this crate, while still letting the VM dispatch through here.
142pub trait ExternalFormHandler: Send + Sync {
143    /// Return true if the form ID was handled.
144    fn dispatch_form(
145        &self,
146        ctx: &mut CommandContext,
147        form_id: u32,
148        args: &[Value],
149    ) -> anyhow::Result<bool>;
150}
151/// Cooperative script-process boundary, mirroring Siglus' `TNM_PROC_TYPE_*`
152/// model at the VM/runtime boundary.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum ProcKind {
155    Script,
156    Disp,
157    Frame,
158    Command,
159    MessageBlock,
160    MessageWait,
161    KeyWait,
162    TimeWait,
163    MovieWait,
164    WipeWait,
165    AudioWait,
166    EventWait,
167    Selection,
168    SystemModal,
169}
170
171#[derive(Debug, Clone, Default)]
172pub struct VmCallMeta {
173    pub element: Vec<i32>,
174    pub al_id: i64,
175    pub ret_form: i64,
176}
177
178#[derive(Debug, Clone)]
179pub struct DebugActiveTextureEntry {
180    pub image_id: ImageId,
181    pub width: u32,
182    pub height: u32,
183    pub source_label: String,
184    pub submitted_this_frame: bool,
185    pub visible_refs: usize,
186    pub total_refs: usize,
187    pub ref_summary: String,
188}
189
190#[derive(Debug, Default, Clone)]
191struct DebugActiveTextureAccum {
192    width: u32,
193    height: u32,
194    source_label: String,
195    submitted_this_frame: bool,
196    visible_refs: usize,
197    total_refs: usize,
198    ref_labels: Vec<String>,
199}
200
201fn sg_mwnd_state_trace_runtime(
202    scene: &str,
203    scene_no: &str,
204    line: i64,
205    reason: &str,
206    stage_idx: i64,
207    mwnd_idx: usize,
208    old_open: bool,
209    new_open: bool,
210    m: &globals::MwndState,
211) {
212    if std::env::var_os("SG_DEBUG").is_none() {
213        return;
214    }
215    eprintln!(
216        "[SG_DEBUG][MWND_STATE_TRACE] scene={} scene_no={} line={} reason={} stage={} mwnd={} old_open={} new_open={} buttons={} faces={} objects={} waku={} filter={} pos={:?} size={:?} open_anim=({}, {}) close_anim=({}, {}) selection={} msg_len={} name_len={}",
217        scene,
218        scene_no,
219        line,
220        reason,
221        stage_idx,
222        mwnd_idx,
223        old_open,
224        new_open,
225        m.button_list.len(),
226        m.face_list.len(),
227        m.object_list.len(),
228        if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
229        if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
230        m.window_pos,
231        m.window_size,
232        m.open_anime_type,
233        m.open_anime_time,
234        m.close_anime_type,
235        m.close_anime_time,
236        m.selection.is_some(),
237        m.msg_text.len(),
238        m.name_text.len(),
239    );
240}
241
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
244pub enum RuntimeSaveKind {
245    Normal,
246    Quick,
247    End,
248    Inner,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub struct RuntimeSaveRequest {
253    pub kind: RuntimeSaveKind,
254    pub index: usize,
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub struct RuntimeLoadRequest {
259    pub kind: RuntimeSaveKind,
260    pub index: usize,
261}
262
263/// Engine-side cache mirroring the C++ `S_tnm_local_save` (a.k.a. `Gp_eng->m_local_save`).
264/// Populated by GLOBAL_SAVEPOINT (`tnm_save_local`) and after a successful load
265/// (`tnm_load_local_on_file`). All save-to-file paths (normal / quick / end) must
266/// pull from this cache instead of re-serializing the live runtime.
267#[derive(Debug, Clone, Default)]
268pub struct LocalSaveSnapshot {
269    pub save_id: [u16; 7],
270    pub append_dir: String,
271    pub append_name: String,
272    pub save_scene_title: String,
273    pub save_msg: String,
274    pub save_full_msg: String,
275    pub local_stream: Vec<u8>,
276    pub local_ex_stream: Vec<u8>,
277    pub sel_saves: Vec<crate::original_save::OriginalLocalSaveEnvelope>,
278}
279
280#[derive(Debug, Clone, Copy)]
281struct MouseCursorFrameRuntime {
282    image_id: ImageId,
283    hot_x: i32,
284    hot_y: i32,
285}
286
287#[derive(Debug, Clone)]
288struct MouseCursorRuntime {
289    frames: Vec<MouseCursorFrameRuntime>,
290    anime_speed_ms: i64,
291}
292
293pub struct CommandContext {
294    pub project_dir: PathBuf,
295
296    pub images: ImageManager,
297    pub layers: LayerManager,
298    /// 1x1 white sprite used for screen-space overlays (filters, etc.).
299    pub solid_white: ImageId,
300
301    pub audio: AudioHub,
302
303    pub bgm: BgmEngine,
304    pub koe: KoeEngine,
305    pub pcm: PcmEngine,
306    pub se: SeEngine,
307
308    pub movie: MovieManager,
309
310    /// Runtime numeric constants (form/element/op codes).
311    pub ids: constants::RuntimeConstants,
312
313    /// Graphics runtime state for stage/object sprite binding.
314    pub gfx: graphics::GfxRuntime,
315
316    /// UI runtime (text window, message waits, etc.).
317    pub ui: ui::UiRuntime,
318    /// Shared font cache for stage/object text rendering.
319    pub font_cache: FontCache,
320
321    /// Runtime-visible input state (button manager, waits, runtime systems).
322    pub input: input::InputState,
323    /// Script-visible input state (`Gp_script_input` in the original engine).
324    pub script_input: input::InputState,
325
326    /// Current render target size (used for UI layout).
327    pub screen_w: u32,
328    pub screen_h: u32,
329
330    /// VM blocking state (WAIT / WAIT_KEY).
331    pub wait: wait::VmWait,
332
333    /// Cooperative proc boundary generation. Form handlers bump this when they
334    /// perform an original-engine proc switch/push.
335    proc_generation: u64,
336    last_proc_kind: ProcKind,
337
338    /// Lightweight network/browser helper mirroring the engine's `tnm_net` slot.
339    pub net: net::TnmNet,
340
341    /// Gameexe-driven asset tables (CGTABLE / DATABASE / THUMBTABLE).
342    pub tables: tables::AssetTables,
343
344    /// Value stack used by form handlers to return results.
345    pub stack: Vec<Value>,
346
347    pub unknown: unknown::UnknownOpRecorder,
348
349    pub globals: globals::GlobalState,
350    pub tonecurve: tonecurve::ToneCurveRuntime,
351
352    pub excall_state: ExcallCompatState,
353
354    /// Last fully presented scene list before wipe composition.
355    pub last_presented_render_list: Vec<RenderSprite>,
356    /// Offscreen target image for the front/old stage during dual-source wipes.
357    pub wipe_front_rt_image: Option<ImageId>,
358    /// Offscreen target image for the next/new stage during dual-source wipes.
359    pub wipe_next_rt_image: Option<ImageId>,
360    /// Legacy runtime slot for overlay intermediate images. GPU overlay composition now bypasses it.
361    pub overlay_rt_image: Option<ImageId>,
362
363    mouse_cursor_cache: HashMap<(i64, String), MouseCursorRuntime>,
364
365    /// Optional project-provided form handler (game-specific).
366    pub external_forms: Option<Arc<dyn ExternalFormHandler>>,
367
368    /// Optional platform-native UI backend used by mobile ports.
369    pub native_ui_backend: Option<Arc<dyn native_ui::NativeUiBackend>>,
370    pub native_ui: native_ui::NativeUiRuntime,
371
372    /// Current scene number tracked by the VM.
373    pub current_scene_no: Option<i64>,
374    /// Current scene name tracked by the VM.
375    pub current_scene_name: Option<String>,
376    /// Current source line tracked by the VM (`CD_NL`).
377    pub current_line_no: i64,
378
379    /// Current VM-originated form call metadata. Form handlers read this instead of
380    /// relying on trailing wrapper arguments.
381    pub vm_call: Option<VmCallMeta>,
382
383    /// Set by concrete message/voice command handlers when original C++ consumes
384    /// the following read-flag integer through Gp_lexer->pop_ret<int>().
385    pending_read_flag_no: bool,
386    pending_selbtn_read_flag_no: bool,
387
388    /// Deferred VM-owned save request. The form handler can only see CommandContext;
389    /// the VM consumes this after the command returns so the saved stream includes
390    /// the current lexer pc and stacks.
391    pending_runtime_save: Option<RuntimeSaveRequest>,
392    /// Deferred VM-owned load request, consumed by SceneVm after the command returns.
393    pending_runtime_load: Option<RuntimeLoadRequest>,
394    runtime_load_completed: bool,
395
396    /// Engine-equivalent of `Gp_eng->m_local_save`. Built at GLOBAL_SAVEPOINT and
397    /// refreshed by `tnm_load_local_on_file`-equivalent load. Save-to-file dispatch
398    /// reads from here so the saved stream reflects the savepoint, not the live
399    /// menu/UI state when the user picks a slot.
400    pub local_save_snapshot: Option<LocalSaveSnapshot>,
401
402    /// Set when the engine wants the next safe boundary (post-command, pre-next-step)
403    /// to (re)build `local_save_snapshot`. Mirrors C++ `tnm_msg_proc_start_msg_block`
404    /// calling `tnm_init_local_save` + `tnm_set_save_point` at message-block start.
405    /// Form handlers can't build the snapshot themselves (they don't see SceneVm),
406    /// so they request via this flag and the VM drains it at a safe point.
407    pub pending_auto_savepoint: bool,
408
409    frame_clock_last: Option<crate::platform_time::Instant>,
410    last_button_hover_sound_pos: Option<(i32, i32)>,
411    suppress_next_right_syscom_open: bool,
412}
413
414impl CommandContext {
415    pub fn sync_script_input_from_runtime(&mut self) {
416        self.script_input = self.input.clone();
417    }
418
419    pub fn proc_generation(&self) -> u64 {
420        self.proc_generation
421    }
422
423    pub fn request_read_flag_no(&mut self) {
424        self.pending_read_flag_no = true;
425    }
426
427    pub fn request_read_flag_no_for_selbtn(&mut self) {
428        self.pending_read_flag_no = true;
429        self.pending_selbtn_read_flag_no = true;
430    }
431
432    pub fn take_read_flag_no_request(&mut self) -> bool {
433        let requested = std::mem::take(&mut self.pending_read_flag_no);
434        if !requested {
435            self.pending_selbtn_read_flag_no = false;
436        }
437        requested
438    }
439
440    pub fn submit_read_flag_no(&mut self, value: i32) {
441        if std::mem::take(&mut self.pending_selbtn_read_flag_no) {
442            self.globals.selbtn.read_flag_flag_no = value as i64;
443        }
444    }
445
446    pub fn request_runtime_save(&mut self, kind: RuntimeSaveKind, index: usize) {
447        self.pending_runtime_save = Some(RuntimeSaveRequest { kind, index });
448    }
449
450    /// Form-handler entrypoint matching C++ `tnm_msg_proc_start_msg_block`'s
451    /// `tnm_init_local_save` + (gated) `tnm_set_save_point` calls. Honors the
452    /// `dont_set_save_point` script flag (suspends auto SAVEPOINT for special
453    /// blocks). The VM drains the request at the next safe boundary and runs
454    /// `build_local_save_snapshot`.
455    pub fn request_auto_savepoint(&mut self) {
456        if self.globals.script.dont_set_save_point {
457            return;
458        }
459        self.pending_auto_savepoint = true;
460    }
461
462    pub fn take_pending_auto_savepoint(&mut self) -> bool {
463        std::mem::take(&mut self.pending_auto_savepoint)
464    }
465
466    pub fn request_runtime_load(&mut self, kind: RuntimeSaveKind, index: usize) {
467        self.pending_runtime_load = Some(RuntimeLoadRequest { kind, index });
468    }
469
470    pub fn take_runtime_save_request(&mut self) -> Option<RuntimeSaveRequest> {
471        self.pending_runtime_save.take()
472    }
473
474    pub fn take_runtime_load_request(&mut self) -> Option<RuntimeLoadRequest> {
475        self.pending_runtime_load.take()
476    }
477
478    pub fn begin_runtime_load_apply(&mut self) {
479        // Mirror C++ `tnm_finish_local()` + `tnm_reinit_local(false)`. The loaded
480        // local stream re-populates per-scene state via `parse_original_local_stream`,
481        // but anything that isn't *always* present in the stream (or that lives outside
482        // it entirely - layers, UI, focus, menu, pending button actions, etc.) must be
483        // torn down here so the save/load menu we're loading away from can't leak
484        // through to the restored scene. Without this, a stage that the live save menu
485        // populated but the snapshot omits would survive across the load.
486        self.layers.clear_all();
487        self.gfx = graphics::GfxRuntime::default();
488        self.ui = ui::UiRuntime::default();
489        self.wait = wait::VmWait::default();
490        self.stack.clear();
491        self.last_presented_render_list.clear();
492        self.wipe_front_rt_image = None;
493        self.wipe_next_rt_image = None;
494        self.overlay_rt_image = None;
495        self.vm_call = None;
496        self.pending_read_flag_no = false;
497        self.pending_selbtn_read_flag_no = false;
498        self.frame_clock_last = None;
499        self.last_button_hover_sound_pos = None;
500
501        self.globals.focused_editbox = None;
502        self.globals.focused_stage_group = None;
503        self.globals.focused_stage_mwnd = None;
504        self.globals.current_stage_object = None;
505        self.globals.current_object_chain = None;
506        self.globals.pending_button_actions.clear();
507        self.globals.pending_frame_action_finishes.clear();
508        self.globals.capture_for_object_image = None;
509        self.globals.save_thumb_capture_image = None;
510        self.globals.selbtn = globals::BtnSelectRuntimeState::default();
511        self.globals.syscom.pending_proc = None;
512        self.globals.syscom.menu_open = false;
513        self.globals.syscom.menu_kind = None;
514        self.globals.syscom.menu_result = None;
515        self.globals.syscom.msg_back_open = false;
516        self.globals.syscom.msg_back_proc_initialized = false;
517        self.globals.system.messagebox_modal = None;
518        self.globals.system.messagebox_modal_result = None;
519        self.globals.finish_wipe();
520
521        // Per-scene state that lives in `globals` and is reconstructed from the
522        // loaded local stream. Wipe before parsing so that snapshot entries that
523        // are simply *absent* (the snapshot has no mask list, no editbox, etc.)
524        // truly become absent post-load instead of inheriting from the menu.
525        self.globals.stage_forms.clear();
526        self.globals.screen_forms.clear();
527        self.globals.counter_lists.clear();
528        self.globals.frame_actions.clear();
529        self.globals.frame_action_lists.clear();
530        self.globals.mask_lists.clear();
531        self.globals.pcm_event_lists.clear();
532        self.globals.editbox_lists.clear();
533        self.globals.msgbk_forms.clear();
534    }
535
536    pub fn mark_runtime_load_completed(&mut self) {
537        self.runtime_load_completed = true;
538    }
539
540    pub fn take_runtime_load_completed(&mut self) -> bool {
541        std::mem::take(&mut self.runtime_load_completed)
542    }
543
544    pub fn needs_continuous_frame(&self) -> bool {
545        fn frame_action_needs_tick(fa: &globals::ObjectFrameActionState) -> bool {
546            fa.counter.is_running() || (!fa.cmd_name.is_empty() && !fa.end_flag)
547        }
548
549        fn screen_effect_needs_tick(e: &globals::ScreenEffectState) -> bool {
550            e.x.check_event()
551                || e.y.check_event()
552                || e.z.check_event()
553                || e.mono.check_event()
554                || e.reverse.check_event()
555                || e.bright.check_event()
556                || e.dark.check_event()
557                || e.color_r.check_event()
558                || e.color_g.check_event()
559                || e.color_b.check_event()
560                || e.color_rate.check_event()
561                || e.color_add_r.check_event()
562                || e.color_add_g.check_event()
563                || e.color_add_b.check_event()
564        }
565
566        fn object_needs_tick(obj: &globals::ObjectState) -> bool {
567            obj.any_event_active()
568                || frame_action_needs_tick(&obj.frame_action)
569                || obj.frame_action_ch.iter().any(frame_action_needs_tick)
570                || obj.movie.playing
571                || obj.gan.is_active()
572                || obj.runtime.child_objects.iter().any(object_needs_tick)
573        }
574
575        if self.wait.needs_runtime_poll() {
576            return true;
577        }
578        if self
579            .ui
580            .needs_continuous_frame(&self.globals.script, &self.globals.syscom)
581        {
582            return true;
583        }
584        if self.globals.mov.playing || self.globals.wipe.is_some() {
585            return true;
586        }
587        if self.custom_mouse_cursor_needs_tick() {
588            return true;
589        }
590        if self.globals.pending_frame_action_finishes.is_empty() == false
591            || self.globals.pending_button_actions.is_empty() == false
592        {
593            return true;
594        }
595        if self
596            .globals
597            .counter_lists
598            .values()
599            .any(|v| v.iter().any(|c| c.is_running()))
600        {
601            return true;
602        }
603        if self
604            .globals
605            .int_event_roots
606            .values()
607            .any(|e| e.check_event())
608            || self
609                .globals
610                .int_event_lists
611                .values()
612                .any(|v| v.iter().any(|e| e.check_event()))
613        {
614            return true;
615        }
616        if self
617            .globals
618            .frame_actions
619            .values()
620            .any(frame_action_needs_tick)
621            || self
622                .globals
623                .frame_action_lists
624                .values()
625                .any(|v| v.iter().any(frame_action_needs_tick))
626        {
627            return true;
628        }
629        if self.globals.screen_forms.values().any(|screen| {
630            screen.effect_list.iter().any(screen_effect_needs_tick)
631                || screen.quake_list.iter().any(|q| q.until.is_some())
632                || screen.shake.until.is_some()
633        }) {
634            return true;
635        }
636        let mwnd_ui_state = self
637            .ui
638            .current_mwnd_window_render_state(self.screen_w, self.screen_h);
639        self.globals.stage_forms.values().any(|stage| {
640            stage.object_lists.iter().any(|(&stage_idx, list)| {
641                list.iter().enumerate().any(|(obj_idx, obj)| {
642                    !stage.is_embedded_object_slot(stage_idx, obj_idx) && object_needs_tick(obj)
643                })
644            }) || stage.mwnd_lists.values().any(|list| {
645                list.iter().any(|m| {
646                    let Some((window_x, window_y)) = m.window_pos else {
647                        return false;
648                    };
649                    let Some((window_w, window_h)) = m.window_size else {
650                        return false;
651                    };
652                    if window_w <= 0 || window_h <= 0 {
653                        return false;
654                    }
655                    let visible_or_animating = m.open
656                        || mwnd_ui_state.map_or(false, |ui| {
657                            ui.x as i64 == window_x
658                                && ui.y as i64 == window_y
659                                && ui.w as i64 == window_w
660                                && ui.h as i64 == window_h
661                        });
662                    visible_or_animating
663                        && (m.object_list.iter().any(object_needs_tick)
664                            || m.button_list.iter().any(object_needs_tick)
665                            || m.face_list.iter().any(object_needs_tick))
666                })
667            })
668        })
669    }
670
671    pub fn last_proc_kind(&self) -> ProcKind {
672        self.last_proc_kind
673    }
674
675    pub fn request_proc_boundary(&mut self, kind: ProcKind) {
676        self.last_proc_kind = kind;
677        self.proc_generation = self.proc_generation.wrapping_add(1);
678    }
679
680    pub fn request_disp_proc_boundary(&mut self) {
681        self.request_proc_boundary(ProcKind::Disp);
682    }
683
684    pub fn request_message_block_proc_boundary(&mut self) {
685        self.request_proc_boundary(ProcKind::MessageBlock);
686    }
687
688    pub fn request_message_wait_proc_boundary(&mut self) {
689        self.request_proc_boundary(ProcKind::MessageWait);
690    }
691
692    pub fn request_wait_proc_boundary(&mut self, kind: ProcKind) {
693        self.request_proc_boundary(kind);
694    }
695
696    pub fn notify_wait_key(&mut self) -> bool {
697        let wipe_skipped = {
698            let wait = &mut self.wait;
699            let globals = &mut self.globals;
700            wait.notify_key(globals, &self.ids)
701        };
702        self.finish_skipped_movie_waits();
703        if wipe_skipped {
704            self.globals.finish_wipe();
705        }
706        wipe_skipped
707    }
708
709    pub fn notify_movie_wait_down_up(&mut self, result: i64) -> bool {
710        let skipped = {
711            let wait = &mut self.wait;
712            let globals = &mut self.globals;
713            wait.notify_movie_down_up(globals, &self.ids, result)
714        };
715        if skipped {
716            if sg_debug_enabled() {
717                eprintln!("[SG_DEBUG][WAIT_KEY] down_up result={}", result);
718            }
719            self.finish_skipped_movie_waits();
720            if !self.globals.mov.playing && self.globals.mov.file_name.is_some() {
721                self.close_global_movie_runtime();
722            }
723        }
724        skipped
725    }
726
727    fn should_wheel_advance_message(&self) -> bool {
728        const GET_WHEEL_NEXT_MESSAGE_ONOFF: i32 = 305;
729        self.globals
730            .syscom
731            .config_int
732            .get(&GET_WHEEL_NEXT_MESSAGE_ONOFF)
733            .copied()
734            .unwrap_or(1)
735            != 0
736    }
737
738    fn should_stop_koe_on_advance(&self) -> bool {
739        const GET_KOE_DONT_STOP_ONOFF: i32 = 308;
740        let syscom_dont_stop = self
741            .globals
742            .syscom
743            .config_int
744            .get(&GET_KOE_DONT_STOP_ONOFF)
745            .copied()
746            .unwrap_or(0)
747            != 0;
748        let script = &self.globals.script;
749        let mut dont_stop = syscom_dont_stop || script.koe_dont_stop_on_flag;
750        if script.koe_dont_stop_off_flag {
751            dont_stop = false;
752        }
753        !dont_stop
754    }
755
756    fn is_modifier_key(k: input::VmKey) -> bool {
757        matches!(k, input::VmKey::Shift | input::VmKey::Alt)
758    }
759
760    fn sync_editbox_runtime(&mut self) {
761        let sw = self.screen_w as i32;
762        let sh = self.screen_h as i32;
763        let display_cnt = self.globals.change_display_mode_proc_cnt;
764        for list in self.globals.editbox_lists.values_mut() {
765            for eb in &mut list.boxes {
766                eb.update_rect(sw, sh);
767                eb.frame(display_cnt);
768            }
769        }
770        if let Some((form_id, idx)) = self.globals.focused_editbox {
771            let keep = self
772                .globals
773                .editbox_lists
774                .get(&form_id)
775                .and_then(|list| list.boxes.get(idx))
776                .map(|eb| eb.created && eb.visible)
777                .unwrap_or(false);
778            if !keep {
779                self.globals.focused_editbox = None;
780            }
781        }
782    }
783
784    fn toggle_screen_size_mode_for_editbox(&mut self) {
785        const GET_WINDOW_MODE: i32 = syscom_op::GET_WINDOW_MODE;
786        let current = self
787            .globals
788            .syscom
789            .config_int
790            .get(&GET_WINDOW_MODE)
791            .copied()
792            .unwrap_or(0);
793        let next = if current == 0 { 1 } else { 0 };
794        self.globals.syscom.config_int.insert(GET_WINDOW_MODE, next);
795        self.globals.change_display_mode_proc_cnt =
796            self.globals.change_display_mode_proc_cnt.max(2);
797    }
798
799    fn move_editbox_focus(&mut self, forward: bool) {
800        let Some((form_id, idx)) = self.globals.focused_editbox else {
801            return;
802        };
803        let Some(list) = self.globals.editbox_lists.get(&form_id) else {
804            return;
805        };
806        let len = list.boxes.len();
807        if len == 0 {
808            return;
809        }
810        let mut cur = idx;
811        for _ in 0..len {
812            cur = if forward {
813                (cur + 1) % len
814            } else {
815                (cur + len - 1) % len
816            };
817            if let Some(eb) = list.boxes.get(cur) {
818                if eb.created {
819                    self.globals.focused_editbox = Some((form_id, cur));
820                    return;
821                }
822            }
823        }
824    }
825
826    /// Advance the current message wait.
827    ///
828    /// Returns true when the input was consumed only to reveal the rest of the
829    /// typewriter text. In that case the VM key wait must stay blocked.
830    fn advance_message_wait(&mut self, allow: bool) -> bool {
831        if !allow || !self.ui.mwnd.msg.waiting {
832            return false;
833        }
834        if !self.ui.message_wait_text_fully_revealed() {
835            self.ui.reveal_message_now();
836            return true;
837        }
838        let clear_message_window = self.ui.end_wait_message();
839        if clear_message_window {
840            self.clear_current_mwnd_after_wait();
841        }
842        if self.should_stop_koe_on_advance() {
843            let _ = self.se.stop(None);
844            let _ = self.pcm.stop_all(None);
845        }
846        false
847    }
848
849    fn clear_current_mwnd_after_wait(&mut self) {
850        let default_form_id = if self.ids.form_global_stage != 0 {
851            self.ids.form_global_stage
852        } else {
853            constants::global_form::STAGE_ALT
854        };
855        let target = self.globals.focused_stage_mwnd.unwrap_or((
856            default_form_id,
857            self.globals.current_mwnd_stage_idx,
858            self.globals.current_mwnd_no.unwrap_or(0),
859        ));
860        let (form_id, stage_idx, mwnd_idx) = target;
861        if let Some(m) = self
862            .globals
863            .stage_forms
864            .get_mut(&form_id)
865            .and_then(|st| st.mwnd_lists.get_mut(&stage_idx))
866            .and_then(|list| list.get_mut(mwnd_idx))
867        {
868            m.msg_text.clear();
869            m.name_text.clear();
870            m.key_icon_appear = false;
871            m.key_icon_pos = None;
872            m.text_dirty = false;
873        }
874        self.ui.clear_name();
875    }
876    pub fn new(project_dir: PathBuf) -> Self {
877        let mut unknown = unknown::UnknownOpRecorder::default();
878        let tables = tables::AssetTables::load(&project_dir, &mut unknown);
879
880        let ids = constants::RuntimeConstants::default();
881
882        let audio = AudioHub::new();
883        let mut images = ImageManager::new(project_dir.clone());
884        let solid_white = images.solid_rgba((255, 255, 255, 255));
885        let tonecurve = tonecurve::ToneCurveRuntime::new(&project_dir);
886
887        let mut ctx = Self {
888            images,
889            layers: LayerManager::default(),
890            audio,
891            bgm: BgmEngine::new(project_dir.clone()),
892            koe: KoeEngine::new(project_dir.clone()),
893            pcm: PcmEngine::new(project_dir.clone()),
894            se: SeEngine::new(project_dir.clone()),
895            movie: MovieManager::new(project_dir.clone()),
896            project_dir,
897            solid_white,
898            tables,
899            stack: Vec::new(),
900            unknown,
901            ids,
902            gfx: graphics::GfxRuntime::default(),
903            ui: ui::UiRuntime::default(),
904            font_cache: FontCache::new(),
905            input: input::InputState::default(),
906            script_input: input::InputState::default(),
907            wait: wait::VmWait::default(),
908            proc_generation: 0,
909            last_proc_kind: ProcKind::Script,
910            net: net::TnmNet::default(),
911
912            screen_w: 1280,
913            screen_h: 720,
914            globals: globals::GlobalState::default(),
915            tonecurve,
916            excall_state: ExcallCompatState::default(),
917            last_presented_render_list: Vec::new(),
918            wipe_front_rt_image: None,
919            wipe_next_rt_image: None,
920            overlay_rt_image: None,
921            mouse_cursor_cache: HashMap::new(),
922            external_forms: None,
923            native_ui_backend: None,
924            native_ui: native_ui::NativeUiRuntime::default(),
925            current_scene_no: None,
926            current_scene_name: None,
927            current_line_no: -1,
928            vm_call: None,
929            pending_read_flag_no: false,
930            pending_selbtn_read_flag_no: false,
931            pending_runtime_save: None,
932            pending_runtime_load: None,
933            runtime_load_completed: false,
934            local_save_snapshot: None,
935            pending_auto_savepoint: false,
936            frame_clock_last: None,
937            last_button_hover_sound_pos: None,
938            suppress_next_right_syscom_open: false,
939        };
940        ctx.apply_gameexe_runtime_defaults();
941        ctx
942    }
943
944    fn apply_gameexe_runtime_defaults(&mut self) {
945        self.globals.script.cursor_no = self.mouse_cursor_default_no();
946        self.globals.script.font_bold = self.tables.font_defaults.futoku;
947        self.globals.script.font_shadow = self.tables.font_defaults.shadow;
948        let text = self.gameexe_color(self.tables.mwnd_render.moji_color);
949        let shadow = self.gameexe_color(self.tables.mwnd_render.shadow_color);
950        let fuchi = (self.tables.mwnd_render.fuchi_color >= 0)
951            .then_some(self.gameexe_color(self.tables.mwnd_render.fuchi_color));
952        self.ui.set_text_colors_full(text, shadow, fuchi);
953    }
954
955    fn gameexe_color(&self, color_no: i64) -> (u8, u8, u8) {
956        if color_no >= 0 {
957            if let Some(&c) = self.tables.color_table.get(color_no as usize) {
958                return c;
959            }
960        }
961        (255, 255, 255)
962    }
963
964    fn gameexe_value(&self, key: &str) -> Option<&str> {
965        self.tables.gameexe.as_ref()?.get_value(key)
966    }
967
968    fn gameexe_raw(&self, key: &str) -> Option<&str> {
969        self.tables.gameexe.as_ref()?.get_unquoted(key)
970    }
971
972    fn gameexe_string(&self, key: &str) -> Option<String> {
973        self.gameexe_raw(key)
974            .map(|s| s.trim().to_string())
975            .filter(|s| !s.is_empty())
976    }
977
978    fn gameexe_rgba_default(&self, key: &str, default: (u8, u8, u8, u8)) -> (u8, u8, u8, u8) {
979        let vals = Self::parse_i64_list(self.gameexe_value(key));
980        if vals.len() >= 4 {
981            (
982                vals[0].clamp(0, 255) as u8,
983                vals[1].clamp(0, 255) as u8,
984                vals[2].clamp(0, 255) as u8,
985                vals[3].clamp(0, 255) as u8,
986            )
987        } else {
988            default
989        }
990    }
991
992    fn syscom_filter_config_rgba(&self) -> (u8, u8, u8, u8) {
993        let default = self.gameexe_rgba_default("CONFIG.FILTER_COLOR", (0, 0, 0, 128));
994        let cfg = &self.globals.syscom.config_int;
995        let pick = |key: i32, fallback: u8| -> u8 {
996            cfg.get(&key)
997                .copied()
998                .unwrap_or(fallback as i64)
999                .clamp(0, 255) as u8
1000        };
1001        (
1002            pick(syscom_op::GET_FILTER_COLOR_R, default.0),
1003            pick(syscom_op::GET_FILTER_COLOR_G, default.1),
1004            pick(syscom_op::GET_FILTER_COLOR_B, default.2),
1005            pick(syscom_op::GET_FILTER_COLOR_A, default.3),
1006        )
1007    }
1008
1009    fn parse_first_i64(raw: &str) -> Option<i64> {
1010        raw.split(|c: char| c == ',' || c.is_whitespace())
1011            .find_map(|part| {
1012                let t = part.trim();
1013                if t.is_empty() {
1014                    None
1015                } else {
1016                    t.parse::<i64>().ok()
1017                }
1018            })
1019    }
1020
1021    fn parse_i64_list(raw: Option<&str>) -> Vec<i64> {
1022        let Some(raw) = raw else {
1023            return Vec::new();
1024        };
1025        raw.split(|c: char| c == ',' || c.is_whitespace())
1026            .filter_map(|part| {
1027                let t = part.trim();
1028                if t.is_empty() {
1029                    None
1030                } else {
1031                    t.parse::<i64>().ok()
1032                }
1033            })
1034            .collect()
1035    }
1036
1037    fn gameexe_i64_default(&self, key: &str, default: i64) -> i64 {
1038        self.gameexe_value(key)
1039            .and_then(Self::parse_first_i64)
1040            .unwrap_or(default)
1041    }
1042
1043    fn mouse_cursor_count(&self) -> usize {
1044        self.gameexe_value("#MOUSE_CURSOR.CNT")
1045            .or_else(|| self.gameexe_value("MOUSE_CURSOR.CNT"))
1046            .and_then(Self::parse_first_i64)
1047            .filter(|v| *v >= 0)
1048            .map(|v| v as usize)
1049            .unwrap_or(16)
1050            .min(256)
1051    }
1052
1053    fn mouse_cursor_default_no(&self) -> i64 {
1054        let cnt = self.mouse_cursor_count() as i64;
1055        let no = self
1056            .gameexe_value("#MOUSE_CURSOR.DEFAULT")
1057            .or_else(|| self.gameexe_value("MOUSE_CURSOR.DEFAULT"))
1058            .and_then(Self::parse_first_i64)
1059            .unwrap_or(-1);
1060        if no >= 0 && no < cnt { no } else { -1 }
1061    }
1062
1063    fn mouse_cursor_file_name(&self, cursor_no: i64) -> Option<String> {
1064        if cursor_no < 0 || cursor_no as usize >= self.mouse_cursor_count() {
1065            return None;
1066        }
1067        self.tables
1068            .gameexe
1069            .as_ref()
1070            .and_then(|cfg| cfg.get_indexed_field_unquoted("MOUSE_CURSOR", cursor_no as usize, "FILE"))
1071            .map(|s| s.trim().to_string())
1072            .filter(|s| !s.is_empty())
1073    }
1074
1075    fn mouse_cursor_anime_speed(&self, cursor_no: i64) -> i64 {
1076        if cursor_no < 0 || cursor_no as usize >= self.mouse_cursor_count() {
1077            return 100;
1078        }
1079        self.tables
1080            .gameexe
1081            .as_ref()
1082            .and_then(|cfg| cfg.get_indexed_field("MOUSE_CURSOR", cursor_no as usize, "SPEED"))
1083            .and_then(Self::parse_first_i64)
1084            .unwrap_or(100)
1085    }
1086
1087    fn load_mouse_cursor_runtime(&mut self, cursor_no: i64) -> Option<&MouseCursorRuntime> {
1088        let append_dir = self.images.current_append_dir().to_string();
1089        let key = (cursor_no, append_dir);
1090        if !self.mouse_cursor_cache.contains_key(&key) {
1091            if let Some(loaded) = self.load_mouse_cursor_runtime_uncached(cursor_no) {
1092                self.mouse_cursor_cache.insert(key.clone(), loaded);
1093            }
1094        }
1095        self.mouse_cursor_cache.get(&key)
1096    }
1097
1098    fn load_mouse_cursor_runtime_uncached(&mut self, cursor_no: i64) -> Option<MouseCursorRuntime> {
1099        let file_name = self.mouse_cursor_file_name(cursor_no)?;
1100        let (path, pct) = match crate::resource::find_g00_image_with_append_dir(
1101            &self.project_dir,
1102            self.images.current_append_dir(),
1103            &file_name,
1104        ) {
1105            Ok(v) => v,
1106            Err(err) => {
1107                self.unknown.record_note(&format!(
1108                    "mouse_cursor.not_found:no={cursor_no}:file={file_name}:{err}"
1109                ));
1110                return None;
1111            }
1112        };
1113        if pct != crate::resource::PctType::G00 {
1114            self.unknown.record_note(&format!(
1115                "mouse_cursor.unsupported_type:no={cursor_no}:file={file_name}:path={}",
1116                path.display()
1117            ));
1118            return None;
1119        }
1120        let bytes = match fs::read(&path) {
1121            Ok(v) => v,
1122            Err(err) => {
1123                self.unknown.record_note(&format!(
1124                    "mouse_cursor.read_failed:no={cursor_no}:path={}:{}",
1125                    path.display(),
1126                    err
1127                ));
1128                return None;
1129            }
1130        };
1131        let decoded = match crate::assets::g00::decode_g00(&bytes) {
1132            Ok(v) => v,
1133            Err(err) => {
1134                self.unknown.record_note(&format!(
1135                    "mouse_cursor.decode_failed:no={cursor_no}:path={}:{}",
1136                    path.display(),
1137                    err
1138                ));
1139                return None;
1140            }
1141        };
1142        if decoded.frames.is_empty() {
1143            return None;
1144        }
1145        let mut frames = Vec::with_capacity(decoded.frames.len());
1146        for (idx, img) in decoded.frames.iter().enumerate() {
1147            if img.width != 32 || img.height != 32 {
1148                self.unknown.record_note(&format!(
1149                    "mouse_cursor.invalid_size:no={cursor_no}:file={file_name}:patno={idx}:{}x{}",
1150                    img.width, img.height
1151                ));
1152                return None;
1153            }
1154            let image_id = match self.images.load_file(&path, idx) {
1155                Ok(id) => id,
1156                Err(err) => {
1157                    self.unknown.record_note(&format!(
1158                        "mouse_cursor.frame_load_failed:no={cursor_no}:file={file_name}:patno={idx}:{err}"
1159                    ));
1160                    return None;
1161                }
1162            };
1163            frames.push(MouseCursorFrameRuntime {
1164                image_id,
1165                hot_x: img.center_x,
1166                hot_y: img.center_y,
1167            });
1168        }
1169        Some(MouseCursorRuntime {
1170            frames,
1171            anime_speed_ms: self.mouse_cursor_anime_speed(cursor_no),
1172        })
1173    }
1174
1175    pub fn has_active_custom_mouse_cursor(&mut self) -> bool {
1176        let cursor_no = self.globals.script.cursor_no;
1177        self.load_mouse_cursor_runtime(cursor_no).is_some()
1178    }
1179
1180    fn custom_mouse_cursor_needs_tick(&self) -> bool {
1181        let cursor_no = self.globals.script.cursor_no;
1182        if cursor_no < 0 || cursor_no as usize >= self.mouse_cursor_count() {
1183            return false;
1184        }
1185        self.mouse_cursor_anime_speed(cursor_no) > 0 && self.mouse_cursor_file_name(cursor_no).is_some()
1186    }
1187
1188    fn append_mouse_cursor_sprite(&mut self, list: &mut Vec<RenderSprite>) {
1189        if !self.globals.script.cursor_runtime_visible || self.globals.script.cursor_disp_off {
1190            return;
1191        }
1192        if !self.input.has_mouse_position() {
1193            return;
1194        }
1195        let cursor_no = self.globals.script.cursor_no;
1196        let cur_time = self.globals.local_real_time.max(0) as u64;
1197        let frame = {
1198            let Some(cursor) = self.load_mouse_cursor_runtime(cursor_no) else {
1199                return;
1200            };
1201            if cursor.frames.is_empty() {
1202                return;
1203            }
1204            let pat_no = if cursor.anime_speed_ms <= 0 {
1205                0usize
1206            } else {
1207                ((cur_time / cursor.anime_speed_ms as u64) as usize) % cursor.frames.len()
1208            };
1209            cursor.frames[pat_no]
1210        };
1211
1212        let mut sprite = Sprite::default();
1213        sprite.image_id = Some(frame.image_id);
1214        sprite.visible = true;
1215        sprite.fit = SpriteFit::PixelRect;
1216        sprite.size_mode = SpriteSizeMode::Intrinsic;
1217        sprite.x = self.input.mouse_x.saturating_sub(frame.hot_x);
1218        sprite.y = self.input.mouse_y.saturating_sub(frame.hot_y);
1219        sprite.alpha = 255;
1220        sprite.tr = 255;
1221        sprite.alpha_blend = true;
1222        sprite.alpha_test = false;
1223        sprite.object_anchor = false;
1224        sprite.order = i32::MAX;
1225        list.push(RenderSprite::with_sorter(None, None, i32::MAX, i32::MAX, sprite));
1226    }
1227
1228    fn gameexe_pair_default(&self, key: &str, default: (i64, i64)) -> (i64, i64) {
1229        let vals = Self::parse_i64_list(self.gameexe_value(key));
1230        if vals.len() >= 2 {
1231            (vals[0], vals[1])
1232        } else {
1233            default
1234        }
1235    }
1236
1237    fn gameexe_rect_default(
1238        &self,
1239        key: &str,
1240        default: (i64, i64, i64, i64),
1241    ) -> (i64, i64, i64, i64) {
1242        let vals = Self::parse_i64_list(self.gameexe_value(key));
1243        if vals.len() >= 4 {
1244            (vals[0], vals[1], vals[2], vals[3])
1245        } else {
1246            default
1247        }
1248    }
1249
1250    fn msg_back_button_pos(&self, key: &str, default: (i32, i32)) -> (i32, i32) {
1251        let vals = Self::parse_i64_list(self.gameexe_value(key));
1252        if vals.len() >= 2 {
1253            (vals[0] as i32, vals[1] as i32)
1254        } else {
1255            default
1256        }
1257    }
1258
1259    pub fn lookup_scene_no(&self, scene_name: &str) -> Result<i64> {
1260        if scene_name.is_empty() {
1261            anyhow::bail!("empty scene name")
1262        }
1263        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1264        let pck = {
1265            let scene_pck_path = self.project_dir.join("Scene.pck");
1266            let bytes = crate::resource::read_file_bytes(&scene_pck_path)?;
1267            let exe = ["key.toml", "Key.toml"]
1268                .iter()
1269                .find_map(|name| {
1270                    let p = self.project_dir.join(name);
1271                    if !crate::resource::wasm_path_is_file(&p) {
1272                        return None;
1273                    }
1274                    let text = crate::resource::read_file_to_string(&p).ok()?;
1275                    siglus_assets::key_toml::parse_key_toml(&text)
1276                        .ok()
1277                        .and_then(|cfg| cfg.exe_key16)
1278                        .map(|v| v.to_vec())
1279                });
1280            let opt = ScenePckDecodeOptions {
1281                exe_angou_element: exe,
1282                easy_angou_code: Some(siglus_assets::keys::SCENE_KEY.to_vec()),
1283            };
1284            ScenePck::load_and_rebuild_from_bytes(bytes, &opt)?
1285        };
1286        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1287        let pck = {
1288            let scene_pck_path = find_scene_pck_in_project(&self.project_dir)?;
1289            let opt = ScenePckDecodeOptions::from_project_dir(&self.project_dir)?;
1290            ScenePck::load_and_rebuild(&scene_pck_path, &opt)?
1291        };
1292        let scene_no = pck
1293            .find_scene_no(scene_name)
1294            .ok_or_else(|| anyhow::anyhow!("scene not found: {}", scene_name))?;
1295        Ok(scene_no as i64)
1296    }
1297
1298    pub fn reset_for_scene_restart(&mut self) {
1299        self.audio = AudioHub::new();
1300        self.bgm = BgmEngine::new(self.project_dir.clone());
1301        self.koe = KoeEngine::new(self.project_dir.clone());
1302        self.pcm = PcmEngine::new(self.project_dir.clone());
1303        self.se = SeEngine::new(self.project_dir.clone());
1304        self.movie = MovieManager::new(self.project_dir.clone());
1305        self.images = ImageManager::new(self.project_dir.clone());
1306        self.mouse_cursor_cache.clear();
1307        self.solid_white = self.images.solid_rgba((255, 255, 255, 255));
1308        self.layers.clear_all();
1309        self.gfx = graphics::GfxRuntime::default();
1310        self.ui = ui::UiRuntime::default();
1311        self.font_cache = FontCache::new();
1312        self.wait = wait::VmWait::default();
1313        self.stack.clear();
1314        self.globals = globals::GlobalState::default();
1315        self.tonecurve = tonecurve::ToneCurveRuntime::new(&self.project_dir);
1316        self.excall_state = ExcallCompatState::default();
1317        self.last_presented_render_list.clear();
1318        self.wipe_front_rt_image = None;
1319        self.wipe_next_rt_image = None;
1320        self.overlay_rt_image = None;
1321        self.input.clear_all();
1322        self.vm_call = None;
1323        self.pending_read_flag_no = false;
1324        self.pending_selbtn_read_flag_no = false;
1325        self.runtime_load_completed = false;
1326        self.frame_clock_last = None;
1327        self.last_button_hover_sound_pos = None;
1328        self.apply_gameexe_runtime_defaults();
1329    }
1330
1331    /// Install or clear an external form handler.
1332    pub fn set_external_form_handler(&mut self, h: Option<Arc<dyn ExternalFormHandler>>) {
1333        self.external_forms = h;
1334    }
1335
1336    // ------------------------------------------------------------------
1337    // Object button runtime
1338    // ------------------------------------------------------------------
1339
1340    fn active_button_stage_form_id(&self) -> Option<u32> {
1341        const EXCALL_LOCAL_NS_XOR: u32 = 0x4000;
1342        let normal_stage_form = self.ids.form_global_stage;
1343        if self.excall_state.ex_call_flag {
1344            if self.excall_state.ready {
1345                Some(normal_stage_form ^ EXCALL_LOCAL_NS_XOR)
1346            } else {
1347                None
1348            }
1349        } else {
1350            Some(normal_stage_form)
1351        }
1352    }
1353
1354    fn load_any_image_for_hit(
1355        images: &mut ImageManager,
1356        file: &str,
1357        patno: i64,
1358    ) -> Option<crate::image_manager::ImageId> {
1359        let pat_u32 = if patno < 0 { 0 } else { patno as u32 };
1360        if let Ok(id) = images.load_g00(file, pat_u32) {
1361            return Some(id);
1362        }
1363        if let Ok(id) = images.load_bg(file) {
1364            return Some(id);
1365        }
1366        None
1367    }
1368
1369    fn hit_test_sprite_rect(x: i32, y: i32, w: u32, h: u32, mx: i32, my: i32) -> bool {
1370        let x2 = x.saturating_add(w as i32);
1371        let y2 = y.saturating_add(h as i32);
1372        mx >= x && mx < x2 && my >= y && my < y2
1373    }
1374
1375    fn alpha_test_image(img: &crate::assets::RgbaImage, local_x: i32, local_y: i32) -> bool {
1376        if local_x < 0 || local_y < 0 {
1377            return false;
1378        }
1379        let lx = local_x as u32;
1380        let ly = local_y as u32;
1381        if lx >= img.width || ly >= img.height {
1382            return false;
1383        }
1384        let idx = ((ly * img.width + lx) * 4 + 3) as usize;
1385        img.rgba.get(idx).copied().unwrap_or(0) != 0
1386    }
1387
1388    fn play_button_se_no(&mut self, se_no: i64) {
1389        if se_no < 0 {
1390            return;
1391        }
1392        let Some(file_name) = self
1393            .tables
1394            .se_file_names
1395            .get(se_no as usize)
1396            .and_then(|v| v.as_deref())
1397            .filter(|s| !s.is_empty())
1398        else {
1399            self.unknown
1400                .record_note(&format!("se.table.missing:{se_no}"));
1401            return;
1402        };
1403        if self.se.play_file_name(&mut self.audio, file_name).is_err() {
1404            self.unknown
1405                .record_note(&format!("se.play.failed:{se_no}:{file_name}"));
1406        }
1407    }
1408
1409    fn button_template_se_no(&self, template_no: i64, event: ButtonSeEvent) -> Option<i64> {
1410        if template_no < 0 {
1411            return None;
1412        }
1413        let template = self.tables.button_se_templates.get(template_no as usize)?;
1414        let se_no = match event {
1415            ButtonSeEvent::Hit => template.hit_no,
1416            ButtonSeEvent::Push => template.push_no,
1417            ButtonSeEvent::Decide => template.decide_no,
1418        };
1419        (se_no >= 0).then_some(se_no)
1420    }
1421
1422    fn play_button_template_se(&mut self, template_no: i64, event: ButtonSeEvent) {
1423        if let Some(se_no) = self.button_template_se_no(template_no, event) {
1424            self.play_button_se_no(se_no);
1425        }
1426    }
1427
1428    fn update_object_button_hover(&mut self) {
1429        if !self.input.has_mouse_position() {
1430            return;
1431        }
1432        let mx = self.input.mouse_x;
1433        let my = self.input.mouse_y;
1434        let play_hover_sound = match self.last_button_hover_sound_pos {
1435            Some((last_x, last_y)) if last_x == mx && last_y == my => false,
1436            Some(_) => true,
1437            None => false,
1438        };
1439        self.last_button_hover_sound_pos = Some((mx, my));
1440        let Some(form_id) = self.active_button_stage_form_id() else {
1441            return;
1442        };
1443        let mut hit_sounds = Vec::new();
1444        if sg_input_trace_enabled() {
1445            eprintln!("[SG_DEBUG][INPUT] hover mouse=({}, {})", mx, my);
1446        }
1447
1448        {
1449            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
1450                return;
1451            };
1452
1453            let embedded_by_stage: HashMap<i64, HashSet<usize>> = st
1454                .embedded_object_slots
1455                .iter()
1456                .fold(HashMap::new(), |mut acc, (key, &slot)| {
1457                    if let Some((stage, _)) = key.split_once(':') {
1458                        if let Ok(stage_idx) = stage.parse::<i64>() {
1459                            acc.entry(stage_idx)
1460                                .or_insert_with(HashSet::new)
1461                                .insert(slot);
1462                        }
1463                    }
1464                    acc
1465                });
1466            let images = &mut self.images;
1467            let layers = &self.layers;
1468            let gfx = &self.gfx;
1469            let ids = &self.ids;
1470            let (object_lists, group_lists) = (&mut st.object_lists, &mut st.group_lists);
1471
1472            let mut stage_ids: Vec<i64> = object_lists.keys().copied().collect();
1473            stage_ids.sort_unstable();
1474            for stage_idx in &stage_ids {
1475                let Some(objs) = object_lists.get_mut(stage_idx) else {
1476                    continue;
1477                };
1478                for (obj_idx, obj) in objs.iter_mut().enumerate() {
1479                    if embedded_by_stage
1480                        .get(stage_idx)
1481                        .map_or(false, |slots| slots.contains(&obj_idx))
1482                    {
1483                        continue;
1484                    }
1485                    clear_button_hit_recursive(obj);
1486                }
1487            }
1488
1489            let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
1490            group_stage_ids.sort_unstable();
1491            for stage_idx in group_stage_ids {
1492                let Some(groups) = group_lists.get_mut(&stage_idx) else {
1493                    continue;
1494                };
1495                for (group_idx, g) in groups.iter_mut().enumerate() {
1496                    if !g.started {
1497                        g.hit_button_no = -1;
1498                        g.hit_runtime_slot = None;
1499                        continue;
1500                    }
1501                    let Some(objs) = object_lists.get_mut(&stage_idx) else {
1502                        g.hit_button_no = -1;
1503                        g.hit_runtime_slot = None;
1504                        continue;
1505                    };
1506
1507                    let mut best: Option<ButtonHitCandidate> = None;
1508                    let mut tied = false;
1509                    for (obj_idx, obj) in objs.iter_mut().enumerate() {
1510                        if embedded_by_stage
1511                            .get(&stage_idx)
1512                            .map_or(false, |slots| slots.contains(&obj_idx))
1513                        {
1514                            continue;
1515                        }
1516                        if let Some(hit) = hit_test_object_button_recursive(
1517                            images,
1518                            layers,
1519                            gfx,
1520                            ids,
1521                            &self.globals.syscom,
1522                            stage_idx,
1523                            group_idx,
1524                            mx,
1525                            my,
1526                            obj_idx,
1527                            obj,
1528                            None,
1529                        ) {
1530                            merge_button_hit(&mut best, &mut tied, hit);
1531                        }
1532                    }
1533
1534                    if !tied {
1535                        if let Some(hit) = best {
1536                            g.hit_button_no = hit.button_no;
1537                            g.hit_runtime_slot = Some(hit.runtime_slot);
1538                            if sg_debug_enabled() {
1539                                eprintln!(
1540                                    "[SG_DEBUG][INPUT] group stage={} group={} hit_button={} slot={} order={} started={} pushed={} decided={}",
1541                                    stage_idx, group_idx, hit.button_no, hit.runtime_slot, hit.sort_key.display_tuple(), g.started, g.pushed_button_no, g.decided_button_no
1542                                );
1543                            }
1544                            if play_hover_sound && !hit.was_hit {
1545                                hit_sounds.push(hit.se_no);
1546                            }
1547                            for (obj_idx, obj) in objs.iter_mut().enumerate() {
1548                                if embedded_by_stage
1549                                    .get(&stage_idx)
1550                                    .map_or(false, |slots| slots.contains(&obj_idx))
1551                                {
1552                                    continue;
1553                                }
1554                                set_button_hit_by_runtime_slot_recursive(
1555                                    obj_idx,
1556                                    obj,
1557                                    hit.runtime_slot,
1558                                );
1559                            }
1560                        } else {
1561                            g.hit_button_no = -1;
1562                            g.hit_runtime_slot = None;
1563                            if sg_debug_enabled() {
1564                                eprintln!(
1565                                    "[SG_DEBUG][INPUT] group stage={} group={} no_hit started={}",
1566                                    stage_idx, group_idx, g.started
1567                                );
1568                            }
1569                        }
1570                    } else {
1571                        g.hit_button_no = -1;
1572                        g.hit_runtime_slot = None;
1573                        if sg_debug_enabled() {
1574                            eprintln!(
1575                                "[SG_DEBUG][INPUT] group stage={} group={} hit_tie",
1576                                stage_idx, group_idx
1577                            );
1578                        }
1579                    }
1580                }
1581            }
1582
1583            let mut standalone_best: Option<ButtonHitCandidate> = None;
1584            let mut standalone_tied = false;
1585            for stage_idx in &stage_ids {
1586                let Some(objs) = object_lists.get_mut(stage_idx) else {
1587                    continue;
1588                };
1589                for (obj_idx, obj) in objs.iter_mut().enumerate() {
1590                    if embedded_by_stage
1591                        .get(stage_idx)
1592                        .map_or(false, |slots| slots.contains(&obj_idx))
1593                    {
1594                        continue;
1595                    }
1596                    if let Some(hit) = hit_test_standalone_action_button_recursive(
1597                        images,
1598                        layers,
1599                        gfx,
1600                        ids,
1601                        &self.globals.syscom,
1602                        *stage_idx,
1603                        mx,
1604                        my,
1605                        obj_idx,
1606                        obj,
1607                        None,
1608                    ) {
1609                        merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
1610                    }
1611                }
1612            }
1613            if !standalone_tied {
1614                if let Some(hit) = standalone_best {
1615                    if play_hover_sound && !hit.was_hit {
1616                        hit_sounds.push(hit.se_no);
1617                    }
1618                    for stage_idx in &stage_ids {
1619                        let Some(objs) = object_lists.get_mut(stage_idx) else {
1620                            continue;
1621                        };
1622                        for (obj_idx, obj) in objs.iter_mut().enumerate() {
1623                            if embedded_by_stage
1624                                .get(stage_idx)
1625                                .map_or(false, |slots| slots.contains(&obj_idx))
1626                            {
1627                                continue;
1628                            }
1629                            set_button_hit_by_runtime_slot_recursive(
1630                                obj_idx,
1631                                obj,
1632                                hit.runtime_slot,
1633                            );
1634                        }
1635                    }
1636                }
1637            }
1638        }
1639
1640        {
1641            let mwnd_ui_state = self
1642                .ui
1643                .current_mwnd_window_render_state(self.screen_w, self.screen_h);
1644            let mwnd_hidden =
1645                self.globals.script.mwnd_disp_off_flag
1646                    || self.globals.syscom.hide_mwnd.onoff
1647                    || self.globals.syscom.msg_back_open;
1648            if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
1649                let images = &mut self.images;
1650                let layers = &self.layers;
1651                let gfx = &self.gfx;
1652                let ids = &self.ids;
1653                let mut standalone_best: Option<ButtonHitCandidate> = None;
1654                let mut standalone_tied = false;
1655                let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
1656                stage_ids.sort_unstable();
1657                for stage_idx in &stage_ids {
1658                    let Some(mwnds) = st.mwnd_lists.get_mut(stage_idx) else {
1659                        continue;
1660                    };
1661                    for mwnd in mwnds {
1662                        for obj in &mut mwnd.button_list {
1663                            clear_button_hit_recursive(obj);
1664                        }
1665                        for obj in &mut mwnd.face_list {
1666                            clear_button_hit_recursive(obj);
1667                        }
1668                        for obj in &mut mwnd.object_list {
1669                            clear_button_hit_recursive(obj);
1670                        }
1671                        if mwnd_hidden || !mwnd.open {
1672                            continue;
1673                        }
1674                        let Some((window_x, window_y)) = mwnd.window_pos else {
1675                            continue;
1676                        };
1677                        let Some((window_w, window_h)) = mwnd.window_size else {
1678                            continue;
1679                        };
1680                        if window_w <= 0 || window_h <= 0 {
1681                            continue;
1682                        }
1683                        let ui_state = mwnd_ui_state.filter(|ui| {
1684                            ui.x as i64 == window_x
1685                                && ui.y as i64 == window_y
1686                                && ui.w as i64 == window_w
1687                                && ui.h as i64 == window_h
1688                        });
1689                        let anim_parent =
1690                            ui_state.map(|ui| mwnd_anim_parent_from_ui_state(mwnd, ui));
1691                        let button_len = mwnd.button_list.len();
1692                        for button_idx in 0..button_len {
1693                            let skip = {
1694                                let obj = &mwnd.button_list[button_idx];
1695                                !object_button_renderable_by_syscom(&self.globals.syscom, obj)
1696                                    || button_effective_disabled(
1697                                        &self.globals.syscom,
1698                                        obj,
1699                                        Some(button_idx),
1700                                    )
1701                                    || self.globals.syscom.mwnd_btn_touch_disable
1702                            };
1703                            if skip {
1704                                continue;
1705                            }
1706                            let parent = apply_mwnd_window_anim_parent(
1707                                mwnd_button_parent_render_state(
1708                                    mwnd, button_idx, window_x, window_y, window_w, window_h,
1709                                ),
1710                                anim_parent,
1711                            );
1712                            let obj = &mut mwnd.button_list[button_idx];
1713                            if let Some(hit) = hit_test_standalone_action_button_recursive(
1714                                images,
1715                                layers,
1716                                gfx,
1717                                ids,
1718                                &self.globals.syscom,
1719                                *stage_idx,
1720                                mx,
1721                                my,
1722                                button_idx,
1723                                obj,
1724                                Some(parent),
1725                            ) {
1726                                merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
1727                            }
1728                        }
1729                        let face_len = mwnd.face_list.len();
1730                        for face_idx in 0..face_len {
1731                            let parent = apply_mwnd_window_anim_parent(
1732                                mwnd_face_parent_render_state(mwnd, face_idx, window_x, window_y),
1733                                anim_parent,
1734                            );
1735                            let obj = &mut mwnd.face_list[face_idx];
1736                            if let Some(hit) = hit_test_standalone_action_button_recursive(
1737                                images,
1738                                layers,
1739                                gfx,
1740                                ids,
1741                                &self.globals.syscom,
1742                                *stage_idx,
1743                                mx,
1744                                my,
1745                                face_idx,
1746                                obj,
1747                                Some(parent),
1748                            ) {
1749                                merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
1750                            }
1751                        }
1752                        let object_parent = apply_mwnd_window_anim_parent(
1753                            mwnd_parent_render_state_at(mwnd, window_x, window_y),
1754                            anim_parent,
1755                        );
1756                        let object_len = mwnd.object_list.len();
1757                        for object_idx in 0..object_len {
1758                            let obj = &mut mwnd.object_list[object_idx];
1759                            if let Some(hit) = hit_test_standalone_action_button_recursive(
1760                                images,
1761                                layers,
1762                                gfx,
1763                                ids,
1764                                &self.globals.syscom,
1765                                *stage_idx,
1766                                mx,
1767                                my,
1768                                object_idx,
1769                                obj,
1770                                Some(object_parent),
1771                            ) {
1772                                merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
1773                            }
1774                        }
1775                    }
1776                }
1777                if !standalone_tied {
1778                    if let Some(hit) = standalone_best {
1779                        if play_hover_sound && !hit.was_hit {
1780                            hit_sounds.push(hit.se_no);
1781                        }
1782                        for stage_idx in &stage_ids {
1783                            let Some(mwnds) = st.mwnd_lists.get_mut(stage_idx) else {
1784                                continue;
1785                            };
1786                            for mwnd in mwnds {
1787                                for (button_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
1788                                    set_button_hit_by_runtime_slot_recursive(
1789                                        button_idx,
1790                                        obj,
1791                                        hit.runtime_slot,
1792                                    );
1793                                }
1794                                for (face_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
1795                                    set_button_hit_by_runtime_slot_recursive(
1796                                        face_idx,
1797                                        obj,
1798                                        hit.runtime_slot,
1799                                    );
1800                                }
1801                                for (object_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
1802                                    set_button_hit_by_runtime_slot_recursive(
1803                                        object_idx,
1804                                        obj,
1805                                        hit.runtime_slot,
1806                                    );
1807                                }
1808                            }
1809                        }
1810                    }
1811                }
1812            }
1813        }
1814
1815        for se_no in hit_sounds {
1816            self.play_button_template_se(se_no, ButtonSeEvent::Hit);
1817        }
1818    }
1819
1820    fn handle_object_button_mouse_down(&mut self, b: input::VmMouseButton) -> bool {
1821        // The original button manager separates pushed_this_frame from decided_this_frame.
1822        // Press starts the push state; release inside the same button decides it.
1823        self.update_object_button_hover();
1824
1825        let Some(form_id) = self.active_button_stage_form_id() else {
1826            return false;
1827        };
1828        let mut template_sounds = Vec::new();
1829        let mut direct_sounds = Vec::new();
1830        let mut consumed_button = false;
1831
1832        {
1833            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
1834                return false;
1835            };
1836
1837            let embedded_by_stage: HashMap<i64, HashSet<usize>> = st
1838                .embedded_object_slots
1839                .iter()
1840                .fold(HashMap::new(), |mut acc, (key, &slot)| {
1841                    if let Some((stage, _)) = key.split_once(':') {
1842                        if let Ok(stage_idx) = stage.parse::<i64>() {
1843                            acc.entry(stage_idx)
1844                                .or_insert_with(HashSet::new)
1845                                .insert(slot);
1846                        }
1847                    }
1848                    acc
1849                });
1850            let (object_lists, group_lists) = (&mut st.object_lists, &mut st.group_lists);
1851
1852            match b {
1853                input::VmMouseButton::Left => {
1854                    let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
1855                    group_stage_ids.sort_unstable();
1856                    for stage_idx in group_stage_ids {
1857                        let Some(groups) = group_lists.get_mut(&stage_idx) else {
1858                            continue;
1859                        };
1860                        for (group_idx, g) in groups.iter_mut().enumerate() {
1861                            if !g.started {
1862                                continue;
1863                            }
1864                            let hit = g.hit_button_no;
1865                            let Some(hit_slot) = g.hit_runtime_slot else {
1866                                continue;
1867                            };
1868                            if hit < 0 {
1869                                continue;
1870                            }
1871                            if g.pushed_runtime_slot != Some(hit_slot) {
1872                                if let Some(objs) = object_lists.get(&stage_idx) {
1873                                    if let Some(se_no) =
1874                                        find_button_se_no_in_list_by_runtime_slot(objs, hit_slot)
1875                                    {
1876                                        template_sounds.push(se_no);
1877                                    }
1878                                }
1879                            }
1880                            g.pushed_button_no = hit;
1881                            g.pushed_runtime_slot = Some(hit_slot);
1882                            if let Some(objs) = object_lists.get_mut(&stage_idx) {
1883                                for (obj_idx, obj) in objs.iter_mut().enumerate() {
1884                                    set_button_pushed_by_runtime_slot_recursive(
1885                                        obj_idx, obj, hit_slot,
1886                                    );
1887                                }
1888                            }
1889                        }
1890                    }
1891
1892                    let mut stage_ids: Vec<i64> = object_lists.keys().copied().collect();
1893                    stage_ids.sort_unstable();
1894                    for stage_idx in stage_ids {
1895                        let Some(objs) = object_lists.get_mut(&stage_idx) else {
1896                            continue;
1897                        };
1898                        for (obj_idx, obj) in objs.iter_mut().enumerate() {
1899                            if embedded_by_stage
1900                                .get(&stage_idx)
1901                                .map_or(false, |slots| slots.contains(&obj_idx))
1902                            {
1903                                continue;
1904                            }
1905                            if standalone_button_hit_recursive(obj) {
1906                                consumed_button = true;
1907                            }
1908                            if let Some(se_no) =
1909                                mark_standalone_button_pushed_from_hit_recursive(obj_idx, obj)
1910                            {
1911                                template_sounds.push(se_no);
1912                            }
1913                        }
1914                    }
1915                }
1916                input::VmMouseButton::Right => {
1917                    let mut candidates: Vec<(i64, usize, i64)> = Vec::new();
1918                    let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
1919                    group_stage_ids.sort_unstable();
1920                    for stage_idx in group_stage_ids {
1921                        let Some(groups) = group_lists.get(&stage_idx) else {
1922                            continue;
1923                        };
1924                        for (group_idx, g) in groups.iter().enumerate() {
1925                            if g.started && g.cancel_flag {
1926                                candidates.push((g.cancel_priority, group_idx, stage_idx));
1927                            }
1928                        }
1929                    }
1930                    candidates.sort_by(|a, b| b.0.cmp(&a.0));
1931                    if let Some((_priority, group_idx, stage_idx)) = candidates.first().copied() {
1932                        if let Some(groups) = group_lists.get_mut(&stage_idx) {
1933                            if let Some(g) = groups.get_mut(group_idx) {
1934                                let was_waiting = g.wait_flag;
1935                                let cancel_se_no = g.cancel_se_no;
1936                                if g.cancel().is_some() {
1937                                    if sg_debug_enabled() {
1938                                        eprintln!(
1939                                            "[SG_DEBUG][GROUP] cancel form={} stage={} group={} wait={} result_button={} se={}",
1940                                            form_id, stage_idx, group_idx, was_waiting, g.result_button_no, cancel_se_no
1941                                        );
1942                                    }
1943                                    if was_waiting {
1944                                        self.stack.push(Value::Int(globals::TNM_GROUP_CANCELED));
1945                                    }
1946                                    g.wait_flag = false;
1947                                    direct_sounds.push(cancel_se_no);
1948                                    if self.globals.focused_stage_group
1949                                        == Some((form_id, stage_idx, group_idx))
1950                                    {
1951                                        self.globals.focused_stage_group = None;
1952                                    }
1953                                }
1954                            }
1955                        }
1956                    }
1957                }
1958                _ => {}
1959            }
1960        }
1961
1962        {
1963            let mwnd_hidden =
1964                self.globals.script.mwnd_disp_off_flag
1965                    || self.globals.syscom.hide_mwnd.onoff
1966                    || self.globals.syscom.msg_back_open;
1967            let syscom = self.globals.syscom.clone();
1968            if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
1969                let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
1970                stage_ids.sort_unstable();
1971                for stage_idx in stage_ids {
1972                    let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
1973                        continue;
1974                    };
1975                    for mwnd in mwnds {
1976                        if mwnd_hidden || !mwnd.open {
1977                            continue;
1978                        }
1979                        let Some((_, _)) = mwnd.window_pos else {
1980                            continue;
1981                        };
1982                        let Some((window_w, window_h)) = mwnd.window_size else {
1983                            continue;
1984                        };
1985                        if window_w <= 0 || window_h <= 0 {
1986                            continue;
1987                        }
1988                        for (button_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
1989                            if !object_button_renderable_by_syscom(&syscom, obj)
1990                                || button_effective_disabled(&syscom, obj, Some(button_idx))
1991                                || syscom.mwnd_btn_touch_disable
1992                            {
1993                                continue;
1994                            }
1995                            if standalone_button_hit_recursive(obj) {
1996                                consumed_button = true;
1997                            }
1998                            if let Some(se_no) =
1999                                mark_standalone_button_pushed_from_hit_recursive(button_idx, obj)
2000                            {
2001                                template_sounds.push(se_no);
2002                            }
2003                        }
2004                        for (face_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
2005                            if !object_button_renderable_by_syscom(&syscom, obj)
2006                                || button_effective_disabled(&syscom, obj, None)
2007                                || syscom.mwnd_btn_touch_disable
2008                            {
2009                                continue;
2010                            }
2011                            if standalone_button_hit_recursive(obj) {
2012                                consumed_button = true;
2013                            }
2014                            if let Some(se_no) =
2015                                mark_standalone_button_pushed_from_hit_recursive(face_idx, obj)
2016                            {
2017                                template_sounds.push(se_no);
2018                            }
2019                        }
2020                        for (object_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
2021                            if !object_button_renderable_by_syscom(&syscom, obj)
2022                                || button_effective_disabled(&syscom, obj, None)
2023                                || syscom.mwnd_btn_touch_disable
2024                            {
2025                                continue;
2026                            }
2027                            if standalone_button_hit_recursive(obj) {
2028                                consumed_button = true;
2029                            }
2030                            if let Some(se_no) =
2031                                mark_standalone_button_pushed_from_hit_recursive(object_idx, obj)
2032                            {
2033                                template_sounds.push(se_no);
2034                            }
2035                        }
2036                    }
2037                }
2038            }
2039        }
2040
2041        let consumed = consumed_button || !template_sounds.is_empty() || !direct_sounds.is_empty();
2042        for se_no in template_sounds {
2043            self.play_button_template_se(se_no, ButtonSeEvent::Push);
2044        }
2045        for se_no in direct_sounds {
2046            self.play_button_se_no(se_no);
2047        }
2048        consumed
2049    }
2050
2051    fn handle_object_button_mouse_up(&mut self, b: input::VmMouseButton) -> bool {
2052        if !matches!(b, input::VmMouseButton::Left) {
2053            return false;
2054        }
2055
2056        self.update_object_button_hover();
2057
2058        let Some(form_id) = self.active_button_stage_form_id() else {
2059            return false;
2060        };
2061        let mut pending_button_actions = Vec::new();
2062        let mut sounds = Vec::new();
2063        let mut consumed_button = false;
2064
2065        {
2066            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
2067                return false;
2068            };
2069
2070            let embedded_by_stage: HashMap<i64, HashSet<usize>> = st
2071                .embedded_object_slots
2072                .iter()
2073                .fold(HashMap::new(), |mut acc, (key, &slot)| {
2074                    if let Some((stage, _)) = key.split_once(':') {
2075                        if let Ok(stage_idx) = stage.parse::<i64>() {
2076                            acc.entry(stage_idx)
2077                                .or_insert_with(HashSet::new)
2078                                .insert(slot);
2079                        }
2080                    }
2081                    acc
2082                });
2083            let (object_lists, group_lists) = (&mut st.object_lists, &mut st.group_lists);
2084
2085            let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
2086            group_stage_ids.sort_unstable();
2087            for stage_idx in group_stage_ids {
2088                let Some(groups) = group_lists.get_mut(&stage_idx) else {
2089                    continue;
2090                };
2091                for (group_idx, g) in groups.iter_mut().enumerate() {
2092                    if !g.started {
2093                        continue;
2094                    }
2095                    let pushed = g.pushed_button_no;
2096                    let pushed_slot = g.pushed_runtime_slot;
2097                    let release_keeps_push = pushed_slot
2098                        .and_then(|slot| {
2099                            object_lists.get(&stage_idx).map(|objs| {
2100                                object_button_push_keep_in_list_by_runtime_slot(objs, slot)
2101                            })
2102                        })
2103                        .unwrap_or(false);
2104                    let released_on_same_button = pushed >= 0
2105                        && pushed_slot.is_some()
2106                        && (g.hit_runtime_slot == pushed_slot || release_keeps_push);
2107                    if released_on_same_button {
2108                        let was_waiting = g.wait_flag;
2109                        let action_slot = pushed_slot.unwrap();
2110                        if g.decide(pushed) {
2111                            if sg_debug_enabled() {
2112                                eprintln!(
2113                                    "[SG_DEBUG][GROUP] decide form={} stage={} group={} button={} slot={} wait={}",
2114                                    form_id, stage_idx, group_idx, pushed, action_slot, was_waiting
2115                                );
2116                            }
2117                            if let Some(objs) = object_lists.get(&stage_idx) {
2118                                if let Some(se_no) =
2119                                    find_button_se_no_in_list_by_runtime_slot(objs, action_slot)
2120                                {
2121                                    sounds.push(se_no);
2122                                }
2123                                for (obj_idx, obj) in objs.iter().enumerate() {
2124                                    if embedded_by_stage
2125                                        .get(&stage_idx)
2126                                        .map_or(false, |slots| slots.contains(&obj_idx))
2127                                    {
2128                                        continue;
2129                                    }
2130                                    collect_button_decided_action_by_runtime_slot_recursive(
2131                                        obj_idx,
2132                                        obj,
2133                                        action_slot,
2134                                        &mut pending_button_actions,
2135                                    );
2136                                }
2137                            }
2138                            if was_waiting {
2139                                self.stack.push(Value::Int(pushed));
2140                                g.wait_flag = false;
2141                                if self.globals.focused_stage_group
2142                                    == Some((form_id, stage_idx, group_idx))
2143                                {
2144                                    self.globals.focused_stage_group = None;
2145                                }
2146                            }
2147                        }
2148                    } else {
2149                        g.pushed_button_no = -1;
2150                        g.pushed_runtime_slot = None;
2151                    }
2152                }
2153            }
2154
2155            let mut stage_ids: Vec<i64> = object_lists.keys().copied().collect();
2156            stage_ids.sort_unstable();
2157            for stage_idx in &stage_ids {
2158                let Some(objs) = object_lists.get(stage_idx) else {
2159                    continue;
2160                };
2161                for (obj_idx, obj) in objs.iter().enumerate() {
2162                    if embedded_by_stage
2163                        .get(stage_idx)
2164                        .map_or(false, |slots| slots.contains(&obj_idx))
2165                    {
2166                        continue;
2167                    }
2168                    if standalone_button_pushed_recursive(obj) {
2169                        consumed_button = true;
2170                    }
2171                    collect_standalone_button_decided_actions_recursive(
2172                        obj,
2173                        &mut pending_button_actions,
2174                        &mut sounds,
2175                    );
2176                }
2177            }
2178
2179            for stage_idx in &stage_ids {
2180                let Some(objs) = object_lists.get_mut(stage_idx) else {
2181                    continue;
2182                };
2183                for (obj_idx, obj) in objs.iter_mut().enumerate() {
2184                    if embedded_by_stage
2185                        .get(stage_idx)
2186                        .map_or(false, |slots| slots.contains(&obj_idx))
2187                    {
2188                        continue;
2189                    }
2190                    clear_button_pushed_recursive(obj);
2191                }
2192            }
2193        }
2194
2195        {
2196            let mwnd_hidden =
2197                self.globals.script.mwnd_disp_off_flag
2198                    || self.globals.syscom.hide_mwnd.onoff
2199                    || self.globals.syscom.msg_back_open;
2200            let syscom = self.globals.syscom.clone();
2201            if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
2202                let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
2203                stage_ids.sort_unstable();
2204                for stage_idx in &stage_ids {
2205                    let Some(mwnds) = st.mwnd_lists.get(stage_idx) else {
2206                        continue;
2207                    };
2208                    for mwnd in mwnds {
2209                        if mwnd_hidden || !mwnd.open {
2210                            continue;
2211                        }
2212                        let Some((_, _)) = mwnd.window_pos else {
2213                            continue;
2214                        };
2215                        let Some((window_w, window_h)) = mwnd.window_size else {
2216                            continue;
2217                        };
2218                        if window_w <= 0 || window_h <= 0 {
2219                            continue;
2220                        }
2221                        for (button_idx, obj) in mwnd.button_list.iter().enumerate() {
2222                            if !object_button_renderable_by_syscom(&syscom, obj)
2223                                || button_effective_disabled(&syscom, obj, Some(button_idx))
2224                                || syscom.mwnd_btn_touch_disable
2225                            {
2226                                continue;
2227                            }
2228                            collect_standalone_button_decided_actions_recursive(
2229                                obj,
2230                                &mut pending_button_actions,
2231                                &mut sounds,
2232                            );
2233                        }
2234                        for obj in &mwnd.face_list {
2235                            if !object_button_renderable_by_syscom(&syscom, obj)
2236                                || button_effective_disabled(&syscom, obj, None)
2237                                || syscom.mwnd_btn_touch_disable
2238                            {
2239                                continue;
2240                            }
2241                            collect_standalone_button_decided_actions_recursive(
2242                                obj,
2243                                &mut pending_button_actions,
2244                                &mut sounds,
2245                            );
2246                        }
2247                        for obj in &mwnd.object_list {
2248                            if !object_button_renderable_by_syscom(&syscom, obj)
2249                                || button_effective_disabled(&syscom, obj, None)
2250                                || syscom.mwnd_btn_touch_disable
2251                            {
2252                                continue;
2253                            }
2254                            collect_standalone_button_decided_actions_recursive(
2255                                obj,
2256                                &mut pending_button_actions,
2257                                &mut sounds,
2258                            );
2259                        }
2260                    }
2261                }
2262                for stage_idx in &stage_ids {
2263                    let Some(mwnds) = st.mwnd_lists.get_mut(stage_idx) else {
2264                        continue;
2265                    };
2266                    for mwnd in mwnds {
2267                        for obj in &mut mwnd.button_list {
2268                            clear_button_pushed_recursive(obj);
2269                        }
2270                        for obj in &mut mwnd.face_list {
2271                            clear_button_pushed_recursive(obj);
2272                        }
2273                        for obj in &mut mwnd.object_list {
2274                            clear_button_pushed_recursive(obj);
2275                        }
2276                    }
2277                }
2278            }
2279        }
2280
2281        let consumed = consumed_button || !pending_button_actions.is_empty() || !sounds.is_empty();
2282        self.globals
2283            .pending_button_actions
2284            .extend(pending_button_actions);
2285        for se_no in sounds {
2286            self.play_button_template_se(se_no, ButtonSeEvent::Decide);
2287        }
2288        consumed
2289    }
2290    // ------------------------------------------------------------------
2291    // Input bridge (platform event -> VM state)
2292    // ------------------------------------------------------------------
2293
2294    pub fn platform_shortcuts_blocked(&self) -> bool {
2295        self.globals.system.messagebox_modal.is_some()
2296            || self.globals.syscom.menu_open
2297            || self.globals.syscom.msg_back_open
2298            || self.globals.selbtn.started
2299            || self.globals.focused_editbox.is_some()
2300    }
2301
2302    fn is_vm_key_disabled(&self, k: input::VmKey) -> bool {
2303        input::vmkey_to_vk_code(k)
2304            .map(|vk| self.globals.script.key_disable.contains(&vk))
2305            .unwrap_or(false)
2306    }
2307
2308    pub fn on_key_down(&mut self, k: input::VmKey) {
2309        if self.handle_system_messagebox_key(k) {
2310            return;
2311        }
2312        if self.handle_msg_back_key(k) {
2313            return;
2314        }
2315        if self.globals.syscom.hide_mwnd.onoff
2316            && matches!(k, input::VmKey::Enter | input::VmKey::Escape | input::VmKey::Space)
2317        {
2318            self.input.on_key_down(k);
2319            return;
2320        }
2321        if self.handle_selbtn_key(k) {
2322            return;
2323        }
2324        if self.is_vm_key_disabled(k) {
2325            return;
2326        }
2327        self.input.on_key_down(k);
2328        if Self::is_modifier_key(k) {
2329            return;
2330        }
2331
2332        // EditBox runtime: map common keys and focus changes.
2333        self.handle_editbox_key(k);
2334
2335        let handled_mwnd_selection = self.handle_mwnd_selection_key(k);
2336
2337        // Stage group selection runtime: map Enter/Escape to a decision.
2338        if !handled_mwnd_selection {
2339            if let Some((form_id, stage_idx, group_idx)) = self.globals.focused_stage_group {
2340                if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
2341                    if let Some(list) = st.group_lists.get_mut(&stage_idx) {
2342                        if let Some(g) = list.get_mut(group_idx) {
2343                            match k {
2344                                input::VmKey::Enter => {
2345                                    let button_no = if g.hit_button_no >= 0 {
2346                                        g.hit_button_no
2347                                    } else {
2348                                        0
2349                                    };
2350                                    let was_waiting = g.wait_flag;
2351                                    if g.decide(button_no) {
2352                                        if sg_debug_enabled() {
2353                                            eprintln!(
2354                                                "[SG_DEBUG][GROUP] key_decide form={} stage={} group={} button={} wait={}",
2355                                                form_id, stage_idx, group_idx, button_no, was_waiting
2356                                            );
2357                                        }
2358                                        if was_waiting {
2359                                            self.stack.push(Value::Int(button_no));
2360                                        }
2361                                        g.wait_flag = false;
2362                                        self.globals.focused_stage_group = None;
2363                                    }
2364                                }
2365                                input::VmKey::Escape => {
2366                                    let was_waiting = g.wait_flag;
2367                                    if g.cancel().is_some() {
2368                                        if sg_debug_enabled() {
2369                                            eprintln!(
2370                                                "[SG_DEBUG][GROUP] key_cancel form={} stage={} group={} wait={} result_button={}",
2371                                                form_id, stage_idx, group_idx, was_waiting, g.result_button_no
2372                                            );
2373                                        }
2374                                        if was_waiting {
2375                                            self.stack
2376                                                .push(Value::Int(globals::TNM_GROUP_CANCELED));
2377                                        }
2378                                        g.wait_flag = false;
2379                                        self.globals.focused_stage_group = None;
2380                                    }
2381                                }
2382                                _ => {}
2383                            }
2384                        }
2385                    }
2386                }
2387            }
2388        }
2389
2390        if !self.advance_message_wait(true) {
2391            self.notify_wait_key();
2392        }
2393    }
2394
2395    pub fn on_key_up(&mut self, k: input::VmKey) {
2396        if self.is_vm_key_disabled(k) {
2397            return;
2398        }
2399        self.input.on_key_up(k);
2400        if self.globals.syscom.hide_mwnd.onoff
2401            && matches!(k, input::VmKey::Enter | input::VmKey::Escape | input::VmKey::Space)
2402        {
2403            self.globals.syscom.hide_mwnd.onoff = false;
2404            return;
2405        }
2406        if let Some(vk) = input::vmkey_to_vk_code(k) {
2407            if self.input.vk_down_up_stock(vk) {
2408                match k {
2409                    input::VmKey::Enter | input::VmKey::Space => {
2410                        self.notify_movie_wait_down_up(1);
2411                    }
2412                    input::VmKey::Escape => {
2413                        self.notify_movie_wait_down_up(-1);
2414                    }
2415                    _ => {}
2416                }
2417            }
2418        }
2419    }
2420
2421    pub fn on_text_input(&mut self, text: &str) {
2422        if self.globals.system.messagebox_modal.is_some() {
2423            return;
2424        }
2425        let Some((form_id, idx)) = self.globals.focused_editbox else {
2426            return;
2427        };
2428        let Some(list) = self.globals.editbox_lists.get_mut(&form_id) else {
2429            return;
2430        };
2431        let Some(eb) = list.boxes.get_mut(idx) else {
2432            return;
2433        };
2434        if !eb.created || !eb.visible {
2435            return;
2436        }
2437        if !text.is_empty() {
2438            eb.insert_text_at_cursor(text);
2439        }
2440    }
2441
2442    pub fn focused_editbox_ime_area(&self) -> Option<(i32, i32, i32, i32)> {
2443        let (form_id, idx) = self.globals.focused_editbox?;
2444        let eb = self.globals.editbox_lists.get(&form_id)?.boxes.get(idx)?;
2445        if !eb.created || !eb.visible {
2446            return None;
2447        }
2448        Some((
2449            eb.window_x,
2450            eb.window_y,
2451            eb.window_w.max(1),
2452            eb.window_h.max(1),
2453        ))
2454    }
2455
2456    fn open_syscom_menu_from_cancel_key(&mut self) -> bool {
2457        // Original C++ cancel_call_proc(): right-click/Escape/Z is VK_EX_CANCEL.
2458        // When the local syscom menu is enabled, it clears read-skip and calls
2459        // tnm_syscom_open(); tnm_syscom_open() enters CANCEL_SCENE when configured.
2460        if self.globals.syscom.syscom_menu_disable {
2461            return false;
2462        }
2463        if self.globals.syscom.msg_back_open || self.globals.syscom.hide_mwnd.onoff {
2464            return false;
2465        }
2466        // Original C++ cancel_call_proc() does not open another cancel/syscom scene
2467        // while an EXCALL scene is active.  This is required for syscom scenes such
2468        // as CANCEL_SCENE/sys10_qm00: the right-click stock must remain available to
2469        // that scene's own script instead of recursively opening CANCEL_SCENE again.
2470        if self.excall_state.ex_call_flag {
2471            return false;
2472        }
2473        if self.movie.current().is_some() {
2474            return false;
2475        }
2476        self.globals.syscom.read_skip.onoff = false;
2477        self.globals.syscom.pending_proc = Some(globals::SyscomPendingProc {
2478            kind: globals::SyscomPendingProcKind::OpenSyscomMenu,
2479            warning: false,
2480            se_play: false,
2481            fade_out: false,
2482            leave_msgbk: false,
2483            save_id: 0,
2484        });
2485        if std::env::var_os("SG_PROC_FLOW_TRACE").is_some() {
2486            eprintln!(
2487                "[SG_PROC_FLOW] open_syscom_menu_from_cancel_key scene={:?} line={} pending_proc={:?}",
2488                self.current_scene_name,
2489                self.current_line_no,
2490                self.globals.syscom.pending_proc
2491            );
2492        }
2493        true
2494    }
2495
2496    pub fn on_mouse_move(&mut self, x: i32, y: i32) {
2497        self.input.on_mouse_move(x, y);
2498        if let Some(idx) = self.selbtn_hit_index(x, y) {
2499            if self.globals.selbtn.cursor != idx {
2500                self.globals.selbtn.cursor = idx;
2501                self.sync_selbtn_item_selection();
2502            }
2503            return;
2504        }
2505        if self.handle_msg_back_mouse_move() {
2506            return;
2507        }
2508        self.update_object_button_hover();
2509    }
2510
2511    pub fn on_mouse_down(&mut self, b: input::VmMouseButton) {
2512        if sg_input_trace_enabled() {
2513            eprintln!(
2514                "[SG_DEBUG][INPUT] mouse_down {:?} at=({}, {})",
2515                b, self.input.mouse_x, self.input.mouse_y
2516            );
2517        }
2518        if self.handle_system_messagebox_click(b) {
2519            return;
2520        }
2521        if self.globals.syscom.msg_back_open {
2522            self.input.on_mouse_down(b);
2523            self.handle_msg_back_mouse_down(b);
2524            return;
2525        }
2526        if self.handle_selbtn_mouse_click(b) {
2527            return;
2528        }
2529        let handled_mwnd_selection = self.handle_mwnd_selection_click(b);
2530        self.input.on_mouse_down(b);
2531        self.update_editbox_focus_from_mouse_down(b);
2532        let handled_button = if !handled_mwnd_selection {
2533            self.handle_object_button_mouse_down(b)
2534        } else {
2535            false
2536        };
2537        if matches!(b, input::VmMouseButton::Right) && handled_button {
2538            self.suppress_next_right_syscom_open = true;
2539        }
2540        if !handled_button {
2541            if !self.advance_message_wait(true) {
2542                self.notify_wait_key();
2543            }
2544        }
2545    }
2546
2547    fn update_editbox_focus_from_mouse_down(&mut self, b: input::VmMouseButton) {
2548        if !matches!(b, input::VmMouseButton::Left) {
2549            return;
2550        }
2551        let x = self.input.mouse_x;
2552        let y = self.input.mouse_y;
2553        let mut new_focus = None;
2554        for (form_id, list) in self.globals.editbox_lists.iter() {
2555            for (idx, eb) in list.boxes.iter().enumerate() {
2556                if eb.contains_point(x, y) {
2557                    new_focus = Some((*form_id, idx));
2558                    break;
2559                }
2560            }
2561            if new_focus.is_some() {
2562                break;
2563            }
2564        }
2565        if new_focus.is_some() {
2566            self.globals.focused_editbox = new_focus;
2567        }
2568    }
2569
2570    pub fn on_mouse_up(&mut self, b: input::VmMouseButton) {
2571        if sg_input_trace_enabled() {
2572            eprintln!(
2573                "[SG_DEBUG][INPUT] mouse_up {:?} at=({}, {})",
2574                b, self.input.mouse_x, self.input.mouse_y
2575            );
2576        }
2577        self.input.on_mouse_up(b);
2578        if self.handle_msg_back_mouse_up(b) {
2579            return;
2580        }
2581        let movie_skipped = match b {
2582            input::VmMouseButton::Left if self.input.vk_down_up_stock(0x01) => {
2583                self.notify_movie_wait_down_up(1)
2584            }
2585            input::VmMouseButton::Right if self.input.vk_down_up_stock(0x02) => {
2586                self.notify_movie_wait_down_up(-1)
2587            }
2588            _ => false,
2589        };
2590        if movie_skipped {
2591            return;
2592        }
2593        if matches!(b, input::VmMouseButton::Right) && self.input.vk_down_up_stock(0x02) {
2594            if std::mem::take(&mut self.suppress_next_right_syscom_open) {
2595                return;
2596            }
2597            if self.open_syscom_menu_from_cancel_key() {
2598                return;
2599            }
2600        }
2601        let handled_button = self.handle_object_button_mouse_up(b);
2602        if !handled_button {
2603            self.notify_wait_key();
2604        }
2605    }
2606
2607    pub fn on_mouse_wheel(&mut self, delta_y: i32) {
2608        self.input.on_mouse_wheel(delta_y);
2609        if self.globals.syscom.msg_back_open {
2610            if delta_y > 0 {
2611                self.msg_back_target_up();
2612            } else if delta_y < 0 {
2613                self.msg_back_target_down();
2614            }
2615            return;
2616        }
2617        if delta_y > 0 && self.msg_back_is_enable() {
2618            self.open_msg_back_proc();
2619            return;
2620        }
2621        if !self.advance_message_wait(self.should_wheel_advance_message()) {
2622            self.notify_wait_key();
2623        }
2624    }
2625
2626    fn finish_skipped_movie_waits(&mut self) {
2627        while let Some(info) = self.wait.take_movie_skip() {
2628            let Some(st) = self.globals.stage_forms.get_mut(&info.stage_form_id) else {
2629                continue;
2630            };
2631            let Some(list) = st.object_lists.get_mut(&info.stage_idx) else {
2632                continue;
2633            };
2634            let Some(obj) = find_object_by_runtime_slot_mut(list, info.runtime_slot) else {
2635                continue;
2636            };
2637
2638            // Key skip triggers C_elm_object::init_type(true) on the actual object that owns
2639            // the movie, including nested CHILD objects addressed by runtime slot.
2640            let audio_id = obj.movie.audio_id.take();
2641            let backend = obj.backend.clone();
2642            obj.init_type_like();
2643
2644            if let Some(id) = audio_id {
2645                self.movie.stop_audio(id);
2646            }
2647            if let globals::ObjectBackend::Movie {
2648                layer_id,
2649                sprite_id,
2650                ..
2651            } = backend
2652            {
2653                if let Some(layer) = self.layers.layer_mut(layer_id) {
2654                    if let Some(sprite) = layer.sprite_mut(sprite_id) {
2655                        sprite.visible = false;
2656                        sprite.image_id = None;
2657                    }
2658                }
2659            }
2660        }
2661    }
2662
2663    fn handle_editbox_key(&mut self, k: input::VmKey) {
2664        let Some((form_id, idx)) = self.globals.focused_editbox else {
2665            return;
2666        };
2667        let alt_down = self.input.vk_is_down(0x12);
2668        let shift_down = self.input.vk_is_down(0x10);
2669        let mut move_focus: Option<bool> = None;
2670        let mut toggle_screen = false;
2671        {
2672            let Some(list) = self.globals.editbox_lists.get_mut(&form_id) else {
2673                return;
2674            };
2675            let Some(eb) = list.boxes.get_mut(idx) else {
2676                return;
2677            };
2678            if !eb.created || !eb.visible {
2679                return;
2680            }
2681
2682            match k {
2683                input::VmKey::Enter => {
2684                    if alt_down {
2685                        toggle_screen = true;
2686                    } else {
2687                        eb.action_flag = crate::runtime::globals::EDITBOX_ACTION_DECIDED;
2688                    }
2689                }
2690                input::VmKey::Escape => {
2691                    eb.action_flag = crate::runtime::globals::EDITBOX_ACTION_CANCELED;
2692                }
2693                input::VmKey::Backspace => {
2694                    eb.backspace_like();
2695                }
2696                input::VmKey::Tab => {
2697                    move_focus = Some(!shift_down);
2698                }
2699                _ => {}
2700            }
2701        }
2702        if toggle_screen {
2703            self.toggle_screen_size_mode_for_editbox();
2704        }
2705        if let Some(forward) = move_focus {
2706            self.move_editbox_focus(forward);
2707        }
2708    }
2709
2710    pub fn wait_poll(&mut self) -> bool {
2711        self.poll_native_messagebox_result();
2712        let (wait, stack, bgm, koe, se, pcm, globals) = (
2713            &mut self.wait,
2714            &mut self.stack,
2715            &mut self.bgm,
2716            &mut self.koe,
2717            &mut self.se,
2718            &mut self.pcm,
2719            &mut self.globals,
2720        );
2721        wait.poll(stack, bgm, koe, se, pcm, globals, &self.ids)
2722    }
2723
2724    pub fn push(&mut self, v: Value) {
2725        self.stack.push(v);
2726    }
2727
2728    pub fn pop(&mut self) -> Option<Value> {
2729        self.stack.pop()
2730    }
2731
2732    pub fn set_native_ui_backend(
2733        &mut self,
2734        backend: Option<Arc<dyn native_ui::NativeUiBackend>>,
2735    ) {
2736        self.native_ui_backend = backend;
2737    }
2738
2739    /// Return the game title for platform UI and runtime dialogs.
2740    ///
2741    /// The value is read from Gameexe `GAMENAME` when available. If Gameexe is
2742    /// missing, undecodable, or the field is empty, this returns the project
2743    /// directory name, then `Siglus` as the final fallback.
2744    pub fn game_title(&self) -> String {
2745        game_title::resolve_game_title(self.tables.gameexe.as_ref(), &self.project_dir)
2746    }
2747
2748    /// Return the game display name for bundle/mobile UI.
2749    pub fn game_name(&self) -> String {
2750        self.game_title()
2751    }
2752
2753    /// Return display metadata for platform UI.
2754    ///
2755    /// If the game directory contains `cover.png`, `cover.jpg`, `cover.jpeg`,
2756    /// `thumbnail.png`, or `icon.png`, `cover` is populated. Otherwise callers
2757    /// should display the game name.
2758    pub fn game_display_info(&self) -> game_display_info::GameDisplayInfo {
2759        let cover = game_display_info::resolve_game_cover_from_project_dir(&self.project_dir);
2760        let name = self.game_name();
2761        game_display_info::GameDisplayInfo {
2762            title: name.clone(),
2763            name,
2764            cover,
2765        }
2766    }
2767
2768    /// Return the optional cover for bundle/mobile UI.
2769    pub fn game_cover(&self) -> Option<game_display_info::GameCover> {
2770        game_display_info::resolve_game_cover_from_project_dir(&self.project_dir)
2771    }
2772
2773    pub fn submit_native_messagebox_result(&mut self, request_id: u64, value: i64) {
2774        self.native_ui
2775            .enqueue_messagebox_result(request_id, value);
2776        self.poll_native_messagebox_result();
2777    }
2778
2779    pub fn request_system_messagebox(
2780        &mut self,
2781        kind: i32,
2782        debug_only: bool,
2783        text: String,
2784        buttons: Vec<globals::SystemMessageBoxButton>,
2785    ) {
2786        self.request_system_messagebox_internal(kind, debug_only, text, buttons, true);
2787    }
2788
2789    pub fn request_system_messagebox_no_return(
2790        &mut self,
2791        kind: i32,
2792        debug_only: bool,
2793        text: String,
2794        buttons: Vec<globals::SystemMessageBoxButton>,
2795    ) {
2796        self.request_system_messagebox_internal(kind, debug_only, text, buttons, false);
2797    }
2798
2799    fn request_system_messagebox_internal(
2800        &mut self,
2801        kind: i32,
2802        debug_only: bool,
2803        text: String,
2804        buttons: Vec<globals::SystemMessageBoxButton>,
2805        complete_wait_with_value: bool,
2806    ) {
2807        let request_id = self.native_ui.next_messagebox_request_id();
2808        let native_pending = self.native_ui_backend.is_some();
2809        self.globals.system.messagebox_modal_result = None;
2810        self.globals.system.messagebox_modal = Some(globals::SystemMessageBoxModalState {
2811            request_id,
2812            kind,
2813            text: text.clone(),
2814            debug_only,
2815            buttons,
2816            cursor: 0,
2817            native_pending,
2818            complete_wait_with_value,
2819        });
2820        self.wait.wait_system_modal();
2821
2822        if let Some(backend) = self.native_ui_backend.as_ref() {
2823            backend.show_system_messagebox(native_ui::NativeMessageBoxRequest {
2824                request_id,
2825                kind: native_ui::NativeMessageBoxKind::from_system_op(kind),
2826                title: self.game_title(),
2827                message: text,
2828                buttons: self.globals.system.messagebox_modal
2829                    .as_ref()
2830                    .map(|modal| {
2831                        modal
2832                            .buttons
2833                            .iter()
2834                            .map(|button| native_ui::NativeMessageBoxButton {
2835                                label: button.label.clone(),
2836                                value: button.value,
2837                            })
2838                            .collect()
2839                    })
2840                    .unwrap_or_default(),
2841                debug_only,
2842            });
2843        }
2844    }
2845
2846    fn poll_native_messagebox_result(&mut self) {
2847        while let Some(result) = self.native_ui.pop_messagebox_result() {
2848            let Some(modal) = self.globals.system.messagebox_modal.as_ref() else {
2849                continue;
2850            };
2851            if modal.request_id != result.request_id {
2852                continue;
2853            }
2854            let max_value = modal.buttons.iter().map(|b| b.value).max().unwrap_or(0);
2855            let value = result.value.clamp(0, max_value);
2856            self.finish_system_messagebox(value);
2857            break;
2858        }
2859    }
2860
2861    pub fn set_screen_size(&mut self, w: u32, h: u32) {
2862        self.screen_w = w;
2863        self.screen_h = h;
2864        self.ui.sync_layout(&mut self.layers, w, h);
2865        self.sync_editbox_runtime();
2866    }
2867
2868    pub fn tick_frame(&mut self) {
2869        let now = crate::platform_time::Instant::now();
2870        let last = self.frame_clock_last.replace(now);
2871        let elapsed_ms = last
2872            .map(|t| now.saturating_duration_since(t).as_millis() as i32)
2873            .unwrap_or(16);
2874        let real_delta_ms = elapsed_ms.max(0);
2875        let game_delta_ms = real_delta_ms;
2876        let trace = std::env::var_os("SG_CTX_TICK_TRACE").is_some();
2877        if trace {
2878            eprintln!(
2879                "[SG_CTX_TICK] start game_delta_ms={} real_delta_ms={}",
2880                game_delta_ms, real_delta_ms
2881            );
2882        }
2883        self.sync_editbox_runtime();
2884        self.poll_native_messagebox_result();
2885        if trace {
2886            eprintln!("[SG_CTX_TICK] after sync_editbox_runtime");
2887        }
2888        self.sync_mwnd_window_ui();
2889        if trace {
2890            eprintln!("[SG_CTX_TICK] after sync_mwnd_window_ui");
2891        }
2892        self.ui.tick(
2893            &mut self.layers,
2894            &mut self.images,
2895            &self.project_dir,
2896            self.screen_w,
2897            self.screen_h,
2898            &self.globals.script,
2899            &self.globals.syscom,
2900            &self.globals.editbox_lists,
2901            self.globals.focused_editbox,
2902        );
2903        // Apply syscom flags that should skip visual transitions immediately.
2904        self.apply_syscom_skip_flags();
2905        if trace {
2906            eprintln!("[SG_CTX_TICK] after apply_syscom_skip_flags");
2907        }
2908        // Sync message length for auto-mode timing.
2909        self.globals.script.auto_mode_moji_cnt =
2910            self.ui.message_text().unwrap_or("").chars().count() as i64;
2911        if self
2912            .ui
2913            .auto_advance_due(&self.globals.script, &self.globals.syscom)
2914        {
2915            if !self.advance_message_wait(true) {
2916                self.notify_wait_key();
2917            }
2918        }
2919        // Message-window hide is visibility-only here.  UI.tick applies script hide,
2920        // SYSCOM hide, and message-back proc state without clearing message contents.
2921        self.sync_syscom_menu_ui();
2922        if trace {
2923            eprintln!("[SG_CTX_TICK] after sync_syscom_menu_ui");
2924        }
2925        self.sync_mwnd_selection_ui();
2926        if trace {
2927            eprintln!("[SG_CTX_TICK] after sync_mwnd_selection_ui");
2928        }
2929        self.globals.tick_frame(game_delta_ms, real_delta_ms);
2930        if trace {
2931            eprintln!("[SG_CTX_TICK] after globals.tick_frame");
2932        }
2933        self.apply_object_event_animations();
2934        if trace {
2935            eprintln!("[SG_CTX_TICK] after apply_object_event_animations");
2936        }
2937        self.sync_weather_objects(game_delta_ms, real_delta_ms);
2938        if trace {
2939            eprintln!("[SG_CTX_TICK] after sync_weather_objects");
2940        }
2941        let _ = self.bgm.tick(&mut self.audio);
2942        if trace {
2943            eprintln!("[SG_CTX_TICK] after bgm.tick");
2944        }
2945        self.sync_movie_objects();
2946        if trace {
2947            eprintln!("[SG_CTX_TICK] after sync_movie_objects");
2948        }
2949        self.sync_global_movie();
2950        if trace {
2951            eprintln!("[SG_CTX_TICK] after sync_global_movie");
2952        }
2953        self.update_object_button_hover();
2954        if trace {
2955            eprintln!("[SG_CTX_TICK] after update_object_button_hover");
2956        }
2957        self.apply_object_disp_override();
2958        if trace {
2959            eprintln!("[SG_CTX_TICK] after apply_object_disp_override");
2960        }
2961    }
2962
2963    fn apply_syscom_skip_flags(&mut self) {
2964        const GET_NO_WIPE_ANIME_ONOFF: i32 = 286;
2965        const GET_SKIP_WIPE_ANIME_ONOFF: i32 = 288;
2966        let cfg = &self.globals.syscom.config_int;
2967        let no_wipe = cfg.get(&GET_NO_WIPE_ANIME_ONOFF).copied().unwrap_or(0) != 0;
2968        let skip_wipe = cfg.get(&GET_SKIP_WIPE_ANIME_ONOFF).copied().unwrap_or(0) != 0;
2969        if (no_wipe || skip_wipe) && self.globals.wipe.is_some() {
2970            self.globals.finish_wipe();
2971        }
2972    }
2973
2974    fn apply_object_event_animations(&mut self) {
2975        let ids = self.ids.clone();
2976        let gfx = &mut self.gfx;
2977        let images = &mut self.images;
2978        let layers = &mut self.layers;
2979        let mwnd_ui_state = self
2980            .ui
2981            .current_mwnd_window_render_state(self.screen_w, self.screen_h);
2982        let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
2983        form_ids.sort_unstable();
2984        for form_id in form_ids {
2985            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
2986                continue;
2987            };
2988            let mut stage_ids: Vec<i64> = st
2989                .object_lists
2990                .keys()
2991                .chain(st.mwnd_lists.keys())
2992                .copied()
2993                .collect();
2994            stage_ids.sort_unstable();
2995            stage_ids.dedup();
2996            for stage_idx in stage_ids {
2997                let embedded_prefix = format!("{stage_idx}:");
2998                let embedded_slots: HashSet<usize> = st
2999                    .embedded_object_slots
3000                    .iter()
3001                    .filter_map(|(key, &slot)| key.starts_with(&embedded_prefix).then_some(slot))
3002                    .collect();
3003                let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
3004                    continue;
3005                };
3006                for (obj_idx, obj) in objs.iter_mut().enumerate() {
3007                    if embedded_slots.contains(&obj_idx) {
3008                        continue;
3009                    }
3010                    apply_object_event_animations_recursive(
3011                        &ids,
3012                        gfx,
3013                        images,
3014                        layers,
3015                        stage_idx,
3016                        object_runtime_slot(obj_idx, obj) as i64,
3017                        obj,
3018                    );
3019                }
3020            }
3021
3022            let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
3023            mwnd_stage_ids.sort_unstable();
3024            for stage_idx in mwnd_stage_ids {
3025                let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
3026                    continue;
3027                };
3028                for mwnd in mwnds {
3029                    let Some((window_x, window_y)) = mwnd.window_pos else {
3030                        continue;
3031                    };
3032                    let Some((window_w, window_h)) = mwnd.window_size else {
3033                        continue;
3034                    };
3035                    if window_w <= 0 || window_h <= 0 {
3036                        continue;
3037                    }
3038                    let visible_or_animating = mwnd.open
3039                        || mwnd_ui_state.map_or(false, |ui| {
3040                            ui.x as i64 == window_x
3041                                && ui.y as i64 == window_y
3042                                && ui.w as i64 == window_w
3043                                && ui.h as i64 == window_h
3044                        });
3045                    if !visible_or_animating {
3046                        continue;
3047                    }
3048                    for (obj_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
3049                        apply_object_event_animations_recursive(
3050                            &ids,
3051                            gfx,
3052                            images,
3053                            layers,
3054                            stage_idx,
3055                            object_runtime_slot(obj_idx, obj) as i64,
3056                            obj,
3057                        );
3058                    }
3059                    for (obj_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
3060                        apply_object_event_animations_recursive(
3061                            &ids,
3062                            gfx,
3063                            images,
3064                            layers,
3065                            stage_idx,
3066                            object_runtime_slot(obj_idx, obj) as i64,
3067                            obj,
3068                        );
3069                    }
3070                    for (obj_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
3071                        apply_object_event_animations_recursive(
3072                            &ids,
3073                            gfx,
3074                            images,
3075                            layers,
3076                            stage_idx,
3077                            object_runtime_slot(obj_idx, obj) as i64,
3078                            obj,
3079                        );
3080                    }
3081                }
3082            }
3083        }
3084    }
3085
3086    fn apply_object_masks(&mut self) {
3087        let Some(mask_info) = self.build_mask_info() else {
3088            return;
3089        };
3090        if mask_info.is_empty() {
3091            return;
3092        }
3093
3094        let mut resolved_masks = HashMap::new();
3095        for (mask_name, _, _) in mask_info.iter().flatten() {
3096            if resolved_masks.contains_key(mask_name) {
3097                continue;
3098            }
3099            if let Some(id) = self.resolve_mask_image(mask_name) {
3100                resolved_masks.insert(mask_name.clone(), id);
3101            }
3102        }
3103
3104        let ids = self.ids.clone();
3105        let gfx = &mut self.gfx;
3106        let images = &mut self.images;
3107        let layers = &mut self.layers;
3108        let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
3109        form_ids.sort_unstable();
3110        for form_id in form_ids {
3111            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
3112                continue;
3113            };
3114            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
3115            stage_ids.sort_unstable();
3116            for stage_idx in stage_ids {
3117                let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
3118                    continue;
3119                };
3120                for (obj_idx, obj) in objs.iter_mut().enumerate() {
3121                    apply_object_masks_recursive(
3122                        &ids,
3123                        gfx,
3124                        images,
3125                        layers,
3126                        stage_idx,
3127                        object_runtime_slot(obj_idx, obj) as i64,
3128                        obj,
3129                        &mask_info,
3130                        &resolved_masks,
3131                    );
3132                }
3133            }
3134        }
3135    }
3136
3137    fn active_mask_list(&self) -> Option<&globals::MaskListState> {
3138        if self.ids.form_global_mask != 0 {
3139            return self.globals.mask_lists.get(&self.ids.form_global_mask);
3140        }
3141        None
3142    }
3143
3144    fn build_mask_info(&self) -> Option<Vec<Option<(String, i32, i32)>>> {
3145        let ml = self.active_mask_list()?;
3146        let mut out = Vec::with_capacity(ml.masks.len());
3147        for m in &ml.masks {
3148            let Some(name) = m.name.as_ref() else {
3149                out.push(None);
3150                continue;
3151            };
3152            if name.is_empty() {
3153                out.push(None);
3154                continue;
3155            }
3156            let x = m.x_event.get_total_value();
3157            let y = m.y_event.get_total_value();
3158            out.push(Some((name.clone(), x, y)));
3159        }
3160        Some(out)
3161    }
3162
3163    fn resolve_mask_image(&mut self, name: &str) -> Option<ImageId> {
3164        if name.is_empty() {
3165            return None;
3166        }
3167        if let Some(path) = resolve_mask_path(&self.project_dir, name) {
3168            if let Ok(id) = self.images.load_file(&path, 0) {
3169                return Some(id);
3170            }
3171        }
3172        if let Ok(id) = self.images.load_g00(name, 0) {
3173            return Some(id);
3174        }
3175        if let Ok(id) = self.images.load_bg(name) {
3176            return Some(id);
3177        }
3178        None
3179    }
3180
3181    fn apply_object_tonecurves(&mut self) {
3182        let ids = self.ids.clone();
3183        let gfx = &mut self.gfx;
3184        let images = &mut self.images;
3185        let layers = &mut self.layers;
3186        let tonecurve = &mut self.tonecurve;
3187        let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
3188        form_ids.sort_unstable();
3189        for form_id in form_ids {
3190            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
3191                continue;
3192            };
3193            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
3194            stage_ids.sort_unstable();
3195            for stage_idx in stage_ids {
3196                let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
3197                    continue;
3198                };
3199                for (obj_idx, obj) in objs.iter_mut().enumerate() {
3200                    apply_object_tonecurves_recursive(
3201                        &ids,
3202                        gfx,
3203                        images,
3204                        layers,
3205                        tonecurve,
3206                        stage_idx,
3207                        object_runtime_slot(obj_idx, obj) as i64,
3208                        obj,
3209                    );
3210                }
3211            }
3212        }
3213    }
3214
3215    fn apply_gan_effects(&mut self, sprites: &mut Vec<RenderSprite>) {
3216        let mut index: HashMap<(Option<LayerId>, Option<SpriteId>), usize> = HashMap::new();
3217        for (i, s) in sprites.iter().enumerate() {
3218            index.insert((s.layer_id, s.sprite_id), i);
3219        }
3220
3221        let gfx = &mut self.gfx;
3222        let images = &mut self.images;
3223        let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
3224        form_ids.sort_unstable();
3225        for form_id in form_ids {
3226            let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
3227                continue;
3228            };
3229
3230            let mut object_stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
3231            object_stage_ids.sort_unstable();
3232            for stage_idx in object_stage_ids {
3233                let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
3234                    continue;
3235                };
3236                for (obj_idx, obj) in objs.iter_mut().enumerate() {
3237                    apply_gan_effects_recursive(
3238                        gfx,
3239                        images,
3240                        sprites,
3241                        &index,
3242                        stage_idx,
3243                        object_runtime_slot(obj_idx, obj) as i64,
3244                        obj,
3245                    );
3246                }
3247            }
3248
3249            let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
3250            mwnd_stage_ids.sort_unstable();
3251            for stage_idx in mwnd_stage_ids {
3252                let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
3253                    continue;
3254                };
3255                for mwnd in mwnds {
3256                    for (obj_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
3257                        apply_gan_effects_recursive(
3258                            gfx,
3259                            images,
3260                            sprites,
3261                            &index,
3262                            stage_idx,
3263                            object_runtime_slot(obj_idx, obj) as i64,
3264                            obj,
3265                        );
3266                    }
3267                    for (obj_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
3268                        apply_gan_effects_recursive(
3269                            gfx,
3270                            images,
3271                            sprites,
3272                            &index,
3273                            stage_idx,
3274                            object_runtime_slot(obj_idx, obj) as i64,
3275                            obj,
3276                        );
3277                    }
3278                    for (obj_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
3279                        apply_gan_effects_recursive(
3280                            gfx,
3281                            images,
3282                            sprites,
3283                            &index,
3284                            stage_idx,
3285                            object_runtime_slot(obj_idx, obj) as i64,
3286                            obj,
3287                        );
3288                    }
3289                }
3290            }
3291
3292            let mut btnsel_stage_ids: Vec<i64> = st.btnselitem_lists.keys().copied().collect();
3293            btnsel_stage_ids.sort_unstable();
3294            for stage_idx in btnsel_stage_ids {
3295                let Some(items) = st.btnselitem_lists.get_mut(&stage_idx) else {
3296                    continue;
3297                };
3298                for item in items {
3299                    for (obj_idx, obj) in item.object_list.iter_mut().enumerate() {
3300                        apply_gan_effects_recursive(
3301                            gfx,
3302                            images,
3303                            sprites,
3304                            &index,
3305                            stage_idx,
3306                            object_runtime_slot(obj_idx, obj) as i64,
3307                            obj,
3308                        );
3309                    }
3310                }
3311            }
3312        }
3313    }
3314
3315    fn apply_object_disp_override(&mut self) {
3316        const GET_OBJECT_DISP_ONOFF: i32 = 278;
3317        let disp_on = self
3318            .globals
3319            .syscom
3320            .config_int
3321            .get(&GET_OBJECT_DISP_ONOFF)
3322            .copied()
3323            .unwrap_or(1)
3324            != 0;
3325        if disp_on {
3326            return;
3327        }
3328
3329        let ui_layer = self.ui.mwnd.layer;
3330        for (stage_idx, list) in self
3331            .globals
3332            .stage_forms
3333            .values()
3334            .flat_map(|st| st.object_lists.iter())
3335        {
3336            for (obj_idx, obj) in list.iter().enumerate() {
3337                match &obj.backend {
3338                    globals::ObjectBackend::Rect {
3339                        layer_id,
3340                        sprite_id,
3341                        ..
3342                    }
3343                    | globals::ObjectBackend::String {
3344                        layer_id,
3345                        sprite_id,
3346                        ..
3347                    }
3348                    | globals::ObjectBackend::Movie {
3349                        layer_id,
3350                        sprite_id,
3351                        ..
3352                    } => {
3353                        if Some(*layer_id) == ui_layer {
3354                            continue;
3355                        }
3356                        if let Some(layer) = self.layers.layer_mut(*layer_id) {
3357                            if let Some(spr) = layer.sprite_mut(*sprite_id) {
3358                                spr.visible = false;
3359                            }
3360                        }
3361                    }
3362                    globals::ObjectBackend::Number {
3363                        layer_id,
3364                        sprite_ids,
3365                    }
3366                    | globals::ObjectBackend::Weather {
3367                        layer_id,
3368                        sprite_ids,
3369                    } => {
3370                        if Some(*layer_id) == ui_layer {
3371                            continue;
3372                        }
3373                        if let Some(layer) = self.layers.layer_mut(*layer_id) {
3374                            for sid in sprite_ids {
3375                                if let Some(spr) = layer.sprite_mut(*sid) {
3376                                    spr.visible = false;
3377                                }
3378                            }
3379                        }
3380                    }
3381                    globals::ObjectBackend::Gfx => {
3382                        if let Some((lid, sid)) =
3383                            self.gfx.object_sprite_binding(*stage_idx, obj_idx as i64)
3384                        {
3385                            if Some(lid) == ui_layer {
3386                                continue;
3387                            }
3388                            if let Some(layer) = self.layers.layer_mut(lid) {
3389                                if let Some(spr) = layer.sprite_mut(sid) {
3390                                    spr.visible = false;
3391                                }
3392                            }
3393                        }
3394                    }
3395                    _ => {}
3396                }
3397            }
3398        }
3399    }
3400
3401    fn handle_system_messagebox_key(&mut self, k: input::VmKey) -> bool {
3402        let Some(modal) = self.globals.system.messagebox_modal.as_mut() else {
3403            return false;
3404        };
3405        if modal.native_pending {
3406            return true;
3407        }
3408        let mut finish_value: Option<i64> = None;
3409        match k {
3410            input::VmKey::ArrowLeft | input::VmKey::ArrowUp => {
3411                let len = modal.buttons.len();
3412                if len > 0 {
3413                    modal.cursor = if modal.cursor == 0 {
3414                        len - 1
3415                    } else {
3416                        modal.cursor - 1
3417                    };
3418                }
3419            }
3420            input::VmKey::ArrowRight | input::VmKey::ArrowDown | input::VmKey::Tab => {
3421                let len = modal.buttons.len();
3422                if len > 0 {
3423                    modal.cursor = (modal.cursor + 1) % len;
3424                }
3425            }
3426            input::VmKey::Enter | input::VmKey::Space => {
3427                finish_value = Some(modal.selected_value());
3428            }
3429            input::VmKey::Escape => {
3430                finish_value = Some(modal.cancel_value());
3431            }
3432            input::VmKey::Digit(d) => {
3433                let idx = d.saturating_sub(1) as usize;
3434                if idx < modal.buttons.len() {
3435                    modal.cursor = idx;
3436                    finish_value = Some(modal.selected_value());
3437                }
3438            }
3439            _ => {}
3440        }
3441        if let Some(value) = finish_value {
3442            self.finish_system_messagebox(value);
3443        }
3444        true
3445    }
3446
3447    fn handle_system_messagebox_click(&mut self, b: input::VmMouseButton) -> bool {
3448        let Some(modal) = self.globals.system.messagebox_modal.as_mut() else {
3449            return false;
3450        };
3451        if modal.native_pending {
3452            return true;
3453        }
3454        match b {
3455            input::VmMouseButton::Left => {
3456                let len = modal.buttons.len().max(1);
3457                let bw = (self.screen_w as i32 / len as i32).max(1);
3458                let mut idx = (self.input.mouse_x.max(0) / bw) as usize;
3459                if idx >= len {
3460                    idx = len - 1;
3461                }
3462                modal.cursor = idx;
3463                let value = modal.selected_value();
3464                self.finish_system_messagebox(value);
3465            }
3466            input::VmMouseButton::Right => {
3467                let value = modal.cancel_value();
3468                self.finish_system_messagebox(value);
3469            }
3470            _ => {}
3471        }
3472        true
3473    }
3474
3475    fn finish_system_messagebox(&mut self, value: i64) {
3476        let complete_wait_with_value = self
3477            .globals
3478            .system
3479            .messagebox_modal
3480            .as_ref()
3481            .map(|modal| modal.complete_wait_with_value)
3482            .unwrap_or(false);
3483        self.globals.system.messagebox_modal = None;
3484        self.globals.system.messagebox_modal_result = Some(value);
3485        if complete_wait_with_value {
3486            self.wait.finish_system_modal(Value::Int(value));
3487        } else {
3488            self.wait.finish_system_modal_void();
3489        }
3490        self.ui.set_sys_overlay(false, String::new());
3491    }
3492
3493    fn sync_system_messagebox_ui(&mut self) -> bool {
3494        if self.globals.system.messagebox_modal.is_some() {
3495            // Desktop ports provide a separate winit dialog through NativeUiBackend;
3496            // mobile ports provide their native callback. Do not synthesize a
3497            // screen-internal overlay for message boxes.
3498            return true;
3499        }
3500        false
3501    }
3502
3503    fn msg_back_state(&self) -> Option<&globals::MsgBackState> {
3504        let form_id = self.ids.form_global_msgbk;
3505        if form_id == 0 {
3506            return None;
3507        }
3508        self.globals.msgbk_forms.get(&form_id)
3509    }
3510
3511    fn sync_msg_back_history_capacity(&mut self) {
3512        let max_count = self
3513            .gameexe_i64_default("MSGBK.HISTORY_CNT", 256)
3514            .clamp(1, 4096) as usize;
3515        let form_id = self.ids.form_global_msgbk;
3516        if form_id == 0 {
3517            return;
3518        }
3519        if let Some(st) = self.globals.msgbk_forms.get_mut(&form_id) {
3520            st.set_history_cnt_max(max_count);
3521        }
3522    }
3523
3524    fn msg_back_entry_has_content(entry: &globals::MsgBackEntry) -> bool {
3525        entry.pct_flag
3526            || !entry.msg_str.is_empty()
3527            || !entry.disp_name.is_empty()
3528            || !entry.original_name.is_empty()
3529            || !entry.koe_no_list.is_empty()
3530    }
3531
3532    fn msg_back_visible_entry_indices(&self) -> Vec<usize> {
3533        self.msg_back_state()
3534            .map(|st| {
3535                st.ordered_history_indices()
3536                    .into_iter()
3537                    .filter(|&i| st.history.get(i).map_or(false, Self::msg_back_entry_has_content))
3538                    .collect()
3539            })
3540            .unwrap_or_default()
3541    }
3542
3543    fn msg_back_is_enable(&self) -> bool {
3544        self.globals.syscom.msg_back.check_enabled() != 0 && !self.globals.script.msg_back_disable
3545    }
3546
3547    fn msg_back_line_step(&self) -> i32 {
3548        let moji_size = self.gameexe_i64_default("MSGBK.MOJI_SIZE", 24).max(1) as i32;
3549        let moji_space = self.gameexe_pair_default("MSGBK.MOJI_SPACE", (-1, 10));
3550        (moji_size + moji_space.1 as i32).max(1)
3551    }
3552
3553    fn msg_back_text_area_width(moji_cnt: (i64, i64), moji_size: i32, moji_space: (i64, i64)) -> i32 {
3554        let cols = moji_cnt.0.max(1) as i32;
3555        moji_size
3556            .saturating_mul(cols)
3557            .saturating_add((moji_space.0 as i32).saturating_mul((cols - 1).max(0)))
3558            .max(1)
3559    }
3560
3561    fn msg_back_text_area_height(moji_cnt: (i64, i64), moji_size: i32, moji_space: (i64, i64)) -> i32 {
3562        let rows = moji_cnt.1.max(1) as i32;
3563        moji_size
3564            .saturating_mul(rows)
3565            .saturating_add((moji_space.1 as i32).saturating_mul((rows - 1).max(0)))
3566            .max(1)
3567    }
3568
3569    fn msg_back_is_hankaku(ch: char) -> bool {
3570        ch.is_ascii() || matches!(ch as u32, 0xFF61..=0xFF9F)
3571    }
3572
3573    fn msg_back_is_kinsoku_moji(ch: char) -> bool {
3574        matches!(
3575            ch,
3576            'ぁ' | 'ぃ' | 'ぅ' | 'ぇ' | 'ぉ' | 'っ' | 'ゃ' | 'ゅ' | 'ょ' | 'ゎ'
3577                | 'ァ' | 'ィ' | 'ゥ' | 'ェ' | 'ォ' | 'ッ' | 'ャ' | 'ュ' | 'ョ' | 'ヮ'
3578                | 'ヵ' | 'ヶ' | '゙' | '゚' | '。' | '、' | '!' | '?' | ':' | ';' | '」'
3579                | ')' | ']' | '>' | '}' | '\'' | '"' | 'ー' | '・' | '.' | ','
3580                | 'ァ' | 'ィ' | 'ゥ' | 'ェ' | 'ォ' | 'ッ' | 'ャ' | 'ュ' | 'ョ'
3581        )
3582    }
3583
3584    fn msg_back_entry_text(entry: &globals::MsgBackEntry) -> String {
3585        if entry.pct_flag {
3586            return String::new();
3587        }
3588        let mut out = String::new();
3589        if !entry.disp_name.is_empty() {
3590            out.push_str(&entry.disp_name);
3591            out.push('\u{0007}');
3592        }
3593        if !entry.msg_str.is_empty() {
3594            out.push_str(&entry.msg_str);
3595            out.push('\u{0007}');
3596        }
3597        out
3598    }
3599
3600    fn msg_back_measure_entry_text(
3601        entry: &globals::MsgBackEntry,
3602        moji_cnt: (i64, i64),
3603        moji_size: i32,
3604        moji_space: (i64, i64),
3605    ) -> (String, i32) {
3606        let text = Self::msg_back_entry_text(entry);
3607        if text.is_empty() {
3608            return (text, moji_size.max(1));
3609        }
3610
3611        let msg_w = Self::msg_back_text_area_width(moji_cnt, moji_size, moji_space);
3612        let msg_h = Self::msg_back_text_area_height(moji_cnt, moji_size, moji_space);
3613        let space_x = moji_space.0 as i32;
3614        let space_y = moji_space.1 as i32;
3615        let line_step = (moji_size + space_y).max(1);
3616        let mut x = 0i32;
3617        let mut y = 0i32;
3618        let mut indent_pos = 0i32;
3619        let mut indent_moji = '\0';
3620        let mut indent_cnt = 0i32;
3621        let mut line_head = true;
3622
3623        let clear_indent = |indent_pos: &mut i32, indent_moji: &mut char, indent_cnt: &mut i32| {
3624            *indent_pos = 0;
3625            *indent_moji = '\0';
3626            *indent_cnt = 0;
3627        };
3628        let new_line_indent = |x: &mut i32, y: &mut i32, indent_pos: i32| {
3629            *x = indent_pos;
3630            *y = (*y).saturating_add(line_step);
3631        };
3632
3633        for ch in text.chars() {
3634            if ch == '\r' {
3635                continue;
3636            }
3637            if ch == '\n' {
3638                new_line_indent(&mut x, &mut y, indent_pos);
3639                line_head = true;
3640                continue;
3641            }
3642            if ch == '\u{0007}' {
3643                clear_indent(&mut indent_pos, &mut indent_moji, &mut indent_cnt);
3644                new_line_indent(&mut x, &mut y, indent_pos);
3645                line_head = true;
3646                continue;
3647            }
3648
3649            let this_moji_size = if Self::msg_back_is_hankaku(ch) {
3650                (moji_size / 2).max(1)
3651            } else {
3652                moji_size.max(1)
3653            };
3654            let this_check_size = this_moji_size.saturating_add(space_x);
3655            let mut auto_indent = false;
3656            if x.saturating_add(this_check_size) > msg_w.saturating_add(moji_size) {
3657                new_line_indent(&mut x, &mut y, indent_pos);
3658                auto_indent = true;
3659            } else if x.saturating_add(this_check_size) > msg_w && !Self::msg_back_is_kinsoku_moji(ch) {
3660                new_line_indent(&mut x, &mut y, indent_pos);
3661                auto_indent = true;
3662            }
3663            if auto_indent && (ch == ' ' || ch == ' ') {
3664                continue;
3665            }
3666            if y >= msg_h {
3667                break;
3668            }
3669
3670            x = x.saturating_add(this_moji_size).saturating_add(space_x);
3671
3672            if ch == '「' || ch == '『' || ch == '(' {
3673                if line_head {
3674                    indent_pos = x;
3675                    indent_moji = ch;
3676                    indent_cnt = 1;
3677                } else if ch == indent_moji {
3678                    indent_cnt += 1;
3679                }
3680            }
3681            if indent_cnt > 0 {
3682                if (indent_moji == '「' && ch == '」')
3683                    || (indent_moji == '『' && ch == '』')
3684                    || (indent_moji == '(' && ch == ')')
3685                {
3686                    indent_cnt -= 1;
3687                    if indent_cnt == 0 {
3688                        clear_indent(&mut indent_pos, &mut indent_moji, &mut indent_cnt);
3689                    }
3690                }
3691            }
3692            line_head = false;
3693        }
3694
3695        let height = y.saturating_sub(space_y).max(moji_size.max(1));
3696        (text, height)
3697    }
3698
3699    fn msg_back_image_size_by_name(&mut self, file: Option<&str>) -> Option<(i32, i32)> {
3700        let raw = file.map(str::trim).filter(|s| !s.is_empty())?;
3701        let id = self
3702            .images
3703            .load_g00(raw, 0)
3704            .or_else(|_| self.images.load_bg_frame(raw, 0))
3705            .or_else(|_| {
3706                let path = self.project_dir.join(raw);
3707                self.images.load_file(&path, 0)
3708            })
3709            .ok()?;
3710        self.images
3711            .get(id)
3712            .map(|img| (img.width as i32, img.height as i32))
3713    }
3714
3715    fn msg_back_image_size_from_gameexe(&mut self, key: &str) -> Option<(i32, i32)> {
3716        let file = self.gameexe_string(key);
3717        self.msg_back_image_size_by_name(file.as_deref())
3718    }
3719
3720    fn build_msg_back_layout(&mut self) -> MsgBackLayout {
3721        self.sync_msg_back_history_capacity();
3722        let mut out = MsgBackLayout::default();
3723        let indices = self.msg_back_visible_entry_indices();
3724        let entries: Vec<(usize, globals::MsgBackEntry)> = {
3725            let Some(st) = self.msg_back_state() else {
3726                return out;
3727            };
3728            indices
3729                .into_iter()
3730                .filter_map(|history_index| {
3731                    st.history
3732                        .get(history_index)
3733                        .cloned()
3734                        .map(|entry| (history_index, entry))
3735                })
3736                .collect()
3737        };
3738        if entries.is_empty() {
3739            return out;
3740        }
3741
3742        let moji_cnt = self.gameexe_pair_default("MSGBK.MOJI_CNT", (20, 15));
3743        let moji_size = self.gameexe_i64_default("MSGBK.MOJI_SIZE", 24).max(1) as i32;
3744        let moji_space = self.gameexe_pair_default("MSGBK.MOJI_SPACE", (-1, 10));
3745        let separator_file = self.gameexe_string("MSGBK.SEPARATOR_FILE");
3746        let separator_top_file = self.gameexe_string("MSGBK.SEPARATOR_TOP_FILE");
3747        let separator_bottom_file = self.gameexe_string("MSGBK.SEPARATOR_BOTTOM_FILE");
3748        let separator_height = self
3749            .msg_back_image_size_by_name(separator_file.as_deref())
3750            .map(|(_, h)| h.max(0))
3751            .unwrap_or(0);
3752        let separator_top_height = self
3753            .msg_back_image_size_by_name(separator_top_file.as_deref())
3754            .map(|(_, h)| h.max(0))
3755            .unwrap_or(0);
3756        let separator_bottom_height = self
3757            .msg_back_image_size_by_name(separator_bottom_file.as_deref())
3758            .map(|(_, h)| h.max(0))
3759            .unwrap_or(0);
3760
3761        if separator_top_file.is_some() && separator_top_height > 0 {
3762            out.separators.push(MsgBackSeparatorLayout {
3763                file: separator_top_file.clone(),
3764                total_pos: -separator_top_height,
3765                height: separator_top_height,
3766            });
3767        }
3768
3769        let mut total_height = 0i32;
3770        let mut last_margin = 0i32;
3771        for (visible_pos, (history_index, entry)) in entries.iter().enumerate() {
3772            if entry.pct_flag {
3773                let total_pos = total_height;
3774                let height = self
3775                    .msg_back_image_size_by_name(Some(entry.msg_str.as_str()))
3776                    .map(|(_, h)| h.max(1))
3777                    .unwrap_or_else(|| moji_size.max(1));
3778                out.entries.push(MsgBackLayoutEntry {
3779                    history_index: *history_index,
3780                    text: String::new(),
3781                    total_pos,
3782                    height,
3783                });
3784                total_height = total_height.saturating_add(height);
3785                last_margin = 0;
3786            } else {
3787                let (text, height) = Self::msg_back_measure_entry_text(entry, moji_cnt, moji_size, moji_space);
3788                let total_pos = total_height.saturating_add(last_margin);
3789                out.entries.push(MsgBackLayoutEntry {
3790                    history_index: *history_index,
3791                    text,
3792                    total_pos,
3793                    height,
3794                });
3795                total_height = total_height
3796                    .saturating_add(last_margin)
3797                    .saturating_add(height);
3798                last_margin = moji_size;
3799            }
3800
3801            if visible_pos + 1 < entries.len() {
3802                if separator_file.is_some() && separator_height > 0 {
3803                    out.separators.push(MsgBackSeparatorLayout {
3804                        file: separator_file.clone(),
3805                        total_pos: total_height,
3806                        height: separator_height,
3807                    });
3808                    total_height = total_height.saturating_add(separator_height);
3809                    last_margin = 0;
3810                }
3811            } else if separator_bottom_file.is_some() && separator_bottom_height > 0 {
3812                out.separators.push(MsgBackSeparatorLayout {
3813                    file: separator_bottom_file.clone(),
3814                    total_pos: total_height,
3815                    height: separator_bottom_height,
3816                });
3817                total_height = total_height.saturating_add(separator_bottom_height);
3818                last_margin = 0;
3819            }
3820        }
3821        out.total_height = total_height.max(0);
3822        out
3823    }
3824
3825    fn msg_back_slider_track(&self) -> (i32, i32, i32) {
3826        let vals = Self::parse_i64_list(self.gameexe_value("MSGBK_ITEM.SLIDER.POS"));
3827        if vals.len() >= 3 {
3828            (vals[0] as i32, vals[1] as i32, vals[2] as i32)
3829        } else {
3830            (0, 0, 0)
3831        }
3832    }
3833
3834    fn msg_back_slider_size_i32(&mut self) -> (i32, i32) {
3835        if let Some((w, h)) = self.msg_back_image_size_from_gameexe("MSGBK_ITEM.SLIDER.FILE") {
3836            return (w.max(0), h.max(0));
3837        }
3838        self.ui
3839            .msg_back_slider_size()
3840            .map(|(w, h)| (w as i32, h as i32))
3841            .unwrap_or((0, 0))
3842    }
3843
3844    fn limit_i32(a: i32, v: i32, b: i32) -> i32 {
3845        let lo = a.min(b);
3846        let hi = a.max(b);
3847        v.clamp(lo, hi)
3848    }
3849
3850    fn linear_i32(x: i32, x1: i32, y1: i32, x2: i32, y2: i32) -> i32 {
3851        if x1 == x2 {
3852            return y1;
3853        }
3854        let num = (x as i64 - x1 as i64) * (y2 as i64 - y1 as i64);
3855        (y1 as i64 + num / (x2 as i64 - x1 as i64)) as i32
3856    }
3857
3858    fn msg_back_scroll_limits(&self, layout: &MsgBackLayout) -> Option<(i32, i32)> {
3859        let first = layout.entries.first()?;
3860        let last = layout.entries.last()?;
3861        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
3862        let wind_height = window_size.1.max(1) as i32;
3863        let msgsp = wind_height / 2 - first.height / 2;
3864        let mut msgep = wind_height / 2 + last.height / 2 - layout.total_height;
3865        if layout.entries.len() == 1 {
3866            msgep = msgsp;
3867        }
3868        Some((msgep, msgsp))
3869    }
3870
3871    fn msg_back_calc_target_no_from_scroll(&mut self, layout: &MsgBackLayout) {
3872        if layout.entries.is_empty() {
3873            self.globals.syscom.msg_back_target_no = -1;
3874            return;
3875        }
3876        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
3877        let center = (window_size.1.max(1) as i32) / 2;
3878        let mut target = layout.entries.last().map(|e| e.history_index as isize).unwrap_or(-1);
3879        for entry in layout.entries.iter().rev() {
3880            if self.globals.syscom.msg_back_scroll_pos
3881                .saturating_add(entry.total_pos)
3882                .saturating_add(entry.height)
3883                >= center
3884            {
3885                target = entry.history_index as isize;
3886            }
3887        }
3888        self.globals.syscom.msg_back_target_no = target;
3889    }
3890
3891    fn msg_back_calc_slider_pos_from_scroll(&mut self, layout: &MsgBackLayout) {
3892        let Some((msgep, msgsp)) = self.msg_back_scroll_limits(layout) else {
3893            let (_x, top, _bottom) = self.msg_back_slider_track();
3894            self.globals.syscom.msg_back_scroll_pos = 0;
3895            self.globals.syscom.msg_back_slider_pos = top;
3896            return;
3897        };
3898        let (_x, top, bottom) = self.msg_back_slider_track();
3899        let slider_h = self.msg_back_slider_size_i32().1.max(0);
3900        let slider_end = bottom.saturating_sub(slider_h);
3901        self.globals.syscom.msg_back_scroll_pos =
3902            Self::limit_i32(msgep, self.globals.syscom.msg_back_scroll_pos, msgsp);
3903        self.globals.syscom.msg_back_slider_pos = Self::linear_i32(
3904            self.globals.syscom.msg_back_scroll_pos,
3905            msgep,
3906            slider_end,
3907            msgsp,
3908            top,
3909        );
3910        self.globals.syscom.msg_back_slider_pos =
3911            Self::limit_i32(top, self.globals.syscom.msg_back_slider_pos, slider_end);
3912    }
3913
3914    fn msg_back_calc_scroll_pos_from_slider(&mut self, layout: &MsgBackLayout) {
3915        let Some((msgep, msgsp)) = self.msg_back_scroll_limits(layout) else {
3916            self.globals.syscom.msg_back_scroll_pos = 0;
3917            return;
3918        };
3919        let (_x, top, bottom) = self.msg_back_slider_track();
3920        let slider_h = self.msg_back_slider_size_i32().1.max(0);
3921        let slider_end = bottom.saturating_sub(slider_h);
3922        self.globals.syscom.msg_back_slider_pos =
3923            Self::limit_i32(top, self.globals.syscom.msg_back_slider_pos, slider_end);
3924        self.globals.syscom.msg_back_scroll_pos = Self::linear_i32(
3925            self.globals.syscom.msg_back_slider_pos,
3926            top,
3927            msgsp,
3928            slider_end,
3929            msgep,
3930        );
3931        self.globals.syscom.msg_back_scroll_pos =
3932            Self::limit_i32(msgep, self.globals.syscom.msg_back_scroll_pos, msgsp);
3933    }
3934
3935    fn msg_back_calc_scroll_pos_from_target(&mut self, layout: &MsgBackLayout) {
3936        if layout.entries.is_empty() {
3937            self.globals.syscom.msg_back_target_no = -1;
3938            self.globals.syscom.msg_back_scroll_pos = 0;
3939            return;
3940        }
3941        let target_no = self.globals.syscom.msg_back_target_no;
3942        let entry = layout
3943            .entries
3944            .iter()
3945            .find(|entry| entry.history_index as isize == target_no)
3946            .unwrap_or_else(|| layout.entries.last().expect("layout is not empty"));
3947        self.globals.syscom.msg_back_target_no = entry.history_index as isize;
3948        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
3949        let wind_height = window_size.1.max(1) as i32;
3950        self.globals.syscom.msg_back_scroll_pos =
3951            wind_height / 2 - (entry.total_pos + entry.height / 2);
3952    }
3953
3954    fn msg_back_update_pos_from_scroll(&mut self, layout: &MsgBackLayout) {
3955        self.msg_back_calc_target_no_from_scroll(layout);
3956        self.msg_back_calc_slider_pos_from_scroll(layout);
3957    }
3958
3959    fn msg_back_update_pos_from_slider(&mut self, layout: &MsgBackLayout) {
3960        self.msg_back_calc_scroll_pos_from_slider(layout);
3961        self.msg_back_calc_target_no_from_scroll(layout);
3962    }
3963
3964    fn msg_back_update_pos_from_target(&mut self, layout: &MsgBackLayout) {
3965        self.msg_back_calc_scroll_pos_from_target(layout);
3966        self.msg_back_calc_slider_pos_from_scroll(layout);
3967    }
3968
3969    fn msg_back_target_up(&mut self) {
3970        let layout = self.build_msg_back_layout();
3971        if layout.entries.is_empty() {
3972            return;
3973        }
3974        let current = self.globals.syscom.msg_back_target_no;
3975        let pos = layout
3976            .entries
3977            .iter()
3978            .position(|entry| entry.history_index as isize == current)
3979            .unwrap_or_else(|| layout.entries.len().saturating_sub(1));
3980        let next_pos = pos.saturating_sub(1);
3981        self.globals.syscom.msg_back_target_no = layout.entries[next_pos].history_index as isize;
3982        self.msg_back_update_pos_from_target(&layout);
3983    }
3984
3985    fn msg_back_target_down(&mut self) {
3986        let layout = self.build_msg_back_layout();
3987        if layout.entries.is_empty() {
3988            return;
3989        }
3990        let current = self.globals.syscom.msg_back_target_no;
3991        let pos = layout
3992            .entries
3993            .iter()
3994            .position(|entry| entry.history_index as isize == current)
3995            .unwrap_or_else(|| layout.entries.len().saturating_sub(1));
3996        let next_pos = (pos + 1).min(layout.entries.len() - 1);
3997        self.globals.syscom.msg_back_target_no = layout.entries[next_pos].history_index as isize;
3998        self.msg_back_update_pos_from_target(&layout);
3999    }
4000
4001    fn msg_back_window_contains(&self, x: i32, y: i32) -> bool {
4002        let window_pos = self.gameexe_pair_default("MSGBK.WINDOW_POS", (10, 10));
4003        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
4004        let left = window_pos.0 as i32;
4005        let top = window_pos.1 as i32;
4006        let right = left.saturating_add(window_size.0.max(1) as i32);
4007        let bottom = top.saturating_add(window_size.1.max(1) as i32);
4008        left <= x && x < right && top <= y && y < bottom
4009    }
4010
4011    fn msg_back_initialize_open_state(&mut self, layout: &MsgBackLayout) {
4012        let (_x, _top, bottom) = self.msg_back_slider_track();
4013        let slider_h = self.msg_back_slider_size_i32().1.max(0);
4014        self.globals.syscom.msg_back_msg_total_height = layout.total_height;
4015
4016        // C_elm_msg_back::open() first places the slider at the bottom, derives
4017        // scroll/slider from that position, and only then assigns m_target_no to
4018        // m_history_last_pos. Do not use the last message target to drive the
4019        // initial scroll position here.
4020        self.globals.syscom.msg_back_slider_pos = bottom.saturating_sub(slider_h);
4021        self.msg_back_update_pos_from_slider(layout);
4022        self.msg_back_update_pos_from_scroll(layout);
4023        self.globals.syscom.msg_back_target_no = if layout.entries.is_empty() {
4024            -1
4025        } else if let Some(st) = self.msg_back_state() {
4026            if layout
4027                .entries
4028                .iter()
4029                .any(|entry| entry.history_index == st.history_last_pos)
4030            {
4031                st.history_last_pos as isize
4032            } else {
4033                layout.entries.last().map(|entry| entry.history_index as isize).unwrap_or(-1)
4034            }
4035        } else {
4036            layout.entries.last().map(|entry| entry.history_index as isize).unwrap_or(-1)
4037        };
4038        self.globals.syscom.msg_back_slider_dragging = false;
4039        self.globals.syscom.msg_back_content_dragging = false;
4040        self.globals.syscom.msg_back_proc_initialized = true;
4041    }
4042
4043    fn open_msg_back_proc(&mut self) {
4044        if !self.msg_back_is_enable() {
4045            return;
4046        }
4047        self.globals.syscom.read_skip.onoff = false;
4048        self.globals.syscom.msg_back_open = true;
4049        self.globals.syscom.pending_proc = Some(globals::SyscomPendingProc {
4050            kind: globals::SyscomPendingProcKind::MsgBack,
4051            warning: false,
4052            se_play: false,
4053            fade_out: false,
4054            leave_msgbk: false,
4055            save_id: 0,
4056        });
4057        let layout = self.build_msg_back_layout();
4058        self.msg_back_initialize_open_state(&layout);
4059    }
4060
4061    fn close_msg_back_proc(&mut self) {
4062        self.globals.syscom.msg_back_open = false;
4063        self.globals.syscom.msg_back_slider_dragging = false;
4064        self.globals.syscom.msg_back_content_dragging = false;
4065        self.globals.syscom.msg_back_proc_initialized = false;
4066        self.ui.set_msg_back_projection(None);
4067        self.ui.set_sys_overlay(false, String::new());
4068    }
4069
4070    fn handle_msg_back_key(&mut self, k: input::VmKey) -> bool {
4071        if !self.globals.syscom.msg_back_open {
4072            return false;
4073        }
4074        match k {
4075            input::VmKey::Escape | input::VmKey::Enter | input::VmKey::Space => {
4076                self.close_msg_back_proc();
4077            }
4078            input::VmKey::ArrowUp | input::VmKey::ArrowLeft => self.msg_back_target_up(),
4079            input::VmKey::ArrowDown | input::VmKey::ArrowRight => self.msg_back_target_down(),
4080            input::VmKey::F(5) => {
4081                let layout = self.build_msg_back_layout();
4082                if let Some(entry) = layout.entries.first() {
4083                    self.globals.syscom.msg_back_target_no = entry.history_index as isize;
4084                    self.msg_back_update_pos_from_target(&layout);
4085                }
4086            }
4087            input::VmKey::F(6) => {
4088                let layout = self.build_msg_back_layout();
4089                if let Some(entry) = layout.entries.last() {
4090                    self.globals.syscom.msg_back_target_no = entry.history_index as isize;
4091                    self.msg_back_update_pos_from_target(&layout);
4092                }
4093            }
4094            _ => {}
4095        }
4096        true
4097    }
4098
4099    fn handle_msg_back_mouse_down(&mut self, b: input::VmMouseButton) -> bool {
4100        if !self.globals.syscom.msg_back_open {
4101            return false;
4102        }
4103        match b {
4104            input::VmMouseButton::Right => {
4105                self.close_msg_back_proc();
4106            }
4107            input::VmMouseButton::Left => {
4108                match self.ui.msg_back_hit_action(self.input.mouse_x, self.input.mouse_y) {
4109                    Some(ui::MsgBackHitAction::Close) => self.close_msg_back_proc(),
4110                    Some(ui::MsgBackHitAction::Up) => self.msg_back_target_up(),
4111                    Some(ui::MsgBackHitAction::Down) => self.msg_back_target_down(),
4112                    Some(ui::MsgBackHitAction::Slider) => {
4113                        self.globals.syscom.msg_back_slider_dragging = true;
4114                        self.globals.syscom.msg_back_slider_drag_start_mouse = self.input.mouse_y;
4115                        self.globals.syscom.msg_back_slider_drag_start_pos =
4116                            self.globals.syscom.msg_back_slider_pos;
4117                    }
4118                    None => {
4119                        if self.msg_back_window_contains(self.input.mouse_x, self.input.mouse_y) {
4120                            self.globals.syscom.msg_back_content_dragging = true;
4121                            self.globals.syscom.msg_back_content_drag_start_mouse = self.input.mouse_y;
4122                            self.globals.syscom.msg_back_content_drag_start_scroll_pos =
4123                                self.globals.syscom.msg_back_scroll_pos;
4124                        }
4125                    }
4126                }
4127            }
4128            _ => {}
4129        }
4130        true
4131    }
4132
4133    fn handle_msg_back_mouse_up(&mut self, b: input::VmMouseButton) -> bool {
4134        if !self.globals.syscom.msg_back_open {
4135            return false;
4136        }
4137        if matches!(b, input::VmMouseButton::Left) {
4138            self.globals.syscom.msg_back_slider_dragging = false;
4139            self.globals.syscom.msg_back_content_dragging = false;
4140        }
4141        true
4142    }
4143
4144    fn handle_msg_back_mouse_move(&mut self) -> bool {
4145        if !self.globals.syscom.msg_back_open {
4146            return false;
4147        }
4148        if self.globals.syscom.msg_back_slider_dragging {
4149            let layout = self.build_msg_back_layout();
4150            self.globals.syscom.msg_back_slider_pos = self
4151                .globals
4152                .syscom
4153                .msg_back_slider_drag_start_pos
4154                .saturating_add(self.input.mouse_y - self.globals.syscom.msg_back_slider_drag_start_mouse);
4155            self.msg_back_update_pos_from_slider(&layout);
4156            return true;
4157        }
4158        if self.globals.syscom.msg_back_content_dragging {
4159            let layout = self.build_msg_back_layout();
4160            self.globals.syscom.msg_back_scroll_pos = self
4161                .globals
4162                .syscom
4163                .msg_back_content_drag_start_scroll_pos
4164                .saturating_sub(self.globals.syscom.msg_back_content_drag_start_mouse - self.input.mouse_y);
4165            self.msg_back_update_pos_from_scroll(&layout);
4166            return true;
4167        }
4168
4169        let layout = self.build_msg_back_layout();
4170        self.globals.syscom.msg_back_mouse_target_no = -1;
4171        let window_pos = self.gameexe_pair_default("MSGBK.WINDOW_POS", (10, 10));
4172        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
4173        let disp_margin = self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20));
4174        let local_x = self.input.mouse_x.saturating_sub(window_pos.0 as i32);
4175        let local_y = self.input.mouse_y.saturating_sub(window_pos.1 as i32);
4176        let in_display_rect = local_x >= disp_margin.0 as i32
4177            && local_x < window_size.0.max(1) as i32 - disp_margin.2 as i32
4178            && local_y >= disp_margin.1 as i32
4179            && local_y < window_size.1.max(1) as i32 - disp_margin.3 as i32;
4180        if in_display_rect {
4181            for entry in layout.entries.iter() {
4182                let top = entry.total_pos.saturating_add(self.globals.syscom.msg_back_scroll_pos);
4183                let bottom = top.saturating_add(entry.height);
4184                if top <= local_y && local_y < bottom {
4185                    self.globals.syscom.msg_back_mouse_target_no = entry.history_index as isize;
4186                    break;
4187                }
4188            }
4189        }
4190        false
4191    }
4192
4193    fn msg_back_build_visible_text(&self, layout: &MsgBackLayout) -> (String, i32) {
4194        if layout.entries.is_empty() {
4195            return (String::new(), self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20)).1 as i32);
4196        }
4197        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
4198        let disp_margin = self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20));
4199        let clip_top = disp_margin.1 as i32;
4200        let clip_bottom = window_size.1.max(1) as i32 - disp_margin.3 as i32;
4201        let scroll = self.globals.syscom.msg_back_scroll_pos;
4202        let line_step = self.msg_back_line_step();
4203        let mut first_idx = None;
4204        let mut last_idx = None;
4205        for (i, entry) in layout.entries.iter().enumerate() {
4206            let top = entry.total_pos.saturating_add(scroll);
4207            let bottom = top.saturating_add(entry.height);
4208            if bottom > clip_top && top < clip_bottom {
4209                if first_idx.is_none() {
4210                    first_idx = Some(i);
4211                }
4212                last_idx = Some(i);
4213            }
4214        }
4215        let Some(first) = first_idx else {
4216            let target_no = self.globals.syscom.msg_back_target_no;
4217            let entry = layout
4218                .entries
4219                .iter()
4220                .find(|entry| entry.history_index as isize == target_no)
4221                .unwrap_or_else(|| layout.entries.last().expect("layout is not empty"));
4222            return (entry.text.clone(), entry.total_pos.saturating_add(scroll));
4223        };
4224        let last = last_idx.unwrap_or(first);
4225        let mut text = String::new();
4226        for i in first..=last {
4227            let entry = &layout.entries[i];
4228            if entry.text.is_empty() {
4229                continue;
4230            }
4231            if !text.is_empty() {
4232                let prev = &layout.entries[i - 1];
4233                let gap = entry.total_pos - (prev.total_pos + prev.height);
4234                let blank_lines = (gap / line_step).max(0) as usize;
4235                for _ in 0..blank_lines {
4236                    text.push('\n');
4237                }
4238            }
4239            text.push_str(&entry.text);
4240            if !text.ends_with('\n') {
4241                text.push('\n');
4242            }
4243        }
4244        (text, layout.entries[first].total_pos.saturating_add(scroll))
4245    }
4246
4247    fn build_msg_back_projection(&mut self) -> Option<ui::MsgBackUiProjection> {
4248        if !self.globals.syscom.msg_back_open {
4249            return None;
4250        }
4251        let layout = self.build_msg_back_layout();
4252        self.globals.syscom.msg_back_msg_total_height = layout.total_height;
4253        if !self.globals.syscom.msg_back_proc_initialized {
4254            self.msg_back_initialize_open_state(&layout);
4255        } else {
4256            self.msg_back_update_pos_from_scroll(&layout);
4257        }
4258
4259        let window_pos = self.gameexe_pair_default("MSGBK.WINDOW_POS", (10, 10));
4260        let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
4261        let disp_margin = self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20));
4262        let filter_margin = self.gameexe_rect_default("MSGBK.FILTER_MARGIN", (0, 0, 0, 0));
4263        let filter_rgba = self.gameexe_rgba_default("MSGBK.FILTER_COLOR", (0, 0, 0, 0));
4264        let filter_config_rgba = self.syscom_filter_config_rgba();
4265        let moji_space = self.gameexe_pair_default("MSGBK.MOJI_SPACE", (-1, 10));
4266        let moji_size = self.gameexe_i64_default("MSGBK.MOJI_SIZE", 24).max(1);
4267        let msg_pos = self.gameexe_i64_default("MSGBK.MESSAGE_POS", 30) as i32;
4268        let order = self.gameexe_i64_default("MSGBK.ORDER", 10000) as i32;
4269        let scroll = self.globals.syscom.msg_back_scroll_pos;
4270        let (dl, dt, dr, db) = disp_margin;
4271        let clip_top = dt as i32;
4272        let clip_bottom = window_size.1.max(1) as i32 - db as i32;
4273        let moji_cnt = self.gameexe_pair_default("MSGBK.MOJI_CNT", (20, 15));
4274        let text_width = Self::msg_back_text_area_width(moji_cnt, moji_size as i32, moji_space) as u32;
4275        let base_style = TextStyle {
4276            color: self.gameexe_color(self.tables.mwnd_render.moji_color),
4277            shadow_color: self.gameexe_color(self.tables.mwnd_render.shadow_color),
4278            fuchi_color: self.gameexe_color(self.tables.mwnd_render.fuchi_color),
4279            shadow: self.globals.script.font_shadow != 0,
4280            fuchi: self.tables.mwnd_render.fuchi_color >= 0,
4281            bold: self.globals.script.font_bold != 0,
4282        };
4283        let active_style = TextStyle {
4284            color: self.gameexe_color(self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_COLOR", 7)),
4285            shadow_color: self.gameexe_color(self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_SHADOW_COLOR", 0)),
4286            fuchi_color: self.gameexe_color(self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_FUCHI_COLOR", 0)),
4287            shadow: self.globals.script.font_shadow != 0,
4288            fuchi: self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_FUCHI_COLOR", 0) >= 0,
4289            bold: self.globals.script.font_bold != 0,
4290        };
4291        let debug_style = TextStyle {
4292            color: self.gameexe_color(self.gameexe_i64_default("MSGBK.DEBUG_MOJI_COLOR", 5)),
4293            shadow_color: self.gameexe_color(self.gameexe_i64_default("MSGBK.DEBUG_MOJI_SHADOW_COLOR", 0)),
4294            fuchi_color: self.gameexe_color(self.gameexe_i64_default("MSGBK.DEBUG_MOJI_FUCHI_COLOR", 0)),
4295            shadow: self.globals.script.font_shadow != 0,
4296            fuchi: self.gameexe_i64_default("MSGBK.DEBUG_MOJI_FUCHI_COLOR", 0) >= 0,
4297            bold: self.globals.script.font_bold != 0,
4298        };
4299
4300        let koe_btn_file = self.gameexe_string("MSGBK_ITEM.KOE_BTN.FILE");
4301        let koe_btn_pos = self.msg_back_button_pos("MSGBK_ITEM.KOE_BTN.POS", (-20, -10));
4302        let load_btn_file = self.gameexe_string("MSGBK_ITEM.LOAD_BTN.FILE");
4303        let load_btn_pos = self.msg_back_button_pos("MSGBK_ITEM.LOAD_BTN.POS", (-20, 0));
4304
4305        let mut text_entries = Vec::new();
4306        let mut koe_buttons = Vec::new();
4307        let mut load_buttons = Vec::new();
4308        let separators = layout
4309            .separators
4310            .iter()
4311            .filter_map(|sep| {
4312                if sep.file.is_none() || sep.height <= 0 {
4313                    return None;
4314                }
4315                let local_y = sep.total_pos.saturating_add(scroll);
4316                let bottom = local_y.saturating_add(sep.height);
4317                if bottom > clip_top && local_y < clip_bottom {
4318                    Some(ui::MsgBackImageProjection {
4319                        file: sep.file.clone(),
4320                        x: 0,
4321                        y: local_y,
4322                    })
4323                } else {
4324                    None
4325                }
4326            })
4327            .collect::<Vec<_>>();
4328
4329        if let Some(st) = self.msg_back_state() {
4330            for layout_entry in layout.entries.iter() {
4331                let Some(entry) = st.history.get(layout_entry.history_index) else {
4332                    continue;
4333                };
4334                let local_y = layout_entry.total_pos.saturating_add(scroll);
4335                let bottom = local_y.saturating_add(layout_entry.height);
4336                let is_in_rect = bottom > clip_top && local_y < clip_bottom;
4337                if is_in_rect && !layout_entry.text.is_empty() {
4338                    let mut style = base_style;
4339                    if self.globals.system.debug_flag
4340                        && self.globals.syscom.msg_back_target_no == layout_entry.history_index as isize
4341                    {
4342                        style = debug_style;
4343                    }
4344                    if self.globals.syscom.msg_back_mouse_target_no == layout_entry.history_index as isize {
4345                        style = active_style;
4346                    }
4347                    text_entries.push(ui::MsgBackTextProjection {
4348                        history_index: layout_entry.history_index,
4349                        text: layout_entry.text.clone(),
4350                        x: msg_pos,
4351                        y: local_y,
4352                        width: text_width,
4353                        height: layout_entry.height.max(1) as u32,
4354                        style,
4355                    });
4356                }
4357                if is_in_rect && entry.pct_flag {
4358                    koe_buttons.push(ui::MsgBackEntryButtonProjection {
4359                        history_index: layout_entry.history_index,
4360                        file: Some(entry.msg_str.clone()),
4361                        x: msg_pos.saturating_add(entry.pct_pos_x),
4362                        y: local_y.saturating_add(entry.pct_pos_y),
4363                    });
4364                } else if is_in_rect && !entry.koe_no_list.is_empty() {
4365                    koe_buttons.push(ui::MsgBackEntryButtonProjection {
4366                        history_index: layout_entry.history_index,
4367                        file: koe_btn_file.clone(),
4368                        x: msg_pos.saturating_add(koe_btn_pos.0),
4369                        y: local_y.saturating_add(koe_btn_pos.1),
4370                    });
4371                }
4372                if is_in_rect && entry.save_id_check_flag {
4373                    load_buttons.push(ui::MsgBackEntryButtonProjection {
4374                        history_index: layout_entry.history_index,
4375                        file: load_btn_file.clone(),
4376                        x: msg_pos.saturating_add(load_btn_pos.0),
4377                        y: local_y.saturating_add(load_btn_pos.1),
4378                    });
4379                }
4380            }
4381        }
4382
4383        let (slider_x, _slider_top, _slider_bottom) = self.msg_back_slider_track();
4384        if std::env::var_os("SG_MSGBK_TRACE").is_some() {
4385            eprintln!(
4386                "[SG_MSGBK_TRACE][PROJECTION] entries={} separators={} text={} koe={} load={} total_height={} scroll={} slider={} target={} mouse_target={}",
4387                layout.entries.len(),
4388                layout.separators.len(),
4389                text_entries.len(),
4390                koe_buttons.len(),
4391                load_buttons.len(),
4392                layout.total_height,
4393                self.globals.syscom.msg_back_scroll_pos,
4394                self.globals.syscom.msg_back_slider_pos,
4395                self.globals.syscom.msg_back_target_no,
4396                self.globals.syscom.msg_back_mouse_target_no
4397            );
4398            for entry in &layout.entries {
4399                eprintln!(
4400                    "[SG_MSGBK_TRACE][LAYOUT] history_index={} total_pos={} height={} has_text={}",
4401                    entry.history_index,
4402                    entry.total_pos,
4403                    entry.height,
4404                    !entry.text.is_empty()
4405                );
4406            }
4407            for sep in &layout.separators {
4408                eprintln!(
4409                    "[SG_MSGBK_TRACE][SEPARATOR] file={:?} total_pos={} height={}",
4410                    sep.file,
4411                    sep.total_pos,
4412                    sep.height
4413                );
4414            }
4415        }
4416        Some(ui::MsgBackUiProjection {
4417            window_x: window_pos.0 as i32,
4418            window_y: window_pos.1 as i32,
4419            window_w: window_size.0.max(1) as u32,
4420            window_h: window_size.1.max(1) as u32,
4421            disp_margin,
4422            msg_pos,
4423            moji_size,
4424            moji_space: Some(moji_space),
4425            order,
4426            filter_layer_rep: self.tables.mwnd_render.filter_layer_rep as i32,
4427            waku_layer_rep: self.tables.mwnd_render.waku_layer_rep as i32,
4428            moji_layer_rep: self.tables.mwnd_render.moji_layer_rep as i32,
4429            waku_file: self.gameexe_string("MSGBK.BACK_FILE"),
4430            filter_file: self.gameexe_string("MSGBK.FILTER_FILE"),
4431            filter_margin,
4432            filter_rgba,
4433            filter_config_rgba,
4434            text_entries,
4435            separators,
4436            koe_buttons,
4437            load_buttons,
4438            close_btn_file: self.gameexe_string("MSGBK_ITEM.CLOSE_BTN.FILE"),
4439            close_btn_pos: self.msg_back_button_pos("MSGBK_ITEM.CLOSE_BTN.POS", (0, 0)),
4440            msg_up_btn_file: self.gameexe_string("MSGBK_ITEM.MSG_UP_BTN.FILE"),
4441            msg_up_btn_pos: self.msg_back_button_pos("MSGBK_ITEM.MSG_UP_BTN.POS", (0, 0)),
4442            msg_down_btn_file: self.gameexe_string("MSGBK_ITEM.MSG_DOWN_BTN.FILE"),
4443            msg_down_btn_pos: self.msg_back_button_pos("MSGBK_ITEM.MSG_DOWN_BTN.POS", (0, 0)),
4444            slider_file: self.gameexe_string("MSGBK_ITEM.SLIDER.FILE"),
4445            slider_rect: (slider_x, self.msg_back_slider_track().1, slider_x, self.msg_back_slider_track().2),
4446            slider_pos: (slider_x, self.globals.syscom.msg_back_slider_pos),
4447            ex_btn_files: [
4448                self.gameexe_string("MSGBK_ITEM.EX_BTN_1.FILE"),
4449                self.gameexe_string("MSGBK_ITEM.EX_BTN_2.FILE"),
4450                self.gameexe_string("MSGBK_ITEM.EX_BTN_3.FILE"),
4451                self.gameexe_string("MSGBK_ITEM.EX_BTN_4.FILE"),
4452            ],
4453            ex_btn_pos: [
4454                self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_1.POS", (0, 0)),
4455                self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_2.POS", (0, 0)),
4456                self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_3.POS", (0, 0)),
4457                self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_4.POS", (0, 0)),
4458            ],
4459        })
4460    }
4461
4462    fn sync_syscom_menu_ui(&mut self) {
4463        self.ui.set_msg_back_projection(None);
4464        self.ui.set_sys_overlay(false, String::new());
4465        if self.sync_system_messagebox_ui() {
4466            return;
4467        }
4468        if self.globals.syscom.msg_back_open {
4469            let projection = self.build_msg_back_projection();
4470            self.ui.set_msg_back_projection(projection);
4471            return;
4472        }
4473        if self.globals.syscom.menu_open {
4474            log::error!("SYSCOM menu proc is not implemented; fake Rust text menu is disabled");
4475            self.globals.syscom.menu_open = false;
4476            self.globals.syscom.menu_kind = None;
4477            self.globals.syscom.menu_result = None;
4478        }
4479    }
4480
4481    fn selbtn_choice_selectable(choice: &globals::BtnSelectChoiceState) -> bool {
4482        choice.item_type == TNM_SEL_ITEM_TYPE_ON_I64
4483    }
4484
4485    fn next_selbtn_cursor(&self, dir: i32) -> usize {
4486        let choices = &self.globals.selbtn.choices;
4487        if choices.is_empty() {
4488            return 0;
4489        }
4490        let len = choices.len() as i32;
4491        let mut idx = self.globals.selbtn.cursor.min(choices.len() - 1) as i32;
4492        for _ in 0..choices.len() {
4493            idx = (idx + dir).rem_euclid(len);
4494            if Self::selbtn_choice_selectable(&choices[idx as usize]) {
4495                return idx as usize;
4496            }
4497        }
4498        self.globals.selbtn.cursor.min(choices.len() - 1)
4499    }
4500
4501    fn sync_selbtn_item_selection(&mut self) {
4502        if let Some(st) = self.globals.stage_forms.get_mut(&self.ids.form_global_stage) {
4503            if let Some(items) = st.btnselitem_lists.get_mut(&TNM_STAGE_FRONT_I64) {
4504                for (idx, item) in items.iter_mut().enumerate() {
4505                    item.selected = idx == self.globals.selbtn.cursor;
4506                    let selectable = item.item_type == TNM_SEL_ITEM_TYPE_ON_I64;
4507                    item.button_state = if item.item_type == TNM_SEL_ITEM_TYPE_READ_I64 {
4508                        TNM_BTN_STATE_DISABLE
4509                    } else if item.selected && selectable {
4510                        TNM_BTN_STATE_HIT
4511                    } else {
4512                        TNM_BTN_STATE_NORMAL
4513                    };
4514                }
4515            }
4516        }
4517    }
4518
4519    fn hide_selbtn_object_backing(&mut self, obj: &globals::ObjectState) {
4520        match obj.backend {
4521            globals::ObjectBackend::Rect { layer_id, sprite_id, .. }
4522            | globals::ObjectBackend::String { layer_id, sprite_id, .. }
4523            | globals::ObjectBackend::Movie { layer_id, sprite_id, .. } => {
4524                if let Some(layer) = self.layers.layer_mut(layer_id) {
4525                    if let Some(sprite) = layer.sprite_mut(sprite_id) {
4526                        sprite.visible = false;
4527                        sprite.image_id = None;
4528                    }
4529                }
4530            }
4531            globals::ObjectBackend::Number { layer_id, ref sprite_ids }
4532            | globals::ObjectBackend::Weather { layer_id, ref sprite_ids } => {
4533                if let Some(layer) = self.layers.layer_mut(layer_id) {
4534                    for &sprite_id in sprite_ids {
4535                        if let Some(sprite) = layer.sprite_mut(sprite_id) {
4536                            sprite.visible = false;
4537                            sprite.image_id = None;
4538                        }
4539                    }
4540                }
4541            }
4542            globals::ObjectBackend::Gfx => {
4543                if let Some(slot) = obj.nested_runtime_slot {
4544                    let _ = self.gfx.object_clear(
4545                        &mut self.images,
4546                        &mut self.layers,
4547                        TNM_STAGE_FRONT_I64,
4548                        slot as i64,
4549                    );
4550                }
4551            }
4552            globals::ObjectBackend::None => {}
4553        }
4554        for child in &obj.runtime.child_objects {
4555            self.hide_selbtn_object_backing(child);
4556        }
4557    }
4558
4559    fn clear_selbtn_items_from_front_stage(&mut self) {
4560        let old_items = self
4561            .globals
4562            .stage_forms
4563            .get(&self.ids.form_global_stage)
4564            .and_then(|st| st.btnselitem_lists.get(&TNM_STAGE_FRONT_I64))
4565            .cloned()
4566            .unwrap_or_default();
4567        for item in &old_items {
4568            for obj in item.generated_objects.iter().chain(item.object_list.iter()) {
4569                self.hide_selbtn_object_backing(obj);
4570            }
4571        }
4572        if let Some(st) = self.globals.stage_forms.get_mut(&self.ids.form_global_stage) {
4573            st.btnselitem_lists.remove(&TNM_STAGE_FRONT_I64);
4574        }
4575    }
4576
4577    fn handle_selbtn_key(&mut self, k: input::VmKey) -> bool {
4578        if !self.globals.selbtn.started {
4579            return false;
4580        }
4581        match k {
4582            input::VmKey::ArrowUp => {
4583                self.globals.selbtn.cursor = self.next_selbtn_cursor(-1);
4584                self.sync_selbtn_item_selection();
4585                true
4586            }
4587            input::VmKey::ArrowDown => {
4588                self.globals.selbtn.cursor = self.next_selbtn_cursor(1);
4589                self.sync_selbtn_item_selection();
4590                true
4591            }
4592            input::VmKey::Enter => {
4593                let idx = self.globals.selbtn.cursor;
4594                if self
4595                    .globals
4596                    .selbtn
4597                    .choices
4598                    .get(idx)
4599                    .is_some_and(Self::selbtn_choice_selectable)
4600                {
4601                    self.finish_selbtn(idx as i64);
4602                }
4603                true
4604            }
4605            input::VmKey::Escape if self.globals.selbtn.cancel_enable => {
4606                self.finish_selbtn(-1);
4607                true
4608            }
4609            _ => true,
4610        }
4611    }
4612
4613    fn handle_selbtn_mouse_click(&mut self, b: input::VmMouseButton) -> bool {
4614        if !self.globals.selbtn.started {
4615            return false;
4616        }
4617        match b {
4618            input::VmMouseButton::Left => {
4619                if let Some(idx) = self.selbtn_hit_index(self.input.mouse_x, self.input.mouse_y) {
4620                    self.globals.selbtn.cursor = idx;
4621                    self.sync_selbtn_item_selection();
4622                    self.finish_selbtn(idx as i64);
4623                }
4624                true
4625            }
4626            input::VmMouseButton::Right if self.globals.selbtn.cancel_enable => {
4627                self.finish_selbtn(-1);
4628                true
4629            }
4630            _ => true,
4631        }
4632    }
4633
4634    fn finish_selbtn(&mut self, result: i64) {
4635        self.globals.selbtn.result = result;
4636        self.globals.selbtn.started = false;
4637        if result >= 0 {
4638            if let Some(choice) = self.globals.selbtn.choices.get(result as usize) {
4639                self.globals.syscom.system_extra_str_value = choice.text.clone();
4640            }
4641        } else {
4642            self.globals.syscom.system_extra_str_value = "(キャンセル)".to_string();
4643        }
4644        self.clear_selbtn_items_from_front_stage();
4645        self.stack.push(Value::Int(result));
4646        self.notify_wait_key();
4647    }
4648
4649    fn selbtn_hit_index(&self, mx: i32, my: i32) -> Option<usize> {
4650        if !self.globals.selbtn.started || self.globals.selbtn.choices.is_empty() {
4651            return None;
4652        }
4653        for (idx, choice) in self.globals.selbtn.choices.iter().enumerate().rev() {
4654            if !Self::selbtn_choice_selectable(choice) {
4655                continue;
4656            }
4657            let (x, y) = choice.pos;
4658            let (w, h) = choice.size;
4659            let x0 = x as i32;
4660            let y0 = y as i32;
4661            let x1 = x.saturating_add(w.max(1)) as i32;
4662            let y1 = y.saturating_add(h.max(1)) as i32;
4663            if mx >= x0 && mx < x1 && my >= y0 && my < y1 {
4664                return Some(idx);
4665            }
4666        }
4667        None
4668    }
4669
4670    fn handle_mwnd_selection_key(&mut self, k: input::VmKey) -> bool {
4671        let Some((form_id, stage_idx, mwnd_idx)) = self.globals.focused_stage_mwnd else {
4672            return false;
4673        };
4674        let trace_scene = self.current_scene_name.as_deref().unwrap_or("<none>").to_string();
4675        let trace_scene_no = self.current_scene_no.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string());
4676        let trace_line = self.current_line_no;
4677        let mut clear_focus = false;
4678        let mut handled = false;
4679        let mut close_anim: Option<(i64, i64)> = None;
4680        let mut result_to_push: Option<i64> = None;
4681        if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
4682            if let Some(list) = st.mwnd_lists.get_mut(&stage_idx) {
4683                if let Some(m) = list.get_mut(mwnd_idx) {
4684                    let close_time = m.close_anime_time;
4685                    let close_type = m.close_anime_type;
4686                    let mut close_after = false;
4687                    let mut clear_selection = false;
4688                    if let Some(sel) = m.selection.as_mut() {
4689                        handled = match k {
4690                            input::VmKey::ArrowUp => {
4691                                if !sel.choices.is_empty() {
4692                                    sel.cursor = if sel.cursor == 0 {
4693                                        sel.choices.len() - 1
4694                                    } else {
4695                                        sel.cursor - 1
4696                                    };
4697                                }
4698                                true
4699                            }
4700                            input::VmKey::ArrowDown => {
4701                                if !sel.choices.is_empty() {
4702                                    sel.cursor = (sel.cursor + 1) % sel.choices.len();
4703                                }
4704                                true
4705                            }
4706                            input::VmKey::Enter => {
4707                                sel.result = (sel.cursor as i64) + 1;
4708                                result_to_push = Some(sel.result);
4709                                close_after = sel.close_mwnd;
4710                                clear_selection = true;
4711                                clear_focus = true;
4712                                true
4713                            }
4714                            input::VmKey::Escape if sel.cancel_enable => {
4715                                sel.result = -1;
4716                                result_to_push = Some(sel.result);
4717                                close_after = sel.close_mwnd;
4718                                clear_selection = true;
4719                                clear_focus = true;
4720                                true
4721                            }
4722                            _ => false,
4723                        };
4724                    } else {
4725                        clear_focus = true;
4726                    }
4727                    if clear_selection {
4728                        m.selection = None;
4729                    }
4730                    if close_after {
4731                        let old_open = m.open;
4732                        m.open = false;
4733                        sg_mwnd_state_trace_runtime(&trace_scene, &trace_scene_no, trace_line, "MWND_SELECTION_KEY_CLOSE", stage_idx, mwnd_idx, old_open, m.open, m);
4734                        close_anim = Some((close_type, close_time));
4735                    }
4736                } else {
4737                    clear_focus = true;
4738                }
4739            } else {
4740                clear_focus = true;
4741            }
4742        } else {
4743            clear_focus = true;
4744        }
4745        if clear_focus {
4746            self.globals.focused_stage_mwnd = None;
4747        }
4748        if let Some(v) = result_to_push {
4749            self.stack.push(Value::Int(v));
4750        }
4751        if let Some((ty, ms)) = close_anim {
4752            self.ui.begin_mwnd_close(ty, ms);
4753        }
4754        handled
4755    }
4756
4757    fn handle_mwnd_selection_click(&mut self, b: input::VmMouseButton) -> bool {
4758        let Some((form_id, stage_idx, mwnd_idx)) = self.globals.focused_stage_mwnd else {
4759            return false;
4760        };
4761        let trace_scene = self.current_scene_name.as_deref().unwrap_or("<none>").to_string();
4762        let trace_scene_no = self.current_scene_no.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string());
4763        let trace_line = self.current_line_no;
4764        let mut clear_focus = false;
4765        let mut handled = false;
4766        let mut close_anim: Option<(i64, i64)> = None;
4767        let mut result_to_push: Option<i64> = None;
4768        if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
4769            if let Some(list) = st.mwnd_lists.get_mut(&stage_idx) {
4770                if let Some(m) = list.get_mut(mwnd_idx) {
4771                    let close_time = m.close_anime_time;
4772                    let close_type = m.close_anime_type;
4773                    let mut close_after = false;
4774                    let mut clear_selection = false;
4775                    if let Some(sel) = m.selection.as_mut() {
4776                        handled = match b {
4777                            input::VmMouseButton::Left => {
4778                                sel.result = (sel.cursor as i64) + 1;
4779                                result_to_push = Some(sel.result);
4780                                close_after = sel.close_mwnd;
4781                                clear_selection = true;
4782                                clear_focus = true;
4783                                true
4784                            }
4785                            input::VmMouseButton::Right if sel.cancel_enable => {
4786                                sel.result = -1;
4787                                result_to_push = Some(sel.result);
4788                                close_after = sel.close_mwnd;
4789                                clear_selection = true;
4790                                clear_focus = true;
4791                                true
4792                            }
4793                            _ => false,
4794                        };
4795                    } else {
4796                        clear_focus = true;
4797                    }
4798                    if clear_selection {
4799                        m.selection = None;
4800                    }
4801                    if close_after {
4802                        let old_open = m.open;
4803                        m.open = false;
4804                        sg_mwnd_state_trace_runtime(&trace_scene, &trace_scene_no, trace_line, "MWND_SELECTION_MOUSE_CLOSE", stage_idx, mwnd_idx, old_open, m.open, m);
4805                        close_anim = Some((close_type, close_time));
4806                    }
4807                } else {
4808                    clear_focus = true;
4809                }
4810            } else {
4811                clear_focus = true;
4812            }
4813        } else {
4814            clear_focus = true;
4815        }
4816        if clear_focus {
4817            self.globals.focused_stage_mwnd = None;
4818        }
4819        if let Some(v) = result_to_push {
4820            self.stack.push(Value::Int(v));
4821        }
4822        if let Some((ty, ms)) = close_anim {
4823            self.ui.begin_mwnd_close(ty, ms);
4824        }
4825        handled
4826    }
4827
4828    fn sync_mwnd_window_ui(&mut self) {
4829        let focused = self.globals.focused_stage_mwnd;
4830        let mut selected: Option<crate::runtime::ui::MwndProjectionState> = None;
4831
4832        for (form_id, st) in &self.globals.stage_forms {
4833            for (stage_idx, list) in &st.mwnd_lists {
4834                for (mwnd_idx, m) in list.iter().enumerate() {
4835                    if !m.open {
4836                        continue;
4837                    }
4838                    let key_icon_template = if m.icon_no >= 0 {
4839                        self.tables.icon_templates.get(m.icon_no as usize)
4840                    } else {
4841                        None
4842                    };
4843                    let page_icon_template = if m.page_icon_no >= 0 {
4844                        self.tables.icon_templates.get(m.page_icon_no as usize)
4845                    } else {
4846                        None
4847                    };
4848                    let candidate = crate::runtime::ui::MwndProjectionState {
4849                        bg_file: if m.waku_file.is_empty() {
4850                            None
4851                        } else {
4852                            Some(m.waku_file.clone())
4853                        },
4854                        filter_file: if m.filter_file.is_empty() {
4855                            None
4856                        } else {
4857                            Some(m.filter_file.clone())
4858                        },
4859                        filter_margin: m.filter_margin,
4860                        filter_color: m.filter_color,
4861                        filter_config_color: m.filter_config_color,
4862                        filter_config_tr: m.filter_config_tr,
4863                        face_file: if m.face_file.is_empty() {
4864                            None
4865                        } else {
4866                            Some(m.face_file.clone())
4867                        },
4868                        face_no: m.face_no,
4869                        rep_pos: m.rep_pos,
4870                        window_pos: m.window_pos,
4871                        window_size: m.window_size,
4872                        message_pos: m.message_pos,
4873                        message_margin: m.message_margin,
4874                        window_moji_cnt: m.window_moji_cnt,
4875                        moji_size: m.moji_size,
4876                        moji_space: m.moji_space,
4877                        mwnd_extend_type: m.mwnd_extend_type,
4878                        moji_color: m.moji_color,
4879                        shadow_color: m.shadow_color,
4880                        fuchi_color: m.fuchi_color,
4881                        chara_moji_color: m.chara_moji_color,
4882                        chara_shadow_color: m.chara_shadow_color,
4883                        chara_fuchi_color: m.chara_fuchi_color,
4884                        name_moji_color: m.name_moji_color,
4885                        name_shadow_color: m.name_shadow_color,
4886                        name_fuchi_color: m.name_fuchi_color,
4887                        key_icon_file: key_icon_template.and_then(|t| {
4888                            if t.file_name.is_empty() {
4889                                None
4890                            } else {
4891                                Some(t.file_name.clone())
4892                            }
4893                        }),
4894                        key_icon_pat_cnt: key_icon_template.map(|t| t.anime_pat_cnt).unwrap_or(1),
4895                        key_icon_speed: key_icon_template.map(|t| t.anime_speed).unwrap_or(100),
4896                        page_icon_file: page_icon_template.and_then(|t| {
4897                            if t.file_name.is_empty() {
4898                                None
4899                            } else {
4900                                Some(t.file_name.clone())
4901                            }
4902                        }),
4903                        page_icon_pat_cnt: page_icon_template.map(|t| t.anime_pat_cnt).unwrap_or(1),
4904                        page_icon_speed: page_icon_template.map(|t| t.anime_speed).unwrap_or(100),
4905                        key_icon_appear: m.key_icon_appear,
4906                        key_icon_mode: m.key_icon_mode,
4907                        key_icon_pos: m.key_icon_pos,
4908                        icon_pos_type: m.icon_pos_type,
4909                        icon_pos_base: m.icon_pos_base,
4910                        icon_pos: m.icon_pos,
4911                        slide_enabled: m.slide_msg,
4912                        slide_time: m.slide_time,
4913                        name_text: m.name_text.clone(),
4914                        msg_text: m.msg_text.clone(),
4915                    };
4916                    let is_focused = focused == Some((*form_id, *stage_idx, mwnd_idx));
4917                    if is_focused || selected.is_none() {
4918                        selected = Some(candidate);
4919                    }
4920                }
4921            }
4922        }
4923
4924        if let Some(proj) = selected {
4925            let msg_moji_no = proj
4926                .chara_moji_color
4927                .or(proj.moji_color)
4928                .unwrap_or(self.tables.mwnd_render.moji_color);
4929            let msg_shadow_no = proj
4930                .chara_shadow_color
4931                .or(proj.shadow_color)
4932                .unwrap_or(self.tables.mwnd_render.shadow_color);
4933            let msg_fuchi_no = proj
4934                .chara_fuchi_color
4935                .or(proj.fuchi_color)
4936                .unwrap_or(self.tables.mwnd_render.fuchi_color);
4937            let name_moji_no = proj
4938                .name_moji_color
4939                .or(proj.moji_color)
4940                .unwrap_or(self.tables.mwnd_render.moji_color);
4941            let name_shadow_no = proj
4942                .name_shadow_color
4943                .or(proj.shadow_color)
4944                .unwrap_or(self.tables.mwnd_render.shadow_color);
4945            let name_fuchi_no = proj
4946                .name_fuchi_color
4947                .or(proj.fuchi_color)
4948                .unwrap_or(self.tables.mwnd_render.fuchi_color);
4949            let msg_text_color = self.gameexe_color(msg_moji_no);
4950            let msg_shadow_color = self.gameexe_color(msg_shadow_no);
4951            let msg_fuchi_color = (msg_fuchi_no >= 0).then_some(self.gameexe_color(msg_fuchi_no));
4952            let name_text_color = self.gameexe_color(name_moji_no);
4953            let name_shadow_color = self.gameexe_color(name_shadow_no);
4954            let name_fuchi_color = (name_fuchi_no >= 0).then_some(self.gameexe_color(name_fuchi_no));
4955            self.ui.set_mwnd_text_colors_full(
4956                msg_text_color,
4957                msg_shadow_color,
4958                msg_fuchi_color,
4959                name_text_color,
4960                name_shadow_color,
4961                name_fuchi_color,
4962            );
4963            self.ui.apply_mwnd_projection(&proj);
4964        } else if !self.ui.mwnd.anim.visible {
4965            self.ui.clear_mwnd_window_state();
4966        }
4967    }
4968
4969    fn sync_mwnd_selection_ui(&mut self) {
4970        if self.globals.system.messagebox_modal.is_some() {
4971            return;
4972        }
4973        self.ui.set_sys_overlay(false, String::new());
4974    }
4975
4976    fn sync_movie_objects(&mut self) {
4977        let (globals, layers, movie_mgr, audio, gfx, images, ids) = (
4978            &mut self.globals,
4979            &mut self.layers,
4980            &mut self.movie,
4981            &mut self.audio,
4982            &mut self.gfx,
4983            &mut self.images,
4984            &self.ids,
4985        );
4986        let mut decoded_any = false;
4987        let mut form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
4988        form_ids.sort_unstable();
4989        for form_id in form_ids {
4990            let Some(st) = globals.stage_forms.get_mut(&form_id) else {
4991                continue;
4992            };
4993            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
4994            stage_ids.sort_unstable();
4995            for stage_idx in stage_ids {
4996                let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
4997                    continue;
4998                };
4999                for (obj_idx, obj) in objs.iter_mut().enumerate() {
5000                    sync_movie_object_recursive(
5001                        ids,
5002                        layers,
5003                        movie_mgr,
5004                        audio,
5005                        gfx,
5006                        images,
5007                        stage_idx,
5008                        object_runtime_slot(obj_idx, obj) as i64,
5009                        obj,
5010                        &mut decoded_any,
5011                    );
5012                }
5013            }
5014        }
5015        let _ = decoded_any;
5016    }
5017
5018    fn close_global_movie_runtime(&mut self) {
5019        let was_active = self.globals.mov.playing
5020            || self.globals.mov.file_name.is_some()
5021            || self.globals.mov.audio_id.is_some()
5022            || self.globals.mov.image_id.is_some();
5023
5024        if let Some(id) = self.globals.mov.audio_id.take() {
5025            self.movie.stop_audio(id);
5026        }
5027        if was_active {
5028            self.movie.stop();
5029        }
5030        if let (Some(layer_id), Some(sprite_id)) =
5031            (self.globals.mov.layer_id, self.globals.mov.sprite_id)
5032        {
5033            if let Some(sprite) = self
5034                .layers
5035                .layer_mut(layer_id)
5036                .and_then(|l| l.sprite_mut(sprite_id))
5037            {
5038                sprite.visible = false;
5039                sprite.image_id = None;
5040            }
5041        }
5042        self.globals.mov.image_id = None;
5043        self.globals.mov.last_frame_idx = None;
5044
5045        if was_active {
5046            self.globals.mov.stop();
5047        }
5048    }
5049
5050    fn sync_global_movie(&mut self) {
5051        let trace = std::env::var_os("SG_MOVIE_TRACE").is_some();
5052        let file_name = self.globals.mov.file_name.clone();
5053
5054        if !self.globals.mov.playing || file_name.as_deref().unwrap_or("").is_empty() {
5055            // Native Siglus closes C_elm_mov when a MOV wait naturally finishes or is skipped.
5056            // Keep that lifecycle here so the movie window, image, and movie audio track do not
5057            // survive past the wait procedure.
5058            self.close_global_movie_runtime();
5059            return;
5060        }
5061        let file_name = file_name.expect("checked global movie file name");
5062
5063        if let Some(id) = self.globals.mov.audio_id {
5064            if self.movie.audio_playback_finished(id) {
5065                self.globals.mov.audio_id = None;
5066                self.globals.mov.audio_start_attempted = false;
5067            }
5068        }
5069
5070        let (x, y, width, height, timer_ms, last_frame_idx, image_id, need_audio) = {
5071            let m = &self.globals.mov;
5072            (
5073                m.x,
5074                m.y,
5075                m.width.max(1),
5076                m.height.max(1),
5077                m.timer_ms,
5078                m.last_frame_idx,
5079                m.image_id,
5080                m.audio_id.is_none() && !m.audio_start_attempted,
5081            )
5082        };
5083
5084        let (layer_id, sprite_id) = match (self.globals.mov.layer_id, self.globals.mov.sprite_id) {
5085            (Some(layer_id), Some(sprite_id))
5086                if self
5087                    .layers
5088                    .layer(layer_id)
5089                    .and_then(|l| l.sprite(sprite_id))
5090                    .is_some() =>
5091            {
5092                (layer_id, sprite_id)
5093            }
5094            _ => {
5095                let layer_id = self.layers.create_layer();
5096                let sprite_id = self
5097                    .layers
5098                    .layer_mut(layer_id)
5099                    .expect("newly created global movie layer")
5100                    .create_sprite();
5101                self.globals.mov.layer_id = Some(layer_id);
5102                self.globals.mov.sprite_id = Some(sprite_id);
5103                (layer_id, sprite_id)
5104            }
5105        };
5106
5107        let polled = match self.movie.poll_global_movie_frame(&file_name, timer_ms) {
5108            Ok(Some(frame)) => frame,
5109            Ok(None) => {
5110                // Native Siglus starts MOV playback without blocking the UI thread.
5111                // Keep only the movie timer at the start until the first frame exists.
5112                // Do not reset the global frame clock here, because that throttles all
5113                // counters, frame actions, and object events while the decoder warms up.
5114                if last_frame_idx.is_none() {
5115                    self.globals.mov.timer_ms = 0;
5116                }
5117                return;
5118            }
5119            Err(err) => {
5120                eprintln!("[SG_MOV] error file={} err={:#}", file_name, err);
5121                self.globals.mov.playing = false;
5122                return;
5123            }
5124        };
5125
5126        if let Some(ms) = polled.clamped_timer_ms {
5127            self.globals.mov.timer_ms = ms;
5128        }
5129        if self.globals.mov.total_ms.is_none() || polled.total_ms.is_some() {
5130            self.globals.mov.total_ms = polled.total_ms.or(self.globals.mov.total_ms);
5131        }
5132        if let Some(total) = self.globals.mov.total_ms {
5133            if total > 0 && self.globals.mov.timer_ms >= total {
5134                self.globals.mov.timer_ms = total;
5135                self.globals.mov.playing = false;
5136            }
5137        }
5138        let waiting_for_movie_audio_start =
5139            need_audio && polled.audio.is_none() && !polled.audio_ready;
5140        let _ = polled.decoded_now;
5141
5142        let frame = polled.frame.clone();
5143        let frame_idx = polled.frame_idx;
5144
5145        if need_audio {
5146            if let Some(track) = polled.audio.as_ref() {
5147                match self
5148                    .movie
5149                    .start_audio(&mut self.audio, track, self.globals.mov.timer_ms)
5150                {
5151                    Ok(id) => {
5152                        self.globals.mov.audio_id = Some(id);
5153                        self.globals.mov.audio_start_attempted = false;
5154                        if trace || sg_debug_enabled() {
5155                            eprintln!(
5156                                "[SG_DEBUG][MOV] audio_start file={} samples={} channels={} rate={} offset_ms={}",
5157                                file_name,
5158                                track.samples.len(),
5159                                track.channels,
5160                                track.sample_rate,
5161                                self.globals.mov.timer_ms
5162                            );
5163                        }
5164                    }
5165                    Err(err) => {
5166                        eprintln!(
5167                            "[SG_MOV] audio_start.failed file={} channels={} rate={} samples={} err={:#}",
5168                            file_name,
5169                            track.channels,
5170                            track.sample_rate,
5171                            track.samples.len(),
5172                            err
5173                        );
5174                    }
5175                }
5176            } else if polled.audio_ready {
5177                self.globals.mov.audio_start_attempted = true;
5178                if trace || sg_debug_enabled() {
5179                    eprintln!("[SG_DEBUG][MOV] audio_track.missing file={}", file_name);
5180                }
5181            }
5182        }
5183
5184        let img_id = if image_id.is_some() && last_frame_idx != Some(frame_idx) {
5185            let id = image_id.unwrap();
5186            let _ = self.images.replace_image_arc(id, frame.clone());
5187            id
5188        } else if let Some(id) = image_id {
5189            id
5190        } else {
5191            self.images.insert_image_arc(frame.clone())
5192        };
5193        self.globals.mov.image_id = Some(img_id);
5194        self.globals.mov.last_frame_idx = Some(frame_idx);
5195
5196        if let Some(sprite) = self
5197            .layers
5198            .layer_mut(layer_id)
5199            .and_then(|l| l.sprite_mut(sprite_id))
5200        {
5201            sprite.visible = true;
5202            sprite.image_id = Some(img_id);
5203            sprite.fit = SpriteFit::PixelRect;
5204            sprite.size_mode = SpriteSizeMode::Explicit { width, height };
5205            sprite.x = x;
5206            sprite.y = y;
5207            sprite.alpha = 255;
5208            sprite.tr = 255;
5209            sprite.alpha_blend = true;
5210            sprite.order = i32::MAX - 16;
5211        }
5212
5213        if waiting_for_movie_audio_start && self.globals.mov.audio_id.is_none() {
5214            self.globals.mov.timer_ms = 0;
5215        }
5216
5217        if trace {
5218            eprintln!(
5219                "[SG_MOVIE_TRACE] global MOV frame file={} idx={} timer={} pos=({}, {}) size={}x{} layer={} sprite={}",
5220                file_name, frame_idx, self.globals.mov.timer_ms, x, y, width, height, layer_id, sprite_id
5221            );
5222        }
5223    }
5224
5225    fn sync_weather_objects(&mut self, game_delta_ms: i32, real_delta_ms: i32) {
5226        let screen_w = self.screen_w.max(1) as i64;
5227        let screen_h = self.screen_h.max(1) as i64;
5228        let (globals, layers, images, ids) = (
5229            &mut self.globals,
5230            &mut self.layers,
5231            &mut self.images,
5232            &self.ids,
5233        );
5234        let mut form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
5235        form_ids.sort_unstable();
5236        for form_id in form_ids {
5237            let Some(st) = globals.stage_forms.get_mut(&form_id) else {
5238                continue;
5239            };
5240            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
5241            stage_ids.sort_unstable();
5242            for stage_idx in stage_ids {
5243                let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
5244                    continue;
5245                };
5246                for obj in objs.iter_mut() {
5247                    sync_weather_object_recursive(
5248                        ids,
5249                        layers,
5250                        images,
5251                        screen_w,
5252                        screen_h,
5253                        game_delta_ms,
5254                        real_delta_ms,
5255                        obj,
5256                    );
5257                }
5258            }
5259        }
5260    }
5261
5262    fn repair_missing_gfx_leaf_images(&mut self) {
5263        fn collect(
5264            ids: &crate::runtime::constants::RuntimeConstants,
5265            stage_idx: i64,
5266            objs: &[globals::ObjectState],
5267            out: &mut Vec<(i64, usize, String, i64)>,
5268        ) {
5269            for (idx, obj) in objs.iter().enumerate() {
5270                if obj.used && matches!(obj.backend, globals::ObjectBackend::Gfx) {
5271                    let slot = object_runtime_slot(idx, obj);
5272                    let file = obj.file_name.clone();
5273                    if let Some(file) = file {
5274                        if !file.is_empty() {
5275                            let patno = obj.lookup_int_prop(ids, ids.obj_patno).unwrap_or(0);
5276                            out.push((stage_idx, slot, file, patno));
5277                        }
5278                    }
5279                }
5280                if !obj.runtime.child_objects.is_empty() {
5281                    collect(ids, stage_idx, &obj.runtime.child_objects, out);
5282                }
5283            }
5284        }
5285
5286        let mut tasks: Vec<(i64, usize, String, i64)> = Vec::new();
5287        let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
5288        form_ids.sort_unstable();
5289        for form_id in form_ids {
5290            let Some(st) = self.globals.stage_forms.get(&form_id) else {
5291                continue;
5292            };
5293            let mut stage_ids: Vec<i64> = st
5294                .object_lists
5295                .keys()
5296                .chain(st.mwnd_lists.keys())
5297                .copied()
5298                .collect();
5299            stage_ids.sort_unstable();
5300            stage_ids.dedup();
5301            for stage_idx in stage_ids {
5302                if let Some(objs) = st.object_lists.get(&stage_idx) {
5303                    collect(&self.ids, stage_idx, objs, &mut tasks);
5304                }
5305                if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
5306                    for mwnd in mwnds {
5307                        collect(&self.ids, stage_idx, &mwnd.button_list, &mut tasks);
5308                        collect(&self.ids, stage_idx, &mwnd.face_list, &mut tasks);
5309                        collect(&self.ids, stage_idx, &mwnd.object_list, &mut tasks);
5310                    }
5311                }
5312            }
5313        }
5314
5315        for (stage_idx, runtime_slot, state_file, state_patno) in tasks {
5316            let Some((layer_id, sprite_id)) = self
5317                .gfx
5318                .object_sprite_binding(stage_idx, runtime_slot as i64)
5319            else {
5320                continue;
5321            };
5322            let needs_image = self
5323                .layers
5324                .layer(layer_id)
5325                .and_then(|layer| layer.sprite(sprite_id))
5326                .map(|sprite| sprite.image_id.is_none())
5327                .unwrap_or(false);
5328            if !needs_image {
5329                continue;
5330            }
5331
5332            let file = self
5333                .gfx
5334                .object_peek_file(stage_idx, runtime_slot as i64)
5335                .unwrap_or_else(|| state_file.clone());
5336            if file.is_empty() {
5337                continue;
5338            }
5339            let patno = self
5340                .gfx
5341                .object_peek_patno(stage_idx, runtime_slot as i64)
5342                .unwrap_or(state_patno)
5343                .max(0) as u32;
5344
5345            let img_id = match self.images.load_g00(&file, patno) {
5346                Ok(id) => Ok(id),
5347                Err(_) => self.images.load_bg_frame(&file, patno as usize),
5348            };
5349            match img_id {
5350                Ok(img_id) => {
5351                    if let Some(layer) = self.layers.layer_mut(layer_id) {
5352                        if let Some(sprite) = layer.sprite_mut(sprite_id) {
5353                            sprite.image_id = Some(img_id);
5354                            if let Some(img) = self.images.get(img_id) {
5355                                sprite.object_anchor = true;
5356                                sprite.texture_center_x = img.center_x as f32;
5357                                sprite.texture_center_y = img.center_y as f32;
5358                            } else {
5359                                sprite.object_anchor = false;
5360                                sprite.texture_center_x = 0.0;
5361                                sprite.texture_center_y = 0.0;
5362                            }
5363                        }
5364                    }
5365                }
5366                Err(err) => {
5367                    self.unknown.record_note(&format!(
5368                        "gfx.image.repair.failed:stage={stage_idx}:slot={runtime_slot}:file={file}:patno={patno}:{err}"
5369                    ));
5370                }
5371            }
5372        }
5373    }
5374
5375    /// Build a render list and apply screen/wipe effects.
5376    ///
5377    /// Original Siglus does not render from a flat layer list. It first builds a
5378    /// stage/object sprite tree and then flattens that tree. We mirror that shape here:
5379    /// use the existing layer-backed sprites only as leaf payloads, but rebuild the final
5380    /// submission order from stage -> top-level object -> child objects.
5381    fn build_render_list_pre_wipe(&mut self) -> (Vec<RenderSprite>, Vec<String>) {
5382        self.layers.reset_runtime_effects();
5383        self.repair_missing_gfx_leaf_images();
5384        self.apply_object_masks();
5385        self.apply_object_tonecurves();
5386        let base = self.layers.render_list();
5387        let (mut list, debug_lines) =
5388            build_siglus_object_render_list(self, &base, TNM_STAGE_FRONT_I64);
5389        apply_quake(&self.globals, &mut list);
5390        apply_button_visuals(self, &mut list);
5391        apply_selbtn_item_visuals(self, &mut list);
5392        self.apply_gan_effects(&mut list);
5393        apply_screen_effects(&self.globals, &self.ids, &mut list);
5394        (list, debug_lines)
5395    }
5396
5397    pub fn render_list_with_effects(&mut self) -> Vec<RenderSprite> {
5398        self.render_list_with_effects_inner(true)
5399    }
5400
5401    fn render_list_with_effects_inner(&mut self, include_mouse_cursor: bool) -> Vec<RenderSprite> {
5402        let (pre_wipe_list, debug_lines) = self.build_render_list_pre_wipe();
5403        let mut list = if self.globals.wipe.is_some() {
5404            let base = self.layers.render_list();
5405            let (next_list, next_debug_lines) = build_siglus_object_render_list(self, &base, TNM_STAGE_NEXT_I64);
5406            if config_button_trace_enabled() {
5407                eprintln!(
5408                    "[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] wipe_active=true pre_wipe_len={} next_len={} next_debug_lines={} wipe_type={:?}",
5409                    pre_wipe_list.len(),
5410                    next_list.len(),
5411                    next_debug_lines.len(),
5412                    self.globals.wipe.as_ref().map(|w| w.wipe_type)
5413                );
5414                for line in next_debug_lines.iter().filter(|line| line.contains("CONFIG_BUTTON_TRACE")) {
5415                    eprintln!("{}", line);
5416                }
5417            }
5418            if let Some(composed) = build_dual_source_wipe_list(self, &pre_wipe_list, &next_list) {
5419                if config_button_trace_enabled() {
5420                    eprintln!("[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] wipe_compose=dual_source");
5421                }
5422                composed
5423            } else if let Some(composed) =
5424                build_regular_stage_wipe_list(self, &pre_wipe_list, &next_list)
5425            {
5426                if config_button_trace_enabled() {
5427                    eprintln!("[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] wipe_compose=regular");
5428                }
5429                composed
5430            } else {
5431                if config_button_trace_enabled() {
5432                    eprintln!("[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] wipe_compose=effect_fallback");
5433                }
5434                let mut l = pre_wipe_list.clone();
5435                apply_wipe_effect(self, &mut l);
5436                l.retain(render_sprite_visible_for_submit);
5437                l
5438            }
5439        } else {
5440            if config_button_trace_enabled() {
5441                eprintln!(
5442                    "[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] wipe_active=false pre_wipe_len={}",
5443                    pre_wipe_list.len()
5444                );
5445            }
5446            pre_wipe_list.clone()
5447        };
5448        let before_retain_len = list.len();
5449        list.retain(render_sprite_visible_for_submit);
5450        if config_button_trace_enabled() && before_retain_len != list.len() {
5451            eprintln!(
5452                "[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] final_retain before={} after={}",
5453                before_retain_len,
5454                list.len()
5455            );
5456        }
5457        if config_button_trace_enabled() {
5458            trace_final_render_order(self, &list);
5459        }
5460        if save_load_render_trace_enabled() {
5461            trace_save_load_render_sprites(self, &list);
5462        }
5463        overlay_precompose_if_needed(self, &mut list);
5464        if include_mouse_cursor {
5465            self.append_mouse_cursor_sprite(&mut list);
5466        }
5467        if self.globals.wipe.is_none() {
5468            self.last_presented_render_list = pre_wipe_list.clone();
5469        }
5470        if sg_render_tree_debug_enabled() {
5471            use std::sync::atomic::{AtomicU64, Ordering};
5472            static FRAME_NO: AtomicU64 = AtomicU64::new(0);
5473            let frame_no = FRAME_NO.fetch_add(1, Ordering::Relaxed) + 1;
5474            eprintln!("[SG_DEBUG] ===== frame {} =====", frame_no);
5475            for line in debug_lines {
5476                eprintln!("{}", line);
5477            }
5478            if let Some(wipe) = self.globals.wipe.as_ref() {
5479                eprintln!(
5480                    "[SG_DEBUG] wipe active type={} progress={:.3} range=({},{})->({},{}) with_low={} wait={}",
5481                    wipe.wipe_type,
5482                    wipe.progress(),
5483                    wipe.begin_order,
5484                    wipe.begin_layer,
5485                    wipe.end_order,
5486                    wipe.end_layer,
5487                    wipe.with_low_order,
5488                    wipe.wait_flag,
5489                );
5490            }
5491            eprintln!("[SG_DEBUG] submitted_render_list len={}", list.len());
5492            for (i, rs) in list.iter().enumerate() {
5493                eprintln!(
5494                    "[SG_DEBUG]   render[{}] layer={:?} sprite={:?} img={:?} pos=({}, {}) sorter=({}, {}) order={} alpha={} tr={} alpha_blend={} blend={:?} fit={:?} size={:?} dst_clip={:?} src_clip={:?} scale=({:.3}, {:.3}) rot={:.3} anchor={} tex_center=({:.3},{:.3}) pivot=({:.3},{:.3},{:.3})",
5495                    i,
5496                    rs.layer_id,
5497                    rs.sprite_id,
5498                    rs.sprite.image_id,
5499                    rs.sprite.x,
5500                    rs.sprite.y,
5501                    rs.sorter_order,
5502                    rs.sorter_layer,
5503                    rs.sprite.order,
5504                    rs.sprite.alpha,
5505                    rs.sprite.tr,
5506                    rs.sprite.alpha_blend,
5507                    rs.sprite.blend,
5508                    rs.sprite.fit,
5509                    rs.sprite.size_mode,
5510                    rs.sprite.dst_clip,
5511                    rs.sprite.src_clip,
5512                    rs.sprite.scale_x,
5513                    rs.sprite.scale_y,
5514                    rs.sprite.rotate,
5515                    rs.sprite.object_anchor,
5516                    rs.sprite.texture_center_x,
5517                    rs.sprite.texture_center_y,
5518                    rs.sprite.pivot_x,
5519                    rs.sprite.pivot_y,
5520                    rs.sprite.pivot_z,
5521                );
5522            }
5523        }
5524        list
5525    }
5526
5527    pub fn debug_active_texture_entries(
5528        &self,
5529        submitted: &[RenderSprite],
5530    ) -> Vec<DebugActiveTextureEntry> {
5531        let mut submitted_keys: HashSet<(LayerId, SpriteId)> = HashSet::new();
5532        let mut submitted_images: HashSet<ImageId> = HashSet::new();
5533        for rs in submitted {
5534            if let Some(id) = rs.sprite.image_id {
5535                submitted_images.insert(id);
5536            }
5537            if let (Some(layer_id), Some(sprite_id)) = (rs.layer_id, rs.sprite_id) {
5538                submitted_keys.insert((layer_id, sprite_id));
5539            }
5540        }
5541
5542        let mut acc: HashMap<ImageId, DebugActiveTextureAccum> = HashMap::new();
5543        let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
5544        form_ids.sort_unstable();
5545        for form_id in form_ids {
5546            let Some(st) = self.globals.stage_forms.get(&form_id) else {
5547                continue;
5548            };
5549            let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
5550            stage_ids.sort_unstable();
5551            for stage_idx in stage_ids {
5552                let Some(list) = st.object_lists.get(&stage_idx) else {
5553                    continue;
5554                };
5555                for (obj_idx, obj) in list.iter().enumerate() {
5556                    collect_debug_active_textures_from_object(
5557                        self,
5558                        form_id,
5559                        stage_idx,
5560                        obj_idx,
5561                        obj,
5562                        &submitted_keys,
5563                        &submitted_images,
5564                        &mut acc,
5565                    );
5566                }
5567            }
5568        }
5569
5570        let mut out: Vec<DebugActiveTextureEntry> = acc
5571            .into_iter()
5572            .map(|(image_id, entry)| DebugActiveTextureEntry {
5573                image_id,
5574                width: entry.width,
5575                height: entry.height,
5576                source_label: entry.source_label,
5577                submitted_this_frame: entry.submitted_this_frame,
5578                visible_refs: entry.visible_refs,
5579                total_refs: entry.total_refs,
5580                ref_summary: if entry.ref_labels.is_empty() {
5581                    String::new()
5582                } else {
5583                    entry.ref_labels.join(" | ")
5584                },
5585            })
5586            .collect();
5587        out.sort_by(|a, b| {
5588            b.submitted_this_frame
5589                .cmp(&a.submitted_this_frame)
5590                .then_with(|| b.visible_refs.cmp(&a.visible_refs))
5591                .then_with(|| b.total_refs.cmp(&a.total_refs))
5592                .then_with(|| a.image_id.0.cmp(&b.image_id.0))
5593        });
5594        out
5595    }
5596
5597    /// Capture the current frame (UI + scene) into a CPU RGBA buffer.
5598    pub fn capture_frame_rgba(&mut self) -> RgbaImage {
5599        let sprites = self.render_list_with_effects_inner(false);
5600        soft_render::render_to_image(&self.images, &sprites, self.screen_w, self.screen_h)
5601    }
5602
5603    /// Capture only sprites up to the original engine order/layer cut line.
5604    pub fn capture_frame_rgba_until(&mut self, end_order: i64, end_layer: i64) -> RgbaImage {
5605        let order = end_order.clamp(i32::MIN as i64 / 1024, i32::MAX as i64 / 1024);
5606        let layer = end_layer.clamp(-1023, 1023);
5607        let limit = order
5608            .saturating_mul(1024)
5609            .saturating_add(layer)
5610            .clamp(i32::MIN as i64, i32::MAX as i64) as i32;
5611        let mut sprites = self.render_list_with_effects_inner(false);
5612        sprites.retain(|rs| rs.sprite.order <= limit);
5613        soft_render::render_to_image(&self.images, &sprites, self.screen_w, self.screen_h)
5614    }
5615}
5616
5617fn collect_debug_active_textures_from_object(
5618    ctx: &CommandContext,
5619    stage_form_id: u32,
5620    stage_idx: i64,
5621    obj_idx: usize,
5622    obj: &globals::ObjectState,
5623    submitted_keys: &HashSet<(LayerId, SpriteId)>,
5624    submitted_images: &HashSet<ImageId>,
5625    out: &mut HashMap<ImageId, DebugActiveTextureAccum>,
5626) {
5627    if !object_participates_in_tree(obj) {
5628        return;
5629    }
5630
5631    let info = effective_object_info(ctx, stage_idx, obj_idx, obj);
5632    let bound = fetch_bound_render_sprites_any(ctx, stage_idx, info.runtime_slot, obj);
5633    for rs in bound {
5634        let Some(image_id) = rs.sprite.image_id else {
5635            continue;
5636        };
5637        let submitted = submitted_images.contains(&image_id)
5638            || rs
5639                .layer_id
5640                .zip(rs.sprite_id)
5641                .map(|key| submitted_keys.contains(&key))
5642                .unwrap_or(false);
5643        let debug_img = ctx.images.debug_image_info(image_id);
5644        let entry = out
5645            .entry(image_id)
5646            .or_insert_with(|| DebugActiveTextureAccum {
5647                width: debug_img.as_ref().map(|d| d.width).unwrap_or(0),
5648                height: debug_img.as_ref().map(|d| d.height).unwrap_or(0),
5649                source_label: debug_img
5650                    .as_ref()
5651                    .and_then(|d| {
5652                        d.source_path.as_ref().map(|p| {
5653                            if let Some(frame_index) = d.frame_index {
5654                                format!("{}#{}", p.display(), frame_index)
5655                            } else {
5656                                p.display().to_string()
5657                            }
5658                        })
5659                    })
5660                    .unwrap_or_else(|| {
5661                        obj.file_name
5662                            .clone()
5663                            .unwrap_or_else(|| "<dynamic>".to_string())
5664                    }),
5665                submitted_this_frame: false,
5666                visible_refs: 0,
5667                total_refs: 0,
5668                ref_labels: Vec::new(),
5669            });
5670        entry.submitted_this_frame |= submitted;
5671        entry.total_refs += 1;
5672        if info.disp {
5673            entry.visible_refs += 1;
5674        }
5675        let file = obj.file_name.as_deref().unwrap_or("-");
5676        let ref_label = format!(
5677            "sf{} st{} slot{} {} disp={} backend={}",
5678            stage_form_id,
5679            stage_idx,
5680            info.runtime_slot,
5681            file,
5682            if info.disp { 1 } else { 0 },
5683            debug_object_backend_name(obj)
5684        );
5685        if !entry.ref_labels.iter().any(|s| s == &ref_label) {
5686            if entry.ref_labels.len() < 3 {
5687                entry.ref_labels.push(ref_label);
5688            } else if entry.ref_labels.len() == 3 {
5689                entry.ref_labels.push("...".to_string());
5690            }
5691        }
5692    }
5693
5694    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
5695        collect_debug_active_textures_from_object(
5696            ctx,
5697            stage_form_id,
5698            stage_idx,
5699            child_idx,
5700            child,
5701            submitted_keys,
5702            submitted_images,
5703            out,
5704        );
5705    }
5706}
5707
5708fn debug_object_backend_name(obj: &globals::ObjectState) -> &'static str {
5709    match &obj.backend {
5710        globals::ObjectBackend::None => "None",
5711        globals::ObjectBackend::Gfx => "Gfx",
5712        globals::ObjectBackend::Rect { .. } => "Rect",
5713        globals::ObjectBackend::String { .. } => "String",
5714        globals::ObjectBackend::Number { .. } => "Number",
5715        globals::ObjectBackend::Weather { .. } => "Weather",
5716        globals::ObjectBackend::Movie { .. } => "Movie",
5717    }
5718}
5719
5720fn sg_debug_enabled() -> bool {
5721    matches!(
5722        std::env::var("SG_DEBUG").ok().as_deref(),
5723        Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
5724    )
5725}
5726
5727fn sg_input_trace_enabled() -> bool {
5728    matches!(
5729        std::env::var("SG_INPUT_TRACE").ok().as_deref(),
5730        Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
5731    )
5732}
5733
5734fn sg_mwnd_object_trace_enabled() -> bool {
5735    matches!(
5736        std::env::var("SG_MWND_OBJECT_TRACE").ok().as_deref(),
5737        Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
5738    )
5739}
5740
5741fn sg_render_tree_debug_enabled() -> bool {
5742    matches!(
5743        std::env::var("SG_RENDER_TREE_DEBUG").ok().as_deref(),
5744        Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
5745    )
5746}
5747
5748fn config_button_trace_enabled() -> bool {
5749    matches!(
5750        std::env::var("SG_CONFIG_BUTTON_TRACE").ok().as_deref(),
5751        Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
5752    )
5753}
5754
5755fn config_button_trace_object(obj: &globals::ObjectState) -> bool {
5756    if obj.button.enabled || obj.button.state == TNM_BTN_STATE_DISABLE {
5757        return true;
5758    }
5759    let Some(file) = obj.file_name.as_deref() else {
5760        return false;
5761    };
5762    let f = file.to_ascii_lowercase();
5763    f.starts_with("mn_")
5764        || f.contains("config")
5765        || f.contains("conf")
5766        || f.contains("sys")
5767        || f.contains("mw")
5768}
5769
5770fn config_tr_write_trace_file(file: Option<&str>) -> bool {
5771    let Some(name) = file else {
5772        return false;
5773    };
5774    name.starts_with("mn_sm_menu_cbox")
5775        || name.starts_with("mn_cfa_tab_pbtn")
5776        || name.starts_with("mn_cfb_")
5777        || name.starts_with("mn_cfe_")
5778        || name.starts_with("mn_tt_menu")
5779        || name.starts_with("mn_tt_copy")
5780}
5781
5782fn config_tr_write_trace_object(obj_i64: i64, obj: &globals::ObjectState) -> bool {
5783    (100057..=100067).contains(&obj_i64) || config_tr_write_trace_file(obj.file_name.as_deref())
5784}
5785
5786fn trace_config_event_frame_prop_write(
5787    ids: &constants::RuntimeConstants,
5788    stage_i64: i64,
5789    obj_i64: i64,
5790    obj: &globals::ObjectState,
5791    prop_id: i32,
5792    old_value: i64,
5793    new_value: i64,
5794) {
5795    if !sg_debug_enabled() || !config_tr_write_trace_object(obj_i64, obj) {
5796        return;
5797    }
5798    let prop = if ids.obj_tr != 0 && prop_id == ids.obj_tr {
5799        "TR"
5800    } else if ids.obj_alpha != 0 && prop_id == ids.obj_alpha {
5801        "ALPHA"
5802    } else {
5803        return;
5804    };
5805    eprintln!(
5806        "[SG_DEBUG][CONFIG_TR_WRITE_TRACE][EVENT_FRAME] stage={} runtime_slot={} file={} prop={} old={} new={} disp={} tr={} alpha={} backend={:?} used={} children={}",
5807        stage_i64,
5808        obj_i64,
5809        obj.file_name.as_deref().unwrap_or("-"),
5810        prop,
5811        old_value,
5812        new_value,
5813        obj.get_int_prop(ids, ids.obj_disp),
5814        obj.get_int_prop(ids, ids.obj_tr),
5815        obj.get_int_prop(ids, ids.obj_alpha),
5816        obj.backend,
5817        obj.used,
5818        obj.runtime.child_objects.len(),
5819    );
5820}
5821
5822fn save_load_render_trace_enabled() -> bool {
5823    std::env::var_os("SG_SAVELOAD_TRACE").is_some()
5824}
5825
5826fn trace_save_load_render_sprites(ctx: &CommandContext, list: &[RenderSprite]) {
5827    let scene = ctx.current_scene_name.as_deref().unwrap_or("<none>");
5828    let scene_match = scene.contains("sys10_sv") || scene.contains("save") || scene.contains("load");
5829    let mut emitted = 0usize;
5830    for (idx, rs) in list.iter().enumerate() {
5831        let Some(image_id) = rs.sprite.image_id else {
5832            continue;
5833        };
5834        let info = ctx.images.debug_image_info(image_id);
5835        let width = info.as_ref().map(|d| d.width).unwrap_or(0);
5836        let height = info.as_ref().map(|d| d.height).unwrap_or(0);
5837        let source_path = info
5838            .as_ref()
5839            .and_then(|d| d.source_path.as_ref())
5840            .map(|p| p.display().to_string())
5841            .unwrap_or_else(|| "-".to_string());
5842        let source_path_lc = source_path.to_ascii_lowercase();
5843        let source = render_sprite_source_name(ctx, rs);
5844        let source_lc = source.to_ascii_lowercase();
5845        let near_origin = rs.sprite.x.abs() <= 4 && rs.sprite.y.abs() <= 4 && width >= 16 && height >= 16;
5846        let unowned = source.starts_with("unowned:");
5847        let path_match = source_path_lc.contains("savedata")
5848            || source_path_lc.contains("thumb")
5849            || source_path_lc.contains("capture")
5850            || source_path_lc.contains("mn_sv")
5851            || source_lc.contains("mn_sv")
5852            || source_lc.contains("save")
5853            || source_lc.contains("thumb")
5854            || source_lc.contains("capture");
5855        if !(scene_match || near_origin || unowned || path_match) {
5856            continue;
5857        }
5858        if !(near_origin || unowned || path_match) {
5859            continue;
5860        }
5861        eprintln!(
5862            "[SG_SAVELOAD_TRACE][RENDER] idx={} scene={} source={} layer_id={:?} sprite_id={:?} image={:?} image_size={}x{} image_version={} image_source={} frame={:?} pos=({}, {}) visible={} alpha={} tr={} order=({}, {}) packed_order={} fit={:?} size_mode={:?} clip={:?}",
5863            idx,
5864            scene,
5865            source,
5866            rs.layer_id,
5867            rs.sprite_id,
5868            rs.sprite.image_id,
5869            width,
5870            height,
5871            info.as_ref().map(|d| d.version).unwrap_or(0),
5872            source_path,
5873            info.as_ref().and_then(|d| d.frame_index),
5874            rs.sprite.x,
5875            rs.sprite.y,
5876            rs.sprite.visible,
5877            rs.sprite.alpha,
5878            rs.sprite.tr,
5879            rs.sorter_order,
5880            rs.sorter_layer,
5881            rs.sprite.order,
5882            rs.sprite.fit,
5883            rs.sprite.size_mode,
5884            rs.sprite.dst_clip
5885        );
5886        emitted += 1;
5887        if emitted >= 120 {
5888            eprintln!("[SG_SAVELOAD_TRACE][RENDER] truncated after {} entries", emitted);
5889            break;
5890        }
5891    }
5892}
5893
5894fn trace_final_render_order(ctx: &CommandContext, list: &[RenderSprite]) {
5895    eprintln!(
5896        "[SG_DEBUG][CONFIG_BUTTON_TRACE][FINAL_ORDER] len={} wipe_active={} selected_stage=front",
5897        list.len(),
5898        ctx.globals.wipe.is_some()
5899    );
5900    for (idx, rs) in list.iter().enumerate() {
5901        let source = render_sprite_source_name(ctx, rs);
5902        eprintln!(
5903            "[SG_DEBUG][CONFIG_BUTTON_TRACE][FINAL_ORDER] idx={} source={} layer_id={:?} sprite_id={:?} sorter=({}, {}) packed_order={} visible={} alpha={} tr={} pos=({}, {}) z={} fit={:?} size={:?} image={:?} blend={:?} clip={:?}",
5904            idx,
5905            source,
5906            rs.layer_id,
5907            rs.sprite_id,
5908            rs.sorter_order,
5909            rs.sorter_layer,
5910            rs.sprite.order,
5911            rs.sprite.visible,
5912            rs.sprite.alpha,
5913            rs.sprite.tr,
5914            rs.sprite.x,
5915            rs.sprite.y,
5916            rs.sprite.z,
5917            rs.sprite.fit,
5918            rs.sprite.size_mode,
5919            rs.sprite.image_id,
5920            rs.sprite.blend,
5921            rs.sprite.dst_clip
5922        );
5923    }
5924}
5925
5926fn render_sprite_source_name(ctx: &CommandContext, rs: &RenderSprite) -> String {
5927    let Some(layer_id) = rs.layer_id else {
5928        return "background".to_string();
5929    };
5930    let Some(sprite_id) = rs.sprite_id else {
5931        return "background".to_string();
5932    };
5933    let mut found: Vec<String> = Vec::new();
5934    let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
5935    form_ids.sort_unstable();
5936    for form_id in form_ids {
5937        let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
5938            continue;
5939        };
5940        let mut stage_ids: Vec<i64> = st
5941            .object_lists
5942            .keys()
5943            .chain(st.mwnd_lists.keys())
5944            .chain(st.btnselitem_lists.keys())
5945            .copied()
5946            .collect();
5947        stage_ids.sort_unstable();
5948        stage_ids.dedup();
5949        for stage_idx in stage_ids {
5950            if let Some(list) = st.object_lists.get(&stage_idx) {
5951                for (obj_idx, obj) in list.iter().enumerate() {
5952                    collect_render_sprite_source_for_object(
5953                        ctx,
5954                        form_id,
5955                        stage_idx,
5956                        obj_idx,
5957                        obj,
5958                        layer_id,
5959                        sprite_id,
5960                        "object",
5961                        &mut found,
5962                    );
5963                }
5964            }
5965            if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
5966                for (mwnd_idx, m) in mwnds.iter().enumerate() {
5967                    for (obj_idx, obj) in m.button_list.iter().enumerate() {
5968                        collect_render_sprite_source_for_object(
5969                            ctx,
5970                            form_id,
5971                            stage_idx,
5972                            obj_idx,
5973                            obj,
5974                            layer_id,
5975                            sprite_id,
5976                            &format!("mwnd{mwnd_idx}.button"),
5977                            &mut found,
5978                        );
5979                    }
5980                    for (obj_idx, obj) in m.face_list.iter().enumerate() {
5981                        collect_render_sprite_source_for_object(
5982                            ctx,
5983                            form_id,
5984                            stage_idx,
5985                            obj_idx,
5986                            obj,
5987                            layer_id,
5988                            sprite_id,
5989                            &format!("mwnd{mwnd_idx}.face"),
5990                            &mut found,
5991                        );
5992                    }
5993                    for (obj_idx, obj) in m.object_list.iter().enumerate() {
5994                        collect_render_sprite_source_for_object(
5995                            ctx,
5996                            form_id,
5997                            stage_idx,
5998                            obj_idx,
5999                            obj,
6000                            layer_id,
6001                            sprite_id,
6002                            &format!("mwnd{mwnd_idx}.object"),
6003                            &mut found,
6004                        );
6005                    }
6006                }
6007            }
6008        }
6009    }
6010    if found.is_empty() {
6011        format!("unowned:{layer_id}/{sprite_id}")
6012    } else {
6013        found.join("|")
6014    }
6015}
6016
6017fn collect_render_sprite_source_for_object(
6018    ctx: &CommandContext,
6019    form_id: u32,
6020    stage_idx: i64,
6021    obj_idx: usize,
6022    obj: &globals::ObjectState,
6023    layer_id: LayerId,
6024    sprite_id: SpriteId,
6025    source_kind: &str,
6026    found: &mut Vec<String>,
6027) {
6028    let file = obj.file_name.as_deref().unwrap_or("-");
6029    if object_backend_owns_sprite(ctx, stage_idx, obj_idx, obj, layer_id, sprite_id) {
6030        found.push(format!(
6031            "form{form_id}:stage{stage_idx}:{source_kind}[{obj_idx}]:slot{}:file{}",
6032            effective_object_slot_for_trace(obj_idx, obj),
6033            file
6034        ));
6035    }
6036    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
6037        collect_render_sprite_source_for_object(
6038            ctx,
6039            form_id,
6040            stage_idx,
6041            child_idx,
6042            child,
6043            layer_id,
6044            sprite_id,
6045            &format!("{source_kind}[{obj_idx}].child"),
6046            found,
6047        );
6048    }
6049}
6050
6051fn effective_object_slot_for_trace(obj_idx: usize, obj: &globals::ObjectState) -> i64 {
6052    obj.runtime_slot_or(obj_idx) as i64
6053}
6054
6055fn object_backend_owns_sprite(
6056    ctx: &CommandContext,
6057    stage_idx: i64,
6058    obj_idx: usize,
6059    obj: &globals::ObjectState,
6060    layer_id: LayerId,
6061    sprite_id: SpriteId,
6062) -> bool {
6063    match &obj.backend {
6064        globals::ObjectBackend::Gfx => ctx
6065            .gfx
6066            .object_sprite_binding(stage_idx, effective_object_slot_for_trace(obj_idx, obj))
6067            == Some((layer_id, sprite_id)),
6068        globals::ObjectBackend::Rect {
6069            layer_id: lid,
6070            sprite_id: sid,
6071            ..
6072        }
6073        | globals::ObjectBackend::String {
6074            layer_id: lid,
6075            sprite_id: sid,
6076            ..
6077        }
6078        | globals::ObjectBackend::Movie {
6079            layer_id: lid,
6080            sprite_id: sid,
6081            ..
6082        } => *lid == layer_id && *sid == sprite_id,
6083        globals::ObjectBackend::Number {
6084            layer_id: lid,
6085            sprite_ids,
6086        }
6087        | globals::ObjectBackend::Weather {
6088            layer_id: lid,
6089            sprite_ids,
6090        } => *lid == layer_id && sprite_ids.iter().any(|sid| *sid == sprite_id),
6091        globals::ObjectBackend::None => false,
6092    }
6093}
6094
6095#[derive(Debug, Clone, Default)]
6096struct ObjectRenderInfo {
6097    runtime_slot: usize,
6098    used: bool,
6099    object_type: i64,
6100    disp: bool,
6101    x: i64,
6102    y: i64,
6103    x_rep: i64,
6104    y_rep: i64,
6105    z_rep: i64,
6106    order: i64,
6107    layer: i64,
6108    alpha: i64,
6109    tr: i64,
6110    tr_rep: i64,
6111    mono: i64,
6112    reverse: i64,
6113    bright: i64,
6114    dark: i64,
6115    color_rate: i64,
6116    color_add_r: i64,
6117    color_add_g: i64,
6118    color_add_b: i64,
6119    color_r: i64,
6120    color_g: i64,
6121    color_b: i64,
6122    z: i64,
6123    world_no: i64,
6124    center_x: i64,
6125    center_y: i64,
6126    center_z: i64,
6127    center_rep_x: i64,
6128    center_rep_y: i64,
6129    center_rep_z: i64,
6130    scale_x: i64,
6131    scale_y: i64,
6132    scale_z: i64,
6133    rotate_x: i64,
6134    rotate_y: i64,
6135    rotate_z: i64,
6136    culling: bool,
6137    alpha_test: bool,
6138    alpha_blend: bool,
6139    fog_use: bool,
6140    light_no: i64,
6141    blend: crate::layer::SpriteBlend,
6142    child_sort_type: i64,
6143    dst_clip: Option<ClipRect>,
6144    billboard: bool,
6145    file_name: Option<String>,
6146    mesh_animation: crate::mesh3d::MeshAnimationState,
6147}
6148
6149#[derive(Debug, Clone, Copy)]
6150struct ParentRenderState {
6151    world_no: i64,
6152    pos_x: f32,
6153    pos_y: f32,
6154    pos_z: f32,
6155    center_rep_x: f32,
6156    center_rep_y: f32,
6157    center_rep_z: f32,
6158    scale_x: f32,
6159    scale_y: f32,
6160    scale_z: f32,
6161    rotate_x: f32,
6162    rotate_y: f32,
6163    rotate_z: f32,
6164    tr: i32,
6165    mono: i32,
6166    reverse: i32,
6167    bright: i32,
6168    dark: i32,
6169    color_rate: i32,
6170    color_r: i32,
6171    color_g: i32,
6172    color_b: i32,
6173    color_add_r: i32,
6174    color_add_g: i32,
6175    color_add_b: i32,
6176    blend: crate::layer::SpriteBlend,
6177    dst_clip: Option<ClipRect>,
6178    mask_image_id: Option<ImageId>,
6179    mask_offset_x: i32,
6180    mask_offset_y: i32,
6181    tonecurve_image_id: Option<ImageId>,
6182    tonecurve_row: f32,
6183    tonecurve_sat: f32,
6184}
6185
6186fn object_runtime_slot(obj_idx: usize, obj: &globals::ObjectState) -> usize {
6187    obj.runtime_slot_or(obj_idx)
6188}
6189
6190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6191enum ButtonSeEvent {
6192    Hit,
6193    Push,
6194    Decide,
6195}
6196
6197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6198struct ButtonSortKey {
6199    order: i64,
6200    layer: i64,
6201}
6202
6203impl ButtonSortKey {
6204    fn display_tuple(self) -> String {
6205        format!("({}, {})", self.order, self.layer)
6206    }
6207}
6208
6209#[derive(Debug, Clone)]
6210struct ButtonVisualState {
6211    state: i64,
6212    action_no: i64,
6213    file_name: Option<String>,
6214    base_patno: i64,
6215    cut_no: i64,
6216}
6217
6218const TNM_BTN_STATE_NORMAL: i64 = 0;
6219const TNM_BTN_STATE_HIT: i64 = 1;
6220const TNM_BTN_STATE_PUSH: i64 = 2;
6221const TNM_BTN_STATE_SELECT: i64 = 3;
6222const TNM_BTN_STATE_DISABLE: i64 = 4;
6223
6224const TNM_SYSCOM_TYPE_NONE: i64 = 0;
6225const TNM_SYSCOM_TYPE_SAVE: i64 = 1;
6226const TNM_SYSCOM_TYPE_LOAD: i64 = 2;
6227const TNM_SYSCOM_TYPE_READ_SKIP: i64 = 3;
6228const TNM_SYSCOM_TYPE_AUTO_MODE: i64 = 4;
6229const TNM_SYSCOM_TYPE_RETURN_SEL: i64 = 5;
6230const TNM_SYSCOM_TYPE_HIDE_MWND: i64 = 6;
6231const TNM_SYSCOM_TYPE_MSG_BACK: i64 = 7;
6232const TNM_SYSCOM_TYPE_KOE_PLAY: i64 = 8;
6233const TNM_SYSCOM_TYPE_QUICK_SAVE: i64 = 9;
6234const TNM_SYSCOM_TYPE_QUICK_LOAD: i64 = 10;
6235const TNM_SYSCOM_TYPE_CONFIG: i64 = 11;
6236const TNM_SYSCOM_TYPE_LOCAL_EXTRA_SWITCH: i64 = 12;
6237const TNM_SYSCOM_TYPE_LOCAL_EXTRA_MODE: i64 = 13;
6238const TNM_SYSCOM_TYPE_GLOBAL_EXTRA_SWITCH: i64 = 14;
6239const TNM_SYSCOM_TYPE_GLOBAL_EXTRA_MODE: i64 = 15;
6240
6241#[derive(Debug, Clone, Copy)]
6242struct ButtonHitCandidate {
6243    button_no: i64,
6244    sort_key: ButtonSortKey,
6245    runtime_slot: usize,
6246    se_no: i64,
6247    was_hit: bool,
6248}
6249
6250#[derive(Debug, Clone, Copy)]
6251struct ButtonOwnerInfo {
6252    button_no: i64,
6253    runtime_slot: usize,
6254    se_no: i64,
6255    was_hit: bool,
6256}
6257
6258fn push_object_button_decided_action(
6259    obj: &globals::ObjectState,
6260    out: &mut Vec<globals::PendingButtonAction>,
6261) {
6262    if !obj.button.decided_action_scn_name.is_empty() {
6263        if sg_debug_enabled() {
6264            eprintln!(
6265                "[SG_DEBUG][BUTTON_TRACE][CALLBACK] enqueue user_call file={:?} button_no={} group_no={} action_no={} state={} hit={} pushed={} call={}::{}/{}",
6266                obj.file_name,
6267                obj.button.button_no,
6268                obj.button.group_no,
6269                obj.button.action_no,
6270                obj.button.state,
6271                obj.button.hit,
6272                obj.button.pushed,
6273                obj.button.decided_action_scn_name,
6274                obj.button.decided_action_cmd_name,
6275                obj.button.decided_action_z_no
6276            );
6277        }
6278        out.push(globals::PendingButtonAction {
6279            kind: globals::PendingButtonActionKind::UserCall {
6280                scn_name: obj.button.decided_action_scn_name.clone(),
6281                cmd_name: obj.button.decided_action_cmd_name.clone(),
6282                z_no: obj.button.decided_action_z_no,
6283            },
6284        });
6285    } else if obj.button.sys_type != 0 {
6286        if sg_debug_enabled() {
6287            eprintln!(
6288                "[SG_DEBUG][BUTTON_TRACE][CALLBACK] enqueue syscom file={:?} button_no={} group_no={} action_no={} state={} hit={} pushed={} sys_type={} sys_opt={} mode={}",
6289                obj.file_name,
6290                obj.button.button_no,
6291                obj.button.group_no,
6292                obj.button.action_no,
6293                obj.button.state,
6294                obj.button.hit,
6295                obj.button.pushed,
6296                obj.button.sys_type,
6297                obj.button.sys_type_opt,
6298                obj.button.mode
6299            );
6300        }
6301        out.push(globals::PendingButtonAction {
6302            kind: globals::PendingButtonActionKind::Syscom {
6303                sys_type: obj.button.sys_type,
6304                sys_type_opt: obj.button.sys_type_opt,
6305                mode: obj.button.mode,
6306            },
6307        });
6308    } else if sg_debug_enabled() {
6309        eprintln!(
6310            "[SG_DEBUG][BUTTON_TRACE][CALLBACK] no_callback file={:?} button_no={} group_no={} action_no={} state={} hit={} pushed={}",
6311            obj.file_name,
6312            obj.button.button_no,
6313            obj.button.group_no,
6314            obj.button.action_no,
6315            obj.button.state,
6316            obj.button.hit,
6317            obj.button.pushed
6318        );
6319    }
6320}
6321
6322fn syscom_feature_enabled_for_button(
6323    syscom: &globals::SyscomRuntimeState,
6324    button: &globals::ObjectButtonState,
6325) -> bool {
6326    match button.sys_type {
6327        TNM_SYSCOM_TYPE_NONE => true,
6328        TNM_SYSCOM_TYPE_SAVE => syscom.save_feature.check_enabled() != 0,
6329        TNM_SYSCOM_TYPE_LOAD => syscom.load_feature.check_enabled() != 0,
6330        TNM_SYSCOM_TYPE_READ_SKIP => syscom.read_skip.check_enabled() != 0,
6331        TNM_SYSCOM_TYPE_AUTO_MODE => syscom.auto_mode.check_enabled() != 0,
6332        TNM_SYSCOM_TYPE_RETURN_SEL => syscom.return_to_sel.check_enabled() != 0,
6333        TNM_SYSCOM_TYPE_HIDE_MWND => syscom.hide_mwnd.check_enabled() != 0,
6334        TNM_SYSCOM_TYPE_MSG_BACK => syscom.msg_back.check_enabled() != 0,
6335        TNM_SYSCOM_TYPE_KOE_PLAY => true,
6336        TNM_SYSCOM_TYPE_QUICK_SAVE => syscom.save_feature.check_enabled() != 0,
6337        TNM_SYSCOM_TYPE_QUICK_LOAD => syscom.load_feature.check_enabled() != 0,
6338        TNM_SYSCOM_TYPE_CONFIG => true,
6339        TNM_SYSCOM_TYPE_LOCAL_EXTRA_SWITCH => syscom.local_extra_switch.check_enabled() != 0,
6340        TNM_SYSCOM_TYPE_LOCAL_EXTRA_MODE => syscom.local_extra_mode.check_enabled() != 0,
6341        TNM_SYSCOM_TYPE_GLOBAL_EXTRA_SWITCH | TNM_SYSCOM_TYPE_GLOBAL_EXTRA_MODE => true,
6342        _ => true,
6343    }
6344}
6345
6346fn syscom_mode_for_button(
6347    syscom: &globals::SyscomRuntimeState,
6348    button: &globals::ObjectButtonState,
6349) -> i64 {
6350    match button.sys_type {
6351        TNM_SYSCOM_TYPE_READ_SKIP => i64::from(syscom.read_skip.onoff),
6352        TNM_SYSCOM_TYPE_AUTO_MODE => i64::from(syscom.auto_mode.onoff),
6353        TNM_SYSCOM_TYPE_LOCAL_EXTRA_SWITCH => i64::from(syscom.local_extra_switch.onoff),
6354        TNM_SYSCOM_TYPE_LOCAL_EXTRA_MODE => syscom.local_extra_mode.value,
6355        _ => 0,
6356    }
6357}
6358
6359fn button_syscom_mode_visible(
6360    syscom: &globals::SyscomRuntimeState,
6361    button: &globals::ObjectButtonState,
6362) -> bool {
6363    button.sys_type == TNM_SYSCOM_TYPE_NONE || syscom_mode_for_button(syscom, button) == button.mode
6364}
6365
6366fn mwnd_button_forced_disabled(
6367    syscom: &globals::SyscomRuntimeState,
6368    mwnd_button_idx: Option<usize>,
6369) -> bool {
6370    if syscom.mwnd_btn_disable_all {
6371        return true;
6372    }
6373    mwnd_button_idx
6374        .and_then(|idx| syscom.mwnd_btn_disable.get(&(idx as i64)))
6375        .copied()
6376        .unwrap_or(false)
6377}
6378
6379fn button_effective_disabled(
6380    syscom: &globals::SyscomRuntimeState,
6381    obj: &globals::ObjectState,
6382    mwnd_button_idx: Option<usize>,
6383) -> bool {
6384    button_disabled_reason(syscom, obj, mwnd_button_idx).is_some()
6385}
6386
6387fn button_disabled_reason(
6388    syscom: &globals::SyscomRuntimeState,
6389    obj: &globals::ObjectState,
6390    mwnd_button_idx: Option<usize>,
6391) -> Option<&'static str> {
6392    if obj.button.is_disabled() {
6393        return Some("object_state_disable");
6394    }
6395    if mwnd_button_forced_disabled(syscom, mwnd_button_idx) {
6396        return Some("syscom_mwnd_button_disable");
6397    }
6398    if !syscom_feature_enabled_for_button(syscom, &obj.button) {
6399        return Some("syscom_feature_disable");
6400    }
6401    None
6402}
6403
6404fn button_state_name(state: i64) -> &'static str {
6405    match state {
6406        TNM_BTN_STATE_NORMAL => "normal",
6407        TNM_BTN_STATE_HIT => "hit",
6408        TNM_BTN_STATE_PUSH => "push",
6409        TNM_BTN_STATE_SELECT => "select",
6410        TNM_BTN_STATE_DISABLE => "disable",
6411        _ => "unknown",
6412    }
6413}
6414
6415fn object_button_renderable_by_syscom(
6416    syscom: &globals::SyscomRuntimeState,
6417    obj: &globals::ObjectState,
6418) -> bool {
6419    !obj.button.enabled || button_syscom_mode_visible(syscom, &obj.button)
6420}
6421
6422fn button_real_state_for_visual(
6423    syscom: &globals::SyscomRuntimeState,
6424    st: &globals::StageFormState,
6425    stage_idx: i64,
6426    obj: &globals::ObjectState,
6427    mwnd_button_idx: Option<usize>,
6428) -> i64 {
6429    if let Some(reason) = button_disabled_reason(syscom, obj, mwnd_button_idx) {
6430        if sg_debug_enabled() {
6431            eprintln!(
6432                "[SG_DEBUG][BUTTON_TRACE][VISUAL] real_state=disable reason={} stage={} file={:?} mwnd_button_idx={:?} button_no={} group_no={} group_idx={:?} action_no={} raw_state={} enabled={} hit={} pushed={} sys_type={} sys_opt={} mode={} touch_disable={}",
6433                reason,
6434                stage_idx,
6435                obj.file_name,
6436                mwnd_button_idx,
6437                obj.button.button_no,
6438                obj.button.group_no,
6439                obj.button.group_idx(),
6440                obj.button.action_no,
6441                obj.button.state,
6442                obj.button.enabled,
6443                obj.button.hit,
6444                obj.button.pushed,
6445                obj.button.sys_type,
6446                obj.button.sys_type_opt,
6447                obj.button.mode,
6448                syscom.mwnd_btn_touch_disable
6449            );
6450        }
6451        return TNM_BTN_STATE_DISABLE;
6452    }
6453    if obj.button.state == TNM_BTN_STATE_SELECT || obj.button.state == TNM_BTN_STATE_DISABLE {
6454        return obj.button.state;
6455    }
6456    if syscom.mwnd_btn_touch_disable {
6457        if sg_debug_enabled() && obj.button.enabled {
6458            eprintln!(
6459                "[SG_DEBUG][BUTTON_TRACE][VISUAL] real_state=normal reason=touch_disable stage={} file={:?} mwnd_button_idx={:?} button_no={} group_no={} action_no={}",
6460                stage_idx, obj.file_name, mwnd_button_idx, obj.button.button_no, obj.button.group_no, obj.button.action_no
6461            );
6462        }
6463        return TNM_BTN_STATE_NORMAL;
6464    }
6465    if let Some(gidx) = obj.button.group_idx() {
6466        if let Some(gl) = st
6467            .group_lists
6468            .get(&stage_idx)
6469            .and_then(|groups| groups.get(gidx))
6470        {
6471            if gl.decided_button_no == obj.button.button_no {
6472                return TNM_BTN_STATE_PUSH;
6473            }
6474            if gl.hit_button_no == obj.button.button_no {
6475                return TNM_BTN_STATE_HIT;
6476            }
6477            if gl.pushed_button_no == obj.button.button_no {
6478                return TNM_BTN_STATE_PUSH;
6479            }
6480        }
6481    } else if obj.button.pushed {
6482        return TNM_BTN_STATE_PUSH;
6483    } else if obj.button.hit {
6484        return TNM_BTN_STATE_HIT;
6485    }
6486    TNM_BTN_STATE_NORMAL
6487}
6488
6489fn collect_button_decided_action_by_runtime_slot_recursive(
6490    obj_idx: usize,
6491    obj: &globals::ObjectState,
6492    runtime_slot: usize,
6493    out: &mut Vec<globals::PendingButtonAction>,
6494) -> bool {
6495    if object_runtime_slot(obj_idx, obj) == runtime_slot {
6496        if obj.used && obj.button.enabled && obj.button.action_no >= 0 {
6497            push_object_button_decided_action(obj, out);
6498        }
6499        return true;
6500    }
6501    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
6502        if collect_button_decided_action_by_runtime_slot_recursive(
6503            child_idx,
6504            child,
6505            runtime_slot,
6506            out,
6507        ) {
6508            return true;
6509        }
6510    }
6511    false
6512}
6513
6514fn find_button_se_no_by_runtime_slot_recursive(
6515    obj_idx: usize,
6516    obj: &globals::ObjectState,
6517    runtime_slot: usize,
6518) -> Option<i64> {
6519    if object_runtime_slot(obj_idx, obj) == runtime_slot {
6520        return (obj.used && obj.button.enabled && obj.button.action_no >= 0)
6521            .then_some(obj.button.se_no);
6522    }
6523    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
6524        if let Some(se_no) =
6525            find_button_se_no_by_runtime_slot_recursive(child_idx, child, runtime_slot)
6526        {
6527            return Some(se_no);
6528        }
6529    }
6530    None
6531}
6532
6533fn find_button_se_no_in_list_by_runtime_slot(
6534    objs: &[globals::ObjectState],
6535    runtime_slot: usize,
6536) -> Option<i64> {
6537    for (obj_idx, obj) in objs.iter().enumerate() {
6538        if let Some(se_no) = find_button_se_no_by_runtime_slot_recursive(obj_idx, obj, runtime_slot)
6539        {
6540            return Some(se_no);
6541        }
6542    }
6543    None
6544}
6545
6546fn set_button_pushed_by_runtime_slot_recursive(
6547    obj_idx: usize,
6548    obj: &mut globals::ObjectState,
6549    runtime_slot: usize,
6550) -> bool {
6551    if object_runtime_slot(obj_idx, obj) == runtime_slot {
6552        if obj.button.enabled {
6553            obj.button.last_pushed = obj.button.pushed;
6554            obj.button.pushed = true;
6555        }
6556        return true;
6557    }
6558    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
6559        if set_button_pushed_by_runtime_slot_recursive(child_idx, child, runtime_slot) {
6560            return true;
6561        }
6562    }
6563    false
6564}
6565
6566fn object_button_push_keep_by_runtime_slot_recursive(
6567    obj_idx: usize,
6568    obj: &globals::ObjectState,
6569    runtime_slot: usize,
6570) -> bool {
6571    if object_runtime_slot(obj_idx, obj) == runtime_slot {
6572        return obj.button.enabled && obj.button.push_keep;
6573    }
6574    obj.runtime
6575        .child_objects
6576        .iter()
6577        .enumerate()
6578        .any(|(child_idx, child)| {
6579            object_button_push_keep_by_runtime_slot_recursive(child_idx, child, runtime_slot)
6580        })
6581}
6582
6583fn object_button_push_keep_in_list_by_runtime_slot(
6584    objs: &[globals::ObjectState],
6585    runtime_slot: usize,
6586) -> bool {
6587    objs.iter().enumerate().any(|(obj_idx, obj)| {
6588        object_button_push_keep_by_runtime_slot_recursive(obj_idx, obj, runtime_slot)
6589    })
6590}
6591
6592fn clear_button_hit_recursive(obj: &mut globals::ObjectState) {
6593    if obj.button.enabled {
6594        obj.button.last_hit = obj.button.hit;
6595        obj.button.hit = false;
6596    }
6597    for child in &mut obj.runtime.child_objects {
6598        clear_button_hit_recursive(child);
6599    }
6600}
6601
6602fn set_button_hit_by_runtime_slot_recursive(
6603    obj_idx: usize,
6604    obj: &mut globals::ObjectState,
6605    runtime_slot: usize,
6606) -> bool {
6607    if object_runtime_slot(obj_idx, obj) == runtime_slot {
6608        obj.button.hit = true;
6609        return true;
6610    }
6611    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
6612        if set_button_hit_by_runtime_slot_recursive(child_idx, child, runtime_slot) {
6613            return true;
6614        }
6615    }
6616    false
6617}
6618
6619fn set_button_pushed_recursive(obj: &mut globals::ObjectState, group_idx: usize, button_no: i64) {
6620    if obj.button.enabled
6621        && obj.button.group_idx() == Some(group_idx)
6622        && obj.button.button_no == button_no
6623    {
6624        obj.button.last_pushed = obj.button.pushed;
6625        obj.button.pushed = true;
6626    }
6627    for child in &mut obj.runtime.child_objects {
6628        set_button_pushed_recursive(child, group_idx, button_no);
6629    }
6630}
6631
6632fn mark_standalone_button_pushed_from_hit_recursive(
6633    _obj_idx: usize,
6634    obj: &mut globals::ObjectState,
6635) -> Option<i64> {
6636    if has_standalone_button_action(obj) && obj.button.hit {
6637        let was_pushed = obj.button.pushed;
6638        obj.button.last_pushed = obj.button.pushed;
6639        obj.button.pushed = true;
6640        if !was_pushed {
6641            return Some(obj.button.se_no);
6642        }
6643    }
6644    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
6645        if let Some(se_no) = mark_standalone_button_pushed_from_hit_recursive(child_idx, child) {
6646            return Some(se_no);
6647        }
6648    }
6649    None
6650}
6651fn standalone_button_hit_recursive(obj: &globals::ObjectState) -> bool {
6652    if has_standalone_button_action(obj) && obj.button.hit {
6653        return true;
6654    }
6655    obj.runtime
6656        .child_objects
6657        .iter()
6658        .any(standalone_button_hit_recursive)
6659}
6660
6661fn standalone_button_pushed_recursive(obj: &globals::ObjectState) -> bool {
6662    if has_standalone_button_action(obj) && obj.button.pushed {
6663        return true;
6664    }
6665    obj.runtime
6666        .child_objects
6667        .iter()
6668        .any(standalone_button_pushed_recursive)
6669}
6670
6671fn clear_button_pushed_recursive(obj: &mut globals::ObjectState) {
6672    if obj.button.enabled {
6673        obj.button.last_pushed = obj.button.pushed;
6674        obj.button.pushed = false;
6675    }
6676    for child in &mut obj.runtime.child_objects {
6677        clear_button_pushed_recursive(child);
6678    }
6679}
6680
6681fn object_button_push_keep_recursive(
6682    obj: &globals::ObjectState,
6683    group_idx: usize,
6684    button_no: i64,
6685) -> bool {
6686    if obj.button.enabled
6687        && obj.button.group_idx() == Some(group_idx)
6688        && obj.button.button_no == button_no
6689        && obj.button.push_keep
6690    {
6691        return true;
6692    }
6693    obj.runtime
6694        .child_objects
6695        .iter()
6696        .any(|child| object_button_push_keep_recursive(child, group_idx, button_no))
6697}
6698
6699fn hit_test_render_sprite(
6700    images: &mut ImageManager,
6701    sprite: &Sprite,
6702    mx: i32,
6703    my: i32,
6704    alpha_test: bool,
6705) -> bool {
6706    if !sprite.visible || sprite.tr == 0 {
6707        return false;
6708    }
6709    if let Some(clip) = sprite.dst_clip {
6710        if mx < clip.left || my < clip.top || mx >= clip.right || my >= clip.bottom {
6711            return false;
6712        }
6713    }
6714    let Some(img_id) = sprite.image_id else {
6715        return false;
6716    };
6717    let Some(img) = images.get(img_id).map(|a| a.as_ref()) else {
6718        return false;
6719    };
6720    let (w, h) = match sprite.size_mode {
6721        SpriteSizeMode::Intrinsic => (img.width as f32, img.height as f32),
6722        SpriteSizeMode::Explicit { width, height } => (width as f32, height as f32),
6723    };
6724    let (anchor_x, anchor_y) = match sprite.fit {
6725        SpriteFit::PixelRect => (sprite.x as f32, sprite.y as f32),
6726        SpriteFit::FullScreen => (0.0, 0.0),
6727    };
6728    if sprite.scale_x == 0.0 || sprite.scale_y == 0.0 {
6729        return false;
6730    }
6731    let (origin_x, origin_y) = if sprite.object_anchor {
6732        (anchor_x, anchor_y)
6733    } else {
6734        (anchor_x + sprite.pivot_x, anchor_y + sprite.pivot_y)
6735    };
6736    let mut px = mx as f32 - origin_x;
6737    let mut py = my as f32 - origin_y;
6738    if sprite.rotate != 0.0 {
6739        let (s, c) = (-sprite.rotate).sin_cos();
6740        let rx = px * c - py * s;
6741        let ry = px * s + py * c;
6742        px = rx;
6743        py = ry;
6744    }
6745    let (tex_center_x, tex_center_y) = if sprite.object_anchor {
6746        (sprite.texture_center_x, sprite.texture_center_y)
6747    } else {
6748        (0.0, 0.0)
6749    };
6750    let local_x = px / sprite.scale_x + sprite.pivot_x + tex_center_x;
6751    let local_y = py / sprite.scale_y + sprite.pivot_y + tex_center_y;
6752    if !(0.0 <= local_x && local_x < w && 0.0 <= local_y && local_y < h) {
6753        return false;
6754    }
6755    if alpha_test {
6756        let (sx, sy) = match sprite.src_clip {
6757            Some(src) => (
6758                src.left.saturating_add(local_x.floor() as i32),
6759                src.top.saturating_add(local_y.floor() as i32),
6760            ),
6761            None => (local_x.floor() as i32, local_y.floor() as i32),
6762        };
6763        if !CommandContext::alpha_test_image(img, sx, sy) {
6764            return false;
6765        }
6766    }
6767    true
6768}
6769
6770fn hit_test_layer_sprite(
6771    images: &mut ImageManager,
6772    layers: &LayerManager,
6773    layer_id: LayerId,
6774    sprite_id: SpriteId,
6775    mx: i32,
6776    my: i32,
6777    alpha_test: bool,
6778) -> bool {
6779    let Some(spr) = layers.layer(layer_id).and_then(|l| l.sprite(sprite_id)) else {
6780        return false;
6781    };
6782    hit_test_render_sprite(images, spr, mx, my, alpha_test)
6783}
6784
6785fn object_button_sort_key(
6786    ids: &constants::RuntimeConstants,
6787    gfx: &graphics::GfxRuntime,
6788    stage_idx: i64,
6789    runtime_slot: usize,
6790    obj: &globals::ObjectState,
6791) -> ButtonSortKey {
6792    let embedded_tree_object = obj.nested_runtime_slot.is_some();
6793    let layer = obj
6794        .lookup_int_prop(ids, ids.obj_layer)
6795        .or_else(|| {
6796            if embedded_tree_object {
6797                None
6798            } else {
6799                gfx.object_peek_layer(stage_idx, runtime_slot as i64)
6800            }
6801        })
6802        .unwrap_or(obj.base.layer);
6803    let order = obj
6804        .lookup_int_prop(ids, ids.obj_order)
6805        .or_else(|| {
6806            if embedded_tree_object {
6807                None
6808            } else {
6809                gfx.object_peek_order(stage_idx, runtime_slot as i64)
6810            }
6811        })
6812        .unwrap_or(obj.base.order);
6813    ButtonSortKey { order, layer }
6814}
6815
6816fn button_sort_ge(lhs: ButtonSortKey, rhs: ButtonSortKey) -> bool {
6817    lhs.order > rhs.order || (lhs.order == rhs.order && lhs.layer >= rhs.layer)
6818}
6819
6820fn has_standalone_button_action(obj: &globals::ObjectState) -> bool {
6821    obj.used
6822        && obj.button.enabled
6823        && !obj.button.is_disabled()
6824        && obj.button.group_idx().is_none()
6825        && obj.button.action_no >= 0
6826}
6827
6828fn merge_button_hit(
6829    best: &mut Option<ButtonHitCandidate>,
6830    tied: &mut bool,
6831    hit: ButtonHitCandidate,
6832) {
6833    match *best {
6834        None => {
6835            *best = Some(hit);
6836            *tied = false;
6837        }
6838        Some(prev) if button_sort_ge(hit.sort_key, prev.sort_key) => {
6839            // C_tnm_btn_mng::hit_test_proc uses >=, so equal order/layer means
6840            // the later registered button wins rather than producing a tie.
6841            *best = Some(hit);
6842            *tied = false;
6843        }
6844        _ => {}
6845    }
6846}
6847
6848fn object_event_value(
6849    ids: &constants::RuntimeConstants,
6850    obj: &globals::ObjectState,
6851    event_op: i32,
6852    current: i64,
6853) -> i64 {
6854    if event_op != 0 {
6855        obj.int_event_by_op(ids, event_op)
6856            .map(|ev| ev.get_total_value() as i64)
6857            .unwrap_or(current)
6858    } else {
6859        current
6860    }
6861}
6862
6863fn object_button_effective_gfx_hit(
6864    images: &mut ImageManager,
6865    layers: &LayerManager,
6866    gfx: &graphics::GfxRuntime,
6867    ids: &constants::RuntimeConstants,
6868    stage_idx: i64,
6869    runtime_slot: usize,
6870    obj: &globals::ObjectState,
6871    mx: i32,
6872    my: i32,
6873    parent_state: Option<ParentRenderState>,
6874) -> Option<ButtonSortKey> {
6875    let embedded_tree_object = obj.nested_runtime_slot.is_some();
6876    let disp = obj
6877        .lookup_int_prop(ids, ids.obj_disp)
6878        .or_else(|| {
6879            if embedded_tree_object {
6880                None
6881            } else {
6882                gfx.object_peek_disp(stage_idx, runtime_slot as i64)
6883            }
6884        })
6885        .unwrap_or(obj.base.disp);
6886    if disp == 0 {
6887        return None;
6888    }
6889
6890    let mut tr = obj.lookup_int_prop(ids, ids.obj_tr).unwrap_or(obj.base.tr);
6891    tr = object_event_value(ids, obj, ids.obj_tr_eve, tr);
6892    tr = obj
6893        .runtime
6894        .prop_event_lists
6895        .tr_rep
6896        .iter()
6897        .fold(tr, |acc, ev| {
6898            acc.saturating_mul(ev.get_total_value() as i64)
6899                .div_euclid(255)
6900        });
6901    if tr <= 0 {
6902        return None;
6903    }
6904
6905    if parent_state.is_none() {
6906        if let Some((layer_id, sprite_id)) =
6907            gfx.object_sprite_binding(stage_idx, runtime_slot as i64)
6908        {
6909            if hit_test_layer_sprite(
6910                images,
6911                layers,
6912                layer_id,
6913                sprite_id,
6914                mx,
6915                my,
6916                obj.button.alpha_test,
6917            ) {
6918                return Some(object_button_sort_key(
6919                    ids,
6920                    gfx,
6921                    stage_idx,
6922                    runtime_slot,
6923                    obj,
6924                ));
6925            }
6926            return None;
6927        }
6928    }
6929
6930    let (base_x, base_y) = if embedded_tree_object {
6931        (obj.base.x, obj.base.y)
6932    } else {
6933        gfx.object_peek_pos(stage_idx, runtime_slot as i64)
6934            .unwrap_or((obj.base.x, obj.base.y))
6935    };
6936    let mut x = obj.lookup_int_prop(ids, ids.obj_x).unwrap_or(base_x);
6937    let mut y = obj.lookup_int_prop(ids, ids.obj_y).unwrap_or(base_y);
6938    x = object_event_value(ids, obj, ids.obj_x_eve, x);
6939    y = object_event_value(ids, obj, ids.obj_y_eve, y);
6940    x += obj
6941        .runtime
6942        .prop_event_lists
6943        .x_rep
6944        .iter()
6945        .map(|ev| ev.get_total_value() as i64)
6946        .sum::<i64>();
6947    y += obj
6948        .runtime
6949        .prop_event_lists
6950        .y_rep
6951        .iter()
6952        .map(|ev| ev.get_total_value() as i64)
6953        .sum::<i64>();
6954
6955    let mut scale_x = obj
6956        .lookup_int_prop(ids, ids.obj_scale_x)
6957        .unwrap_or(obj.base.scale_x);
6958    let mut scale_y = obj
6959        .lookup_int_prop(ids, ids.obj_scale_y)
6960        .unwrap_or(obj.base.scale_y);
6961    scale_x = object_event_value(ids, obj, ids.obj_scale_x_eve, scale_x);
6962    scale_y = object_event_value(ids, obj, ids.obj_scale_y_eve, scale_y);
6963    if scale_x == 0 || scale_y == 0 {
6964        return None;
6965    }
6966
6967    let mut patno = obj
6968        .lookup_int_prop(ids, ids.obj_patno)
6969        .or_else(|| gfx.object_peek_patno(stage_idx, runtime_slot as i64))
6970        .unwrap_or(obj.base.patno);
6971    patno = object_event_value(ids, obj, ids.obj_patno_eve, patno);
6972    patno = patno.saturating_add(obj.button.cut_no);
6973
6974    let file_name = obj.file_name.as_ref()?;
6975    let img_id = CommandContext::load_any_image_for_hit(images, file_name.as_str(), patno)?;
6976
6977    let mut sprite = Sprite::default();
6978    sprite.image_id = Some(img_id);
6979    if let Some(img) = images.get(img_id) {
6980        sprite.object_anchor = true;
6981        sprite.texture_center_x = img.center_x as f32;
6982        sprite.texture_center_y = img.center_y as f32;
6983    } else {
6984        sprite.object_anchor = false;
6985        sprite.texture_center_x = 0.0;
6986        sprite.texture_center_y = 0.0;
6987    }
6988    sprite.visible = true;
6989    let center_x = obj.lookup_int_prop(ids, ids.obj_center_x).unwrap_or(obj.base.center_x);
6990    let center_y = obj.lookup_int_prop(ids, ids.obj_center_y).unwrap_or(obj.base.center_y);
6991    let center_z = obj.lookup_int_prop(ids, ids.obj_center_z).unwrap_or(obj.base.center_z);
6992    let center_rep_x = obj.lookup_int_prop(ids, ids.obj_center_rep_x).unwrap_or(obj.base.center_rep_x);
6993    let center_rep_y = obj.lookup_int_prop(ids, ids.obj_center_rep_y).unwrap_or(obj.base.center_rep_y);
6994    let center_rep_z = obj.lookup_int_prop(ids, ids.obj_center_rep_z).unwrap_or(obj.base.center_rep_z);
6995    sprite.x = x as i32;
6996    sprite.y = y as i32;
6997    sprite.pivot_x = (center_x + center_rep_x) as f32;
6998    sprite.pivot_y = (center_y + center_rep_y) as f32;
6999    sprite.pivot_z = (center_z + center_rep_z) as f32;
7000    sprite.scale_x = scale_x as f32 / 1000.0;
7001    sprite.scale_y = scale_y as f32 / 1000.0;
7002    sprite.tr = tr.clamp(0, 255) as u8;
7003    if let Some(parent) = parent_state {
7004        let dummy = ObjectRenderInfo::default();
7005        apply_parent_render_state_to_sprite(&mut sprite, &dummy, &parent);
7006    }
7007    sprite.x = (sprite.x as i64 + center_rep_x).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
7008    sprite.y = (sprite.y as i64 + center_rep_y).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
7009    sprite.z += center_rep_z as f32;
7010
7011    if !hit_test_render_sprite(images, &sprite, mx, my, obj.button.alpha_test) {
7012        return None;
7013    }
7014
7015    Some(object_button_sort_key(
7016        ids,
7017        gfx,
7018        stage_idx,
7019        runtime_slot,
7020        obj,
7021    ))
7022}
7023
7024fn collect_standalone_button_decided_actions_recursive(
7025    obj: &globals::ObjectState,
7026    out: &mut Vec<globals::PendingButtonAction>,
7027    sounds: &mut Vec<i64>,
7028) {
7029    if has_standalone_button_action(obj)
7030        && obj.button.pushed
7031        && (obj.button.hit || obj.button.push_keep)
7032    {
7033        push_object_button_decided_action(obj, out);
7034        sounds.push(obj.button.se_no);
7035    }
7036    for child in &obj.runtime.child_objects {
7037        collect_standalone_button_decided_actions_recursive(child, out, sounds);
7038    }
7039}
7040
7041#[derive(Debug, Clone, Copy)]
7042struct ButtonObjectRenderInfo {
7043    disp: bool,
7044    x: i64,
7045    y: i64,
7046    z: i64,
7047    x_rep: i64,
7048    y_rep: i64,
7049    z_rep: i64,
7050    center_x: i64,
7051    center_y: i64,
7052    center_z: i64,
7053    center_rep_x: i64,
7054    center_rep_y: i64,
7055    center_rep_z: i64,
7056    scale_x: i64,
7057    scale_y: i64,
7058    scale_z: i64,
7059    rotate_x: i64,
7060    rotate_y: i64,
7061    rotate_z: i64,
7062    tr: i64,
7063    tr_rep: i64,
7064    world_no: i64,
7065    dst_clip: Option<ClipRect>,
7066}
7067
7068fn fetch_bound_render_sprites_for_hit(
7069    layers: &LayerManager,
7070    gfx: &graphics::GfxRuntime,
7071    stage_idx: i64,
7072    runtime_slot: usize,
7073    obj: &globals::ObjectState,
7074) -> Vec<RenderSprite> {
7075    fn push_one(layers: &LayerManager, lid: LayerId, sid: SpriteId, out: &mut Vec<RenderSprite>) {
7076        let Some(layer) = layers.layer(lid) else {
7077            return;
7078        };
7079        let Some(sprite) = layer.sprite(sid) else {
7080            return;
7081        };
7082        if sprite.image_id.is_none() {
7083            return;
7084        }
7085        out.push(RenderSprite::new(Some(lid), Some(sid), sprite.clone()));
7086    }
7087    let mut out = Vec::new();
7088    match &obj.backend {
7089        globals::ObjectBackend::Gfx => {
7090            if let Some((lid, sid)) = gfx.object_sprite_binding(stage_idx, runtime_slot as i64) {
7091                push_one(layers, lid, sid, &mut out);
7092            }
7093        }
7094        globals::ObjectBackend::Rect {
7095            layer_id,
7096            sprite_id,
7097            ..
7098        }
7099        | globals::ObjectBackend::String {
7100            layer_id,
7101            sprite_id,
7102            ..
7103        }
7104        | globals::ObjectBackend::Movie {
7105            layer_id,
7106            sprite_id,
7107            ..
7108        } => {
7109            push_one(layers, *layer_id, *sprite_id, &mut out);
7110        }
7111        globals::ObjectBackend::Number {
7112            layer_id,
7113            sprite_ids,
7114        }
7115        | globals::ObjectBackend::Weather {
7116            layer_id,
7117            sprite_ids,
7118        } => {
7119            for sid in sprite_ids {
7120                push_one(layers, *layer_id, *sid, &mut out);
7121            }
7122        }
7123        globals::ObjectBackend::None => {}
7124    }
7125    out
7126}
7127
7128fn button_object_render_info(
7129    ids: &constants::RuntimeConstants,
7130    gfx: &graphics::GfxRuntime,
7131    stage_idx: i64,
7132    obj_idx: usize,
7133    obj: &globals::ObjectState,
7134) -> ButtonObjectRenderInfo {
7135    let runtime_slot = object_runtime_slot(obj_idx, obj);
7136    let embedded_tree_object = obj.nested_runtime_slot.is_some();
7137    let use_gfx_object_state =
7138        matches!(obj.backend, globals::ObjectBackend::Gfx) && !embedded_tree_object;
7139    let extra = |id: i32, default: i64| -> i64 {
7140        if id != 0 {
7141            obj.lookup_int_prop(ids, id).unwrap_or(default)
7142        } else {
7143            default
7144        }
7145    };
7146    let gfx_disp = || {
7147        if use_gfx_object_state {
7148            gfx.object_peek_disp(stage_idx, runtime_slot as i64)
7149        } else {
7150            None
7151        }
7152    };
7153    let gfx_pos = || {
7154        if use_gfx_object_state {
7155            gfx.object_peek_pos(stage_idx, runtime_slot as i64)
7156        } else {
7157            None
7158        }
7159    };
7160    let x_rep = obj
7161        .runtime
7162        .prop_event_lists
7163        .x_rep
7164        .iter()
7165        .map(|ev| ev.get_total_value() as i64)
7166        .sum::<i64>();
7167    let y_rep = obj
7168        .runtime
7169        .prop_event_lists
7170        .y_rep
7171        .iter()
7172        .map(|ev| ev.get_total_value() as i64)
7173        .sum::<i64>();
7174    let z_rep = obj
7175        .runtime
7176        .prop_event_lists
7177        .z_rep
7178        .iter()
7179        .map(|ev| ev.get_total_value() as i64)
7180        .sum::<i64>();
7181    let tr_rep = obj
7182        .runtime
7183        .prop_event_lists
7184        .tr_rep
7185        .iter()
7186        .fold(255i64, |acc, ev| {
7187            acc.saturating_mul(ev.get_total_value() as i64)
7188                .div_euclid(255)
7189        });
7190    let dst_clip = if extra(ids.obj_clip_use, obj.base.clip_use) != 0 {
7191        Some(ClipRect {
7192            left: extra(ids.obj_clip_left, obj.base.clip_left) as i32,
7193            top: extra(ids.obj_clip_top, obj.base.clip_top) as i32,
7194            right: extra(ids.obj_clip_right, obj.base.clip_right) as i32,
7195            bottom: extra(ids.obj_clip_bottom, obj.base.clip_bottom) as i32,
7196        })
7197    } else {
7198        None
7199    };
7200    ButtonObjectRenderInfo {
7201        disp: extra(ids.obj_disp, gfx_disp().unwrap_or(obj.base.disp)) != 0,
7202        x: object_event_value(
7203            ids,
7204            obj,
7205            ids.obj_x_eve,
7206            extra(ids.obj_x, gfx_pos().map(|v| v.0).unwrap_or(obj.base.x)),
7207        ),
7208        y: object_event_value(
7209            ids,
7210            obj,
7211            ids.obj_y_eve,
7212            extra(ids.obj_y, gfx_pos().map(|v| v.1).unwrap_or(obj.base.y)),
7213        ),
7214        z: object_event_value(ids, obj, ids.obj_z_eve, extra(ids.obj_z, obj.base.z)),
7215        x_rep,
7216        y_rep,
7217        z_rep,
7218        center_x: object_event_value(
7219            ids,
7220            obj,
7221            ids.obj_center_x_eve,
7222            extra(ids.obj_center_x, obj.base.center_x),
7223        ),
7224        center_y: object_event_value(
7225            ids,
7226            obj,
7227            ids.obj_center_y_eve,
7228            extra(ids.obj_center_y, obj.base.center_y),
7229        ),
7230        center_z: object_event_value(
7231            ids,
7232            obj,
7233            ids.obj_center_z_eve,
7234            extra(ids.obj_center_z, obj.base.center_z),
7235        ),
7236        center_rep_x: extra(ids.obj_center_rep_x, obj.base.center_rep_x),
7237        center_rep_y: extra(ids.obj_center_rep_y, obj.base.center_rep_y),
7238        center_rep_z: extra(ids.obj_center_rep_z, obj.base.center_rep_z),
7239        scale_x: object_event_value(
7240            ids,
7241            obj,
7242            ids.obj_scale_x_eve,
7243            extra(ids.obj_scale_x, obj.base.scale_x),
7244        ),
7245        scale_y: object_event_value(
7246            ids,
7247            obj,
7248            ids.obj_scale_y_eve,
7249            extra(ids.obj_scale_y, obj.base.scale_y),
7250        ),
7251        scale_z: object_event_value(
7252            ids,
7253            obj,
7254            ids.obj_scale_z_eve,
7255            extra(ids.obj_scale_z, obj.base.scale_z),
7256        ),
7257        rotate_x: object_event_value(
7258            ids,
7259            obj,
7260            ids.obj_rotate_x_eve,
7261            extra(ids.obj_rotate_x, obj.base.rotate_x),
7262        ),
7263        rotate_y: object_event_value(
7264            ids,
7265            obj,
7266            ids.obj_rotate_y_eve,
7267            extra(ids.obj_rotate_y, obj.base.rotate_y),
7268        ),
7269        rotate_z: object_event_value(
7270            ids,
7271            obj,
7272            ids.obj_rotate_z_eve,
7273            extra(ids.obj_rotate_z, obj.base.rotate_z),
7274        ),
7275        tr: object_event_value(ids, obj, ids.obj_tr_eve, extra(ids.obj_tr, obj.base.tr)),
7276        tr_rep,
7277        world_no: extra(ids.obj_world, obj.base.world),
7278        dst_clip,
7279    }
7280}
7281
7282fn apply_button_object_render_info_to_sprite(sprite: &mut Sprite, info: &ButtonObjectRenderInfo) {
7283    sprite.visible = info.disp;
7284    sprite.x = (info.x + info.x_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
7285    sprite.y = (info.y + info.y_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
7286    sprite.z = (info.z + info.z_rep) as f32;
7287    sprite.pivot_x = (info.center_x + info.center_rep_x) as f32;
7288    sprite.pivot_y = (info.center_y + info.center_rep_y) as f32;
7289    sprite.pivot_z = (info.center_z + info.center_rep_z) as f32;
7290    sprite.scale_x = info.scale_x as f32 / 1000.0;
7291    sprite.scale_y = info.scale_y as f32 / 1000.0;
7292    sprite.scale_z = info.scale_z as f32 / 1000.0;
7293    sprite.rotate = info.rotate_z as f32 * std::f32::consts::PI / 1800.0;
7294    sprite.rotate_x = info.rotate_x as f32 * std::f32::consts::PI / 1800.0;
7295    sprite.rotate_y = info.rotate_y as f32 * std::f32::consts::PI / 1800.0;
7296    sprite.tr = ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255).clamp(0, 255) as u8;
7297    sprite.dst_clip = info.dst_clip;
7298}
7299
7300fn finalize_button_object_center_rep_to_sprite(sprite: &mut Sprite, info: &ButtonObjectRenderInfo) {
7301    let x = (sprite.x as i64 + info.center_rep_x).clamp(i32::MIN as i64, i32::MAX as i64);
7302    let y = (sprite.y as i64 + info.center_rep_y).clamp(i32::MIN as i64, i32::MAX as i64);
7303    sprite.x = x as i32;
7304    sprite.y = y as i32;
7305    sprite.z += info.center_rep_z as f32;
7306}
7307
7308fn object_button_hit_sort_key_from_render(
7309    images: &mut ImageManager,
7310    layers: &LayerManager,
7311    gfx: &graphics::GfxRuntime,
7312    ids: &constants::RuntimeConstants,
7313    syscom: &globals::SyscomRuntimeState,
7314    stage_idx: i64,
7315    obj_idx: usize,
7316    obj: &globals::ObjectState,
7317    mx: i32,
7318    my: i32,
7319    parent_state: Option<ParentRenderState>,
7320) -> Option<ButtonSortKey> {
7321    if !object_button_renderable_by_syscom(syscom, obj)
7322        || button_effective_disabled(syscom, obj, None)
7323        || syscom.mwnd_btn_touch_disable
7324    {
7325        if sg_debug_enabled() && obj.button.enabled {
7326            eprintln!(
7327                "[SG_DEBUG][BUTTON_TRACE][HIT] reject stage={} obj_idx={} runtime_slot={} file={:?} mx={} my={} visible={} disabled_reason={:?} touch_disable={} button_no={} group_no={} group_idx={:?} action_no={} state={} hit={} pushed={} alpha_test={} sys_type={} sys_opt={} mode={}",
7328                stage_idx,
7329                obj_idx,
7330                object_runtime_slot(obj_idx, obj),
7331                obj.file_name,
7332                mx,
7333                my,
7334                object_button_renderable_by_syscom(syscom, obj),
7335                button_disabled_reason(syscom, obj, None),
7336                syscom.mwnd_btn_touch_disable,
7337                obj.button.button_no,
7338                obj.button.group_no,
7339                obj.button.group_idx(),
7340                obj.button.action_no,
7341                obj.button.state,
7342                obj.button.hit,
7343                obj.button.pushed,
7344                obj.button.alpha_test,
7345                obj.button.sys_type,
7346                obj.button.sys_type_opt,
7347                obj.button.mode
7348            );
7349        }
7350        return None;
7351    }
7352    let runtime_slot = object_runtime_slot(obj_idx, obj);
7353    let info = button_object_render_info(ids, gfx, stage_idx, obj_idx, obj);
7354    let mut bound = fetch_bound_render_sprites_for_hit(layers, gfx, stage_idx, runtime_slot, obj);
7355    for rs in &mut bound {
7356        apply_button_object_render_info_to_sprite(&mut rs.sprite, &info);
7357        if let Some(parent) = parent_state {
7358            let dummy = ObjectRenderInfo::default();
7359            apply_parent_render_state_to_sprite(&mut rs.sprite, &dummy, &parent);
7360        }
7361        finalize_button_object_center_rep_to_sprite(&mut rs.sprite, &info);
7362        if hit_test_render_sprite(images, &rs.sprite, mx, my, obj.button.alpha_test) {
7363            let sort_key = object_button_sort_key(ids, gfx, stage_idx, runtime_slot, obj);
7364            if sg_debug_enabled() {
7365                eprintln!(
7366                    "[SG_DEBUG][BUTTON_TRACE][HIT] success stage={} obj_idx={} runtime_slot={} file={:?} mx={} my={} button_no={} group_no={} group_idx={:?} action_no={} state={} hit={} pushed={} alpha_test={} sprite=({:?},{:?}) pos=({}, {}) size_mode={:?} sort={}",
7367                    stage_idx,
7368                    obj_idx,
7369                    runtime_slot,
7370                    obj.file_name,
7371                    mx,
7372                    my,
7373                    obj.button.button_no,
7374                    obj.button.group_no,
7375                    obj.button.group_idx(),
7376                    obj.button.action_no,
7377                    obj.button.state,
7378                    obj.button.hit,
7379                    obj.button.pushed,
7380                    obj.button.alpha_test,
7381                    rs.layer_id,
7382                    rs.sprite_id,
7383                    rs.sprite.x,
7384                    rs.sprite.y,
7385                    rs.sprite.size_mode,
7386                    sort_key.display_tuple()
7387                );
7388            }
7389            return Some(sort_key);
7390        }
7391    }
7392
7393    if bound.is_empty() {
7394        return object_button_effective_gfx_hit(
7395            images,
7396            layers,
7397            gfx,
7398            ids,
7399            stage_idx,
7400            runtime_slot,
7401            obj,
7402            mx,
7403            my,
7404            parent_state,
7405        );
7406    }
7407    None
7408}
7409
7410fn button_parent_render_state(
7411    layers: &LayerManager,
7412    gfx: &graphics::GfxRuntime,
7413    ids: &constants::RuntimeConstants,
7414    stage_idx: i64,
7415    obj_idx: usize,
7416    obj: &globals::ObjectState,
7417    parent_state: Option<ParentRenderState>,
7418) -> ParentRenderState {
7419    let runtime_slot = object_runtime_slot(obj_idx, obj);
7420    let info = button_object_render_info(ids, gfx, stage_idx, obj_idx, obj);
7421    let bound = fetch_bound_render_sprites_for_hit(layers, gfx, stage_idx, runtime_slot, obj);
7422    let mut cur = ParentRenderState {
7423        world_no: info.world_no,
7424        pos_x: (info.x + info.x_rep) as f32,
7425        pos_y: (info.y + info.y_rep) as f32,
7426        pos_z: (info.z + info.z_rep) as f32,
7427        center_rep_x: info.center_rep_x as f32,
7428        center_rep_y: info.center_rep_y as f32,
7429        center_rep_z: info.center_rep_z as f32,
7430        scale_x: info.scale_x as f32 / 1000.0,
7431        scale_y: info.scale_y as f32 / 1000.0,
7432        scale_z: info.scale_z as f32 / 1000.0,
7433        rotate_x: info.rotate_x as f32 * std::f32::consts::PI / 1800.0,
7434        rotate_y: info.rotate_y as f32 * std::f32::consts::PI / 1800.0,
7435        rotate_z: info.rotate_z as f32 * std::f32::consts::PI / 1800.0,
7436        tr: ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255) as i32,
7437        mono: 0,
7438        reverse: 0,
7439        bright: 0,
7440        dark: 0,
7441        color_rate: 0,
7442        color_r: 255,
7443        color_g: 255,
7444        color_b: 255,
7445        color_add_r: 0,
7446        color_add_g: 0,
7447        color_add_b: 0,
7448        blend: crate::layer::SpriteBlend::Normal,
7449        dst_clip: info.dst_clip,
7450        mask_image_id: bound.first().and_then(|s| s.sprite.mask_image_id),
7451        mask_offset_x: bound.first().map(|s| s.sprite.mask_offset_x).unwrap_or(0),
7452        mask_offset_y: bound.first().map(|s| s.sprite.mask_offset_y).unwrap_or(0),
7453        tonecurve_image_id: bound.first().and_then(|s| s.sprite.tonecurve_image_id),
7454        tonecurve_row: bound.first().map(|s| s.sprite.tonecurve_row).unwrap_or(0.0),
7455        tonecurve_sat: bound.first().map(|s| s.sprite.tonecurve_sat).unwrap_or(0.0),
7456    };
7457    if let Some(parent) = parent_state {
7458        cur = compose_parent_render_state(parent, cur);
7459    }
7460    cur
7461}
7462
7463fn hit_test_standalone_action_button_recursive(
7464    images: &mut ImageManager,
7465    layers: &LayerManager,
7466    gfx: &graphics::GfxRuntime,
7467    ids: &constants::RuntimeConstants,
7468    syscom: &globals::SyscomRuntimeState,
7469    stage_idx: i64,
7470    mx: i32,
7471    my: i32,
7472    obj_idx: usize,
7473    obj: &mut globals::ObjectState,
7474    parent_state: Option<ParentRenderState>,
7475) -> Option<ButtonHitCandidate> {
7476    fn recurse(
7477        images: &mut ImageManager,
7478        layers: &LayerManager,
7479        gfx: &graphics::GfxRuntime,
7480        ids: &constants::RuntimeConstants,
7481        syscom: &globals::SyscomRuntimeState,
7482        stage_idx: i64,
7483        mx: i32,
7484        my: i32,
7485        obj_idx: usize,
7486        obj: &mut globals::ObjectState,
7487        parent_state: Option<ParentRenderState>,
7488        inherited_owner: Option<ButtonOwnerInfo>,
7489    ) -> Option<ButtonHitCandidate> {
7490        let runtime_slot = object_runtime_slot(obj_idx, obj);
7491        let current_owner = if has_standalone_button_action(obj) && !obj.base.no_event_hint {
7492            Some(ButtonOwnerInfo {
7493                button_no: obj.button.button_no,
7494                runtime_slot,
7495                se_no: obj.button.se_no,
7496                was_hit: obj.button.last_hit,
7497            })
7498        } else {
7499            None
7500        };
7501        let effective_owner = current_owner.or(inherited_owner);
7502
7503        let mut best = None;
7504        let mut tied = false;
7505        if let Some(owner) = effective_owner {
7506            if !obj.base.no_event_hint {
7507                if let Some(sort_key) = object_button_hit_sort_key_from_render(
7508                    images,
7509                    layers,
7510                    gfx,
7511                    ids,
7512                    syscom,
7513                    stage_idx,
7514                    obj_idx,
7515                    obj,
7516                    mx,
7517                    my,
7518                    parent_state,
7519                ) {
7520                    best = Some(ButtonHitCandidate {
7521                        button_no: owner.button_no,
7522                        sort_key,
7523                        runtime_slot: owner.runtime_slot,
7524                        se_no: owner.se_no,
7525                        was_hit: owner.was_hit,
7526                    });
7527                }
7528            }
7529        }
7530        let cur_parent_state =
7531            button_parent_render_state(layers, gfx, ids, stage_idx, obj_idx, obj, parent_state);
7532        for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
7533            if let Some(hit) = recurse(
7534                images,
7535                layers,
7536                gfx,
7537                ids,
7538                syscom,
7539                stage_idx,
7540                mx,
7541                my,
7542                child_idx,
7543                child,
7544                Some(cur_parent_state),
7545                effective_owner,
7546            ) {
7547                merge_button_hit(&mut best, &mut tied, hit);
7548            }
7549        }
7550        if tied {
7551            None
7552        } else {
7553            best
7554        }
7555    }
7556
7557    recurse(
7558        images,
7559        layers,
7560        gfx,
7561        ids,
7562        syscom,
7563        stage_idx,
7564        mx,
7565        my,
7566        obj_idx,
7567        obj,
7568        parent_state,
7569        None,
7570    )
7571}
7572
7573fn hit_test_object_button_recursive(
7574    images: &mut ImageManager,
7575    layers: &LayerManager,
7576    gfx: &graphics::GfxRuntime,
7577    ids: &constants::RuntimeConstants,
7578    syscom: &globals::SyscomRuntimeState,
7579    stage_idx: i64,
7580    group_idx: usize,
7581    mx: i32,
7582    my: i32,
7583    obj_idx: usize,
7584    obj: &mut globals::ObjectState,
7585    parent_state: Option<ParentRenderState>,
7586) -> Option<ButtonHitCandidate> {
7587    fn recurse(
7588        images: &mut ImageManager,
7589        layers: &LayerManager,
7590        gfx: &graphics::GfxRuntime,
7591        ids: &constants::RuntimeConstants,
7592        syscom: &globals::SyscomRuntimeState,
7593        stage_idx: i64,
7594        group_idx: usize,
7595        mx: i32,
7596        my: i32,
7597        obj_idx: usize,
7598        obj: &mut globals::ObjectState,
7599        parent_state: Option<ParentRenderState>,
7600        inherited_owner: Option<ButtonOwnerInfo>,
7601    ) -> Option<ButtonHitCandidate> {
7602        let runtime_slot = object_runtime_slot(obj_idx, obj);
7603        let current_owner = if obj.used
7604            && obj.button.enabled
7605            && !obj.button.is_disabled()
7606            && !obj.base.no_event_hint
7607            && obj.button.action_no >= 0
7608            && obj.button.group_idx() == Some(group_idx)
7609        {
7610            Some(ButtonOwnerInfo {
7611                button_no: obj.button.button_no,
7612                runtime_slot,
7613                se_no: obj.button.se_no,
7614                was_hit: obj.button.last_hit,
7615            })
7616        } else {
7617            None
7618        };
7619        let effective_owner = current_owner.or(inherited_owner);
7620
7621        let mut best = None;
7622        let mut tied = false;
7623        if let Some(owner) = effective_owner {
7624            if !obj.base.no_event_hint {
7625                if let Some(sort_key) = object_button_hit_sort_key_from_render(
7626                    images,
7627                    layers,
7628                    gfx,
7629                    ids,
7630                    syscom,
7631                    stage_idx,
7632                    obj_idx,
7633                    obj,
7634                    mx,
7635                    my,
7636                    parent_state,
7637                ) {
7638                    best = Some(ButtonHitCandidate {
7639                        button_no: owner.button_no,
7640                        sort_key,
7641                        runtime_slot: owner.runtime_slot,
7642                        se_no: owner.se_no,
7643                        was_hit: owner.was_hit,
7644                    });
7645                }
7646            }
7647        }
7648        let cur_parent_state =
7649            button_parent_render_state(layers, gfx, ids, stage_idx, obj_idx, obj, parent_state);
7650        for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
7651            if let Some(hit) = recurse(
7652                images,
7653                layers,
7654                gfx,
7655                ids,
7656                syscom,
7657                stage_idx,
7658                group_idx,
7659                mx,
7660                my,
7661                child_idx,
7662                child,
7663                Some(cur_parent_state),
7664                effective_owner,
7665            ) {
7666                merge_button_hit(&mut best, &mut tied, hit);
7667            }
7668        }
7669        if tied {
7670            None
7671        } else {
7672            best
7673        }
7674    }
7675
7676    recurse(
7677        images,
7678        layers,
7679        gfx,
7680        ids,
7681        syscom,
7682        stage_idx,
7683        group_idx,
7684        mx,
7685        my,
7686        obj_idx,
7687        obj,
7688        parent_state,
7689        None,
7690    )
7691}
7692
7693fn find_object_by_runtime_slot_mut(
7694    mut objects: &mut [globals::ObjectState],
7695    runtime_slot: usize,
7696) -> Option<&mut globals::ObjectState> {
7697    let mut idx = 0usize;
7698    while let Some((obj, tail)) = objects.split_first_mut() {
7699        if obj.runtime_slot_or(idx) == runtime_slot {
7700            return Some(obj);
7701        }
7702        if let Some(found) =
7703            find_object_by_runtime_slot_mut(&mut obj.runtime.child_objects, runtime_slot)
7704        {
7705            return Some(found);
7706        }
7707        objects = tail;
7708        idx += 1;
7709    }
7710    None
7711}
7712
7713fn intersect_clip_rect(lhs: ClipRect, rhs: ClipRect) -> Option<ClipRect> {
7714    let left = lhs.left.max(rhs.left);
7715    let top = lhs.top.max(rhs.top);
7716    let right = lhs.right.min(rhs.right);
7717    let bottom = lhs.bottom.min(rhs.bottom);
7718    if left < right && top < bottom {
7719        Some(ClipRect {
7720            left,
7721            top,
7722            right,
7723            bottom,
7724        })
7725    } else {
7726        None
7727    }
7728}
7729
7730fn transform_clip_rect_by_parent(clip: ClipRect, parent: &ParentRenderState) -> ClipRect {
7731    let (sin_z, cos_z) = parent.rotate_z.sin_cos();
7732    let mut min_x = f32::INFINITY;
7733    let mut min_y = f32::INFINITY;
7734    let mut max_x = f32::NEG_INFINITY;
7735    let mut max_y = f32::NEG_INFINITY;
7736
7737    for (x, y) in [
7738        (clip.left as f32, clip.top as f32),
7739        (clip.right as f32, clip.top as f32),
7740        (clip.left as f32, clip.bottom as f32),
7741        (clip.right as f32, clip.bottom as f32),
7742    ] {
7743        let rel_x = (x - parent.center_rep_x) * parent.scale_x;
7744        let rel_y = (y - parent.center_rep_y) * parent.scale_y;
7745        let rot_x = rel_x * cos_z - rel_y * sin_z;
7746        let rot_y = rel_x * sin_z + rel_y * cos_z;
7747        let tx = parent.pos_x + parent.center_rep_x + rot_x;
7748        let ty = parent.pos_y + parent.center_rep_y + rot_y;
7749        min_x = min_x.min(tx);
7750        min_y = min_y.min(ty);
7751        max_x = max_x.max(tx);
7752        max_y = max_y.max(ty);
7753    }
7754
7755    ClipRect {
7756        left: min_x.floor() as i32,
7757        top: min_y.floor() as i32,
7758        right: max_x.ceil() as i32,
7759        bottom: max_y.ceil() as i32,
7760    }
7761}
7762
7763fn compose_clip_rect(
7764    parent_clip: Option<ClipRect>,
7765    child_clip: Option<ClipRect>,
7766    parent: &ParentRenderState,
7767) -> Option<ClipRect> {
7768    match (parent_clip, child_clip) {
7769        (Some(parent_clip), Some(child_clip)) => intersect_clip_rect(
7770            parent_clip,
7771            transform_clip_rect_by_parent(child_clip, parent),
7772        ),
7773        (Some(parent_clip), None) => Some(parent_clip),
7774        (None, Some(child_clip)) => Some(transform_clip_rect_by_parent(child_clip, parent)),
7775        (None, None) => None,
7776    }
7777}
7778fn compose_parent_render_state(
7779    parent: ParentRenderState,
7780    mut cur: ParentRenderState,
7781) -> ParentRenderState {
7782    if cur.world_no < 0 {
7783        cur.world_no = parent.world_no;
7784    }
7785
7786    let child_clip = cur.dst_clip;
7787
7788    cur.pos_x = (cur.pos_x - parent.center_rep_x) * parent.scale_x + parent.center_rep_x;
7789    cur.pos_y = (cur.pos_y - parent.center_rep_y) * parent.scale_y + parent.center_rep_y;
7790    cur.pos_z = (cur.pos_z - parent.center_rep_z) * parent.scale_z + parent.center_rep_z;
7791    {
7792        let tmp_x = cur.pos_x;
7793        let tmp_y = cur.pos_y;
7794        let (s, c) = parent.rotate_z.sin_cos();
7795        cur.pos_x = (tmp_x - parent.center_rep_x) * c - (tmp_y - parent.center_rep_y) * s
7796            + parent.center_rep_x;
7797        cur.pos_y = (tmp_x - parent.center_rep_x) * s
7798            + (tmp_y - parent.center_rep_y) * c
7799            + parent.center_rep_y;
7800    }
7801    cur.pos_x += parent.pos_x;
7802    cur.pos_y += parent.pos_y;
7803    cur.pos_z += parent.pos_z;
7804    cur.scale_x *= parent.scale_x;
7805    cur.scale_y *= parent.scale_y;
7806    cur.scale_z *= parent.scale_z;
7807    cur.rotate_x += parent.rotate_x;
7808    cur.rotate_y += parent.rotate_y;
7809    cur.rotate_z += parent.rotate_z;
7810
7811    cur.dst_clip = compose_clip_rect(parent.dst_clip, child_clip, &parent);
7812
7813    cur.tr = (cur.tr * parent.tr / 255).clamp(0, 255);
7814    cur.mono = combine_lerp(cur.mono as u8, parent.mono) as i32;
7815    cur.reverse = combine_lerp(cur.reverse as u8, parent.reverse) as i32;
7816    cur.bright = combine_lerp(cur.bright as u8, parent.bright) as i32;
7817    cur.dark = combine_lerp(cur.dark as u8, parent.dark) as i32;
7818    if cur.color_rate + parent.color_rate > 0 {
7819        let parent_rate = (parent.color_rate * 255 * 255)
7820            / (255 * 255 - (255 - cur.color_rate) * (255 - parent.color_rate)).max(1);
7821        cur.color_r = blend_color(cur.color_r as u8, parent.color_r, parent_rate) as i32;
7822        cur.color_g = blend_color(cur.color_g as u8, parent.color_g, parent_rate) as i32;
7823        cur.color_b = blend_color(cur.color_b as u8, parent.color_b, parent_rate) as i32;
7824    }
7825    cur.color_rate = combine_lerp(cur.color_rate as u8, parent.color_rate) as i32;
7826    cur.color_add_r = clamp_add(cur.color_add_r as u8, parent.color_add_r) as i32;
7827    cur.color_add_g = clamp_add(cur.color_add_g as u8, parent.color_add_g) as i32;
7828    cur.color_add_b = clamp_add(cur.color_add_b as u8, parent.color_add_b) as i32;
7829    if matches!(cur.blend, crate::layer::SpriteBlend::Normal) {
7830        cur.blend = parent.blend;
7831    }
7832    if cur.mask_image_id.is_none() {
7833        cur.mask_image_id = parent.mask_image_id;
7834        cur.mask_offset_x = parent.mask_offset_x;
7835        cur.mask_offset_y = parent.mask_offset_y;
7836    }
7837    if cur.tonecurve_image_id.is_none() {
7838        cur.tonecurve_image_id = parent.tonecurve_image_id;
7839        cur.tonecurve_row = parent.tonecurve_row;
7840        cur.tonecurve_sat = parent.tonecurve_sat;
7841    }
7842    cur
7843}
7844
7845fn apply_object_event_animations_recursive(
7846    ids: &constants::RuntimeConstants,
7847    gfx: &mut graphics::GfxRuntime,
7848    images: &mut ImageManager,
7849    layers: &mut LayerManager,
7850    stage_i64: i64,
7851    obj_i64: i64,
7852    obj: &mut globals::ObjectState,
7853) {
7854    if obj.any_event_active() {
7855        let read_ev = |op_id: i32, obj: &globals::ObjectState| -> Option<i64> {
7856            if op_id == 0 {
7857                None
7858            } else {
7859                obj.int_event_by_op(ids, op_id)
7860                    .filter(|ev| ev.check_event())
7861                    .map(|ev| ev.get_total_value() as i64)
7862            }
7863        };
7864        let read_list0 = |op_id: i32, obj: &globals::ObjectState| -> Option<i64> {
7865            if op_id == 0 {
7866                None
7867            } else {
7868                obj.int_event_list_by_op(ids, op_id)
7869                    .and_then(|list| list.get(0))
7870                    .filter(|ev| ev.check_event())
7871                    .map(|ev| ev.get_total_value() as i64)
7872            }
7873        };
7874
7875        let x: Option<i64> = read_ev(ids.obj_x_eve, obj);
7876        let y: Option<i64> = read_ev(ids.obj_y_eve, obj);
7877        let x_rep: Option<i64> = read_list0(ids.obj_x_rep_eve, obj);
7878        let y_rep: Option<i64> = read_list0(ids.obj_y_rep_eve, obj);
7879        let z_rep: Option<i64> = read_list0(ids.obj_z_rep_eve, obj);
7880        let alpha: Option<i64> = None;
7881        let patno: Option<i64> = read_ev(ids.obj_patno_eve, obj);
7882        let order: Option<i64> = None;
7883        let layer_no: Option<i64> = None;
7884        let z: Option<i64> = read_ev(ids.obj_z_eve, obj);
7885        let center_x: Option<i64> = read_ev(ids.obj_center_x_eve, obj);
7886        let center_y: Option<i64> = read_ev(ids.obj_center_y_eve, obj);
7887        let center_z: Option<i64> = read_ev(ids.obj_center_z_eve, obj);
7888        let center_rep_x: Option<i64> = read_ev(ids.obj_center_rep_x_eve, obj);
7889        let center_rep_y: Option<i64> = read_ev(ids.obj_center_rep_y_eve, obj);
7890        let center_rep_z: Option<i64> = read_ev(ids.obj_center_rep_z_eve, obj);
7891        let scale_x: Option<i64> = read_ev(ids.obj_scale_x_eve, obj);
7892        let scale_y: Option<i64> = read_ev(ids.obj_scale_y_eve, obj);
7893        let scale_z: Option<i64> = read_ev(ids.obj_scale_z_eve, obj);
7894        let rotate_x: Option<i64> = read_ev(ids.obj_rotate_x_eve, obj);
7895        let rotate_y: Option<i64> = read_ev(ids.obj_rotate_y_eve, obj);
7896        let rotate_z: Option<i64> = read_ev(ids.obj_rotate_z_eve, obj);
7897        let clip_left: Option<i64> = read_ev(ids.obj_clip_left_eve, obj);
7898        let clip_top: Option<i64> = read_ev(ids.obj_clip_top_eve, obj);
7899        let clip_right: Option<i64> = read_ev(ids.obj_clip_right_eve, obj);
7900        let clip_bottom: Option<i64> = read_ev(ids.obj_clip_bottom_eve, obj);
7901        let src_clip_left: Option<i64> = read_ev(ids.obj_src_clip_left_eve, obj);
7902        let src_clip_top: Option<i64> = read_ev(ids.obj_src_clip_top_eve, obj);
7903        let src_clip_right: Option<i64> = read_ev(ids.obj_src_clip_right_eve, obj);
7904        let src_clip_bottom: Option<i64> = read_ev(ids.obj_src_clip_bottom_eve, obj);
7905        let tr: Option<i64> = read_ev(ids.obj_tr_eve, obj);
7906        let tr_rep: Option<i64> = read_list0(ids.obj_tr_rep_eve, obj);
7907        let mono: Option<i64> = read_ev(ids.obj_mono_eve, obj);
7908        let reverse: Option<i64> = read_ev(ids.obj_reverse_eve, obj);
7909        let bright: Option<i64> = read_ev(ids.obj_bright_eve, obj);
7910        let dark: Option<i64> = read_ev(ids.obj_dark_eve, obj);
7911        let color_rate: Option<i64> = read_ev(ids.obj_color_rate_eve, obj);
7912        let color_add_r: Option<i64> = read_ev(ids.obj_color_add_r_eve, obj);
7913        let color_add_g: Option<i64> = read_ev(ids.obj_color_add_g_eve, obj);
7914        let color_add_b: Option<i64> = read_ev(ids.obj_color_add_b_eve, obj);
7915        let color_r: Option<i64> = read_ev(ids.obj_color_r_eve, obj);
7916        let color_g: Option<i64> = read_ev(ids.obj_color_g_eve, obj);
7917        let color_b: Option<i64> = read_ev(ids.obj_color_b_eve, obj);
7918
7919        let mut set_extra_prop = |prop_id: i32, val: Option<i64>| {
7920            if prop_id != 0 {
7921                if let Some(v) = val {
7922                    let old_value = obj.get_int_prop(ids, prop_id);
7923                    trace_config_event_frame_prop_write(
7924                        ids,
7925                        stage_i64,
7926                        obj_i64,
7927                        obj,
7928                        prop_id,
7929                        old_value,
7930                        v,
7931                    );
7932                    obj.set_int_prop_from_event_frame(ids, prop_id, v);
7933                }
7934            }
7935        };
7936        set_extra_prop(ids.obj_x, x);
7937        set_extra_prop(ids.obj_y, y);
7938        // REP event lists are consumed directly by ObjectRenderInfo and hit testing.
7939        // Do not write animated totals back through obj_x_rep/obj_y_rep/obj_z_rep,
7940        // because those properties alias the same event-list storage.
7941        set_extra_prop(ids.obj_alpha, alpha);
7942        set_extra_prop(ids.obj_patno, patno);
7943        set_extra_prop(ids.obj_order, order);
7944        set_extra_prop(ids.obj_layer, layer_no);
7945        set_extra_prop(ids.obj_z, z);
7946        set_extra_prop(ids.obj_center_x, center_x);
7947        set_extra_prop(ids.obj_center_y, center_y);
7948        set_extra_prop(ids.obj_center_z, center_z);
7949        set_extra_prop(ids.obj_center_rep_x, center_rep_x);
7950        set_extra_prop(ids.obj_center_rep_y, center_rep_y);
7951        set_extra_prop(ids.obj_center_rep_z, center_rep_z);
7952        set_extra_prop(ids.obj_scale_x, scale_x);
7953        set_extra_prop(ids.obj_scale_y, scale_y);
7954        set_extra_prop(ids.obj_scale_z, scale_z);
7955        set_extra_prop(ids.obj_rotate_x, rotate_x);
7956        set_extra_prop(ids.obj_rotate_y, rotate_y);
7957        set_extra_prop(ids.obj_rotate_z, rotate_z);
7958        set_extra_prop(ids.obj_clip_left, clip_left);
7959        set_extra_prop(ids.obj_clip_top, clip_top);
7960        set_extra_prop(ids.obj_clip_right, clip_right);
7961        set_extra_prop(ids.obj_clip_bottom, clip_bottom);
7962        set_extra_prop(ids.obj_src_clip_left, src_clip_left);
7963        set_extra_prop(ids.obj_src_clip_top, src_clip_top);
7964        set_extra_prop(ids.obj_src_clip_right, src_clip_right);
7965        set_extra_prop(ids.obj_src_clip_bottom, src_clip_bottom);
7966        set_extra_prop(ids.obj_tr, tr);
7967        // obj_tr_rep also aliases prop_event_lists.tr_rep and must not be overwritten here.
7968        set_extra_prop(ids.obj_mono, mono);
7969        set_extra_prop(ids.obj_reverse, reverse);
7970        set_extra_prop(ids.obj_bright, bright);
7971        set_extra_prop(ids.obj_dark, dark);
7972        set_extra_prop(ids.obj_color_rate, color_rate);
7973        set_extra_prop(ids.obj_color_add_r, color_add_r);
7974        set_extra_prop(ids.obj_color_add_g, color_add_g);
7975        set_extra_prop(ids.obj_color_add_b, color_add_b);
7976        set_extra_prop(ids.obj_color_r, color_r);
7977        set_extra_prop(ids.obj_color_g, color_g);
7978        set_extra_prop(ids.obj_color_b, color_b);
7979
7980        if !(x.is_none()
7981            && y.is_none()
7982            && x_rep.is_none()
7983            && y_rep.is_none()
7984            && z_rep.is_none()
7985            && alpha.is_none()
7986            && patno.is_none()
7987            && order.is_none()
7988            && layer_no.is_none()
7989            && z.is_none()
7990            && center_x.is_none()
7991            && center_y.is_none()
7992            && center_z.is_none()
7993            && center_rep_x.is_none()
7994            && center_rep_y.is_none()
7995            && center_rep_z.is_none()
7996            && scale_x.is_none()
7997            && scale_y.is_none()
7998            && scale_z.is_none()
7999            && rotate_x.is_none()
8000            && rotate_y.is_none()
8001            && rotate_z.is_none()
8002            && clip_left.is_none()
8003            && clip_top.is_none()
8004            && clip_right.is_none()
8005            && clip_bottom.is_none()
8006            && src_clip_left.is_none()
8007            && src_clip_top.is_none()
8008            && src_clip_right.is_none()
8009            && src_clip_bottom.is_none()
8010            && tr.is_none()
8011            && tr_rep.is_none()
8012            && mono.is_none()
8013            && reverse.is_none()
8014            && bright.is_none()
8015            && dark.is_none()
8016            && color_rate.is_none()
8017            && color_add_r.is_none()
8018            && color_add_g.is_none()
8019            && color_add_b.is_none()
8020            && color_r.is_none()
8021            && color_g.is_none()
8022            && color_b.is_none())
8023        {
8024            match &obj.backend {
8025                globals::ObjectBackend::Gfx => {
8026                    if let Some(ax) = x {
8027                        let _ = gfx.object_set_x(images, layers, stage_i64, obj_i64, ax);
8028                    }
8029                    if let Some(ay) = y {
8030                        let _ = gfx.object_set_y(images, layers, stage_i64, obj_i64, ay);
8031                    }
8032                    if let Some(a) = alpha {
8033                        let _ = gfx.object_set_alpha(images, layers, stage_i64, obj_i64, a);
8034                    }
8035                    if let Some(p) = patno {
8036                        let _ = gfx.object_set_pat_no(images, layers, stage_i64, obj_i64, p);
8037                    }
8038                    if let Some(o) = order {
8039                        let _ = gfx.object_set_order(images, layers, stage_i64, obj_i64, o);
8040                    }
8041                    if let Some(l) = layer_no {
8042                        let _ = gfx.object_set_layer(images, layers, stage_i64, obj_i64, l);
8043                    }
8044                    if let Some(zv) = z {
8045                        let _ = gfx.object_set_z(stage_i64, obj_i64, zv);
8046                    }
8047                    if center_x.is_some() || center_y.is_some() {
8048                        let cx = center_x
8049                            .or_else(|| {
8050                                (ids.obj_center_x != 0)
8051                                    .then_some(obj.get_int_prop(ids, ids.obj_center_x))
8052                            })
8053                            .unwrap_or(0);
8054                        let cy = center_y
8055                            .or_else(|| {
8056                                (ids.obj_center_y != 0)
8057                                    .then_some(obj.get_int_prop(ids, ids.obj_center_y))
8058                            })
8059                            .unwrap_or(0);
8060                        let _ = gfx.object_set_center(images, layers, stage_i64, obj_i64, cx, cy);
8061                    }
8062                    if scale_x.is_some() || scale_y.is_some() {
8063                        let sx = scale_x
8064                            .or_else(|| {
8065                                (ids.obj_scale_x != 0)
8066                                    .then_some(obj.get_int_prop(ids, ids.obj_scale_x))
8067                            })
8068                            .unwrap_or(1000);
8069                        let sy = scale_y
8070                            .or_else(|| {
8071                                (ids.obj_scale_y != 0)
8072                                    .then_some(obj.get_int_prop(ids, ids.obj_scale_y))
8073                            })
8074                            .unwrap_or(1000);
8075                        let _ = gfx.object_set_scale(images, layers, stage_i64, obj_i64, sx, sy);
8076                    }
8077                    if let Some(rz) = rotate_z {
8078                        let _ = gfx.object_set_rotate(images, layers, stage_i64, obj_i64, rz);
8079                    }
8080                    if clip_left.is_some()
8081                        || clip_top.is_some()
8082                        || clip_right.is_some()
8083                        || clip_bottom.is_some()
8084                    {
8085                        let use_flag = if ids.obj_clip_use != 0 {
8086                            obj.get_int_prop(ids, ids.obj_clip_use)
8087                        } else {
8088                            0
8089                        };
8090                        let left = clip_left
8091                            .or_else(|| {
8092                                (ids.obj_clip_left != 0)
8093                                    .then_some(obj.get_int_prop(ids, ids.obj_clip_left))
8094                            })
8095                            .unwrap_or(0);
8096                        let top = clip_top
8097                            .or_else(|| {
8098                                (ids.obj_clip_top != 0)
8099                                    .then_some(obj.get_int_prop(ids, ids.obj_clip_top))
8100                            })
8101                            .unwrap_or(0);
8102                        let right = clip_right
8103                            .or_else(|| {
8104                                (ids.obj_clip_right != 0)
8105                                    .then_some(obj.get_int_prop(ids, ids.obj_clip_right))
8106                            })
8107                            .unwrap_or(0);
8108                        let bottom = clip_bottom
8109                            .or_else(|| {
8110                                (ids.obj_clip_bottom != 0)
8111                                    .then_some(obj.get_int_prop(ids, ids.obj_clip_bottom))
8112                            })
8113                            .unwrap_or(0);
8114                        let _ = gfx.object_set_clip(
8115                            images, layers, stage_i64, obj_i64, use_flag, left, top, right, bottom,
8116                        );
8117                    }
8118                    if src_clip_left.is_some()
8119                        || src_clip_top.is_some()
8120                        || src_clip_right.is_some()
8121                        || src_clip_bottom.is_some()
8122                    {
8123                        let use_flag = if ids.obj_src_clip_use != 0 {
8124                            obj.lookup_int_prop(ids, ids.obj_src_clip_use).unwrap_or(0)
8125                        } else {
8126                            0
8127                        };
8128                        let left = src_clip_left
8129                            .or_else(|| {
8130                                if ids.obj_src_clip_left != 0 {
8131                                    obj.lookup_int_prop(ids, ids.obj_src_clip_left)
8132                                } else {
8133                                    None
8134                                }
8135                            })
8136                            .unwrap_or(0);
8137                        let top = src_clip_top
8138                            .or_else(|| {
8139                                if ids.obj_src_clip_top != 0 {
8140                                    obj.lookup_int_prop(ids, ids.obj_src_clip_top)
8141                                } else {
8142                                    None
8143                                }
8144                            })
8145                            .unwrap_or(0);
8146                        let right = src_clip_right
8147                            .or_else(|| {
8148                                if ids.obj_src_clip_right != 0 {
8149                                    obj.lookup_int_prop(ids, ids.obj_src_clip_right)
8150                                } else {
8151                                    None
8152                                }
8153                            })
8154                            .unwrap_or(0);
8155                        let bottom = src_clip_bottom
8156                            .or_else(|| {
8157                                if ids.obj_src_clip_bottom != 0 {
8158                                    obj.lookup_int_prop(ids, ids.obj_src_clip_bottom)
8159                                } else {
8160                                    None
8161                                }
8162                            })
8163                            .unwrap_or(0);
8164                        let _ = gfx.object_set_src_clip(
8165                            images, layers, stage_i64, obj_i64, use_flag, left, top, right, bottom,
8166                        );
8167                    }
8168                    if let Some(v) = tr {
8169                        let _ = gfx.object_set_tr(images, layers, stage_i64, obj_i64, v);
8170                    }
8171                    if let Some(v) = mono {
8172                        let _ = gfx.object_set_mono(images, layers, stage_i64, obj_i64, v);
8173                    }
8174                    if let Some(v) = reverse {
8175                        let _ = gfx.object_set_reverse(images, layers, stage_i64, obj_i64, v);
8176                    }
8177                    if let Some(v) = bright {
8178                        let _ = gfx.object_set_bright(images, layers, stage_i64, obj_i64, v);
8179                    }
8180                    if let Some(v) = dark {
8181                        let _ = gfx.object_set_dark(images, layers, stage_i64, obj_i64, v);
8182                    }
8183                    if let Some(v) = color_rate {
8184                        let _ = gfx.object_set_color_rate(images, layers, stage_i64, obj_i64, v);
8185                    }
8186                    if color_add_r.is_some() || color_add_g.is_some() || color_add_b.is_some() {
8187                        let r = color_add_r.unwrap_or_else(|| {
8188                            if ids.obj_color_add_r != 0 {
8189                                obj.get_int_prop(ids, ids.obj_color_add_r)
8190                            } else {
8191                                0
8192                            }
8193                        });
8194                        let g = color_add_g.unwrap_or_else(|| {
8195                            if ids.obj_color_add_g != 0 {
8196                                obj.get_int_prop(ids, ids.obj_color_add_g)
8197                            } else {
8198                                0
8199                            }
8200                        });
8201                        let b = color_add_b.unwrap_or_else(|| {
8202                            if ids.obj_color_add_b != 0 {
8203                                obj.get_int_prop(ids, ids.obj_color_add_b)
8204                            } else {
8205                                0
8206                            }
8207                        });
8208                        let _ =
8209                            gfx.object_set_color_add(images, layers, stage_i64, obj_i64, r, g, b);
8210                    }
8211                    if color_r.is_some() || color_g.is_some() || color_b.is_some() {
8212                        let r = color_r.unwrap_or_else(|| {
8213                            if ids.obj_color_r != 0 {
8214                                obj.get_int_prop(ids, ids.obj_color_r)
8215                            } else {
8216                                0
8217                            }
8218                        });
8219                        let g = color_g.unwrap_or_else(|| {
8220                            if ids.obj_color_g != 0 {
8221                                obj.get_int_prop(ids, ids.obj_color_g)
8222                            } else {
8223                                0
8224                            }
8225                        });
8226                        let b = color_b.unwrap_or_else(|| {
8227                            if ids.obj_color_b != 0 {
8228                                obj.get_int_prop(ids, ids.obj_color_b)
8229                            } else {
8230                                0
8231                            }
8232                        });
8233                        let _ = gfx.object_set_color(images, layers, stage_i64, obj_i64, r, g, b);
8234                    }
8235                }
8236                globals::ObjectBackend::Rect {
8237                    layer_id,
8238                    sprite_id,
8239                    ..
8240                }
8241                | globals::ObjectBackend::String {
8242                    layer_id,
8243                    sprite_id,
8244                    ..
8245                } => {
8246                    if let Some(layer) = layers.layer_mut(*layer_id) {
8247                        if let Some(spr) = layer.sprite_mut(*sprite_id) {
8248                            if let Some(ax) = x {
8249                                spr.x = ax as i32;
8250                            }
8251                            if let Some(ay) = y {
8252                                spr.y = ay as i32;
8253                            }
8254                            if let Some(v) = alpha {
8255                                spr.alpha = v.clamp(0, 255) as u8;
8256                            }
8257                            if let Some(v) = order {
8258                                spr.order = v as i32;
8259                            }
8260                            if let Some(v) = tr {
8261                                spr.tr = v.clamp(0, 255) as u8;
8262                            }
8263                        }
8264                    }
8265                }
8266                globals::ObjectBackend::Number { .. }
8267                | globals::ObjectBackend::Weather { .. }
8268                | globals::ObjectBackend::Movie { .. }
8269                | globals::ObjectBackend::None => {}
8270            }
8271        }
8272    }
8273
8274    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
8275        apply_object_event_animations_recursive(
8276            ids,
8277            gfx,
8278            images,
8279            layers,
8280            stage_i64,
8281            object_runtime_slot(child_idx, child) as i64,
8282            child,
8283        );
8284    }
8285}
8286
8287const WEATHER_APPEAR_MS: i64 = 1000;
8288const WEATHER_DISAPPEAR_MS: i64 = 1000;
8289const WEATHER_ANGLE_FULL: f64 = 3600.0;
8290
8291fn weather_alpha_for_state(state: i64, cur: i64, len: i64) -> u8 {
8292    match state {
8293        1 => ((cur.clamp(0, WEATHER_APPEAR_MS) * 255) / WEATHER_APPEAR_MS).clamp(0, 255) as u8,
8294        2 => 255,
8295        3 => {
8296            let len = if len <= 0 { WEATHER_DISAPPEAR_MS } else { len };
8297            ((len.saturating_sub(cur).clamp(0, len) * 255) / len).clamp(0, 255) as u8
8298        }
8299        _ => 0,
8300    }
8301}
8302
8303fn weather_wave(time: i64, period: i64, power: i64) -> i64 {
8304    if period == 0 || power == 0 {
8305        return 0;
8306    }
8307    let rad = (time as f64 / period.abs() as f64) * std::f64::consts::TAU;
8308    (rad.sin() * power as f64).round() as i64
8309}
8310
8311fn weather_pattern(obj: &mut globals::ObjectState, idx: usize) -> i64 {
8312    let p = obj.weather_param.clone();
8313    let first = p.pat_no_00.min(p.pat_no_01);
8314    let last = p.pat_no_00.max(p.pat_no_01);
8315    let span = (last - first + 1).max(1);
8316    match p.pat_mode {
8317        1 => {
8318            let pat_time = p.pat_time.max(1);
8319            let t = obj
8320                .weather_work
8321                .sub
8322                .get(idx)
8323                .map(|s| s.move_cur_time.max(0))
8324                .unwrap_or(0);
8325            first + ((t / pat_time) % span)
8326        }
8327        2 => first + obj.weather_work.rand_mod(span),
8328        _ => p.pat_no_00,
8329    }
8330}
8331
8332fn ensure_weather_sprites(
8333    layers: &mut LayerManager,
8334    obj: &mut globals::ObjectState,
8335) -> Option<(LayerId, Vec<SpriteId>)> {
8336    let required = obj.weather_sprite_count();
8337    let (layer_id, sprite_ids) = match &mut obj.backend {
8338        globals::ObjectBackend::Weather {
8339            layer_id,
8340            sprite_ids,
8341        } => (*layer_id, sprite_ids),
8342        _ => return None,
8343    };
8344    if let Some(layer) = layers.layer_mut(layer_id) {
8345        while sprite_ids.len() < required {
8346            let sid = layer.create_sprite();
8347            if let Some(sprite) = layer.sprite_mut(sid) {
8348                sprite.fit = SpriteFit::PixelRect;
8349                sprite.size_mode = SpriteSizeMode::Intrinsic;
8350                sprite.visible = false;
8351                sprite.image_id = None;
8352            }
8353            sprite_ids.push(sid);
8354        }
8355    }
8356    Some((layer_id, sprite_ids.clone()))
8357}
8358
8359fn set_weather_sprite(
8360    ids: &constants::RuntimeConstants,
8361    layers: &mut LayerManager,
8362    images: &mut ImageManager,
8363    obj: &globals::ObjectState,
8364    layer_id: LayerId,
8365    sprite_id: SpriteId,
8366    image_id: Option<ImageId>,
8367    x: i64,
8368    y: i64,
8369    alpha: u8,
8370    scale_x: i64,
8371    scale_y: i64,
8372) {
8373    let Some(layer) = layers.layer_mut(layer_id) else {
8374        return;
8375    };
8376    let Some(sprite) = layer.sprite_mut(sprite_id) else {
8377        return;
8378    };
8379    sprite.image_id = image_id;
8380    sprite.visible = image_id.is_some() && obj.get_int_prop(ids, ids.obj_disp) != 0 && alpha > 0;
8381    sprite.fit = SpriteFit::PixelRect;
8382    sprite.size_mode = SpriteSizeMode::Intrinsic;
8383    sprite.x = obj
8384        .lookup_int_prop(ids, ids.obj_x)
8385        .unwrap_or(0)
8386        .saturating_add(x) as i32;
8387    sprite.y = obj
8388        .lookup_int_prop(ids, ids.obj_y)
8389        .unwrap_or(0)
8390        .saturating_add(y) as i32;
8391    sprite.alpha = if ids.obj_alpha != 0 {
8392        obj.lookup_int_prop(ids, ids.obj_alpha)
8393            .unwrap_or(obj.base.alpha)
8394    } else {
8395        obj.base.alpha
8396    }
8397    .clamp(0, 255) as u8;
8398    sprite.tr = ((obj
8399        .lookup_int_prop(ids, ids.obj_tr)
8400        .unwrap_or(255)
8401        .clamp(0, 255)
8402        * alpha as i64)
8403        / 255)
8404        .clamp(0, 255) as u8;
8405    sprite.order = obj.lookup_int_prop(ids, ids.obj_order).unwrap_or(0) as i32;
8406    sprite.scale_x = (scale_x as f32) / 1000.0;
8407    sprite.scale_y = (scale_y as f32) / 1000.0;
8408    sprite.blend =
8409        crate::layer::SpriteBlend::from_i64(obj.lookup_int_prop(ids, ids.obj_blend).unwrap_or(0));
8410    if let Some(img) = image_id.and_then(|id| images.get(id)) {
8411        if matches!(sprite.size_mode, SpriteSizeMode::Intrinsic) {
8412            let _ = (img.width, img.height);
8413        }
8414    }
8415}
8416
8417fn sync_weather_object_recursive(
8418    ids: &constants::RuntimeConstants,
8419    layers: &mut LayerManager,
8420    images: &mut ImageManager,
8421    screen_w: i64,
8422    screen_h: i64,
8423    game_delta_ms: i32,
8424    real_delta_ms: i32,
8425    obj: &mut globals::ObjectState,
8426) {
8427    if obj.used && obj.object_type == 4 && matches!(obj.weather_param.weather_type, 1 | 2) {
8428        obj.update_weather_time(game_delta_ms, real_delta_ms, screen_w, screen_h);
8429        let Some((layer_id, sprite_ids)) = ensure_weather_sprites(layers, obj) else {
8430            return;
8431        };
8432
8433        let file_name = obj.file_name.clone().unwrap_or_default();
8434        let cnt_max = obj.weather_work.cnt_max.min(obj.weather_work.sub.len());
8435        let mut used = 0usize;
8436        for idx in 0..cnt_max {
8437            let sub = obj.weather_work.sub[idx].clone();
8438            if sub.state == 0 {
8439                continue;
8440            }
8441            let pat_no = weather_pattern(obj, idx).max(0) as u32;
8442            let image_id = if file_name.is_empty() {
8443                None
8444            } else {
8445                images.load_g00(&file_name, pat_no).ok()
8446            };
8447            let alpha = weather_alpha_for_state(sub.state, sub.state_cur_time, sub.state_time_len);
8448
8449            if obj.weather_param.weather_type == 1 {
8450                let move_x = if sub.move_time_x == 0 {
8451                    0
8452                } else {
8453                    1000i64.saturating_mul(sub.move_cur_time) / sub.move_time_x
8454                };
8455                let move_y = if sub.move_time_y == 0 {
8456                    0
8457                } else {
8458                    1000i64.saturating_mul(sub.move_cur_time) / sub.move_time_y
8459                };
8460                let mut x = sub.move_start_pos_x
8461                    + move_x
8462                    + weather_wave(sub.sin_cur_time, sub.sin_time_x, sub.sin_power_x);
8463                let mut y = sub.move_start_pos_y
8464                    + move_y
8465                    + weather_wave(sub.sin_cur_time, sub.sin_time_y, sub.sin_power_y);
8466                x = ((x % screen_w) + screen_w) % screen_w;
8467                y = ((y % screen_h) + screen_h) % screen_h;
8468                let offsets = [
8469                    (0, 0),
8470                    (-screen_w, 0),
8471                    (0, -screen_h),
8472                    (-screen_w, -screen_h),
8473                ];
8474                for (ox, oy) in offsets {
8475                    if let Some(&sid) = sprite_ids.get(used) {
8476                        set_weather_sprite(
8477                            ids,
8478                            layers,
8479                            images,
8480                            obj,
8481                            layer_id,
8482                            sid,
8483                            image_id,
8484                            x + ox,
8485                            y + oy,
8486                            alpha,
8487                            sub.scale_x,
8488                            sub.scale_y,
8489                        );
8490                    }
8491                    used += 1;
8492                }
8493            } else {
8494                let mt = sub.move_time_x.max(1);
8495                let t = sub.move_cur_time.max(0);
8496                let distance = sub.move_start_distance.saturating_add(
8497                    1000i64.saturating_mul(t).saturating_mul(t) / mt.saturating_mul(mt),
8498                );
8499                let degree = sub.move_start_degree
8500                    + if sub.center_rotate == 0 {
8501                        0
8502                    } else {
8503                        sub.center_rotate.saturating_mul(t) / 1000
8504                    };
8505                let rad = degree as f64 / WEATHER_ANGLE_FULL * std::f64::consts::TAU;
8506                let wave_x = weather_wave(sub.sin_cur_time, sub.sin_time_x, sub.sin_power_x);
8507                let wave_y = weather_wave(sub.sin_cur_time, sub.sin_time_y, sub.sin_power_y);
8508                let x = obj.weather_param.center_x
8509                    + (rad.cos() * distance as f64).round() as i64
8510                    + wave_x;
8511                let y = obj.weather_param.center_y
8512                    + (rad.sin() * distance as f64).round() as i64
8513                    + wave_y;
8514                let zoom_span = sub.zoom_max.saturating_sub(sub.zoom_min);
8515                let zoom = if sub.active_time_len <= 0 {
8516                    sub.zoom_min
8517                } else {
8518                    sub.zoom_min
8519                        + zoom_span.saturating_mul(t.min(sub.active_time_len)) / sub.active_time_len
8520                };
8521                if let Some(&sid) = sprite_ids.get(used) {
8522                    set_weather_sprite(
8523                        ids,
8524                        layers,
8525                        images,
8526                        obj,
8527                        layer_id,
8528                        sid,
8529                        image_id,
8530                        x,
8531                        y,
8532                        alpha,
8533                        sub.scale_x.saturating_mul(zoom) / 1000,
8534                        sub.scale_y.saturating_mul(zoom) / 1000,
8535                    );
8536                }
8537                used += 1;
8538            }
8539        }
8540
8541        if let Some(layer) = layers.layer_mut(layer_id) {
8542            for sid in sprite_ids.into_iter().skip(used) {
8543                if let Some(sprite) = layer.sprite_mut(sid) {
8544                    sprite.visible = false;
8545                    sprite.image_id = None;
8546                }
8547            }
8548        }
8549    }
8550
8551    for child in &mut obj.runtime.child_objects {
8552        sync_weather_object_recursive(
8553            ids,
8554            layers,
8555            images,
8556            screen_w,
8557            screen_h,
8558            game_delta_ms,
8559            real_delta_ms,
8560            child,
8561        );
8562    }
8563}
8564
8565fn install_object_movie_preview_if_missing(
8566    layers: &mut LayerManager,
8567    movie_mgr: &mut MovieManager,
8568    images: &mut ImageManager,
8569    obj: &mut globals::ObjectState,
8570    stage_idx: i64,
8571    obj_idx: i64,
8572    file: &str,
8573    trace: bool,
8574) {
8575    let globals::ObjectBackend::Movie {
8576        layer_id,
8577        sprite_id,
8578        image_id,
8579        width,
8580        height,
8581    } = &mut obj.backend
8582    else {
8583        return;
8584    };
8585
8586    if image_id.is_some() {
8587        return;
8588    }
8589
8590    match movie_mgr.ensure_omv_preview_frame(file) {
8591        Ok(frame) => {
8592            let img_id = images.insert_image_arc(frame.clone());
8593            *image_id = Some(img_id);
8594            *width = frame.width;
8595            *height = frame.height;
8596            obj.movie.frame_image_ids[0] = Some(img_id);
8597            obj.movie.frame_image_cursor = 0;
8598            if let Some(layer) = layers.layer_mut(*layer_id) {
8599                if let Some(sprite) = layer.sprite_mut(*sprite_id) {
8600                    sprite.image_id = Some(img_id);
8601                    sprite.object_anchor = true;
8602                    sprite.texture_center_x = 0.0;
8603                    sprite.texture_center_y = 0.0;
8604                }
8605            }
8606            if trace || sg_debug_enabled() {
8607                eprintln!(
8608                    "[SG_DEBUG][MOV] object_movie.preview_installed stage={} obj={} file={} image={:?} size={}x{}",
8609                    stage_idx, obj_idx, file, img_id, frame.width, frame.height
8610                );
8611            }
8612        }
8613        Err(err) => {
8614            if trace || sg_debug_enabled() {
8615                eprintln!(
8616                    "[SG_DEBUG][MOV] object_movie.preview_failed stage={} obj={} file={} err={:#}",
8617                    stage_idx, obj_idx, file, err
8618                );
8619            }
8620        }
8621    }
8622}
8623
8624fn install_object_movie_stream_frame(
8625    layers: &mut LayerManager,
8626    images: &mut ImageManager,
8627    obj: &mut globals::ObjectState,
8628    stage_idx: i64,
8629    obj_idx: i64,
8630    file: &str,
8631    frame_idx: usize,
8632    frame: std::sync::Arc<crate::assets::RgbaImage>,
8633    trace: bool,
8634) {
8635    let globals::ObjectBackend::Movie {
8636        layer_id,
8637        sprite_id,
8638        image_id,
8639        width,
8640        height,
8641    } = &mut obj.backend
8642    else {
8643        return;
8644    };
8645
8646    let next_cursor = obj.movie.frame_image_cursor ^ 1;
8647    let img_id = if let Some(id) = obj.movie.frame_image_ids[next_cursor] {
8648        let _ = images.replace_image_arc(id, frame.clone());
8649        id
8650    } else {
8651        let id = images.insert_image_arc(frame.clone());
8652        obj.movie.frame_image_ids[next_cursor] = Some(id);
8653        id
8654    };
8655    obj.movie.frame_image_cursor = next_cursor;
8656
8657    *image_id = Some(img_id);
8658    *width = frame.width;
8659    *height = frame.height;
8660    if let Some(layer) = layers.layer_mut(*layer_id) {
8661        if let Some(sprite) = layer.sprite_mut(*sprite_id) {
8662            sprite.image_id = Some(img_id);
8663            sprite.object_anchor = true;
8664            sprite.texture_center_x = 0.0;
8665            sprite.texture_center_y = 0.0;
8666        }
8667    }
8668    if trace || sg_debug_enabled() {
8669        eprintln!(
8670            "[SG_DEBUG][MOV] object_movie.frame stage={} obj={} file={} frame={} image={:?} size={}x{} timer_ms={}",
8671            stage_idx, obj_idx, file, frame_idx, img_id, frame.width, frame.height, obj.movie.timer_ms
8672        );
8673    }
8674}
8675
8676fn sync_movie_object_recursive(
8677    ids: &constants::RuntimeConstants,
8678    layers: &mut LayerManager,
8679    movie_mgr: &mut MovieManager,
8680    audio: &mut AudioHub,
8681    gfx: &mut graphics::GfxRuntime,
8682    images: &mut ImageManager,
8683    stage_idx: i64,
8684    obj_idx: i64,
8685    obj: &mut globals::ObjectState,
8686    decoded_any: &mut bool,
8687) {
8688    let trace = std::env::var_os("SG_MOVIE_TRACE").is_some();
8689    if obj.used && obj.object_type == 9 {
8690        if let Some(file_name) = obj.file_name.clone() {
8691            if trace {
8692                eprintln!("[SG_MOVIE_TRACE] enter stage={} obj={} file={} playing={} pause={} backend={:?} children={}", stage_idx, obj_idx, file_name, obj.movie.playing, obj.movie.pause_flag, obj.backend, obj.runtime.child_objects.len());
8693            }
8694            let file = file_name.as_str();
8695            if obj.movie.just_finished {
8696                if let Some(id) = obj.movie.audio_id.take() {
8697                    movie_mgr.stop_audio(id);
8698                }
8699                obj.movie.just_finished = false;
8700                if obj.movie.auto_free_flag {
8701                    // Original OMV objects are freed after the player has actually
8702                    // reached EOS.  Keep the object alive if no decoded frame was ever
8703                    // installed; otherwise metadata/timing mismatches can erase a movie
8704                    // object immediately after CREATE_MOVIE.
8705                    if obj.movie.last_frame_idx.is_some() {
8706                        if let globals::ObjectBackend::Movie {
8707                            layer_id,
8708                            sprite_id,
8709                            ..
8710                        } = obj.backend
8711                        {
8712                            if let Some(layer) = layers.layer_mut(layer_id) {
8713                                if let Some(sprite) = layer.sprite_mut(sprite_id) {
8714                                    sprite.visible = false;
8715                                    sprite.image_id = None;
8716                                }
8717                            }
8718                        }
8719                        obj.init_type_like();
8720                    } else {
8721                        obj.movie.playing = true;
8722                    }
8723                }
8724            } else if !obj.movie.playing {
8725                if let Some(id) = obj.movie.audio_id.take() {
8726                    movie_mgr.stop_audio(id);
8727                }
8728            }
8729
8730            if obj.object_type == 9 {
8731                let (layer_id, sprite_id) = if let globals::ObjectBackend::Movie {
8732                    layer_id,
8733                    sprite_id,
8734                    ..
8735                } = &obj.backend
8736                {
8737                    (*layer_id, *sprite_id)
8738                } else {
8739                    let Some(layer_id) = gfx.ensure_stage_layer_id(layers, stage_idx) else {
8740                        return;
8741                    };
8742                    let Some(layer) = layers.layer_mut(layer_id) else {
8743                        return;
8744                    };
8745                    let sid = layer.create_sprite();
8746                    if let Some(sprite) = layer.sprite_mut(sid) {
8747                        sprite.visible = true;
8748                        sprite.alpha = 255;
8749                        sprite.fit = SpriteFit::PixelRect;
8750                        sprite.size_mode = SpriteSizeMode::Intrinsic;
8751                        sprite.object_anchor = true;
8752                        sprite.texture_center_x = 0.0;
8753                        sprite.texture_center_y = 0.0;
8754                        sprite.x = 0;
8755                        sprite.y = 0;
8756                        sprite.order = 0;
8757                    }
8758                    obj.backend = globals::ObjectBackend::Movie {
8759                        layer_id,
8760                        sprite_id: sid,
8761                        image_id: None,
8762                        width: 0,
8763                        height: 0,
8764                    };
8765                    (layer_id, sid)
8766                };
8767
8768                if let Some(layer) = layers.layer_mut(layer_id) {
8769                    if let Some(sprite) = layer.sprite_mut(sprite_id) {
8770                        let render_info =
8771                            button_object_render_info(ids, gfx, stage_idx, obj_idx as usize, obj);
8772                        apply_button_object_render_info_to_sprite(sprite, &render_info);
8773                        finalize_button_object_center_rep_to_sprite(sprite, &render_info);
8774                        if ids.obj_alpha != 0 {
8775                            sprite.alpha = obj
8776                                .lookup_int_prop(ids, ids.obj_alpha)
8777                                .unwrap_or(255)
8778                                .clamp(0, 255) as u8;
8779                        }
8780                        if ids.obj_order != 0 {
8781                            sprite.order =
8782                                obj.lookup_int_prop(ids, ids.obj_order).unwrap_or(0) as i32;
8783                        }
8784                        sprite.blend = crate::layer::SpriteBlend::from_i64(
8785                            obj.lookup_int_prop(ids, ids.obj_blend).unwrap_or(0),
8786                        );
8787                        // C++ OBJECT movie uses movie_frame() and trp_to_rp(), so OBJECT.CENTER
8788                        // must shift the OMV quad just like a texture object. The dynamic OMV
8789                        // texture itself keeps the C_d3d_texture default center of (0, 0).
8790                        sprite.object_anchor = true;
8791                        sprite.texture_center_x = 0.0;
8792                        sprite.texture_center_y = 0.0;
8793                    }
8794                }
8795
8796                // Object movie sprites need a texture immediately after CREATE_MOVIE.
8797                // The streaming decoder can return None while its worker is warming up;
8798                // without this preview surface the object stays as image_id=None/0x0 and
8799                // is filtered out by render submission. The stream path below replaces it.
8800                install_object_movie_preview_if_missing(
8801                    layers, movie_mgr, images, obj, stage_idx, obj_idx, file, trace,
8802                );
8803
8804                if obj.movie.seeked || obj.movie.just_looped {
8805                    if let Some(id) = obj.movie.audio_id.take() {
8806                        movie_mgr.stop_audio(id);
8807                    }
8808                }
8809                obj.movie.seeked = false;
8810                obj.movie.just_looped = false;
8811
8812                if obj.movie.pause_flag {
8813                    if let Some(id) = obj.movie.audio_id {
8814                        movie_mgr.pause_audio(id);
8815                    }
8816                } else if obj.movie.playing {
8817                    if let Some(id) = obj.movie.audio_id {
8818                        movie_mgr.resume_audio(id);
8819                    }
8820                }
8821
8822                if obj.movie.pause_flag {
8823                    if let globals::ObjectBackend::Movie {
8824                        layer_id,
8825                        sprite_id,
8826                        image_id,
8827                        width,
8828                        height,
8829                    } = &mut obj.backend
8830                    {
8831                        if image_id.is_none() {
8832                            match movie_mgr.ensure_preview_frame(file) {
8833                                Ok(frame) => {
8834                                    let img_id = images.insert_image_arc(frame.clone());
8835                                    *image_id = Some(img_id);
8836                                    *width = frame.width;
8837                                    *height = frame.height;
8838                                    if let Some(layer) = layers.layer_mut(*layer_id) {
8839                                        if let Some(sprite) = layer.sprite_mut(*sprite_id) {
8840                                            sprite.image_id = Some(img_id);
8841                                            sprite.object_anchor = true;
8842                                            sprite.texture_center_x = 0.0;
8843                                            sprite.texture_center_y = 0.0;
8844                                        }
8845                                    }
8846                                    if trace {
8847                                        eprintln!(
8848                                            "[SG_MOVIE_TRACE] installed paused preview stage={} obj={} file={} size={}x{}",
8849                                            stage_idx,
8850                                            obj_idx,
8851                                            file,
8852                                            frame.width,
8853                                            frame.height,
8854                                        );
8855                                    }
8856                                }
8857                                Err(err) => {
8858                                    if trace {
8859                                        eprintln!(
8860                                            "[SG_MOVIE_TRACE] paused preview decode failed stage={} obj={} file={} err={:#}",
8861                                            stage_idx,
8862                                            obj_idx,
8863                                            file,
8864                                            err,
8865                                        );
8866                                    }
8867                                }
8868                            }
8869                        }
8870                    }
8871                    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
8872                        sync_movie_object_recursive(
8873                            ids,
8874                            layers,
8875                            movie_mgr,
8876                            audio,
8877                            gfx,
8878                            images,
8879                            stage_idx,
8880                            object_runtime_slot(child_idx, child) as i64,
8881                            child,
8882                            decoded_any,
8883                        );
8884                    }
8885                    return;
8886                }
8887
8888                if trace {
8889                    eprintln!(
8890                        "[SG_MOVIE_TRACE] poll_stream stage={} obj={} file={}",
8891                        stage_idx, obj_idx, file
8892                    );
8893                }
8894                if let Some(id) = obj.movie.audio_id {
8895                    if movie_mgr.audio_playback_finished(id) {
8896                        obj.movie.audio_id = None;
8897                    }
8898                }
8899                let polled = match movie_mgr.poll_global_movie_frame_with_loop(
8900                    file,
8901                    obj.movie.timer_ms,
8902                    obj.movie.loop_flag,
8903                ) {
8904                    Ok(Some(frame)) => frame,
8905                    Ok(None) => {
8906                        if obj.movie.last_frame_idx.is_none() {
8907                            obj.movie.timer_ms = 0;
8908                            obj.movie.last_tick = Some(crate::platform_time::Instant::now());
8909                        }
8910                        for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
8911                            sync_movie_object_recursive(
8912                                ids,
8913                                layers,
8914                                movie_mgr,
8915                                audio,
8916                                gfx,
8917                                images,
8918                                stage_idx,
8919                                object_runtime_slot(child_idx, child) as i64,
8920                                child,
8921                                decoded_any,
8922                            );
8923                        }
8924                        return;
8925                    }
8926                    Err(err) => {
8927                        eprintln!(
8928                            "[SG_MOVIE] object movie error stage={} obj={} file={}: {:#}",
8929                            stage_idx, obj_idx, file, err
8930                        );
8931                        obj.movie.playing = false;
8932                        for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
8933                            sync_movie_object_recursive(
8934                                ids,
8935                                layers,
8936                                movie_mgr,
8937                                audio,
8938                                gfx,
8939                                images,
8940                                stage_idx,
8941                                object_runtime_slot(child_idx, child) as i64,
8942                                child,
8943                                decoded_any,
8944                            );
8945                        }
8946                        return;
8947                    }
8948                };
8949                if obj.movie.total_ms.is_none() || polled.total_ms.is_some() {
8950                    obj.movie.total_ms = polled.total_ms.or(obj.movie.total_ms);
8951                }
8952                let frame_idx = polled.frame_idx;
8953                if obj.movie.last_frame_idx != Some(frame_idx) {
8954                    obj.movie.last_frame_idx = Some(frame_idx);
8955                    let frame = polled.frame.clone();
8956                    install_object_movie_stream_frame(
8957                        layers, images, obj, stage_idx, obj_idx, file, frame_idx, frame, trace,
8958                    );
8959                }
8960                let waiting_for_movie_audio_start =
8961                    obj.movie.audio_id.is_none() && polled.audio.is_none() && !polled.audio_ready;
8962                if obj.movie.playing && obj.movie.audio_id.is_none() {
8963                    if let Some(track) = polled.audio.as_ref() {
8964                        if let Ok(id) = movie_mgr.start_audio(audio, track, obj.movie.timer_ms) {
8965                            obj.movie.audio_id = Some(id);
8966                            obj.movie.audio_started_once = true;
8967                        }
8968                    }
8969                }
8970                if waiting_for_movie_audio_start
8971                    && obj.movie.audio_id.is_none()
8972                    && !obj.movie.audio_started_once
8973                {
8974                    obj.movie.timer_ms = 0;
8975                    obj.movie.last_tick = Some(crate::platform_time::Instant::now());
8976                }
8977            }
8978        }
8979    }
8980
8981    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
8982        sync_movie_object_recursive(
8983            ids,
8984            layers,
8985            movie_mgr,
8986            audio,
8987            gfx,
8988            images,
8989            stage_idx,
8990            object_runtime_slot(child_idx, child) as i64,
8991            child,
8992            decoded_any,
8993        );
8994    }
8995}
8996
8997fn apply_object_masks_recursive(
8998    ids: &constants::RuntimeConstants,
8999    gfx: &mut graphics::GfxRuntime,
9000    images: &mut ImageManager,
9001    layers: &mut LayerManager,
9002    stage_i64: i64,
9003    obj_i64: i64,
9004    obj: &mut globals::ObjectState,
9005    mask_info: &[Option<(String, i32, i32)>],
9006    resolved_masks: &HashMap<String, ImageId>,
9007) {
9008    let mask_no = if ids.obj_mask_no != 0 {
9009        obj.lookup_int_prop(ids, ids.obj_mask_no).unwrap_or(-1)
9010    } else {
9011        -1
9012    };
9013    if mask_no >= 0 {
9014        let mask_idx = mask_no as usize;
9015        if let Some(Some((mask_name, mask_x, mask_y))) = mask_info.get(mask_idx) {
9016            if let Some(mask_image_id) = resolved_masks.get(mask_name).copied() {
9017                let targets: Vec<(LayerId, SpriteId)> = match &obj.backend {
9018                    globals::ObjectBackend::Rect {
9019                        layer_id,
9020                        sprite_id,
9021                        ..
9022                    }
9023                    | globals::ObjectBackend::String {
9024                        layer_id,
9025                        sprite_id,
9026                        ..
9027                    }
9028                    | globals::ObjectBackend::Movie {
9029                        layer_id,
9030                        sprite_id,
9031                        ..
9032                    } => vec![(*layer_id, *sprite_id)],
9033                    globals::ObjectBackend::Number {
9034                        layer_id,
9035                        sprite_ids,
9036                    }
9037                    | globals::ObjectBackend::Weather {
9038                        layer_id,
9039                        sprite_ids,
9040                    } => sprite_ids.iter().map(|sid| (*layer_id, *sid)).collect(),
9041                    globals::ObjectBackend::Gfx => gfx
9042                        .object_sprite_binding(stage_i64, obj_i64)
9043                        .into_iter()
9044                        .collect(),
9045                    _ => Vec::new(),
9046                };
9047                for (layer_id, sprite_id) in targets {
9048                    let Some(sprite) = layers
9049                        .layer_mut(layer_id)
9050                        .and_then(|l| l.sprite_mut(sprite_id))
9051                    else {
9052                        continue;
9053                    };
9054                    let Some(base_id) = sprite.image_id else {
9055                        continue;
9056                    };
9057                    let (base_img, base_ver) = match images.get_entry(base_id) {
9058                        Some(v) => v,
9059                        None => continue,
9060                    };
9061                    let (mask_img, mask_ver) = match images.get_entry(mask_image_id) {
9062                        Some(v) => v,
9063                        None => continue,
9064                    };
9065                    let key = (layer_id, sprite_id);
9066                    if let Some(cache) = obj.mask_cache.get(&key) {
9067                        if cache.base_image_id == base_id
9068                            && cache.base_version == base_ver
9069                            && cache.mask_image_id == mask_image_id
9070                            && cache.mask_version == mask_ver
9071                            && cache.mask_x == *mask_x
9072                            && cache.mask_y == *mask_y
9073                        {
9074                            sprite.image_id = Some(cache.masked_image_id);
9075                            continue;
9076                        }
9077                    }
9078                    let masked = apply_mask_image(base_img, mask_img, *mask_x, *mask_y);
9079                    let masked_id = if let Some(cache) = obj.mask_cache.get(&key) {
9080                        let id = cache.masked_image_id;
9081                        let _ = images.replace_image(id, masked);
9082                        id
9083                    } else {
9084                        images.insert_image(masked)
9085                    };
9086                    obj.mask_cache.insert(
9087                        key,
9088                        globals::MaskedSpriteCache {
9089                            base_image_id: base_id,
9090                            base_version: base_ver,
9091                            mask_image_id,
9092                            mask_version: mask_ver,
9093                            mask_x: *mask_x,
9094                            mask_y: *mask_y,
9095                            masked_image_id: masked_id,
9096                        },
9097                    );
9098                    sprite.image_id = Some(masked_id);
9099                }
9100            }
9101        }
9102    }
9103
9104    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
9105        apply_object_masks_recursive(
9106            ids,
9107            gfx,
9108            images,
9109            layers,
9110            stage_i64,
9111            object_runtime_slot(child_idx, child) as i64,
9112            child,
9113            mask_info,
9114            resolved_masks,
9115        );
9116    }
9117}
9118
9119fn apply_object_tonecurves_recursive(
9120    ids: &constants::RuntimeConstants,
9121    gfx: &mut graphics::GfxRuntime,
9122    images: &mut ImageManager,
9123    layers: &mut LayerManager,
9124    tonecurve: &mut tonecurve::ToneCurveRuntime,
9125    stage_i64: i64,
9126    obj_i64: i64,
9127    obj: &mut globals::ObjectState,
9128) {
9129    let tonecurve_no = if ids.obj_tonecurve_no != 0 {
9130        obj.lookup_int_prop(ids, ids.obj_tonecurve_no).unwrap_or(-1)
9131    } else {
9132        -1
9133    };
9134    if tonecurve_no >= 0 {
9135        if let Some((tonecurve_image_id, tonecurve_row, tonecurve_sat)) =
9136            tonecurve.shader_binding(images, tonecurve_no as i32)
9137        {
9138            let targets: Vec<(LayerId, SpriteId)> = match &obj.backend {
9139                globals::ObjectBackend::Rect {
9140                    layer_id,
9141                    sprite_id,
9142                    ..
9143                }
9144                | globals::ObjectBackend::String {
9145                    layer_id,
9146                    sprite_id,
9147                    ..
9148                }
9149                | globals::ObjectBackend::Movie {
9150                    layer_id,
9151                    sprite_id,
9152                    ..
9153                } => vec![(*layer_id, *sprite_id)],
9154                globals::ObjectBackend::Number {
9155                    layer_id,
9156                    sprite_ids,
9157                }
9158                | globals::ObjectBackend::Weather {
9159                    layer_id,
9160                    sprite_ids,
9161                } => sprite_ids.iter().map(|sid| (*layer_id, *sid)).collect(),
9162                globals::ObjectBackend::Gfx => gfx
9163                    .object_sprite_binding(stage_i64, obj_i64)
9164                    .into_iter()
9165                    .collect(),
9166                _ => Vec::new(),
9167            };
9168            for (layer_id, sprite_id) in targets {
9169                if let Some(sprite) = layers
9170                    .layer_mut(layer_id)
9171                    .and_then(|l| l.sprite_mut(sprite_id))
9172                {
9173                    sprite.tonecurve_image_id = Some(tonecurve_image_id);
9174                    sprite.tonecurve_row = tonecurve_row;
9175                    sprite.tonecurve_sat = tonecurve_sat;
9176                }
9177            }
9178        }
9179    }
9180
9181    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
9182        apply_object_tonecurves_recursive(
9183            ids,
9184            gfx,
9185            images,
9186            layers,
9187            tonecurve,
9188            stage_i64,
9189            object_runtime_slot(child_idx, child) as i64,
9190            child,
9191        );
9192    }
9193}
9194
9195fn apply_gan_effects_recursive(
9196    gfx: &mut graphics::GfxRuntime,
9197    images: &mut ImageManager,
9198    sprites: &mut Vec<RenderSprite>,
9199    index: &HashMap<(Option<LayerId>, Option<SpriteId>), usize>,
9200    stage_i64: i64,
9201    obj_i64: i64,
9202    obj: &mut globals::ObjectState,
9203) {
9204    if let Some(pat) = obj.gan.current_pat() {
9205        if !(pat.pat_no == 0 && pat.x == 0 && pat.y == 0 && pat.tr == 255) {
9206            let key: Option<(LayerId, SpriteId)> = match &obj.backend {
9207                globals::ObjectBackend::Rect {
9208                    layer_id,
9209                    sprite_id,
9210                    ..
9211                }
9212                | globals::ObjectBackend::String {
9213                    layer_id,
9214                    sprite_id,
9215                    ..
9216                }
9217                | globals::ObjectBackend::Movie {
9218                    layer_id,
9219                    sprite_id,
9220                    ..
9221                } => Some((*layer_id, *sprite_id)),
9222                globals::ObjectBackend::Gfx => gfx.object_sprite_binding(stage_i64, obj_i64),
9223                _ => None,
9224            };
9225            if let Some((layer_id, sprite_id)) = key {
9226                if let Some(&idx) = index.get(&(Some(layer_id), Some(sprite_id))) {
9227                    let sprite = &mut sprites[idx].sprite;
9228                    if pat.x != 0 {
9229                        sprite.x = sprite.x.saturating_add(pat.x);
9230                    }
9231                    if pat.y != 0 {
9232                        sprite.y = sprite.y.saturating_add(pat.y);
9233                    }
9234                    if pat.tr != 255 {
9235                        let tr = (sprite.tr as i64 * pat.tr as i64 / 255).clamp(0, 255) as u8;
9236                        sprite.tr = tr;
9237                    }
9238                    if pat.pat_no != 0 {
9239                        if let Some(file) = gfx.object_peek_file(stage_i64, obj_i64) {
9240                            let base_pat = gfx.object_peek_patno(stage_i64, obj_i64).unwrap_or(0);
9241                            let pat_no = (base_pat + pat.pat_no as i64).max(0) as u32;
9242                            if let Ok(id) = images.load_g00(&file, pat_no) {
9243                                sprite.image_id = Some(id);
9244                            }
9245                        }
9246                    }
9247                }
9248            }
9249        }
9250    }
9251
9252    for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
9253        apply_gan_effects_recursive(
9254            gfx,
9255            images,
9256            sprites,
9257            index,
9258            stage_i64,
9259            object_runtime_slot(child_idx, child) as i64,
9260            child,
9261        );
9262    }
9263}
9264
9265fn build_parent_render_state(
9266    info: &ObjectRenderInfo,
9267    first_sprite: Option<&Sprite>,
9268) -> ParentRenderState {
9269    ParentRenderState {
9270        world_no: info.world_no,
9271        pos_x: (info.x + info.x_rep) as f32,
9272        pos_y: (info.y + info.y_rep) as f32,
9273        pos_z: (info.z + info.z_rep) as f32,
9274        center_rep_x: info.center_rep_x as f32,
9275        center_rep_y: info.center_rep_y as f32,
9276        center_rep_z: info.center_rep_z as f32,
9277        scale_x: info.scale_x as f32 / 1000.0,
9278        scale_y: info.scale_y as f32 / 1000.0,
9279        scale_z: info.scale_z as f32 / 1000.0,
9280        rotate_x: info.rotate_x as f32 * std::f32::consts::PI / 1800.0,
9281        rotate_y: info.rotate_y as f32 * std::f32::consts::PI / 1800.0,
9282        rotate_z: info.rotate_z as f32 * std::f32::consts::PI / 1800.0,
9283        tr: ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255) as i32,
9284        mono: info.mono.clamp(0, 255) as i32,
9285        reverse: info.reverse.clamp(0, 255) as i32,
9286        bright: info.bright.clamp(0, 255) as i32,
9287        dark: info.dark.clamp(0, 255) as i32,
9288        color_rate: info.color_rate.clamp(0, 255) as i32,
9289        color_r: info.color_r.clamp(0, 255) as i32,
9290        color_g: info.color_g.clamp(0, 255) as i32,
9291        color_b: info.color_b.clamp(0, 255) as i32,
9292        color_add_r: info.color_add_r.clamp(0, 255) as i32,
9293        color_add_g: info.color_add_g.clamp(0, 255) as i32,
9294        color_add_b: info.color_add_b.clamp(0, 255) as i32,
9295        blend: info.blend,
9296        dst_clip: info.dst_clip,
9297        mask_image_id: first_sprite.and_then(|s| s.mask_image_id),
9298        mask_offset_x: first_sprite.map(|s| s.mask_offset_x).unwrap_or(0),
9299        mask_offset_y: first_sprite.map(|s| s.mask_offset_y).unwrap_or(0),
9300        tonecurve_image_id: first_sprite.and_then(|s| s.tonecurve_image_id),
9301        tonecurve_row: first_sprite.map(|s| s.tonecurve_row).unwrap_or(0.0),
9302        tonecurve_sat: first_sprite.map(|s| s.tonecurve_sat).unwrap_or(0.0),
9303    }
9304}
9305
9306fn apply_parent_render_state_to_sprite(
9307    sprite: &mut Sprite,
9308    _info: &ObjectRenderInfo,
9309    state: &ParentRenderState,
9310) {
9311    let local_x = sprite.x as f32;
9312    let local_y = sprite.y as f32;
9313    let local_z = sprite.z;
9314
9315    let mut rel_x = local_x - state.center_rep_x;
9316    let mut rel_y = local_y - state.center_rep_y;
9317    rel_x *= state.scale_x;
9318    rel_y *= state.scale_y;
9319    let (sin_z, cos_z) = state.rotate_z.sin_cos();
9320    let rot_x = rel_x * cos_z - rel_y * sin_z;
9321    let rot_y = rel_x * sin_z + rel_y * cos_z;
9322
9323    sprite.x = (state.pos_x + state.center_rep_x + rot_x).round() as i32;
9324    sprite.y = (state.pos_y + state.center_rep_y + rot_y).round() as i32;
9325    sprite.z = state.pos_z + state.center_rep_z + local_z * state.scale_z;
9326    sprite.pivot_x += state.center_rep_x;
9327    sprite.pivot_y += state.center_rep_y;
9328    sprite.pivot_z += state.center_rep_z;
9329
9330    sprite.scale_x *= state.scale_x;
9331    sprite.scale_y *= state.scale_y;
9332    sprite.scale_z *= state.scale_z;
9333    sprite.rotate_x += state.rotate_x;
9334    sprite.rotate_y += state.rotate_y;
9335    sprite.rotate += state.rotate_z;
9336
9337    sprite.tr = ((sprite.tr as i32 * state.tr.clamp(0, 255)) / 255).clamp(0, 255) as u8;
9338    sprite.mono = combine_lerp(sprite.mono, state.mono);
9339    sprite.reverse = combine_lerp(sprite.reverse, state.reverse);
9340    sprite.bright = combine_lerp(sprite.bright, state.bright);
9341    sprite.dark = combine_lerp(sprite.dark, state.dark);
9342    if (sprite.color_rate as i32) + state.color_rate > 0 {
9343        let parent_rate = (state.color_rate * 255 * 255)
9344            / (255 * 255 - (255 - sprite.color_rate as i32) * (255 - state.color_rate)).max(1);
9345        sprite.color_r = blend_color(sprite.color_r, state.color_r, parent_rate);
9346        sprite.color_g = blend_color(sprite.color_g, state.color_g, parent_rate);
9347        sprite.color_b = blend_color(sprite.color_b, state.color_b, parent_rate);
9348        sprite.color_rate = combine_lerp(sprite.color_rate, state.color_rate);
9349    }
9350    sprite.color_add_r = sprite
9351        .color_add_r
9352        .saturating_add(state.color_add_r.clamp(0, 255) as u8);
9353    sprite.color_add_g = sprite
9354        .color_add_g
9355        .saturating_add(state.color_add_g.clamp(0, 255) as u8);
9356    sprite.color_add_b = sprite
9357        .color_add_b
9358        .saturating_add(state.color_add_b.clamp(0, 255) as u8);
9359    sprite.blend = state.blend;
9360    let child_clip = sprite.dst_clip;
9361    sprite.dst_clip = compose_clip_rect(state.dst_clip, child_clip, state);
9362    if state.dst_clip.is_some() && child_clip.is_some() && sprite.dst_clip.is_none() {
9363        sprite.tr = 0;
9364    }
9365    if sprite.mask_image_id.is_none() {
9366        sprite.mask_image_id = state.mask_image_id;
9367        sprite.mask_offset_x = state.mask_offset_x;
9368        sprite.mask_offset_y = state.mask_offset_y;
9369    }
9370    if sprite.tonecurve_image_id.is_none() {
9371        sprite.tonecurve_image_id = state.tonecurve_image_id;
9372        sprite.tonecurve_row = state.tonecurve_row;
9373        sprite.tonecurve_sat = state.tonecurve_sat;
9374    }
9375
9376    if state.world_no >= 0 {
9377        sprite.world_no = state.world_no as i32;
9378    }
9379}
9380
9381fn apply_world_camera_mode(
9382    sprite: &mut Sprite,
9383    worlds: Option<&Vec<globals::WorldState>>,
9384    screen_w: u32,
9385    screen_h: u32,
9386) {
9387    if sprite.world_no < 0 {
9388        return;
9389    }
9390    let Some(worlds) = worlds else {
9391        return;
9392    };
9393    let Some(world) = worlds.get(sprite.world_no as usize) else {
9394        return;
9395    };
9396
9397    let cam_eye = [
9398        world.camera_eye_x.get_total_value() as f32,
9399        world.camera_eye_y.get_total_value() as f32,
9400        world.camera_eye_z.get_total_value() as f32,
9401    ];
9402    let cam_target = [
9403        world.camera_pint_x.get_total_value() as f32,
9404        world.camera_pint_y.get_total_value() as f32,
9405        world.camera_pint_z.get_total_value() as f32,
9406    ];
9407    let cam_up = [
9408        world.camera_up_x.get_total_value() as f32,
9409        world.camera_up_y.get_total_value() as f32,
9410        world.camera_up_z.get_total_value() as f32,
9411    ];
9412    sprite.camera_view_angle_deg = (world.camera_view_angle as f32) / 10.0;
9413    if world.mono != 0 {
9414        let base = sprite.mono as i32;
9415        let parent = world.mono.clamp(0, 255);
9416        sprite.mono = (255 - (255 - base) * (255 - parent) / 255) as u8;
9417    }
9418
9419    if world.mode == 0 {
9420        let dz = sprite.z - cam_eye[2];
9421        if dz <= 0.0 {
9422            sprite.visible = false;
9423            return;
9424        }
9425        let camera_scale = 1000.0 / dz;
9426        let sw = screen_w as f32;
9427        let sh = screen_h as f32;
9428        sprite.x = (((sprite.x as f32) - cam_eye[0]) * camera_scale + sw * 0.5)
9429            .round()
9430            .clamp(i32::MIN as f32, i32::MAX as f32) as i32;
9431        sprite.y = (((sprite.y as f32) - cam_eye[1]) * camera_scale + sh * 0.5)
9432            .round()
9433            .clamp(i32::MIN as f32, i32::MAX as f32) as i32;
9434        sprite.scale_x *= camera_scale;
9435        sprite.scale_y *= camera_scale;
9436        sprite.z = 0.0;
9437        sprite.pivot_z = 0.0;
9438        sprite.scale_z = 1.0;
9439        sprite.rotate_x = 0.0;
9440        sprite.rotate_y = 0.0;
9441        sprite.billboard = false;
9442        sprite.culling = false;
9443        sprite.fog_use = false;
9444        sprite.light_no = -1;
9445        sprite.light_enabled = false;
9446        sprite.light_diffuse = [1.0, 1.0, 1.0, 1.0];
9447        sprite.light_ambient = [0.0, 0.0, 0.0, 1.0];
9448        sprite.light_specular = [0.0, 0.0, 0.0, 1.0];
9449        sprite.light_factor = 0.0;
9450        sprite.light_kind = -1;
9451        sprite.light_pos = [0.0, 0.0, 0.0, 0.0];
9452        sprite.light_dir = [0.0, 0.0, -1.0, 0.0];
9453        sprite.light_atten = [1.0, 0.0, 0.0, 5000.0];
9454        sprite.light_cone = [0.0, 0.0, 1.0, 0.0];
9455        sprite.fog_enabled = false;
9456        sprite.fog_color = [0.0, 0.0, 0.0, 1.0];
9457        sprite.fog_near = 0.0;
9458        sprite.fog_far = 0.0;
9459        sprite.fog_scroll_x = 0.0;
9460        sprite.fog_texture_image_id = None;
9461        sprite.camera_enabled = false;
9462        sprite.camera_eye = [0.0, 0.0, -1000.0];
9463        sprite.camera_target = [0.0, 0.0, 0.0];
9464        sprite.camera_up = [0.0, 1.0, 0.0];
9465        return;
9466    }
9467
9468    sprite.camera_enabled = true;
9469    sprite.camera_eye = cam_eye;
9470    sprite.camera_target = cam_target;
9471    sprite.camera_up = cam_up;
9472}
9473
9474fn fetch_bound_render_sprites(
9475    ctx: &CommandContext,
9476    stage_idx: i64,
9477    runtime_slot: usize,
9478    obj: &globals::ObjectState,
9479) -> Vec<RenderSprite> {
9480    // Object tree visibility is driven by C_elm_object::disp and parent visibility.
9481    // The backing layer sprite visible bit is only a cached render backend state and
9482    // can be stale for object-owned sprites. Fetch the sprite payload unconditionally;
9483    // append_object_tree_sprites() applies the original object visibility gate.
9484    fetch_bound_render_sprites_impl(ctx, stage_idx, runtime_slot, obj, false)
9485}
9486
9487fn fetch_bound_render_sprites_any(
9488    ctx: &CommandContext,
9489    stage_idx: i64,
9490    runtime_slot: usize,
9491    obj: &globals::ObjectState,
9492) -> Vec<RenderSprite> {
9493    fetch_bound_render_sprites_impl(ctx, stage_idx, runtime_slot, obj, false)
9494}
9495
9496fn fetch_bound_render_sprites_impl(
9497    ctx: &CommandContext,
9498    stage_idx: i64,
9499    runtime_slot: usize,
9500    obj: &globals::ObjectState,
9501    visible_only: bool,
9502) -> Vec<RenderSprite> {
9503    fn push_one(
9504        ctx: &CommandContext,
9505        lid: LayerId,
9506        sid: SpriteId,
9507        visible_only: bool,
9508        out: &mut Vec<RenderSprite>,
9509    ) {
9510        let Some(layer) = ctx.layers.layer(lid) else {
9511            return;
9512        };
9513        let Some(sprite) = layer.sprite(sid) else {
9514            return;
9515        };
9516        if visible_only && !sprite.visible {
9517            return;
9518        }
9519        if sprite.image_id.is_none() {
9520            return;
9521        }
9522        out.push(RenderSprite::new(Some(lid), Some(sid), sprite.clone()));
9523    }
9524
9525    let mut out = Vec::new();
9526    match &obj.backend {
9527        globals::ObjectBackend::Gfx => {
9528            if let Some((lid, sid)) = ctx
9529                .gfx
9530                .object_sprite_binding(stage_idx, runtime_slot as i64)
9531            {
9532                push_one(ctx, lid, sid, visible_only, &mut out);
9533            }
9534        }
9535        globals::ObjectBackend::Rect {
9536            layer_id,
9537            sprite_id,
9538            ..
9539        }
9540        | globals::ObjectBackend::String {
9541            layer_id,
9542            sprite_id,
9543            ..
9544        }
9545        | globals::ObjectBackend::Movie {
9546            layer_id,
9547            sprite_id,
9548            ..
9549        } => {
9550            push_one(ctx, *layer_id, *sprite_id, visible_only, &mut out);
9551        }
9552        globals::ObjectBackend::Number {
9553            layer_id,
9554            sprite_ids,
9555        }
9556        | globals::ObjectBackend::Weather {
9557            layer_id,
9558            sprite_ids,
9559        } => {
9560            for sid in sprite_ids {
9561                push_one(ctx, *layer_id, *sid, visible_only, &mut out);
9562            }
9563        }
9564        globals::ObjectBackend::None => {}
9565    }
9566    out
9567}
9568
9569fn effective_object_info(
9570    ctx: &CommandContext,
9571    stage_idx: i64,
9572    obj_idx: usize,
9573    obj: &globals::ObjectState,
9574) -> ObjectRenderInfo {
9575    let runtime_slot = object_runtime_slot(obj_idx, obj);
9576    let ids = &ctx.ids;
9577    let extra = |id: i32, default: i64| -> i64 {
9578        if id != 0 {
9579            obj.lookup_int_prop(ids, id).unwrap_or(default)
9580        } else {
9581            default
9582        }
9583    };
9584    let extra_str = |id: i32| -> Option<String> {
9585        if id != 0 {
9586            obj.lookup_str_prop(ids, id)
9587        } else {
9588            None
9589        }
9590    };
9591
9592    let dst_clip = if extra(ids.obj_clip_use, obj.base.clip_use) != 0 {
9593        Some(ClipRect {
9594            left: extra(ids.obj_clip_left, obj.base.clip_left) as i32,
9595            top: extra(ids.obj_clip_top, obj.base.clip_top) as i32,
9596            right: extra(ids.obj_clip_right, obj.base.clip_right) as i32,
9597            bottom: extra(ids.obj_clip_bottom, obj.base.clip_bottom) as i32,
9598        })
9599    } else {
9600        None
9601    };
9602
9603    let x_rep_total = obj
9604        .runtime
9605        .prop_event_lists
9606        .x_rep
9607        .iter()
9608        .map(|ev| ev.get_total_value() as i64)
9609        .sum::<i64>();
9610    let y_rep_total = obj
9611        .runtime
9612        .prop_event_lists
9613        .y_rep
9614        .iter()
9615        .map(|ev| ev.get_total_value() as i64)
9616        .sum::<i64>();
9617    let z_rep_total = obj
9618        .runtime
9619        .prop_event_lists
9620        .z_rep
9621        .iter()
9622        .map(|ev| ev.get_total_value() as i64)
9623        .sum::<i64>();
9624    let tr_rep_total = obj
9625        .runtime
9626        .prop_event_lists
9627        .tr_rep
9628        .iter()
9629        .fold(255i64, |acc, ev| {
9630            acc.saturating_mul(ev.get_total_value() as i64)
9631                .div_euclid(255)
9632        });
9633
9634    let mut info = ObjectRenderInfo {
9635        runtime_slot,
9636        used: obj.used,
9637        object_type: obj.object_type,
9638        disp: extra(ids.obj_disp, obj.base.disp) != 0,
9639        x: extra(ids.obj_x, obj.base.x),
9640        y: extra(ids.obj_y, obj.base.y),
9641        x_rep: x_rep_total,
9642        y_rep: y_rep_total,
9643        z_rep: z_rep_total,
9644        order: extra(ids.obj_order, obj.base.order),
9645        layer: extra(ids.obj_layer, obj.base.layer),
9646        alpha: extra(ids.obj_alpha, obj.base.alpha),
9647        tr: extra(ids.obj_tr, obj.base.tr),
9648        tr_rep: tr_rep_total,
9649        mono: extra(ids.obj_mono, obj.base.mono),
9650        reverse: extra(ids.obj_reverse, obj.base.reverse),
9651        bright: extra(ids.obj_bright, obj.base.bright),
9652        dark: extra(ids.obj_dark, obj.base.dark),
9653        color_rate: extra(ids.obj_color_rate, obj.base.color_rate),
9654        color_add_r: extra(ids.obj_color_add_r, obj.base.color_add_r),
9655        color_add_g: extra(ids.obj_color_add_g, obj.base.color_add_g),
9656        color_add_b: extra(ids.obj_color_add_b, obj.base.color_add_b),
9657        color_r: extra(ids.obj_color_r, obj.base.color_r),
9658        color_g: extra(ids.obj_color_g, obj.base.color_g),
9659        color_b: extra(ids.obj_color_b, obj.base.color_b),
9660        z: extra(ids.obj_z, obj.base.z),
9661        world_no: extra(ids.obj_world, obj.base.world),
9662        center_x: extra(ids.obj_center_x, obj.base.center_x),
9663        center_y: extra(ids.obj_center_y, obj.base.center_y),
9664        center_z: extra(ids.obj_center_z, obj.base.center_z),
9665        center_rep_x: extra(ids.obj_center_rep_x, obj.base.center_rep_x),
9666        center_rep_y: extra(ids.obj_center_rep_y, obj.base.center_rep_y),
9667        center_rep_z: extra(ids.obj_center_rep_z, obj.base.center_rep_z),
9668        scale_x: extra(ids.obj_scale_x, obj.base.scale_x),
9669        scale_y: extra(ids.obj_scale_y, obj.base.scale_y),
9670        scale_z: extra(ids.obj_scale_z, obj.base.scale_z),
9671        rotate_x: extra(ids.obj_rotate_x, obj.base.rotate_x),
9672        rotate_y: extra(ids.obj_rotate_y, obj.base.rotate_y),
9673        rotate_z: extra(ids.obj_rotate_z, obj.base.rotate_z),
9674        culling: extra(ids.obj_culling, obj.base.culling) != 0,
9675        alpha_test: extra(ids.obj_alpha_test, obj.base.alpha_test) != 0,
9676        alpha_blend: extra(ids.obj_alpha_blend, obj.base.alpha_blend) != 0,
9677        fog_use: extra(ids.obj_fog_use, obj.base.fog_use) != 0,
9678        light_no: extra(ids.obj_light_no, obj.base.light_no),
9679        blend: crate::layer::SpriteBlend::from_i64(extra(ids.obj_blend, obj.base.blend)),
9680        child_sort_type: obj.base.child_sort_type,
9681        dst_clip,
9682        billboard: obj.object_type == 7,
9683        file_name: obj.file_name.clone(),
9684        mesh_animation: obj.mesh_animation_state.clone(),
9685    };
9686
9687    match &obj.backend {
9688        globals::ObjectBackend::Gfx => {
9689            // C_elm_mwnd_waku::m_btn_list and OBJECT.CHILD entries are internal
9690            // object trees, not top-level C_elm_stage::m_obj_list entries. Their
9691            // Gfx layer sprite is only backing storage. Do not read the backing
9692            // sprite's cached visible/pos/order/layer state here, because it can be
9693            // hidden to prevent raw LayerManager leakage and because the authoritative
9694            // state for tree rendering is the C_elm_object property block.
9695            let embedded_tree_object = obj.nested_runtime_slot.is_some();
9696            if !embedded_tree_object {
9697                if let Some(v) = ctx.gfx.object_peek_disp(stage_idx, runtime_slot as i64) {
9698                    info.disp = v != 0;
9699                }
9700                if let Some((x, y)) = ctx.gfx.object_peek_pos(stage_idx, runtime_slot as i64) {
9701                    info.x = x;
9702                    info.y = y;
9703                }
9704                if let Some(v) = ctx.gfx.object_peek_order(stage_idx, runtime_slot as i64) {
9705                    info.order = v;
9706                }
9707                if let Some(v) = ctx.gfx.object_peek_layer(stage_idx, runtime_slot as i64) {
9708                    info.layer = v;
9709                }
9710                if let Some(v) = ctx.gfx.object_peek_alpha(stage_idx, runtime_slot as i64) {
9711                    info.alpha = v;
9712                }
9713            }
9714            if !embedded_tree_object {
9715                if let Some((lid, sid)) = ctx
9716                    .gfx
9717                    .object_sprite_binding(stage_idx, runtime_slot as i64)
9718                {
9719                    if let Some(layer) = ctx.layers.layer(lid) {
9720                        if let Some(sprite) = layer.sprite(sid) {
9721                            info.tr = sprite.tr as i64;
9722                        }
9723                    }
9724                }
9725            }
9726        }
9727        globals::ObjectBackend::Rect { .. }
9728        | globals::ObjectBackend::String { .. }
9729        | globals::ObjectBackend::Movie { .. }
9730        | globals::ObjectBackend::Number { .. }
9731        | globals::ObjectBackend::Weather { .. } => {
9732            // The backend sprite only stores image handles and backend-only data.
9733            // C++ C_elm_object::frame uses the object parameter block for DISP,
9734            // X/Y, sorter, alpha and TR.  Reading those fields back from the
9735            // storage sprite makes objects created at local (0,0), such as save
9736            // thumbnails, ignore later SET_POS or parent object transforms.
9737        }
9738        globals::ObjectBackend::None => {
9739            if let Some(v) = obj.lookup_int_prop(ids, ids.obj_disp) {
9740                info.disp = v != 0;
9741            } else if obj.object_type == 0 && !obj.runtime.child_objects.is_empty() {
9742                info.disp = true;
9743            }
9744        }
9745    }
9746
9747    let event_total = |event_op: i32, current: i64| -> i64 {
9748        if event_op != 0 {
9749            obj.int_event_by_op(ids, event_op)
9750                .map(|ev| ev.get_total_value() as i64)
9751                .unwrap_or(current)
9752        } else {
9753            current
9754        }
9755    };
9756
9757    info.x = event_total(ids.obj_x_eve, info.x);
9758    info.y = event_total(ids.obj_y_eve, info.y);
9759    info.z = event_total(ids.obj_z_eve, info.z);
9760    info.tr = event_total(ids.obj_tr_eve, info.tr);
9761    info.mono = event_total(ids.obj_mono_eve, info.mono);
9762    info.reverse = event_total(ids.obj_reverse_eve, info.reverse);
9763    info.bright = event_total(ids.obj_bright_eve, info.bright);
9764    info.dark = event_total(ids.obj_dark_eve, info.dark);
9765    info.color_rate = event_total(ids.obj_color_rate_eve, info.color_rate);
9766    info.color_add_r = event_total(ids.obj_color_add_r_eve, info.color_add_r);
9767    info.color_add_g = event_total(ids.obj_color_add_g_eve, info.color_add_g);
9768    info.color_add_b = event_total(ids.obj_color_add_b_eve, info.color_add_b);
9769    info.color_r = event_total(ids.obj_color_r_eve, info.color_r);
9770    info.color_g = event_total(ids.obj_color_g_eve, info.color_g);
9771    info.color_b = event_total(ids.obj_color_b_eve, info.color_b);
9772    info.center_x = event_total(ids.obj_center_x_eve, info.center_x);
9773    info.center_y = event_total(ids.obj_center_y_eve, info.center_y);
9774    info.center_z = event_total(ids.obj_center_z_eve, info.center_z);
9775    info.center_rep_x = event_total(ids.obj_center_rep_x_eve, info.center_rep_x);
9776    info.center_rep_y = event_total(ids.obj_center_rep_y_eve, info.center_rep_y);
9777    info.center_rep_z = event_total(ids.obj_center_rep_z_eve, info.center_rep_z);
9778    info.scale_x = event_total(ids.obj_scale_x_eve, info.scale_x);
9779    info.scale_y = event_total(ids.obj_scale_y_eve, info.scale_y);
9780    info.scale_z = event_total(ids.obj_scale_z_eve, info.scale_z);
9781    info.rotate_x = event_total(ids.obj_rotate_x_eve, info.rotate_x);
9782    info.rotate_y = event_total(ids.obj_rotate_y_eve, info.rotate_y);
9783    info.rotate_z = event_total(ids.obj_rotate_z_eve, info.rotate_z);
9784
9785    if extra(ids.obj_clip_use, 0) != 0 {
9786        info.dst_clip = Some(ClipRect {
9787            left: event_total(ids.obj_clip_left_eve, extra(ids.obj_clip_left, 0)) as i32,
9788            top: event_total(ids.obj_clip_top_eve, extra(ids.obj_clip_top, 0)) as i32,
9789            right: event_total(ids.obj_clip_right_eve, extra(ids.obj_clip_right, 0)) as i32,
9790            bottom: event_total(ids.obj_clip_bottom_eve, extra(ids.obj_clip_bottom, 0)) as i32,
9791        });
9792    }
9793
9794    info
9795}
9796
9797fn configure_sprite_3d(
9798    sprite: &mut crate::layer::Sprite,
9799    info: &ObjectRenderInfo,
9800    _worlds: Option<&Vec<globals::WorldState>>,
9801    _screen_w: u32,
9802    _screen_h: u32,
9803) {
9804    sprite.z = info.z as f32;
9805    sprite.pivot_z = info.center_z as f32;
9806    sprite.scale_z = info.scale_z as f32 / 1000.0;
9807    sprite.rotate_x = info.rotate_x as f32 * std::f32::consts::PI / 1800.0;
9808    sprite.rotate_y = info.rotate_y as f32 * std::f32::consts::PI / 1800.0;
9809    sprite.culling = info.culling;
9810    sprite.alpha_test = info.alpha_test;
9811    sprite.alpha_blend = info.alpha_blend;
9812    sprite.fog_use = info.fog_use;
9813    sprite.light_no = info.light_no as i32;
9814    sprite.world_no = info.world_no as i32;
9815    sprite.billboard = info.billboard;
9816    sprite.mesh_file_name = if info.object_type == 6 {
9817        info.file_name.clone()
9818    } else {
9819        None
9820    };
9821    sprite.mesh_kind = if info.object_type == 6 { 1 } else { 0 };
9822    sprite.shadow_cast = sprite.mesh_kind != 0;
9823    sprite.shadow_receive = sprite.mesh_kind != 0;
9824    sprite.mesh_animation = info.mesh_animation.clone();
9825
9826    let uses_3d = matches!(info.object_type, 6 | 7)
9827        || info.billboard
9828        || info.z != 0
9829        || info.center_z != 0
9830        || info.scale_z != 1000
9831        || info.rotate_x != 0
9832        || info.rotate_y != 0;
9833
9834    sprite.camera_enabled = uses_3d;
9835    sprite.camera_eye = [0.0, 0.0, -1000.0];
9836    sprite.camera_target = [0.0, 0.0, 0.0];
9837    sprite.camera_up = [0.0, 1.0, 0.0];
9838    sprite.camera_view_angle_deg = 45.0;
9839}
9840
9841
9842fn object_motion_trace_enabled() -> bool {
9843    std::env::var_os("SG_OBJECT_MOTION_TRACE").is_some()
9844}
9845
9846fn object_motion_trace_object(obj: &globals::ObjectState) -> bool {
9847    let Some(file_name) = obj.file_name.as_deref() else {
9848        return false;
9849    };
9850    file_name
9851        .rsplit(|c| c == '/' || c == '\\')
9852        .next()
9853        .map(|base| base.to_ascii_lowercase().starts_with("mp_"))
9854        .unwrap_or(false)
9855}
9856
9857fn object_motion_trace_bind(
9858    ctx: &CommandContext,
9859    stage_idx: i64,
9860    info: &ObjectRenderInfo,
9861    obj: &globals::ObjectState,
9862) -> Option<(LayerId, SpriteId)> {
9863    match &obj.backend {
9864        globals::ObjectBackend::Gfx => ctx
9865            .gfx
9866            .object_sprite_binding(stage_idx, info.runtime_slot as i64),
9867        globals::ObjectBackend::Rect {
9868            layer_id,
9869            sprite_id,
9870            ..
9871        }
9872        | globals::ObjectBackend::String {
9873            layer_id,
9874            sprite_id,
9875            ..
9876        }
9877        | globals::ObjectBackend::Movie {
9878            layer_id,
9879            sprite_id,
9880            ..
9881        } => Some((*layer_id, *sprite_id)),
9882        globals::ObjectBackend::Number {
9883            layer_id,
9884            sprite_ids,
9885        }
9886        | globals::ObjectBackend::Weather {
9887            layer_id,
9888            sprite_ids,
9889        } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
9890        globals::ObjectBackend::None => None,
9891    }
9892}
9893
9894fn object_motion_trace_emit(
9895    ctx: &CommandContext,
9896    stage_idx: i64,
9897    obj_idx: usize,
9898    obj: &globals::ObjectState,
9899    info: &ObjectRenderInfo,
9900    parent_visible: bool,
9901    visible: bool,
9902    local_tr: i64,
9903    total_order: i64,
9904    total_layer: i64,
9905    bound_len: usize,
9906    bind_dbg: Option<(LayerId, SpriteId)>,
9907    emitted: bool,
9908    sprite: Option<&Sprite>,
9909) {
9910    let ev = &obj.runtime.prop_events;
9911    let sprite_desc = sprite
9912        .map(|s| {
9913            format!(
9914                "sprite_pos=({}, {}, {:.3}) sprite_order={} sprite_sorter=({}, {}) sprite_alpha={} sprite_tr={} image={:?}",
9915                s.x,
9916                s.y,
9917                s.z,
9918                s.order,
9919                unpack_legacy_sorter_key(s.order).0,
9920                unpack_legacy_sorter_key(s.order).1,
9921                s.alpha,
9922                s.tr,
9923                s.image_id
9924            )
9925        })
9926        .unwrap_or_else(|| "sprite_pos=(none) sprite_order=(none) sprite_sorter=(none) sprite_alpha=(none) sprite_tr=(none) image=None".to_string());
9927    eprintln!(
9928        "[SG_OBJECT_MOTION_TRACE][RENDER] frame={} stage={} obj_idx={} slot={} file={} used={} backend={:?} parent_visible={} visible={} emitted={} disp={} local=({}, {}, {}) rep=({}, {}, {}) center=({}, {}, {}) center_rep=({}, {}, {}) prop_final=({}, {}, {}) order={} layer={} total_order={} total_layer={} alpha={} tr={} tr_rep={} local_tr={} bound_len={} bind={:?} x_eve=active:{} total:{} value:{} time:{}/{} y_eve=active:{} total:{} value:{} time:{}/{} tr_eve=active:{} total:{} value:{} time:{}/{} {}",
9929        ctx.globals.render_frame,
9930        stage_idx,
9931        obj_idx,
9932        info.runtime_slot,
9933        obj.file_name.as_deref().unwrap_or("-"),
9934        obj.used,
9935        obj.backend,
9936        parent_visible,
9937        visible,
9938        emitted,
9939        info.disp,
9940        info.x,
9941        info.y,
9942        info.z,
9943        info.x_rep,
9944        info.y_rep,
9945        info.z_rep,
9946        info.center_x,
9947        info.center_y,
9948        info.center_z,
9949        info.center_rep_x,
9950        info.center_rep_y,
9951        info.center_rep_z,
9952        info.x + info.x_rep + info.center_rep_x,
9953        info.y + info.y_rep + info.center_rep_y,
9954        info.z + info.z_rep + info.center_rep_z,
9955        info.order,
9956        info.layer,
9957        total_order,
9958        total_layer,
9959        info.alpha,
9960        info.tr,
9961        info.tr_rep,
9962        local_tr,
9963        bound_len,
9964        bind_dbg,
9965        ev.x.check_event(),
9966        ev.x.get_total_value(),
9967        ev.x.get_value(),
9968        ev.x.cur_time,
9969        ev.x.end_time,
9970        ev.y.check_event(),
9971        ev.y.get_total_value(),
9972        ev.y.get_value(),
9973        ev.y.cur_time,
9974        ev.y.end_time,
9975        ev.tr.check_event(),
9976        ev.tr.get_total_value(),
9977        ev.tr.get_value(),
9978        ev.tr.cur_time,
9979        ev.tr.end_time,
9980        sprite_desc,
9981    );
9982}
9983
9984fn append_object_tree_sprites(
9985    ctx: &CommandContext,
9986    worlds: Option<&Vec<globals::WorldState>>,
9987    stage_idx: i64,
9988    obj_idx: usize,
9989    obj: &globals::ObjectState,
9990    parent_visible: bool,
9991    parent_order: i64,
9992    parent_layer: i64,
9993    parent_state: Option<ParentRenderState>,
9994    out: &mut Vec<RenderSprite>,
9995    object_keys: &mut HashSet<(LayerId, SpriteId)>,
9996    debug_lines: &mut Vec<String>,
9997) {
9998    if !object_participates_in_tree(obj) {
9999        return;
10000    }
10001
10002    let debug_enabled = sg_render_tree_debug_enabled();
10003    let info = effective_object_info(ctx, stage_idx, obj_idx, obj);
10004    let local_tr = ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255).clamp(0, 255);
10005    let visible = parent_visible
10006        && info.disp
10007        && local_tr > 0
10008        && object_button_renderable_by_syscom(&ctx.globals.syscom, obj);
10009    let total_order = parent_order.saturating_add(info.order);
10010    let total_layer = parent_layer.saturating_add(info.layer);
10011
10012    if debug_enabled {
10013        let bind_dbg = match &obj.backend {
10014            globals::ObjectBackend::Gfx => ctx
10015                .gfx
10016                .object_sprite_binding(stage_idx, info.runtime_slot as i64),
10017            globals::ObjectBackend::Rect {
10018                layer_id,
10019                sprite_id,
10020                ..
10021            }
10022            | globals::ObjectBackend::String {
10023                layer_id,
10024                sprite_id,
10025                ..
10026            }
10027            | globals::ObjectBackend::Movie {
10028                layer_id,
10029                sprite_id,
10030                ..
10031            } => Some((*layer_id, *sprite_id)),
10032            globals::ObjectBackend::Number {
10033                layer_id,
10034                sprite_ids,
10035            }
10036            | globals::ObjectBackend::Weather {
10037                layer_id,
10038                sprite_ids,
10039            } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
10040            globals::ObjectBackend::None => None,
10041        };
10042        debug_lines.push(format!(
10043            "[SG_DEBUG]     obj[{obj_idx}] slot={} used={} type={} backend={:?} file={} disp={} pos=({}, {}) center=({}, {}, {}) center_rep=({}, {}, {}) final_pos=({}, {}, {}) order={} layer={} alpha={} tr={} z={} child_sort={} wipe_copy={} wipe_erase={} bind={:?}",
10044            info.runtime_slot,
10045            obj.used,
10046            obj.object_type,
10047            obj.backend,
10048            obj.file_name.as_deref().unwrap_or("-"),
10049            info.disp,
10050            info.x,
10051            info.y,
10052            info.center_x,
10053            info.center_y,
10054            info.center_z,
10055            info.center_rep_x,
10056            info.center_rep_y,
10057            info.center_rep_z,
10058            info.x + info.x_rep + info.center_rep_x,
10059            info.y + info.y_rep + info.center_rep_y,
10060            info.z + info.z_rep + info.center_rep_z,
10061            info.order,
10062            info.layer,
10063            info.alpha,
10064            info.tr,
10065            info.z,
10066            info.child_sort_type,
10067            obj.get_int_prop(&ctx.ids, ctx.ids.obj_wipe_copy),
10068            obj.get_int_prop(&ctx.ids, ctx.ids.obj_wipe_erase),
10069            bind_dbg,
10070        ));
10071    }
10072
10073    if debug_enabled && obj.button.enabled {
10074        debug_lines.push(format!(
10075            "[SG_DEBUG]       button enabled=true no={} group_no={} group_idx={:?} cut={} action={} se={} state={} hit={} pushed={} alpha_test={} call={}::{}/{}",
10076            obj.button.button_no,
10077            obj.button.group_no,
10078            obj.button.group_idx(),
10079            obj.button.cut_no,
10080            obj.button.action_no,
10081            obj.button.se_no,
10082            obj.button.state,
10083            obj.button.hit,
10084            obj.button.pushed,
10085            obj.button.alpha_test,
10086            obj.button.decided_action_scn_name,
10087            obj.button.decided_action_cmd_name,
10088            obj.button.decided_action_z_no,
10089        ));
10090    }
10091    if debug_enabled && (!obj.frame_action.cmd_name.is_empty() || obj.frame_action.end_flag) {
10092        debug_lines.push(format!(
10093            "[SG_DEBUG]       frame_action cmd={}::{} count={} end_time={} real={} end_flag={} args={:?}",
10094            obj.frame_action.scn_name,
10095            obj.frame_action.cmd_name,
10096            obj.frame_action.counter.get_count(),
10097            obj.frame_action.end_time,
10098            obj.frame_action.real_time_flag,
10099            obj.frame_action.end_flag,
10100            obj.frame_action.args,
10101        ));
10102    }
10103    for (fa_idx, fa) in obj.frame_action_ch.iter().enumerate() {
10104        if debug_enabled && (!fa.cmd_name.is_empty() || fa.end_flag) {
10105            debug_lines.push(format!(
10106                "[SG_DEBUG]       frame_action_ch[{}] cmd={}::{} count={} end_time={} real={} end_flag={} args={:?}",
10107                fa_idx,
10108                fa.scn_name,
10109                fa.cmd_name,
10110                fa.counter.get_count(),
10111                fa.end_time,
10112                fa.real_time_flag,
10113                fa.end_flag,
10114                fa.args,
10115            ));
10116        }
10117    }
10118    let ev = &obj.runtime.prop_events;
10119    if debug_enabled
10120        && (ev.color_rate.check_event()
10121            || ev.tr.check_event()
10122            || ev.x.check_event()
10123            || ev.y.check_event())
10124    {
10125        debug_lines.push(format!(
10126            "[SG_DEBUG]       active_events x={}/{} t={}/{} y={}/{} t={}/{} tr={}/{} t={}/{} color_rate={}/{} t={}/{}",
10127            ev.x.get_total_value(), ev.x.get_value(), ev.x.cur_time, ev.x.end_time,
10128            ev.y.get_total_value(), ev.y.get_value(), ev.y.cur_time, ev.y.end_time,
10129            ev.tr.get_total_value(), ev.tr.get_value(), ev.tr.cur_time, ev.tr.end_time,
10130            ev.color_rate.get_total_value(), ev.color_rate.get_value(), ev.color_rate.cur_time, ev.color_rate.end_time,
10131        ));
10132    }
10133    if debug_enabled
10134        && (!obj.runtime.prop_event_lists.x_rep.is_empty()
10135            || !obj.runtime.prop_event_lists.y_rep.is_empty()
10136            || !obj.runtime.prop_event_lists.tr_rep.is_empty())
10137    {
10138        let fmt_list = |list: &Vec<crate::runtime::int_event::IntEvent>| -> Vec<String> {
10139            list.iter()
10140                .enumerate()
10141                .filter(|(_, ev)| {
10142                    ev.check_event()
10143                        || ev.get_total_value() != ev.def_value
10144                        || ev.get_value() != ev.def_value
10145                })
10146                .map(|(idx, ev)| {
10147                    format!(
10148                        "{}:{}/{} t={}/{} active={}",
10149                        idx,
10150                        ev.get_total_value(),
10151                        ev.get_value(),
10152                        ev.cur_time,
10153                        ev.end_time,
10154                        ev.check_event()
10155                    )
10156                })
10157                .collect()
10158        };
10159        let x_rep = fmt_list(&obj.runtime.prop_event_lists.x_rep);
10160        let y_rep = fmt_list(&obj.runtime.prop_event_lists.y_rep);
10161        let tr_rep = fmt_list(&obj.runtime.prop_event_lists.tr_rep);
10162        if !x_rep.is_empty() || !y_rep.is_empty() || !tr_rep.is_empty() {
10163            debug_lines.push(format!(
10164                "[SG_DEBUG]       rep_events x={:?} y={:?} tr={:?}",
10165                x_rep, y_rep, tr_rep,
10166            ));
10167        }
10168    }
10169
10170    let mut bound = fetch_bound_render_sprites(ctx, stage_idx, info.runtime_slot, obj);
10171    let motion_trace = object_motion_trace_enabled() && object_motion_trace_object(obj);
10172    let motion_bind_dbg = if motion_trace {
10173        object_motion_trace_bind(ctx, stage_idx, &info, obj)
10174    } else {
10175        None
10176    };
10177    if motion_trace && (!visible || bound.is_empty()) {
10178        object_motion_trace_emit(
10179            ctx,
10180            stage_idx,
10181            obj_idx,
10182            obj,
10183            &info,
10184            parent_visible,
10185            visible,
10186            local_tr,
10187            total_order,
10188            total_layer,
10189            bound.len(),
10190            motion_bind_dbg,
10191            false,
10192            None,
10193        );
10194    }
10195    if config_button_trace_enabled() && config_button_trace_object(obj) {
10196        let bind_dbg = match &obj.backend {
10197            globals::ObjectBackend::Gfx => ctx
10198                .gfx
10199                .object_sprite_binding(stage_idx, info.runtime_slot as i64),
10200            globals::ObjectBackend::Rect {
10201                layer_id,
10202                sprite_id,
10203                ..
10204            }
10205            | globals::ObjectBackend::String {
10206                layer_id,
10207                sprite_id,
10208                ..
10209            }
10210            | globals::ObjectBackend::Movie {
10211                layer_id,
10212                sprite_id,
10213                ..
10214            } => Some((*layer_id, *sprite_id)),
10215            globals::ObjectBackend::Number {
10216                layer_id,
10217                sprite_ids,
10218            }
10219            | globals::ObjectBackend::Weather {
10220                layer_id,
10221                sprite_ids,
10222            } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
10223            globals::ObjectBackend::None => None,
10224        };
10225        let syscom_renderable = object_button_renderable_by_syscom(&ctx.globals.syscom, obj);
10226        debug_lines.push(format!(
10227            "[SG_DEBUG][CONFIG_BUTTON_TRACE][COLLECT] stage={} obj_idx={} runtime_slot={} file={} backend={:?} participates={} parent_visible={} disp={} local_tr={} tr={} tr_rep={} syscom_renderable={} visible={} bound_len={} bind={:?} order={} layer={} total_order={} total_layer={} button_enabled={} button_state={} button_no={} group_no={} action_no={} hit={} pushed={} disabled_reason={:?} parent_state={}",
10228            stage_idx,
10229            obj_idx,
10230            info.runtime_slot,
10231            obj.file_name.as_deref().unwrap_or("-"),
10232            obj.backend,
10233            object_participates_in_tree(obj),
10234            parent_visible,
10235            info.disp,
10236            local_tr,
10237            info.tr,
10238            info.tr_rep,
10239            syscom_renderable,
10240            visible,
10241            bound.len(),
10242            bind_dbg,
10243            info.order,
10244            info.layer,
10245            total_order,
10246            total_layer,
10247            obj.button.enabled,
10248            obj.button.state,
10249            obj.button.button_no,
10250            obj.button.group_no,
10251            obj.button.action_no,
10252            obj.button.hit,
10253            obj.button.pushed,
10254            button_disabled_reason(&ctx.globals.syscom, obj, None),
10255            parent_state.is_some()
10256        ));
10257    }
10258    for rs in &bound {
10259        if let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) {
10260            object_keys.insert((lid, sid));
10261        }
10262    }
10263    let mut cur_parent_state = build_parent_render_state(&info, bound.first().map(|rs| &rs.sprite));
10264    if let Some(parent) = parent_state {
10265        cur_parent_state = compose_parent_render_state(parent, cur_parent_state);
10266    }
10267
10268    if visible {
10269        if obj.object_type == 4 {
10270            let out_len_before = out.len();
10271            append_weather_sprites(
10272                ctx,
10273                worlds,
10274                obj,
10275                &info,
10276                total_order,
10277                total_layer,
10278                &bound,
10279                out,
10280            );
10281            for rs in out[out_len_before..].iter_mut() {
10282                if let Some(parent) = parent_state {
10283                    apply_parent_render_state_to_sprite(&mut rs.sprite, &info, &parent);
10284                }
10285                finalize_object_center_rep_to_sprite(&mut rs.sprite, &info);
10286                apply_world_camera_mode(&mut rs.sprite, worlds, ctx.screen_w, ctx.screen_h);
10287                apply_runtime_light_and_fog(ctx, &mut rs.sprite);
10288                if motion_trace {
10289                    object_motion_trace_emit(
10290                        ctx,
10291                        stage_idx,
10292                        obj_idx,
10293                        obj,
10294                        &info,
10295                        parent_visible,
10296                        visible,
10297                        local_tr,
10298                        total_order,
10299                        total_layer,
10300                        bound.len(),
10301                        motion_bind_dbg,
10302                        rs.sprite.tr > 0,
10303                        Some(&rs.sprite),
10304                    );
10305                }
10306            }
10307        } else {
10308            let bound_len_for_trace = bound.len();
10309            for mut rs in bound.drain(..) {
10310                apply_object_render_info_to_sprite(&mut rs.sprite, &info);
10311                rs.set_sorter(total_order, total_layer);
10312                rs.sprite.order = legacy_packed_sorter_key(total_order, total_layer);
10313                configure_sprite_3d(&mut rs.sprite, &info, worlds, ctx.screen_w, ctx.screen_h);
10314                if let Some(parent) = parent_state {
10315                    apply_parent_render_state_to_sprite(&mut rs.sprite, &info, &parent);
10316                }
10317                finalize_object_center_rep_to_sprite(&mut rs.sprite, &info);
10318                apply_world_camera_mode(&mut rs.sprite, worlds, ctx.screen_w, ctx.screen_h);
10319                apply_runtime_light_and_fog(ctx, &mut rs.sprite);
10320                if motion_trace {
10321                    object_motion_trace_emit(
10322                        ctx,
10323                        stage_idx,
10324                        obj_idx,
10325                        obj,
10326                        &info,
10327                        parent_visible,
10328                        visible,
10329                        local_tr,
10330                        total_order,
10331                        total_layer,
10332                        bound_len_for_trace,
10333                        motion_bind_dbg,
10334                        rs.sprite.tr > 0,
10335                        Some(&rs.sprite),
10336                    );
10337                }
10338                if rs.sprite.tr > 0 {
10339                    out.push(rs);
10340                }
10341            }
10342        }
10343    }
10344
10345    if config_button_trace_enabled() && config_button_trace_object(obj) {
10346        debug_lines.push(format!(
10347            "[SG_DEBUG][CONFIG_BUTTON_TRACE][EMIT_DONE] stage={} obj_idx={} runtime_slot={} file={} out_len_now={} visible={} child_count={}",
10348            stage_idx,
10349            obj_idx,
10350            info.runtime_slot,
10351            obj.file_name.as_deref().unwrap_or("-"),
10352            out.len(),
10353            visible,
10354            obj.runtime.child_objects.len()
10355        ));
10356    }
10357
10358    if debug_enabled && !obj.runtime.child_objects.is_empty() {
10359        debug_lines.push(format!(
10360            "[SG_DEBUG]       child_list op=93 len={}",
10361            obj.runtime.child_objects.len()
10362        ));
10363    }
10364    if sg_mwnd_object_trace_enabled() && !obj.runtime.child_objects.is_empty() {
10365        for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
10366            let child_info = effective_object_info(ctx, stage_idx, child_idx, child);
10367            let child_bind = match &child.backend {
10368                globals::ObjectBackend::Gfx => ctx
10369                    .gfx
10370                    .object_sprite_binding(stage_idx, child_info.runtime_slot as i64),
10371                globals::ObjectBackend::Rect {
10372                    layer_id,
10373                    sprite_id,
10374                    ..
10375                }
10376                | globals::ObjectBackend::String {
10377                    layer_id,
10378                    sprite_id,
10379                    ..
10380                }
10381                | globals::ObjectBackend::Movie {
10382                    layer_id,
10383                    sprite_id,
10384                    ..
10385                } => Some((*layer_id, *sprite_id)),
10386                globals::ObjectBackend::Number {
10387                    layer_id,
10388                    sprite_ids,
10389                }
10390                | globals::ObjectBackend::Weather {
10391                    layer_id,
10392                    sprite_ids,
10393                } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
10394                globals::ObjectBackend::None => None,
10395            };
10396            debug_lines.push(format!(
10397                "[SG_DEBUG][MWND_OBJECT_TRACE]       child parent_slot={} parent_obj_idx={} child[{}] slot={} participates={} used={} type={} backend={:?} file={} disp={} pos=({}, {}) center=({}, {}, {}) center_rep=({}, {}, {}) final_pos=({}, {}, {}) order={} layer={} alpha={} tr={} nested_slot={:?} bind={:?} grandchildren={}",
10398                info.runtime_slot,
10399                obj_idx,
10400                child_idx,
10401                child_info.runtime_slot,
10402                object_participates_in_tree(child),
10403                child.used,
10404                child.object_type,
10405                child.backend,
10406                child.file_name.as_deref().unwrap_or("-"),
10407                child_info.disp,
10408                child_info.x,
10409                child_info.y,
10410                child_info.center_x,
10411                child_info.center_y,
10412                child_info.center_z,
10413                child_info.center_rep_x,
10414                child_info.center_rep_y,
10415                child_info.center_rep_z,
10416                child_info.x + child_info.x_rep + child_info.center_rep_x,
10417                child_info.y + child_info.y_rep + child_info.center_rep_y,
10418                child_info.z + child_info.z_rep + child_info.center_rep_z,
10419                child_info.order,
10420                child_info.layer,
10421                child_info.alpha,
10422                child_info.tr,
10423                child.nested_runtime_slot,
10424                child_bind,
10425                child.runtime.child_objects.len()
10426            ));
10427        }
10428    }
10429
10430    let mut children: Vec<(usize, &globals::ObjectState)> = Vec::new();
10431    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
10432        if object_participates_in_tree(child) {
10433            children.push((child_idx, child));
10434        }
10435    }
10436    match info.child_sort_type {
10437        0 => {
10438            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10439                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10440                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10441                (l.order, l.layer).cmp(&(r.order, r.layer))
10442            });
10443        }
10444        2 => {
10445            children.sort_by(|(_, lhs), (_, rhs)| lhs.file_name.cmp(&rhs.file_name));
10446        }
10447        3 => {
10448            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10449                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10450                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10451                l.x.cmp(&r.x)
10452            });
10453        }
10454        4 => {
10455            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10456                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10457                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10458                r.x.cmp(&l.x)
10459            });
10460        }
10461        5 => {
10462            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10463                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10464                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10465                l.y.cmp(&r.y)
10466            });
10467        }
10468        6 => {
10469            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10470                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10471                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10472                r.y.cmp(&l.y)
10473            });
10474        }
10475        7 => {
10476            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10477                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10478                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10479                l.z.cmp(&r.z)
10480            });
10481        }
10482        8 => {
10483            children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
10484                let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
10485                let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
10486                r.z.cmp(&l.z)
10487            });
10488        }
10489        _ => {}
10490    }
10491    let child_tree_container = matches!(obj.backend, globals::ObjectBackend::None)
10492        && !obj.runtime.child_objects.is_empty();
10493    let recurse_children = if child_tree_container || matches!(obj.object_type, 3 | 4 | 5) {
10494        // Containers, STRING, NUMBER, and WEATHER keep traversing their child object list.
10495        // The parent node supplies the transform; sprite emission belongs to the descendants.
10496        parent_visible
10497    } else {
10498        visible
10499    };
10500    for (child_idx, child) in children {
10501        append_object_tree_sprites(
10502            ctx,
10503            worlds,
10504            stage_idx,
10505            child_idx,
10506            child,
10507            recurse_children,
10508            total_order,
10509            total_layer,
10510            Some(cur_parent_state),
10511            out,
10512            object_keys,
10513            debug_lines,
10514        );
10515    }
10516}
10517
10518fn append_weather_sprites(
10519    ctx: &CommandContext,
10520    worlds: Option<&Vec<globals::WorldState>>,
10521    obj: &globals::ObjectState,
10522    info: &ObjectRenderInfo,
10523    total_order: i64,
10524    total_layer: i64,
10525    bound: &[RenderSprite],
10526    out: &mut Vec<RenderSprite>,
10527) {
10528    let Some(template) = bound.first() else {
10529        return;
10530    };
10531    let wp = &obj.weather_param;
10532    let cnt = wp.cnt.clamp(0, 256) as usize;
10533    if cnt == 0 {
10534        return;
10535    }
10536    let frame = ctx.globals.render_frame as f32;
10537    let sw = ctx.screen_w as f32;
10538    let sh = ctx.screen_h as f32;
10539    let base_x = info.x as f32;
10540    let base_y = info.y as f32;
10541    let scale_x = (wp.scale_x as f32 / 1000.0).max(0.01);
10542    let scale_y = (wp.scale_y as f32 / 1000.0).max(0.01);
10543    for i in 0..cnt {
10544        let phase = i as f32 / cnt.max(1) as f32;
10545        let mut rs = template.clone();
10546        let mut x = base_x;
10547        let mut y = base_y;
10548        let mut zoom = 1.0f32;
10549        match wp.weather_type {
10550            2 => {
10551                let period = (wp.move_time.abs().max(1)) as f32;
10552                let t = (frame / period + phase).fract();
10553                let angle =
10554                    (wp.center_rotate as f32 / 10.0).to_radians() + t * std::f32::consts::TAU;
10555                let radius = (wp.appear_range as f32) * (0.2 + ((phase * 17.0).sin().abs() * 0.8));
10556                x += wp.center_x as f32 + angle.cos() * radius;
10557                y += wp.center_y as f32 + angle.sin() * radius * 0.65;
10558                let zmin = wp.zoom_min as f32 / 1000.0;
10559                let zmax = wp.zoom_max as f32 / 1000.0;
10560                let mix = (0.5
10561                    + 0.5 * ((frame / period) * std::f32::consts::TAU + phase * 9.0).sin())
10562                .clamp(0.0, 1.0);
10563                zoom = zmin + (zmax - zmin) * mix;
10564            }
10565            _ => {
10566                let tx = (wp.move_time_x.abs().max(1)) as f32;
10567                let ty = (wp.move_time_y.abs().max(1)) as f32;
10568                let ux = (frame / tx + phase).fract();
10569                let uy = (frame / ty + phase * 1.37).fract();
10570                x += (ux - 0.5) * sw * 1.4;
10571                y += (uy - 0.5) * sh * 1.4;
10572                if wp.sin_time_x != 0 {
10573                    x += ((frame / wp.sin_time_x.abs().max(1) as f32) * std::f32::consts::TAU
10574                        + phase * 7.0)
10575                        .sin()
10576                        * wp.sin_power_x as f32;
10577                }
10578                if wp.sin_time_y != 0 {
10579                    y += ((frame / wp.sin_time_y.abs().max(1) as f32) * std::f32::consts::TAU
10580                        + phase * 11.0)
10581                        .sin()
10582                        * wp.sin_power_y as f32;
10583                }
10584            }
10585        }
10586        rs.sprite.x = x.round() as i32;
10587        rs.sprite.y = y.round() as i32;
10588        rs.sprite.scale_x *= scale_x * zoom;
10589        rs.sprite.scale_y *= scale_y * zoom;
10590        rs.set_sorter(total_order, total_layer);
10591        rs.sprite.order = legacy_packed_sorter_key(total_order, total_layer);
10592        if wp.active_time > 0 {
10593            let life = (frame + phase * wp.active_time as f32).rem_euclid(wp.active_time as f32)
10594                / wp.active_time as f32;
10595            let fade = if life < 0.1 {
10596                life / 0.1
10597            } else if life > 0.9 {
10598                (1.0 - life) / 0.1
10599            } else {
10600                1.0
10601            };
10602            rs.sprite.alpha = ((rs.sprite.alpha as f32) * fade.clamp(0.0, 1.0))
10603                .round()
10604                .clamp(0.0, 255.0) as u8;
10605        }
10606        configure_sprite_3d(&mut rs.sprite, info, worlds, ctx.screen_w, ctx.screen_h);
10607        apply_world_camera_mode(&mut rs.sprite, worlds, ctx.screen_w, ctx.screen_h);
10608        apply_runtime_light_and_fog(ctx, &mut rs.sprite);
10609        out.push(rs);
10610    }
10611}
10612
10613fn apply_object_render_info_to_sprite(sprite: &mut Sprite, info: &ObjectRenderInfo) {
10614    sprite.visible = info.disp;
10615    sprite.x = (info.x + info.x_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
10616    sprite.y = (info.y + info.y_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
10617    sprite.z = (info.z + info.z_rep) as f32;
10618    sprite.pivot_x = (info.center_x + info.center_rep_x) as f32;
10619    sprite.pivot_y = (info.center_y + info.center_rep_y) as f32;
10620    sprite.pivot_z = (info.center_z + info.center_rep_z) as f32;
10621    sprite.scale_x = info.scale_x as f32 / 1000.0;
10622    sprite.scale_y = info.scale_y as f32 / 1000.0;
10623    sprite.scale_z = info.scale_z as f32 / 1000.0;
10624    sprite.rotate = info.rotate_z as f32 * std::f32::consts::PI / 1800.0;
10625    sprite.rotate_x = info.rotate_x as f32 * std::f32::consts::PI / 1800.0;
10626    sprite.rotate_y = info.rotate_y as f32 * std::f32::consts::PI / 1800.0;
10627    sprite.alpha = info.alpha.clamp(0, 255) as u8;
10628    sprite.tr = ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255).clamp(0, 255) as u8;
10629    sprite.mono = info.mono.clamp(0, 255) as u8;
10630    sprite.reverse = info.reverse.clamp(0, 255) as u8;
10631    sprite.bright = info.bright.clamp(0, 255) as u8;
10632    sprite.dark = info.dark.clamp(0, 255) as u8;
10633    sprite.color_rate = info.color_rate.clamp(0, 255) as u8;
10634    sprite.color_add_r = info.color_add_r.clamp(0, 255) as u8;
10635    sprite.color_add_g = info.color_add_g.clamp(0, 255) as u8;
10636    sprite.color_add_b = info.color_add_b.clamp(0, 255) as u8;
10637    sprite.color_r = info.color_r.clamp(0, 255) as u8;
10638    sprite.color_g = info.color_g.clamp(0, 255) as u8;
10639    sprite.color_b = info.color_b.clamp(0, 255) as u8;
10640    sprite.blend = info.blend;
10641    sprite.dst_clip = info.dst_clip;
10642}
10643
10644fn finalize_object_center_rep_to_sprite(sprite: &mut Sprite, info: &ObjectRenderInfo) {
10645    let x = (sprite.x as i64 + info.center_rep_x).clamp(i32::MIN as i64, i32::MAX as i64);
10646    let y = (sprite.y as i64 + info.center_rep_y).clamp(i32::MIN as i64, i32::MAX as i64);
10647    sprite.x = x as i32;
10648    sprite.y = y as i32;
10649    sprite.z += info.center_rep_z as f32;
10650}
10651
10652fn object_participates_in_tree(obj: &globals::ObjectState) -> bool {
10653    if obj.used {
10654        return true;
10655    }
10656    if !obj.runtime.child_objects.is_empty() {
10657        return true;
10658    }
10659    !matches!(obj.backend, globals::ObjectBackend::None)
10660}
10661
10662fn mark_object_tree_sprite_keys(
10663    ctx: &CommandContext,
10664    stage_idx: i64,
10665    obj_idx: usize,
10666    obj: &globals::ObjectState,
10667    object_keys: &mut HashSet<(LayerId, SpriteId)>,
10668) {
10669    let runtime_slot = object_runtime_slot(obj_idx, obj);
10670    for rs in fetch_bound_render_sprites_any(ctx, stage_idx, runtime_slot, obj) {
10671        if let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) {
10672            object_keys.insert((lid, sid));
10673        }
10674    }
10675    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
10676        mark_object_tree_sprite_keys(ctx, stage_idx, child_idx, child, object_keys);
10677    }
10678}
10679
10680fn mark_mwnd_owned_sprite_keys(
10681    ctx: &CommandContext,
10682    stage_idx: i64,
10683    m: &globals::MwndState,
10684    object_keys: &mut HashSet<(LayerId, SpriteId)>,
10685) {
10686    for (idx, obj) in m.button_list.iter().enumerate() {
10687        mark_object_tree_sprite_keys(ctx, stage_idx, idx, obj, object_keys);
10688    }
10689    for (idx, obj) in m.face_list.iter().enumerate() {
10690        mark_object_tree_sprite_keys(ctx, stage_idx, idx, obj, object_keys);
10691    }
10692    for (idx, obj) in m.object_list.iter().enumerate() {
10693        mark_object_tree_sprite_keys(ctx, stage_idx, idx, obj, object_keys);
10694    }
10695}
10696
10697fn mwnd_parent_render_state_at(
10698    m: &globals::MwndState,
10699    window_x: i64,
10700    window_y: i64,
10701) -> ParentRenderState {
10702    ParentRenderState {
10703        // C++ C_elm_mwnd::frame builds the MWND render parent from a fresh
10704        // S_tnm_render_param and never assigns p_world before passing it to
10705        // C_elm_mwnd_waku::frame.  Waku buttons therefore remain 2D UI sprites
10706        // even if the MWND form has a WORLD value.  Do not inherit m.world here:
10707        // doing so routes message-window buttons through the 3D/depth path, which
10708        // makes their textures appear in the renderer chain while the final frame
10709        // can depth-test them away behind earlier quads.
10710        world_no: -1,
10711        pos_x: window_x as f32,
10712        pos_y: window_y as f32,
10713        pos_z: 0.0,
10714        center_rep_x: 0.0,
10715        center_rep_y: 0.0,
10716        center_rep_z: 0.0,
10717        scale_x: 1.0,
10718        scale_y: 1.0,
10719        scale_z: 1.0,
10720        rotate_x: 0.0,
10721        rotate_y: 0.0,
10722        rotate_z: 0.0,
10723        tr: 255,
10724        mono: 0,
10725        reverse: 0,
10726        bright: 0,
10727        dark: 0,
10728        color_rate: 0,
10729        color_r: 0,
10730        color_g: 0,
10731        color_b: 0,
10732        color_add_r: 0,
10733        color_add_g: 0,
10734        color_add_b: 0,
10735        blend: crate::layer::SpriteBlend::Normal,
10736        dst_clip: None,
10737        mask_image_id: None,
10738        mask_offset_x: 0,
10739        mask_offset_y: 0,
10740        tonecurve_image_id: None,
10741        tonecurve_row: 0.0,
10742        tonecurve_sat: 0.0,
10743    }
10744}
10745
10746fn mwnd_parent_render_state(m: &globals::MwndState) -> ParentRenderState {
10747    let (x, y) = m.window_pos.unwrap_or((0, 0));
10748    mwnd_parent_render_state_at(m, x, y)
10749}
10750
10751fn mwnd_window_rect_for_embedded(
10752    ctx: &CommandContext,
10753    m: &globals::MwndState,
10754) -> Option<(
10755    i64,
10756    i64,
10757    i64,
10758    i64,
10759    Option<crate::runtime::ui::MwndWindowRenderState>,
10760)> {
10761    let (x, y) = m.window_pos?;
10762    let (w, h) = m.window_size?;
10763    if w <= 0 || h <= 0 {
10764        return None;
10765    }
10766    let ui_state = ctx
10767        .ui
10768        .current_mwnd_window_render_state(ctx.screen_w, ctx.screen_h)
10769        .filter(|ui| ui.x as i64 == x && ui.y as i64 == y && ui.w as i64 == w && ui.h as i64 == h);
10770    Some((x, y, w, h, ui_state))
10771}
10772
10773fn mwnd_anim_parent_from_ui_state(
10774    m: &globals::MwndState,
10775    ui: crate::runtime::ui::MwndWindowRenderState,
10776) -> ParentRenderState {
10777    let mut parent = mwnd_parent_render_state_at(m, 0, 0);
10778    parent.pos_x = ui.dx as f32;
10779    parent.pos_y = ui.dy as f32;
10780    parent.center_rep_x = ui.pivot_abs_x - ui.dx as f32;
10781    parent.center_rep_y = ui.pivot_abs_y - ui.dy as f32;
10782    parent.scale_x = ui.scale_x;
10783    parent.scale_y = ui.scale_y;
10784    parent.rotate_z = ui.rotate;
10785    parent.tr = ui.alpha as i32;
10786    parent
10787}
10788
10789fn apply_mwnd_window_anim_parent(
10790    parent: ParentRenderState,
10791    anim_parent: Option<ParentRenderState>,
10792) -> ParentRenderState {
10793    match anim_parent {
10794        Some(anim) => compose_parent_render_state(anim, parent),
10795        None => parent,
10796    }
10797}
10798
10799fn append_mwnd_embedded_object_list_sprites(
10800    ctx: &CommandContext,
10801    worlds: Option<&Vec<globals::WorldState>>,
10802    stage_idx: i64,
10803    list: &[globals::ObjectState],
10804    parent: ParentRenderState,
10805    parent_order: i64,
10806    parent_layer: i64,
10807    out: &mut Vec<RenderSprite>,
10808    object_keys: &mut HashSet<(LayerId, SpriteId)>,
10809    debug: &mut Vec<String>,
10810) {
10811    for (obj_idx, obj) in list.iter().enumerate() {
10812        if !object_participates_in_tree(obj) {
10813            continue;
10814        }
10815        append_object_tree_sprites(
10816            ctx,
10817            worlds,
10818            stage_idx,
10819            obj_idx,
10820            obj,
10821            true,
10822            parent_order,
10823            parent_layer,
10824            Some(parent),
10825            out,
10826            object_keys,
10827            debug,
10828        );
10829    }
10830}
10831
10832fn mwnd_button_parent_render_state(
10833    m: &globals::MwndState,
10834    button_idx: usize,
10835    window_x: i64,
10836    window_y: i64,
10837    window_w: i64,
10838    window_h: i64,
10839) -> ParentRenderState {
10840    let mut parent = mwnd_parent_render_state_at(m, window_x, window_y);
10841    let Some(&(pos_base, x, y)) = m.waku_button_layout.get(button_idx) else {
10842        return parent;
10843    };
10844    match pos_base {
10845        1 => {
10846            parent.pos_x += (window_w - x) as f32;
10847            parent.pos_y += y as f32;
10848        }
10849        2 => {
10850            parent.pos_x += x as f32;
10851            parent.pos_y += (window_h - y) as f32;
10852        }
10853        3 => {
10854            parent.pos_x += (window_w - x) as f32;
10855            parent.pos_y += (window_h - y) as f32;
10856        }
10857        _ => {
10858            parent.pos_x += x as f32;
10859            parent.pos_y += y as f32;
10860        }
10861    }
10862    parent
10863}
10864
10865fn mwnd_face_parent_render_state(
10866    m: &globals::MwndState,
10867    face_idx: usize,
10868    window_x: i64,
10869    window_y: i64,
10870) -> ParentRenderState {
10871    let mut parent = mwnd_parent_render_state_at(m, window_x, window_y);
10872    if let Some(&(x, y)) = m.waku_face_pos.get(face_idx) {
10873        parent.pos_x += x as f32;
10874        parent.pos_y += y as f32;
10875    }
10876    parent
10877}
10878
10879fn btnselitem_parent_render_state(item: &globals::BtnSelItemState) -> ParentRenderState {
10880    ParentRenderState {
10881        world_no: -1,
10882        pos_x: item.pos.0 as f32,
10883        pos_y: item.pos.1 as f32,
10884        pos_z: 0.0,
10885        center_rep_x: 0.0,
10886        center_rep_y: 0.0,
10887        center_rep_z: 0.0,
10888        scale_x: 1.0,
10889        scale_y: 1.0,
10890        scale_z: 1.0,
10891        rotate_x: 0.0,
10892        rotate_y: 0.0,
10893        rotate_z: 0.0,
10894        tr: if item.visible { 255 } else { 0 },
10895        mono: 0,
10896        reverse: 0,
10897        bright: 0,
10898        dark: 0,
10899        color_rate: 0,
10900        color_r: 0,
10901        color_g: 0,
10902        color_b: 0,
10903        color_add_r: 0,
10904        color_add_g: 0,
10905        color_add_b: 0,
10906        blend: crate::layer::SpriteBlend::Normal,
10907        dst_clip: None,
10908        mask_image_id: None,
10909        mask_offset_x: 0,
10910        mask_offset_y: 0,
10911        tonecurve_image_id: None,
10912        tonecurve_row: 0.0,
10913        tonecurve_sat: 0.0,
10914    }
10915}
10916
10917fn append_btnselitem_sprites(
10918    ctx: &CommandContext,
10919    worlds: Option<&Vec<globals::WorldState>>,
10920    stage_idx: i64,
10921    items: &[globals::BtnSelItemState],
10922    out: &mut Vec<RenderSprite>,
10923    object_keys: &mut HashSet<(LayerId, SpriteId)>,
10924    debug: &mut Vec<String>,
10925) {
10926    for (item_idx, item) in items.iter().enumerate() {
10927        if !item.visible {
10928            continue;
10929        }
10930        let parent = btnselitem_parent_render_state(item);
10931        let parent_order = ctx.tables.mwnd_render.order;
10932        for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
10933            append_object_tree_sprites(
10934                ctx,
10935                worlds,
10936                stage_idx,
10937                obj_idx,
10938                obj,
10939                true,
10940                parent_order,
10941                0,
10942                Some(parent),
10943                out,
10944                object_keys,
10945                debug,
10946            );
10947        }
10948        for (obj_idx, obj) in item.object_list.iter().enumerate() {
10949            append_object_tree_sprites(
10950                ctx,
10951                worlds,
10952                stage_idx,
10953                obj_idx,
10954                obj,
10955                true,
10956                parent_order,
10957                0,
10958                Some(parent),
10959                out,
10960                object_keys,
10961                debug,
10962            );
10963        }
10964        if sg_render_tree_debug_enabled() && (item.generated_objects.len() + item.object_list.len()) == 0 {
10965            debug.push(format!(
10966                "[SG_DEBUG]     btnselitem[{item_idx}] text_len={} visible={} pos=({}, {}) size=({}, {}) no_objects",
10967                item.text.chars().count(),
10968                item.visible,
10969                item.pos.0,
10970                item.pos.1,
10971                item.size.0,
10972                item.size.1,
10973            ));
10974        }
10975    }
10976}
10977
10978#[derive(Clone)]
10979struct SelBtnSpriteVisual {
10980    component: u8,
10981    action_no: i64,
10982    state: i64,
10983    base_file: Option<String>,
10984    base_patno: i64,
10985}
10986
10987fn collect_selbtn_sprite_visuals_recursive(
10988    obj: &globals::ObjectState,
10989    component: u8,
10990    action_no: i64,
10991    state: i64,
10992    map: &mut HashMap<(LayerId, SpriteId), SelBtnSpriteVisual>,
10993) {
10994    match obj.backend {
10995        globals::ObjectBackend::Rect { layer_id, sprite_id, .. }
10996        | globals::ObjectBackend::String { layer_id, sprite_id, .. }
10997        | globals::ObjectBackend::Movie { layer_id, sprite_id, .. } => {
10998            map.insert(
10999                (layer_id, sprite_id),
11000                SelBtnSpriteVisual {
11001                    component,
11002                    action_no,
11003                    state,
11004                    base_file: obj.file_name.clone(),
11005                    base_patno: obj.base.patno,
11006                },
11007            );
11008        }
11009        globals::ObjectBackend::Number { layer_id, ref sprite_ids }
11010        | globals::ObjectBackend::Weather { layer_id, ref sprite_ids } => {
11011            for &sprite_id in sprite_ids {
11012                map.insert(
11013                    (layer_id, sprite_id),
11014                    SelBtnSpriteVisual {
11015                        component,
11016                        action_no,
11017                        state,
11018                        base_file: obj.file_name.clone(),
11019                        base_patno: obj.base.patno,
11020                    },
11021                );
11022            }
11023        }
11024        globals::ObjectBackend::Gfx | globals::ObjectBackend::None => {}
11025    }
11026
11027    for child in &obj.runtime.child_objects {
11028        collect_selbtn_sprite_visuals_recursive(child, component, action_no, state, map);
11029    }
11030}
11031
11032fn apply_selbtn_item_visuals(ctx: &mut CommandContext, sprites: &mut [RenderSprite]) {
11033    let mut map: HashMap<(LayerId, SpriteId), SelBtnSpriteVisual> = HashMap::new();
11034    for st in ctx.globals.stage_forms.values() {
11035        for items in st.btnselitem_lists.values() {
11036            for item in items {
11037                if !item.visible {
11038                    continue;
11039                }
11040                for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
11041                    let component = match obj_idx {
11042                        0 => 0,
11043                        1 => 1,
11044                        _ => 2,
11045                    };
11046                    collect_selbtn_sprite_visuals_recursive(
11047                        obj,
11048                        component,
11049                        item.button_action_no,
11050                        item.button_state,
11051                        &mut map,
11052                    );
11053                }
11054                for obj in &item.object_list {
11055                    collect_selbtn_sprite_visuals_recursive(
11056                        obj,
11057                        3,
11058                        item.button_action_no,
11059                        item.button_state,
11060                        &mut map,
11061                    );
11062                }
11063            }
11064        }
11065    }
11066
11067    if map.is_empty() {
11068        return;
11069    }
11070
11071    for rs in sprites.iter_mut() {
11072        let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) else {
11073            continue;
11074        };
11075        let Some(visual) = map.get(&(lid, sid)).cloned() else {
11076            continue;
11077        };
11078        let pat = button_action_pattern(&ctx.tables, visual.action_no, visual.state);
11079
11080        rs.sprite.x = rs.sprite.x.saturating_add(pat.rep_pos_x as i32);
11081        rs.sprite.y = rs.sprite.y.saturating_add(pat.rep_pos_y as i32);
11082
11083        match visual.component {
11084            0 => {
11085                if let Some(file_name) = visual.base_file.as_deref().filter(|s| !s.is_empty()) {
11086                    let patno = visual
11087                        .base_patno
11088                        .saturating_add(pat.rep_pat_no)
11089                        .max(0) as u32;
11090                    let image_id = match ctx.images.load_g00(file_name, patno) {
11091                        Ok(id) => Some(id),
11092                        Err(_) => ctx.images.load_bg_frame(file_name, patno as usize).ok(),
11093                    };
11094                    if let Some(image_id) = image_id {
11095                        rs.sprite.image_id = Some(image_id);
11096                        if let Some(img) = ctx.images.get(image_id) {
11097                            rs.sprite.object_anchor = true;
11098                            rs.sprite.texture_center_x = img.center_x as f32;
11099                            rs.sprite.texture_center_y = img.center_y as f32;
11100                        }
11101                    }
11102                }
11103                rs.sprite.tr = ((rs.sprite.tr as i64 * pat.rep_tr.clamp(0, 255)) / 255)
11104                    .clamp(0, 255) as u8;
11105                rs.sprite.bright = (rs.sprite.bright as i64 + pat.rep_bright).clamp(0, 255) as u8;
11106                rs.sprite.dark = (rs.sprite.dark as i64 + pat.rep_dark).clamp(0, 255) as u8;
11107            }
11108            1 => {
11109                let (cfg_r, cfg_g, cfg_b, cfg_a) = ctx.syscom_filter_config_rgba();
11110                rs.sprite.alpha = 255;
11111                rs.sprite.tr = ((cfg_a as i64 * pat.rep_tr.clamp(0, 255)) / 255)
11112                    .clamp(0, 255) as u8;
11113                rs.sprite.alpha_test = true;
11114                rs.sprite.alpha_blend = true;
11115                rs.sprite.color_rate = 0;
11116                rs.sprite.color_add_r = cfg_r;
11117                rs.sprite.color_add_g = cfg_g;
11118                rs.sprite.color_add_b = cfg_b;
11119                rs.sprite.color_r = 0;
11120                rs.sprite.color_g = 0;
11121                rs.sprite.color_b = 0;
11122                rs.sprite.bright = 0;
11123                rs.sprite.dark = 0;
11124                rs.sprite.mask_mode = 0;
11125            }
11126            _ => {}
11127        }
11128    }
11129}
11130
11131fn append_mwnd_embedded_sprites(
11132    ctx: &CommandContext,
11133    worlds: Option<&Vec<globals::WorldState>>,
11134    stage_idx: i64,
11135    m: &globals::MwndState,
11136    out: &mut Vec<RenderSprite>,
11137    object_keys: &mut HashSet<(LayerId, SpriteId)>,
11138    debug: &mut Vec<String>,
11139) {
11140    if ctx.globals.script.mwnd_disp_off_flag
11141        || ctx.globals.syscom.hide_mwnd.onoff
11142        || ctx.globals.syscom.msg_back_open {
11143        if config_button_trace_enabled() {
11144            debug.push(format!(
11145                "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_SKIP] stage={} reason=hidden script_off={} sys_hide={} open={} buttons={} objects={} waku={} filter={} pos={:?} size={:?}",
11146                stage_idx,
11147                ctx.globals.script.mwnd_disp_off_flag,
11148                ctx.globals.syscom.hide_mwnd.onoff,
11149                m.open,
11150                m.button_list.len(),
11151                m.object_list.len(),
11152                if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
11153                if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
11154                m.window_pos,
11155                m.window_size
11156            ));
11157        }
11158        return;
11159    }
11160    let Some((window_x, window_y, window_w, window_h, ui_state)) =
11161        mwnd_window_rect_for_embedded(ctx, m)
11162    else {
11163        if config_button_trace_enabled() {
11164            debug.push(format!(
11165                "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_SKIP] stage={} reason=no_window_rect open={} buttons={} objects={} waku={} filter={} pos={:?} size={:?}",
11166                stage_idx,
11167                m.open,
11168                m.button_list.len(),
11169                m.object_list.len(),
11170                if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
11171                if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
11172                m.window_pos,
11173                m.window_size
11174            ));
11175        }
11176        return;
11177    };
11178    if !m.open && ui_state.is_none() {
11179        if config_button_trace_enabled() {
11180            debug.push(format!(
11181                "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_SKIP] stage={} reason=closed_no_anim open={} buttons={} objects={} waku={} filter={} rect=({}, {}, {}, {})",
11182                stage_idx,
11183                m.open,
11184                m.button_list.len(),
11185                m.object_list.len(),
11186                if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
11187                if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
11188                window_x, window_y, window_w, window_h
11189            ));
11190        }
11191        return;
11192    }
11193    let mwnd_order_source = if m.order <= 0 {
11194        ctx.tables.mwnd_render.order.max(1)
11195    } else {
11196        m.order
11197    };
11198    let mwnd_order = mwnd_order_source;
11199    let mwnd_layer = m.layer;
11200    let anim_parent = ui_state.map(|ui| mwnd_anim_parent_from_ui_state(m, ui));
11201    if config_button_trace_enabled() {
11202        debug.push(format!(
11203            "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_COLLECT] stage={} open={} buttons={} faces={} objects={} waku={} filter={} rect=({}, {}, {}, {}) ui_anim={} order={} layer={} hide_flags=(script:{},sys:{})",
11204            stage_idx,
11205            m.open,
11206            m.button_list.len(),
11207            m.face_list.len(),
11208            m.object_list.len(),
11209            if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
11210            if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
11211            window_x, window_y, window_w, window_h,
11212            anim_parent.is_some(),
11213            mwnd_order,
11214            mwnd_layer,
11215            ctx.globals.script.mwnd_disp_off_flag,
11216            ctx.globals.syscom.hide_mwnd.onoff
11217        ));
11218    }
11219    for (button_idx, obj) in m.button_list.iter().enumerate() {
11220        if !object_participates_in_tree(obj) {
11221            if config_button_trace_enabled() {
11222                debug.push(format!(
11223                    "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_BUTTON_SKIP] stage={} button_idx={} reason=not_participating file={} used={} type={} disp={} backend={:?}",
11224                    stage_idx,
11225                    button_idx,
11226                    obj.file_name.as_deref().unwrap_or("-"),
11227                    obj.used,
11228                    obj.object_type,
11229                    obj.base.disp,
11230                    obj.backend
11231                ));
11232            }
11233            continue;
11234        }
11235        let local_parent =
11236            mwnd_button_parent_render_state(m, button_idx, window_x, window_y, window_w, window_h);
11237        let parent = apply_mwnd_window_anim_parent(local_parent, anim_parent);
11238        if sg_render_tree_debug_enabled() {
11239            debug.push(format!(
11240                "[SG_DEBUG]       mwnd_button_parent[{}] file={} pos=({}, {}) local_base={:?} order={} layer={}",
11241                button_idx,
11242                obj.file_name.as_deref().unwrap_or("-"),
11243                parent.pos_x,
11244                parent.pos_y,
11245                m.waku_button_layout.get(button_idx),
11246                mwnd_order,
11247                mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
11248            ));
11249        }
11250        append_object_tree_sprites(
11251            ctx,
11252            worlds,
11253            stage_idx,
11254            button_idx,
11255            obj,
11256            true,
11257            mwnd_order,
11258            mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
11259            Some(parent),
11260            out,
11261            object_keys,
11262            debug,
11263        );
11264    }
11265    for (face_idx, obj) in m.face_list.iter().enumerate() {
11266        if !object_participates_in_tree(obj) {
11267            continue;
11268        }
11269        let parent = apply_mwnd_window_anim_parent(
11270            mwnd_face_parent_render_state(m, face_idx, window_x, window_y),
11271            anim_parent,
11272        );
11273        append_object_tree_sprites(
11274            ctx,
11275            worlds,
11276            stage_idx,
11277            face_idx,
11278            obj,
11279            true,
11280            mwnd_order,
11281            mwnd_layer.saturating_add(ctx.tables.mwnd_render.face_layer_rep),
11282            Some(parent),
11283            out,
11284            object_keys,
11285            debug,
11286        );
11287    }
11288    let parent = apply_mwnd_window_anim_parent(
11289        mwnd_parent_render_state_at(m, window_x, window_y),
11290        anim_parent,
11291    );
11292    append_mwnd_embedded_object_list_sprites(
11293        ctx,
11294        worlds,
11295        stage_idx,
11296        &m.object_list,
11297        parent,
11298        mwnd_order,
11299        mwnd_layer,
11300        out,
11301        object_keys,
11302        debug,
11303    );
11304}
11305
11306fn mwnd_sort_base(
11307    ctx: &CommandContext,
11308    m: &globals::MwndState,
11309) -> (i64, i64) {
11310    let order = if m.order <= 0 {
11311        ctx.tables.mwnd_render.order.max(1)
11312    } else {
11313        m.order
11314    };
11315    (order, m.layer)
11316}
11317
11318fn selected_mwnd_sort_base(ctx: &CommandContext) -> Option<(i64, i64)> {
11319    if let Some((focused_form, focused_stage, focused_idx)) = ctx.globals.focused_stage_mwnd {
11320        if let Some(m) = ctx
11321            .globals
11322            .stage_forms
11323            .get(&focused_form)
11324            .and_then(|st| st.mwnd_lists.get(&focused_stage))
11325            .and_then(|list| list.get(focused_idx))
11326            .filter(|m| m.open)
11327        {
11328            return Some(mwnd_sort_base(ctx, m));
11329        }
11330    }
11331
11332    let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
11333    form_ids.sort_unstable();
11334    for form_id in form_ids {
11335        let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
11336            continue;
11337        };
11338        let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
11339        stage_ids.sort_unstable();
11340        for stage_idx in stage_ids {
11341            let Some(list) = st.mwnd_lists.get(&stage_idx) else {
11342                continue;
11343            };
11344            for m in list {
11345                if m.open {
11346                    return Some(mwnd_sort_base(ctx, m));
11347                }
11348            }
11349        }
11350    }
11351    None
11352}
11353
11354fn normalize_mwnd_ui_sprite_sorter(ctx: &CommandContext, order: i32) -> (i32, i32) {
11355    let Some((mwnd_order, mwnd_layer)) = selected_mwnd_sort_base(ctx) else {
11356        return unpack_legacy_sorter_key(order);
11357    };
11358    let layer = match order {
11359        // UiRuntime stores C++ MWND-owned sprites with sentinel orders. Translate
11360        // those sentinels back to C_elm_mwnd_waku/C_elm_mwnd_moji sorter layers.
11361        1_000_000 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
11362        1_000_005 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.filter_layer_rep),
11363        1_000_008 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.face_layer_rep),
11364        1_000_010 | 1_000_020 => {
11365            mwnd_layer.saturating_add(ctx.tables.mwnd_render.moji_layer_rep)
11366        }
11367        1_000_030 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
11368        _ => return unpack_legacy_sorter_key(order),
11369    };
11370    (
11371        mwnd_order.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
11372        layer.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
11373    )
11374}
11375
11376const TNM_STAGE_FRONT_I64: i64 = 1;
11377const TNM_SEL_ITEM_TYPE_ON_I64: i64 = 1;
11378const TNM_SEL_ITEM_TYPE_READ_I64: i64 = 2;
11379const TNM_STAGE_NEXT_I64: i64 = 2;
11380
11381fn mark_all_stage_owned_sprite_keys(
11382    ctx: &CommandContext,
11383    object_keys: &mut HashSet<(LayerId, SpriteId)>,
11384) {
11385    let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
11386    form_ids.sort_unstable();
11387    for form_id in form_ids {
11388        let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
11389            continue;
11390        };
11391
11392        let mut stage_ids: Vec<i64> = st
11393            .object_lists
11394            .keys()
11395            .chain(st.mwnd_lists.keys())
11396            .chain(st.btnselitem_lists.keys())
11397            .copied()
11398            .collect();
11399        stage_ids.sort_unstable();
11400        stage_ids.dedup();
11401
11402        for stage_idx in stage_ids {
11403            if let Some(list) = st.object_lists.get(&stage_idx) {
11404                for (obj_idx, obj) in list.iter().enumerate() {
11405                    mark_object_tree_sprite_keys(ctx, stage_idx, obj_idx, obj, object_keys);
11406                }
11407            }
11408            if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
11409                for m in mwnds {
11410                    mark_mwnd_owned_sprite_keys(ctx, stage_idx, m, object_keys);
11411                }
11412            }
11413            if let Some(items) = st.btnselitem_lists.get(&stage_idx) {
11414                for item in items {
11415                    for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
11416                        mark_object_tree_sprite_keys(ctx, stage_idx, obj_idx, obj, object_keys);
11417                    }
11418                    for (obj_idx, obj) in item.object_list.iter().enumerate() {
11419                        mark_object_tree_sprite_keys(ctx, stage_idx, obj_idx, obj, object_keys);
11420                    }
11421                }
11422            }
11423        }
11424    }
11425}
11426
11427fn build_siglus_object_render_list(
11428    ctx: &CommandContext,
11429    base: &[RenderSprite],
11430    selected_stage: i64,
11431) -> (Vec<RenderSprite>, Vec<String>) {
11432    let debug_enabled = sg_render_tree_debug_enabled();
11433    let mut object_keys: HashSet<(LayerId, SpriteId)> = HashSet::new();
11434    // Original Siglus builds the draw list from C_elm_stage::get_sprite_tree()
11435    // for the selected stage. LayerManager is only a backend storage cache here;
11436    // object-owned backing sprites from BACK/NEXT or hidden objects must not leak
11437    // through the generic layer render list.
11438    mark_all_stage_owned_sprite_keys(ctx, &mut object_keys);
11439    let focused_mwnd = ctx.globals.focused_stage_mwnd;
11440    let mut object_list = Vec::new();
11441    let mut debug = Vec::new();
11442    if config_button_trace_enabled() {
11443        debug.push(format!(
11444            "[SG_DEBUG][CONFIG_BUTTON_TRACE][BUILD] selected_stage={} focused_mwnd={:?} base_len={} wipe_active={}",
11445            selected_stage,
11446            ctx.globals.focused_stage_mwnd,
11447            base.len(),
11448            ctx.globals.wipe.is_some()
11449        ));
11450    }
11451
11452    let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
11453    form_ids.sort_unstable();
11454    for form_id in form_ids {
11455        let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
11456            continue;
11457        };
11458        if debug_enabled {
11459            debug.push(format!("[SG_DEBUG] stage_form {}", form_id));
11460        }
11461        let mut stage_ids: Vec<i64> = st
11462            .object_lists
11463            .keys()
11464            .chain(st.mwnd_lists.keys())
11465            .chain(st.group_lists.keys())
11466            .chain(st.btnselitem_lists.keys())
11467            .chain(st.world_lists.keys())
11468            .chain(st.effect_lists.keys())
11469            .chain(st.quake_lists.keys())
11470            .copied()
11471            .collect();
11472        stage_ids.sort_unstable();
11473        stage_ids.dedup();
11474        for stage_idx in stage_ids {
11475            let worlds = st.world_lists.get(&stage_idx);
11476            if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
11477                for m in mwnds {
11478                    mark_mwnd_owned_sprite_keys(ctx, stage_idx, m, &mut object_keys);
11479                }
11480            }
11481
11482            let active_cnt = st
11483                .object_lists
11484                .get(&stage_idx)
11485                .map(|list| {
11486                    list.iter()
11487                        .enumerate()
11488                        .filter(|(obj_idx, o)| {
11489                            !st.is_embedded_object_slot(stage_idx, *obj_idx)
11490                                && object_participates_in_tree(o)
11491                        })
11492                        .count()
11493                })
11494                .unwrap_or(0);
11495            let mwnd_embedded_cnt = st
11496                .mwnd_lists
11497                .get(&stage_idx)
11498                .map(|mwnds| {
11499                    mwnds
11500                        .iter()
11501                        .map(|m| m.button_list.len() + m.face_list.len() + m.object_list.len())
11502                        .sum::<usize>()
11503                })
11504                .unwrap_or(0);
11505            let group_cnt = st.group_lists.get(&stage_idx).map(|v| v.len()).unwrap_or(0);
11506            let btnselitem_cnt = st
11507                .btnselitem_lists
11508                .get(&stage_idx)
11509                .map(|v| v.len())
11510                .unwrap_or(0);
11511            let world_cnt = st.world_lists.get(&stage_idx).map(|v| v.len()).unwrap_or(0);
11512            let effect_cnt = st
11513                .effect_lists
11514                .get(&stage_idx)
11515                .map(|v| v.len())
11516                .unwrap_or(0);
11517            let quake_cnt = st.quake_lists.get(&stage_idx).map(|v| v.len()).unwrap_or(0);
11518            if active_cnt == 0
11519                && mwnd_embedded_cnt == 0
11520                && group_cnt == 0
11521                && btnselitem_cnt == 0
11522                && world_cnt == 0
11523                && effect_cnt == 0
11524                && quake_cnt == 0
11525            {
11526                continue;
11527            }
11528            if debug_enabled {
11529                debug.push(format!(
11530                    "[SG_DEBUG]   stage {} active_objects={} mwnd_embedded={} groups={} btnselitems={} worlds={} effects={} quakes={}",
11531                    stage_idx, active_cnt, mwnd_embedded_cnt, group_cnt, btnselitem_cnt, world_cnt, effect_cnt, quake_cnt
11532                ));
11533                if let Some(effects) = st.effect_lists.get(&stage_idx) {
11534                    for (effect_idx, effect) in effects.iter().enumerate() {
11535                        debug.push(format!(
11536                            "[SG_DEBUG]     effect[{}] range=({},{})->({},{}) wipe_copy={} wipe_erase={} xy=({}, {}) color_rate={} bright={} dark={} tr-like-mono={}",
11537                            effect_idx,
11538                            effect.begin_order,
11539                            effect.begin_layer,
11540                            effect.end_order,
11541                            effect.end_layer,
11542                            effect.wipe_copy,
11543                            effect.wipe_erase,
11544                            effect.x.get_total_value(),
11545                            effect.y.get_total_value(),
11546                            effect.color_rate.get_total_value(),
11547                            effect.bright.get_total_value(),
11548                            effect.dark.get_total_value(),
11549                            effect.mono.get_total_value(),
11550                        ));
11551                    }
11552                }
11553                if let Some(quakes) = st.quake_lists.get(&stage_idx) {
11554                    for (quake_idx, quake) in quakes.iter().enumerate() {
11555                        debug.push(format!(
11556                            "[SG_DEBUG]     quake[{}] type={} power={} vec={} center=({}, {}) order_range={}..{} active={}",
11557                            quake_idx,
11558                            quake.quake_type,
11559                            quake.power,
11560                            quake.vec,
11561                            quake.center_x,
11562                            quake.center_y,
11563                            quake.begin_order,
11564                            quake.end_order,
11565                            quake.until.is_some(),
11566                        ));
11567                    }
11568                }
11569            }
11570            if stage_idx != selected_stage {
11571                if config_button_trace_enabled() {
11572                    let mwnd_summary = st.mwnd_lists.get(&stage_idx).map(|mwnds| {
11573                        mwnds.iter().enumerate().map(|(idx, m)| {
11574                            format!(
11575                                "{}:open={} buttons={} objects={} waku={} filter={} pos={:?} size={:?}",
11576                                idx,
11577                                m.open,
11578                                m.button_list.len(),
11579                                m.object_list.len(),
11580                                if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
11581                                if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
11582                                m.window_pos,
11583                                m.window_size
11584                            )
11585                        }).collect::<Vec<_>>()
11586                    }).unwrap_or_default();
11587                    debug.push(format!(
11588                        "[SG_DEBUG][CONFIG_BUTTON_TRACE][STAGE_SKIP] form={} stage={} selected_stage={} active_objects={} mwnd_embedded={} mwnds={:?} focused_mwnd={:?}",
11589                        form_id,
11590                        stage_idx,
11591                        selected_stage,
11592                        active_cnt,
11593                        mwnd_embedded_cnt,
11594                        mwnd_summary,
11595                        focused_mwnd
11596                    ));
11597                }
11598                if let Some((focused_form, focused_stage, focused_idx)) = focused_mwnd {
11599                    if focused_form == form_id && focused_stage == stage_idx {
11600                        if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
11601                            if let Some(m) = mwnds.get(focused_idx) {
11602                                append_mwnd_embedded_sprites(
11603                                    ctx,
11604                                    worlds,
11605                                    stage_idx,
11606                                    m,
11607                                    &mut object_list,
11608                                    &mut object_keys,
11609                                    &mut debug,
11610                                );
11611                            }
11612                        }
11613                    }
11614                }
11615                continue;
11616            }
11617            if let Some(list) = st.object_lists.get(&stage_idx) {
11618                let mut top: Vec<(usize, &globals::ObjectState)> = list
11619                    .iter()
11620                    .enumerate()
11621                    .filter(|(obj_idx, o)| {
11622                        !st.is_embedded_object_slot(stage_idx, *obj_idx)
11623                            && object_participates_in_tree(o)
11624                    })
11625                    .collect();
11626                top.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
11627                    let l = effective_object_info(ctx, stage_idx, *lhs_idx, lhs);
11628                    let r = effective_object_info(ctx, stage_idx, *rhs_idx, rhs);
11629                    (l.order, l.layer).cmp(&(r.order, r.layer))
11630                });
11631                for (obj_idx, obj) in top {
11632                    append_object_tree_sprites(
11633                        ctx,
11634                        worlds,
11635                        stage_idx,
11636                        obj_idx,
11637                        obj,
11638                        true,
11639                        0,
11640                        0,
11641                        None,
11642                        &mut object_list,
11643                        &mut object_keys,
11644                        &mut debug,
11645                    );
11646                }
11647            }
11648            if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
11649                for (mwnd_idx, m) in mwnds.iter().enumerate() {
11650                    if debug_enabled {
11651                        let embedded_cnt =
11652                            m.button_list.len() + m.face_list.len() + m.object_list.len();
11653                        if m.open
11654                            || embedded_cnt != 0
11655                            || !m.msg_text.is_empty()
11656                            || !m.name_text.is_empty()
11657                            || m.selection.is_some()
11658                        {
11659                            debug.push(format!(
11660                                "[SG_DEBUG]     mwnd[{mwnd_idx}] open={} order={} layer={} world={} msg_len={} name_len={} embedded={} button={} face={} object={} waku={} filter={} face_file={} open_anim=({}, {}) close_anim=({}, {}) selection={} hide_flags=(script:{},sys:{})",
11661                                m.open,
11662                                m.order,
11663                                m.layer,
11664                                m.world,
11665                                m.msg_text.chars().count(),
11666                                m.name_text.chars().count(),
11667                                embedded_cnt,
11668                                m.button_list.len(),
11669                                m.face_list.len(),
11670                                m.object_list.len(),
11671                                if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
11672                                if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
11673                                if m.face_file.is_empty() { "-" } else { m.face_file.as_str() },
11674                                m.open_anime_type,
11675                                m.open_anime_time,
11676                                m.close_anime_type,
11677                                m.close_anime_time,
11678                                m.selection.is_some(),
11679                                ctx.globals.script.mwnd_disp_off_flag,
11680                                ctx.globals.syscom.hide_mwnd.onoff,
11681                            ));
11682                        }
11683                    }
11684                    append_mwnd_embedded_sprites(
11685                        ctx,
11686                        worlds,
11687                        stage_idx,
11688                        m,
11689                        &mut object_list,
11690                        &mut object_keys,
11691                        &mut debug,
11692                    );
11693                }
11694            }
11695            if let Some(items) = st.btnselitem_lists.get(&stage_idx) {
11696                append_btnselitem_sprites(
11697                    ctx,
11698                    worlds,
11699                    stage_idx,
11700                    items,
11701                    &mut object_list,
11702                    &mut object_keys,
11703                    &mut debug,
11704                );
11705            }
11706        }
11707    }
11708
11709    let mut bg = Vec::new();
11710    let mut rest = Vec::new();
11711    for rs in base {
11712        match (rs.layer_id, rs.sprite_id) {
11713            (Some(lid), Some(sid)) if object_keys.contains(&(lid, sid)) => {}
11714            (None, None) if render_sprite_visible_for_submit(rs) => bg.push(rs.clone()),
11715            (None, None) => {}
11716            _ if render_sprite_visible_for_submit(rs) => rest.push(rs.clone()),
11717            _ => {}
11718        }
11719    }
11720
11721    let rest_len = rest.len();
11722    let mut ordered: Vec<(i32, i32, usize, RenderSprite)> =
11723        Vec::with_capacity(rest.len() + object_list.len());
11724    for (idx, mut rs) in rest.into_iter().enumerate() {
11725        // LayerManager ids are storage handles. They are not Siglus script-layer
11726        // values. For MWND UI-runtime sprites, translate the sentinel order into
11727        // the same C++ S_tnm_sorter(order, layer) pair that C_elm_mwnd_waku uses.
11728        let (order, layer) = normalize_mwnd_ui_sprite_sorter(ctx, rs.sprite.order);
11729        rs.set_sorter(order as i64, layer as i64);
11730        rs.sprite.order = legacy_packed_sorter_key(order as i64, layer as i64);
11731        ordered.push((order, layer, idx, rs));
11732    }
11733    for (idx, rs) in object_list.into_iter().enumerate() {
11734        // Object tree sprites carry the original C++ S_tnm_sorter(order, layer)
11735        // separately from the backend layer id. Do not derive ordering from
11736        // LayerManager; it is only storage.
11737        ordered.push((
11738            rs.sorter_order,
11739            rs.sorter_layer,
11740            rest_len.saturating_add(idx),
11741            rs,
11742        ));
11743    }
11744    ordered.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
11745
11746    let mut final_list = Vec::with_capacity(bg.len() + ordered.len());
11747    final_list.extend(bg);
11748    final_list.extend(ordered.into_iter().map(|(_, _, _, rs)| rs));
11749    (final_list, debug)
11750}
11751
11752fn trace_codes_enabled() -> bool {
11753    std::env::var_os("SIGLUS_TRACE_CODES").is_some()
11754}
11755
11756pub fn dispatch_form_code(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
11757    ctx.images
11758        .set_current_append_dir(ctx.globals.append_dir.clone());
11759    ctx.movie
11760        .set_current_append_dir(ctx.globals.append_dir.clone());
11761    ctx.bgm
11762        .set_current_append_dir(ctx.globals.append_dir.clone());
11763
11764    let code = opcode::OpCode::form(form_id);
11765    if trace_codes_enabled() {
11766        let chain = ctx
11767            .vm_call
11768            .as_ref()
11769            .map(|call| call.element.clone())
11770            .unwrap_or_default();
11771        eprintln!(
11772            "[TRACE code] form={} chain={chain:?} argc={} args={args:?}",
11773            form_id,
11774            args.len()
11775        );
11776    }
11777
11778    opcode::dispatch_code(ctx, code, args)
11779}
11780
11781pub fn dispatch_named_command(
11782    ctx: &mut CommandContext,
11783    name: &str,
11784    args: &[Value],
11785) -> Result<bool> {
11786    let cmd = Command {
11787        name: name.to_string(),
11788        code: None,
11789        args: args.to_vec(),
11790    };
11791
11792    if commands::misc::handle(ctx, &cmd)? {
11793        return Ok(true);
11794    }
11795    if commands::text::handle(ctx, &cmd)? {
11796        return Ok(true);
11797    }
11798    if commands::audio::handle(ctx, &cmd)? {
11799        return Ok(true);
11800    }
11801    if commands::bg::handle(ctx, &cmd)? {
11802        return Ok(true);
11803    }
11804    if commands::chr::handle(ctx, &cmd)? {
11805        return Ok(true);
11806    }
11807    if commands::layer::handle(ctx, &cmd)? {
11808        return Ok(true);
11809    }
11810
11811    Ok(false)
11812}
11813
11814pub fn dispatch(ctx: &mut CommandContext, cmd: &Command) -> Result<()> {
11815    if let Some(code) = cmd.code {
11816        let handled = dispatch_form_code(ctx, code.id, &cmd.args)?;
11817        if !handled {
11818            anyhow::bail!("unhandled form code {}", code.id);
11819        }
11820        return Ok(());
11821    }
11822
11823    let handled = dispatch_named_command(ctx, &cmd.name, &cmd.args)?;
11824    if !handled {
11825        anyhow::bail!("unhandled command {}", cmd.name);
11826    }
11827    Ok(())
11828}
11829
11830fn apply_button_visuals(ctx: &mut CommandContext, sprites: &mut [RenderSprite]) {
11831    let mut map: HashMap<(LayerId, SpriteId), ButtonVisualState> = HashMap::new();
11832
11833    let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
11834    form_ids.sort_unstable();
11835    for form_id in form_ids {
11836        let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
11837            continue;
11838        };
11839        let mut stage_ids: Vec<i64> = st
11840            .object_lists
11841            .keys()
11842            .chain(st.mwnd_lists.keys())
11843            .chain(st.btnselitem_lists.keys())
11844            .copied()
11845            .collect();
11846        stage_ids.sort_unstable();
11847        stage_ids.dedup();
11848        for stage_idx in stage_ids {
11849            if let Some(objs) = st.object_lists.get(&stage_idx) {
11850                for (obj_idx, obj) in objs.iter().enumerate() {
11851                    collect_button_visuals_recursive(
11852                        ctx, st, stage_idx, obj_idx, obj, &mut map, None, None,
11853                    );
11854                }
11855            }
11856            if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
11857                for m in mwnds {
11858                    for (obj_idx, obj) in m.button_list.iter().enumerate() {
11859                        collect_button_visuals_recursive(
11860                            ctx,
11861                            st,
11862                            stage_idx,
11863                            obj_idx,
11864                            obj,
11865                            &mut map,
11866                            None,
11867                            Some(obj_idx),
11868                        );
11869                    }
11870                    for (obj_idx, obj) in m.face_list.iter().enumerate() {
11871                        collect_button_visuals_recursive(
11872                            ctx, st, stage_idx, obj_idx, obj, &mut map, None, None,
11873                        );
11874                    }
11875                    for (obj_idx, obj) in m.object_list.iter().enumerate() {
11876                        collect_button_visuals_recursive(
11877                            ctx, st, stage_idx, obj_idx, obj, &mut map, None, None,
11878                        );
11879                    }
11880                }
11881            }
11882            if let Some(items) = st.btnselitem_lists.get(&stage_idx) {
11883                for item in items {
11884                    for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
11885                        collect_button_visuals_recursive(
11886                            ctx, st, stage_idx, obj_idx, obj, &mut map, None, None,
11887                        );
11888                    }
11889                    for (obj_idx, obj) in item.object_list.iter().enumerate() {
11890                        collect_button_visuals_recursive(
11891                            ctx, st, stage_idx, obj_idx, obj, &mut map, None, None,
11892                        );
11893                    }
11894                }
11895            }
11896        }
11897    }
11898
11899    if map.is_empty() {
11900        return;
11901    }
11902
11903    for rs in sprites.iter_mut() {
11904        let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) else {
11905            continue;
11906        };
11907        let Some(visual) = map.get(&(lid, sid)).cloned() else {
11908            continue;
11909        };
11910        apply_button_state_visual(&ctx.tables, &mut ctx.images, &mut rs.sprite, visual);
11911    }
11912}
11913
11914fn collect_button_visuals_recursive(
11915    ctx: &CommandContext,
11916    st: &globals::StageFormState,
11917    stage_idx: i64,
11918    obj_idx: usize,
11919    obj: &globals::ObjectState,
11920    map: &mut HashMap<(LayerId, SpriteId), ButtonVisualState>,
11921    inherited_visual: Option<ButtonVisualState>,
11922    mwnd_button_idx: Option<usize>,
11923) {
11924    use globals::ObjectBackend;
11925
11926    let mut effective_visual = inherited_visual;
11927    if obj.button.enabled || obj.button.state == TNM_BTN_STATE_DISABLE {
11928        if !button_syscom_mode_visible(&ctx.globals.syscom, &obj.button) {
11929            effective_visual = None;
11930        } else {
11931            let state = button_real_state_for_visual(
11932                &ctx.globals.syscom,
11933                st,
11934                stage_idx,
11935                obj,
11936                mwnd_button_idx,
11937            );
11938            if sg_debug_enabled() {
11939                let runtime_slot = object_runtime_slot(obj_idx, obj);
11940                eprintln!(
11941                    "[SG_DEBUG][BUTTON_TRACE][VISUAL] collect stage={} obj_idx={} runtime_slot={} file={:?} mwnd_button_idx={:?} state={}({}) raw_state={} enabled={} visible={} disabled_reason={:?} button_no={} group_no={} group_idx={:?} action_no={} cut_no={} hit={} pushed={} sys_type={} sys_opt={} mode={} call={}::{}/{}",
11942                    stage_idx,
11943                    obj_idx,
11944                    runtime_slot,
11945                    obj.file_name,
11946                    mwnd_button_idx,
11947                    state,
11948                    button_state_name(state),
11949                    obj.button.state,
11950                    obj.button.enabled,
11951                    button_syscom_mode_visible(&ctx.globals.syscom, &obj.button),
11952                    button_disabled_reason(&ctx.globals.syscom, obj, mwnd_button_idx),
11953                    obj.button.button_no,
11954                    obj.button.group_no,
11955                    obj.button.group_idx(),
11956                    obj.button.action_no,
11957                    obj.button.cut_no,
11958                    obj.button.hit,
11959                    obj.button.pushed,
11960                    obj.button.sys_type,
11961                    obj.button.sys_type_opt,
11962                    obj.button.mode,
11963                    obj.button.decided_action_scn_name,
11964                    obj.button.decided_action_cmd_name,
11965                    obj.button.decided_action_z_no
11966                );
11967            }
11968            let base_patno = obj
11969                .lookup_int_prop(&ctx.ids, ctx.ids.obj_patno)
11970                .unwrap_or(obj.base.patno);
11971            effective_visual = Some(ButtonVisualState {
11972                state,
11973                action_no: obj.button.action_no,
11974                file_name: obj.file_name.clone(),
11975                base_patno,
11976                cut_no: obj.button.cut_no,
11977            });
11978        }
11979    }
11980
11981    if let Some(visual) = effective_visual.clone() {
11982        let runtime_slot = object_runtime_slot(obj_idx, obj);
11983        match &obj.backend {
11984            ObjectBackend::Gfx => {
11985                if let Some((lid, sid)) = ctx
11986                    .gfx
11987                    .object_sprite_binding(stage_idx, runtime_slot as i64)
11988                {
11989                    map.insert((lid, sid), visual.clone());
11990                }
11991            }
11992            ObjectBackend::Rect {
11993                layer_id,
11994                sprite_id,
11995                ..
11996            } => {
11997                map.insert((*layer_id, *sprite_id), visual.clone());
11998            }
11999            ObjectBackend::String {
12000                layer_id,
12001                sprite_id,
12002                ..
12003            } => {
12004                map.insert((*layer_id, *sprite_id), visual.clone());
12005            }
12006            ObjectBackend::Movie {
12007                layer_id,
12008                sprite_id,
12009                ..
12010            } => {
12011                map.insert((*layer_id, *sprite_id), visual.clone());
12012            }
12013            ObjectBackend::Number {
12014                layer_id,
12015                sprite_ids,
12016            }
12017            | ObjectBackend::Weather {
12018                layer_id,
12019                sprite_ids,
12020            } => {
12021                for sid in sprite_ids {
12022                    map.insert((*layer_id, *sid), visual.clone());
12023                }
12024            }
12025            ObjectBackend::None => {}
12026        }
12027    }
12028
12029    for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
12030        collect_button_visuals_recursive(
12031            ctx,
12032            st,
12033            stage_idx,
12034            child_idx,
12035            child,
12036            map,
12037            effective_visual.clone(),
12038            None,
12039        );
12040    }
12041}
12042
12043fn button_action_pattern(
12044    tables: &tables::AssetTables,
12045    action_no: i64,
12046    state: i64,
12047) -> tables::ButtonActionPattern {
12048    let state_idx = state.clamp(0, 4) as usize;
12049    if action_no >= 0 {
12050        if let Some(tpl) = tables.button_action_templates.get(action_no as usize) {
12051            return tpl.state[state_idx];
12052        }
12053    }
12054    tables::ButtonActionTemplate::default().state[state_idx]
12055}
12056
12057fn apply_button_state_visual(
12058    tables: &tables::AssetTables,
12059    images: &mut ImageManager,
12060    sprite: &mut Sprite,
12061    visual: ButtonVisualState,
12062) {
12063    let pat = button_action_pattern(tables, visual.action_no, visual.state);
12064
12065    if let Some(file_name) = visual.file_name.as_deref().filter(|s| !s.is_empty()) {
12066        let patno = visual
12067            .base_patno
12068            .saturating_add(visual.cut_no)
12069            .saturating_add(pat.rep_pat_no)
12070            .max(0) as u32;
12071        let image_id = match images.load_g00(file_name, patno) {
12072            Ok(id) => Some(id),
12073            Err(_) => images.load_bg_frame(file_name, patno as usize).ok(),
12074        };
12075        if let Some(image_id) = image_id {
12076            sprite.image_id = Some(image_id);
12077            if let Some(img) = images.get(image_id) {
12078                sprite.object_anchor = true;
12079                sprite.texture_center_x = img.center_x as f32;
12080                sprite.texture_center_y = img.center_y as f32;
12081            } else {
12082                sprite.object_anchor = false;
12083                sprite.texture_center_x = 0.0;
12084                sprite.texture_center_y = 0.0;
12085            }
12086        }
12087    }
12088    sprite.x = sprite.x.saturating_add(pat.rep_pos_x as i32);
12089    sprite.y = sprite.y.saturating_add(pat.rep_pos_y as i32);
12090    sprite.tr = ((sprite.tr as i64 * pat.rep_tr.clamp(0, 255)) / 255).clamp(0, 255) as u8;
12091    sprite.bright = (sprite.bright as i64 + pat.rep_bright).clamp(0, 255) as u8;
12092    sprite.dark = (sprite.dark as i64 + pat.rep_dark).clamp(0, 255) as u8;
12093}
12094
12095fn unpack_legacy_sorter_key(order: i32) -> (i32, i32) {
12096    if order.abs() >= 1024 {
12097        (order.div_euclid(1024), order.rem_euclid(1024))
12098    } else {
12099        (0, order)
12100    }
12101}
12102
12103fn legacy_packed_sorter_key(order: i64, layer: i64) -> i32 {
12104    order
12105        .clamp(i32::MIN as i64 / 1024, i32::MAX as i64 / 1024)
12106        .saturating_mul(1024)
12107        .saturating_add(layer.clamp(-1023, 1023)) as i32
12108}
12109
12110fn sorter_key(order: i32, layer: i32) -> (i32, i32) {
12111    (order, layer)
12112}
12113
12114fn sprite_sorter_key(rs: &RenderSprite) -> (i32, i32) {
12115    (rs.sorter_order, rs.sorter_layer)
12116}
12117
12118fn quake_order_affects_sprite(quake: &globals::ScreenQuakeState, rs: &RenderSprite) -> bool {
12119    let order = rs.sorter_order;
12120    let (lo, hi) = if quake.begin_order <= quake.end_order {
12121        (quake.begin_order, quake.end_order)
12122    } else {
12123        (quake.end_order, quake.begin_order)
12124    };
12125    lo <= order && order <= hi
12126}
12127
12128fn apply_quake(globals: &globals::GlobalState, sprites: &mut [RenderSprite]) {
12129    let mut dx_total: f32 = 0.0;
12130    let mut dy_total: f32 = 0.0;
12131    let mut screen_form_ids: Vec<u32> = globals.screen_forms.keys().copied().collect();
12132    screen_form_ids.sort_unstable();
12133    for form_id in screen_form_ids {
12134        let Some(st) = globals.screen_forms.get(&form_id) else {
12135            continue;
12136        };
12137        if let Some(t) = st.shake.until {
12138            if crate::platform_time::Instant::now() < t {
12139                let power = 6.0f32;
12140                let ms = crate::platform_time::unix_time_millis() as f32;
12141                dx_total += (ms * 0.021).sin() * power;
12142                dy_total += (ms * 0.019).cos() * power;
12143            }
12144        }
12145        for quake in &st.quake_list {
12146            if quake.until.is_none() {
12147                continue;
12148            }
12149            let power = quake.power.min(32) as f32;
12150            if power <= 0.0 {
12151                continue;
12152            }
12153            let t = crate::platform_time::unix_time_millis() as f32;
12154            let mut dx = (t * 0.02).sin() * power;
12155            let mut dy = (t * 0.017).cos() * power;
12156            if quake.vec != 0 {
12157                let angle = (quake.vec as f32) * std::f32::consts::PI / 180.0;
12158                let (s, c) = angle.sin_cos();
12159                let rx = dx * c - dy * s;
12160                let ry = dx * s + dy * c;
12161                dx = rx;
12162                dy = ry;
12163            }
12164            dx_total += dx;
12165            dy_total += dy;
12166        }
12167    }
12168
12169    let mut stage_quakes: Vec<globals::ScreenQuakeState> = Vec::new();
12170    let mut stage_form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
12171    stage_form_ids.sort_unstable();
12172    for form_id in stage_form_ids {
12173        let Some(st) = globals.stage_forms.get(&form_id) else {
12174            continue;
12175        };
12176        let mut stage_ids: Vec<i64> = st.quake_lists.keys().copied().collect();
12177        stage_ids.sort_unstable();
12178        for stage_idx in stage_ids {
12179            if stage_idx != TNM_STAGE_FRONT_I64 {
12180                continue;
12181            }
12182            if let Some(quakes) = st.quake_lists.get(&stage_idx) {
12183                stage_quakes.extend(quakes.iter().filter(|q| q.until.is_some()).cloned());
12184            }
12185        }
12186    }
12187
12188    let apply_to_all = dx_total != 0.0 || dy_total != 0.0;
12189    if !apply_to_all && stage_quakes.is_empty() {
12190        return;
12191    }
12192
12193    for rs in sprites.iter_mut() {
12194        let mut dx = if apply_to_all { dx_total } else { 0.0 };
12195        let mut dy = if apply_to_all { dy_total } else { 0.0 };
12196        for quake in &stage_quakes {
12197            if !quake_order_affects_sprite(quake, rs) {
12198                continue;
12199            }
12200            let power = quake.power.min(32) as f32;
12201            if power <= 0.0 {
12202                continue;
12203            }
12204            let t = crate::platform_time::unix_time_millis() as f32;
12205            let mut qdx = (t * 0.02).sin() * power;
12206            let mut qdy = (t * 0.017).cos() * power;
12207            if quake.vec != 0 {
12208                let angle = (quake.vec as f32) * std::f32::consts::PI / 180.0;
12209                let (s, c) = angle.sin_cos();
12210                let rx = qdx * c - qdy * s;
12211                let ry = qdx * s + qdy * c;
12212                qdx = rx;
12213                qdy = ry;
12214            }
12215            dx += qdx;
12216            dy += qdy;
12217        }
12218        if dx != 0.0 || dy != 0.0 {
12219            rs.sprite.x = rs.sprite.x.saturating_add(dx as i32);
12220            rs.sprite.y = rs.sprite.y.saturating_add(dy as i32);
12221        }
12222    }
12223}
12224
12225#[derive(Debug, Clone, Copy)]
12226struct EffectParam {
12227    x: i32,
12228    y: i32,
12229    mono: i32,
12230    reverse: i32,
12231    bright: i32,
12232    dark: i32,
12233    color_r: i32,
12234    color_g: i32,
12235    color_b: i32,
12236    color_rate: i32,
12237    color_add_r: i32,
12238    color_add_g: i32,
12239    color_add_b: i32,
12240    begin_order: i32,
12241    begin_layer: i32,
12242    end_order: i32,
12243    end_layer: i32,
12244}
12245
12246fn apply_screen_effects(
12247    globals: &globals::GlobalState,
12248    ids: &constants::RuntimeConstants,
12249    sprites: &mut [RenderSprite],
12250) {
12251    let effects = collect_screen_effects(globals, ids);
12252    if effects.is_empty() {
12253        return;
12254    }
12255    for effect in &effects {
12256        for rs in sprites.iter_mut() {
12257            if !in_sorter_range(rs, effect) {
12258                continue;
12259            }
12260            apply_effect_to_sprite(&mut rs.sprite, effect);
12261        }
12262    }
12263}
12264
12265fn read_effect_event(ev: &crate::runtime::int_event::IntEvent) -> i32 {
12266    ev.get_total_value() as i32
12267}
12268
12269fn effect_param_from_state(effect: &globals::ScreenEffectState) -> EffectParam {
12270    EffectParam {
12271        x: read_effect_event(&effect.x),
12272        y: read_effect_event(&effect.y),
12273        mono: read_effect_event(&effect.mono),
12274        reverse: read_effect_event(&effect.reverse),
12275        bright: read_effect_event(&effect.bright),
12276        dark: read_effect_event(&effect.dark),
12277        color_r: read_effect_event(&effect.color_r),
12278        color_g: read_effect_event(&effect.color_g),
12279        color_b: read_effect_event(&effect.color_b),
12280        color_rate: read_effect_event(&effect.color_rate),
12281        color_add_r: read_effect_event(&effect.color_add_r),
12282        color_add_g: read_effect_event(&effect.color_add_g),
12283        color_add_b: read_effect_event(&effect.color_add_b),
12284        begin_order: effect.begin_order,
12285        begin_layer: effect.begin_layer,
12286        end_order: effect.end_order,
12287        end_layer: effect.end_layer,
12288    }
12289}
12290
12291fn collect_screen_effects(
12292    globals: &globals::GlobalState,
12293    _ids: &constants::RuntimeConstants,
12294) -> Vec<EffectParam> {
12295    let mut out = Vec::new();
12296    let mut screen_form_ids: Vec<u32> = globals.screen_forms.keys().copied().collect();
12297    screen_form_ids.sort_unstable();
12298    for form_id in screen_form_ids {
12299        let Some(st) = globals.screen_forms.get(&form_id) else {
12300            continue;
12301        };
12302        for effect in &st.effect_list {
12303            let rp = effect_param_from_state(effect);
12304            if effect_is_use(&rp) {
12305                out.push(rp);
12306            }
12307        }
12308    }
12309
12310    let mut stage_form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
12311    stage_form_ids.sort_unstable();
12312    for form_id in stage_form_ids {
12313        let Some(st) = globals.stage_forms.get(&form_id) else {
12314            continue;
12315        };
12316        let mut stage_ids: Vec<i64> = st.effect_lists.keys().copied().collect();
12317        stage_ids.sort_unstable();
12318        for stage_idx in stage_ids {
12319            if stage_idx != TNM_STAGE_FRONT_I64 {
12320                continue;
12321            }
12322            let Some(effects) = st.effect_lists.get(&stage_idx) else {
12323                continue;
12324            };
12325            for effect in effects {
12326                let rp = effect_param_from_state(effect);
12327                if effect_is_use(&rp) {
12328                    out.push(rp);
12329                }
12330            }
12331        }
12332    }
12333    out
12334}
12335
12336fn effect_is_use(effect: &EffectParam) -> bool {
12337    effect.x != 0
12338        || effect.y != 0
12339        || effect.mono != 0
12340        || effect.reverse != 0
12341        || effect.bright != 0
12342        || effect.dark != 0
12343        || effect.color_r != 0
12344        || effect.color_g != 0
12345        || effect.color_b != 0
12346        || effect.color_rate != 0
12347        || effect.color_add_r != 0
12348        || effect.color_add_g != 0
12349        || effect.color_add_b != 0
12350}
12351
12352fn in_sorter_range(rs: &RenderSprite, effect: &EffectParam) -> bool {
12353    let key = sprite_sorter_key(rs);
12354    let begin = sorter_key(effect.begin_order, effect.begin_layer);
12355    let end = sorter_key(effect.end_order, effect.end_layer);
12356    if begin <= end {
12357        begin <= key && key <= end
12358    } else {
12359        end <= key && key <= begin
12360    }
12361}
12362
12363fn apply_effect_to_sprite(sprite: &mut Sprite, effect: &EffectParam) {
12364    sprite.x = sprite.x.saturating_add(effect.x);
12365    sprite.y = sprite.y.saturating_add(effect.y);
12366
12367    sprite.mono = combine_lerp(sprite.mono, effect.mono);
12368    sprite.reverse = combine_lerp(sprite.reverse, effect.reverse);
12369    sprite.bright = combine_lerp(sprite.bright, effect.bright);
12370    sprite.dark = combine_lerp(sprite.dark, effect.dark);
12371
12372    // Color rate uses the original blend formula.
12373    let sr = sprite.color_rate as i32;
12374    let pr = clamp_u8(effect.color_rate);
12375    if sr + pr > 0 {
12376        let parent_rate = (pr * 255 * 255) / (255 * 255 - (255 - sr) * (255 - pr));
12377        sprite.color_r = blend_color(sprite.color_r, effect.color_r, parent_rate);
12378        sprite.color_g = blend_color(sprite.color_g, effect.color_g, parent_rate);
12379        sprite.color_b = blend_color(sprite.color_b, effect.color_b, parent_rate);
12380    }
12381    sprite.color_rate = (255 - (255 - sr) * (255 - pr) / 255) as u8;
12382
12383    sprite.color_add_r = clamp_add(sprite.color_add_r, effect.color_add_r);
12384    sprite.color_add_g = clamp_add(sprite.color_add_g, effect.color_add_g);
12385    sprite.color_add_b = clamp_add(sprite.color_add_b, effect.color_add_b);
12386}
12387
12388fn combine_lerp(base: u8, parent: i32) -> u8 {
12389    let parent = clamp_u8(parent);
12390    (255 - (255 - base as i32) * (255 - parent) / 255) as u8
12391}
12392
12393fn blend_color(base: u8, parent: i32, rate: i32) -> u8 {
12394    let parent = clamp_u8(parent);
12395    let base = base as i32;
12396    ((base * (255 - rate) + parent * rate) / 255) as u8
12397}
12398
12399fn clamp_u8(v: i32) -> i32 {
12400    v.clamp(0, 255)
12401}
12402
12403fn clamp_add(base: u8, add: i32) -> u8 {
12404    let v = base as i32 + add;
12405    v.clamp(0, 255) as u8
12406}
12407
12408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12409enum WipePartition {
12410    Under,
12411    Target,
12412    Over,
12413}
12414
12415fn render_sprite_sorter(rs: &RenderSprite) -> (i32, i32) {
12416    // LayerId/SpriteId are backend storage handles and must not be used for
12417    // Siglus wipe/effect ranges. Use the C++ S_tnm_sorter pair carried by the
12418    // submitted render sprite.
12419    (rs.sorter_order, rs.sorter_layer)
12420}
12421
12422fn classify_wipe_partition(
12423    rs: &RenderSprite,
12424    begin_layer: i32,
12425    end_layer: i32,
12426    begin_order: i32,
12427    end_order: i32,
12428    with_low: bool,
12429) -> WipePartition {
12430    let (order, layer) = render_sprite_sorter(rs);
12431    if layer < begin_layer {
12432        return WipePartition::Under;
12433    }
12434    if layer > end_layer {
12435        return WipePartition::Over;
12436    }
12437    let affected = if with_low {
12438        order <= end_order
12439    } else {
12440        order >= begin_order && order <= end_order
12441    };
12442    if affected {
12443        WipePartition::Target
12444    } else if order < begin_order {
12445        WipePartition::Under
12446    } else {
12447        WipePartition::Over
12448    }
12449}
12450
12451fn upsert_runtime_image_slot(
12452    images: &mut ImageManager,
12453    slot: &mut Option<ImageId>,
12454    img: RgbaImage,
12455) -> ImageId {
12456    if let Some(id) = *slot {
12457        let _ = images.replace_image(id, img);
12458        id
12459    } else {
12460        let id = images.insert_image(img);
12461        *slot = Some(id);
12462        id
12463    }
12464}
12465
12466fn overlay_precompose_if_needed(ctx: &mut CommandContext, _sprites: &mut Vec<RenderSprite>) {
12467    // Overlay composition now stays in the GPU renderer. Keep the runtime slot cleared so
12468    // stale CPU fallback images are not reused by other paths.
12469    ctx.overlay_rt_image = None;
12470}
12471
12472fn sprite_forward_dir(sprite: &Sprite) -> [f32; 3] {
12473    let (sx, cx) = sprite.rotate_x.sin_cos();
12474    let (sy, cy) = sprite.rotate_y.sin_cos();
12475    let x = -sy * cx;
12476    let y = sx;
12477    let z = -cy * cx;
12478    let len = (x * x + y * y + z * z).sqrt().max(1e-6);
12479    [x / len, y / len, z / len]
12480}
12481
12482fn apply_runtime_light_and_fog(ctx: &CommandContext, sprite: &mut Sprite) {
12483    sprite.light_enabled = false;
12484    sprite.mesh_runtime_lights.clear();
12485    sprite.light_diffuse = [1.0, 1.0, 1.0, 1.0];
12486    sprite.light_ambient = [0.0, 0.0, 0.0, 1.0];
12487    sprite.light_specular = [0.0, 0.0, 0.0, 1.0];
12488    sprite.light_factor = 0.0;
12489    sprite.light_kind = -1;
12490    sprite.light_pos = [0.0, 0.0, 0.0, 0.0];
12491    sprite.light_dir = [0.0, 0.0, -1.0, 0.0];
12492    sprite.light_atten = [1.0, 0.0, 0.0, 5000.0];
12493    sprite.light_cone = [0.0, 0.0, 1.0, 0.0];
12494    sprite.shadow_cast = sprite.mesh_kind != 0;
12495    sprite.shadow_receive = sprite.mesh_kind != 0;
12496    sprite.fog_enabled = false;
12497    sprite.fog_color = [0.0, 0.0, 0.0, 1.0];
12498    sprite.fog_near = 0.0;
12499    sprite.fog_far = 0.0;
12500    sprite.fog_scroll_x = 0.0;
12501    sprite.fog_texture_image_id = None;
12502
12503    if sprite.light_no >= 0 {
12504        let camera_default_light;
12505        let light = if let Some(light) = ctx.globals.lights.get(&sprite.light_no) {
12506            Some(light)
12507        } else if sprite.light_no == 0 {
12508            camera_default_light = siglus_default_camera_light(sprite);
12509            Some(&camera_default_light)
12510        } else {
12511            None
12512        };
12513        if let Some(light) = light {
12514            let n = if sprite.billboard {
12515                [0.0, 0.0, -1.0]
12516            } else {
12517                sprite_forward_dir(sprite)
12518            };
12519            let pos = [sprite.x as f32, sprite.y as f32, sprite.z];
12520            let mut ndotl = 0.0f32;
12521            let mut attenuation = 1.0f32;
12522            match light.kind {
12523                globals::LightType::Directional => {
12524                    let l = [-light.dir[0], -light.dir[1], -light.dir[2]];
12525                    let ll = (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt().max(1e-6);
12526                    ndotl = (n[0] * (l[0] / ll) + n[1] * (l[1] / ll) + n[2] * (l[2] / ll)).max(0.0);
12527                }
12528                globals::LightType::Point
12529                | globals::LightType::Spot
12530                | globals::LightType::ShadowMapSpot => {
12531                    let mut l = [
12532                        light.pos[0] - pos[0],
12533                        light.pos[1] - pos[1],
12534                        light.pos[2] - pos[2],
12535                    ];
12536                    let dist = (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt().max(1e-6);
12537                    l = [l[0] / dist, l[1] / dist, l[2] / dist];
12538                    ndotl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
12539                    attenuation = 1.0
12540                        / (light.attenuation0
12541                            + light.attenuation1 * dist
12542                            + light.attenuation2 * dist * dist)
12543                            .max(1.0);
12544                    if light.range > 0.0 {
12545                        attenuation *= (1.0 - dist / light.range).clamp(0.0, 1.0);
12546                    }
12547                    if matches!(
12548                        light.kind,
12549                        globals::LightType::Spot | globals::LightType::ShadowMapSpot
12550                    ) {
12551                        let spot_dir = [light.dir[0], light.dir[1], light.dir[2]];
12552                        let sll = (spot_dir[0] * spot_dir[0]
12553                            + spot_dir[1] * spot_dir[1]
12554                            + spot_dir[2] * spot_dir[2])
12555                            .sqrt()
12556                            .max(1e-6);
12557                        let cosang = (l[0] * (-spot_dir[0] / sll)
12558                            + l[1] * (-spot_dir[1] / sll)
12559                            + l[2] * (-spot_dir[2] / sll))
12560                            .clamp(-1.0, 1.0);
12561                        let cos_theta = (light.theta_deg.to_radians() * 0.5).cos();
12562                        let cos_phi = (light.phi_deg.to_radians() * 0.5).cos();
12563                        let spot = if cosang >= cos_theta {
12564                            1.0
12565                        } else if cosang <= cos_phi {
12566                            0.0
12567                        } else {
12568                            ((cosang - cos_phi) / (cos_theta - cos_phi).max(1e-6))
12569                                .powf(light.falloff.max(0.01))
12570                        };
12571                        attenuation *= spot;
12572                    }
12573                }
12574                globals::LightType::None => {}
12575            }
12576            sprite.light_enabled = !matches!(light.kind, globals::LightType::None);
12577            sprite.light_diffuse = light.diffuse;
12578            sprite.light_ambient = light.ambient;
12579            sprite.light_specular = light.specular;
12580            sprite.light_factor = (ndotl * attenuation).clamp(0.0, 1.0);
12581            sprite.light_kind = light.kind as i32;
12582            sprite.light_pos = [light.pos[0], light.pos[1], light.pos[2], 1.0];
12583            let dir_len = (light.dir[0] * light.dir[0]
12584                + light.dir[1] * light.dir[1]
12585                + light.dir[2] * light.dir[2])
12586                .sqrt()
12587                .max(1e-6);
12588            sprite.light_dir = [
12589                light.dir[0] / dir_len,
12590                light.dir[1] / dir_len,
12591                light.dir[2] / dir_len,
12592                0.0,
12593            ];
12594            sprite.light_atten = [
12595                light.attenuation0,
12596                light.attenuation1,
12597                light.attenuation2,
12598                light.range,
12599            ];
12600            sprite.light_cone = [
12601                (light.theta_deg.to_radians() * 0.5).cos(),
12602                (light.phi_deg.to_radians() * 0.5).cos(),
12603                light.falloff,
12604                if matches!(light.kind, globals::LightType::ShadowMapSpot) {
12605                    1.0
12606                } else {
12607                    0.0
12608                },
12609            ];
12610            if matches!(light.kind, globals::LightType::ShadowMapSpot) {
12611                sprite.shadow_cast = sprite.mesh_kind != 0;
12612                sprite.shadow_receive = sprite.camera_enabled;
12613            }
12614        }
12615    }
12616
12617    if sprite.mesh_kind != 0 || sprite.camera_enabled {
12618        let mut ids: Vec<i32> = ctx.globals.lights.keys().copied().collect();
12619        if !ctx.globals.lights.contains_key(&0) {
12620            ids.push(0);
12621        }
12622        ids.sort_unstable();
12623        ids.dedup();
12624        for light_id in ids {
12625            let camera_default_light;
12626            let light = if let Some(light) = ctx.globals.lights.get(&light_id) {
12627                light
12628            } else if light_id == 0 {
12629                camera_default_light = siglus_default_camera_light(sprite);
12630                &camera_default_light
12631            } else {
12632                continue;
12633            };
12634            if matches!(light.kind, globals::LightType::None) {
12635                continue;
12636            }
12637            let dir_len = (light.dir[0] * light.dir[0]
12638                + light.dir[1] * light.dir[1]
12639                + light.dir[2] * light.dir[2])
12640                .sqrt()
12641                .max(1e-6);
12642            sprite.mesh_runtime_lights.push(SpriteRuntimeLight {
12643                id: light_id,
12644                kind: light.kind as i32,
12645                diffuse: light.diffuse,
12646                ambient: light.ambient,
12647                specular: light.specular,
12648                pos: [light.pos[0], light.pos[1], light.pos[2], 1.0],
12649                dir: [
12650                    light.dir[0] / dir_len,
12651                    light.dir[1] / dir_len,
12652                    light.dir[2] / dir_len,
12653                    0.0,
12654                ],
12655                atten: [
12656                    light.attenuation0,
12657                    light.attenuation1,
12658                    light.attenuation2,
12659                    light.range,
12660                ],
12661                cone: [
12662                    (light.theta_deg.to_radians() * 0.5).cos(),
12663                    (light.phi_deg.to_radians() * 0.5).cos(),
12664                    light.falloff,
12665                    if matches!(light.kind, globals::LightType::ShadowMapSpot) {
12666                        1.0
12667                    } else {
12668                        0.0
12669                    },
12670                ],
12671            });
12672        }
12673    }
12674
12675    if sprite.fog_use && sprite.camera_enabled && ctx.globals.fog_global.enabled {
12676        let fog = &ctx.globals.fog_global;
12677        sprite.fog_enabled = true;
12678        sprite.fog_color = fog.color;
12679        sprite.fog_near = fog.near;
12680        sprite.fog_far = fog.far;
12681        sprite.fog_scroll_x = fog.scroll_x;
12682        sprite.fog_texture_image_id = fog.texture_image_id;
12683    }
12684}
12685
12686fn siglus_default_camera_light(sprite: &Sprite) -> globals::LightState {
12687    let mut light = globals::LightState::directional(0, [0.0, 1.0, 0.0]);
12688    light.pos = sprite.camera_eye;
12689    light.diffuse = [1.0, 1.0, 1.0, 1.0];
12690    light.ambient = [3.0, 3.0, 3.0, 1.0];
12691    light.specular = [0.0, 0.0, 0.0, 1.0];
12692    light
12693}
12694
12695fn render_sprite_visible_for_submit(rs: &RenderSprite) -> bool {
12696    let has_payload = rs.sprite.image_id.is_some()
12697        || (rs.sprite.mesh_kind != 0 && rs.sprite.mesh_file_name.is_some());
12698    rs.sprite.visible && has_payload && rs.sprite.alpha > 0 && rs.sprite.tr > 0
12699}
12700
12701fn scale_sprite_tr(sprite: &mut Sprite, rate: f32) {
12702    sprite.tr = ((sprite.tr as f32) * rate.clamp(0.0, 1.0))
12703        .round()
12704        .clamp(0.0, 255.0) as u8;
12705}
12706
12707fn build_regular_stage_wipe_list(
12708    ctx: &mut CommandContext,
12709    front_stage: &[RenderSprite],
12710    next_stage: &[RenderSprite],
12711) -> Option<Vec<RenderSprite>> {
12712    let wipe = ctx.globals.wipe.as_ref()?;
12713    let wipe_type = wipe.wipe_type;
12714    if (220..=243).contains(&wipe_type) {
12715        return None;
12716    }
12717
12718    let begin_layer = wipe.begin_layer;
12719    let end_layer = wipe.end_layer;
12720    let begin_order = wipe.begin_order;
12721    let end_order = wipe.end_order;
12722    let with_low = wipe.with_low_order != 0;
12723    let raw_progress = wipe.progress();
12724    let progress = match wipe.speed_mode {
12725        1 => raw_progress * raw_progress,
12726        2 => 1.0 - (1.0 - raw_progress) * (1.0 - raw_progress),
12727        3 => raw_progress * raw_progress * (3.0 - 2.0 * raw_progress),
12728        _ => raw_progress,
12729    };
12730
12731    let mut under = Vec::new();
12732    let mut front_target = Vec::new();
12733    let mut over = Vec::new();
12734    for rs in front_stage.iter().cloned() {
12735        match classify_wipe_partition(
12736            &rs,
12737            begin_layer,
12738            end_layer,
12739            begin_order,
12740            end_order,
12741            with_low,
12742        ) {
12743            WipePartition::Under => under.push(rs),
12744            WipePartition::Target => front_target.push(rs),
12745            WipePartition::Over => over.push(rs),
12746        }
12747    }
12748
12749    let mut next_target = Vec::new();
12750    for rs in next_stage.iter().cloned() {
12751        if matches!(
12752            classify_wipe_partition(
12753                &rs,
12754                begin_layer,
12755                end_layer,
12756                begin_order,
12757                end_order,
12758                with_low
12759            ),
12760            WipePartition::Target
12761        ) {
12762            next_target.push(rs);
12763        }
12764    }
12765
12766    let mut out = Vec::new();
12767    out.extend(under);
12768    match wipe_type {
12769        1 => out.extend(front_target),
12770        2 => out.extend(next_target),
12771        _ => {
12772            out.extend(next_target);
12773            for mut rs in front_target {
12774                scale_sprite_tr(&mut rs.sprite, progress);
12775                out.push(rs);
12776            }
12777        }
12778    }
12779    out.extend(over);
12780    out.retain(render_sprite_visible_for_submit);
12781    Some(out)
12782}
12783
12784fn build_dual_source_wipe_list(
12785    ctx: &mut CommandContext,
12786    current: &[RenderSprite],
12787    next_stage: &[RenderSprite],
12788) -> Option<Vec<RenderSprite>> {
12789    let wipe = ctx.globals.wipe.as_ref()?;
12790    let wipe_type = wipe.wipe_type;
12791    if !(220..=243).contains(&wipe_type) {
12792        return None;
12793    }
12794
12795    let front = if next_stage.is_empty() {
12796        current.to_vec()
12797    } else {
12798        next_stage.to_vec()
12799    };
12800
12801    let begin_layer = wipe.begin_layer;
12802    let end_layer = wipe.end_layer;
12803    let begin_order = wipe.begin_order;
12804    let end_order = wipe.end_order;
12805    let with_low = wipe.with_low_order != 0;
12806    let progress = wipe.progress();
12807    let option = wipe.option.clone();
12808
12809    let mut under = Vec::new();
12810    let mut front_target = Vec::new();
12811    let mut over = Vec::new();
12812    for rs in front.into_iter() {
12813        match classify_wipe_partition(
12814            &rs,
12815            begin_layer,
12816            end_layer,
12817            begin_order,
12818            end_order,
12819            with_low,
12820        ) {
12821            WipePartition::Under => under.push(rs),
12822            WipePartition::Target => front_target.push(rs),
12823            WipePartition::Over => over.push(rs),
12824        }
12825    }
12826    let mut next_target = Vec::new();
12827    for rs in current.iter().cloned() {
12828        if matches!(
12829            classify_wipe_partition(
12830                &rs,
12831                begin_layer,
12832                end_layer,
12833                begin_order,
12834                end_order,
12835                with_low
12836            ),
12837            WipePartition::Target
12838        ) {
12839            next_target.push(rs);
12840        }
12841    }
12842
12843    let front_img =
12844        soft_render::render_to_image(&ctx.images, &front_target, ctx.screen_w, ctx.screen_h);
12845    let next_img =
12846        soft_render::render_to_image(&ctx.images, &next_target, ctx.screen_w, ctx.screen_h);
12847    let front_id =
12848        upsert_runtime_image_slot(&mut ctx.images, &mut ctx.wipe_front_rt_image, front_img);
12849    let next_id = upsert_runtime_image_slot(&mut ctx.images, &mut ctx.wipe_next_rt_image, next_img);
12850
12851    let mut comp = crate::layer::Sprite::default();
12852    comp.visible = true;
12853    comp.fit = SpriteFit::FullScreen;
12854    comp.image_id = Some(next_id);
12855    comp.wipe_src_image_id = Some(front_id);
12856    comp.alpha_blend = true;
12857    comp.alpha_test = false;
12858    comp.tr = 255;
12859    comp.alpha = 255;
12860
12861    match wipe_type {
12862        220 => {
12863            let axis = option.get(0).copied().unwrap_or(0);
12864            let denom = option.get(1).copied().unwrap_or(1).max(1) as f32;
12865            let wave_num = option.get(2).copied().unwrap_or(3) as f32;
12866            let power = option.get(3).copied().unwrap_or(0) as f32;
12867            comp.wipe_fx_mode = if axis == 0 { 12 } else { 11 };
12868            comp.wipe_fx_params = [
12869                if axis == 0 {
12870                    ctx.screen_h as f32 / denom
12871                } else {
12872                    ctx.screen_w as f32 / denom
12873                },
12874                wave_num,
12875                power,
12876                progress,
12877            ];
12878        }
12879        221 => {
12880            let axis = option.get(0).copied().unwrap_or(0);
12881            let denom = option.get(1).copied().unwrap_or(1).max(1) as f32;
12882            let wave_num = option.get(2).copied().unwrap_or(3) as f32;
12883            let power = option.get(3).copied().unwrap_or(0) as f32;
12884            let front_bias = if option.get(4).copied().unwrap_or(0) != 0 {
12885                1.0
12886            } else {
12887                0.0
12888            };
12889            comp.wipe_fx_mode = if axis == 0 { 12 } else { 11 };
12890            comp.wipe_fx_params = [
12891                if axis == 0 {
12892                    ctx.screen_h as f32 / denom
12893                } else {
12894                    ctx.screen_w as f32 / denom
12895                },
12896                wave_num,
12897                power,
12898                progress,
12899            ];
12900            comp.tonecurve_row = 221.0;
12901            comp.tonecurve_sat = front_bias;
12902        }
12903        230 => {
12904            let (st, ed) = mosaic_size_pair(option.get(0).copied().unwrap_or(0));
12905            let cut = if progress < 0.5 {
12906                st + (ed - st) * (progress / 0.5)
12907            } else {
12908                ed + (st - ed) * ((progress - 0.5) / 0.5)
12909            };
12910            comp.wipe_fx_mode = 10;
12911            comp.wipe_fx_params = [
12912                cut.max(0.0005),
12913                ctx.screen_w as f32 / ctx.screen_h.max(1) as f32,
12914                progress,
12915                230.0,
12916            ];
12917        }
12918        231 => {
12919            let (mut st, mut ed) = mosaic_size_pair(option.get(0).copied().unwrap_or(0));
12920            let fade_mode = option.get(1).copied().unwrap_or(0);
12921            if fade_mode == 1 {
12922                std::mem::swap(&mut st, &mut ed);
12923            }
12924            let cut = (st + (ed - st) * progress).max(0.0005);
12925            comp.wipe_fx_mode = 10;
12926            comp.wipe_fx_params = [
12927                cut,
12928                ctx.screen_w as f32 / ctx.screen_h.max(1) as f32,
12929                progress,
12930                231.0,
12931            ];
12932            comp.tonecurve_sat = fade_mode as f32;
12933        }
12934        240 | 242 => {
12935            let (alpha_type, alpha_reverse, bp_type, bp_reverse, blur_coeff) = if wipe_type == 240 {
12936                (
12937                    option.get(2).copied().unwrap_or(0),
12938                    option.get(3).copied().unwrap_or(0),
12939                    option.get(4).copied().unwrap_or(0),
12940                    option.get(5).copied().unwrap_or(0),
12941                    option.get(6).copied().unwrap_or(1) as f32,
12942                )
12943            } else {
12944                (
12945                    option.get(0).copied().unwrap_or(0),
12946                    option.get(1).copied().unwrap_or(0),
12947                    option.get(2).copied().unwrap_or(0),
12948                    option.get(3).copied().unwrap_or(0),
12949                    option.get(4).copied().unwrap_or(1) as f32,
12950                )
12951            };
12952            let alpha_f = effect_curve(alpha_type, alpha_reverse != 0, progress);
12953            let bp = effect_curve(bp_type, bp_reverse != 0, progress);
12954            let (cx, cy) = if wipe_type == 242 {
12955                (0.5, 0.5)
12956            } else {
12957                (
12958                    option.get(0).copied().unwrap_or(ctx.screen_w as i32 / 2) as f32
12959                        / ctx.screen_w.max(1) as f32,
12960                    option.get(1).copied().unwrap_or(ctx.screen_h as i32 / 2) as f32
12961                        / ctx.screen_h.max(1) as f32,
12962                )
12963            };
12964            comp.wipe_fx_mode = 13;
12965            comp.wipe_fx_params = [cx, cy, bp, blur_coeff];
12966            comp.tonecurve_row = alpha_f;
12967            comp.tonecurve_sat = wipe_type as f32;
12968        }
12969        241 | 243 => {
12970            let (alpha_type, alpha_reverse, bp_type, bp_reverse, blur_coeff, front_bias) =
12971                if wipe_type == 241 {
12972                    (
12973                        option.get(2).copied().unwrap_or(0),
12974                        option.get(3).copied().unwrap_or(0),
12975                        option.get(4).copied().unwrap_or(0),
12976                        option.get(5).copied().unwrap_or(0),
12977                        option.get(6).copied().unwrap_or(1) as f32,
12978                        if option.get(7).copied().unwrap_or(0) == 0 {
12979                            1.0
12980                        } else {
12981                            0.0
12982                        },
12983                    )
12984                } else {
12985                    (
12986                        option.get(0).copied().unwrap_or(0),
12987                        option.get(1).copied().unwrap_or(0),
12988                        option.get(2).copied().unwrap_or(0),
12989                        option.get(3).copied().unwrap_or(0),
12990                        option.get(4).copied().unwrap_or(1) as f32,
12991                        if option.get(5).copied().unwrap_or(0) == 0 {
12992                            1.0
12993                        } else {
12994                            0.0
12995                        },
12996                    )
12997                };
12998            let alpha_f = effect_curve(alpha_type, alpha_reverse != 0, progress);
12999            let bp = effect_curve(bp_type, bp_reverse != 0, progress);
13000            comp.wipe_fx_mode = 13;
13001            comp.wipe_fx_params = [0.5, 0.5, bp, blur_coeff];
13002            comp.tonecurve_row = alpha_f;
13003            comp.tonecurve_sat = front_bias * 1000.0 + wipe_type as f32;
13004        }
13005        _ => return None,
13006    }
13007
13008    let mut out = Vec::with_capacity(under.len() + 1 + over.len());
13009    out.extend(under);
13010    out.push(RenderSprite::new(None, None, comp));
13011    out.extend(over);
13012    Some(out)
13013}
13014
13015fn apply_wipe_effect(ctx: &mut CommandContext, sprites: &mut [RenderSprite]) {
13016    let Some(wipe) = ctx.globals.wipe.as_mut() else {
13017        return;
13018    };
13019    let mut mask_cache = std::mem::take(&mut wipe.mask_cache);
13020    let mask_file = wipe.mask_file.clone();
13021    let mask_image_id = wipe.mask_image_id;
13022    let wipe_type = wipe.wipe_type;
13023    let speed_mode = wipe.speed_mode;
13024    let option = wipe.option.clone();
13025    let begin_layer = wipe.begin_layer;
13026    let end_layer = wipe.end_layer;
13027    let begin_order = wipe.begin_order;
13028    let end_order = wipe.end_order;
13029    let with_low = wipe.with_low_order != 0;
13030    let mut progress = wipe.progress();
13031    let _ = wipe;
13032    progress = match speed_mode {
13033        1 => progress * progress,
13034        2 => 1.0 - (1.0 - progress) * (1.0 - progress),
13035        3 => progress * progress * (3.0 - 2.0 * progress),
13036        _ => progress,
13037    };
13038    let fade = (progress * 255.0).clamp(0.0, 255.0) as u8;
13039
13040    for rs in sprites.iter_mut() {
13041        let (order, layer) = render_sprite_sorter(rs);
13042        if layer < begin_layer || layer > end_layer {
13043            continue;
13044        }
13045        if !with_low && (order < begin_order || order > end_order) {
13046            continue;
13047        }
13048        if with_low && order < begin_order {
13049            // Include lower orders when requested.
13050        } else if order < begin_order || order > end_order {
13051            continue;
13052        }
13053
13054        rs.sprite.wipe_fx_mode = 0;
13055        rs.sprite.wipe_fx_params = [0.0; 4];
13056
13057        if mask_file.is_some() {
13058            let Some(mask_id) = mask_image_id else {
13059                continue;
13060            };
13061            let reverse = option.get(0).copied().unwrap_or(0) != 0;
13062            let t = if reverse { 1.0 - progress } else { progress };
13063            let bucket = (t * 255.0).round().clamp(0.0, 255.0) as u16;
13064
13065            if let Some(base_id) = rs.sprite.image_id {
13066                let Some((base_img, base_ver)) = ctx.images.get_entry(base_id) else {
13067                    continue;
13068                };
13069                let Some((mask_img, mask_ver)) = ctx.images.get_entry(mask_id) else {
13070                    continue;
13071                };
13072
13073                let key = (base_id, base_ver, mask_id, mask_ver, bucket);
13074                if let Some(&masked_id) = mask_cache.get(&key) {
13075                    rs.sprite.image_id = Some(masked_id);
13076                } else {
13077                    let masked = apply_wipe_mask_image(base_img, mask_img, t);
13078                    let masked_id = ctx.images.insert_image(masked);
13079                    mask_cache.insert(key, masked_id);
13080                    rs.sprite.image_id = Some(masked_id);
13081                }
13082            }
13083
13084            rs.sprite.tr = ((rs.sprite.tr as f32) * (fade as f32 / 255.0)) as u8;
13085            continue;
13086        }
13087
13088        match wipe_type {
13089            1 | 2 | 3 | 4 | 5 | 6 => {
13090                if let Some((left, top, right, bottom)) = sprite_bounds(&rs.sprite, ctx) {
13091                    let w = (right - left).max(1);
13092                    let h = (bottom - top).max(1);
13093                    let clip = match wipe_type {
13094                        1 => {
13095                            let x = left + ((w as f32) * progress) as i32;
13096                            ClipRect {
13097                                left,
13098                                top,
13099                                right: x,
13100                                bottom,
13101                            }
13102                        }
13103                        2 => {
13104                            let x = right - ((w as f32) * progress) as i32;
13105                            ClipRect {
13106                                left: x,
13107                                top,
13108                                right,
13109                                bottom,
13110                            }
13111                        }
13112                        3 => {
13113                            let y = top + ((h as f32) * progress) as i32;
13114                            ClipRect {
13115                                left,
13116                                top,
13117                                right,
13118                                bottom: y,
13119                            }
13120                        }
13121                        4 => {
13122                            let y = bottom - ((h as f32) * progress) as i32;
13123                            ClipRect {
13124                                left,
13125                                top: y,
13126                                right,
13127                                bottom,
13128                            }
13129                        }
13130                        5 => {
13131                            let cx = left + w / 2;
13132                            let cy = top + h / 2;
13133                            let hw = ((w as f32) * progress / 2.0) as i32;
13134                            let hh = ((h as f32) * progress / 2.0) as i32;
13135                            ClipRect {
13136                                left: cx - hw,
13137                                top: cy - hh,
13138                                right: cx + hw,
13139                                bottom: cy + hh,
13140                            }
13141                        }
13142                        6 => {
13143                            let cx = left + w / 2;
13144                            let cy = top + h / 2;
13145                            let hw = ((w as f32) * (1.0 - progress) / 2.0) as i32;
13146                            let hh = ((h as f32) * (1.0 - progress) / 2.0) as i32;
13147                            ClipRect {
13148                                left: cx - hw,
13149                                top: cy - hh,
13150                                right: cx + hw,
13151                                bottom: cy + hh,
13152                            }
13153                        }
13154                        _ => ClipRect {
13155                            left,
13156                            top,
13157                            right,
13158                            bottom,
13159                        },
13160                    };
13161                    rs.sprite.dst_clip = Some(clip);
13162                    rs.sprite.tr = ((rs.sprite.tr as f32) * (fade as f32 / 255.0)) as u8;
13163                } else {
13164                    rs.sprite.tr = ((rs.sprite.tr as f32) * (fade as f32 / 255.0)) as u8;
13165                }
13166            }
13167            220 | 221 => {
13168                if let Some((left, top, right, bottom)) = sprite_bounds(&rs.sprite, ctx) {
13169                    let w = (right - left).max(1) as f32;
13170                    let h = (bottom - top).max(1) as f32;
13171                    let denom = option.get(1).copied().unwrap_or(1).max(1) as f32;
13172                    let wave_num = option.get(2).copied().unwrap_or(3) as f32;
13173                    let power = option.get(3).copied().unwrap_or(0) as f32;
13174                    let reverse = option.get(4).copied().unwrap_or(0) != 0;
13175                    let progress_eff = if wipe_type == 221 && reverse {
13176                        1.0 - progress
13177                    } else {
13178                        progress
13179                    };
13180                    rs.sprite.wipe_fx_mode = if option.get(0).copied().unwrap_or(0) == 0 {
13181                        3
13182                    } else {
13183                        2
13184                    };
13185                    rs.sprite.wipe_fx_params = [
13186                        if option.get(0).copied().unwrap_or(0) == 0 {
13187                            h / denom
13188                        } else {
13189                            w / denom
13190                        },
13191                        wave_num,
13192                        power,
13193                        progress_eff,
13194                    ];
13195                    rs.sprite.tr = ((rs.sprite.tr as f32)
13196                        * (255.0 * progress_eff).clamp(0.0, 255.0)
13197                        / 255.0) as u8;
13198                }
13199            }
13200            230 | 231 => {
13201                if let Some(id) = rs.sprite.image_id {
13202                    if let Some((img, _)) = ctx.images.get_entry(id) {
13203                        let (mut st, mut ed) =
13204                            mosaic_size_pair(option.get(0).copied().unwrap_or(0));
13205                        let mut cut = if wipe_type == 230 {
13206                            if progress < 0.5 {
13207                                st + (ed - st) * (progress / 0.5)
13208                            } else {
13209                                ed + (st - ed) * ((progress - 0.5) / 0.5)
13210                            }
13211                        } else {
13212                            if option.get(1).copied().unwrap_or(0) == 1 {
13213                                std::mem::swap(&mut st, &mut ed);
13214                            }
13215                            st + (ed - st) * progress
13216                        };
13217                        cut = cut.max(0.0005);
13218                        rs.sprite.wipe_fx_mode = 1;
13219                        rs.sprite.wipe_fx_params =
13220                            [cut, img.width as f32 / img.height.max(1) as f32, 0.0, 0.0];
13221                        if wipe_type == 231 {
13222                            let trf = if option.get(1).copied().unwrap_or(0) == 0 {
13223                                1.0 - progress
13224                            } else {
13225                                progress
13226                            };
13227                            rs.sprite.tr = ((rs.sprite.tr as f32) * (255.0 * trf).clamp(0.0, 255.0)
13228                                / 255.0) as u8;
13229                        }
13230                    }
13231                }
13232            }
13233            240 | 241 | 242 | 243 => {
13234                if let Some(id) = rs.sprite.image_id {
13235                    if let Some((img, _)) = ctx.images.get_entry(id) {
13236                        let (alpha_type, alpha_reverse, bp_type, bp_reverse, blur_coeff) =
13237                            if wipe_type == 240 || wipe_type == 241 {
13238                                (
13239                                    option.get(2).copied().unwrap_or(0),
13240                                    option.get(3).copied().unwrap_or(0) != 0,
13241                                    option.get(4).copied().unwrap_or(0),
13242                                    option.get(5).copied().unwrap_or(0) != 0,
13243                                    option.get(6).copied().unwrap_or(1) as f32,
13244                                )
13245                            } else {
13246                                (
13247                                    option.get(0).copied().unwrap_or(0),
13248                                    option.get(1).copied().unwrap_or(0) != 0,
13249                                    option.get(2).copied().unwrap_or(0),
13250                                    option.get(3).copied().unwrap_or(0) != 0,
13251                                    option.get(4).copied().unwrap_or(1) as f32,
13252                                )
13253                            };
13254                        let alpha_f = effect_curve(alpha_type, alpha_reverse, progress);
13255                        let bp = effect_curve(bp_type, bp_reverse, progress);
13256                        let (cx, cy) = if wipe_type == 242 || wipe_type == 243 {
13257                            let seed = ((rs.sprite.order as i64 * 1103515245
13258                                + rs.sprite.x as i64 * 12345
13259                                + rs.sprite.y as i64 * 34567
13260                                + (progress * 997.0) as i64)
13261                                & 0x7fffffff) as u64;
13262                            (
13263                                (seed % img.width.max(1) as u64) as f32 / img.width.max(1) as f32,
13264                                (((seed / 97) % img.height.max(1) as u64) as f32)
13265                                    / img.height.max(1) as f32,
13266                            )
13267                        } else {
13268                            (
13269                                option.get(0).copied().unwrap_or(img.width as i32 / 2) as f32
13270                                    / img.width.max(1) as f32,
13271                                option.get(1).copied().unwrap_or(img.height as i32 / 2) as f32
13272                                    / img.height.max(1) as f32,
13273                            )
13274                        };
13275                        rs.sprite.wipe_fx_mode = 4;
13276                        rs.sprite.wipe_fx_params = [cx, cy, bp, blur_coeff];
13277                        rs.sprite.tr = ((rs.sprite.tr as f32) * (255.0 * alpha_f).clamp(0.0, 255.0)
13278                            / 255.0) as u8;
13279                    }
13280                }
13281            }
13282            _ => {
13283                rs.sprite.tr = ((rs.sprite.tr as f32) * (fade as f32 / 255.0)) as u8;
13284            }
13285        }
13286    }
13287
13288    if let Some(wipe) = ctx.globals.wipe.as_mut() {
13289        wipe.mask_cache = mask_cache;
13290    }
13291}
13292
13293fn mosaic_size_pair(kind: i32) -> (f32, f32) {
13294    match kind {
13295        0 => (0.001, 0.025),
13296        1 => (0.002, 0.04),
13297        2 => (0.003, 0.06),
13298        3 => (0.004, 0.08),
13299        4 => (0.005, 0.10),
13300        5 => (0.006, 0.15),
13301        6 => (0.007, 0.20),
13302        7 => (0.008, 0.30),
13303        8 => (0.009, 0.40),
13304        9 => (0.010, 0.50),
13305        _ => (0.005, 0.10),
13306    }
13307}
13308
13309fn effect_curve(kind: i32, reverse: bool, progress: f32) -> f32 {
13310    let mut v = if kind == 0 {
13311        1.0 - progress
13312    } else if kind == 10 {
13313        progress
13314    } else if (1..10).contains(&kind) {
13315        let threshold = kind as f32 / 10.0;
13316        if progress < threshold {
13317            if threshold <= 0.0 {
13318                1.0
13319            } else {
13320                progress / threshold
13321            }
13322        } else {
13323            let span = (1.0 - threshold).max(1e-5);
13324            ((1.0 - progress) / span).clamp(0.0, 1.0)
13325        }
13326    } else {
13327        1.0
13328    };
13329    if reverse {
13330        v = 1.0 - v;
13331    }
13332    v.clamp(0.0, 1.0)
13333}
13334
13335fn sprite_bounds(sprite: &Sprite, ctx: &CommandContext) -> Option<(i32, i32, i32, i32)> {
13336    match sprite.fit {
13337        crate::layer::SpriteFit::FullScreen => {
13338            let w = ctx.screen_w as i32;
13339            let h = ctx.screen_h as i32;
13340            Some((0, 0, w, h))
13341        }
13342        crate::layer::SpriteFit::PixelRect => {
13343            let (mut w, mut h) = match sprite.size_mode {
13344                crate::layer::SpriteSizeMode::Explicit { width, height } => {
13345                    (width as i32, height as i32)
13346                }
13347                crate::layer::SpriteSizeMode::Intrinsic => {
13348                    let Some(id) = sprite.image_id else {
13349                        return None;
13350                    };
13351                    let (img, _) = ctx.images.get_entry(id)?;
13352                    (img.width as i32, img.height as i32)
13353                }
13354            };
13355            w = ((w as f32) * sprite.scale_x) as i32;
13356            h = ((h as f32) * sprite.scale_y) as i32;
13357            let left = sprite.x;
13358            let top = sprite.y;
13359            Some((left, top, left + w, top + h))
13360        }
13361    }
13362}
13363
13364fn resolve_mask_path(project_dir: &Path, raw: &str) -> Option<PathBuf> {
13365    let mut norm = raw.replace('\\', "/");
13366    let mut candidates = Vec::new();
13367
13368    if !norm.contains('.') {
13369        for ext in ["png", "bmp", "jpg", "jpeg", "g00"] {
13370            candidates.push(project_dir.join(format!("{}.{}", norm, ext)));
13371            candidates.push(project_dir.join("dat").join(format!("{}.{}", norm, ext)));
13372            candidates.push(project_dir.join("mask").join(format!("{}.{}", norm, ext)));
13373        }
13374    }
13375
13376    candidates.push(project_dir.join(&norm));
13377    candidates.push(project_dir.join("dat").join(&norm));
13378    candidates.push(project_dir.join("mask").join(&norm));
13379
13380    for c in candidates {
13381        if c.exists() {
13382            return Some(c);
13383        }
13384    }
13385    None
13386}
13387
13388fn apply_mask_image(base: &RgbaImage, mask: &RgbaImage, mask_x: i32, mask_y: i32) -> RgbaImage {
13389    let mut out = base.clone();
13390    let bw = base.width as i32;
13391    let bh = base.height as i32;
13392    let mw = mask.width as i32;
13393    let mh = mask.height as i32;
13394
13395    for y in 0..bh {
13396        for x in 0..bw {
13397            let mx = x + mask_x;
13398            let my = y + mask_y;
13399            let mask_alpha = if mx >= 0 && my >= 0 && mx < mw && my < mh {
13400                let mi = ((my as u32 * mask.width + mx as u32) * 4) as usize;
13401                let mr = mask.rgba[mi] as f32 / 255.0;
13402                let mg = mask.rgba[mi + 1] as f32 / 255.0;
13403                let mb = mask.rgba[mi + 2] as f32 / 255.0;
13404                let ma = mask.rgba[mi + 3] as f32 / 255.0;
13405                let l = mr * 0.299 + mg * 0.587 + mb * 0.114;
13406                (l * ma).clamp(0.0, 1.0)
13407            } else {
13408                0.0
13409            };
13410
13411            let bi = ((y as u32 * base.width + x as u32) * 4) as usize;
13412            let ba = out.rgba[bi + 3] as f32 / 255.0;
13413            let na = (ba * mask_alpha).clamp(0.0, 1.0);
13414            out.rgba[bi + 3] = (na * 255.0).round().clamp(0.0, 255.0) as u8;
13415        }
13416    }
13417
13418    out
13419}
13420
13421fn apply_wipe_mask_image(base: &RgbaImage, mask: &RgbaImage, threshold: f32) -> RgbaImage {
13422    let mut out = base.clone();
13423    let bw = base.width as i32;
13424    let bh = base.height as i32;
13425    let mw = mask.width as i32;
13426    let mh = mask.height as i32;
13427
13428    for y in 0..bh {
13429        for x in 0..bw {
13430            let mx = (x * mw) / bw;
13431            let my = (y * mh) / bh;
13432            let mi = ((my as u32 * mask.width + mx as u32) * 4) as usize;
13433            let mr = mask.rgba[mi] as f32 / 255.0;
13434            let mg = mask.rgba[mi + 1] as f32 / 255.0;
13435            let mb = mask.rgba[mi + 2] as f32 / 255.0;
13436            let ma = mask.rgba[mi + 3] as f32 / 255.0;
13437            let l = (mr * 0.299 + mg * 0.587 + mb * 0.114) * ma;
13438
13439            let bi = ((y as u32 * base.width + x as u32) * 4) as usize;
13440            if l > threshold {
13441                out.rgba[bi + 3] = 0;
13442            }
13443        }
13444    }
13445
13446    out
13447}
13448
13449fn ensure_font_list(syscom: &mut globals::SyscomRuntimeState, project_dir: &Path) {
13450    if !syscom.font_list.is_empty() {
13451        return;
13452    }
13453
13454    let mut seen = HashSet::new();
13455    for dir in [project_dir.join("font"), project_dir.join("fonts")] {
13456        let Ok(entries) = fs::read_dir(dir) else {
13457            continue;
13458        };
13459        for entry in entries.flatten() {
13460            let path = entry.path();
13461            if !path.is_file() {
13462                continue;
13463            }
13464            let ext = path
13465                .extension()
13466                .and_then(|s| s.to_str())
13467                .unwrap_or("")
13468                .to_ascii_lowercase();
13469            if ext == "ttf" || ext == "otf" || ext == "ttc" {
13470                if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
13471                    if seen.insert(name.to_string()) {
13472                        syscom.font_list.push(name.to_string());
13473                    }
13474                }
13475            }
13476        }
13477    }
13478
13479    for name in embedded_default_font_names() {
13480        if seen.insert((*name).to_string()) {
13481            syscom.font_list.push((*name).to_string());
13482        }
13483    }
13484    syscom.font_list.sort();
13485}