1pub 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 Element(Vec<i32>),
54 List(Vec<Value>),
56 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 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#[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
138pub trait ExternalFormHandler: Send + Sync {
143 fn dispatch_form(
145 &self,
146 ctx: &mut CommandContext,
147 form_id: u32,
148 args: &[Value],
149 ) -> anyhow::Result<bool>;
150}
151#[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#[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 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 pub ids: constants::RuntimeConstants,
312
313 pub gfx: graphics::GfxRuntime,
315
316 pub ui: ui::UiRuntime,
318 pub font_cache: FontCache,
320
321 pub input: input::InputState,
323 pub script_input: input::InputState,
325
326 pub screen_w: u32,
328 pub screen_h: u32,
329
330 pub wait: wait::VmWait,
332
333 proc_generation: u64,
336 last_proc_kind: ProcKind,
337
338 pub net: net::TnmNet,
340
341 pub tables: tables::AssetTables,
343
344 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 pub last_presented_render_list: Vec<RenderSprite>,
356 pub wipe_front_rt_image: Option<ImageId>,
358 pub wipe_next_rt_image: Option<ImageId>,
360 pub overlay_rt_image: Option<ImageId>,
362
363 mouse_cursor_cache: HashMap<(i64, String), MouseCursorRuntime>,
364
365 pub external_forms: Option<Arc<dyn ExternalFormHandler>>,
367
368 pub native_ui_backend: Option<Arc<dyn native_ui::NativeUiBackend>>,
370 pub native_ui: native_ui::NativeUiRuntime,
371
372 pub current_scene_no: Option<i64>,
374 pub current_scene_name: Option<String>,
376 pub current_line_no: i64,
378
379 pub vm_call: Option<VmCallMeta>,
382
383 pending_read_flag_no: bool,
386 pending_selbtn_read_flag_no: bool,
387
388 pending_runtime_save: Option<RuntimeSaveRequest>,
392 pending_runtime_load: Option<RuntimeLoadRequest>,
394 runtime_load_completed: bool,
395
396 pub local_save_snapshot: Option<LocalSaveSnapshot>,
401
402 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 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 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 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 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 pub fn set_external_form_handler(&mut self, h: Option<Arc<dyn ExternalFormHandler>>) {
1333 self.external_forms = h;
1334 }
1335
1336 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 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 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 self.handle_editbox_key(k);
2334
2335 let handled_mwnd_selection = self.handle_mwnd_selection_key(k);
2336
2337 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 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 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 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 pub fn game_title(&self) -> String {
2745 game_title::resolve_game_title(self.tables.gameexe.as_ref(), &self.project_dir)
2746 }
2747
2748 pub fn game_name(&self) -> String {
2750 self.game_title()
2751 }
2752
2753 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 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 self.apply_syscom_skip_flags();
2905 if trace {
2906 eprintln!("[SG_CTX_TICK] after apply_syscom_skip_flags");
2907 }
2908 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 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 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 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 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 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 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 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 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 *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 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 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 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 sprite.object_anchor = true;
8791 sprite.texture_center_x = 0.0;
8792 sprite.texture_center_y = 0.0;
8793 }
8794 }
8795
8796 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 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 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 }
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 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 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 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 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 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 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 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 (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 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 } 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}