1pub mod constants;
7pub mod forms;
8pub mod graphics;
9pub mod input;
10pub mod opcode;
11
12pub use opcode::OpCode;
13pub mod game_display_info;
14pub mod game_title;
15pub mod gan;
16pub mod globals;
17pub mod int_event;
18pub mod native_ui;
19pub mod net;
20mod scene_metadata;
21pub mod string_semantics;
22pub mod tables;
23pub mod tonecurve;
24#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
25pub mod twitter;
26pub mod ui;
27pub mod unknown;
28pub mod wait;
29mod wipe;
30pub(crate) mod wipe_mask;
31use crate::runtime::forms::codes::syscom_op;
32use crate::runtime::forms::pcmevent as pcmevent_form;
33use crate::runtime::forms::syscom as syscom_form;
34use scene_metadata::SceneMetadata;
35
36use crate::runtime::globals::{HashMap, HashSet};
37use anyhow::{Result, anyhow};
38use std::cell::RefCell;
39use std::rc::Rc;
40use std::sync::atomic::{AtomicU64, Ordering};
41use std::sync::{Arc, OnceLock};
42
43use crate::assets::RgbaImage;
44use crate::audio::{AudioHub, BgmEngine, KoeEngine, PcmEngine, SeEngine};
45use crate::image_manager::{ImageHandle, ImageManager};
46use crate::layer::{
47 ClipRect, LayerId, LayerManager, RenderFrame, RenderSprite, Sprite, SpriteFit, SpriteId,
48 SpriteRuntimeLight, SpriteSizeMode, WipeRenderPlan,
49};
50use crate::movie::MovieManager;
51use crate::text_render::{FontCache, TextStyle, embedded_default_font_names};
52use siglus_assets::scene_pck::{ScenePck, ScenePckDecodeOptions};
53use std::fs;
54use std::path::{Path, PathBuf};
55
56#[derive(Debug, Clone)]
57pub enum Value {
58 Int(i64),
59 Str(String),
60 Element(Vec<i32>),
62 List(Vec<Value>),
64 NamedArg {
66 id: i32,
67 value: Box<Value>,
68 },
69}
70
71impl Value {
72 pub fn as_i64(&self) -> Option<i64> {
73 match self {
74 Value::Int(v) => Some(*v),
75 Value::NamedArg { value, .. } => value.as_i64(),
76 _ => None,
77 }
78 }
79
80 pub fn named_id(&self) -> Option<i32> {
81 match self {
82 Value::NamedArg { id, .. } => Some(*id),
83 _ => None,
84 }
85 }
86
87 pub fn unwrap_named(&self) -> &Value {
88 match self {
89 Value::NamedArg { value, .. } => value.as_ref(),
90 _ => self,
91 }
92 }
93 pub fn as_str(&self) -> Option<&str> {
94 match self {
95 Value::Str(s) => Some(s.as_str()),
96 Value::NamedArg { value, .. } => value.as_str(),
97 _ => None,
98 }
99 }
100}
101
102#[derive(Debug, Clone)]
103pub struct Command {
104 pub name: String,
105 pub code: Option<opcode::OpCode>,
107 pub args: Vec<Value>,
108}
109
110#[derive(Debug, Clone)]
111struct MsgBackLayoutEntry {
112 history_index: usize,
113 text: String,
114 total_pos: i32,
115 height: i32,
116}
117
118#[derive(Debug, Clone)]
119struct MsgBackSeparatorLayout {
120 file: Option<String>,
121 total_pos: i32,
122 height: i32,
123}
124
125#[derive(Debug, Clone, Default)]
126struct MsgBackLayout {
127 entries: Vec<MsgBackLayoutEntry>,
128 separators: Vec<MsgBackSeparatorLayout>,
129 total_height: i32,
130}
131
132#[derive(Debug, Clone)]
136pub struct ExcallCompatState {
137 pub ready: bool,
138 pub pending_free: bool,
142 pub ex_call_flag: bool,
143 pub flag_204: bool,
144 pub flag_2148: bool,
145 pub script_proc_requested: bool,
146 pub script_proc_pop_requested: bool,
147
148 pub font_name: String,
152 pub font_bold: i64,
153 pub font_shadow: i64,
154 pub joypad_mode_override: i64,
156}
157
158impl Default for ExcallCompatState {
159 fn default() -> Self {
160 Self {
161 ready: false,
162 pending_free: false,
163 ex_call_flag: false,
164 flag_204: false,
165 flag_2148: false,
166 script_proc_requested: false,
167 script_proc_pop_requested: false,
168 font_name: String::new(),
169 font_bold: -1,
170 font_shadow: -1,
171 joypad_mode_override: -1,
172 }
173 }
174}
175
176pub trait ExternalFormHandler: Send + Sync {
181 fn dispatch_form(
183 &self,
184 ctx: &mut CommandContext,
185 form_id: u32,
186 args: &[Value],
187 ) -> anyhow::Result<bool>;
188}
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum ProcKind {
193 Script,
194 Disp,
195 Frame,
196 Command,
197 MessageBlock,
198 MessageWait,
199 KeyWait,
200 TimeWait,
201 MovieWait,
202 WipeWait,
203 AudioWait,
204 EventWait,
205 Selection,
206 SystemModal,
207}
208
209#[derive(Debug, Default)]
214pub struct IntVecPool {
215 spare: Vec<Vec<i32>>,
216}
217
218impl IntVecPool {
219 const MAX_SPARE: usize = 32;
220 const MAX_CAPACITY: usize = 64;
221
222 pub fn take(&mut self) -> Vec<i32> {
224 self.spare.pop().unwrap_or_default()
225 }
226
227 pub fn take_copy(&mut self, items: &[i32]) -> Vec<i32> {
229 let mut v = self.take();
230 v.extend_from_slice(items);
231 v
232 }
233
234 pub fn give(&mut self, mut v: Vec<i32>) {
236 if self.spare.len() < Self::MAX_SPARE && v.capacity() <= Self::MAX_CAPACITY {
237 v.clear();
238 self.spare.push(v);
239 }
240 }
241}
242
243impl Clone for IntVecPool {
244 fn clone(&self) -> Self {
246 Self::default()
247 }
248}
249
250#[derive(Debug, Clone, Default)]
251pub struct VmCallMeta {
252 pub element: Vec<i32>,
253 pub al_id: i64,
254 pub ret_form: i64,
255}
256
257#[derive(Debug, Clone)]
258pub struct DebugActiveTextureEntry {
259 pub image_id: ImageHandle,
260 pub width: u32,
261 pub height: u32,
262 pub source_label: String,
263 pub submitted_this_frame: bool,
264 pub visible_refs: usize,
265 pub total_refs: usize,
266 pub ref_summary: String,
267}
268
269#[derive(Debug, Default, Clone)]
270struct DebugActiveTextureAccum {
271 width: u32,
272 height: u32,
273 source_label: String,
274 submitted_this_frame: bool,
275 visible_refs: usize,
276 total_refs: usize,
277 ref_labels: Vec<String>,
278}
279
280fn sg_mwnd_state_trace_runtime(
281 scene: &str,
282 scene_no: &str,
283 line: i64,
284 reason: &str,
285 stage_idx: i64,
286 mwnd_idx: usize,
287 old_open: bool,
288 new_open: bool,
289 m: &globals::MwndState,
290) {
291 if !sg_debug_enabled() {
292 return;
293 }
294 eprintln!(
295 "[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={}",
296 scene,
297 scene_no,
298 line,
299 reason,
300 stage_idx,
301 mwnd_idx,
302 old_open,
303 new_open,
304 m.button_list.len(),
305 m.face_list.len(),
306 m.object_list.len(),
307 if m.waku_file.is_empty() {
308 "-"
309 } else {
310 m.waku_file.as_str()
311 },
312 if m.filter_file.is_empty() {
313 "-"
314 } else {
315 m.filter_file.as_str()
316 },
317 m.window_pos,
318 m.window_size,
319 m.open_anime_type,
320 m.open_anime_time,
321 m.close_anime_type,
322 m.close_anime_time,
323 m.selection.is_some(),
324 m.msg_text.len(),
325 m.name_text.len(),
326 );
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum RuntimeSaveKind {
331 Normal,
332 Quick,
333 End,
334 Inner,
335}
336
337pub trait FrameCaptureBackend {
341 fn capture_render_frame(
342 &mut self,
343 images: &ImageManager,
344 frame: &RenderFrame,
345 logical_width: u32,
346 logical_height: u32,
347 ) -> Result<RgbaImage>;
348}
349
350pub type FrameCaptureBackendRef = Rc<RefCell<dyn FrameCaptureBackend>>;
351
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub struct RuntimeSaveRequest {
354 pub kind: RuntimeSaveKind,
355 pub index: usize,
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub struct RuntimeLoadRequest {
360 pub kind: RuntimeSaveKind,
361 pub index: usize,
362}
363
364#[derive(Debug, Clone, Default)]
369pub struct LocalSaveSnapshot {
370 pub save_id: [u16; 7],
371 pub append_dir: String,
372 pub append_name: String,
373 pub save_scene_title: String,
374 pub save_msg: String,
375 pub save_full_msg: String,
376 pub local_stream: Vec<u8>,
377 pub local_ex_stream: Vec<u8>,
378 pub sel_saves: Vec<crate::original_save::OriginalLocalSaveEnvelope>,
379}
380
381#[derive(Debug, Clone)]
382struct MouseCursorFrameRuntime {
383 image_id: ImageHandle,
384 hot_x: i32,
385 hot_y: i32,
386}
387
388#[derive(Debug, Clone)]
389struct MouseCursorRuntime {
390 frames: Vec<MouseCursorFrameRuntime>,
391 anime_speed_ms: i64,
392}
393
394pub struct CommandContext {
395 pub int_vec_pool: IntVecPool,
397 pub project_dir: PathBuf,
398
399 pub emote_key: Option<u32>,
403 pub images: ImageManager,
404 pub layers: LayerManager,
405 pub solid_white: ImageHandle,
407
408 pub bgm: BgmEngine,
411 pub audio: AudioHub,
412
413 pub koe: KoeEngine,
414 pub pcm: PcmEngine,
415 pub se: SeEngine,
416
417 pub movie: MovieManager,
418
419 pub ids: constants::RuntimeConstants,
421
422 pub gfx: graphics::GfxRuntime,
424
425 pub ui: ui::UiRuntime,
427 pub font_cache: FontCache,
429
430 pub input: input::InputState,
432 pub script_input: input::InputState,
434
435 pub screen_w: u32,
437 pub screen_h: u32,
438
439 frame_capture_backend: Option<FrameCaptureBackendRef>,
441
442 pub wait: wait::VmWait,
444
445 proc_generation: u64,
448 last_proc_kind: ProcKind,
449
450 pub net: net::TnmNet,
452
453 pub tables: tables::AssetTables,
455
456 scene_metadata: RefCell<Option<(String, Arc<SceneMetadata>)>>,
459
460 pub stack: Vec<Value>,
462
463 pub unknown: unknown::UnknownOpRecorder,
464
465 pub globals: globals::GlobalState,
466 pub tonecurve: tonecurve::ToneCurveRuntime,
467
468 pub excall_state: ExcallCompatState,
469
470 render_object_keys_scratch: HashSet<(LayerId, SpriteId)>,
474 mouse_cursor_cache: HashMap<(i64, String), MouseCursorRuntime>,
475 failed_gfx_image_repairs: HashSet<(String, i64, usize, String, u32)>,
476
477 pub external_forms: Option<Arc<dyn ExternalFormHandler>>,
479
480 pub native_ui_backend: Option<Arc<dyn native_ui::NativeUiBackend>>,
482 pub native_ui: native_ui::NativeUiRuntime,
483
484 pub current_scene_no: Option<i64>,
486 pub current_scene_name: Option<String>,
488 pub current_line_no: i64,
490
491 pub vm_call: Option<VmCallMeta>,
494
495 pending_read_flag_no: bool,
498 pending_selbtn_read_flag_no: bool,
499 pending_mwnd_read_flag_target: Option<(u32, i64, usize, i64)>,
503
504 skip_because_skip_trigger: bool,
509 msg_wait_skip_by_click: bool,
510 read_skip_enable_flag: bool,
511 cant_auto_skip_before_click: bool,
512 button_decide_capture: bool,
517
518 disp_because_msg_wait_cnt: i32,
524 disp_because_msg_wait_cnt_max: i32,
525 frame_rate_100msec: [i32; 10],
526 frame_rate_100msec_index: usize,
527 frame_rate_100msec_total: i32,
528 frame_rate_bucket_frame_cnt: u32,
529 frame_rate_bucket_elapsed_ms: u64,
530 frame_main_proc_started_at: Option<crate::platform_time::Instant>,
534
535 pending_runtime_save: Option<RuntimeSaveRequest>,
539 pending_runtime_load: Option<RuntimeLoadRequest>,
541 pub pending_scene_restart: Option<(String, i32)>,
545 runtime_load_completed: bool,
546
547 pub local_save_snapshot: Option<LocalSaveSnapshot>,
552
553 pub pending_auto_savepoint: bool,
559
560 pending_sel_point_result: Option<i32>,
565
566 frame_clock_last: Option<crate::platform_time::Instant>,
567 frame_clock_sub_ms_ns: u32,
572 last_button_hover_sound_pos: Option<(i32, i32)>,
573 suppress_next_right_syscom_open: bool,
574}
575
576fn accumulate_integer_millis(elapsed: std::time::Duration, sub_ms_ns: &mut u32) -> i32 {
584 const NS_PER_MS: u128 = 1_000_000;
585 let total_ns = elapsed.as_nanos().saturating_add(u128::from(*sub_ms_ns));
586 let elapsed_ms = total_ns / NS_PER_MS;
587 *sub_ms_ns = (total_ns % NS_PER_MS) as u32;
588 elapsed_ms.min(i32::MAX as u128) as i32
589}
590
591#[cfg(test)]
592mod frame_clock_tests {
593 use super::accumulate_integer_millis;
594 use std::time::Duration;
595
596 #[test]
597 fn sub_millisecond_frames_accumulate_instead_of_disappearing() {
598 let mut remainder = 0;
599 assert_eq!(
600 accumulate_integer_millis(Duration::from_micros(400), &mut remainder),
601 0
602 );
603 assert_eq!(remainder, 400_000);
604 assert_eq!(
605 accumulate_integer_millis(Duration::from_micros(400), &mut remainder),
606 0
607 );
608 assert_eq!(remainder, 800_000);
609 assert_eq!(
610 accumulate_integer_millis(Duration::from_micros(400), &mut remainder),
611 1
612 );
613 assert_eq!(remainder, 200_000);
614 }
615
616 #[test]
617 fn ordinary_frame_delta_preserves_fractional_remainder() {
618 let mut remainder = 0;
619 assert_eq!(
620 accumulate_integer_millis(Duration::from_micros(16_666), &mut remainder),
621 16
622 );
623 assert_eq!(remainder, 666_000);
624 assert_eq!(
625 accumulate_integer_millis(Duration::from_micros(16_666), &mut remainder),
626 17
627 );
628 assert_eq!(remainder, 332_000);
629 }
630}
631
632impl CommandContext {
633 pub fn sync_script_input_from_runtime(&mut self) {
634 self.script_input = self.input.clone();
635 }
636
637 pub fn proc_generation(&self) -> u64 {
638 self.proc_generation
639 }
640
641 pub fn request_read_flag_no(&mut self) {
642 self.pending_read_flag_no = true;
643 if self.pending_mwnd_read_flag_target.is_none() {
646 let form_id = if self.ids.form_global_stage != 0 {
647 self.ids.form_global_stage
648 } else {
649 crate::runtime::constants::global_form::STAGE_ALT
650 };
651 self.pending_mwnd_read_flag_target = Some((
652 form_id,
653 self.globals.current_mwnd_stage_idx,
654 self.globals.current_mwnd_no.unwrap_or(0),
655 self.current_scene_no.unwrap_or(-1),
656 ));
657 }
658 }
659
660 pub fn request_read_flag_no_for_mwnd(&mut self, form_id: u32, stage_idx: i64, mwnd_idx: usize) {
661 self.pending_read_flag_no = true;
662 self.pending_selbtn_read_flag_no = false;
663 self.pending_mwnd_read_flag_target = Some((
664 form_id,
665 stage_idx,
666 mwnd_idx,
667 self.current_scene_no.unwrap_or(-1),
668 ));
669 }
670
671 pub fn request_read_flag_no_for_selbtn(&mut self) {
672 self.pending_read_flag_no = true;
673 self.pending_selbtn_read_flag_no = true;
674 self.pending_mwnd_read_flag_target = None;
675 }
676
677 pub fn take_read_flag_no_request(&mut self) -> bool {
678 let requested = std::mem::take(&mut self.pending_read_flag_no);
679 if !requested {
680 self.pending_selbtn_read_flag_no = false;
681 self.pending_mwnd_read_flag_target = None;
682 }
683 requested
684 }
685
686 pub fn discard_read_flag_no_request(&mut self) {
691 self.pending_read_flag_no = false;
692 self.pending_selbtn_read_flag_no = false;
693 self.pending_mwnd_read_flag_target = None;
694 }
695
696 pub fn submit_read_flag_no(&mut self, value: i32) {
697 if std::mem::take(&mut self.pending_selbtn_read_flag_no) {
698 self.pending_mwnd_read_flag_target = None;
699 let scene_no = self.current_scene_no.unwrap_or(-1);
700 self.globals.selbtn.read_flag_scene_no = scene_no;
701 self.globals.selbtn.read_flag_flag_no = value as i64;
702 self.set_current_read_flag_for_skip(scene_no, value as i64);
707 return;
708 }
709
710 let Some((form_id, stage_idx, mwnd_idx, scene_no)) =
711 self.pending_mwnd_read_flag_target.take()
712 else {
713 return;
714 };
715 self.set_current_read_flag_for_skip(scene_no, value as i64);
716 let mut commit_now = false;
717 if let Some(mwnd) = self
718 .globals
719 .stage_forms
720 .get_mut(&form_id)
721 .and_then(|st| st.mwnd_lists.get_mut(&stage_idx))
722 .and_then(|list| list.get_mut(mwnd_idx))
723 {
724 mwnd.read_flag_stock.push((scene_no, value as i64));
725 commit_now = mwnd.clear_ready;
729 }
730 if commit_now {
731 let stock = self
732 .globals
733 .stage_forms
734 .get_mut(&form_id)
735 .and_then(|st| st.mwnd_lists.get_mut(&stage_idx))
736 .and_then(|list| list.get_mut(mwnd_idx))
737 .map(|mwnd| std::mem::take(&mut mwnd.read_flag_stock))
738 .unwrap_or_default();
739 for (scene_no, flag_no) in stock {
740 self.globals.set_read_flag(scene_no, flag_no);
741 }
742 }
743 }
744
745 pub fn request_runtime_save(&mut self, kind: RuntimeSaveKind, index: usize) {
746 self.pending_runtime_save = Some(RuntimeSaveRequest { kind, index });
747 }
748
749 pub fn request_auto_savepoint(&mut self) {
755 self.globals.syscom.current_save_message.clear();
756 if self.globals.script.dont_set_save_point {
757 return;
758 }
759 self.pending_auto_savepoint = true;
760 }
761
762 pub fn take_pending_auto_savepoint(&mut self) -> bool {
763 std::mem::take(&mut self.pending_auto_savepoint)
764 }
765
766 pub fn request_sel_point_with_result(&mut self, result: i64) {
767 self.pending_sel_point_result = Some(result.clamp(i32::MIN as i64, i32::MAX as i64) as i32);
768 }
769
770 pub fn take_pending_sel_point_result(&mut self) -> Option<i32> {
771 self.pending_sel_point_result.take()
772 }
773
774 pub fn request_runtime_load(&mut self, kind: RuntimeSaveKind, index: usize) {
775 self.pending_runtime_load = Some(RuntimeLoadRequest { kind, index });
776 }
777
778 pub fn take_runtime_save_request(&mut self) -> Option<RuntimeSaveRequest> {
779 self.pending_runtime_save.take()
780 }
781
782 pub fn take_runtime_load_request(&mut self) -> Option<RuntimeLoadRequest> {
783 self.pending_runtime_load.take()
784 }
785
786 pub fn begin_runtime_load_apply(&mut self) {
787 self.layers.clear_all();
795 self.gfx = graphics::GfxRuntime::default();
796 self.ui = ui::UiRuntime::default();
797 self.wait = wait::VmWait::default();
798 self.stack.clear();
799 self.render_object_keys_scratch.clear();
800 self.render_object_keys_scratch.shrink_to_fit();
801 self.vm_call = None;
802 self.pending_read_flag_no = false;
803 self.pending_selbtn_read_flag_no = false;
804 self.pending_mwnd_read_flag_target = None;
805 self.pending_sel_point_result = None;
806 self.frame_clock_last = None;
807 self.frame_clock_sub_ms_ns = 0;
808 self.frame_main_proc_started_at = None;
809 self.disp_because_msg_wait_cnt = 0;
810 self.disp_because_msg_wait_cnt_max = 0;
811 self.last_button_hover_sound_pos = None;
812 self.excall_state = ExcallCompatState::default();
816
817 self.globals.focused_editbox = None;
818 self.globals.focused_stage_group = None;
819 self.globals.focused_stage_mwnd = None;
820 self.globals.current_stage_object = None;
821 self.globals.current_object_chain = None;
822 self.globals.pending_button_actions.clear();
823 self.globals.pending_frame_action_finishes.clear();
824 self.globals.capture_for_object_image = None;
825 self.globals.save_thumb_capture_image = None;
826 self.globals.save_thumb_capture_prior = crate::runtime::forms::syscom::CAPTURE_PRIOR_NONE;
827 self.globals.selbtn = globals::BtnSelectRuntimeState::default();
828 self.globals.syscom.pending_proc = None;
829 self.globals.syscom.menu_open = false;
830 self.globals.syscom.menu_kind = None;
831 self.globals.syscom.menu_result = None;
832 self.globals.syscom.fallback_dialog = None;
833 self.globals.syscom.fallback_origin = None;
834 self.globals.syscom.msg_back_open = false;
835 self.globals.syscom.msg_back_proc_initialized = false;
836 self.globals.system.messagebox_modal = None;
837 self.globals.system.messagebox_modal_result = None;
838 self.globals.finish_wipe();
839
840 self.globals.stage_forms.clear();
845 self.globals.screen_forms.clear();
846 self.globals.counter_lists.clear();
847 self.globals.frame_actions.clear();
848 self.globals.frame_action_lists.clear();
849 self.globals.mask_lists.clear();
850 self.globals.pcm_event_lists.clear();
851 self.globals.editbox_lists.clear();
852 self.globals.msgbk_forms.clear();
853 }
854
855 pub fn mark_runtime_load_completed(&mut self) {
856 self.runtime_load_completed = true;
857 }
858
859 pub fn take_runtime_load_completed(&mut self) -> bool {
860 std::mem::take(&mut self.runtime_load_completed)
861 }
862
863 pub fn needs_continuous_frame(&self) -> bool {
864 fn frame_action_needs_tick(fa: &globals::ObjectFrameActionState) -> bool {
865 fa.counter.is_running() || (!fa.cmd_name.is_empty() && !fa.end_flag)
866 }
867
868 fn screen_effect_needs_tick(e: &globals::ScreenEffectState) -> bool {
869 e.x.check_event()
870 || e.y.check_event()
871 || e.z.check_event()
872 || e.mono.check_event()
873 || e.reverse.check_event()
874 || e.bright.check_event()
875 || e.dark.check_event()
876 || e.color_r.check_event()
877 || e.color_g.check_event()
878 || e.color_b.check_event()
879 || e.color_rate.check_event()
880 || e.color_add_r.check_event()
881 || e.color_add_g.check_event()
882 || e.color_add_b.check_event()
883 }
884
885 fn object_needs_tick(obj: &globals::ObjectState) -> bool {
886 obj.any_event_active()
887 || frame_action_needs_tick(&obj.frame_action)
888 || obj.frame_action_ch.iter().any(frame_action_needs_tick)
889 || obj.movie.playing
890 || obj.gan.is_active()
891 || (obj.object_type == 12 && obj.emote.runtime.is_some())
895 || obj.runtime.child_objects.iter().any(object_needs_tick)
896 }
897
898 if self.wait.needs_continuous_frame() {
899 return true;
900 }
901 let selbtn = &self.globals.selbtn;
907 if selbtn.open_anime_type > 0
908 || selbtn.close_anime_type > 0
909 || selbtn.decide_anime_type > 0
910 || selbtn.capture_now_flag
911 {
912 return true;
913 }
914 if self.pcm.needs_tick() {
915 return true;
916 }
917 if self
918 .ui
919 .needs_continuous_frame(&self.globals.script, &self.globals.syscom)
920 {
921 return true;
922 }
923 if self.globals.mov.playing || self.globals.wipe.is_some() {
924 return true;
925 }
926 if self.custom_mouse_cursor_needs_tick() {
927 return true;
928 }
929 if !self.globals.pending_frame_action_finishes.is_empty()
930 || !self.globals.pending_button_actions.is_empty()
931 {
932 return true;
933 }
934 if self
935 .globals
936 .counter_lists
937 .values()
938 .any(|v| v.iter().any(|c| c.is_running()))
939 {
940 return true;
941 }
942 if self
943 .globals
944 .int_event_roots
945 .values()
946 .any(|e| e.check_event())
947 || self
948 .globals
949 .int_event_lists
950 .values()
951 .any(|v| v.iter().any(|e| e.check_event()))
952 {
953 return true;
954 }
955 if self
956 .globals
957 .frame_actions
958 .values()
959 .any(frame_action_needs_tick)
960 || self
961 .globals
962 .frame_action_lists
963 .values()
964 .any(|v| v.iter().any(frame_action_needs_tick))
965 {
966 return true;
967 }
968 if self.globals.screen_forms.values().any(|screen| {
969 screen.effect_list.iter().any(screen_effect_needs_tick)
970 || screen.quake_list.iter().any(|q| q.is_active())
971 || screen.shake.is_active()
972 }) {
973 return true;
974 }
975 let wipe_active = self.globals.wipe.is_some();
976 let mwnd_ui_state = self
977 .ui
978 .current_mwnd_window_render_state(self.screen_w, self.screen_h);
979 self.globals.stage_forms.values().any(|stage| {
980 stage.object_lists.iter().any(|(&stage_idx, list)| {
981 if stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
982 return false;
983 }
984 list.iter().enumerate().any(|(obj_idx, obj)| {
985 !stage.is_embedded_object_slot(stage_idx, obj_idx)
986 && stage.object_slot_is_used(stage_idx, obj_idx)
987 && object_needs_tick(obj)
988 })
989 }) || stage.mwnd_lists.iter().any(|(&stage_idx, list)| {
990 if stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
991 return false;
992 }
993 list.iter().any(|m| {
994 let Some((window_x, window_y)) = m.window_pos else {
995 return false;
996 };
997 let Some((window_w, window_h)) = m.window_size else {
998 return false;
999 };
1000 if window_w <= 0 || window_h <= 0 {
1001 return false;
1002 }
1003 let visible_or_animating = m.open
1004 || mwnd_ui_state.is_some_and(|ui| {
1005 ui.x as i64 == window_x
1006 && ui.y as i64 == window_y
1007 && ui.w as i64 == window_w
1008 && ui.h as i64 == window_h
1009 });
1010 visible_or_animating
1011 && (m.object_list.iter().any(object_needs_tick)
1012 || m.button_list.iter().any(object_needs_tick)
1013 || m.face_list.iter().any(object_needs_tick))
1014 })
1015 })
1016 })
1017 }
1018
1019 pub fn last_proc_kind(&self) -> ProcKind {
1020 self.last_proc_kind
1021 }
1022
1023 pub fn request_proc_boundary(&mut self, kind: ProcKind) {
1024 self.last_proc_kind = kind;
1025 self.proc_generation = self.proc_generation.wrapping_add(1);
1026 }
1027
1028 pub fn request_disp_proc_boundary(&mut self) {
1029 self.request_proc_boundary(ProcKind::Disp);
1030 }
1031
1032 pub fn request_message_block_proc_boundary(&mut self) {
1033 self.request_proc_boundary(ProcKind::MessageBlock);
1034 }
1035
1036 pub fn request_message_wait_proc_boundary(&mut self) {
1037 self.request_proc_boundary(ProcKind::MessageWait);
1038 }
1039
1040 pub fn request_wait_proc_boundary(&mut self, kind: ProcKind) {
1041 self.request_proc_boundary(kind);
1042 }
1043
1044 pub fn finish_wipe_runtime(&mut self) {
1051 let Some(stage_form_id) = self.globals.wipe.as_ref().map(|w| w.stage_form_id) else {
1052 return;
1053 };
1054 self.globals.finish_wipe();
1055 crate::runtime::forms::stage::reinit_wipe_next_stage(self, stage_form_id);
1056 }
1057
1058 pub fn notify_wait_key(&mut self) -> bool {
1059 let wipe_skipped = {
1060 let wait = &mut self.wait;
1061 let globals = &mut self.globals;
1062 wait.notify_key(globals, &self.ids)
1063 };
1064 self.finish_skipped_movie_waits();
1065 if wipe_skipped {
1066 self.finish_wipe_runtime();
1067 }
1068 wipe_skipped
1069 }
1070
1071 pub fn notify_movie_wait_down_up(&mut self, result: i64) -> bool {
1072 let skipped = {
1073 let wait = &mut self.wait;
1074 let globals = &mut self.globals;
1075 wait.notify_movie_down_up(globals, &self.ids, result)
1076 };
1077 if skipped {
1078 if sg_debug_enabled() {
1079 eprintln!("[SG_DEBUG][WAIT_KEY] down_up result={}", result);
1080 }
1081 self.finish_skipped_movie_waits();
1082 if !self.globals.mov.playing && self.globals.mov.file_name.is_some() {
1083 self.close_global_movie_runtime();
1084 }
1085 }
1086 skipped
1087 }
1088
1089 fn runtime_ctrl_skip_flags(&self) -> (bool, bool) {
1090 if self.globals.focused_editbox.is_some() {
1093 return (false, false);
1094 }
1095 let ctrl = self.input.vk_is_down(0x11);
1096 let shift = self.input.vk_is_down(0x10);
1097 (ctrl && !shift, ctrl && shift)
1098 }
1099
1100 fn runtime_is_skip_unread_msg(&self) -> bool {
1101 let configured = self
1102 .globals
1103 .syscom
1104 .config_int
1105 .get(&syscom_op::GET_SKIP_UNREAD_MESSAGE_ONOFF)
1106 .copied()
1107 .unwrap_or({
1108 if self.globals.syscom.original_config.skip_unread_message_flag {
1109 1
1110 } else {
1111 0
1112 }
1113 })
1114 != 0;
1115 self.globals.script.skip_unread_message || configured
1116 }
1117
1118 fn runtime_read_skip_is_enable(&self) -> bool {
1119 let f = self.globals.syscom.read_skip;
1120 f.enable && f.exist && (self.runtime_is_skip_unread_msg() || self.read_skip_enable_flag)
1121 }
1122
1123 fn set_current_read_flag_for_skip(&mut self, scene_no: i64, flag_no: i64) {
1124 self.globals.script.cur_read_flag_scn_no = scene_no;
1125 self.globals.script.cur_read_flag_flag_no = flag_no;
1126 self.runtime_update_read_skip();
1129 self.runtime_update_start_skip();
1130 }
1131
1132 fn runtime_update_read_skip(&mut self) {
1133 let scene_no = self.globals.script.cur_read_flag_scn_no;
1134 let flag_no = self.globals.script.cur_read_flag_flag_no;
1135 self.read_skip_enable_flag = self.globals.read_flag(scene_no, flag_no);
1136 if !self.read_skip_enable_flag && !self.runtime_is_skip_unread_msg() {
1137 self.globals.syscom.read_skip.onoff = false;
1138 self.cant_auto_skip_before_click = true;
1139 }
1140 if !self.runtime_read_skip_is_enable() {
1141 self.globals.syscom.read_skip.onoff = false;
1142 }
1143 }
1144
1145 fn runtime_update_start_skip(&mut self) {
1146 if self.cant_auto_skip_before_click {
1147 return;
1148 }
1149 if self.globals.syscom.auto_skip.onoff
1150 && (self.read_skip_enable_flag || self.runtime_is_skip_unread_msg())
1151 && self.runtime_read_skip_is_enable()
1152 {
1153 self.globals.syscom.read_skip.onoff = true;
1154 }
1155 }
1156
1157 fn runtime_update_read_skip_menu(&mut self) {
1158 if !self.runtime_read_skip_is_enable() {
1159 self.globals.syscom.read_skip.onoff = false;
1160 }
1161 }
1162
1163 fn runtime_is_skipping(&self) -> bool {
1164 if self.globals.syscom.msg_back_open {
1166 return false;
1167 }
1168 let script = &self.globals.script;
1169 if script.ctrl_disable {
1170 return false;
1171 }
1172 let (ctrl_skip, _) = self.runtime_ctrl_skip_flags();
1173 if ctrl_skip {
1174 return true;
1175 }
1176 if script.skip_disable {
1177 return false;
1178 }
1179 self.skip_because_skip_trigger || self.globals.syscom.read_skip.onoff
1180 }
1181
1182 fn runtime_is_skipping_msg(&self) -> bool {
1183 if self.globals.syscom.msg_back_open {
1185 return false;
1186 }
1187 let script = &self.globals.script;
1188 if script.ctrl_disable {
1189 return false;
1190 }
1191 let (ctrl_skip, ctrl_shift_skip) = self.runtime_ctrl_skip_flags();
1192 if ctrl_skip || ctrl_shift_skip {
1193 return true;
1194 }
1195 if script.skip_disable {
1196 return false;
1197 }
1198 self.skip_because_skip_trigger || self.globals.syscom.read_skip.onoff
1199 }
1200
1201 fn clear_script_trigger_skip(&mut self) {
1202 self.skip_because_skip_trigger = false;
1203 }
1204
1205 fn register_skip_user_action(&mut self) {
1206 self.cant_auto_skip_before_click = false;
1208 }
1209
1210 fn stop_read_skip_by_decide_down(&mut self, vk: u8) -> bool {
1211 self.register_skip_user_action();
1212 if self.globals.syscom.read_skip.onoff
1217 && !self.globals.script.not_stop_skip_by_click
1218 && self.input.use_vk_down_stock(vk)
1219 {
1220 self.globals.syscom.read_skip.onoff = false;
1221 self.cant_auto_skip_before_click = true;
1222 return true;
1223 }
1224 false
1225 }
1226
1227 fn process_script_skip_trigger_decide(&mut self, vk: u8) -> bool {
1230 if self.excall_state.ex_call_flag || !self.globals.script.skip_trigger {
1231 return false;
1232 }
1233
1234 if self.wait.message_reveal_waiting() && !self.ui.message_wait_text_fully_revealed() {
1235 if self.globals.script.not_skip_msg_by_click || !self.input.use_vk_down_up_stock(vk) {
1236 return false;
1237 }
1238 self.msg_wait_skip_by_click = true;
1239 self.ui.reveal_message_now();
1240 return true;
1241 }
1242
1243 if self.input.use_vk_up_stock(vk) {
1247 self.skip_because_skip_trigger = true;
1248 return true;
1249 }
1250 false
1251 }
1252
1253 fn process_script_skip_trigger_wheel_down(&mut self) -> bool {
1256 if self.excall_state.ex_call_flag || !self.globals.script.skip_trigger {
1257 return false;
1258 }
1259 if self.wait.message_reveal_waiting() && !self.ui.message_wait_text_fully_revealed() {
1260 if self.globals.script.not_skip_msg_by_click {
1261 return false;
1262 }
1263 self.msg_wait_skip_by_click = true;
1264 self.ui.reveal_message_now();
1265 return true;
1266 }
1267 self.skip_because_skip_trigger = true;
1268 true
1269 }
1270
1271 fn should_wheel_advance_message(&self) -> bool {
1272 const GET_WHEEL_NEXT_MESSAGE_ONOFF: i32 = 305;
1273 self.globals
1274 .syscom
1275 .config_int
1276 .get(&GET_WHEEL_NEXT_MESSAGE_ONOFF)
1277 .copied()
1278 .unwrap_or(1)
1279 != 0
1280 }
1281
1282 fn should_stop_koe_on_advance(&self) -> bool {
1283 const GET_KOE_DONT_STOP_ONOFF: i32 = 308;
1284 let syscom_dont_stop = self
1285 .globals
1286 .syscom
1287 .config_int
1288 .get(&GET_KOE_DONT_STOP_ONOFF)
1289 .copied()
1290 .unwrap_or(0)
1291 != 0;
1292 let script = &self.globals.script;
1293 let mut dont_stop = syscom_dont_stop || script.koe_dont_stop_on_flag;
1294 if script.koe_dont_stop_off_flag {
1295 dont_stop = false;
1296 }
1297 !dont_stop
1298 }
1299
1300 fn is_modifier_key(k: input::VmKey) -> bool {
1301 matches!(
1302 k,
1303 input::VmKey::Shift | input::VmKey::Control | input::VmKey::Meta | input::VmKey::Alt
1304 )
1305 }
1306
1307 fn cancel_pending_editbox_composition(&mut self) {
1308 let Some((form_id, idx)) = self.globals.focused_editbox else {
1309 return;
1310 };
1311 if let Some(eb) = self
1312 .globals
1313 .editbox_lists
1314 .get_mut(&form_id)
1315 .and_then(|list| list.boxes.get_mut(idx))
1316 {
1317 eb.cancel_pending_composition_clear();
1318 }
1319 }
1320
1321 pub(crate) fn set_focused_editbox(&mut self, target: Option<(u32, usize)>) {
1322 let old = self.globals.focused_editbox;
1323 if old == target {
1324 return;
1325 }
1326 if let Some((form_id, idx)) = old
1327 && let Some(eb) = self
1328 .globals
1329 .editbox_lists
1330 .get_mut(&form_id)
1331 .and_then(|list| list.boxes.get_mut(idx))
1332 {
1333 eb.cancel_composition();
1334 eb.mouse_selecting = false;
1335 }
1336 self.globals.focused_editbox = target;
1337 if let Some((form_id, idx)) = target
1338 && let Some(eb) = self
1339 .globals
1340 .editbox_lists
1341 .get_mut(&form_id)
1342 .and_then(|list| list.boxes.get_mut(idx))
1343 {
1344 eb.ensure_caret_visible_for_focus();
1345 }
1346 }
1347
1348 fn sync_editbox_runtime(&mut self) {
1349 let sw = self.screen_w as i32;
1350 let sh = self.screen_h as i32;
1351 let display_cnt = self.globals.change_display_mode_proc_cnt;
1352 for list in self.globals.editbox_lists.values_mut() {
1353 for eb in &mut list.boxes {
1354 eb.update_rect(sw, sh);
1355 eb.frame(display_cnt);
1356 }
1357 }
1358 if let Some((form_id, idx)) = self.globals.focused_editbox {
1359 let keep = self
1360 .globals
1361 .editbox_lists
1362 .get(&form_id)
1363 .and_then(|list| list.boxes.get(idx))
1364 .map(|eb| eb.created)
1365 .unwrap_or(false);
1366 if !keep {
1367 self.set_focused_editbox(None);
1368 }
1369 }
1370 }
1371
1372 fn toggle_screen_size_mode_for_editbox(&mut self) {
1373 const GET_WINDOW_MODE: i32 = syscom_op::GET_WINDOW_MODE;
1374 let current = self
1375 .globals
1376 .syscom
1377 .config_int
1378 .get(&GET_WINDOW_MODE)
1379 .copied()
1380 .unwrap_or(0);
1381 let next = if current == 0 { 1 } else { 0 };
1382 self.globals.syscom.config_int.insert(GET_WINDOW_MODE, next);
1383 self.globals.change_display_mode_proc_cnt =
1384 self.globals.change_display_mode_proc_cnt.max(2);
1385 }
1386
1387 fn move_editbox_focus(&mut self, forward: bool) {
1388 let Some((form_id, idx)) = self.globals.focused_editbox else {
1389 return;
1390 };
1391 let target = {
1392 let Some(list) = self.globals.editbox_lists.get(&form_id) else {
1393 return;
1394 };
1395 let len = list.boxes.len();
1396 if len == 0 {
1397 return;
1398 }
1399 let mut cur = idx;
1400 let mut target = None;
1401 for _ in 0..len {
1402 cur = if forward {
1403 (cur + 1) % len
1404 } else {
1405 (cur + len - 1) % len
1406 };
1407 if list.boxes.get(cur).is_some_and(|eb| eb.created) {
1408 target = Some((form_id, cur));
1409 break;
1410 }
1411 }
1412 target
1413 };
1414 if target.is_some() {
1415 self.set_focused_editbox(target);
1416 }
1417 }
1418
1419 fn advance_message_wait(&mut self, allow: bool) -> bool {
1424 if !allow || !self.ui.mwnd.msg.waiting {
1425 return false;
1426 }
1427 if !self.ui.message_wait_text_fully_revealed() {
1428 self.ui.reveal_message_now();
1429 return true;
1430 }
1431 if !self.wait.message_key_waiting() {
1434 return false;
1435 }
1436 self.wait.finish_message_key_wait();
1437 match self.ui.end_wait_message() {
1438 ui::MessageWaitClearAction::None => {}
1439 ui::MessageWaitClearAction::Clear => self.clear_current_mwnd_after_wait(),
1440 ui::MessageWaitClearAction::NovelClear => {
1441 forms::stage::novel_clear_current_mwnd_after_wait(self);
1442 }
1443 }
1444 if self.should_stop_koe_on_advance() {
1445 let _ = self.koe.stop(None);
1448 self.globals.sound_routing.bgmfade2_need_flag = false;
1449 }
1450 false
1451 }
1452
1453 fn clear_current_mwnd_after_wait(&mut self) {
1454 self.clear_script_trigger_skip();
1457 let default_form_id = if self.ids.form_global_stage != 0 {
1458 self.ids.form_global_stage
1459 } else {
1460 constants::global_form::STAGE_ALT
1461 };
1462 let target = self.globals.focused_stage_mwnd.unwrap_or((
1463 default_form_id,
1464 self.globals.current_mwnd_stage_idx,
1465 self.globals.current_mwnd_no.unwrap_or(0),
1466 ));
1467 let (form_id, stage_idx, mwnd_idx) = target;
1468 let mut read_flags = Vec::new();
1469 if let Some(m) = self
1470 .globals
1471 .stage_forms
1472 .get_mut(&form_id)
1473 .and_then(|st| st.mwnd_lists.get_mut(&stage_idx))
1474 .and_then(|list| list.get_mut(mwnd_idx))
1475 {
1476 m.clear_ready = true;
1481 m.msg_block_started = false;
1482 m.multi_msg = false;
1483 m.key_icon_appear = false;
1484 m.key_icon_pos = None;
1485 m.text_dirty = false;
1486 read_flags = std::mem::take(&mut m.read_flag_stock);
1487 }
1488 for (scene_no, flag_no) in read_flags {
1489 self.globals.set_read_flag(scene_no, flag_no);
1490 }
1491 self.globals.script.multi_msg_mode = false;
1492 self.globals.script.cur_koe_no = -1;
1493 self.globals.script.cur_chr_no = -1;
1494 self.globals.script.auto_mode_moji_cnt = 0;
1495 self.globals.syscom.replay_koe = None;
1496 if self.globals.script.async_msg_mode_once {
1497 self.globals.script.async_msg_mode = false;
1498 self.globals.script.async_msg_mode_once = false;
1499 }
1500 }
1501 pub fn new(project_dir: PathBuf) -> Self {
1502 let mut unknown = unknown::UnknownOpRecorder::default();
1503 let tables = tables::AssetTables::load(&project_dir, &mut unknown);
1504 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1507 let emote_key = crate::emote_key::preload_emote_key(&project_dir);
1508 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1509 let emote_key = crate::resource::load_project_emote_key(&project_dir)
1510 .ok()
1511 .flatten();
1512 let initial_append = crate::resource::initial_select_ini_append(&project_dir);
1513
1514 let ids = constants::RuntimeConstants::default();
1515
1516 let audio = AudioHub::new();
1517 let mut images = ImageManager::new(project_dir.clone());
1518 let solid_white = images.solid_rgba((255, 255, 255, 255));
1519 let tonecurve = tonecurve::ToneCurveRuntime::new(&project_dir);
1520
1521 let mut ctx = Self {
1522 images,
1523 layers: LayerManager::default(),
1524 audio,
1525 bgm: BgmEngine::new(project_dir.clone()),
1526 koe: KoeEngine::new(project_dir.clone()),
1527 pcm: PcmEngine::new(project_dir.clone()),
1528 se: SeEngine::new(project_dir.clone()),
1529 movie: MovieManager::new(project_dir.clone()),
1530 project_dir,
1531 emote_key,
1532 solid_white,
1533 tables,
1534 scene_metadata: RefCell::new(None),
1535 stack: Vec::new(),
1536 unknown,
1537 ids,
1538 int_vec_pool: IntVecPool::default(),
1539 gfx: graphics::GfxRuntime::default(),
1540 ui: ui::UiRuntime::default(),
1541 font_cache: FontCache::new(),
1542 input: input::InputState::default(),
1543 script_input: input::InputState::default(),
1544 wait: wait::VmWait::default(),
1545 proc_generation: 0,
1546 last_proc_kind: ProcKind::Script,
1547 net: net::TnmNet::default(),
1548
1549 screen_w: 1280,
1550 screen_h: 720,
1551 frame_capture_backend: None,
1552 globals: globals::GlobalState::default(),
1553 tonecurve,
1554 excall_state: ExcallCompatState::default(),
1555 render_object_keys_scratch: HashSet::default(),
1556 mouse_cursor_cache: HashMap::default(),
1557 failed_gfx_image_repairs: HashSet::default(),
1558 external_forms: None,
1559 native_ui_backend: None,
1560 native_ui: native_ui::NativeUiRuntime::default(),
1561 current_scene_no: None,
1562 current_scene_name: None,
1563 current_line_no: -1,
1564 vm_call: None,
1565 pending_read_flag_no: false,
1566 pending_selbtn_read_flag_no: false,
1567 pending_mwnd_read_flag_target: None,
1568 skip_because_skip_trigger: false,
1569 msg_wait_skip_by_click: false,
1570 read_skip_enable_flag: false,
1571 cant_auto_skip_before_click: false,
1572 button_decide_capture: false,
1573 disp_because_msg_wait_cnt: 0,
1574 disp_because_msg_wait_cnt_max: 0,
1575 frame_rate_100msec: [0; 10],
1576 frame_rate_100msec_index: 0,
1577 frame_rate_100msec_total: 0,
1578 frame_rate_bucket_frame_cnt: 0,
1579 frame_rate_bucket_elapsed_ms: 0,
1580 frame_main_proc_started_at: None,
1581 pending_runtime_save: None,
1582 pending_runtime_load: None,
1583 pending_scene_restart: None,
1584 runtime_load_completed: false,
1585 local_save_snapshot: None,
1586 pending_auto_savepoint: false,
1587 pending_sel_point_result: None,
1588 frame_clock_last: None,
1589 frame_clock_sub_ms_ns: 0,
1590 last_button_hover_sound_pos: None,
1591 suppress_next_right_syscom_open: false,
1592 };
1593 ctx.set_active_append(initial_append.dir, initial_append.name);
1594 ctx.apply_gameexe_runtime_defaults();
1595 ctx
1596 }
1597
1598 pub fn set_active_append(&mut self, append_dir: String, append_name: String) {
1602 if !self.globals.append_dir.eq_ignore_ascii_case(&append_dir) {
1603 self.scene_metadata.get_mut().take();
1604 }
1605 self.globals.append_dir = append_dir;
1606 self.globals.append_name = append_name;
1607 let active_append = self.globals.append_dir.clone();
1608 self.images.set_current_append_dir_ref(&active_append);
1609 self.movie.set_current_append_dir_ref(&active_append);
1610 self.bgm.set_current_append_dir_ref(&active_append);
1611 }
1612
1613 pub fn reset_active_append_to_initial(&mut self) {
1616 let append = crate::resource::initial_select_ini_append(&self.project_dir);
1617 self.set_active_append(append.dir, append.name);
1618 }
1619
1620 pub(crate) fn koe_jitan_rate(&self, explicit: Option<bool>, replay: bool) -> u16 {
1627 use crate::runtime::forms::codes::syscom_op::*;
1628
1629 let cfg = |op: i32, fallback: i64| {
1630 self.globals
1631 .syscom
1632 .config_int
1633 .get(&op)
1634 .copied()
1635 .unwrap_or(fallback)
1636 };
1637 let original = &self.globals.syscom.original_config;
1638
1639 let enabled = if let Some(enabled) = explicit {
1640 enabled
1641 } else if replay {
1642 cfg(
1643 GET_JITAN_KOE_REPLAY_ONOFF,
1644 if original.jitan_msgbk_onoff { 1 } else { 0 },
1645 ) != 0
1646 } else {
1647 let auto_mode =
1650 self.globals.script.auto_mode_flag || self.globals.syscom.auto_mode.onoff;
1651 let (op, fallback) = if auto_mode {
1652 (
1653 GET_JITAN_AUTO_MODE_ONOFF,
1654 if original.jitan_auto_mode_onoff { 1 } else { 0 },
1655 )
1656 } else {
1657 (
1658 GET_JITAN_NORMAL_ONOFF,
1659 if original.jitan_normal_onoff { 1 } else { 0 },
1660 )
1661 };
1662 cfg(op, fallback) != 0
1663 };
1664
1665 if !enabled {
1666 return 100;
1667 }
1668
1669 cfg(GET_JITAN_SPEED, original.jitan_speed).clamp(100, 400) as u16
1673 }
1674
1675 pub(crate) fn effective_font_name(&self) -> &str {
1676 let config = &self.globals.syscom.original_config.font_name;
1677 if self.globals.syscom.msg_back_open {
1678 return config;
1679 }
1680 if self.excall_state.ex_call_flag {
1681 if self.excall_state.font_name.is_empty() {
1682 config
1683 } else {
1684 &self.excall_state.font_name
1685 }
1686 } else if self.globals.script.font_name.is_empty() {
1687 config
1688 } else {
1689 &self.globals.script.font_name
1690 }
1691 }
1692
1693 pub(crate) fn effective_font_shadow_mode(&self) -> i64 {
1694 let config = self.globals.syscom.original_config.font_shadow;
1695 let value = if self.globals.syscom.msg_back_open {
1696 config
1697 } else if self.excall_state.ex_call_flag {
1698 if self.excall_state.font_shadow >= 0 {
1699 self.excall_state.font_shadow
1700 } else {
1701 config
1702 }
1703 } else if self.globals.script.font_shadow >= 0 {
1704 self.globals.script.font_shadow
1705 } else {
1706 config
1707 };
1708 crate::text_render::normalize_font_shadow_mode(value)
1709 }
1710
1711 pub(crate) fn effective_font_bold(&self) -> bool {
1712 let config = self.globals.syscom.original_config.font_futoku;
1713 if self.globals.syscom.msg_back_open {
1714 config
1715 } else if self.excall_state.ex_call_flag {
1716 if self.excall_state.font_bold >= 0 {
1717 self.excall_state.font_bold != 0
1718 } else {
1719 config
1720 }
1721 } else if self.globals.script.font_bold >= 0 {
1722 self.globals.script.font_bold != 0
1723 } else {
1724 config
1725 }
1726 }
1727
1728 fn apply_gameexe_runtime_defaults(&mut self) {
1729 self.initialize_flag_lists();
1730 self.globals.script.cursor_no = self.mouse_cursor_default_no();
1731 self.globals.script.font_bold = -1;
1734 self.globals.script.font_shadow = -1;
1735 let text = self.gameexe_color(self.tables.mwnd_render.moji_color);
1736 let shadow = self.gameexe_color(self.tables.mwnd_render.shadow_color);
1737 let fuchi = (self.tables.mwnd_render.fuchi_color >= 0)
1738 .then_some(self.gameexe_color(self.tables.mwnd_render.fuchi_color));
1739 self.ui.set_text_colors_full(text, shadow, fuchi);
1740 }
1741
1742 fn configured_flag_count(&self, global: bool) -> usize {
1743 let keys = if global {
1744 ["#GLOBAL_FLAG.CNT", "GLOBAL_FLAG.CNT"]
1745 } else {
1746 ["#FLAG.CNT", "FLAG.CNT"]
1747 };
1748 self.tables
1749 .gameexe
1750 .as_ref()
1751 .and_then(|cfg| keys.into_iter().find_map(|key| cfg.get_usize(key)))
1752 .unwrap_or(1000)
1753 .min(10000)
1754 }
1755
1756 fn initialize_flag_lists(&mut self) {
1757 use crate::runtime::forms::codes;
1758
1759 let local_count = self.configured_flag_count(false);
1760 let global_count = self.configured_flag_count(true);
1761 for form in [
1762 codes::ELM_GLOBAL_A,
1763 codes::ELM_GLOBAL_B,
1764 codes::ELM_GLOBAL_C,
1765 codes::ELM_GLOBAL_D,
1766 codes::ELM_GLOBAL_E,
1767 codes::ELM_GLOBAL_F,
1768 codes::ELM_GLOBAL_X,
1769 ] {
1770 let list = self.globals.int_lists.entry(form as u32).or_default();
1771 if list.len() < local_count {
1772 list.resize(local_count, 0);
1773 }
1774 }
1775 for form in [codes::ELM_GLOBAL_G, codes::ELM_GLOBAL_Z] {
1776 let list = self.globals.int_lists.entry(form as u32).or_default();
1777 if list.len() < global_count {
1778 list.resize(global_count, 0);
1779 }
1780 }
1781 let local_strings = self
1782 .globals
1783 .str_lists
1784 .entry(codes::ELM_GLOBAL_S as u32)
1785 .or_default();
1786 if local_strings.len() < local_count {
1787 local_strings.resize_with(local_count, String::new);
1788 }
1789 let global_strings = self
1790 .globals
1791 .str_lists
1792 .entry(codes::ELM_GLOBAL_M as u32)
1793 .or_default();
1794 if global_strings.len() < global_count {
1795 global_strings.resize_with(global_count, String::new);
1796 }
1797 for form in [
1798 codes::ELM_GLOBAL_NAMAE_LOCAL,
1799 codes::ELM_GLOBAL_NAMAE_GLOBAL,
1800 ] {
1801 let list = self.globals.str_lists.entry(form as u32).or_default();
1802 let name_count = 26 + 26 * 26;
1803 if list.len() < name_count {
1804 list.resize_with(name_count, String::new);
1805 }
1806 }
1807 }
1808
1809 fn gameexe_color(&self, color_no: i64) -> (u8, u8, u8) {
1810 if color_no >= 0
1811 && let Some(&c) = self.tables.color_table.get(color_no as usize)
1812 {
1813 return c;
1814 }
1815 (255, 255, 255)
1816 }
1817
1818 fn gameexe_value(&self, key: &str) -> Option<&str> {
1819 self.tables.gameexe.as_ref()?.get_value(key)
1820 }
1821
1822 fn gameexe_raw(&self, key: &str) -> Option<&str> {
1823 self.tables.gameexe.as_ref()?.get_unquoted(key)
1824 }
1825
1826 fn gameexe_string(&self, key: &str) -> Option<String> {
1827 self.gameexe_raw(key)
1828 .map(|s| s.trim().to_string())
1829 .filter(|s| !s.is_empty())
1830 }
1831
1832 fn gameexe_rgba_default(&self, key: &str, default: (u8, u8, u8, u8)) -> (u8, u8, u8, u8) {
1833 let vals = Self::parse_i64_list(self.gameexe_value(key));
1834 if vals.len() >= 4 {
1835 (
1836 vals[0].clamp(0, 255) as u8,
1837 vals[1].clamp(0, 255) as u8,
1838 vals[2].clamp(0, 255) as u8,
1839 vals[3].clamp(0, 255) as u8,
1840 )
1841 } else {
1842 default
1843 }
1844 }
1845
1846 fn syscom_filter_config_rgba(&self) -> (u8, u8, u8, u8) {
1847 let default = self.gameexe_rgba_default("CONFIG.FILTER_COLOR", (0, 0, 0, 128));
1848 let cfg = &self.globals.syscom.config_int;
1849 let pick = |key: i32, fallback: u8| -> u8 {
1850 cfg.get(&key)
1851 .copied()
1852 .unwrap_or(fallback as i64)
1853 .clamp(0, 255) as u8
1854 };
1855 (
1856 pick(syscom_op::GET_FILTER_COLOR_R, default.0),
1857 pick(syscom_op::GET_FILTER_COLOR_G, default.1),
1858 pick(syscom_op::GET_FILTER_COLOR_B, default.2),
1859 pick(syscom_op::GET_FILTER_COLOR_A, default.3),
1860 )
1861 }
1862
1863 fn parse_first_i64(raw: &str) -> Option<i64> {
1864 raw.split(|c: char| c == ',' || c.is_whitespace())
1865 .find_map(|part| {
1866 let t = part.trim();
1867 if t.is_empty() {
1868 None
1869 } else {
1870 t.parse::<i64>().ok()
1871 }
1872 })
1873 }
1874
1875 fn parse_i64_list(raw: Option<&str>) -> Vec<i64> {
1876 let Some(raw) = raw else {
1877 return Vec::new();
1878 };
1879 raw.split(|c: char| c == ',' || c.is_whitespace())
1880 .filter_map(|part| {
1881 let t = part.trim();
1882 if t.is_empty() {
1883 None
1884 } else {
1885 t.parse::<i64>().ok()
1886 }
1887 })
1888 .collect()
1889 }
1890
1891 fn gameexe_i64_default(&self, key: &str, default: i64) -> i64 {
1892 self.gameexe_value(key)
1893 .and_then(Self::parse_first_i64)
1894 .unwrap_or(default)
1895 }
1896
1897 fn mouse_cursor_count(&self) -> usize {
1898 self.gameexe_value("#MOUSE_CURSOR.CNT")
1899 .or_else(|| self.gameexe_value("MOUSE_CURSOR.CNT"))
1900 .and_then(Self::parse_first_i64)
1901 .filter(|v| *v >= 0)
1902 .map(|v| v as usize)
1903 .unwrap_or(16)
1904 .min(256)
1905 }
1906
1907 fn mouse_cursor_default_no(&self) -> i64 {
1908 let cnt = self.mouse_cursor_count() as i64;
1909 let no = self
1910 .gameexe_value("#MOUSE_CURSOR.DEFAULT")
1911 .or_else(|| self.gameexe_value("MOUSE_CURSOR.DEFAULT"))
1912 .and_then(Self::parse_first_i64)
1913 .unwrap_or(-1);
1914 if no >= 0 && no < cnt { no } else { -1 }
1915 }
1916
1917 fn mouse_cursor_file_name(&self, cursor_no: i64) -> Option<String> {
1918 if cursor_no < 0 || cursor_no as usize >= self.mouse_cursor_count() {
1919 return None;
1920 }
1921 self.tables
1922 .gameexe
1923 .as_ref()
1924 .and_then(|cfg| {
1925 cfg.get_indexed_field_unquoted("MOUSE_CURSOR", cursor_no as usize, "FILE")
1926 })
1927 .map(|s| s.trim().to_string())
1928 .filter(|s| !s.is_empty())
1929 }
1930
1931 fn mouse_cursor_anime_speed(&self, cursor_no: i64) -> i64 {
1932 if cursor_no < 0 || cursor_no as usize >= self.mouse_cursor_count() {
1933 return 100;
1934 }
1935 self.tables
1936 .gameexe
1937 .as_ref()
1938 .and_then(|cfg| cfg.get_indexed_field("MOUSE_CURSOR", cursor_no as usize, "SPEED"))
1939 .and_then(Self::parse_first_i64)
1940 .unwrap_or(100)
1941 }
1942
1943 fn load_mouse_cursor_runtime(&mut self, cursor_no: i64) -> Option<&MouseCursorRuntime> {
1944 let append_dir = self.images.current_append_dir().to_string();
1945 let key = (cursor_no, append_dir);
1946 if !self.mouse_cursor_cache.contains_key(&key)
1947 && let Some(loaded) = self.load_mouse_cursor_runtime_uncached(cursor_no)
1948 {
1949 self.mouse_cursor_cache.insert(key.clone(), loaded);
1950 }
1951 self.mouse_cursor_cache.get(&key)
1952 }
1953
1954 fn load_mouse_cursor_runtime_uncached(&mut self, cursor_no: i64) -> Option<MouseCursorRuntime> {
1955 let file_name = self.mouse_cursor_file_name(cursor_no)?;
1956 let (path, pct) = match crate::resource::find_g00_image_with_append_dir(
1957 &self.project_dir,
1958 self.images.current_append_dir(),
1959 &file_name,
1960 ) {
1961 Ok(v) => v,
1962 Err(err) => {
1963 self.unknown.record_note(&format!(
1964 "mouse_cursor.not_found:no={cursor_no}:file={file_name}:{err}"
1965 ));
1966 return None;
1967 }
1968 };
1969 if pct != crate::resource::PctType::G00 {
1970 self.unknown.record_note(&format!(
1971 "mouse_cursor.unsupported_type:no={cursor_no}:file={file_name}:path={}",
1972 path.display()
1973 ));
1974 return None;
1975 }
1976 let bytes = match crate::resource::read_file_bytes(&path) {
1977 Ok(v) => v,
1978 Err(err) => {
1979 self.unknown.record_note(&format!(
1980 "mouse_cursor.read_failed:no={cursor_no}:path={}:{}",
1981 path.display(),
1982 err
1983 ));
1984 return None;
1985 }
1986 };
1987 let decoded = match crate::assets::g00::decode_g00(&bytes) {
1988 Ok(v) => v,
1989 Err(err) => {
1990 self.unknown.record_note(&format!(
1991 "mouse_cursor.decode_failed:no={cursor_no}:path={}:{}",
1992 path.display(),
1993 err
1994 ));
1995 return None;
1996 }
1997 };
1998 if decoded.frames.is_empty() {
1999 return None;
2000 }
2001 let mut frames = Vec::with_capacity(decoded.frames.len());
2002 for (idx, img) in decoded.frames.iter().enumerate() {
2003 if img.width != 32 || img.height != 32 {
2004 self.unknown.record_note(&format!(
2005 "mouse_cursor.invalid_size:no={cursor_no}:file={file_name}:patno={idx}:{}x{}",
2006 img.width, img.height
2007 ));
2008 return None;
2009 }
2010 let image_id = match self.images.load_file(&path, idx) {
2011 Ok(id) => id,
2012 Err(err) => {
2013 self.unknown.record_note(&format!(
2014 "mouse_cursor.frame_load_failed:no={cursor_no}:file={file_name}:patno={idx}:{err}"
2015 ));
2016 return None;
2017 }
2018 };
2019 frames.push(MouseCursorFrameRuntime {
2020 image_id,
2021 hot_x: img.center_x,
2022 hot_y: img.center_y,
2023 });
2024 }
2025 Some(MouseCursorRuntime {
2026 frames,
2027 anime_speed_ms: self.mouse_cursor_anime_speed(cursor_no),
2028 })
2029 }
2030
2031 pub fn has_active_custom_mouse_cursor(&mut self) -> bool {
2032 let cursor_no = self.globals.script.cursor_no;
2033 self.load_mouse_cursor_runtime(cursor_no).is_some()
2034 }
2035
2036 fn custom_mouse_cursor_needs_tick(&self) -> bool {
2037 let cursor_no = self.globals.script.cursor_no;
2038 if cursor_no < 0 || cursor_no as usize >= self.mouse_cursor_count() {
2039 return false;
2040 }
2041 self.mouse_cursor_anime_speed(cursor_no) > 0
2042 && self.mouse_cursor_file_name(cursor_no).is_some()
2043 }
2044
2045 fn append_mouse_cursor_sprite(&mut self, list: &mut Vec<RenderSprite>) {
2046 if !self.globals.script.cursor_runtime_visible || self.globals.script.cursor_disp_off {
2047 return;
2048 }
2049 if !self.input.has_mouse_position() {
2050 return;
2051 }
2052 let cursor_no = self.globals.script.cursor_no;
2053 let cur_time = self.globals.local_real_time.max(0) as u64;
2054 let frame = {
2055 let Some(cursor) = self.load_mouse_cursor_runtime(cursor_no) else {
2056 return;
2057 };
2058 if cursor.frames.is_empty() {
2059 return;
2060 }
2061 let pat_no = if cursor.anime_speed_ms <= 0 {
2062 0usize
2063 } else {
2064 ((cur_time / cursor.anime_speed_ms as u64) as usize) % cursor.frames.len()
2065 };
2066 cursor.frames[pat_no].clone()
2067 };
2068
2069 let mut sprite = Sprite {
2070 image_id: Some(frame.image_id.clone()),
2071 visible: true,
2072 fit: SpriteFit::PixelRect,
2073 size_mode: SpriteSizeMode::Intrinsic,
2074 x: self.input.mouse_x.saturating_sub(frame.hot_x),
2075 y: self.input.mouse_y.saturating_sub(frame.hot_y),
2076 alpha: 255,
2077 tr: 255,
2078 alpha_blend: true,
2079 alpha_test: false,
2080 object_anchor: false,
2081 order: i32::MAX,
2082 ..Default::default()
2083 };
2084
2085 list.push(RenderSprite::with_sorter(
2086 None,
2087 None,
2088 i32::MAX,
2089 i32::MAX,
2090 sprite,
2091 ));
2092 }
2093
2094 fn gameexe_pair_default(&self, key: &str, default: (i64, i64)) -> (i64, i64) {
2095 let vals = Self::parse_i64_list(self.gameexe_value(key));
2096 if vals.len() >= 2 {
2097 (vals[0], vals[1])
2098 } else {
2099 default
2100 }
2101 }
2102
2103 fn gameexe_rect_default(
2104 &self,
2105 key: &str,
2106 default: (i64, i64, i64, i64),
2107 ) -> (i64, i64, i64, i64) {
2108 let vals = Self::parse_i64_list(self.gameexe_value(key));
2109 if vals.len() >= 4 {
2110 (vals[0], vals[1], vals[2], vals[3])
2111 } else {
2112 default
2113 }
2114 }
2115
2116 fn msg_back_button_pos(&self, key: &str, default: (i32, i32)) -> (i32, i32) {
2117 let vals = Self::parse_i64_list(self.gameexe_value(key));
2118 if vals.len() >= 2 {
2119 (vals[0] as i32, vals[1] as i32)
2120 } else {
2121 default
2122 }
2123 }
2124
2125 #[doc(hidden)]
2126 pub fn install_scene_metadata(&self, append_dir: &str, pck: &ScenePck) -> Result<()> {
2127 let mut slot = self.scene_metadata.borrow_mut();
2128 if slot
2129 .as_ref()
2130 .is_some_and(|(cached_append, _)| cached_append.eq_ignore_ascii_case(append_dir))
2131 {
2132 return Ok(());
2133 }
2134 *slot = Some((
2135 append_dir.to_string(),
2136 Arc::new(SceneMetadata::from_pack(pck)?),
2137 ));
2138 Ok(())
2139 }
2140
2141 pub(crate) fn scene_metadata(&self) -> Result<Arc<SceneMetadata>> {
2142 let active_append = self.globals.append_dir.clone();
2143 if let Some((cached_append, metadata)) = self.scene_metadata.borrow().as_ref()
2144 && cached_append.eq_ignore_ascii_case(&active_append)
2145 {
2146 return Ok(Arc::clone(metadata));
2147 }
2148
2149 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
2150 let pck = {
2151 let scene_pck_path =
2152 crate::resource::find_scene_pck_path_for_append(&self.project_dir, &active_append)?;
2153 let bytes = crate::resource::read_file_bytes(&scene_pck_path)?;
2154 let key_cfg = ["key.toml", "Key.toml"].iter().find_map(|name| {
2155 let p = self.project_dir.join(name);
2156 if !crate::resource::wasm_path_is_file(&p) {
2157 return None;
2158 }
2159 let text = crate::resource::read_file_to_string(&p).ok()?;
2160 siglus_assets::key_toml::parse_key_toml(&text).ok()
2161 });
2162 let exe = key_cfg
2163 .as_ref()
2164 .and_then(|cfg| cfg.exe_key16)
2165 .map(|v| v.to_vec());
2166 let string_encryption_override = key_cfg
2167 .map(|cfg| cfg.override_string_encryption)
2168 .unwrap_or_default();
2169 let opt = ScenePckDecodeOptions {
2170 exe_angou_element: exe,
2171 easy_angou_code: Some(siglus_assets::keys::SCENE_KEY.to_vec()),
2172 string_encryption_override,
2173 };
2174 ScenePck::load_lazy_from_bytes(bytes, &opt)?
2175 };
2176 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
2177 let pck = {
2178 let scene_pck_path =
2179 crate::resource::find_scene_pck_path_for_append(&self.project_dir, &active_append)?;
2180 let opt = crate::resource::load_scene_pck_decode_options(&self.project_dir)?;
2181 ScenePck::load_lazy(&scene_pck_path, &opt)?
2182 };
2183 self.install_scene_metadata(&active_append, &pck)?;
2184 let slot = self.scene_metadata.borrow();
2185 Ok(Arc::clone(
2186 &slot.as_ref().expect("scene metadata installed").1,
2187 ))
2188 }
2189
2190 pub fn lookup_scene_no(&self, scene_name: &str) -> Result<i64> {
2191 if scene_name.is_empty() {
2192 anyhow::bail!("empty scene name")
2193 }
2194 let scene_no = self
2195 .scene_metadata()?
2196 .find_scene_no(scene_name)
2197 .ok_or_else(|| anyhow::anyhow!("scene not found: {}", scene_name))?;
2198 Ok(scene_no as i64)
2199 }
2200
2201 pub fn reset_for_scene_restart(&mut self) {
2210 use crate::runtime::forms::codes;
2211
2212 let append_dir = self.globals.append_dir.clone();
2213 let append_name = self.globals.append_name.clone();
2214
2215 self.bgm = BgmEngine::new(self.project_dir.clone());
2218 self.audio = AudioHub::new();
2219 self.koe = KoeEngine::new(self.project_dir.clone());
2220 self.pcm = PcmEngine::new(self.project_dir.clone());
2221 self.se = SeEngine::new(self.project_dir.clone());
2222 self.movie = MovieManager::new(self.project_dir.clone());
2223 self.images = ImageManager::new(self.project_dir.clone());
2224 self.mouse_cursor_cache.clear();
2225 self.solid_white = self.images.solid_rgba((255, 255, 255, 255));
2226 self.layers.clear_all();
2227 self.gfx = graphics::GfxRuntime::default();
2228 self.ui = ui::UiRuntime::default();
2229 self.font_cache = FontCache::new();
2230 self.wait = wait::VmWait::default();
2231 self.stack.clear();
2232
2233 let local_count = self.configured_flag_count(false);
2236 for form in [
2237 codes::ELM_GLOBAL_A,
2238 codes::ELM_GLOBAL_B,
2239 codes::ELM_GLOBAL_C,
2240 codes::ELM_GLOBAL_D,
2241 codes::ELM_GLOBAL_E,
2242 codes::ELM_GLOBAL_F,
2243 codes::ELM_GLOBAL_X,
2244 ] {
2245 self.globals
2246 .int_lists
2247 .insert(form as u32, vec![0; local_count]);
2248 }
2249 self.globals
2250 .str_lists
2251 .insert(codes::ELM_GLOBAL_S as u32, vec![String::new(); local_count]);
2252 self.globals.str_lists.insert(
2253 codes::ELM_GLOBAL_NAMAE_LOCAL as u32,
2254 vec![String::new(); 26 + 26 * 26],
2255 );
2256
2257 self.globals.counter_lists.clear();
2259 self.globals.pcm_event_lists.clear();
2260 self.globals.pcmch_persistent.clear();
2261 self.globals.sound_routing = globals::SoundRoutingState::default();
2262 self.globals.int_event_roots.clear();
2263 self.globals.int_event_lists.clear();
2264 self.globals.int_props.clear();
2265 self.globals.str_props.clear();
2266 self.globals.g00buf.clear();
2267 self.globals.g00buf_names.clear();
2268 self.globals.mask_lists.clear();
2269 self.globals.editbox_lists.clear();
2270 self.globals.focused_editbox = None;
2271 self.globals.frame_actions.clear();
2272 self.globals.frame_action_lists.clear();
2273 self.globals.pending_frame_action_finishes.clear();
2274 self.globals.pending_button_actions.clear();
2275 self.globals.stage_forms.clear();
2276 self.globals.focused_stage_group = None;
2277 self.globals.focused_stage_mwnd = None;
2278 self.globals.current_mwnd_element = vec![
2279 forms::codes::ELM_GLOBAL_FRONT,
2280 forms::codes::ELM_STAGE_MWND,
2281 forms::codes::ELM_ARRAY,
2282 0,
2283 ];
2284 self.globals.current_sel_mwnd_element = vec![
2285 forms::codes::ELM_GLOBAL_FRONT,
2286 forms::codes::ELM_STAGE_MWND,
2287 forms::codes::ELM_ARRAY,
2288 1,
2289 ];
2290 self.globals.last_mwnd_element.clear();
2291 self.globals.current_mwnd_no = Some(0);
2292 self.globals.current_mwnd_stage_idx = 1;
2293 self.globals.current_sel_mwnd_no = Some(1);
2294 self.globals.current_sel_mwnd_stage_idx = 1;
2295 self.globals.last_mwnd_no = None;
2296 self.globals.last_mwnd_stage_idx = 1;
2297 self.globals.local_real_time = 0;
2298 self.globals.local_game_time = 0;
2299 self.globals.local_wipe_time = 0;
2300 self.globals.local_flag_h.clear();
2301 self.globals.local_flag_i.clear();
2302 self.globals.local_flag_j.clear();
2303 self.globals.selbtn = globals::BtnSelectRuntimeState::default();
2304 self.globals.current_stage_object = None;
2305 self.globals.current_object_chain = None;
2306 self.globals.screen_forms.clear();
2307 self.globals.msgbk_forms.clear();
2308 self.globals.script = globals::ScriptRuntimeState::default();
2309 self.globals.mov = globals::GlobalMovieState::default();
2310 self.globals.capture_image = None;
2311 self.globals.capture_for_object_image = None;
2312 self.globals.save_thumb_capture_image = None;
2313 self.globals.save_thumb_capture_prior = 0;
2314 self.globals.wipe = None;
2315 self.globals.lights.clear();
2316 self.globals.fog_global = globals::FogGlobalState::default();
2317
2318 let total_play_time = self.globals.syscom.total_play_time;
2322 let system_extra_int_value = self.globals.syscom.system_extra_int_value;
2323 let system_extra_str_value =
2324 std::mem::take(&mut self.globals.syscom.system_extra_str_value);
2325 let config_int = std::mem::take(&mut self.globals.syscom.config_int);
2326 let config_str = std::mem::take(&mut self.globals.syscom.config_str);
2327 let original_config = self.globals.syscom.original_config.clone();
2328 let font_list = std::mem::take(&mut self.globals.syscom.font_list);
2329 let return_scene_once = self.globals.syscom.return_scene_once.take();
2330 self.globals.syscom = globals::SyscomRuntimeState::default();
2331 self.globals.syscom.total_play_time = total_play_time;
2332 self.globals.syscom.system_extra_int_value = system_extra_int_value;
2333 self.globals.syscom.system_extra_str_value = system_extra_str_value;
2334 self.globals.syscom.config_int = config_int;
2335 self.globals.syscom.config_str = config_str;
2336 self.globals.syscom.original_config = original_config;
2337 self.globals.syscom.font_list = font_list;
2338 self.globals.syscom.return_scene_once = return_scene_once;
2339
2340 self.tonecurve = tonecurve::ToneCurveRuntime::new(&self.project_dir);
2341 self.excall_state = ExcallCompatState::default();
2342 self.render_object_keys_scratch.clear();
2343 self.render_object_keys_scratch.shrink_to_fit();
2344 self.input.clear_all();
2345 self.script_input.clear_all();
2346 self.vm_call = None;
2347 self.pending_read_flag_no = false;
2348 self.pending_selbtn_read_flag_no = false;
2349 self.pending_mwnd_read_flag_target = None;
2350 self.skip_because_skip_trigger = false;
2351 self.msg_wait_skip_by_click = false;
2352 self.read_skip_enable_flag = false;
2353 self.cant_auto_skip_before_click = false;
2354 self.button_decide_capture = false;
2355 self.disp_because_msg_wait_cnt = 0;
2356 self.disp_because_msg_wait_cnt_max = 0;
2357 self.frame_rate_100msec = [0; 10];
2358 self.frame_rate_100msec_index = 0;
2359 self.frame_rate_100msec_total = 0;
2360 self.frame_rate_bucket_frame_cnt = 0;
2361 self.frame_rate_bucket_elapsed_ms = 0;
2362 self.frame_main_proc_started_at = None;
2363 self.pending_runtime_save = None;
2364 self.pending_runtime_load = None;
2365 self.pending_scene_restart = None;
2366 self.runtime_load_completed = false;
2367 self.local_save_snapshot = None;
2368 self.pending_auto_savepoint = false;
2369 self.pending_sel_point_result = None;
2370 self.frame_clock_last = None;
2371 self.frame_clock_sub_ms_ns = 0;
2372 self.last_button_hover_sound_pos = None;
2373
2374 self.set_active_append(append_dir, append_name);
2375 self.apply_gameexe_runtime_defaults();
2376 forms::syscom::apply_audio_config(self);
2377 }
2378
2379 pub fn set_external_form_handler(&mut self, h: Option<Arc<dyn ExternalFormHandler>>) {
2381 self.external_forms = h;
2382 }
2383
2384 fn active_button_stage_form_id(&self) -> Option<u32> {
2389 const EXCALL_LOCAL_NS_XOR: u32 = 0x4000;
2390 let normal_stage_form = if self.ids.form_global_stage != 0 {
2391 self.ids.form_global_stage
2392 } else {
2393 crate::runtime::forms::codes::FORM_GLOBAL_STAGE
2394 };
2395 if self.excall_state.ex_call_flag {
2396 if self.excall_state.ready {
2397 Some(normal_stage_form ^ EXCALL_LOCAL_NS_XOR)
2398 } else {
2399 None
2400 }
2401 } else {
2402 Some(normal_stage_form)
2403 }
2404 }
2405
2406 fn load_any_image_for_hit(
2407 images: &mut ImageManager,
2408 file: &str,
2409 patno: i64,
2410 ) -> Option<crate::image_manager::ImageHandle> {
2411 let pat_u32 = if patno < 0 { 0 } else { patno as u32 };
2412 if let Ok(id) = images.load_g00(file, pat_u32) {
2413 return Some(id);
2414 }
2415 if let Ok(id) = images.load_bg(file) {
2416 return Some(id);
2417 }
2418 None
2419 }
2420
2421 fn hit_test_sprite_rect(x: i32, y: i32, w: u32, h: u32, mx: i32, my: i32) -> bool {
2422 let x2 = x.saturating_add(w as i32);
2423 let y2 = y.saturating_add(h as i32);
2424 mx >= x && mx < x2 && my >= y && my < y2
2425 }
2426
2427 fn alpha_test_image(img: &crate::assets::RgbaImage, local_x: i32, local_y: i32) -> bool {
2428 if local_x < 0 || local_y < 0 {
2429 return false;
2430 }
2431 let lx = local_x as u32;
2432 let ly = local_y as u32;
2433 if lx >= img.width || ly >= img.height {
2434 return false;
2435 }
2436 let idx = ((ly * img.width + lx) * 4 + 3) as usize;
2437 img.rgba.get(idx).copied().unwrap_or(0) != 0
2438 }
2439
2440 fn play_button_se_no(&mut self, se_no: i64) {
2441 if se_no < 0 {
2442 return;
2443 }
2444 let Some(file_name) = self
2445 .tables
2446 .se_file_names
2447 .get(se_no as usize)
2448 .and_then(|v| v.as_deref())
2449 .filter(|s| !s.is_empty())
2450 else {
2451 self.unknown
2452 .record_note(&format!("se.table.missing:{se_no}"));
2453 return;
2454 };
2455 if self.se.play_file_name(&mut self.audio, file_name).is_err() {
2456 self.unknown
2457 .record_note(&format!("se.play.failed:{se_no}:{file_name}"));
2458 }
2459 }
2460
2461 fn button_template_se_no(&self, template_no: i64, event: ButtonSeEvent) -> Option<i64> {
2462 if template_no < 0 {
2463 return None;
2464 }
2465 let template = self.tables.button_se_templates.get(template_no as usize)?;
2466 let se_no = match event {
2467 ButtonSeEvent::Hit => template.hit_no,
2468 ButtonSeEvent::Push => template.push_no,
2469 ButtonSeEvent::Decide => template.decide_no,
2470 };
2471 (se_no >= 0).then_some(se_no)
2472 }
2473
2474 fn play_button_template_se(&mut self, template_no: i64, event: ButtonSeEvent) {
2475 if let Some(se_no) = self.button_template_se_no(template_no, event) {
2476 self.play_button_se_no(se_no);
2477 }
2478 }
2479
2480 fn update_object_button_hover(&mut self) {
2481 if !self.input.has_mouse_position() {
2482 return;
2483 }
2484 let mx = self.input.mouse_x;
2485 let my = self.input.mouse_y;
2486 let play_hover_sound = match self.last_button_hover_sound_pos {
2487 Some((last_x, last_y)) if last_x == mx && last_y == my => false,
2488 Some(_) => true,
2489 None => false,
2490 };
2491 self.last_button_hover_sound_pos = Some((mx, my));
2492 let Some(form_id) = self.active_button_stage_form_id() else {
2493 return;
2494 };
2495 let read_skip_runtime_enabled = self.runtime_read_skip_is_enable();
2496 let mut hit_sounds = Vec::new();
2497 if sg_input_trace_enabled() {
2498 eprintln!("[SG_DEBUG][INPUT] hover mouse=({}, {})", mx, my);
2499 }
2500
2501 {
2502 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
2503 return;
2504 };
2505
2506 let images = &mut self.images;
2507 let layers = &self.layers;
2508 let gfx = &self.gfx;
2509 let ids = &self.ids;
2510 let slot_use_by_stage = st.object_slot_use.clone();
2513 let (object_lists, group_lists, embedded_by_stage) = (
2514 &mut st.object_lists,
2515 &mut st.group_lists,
2516 &st.embedded_object_slots_by_stage,
2517 );
2518
2519 let mut stage_ids: Vec<i64> = object_lists.keys().copied().collect();
2520 stage_ids.sort_unstable();
2521 for stage_idx in &stage_ids {
2522 let Some(objs) = object_lists.get_mut(stage_idx) else {
2523 continue;
2524 };
2525 for (obj_idx, obj) in objs.iter_mut().enumerate() {
2526 if embedded_by_stage
2527 .get(stage_idx)
2528 .is_some_and(|slots| slots.contains(&obj_idx))
2529 {
2530 continue;
2531 }
2532 if !slot_use_by_stage
2533 .get(stage_idx)
2534 .and_then(|flags| flags.get(obj_idx))
2535 .copied()
2536 .unwrap_or(true)
2537 {
2538 continue;
2539 }
2540 clear_button_hit_recursive(obj);
2541 }
2542 }
2543
2544 let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
2545 group_stage_ids.sort_unstable();
2546 for stage_idx in group_stage_ids {
2547 let Some(groups) = group_lists.get_mut(&stage_idx) else {
2548 continue;
2549 };
2550 for (group_idx, g) in groups.iter_mut().enumerate() {
2551 if !g.is_doing() {
2552 g.hit_button_no = -1;
2553 g.hit_runtime_slot = None;
2554 continue;
2555 }
2556 let Some(objs) = object_lists.get_mut(&stage_idx) else {
2557 g.hit_button_no = -1;
2558 g.hit_runtime_slot = None;
2559 continue;
2560 };
2561
2562 let mut best: Option<ButtonHitCandidate> = None;
2563 let mut tied = false;
2564 for (obj_idx, obj) in objs.iter_mut().enumerate() {
2565 if embedded_by_stage
2566 .get(&stage_idx)
2567 .is_some_and(|slots| slots.contains(&obj_idx))
2568 {
2569 continue;
2570 }
2571 if !slot_use_by_stage
2572 .get(&stage_idx)
2573 .and_then(|flags| flags.get(obj_idx))
2574 .copied()
2575 .unwrap_or(true)
2576 {
2577 continue;
2578 }
2579 if let Some((button_no, slot)) =
2584 pushed_group_button_recursive(obj_idx, obj, group_idx)
2585 {
2586 g.pushed_button_no = button_no;
2587 g.pushed_runtime_slot = Some(slot);
2588 }
2589 if let Some(hit) = hit_test_object_button_recursive(
2590 images,
2591 layers,
2592 gfx,
2593 ids,
2594 &self.globals.syscom,
2595 read_skip_runtime_enabled,
2596 stage_idx,
2597 group_idx,
2598 mx,
2599 my,
2600 obj_idx,
2601 obj,
2602 None,
2603 ) {
2604 merge_button_hit(&mut best, &mut tied, hit);
2605 }
2606 }
2607
2608 if !tied {
2609 if let Some(hit) = best {
2610 if hit.event_enabled {
2611 g.hit_button_no = hit.button_no;
2612 g.hit_runtime_slot = Some(hit.runtime_slot);
2613 } else {
2614 g.hit_button_no = -1;
2618 g.hit_runtime_slot = None;
2619 }
2620 if sg_debug_enabled() {
2621 eprintln!(
2622 "[SG_DEBUG][INPUT] group stage={} group={} hit_button={} slot={} order={} started={} pushed={} decided={}",
2623 stage_idx,
2624 group_idx,
2625 hit.button_no,
2626 hit.runtime_slot,
2627 hit.sort_key.display_tuple(),
2628 g.started,
2629 g.pushed_button_no,
2630 g.decided_button_no
2631 );
2632 }
2633 if hit.event_enabled && play_hover_sound && !hit.was_hit {
2634 hit_sounds.push(hit.se_no);
2635 }
2636 for (obj_idx, obj) in objs.iter_mut().enumerate() {
2637 if embedded_by_stage
2638 .get(&stage_idx)
2639 .is_some_and(|slots| slots.contains(&obj_idx))
2640 {
2641 continue;
2642 }
2643 if !slot_use_by_stage
2644 .get(&stage_idx)
2645 .and_then(|flags| flags.get(obj_idx))
2646 .copied()
2647 .unwrap_or(true)
2648 {
2649 continue;
2650 }
2651 set_button_hit_by_runtime_slot_recursive(
2652 obj_idx,
2653 obj,
2654 hit.runtime_slot,
2655 );
2656 }
2657 } else {
2658 g.hit_button_no = -1;
2659 g.hit_runtime_slot = None;
2660 if sg_debug_enabled() {
2661 eprintln!(
2662 "[SG_DEBUG][INPUT] group stage={} group={} no_hit started={}",
2663 stage_idx, group_idx, g.started
2664 );
2665 }
2666 }
2667 } else {
2668 g.hit_button_no = -1;
2669 g.hit_runtime_slot = None;
2670 if sg_debug_enabled() {
2671 eprintln!(
2672 "[SG_DEBUG][INPUT] group stage={} group={} hit_tie",
2673 stage_idx, group_idx
2674 );
2675 }
2676 }
2677 }
2678 }
2679
2680 let mut standalone_best: Option<ButtonHitCandidate> = None;
2681 let mut standalone_tied = false;
2682 for stage_idx in &stage_ids {
2683 let Some(objs) = object_lists.get_mut(stage_idx) else {
2684 continue;
2685 };
2686 for (obj_idx, obj) in objs.iter_mut().enumerate() {
2687 if embedded_by_stage
2688 .get(stage_idx)
2689 .is_some_and(|slots| slots.contains(&obj_idx))
2690 {
2691 continue;
2692 }
2693 if !slot_use_by_stage
2694 .get(stage_idx)
2695 .and_then(|flags| flags.get(obj_idx))
2696 .copied()
2697 .unwrap_or(true)
2698 {
2699 continue;
2700 }
2701 if let Some(hit) = hit_test_standalone_action_button_recursive(
2702 images,
2703 layers,
2704 gfx,
2705 ids,
2706 &self.globals.syscom,
2707 read_skip_runtime_enabled,
2708 None,
2709 *stage_idx,
2710 mx,
2711 my,
2712 obj_idx,
2713 obj,
2714 None,
2715 ) {
2716 merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
2717 }
2718 }
2719 }
2720 if !standalone_tied && let Some(hit) = standalone_best {
2721 if hit.event_enabled && play_hover_sound && !hit.was_hit {
2722 hit_sounds.push(hit.se_no);
2723 }
2724 for stage_idx in &stage_ids {
2725 let Some(objs) = object_lists.get_mut(stage_idx) else {
2726 continue;
2727 };
2728 for (obj_idx, obj) in objs.iter_mut().enumerate() {
2729 if embedded_by_stage
2730 .get(stage_idx)
2731 .is_some_and(|slots| slots.contains(&obj_idx))
2732 {
2733 continue;
2734 }
2735 if !slot_use_by_stage
2736 .get(stage_idx)
2737 .and_then(|flags| flags.get(obj_idx))
2738 .copied()
2739 .unwrap_or(true)
2740 {
2741 continue;
2742 }
2743 set_button_hit_by_runtime_slot_recursive(obj_idx, obj, hit.runtime_slot);
2744 }
2745 }
2746 }
2747 }
2748
2749 {
2750 let mwnd_ui_state = self
2751 .ui
2752 .current_mwnd_window_render_state(self.screen_w, self.screen_h);
2753 let mwnd_hidden = self.globals.script.mwnd_disp_off_flag
2754 || self.globals.syscom.hide_mwnd.onoff
2755 || self.globals.syscom.msg_back_open;
2756 if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
2757 let images = &mut self.images;
2758 let layers = &self.layers;
2759 let gfx = &self.gfx;
2760 let ids = &self.ids;
2761 let mut standalone_best: Option<ButtonHitCandidate> = None;
2762 let mut standalone_tied = false;
2763 let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
2764 stage_ids.sort_unstable();
2765 for stage_idx in &stage_ids {
2766 let Some(mwnds) = st.mwnd_lists.get_mut(stage_idx) else {
2767 continue;
2768 };
2769 for mwnd in mwnds {
2770 for obj in &mut mwnd.button_list {
2771 clear_button_hit_recursive(obj);
2772 }
2773 for obj in &mut mwnd.face_list {
2774 clear_button_hit_recursive(obj);
2775 }
2776 for obj in &mut mwnd.object_list {
2777 clear_button_hit_recursive(obj);
2778 }
2779 if mwnd_hidden || !mwnd.open {
2780 continue;
2781 }
2782 let Some((window_x, window_y)) = mwnd.window_pos else {
2783 continue;
2784 };
2785 let Some((window_w, window_h)) = mwnd.window_size else {
2786 continue;
2787 };
2788 if window_w <= 0 || window_h <= 0 {
2789 continue;
2790 }
2791 let ui_state = mwnd_ui_state.filter(|ui| {
2792 ui.x as i64 == window_x
2793 && ui.y as i64 == window_y
2794 && ui.w as i64 == window_w
2795 && ui.h as i64 == window_h
2796 });
2797 let anim_parent =
2798 ui_state.map(|ui| mwnd_anim_parent_from_ui_state(mwnd, ui));
2799 let button_len = mwnd.button_list.len();
2800 for button_idx in 0..button_len {
2801 let skip = {
2802 let obj = &mwnd.button_list[button_idx];
2803 !object_button_renderable_by_syscom(&self.globals.syscom, obj)
2804 };
2805 if skip {
2806 continue;
2807 }
2808 let parent = apply_mwnd_window_anim_parent(
2809 mwnd_button_parent_render_state(
2810 mwnd, button_idx, window_x, window_y, window_w, window_h,
2811 ),
2812 anim_parent.clone(),
2813 );
2814 let obj = &mut mwnd.button_list[button_idx];
2815 if let Some(hit) = hit_test_standalone_action_button_recursive(
2816 images,
2817 layers,
2818 gfx,
2819 ids,
2820 &self.globals.syscom,
2821 read_skip_runtime_enabled,
2822 Some(button_idx),
2823 *stage_idx,
2824 mx,
2825 my,
2826 button_idx,
2827 obj,
2828 Some(parent),
2829 ) {
2830 merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
2831 }
2832 }
2833 let face_len = mwnd.face_list.len();
2834 for face_idx in 0..face_len {
2835 let parent = apply_mwnd_window_anim_parent(
2836 mwnd_face_parent_render_state(mwnd, face_idx, window_x, window_y),
2837 anim_parent.clone(),
2838 );
2839 let obj = &mut mwnd.face_list[face_idx];
2840 if let Some(hit) = hit_test_standalone_action_button_recursive(
2841 images,
2842 layers,
2843 gfx,
2844 ids,
2845 &self.globals.syscom,
2846 read_skip_runtime_enabled,
2847 None,
2848 *stage_idx,
2849 mx,
2850 my,
2851 face_idx,
2852 obj,
2853 Some(parent),
2854 ) {
2855 merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
2856 }
2857 }
2858 let object_parent = apply_mwnd_window_anim_parent(
2859 mwnd_parent_render_state_at(mwnd, window_x, window_y),
2860 anim_parent,
2861 );
2862 let object_len = mwnd.object_list.len();
2863 for object_idx in 0..object_len {
2864 let obj = &mut mwnd.object_list[object_idx];
2865 if let Some(hit) = hit_test_standalone_action_button_recursive(
2866 images,
2867 layers,
2868 gfx,
2869 ids,
2870 &self.globals.syscom,
2871 read_skip_runtime_enabled,
2872 None,
2873 *stage_idx,
2874 mx,
2875 my,
2876 object_idx,
2877 obj,
2878 Some(object_parent.clone()),
2879 ) {
2880 merge_button_hit(&mut standalone_best, &mut standalone_tied, hit);
2881 }
2882 }
2883 }
2884 }
2885 if !standalone_tied && let Some(hit) = standalone_best {
2886 if hit.event_enabled && play_hover_sound && !hit.was_hit {
2887 hit_sounds.push(hit.se_no);
2888 }
2889 for stage_idx in &stage_ids {
2890 let Some(mwnds) = st.mwnd_lists.get_mut(stage_idx) else {
2891 continue;
2892 };
2893 for mwnd in mwnds {
2894 for (button_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
2895 set_button_hit_by_runtime_slot_recursive(
2896 button_idx,
2897 obj,
2898 hit.runtime_slot,
2899 );
2900 }
2901 for (face_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
2902 set_button_hit_by_runtime_slot_recursive(
2903 face_idx,
2904 obj,
2905 hit.runtime_slot,
2906 );
2907 }
2908 for (object_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
2909 set_button_hit_by_runtime_slot_recursive(
2910 object_idx,
2911 obj,
2912 hit.runtime_slot,
2913 );
2914 }
2915 }
2916 }
2917 }
2918 }
2919 }
2920
2921 for se_no in hit_sounds {
2922 self.play_button_template_se(se_no, ButtonSeEvent::Hit);
2923 }
2924 self.update_mwnd_message_button_hover(play_hover_sound);
2925 }
2926
2927 fn update_mwnd_message_button_hover(&mut self, play_hover_sound: bool) -> bool {
2928 let hit = self.ui.mwnd_message_button_hit(
2929 self.input.mouse_x,
2930 self.input.mouse_y,
2931 self.screen_w,
2932 self.screen_h,
2933 );
2934 let mut play_se = None;
2935 let mut active = false;
2936 for st in self.globals.stage_forms.values_mut() {
2937 for groups in st.group_lists.values_mut() {
2938 for group in groups {
2939 group.hit_message_button = false;
2940 }
2941 }
2942 }
2943 if let Some(hit) = hit
2944 && let Some(group) = self
2945 .globals
2946 .stage_forms
2947 .get_mut(&hit.form_id)
2948 .and_then(|st| st.group_lists.get_mut(&hit.stage_idx))
2949 .and_then(|groups| groups.get_mut(hit.group_no.max(0) as usize))
2950 && group.is_doing()
2951 {
2952 let changed = group.hit_button_no != hit.btn_no || !group.hit_message_button;
2953 group.hit_button_no = hit.btn_no;
2954 group.hit_runtime_slot = None;
2955 group.hit_message_button = true;
2956 group.message_button_se_no = hit.se_no;
2957 active = true;
2958 if changed && play_hover_sound {
2959 play_se = Some(hit.se_no);
2960 }
2961 }
2962 if let Some(se) = play_se {
2963 self.play_button_template_se(se, ButtonSeEvent::Hit);
2964 }
2965 active
2966 }
2967
2968 fn handle_mwnd_message_button_mouse_down(&mut self) -> bool {
2969 let mut play_se = None;
2970 let mut consumed = false;
2971 for st in self.globals.stage_forms.values_mut() {
2972 for groups in st.group_lists.values_mut() {
2973 for group in groups {
2974 if group.is_doing() && group.hit_message_button && group.hit_button_no >= 0 {
2975 group.pushed_button_no = group.hit_button_no;
2976 group.pushed_runtime_slot = None;
2977 group.pushed_message_button = true;
2978 play_se = Some(group.message_button_se_no);
2979 consumed = true;
2980 }
2981 }
2982 }
2983 }
2984 if let Some(se) = play_se {
2985 self.play_button_template_se(se, ButtonSeEvent::Push);
2986 }
2987 consumed
2988 }
2989
2990 fn handle_mwnd_message_button_mouse_up(&mut self) -> bool {
2991 let mut play_se = None;
2992 let mut result_to_push = None;
2993 let mut consumed = false;
2994 let mut clear_focus = None;
2995 for (form_id, st) in self.globals.stage_forms.iter_mut() {
2996 for (stage_idx, groups) in st.group_lists.iter_mut() {
2997 for (group_idx, group) in groups.iter_mut().enumerate() {
2998 if !group.pushed_message_button {
2999 continue;
3000 }
3001 consumed = true;
3002 let same = group.hit_message_button
3003 && group.hit_button_no == group.pushed_button_no
3004 && group.pushed_button_no >= 0;
3005 let pushed = group.pushed_button_no;
3006 let se_no = group.message_button_se_no;
3007 let was_waiting = group.wait_flag;
3008 if same && group.decide(pushed) {
3009 play_se = Some(se_no);
3010 if was_waiting {
3011 group.wait_flag = false;
3012 result_to_push = Some(pushed);
3013 clear_focus = Some((*form_id, *stage_idx, group_idx));
3014 }
3015 } else {
3016 group.pushed_button_no = -1;
3017 group.pushed_message_button = false;
3018 }
3019 }
3020 }
3021 }
3022 if let Some(value) = result_to_push {
3023 self.stack.push(Value::Int(value));
3024 }
3025 if clear_focus.is_some() && self.globals.focused_stage_group == clear_focus {
3026 self.globals.focused_stage_group = None;
3027 }
3028 if let Some(se) = play_se {
3029 self.play_button_template_se(se, ButtonSeEvent::Decide);
3030 }
3031 consumed
3032 }
3033
3034 fn handle_object_button_mouse_down(&mut self, b: input::VmMouseButton) -> bool {
3035 self.update_object_button_hover();
3038 if matches!(b, input::VmMouseButton::Left) && self.handle_mwnd_message_button_mouse_down() {
3039 return true;
3040 }
3041
3042 let Some(form_id) = self.active_button_stage_form_id() else {
3043 return false;
3044 };
3045 let read_skip_runtime_enabled = self.runtime_read_skip_is_enable();
3046 let mut template_sounds = Vec::new();
3047 let mut direct_sounds = Vec::new();
3048 let mut consumed_button = false;
3049
3050 {
3051 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
3052 return false;
3053 };
3054
3055 let slot_use_by_stage = st.object_slot_use.clone();
3056 let (object_lists, group_lists, embedded_by_stage) = (
3057 &mut st.object_lists,
3058 &mut st.group_lists,
3059 &st.embedded_object_slots_by_stage,
3060 );
3061
3062 match b {
3063 input::VmMouseButton::Left => {
3064 let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
3065 group_stage_ids.sort_unstable();
3066 for stage_idx in group_stage_ids {
3067 let Some(groups) = group_lists.get_mut(&stage_idx) else {
3068 continue;
3069 };
3070 for (group_idx, g) in groups.iter_mut().enumerate() {
3071 if !g.is_doing() {
3072 continue;
3073 }
3074 let hit = g.hit_button_no;
3075 let Some(hit_slot) = g.hit_runtime_slot else {
3076 continue;
3077 };
3078 if hit < 0 {
3079 continue;
3080 }
3081 if g.pushed_runtime_slot != Some(hit_slot)
3082 && let Some(objs) = object_lists.get(&stage_idx)
3083 && let Some(se_no) =
3084 find_button_se_no_in_list_by_runtime_slot(objs, hit_slot)
3085 {
3086 template_sounds.push(se_no);
3087 }
3088 g.pushed_button_no = hit;
3089 g.pushed_runtime_slot = Some(hit_slot);
3090 if let Some(objs) = object_lists.get_mut(&stage_idx) {
3091 for (obj_idx, obj) in objs.iter_mut().enumerate() {
3092 if embedded_by_stage
3093 .get(&stage_idx)
3094 .is_some_and(|slots| slots.contains(&obj_idx))
3095 || !slot_use_by_stage
3096 .get(&stage_idx)
3097 .and_then(|flags| flags.get(obj_idx))
3098 .copied()
3099 .unwrap_or(true)
3100 {
3101 continue;
3102 }
3103 set_button_pushed_by_runtime_slot_recursive(
3104 obj_idx, obj, hit_slot,
3105 );
3106 }
3107 }
3108 }
3109 }
3110
3111 let mut stage_ids: Vec<i64> = object_lists.keys().copied().collect();
3112 stage_ids.sort_unstable();
3113 for stage_idx in stage_ids {
3114 let Some(objs) = object_lists.get_mut(&stage_idx) else {
3115 continue;
3116 };
3117 for (obj_idx, obj) in objs.iter_mut().enumerate() {
3118 if embedded_by_stage
3119 .get(&stage_idx)
3120 .is_some_and(|slots| slots.contains(&obj_idx))
3121 || !slot_use_by_stage
3122 .get(&stage_idx)
3123 .and_then(|flags| flags.get(obj_idx))
3124 .copied()
3125 .unwrap_or(true)
3126 {
3127 continue;
3128 }
3129 if registered_button_hit_recursive(obj) {
3130 consumed_button = true;
3131 }
3132 if let Some(se_no) = mark_standalone_button_pushed_from_hit_recursive(
3133 obj_idx,
3134 obj,
3135 &self.globals.syscom,
3136 read_skip_runtime_enabled,
3137 None,
3138 ) {
3139 template_sounds.push(se_no);
3140 }
3141 }
3142 }
3143 }
3144 input::VmMouseButton::Right => {
3145 let mut candidates: Vec<(i64, usize, i64)> = Vec::new();
3146 let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
3147 group_stage_ids.sort_unstable();
3148 for stage_idx in group_stage_ids {
3149 let Some(groups) = group_lists.get(&stage_idx) else {
3150 continue;
3151 };
3152 for (group_idx, g) in groups.iter().enumerate() {
3153 if g.is_doing() && g.cancel_flag {
3154 candidates.push((g.cancel_priority, group_idx, stage_idx));
3155 }
3156 }
3157 }
3158 candidates.sort_by_key(|a| std::cmp::Reverse(a.0));
3159 if let Some((_priority, group_idx, stage_idx)) = candidates.first().copied()
3160 && let Some(groups) = group_lists.get_mut(&stage_idx)
3161 && let Some(g) = groups.get_mut(group_idx)
3162 {
3163 let was_waiting = g.wait_flag;
3164 let cancel_se_no = g.cancel_se_no;
3165 if g.cancel().is_some() {
3166 if sg_debug_enabled() {
3167 eprintln!(
3168 "[SG_DEBUG][GROUP] cancel form={} stage={} group={} wait={} result_button={} se={}",
3169 form_id,
3170 stage_idx,
3171 group_idx,
3172 was_waiting,
3173 g.result_button_no,
3174 cancel_se_no
3175 );
3176 }
3177 if was_waiting {
3178 self.stack.push(Value::Int(globals::TNM_GROUP_CANCELED));
3179 }
3180 g.wait_flag = false;
3181 direct_sounds.push(cancel_se_no);
3182 if self.globals.focused_stage_group
3183 == Some((form_id, stage_idx, group_idx))
3184 {
3185 self.globals.focused_stage_group = None;
3186 }
3187 }
3188 }
3189 }
3190 _ => {}
3191 }
3192 }
3193
3194 {
3195 let mwnd_hidden = self.globals.script.mwnd_disp_off_flag
3196 || self.globals.syscom.hide_mwnd.onoff
3197 || self.globals.syscom.msg_back_open;
3198 let syscom = self.globals.syscom.clone();
3199 if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
3200 let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
3201 stage_ids.sort_unstable();
3202 for stage_idx in stage_ids {
3203 let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
3204 continue;
3205 };
3206 for mwnd in mwnds {
3207 if mwnd_hidden || !mwnd.open {
3208 continue;
3209 }
3210 let Some((_, _)) = mwnd.window_pos else {
3211 continue;
3212 };
3213 let Some((window_w, window_h)) = mwnd.window_size else {
3214 continue;
3215 };
3216 if window_w <= 0 || window_h <= 0 {
3217 continue;
3218 }
3219 for (button_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
3220 if !object_button_renderable_by_syscom(&syscom, obj) {
3221 continue;
3222 }
3223 if registered_button_hit_recursive(obj) {
3224 consumed_button = true;
3225 }
3226 if let Some(se_no) = mark_standalone_button_pushed_from_hit_recursive(
3227 button_idx,
3228 obj,
3229 &syscom,
3230 read_skip_runtime_enabled,
3231 Some(button_idx),
3232 ) {
3233 template_sounds.push(se_no);
3234 }
3235 }
3236 for (face_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
3237 if !object_button_renderable_by_syscom(&syscom, obj) {
3238 continue;
3239 }
3240 if registered_button_hit_recursive(obj) {
3241 consumed_button = true;
3242 }
3243 if let Some(se_no) = mark_standalone_button_pushed_from_hit_recursive(
3244 face_idx,
3245 obj,
3246 &syscom,
3247 read_skip_runtime_enabled,
3248 None,
3249 ) {
3250 template_sounds.push(se_no);
3251 }
3252 }
3253 for (object_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
3254 if !object_button_renderable_by_syscom(&syscom, obj) {
3255 continue;
3256 }
3257 if registered_button_hit_recursive(obj) {
3258 consumed_button = true;
3259 }
3260 if let Some(se_no) = mark_standalone_button_pushed_from_hit_recursive(
3261 object_idx,
3262 obj,
3263 &syscom,
3264 read_skip_runtime_enabled,
3265 None,
3266 ) {
3267 template_sounds.push(se_no);
3268 }
3269 }
3270 }
3271 }
3272 }
3273 }
3274
3275 let consumed = consumed_button || !template_sounds.is_empty() || !direct_sounds.is_empty();
3276 if consumed && matches!(b, input::VmMouseButton::Left) {
3277 self.button_decide_capture = true;
3281 let _ = self.input.use_vk_down_stock(0x01);
3282 }
3283 for se_no in template_sounds {
3284 self.play_button_template_se(se_no, ButtonSeEvent::Push);
3285 }
3286 for se_no in direct_sounds {
3287 self.play_button_se_no(se_no);
3288 }
3289 consumed
3290 }
3291
3292 fn handle_object_button_mouse_up(&mut self, b: input::VmMouseButton) -> bool {
3293 if !matches!(b, input::VmMouseButton::Left) {
3294 return false;
3295 }
3296 let captured_by_button_manager = std::mem::take(&mut self.button_decide_capture);
3297
3298 self.update_object_button_hover();
3299 if self.handle_mwnd_message_button_mouse_up() {
3300 return true;
3301 }
3302
3303 let Some(form_id) = self.active_button_stage_form_id() else {
3304 if captured_by_button_manager {
3305 let _ = self.input.use_vk_up_stock(0x01);
3306 }
3307 return captured_by_button_manager;
3308 };
3309 let read_skip_runtime_enabled = self.runtime_read_skip_is_enable();
3310 let syscom = self.globals.syscom.clone();
3311 let mut pending_button_actions = Vec::new();
3312 let mut sounds = Vec::new();
3313 let mut consumed_button = false;
3314
3315 {
3316 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
3317 return false;
3318 };
3319
3320 let slot_use_by_stage = st.object_slot_use.clone();
3321 let (object_lists, group_lists, embedded_by_stage) = (
3322 &mut st.object_lists,
3323 &mut st.group_lists,
3324 &st.embedded_object_slots_by_stage,
3325 );
3326
3327 let mut group_stage_ids: Vec<i64> = group_lists.keys().copied().collect();
3328 group_stage_ids.sort_unstable();
3329 for stage_idx in group_stage_ids {
3330 let Some(groups) = group_lists.get_mut(&stage_idx) else {
3331 continue;
3332 };
3333 for (group_idx, g) in groups.iter_mut().enumerate() {
3334 if !g.is_doing() {
3335 continue;
3336 }
3337 let pushed = g.pushed_button_no;
3338 let pushed_slot = g.pushed_runtime_slot;
3339 let release_keeps_push = pushed_slot
3340 .and_then(|slot| {
3341 object_lists.get(&stage_idx).map(|objs| {
3342 object_button_push_keep_in_list_by_runtime_slot(objs, slot)
3343 })
3344 })
3345 .unwrap_or(false);
3346 let released_on_same_button = pushed >= 0
3347 && pushed_slot.is_some()
3348 && (g.hit_runtime_slot == pushed_slot || release_keeps_push);
3349 if released_on_same_button {
3350 let was_waiting = g.wait_flag;
3351 let action_slot = pushed_slot.unwrap();
3352 let event_enabled = object_lists
3353 .get(&stage_idx)
3354 .map(|objs| {
3355 button_event_enabled_in_list_by_runtime_slot(
3356 objs,
3357 action_slot,
3358 &syscom,
3359 read_skip_runtime_enabled,
3360 )
3361 })
3362 .unwrap_or(false);
3363 if event_enabled && g.decide(pushed) {
3364 if sg_debug_enabled() {
3365 eprintln!(
3366 "[SG_DEBUG][GROUP] decide form={} stage={} group={} button={} slot={} wait={}",
3367 form_id, stage_idx, group_idx, pushed, action_slot, was_waiting
3368 );
3369 }
3370 if let Some(objs) = object_lists.get(&stage_idx) {
3371 if let Some(se_no) =
3372 find_button_se_no_in_list_by_runtime_slot(objs, action_slot)
3373 {
3374 sounds.push(se_no);
3375 }
3376 for (obj_idx, obj) in objs.iter().enumerate() {
3377 if embedded_by_stage
3378 .get(&stage_idx)
3379 .is_some_and(|slots| slots.contains(&obj_idx))
3380 || !slot_use_by_stage
3381 .get(&stage_idx)
3382 .and_then(|flags| flags.get(obj_idx))
3383 .copied()
3384 .unwrap_or(true)
3385 {
3386 continue;
3387 }
3388 collect_button_decided_action_by_runtime_slot_recursive(
3389 obj_idx,
3390 obj,
3391 action_slot,
3392 &mut pending_button_actions,
3393 );
3394 }
3395 }
3396 if was_waiting {
3397 self.stack.push(Value::Int(pushed));
3398 g.wait_flag = false;
3399 if self.globals.focused_stage_group
3400 == Some((form_id, stage_idx, group_idx))
3401 {
3402 self.globals.focused_stage_group = None;
3403 }
3404 }
3405 }
3406 } else {
3407 g.pushed_button_no = -1;
3408 g.pushed_runtime_slot = None;
3409 }
3410 }
3411 }
3412
3413 let mut stage_ids: Vec<i64> = object_lists.keys().copied().collect();
3414 stage_ids.sort_unstable();
3415 for stage_idx in &stage_ids {
3416 let Some(objs) = object_lists.get(stage_idx) else {
3417 continue;
3418 };
3419 for (obj_idx, obj) in objs.iter().enumerate() {
3420 if embedded_by_stage
3421 .get(stage_idx)
3422 .is_some_and(|slots| slots.contains(&obj_idx))
3423 || !slot_use_by_stage
3424 .get(stage_idx)
3425 .and_then(|flags| flags.get(obj_idx))
3426 .copied()
3427 .unwrap_or(true)
3428 {
3429 continue;
3430 }
3431 if standalone_button_pushed_recursive(obj) {
3432 consumed_button = true;
3433 }
3434 collect_standalone_button_decided_actions_recursive(
3435 obj,
3436 &mut pending_button_actions,
3437 &mut sounds,
3438 &syscom,
3439 read_skip_runtime_enabled,
3440 None,
3441 );
3442 }
3443 }
3444
3445 for stage_idx in &stage_ids {
3446 let Some(objs) = object_lists.get_mut(stage_idx) else {
3447 continue;
3448 };
3449 for (obj_idx, obj) in objs.iter_mut().enumerate() {
3450 if embedded_by_stage
3451 .get(stage_idx)
3452 .is_some_and(|slots| slots.contains(&obj_idx))
3453 || !slot_use_by_stage
3454 .get(stage_idx)
3455 .and_then(|flags| flags.get(obj_idx))
3456 .copied()
3457 .unwrap_or(true)
3458 {
3459 continue;
3460 }
3461 clear_button_pushed_recursive(obj);
3462 }
3463 }
3464 }
3465
3466 {
3467 let mwnd_hidden = self.globals.script.mwnd_disp_off_flag
3468 || self.globals.syscom.hide_mwnd.onoff
3469 || self.globals.syscom.msg_back_open;
3470 let syscom = self.globals.syscom.clone();
3471 if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
3472 let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
3473 stage_ids.sort_unstable();
3474 for stage_idx in &stage_ids {
3475 let Some(mwnds) = st.mwnd_lists.get(stage_idx) else {
3476 continue;
3477 };
3478 for mwnd in mwnds {
3479 if mwnd_hidden || !mwnd.open {
3480 continue;
3481 }
3482 let Some((_, _)) = mwnd.window_pos else {
3483 continue;
3484 };
3485 let Some((window_w, window_h)) = mwnd.window_size else {
3486 continue;
3487 };
3488 if window_w <= 0 || window_h <= 0 {
3489 continue;
3490 }
3491 for (button_idx, obj) in mwnd.button_list.iter().enumerate() {
3492 if !object_button_renderable_by_syscom(&syscom, obj)
3493 || button_effective_disabled(
3494 &syscom,
3495 obj,
3496 Some(button_idx),
3497 read_skip_runtime_enabled,
3498 )
3499 || syscom.mwnd_btn_touch_disable
3500 {
3501 continue;
3502 }
3503 collect_standalone_button_decided_actions_recursive(
3504 obj,
3505 &mut pending_button_actions,
3506 &mut sounds,
3507 &syscom,
3508 read_skip_runtime_enabled,
3509 Some(button_idx),
3510 );
3511 }
3512 for obj in &mwnd.face_list {
3513 if !object_button_renderable_by_syscom(&syscom, obj)
3514 || button_effective_disabled(
3515 &syscom,
3516 obj,
3517 None,
3518 read_skip_runtime_enabled,
3519 )
3520 || syscom.mwnd_btn_touch_disable
3521 {
3522 continue;
3523 }
3524 collect_standalone_button_decided_actions_recursive(
3525 obj,
3526 &mut pending_button_actions,
3527 &mut sounds,
3528 &syscom,
3529 read_skip_runtime_enabled,
3530 None,
3531 );
3532 }
3533 for obj in &mwnd.object_list {
3534 if !object_button_renderable_by_syscom(&syscom, obj)
3535 || button_effective_disabled(
3536 &syscom,
3537 obj,
3538 None,
3539 read_skip_runtime_enabled,
3540 )
3541 || syscom.mwnd_btn_touch_disable
3542 {
3543 continue;
3544 }
3545 collect_standalone_button_decided_actions_recursive(
3546 obj,
3547 &mut pending_button_actions,
3548 &mut sounds,
3549 &syscom,
3550 read_skip_runtime_enabled,
3551 None,
3552 );
3553 }
3554 }
3555 }
3556 for stage_idx in &stage_ids {
3557 let Some(mwnds) = st.mwnd_lists.get_mut(stage_idx) else {
3558 continue;
3559 };
3560 for mwnd in mwnds {
3561 for obj in &mut mwnd.button_list {
3562 clear_button_pushed_recursive(obj);
3563 }
3564 for obj in &mut mwnd.face_list {
3565 clear_button_pushed_recursive(obj);
3566 }
3567 for obj in &mut mwnd.object_list {
3568 clear_button_pushed_recursive(obj);
3569 }
3570 }
3571 }
3572 }
3573 }
3574
3575 let consumed = captured_by_button_manager
3576 || consumed_button
3577 || !pending_button_actions.is_empty()
3578 || !sounds.is_empty();
3579 if consumed {
3580 let _ = self.input.use_vk_up_stock(0x01);
3584 }
3585 self.globals
3586 .pending_button_actions
3587 .extend(pending_button_actions);
3588 for se_no in sounds {
3589 self.play_button_template_se(se_no, ButtonSeEvent::Decide);
3590 }
3591 consumed
3592 }
3593 pub fn platform_shortcuts_blocked(&self) -> bool {
3598 self.globals.system.messagebox_modal.is_some()
3599 || self.globals.syscom.menu_open
3600 || self.globals.syscom.msg_back_open
3601 || self.globals.selbtn.started
3602 || self.globals.selbtn.processing_flag_0
3603 || self.globals.focused_editbox.is_some()
3604 }
3605
3606 fn is_vm_key_disabled(&self, k: input::VmKey) -> bool {
3607 input::vmkey_to_vk_code(k)
3608 .map(|vk| self.globals.script.key_disable.contains(&vk))
3609 .unwrap_or(false)
3610 }
3611
3612 pub fn on_key_down(&mut self, k: input::VmKey) {
3613 if self.handle_system_messagebox_key(k) {
3614 return;
3615 }
3616 if self.handle_msg_back_key(k) {
3617 return;
3618 }
3619 self.cancel_pending_editbox_composition();
3620
3621 let editbox_active = self.editbox_accepts_keyboard_input();
3624 let mut input_recorded = false;
3625 if editbox_active {
3626 if self.is_vm_key_disabled(k) {
3627 return;
3628 }
3629 self.input.on_key_down(k);
3630 input_recorded = true;
3631 if Self::is_modifier_key(k) {
3632 return;
3633 }
3634 if self.handle_editbox_key(k) {
3635 return;
3636 }
3637 }
3638
3639 if self.globals.syscom.hide_mwnd.onoff
3640 && matches!(
3641 k,
3642 input::VmKey::Enter | input::VmKey::Escape | input::VmKey::Space
3643 )
3644 {
3645 if !input_recorded {
3646 self.input.on_key_down(k);
3647 }
3648 return;
3649 }
3650 if self.handle_selbtn_key(k) {
3651 return;
3652 }
3653 if self.is_vm_key_disabled(k) {
3654 return;
3655 }
3656 if !input_recorded {
3657 self.input.on_key_down(k);
3658 }
3659 match k {
3660 input::VmKey::Enter | input::VmKey::Space => {
3661 if let Some(vk) = input::vmkey_to_vk_code(k) {
3662 let _ = self.stop_read_skip_by_decide_down(vk);
3663 }
3664 }
3665 input::VmKey::Escape | input::VmKey::Control => self.register_skip_user_action(),
3666 _ => {}
3667 }
3668 if Self::is_modifier_key(k) {
3669 return;
3670 }
3671
3672 let handled_mwnd_selection = self.handle_mwnd_selection_key(k);
3673
3674 if !handled_mwnd_selection
3676 && let Some((form_id, stage_idx, group_idx)) = self.globals.focused_stage_group
3677 && let Some(st) = self.globals.stage_forms.get_mut(&form_id)
3678 && let Some(list) = st.group_lists.get_mut(&stage_idx)
3679 && let Some(g) = list.get_mut(group_idx)
3680 {
3681 match k {
3682 input::VmKey::Enter => {
3683 let button_no = if g.hit_button_no >= 0 {
3684 g.hit_button_no
3685 } else {
3686 0
3687 };
3688 let was_waiting = g.wait_flag;
3689 if g.decide(button_no) {
3690 if sg_debug_enabled() {
3691 eprintln!(
3692 "[SG_DEBUG][GROUP] key_decide form={} stage={} group={} button={} wait={}",
3693 form_id, stage_idx, group_idx, button_no, was_waiting
3694 );
3695 }
3696 if was_waiting {
3697 self.stack.push(Value::Int(button_no));
3698 }
3699 g.wait_flag = false;
3700 self.globals.focused_stage_group = None;
3701 }
3702 }
3703 input::VmKey::Escape => {
3704 let was_waiting = g.wait_flag;
3705 if g.cancel().is_some() {
3706 if sg_debug_enabled() {
3707 eprintln!(
3708 "[SG_DEBUG][GROUP] key_cancel form={} stage={} group={} wait={} result_button={}",
3709 form_id, stage_idx, group_idx, was_waiting, g.result_button_no
3710 );
3711 }
3712 if was_waiting {
3713 self.stack.push(Value::Int(globals::TNM_GROUP_CANCELED));
3714 }
3715 g.wait_flag = false;
3716 self.globals.focused_stage_group = None;
3717 }
3718 }
3719 _ => {}
3720 }
3721 }
3722 }
3723
3724 pub fn on_key_up(&mut self, k: input::VmKey) {
3725 self.cancel_pending_editbox_composition();
3726 if self.is_vm_key_disabled(k) {
3727 return;
3728 }
3729 self.input.on_key_up(k);
3730 if self.editbox_accepts_keyboard_input()
3731 && !matches!(k, input::VmKey::F(_) | input::VmKey::Other(_))
3732 {
3733 return;
3734 }
3735 if self.globals.syscom.hide_mwnd.onoff
3736 && matches!(
3737 k,
3738 input::VmKey::Enter | input::VmKey::Escape | input::VmKey::Space
3739 )
3740 {
3741 self.globals.syscom.hide_mwnd.onoff = false;
3742 return;
3743 }
3744 if let Some(vk) = input::vmkey_to_vk_code(k) {
3745 match k {
3746 input::VmKey::Enter | input::VmKey::Space => {
3747 if self.process_script_skip_trigger_decide(vk) {
3752 return;
3753 }
3754 if self.input.vk_down_up_stock(vk) {
3755 if self.notify_movie_wait_down_up(1) {
3756 return;
3757 }
3758 if !self.advance_message_wait(true) {
3759 self.notify_wait_key();
3761 }
3762 }
3763 }
3764 input::VmKey::Escape if self.input.vk_down_up_stock(vk) => {
3765 let _ = self.notify_movie_wait_down_up(-1);
3766 }
3767 _ => {}
3768 }
3769 }
3770 }
3771
3772 pub fn on_text_input(&mut self, text: &str) {
3773 if self.globals.system.messagebox_modal.is_some() {
3774 return;
3775 }
3776 let Some((form_id, idx)) = self.globals.focused_editbox else {
3777 return;
3778 };
3779 let Some(list) = self.globals.editbox_lists.get_mut(&form_id) else {
3780 return;
3781 };
3782 let Some(eb) = list.boxes.get_mut(idx) else {
3783 return;
3784 };
3785 if !eb.created || !eb.visible {
3786 return;
3787 }
3788 eb.commit_text(text);
3789 }
3790
3791 pub fn on_ime_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) {
3792 if self.globals.system.messagebox_modal.is_some() {
3793 return;
3794 }
3795 let Some((form_id, idx)) = self.globals.focused_editbox else {
3796 return;
3797 };
3798 let Some(eb) = self
3799 .globals
3800 .editbox_lists
3801 .get_mut(&form_id)
3802 .and_then(|list| list.boxes.get_mut(idx))
3803 else {
3804 return;
3805 };
3806 if !eb.created || !eb.visible {
3807 return;
3808 }
3809 eb.set_ime_preedit(text, cursor);
3810 }
3811
3812 pub fn on_ime_disabled(&mut self) {
3813 let Some((form_id, idx)) = self.globals.focused_editbox else {
3814 return;
3815 };
3816 if let Some(eb) = self
3817 .globals
3818 .editbox_lists
3819 .get_mut(&form_id)
3820 .and_then(|list| list.boxes.get_mut(idx))
3821 {
3822 eb.cancel_composition();
3823 }
3824 }
3825
3826 pub fn editbox_accepts_keyboard_input(&self) -> bool {
3827 let Some((form_id, idx)) = self.globals.focused_editbox else {
3828 return false;
3829 };
3830 self.globals
3831 .editbox_lists
3832 .get(&form_id)
3833 .and_then(|list| list.boxes.get(idx))
3834 .map(|eb| eb.created && eb.visible)
3835 .unwrap_or(false)
3836 }
3837
3838 pub fn editbox_accepts_direct_text(&self) -> bool {
3839 let Some((form_id, idx)) = self.globals.focused_editbox else {
3840 return false;
3841 };
3842 let Some(editbox) = self
3843 .globals
3844 .editbox_lists
3845 .get(&form_id)
3846 .and_then(|list| list.boxes.get(idx))
3847 else {
3848 return false;
3849 };
3850 editbox.created && editbox.visible && !editbox.is_composing()
3851 }
3852
3853 pub fn focused_editbox_ime_area(&self) -> Option<(i32, i32, i32, i32)> {
3854 let (form_id, idx) = self.globals.focused_editbox?;
3855 let eb = self.globals.editbox_lists.get(&form_id)?.boxes.get(idx)?;
3856 if !eb.created || !eb.visible {
3857 return None;
3858 }
3859 Some((
3860 eb.caret_window_x().max(eb.window_x),
3861 eb.window_y.saturating_add(1),
3862 1,
3863 eb.window_moji_size.max(1).min(eb.window_h.max(1)),
3864 ))
3865 }
3866
3867 fn open_syscom_menu_from_cancel_key(&mut self) -> bool {
3868 if self.globals.syscom.syscom_menu_disable {
3872 return false;
3873 }
3874 if self.globals.syscom.msg_back_open || self.globals.syscom.hide_mwnd.onoff {
3875 return false;
3876 }
3877 if self.excall_state.ex_call_flag {
3882 return false;
3883 }
3884 if self.movie.current().is_some() {
3885 return false;
3886 }
3887 self.globals.syscom.read_skip.onoff = false;
3888 self.globals.syscom.pending_proc = Some(globals::SyscomPendingProc {
3889 kind: globals::SyscomPendingProcKind::OpenSyscomMenu,
3890 warning: false,
3891 se_play: false,
3892 fade_out: false,
3893 leave_msgbk: false,
3894 save_id: 0,
3895 });
3896 if env_is_set!("SG_PROC_FLOW_TRACE") {
3897 eprintln!(
3898 "[SG_PROC_FLOW] open_syscom_menu_from_cancel_key scene={:?} line={} pending_proc={:?}",
3899 self.current_scene_name, self.current_line_no, self.globals.syscom.pending_proc
3900 );
3901 }
3902 true
3903 }
3904
3905 pub fn on_mouse_move(&mut self, x: i32, y: i32) {
3906 self.input.on_mouse_move(x, y);
3907 if self.update_editbox_drag_selection() {
3908 return;
3909 }
3910 if let Some(idx) = self.selbtn_hit_index(x, y) {
3911 if let Some(pressed) = self.globals.selbtn.pressed_index {
3912 let inside = pressed == idx;
3913 if self.globals.selbtn.pressed_inside != inside {
3914 self.globals.selbtn.pressed_inside = inside;
3915 self.sync_selbtn_item_selection();
3916 }
3917 }
3918 if self.globals.selbtn.cursor != idx {
3919 self.globals.selbtn.cursor = idx;
3920 self.sync_selbtn_item_selection();
3921 }
3922 return;
3923 }
3924 if self.globals.selbtn.pressed_index.is_some() && self.globals.selbtn.pressed_inside {
3925 self.globals.selbtn.pressed_inside = false;
3926 self.sync_selbtn_item_selection();
3927 }
3928 if self.handle_msg_back_mouse_move() {
3929 return;
3930 }
3931 self.update_object_button_hover();
3932 }
3933
3934 pub fn on_mouse_down(&mut self, b: input::VmMouseButton) {
3935 if sg_input_trace_enabled() {
3936 eprintln!(
3937 "[SG_DEBUG][INPUT] mouse_down {:?} at=({}, {})",
3938 b, self.input.mouse_x, self.input.mouse_y
3939 );
3940 }
3941 if self.handle_system_messagebox_click(b) {
3942 return;
3943 }
3944 if self.globals.syscom.msg_back_open {
3945 self.input.on_mouse_down(b);
3946 self.handle_msg_back_mouse_down(b);
3947 return;
3948 }
3949 self.cancel_pending_editbox_composition();
3950 if self.begin_editbox_mouse_down(b) {
3951 self.input.on_mouse_down(b);
3952 return;
3953 }
3954 if self.handle_selbtn_mouse_down(b) {
3955 return;
3956 }
3957 let handled_mwnd_selection = self.handle_mwnd_selection_click(b);
3958 self.input.on_mouse_down(b);
3959 let handled_button = if !handled_mwnd_selection {
3960 self.handle_object_button_mouse_down(b)
3961 } else {
3962 false
3963 };
3964 if matches!(b, input::VmMouseButton::Right) && handled_button {
3965 self.suppress_next_right_syscom_open = true;
3966 }
3967 if !handled_button && !handled_mwnd_selection {
3968 match b {
3969 input::VmMouseButton::Left => {
3970 let _ = self.stop_read_skip_by_decide_down(0x01);
3971 }
3972 input::VmMouseButton::Right => self.register_skip_user_action(),
3973 _ => {}
3974 }
3975 }
3976 }
3977
3978 fn begin_editbox_mouse_down(&mut self, b: input::VmMouseButton) -> bool {
3979 if !matches!(b, input::VmMouseButton::Left) {
3980 return false;
3981 }
3982 let x = self.input.mouse_x;
3983 let y = self.input.mouse_y;
3984 let mut hits = Vec::new();
3985 for (form_id, list) in &self.globals.editbox_lists {
3986 for (idx, eb) in list.boxes.iter().enumerate() {
3987 if eb.contains_point(x, y) {
3988 hits.push((*form_id, idx));
3989 }
3990 }
3991 }
3992 hits.sort_unstable();
3993 let Some(target) = hits.into_iter().next_back() else {
3994 self.set_focused_editbox(None);
3995 return false;
3996 };
3997 self.set_focused_editbox(Some(target));
3998 let extend = self.input.vk_is_down(0x10);
3999 if let Some(eb) = self
4000 .globals
4001 .editbox_lists
4002 .get_mut(&target.0)
4003 .and_then(|list| list.boxes.get_mut(target.1))
4004 {
4005 eb.set_cursor_from_window_x(x, extend);
4006 eb.mouse_selecting = true;
4007 }
4008 true
4009 }
4010
4011 fn update_editbox_drag_selection(&mut self) -> bool {
4012 let Some((form_id, idx)) = self.globals.focused_editbox else {
4013 return false;
4014 };
4015 if !self.input.vk_is_down(0x01) {
4016 return false;
4017 }
4018 let x = self.input.mouse_x;
4019 let Some(eb) = self
4020 .globals
4021 .editbox_lists
4022 .get_mut(&form_id)
4023 .and_then(|list| list.boxes.get_mut(idx))
4024 else {
4025 return false;
4026 };
4027 if !eb.mouse_selecting {
4028 return false;
4029 }
4030 eb.set_cursor_from_window_x(x, true);
4031 true
4032 }
4033
4034 fn finish_editbox_mouse_up(&mut self, b: input::VmMouseButton) -> bool {
4035 if !matches!(b, input::VmMouseButton::Left) {
4036 return false;
4037 }
4038 let Some((form_id, idx)) = self.globals.focused_editbox else {
4039 return false;
4040 };
4041 let Some(eb) = self
4042 .globals
4043 .editbox_lists
4044 .get_mut(&form_id)
4045 .and_then(|list| list.boxes.get_mut(idx))
4046 else {
4047 return false;
4048 };
4049 let was_selecting = eb.mouse_selecting;
4050 eb.mouse_selecting = false;
4051 was_selecting
4052 }
4053
4054 pub fn on_mouse_up(&mut self, b: input::VmMouseButton) {
4055 if sg_input_trace_enabled() {
4056 eprintln!(
4057 "[SG_DEBUG][INPUT] mouse_up {:?} at=({}, {})",
4058 b, self.input.mouse_x, self.input.mouse_y
4059 );
4060 }
4061 self.input.on_mouse_up(b);
4062 if self.finish_editbox_mouse_up(b) {
4063 return;
4064 }
4065 if self.handle_msg_back_mouse_up(b) {
4066 return;
4067 }
4068 if self.handle_selbtn_mouse_up(b) {
4069 return;
4070 }
4071 let left_down_up =
4072 matches!(b, input::VmMouseButton::Left) && self.input.vk_down_up_stock(0x01);
4073 let right_down_up =
4074 matches!(b, input::VmMouseButton::Right) && self.input.vk_down_up_stock(0x02);
4075 let movie_skipped = if left_down_up {
4076 self.notify_movie_wait_down_up(1)
4077 } else if right_down_up {
4078 self.notify_movie_wait_down_up(-1)
4079 } else {
4080 false
4081 };
4082 if movie_skipped {
4083 return;
4084 }
4085 if right_down_up {
4086 if std::mem::take(&mut self.suppress_next_right_syscom_open) {
4087 return;
4088 }
4089 if self.open_syscom_menu_from_cancel_key() {
4090 return;
4091 }
4092 }
4093 let handled_button = self.handle_object_button_mouse_up(b);
4094 if handled_button {
4095 return;
4096 }
4097 if matches!(b, input::VmMouseButton::Left) && self.process_script_skip_trigger_decide(0x01)
4098 {
4099 return;
4100 }
4101 if left_down_up && !self.advance_message_wait(true) {
4102 self.notify_wait_key();
4103 }
4104 }
4105
4106 pub fn on_mouse_wheel(&mut self, delta_y: i32) {
4107 self.input.on_mouse_wheel(delta_y);
4108 if self.globals.syscom.msg_back_open {
4109 if delta_y > 0 {
4110 self.msg_back_target_up();
4111 } else if delta_y < 0 {
4112 self.msg_back_target_down();
4113 }
4114 return;
4115 }
4116 if delta_y > 0 && self.msg_back_is_enable() {
4117 self.open_msg_back_proc();
4118 return;
4119 }
4120 if delta_y < 0 && self.should_wheel_advance_message() {
4121 if self.process_script_skip_trigger_wheel_down() {
4122 return;
4123 }
4124 let _ = self.advance_message_wait(true);
4125 }
4126 }
4127
4128 fn finish_skipped_movie_waits(&mut self) {
4129 while let Some(info) = self.wait.take_movie_skip() {
4130 let Some(st) = self.globals.stage_forms.get_mut(&info.stage_form_id) else {
4131 continue;
4132 };
4133 let Some(list) = st.object_lists.get_mut(&info.stage_idx) else {
4134 continue;
4135 };
4136 let Some(obj) = find_object_by_runtime_slot_mut(list, info.runtime_slot) else {
4137 continue;
4138 };
4139
4140 let audio_id = obj.movie.audio_id.take();
4143 let backend = obj.backend.clone();
4144 obj.init_type_like();
4145
4146 if let Some(id) = audio_id {
4147 self.movie.stop_audio(id);
4148 }
4149 if let globals::ObjectBackend::Movie {
4150 layer_id,
4151 sprite_id,
4152 ..
4153 } = backend
4154 && let Some(layer) = self.layers.layer_mut(layer_id)
4155 && let Some(sprite) = layer.sprite_mut(sprite_id)
4156 {
4157 sprite.visible = false;
4158 sprite.image_id = None;
4159 }
4160 }
4161 }
4162
4163 fn handle_editbox_key(&mut self, k: input::VmKey) -> bool {
4164 let Some((form_id, idx)) = self.globals.focused_editbox else {
4165 return false;
4166 };
4167 let alt_down = self.input.vk_is_down(0x12);
4168 let shift_down = self.input.vk_is_down(0x10);
4169 let shortcut_down = self.input.vk_is_down(0x11) || self.input.vk_is_down(0x5B);
4170 let clipboard = self.globals.editbox_clipboard.clone();
4171 let mut move_focus: Option<bool> = None;
4172 let mut toggle_screen = false;
4173 let mut copied_text: Option<String> = None;
4174 let consumed = {
4175 let Some(eb) = self
4176 .globals
4177 .editbox_lists
4178 .get_mut(&form_id)
4179 .and_then(|list| list.boxes.get_mut(idx))
4180 else {
4181 return false;
4182 };
4183 if !eb.created || !eb.visible {
4184 return false;
4185 }
4186
4187 if eb.is_composing() {
4190 !matches!(k, input::VmKey::F(_) | input::VmKey::Other(_))
4191 } else {
4192 match k {
4193 input::VmKey::Enter => {
4194 if alt_down {
4195 toggle_screen = true;
4196 } else {
4197 eb.action_flag = crate::runtime::globals::EDITBOX_ACTION_DECIDED;
4198 }
4199 true
4200 }
4201 input::VmKey::Escape => {
4202 eb.action_flag = crate::runtime::globals::EDITBOX_ACTION_CANCELED;
4203 true
4204 }
4205 input::VmKey::Backspace => {
4206 eb.delete_backward(shortcut_down);
4207 true
4208 }
4209 input::VmKey::Delete => {
4210 eb.delete_forward(shortcut_down);
4211 true
4212 }
4213 input::VmKey::ArrowLeft => {
4214 eb.move_cursor_left(shift_down, shortcut_down);
4215 true
4216 }
4217 input::VmKey::ArrowRight => {
4218 eb.move_cursor_right(shift_down, shortcut_down);
4219 true
4220 }
4221 input::VmKey::ArrowUp | input::VmKey::ArrowDown => true,
4222 input::VmKey::Home => {
4223 eb.move_cursor_home(shift_down);
4224 true
4225 }
4226 input::VmKey::End => {
4227 eb.move_cursor_end(shift_down);
4228 true
4229 }
4230 input::VmKey::Tab => {
4231 move_focus = Some(!shift_down);
4232 true
4233 }
4234 input::VmKey::Letter(letter) if shortcut_down => {
4235 match letter.to_ascii_uppercase() {
4236 'A' => eb.select_all(),
4237 'C' => copied_text = eb.selected_text(),
4238 'X' => copied_text = eb.cut_selection(),
4239 'V' if !clipboard.is_empty() => eb.commit_text(&clipboard),
4240 'V' => {}
4241 'Z' if shift_down => eb.redo(),
4242 'Z' => eb.undo(),
4243 'Y' => eb.redo(),
4244 _ => {}
4245 }
4246 true
4247 }
4248 input::VmKey::Space | input::VmKey::Letter(_) | input::VmKey::Digit(_) => true,
4249 input::VmKey::Shift
4250 | input::VmKey::Control
4251 | input::VmKey::Meta
4252 | input::VmKey::Alt => false,
4253 input::VmKey::F(_) | input::VmKey::Other(_) => false,
4254 }
4255 }
4256 };
4257
4258 if let Some(text) = copied_text {
4259 self.globals.editbox_clipboard = text;
4260 }
4261 if toggle_screen {
4262 self.toggle_screen_size_mode_for_editbox();
4263 }
4264 if let Some(forward) = move_focus {
4265 self.move_editbox_focus(forward);
4266 }
4267 consumed
4268 }
4269
4270 pub fn begin_input_frame(&mut self) {
4271 self.msg_wait_skip_by_click = false;
4273 }
4274
4275 pub fn begin_frame_main_proc_pass(&mut self) {
4277 self.frame_main_proc_started_at = Some(crate::platform_time::Instant::now());
4278 }
4279
4280 fn update_skip_frame_rate_sample(&mut self, real_delta_ms: i32) {
4281 self.frame_rate_bucket_elapsed_ms = self
4285 .frame_rate_bucket_elapsed_ms
4286 .saturating_add(real_delta_ms.max(0) as u64);
4287 let periods = self.frame_rate_bucket_elapsed_ms / 100;
4288 if let Some(sample) = (self.frame_rate_bucket_frame_cnt as u64).checked_div(periods) {
4289 let sample = sample.min(i32::MAX as u64) as i32;
4290 self.frame_rate_100msec[self.frame_rate_100msec_index] = sample;
4291 self.frame_rate_100msec_index = (self.frame_rate_100msec_index + 1) % 10;
4292 self.frame_rate_100msec_total = self.frame_rate_100msec.iter().copied().sum();
4293 self.frame_rate_bucket_elapsed_ms -= periods * 100;
4294 self.frame_rate_bucket_frame_cnt = 0;
4295 }
4296 self.frame_rate_bucket_frame_cnt = self.frame_rate_bucket_frame_cnt.saturating_add(1);
4297 }
4298
4299 fn should_draw_after_message_key_wait_advance(&mut self) -> bool {
4300 if self.globals.script.wait_display_vsync_off_flag {
4306 return true;
4307 }
4308
4309 self.disp_because_msg_wait_cnt = self.disp_because_msg_wait_cnt.saturating_add(1);
4310
4311 let pump_over_100ms = self
4312 .frame_main_proc_started_at
4313 .as_ref()
4314 .map(|start| {
4315 crate::platform_time::Instant::now()
4316 .saturating_duration_since(*start)
4317 .as_millis()
4318 > 100
4319 })
4320 .unwrap_or(false);
4321 if pump_over_100ms {
4322 self.disp_because_msg_wait_cnt = 0;
4323 self.disp_because_msg_wait_cnt_max = 0;
4324 return true;
4325 }
4326
4327 if self.disp_because_msg_wait_cnt >= self.disp_because_msg_wait_cnt_max / 100 {
4328 self.disp_because_msg_wait_cnt = 0;
4329 if self.frame_rate_100msec_total < 30 {
4330 self.disp_because_msg_wait_cnt_max = self
4331 .disp_because_msg_wait_cnt_max
4332 .saturating_sub((30 - self.frame_rate_100msec_total) * 10)
4333 .max(0);
4334 } else {
4335 self.disp_because_msg_wait_cnt_max = self
4336 .disp_because_msg_wait_cnt_max
4337 .saturating_add(self.frame_rate_100msec_total - 29)
4338 .min(3000);
4339 }
4340 return true;
4341 }
4342
4343 false
4344 }
4345
4346 pub fn wait_poll(&mut self) -> bool {
4347 self.poll_native_messagebox_result();
4348
4349 let skipping_msg_before_reveal = self.runtime_is_skipping_msg();
4352 if self.wait.message_reveal_waiting()
4353 && (self.msg_wait_skip_by_click || skipping_msg_before_reveal)
4354 && !self.ui.message_wait_text_fully_revealed()
4355 {
4356 self.ui.reveal_message_now();
4357 }
4358
4359 if self.wait.message_reveal_waiting() && self.ui.message_wait_text_fully_revealed() {
4363 let promoted_to_key_wait = self.wait.finish_message_reveal();
4364 if promoted_to_key_wait {
4365 self.globals.script.skip_trigger = false;
4367 } else {
4368 self.ui.finish_message_reveal_wait();
4369 }
4370 }
4371
4372 if self.wait.message_key_waiting() {
4375 self.globals.script.skip_trigger = false;
4376 if self.runtime_is_skipping_msg() {
4377 let was_waiting = self.wait.message_key_waiting();
4378 let _ = self.advance_message_wait(true);
4379 let advanced = was_waiting && !self.wait.message_key_waiting();
4380 if advanced && self.should_draw_after_message_key_wait_advance() {
4381 self.request_disp_proc_boundary();
4385 }
4386 }
4387 }
4388
4389 let skipping = self.runtime_is_skipping();
4390 if !skipping {
4391 self.disp_because_msg_wait_cnt_max = 0;
4394 }
4395 if skipping && self.wait.mwnd_animation_waiting() {
4396 self.ui.finish_mwnd_animation();
4399 }
4400 if skipping && self.wait.wipe {
4401 self.finish_wipe_runtime();
4403 }
4404 let (wait, stack, bgm, koe, se, pcm, globals) = (
4405 &mut self.wait,
4406 &mut self.stack,
4407 &mut self.bgm,
4408 &mut self.koe,
4409 &mut self.se,
4410 &mut self.pcm,
4411 &mut self.globals,
4412 );
4413 wait.poll(stack, bgm, koe, se, pcm, globals, &self.ids, skipping)
4414 }
4415
4416 pub fn push(&mut self, v: Value) {
4417 self.stack.push(v);
4418 }
4419
4420 pub fn pop(&mut self) -> Option<Value> {
4421 self.stack.pop()
4422 }
4423
4424 pub fn set_native_ui_backend(&mut self, backend: Option<Arc<dyn native_ui::NativeUiBackend>>) {
4425 self.native_ui_backend = backend;
4426 }
4427
4428 pub fn game_title(&self) -> String {
4434 game_title::resolve_game_title(self.tables.gameexe.as_ref(), &self.project_dir)
4435 }
4436
4437 pub fn game_name(&self) -> String {
4439 self.game_title()
4440 }
4441
4442 pub fn game_display_info(&self) -> game_display_info::GameDisplayInfo {
4448 let cover = game_display_info::resolve_game_cover_from_project_dir(&self.project_dir);
4449 let name = self.game_name();
4450 game_display_info::GameDisplayInfo {
4451 title: name.clone(),
4452 name,
4453 cover,
4454 }
4455 }
4456
4457 pub fn game_cover(&self) -> Option<game_display_info::GameCover> {
4459 game_display_info::resolve_game_cover_from_project_dir(&self.project_dir)
4460 }
4461
4462 pub fn submit_native_messagebox_result(&mut self, request_id: u64, value: i64) {
4463 self.native_ui.enqueue_messagebox_result(request_id, value);
4464 self.poll_native_messagebox_result();
4465 }
4466
4467 pub fn request_system_messagebox(
4468 &mut self,
4469 kind: i32,
4470 debug_only: bool,
4471 text: String,
4472 buttons: Vec<globals::SystemMessageBoxButton>,
4473 ) {
4474 self.request_system_messagebox_internal(kind, debug_only, text, buttons, true);
4475 }
4476
4477 pub fn request_system_messagebox_no_return(
4478 &mut self,
4479 kind: i32,
4480 debug_only: bool,
4481 text: String,
4482 buttons: Vec<globals::SystemMessageBoxButton>,
4483 ) {
4484 self.request_system_messagebox_internal(kind, debug_only, text, buttons, false);
4485 }
4486
4487 pub fn request_internal_system_messagebox_no_return(
4492 &mut self,
4493 kind: i32,
4494 debug_only: bool,
4495 text: String,
4496 buttons: Vec<globals::SystemMessageBoxButton>,
4497 ) {
4498 let request_id = self.native_ui.next_messagebox_request_id();
4499 self.globals.system.messagebox_modal_result = None;
4500 self.globals.system.messagebox_modal = Some(globals::SystemMessageBoxModalState {
4501 request_id,
4502 kind,
4503 text,
4504 debug_only,
4505 buttons,
4506 cursor: 0,
4507 native_pending: false,
4508 complete_wait_with_value: false,
4509 });
4510 self.wait.wait_system_modal();
4511 }
4512
4513 pub fn request_chihaya_bench_dialog(&mut self, text: String) {
4516 let request_id = self.native_ui.next_messagebox_request_id();
4517 let native_pending = self.native_ui_backend.is_some();
4518 let buttons = vec![globals::SystemMessageBoxButton {
4519 label: "CLOSE".to_string(),
4520 value: 0,
4521 }];
4522 self.globals.system.messagebox_modal_result = None;
4523 self.globals.system.messagebox_modal = Some(globals::SystemMessageBoxModalState {
4524 request_id,
4525 kind: 17, text: text.clone(),
4527 debug_only: false,
4528 buttons,
4529 cursor: 0,
4530 native_pending,
4531 complete_wait_with_value: false,
4532 });
4533 self.wait.wait_system_modal();
4534
4535 if let Some(backend) = self.native_ui_backend.as_ref() {
4536 backend.show_chihaya_bench_dialog(native_ui::NativeChihayaBenchDialogRequest {
4537 request_id,
4538 title: self.game_title(),
4539 text,
4540 });
4541 }
4542 }
4543
4544 fn request_system_messagebox_internal(
4545 &mut self,
4546 kind: i32,
4547 debug_only: bool,
4548 text: String,
4549 buttons: Vec<globals::SystemMessageBoxButton>,
4550 complete_wait_with_value: bool,
4551 ) {
4552 let request_id = self.native_ui.next_messagebox_request_id();
4553 let native_pending = self.native_ui_backend.is_some();
4554 self.globals.system.messagebox_modal_result = None;
4555 self.globals.system.messagebox_modal = Some(globals::SystemMessageBoxModalState {
4556 request_id,
4557 kind,
4558 text: text.clone(),
4559 debug_only,
4560 buttons,
4561 cursor: 0,
4562 native_pending,
4563 complete_wait_with_value,
4564 });
4565 self.wait.wait_system_modal();
4566
4567 if let Some(backend) = self.native_ui_backend.as_ref() {
4568 backend.show_system_messagebox(native_ui::NativeMessageBoxRequest {
4569 request_id,
4570 kind: native_ui::NativeMessageBoxKind::from_system_op(kind),
4571 title: self.game_title(),
4572 message: text,
4573 buttons: self
4574 .globals
4575 .system
4576 .messagebox_modal
4577 .as_ref()
4578 .map(|modal| {
4579 modal
4580 .buttons
4581 .iter()
4582 .map(|button| native_ui::NativeMessageBoxButton {
4583 label: button.label.clone(),
4584 value: button.value,
4585 })
4586 .collect()
4587 })
4588 .unwrap_or_default(),
4589 debug_only,
4590 });
4591 }
4592 }
4593
4594 fn poll_native_messagebox_result(&mut self) {
4595 while let Some(result) = self.native_ui.pop_messagebox_result() {
4596 let Some(modal) = self.globals.system.messagebox_modal.as_ref() else {
4597 continue;
4598 };
4599 if modal.request_id != result.request_id {
4600 continue;
4601 }
4602 let max_value = modal.buttons.iter().map(|b| b.value).max().unwrap_or(0);
4603 let value = result.value.clamp(0, max_value);
4604 self.finish_system_messagebox(value);
4605 break;
4606 }
4607 }
4608
4609 pub fn set_screen_size(&mut self, w: u32, h: u32) {
4610 self.screen_w = w;
4611 self.screen_h = h;
4612 self.ui.sync_layout(&mut self.layers, w, h);
4613 self.sync_editbox_runtime();
4614 }
4615
4616 pub fn tick_frame(&mut self) {
4617 #[cfg(target_os = "vita")]
4618 self.movie.evict_idle_streams();
4619 let now = crate::platform_time::Instant::now();
4620 let last = self.frame_clock_last.replace(now);
4621 let real_delta_ms = match last {
4622 Some(last) => {
4623 let elapsed = now.saturating_duration_since(last);
4624 accumulate_integer_millis(elapsed, &mut self.frame_clock_sub_ms_ns)
4625 }
4626 None => {
4627 self.frame_clock_sub_ms_ns = 0;
4630 16
4631 }
4632 };
4633 let skipping = self.runtime_is_skipping();
4637 let game_delta_ms = if skipping {
4638 real_delta_ms.saturating_mul(32)
4639 } else {
4640 real_delta_ms
4641 };
4642 self.update_selbtn_animation(game_delta_ms as i64);
4643 let trace = env_is_set!("SG_CTX_TICK_TRACE");
4644 if trace {
4645 eprintln!(
4646 "[SG_CTX_TICK] start game_delta_ms={} real_delta_ms={}",
4647 game_delta_ms, real_delta_ms
4648 );
4649 }
4650 self.sync_editbox_runtime();
4651 self.poll_native_messagebox_result();
4652 syscom_form::poll_fallback_dialog(self);
4653 if trace {
4654 eprintln!("[SG_CTX_TICK] after sync_editbox_runtime");
4655 }
4656 self.sync_mwnd_window_ui();
4657 if trace {
4658 eprintln!("[SG_CTX_TICK] after sync_mwnd_window_ui");
4659 }
4660 self.ui.tick(
4661 &mut self.layers,
4662 &mut self.images,
4663 &self.project_dir,
4664 self.screen_w,
4665 self.screen_h,
4666 &self.globals.script,
4667 &self.globals.syscom,
4668 &self.globals.editbox_lists,
4669 self.globals.focused_editbox,
4670 );
4671 for ((form_id, stage_idx, mwnd_idx), visible) in self.ui.mwnd_reveal_counts() {
4672 if let Some(mwnd) = self
4673 .globals
4674 .stage_forms
4675 .get_mut(&form_id)
4676 .and_then(|st| st.mwnd_lists.get_mut(&stage_idx))
4677 .and_then(|list| list.get_mut(mwnd_idx))
4678 {
4679 for page in &mut mwnd.message_pages {
4680 for glyph in &mut page.glyphs {
4681 glyph.appeared = glyph.ruby || glyph.reveal_index <= visible;
4682 }
4683 let page_body = page.glyphs.iter().filter(|glyph| !glyph.ruby).count();
4684 let page_first = page
4685 .glyphs
4686 .iter()
4687 .filter(|glyph| !glyph.ruby)
4688 .map(|glyph| glyph.reveal_index)
4689 .min()
4690 .unwrap_or(1);
4691 page.disp_moji_cnt = visible
4692 .saturating_sub(page_first.saturating_sub(1))
4693 .min(page_body)
4694 .min(i32::MAX as usize) as i64;
4695 }
4696 for glyph in &mut mwnd.glyphs {
4697 glyph.appeared = glyph.ruby || glyph.reveal_index <= visible;
4698 }
4699 let active_first = mwnd
4700 .glyphs
4701 .iter()
4702 .filter(|glyph| !glyph.ruby)
4703 .map(|glyph| glyph.reveal_index)
4704 .min()
4705 .unwrap_or(visible.saturating_add(1));
4706 let active_body = mwnd.glyphs.iter().filter(|glyph| !glyph.ruby).count();
4707 mwnd.disp_moji_cnt = visible
4708 .saturating_sub(active_first.saturating_sub(1))
4709 .min(active_body)
4710 .min(i32::MAX as usize) as i64;
4711 }
4712 }
4713 self.apply_syscom_skip_flags();
4715 if trace {
4716 eprintln!("[SG_CTX_TICK] after apply_syscom_skip_flags");
4717 }
4718 self.globals.script.auto_mode_moji_cnt =
4720 self.ui.message_text().unwrap_or("").chars().count() as i64;
4721 if self
4722 .ui
4723 .auto_advance_due(&self.globals.script, &self.globals.syscom)
4724 && !self.advance_message_wait(true)
4725 {
4726 self.notify_wait_key();
4727 }
4728 self.sync_syscom_menu_ui();
4731 if trace {
4732 eprintln!("[SG_CTX_TICK] after sync_syscom_menu_ui");
4733 }
4734 self.sync_mwnd_selection_ui();
4735 if trace {
4736 eprintln!("[SG_CTX_TICK] after sync_mwnd_selection_ui");
4737 }
4738 let excall_tick = self
4742 .excall_state
4743 .ready
4744 .then(|| crate::runtime::forms::excall::tick_targets(self));
4745 self.globals.tick_frame(
4746 game_delta_ms,
4747 real_delta_ms,
4748 &self.tables.shake_templates,
4749 excall_tick,
4750 );
4751 if trace {
4752 eprintln!("[SG_CTX_TICK] after globals.tick_frame");
4753 }
4754 if self
4755 .globals
4756 .wipe
4757 .as_ref()
4758 .is_some_and(globals::WipeState::is_done)
4759 {
4760 self.finish_wipe_runtime();
4761 if trace {
4762 eprintln!("[SG_CTX_TICK] after finish_wipe_runtime");
4763 }
4764 }
4765 self.apply_object_event_animations();
4766 if trace {
4767 eprintln!("[SG_CTX_TICK] after apply_object_event_animations");
4768 }
4769 self.sync_weather_objects(game_delta_ms, real_delta_ms);
4770 if trace {
4771 eprintln!("[SG_CTX_TICK] after sync_weather_objects");
4772 }
4773 pcmevent_form::tick_all(self, game_delta_ms, real_delta_ms);
4774 let _ = self.bgm.tick(&mut self.audio);
4775 self.pcm.tick();
4776 self.koe.tick(&mut self.audio);
4777 syscom_form::update_audio_routing(self, real_delta_ms, false);
4778 if trace {
4779 eprintln!("[SG_CTX_TICK] after pcmevent/bgm/pcm/audio routing");
4780 }
4781 self.sync_emote_objects();
4782 self.sync_movie_objects();
4783 if trace {
4784 eprintln!("[SG_CTX_TICK] after sync_movie_objects");
4785 }
4786 self.sync_global_movie();
4787 if trace {
4788 eprintln!("[SG_CTX_TICK] after sync_global_movie");
4789 }
4790 self.update_object_button_hover();
4791 if trace {
4792 eprintln!("[SG_CTX_TICK] after update_object_button_hover");
4793 }
4794 self.apply_object_disp_override();
4795 if trace {
4796 eprintln!("[SG_CTX_TICK] after apply_object_disp_override");
4797 }
4798 self.update_skip_frame_rate_sample(real_delta_ms);
4799 }
4800
4801 fn apply_syscom_skip_flags(&mut self) {
4802 let no_wipe = self.globals.syscom.original_config.no_wipe_anime_flag;
4803 if no_wipe && self.globals.wipe.is_some() {
4806 self.finish_wipe_runtime();
4807 }
4808 }
4809
4810 fn apply_object_event_animations(&mut self) {
4811 let wipe_active = self.globals.wipe.is_some();
4812 let ids = self.ids.clone();
4813 let gfx = &mut self.gfx;
4814 let images = &mut self.images;
4815 let layers = &mut self.layers;
4816 let mwnd_ui_state = self
4817 .ui
4818 .current_mwnd_window_render_state(self.screen_w, self.screen_h);
4819 let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
4820 form_ids.sort_unstable();
4821 for form_id in form_ids {
4822 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
4823 continue;
4824 };
4825 let mut stage_ids: Vec<i64> = st
4826 .object_lists
4827 .keys()
4828 .chain(st.mwnd_lists.keys())
4829 .copied()
4830 .collect();
4831 stage_ids.sort_unstable();
4832 stage_ids.dedup();
4833 for stage_idx in stage_ids {
4834 if stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
4835 continue;
4836 }
4837 let embedded_slots = st.embedded_object_slots_by_stage.get(&stage_idx).cloned();
4838 let slot_use = st.object_slot_use.get(&stage_idx).cloned();
4839 let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
4840 continue;
4841 };
4842 for (obj_idx, obj) in objs.iter_mut().enumerate() {
4843 if embedded_slots
4844 .as_ref()
4845 .is_some_and(|slots| slots.contains(&obj_idx))
4846 {
4847 continue;
4848 }
4849 if !slot_use
4850 .as_ref()
4851 .and_then(|flags| flags.get(obj_idx))
4852 .copied()
4853 .unwrap_or(true)
4854 {
4855 continue;
4856 }
4857 apply_object_event_animations_recursive(
4858 &ids,
4859 gfx,
4860 images,
4861 layers,
4862 stage_idx,
4863 object_runtime_slot(obj_idx, obj) as i64,
4864 obj,
4865 );
4866 }
4867 }
4868
4869 let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
4870 mwnd_stage_ids.sort_unstable();
4871 for stage_idx in mwnd_stage_ids {
4872 if stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
4873 continue;
4874 }
4875 let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
4876 continue;
4877 };
4878 for mwnd in mwnds {
4879 let Some((window_x, window_y)) = mwnd.window_pos else {
4880 continue;
4881 };
4882 let Some((window_w, window_h)) = mwnd.window_size else {
4883 continue;
4884 };
4885 if window_w <= 0 || window_h <= 0 {
4886 continue;
4887 }
4888 let visible_or_animating = mwnd.open
4889 || mwnd_ui_state.is_some_and(|ui| {
4890 ui.x as i64 == window_x
4891 && ui.y as i64 == window_y
4892 && ui.w as i64 == window_w
4893 && ui.h as i64 == window_h
4894 });
4895 if !visible_or_animating {
4896 continue;
4897 }
4898 for (obj_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
4899 apply_object_event_animations_recursive(
4900 &ids,
4901 gfx,
4902 images,
4903 layers,
4904 stage_idx,
4905 object_runtime_slot(obj_idx, obj) as i64,
4906 obj,
4907 );
4908 }
4909 for (obj_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
4910 apply_object_event_animations_recursive(
4911 &ids,
4912 gfx,
4913 images,
4914 layers,
4915 stage_idx,
4916 object_runtime_slot(obj_idx, obj) as i64,
4917 obj,
4918 );
4919 }
4920 for (obj_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
4921 apply_object_event_animations_recursive(
4922 &ids,
4923 gfx,
4924 images,
4925 layers,
4926 stage_idx,
4927 object_runtime_slot(obj_idx, obj) as i64,
4928 obj,
4929 );
4930 }
4931 }
4932 }
4933 }
4934 }
4935
4936 fn apply_object_masks(&mut self) {
4937 let Some(mask_info) = self.build_mask_info() else {
4938 return;
4939 };
4940 if mask_info.is_empty() {
4941 return;
4942 }
4943
4944 let mut resolved_masks = HashMap::default();
4945 for (mask_name, _, _) in mask_info.iter().flatten() {
4946 if resolved_masks.contains_key(mask_name) {
4947 continue;
4948 }
4949 if let Some(id) = self.resolve_mask_image(mask_name) {
4950 resolved_masks.insert(mask_name.clone(), id);
4951 }
4952 }
4953
4954 let ids = self.ids.clone();
4955 let gfx = &mut self.gfx;
4956 let layers = &mut self.layers;
4957 let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
4958 form_ids.sort_unstable();
4959 for form_id in form_ids {
4960 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
4961 continue;
4962 };
4963 let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
4964 stage_ids.sort_unstable();
4965 for stage_idx in stage_ids {
4966 let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
4967 continue;
4968 };
4969 for (obj_idx, obj) in objs.iter_mut().enumerate() {
4970 apply_object_masks_recursive(
4971 &ids,
4972 gfx,
4973 layers,
4974 stage_idx,
4975 object_runtime_slot(obj_idx, obj) as i64,
4976 obj,
4977 &mask_info,
4978 &resolved_masks,
4979 );
4980 }
4981 }
4982 }
4983 }
4984
4985 fn active_mask_list(&self) -> Option<&globals::MaskListState> {
4986 if self.ids.form_global_mask != 0 {
4987 return self.globals.mask_lists.get(&self.ids.form_global_mask);
4988 }
4989 None
4990 }
4991
4992 fn build_mask_info(&self) -> Option<Vec<Option<(String, i32, i32)>>> {
4993 let ml = self.active_mask_list()?;
4994 let mut out = Vec::with_capacity(ml.masks.len());
4995 for m in &ml.masks {
4996 let Some(name) = m.name.as_ref() else {
4997 out.push(None);
4998 continue;
4999 };
5000 if name.is_empty() {
5001 out.push(None);
5002 continue;
5003 }
5004 let x = m.x_event.get_total_value();
5005 let y = m.y_event.get_total_value();
5006 out.push(Some((name.clone(), x, y)));
5007 }
5008 Some(out)
5009 }
5010
5011 fn resolve_mask_image(&mut self, name: &str) -> Option<ImageHandle> {
5012 if name.is_empty() {
5013 return None;
5014 }
5015 if let Some(path) = resolve_mask_path(&self.project_dir, name)
5016 && let Ok(id) = self.images.load_file(&path, 0)
5017 {
5018 return Some(id);
5019 }
5020 if let Ok(id) = self.images.load_g00(name, 0) {
5021 return Some(id);
5022 }
5023 if let Ok(id) = self.images.load_bg(name) {
5024 return Some(id);
5025 }
5026 None
5027 }
5028
5029 fn apply_object_tonecurves(&mut self) {
5030 let ids = self.ids.clone();
5031 let gfx = &mut self.gfx;
5032 let images = &mut self.images;
5033 let layers = &mut self.layers;
5034 let tonecurve = &mut self.tonecurve;
5035 let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
5036 form_ids.sort_unstable();
5037 for form_id in form_ids {
5038 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
5039 continue;
5040 };
5041 let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
5042 stage_ids.sort_unstable();
5043 for stage_idx in stage_ids {
5044 let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
5045 continue;
5046 };
5047 for (obj_idx, obj) in objs.iter_mut().enumerate() {
5048 apply_object_tonecurves_recursive(
5049 &ids,
5050 gfx,
5051 images,
5052 layers,
5053 tonecurve,
5054 stage_idx,
5055 object_runtime_slot(obj_idx, obj) as i64,
5056 obj,
5057 );
5058 }
5059 }
5060 }
5061 }
5062
5063 fn apply_gan_effects(&mut self, sprites: &mut Vec<RenderSprite>) {
5064 let mut index: HashMap<(Option<LayerId>, Option<SpriteId>), usize> = HashMap::default();
5065 for (i, s) in sprites.iter().enumerate() {
5066 index.insert((s.layer_id, s.sprite_id), i);
5067 }
5068
5069 let gfx = &mut self.gfx;
5070 let images = &mut self.images;
5071 let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
5072 form_ids.sort_unstable();
5073 for form_id in form_ids {
5074 let Some(st) = self.globals.stage_forms.get_mut(&form_id) else {
5075 continue;
5076 };
5077
5078 let mut object_stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
5079 object_stage_ids.sort_unstable();
5080 for stage_idx in object_stage_ids {
5081 let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
5082 continue;
5083 };
5084 for (obj_idx, obj) in objs.iter_mut().enumerate() {
5085 apply_gan_effects_recursive(
5086 gfx,
5087 images,
5088 sprites,
5089 &index,
5090 stage_idx,
5091 object_runtime_slot(obj_idx, obj) as i64,
5092 obj,
5093 );
5094 }
5095 }
5096
5097 let mut mwnd_stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
5098 mwnd_stage_ids.sort_unstable();
5099 for stage_idx in mwnd_stage_ids {
5100 let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) else {
5101 continue;
5102 };
5103 for mwnd in mwnds {
5104 for (obj_idx, obj) in mwnd.button_list.iter_mut().enumerate() {
5105 apply_gan_effects_recursive(
5106 gfx,
5107 images,
5108 sprites,
5109 &index,
5110 stage_idx,
5111 object_runtime_slot(obj_idx, obj) as i64,
5112 obj,
5113 );
5114 }
5115 for (obj_idx, obj) in mwnd.face_list.iter_mut().enumerate() {
5116 apply_gan_effects_recursive(
5117 gfx,
5118 images,
5119 sprites,
5120 &index,
5121 stage_idx,
5122 object_runtime_slot(obj_idx, obj) as i64,
5123 obj,
5124 );
5125 }
5126 for (obj_idx, obj) in mwnd.object_list.iter_mut().enumerate() {
5127 apply_gan_effects_recursive(
5128 gfx,
5129 images,
5130 sprites,
5131 &index,
5132 stage_idx,
5133 object_runtime_slot(obj_idx, obj) as i64,
5134 obj,
5135 );
5136 }
5137 }
5138 }
5139
5140 let mut btnsel_stage_ids: Vec<i64> = st.btnselitem_lists.keys().copied().collect();
5141 btnsel_stage_ids.sort_unstable();
5142 for stage_idx in btnsel_stage_ids {
5143 let Some(items) = st.btnselitem_lists.get_mut(&stage_idx) else {
5144 continue;
5145 };
5146 for item in items {
5147 for (obj_idx, obj) in item.object_list.iter_mut().enumerate() {
5148 apply_gan_effects_recursive(
5149 gfx,
5150 images,
5151 sprites,
5152 &index,
5153 stage_idx,
5154 object_runtime_slot(obj_idx, obj) as i64,
5155 obj,
5156 );
5157 }
5158 }
5159 }
5160 }
5161 }
5162
5163 fn apply_object_disp_override(&mut self) {
5164 const GET_OBJECT_DISP_ONOFF: i32 = 278;
5165 let disp_on = self
5166 .globals
5167 .syscom
5168 .config_int
5169 .get(&GET_OBJECT_DISP_ONOFF)
5170 .copied()
5171 .unwrap_or(1)
5172 != 0;
5173 if disp_on {
5174 return;
5175 }
5176
5177 let ui_layer = self.ui.mwnd.layer;
5178 for (stage_idx, list) in self
5179 .globals
5180 .stage_forms
5181 .values()
5182 .flat_map(|st| st.object_lists.iter())
5183 {
5184 for (obj_idx, obj) in list.iter().enumerate() {
5185 match &obj.backend {
5186 globals::ObjectBackend::String { layer_id, .. } => {
5187 if Some(*layer_id) == ui_layer {
5188 continue;
5189 }
5190 for (_, sprite_id) in layer_backed_object_sprite_bindings(&obj.backend) {
5191 if let Some(spr) = self
5192 .layers
5193 .layer_mut(*layer_id)
5194 .and_then(|layer| layer.sprite_mut(sprite_id))
5195 {
5196 spr.visible = false;
5197 }
5198 }
5199 }
5200 globals::ObjectBackend::Rect {
5201 layer_id,
5202 sprite_id,
5203 ..
5204 }
5205 | globals::ObjectBackend::Movie {
5206 layer_id,
5207 sprite_id,
5208 ..
5209 } => {
5210 if Some(*layer_id) == ui_layer {
5211 continue;
5212 }
5213 if let Some(layer) = self.layers.layer_mut(*layer_id)
5214 && let Some(spr) = layer.sprite_mut(*sprite_id)
5215 {
5216 spr.visible = false;
5217 }
5218 }
5219 globals::ObjectBackend::Number {
5220 layer_id,
5221 sprite_ids,
5222 }
5223 | globals::ObjectBackend::Weather {
5224 layer_id,
5225 sprite_ids,
5226 } => {
5227 if Some(*layer_id) == ui_layer {
5228 continue;
5229 }
5230 if let Some(layer) = self.layers.layer_mut(*layer_id) {
5231 for sid in sprite_ids {
5232 if let Some(spr) = layer.sprite_mut(*sid) {
5233 spr.visible = false;
5234 }
5235 }
5236 }
5237 }
5238 globals::ObjectBackend::Gfx => {
5239 if let Some((lid, sid)) =
5240 self.gfx.object_sprite_binding(*stage_idx, obj_idx as i64)
5241 {
5242 if Some(lid) == ui_layer {
5243 continue;
5244 }
5245 if let Some(layer) = self.layers.layer_mut(lid)
5246 && let Some(spr) = layer.sprite_mut(sid)
5247 {
5248 spr.visible = false;
5249 }
5250 }
5251 }
5252 _ => {}
5253 }
5254 }
5255 }
5256 }
5257
5258 fn handle_system_messagebox_key(&mut self, k: input::VmKey) -> bool {
5259 let Some(modal) = self.globals.system.messagebox_modal.as_mut() else {
5260 return false;
5261 };
5262 if modal.native_pending {
5263 return true;
5264 }
5265 let mut finish_value: Option<i64> = None;
5266 match k {
5267 input::VmKey::ArrowLeft | input::VmKey::ArrowUp => {
5268 let len = modal.buttons.len();
5269 if len > 0 {
5270 modal.cursor = if modal.cursor == 0 {
5271 len - 1
5272 } else {
5273 modal.cursor - 1
5274 };
5275 }
5276 }
5277 input::VmKey::ArrowRight | input::VmKey::ArrowDown | input::VmKey::Tab => {
5278 let len = modal.buttons.len();
5279 if len > 0 {
5280 modal.cursor = (modal.cursor + 1) % len;
5281 }
5282 }
5283 input::VmKey::Enter | input::VmKey::Space => {
5284 finish_value = Some(modal.selected_value());
5285 }
5286 input::VmKey::Escape => {
5287 finish_value = Some(modal.cancel_value());
5288 }
5289 input::VmKey::Digit(d) => {
5290 let idx = d.saturating_sub(1) as usize;
5291 if idx < modal.buttons.len() {
5292 modal.cursor = idx;
5293 finish_value = Some(modal.selected_value());
5294 }
5295 }
5296 _ => {}
5297 }
5298 if let Some(value) = finish_value {
5299 self.finish_system_messagebox(value);
5300 }
5301 true
5302 }
5303
5304 fn handle_system_messagebox_click(&mut self, b: input::VmMouseButton) -> bool {
5305 let Some(modal) = self.globals.system.messagebox_modal.as_mut() else {
5306 return false;
5307 };
5308 if modal.native_pending {
5309 return true;
5310 }
5311 match b {
5312 input::VmMouseButton::Left => {
5313 let len = modal.buttons.len().max(1);
5314 let max_idx = len - 1;
5315 let mut idx = None;
5323 if let Some((lines, pitch, top)) = self.ui.sys_overlay_metrics() {
5324 if lines >= len + 2 {
5325 let first_button_line = lines - 2 - len;
5326 let y0 = top + first_button_line as f32 * pitch;
5327 idx = Some(
5328 (((self.input.mouse_y as f32 - y0).max(0.0) / pitch) as usize)
5329 .min(max_idx),
5330 );
5331 }
5332 }
5333 let idx = idx.unwrap_or_else(|| {
5334 let body_lines = modal.text.lines().count() as i32;
5335 let first_button_y = 40 + (body_lines + 2) * 30;
5336 (((self.input.mouse_y - first_button_y).max(0) / 30) as usize).min(max_idx)
5337 });
5338 modal.cursor = idx;
5339 let value = modal.selected_value();
5340 self.finish_system_messagebox(value);
5341 }
5342 input::VmMouseButton::Right => {
5343 let value = modal.cancel_value();
5344 self.finish_system_messagebox(value);
5345 }
5346 _ => {}
5347 }
5348 true
5349 }
5350
5351 fn finish_system_messagebox(&mut self, value: i64) {
5352 let complete_wait_with_value = self
5353 .globals
5354 .system
5355 .messagebox_modal
5356 .as_ref()
5357 .map(|modal| modal.complete_wait_with_value)
5358 .unwrap_or(false);
5359 self.globals.system.messagebox_modal = None;
5360 self.globals.system.messagebox_modal_result = Some(value);
5361 if complete_wait_with_value {
5362 self.wait.finish_system_modal(Value::Int(value));
5363 } else {
5364 self.wait.finish_system_modal_void();
5365 }
5366 self.ui.set_sys_overlay(false, String::new());
5367 }
5368
5369 fn sync_system_messagebox_ui(&mut self) -> bool {
5370 let Some(modal) = self.globals.system.messagebox_modal.as_ref() else {
5371 return false;
5372 };
5373 if modal.native_pending {
5374 self.ui.set_sys_overlay(false, String::new());
5376 return true;
5377 }
5378
5379 let mut text = modal.text.clone();
5380 if !text.is_empty() && !text.ends_with('\n') {
5381 text.push('\n');
5382 }
5383 text.push('\n');
5384 for (index, button) in modal.buttons.iter().enumerate() {
5385 if index == modal.cursor {
5386 text.push_str("▶ ");
5387 } else {
5388 text.push_str(" ");
5389 }
5390 text.push_str(&button.label);
5391 if index == modal.cursor {
5392 text.push_str(" ◀");
5393 }
5394 text.push('\n');
5395 }
5396 text.push_str("\n↑↓/←→: select Enter: decide Esc: back");
5397 self.ui.set_sys_overlay(true, text);
5398 true
5399 }
5400
5401 fn msg_back_state(&self) -> Option<&globals::MsgBackState> {
5402 let form_id = self.ids.form_global_msgbk;
5403 if form_id == 0 {
5404 return None;
5405 }
5406 self.globals.msgbk_forms.get(&form_id)
5407 }
5408
5409 fn sync_msg_back_history_capacity(&mut self) {
5410 let max_count = self
5411 .gameexe_i64_default("MSGBK.HISTORY_CNT", 256)
5412 .clamp(1, 4096) as usize;
5413 let form_id = self.ids.form_global_msgbk;
5414 if form_id == 0 {
5415 return;
5416 }
5417 if let Some(st) = self.globals.msgbk_forms.get_mut(&form_id) {
5418 st.set_history_cnt_max(max_count);
5419 }
5420 }
5421
5422 fn msg_back_entry_has_content(entry: &globals::MsgBackEntry) -> bool {
5423 entry.pct_flag
5424 || !entry.msg_str.is_empty()
5425 || !entry.disp_name.is_empty()
5426 || !entry.original_name.is_empty()
5427 || !entry.koe_no_list.is_empty()
5428 }
5429
5430 fn msg_back_visible_entry_indices(&self) -> Vec<usize> {
5431 self.msg_back_state()
5432 .map(|st| {
5433 st.ordered_history_indices()
5434 .into_iter()
5435 .filter(|&i| {
5436 st.history
5437 .get(i)
5438 .is_some_and(Self::msg_back_entry_has_content)
5439 })
5440 .collect()
5441 })
5442 .unwrap_or_default()
5443 }
5444
5445 fn msg_back_is_enable(&self) -> bool {
5446 self.globals.syscom.msg_back.check_enabled() != 0 && !self.globals.script.msg_back_disable
5447 }
5448
5449 fn msg_back_line_step(&self) -> i32 {
5450 let moji_size = self.gameexe_i64_default("MSGBK.MOJI_SIZE", 24).max(1) as i32;
5451 let moji_space = self.gameexe_pair_default("MSGBK.MOJI_SPACE", (-1, 10));
5452 (moji_size + moji_space.1 as i32).max(1)
5453 }
5454
5455 fn msg_back_text_area_width(
5456 moji_cnt: (i64, i64),
5457 moji_size: i32,
5458 moji_space: (i64, i64),
5459 ) -> i32 {
5460 let cols = moji_cnt.0.max(1) as i32;
5461 moji_size
5462 .saturating_mul(cols)
5463 .saturating_add((moji_space.0 as i32).saturating_mul((cols - 1).max(0)))
5464 .max(1)
5465 }
5466
5467 fn msg_back_text_area_height(
5468 moji_cnt: (i64, i64),
5469 moji_size: i32,
5470 moji_space: (i64, i64),
5471 ) -> i32 {
5472 let rows = moji_cnt.1.max(1) as i32;
5473 moji_size
5474 .saturating_mul(rows)
5475 .saturating_add((moji_space.1 as i32).saturating_mul((rows - 1).max(0)))
5476 .max(1)
5477 }
5478
5479 fn msg_back_is_hankaku(ch: char) -> bool {
5480 ch.is_ascii() || matches!(ch as u32, 0xFF61..=0xFF9F)
5481 }
5482
5483 fn msg_back_is_kinsoku_moji(ch: char) -> bool {
5484 matches!(
5485 ch,
5486 'ぁ' | 'ぃ'
5487 | 'ぅ'
5488 | 'ぇ'
5489 | 'ぉ'
5490 | 'っ'
5491 | 'ゃ'
5492 | 'ゅ'
5493 | 'ょ'
5494 | 'ゎ'
5495 | 'ァ'
5496 | 'ィ'
5497 | 'ゥ'
5498 | 'ェ'
5499 | 'ォ'
5500 | 'ッ'
5501 | 'ャ'
5502 | 'ュ'
5503 | 'ョ'
5504 | 'ヮ'
5505 | 'ヵ'
5506 | 'ヶ'
5507 | '゙'
5508 | '゚'
5509 | '。'
5510 | '、'
5511 | '!'
5512 | '?'
5513 | ':'
5514 | ';'
5515 | '」'
5516 | ')'
5517 | ']'
5518 | '>'
5519 | '}'
5520 | '\''
5521 | '"'
5522 | 'ー'
5523 | '・'
5524 | '.'
5525 | ','
5526 | 'ァ'
5527 | 'ィ'
5528 | 'ゥ'
5529 | 'ェ'
5530 | 'ォ'
5531 | 'ッ'
5532 | 'ャ'
5533 | 'ュ'
5534 | 'ョ'
5535 )
5536 }
5537
5538 fn msg_back_entry_text(entry: &globals::MsgBackEntry) -> String {
5539 if entry.pct_flag {
5540 return String::new();
5541 }
5542 let mut out = String::new();
5543 if !entry.disp_name.is_empty() {
5544 out.push_str(&entry.disp_name);
5545 out.push('\u{0007}');
5546 }
5547 if !entry.msg_str.is_empty() {
5548 out.push_str(&entry.msg_str);
5549 out.push('\u{0007}');
5550 }
5551 out
5552 }
5553
5554 fn msg_back_measure_entry_text(
5555 entry: &globals::MsgBackEntry,
5556 moji_cnt: (i64, i64),
5557 moji_size: i32,
5558 moji_space: (i64, i64),
5559 ) -> (String, i32) {
5560 let text = Self::msg_back_entry_text(entry);
5561 if text.is_empty() {
5562 return (text, moji_size.max(1));
5563 }
5564
5565 let msg_w = Self::msg_back_text_area_width(moji_cnt, moji_size, moji_space);
5566 let msg_h = Self::msg_back_text_area_height(moji_cnt, moji_size, moji_space);
5567 let space_x = moji_space.0 as i32;
5568 let space_y = moji_space.1 as i32;
5569 let line_step = (moji_size + space_y).max(1);
5570 let mut x = 0i32;
5571 let mut y = 0i32;
5572 let mut indent_pos = 0i32;
5573 let mut indent_moji = '\0';
5574 let mut indent_cnt = 0i32;
5575 let mut line_head = true;
5576
5577 let clear_indent = |indent_pos: &mut i32, indent_moji: &mut char, indent_cnt: &mut i32| {
5578 *indent_pos = 0;
5579 *indent_moji = '\0';
5580 *indent_cnt = 0;
5581 };
5582 let new_line_indent = |x: &mut i32, y: &mut i32, indent_pos: i32| {
5583 *x = indent_pos;
5584 *y = (*y).saturating_add(line_step);
5585 };
5586
5587 for ch in text.chars() {
5588 if ch == '\r' {
5589 continue;
5590 }
5591 if ch == '\n' {
5592 new_line_indent(&mut x, &mut y, indent_pos);
5593 line_head = true;
5594 continue;
5595 }
5596 if ch == '\u{0007}' {
5597 clear_indent(&mut indent_pos, &mut indent_moji, &mut indent_cnt);
5598 new_line_indent(&mut x, &mut y, indent_pos);
5599 line_head = true;
5600 continue;
5601 }
5602
5603 let this_moji_size = if Self::msg_back_is_hankaku(ch) {
5604 (moji_size / 2).max(1)
5605 } else {
5606 moji_size.max(1)
5607 };
5608 let this_check_size = this_moji_size.saturating_add(space_x);
5609 let mut auto_indent = false;
5610 if x.saturating_add(this_check_size) > msg_w.saturating_add(moji_size)
5611 || (x.saturating_add(this_check_size) > msg_w
5612 && !Self::msg_back_is_kinsoku_moji(ch))
5613 {
5614 new_line_indent(&mut x, &mut y, indent_pos);
5615 auto_indent = true;
5616 }
5617 if auto_indent && (ch == ' ' || ch == ' ') {
5618 continue;
5619 }
5620 if y >= msg_h {
5621 break;
5622 }
5623
5624 x = x.saturating_add(this_moji_size).saturating_add(space_x);
5625
5626 if ch == '「' || ch == '『' || ch == '(' {
5627 if line_head {
5628 indent_pos = x;
5629 indent_moji = ch;
5630 indent_cnt = 1;
5631 } else if ch == indent_moji {
5632 indent_cnt += 1;
5633 }
5634 }
5635 if indent_cnt > 0
5636 && ((indent_moji == '「' && ch == '」')
5637 || (indent_moji == '『' && ch == '』')
5638 || (indent_moji == '(' && ch == ')'))
5639 {
5640 indent_cnt -= 1;
5641 if indent_cnt == 0 {
5642 clear_indent(&mut indent_pos, &mut indent_moji, &mut indent_cnt);
5643 }
5644 }
5645 line_head = false;
5646 }
5647
5648 let height = y.saturating_sub(space_y).max(moji_size.max(1));
5649 (text, height)
5650 }
5651
5652 fn msg_back_image_size_by_name(&mut self, file: Option<&str>) -> Option<(i32, i32)> {
5653 let raw = file.map(str::trim).filter(|s| !s.is_empty())?;
5654 let id = self
5655 .images
5656 .load_g00(raw, 0)
5657 .or_else(|_| self.images.load_bg_frame(raw, 0))
5658 .or_else(|_| {
5659 let path = self.project_dir.join(raw);
5660 self.images.load_file(&path, 0)
5661 })
5662 .ok()?;
5663 self.images
5664 .get(&id)
5665 .map(|img| (img.width as i32, img.height as i32))
5666 }
5667
5668 fn msg_back_image_size_from_gameexe(&mut self, key: &str) -> Option<(i32, i32)> {
5669 let file = self.gameexe_string(key);
5670 self.msg_back_image_size_by_name(file.as_deref())
5671 }
5672
5673 fn build_msg_back_layout(&mut self) -> MsgBackLayout {
5674 self.sync_msg_back_history_capacity();
5675 let mut out = MsgBackLayout::default();
5676 let indices = self.msg_back_visible_entry_indices();
5677 let entries: Vec<(usize, globals::MsgBackEntry)> = {
5678 let Some(st) = self.msg_back_state() else {
5679 return out;
5680 };
5681 indices
5682 .into_iter()
5683 .filter_map(|history_index| {
5684 st.history
5685 .get(history_index)
5686 .cloned()
5687 .map(|entry| (history_index, entry))
5688 })
5689 .collect()
5690 };
5691 if entries.is_empty() {
5692 return out;
5693 }
5694
5695 let moji_cnt = self.gameexe_pair_default("MSGBK.MOJI_CNT", (20, 15));
5696 let moji_size = self.gameexe_i64_default("MSGBK.MOJI_SIZE", 24).max(1) as i32;
5697 let moji_space = self.gameexe_pair_default("MSGBK.MOJI_SPACE", (-1, 10));
5698 let separator_file = self.gameexe_string("MSGBK.SEPARATOR_FILE");
5699 let separator_top_file = self.gameexe_string("MSGBK.SEPARATOR_TOP_FILE");
5700 let separator_bottom_file = self.gameexe_string("MSGBK.SEPARATOR_BOTTOM_FILE");
5701 let separator_height = self
5702 .msg_back_image_size_by_name(separator_file.as_deref())
5703 .map(|(_, h)| h.max(0))
5704 .unwrap_or(0);
5705 let separator_top_height = self
5706 .msg_back_image_size_by_name(separator_top_file.as_deref())
5707 .map(|(_, h)| h.max(0))
5708 .unwrap_or(0);
5709 let separator_bottom_height = self
5710 .msg_back_image_size_by_name(separator_bottom_file.as_deref())
5711 .map(|(_, h)| h.max(0))
5712 .unwrap_or(0);
5713
5714 if separator_top_file.is_some() && separator_top_height > 0 {
5715 out.separators.push(MsgBackSeparatorLayout {
5716 file: separator_top_file.clone(),
5717 total_pos: -separator_top_height,
5718 height: separator_top_height,
5719 });
5720 }
5721
5722 let mut total_height = 0i32;
5723 let mut last_margin = 0i32;
5724 for (visible_pos, (history_index, entry)) in entries.iter().enumerate() {
5725 if entry.pct_flag {
5726 let total_pos = total_height;
5727 let height = self
5728 .msg_back_image_size_by_name(Some(entry.msg_str.as_str()))
5729 .map(|(_, h)| h.max(1))
5730 .unwrap_or_else(|| moji_size.max(1));
5731 out.entries.push(MsgBackLayoutEntry {
5732 history_index: *history_index,
5733 text: String::new(),
5734 total_pos,
5735 height,
5736 });
5737 total_height = total_height.saturating_add(height);
5738 last_margin = 0;
5739 } else {
5740 let (text, height) =
5741 Self::msg_back_measure_entry_text(entry, moji_cnt, moji_size, moji_space);
5742 let total_pos = total_height.saturating_add(last_margin);
5743 out.entries.push(MsgBackLayoutEntry {
5744 history_index: *history_index,
5745 text,
5746 total_pos,
5747 height,
5748 });
5749 total_height = total_height
5750 .saturating_add(last_margin)
5751 .saturating_add(height);
5752 last_margin = moji_size;
5753 }
5754
5755 if visible_pos + 1 < entries.len() {
5756 if separator_file.is_some() && separator_height > 0 {
5757 out.separators.push(MsgBackSeparatorLayout {
5758 file: separator_file.clone(),
5759 total_pos: total_height,
5760 height: separator_height,
5761 });
5762 total_height = total_height.saturating_add(separator_height);
5763 last_margin = 0;
5764 }
5765 } else if separator_bottom_file.is_some() && separator_bottom_height > 0 {
5766 out.separators.push(MsgBackSeparatorLayout {
5767 file: separator_bottom_file.clone(),
5768 total_pos: total_height,
5769 height: separator_bottom_height,
5770 });
5771 total_height = total_height.saturating_add(separator_bottom_height);
5772 last_margin = 0;
5773 }
5774 }
5775 out.total_height = total_height.max(0);
5776 out
5777 }
5778
5779 fn msg_back_slider_track(&self) -> (i32, i32, i32) {
5780 let vals = Self::parse_i64_list(self.gameexe_value("MSGBK_ITEM.SLIDER.POS"));
5781 if vals.len() >= 3 {
5782 (vals[0] as i32, vals[1] as i32, vals[2] as i32)
5783 } else {
5784 (0, 0, 0)
5785 }
5786 }
5787
5788 fn msg_back_slider_size_i32(&mut self) -> (i32, i32) {
5789 if let Some((w, h)) = self.msg_back_image_size_from_gameexe("MSGBK_ITEM.SLIDER.FILE") {
5790 return (w.max(0), h.max(0));
5791 }
5792 self.ui
5793 .msg_back_slider_size()
5794 .map(|(w, h)| (w as i32, h as i32))
5795 .unwrap_or((0, 0))
5796 }
5797
5798 fn limit_i32(a: i32, v: i32, b: i32) -> i32 {
5799 let lo = a.min(b);
5800 let hi = a.max(b);
5801 v.clamp(lo, hi)
5802 }
5803
5804 fn linear_i32(x: i32, x1: i32, y1: i32, x2: i32, y2: i32) -> i32 {
5805 if x1 == x2 {
5806 return y1;
5807 }
5808 let num = (x as i64 - x1 as i64) * (y2 as i64 - y1 as i64);
5809 (y1 as i64 + num / (x2 as i64 - x1 as i64)) as i32
5810 }
5811
5812 fn msg_back_scroll_limits(&self, layout: &MsgBackLayout) -> Option<(i32, i32)> {
5813 let first = layout.entries.first()?;
5814 let last = layout.entries.last()?;
5815 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
5816 let wind_height = window_size.1.max(1) as i32;
5817 let msgsp = wind_height / 2 - first.height / 2;
5818 let mut msgep = wind_height / 2 + last.height / 2 - layout.total_height;
5819 if layout.entries.len() == 1 {
5820 msgep = msgsp;
5821 }
5822 Some((msgep, msgsp))
5823 }
5824
5825 fn msg_back_calc_target_no_from_scroll(&mut self, layout: &MsgBackLayout) {
5826 if layout.entries.is_empty() {
5827 self.globals.syscom.msg_back_target_no = -1;
5828 return;
5829 }
5830 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
5831 let center = (window_size.1.max(1) as i32) / 2;
5832 let mut target = layout
5833 .entries
5834 .last()
5835 .map(|e| e.history_index as isize)
5836 .unwrap_or(-1);
5837 for entry in layout.entries.iter().rev() {
5838 if self
5839 .globals
5840 .syscom
5841 .msg_back_scroll_pos
5842 .saturating_add(entry.total_pos)
5843 .saturating_add(entry.height)
5844 >= center
5845 {
5846 target = entry.history_index as isize;
5847 }
5848 }
5849 self.globals.syscom.msg_back_target_no = target;
5850 }
5851
5852 fn msg_back_calc_slider_pos_from_scroll(&mut self, layout: &MsgBackLayout) {
5853 let Some((msgep, msgsp)) = self.msg_back_scroll_limits(layout) else {
5854 let (_x, top, _bottom) = self.msg_back_slider_track();
5855 self.globals.syscom.msg_back_scroll_pos = 0;
5856 self.globals.syscom.msg_back_slider_pos = top;
5857 return;
5858 };
5859 let (_x, top, bottom) = self.msg_back_slider_track();
5860 let slider_h = self.msg_back_slider_size_i32().1.max(0);
5861 let slider_end = bottom.saturating_sub(slider_h);
5862 self.globals.syscom.msg_back_scroll_pos =
5863 Self::limit_i32(msgep, self.globals.syscom.msg_back_scroll_pos, msgsp);
5864 self.globals.syscom.msg_back_slider_pos = Self::linear_i32(
5865 self.globals.syscom.msg_back_scroll_pos,
5866 msgep,
5867 slider_end,
5868 msgsp,
5869 top,
5870 );
5871 self.globals.syscom.msg_back_slider_pos =
5872 Self::limit_i32(top, self.globals.syscom.msg_back_slider_pos, slider_end);
5873 }
5874
5875 fn msg_back_calc_scroll_pos_from_slider(&mut self, layout: &MsgBackLayout) {
5876 let Some((msgep, msgsp)) = self.msg_back_scroll_limits(layout) else {
5877 self.globals.syscom.msg_back_scroll_pos = 0;
5878 return;
5879 };
5880 let (_x, top, bottom) = self.msg_back_slider_track();
5881 let slider_h = self.msg_back_slider_size_i32().1.max(0);
5882 let slider_end = bottom.saturating_sub(slider_h);
5883 self.globals.syscom.msg_back_slider_pos =
5884 Self::limit_i32(top, self.globals.syscom.msg_back_slider_pos, slider_end);
5885 self.globals.syscom.msg_back_scroll_pos = Self::linear_i32(
5886 self.globals.syscom.msg_back_slider_pos,
5887 top,
5888 msgsp,
5889 slider_end,
5890 msgep,
5891 );
5892 self.globals.syscom.msg_back_scroll_pos =
5893 Self::limit_i32(msgep, self.globals.syscom.msg_back_scroll_pos, msgsp);
5894 }
5895
5896 fn msg_back_calc_scroll_pos_from_target(&mut self, layout: &MsgBackLayout) {
5897 if layout.entries.is_empty() {
5898 self.globals.syscom.msg_back_target_no = -1;
5899 self.globals.syscom.msg_back_scroll_pos = 0;
5900 return;
5901 }
5902 let target_no = self.globals.syscom.msg_back_target_no;
5903 let entry = layout
5904 .entries
5905 .iter()
5906 .find(|entry| entry.history_index as isize == target_no)
5907 .unwrap_or_else(|| layout.entries.last().expect("layout is not empty"));
5908 self.globals.syscom.msg_back_target_no = entry.history_index as isize;
5909 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
5910 let wind_height = window_size.1.max(1) as i32;
5911 self.globals.syscom.msg_back_scroll_pos =
5912 wind_height / 2 - (entry.total_pos + entry.height / 2);
5913 }
5914
5915 fn msg_back_update_pos_from_scroll(&mut self, layout: &MsgBackLayout) {
5916 self.msg_back_calc_target_no_from_scroll(layout);
5917 self.msg_back_calc_slider_pos_from_scroll(layout);
5918 }
5919
5920 fn msg_back_update_pos_from_slider(&mut self, layout: &MsgBackLayout) {
5921 self.msg_back_calc_scroll_pos_from_slider(layout);
5922 self.msg_back_calc_target_no_from_scroll(layout);
5923 }
5924
5925 fn msg_back_update_pos_from_target(&mut self, layout: &MsgBackLayout) {
5926 self.msg_back_calc_scroll_pos_from_target(layout);
5927 self.msg_back_calc_slider_pos_from_scroll(layout);
5928 }
5929
5930 fn msg_back_target_up(&mut self) {
5931 let layout = self.build_msg_back_layout();
5932 if layout.entries.is_empty() {
5933 return;
5934 }
5935 let current = self.globals.syscom.msg_back_target_no;
5936 let pos = layout
5937 .entries
5938 .iter()
5939 .position(|entry| entry.history_index as isize == current)
5940 .unwrap_or_else(|| layout.entries.len().saturating_sub(1));
5941 let next_pos = pos.saturating_sub(1);
5942 self.globals.syscom.msg_back_target_no = layout.entries[next_pos].history_index as isize;
5943 self.msg_back_update_pos_from_target(&layout);
5944 }
5945
5946 fn msg_back_target_down(&mut self) {
5947 let layout = self.build_msg_back_layout();
5948 if layout.entries.is_empty() {
5949 return;
5950 }
5951 let current = self.globals.syscom.msg_back_target_no;
5952 let pos = layout
5953 .entries
5954 .iter()
5955 .position(|entry| entry.history_index as isize == current)
5956 .unwrap_or_else(|| layout.entries.len().saturating_sub(1));
5957 let next_pos = (pos + 1).min(layout.entries.len() - 1);
5958 self.globals.syscom.msg_back_target_no = layout.entries[next_pos].history_index as isize;
5959 self.msg_back_update_pos_from_target(&layout);
5960 }
5961
5962 fn msg_back_window_contains(&self, x: i32, y: i32) -> bool {
5963 let window_pos = self.gameexe_pair_default("MSGBK.WINDOW_POS", (10, 10));
5964 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
5965 let left = window_pos.0 as i32;
5966 let top = window_pos.1 as i32;
5967 let right = left.saturating_add(window_size.0.max(1) as i32);
5968 let bottom = top.saturating_add(window_size.1.max(1) as i32);
5969 left <= x && x < right && top <= y && y < bottom
5970 }
5971
5972 fn msg_back_initialize_open_state(&mut self, layout: &MsgBackLayout) {
5973 let (_x, _top, bottom) = self.msg_back_slider_track();
5974 let slider_h = self.msg_back_slider_size_i32().1.max(0);
5975 self.globals.syscom.msg_back_msg_total_height = layout.total_height;
5976
5977 self.globals.syscom.msg_back_slider_pos = bottom.saturating_sub(slider_h);
5982 self.msg_back_update_pos_from_slider(layout);
5983 self.msg_back_update_pos_from_scroll(layout);
5984 self.globals.syscom.msg_back_target_no = if layout.entries.is_empty() {
5985 -1
5986 } else if let Some(st) = self.msg_back_state() {
5987 if layout
5988 .entries
5989 .iter()
5990 .any(|entry| entry.history_index == st.history_last_pos)
5991 {
5992 st.history_last_pos as isize
5993 } else {
5994 layout
5995 .entries
5996 .last()
5997 .map(|entry| entry.history_index as isize)
5998 .unwrap_or(-1)
5999 }
6000 } else {
6001 layout
6002 .entries
6003 .last()
6004 .map(|entry| entry.history_index as isize)
6005 .unwrap_or(-1)
6006 };
6007 self.globals.syscom.msg_back_slider_dragging = false;
6008 self.globals.syscom.msg_back_content_dragging = false;
6009 self.globals.syscom.msg_back_proc_initialized = true;
6010 }
6011
6012 fn open_msg_back_proc(&mut self) {
6013 if !self.msg_back_is_enable() {
6014 return;
6015 }
6016 self.globals.syscom.read_skip.onoff = false;
6017 self.globals.syscom.msg_back_open = true;
6018 self.globals.syscom.pending_proc = Some(globals::SyscomPendingProc {
6019 kind: globals::SyscomPendingProcKind::MsgBack,
6020 warning: false,
6021 se_play: false,
6022 fade_out: false,
6023 leave_msgbk: false,
6024 save_id: 0,
6025 });
6026 let layout = self.build_msg_back_layout();
6027 self.msg_back_initialize_open_state(&layout);
6028 }
6029
6030 fn close_msg_back_proc(&mut self) {
6031 self.globals.syscom.msg_back_open = false;
6032 self.globals.syscom.msg_back_slider_dragging = false;
6033 self.globals.syscom.msg_back_content_dragging = false;
6034 self.globals.syscom.msg_back_proc_initialized = false;
6035 self.ui.set_msg_back_projection(None);
6036 self.ui.set_sys_overlay(false, String::new());
6037 }
6038
6039 fn replay_msg_back_koe(&mut self, history_index: usize) {
6040 let Some(entry) = self
6041 .globals
6042 .msgbk_forms
6043 .get_mut(&self.ids.form_global_msgbk)
6044 .and_then(|state| state.history.get_mut(history_index))
6045 else {
6046 return;
6047 };
6048 if entry.koe_no_list.is_empty() {
6049 return;
6050 }
6051 let index = usize::try_from(entry.koe_play_no)
6053 .ok()
6054 .filter(|index| *index < entry.koe_no_list.len())
6055 .unwrap_or(0);
6056 let koe_no = entry.koe_no_list[index];
6057 let chara_no = entry.chr_no_list.get(index).copied().unwrap_or(-1);
6058 entry.koe_play_no = index as i64 + 1;
6059
6060 forms::global::remember_global_koe(self, koe_no, chara_no, true);
6063 let jitan_rate = self.koe_jitan_rate(None, true);
6064 if let Err(err) = self.koe.play_koe_no_with_rate(
6065 &mut self.audio,
6066 koe_no,
6067 &self.globals.append_dir,
6068 jitan_rate,
6069 ) {
6070 log::error!("MSGBK voice replay failed koe_no={koe_no} chara_no={chara_no}: {err:#}");
6071 }
6072 }
6073
6074 fn handle_msg_back_key(&mut self, k: input::VmKey) -> bool {
6075 if !self.globals.syscom.msg_back_open {
6076 return false;
6077 }
6078 match k {
6079 input::VmKey::Escape | input::VmKey::Enter | input::VmKey::Space => {
6080 self.close_msg_back_proc();
6081 }
6082 input::VmKey::ArrowUp | input::VmKey::ArrowLeft => self.msg_back_target_up(),
6083 input::VmKey::ArrowDown | input::VmKey::ArrowRight => self.msg_back_target_down(),
6084 input::VmKey::F(5) => {
6085 let layout = self.build_msg_back_layout();
6086 if let Some(entry) = layout.entries.first() {
6087 self.globals.syscom.msg_back_target_no = entry.history_index as isize;
6088 self.msg_back_update_pos_from_target(&layout);
6089 }
6090 }
6091 input::VmKey::F(6) => {
6092 let layout = self.build_msg_back_layout();
6093 if let Some(entry) = layout.entries.last() {
6094 self.globals.syscom.msg_back_target_no = entry.history_index as isize;
6095 self.msg_back_update_pos_from_target(&layout);
6096 }
6097 }
6098 _ => {}
6099 }
6100 true
6101 }
6102
6103 fn handle_msg_back_mouse_down(&mut self, b: input::VmMouseButton) -> bool {
6104 if !self.globals.syscom.msg_back_open {
6105 return false;
6106 }
6107 match b {
6108 input::VmMouseButton::Right => {
6109 self.close_msg_back_proc();
6110 }
6111 input::VmMouseButton::Left => {
6112 match self
6113 .ui
6114 .msg_back_hit_action(self.input.mouse_x, self.input.mouse_y)
6115 {
6116 Some(ui::MsgBackHitAction::Close) => self.close_msg_back_proc(),
6117 Some(ui::MsgBackHitAction::Up) => self.msg_back_target_up(),
6118 Some(ui::MsgBackHitAction::Down) => self.msg_back_target_down(),
6119 Some(ui::MsgBackHitAction::ReplayKoe(index)) => self.replay_msg_back_koe(index),
6120 Some(ui::MsgBackHitAction::Slider) => {
6121 self.globals.syscom.msg_back_slider_dragging = true;
6122 self.globals.syscom.msg_back_slider_drag_start_mouse = self.input.mouse_y;
6123 self.globals.syscom.msg_back_slider_drag_start_pos =
6124 self.globals.syscom.msg_back_slider_pos;
6125 }
6126 None => {
6127 if self.msg_back_window_contains(self.input.mouse_x, self.input.mouse_y) {
6128 self.globals.syscom.msg_back_content_dragging = true;
6129 self.globals.syscom.msg_back_content_drag_start_mouse =
6130 self.input.mouse_y;
6131 self.globals.syscom.msg_back_content_drag_start_scroll_pos =
6132 self.globals.syscom.msg_back_scroll_pos;
6133 }
6134 }
6135 }
6136 }
6137 _ => {}
6138 }
6139 true
6140 }
6141
6142 fn handle_msg_back_mouse_up(&mut self, b: input::VmMouseButton) -> bool {
6143 if !self.globals.syscom.msg_back_open {
6144 return false;
6145 }
6146 if matches!(b, input::VmMouseButton::Left) {
6147 self.globals.syscom.msg_back_slider_dragging = false;
6148 self.globals.syscom.msg_back_content_dragging = false;
6149 }
6150 true
6151 }
6152
6153 fn handle_msg_back_mouse_move(&mut self) -> bool {
6154 if !self.globals.syscom.msg_back_open {
6155 return false;
6156 }
6157 if self.globals.syscom.msg_back_slider_dragging {
6158 let layout = self.build_msg_back_layout();
6159 self.globals.syscom.msg_back_slider_pos = self
6160 .globals
6161 .syscom
6162 .msg_back_slider_drag_start_pos
6163 .saturating_add(
6164 self.input.mouse_y - self.globals.syscom.msg_back_slider_drag_start_mouse,
6165 );
6166 self.msg_back_update_pos_from_slider(&layout);
6167 return true;
6168 }
6169 if self.globals.syscom.msg_back_content_dragging {
6170 let layout = self.build_msg_back_layout();
6171 self.globals.syscom.msg_back_scroll_pos = self
6172 .globals
6173 .syscom
6174 .msg_back_content_drag_start_scroll_pos
6175 .saturating_sub(
6176 self.globals.syscom.msg_back_content_drag_start_mouse - self.input.mouse_y,
6177 );
6178 self.msg_back_update_pos_from_scroll(&layout);
6179 return true;
6180 }
6181
6182 let layout = self.build_msg_back_layout();
6183 self.globals.syscom.msg_back_mouse_target_no = -1;
6184 let window_pos = self.gameexe_pair_default("MSGBK.WINDOW_POS", (10, 10));
6185 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
6186 let disp_margin = self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20));
6187 let local_x = self.input.mouse_x.saturating_sub(window_pos.0 as i32);
6188 let local_y = self.input.mouse_y.saturating_sub(window_pos.1 as i32);
6189 let in_display_rect = local_x >= disp_margin.0 as i32
6190 && local_x < window_size.0.max(1) as i32 - disp_margin.2 as i32
6191 && local_y >= disp_margin.1 as i32
6192 && local_y < window_size.1.max(1) as i32 - disp_margin.3 as i32;
6193 if in_display_rect {
6194 for entry in layout.entries.iter() {
6195 let top = entry
6196 .total_pos
6197 .saturating_add(self.globals.syscom.msg_back_scroll_pos);
6198 let bottom = top.saturating_add(entry.height);
6199 if top <= local_y && local_y < bottom {
6200 self.globals.syscom.msg_back_mouse_target_no = entry.history_index as isize;
6201 break;
6202 }
6203 }
6204 }
6205 false
6206 }
6207
6208 fn msg_back_build_visible_text(&self, layout: &MsgBackLayout) -> (String, i32) {
6209 if layout.entries.is_empty() {
6210 return (
6211 String::new(),
6212 self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20))
6213 .1 as i32,
6214 );
6215 }
6216 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
6217 let disp_margin = self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20));
6218 let clip_top = disp_margin.1 as i32;
6219 let clip_bottom = window_size.1.max(1) as i32 - disp_margin.3 as i32;
6220 let scroll = self.globals.syscom.msg_back_scroll_pos;
6221 let line_step = self.msg_back_line_step();
6222 let mut first_idx = None;
6223 let mut last_idx = None;
6224 for (i, entry) in layout.entries.iter().enumerate() {
6225 let top = entry.total_pos.saturating_add(scroll);
6226 let bottom = top.saturating_add(entry.height);
6227 if bottom > clip_top && top < clip_bottom {
6228 if first_idx.is_none() {
6229 first_idx = Some(i);
6230 }
6231 last_idx = Some(i);
6232 }
6233 }
6234 let Some(first) = first_idx else {
6235 let target_no = self.globals.syscom.msg_back_target_no;
6236 let entry = layout
6237 .entries
6238 .iter()
6239 .find(|entry| entry.history_index as isize == target_no)
6240 .unwrap_or_else(|| layout.entries.last().expect("layout is not empty"));
6241 return (entry.text.clone(), entry.total_pos.saturating_add(scroll));
6242 };
6243 let last = last_idx.unwrap_or(first);
6244 let mut text = String::new();
6245 for i in first..=last {
6246 let entry = &layout.entries[i];
6247 if entry.text.is_empty() {
6248 continue;
6249 }
6250 if !text.is_empty() {
6251 let prev = &layout.entries[i - 1];
6252 let gap = entry.total_pos - (prev.total_pos + prev.height);
6253 let blank_lines = (gap / line_step).max(0) as usize;
6254 for _ in 0..blank_lines {
6255 text.push('\n');
6256 }
6257 }
6258 text.push_str(&entry.text);
6259 if !text.ends_with('\n') {
6260 text.push('\n');
6261 }
6262 }
6263 (text, layout.entries[first].total_pos.saturating_add(scroll))
6264 }
6265
6266 fn build_msg_back_projection(&mut self) -> Option<ui::MsgBackUiProjection> {
6267 if !self.globals.syscom.msg_back_open {
6268 return None;
6269 }
6270 let layout = self.build_msg_back_layout();
6271 self.globals.syscom.msg_back_msg_total_height = layout.total_height;
6272 if !self.globals.syscom.msg_back_proc_initialized {
6273 self.msg_back_initialize_open_state(&layout);
6274 } else {
6275 self.msg_back_update_pos_from_scroll(&layout);
6276 }
6277
6278 let window_pos = self.gameexe_pair_default("MSGBK.WINDOW_POS", (10, 10));
6279 let window_size = self.gameexe_pair_default("MSGBK.WINDOW_SIZE", (780, 580));
6280 let disp_margin = self.gameexe_rect_default("MSGBK.DISP_MARGIN", (20, 20, 20, 20));
6281 let filter_margin = self.gameexe_rect_default("MSGBK.FILTER_MARGIN", (0, 0, 0, 0));
6282 let filter_rgba = self.gameexe_rgba_default("MSGBK.FILTER_COLOR", (0, 0, 0, 0));
6283 let filter_config_rgba = self.syscom_filter_config_rgba();
6284 let moji_space = self.gameexe_pair_default("MSGBK.MOJI_SPACE", (-1, 10));
6285 let moji_size = self.gameexe_i64_default("MSGBK.MOJI_SIZE", 24).max(1);
6286 let msg_pos = self.gameexe_i64_default("MSGBK.MESSAGE_POS", 30) as i32;
6287 let order = self.gameexe_i64_default("MSGBK.ORDER", 10000) as i32;
6288 let scroll = self.globals.syscom.msg_back_scroll_pos;
6289 let (dl, dt, dr, db) = disp_margin;
6290 let clip_top = dt as i32;
6291 let clip_bottom = window_size.1.max(1) as i32 - db as i32;
6292 let moji_cnt = self.gameexe_pair_default("MSGBK.MOJI_CNT", (20, 15));
6293 let text_width =
6294 Self::msg_back_text_area_width(moji_cnt, moji_size as i32, moji_space) as u32;
6295 let font_shadow_mode = self.effective_font_shadow_mode();
6296 let (font_shadow, font_fuchi) =
6297 crate::text_render::font_shadow_mode_flags(font_shadow_mode);
6298 let font_bold = self.effective_font_bold();
6299 let base_style = TextStyle {
6300 color: self.gameexe_color(self.tables.mwnd_render.moji_color),
6301 shadow_color: self.gameexe_color(self.tables.mwnd_render.shadow_color),
6302 fuchi_color: self.gameexe_color(self.tables.mwnd_render.fuchi_color),
6303 shadow_mode: font_shadow_mode,
6304 shadow: font_shadow,
6305 fuchi: font_fuchi,
6306 bold: font_bold,
6307 };
6308 let active_style = TextStyle {
6309 color: self.gameexe_color(self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_COLOR", 7)),
6310 shadow_color: self
6311 .gameexe_color(self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_SHADOW_COLOR", 0)),
6312 fuchi_color: self
6313 .gameexe_color(self.gameexe_i64_default("MSGBK.ACTIVE_MOJI_FUCHI_COLOR", 0)),
6314 shadow_mode: font_shadow_mode,
6315 shadow: font_shadow,
6316 fuchi: font_fuchi,
6317 bold: font_bold,
6318 };
6319 let debug_style = TextStyle {
6320 color: self.gameexe_color(self.gameexe_i64_default("MSGBK.DEBUG_MOJI_COLOR", 5)),
6321 shadow_color: self
6322 .gameexe_color(self.gameexe_i64_default("MSGBK.DEBUG_MOJI_SHADOW_COLOR", 0)),
6323 fuchi_color: self
6324 .gameexe_color(self.gameexe_i64_default("MSGBK.DEBUG_MOJI_FUCHI_COLOR", 0)),
6325 shadow_mode: font_shadow_mode,
6326 shadow: font_shadow,
6327 fuchi: font_fuchi,
6328 bold: font_bold,
6329 };
6330
6331 let koe_btn_file = self.gameexe_string("MSGBK_ITEM.KOE_BTN.FILE");
6332 let koe_btn_pos = self.msg_back_button_pos("MSGBK_ITEM.KOE_BTN.POS", (-20, -10));
6333 let load_btn_file = self.gameexe_string("MSGBK_ITEM.LOAD_BTN.FILE");
6334 let load_btn_pos = self.msg_back_button_pos("MSGBK_ITEM.LOAD_BTN.POS", (-20, 0));
6335
6336 let mut text_entries = Vec::new();
6337 let mut koe_buttons = Vec::new();
6338 let mut load_buttons = Vec::new();
6339 let separators = layout
6340 .separators
6341 .iter()
6342 .filter_map(|sep| {
6343 if sep.file.is_none() || sep.height <= 0 {
6344 return None;
6345 }
6346 let local_y = sep.total_pos.saturating_add(scroll);
6347 let bottom = local_y.saturating_add(sep.height);
6348 if bottom > clip_top && local_y < clip_bottom {
6349 Some(ui::MsgBackImageProjection {
6350 file: sep.file.clone(),
6351 x: 0,
6352 y: local_y,
6353 })
6354 } else {
6355 None
6356 }
6357 })
6358 .collect::<Vec<_>>();
6359
6360 if let Some(st) = self.msg_back_state() {
6361 for layout_entry in layout.entries.iter() {
6362 let Some(entry) = st.history.get(layout_entry.history_index) else {
6363 continue;
6364 };
6365 let local_y = layout_entry.total_pos.saturating_add(scroll);
6366 let bottom = local_y.saturating_add(layout_entry.height);
6367 let is_in_rect = bottom > clip_top && local_y < clip_bottom;
6368 if is_in_rect && !layout_entry.text.is_empty() {
6369 let mut style = base_style;
6370 if self.globals.system.debug_flag
6371 && self.globals.syscom.msg_back_target_no
6372 == layout_entry.history_index as isize
6373 {
6374 style = debug_style;
6375 }
6376 if self.globals.syscom.msg_back_mouse_target_no
6377 == layout_entry.history_index as isize
6378 {
6379 style = active_style;
6380 }
6381 text_entries.push(ui::MsgBackTextProjection {
6382 history_index: layout_entry.history_index,
6383 text: layout_entry.text.clone(),
6384 x: msg_pos,
6385 y: local_y,
6386 width: text_width,
6387 height: layout_entry.height.max(1) as u32,
6388 style,
6389 });
6390 }
6391 if is_in_rect && entry.pct_flag {
6392 koe_buttons.push(ui::MsgBackEntryButtonProjection {
6393 history_index: layout_entry.history_index,
6394 file: Some(entry.msg_str.clone()),
6395 x: msg_pos.saturating_add(entry.pct_pos_x),
6396 y: local_y.saturating_add(entry.pct_pos_y),
6397 });
6398 } else if is_in_rect && !entry.koe_no_list.is_empty() {
6399 koe_buttons.push(ui::MsgBackEntryButtonProjection {
6400 history_index: layout_entry.history_index,
6401 file: koe_btn_file.clone(),
6402 x: msg_pos.saturating_add(koe_btn_pos.0),
6403 y: local_y.saturating_add(koe_btn_pos.1),
6404 });
6405 }
6406 if is_in_rect && entry.save_id_check_flag {
6407 load_buttons.push(ui::MsgBackEntryButtonProjection {
6408 history_index: layout_entry.history_index,
6409 file: load_btn_file.clone(),
6410 x: msg_pos.saturating_add(load_btn_pos.0),
6411 y: local_y.saturating_add(load_btn_pos.1),
6412 });
6413 }
6414 }
6415 }
6416
6417 let (slider_x, _slider_top, _slider_bottom) = self.msg_back_slider_track();
6418 if env_is_set!("SG_MSGBK_TRACE") {
6419 eprintln!(
6420 "[SG_MSGBK_TRACE][PROJECTION] entries={} separators={} text={} koe={} load={} total_height={} scroll={} slider={} target={} mouse_target={}",
6421 layout.entries.len(),
6422 layout.separators.len(),
6423 text_entries.len(),
6424 koe_buttons.len(),
6425 load_buttons.len(),
6426 layout.total_height,
6427 self.globals.syscom.msg_back_scroll_pos,
6428 self.globals.syscom.msg_back_slider_pos,
6429 self.globals.syscom.msg_back_target_no,
6430 self.globals.syscom.msg_back_mouse_target_no
6431 );
6432 for entry in &layout.entries {
6433 eprintln!(
6434 "[SG_MSGBK_TRACE][LAYOUT] history_index={} total_pos={} height={} has_text={}",
6435 entry.history_index,
6436 entry.total_pos,
6437 entry.height,
6438 !entry.text.is_empty()
6439 );
6440 }
6441 for sep in &layout.separators {
6442 eprintln!(
6443 "[SG_MSGBK_TRACE][SEPARATOR] file={:?} total_pos={} height={}",
6444 sep.file, sep.total_pos, sep.height
6445 );
6446 }
6447 }
6448 Some(ui::MsgBackUiProjection {
6449 window_x: window_pos.0 as i32,
6450 window_y: window_pos.1 as i32,
6451 window_w: window_size.0.max(1) as u32,
6452 window_h: window_size.1.max(1) as u32,
6453 disp_margin,
6454 msg_pos,
6455 moji_size,
6456 moji_space: Some(moji_space),
6457 order,
6458 filter_layer_rep: self.tables.mwnd_render.filter_layer_rep as i32,
6459 waku_layer_rep: self.tables.mwnd_render.waku_layer_rep as i32,
6460 moji_layer_rep: self.tables.mwnd_render.moji_layer_rep as i32,
6461 waku_file: self.gameexe_string("MSGBK.BACK_FILE"),
6462 filter_file: self.gameexe_string("MSGBK.FILTER_FILE"),
6463 filter_margin,
6464 filter_rgba,
6465 filter_config_rgba,
6466 text_entries,
6467 separators,
6468 koe_buttons,
6469 load_buttons,
6470 close_btn_file: self.gameexe_string("MSGBK_ITEM.CLOSE_BTN.FILE"),
6471 close_btn_pos: self.msg_back_button_pos("MSGBK_ITEM.CLOSE_BTN.POS", (0, 0)),
6472 msg_up_btn_file: self.gameexe_string("MSGBK_ITEM.MSG_UP_BTN.FILE"),
6473 msg_up_btn_pos: self.msg_back_button_pos("MSGBK_ITEM.MSG_UP_BTN.POS", (0, 0)),
6474 msg_down_btn_file: self.gameexe_string("MSGBK_ITEM.MSG_DOWN_BTN.FILE"),
6475 msg_down_btn_pos: self.msg_back_button_pos("MSGBK_ITEM.MSG_DOWN_BTN.POS", (0, 0)),
6476 slider_file: self.gameexe_string("MSGBK_ITEM.SLIDER.FILE"),
6477 slider_rect: (
6478 slider_x,
6479 self.msg_back_slider_track().1,
6480 slider_x,
6481 self.msg_back_slider_track().2,
6482 ),
6483 slider_pos: (slider_x, self.globals.syscom.msg_back_slider_pos),
6484 ex_btn_files: [
6485 self.gameexe_string("MSGBK_ITEM.EX_BTN_1.FILE"),
6486 self.gameexe_string("MSGBK_ITEM.EX_BTN_2.FILE"),
6487 self.gameexe_string("MSGBK_ITEM.EX_BTN_3.FILE"),
6488 self.gameexe_string("MSGBK_ITEM.EX_BTN_4.FILE"),
6489 ],
6490 ex_btn_pos: [
6491 self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_1.POS", (0, 0)),
6492 self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_2.POS", (0, 0)),
6493 self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_3.POS", (0, 0)),
6494 self.msg_back_button_pos("MSGBK_ITEM.EX_BTN_4.POS", (0, 0)),
6495 ],
6496 })
6497 }
6498
6499 fn sync_syscom_menu_ui(&mut self) {
6500 self.ui.set_msg_back_projection(None);
6501 self.ui.set_sys_overlay(false, String::new());
6502 if self.sync_system_messagebox_ui() {
6503 return;
6504 }
6505 if self.globals.syscom.msg_back_open {
6506 let projection = self.build_msg_back_projection();
6507 self.ui.set_msg_back_projection(projection);
6508 return;
6509 }
6510 if self.globals.syscom.menu_open {
6511 log::error!("SYSCOM menu proc is not implemented; fake Rust text menu is disabled");
6512 self.globals.syscom.menu_open = false;
6513 self.globals.syscom.menu_kind = None;
6514 self.globals.syscom.menu_result = None;
6515 }
6516 }
6517
6518 fn selbtn_choice_selectable(choice: &globals::BtnSelectChoiceState) -> bool {
6519 choice.item_type == TNM_SEL_ITEM_TYPE_ON_I64
6520 }
6521
6522 fn next_selbtn_cursor(&self, dir: i32) -> usize {
6523 let choices = &self.globals.selbtn.choices;
6524 if choices.is_empty() {
6525 return 0;
6526 }
6527 let len = choices.len() as i32;
6528 let mut idx = self.globals.selbtn.cursor.min(choices.len() - 1) as i32;
6529 for _ in 0..choices.len() {
6530 idx = (idx + dir).rem_euclid(len);
6531 if Self::selbtn_choice_selectable(&choices[idx as usize]) {
6532 return idx as usize;
6533 }
6534 }
6535 self.globals.selbtn.cursor.min(choices.len() - 1)
6536 }
6537
6538 fn selbtn_linear_limit(
6539 now: i64,
6540 start: i64,
6541 start_value: i64,
6542 end: i64,
6543 end_value: i64,
6544 ) -> i64 {
6545 if now >= end {
6546 return end_value;
6547 }
6548 if now <= start || start == end {
6549 return start_value;
6550 }
6551 (end_value - start_value).saturating_mul(now - start) / (end - start) + start_value
6552 }
6553
6554 fn selbtn_speed_up_limit(
6555 now: i64,
6556 start: i64,
6557 start_value: i64,
6558 end: i64,
6559 end_value: i64,
6560 ) -> i64 {
6561 if start == end {
6562 return end_value;
6563 }
6564 let current = if start < end {
6565 now.clamp(start, end)
6566 } else {
6567 now.clamp(end, start)
6568 };
6569 let t = (current - start) as f64;
6570 let d = (end - start) as f64;
6571 (t * t * (end_value - start_value) as f64 / (d * d) + start_value as f64) as i64
6572 }
6573
6574 fn selbtn_speed_down_limit(
6575 now: i64,
6576 start: i64,
6577 start_value: i64,
6578 end: i64,
6579 end_value: i64,
6580 ) -> i64 {
6581 if start == end {
6582 return end_value;
6583 }
6584 let current = if start < end {
6585 now.clamp(start, end)
6586 } else {
6587 now.clamp(end, start)
6588 };
6589 let t = (current - end) as f64;
6590 let d = (end - start) as f64;
6591 (-t * t * (end_value - start_value) as f64 / (d * d) + end_value as f64) as i64
6592 }
6593
6594 fn selbtn_accepts_input(&self) -> bool {
6595 let sel = &self.globals.selbtn;
6596 sel.started
6597 && sel.appear_flag
6598 && sel.open_anime_type == 0
6599 && sel.decide_anime_type == 0
6600 && sel.close_anime_type == 0
6601 && !sel.capture_now_flag
6602 }
6603
6604 fn deliver_selbtn_result(&mut self) {
6605 if self.globals.selbtn.result_delivered {
6606 return;
6607 }
6608 let result = self.globals.selbtn.result;
6609 self.globals.selbtn.result_delivered = true;
6610 self.wait.set_selbtn_result(result);
6615 }
6616
6617 fn end_selbtn_close_animation(&mut self) {
6618 self.globals.selbtn.close_anime_type = 0;
6619 self.globals.selbtn.started = false;
6620 self.globals.selbtn.appear_flag = false;
6621 self.globals.selbtn.processing_flag_0 = false;
6622 self.globals.selbtn.processing_flag_1 = false;
6623 self.globals.selbtn.processing_flag_2 = false;
6624 self.globals.selbtn.pressed_index = None;
6625 self.globals.selbtn.pressed_inside = false;
6626 if self.globals.selbtn.capture_flag {
6627 crate::runtime::forms::syscom::free_runtime_save_thumb_capture(
6628 self,
6629 crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
6630 );
6631 }
6632 self.clear_selbtn_items_from_front_stage();
6633 }
6634
6635 fn begin_selbtn_close_animation(&mut self) {
6636 let template_no = self.globals.selbtn.template_no.max(0) as usize;
6637 let tmpl = self
6638 .tables
6639 .sel_btn_templates
6640 .get(template_no)
6641 .cloned()
6642 .unwrap_or_default();
6643 let sel = &mut self.globals.selbtn;
6644 sel.appear_flag = false;
6645 sel.started = false;
6646 sel.close_anime_type = tmpl.close_anime_type.max(0);
6647 sel.close_anime_time = tmpl.close_anime_time.max(0);
6648 sel.close_anime_cur_time = 0;
6649 if sel.decide_sel_no >= 0 && tmpl.decide_anime_type >= 1 && sel.close_anime_type >= 1 {
6650 sel.close_anime_type = 1;
6653 }
6654 sel.processing_flag_1 = false;
6655 let end_immediately = sel.close_anime_type == 0;
6656 if end_immediately {
6657 self.end_selbtn_close_animation();
6658 }
6659 }
6660
6661 fn finish_selbtn(&mut self, result: i64) {
6662 if !self.selbtn_accepts_input() {
6663 return;
6664 }
6665 self.globals.selbtn.result = result;
6666 self.globals.selbtn.started = false;
6667 self.globals.selbtn.pressed_index = None;
6668 self.globals.selbtn.pressed_inside = false;
6669 self.globals.selbtn.decide_sel_no = result;
6670 self.deliver_selbtn_result();
6673 self.request_sel_point_with_result(result);
6674 self.globals.selbtn.processing_flag_2 = false;
6675 if result >= 0 {
6676 if let Some(choice) = self.globals.selbtn.choices.get(result as usize) {
6677 self.globals.syscom.system_extra_str_value = choice.text.clone();
6678 }
6679 } else {
6680 self.globals.syscom.system_extra_str_value = "(キャンセル)".to_string();
6681 }
6682
6683 let read_scene_no = self.globals.selbtn.read_flag_scene_no;
6687 let read_flag_no = self.globals.selbtn.read_flag_flag_no;
6688 let _ = self.globals.set_read_flag(read_scene_no, read_flag_no);
6689 self.globals.selbtn.read_flag_scene_no = -1;
6690 self.globals.selbtn.read_flag_flag_no = -1;
6691
6692 let template_no = self.globals.selbtn.template_no.max(0) as usize;
6693 let tmpl = self
6694 .tables
6695 .sel_btn_templates
6696 .get(template_no)
6697 .cloned()
6698 .unwrap_or_default();
6699 if result >= 0 && tmpl.decide_anime_type >= 1 {
6700 self.globals.selbtn.decide_anime_type = tmpl.decide_anime_type;
6701 self.globals.selbtn.decide_anime_time = tmpl.decide_anime_time.max(0);
6702 self.globals.selbtn.decide_anime_cur_time = 0;
6703 if self.globals.selbtn.decide_anime_time == 0 {
6704 self.globals.selbtn.decide_anime_type = 0;
6705 self.begin_selbtn_close_animation();
6706 }
6707 } else {
6708 self.begin_selbtn_close_animation();
6709 }
6710 }
6711
6712 fn update_selbtn_animation(&mut self, elapsed_ms: i64) {
6713 let elapsed_ms = elapsed_ms.max(0);
6714 let active = {
6715 let sel = &self.globals.selbtn;
6716 sel.appear_flag || sel.close_anime_type > 0 || sel.decide_anime_type > 0
6717 };
6718 if !active {
6719 return;
6720 }
6721
6722 {
6723 let sel = &mut self.globals.selbtn;
6724 sel.open_anime_cur_time = sel.open_anime_cur_time.saturating_add(elapsed_ms);
6725 sel.close_anime_cur_time = sel.close_anime_cur_time.saturating_add(elapsed_ms);
6726 sel.decide_anime_cur_time = sel.decide_anime_cur_time.saturating_add(elapsed_ms);
6727 }
6728
6729 let item_count = self
6730 .globals
6731 .stage_forms
6732 .get(&self.ids.form_global_stage)
6733 .and_then(|st| st.btnselitem_lists.get(&TNM_STAGE_FRONT_I64))
6734 .map(Vec::len)
6735 .unwrap_or(0);
6736 let template_no = self.globals.selbtn.template_no.max(0) as usize;
6737 let tmpl = self
6738 .tables
6739 .sel_btn_templates
6740 .get(template_no)
6741 .cloned()
6742 .unwrap_or_default();
6743 let stagger_y_count = if tmpl.max_y_cnt > 0 {
6744 (tmpl.max_y_cnt as usize).min(item_count)
6745 } else {
6746 item_count
6747 };
6748 let y_count = stagger_y_count.max(1);
6752
6753 if self.globals.selbtn.capture_now_flag {
6757 self.globals.selbtn.capture_now_flag = false;
6758 let scene = self.globals.selbtn.sel_start_call_scn.clone();
6759 let z_no = self.globals.selbtn.sel_start_call_z_no;
6760 if !scene.is_empty() && z_no >= 0 {
6761 self.globals
6762 .pending_button_actions
6763 .push(globals::PendingButtonAction {
6764 kind: globals::PendingButtonActionKind::UserCall {
6765 scn_name: scene,
6766 cmd_name: String::new(),
6767 z_no,
6768 },
6769 });
6770 }
6771 }
6772
6773 let stagger = (stagger_y_count as i64).saturating_mul(50);
6774 let open_stagger = if matches!(self.globals.selbtn.open_anime_type, 2..=6) {
6775 stagger
6776 } else {
6777 0
6778 };
6779 let open_end = self
6780 .globals
6781 .selbtn
6782 .open_anime_time
6783 .saturating_add(open_stagger);
6784 let decide_end = self.globals.selbtn.decide_anime_time;
6785
6786 if self.globals.selbtn.open_anime_type > 0
6787 && self.globals.selbtn.open_anime_cur_time >= open_end
6788 {
6789 self.globals.selbtn.open_anime_type = 0;
6790 if self.globals.selbtn.capture_flag {
6791 self.globals.selbtn.capture_now_flag = true;
6792 crate::runtime::forms::syscom::prepare_runtime_save_thumb_capture_with_priority(
6793 self,
6794 crate::runtime::forms::syscom::CAPTURE_PRIOR_SAVE,
6795 );
6796 }
6797 }
6798 let close_stagger = if matches!(self.globals.selbtn.close_anime_type, 2..=6) {
6799 stagger
6800 } else {
6801 0
6802 };
6803 let close_end = self
6804 .globals
6805 .selbtn
6806 .close_anime_time
6807 .saturating_add(close_stagger);
6808 if self.globals.selbtn.close_anime_type > 0
6809 && self.globals.selbtn.close_anime_cur_time >= close_end
6810 {
6811 self.end_selbtn_close_animation();
6812 return;
6813 }
6814 if self.globals.selbtn.decide_anime_type > 0
6818 && self.globals.selbtn.decide_anime_cur_time >= decide_end
6819 {
6820 self.begin_selbtn_close_animation();
6821 self.globals.selbtn.decide_anime_type = 0;
6822 }
6823
6824 let sel = self.globals.selbtn.clone();
6825 let screen_w = self.screen_w as i64;
6826 let screen_h = self.screen_h as i64;
6827 let decided_pos = if sel.decide_sel_no >= 0 {
6828 self.globals
6829 .selbtn
6830 .choices
6831 .get(sel.decide_sel_no as usize)
6832 .map(|choice| choice.pos)
6833 } else {
6834 None
6835 };
6836 if let Some(items) = self
6837 .globals
6838 .stage_forms
6839 .get_mut(&self.ids.form_global_stage)
6840 .and_then(|st| st.btnselitem_lists.get_mut(&TNM_STAGE_FRONT_I64))
6841 {
6842 for (index, item) in items.iter_mut().enumerate() {
6843 let row = index % y_count;
6844 let mut tr = 255i64;
6845 let mut x_rep = 0i64;
6846 let mut y_rep = 0i64;
6847
6848 match sel.open_anime_type {
6849 1 => {
6850 tr = Self::selbtn_linear_limit(
6851 sel.open_anime_cur_time,
6852 0,
6853 0,
6854 sel.open_anime_time,
6855 255,
6856 );
6857 }
6858 2 => {
6859 let time = sel
6860 .open_anime_cur_time
6861 .saturating_sub(50 * (y_count.saturating_sub(row)) as i64);
6862 tr = Self::selbtn_linear_limit(time, 0, 0, sel.open_anime_time, 255);
6863 y_rep = Self::selbtn_speed_down_limit(
6864 time,
6865 0,
6866 -screen_h,
6867 sel.open_anime_time,
6868 0,
6869 );
6870 }
6871 3 => {
6872 let time = sel.open_anime_cur_time.saturating_sub(50 * row as i64);
6873 tr = Self::selbtn_linear_limit(time, 0, 0, sel.open_anime_time, 255);
6874 y_rep = Self::selbtn_speed_down_limit(
6875 time,
6876 0,
6877 screen_h,
6878 sel.open_anime_time,
6879 0,
6880 );
6881 }
6882 4 => {
6883 let time = sel.open_anime_cur_time.saturating_sub(50 * row as i64);
6884 tr = Self::selbtn_linear_limit(time, 0, 0, sel.open_anime_time, 255);
6885 x_rep = Self::selbtn_speed_down_limit(
6886 time,
6887 0,
6888 -screen_w,
6889 sel.open_anime_time,
6890 0,
6891 );
6892 }
6893 5 => {
6894 let time = sel.open_anime_cur_time.saturating_sub(50 * row as i64);
6895 tr = Self::selbtn_linear_limit(time, 0, 0, sel.open_anime_time, 255);
6896 x_rep = Self::selbtn_speed_down_limit(
6897 time,
6898 0,
6899 screen_w,
6900 sel.open_anime_time,
6901 0,
6902 );
6903 }
6904 6 => {
6905 let time = sel.open_anime_cur_time.saturating_sub(50 * row as i64);
6906 tr = Self::selbtn_linear_limit(time, 0, 0, sel.open_anime_time, 255);
6907 let start = if row % 2 == 0 { screen_w } else { -screen_w };
6908 x_rep =
6909 Self::selbtn_speed_down_limit(time, 0, start, sel.open_anime_time, 0);
6910 }
6911 _ => {}
6912 }
6913
6914 if sel.decide_sel_no >= 0 && index as i64 != sel.decide_sel_no {
6915 match sel.decide_anime_type {
6916 1 => {
6917 tr = Self::selbtn_linear_limit(
6918 sel.decide_anime_cur_time,
6919 0,
6920 255,
6921 sel.decide_anime_time,
6922 0,
6923 );
6924 }
6925 2 => {
6926 if let Some((decided_x, decided_y)) = decided_pos {
6927 let (my_x, my_y) = item.pos;
6928 tr = Self::selbtn_linear_limit(
6929 sel.decide_anime_cur_time,
6930 0,
6931 255,
6932 sel.decide_anime_time,
6933 0,
6934 );
6935 x_rep = Self::selbtn_speed_down_limit(
6936 sel.decide_anime_cur_time,
6937 0,
6938 0,
6939 sel.decide_anime_time,
6940 decided_x - my_x,
6941 );
6942 y_rep = Self::selbtn_speed_down_limit(
6943 sel.decide_anime_cur_time,
6944 0,
6945 0,
6946 sel.decide_anime_time,
6947 decided_y - my_y,
6948 );
6949 }
6950 }
6951 _ => {}
6952 }
6953 }
6954
6955 if sel.close_anime_type >= 1
6956 && tmpl.decide_anime_type >= 1
6957 && sel.decide_sel_no >= 0
6958 && index as i64 != sel.decide_sel_no
6959 {
6960 tr = 0;
6961 } else {
6962 match sel.close_anime_type {
6963 1 => {
6964 tr = Self::selbtn_linear_limit(
6965 sel.close_anime_cur_time,
6966 0,
6967 255,
6968 sel.close_anime_time,
6969 0,
6970 );
6971 }
6972 2 => {
6973 let time = sel.close_anime_cur_time.saturating_sub(50 * row as i64);
6974 tr = Self::selbtn_linear_limit(time, 0, 255, sel.close_anime_time, 0);
6975 y_rep = Self::selbtn_speed_up_limit(
6976 time,
6977 0,
6978 0,
6979 sel.close_anime_time,
6980 -screen_h,
6981 );
6982 }
6983 3 => {
6984 let time = sel
6985 .close_anime_cur_time
6986 .saturating_sub(50 * (y_count.saturating_sub(row)) as i64);
6987 tr = Self::selbtn_linear_limit(time, 0, 255, sel.close_anime_time, 0);
6988 y_rep = Self::selbtn_speed_up_limit(
6989 time,
6990 0,
6991 0,
6992 sel.close_anime_time,
6993 screen_h,
6994 );
6995 }
6996 4 => {
6997 let time = sel.close_anime_cur_time.saturating_sub(50 * row as i64);
6998 tr = Self::selbtn_linear_limit(time, 0, 255, sel.close_anime_time, 0);
6999 x_rep = Self::selbtn_speed_up_limit(
7000 time,
7001 0,
7002 0,
7003 sel.close_anime_time,
7004 -screen_w,
7005 );
7006 }
7007 5 => {
7008 let time = sel.close_anime_cur_time.saturating_sub(50 * row as i64);
7009 tr = Self::selbtn_linear_limit(time, 0, 255, sel.close_anime_time, 0);
7010 x_rep = Self::selbtn_speed_up_limit(
7011 time,
7012 0,
7013 0,
7014 sel.close_anime_time,
7015 screen_w,
7016 );
7017 }
7018 6 => {
7019 let time = sel.close_anime_cur_time.saturating_sub(50 * row as i64);
7020 tr = Self::selbtn_linear_limit(time, 0, 255, sel.close_anime_time, 0);
7021 let end = if row % 2 == 0 { screen_w } else { -screen_w };
7022 x_rep =
7023 Self::selbtn_speed_up_limit(time, 0, 0, sel.close_anime_time, end);
7024 }
7025 _ => {}
7026 }
7027 }
7028
7029 item.animation_offset = (x_rep, y_rep);
7030 item.animation_tr = Some(tr.clamp(0, 255));
7031 }
7032 }
7033 }
7034
7035 fn sync_selbtn_item_selection(&mut self) {
7036 if let Some(st) = self
7037 .globals
7038 .stage_forms
7039 .get_mut(&self.ids.form_global_stage)
7040 && let Some(items) = st.btnselitem_lists.get_mut(&TNM_STAGE_FRONT_I64)
7041 {
7042 for (idx, item) in items.iter_mut().enumerate() {
7043 item.selected = idx == self.globals.selbtn.cursor;
7044 let selectable = item.item_type == TNM_SEL_ITEM_TYPE_ON_I64;
7045 item.button_state = if item.item_type == TNM_SEL_ITEM_TYPE_READ_I64 {
7046 TNM_BTN_STATE_DISABLE
7047 } else if self.globals.selbtn.pressed_index == Some(idx)
7048 && self.globals.selbtn.pressed_inside
7049 && selectable
7050 {
7051 TNM_BTN_STATE_PUSH
7052 } else if item.selected && selectable {
7053 TNM_BTN_STATE_HIT
7054 } else {
7055 TNM_BTN_STATE_NORMAL
7056 };
7057 }
7058 }
7059 }
7060
7061 fn hide_selbtn_object_backing(&mut self, obj: &globals::ObjectState) {
7062 match &obj.backend {
7063 globals::ObjectBackend::Gfx => {
7064 if let Some(slot) = obj.nested_runtime_slot {
7065 let _ = self.gfx.object_clear(
7066 &mut self.images,
7067 &mut self.layers,
7068 TNM_STAGE_FRONT_I64,
7069 slot as i64,
7070 );
7071 }
7072 }
7073 globals::ObjectBackend::None => {}
7074 backend => {
7075 for (layer_id, sprite_id) in layer_backed_object_sprite_bindings(backend) {
7076 if let Some(sprite) = self
7077 .layers
7078 .layer_mut(layer_id)
7079 .and_then(|layer| layer.sprite_mut(sprite_id))
7080 {
7081 sprite.visible = false;
7082 sprite.image_id = None;
7083 }
7084 }
7085 }
7086 }
7087 for child in &obj.runtime.child_objects {
7088 self.hide_selbtn_object_backing(child);
7089 }
7090 }
7091
7092 fn clear_selbtn_items_from_front_stage(&mut self) {
7093 let old_items = self
7094 .globals
7095 .stage_forms
7096 .get(&self.ids.form_global_stage)
7097 .and_then(|st| st.btnselitem_lists.get(&TNM_STAGE_FRONT_I64))
7098 .cloned()
7099 .unwrap_or_default();
7100 for item in &old_items {
7101 for obj in item.generated_objects.iter().chain(item.object_list.iter()) {
7102 self.hide_selbtn_object_backing(obj);
7103 }
7104 }
7105 if let Some(st) = self
7106 .globals
7107 .stage_forms
7108 .get_mut(&self.ids.form_global_stage)
7109 {
7110 st.btnselitem_lists.remove(&TNM_STAGE_FRONT_I64);
7111 }
7112 }
7113
7114 fn handle_selbtn_key(&mut self, k: input::VmKey) -> bool {
7115 if !self.globals.selbtn.started {
7116 return false;
7117 }
7118 if !self.selbtn_accepts_input() {
7119 return true;
7120 }
7121 match k {
7122 input::VmKey::ArrowUp => {
7123 self.globals.selbtn.cursor = self.next_selbtn_cursor(-1);
7124 self.sync_selbtn_item_selection();
7125 true
7126 }
7127 input::VmKey::ArrowDown => {
7128 self.globals.selbtn.cursor = self.next_selbtn_cursor(1);
7129 self.sync_selbtn_item_selection();
7130 true
7131 }
7132 input::VmKey::Enter => {
7133 let idx = self.globals.selbtn.cursor;
7134 if self
7135 .globals
7136 .selbtn
7137 .choices
7138 .get(idx)
7139 .is_some_and(Self::selbtn_choice_selectable)
7140 {
7141 self.finish_selbtn(idx as i64);
7142 }
7143 true
7144 }
7145 input::VmKey::Escape if self.globals.selbtn.cancel_enable => {
7146 self.finish_selbtn(-1);
7147 true
7148 }
7149 _ => true,
7150 }
7151 }
7152
7153 fn handle_selbtn_mouse_down(&mut self, b: input::VmMouseButton) -> bool {
7154 if !self.globals.selbtn.started {
7155 return false;
7156 }
7157 if !self.selbtn_accepts_input() {
7158 self.input.on_mouse_down(b);
7159 return true;
7160 }
7161 self.input.on_mouse_down(b);
7162 match b {
7163 input::VmMouseButton::Left => {
7164 if let Some(idx) = self.selbtn_hit_index(self.input.mouse_x, self.input.mouse_y) {
7165 self.globals.selbtn.cursor = idx;
7166 self.globals.selbtn.pressed_index = Some(idx);
7167 self.globals.selbtn.pressed_inside = true;
7168 self.sync_selbtn_item_selection();
7169 }
7170 true
7171 }
7172 input::VmMouseButton::Right => true,
7173 _ => true,
7174 }
7175 }
7176
7177 fn handle_selbtn_mouse_up(&mut self, b: input::VmMouseButton) -> bool {
7178 let selection_active = self.globals.selbtn.started
7179 || self.globals.selbtn.pressed_index.is_some()
7180 || self.globals.selbtn.decide_anime_type > 0
7181 || self.globals.selbtn.close_anime_type > 0;
7182 if !selection_active {
7183 return false;
7184 }
7185 match b {
7186 input::VmMouseButton::Left => {
7187 let pressed = self.globals.selbtn.pressed_index.take();
7188 let inside = std::mem::take(&mut self.globals.selbtn.pressed_inside);
7189 let released = self.selbtn_hit_index(self.input.mouse_x, self.input.mouse_y);
7190 self.sync_selbtn_item_selection();
7191 if self.selbtn_accepts_input() && inside && pressed.is_some() && pressed == released
7192 {
7193 self.finish_selbtn(pressed.unwrap_or(0) as i64);
7194 }
7195 true
7196 }
7197 input::VmMouseButton::Right => {
7198 if self.selbtn_accepts_input()
7199 && self.globals.selbtn.cancel_enable
7200 && self.input.vk_down_up_stock(0x02)
7201 {
7202 self.finish_selbtn(-1);
7203 }
7204 true
7205 }
7206 _ => true,
7207 }
7208 }
7209
7210 fn selbtn_hit_index(&self, mx: i32, my: i32) -> Option<usize> {
7211 if !self.selbtn_accepts_input() || self.globals.selbtn.choices.is_empty() {
7212 return None;
7213 }
7214 for (idx, choice) in self.globals.selbtn.choices.iter().enumerate().rev() {
7215 if !Self::selbtn_choice_selectable(choice) {
7216 continue;
7217 }
7218 let (x, y) = choice.pos;
7219 let (w, h) = choice.size;
7220 let x0 = x as i32;
7221 let y0 = y as i32;
7222 let x1 = x.saturating_add(w.max(1)) as i32;
7223 let y1 = y.saturating_add(h.max(1)) as i32;
7224 if mx >= x0 && mx < x1 && my >= y0 && my < y1 {
7225 return Some(idx);
7226 }
7227 }
7228 None
7229 }
7230
7231 fn handle_mwnd_selection_key(&mut self, k: input::VmKey) -> bool {
7232 let Some((form_id, stage_idx, mwnd_idx)) = self.globals.focused_stage_mwnd else {
7233 return false;
7234 };
7235 let trace_scene = self
7236 .current_scene_name
7237 .as_deref()
7238 .unwrap_or("<none>")
7239 .to_string();
7240 let trace_scene_no = self
7241 .current_scene_no
7242 .map(|v| v.to_string())
7243 .unwrap_or_else(|| "-".to_string());
7244 let trace_line = self.current_line_no;
7245 let mut clear_focus = false;
7246 let mut handled = false;
7247 let mut close_anim: Option<(i64, i64)> = None;
7248 let mut result_to_push: Option<i64> = None;
7249 if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
7250 if let Some(list) = st.mwnd_lists.get_mut(&stage_idx) {
7251 if let Some(m) = list.get_mut(mwnd_idx) {
7252 let close_time = m.close_anime_time;
7253 let close_type = m.close_anime_type;
7254 let mut close_after = false;
7255 let mut clear_selection = false;
7256 if let Some(sel) = m.selection.as_mut() {
7257 handled = match k {
7258 input::VmKey::ArrowUp => {
7259 if !sel.choices.is_empty() {
7260 sel.cursor = if sel.cursor == 0 {
7261 sel.choices.len() - 1
7262 } else {
7263 sel.cursor - 1
7264 };
7265 }
7266 true
7267 }
7268 input::VmKey::ArrowDown => {
7269 if !sel.choices.is_empty() {
7270 sel.cursor = (sel.cursor + 1) % sel.choices.len();
7271 }
7272 true
7273 }
7274 input::VmKey::Enter => {
7275 sel.result = (sel.cursor as i64) + 1;
7276 result_to_push = Some(sel.result);
7277 close_after = sel.close_mwnd;
7278 clear_selection = true;
7279 clear_focus = true;
7280 true
7281 }
7282 input::VmKey::Escape if sel.cancel_enable => {
7283 sel.result = -1;
7284 result_to_push = Some(sel.result);
7285 close_after = sel.close_mwnd;
7286 clear_selection = true;
7287 clear_focus = true;
7288 true
7289 }
7290 _ => false,
7291 };
7292 } else {
7293 clear_focus = true;
7294 }
7295 if clear_selection {
7296 m.selection = None;
7297 }
7298 if close_after {
7299 let old_open = m.open;
7300 m.open = false;
7301 sg_mwnd_state_trace_runtime(
7302 &trace_scene,
7303 &trace_scene_no,
7304 trace_line,
7305 "MWND_SELECTION_KEY_CLOSE",
7306 stage_idx,
7307 mwnd_idx,
7308 old_open,
7309 m.open,
7310 m,
7311 );
7312 close_anim = Some((close_type, close_time));
7313 }
7314 } else {
7315 clear_focus = true;
7316 }
7317 } else {
7318 clear_focus = true;
7319 }
7320 } else {
7321 clear_focus = true;
7322 }
7323 if clear_focus {
7324 self.globals.focused_stage_mwnd = None;
7325 }
7326 if let Some(v) = result_to_push {
7327 self.stack.push(Value::Int(v));
7328 }
7329 if let Some((ty, ms)) = close_anim {
7330 self.ui.begin_mwnd_close(ty, ms);
7331 }
7332 handled
7333 }
7334
7335 fn handle_mwnd_selection_click(&mut self, b: input::VmMouseButton) -> bool {
7336 let Some((form_id, stage_idx, mwnd_idx)) = self.globals.focused_stage_mwnd else {
7337 return false;
7338 };
7339 let trace_scene = self
7340 .current_scene_name
7341 .as_deref()
7342 .unwrap_or("<none>")
7343 .to_string();
7344 let trace_scene_no = self
7345 .current_scene_no
7346 .map(|v| v.to_string())
7347 .unwrap_or_else(|| "-".to_string());
7348 let trace_line = self.current_line_no;
7349 let mut clear_focus = false;
7350 let mut handled = false;
7351 let mut close_anim: Option<(i64, i64)> = None;
7352 let mut result_to_push: Option<i64> = None;
7353 if let Some(st) = self.globals.stage_forms.get_mut(&form_id) {
7354 if let Some(list) = st.mwnd_lists.get_mut(&stage_idx) {
7355 if let Some(m) = list.get_mut(mwnd_idx) {
7356 let close_time = m.close_anime_time;
7357 let close_type = m.close_anime_type;
7358 let mut close_after = false;
7359 let mut clear_selection = false;
7360 if let Some(sel) = m.selection.as_mut() {
7361 handled = match b {
7362 input::VmMouseButton::Left => {
7363 sel.result = (sel.cursor as i64) + 1;
7364 result_to_push = Some(sel.result);
7365 close_after = sel.close_mwnd;
7366 clear_selection = true;
7367 clear_focus = true;
7368 true
7369 }
7370 input::VmMouseButton::Right if sel.cancel_enable => {
7371 sel.result = -1;
7372 result_to_push = Some(sel.result);
7373 close_after = sel.close_mwnd;
7374 clear_selection = true;
7375 clear_focus = true;
7376 true
7377 }
7378 _ => false,
7379 };
7380 } else {
7381 clear_focus = true;
7382 }
7383 if clear_selection {
7384 m.selection = None;
7385 }
7386 if close_after {
7387 let old_open = m.open;
7388 m.open = false;
7389 sg_mwnd_state_trace_runtime(
7390 &trace_scene,
7391 &trace_scene_no,
7392 trace_line,
7393 "MWND_SELECTION_MOUSE_CLOSE",
7394 stage_idx,
7395 mwnd_idx,
7396 old_open,
7397 m.open,
7398 m,
7399 );
7400 close_anim = Some((close_type, close_time));
7401 }
7402 } else {
7403 clear_focus = true;
7404 }
7405 } else {
7406 clear_focus = true;
7407 }
7408 } else {
7409 clear_focus = true;
7410 }
7411 if clear_focus {
7412 self.globals.focused_stage_mwnd = None;
7413 }
7414 if let Some(v) = result_to_push {
7415 self.stack.push(Value::Int(v));
7416 }
7417 if let Some((ty, ms)) = close_anim {
7418 self.ui.begin_mwnd_close(ty, ms);
7419 }
7420 handled
7421 }
7422
7423 fn sync_mwnd_window_ui(&mut self) {
7424 let focused = self.globals.focused_stage_mwnd;
7425 let wipe_active = self.globals.wipe.is_some();
7426 let color_table = &self.tables.color_table;
7427 let resolve_color = |color_no: i64| -> (u8, u8, u8) {
7428 if color_no >= 0 {
7429 color_table
7430 .get(color_no as usize)
7431 .copied()
7432 .unwrap_or((255, 255, 255))
7433 } else {
7434 (255, 255, 255)
7435 }
7436 };
7437 let mut projections = Vec::new();
7438 let use_chara_color = self.globals.syscom.original_config.message_chrcolor_flag;
7439 let font_shadow_mode = self.effective_font_shadow_mode();
7440 let font_bold = self.effective_font_bold();
7441 let (draw_shadow, draw_fuchi) =
7442 crate::text_render::font_shadow_mode_flags(font_shadow_mode);
7443
7444 for (form_id, st) in &self.globals.stage_forms {
7445 for (stage_idx, list) in &st.mwnd_lists {
7446 if *stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
7447 continue;
7448 }
7449 for (mwnd_idx, m) in list.iter().enumerate() {
7450 if !m.initialized_from_gameexe {
7451 continue;
7452 }
7453 let key_icon_template = if m.icon_no >= 0 {
7454 self.tables.icon_templates.get(m.icon_no as usize)
7455 } else {
7456 None
7457 };
7458 let page_icon_template = if m.page_icon_no >= 0 {
7459 self.tables.icon_templates.get(m.page_icon_no as usize)
7460 } else {
7461 None
7462 };
7463 let msg_moji_no = m
7464 .moji_color
7465 .or(if use_chara_color {
7466 m.chara_moji_color
7467 } else {
7468 None
7469 })
7470 .unwrap_or(m.default_moji_color);
7471 let msg_shadow_no = m
7472 .shadow_color
7473 .or(if use_chara_color {
7474 m.chara_shadow_color
7475 } else {
7476 None
7477 })
7478 .unwrap_or(m.default_shadow_color);
7479 let msg_fuchi_no = m
7480 .fuchi_color
7481 .or(if use_chara_color {
7482 m.chara_fuchi_color
7483 } else {
7484 None
7485 })
7486 .unwrap_or(m.default_fuchi_color);
7487 let name_moji_no = m
7488 .name_moji_color
7489 .or(if use_chara_color {
7490 m.chara_moji_color
7491 } else {
7492 None
7493 })
7494 .unwrap_or(m.default_name_moji_color);
7495 let name_shadow_no = m
7496 .name_shadow_color
7497 .or(if use_chara_color {
7498 m.chara_shadow_color
7499 } else {
7500 None
7501 })
7502 .unwrap_or(m.default_name_shadow_color);
7503 let name_fuchi_no = m
7504 .name_fuchi_color
7505 .or(if use_chara_color {
7506 m.chara_fuchi_color
7507 } else {
7508 None
7509 })
7510 .unwrap_or(m.default_name_fuchi_color);
7511
7512 let select_emoji = |requested_size: i64| -> Option<(&str, i64)> {
7513 let mut best: Option<&crate::runtime::tables::EmojiTemplate> = None;
7514 for item in &self.tables.emoji_templates {
7515 if item.file_name.is_empty() || item.font_size <= 0 {
7516 continue;
7517 }
7518 best = match best {
7519 None => Some(item),
7520 Some(cur) if cur.font_size > requested_size => {
7521 if item.font_size < cur.font_size {
7522 Some(item)
7523 } else {
7524 Some(cur)
7525 }
7526 }
7527 Some(cur)
7528 if item.font_size <= requested_size
7529 && item.font_size > cur.font_size =>
7530 {
7531 Some(item)
7532 }
7533 Some(cur) => Some(cur),
7534 };
7535 }
7536 best.map(|item| (item.file_name.as_str(), item.font_size))
7537 };
7538
7539 let projection = crate::runtime::ui::MwndProjectionState {
7540 bg_file: (!m.waku_file.is_empty()).then(|| m.waku_file.clone()),
7541 filter_file: (!m.filter_file.is_empty()).then(|| m.filter_file.clone()),
7542 filter_margin: m.filter_margin,
7543 filter_color: m.filter_color,
7544 filter_config_color: m.filter_config_color,
7545 filter_config_tr: m.filter_config_tr,
7546 face_file: (!m.face_file.is_empty()).then(|| m.face_file.clone()),
7547 face_no: m.face_no,
7548 rep_pos: m.rep_pos,
7549 window_pos: m.window_pos,
7550 window_size: m.window_size,
7551 message_pos: m.message_pos,
7552 message_margin: m.message_margin,
7553 window_moji_cnt: m.window_moji_cnt,
7554 moji_size: m.moji_size.or(Some(m.default_moji_size.max(1))),
7555 moji_space: m.moji_space,
7556 mwnd_extend_type: m.mwnd_extend_type,
7557 moji_color: m.moji_color,
7558 shadow_color: m.shadow_color,
7559 fuchi_color: m.fuchi_color,
7560 chara_moji_color: m.chara_moji_color,
7561 chara_shadow_color: m.chara_shadow_color,
7562 chara_fuchi_color: m.chara_fuchi_color,
7563 name_moji_color: m.name_moji_color,
7564 name_shadow_color: m.name_shadow_color,
7565 name_fuchi_color: m.name_fuchi_color,
7566 resolved_msg_color: resolve_color(msg_moji_no),
7567 resolved_msg_shadow_color: resolve_color(msg_shadow_no),
7568 resolved_msg_fuchi_color: (msg_fuchi_no >= 0)
7569 .then(|| resolve_color(msg_fuchi_no)),
7570 resolved_name_color: resolve_color(name_moji_no),
7571 resolved_name_shadow_color: resolve_color(name_shadow_no),
7572 resolved_name_fuchi_color: (name_fuchi_no >= 0)
7573 .then(|| resolve_color(name_fuchi_no)),
7574 font_shadow_mode,
7575 font_bold,
7576 key_icon_file: key_icon_template
7577 .and_then(|t| (!t.file_name.is_empty()).then(|| t.file_name.clone())),
7578 key_icon_pat_cnt: key_icon_template.map(|t| t.anime_pat_cnt).unwrap_or(1),
7579 key_icon_speed: key_icon_template.map(|t| t.anime_speed).unwrap_or(100),
7580 page_icon_file: page_icon_template
7581 .and_then(|t| (!t.file_name.is_empty()).then(|| t.file_name.clone())),
7582 page_icon_pat_cnt: page_icon_template.map(|t| t.anime_pat_cnt).unwrap_or(1),
7583 page_icon_speed: page_icon_template.map(|t| t.anime_speed).unwrap_or(100),
7584 key_icon_appear: m.key_icon_appear,
7585 key_icon_mode: m.key_icon_mode,
7586 key_icon_pos: m.key_icon_pos,
7587 icon_pos_type: m.icon_pos_type,
7588 icon_pos_base: m.icon_pos_base,
7589 icon_pos: m.icon_pos,
7590 slide_enabled: m.slide_msg,
7591 slide_time: m.slide_time,
7592 vertical_writing: m.vertical_writing,
7593 name_text: m.name_text.clone(),
7594 name_extend_type: m.name_extend_type,
7595 name_window_align: m.name_window_align,
7596 name_window_pos: m.name_window_pos,
7597 name_window_size: m.name_window_size,
7598 name_window_rect: m.name_window_rect,
7599 name_message_pos: m.name_message_pos,
7600 name_message_pos_rep: m.name_message_pos_rep,
7601 name_message_margin: m.name_message_margin,
7602 name_glyphs: m
7603 .name_glyphs
7604 .iter()
7605 .map(|g| {
7606 let emoji = if g.moji_type == 0 {
7607 None
7608 } else {
7609 select_emoji(g.size.max(1))
7610 };
7611 crate::runtime::ui::MwndGlyphProjection {
7612 moji_type: g.moji_type,
7613 code: g.code,
7614 ch: g.ch,
7615 x: g.x.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
7616 y: g.y.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
7617 size: g.size.max(1).min(i32::MAX as i64) as i32,
7618 color: resolve_color(g.moji_color_no),
7619 shadow_color: resolve_color(g.shadow_color_no),
7620 fuchi_color: resolve_color(g.fuchi_color_no),
7621 shadow_mode: font_shadow_mode,
7622 shadow: draw_shadow,
7623 fuchi: draw_fuchi,
7624 bold: font_bold && g.moji_type == 0,
7625 reveal_index: 0,
7626 ruby: false,
7627 appeared: true,
7628 emoji_file: emoji.map(|(file, _)| file.to_string()),
7629 emoji_font_size: emoji
7630 .map(|(_, size)| size as i32)
7631 .unwrap_or(0),
7632 message_button: None,
7633 }
7634 })
7635 .collect(),
7636 msg_text: m
7637 .message_pages
7638 .iter()
7639 .map(|page| page.msg_text.as_str())
7640 .chain(std::iter::once(m.msg_text.as_str()))
7641 .collect::<String>(),
7642 glyphs: m
7643 .message_pages
7644 .iter()
7645 .flat_map(|page| page.glyphs.iter())
7646 .chain(m.glyphs.iter())
7647 .map(|g| {
7648 let mut color_no = g.moji_color_no;
7649 let mut shadow_color_no = g.shadow_color_no;
7650 let mut fuchi_color_no = g.fuchi_color_no;
7651 if let Some(button) = &g.message_button {
7652 let button_state = st
7653 .group_lists
7654 .get(stage_idx)
7655 .and_then(|groups| {
7656 groups.get(button.group_no.max(0) as usize)
7657 })
7658 .map(|group| {
7659 if group.pushed_button_no == button.btn_no {
7660 2
7661 } else if group.hit_button_no == button.btn_no {
7662 1
7663 } else {
7664 0
7665 }
7666 })
7667 .unwrap_or(0);
7668 if let Some(action) = self
7669 .tables
7670 .message_button_templates
7671 .get(button.action_no.max(0) as usize)
7672 {
7673 let button_color = action.color_no[button_state];
7674 if button_color >= 0 {
7675 color_no = button_color;
7676 shadow_color_no = self.tables.mwnd_render.shadow_color;
7680 fuchi_color_no = self.tables.mwnd_render.fuchi_color;
7681 }
7682 }
7683 }
7684 let emoji = if g.moji_type == 0 {
7685 None
7686 } else {
7687 select_emoji(g.size.max(1))
7688 };
7689 crate::runtime::ui::MwndGlyphProjection {
7690 moji_type: g.moji_type,
7691 code: g.code,
7692 ch: g.ch,
7693 x: g.x.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
7694 y: g.y.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
7695 size: g.size.max(1).min(i32::MAX as i64) as i32,
7696 color: resolve_color(color_no),
7697 shadow_color: resolve_color(shadow_color_no),
7698 fuchi_color: resolve_color(fuchi_color_no),
7699 shadow_mode: font_shadow_mode,
7700 shadow: draw_shadow,
7701 fuchi: draw_fuchi,
7702 bold: font_bold && g.moji_type == 0,
7703 reveal_index: g.reveal_index,
7704 ruby: g.ruby,
7705 appeared: g.appeared,
7706 emoji_file: emoji.map(|(file, _)| file.to_string()),
7707 emoji_font_size: emoji
7708 .map(|(_, size)| size as i32)
7709 .unwrap_or(0),
7710 message_button: g.message_button.as_ref().map(|button| {
7711 crate::runtime::ui::MwndMessageButtonProjection {
7712 btn_no: button.btn_no,
7713 group_no: button.group_no,
7714 action_no: button.action_no,
7715 se_no: button.se_no,
7716 }
7717 }),
7718 }
7719 })
7720 .collect(),
7721 open: m.open,
7722 open_anime_type: m.open_anime_type,
7723 open_anime_time: m.open_anime_time,
7724 close_anime_type: m.close_anime_type,
7725 close_anime_time: m.close_anime_time,
7726 order: m.order,
7727 layer: m.layer,
7728 };
7729 projections.push(((*form_id, *stage_idx, mwnd_idx), projection));
7730 }
7731 }
7732 }
7733
7734 self.ui.apply_mwnd_projection_set(projections, focused);
7735 }
7736
7737 fn sync_mwnd_selection_ui(&mut self) {
7738 if self.globals.system.messagebox_modal.is_some() {
7739 return;
7740 }
7741 self.ui.set_sys_overlay(false, String::new());
7742 }
7743
7744 fn sync_emote_objects(&mut self) {
7745 let koe_playing = self.koe.is_playing_any();
7746 let live_mouth = if koe_playing {
7747 self.koe.current_mouth_volume()
7748 } else {
7749 0.0
7750 };
7751 let koe_chara_no = self.globals.sound_routing.koe_chara_no;
7752 let koe_ex = self.globals.sound_routing.koe_ex_flag;
7753 let mouth_stop = self.globals.script.emote_mouth_stop_flag;
7754
7755 let layers = &mut self.layers;
7756 for stage in self.globals.stage_forms.values_mut() {
7757 let mut stage_ids: Vec<i64> = stage.object_lists.keys().copied().collect();
7758 stage_ids.sort_unstable();
7759 for stage_idx in stage_ids {
7760 let slot_use = stage.object_slot_use.get(&stage_idx).cloned();
7761 let embedded = stage
7762 .embedded_object_slots_by_stage
7763 .get(&stage_idx)
7764 .cloned();
7765 let Some(list) = stage.object_lists.get_mut(&stage_idx) else {
7766 continue;
7767 };
7768 for (obj_idx, obj) in list.iter_mut().enumerate() {
7769 if embedded
7770 .as_ref()
7771 .is_some_and(|slots| slots.contains(&obj_idx))
7772 || !slot_use
7773 .as_ref()
7774 .and_then(|flags| flags.get(obj_idx))
7775 .copied()
7776 .unwrap_or(true)
7777 {
7778 continue;
7779 }
7780 sync_emote_object_recursive(
7781 layers,
7782 obj,
7783 mouth_stop,
7784 koe_playing,
7785 koe_ex,
7786 koe_chara_no,
7787 live_mouth,
7788 );
7789 }
7790 }
7791 for items in stage.btnselitem_lists.values_mut() {
7792 for item in items {
7793 for obj in &mut item.object_list {
7794 sync_emote_object_recursive(
7795 layers,
7796 obj,
7797 mouth_stop,
7798 koe_playing,
7799 koe_ex,
7800 koe_chara_no,
7801 live_mouth,
7802 );
7803 }
7804 }
7805 }
7806 for mwnds in stage.mwnd_lists.values_mut() {
7807 for mwnd in mwnds {
7808 for list in [
7809 &mut mwnd.object_list,
7810 &mut mwnd.button_list,
7811 &mut mwnd.face_list,
7812 ] {
7813 for obj in list {
7814 sync_emote_object_recursive(
7815 layers,
7816 obj,
7817 mouth_stop,
7818 koe_playing,
7819 koe_ex,
7820 koe_chara_no,
7821 live_mouth,
7822 );
7823 }
7824 }
7825 }
7826 }
7827 }
7828 }
7829
7830 fn sync_movie_objects(&mut self) {
7831 let wipe_active = self.globals.wipe.is_some();
7832 let (globals, layers, movie_mgr, audio, gfx, images, ids) = (
7833 &mut self.globals,
7834 &mut self.layers,
7835 &mut self.movie,
7836 &mut self.audio,
7837 &mut self.gfx,
7838 &mut self.images,
7839 &self.ids,
7840 );
7841 let mut decoded_any = false;
7842 let mut form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
7843 form_ids.sort_unstable();
7844 for form_id in form_ids {
7845 let Some(st) = globals.stage_forms.get_mut(&form_id) else {
7846 continue;
7847 };
7848 let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
7849 stage_ids.sort_unstable();
7850 for stage_idx in stage_ids {
7851 if stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
7852 continue;
7853 }
7854 let slot_use = st.object_slot_use.get(&stage_idx).cloned();
7855 let embedded = st.embedded_object_slots_by_stage.get(&stage_idx).cloned();
7856 let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
7857 continue;
7858 };
7859 for (obj_idx, obj) in objs.iter_mut().enumerate() {
7860 if embedded
7861 .as_ref()
7862 .is_some_and(|slots| slots.contains(&obj_idx))
7863 || !slot_use
7864 .as_ref()
7865 .and_then(|flags| flags.get(obj_idx))
7866 .copied()
7867 .unwrap_or(true)
7868 {
7869 continue;
7870 }
7871 sync_movie_object_recursive(
7872 ids,
7873 layers,
7874 movie_mgr,
7875 audio,
7876 gfx,
7877 images,
7878 stage_idx,
7879 object_runtime_slot(obj_idx, obj) as i64,
7880 obj,
7881 &mut decoded_any,
7882 );
7883 }
7884 }
7885 }
7886 let _ = decoded_any;
7887 }
7888
7889 fn close_global_movie_runtime(&mut self) {
7890 let was_active = self.globals.mov.playing
7891 || self.globals.mov.file_name.is_some()
7892 || self.globals.mov.audio_id.is_some()
7893 || self.globals.mov.image_id.is_some();
7894
7895 if let Some(id) = self.globals.mov.audio_id.take() {
7896 self.movie.stop_audio(id);
7897 }
7898 if was_active {
7899 self.movie.stop();
7900 }
7901 if let (Some(layer_id), Some(sprite_id)) =
7902 (self.globals.mov.layer_id, self.globals.mov.sprite_id)
7903 && let Some(sprite) = self
7904 .layers
7905 .layer_mut(layer_id)
7906 .and_then(|l| l.sprite_mut(sprite_id))
7907 {
7908 sprite.visible = false;
7909 sprite.image_id = None;
7910 }
7911 self.globals.mov.image_id = None;
7912 self.globals.mov.last_frame_idx = None;
7913
7914 if was_active {
7915 self.globals.mov.stop();
7916 }
7917 }
7918
7919 fn sync_global_movie(&mut self) {
7920 let trace = env_is_set!("SG_MOVIE_TRACE");
7921 let file_name = self.globals.mov.file_name.clone();
7922
7923 if !self.globals.mov.playing || file_name.as_deref().unwrap_or("").is_empty() {
7924 self.close_global_movie_runtime();
7928 return;
7929 }
7930 let file_name = file_name.expect("checked global movie file name");
7931
7932 if let Some(id) = self.globals.mov.audio_id {
7933 if let Some(position_ms) = self.movie.audio_playback_position_ms(id) {
7934 self.globals.mov.timer_ms = position_ms;
7935 if self.movie.audio_playback_finished(id) {
7936 self.globals.mov.audio_id = None;
7946 self.globals.mov.audio_start_attempted = true;
7947 }
7948 } else {
7949 self.globals.mov.audio_id = None;
7954 self.globals.mov.audio_start_attempted = true;
7955 }
7956 }
7957
7958 let (x, y, width, height, timer_ms, last_frame_idx, image_id, need_audio) = {
7959 let m = &self.globals.mov;
7960 (
7961 m.x,
7962 m.y,
7963 m.width.max(1),
7964 m.height.max(1),
7965 m.timer_ms,
7966 m.last_frame_idx,
7967 m.image_id.clone(),
7968 m.audio_id.is_none() && !m.audio_start_attempted,
7969 )
7970 };
7971
7972 let (layer_id, sprite_id) = match (self.globals.mov.layer_id, self.globals.mov.sprite_id) {
7973 (Some(layer_id), Some(sprite_id))
7974 if self
7975 .layers
7976 .layer(layer_id)
7977 .and_then(|l| l.sprite(sprite_id))
7978 .is_some() =>
7979 {
7980 (layer_id, sprite_id)
7981 }
7982 _ => {
7983 let layer_id = self.layers.create_layer();
7984 let sprite_id = self
7985 .layers
7986 .layer_mut(layer_id)
7987 .expect("newly created global movie layer")
7988 .create_sprite();
7989 self.globals.mov.layer_id = Some(layer_id);
7990 self.globals.mov.sprite_id = Some(sprite_id);
7991 (layer_id, sprite_id)
7992 }
7993 };
7994
7995 let polled = match self.movie.poll_global_movie_frame(&file_name, timer_ms) {
7996 Ok(Some(frame)) => frame,
7997 Ok(None) => {
7998 if last_frame_idx.is_none() {
8003 self.globals.mov.timer_ms = 0;
8004 }
8005 return;
8006 }
8007 Err(err) => {
8008 eprintln!("[SG_MOV] error file={} err={:#}", file_name, err);
8009 self.globals.mov.playing = false;
8010 return;
8011 }
8012 };
8013
8014 if let Some(ms) = polled.clamped_timer_ms {
8015 self.globals.mov.timer_ms = ms;
8016 }
8017 if self.globals.mov.total_ms.is_none() || polled.total_ms.is_some() {
8018 self.globals.mov.total_ms = polled.total_ms.or(self.globals.mov.total_ms);
8019 }
8020 if let Some(total) = self.globals.mov.total_ms
8021 && total > 0
8022 && self.globals.mov.timer_ms >= total
8023 {
8024 self.globals.mov.timer_ms = total;
8025 self.globals.mov.playing = false;
8026 }
8027 let _ = polled.decoded_now;
8032
8033 let frame = polled.frame.clone();
8034 let frame_idx = polled.frame_idx;
8035
8036 if need_audio {
8037 if let Some(track) = polled.audio.as_ref() {
8038 let audio_offset_ms = self.globals.mov.timer_ms;
8044 match self
8045 .movie
8046 .start_audio(&mut self.audio, track, audio_offset_ms, false)
8047 {
8048 Ok(id) => {
8049 self.globals.mov.audio_id = Some(id);
8050 self.globals.mov.audio_start_attempted = false;
8051 if trace || sg_debug_enabled() {
8052 eprintln!(
8053 "[SG_DEBUG][MOV] audio_start file={} samples={} channels={} rate={} offset_ms={}",
8054 file_name,
8055 track.samples.len(),
8056 track.channels,
8057 track.sample_rate,
8058 audio_offset_ms
8059 );
8060 }
8061 }
8062 Err(err) => {
8063 eprintln!(
8064 "[SG_MOV] audio_start.failed file={} channels={} rate={} samples={} err={:#}",
8065 file_name,
8066 track.channels,
8067 track.sample_rate,
8068 track.samples.len(),
8069 err
8070 );
8071 self.globals.mov.audio_start_attempted = true;
8074 }
8075 }
8076 } else if polled.audio_ready {
8077 self.globals.mov.audio_start_attempted = true;
8078 if trace || sg_debug_enabled() {
8079 eprintln!("[SG_DEBUG][MOV] audio_track.missing file={}", file_name);
8080 }
8081 }
8082 }
8083
8084 let frame_idx_changed = last_frame_idx != Some(frame_idx);
8085 let small_rewind = !polled.frame_idx_is_decode_order
8091 && last_frame_idx
8092 .is_some_and(|last| frame_idx < last && frame_idx.saturating_add(1) >= last);
8093 let show_new = frame_idx_changed && !small_rewind;
8094 let img_id = if let Some(id) = image_id {
8095 if show_new {
8096 let _ = self.images.replace_image_arc(&id, frame);
8097 }
8098 id
8099 } else {
8100 self.images.insert_image_arc(frame.clone())
8101 };
8102 self.globals.mov.image_id = Some(img_id.clone());
8103 if show_new {
8104 self.globals.mov.last_frame_idx = Some(frame_idx);
8105 }
8106
8107 if let Some(sprite) = self
8108 .layers
8109 .layer_mut(layer_id)
8110 .and_then(|l| l.sprite_mut(sprite_id))
8111 {
8112 sprite.visible = true;
8113 sprite.image_id = Some(img_id);
8114 sprite.fit = SpriteFit::PixelRect;
8115 sprite.size_mode = SpriteSizeMode::Explicit { width, height };
8116 sprite.x = x;
8117 sprite.y = y;
8118 sprite.alpha = 255;
8119 sprite.tr = 255;
8120 sprite.alpha_blend = true;
8121 sprite.order = i32::MAX - 16;
8122 }
8123
8124 if trace {
8125 eprintln!(
8126 "[SG_MOVIE_TRACE] global MOV frame file={} idx={} timer={} pos=({}, {}) size={}x{} layer={} sprite={}",
8127 file_name,
8128 frame_idx,
8129 self.globals.mov.timer_ms,
8130 x,
8131 y,
8132 width,
8133 height,
8134 layer_id,
8135 sprite_id
8136 );
8137 }
8138 }
8139
8140 fn sync_weather_objects(&mut self, game_delta_ms: i32, real_delta_ms: i32) {
8141 let wipe_active = self.globals.wipe.is_some();
8142 let screen_w = self.screen_w.max(1) as i64;
8143 let screen_h = self.screen_h.max(1) as i64;
8144 let (globals, layers, images, ids) = (
8145 &mut self.globals,
8146 &mut self.layers,
8147 &mut self.images,
8148 &self.ids,
8149 );
8150 let mut form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
8151 form_ids.sort_unstable();
8152 for form_id in form_ids {
8153 let Some(st) = globals.stage_forms.get_mut(&form_id) else {
8154 continue;
8155 };
8156 let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
8157 stage_ids.sort_unstable();
8158 for stage_idx in stage_ids {
8159 if stage_idx == TNM_STAGE_NEXT_I64 && !wipe_active {
8160 continue;
8161 }
8162 let slot_use = st.object_slot_use.get(&stage_idx).cloned();
8163 let embedded = st.embedded_object_slots_by_stage.get(&stage_idx).cloned();
8164 let Some(objs) = st.object_lists.get_mut(&stage_idx) else {
8165 continue;
8166 };
8167 for (obj_idx, obj) in objs.iter_mut().enumerate() {
8168 if embedded
8169 .as_ref()
8170 .is_some_and(|slots| slots.contains(&obj_idx))
8171 || !slot_use
8172 .as_ref()
8173 .and_then(|flags| flags.get(obj_idx))
8174 .copied()
8175 .unwrap_or(true)
8176 {
8177 continue;
8178 }
8179 sync_weather_object_recursive(
8180 ids,
8181 layers,
8182 images,
8183 screen_w,
8184 screen_h,
8185 game_delta_ms,
8186 real_delta_ms,
8187 obj,
8188 );
8189 }
8190 }
8191 }
8192 }
8193
8194 fn repair_missing_gfx_leaf_images(&mut self) {
8195 if !self.gfx.has_missing_bound_object_image(&self.layers) {
8196 return;
8197 }
8198
8199 fn collect(
8200 ids: &crate::runtime::constants::RuntimeConstants,
8201 stage_idx: i64,
8202 objs: &[globals::ObjectState],
8203 out: &mut Vec<(i64, usize, String, i64)>,
8204 ) {
8205 for (idx, obj) in objs.iter().enumerate() {
8206 if obj.object_type != 0
8207 && obj.object_type != 6
8208 && matches!(obj.backend, globals::ObjectBackend::Gfx)
8209 {
8210 let slot = object_runtime_slot(idx, obj);
8211 let file = obj.file_name.clone();
8212 if let Some(file) = file
8213 && !file.is_empty()
8214 {
8215 let patno = obj.lookup_int_prop(ids, ids.obj_patno).unwrap_or(0);
8216 out.push((stage_idx, slot, file, patno));
8217 }
8218 }
8219 if !obj.runtime.child_objects.is_empty() {
8220 collect(ids, stage_idx, &obj.runtime.child_objects, out);
8221 }
8222 }
8223 }
8224
8225 let mut tasks: Vec<(i64, usize, String, i64)> = Vec::new();
8226 let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
8227 form_ids.sort_unstable();
8228 for form_id in form_ids {
8229 let Some(st) = self.globals.stage_forms.get(&form_id) else {
8230 continue;
8231 };
8232 let mut stage_ids: Vec<i64> = st
8233 .object_lists
8234 .keys()
8235 .chain(st.mwnd_lists.keys())
8236 .copied()
8237 .collect();
8238 stage_ids.sort_unstable();
8239 stage_ids.dedup();
8240 for stage_idx in stage_ids {
8241 if let Some(objs) = st.object_lists.get(&stage_idx) {
8242 collect(&self.ids, stage_idx, objs, &mut tasks);
8243 }
8244 if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
8245 for mwnd in mwnds {
8246 collect(&self.ids, stage_idx, &mwnd.button_list, &mut tasks);
8247 collect(&self.ids, stage_idx, &mwnd.face_list, &mut tasks);
8248 collect(&self.ids, stage_idx, &mwnd.object_list, &mut tasks);
8249 }
8250 }
8251 }
8252 }
8253
8254 for (stage_idx, runtime_slot, state_file, state_patno) in tasks {
8255 let Some((layer_id, sprite_id)) = self
8256 .gfx
8257 .object_sprite_binding(stage_idx, runtime_slot as i64)
8258 else {
8259 continue;
8260 };
8261 let needs_image = self
8262 .layers
8263 .layer(layer_id)
8264 .and_then(|layer| layer.sprite(sprite_id))
8265 .map(|sprite| sprite.image_id.is_none())
8266 .unwrap_or(false);
8267 if !needs_image {
8268 continue;
8269 }
8270
8271 let file = self
8272 .gfx
8273 .object_peek_file(stage_idx, runtime_slot as i64)
8274 .unwrap_or_else(|| state_file.clone());
8275 if file.is_empty() {
8276 continue;
8277 }
8278 let patno = self
8279 .gfx
8280 .object_peek_patno(stage_idx, runtime_slot as i64)
8281 .unwrap_or(state_patno)
8282 .max(0) as u32;
8283
8284 let repair_key = (
8285 self.globals.append_dir.clone(),
8286 stage_idx,
8287 runtime_slot,
8288 file.clone(),
8289 patno,
8290 );
8291 if self.failed_gfx_image_repairs.contains(&repair_key) {
8292 continue;
8293 }
8294
8295 let img_id = match self.images.load_g00(&file, patno) {
8296 Ok(id) => Ok(id),
8297 Err(_) => self.images.load_bg_frame(&file, patno as usize),
8298 };
8299 match img_id {
8300 Ok(img_id) => {
8301 if let Some(layer) = self.layers.layer_mut(layer_id)
8302 && let Some(sprite) = layer.sprite_mut(sprite_id)
8303 {
8304 sprite.image_id = Some(img_id.clone());
8305 if let Some(img) = self.images.get(&img_id) {
8306 sprite.object_anchor = true;
8307 sprite.texture_center_x = img.center_x as f32;
8308 sprite.texture_center_y = img.center_y as f32;
8309 } else {
8310 sprite.object_anchor = false;
8311 sprite.texture_center_x = 0.0;
8312 sprite.texture_center_y = 0.0;
8313 }
8314 }
8315 }
8316 Err(err) => {
8317 self.failed_gfx_image_repairs.insert(repair_key);
8318 self.unknown.record_note(&format!(
8319 "gfx.image.repair.failed:stage={stage_idx}:slot={runtime_slot}:file={file}:patno={patno}:{err}"
8320 ));
8321 }
8322 }
8323 }
8324 }
8325
8326 fn build_render_list_pre_wipe(
8333 &mut self,
8334 ) -> (Vec<RenderSprite>, Vec<String>, HashSet<(LayerId, SpriteId)>) {
8335 self.images.organize();
8336 self.layers.reset_runtime_effects();
8337 self.repair_missing_gfx_leaf_images();
8338 self.apply_object_masks();
8339 self.apply_object_tonecurves();
8340 let mut object_keys = std::mem::take(&mut self.render_object_keys_scratch);
8341 object_keys.clear();
8342 mark_all_stage_owned_sprite_keys(self, &mut object_keys);
8343 let base = self.layers.render_list_excluding(|layer_id, sprite_id| {
8344 object_keys.contains(&(layer_id, sprite_id))
8345 });
8346 let (mut list, debug_lines) =
8347 build_siglus_object_render_list(self, &base, TNM_STAGE_FRONT_I64, &mut object_keys);
8348 apply_button_visuals(self, &mut list);
8349 apply_selbtn_item_visuals(self, &mut list);
8350 self.apply_gan_effects(&mut list);
8351 apply_stage_render_effects(&self.globals, &self.ids, TNM_STAGE_FRONT_I64, &mut list);
8352 (list, debug_lines, object_keys)
8353 }
8354
8355 pub fn set_frame_capture_backend(&mut self, backend: Option<FrameCaptureBackendRef>) {
8356 self.frame_capture_backend = backend;
8357 }
8358
8359 pub fn render_frame_with_effects(&mut self) -> RenderFrame {
8360 self.render_frame_with_effects_inner(true)
8361 }
8362
8363 pub fn render_list_with_effects(&mut self) -> Vec<RenderSprite> {
8366 self.render_frame_with_effects().debug_flatten()
8367 }
8368
8369 fn render_frame_with_effects_inner(&mut self, include_mouse_cursor: bool) -> RenderFrame {
8370 let (pre_wipe_list, debug_lines, mut object_keys) = self.build_render_list_pre_wipe();
8371 let frame = if let Some(wipe_state) = self.globals.wipe.as_ref().cloned() {
8372 let (wipe_begin_order, wipe_end_order) =
8373 effective_wipe_render_order_bounds(self, &wipe_state);
8374 let base = self.layers.render_list_excluding(|layer_id, sprite_id| {
8375 object_keys.contains(&(layer_id, sprite_id))
8376 });
8377 let (mut next_list, next_debug_lines) =
8378 build_siglus_object_render_list(self, &base, TNM_STAGE_NEXT_I64, &mut object_keys);
8379 apply_button_visuals(self, &mut next_list);
8380 apply_selbtn_item_visuals(self, &mut next_list);
8381 self.apply_gan_effects(&mut next_list);
8382 apply_stage_render_effects(
8383 &self.globals,
8384 &self.ids,
8385 TNM_STAGE_NEXT_I64,
8386 &mut next_list,
8387 );
8388 if config_button_trace_enabled() {
8389 eprintln!(
8390 "[SG_DEBUG][CONFIG_BUTTON_TRACE][RENDER_PHASE] wipe_active=true pre_wipe_len={} next_len={} next_debug_lines={} wipe_type={}",
8391 pre_wipe_list.len(),
8392 next_list.len(),
8393 next_debug_lines.len(),
8394 wipe_state.wipe_type,
8395 );
8396 for line in next_debug_lines
8397 .iter()
8398 .filter(|line| line.contains("CONFIG_BUTTON_TRACE"))
8399 {
8400 eprintln!("{}", line);
8401 }
8402 }
8403
8404 let with_low = wipe_state.with_low_order != 0;
8405 let mut under = Vec::new();
8406 let mut current = Vec::new();
8407 let mut over = Vec::new();
8408 for rs in pre_wipe_list.iter().cloned() {
8409 match classify_wipe_partition(
8410 &rs,
8411 wipe_state.begin_layer,
8412 wipe_state.end_layer,
8413 wipe_begin_order,
8414 wipe_end_order,
8415 with_low,
8416 ) {
8417 WipePartition::Under => under.push(rs),
8418 WipePartition::Target => current.push(rs),
8419 WipePartition::Over => over.push(rs),
8420 }
8421 }
8422 let mut next = next_list
8423 .into_iter()
8424 .filter(|rs| {
8425 classify_wipe_partition(
8426 rs,
8427 wipe_state.begin_layer,
8428 wipe_state.end_layer,
8429 wipe_begin_order,
8430 wipe_end_order,
8431 with_low,
8432 ) == WipePartition::Target
8433 })
8434 .collect::<Vec<_>>();
8435
8436 under.retain(render_sprite_visible_for_submit);
8437 current.retain(render_sprite_visible_for_submit);
8438 next.retain(render_sprite_visible_for_submit);
8439 over.retain(render_sprite_visible_for_submit);
8440
8441 compose_basic_wipe_scene_inputs(
8454 wipe_state.wipe_type,
8455 &mut under,
8456 &mut current,
8457 &mut next,
8458 );
8459 if include_mouse_cursor {
8460 self.append_mouse_cursor_sprite(&mut over);
8461 }
8462
8463 let progress = wipe::eased_progress(wipe_state.progress(), wipe_state.speed_mode);
8464 RenderFrame {
8465 sprites: Vec::new(),
8466 wipe: Some(WipeRenderPlan {
8467 under,
8468 current,
8469 next,
8470 over,
8471 wipe_type: wipe_state.wipe_type,
8472 option: wipe_state.option,
8473 progress,
8474 mask_image_id: wipe_state.mask_image_id,
8475 random_seed: wipe_state.random_seed,
8476 }),
8477 }
8478 } else {
8479 let mut list = pre_wipe_list;
8480 list.retain(render_sprite_visible_for_submit);
8481 if include_mouse_cursor {
8482 self.append_mouse_cursor_sprite(&mut list);
8483 }
8484 RenderFrame::ordinary(list)
8485 };
8486 self.render_object_keys_scratch = object_keys;
8487
8488 let config_button_trace = config_button_trace_enabled();
8489 let save_load_trace = save_load_render_trace_enabled();
8490 let render_tree_debug = sg_render_tree_debug_enabled();
8491 if config_button_trace || save_load_trace || render_tree_debug {
8492 let debug_list = frame.debug_flatten();
8493 if config_button_trace {
8494 trace_final_render_order(self, &debug_list);
8495 }
8496 if save_load_trace {
8497 trace_save_load_render_sprites(self, &debug_list);
8498 }
8499 if render_tree_debug {
8500 use std::sync::atomic::{AtomicU64, Ordering};
8501 static FRAME_NO: AtomicU64 = AtomicU64::new(0);
8502 let frame_no = FRAME_NO.fetch_add(1, Ordering::Relaxed) + 1;
8503 eprintln!("[SG_DEBUG] ===== frame {} =====", frame_no);
8504 for line in debug_lines {
8505 eprintln!("{}", line);
8506 }
8507 if let Some(wipe) = self.globals.wipe.as_ref() {
8508 eprintln!(
8509 "[SG_DEBUG] wipe active type={} progress={:.3} range=({},{})->({},{}) with_low={} wait={}",
8510 wipe.wipe_type,
8511 wipe.progress(),
8512 wipe.begin_order,
8513 wipe.begin_layer,
8514 wipe.end_order,
8515 wipe.end_layer,
8516 wipe.with_low_order,
8517 wipe.wait_flag,
8518 );
8519 }
8520 eprintln!(
8521 "[SG_DEBUG] submitted_render_list len={}",
8522 frame.submitted_sprite_count()
8523 );
8524 }
8525 }
8526 frame
8527 }
8528
8529 #[allow(clippy::mutable_key_type)]
8531 pub fn debug_active_texture_entries(
8532 &self,
8533 submitted: &[RenderSprite],
8534 ) -> Vec<DebugActiveTextureEntry> {
8535 let mut submitted_keys: HashSet<(LayerId, SpriteId)> = HashSet::default();
8536 let mut submitted_images: HashSet<ImageHandle> = HashSet::default();
8537 for rs in submitted {
8538 if let Some(ref id) = rs.sprite.image_id {
8539 submitted_images.insert(id.clone());
8540 }
8541 if let (Some(layer_id), Some(sprite_id)) = (rs.layer_id, rs.sprite_id) {
8542 submitted_keys.insert((layer_id, sprite_id));
8543 }
8544 }
8545
8546 let mut acc: HashMap<ImageHandle, DebugActiveTextureAccum> = HashMap::default();
8547 let mut form_ids: Vec<u32> = self.globals.stage_forms.keys().copied().collect();
8548 form_ids.sort_unstable();
8549 for form_id in form_ids {
8550 let Some(st) = self.globals.stage_forms.get(&form_id) else {
8551 continue;
8552 };
8553 let mut stage_ids: Vec<i64> = st.object_lists.keys().copied().collect();
8554 stage_ids.sort_unstable();
8555 for stage_idx in stage_ids {
8556 let Some(list) = st.object_lists.get(&stage_idx) else {
8557 continue;
8558 };
8559 for (obj_idx, obj) in list.iter().enumerate() {
8560 if !st.object_slot_is_used(stage_idx, obj_idx) {
8561 continue;
8562 }
8563 collect_debug_active_textures_from_object(
8564 self,
8565 form_id,
8566 stage_idx,
8567 obj_idx,
8568 obj,
8569 &submitted_keys,
8570 &submitted_images,
8571 &mut acc,
8572 );
8573 }
8574 }
8575 }
8576
8577 let mut out: Vec<DebugActiveTextureEntry> = acc
8578 .into_iter()
8579 .map(|(image_id, entry)| DebugActiveTextureEntry {
8580 image_id,
8581 width: entry.width,
8582 height: entry.height,
8583 source_label: entry.source_label,
8584 submitted_this_frame: entry.submitted_this_frame,
8585 visible_refs: entry.visible_refs,
8586 total_refs: entry.total_refs,
8587 ref_summary: if entry.ref_labels.is_empty() {
8588 String::new()
8589 } else {
8590 entry.ref_labels.join(" | ")
8591 },
8592 })
8593 .collect();
8594 out.sort_by(|a, b| {
8595 b.submitted_this_frame
8596 .cmp(&a.submitted_this_frame)
8597 .then_with(|| b.visible_refs.cmp(&a.visible_refs))
8598 .then_with(|| b.total_refs.cmp(&a.total_refs))
8599 .then_with(|| a.image_id.key().cmp(&b.image_id.key()))
8600 });
8601 out
8602 }
8603
8604 pub fn capture_frame_rgba(&mut self) -> Result<RgbaImage> {
8606 let backend = self
8607 .frame_capture_backend
8608 .clone()
8609 .ok_or_else(|| anyhow!("GPU frame capture requested without an attached renderer"))?;
8610 let frame = self.render_frame_with_effects_inner(false);
8611
8612 {
8613 let mut backend = backend.borrow_mut();
8614 backend.capture_render_frame(&self.images, &frame, self.screen_w, self.screen_h)
8615 }
8616 }
8617
8618 pub fn capture_frame_rgba_until(
8620 &mut self,
8621 end_order: i64,
8622 end_layer: i64,
8623 ) -> Result<RgbaImage> {
8624 let backend = self
8625 .frame_capture_backend
8626 .clone()
8627 .ok_or_else(|| anyhow!("GPU frame capture requested without an attached renderer"))?;
8628 let mut frame = self.render_frame_with_effects_inner(false);
8629 let within = |rs: &RenderSprite| {
8630 i64::from(rs.sorter_order) < end_order
8631 || (i64::from(rs.sorter_order) == end_order
8632 && i64::from(rs.sorter_layer) <= end_layer)
8633 };
8634 frame.sprites.retain(within);
8635 if let Some(wipe) = frame.wipe.as_mut() {
8636 wipe.under.retain(within);
8637 wipe.current.retain(within);
8638 wipe.next.retain(within);
8639 wipe.over.retain(within);
8640 }
8641
8642 {
8643 let mut backend = backend.borrow_mut();
8644 backend.capture_render_frame(&self.images, &frame, self.screen_w, self.screen_h)
8645 }
8646 }
8647}
8648
8649#[allow(clippy::mutable_key_type)]
8651fn collect_debug_active_textures_from_object(
8652 ctx: &CommandContext,
8653 stage_form_id: u32,
8654 stage_idx: i64,
8655 obj_idx: usize,
8656 obj: &globals::ObjectState,
8657 submitted_keys: &HashSet<(LayerId, SpriteId)>,
8658 submitted_images: &HashSet<ImageHandle>,
8659 out: &mut HashMap<ImageHandle, DebugActiveTextureAccum>,
8660) {
8661 if !object_participates_in_tree(obj) {
8662 return;
8663 }
8664
8665 let info = effective_object_info(ctx, stage_idx, obj_idx, obj);
8666 let bound = fetch_bound_render_sprites_any(ctx, stage_idx, info.runtime_slot, obj);
8667 for rs in bound {
8668 let Some(image_id) = rs.sprite.image_id else {
8669 continue;
8670 };
8671 let submitted = submitted_images.contains(&image_id)
8672 || rs
8673 .layer_id
8674 .zip(rs.sprite_id)
8675 .map(|key| submitted_keys.contains(&key))
8676 .unwrap_or(false);
8677 let debug_img = ctx.images.debug_image_info(&image_id);
8678 let entry = out
8679 .entry(image_id)
8680 .or_insert_with(|| DebugActiveTextureAccum {
8681 width: debug_img.as_ref().map(|d| d.width).unwrap_or(0),
8682 height: debug_img.as_ref().map(|d| d.height).unwrap_or(0),
8683 source_label: debug_img
8684 .as_ref()
8685 .and_then(|d| {
8686 d.source_path.as_ref().map(|p| {
8687 if let Some(frame_index) = d.frame_index {
8688 format!("{}#{}", p.display(), frame_index)
8689 } else {
8690 p.display().to_string()
8691 }
8692 })
8693 })
8694 .unwrap_or_else(|| {
8695 obj.file_name
8696 .clone()
8697 .unwrap_or_else(|| "<dynamic>".to_string())
8698 }),
8699 submitted_this_frame: false,
8700 visible_refs: 0,
8701 total_refs: 0,
8702 ref_labels: Vec::new(),
8703 });
8704 entry.submitted_this_frame |= submitted;
8705 entry.total_refs += 1;
8706 if info.disp {
8707 entry.visible_refs += 1;
8708 }
8709 let file = obj.file_name.as_deref().unwrap_or("-");
8710 let ref_label = format!(
8711 "sf{} st{} slot{} {} disp={} backend={}",
8712 stage_form_id,
8713 stage_idx,
8714 info.runtime_slot,
8715 file,
8716 if info.disp { 1 } else { 0 },
8717 debug_object_backend_name(obj)
8718 );
8719 if !entry.ref_labels.iter().any(|s| s == &ref_label) {
8720 if entry.ref_labels.len() < 3 {
8721 entry.ref_labels.push(ref_label);
8722 } else if entry.ref_labels.len() == 3 {
8723 entry.ref_labels.push("...".to_string());
8724 }
8725 }
8726 }
8727
8728 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
8729 collect_debug_active_textures_from_object(
8730 ctx,
8731 stage_form_id,
8732 stage_idx,
8733 child_idx,
8734 child,
8735 submitted_keys,
8736 submitted_images,
8737 out,
8738 );
8739 }
8740}
8741
8742fn debug_object_backend_name(obj: &globals::ObjectState) -> &'static str {
8743 match &obj.backend {
8744 globals::ObjectBackend::None => "None",
8745 globals::ObjectBackend::Gfx => "Gfx",
8746 globals::ObjectBackend::Rect { .. } => "Rect",
8747 globals::ObjectBackend::String { .. } => "String",
8748 globals::ObjectBackend::Number { .. } => "Number",
8749 globals::ObjectBackend::Weather { .. } => "Weather",
8750 globals::ObjectBackend::Movie { .. } => "Movie",
8751 }
8752}
8753
8754#[inline]
8755fn cached_env_flag(slot: &'static std::sync::OnceLock<bool>, name: &str) -> bool {
8756 *slot.get_or_init(|| {
8757 matches!(
8758 std::env::var(name).ok().as_deref(),
8759 Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
8760 )
8761 })
8762}
8763
8764fn sg_debug_enabled() -> bool {
8765 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8766 cached_env_flag(&ENABLED, "SG_DEBUG")
8767}
8768
8769fn sg_input_trace_enabled() -> bool {
8770 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8771 cached_env_flag(&ENABLED, "SG_INPUT_TRACE")
8772}
8773
8774fn sg_mwnd_object_trace_enabled() -> bool {
8775 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8776 cached_env_flag(&ENABLED, "SG_MWND_OBJECT_TRACE")
8777}
8778
8779fn sg_render_tree_debug_enabled() -> bool {
8780 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8781 cached_env_flag(&ENABLED, "SG_RENDER_TREE_DEBUG")
8782}
8783
8784fn config_button_trace_enabled() -> bool {
8785 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8786 cached_env_flag(&ENABLED, "SG_CONFIG_BUTTON_TRACE")
8787}
8788
8789fn config_button_trace_object(obj: &globals::ObjectState) -> bool {
8790 if obj.button.enabled || obj.button.state == TNM_BTN_STATE_DISABLE {
8791 return true;
8792 }
8793 let Some(file) = obj.file_name.as_deref() else {
8794 return false;
8795 };
8796 let f = file.to_ascii_lowercase();
8797 f.starts_with("mn_")
8798 || f.contains("config")
8799 || f.contains("conf")
8800 || f.contains("sys")
8801 || f.contains("mw")
8802}
8803
8804fn config_tr_write_trace_file(file: Option<&str>) -> bool {
8805 let Some(name) = file else {
8806 return false;
8807 };
8808 name.starts_with("mn_sm_menu_cbox")
8809 || name.starts_with("mn_cfa_tab_pbtn")
8810 || name.starts_with("mn_cfb_")
8811 || name.starts_with("mn_cfe_")
8812 || name.starts_with("mn_tt_menu")
8813 || name.starts_with("mn_tt_copy")
8814}
8815
8816fn config_tr_write_trace_object(obj_i64: i64, obj: &globals::ObjectState) -> bool {
8817 (100057..=100067).contains(&obj_i64) || config_tr_write_trace_file(obj.file_name.as_deref())
8818}
8819
8820fn trace_config_event_frame_prop_write(
8821 ids: &constants::RuntimeConstants,
8822 stage_i64: i64,
8823 obj_i64: i64,
8824 obj: &globals::ObjectState,
8825 prop_id: i32,
8826 old_value: i64,
8827 new_value: i64,
8828) {
8829 if !sg_debug_enabled() || !config_tr_write_trace_object(obj_i64, obj) {
8830 return;
8831 }
8832 let prop = if ids.obj_tr != 0 && prop_id == ids.obj_tr {
8833 "TR"
8834 } else if ids.obj_alpha != 0 && prop_id == ids.obj_alpha {
8835 "ALPHA"
8836 } else {
8837 return;
8838 };
8839 eprintln!(
8840 "[SG_DEBUG][CONFIG_TR_WRITE_TRACE][EVENT_FRAME] stage={} runtime_slot={} file={} prop={} old={} new={} disp={} tr={} alpha={} backend={:?} used={} children={}",
8841 stage_i64,
8842 obj_i64,
8843 obj.file_name.as_deref().unwrap_or("-"),
8844 prop,
8845 old_value,
8846 new_value,
8847 obj.get_int_prop(ids, ids.obj_disp),
8848 obj.get_int_prop(ids, ids.obj_tr),
8849 obj.get_int_prop(ids, ids.obj_alpha),
8850 obj.backend,
8851 obj.used,
8852 obj.runtime.child_objects.len(),
8853 );
8854}
8855
8856fn save_load_render_trace_enabled() -> bool {
8857 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8858 *ENABLED.get_or_init(|| env_is_set!("SG_SAVELOAD_TRACE"))
8859}
8860
8861fn trace_save_load_render_sprites(ctx: &CommandContext, list: &[RenderSprite]) {
8862 let scene = ctx.current_scene_name.as_deref().unwrap_or("<none>");
8863 let scene_match =
8864 scene.contains("sys10_sv") || scene.contains("save") || scene.contains("load");
8865 let mut emitted = 0usize;
8866 for (idx, rs) in list.iter().enumerate() {
8867 let Some(ref image_id) = rs.sprite.image_id else {
8868 continue;
8869 };
8870 let info = ctx.images.debug_image_info(image_id);
8871 let width = info.as_ref().map(|d| d.width).unwrap_or(0);
8872 let height = info.as_ref().map(|d| d.height).unwrap_or(0);
8873 let source_path = info
8874 .as_ref()
8875 .and_then(|d| d.source_path.as_ref())
8876 .map(|p| p.display().to_string())
8877 .unwrap_or_else(|| "-".to_string());
8878 let source_path_lc = source_path.to_ascii_lowercase();
8879 let source = render_sprite_source_name(ctx, rs);
8880 let source_lc = source.to_ascii_lowercase();
8881 let near_origin =
8882 rs.sprite.x.abs() <= 4 && rs.sprite.y.abs() <= 4 && width >= 16 && height >= 16;
8883 let unowned = source.starts_with("unowned:");
8884 let path_match = source_path_lc.contains("savedata")
8885 || source_path_lc.contains("thumb")
8886 || source_path_lc.contains("capture")
8887 || source_path_lc.contains("mn_sv")
8888 || source_lc.contains("mn_sv")
8889 || source_lc.contains("save")
8890 || source_lc.contains("thumb")
8891 || source_lc.contains("capture");
8892 if !(scene_match || near_origin || unowned || path_match) {
8893 continue;
8894 }
8895 if !(near_origin || unowned || path_match) {
8896 continue;
8897 }
8898 eprintln!(
8899 "[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={:?}",
8900 idx,
8901 scene,
8902 source,
8903 rs.layer_id,
8904 rs.sprite_id,
8905 rs.sprite.image_id,
8906 width,
8907 height,
8908 info.as_ref().map(|d| d.version).unwrap_or(0),
8909 source_path,
8910 info.as_ref().and_then(|d| d.frame_index),
8911 rs.sprite.x,
8912 rs.sprite.y,
8913 rs.sprite.visible,
8914 rs.sprite.alpha,
8915 rs.sprite.tr,
8916 rs.sorter_order,
8917 rs.sorter_layer,
8918 rs.sprite.order,
8919 rs.sprite.fit,
8920 rs.sprite.size_mode,
8921 rs.sprite.dst_clip
8922 );
8923 emitted += 1;
8924 if emitted >= 120 {
8925 eprintln!(
8926 "[SG_SAVELOAD_TRACE][RENDER] truncated after {} entries",
8927 emitted
8928 );
8929 break;
8930 }
8931 }
8932}
8933
8934fn trace_final_render_order(ctx: &CommandContext, list: &[RenderSprite]) {
8935 eprintln!(
8936 "[SG_DEBUG][CONFIG_BUTTON_TRACE][FINAL_ORDER] len={} wipe_active={} selected_stage=front",
8937 list.len(),
8938 ctx.globals.wipe.is_some()
8939 );
8940 for (idx, rs) in list.iter().enumerate() {
8941 let source = render_sprite_source_name(ctx, rs);
8942 eprintln!(
8943 "[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={:?}",
8944 idx,
8945 source,
8946 rs.layer_id,
8947 rs.sprite_id,
8948 rs.sorter_order,
8949 rs.sorter_layer,
8950 rs.sprite.order,
8951 rs.sprite.visible,
8952 rs.sprite.alpha,
8953 rs.sprite.tr,
8954 rs.sprite.x,
8955 rs.sprite.y,
8956 rs.sprite.z,
8957 rs.sprite.fit,
8958 rs.sprite.size_mode,
8959 rs.sprite.image_id,
8960 rs.sprite.blend,
8961 rs.sprite.dst_clip
8962 );
8963 }
8964}
8965
8966fn render_sprite_source_name(ctx: &CommandContext, rs: &RenderSprite) -> String {
8967 let Some(layer_id) = rs.layer_id else {
8968 return "background".to_string();
8969 };
8970 let Some(sprite_id) = rs.sprite_id else {
8971 return "background".to_string();
8972 };
8973 let mut found: Vec<String> = Vec::new();
8974 let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
8975 form_ids.sort_unstable();
8976 for form_id in form_ids {
8977 let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
8978 continue;
8979 };
8980 let mut stage_ids: Vec<i64> = st
8981 .object_lists
8982 .keys()
8983 .chain(st.mwnd_lists.keys())
8984 .chain(st.btnselitem_lists.keys())
8985 .copied()
8986 .collect();
8987 stage_ids.sort_unstable();
8988 stage_ids.dedup();
8989 for stage_idx in stage_ids {
8990 if let Some(list) = st.object_lists.get(&stage_idx) {
8991 for (obj_idx, obj) in list.iter().enumerate() {
8992 collect_render_sprite_source_for_object(
8993 ctx, form_id, stage_idx, obj_idx, obj, layer_id, sprite_id, "object",
8994 &mut found,
8995 );
8996 }
8997 }
8998 if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
8999 for (mwnd_idx, m) in mwnds.iter().enumerate() {
9000 for (obj_idx, obj) in m.button_list.iter().enumerate() {
9001 collect_render_sprite_source_for_object(
9002 ctx,
9003 form_id,
9004 stage_idx,
9005 obj_idx,
9006 obj,
9007 layer_id,
9008 sprite_id,
9009 &format!("mwnd{mwnd_idx}.button"),
9010 &mut found,
9011 );
9012 }
9013 for (obj_idx, obj) in m.face_list.iter().enumerate() {
9014 collect_render_sprite_source_for_object(
9015 ctx,
9016 form_id,
9017 stage_idx,
9018 obj_idx,
9019 obj,
9020 layer_id,
9021 sprite_id,
9022 &format!("mwnd{mwnd_idx}.face"),
9023 &mut found,
9024 );
9025 }
9026 for (obj_idx, obj) in m.object_list.iter().enumerate() {
9027 collect_render_sprite_source_for_object(
9028 ctx,
9029 form_id,
9030 stage_idx,
9031 obj_idx,
9032 obj,
9033 layer_id,
9034 sprite_id,
9035 &format!("mwnd{mwnd_idx}.object"),
9036 &mut found,
9037 );
9038 }
9039 }
9040 }
9041 }
9042 }
9043 if found.is_empty() {
9044 format!("unowned:{layer_id}/{sprite_id}")
9045 } else {
9046 found.join("|")
9047 }
9048}
9049
9050fn collect_render_sprite_source_for_object(
9051 ctx: &CommandContext,
9052 form_id: u32,
9053 stage_idx: i64,
9054 obj_idx: usize,
9055 obj: &globals::ObjectState,
9056 layer_id: LayerId,
9057 sprite_id: SpriteId,
9058 source_kind: &str,
9059 found: &mut Vec<String>,
9060) {
9061 let file = obj.file_name.as_deref().unwrap_or("-");
9062 if object_backend_owns_sprite(ctx, stage_idx, obj_idx, obj, layer_id, sprite_id) {
9063 found.push(format!(
9064 "form{form_id}:stage{stage_idx}:{source_kind}[{obj_idx}]:slot{}:file{}",
9065 effective_object_slot_for_trace(obj_idx, obj),
9066 file
9067 ));
9068 }
9069 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
9070 collect_render_sprite_source_for_object(
9071 ctx,
9072 form_id,
9073 stage_idx,
9074 child_idx,
9075 child,
9076 layer_id,
9077 sprite_id,
9078 &format!("{source_kind}[{obj_idx}].child"),
9079 found,
9080 );
9081 }
9082}
9083
9084fn effective_object_slot_for_trace(obj_idx: usize, obj: &globals::ObjectState) -> i64 {
9085 obj.runtime_slot_or(obj_idx) as i64
9086}
9087
9088fn layer_backed_object_sprite_bindings(
9089 backend: &globals::ObjectBackend,
9090) -> Vec<(LayerId, SpriteId)> {
9091 match backend {
9092 globals::ObjectBackend::Rect {
9093 layer_id,
9094 sprite_id,
9095 ..
9096 }
9097 | globals::ObjectBackend::Movie {
9098 layer_id,
9099 sprite_id,
9100 ..
9101 } => vec![(*layer_id, *sprite_id)],
9102 globals::ObjectBackend::String {
9103 layer_id,
9104 shadow_sprite_id,
9105 fuchi_sprite_id,
9106 sprite_id,
9107 glyphs,
9108 ..
9109 } => {
9110 if glyphs.is_empty() {
9111 vec![
9112 (*layer_id, *shadow_sprite_id),
9113 (*layer_id, *fuchi_sprite_id),
9114 (*layer_id, *sprite_id),
9115 ]
9116 } else {
9117 let mut bindings = Vec::with_capacity(glyphs.len() * 3);
9118 bindings.extend(
9119 glyphs
9120 .iter()
9121 .map(|glyph| (*layer_id, glyph.shadow_sprite_id)),
9122 );
9123 bindings.extend(
9124 glyphs
9125 .iter()
9126 .map(|glyph| (*layer_id, glyph.fuchi_sprite_id)),
9127 );
9128 bindings.extend(glyphs.iter().map(|glyph| (*layer_id, glyph.body_sprite_id)));
9129 bindings
9130 }
9131 }
9132 globals::ObjectBackend::Number {
9133 layer_id,
9134 sprite_ids,
9135 }
9136 | globals::ObjectBackend::Weather {
9137 layer_id,
9138 sprite_ids,
9139 } => sprite_ids.iter().map(|sid| (*layer_id, *sid)).collect(),
9140 globals::ObjectBackend::Gfx | globals::ObjectBackend::None => Vec::new(),
9141 }
9142}
9143
9144fn object_backend_sprite_layer_offset(
9145 ctx: &CommandContext,
9146 backend: &globals::ObjectBackend,
9147 sprite_id: Option<SpriteId>,
9148) -> i64 {
9149 let Some(sprite_id) = sprite_id else {
9150 return 0;
9151 };
9152 let globals::ObjectBackend::String {
9153 shadow_sprite_id,
9154 fuchi_sprite_id,
9155 sprite_id: body_sprite_id,
9156 glyphs,
9157 mwnd_layer_reps: true,
9158 ..
9159 } = backend
9160 else {
9161 return 0;
9162 };
9163 if glyphs
9164 .iter()
9165 .any(|glyph| glyph.shadow_sprite_id == sprite_id)
9166 || (glyphs.is_empty() && sprite_id == *shadow_sprite_id)
9167 {
9168 ctx.tables.mwnd_render.shadow_layer_rep
9169 } else if glyphs
9170 .iter()
9171 .any(|glyph| glyph.fuchi_sprite_id == sprite_id)
9172 || (glyphs.is_empty() && sprite_id == *fuchi_sprite_id)
9173 {
9174 ctx.tables.mwnd_render.fuchi_layer_rep
9175 } else if glyphs.iter().any(|glyph| glyph.body_sprite_id == sprite_id)
9176 || (glyphs.is_empty() && sprite_id == *body_sprite_id)
9177 {
9178 ctx.tables.mwnd_render.moji_layer_rep
9179 } else {
9180 0
9181 }
9182}
9183
9184fn object_backend_sprite_local_offset(
9185 obj: &globals::ObjectState,
9186 sprite_id: Option<SpriteId>,
9187) -> (i64, i64) {
9188 let Some(sprite_id) = sprite_id else {
9189 return (0, 0);
9190 };
9191 if let globals::ObjectBackend::Number { sprite_ids, .. } = &obj.backend {
9192 return sprite_ids
9193 .iter()
9194 .position(|id| *id == sprite_id)
9195 .and_then(|idx| {
9196 obj.runtime
9197 .number_sprite_offsets
9198 .get(idx)
9199 .copied()
9200 .flatten()
9201 })
9202 .map(|x| (i64::from(x), 0))
9203 .unwrap_or((0, 0));
9204 }
9205 let globals::ObjectBackend::String { glyphs, .. } = &obj.backend else {
9206 return (0, 0);
9207 };
9208 for glyph in glyphs {
9209 if glyph.shadow_sprite_id == sprite_id {
9210 return (glyph.shadow_local_x as i64, glyph.shadow_local_y as i64);
9211 }
9212 if glyph.fuchi_sprite_id == sprite_id {
9213 return (glyph.fuchi_local_x as i64, glyph.fuchi_local_y as i64);
9214 }
9215 if glyph.body_sprite_id == sprite_id {
9216 return (glyph.body_local_x as i64, glyph.body_local_y as i64);
9217 }
9218 }
9219 (0, 0)
9220}
9221
9222fn object_backend_owns_sprite(
9223 ctx: &CommandContext,
9224 stage_idx: i64,
9225 obj_idx: usize,
9226 obj: &globals::ObjectState,
9227 layer_id: LayerId,
9228 sprite_id: SpriteId,
9229) -> bool {
9230 match &obj.backend {
9231 globals::ObjectBackend::Gfx => {
9232 ctx.gfx
9233 .object_sprite_binding(stage_idx, effective_object_slot_for_trace(obj_idx, obj))
9234 == Some((layer_id, sprite_id))
9235 }
9236 globals::ObjectBackend::None => false,
9237 backend => layer_backed_object_sprite_bindings(backend)
9238 .into_iter()
9239 .any(|binding| binding == (layer_id, sprite_id)),
9240 }
9241}
9242
9243#[derive(Debug, Clone, Default)]
9244struct ObjectRenderInfo {
9245 runtime_slot: usize,
9246 used: bool,
9247 object_type: i64,
9248 disp: bool,
9249 x: i64,
9250 y: i64,
9251 x_rep: i64,
9252 y_rep: i64,
9253 z_rep: i64,
9254 order: i64,
9255 layer: i64,
9256 alpha: i64,
9257 tr: i64,
9258 tr_rep: i64,
9259 mono: i64,
9260 reverse: i64,
9261 bright: i64,
9262 dark: i64,
9263 color_rate: i64,
9264 color_add_r: i64,
9265 color_add_g: i64,
9266 color_add_b: i64,
9267 color_r: i64,
9268 color_g: i64,
9269 color_b: i64,
9270 z: i64,
9271 world_no: i64,
9272 center_x: i64,
9273 center_y: i64,
9274 center_z: i64,
9275 center_rep_x: i64,
9276 center_rep_y: i64,
9277 center_rep_z: i64,
9278 scale_x: i64,
9279 scale_y: i64,
9280 scale_z: i64,
9281 rotate_x: i64,
9282 rotate_y: i64,
9283 rotate_z: i64,
9284 culling: bool,
9285 alpha_test: bool,
9286 alpha_blend: bool,
9287 fog_use: bool,
9288 light_no: i64,
9289 blend: crate::layer::SpriteBlend,
9290 child_sort_type: i64,
9291 dst_clip: Option<ClipRect>,
9292 billboard: bool,
9293 file_name: Option<String>,
9294 mesh_animation: crate::mesh3d::MeshAnimationState,
9295}
9296
9297#[derive(Debug, Clone)]
9298struct ParentRenderState {
9299 world_no: i64,
9300 pos_x: f32,
9301 pos_y: f32,
9302 pos_z: f32,
9303 center_rep_x: f32,
9304 center_rep_y: f32,
9305 center_rep_z: f32,
9306 scale_x: f32,
9307 scale_y: f32,
9308 scale_z: f32,
9309 rotate_x: f32,
9310 rotate_y: f32,
9311 rotate_z: f32,
9312 tr: i32,
9313 mono: i32,
9314 reverse: i32,
9315 bright: i32,
9316 dark: i32,
9317 color_rate: i32,
9318 color_r: i32,
9319 color_g: i32,
9320 color_b: i32,
9321 color_add_r: i32,
9322 color_add_g: i32,
9323 color_add_b: i32,
9324 blend: crate::layer::SpriteBlend,
9325 dst_clip: Option<ClipRect>,
9326 mask_image_id: Option<ImageHandle>,
9327 mask_offset_x: i32,
9328 mask_offset_y: i32,
9329 tonecurve_image_id: Option<ImageHandle>,
9330 tonecurve_row: f32,
9331 tonecurve_sat: f32,
9332}
9333
9334fn object_runtime_slot(obj_idx: usize, obj: &globals::ObjectState) -> usize {
9335 obj.runtime_slot_or(obj_idx)
9336}
9337
9338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9339enum ButtonSeEvent {
9340 Hit,
9341 Push,
9342 Decide,
9343}
9344
9345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9346struct ButtonSortKey {
9347 order: i64,
9348 layer: i64,
9349}
9350
9351impl ButtonSortKey {
9352 fn display_tuple(self) -> String {
9353 format!("({}, {})", self.order, self.layer)
9354 }
9355}
9356
9357#[derive(Debug, Clone)]
9358struct ButtonVisualState {
9359 state: i64,
9360 action_no: i64,
9361 file_name: Option<String>,
9362 base_patno: i64,
9363 cut_no: i64,
9364}
9365
9366const TNM_BTN_STATE_NORMAL: i64 = 0;
9367const TNM_BTN_STATE_HIT: i64 = 1;
9368const TNM_BTN_STATE_PUSH: i64 = 2;
9369const TNM_BTN_STATE_SELECT: i64 = 3;
9370const TNM_BTN_STATE_DISABLE: i64 = 4;
9371
9372const TNM_SYSCOM_TYPE_NONE: i64 = 0;
9373const TNM_SYSCOM_TYPE_SAVE: i64 = 1;
9374const TNM_SYSCOM_TYPE_LOAD: i64 = 2;
9375const TNM_SYSCOM_TYPE_READ_SKIP: i64 = 3;
9376const TNM_SYSCOM_TYPE_AUTO_MODE: i64 = 4;
9377const TNM_SYSCOM_TYPE_RETURN_SEL: i64 = 5;
9378const TNM_SYSCOM_TYPE_HIDE_MWND: i64 = 6;
9379const TNM_SYSCOM_TYPE_MSG_BACK: i64 = 7;
9380const TNM_SYSCOM_TYPE_KOE_PLAY: i64 = 8;
9381const TNM_SYSCOM_TYPE_QUICK_SAVE: i64 = 9;
9382const TNM_SYSCOM_TYPE_QUICK_LOAD: i64 = 10;
9383const TNM_SYSCOM_TYPE_CONFIG: i64 = 11;
9384const TNM_SYSCOM_TYPE_LOCAL_EXTRA_SWITCH: i64 = 12;
9385const TNM_SYSCOM_TYPE_LOCAL_EXTRA_MODE: i64 = 13;
9386const TNM_SYSCOM_TYPE_GLOBAL_EXTRA_SWITCH: i64 = 14;
9387const TNM_SYSCOM_TYPE_GLOBAL_EXTRA_MODE: i64 = 15;
9388
9389#[derive(Debug, Clone, Copy)]
9390struct ButtonHitCandidate {
9391 button_no: i64,
9392 sort_key: ButtonSortKey,
9393 runtime_slot: usize,
9394 se_no: i64,
9395 was_hit: bool,
9396 event_enabled: bool,
9397}
9398
9399#[derive(Debug, Clone, Copy)]
9400struct ButtonOwnerInfo {
9401 button_no: i64,
9402 runtime_slot: usize,
9403 se_no: i64,
9404 was_hit: bool,
9405 event_enabled: bool,
9406}
9407
9408fn push_object_button_decided_action(
9409 obj: &globals::ObjectState,
9410 out: &mut Vec<globals::PendingButtonAction>,
9411) {
9412 if !obj.button.decided_action_scn_name.is_empty() {
9413 if sg_debug_enabled() {
9414 eprintln!(
9415 "[SG_DEBUG][BUTTON_TRACE][CALLBACK] enqueue user_call file={:?} button_no={} group_no={} action_no={} state={} hit={} pushed={} call={}::{}/{}",
9416 obj.file_name,
9417 obj.button.button_no,
9418 obj.button.group_no,
9419 obj.button.action_no,
9420 obj.button.state,
9421 obj.button.hit,
9422 obj.button.pushed,
9423 obj.button.decided_action_scn_name,
9424 obj.button.decided_action_cmd_name,
9425 obj.button.decided_action_z_no
9426 );
9427 }
9428 out.push(globals::PendingButtonAction {
9429 kind: globals::PendingButtonActionKind::UserCall {
9430 scn_name: obj.button.decided_action_scn_name.clone(),
9431 cmd_name: obj.button.decided_action_cmd_name.clone(),
9432 z_no: obj.button.decided_action_z_no,
9433 },
9434 });
9435 } else if obj.button.sys_type != 0 {
9436 if sg_debug_enabled() {
9437 eprintln!(
9438 "[SG_DEBUG][BUTTON_TRACE][CALLBACK] enqueue syscom file={:?} button_no={} group_no={} action_no={} state={} hit={} pushed={} sys_type={} sys_opt={} mode={}",
9439 obj.file_name,
9440 obj.button.button_no,
9441 obj.button.group_no,
9442 obj.button.action_no,
9443 obj.button.state,
9444 obj.button.hit,
9445 obj.button.pushed,
9446 obj.button.sys_type,
9447 obj.button.sys_type_opt,
9448 obj.button.mode
9449 );
9450 }
9451 out.push(globals::PendingButtonAction {
9452 kind: globals::PendingButtonActionKind::Syscom {
9453 sys_type: obj.button.sys_type,
9454 sys_type_opt: obj.button.sys_type_opt,
9455 mode: obj.button.mode,
9456 },
9457 });
9458 } else if sg_debug_enabled() {
9459 eprintln!(
9460 "[SG_DEBUG][BUTTON_TRACE][CALLBACK] no_callback file={:?} button_no={} group_no={} action_no={} state={} hit={} pushed={}",
9461 obj.file_name,
9462 obj.button.button_no,
9463 obj.button.group_no,
9464 obj.button.action_no,
9465 obj.button.state,
9466 obj.button.hit,
9467 obj.button.pushed
9468 );
9469 }
9470}
9471
9472fn syscom_feature_enabled_for_button(
9473 syscom: &globals::SyscomRuntimeState,
9474 button: &globals::ObjectButtonState,
9475 read_skip_runtime_enabled: bool,
9476) -> bool {
9477 match button.sys_type {
9478 TNM_SYSCOM_TYPE_NONE => true,
9479 TNM_SYSCOM_TYPE_SAVE => syscom.save_feature.check_enabled() != 0,
9480 TNM_SYSCOM_TYPE_LOAD => syscom.load_feature.check_enabled() != 0,
9481 TNM_SYSCOM_TYPE_READ_SKIP => read_skip_runtime_enabled,
9485 TNM_SYSCOM_TYPE_AUTO_MODE => syscom.auto_mode.check_enabled() != 0,
9486 TNM_SYSCOM_TYPE_RETURN_SEL => syscom.return_to_sel.check_enabled() != 0,
9487 TNM_SYSCOM_TYPE_HIDE_MWND => syscom.hide_mwnd.check_enabled() != 0,
9488 TNM_SYSCOM_TYPE_MSG_BACK => syscom.msg_back.check_enabled() != 0,
9489 TNM_SYSCOM_TYPE_KOE_PLAY => true,
9490 TNM_SYSCOM_TYPE_QUICK_SAVE => syscom.save_feature.check_enabled() != 0,
9491 TNM_SYSCOM_TYPE_QUICK_LOAD => syscom.load_feature.check_enabled() != 0,
9492 TNM_SYSCOM_TYPE_CONFIG => true,
9493 TNM_SYSCOM_TYPE_LOCAL_EXTRA_SWITCH => syscom.local_extra_switch.check_enabled() != 0,
9494 TNM_SYSCOM_TYPE_LOCAL_EXTRA_MODE => syscom.local_extra_mode.check_enabled() != 0,
9495 TNM_SYSCOM_TYPE_GLOBAL_EXTRA_SWITCH | TNM_SYSCOM_TYPE_GLOBAL_EXTRA_MODE => true,
9496 _ => true,
9497 }
9498}
9499
9500fn syscom_mode_for_button(
9501 syscom: &globals::SyscomRuntimeState,
9502 button: &globals::ObjectButtonState,
9503) -> i64 {
9504 match button.sys_type {
9505 TNM_SYSCOM_TYPE_READ_SKIP => i64::from(syscom.read_skip.onoff),
9506 TNM_SYSCOM_TYPE_AUTO_MODE => i64::from(syscom.auto_mode.onoff),
9507 TNM_SYSCOM_TYPE_LOCAL_EXTRA_SWITCH => i64::from(syscom.local_extra_switch.onoff),
9508 TNM_SYSCOM_TYPE_LOCAL_EXTRA_MODE => syscom.local_extra_mode.value,
9509 _ => 0,
9510 }
9511}
9512
9513fn button_syscom_mode_visible(
9514 syscom: &globals::SyscomRuntimeState,
9515 button: &globals::ObjectButtonState,
9516) -> bool {
9517 button.sys_type == TNM_SYSCOM_TYPE_NONE || syscom_mode_for_button(syscom, button) == button.mode
9518}
9519
9520fn mwnd_button_forced_disabled(
9521 syscom: &globals::SyscomRuntimeState,
9522 mwnd_button_idx: Option<usize>,
9523) -> bool {
9524 let Some(idx) = mwnd_button_idx else {
9527 return false;
9528 };
9529 if syscom.mwnd_btn_disable_all {
9530 return true;
9531 }
9532 syscom
9533 .mwnd_btn_disable
9534 .get(&(idx as i64))
9535 .copied()
9536 .unwrap_or(false)
9537}
9538
9539fn button_effective_disabled(
9540 syscom: &globals::SyscomRuntimeState,
9541 obj: &globals::ObjectState,
9542 mwnd_button_idx: Option<usize>,
9543 read_skip_runtime_enabled: bool,
9544) -> bool {
9545 button_disabled_reason(syscom, obj, mwnd_button_idx, read_skip_runtime_enabled).is_some()
9546}
9547
9548fn button_disabled_reason(
9549 syscom: &globals::SyscomRuntimeState,
9550 obj: &globals::ObjectState,
9551 mwnd_button_idx: Option<usize>,
9552 read_skip_runtime_enabled: bool,
9553) -> Option<&'static str> {
9554 if obj.button.is_disabled() {
9555 return Some("object_state_disable");
9556 }
9557 if mwnd_button_forced_disabled(syscom, mwnd_button_idx) {
9558 return Some("syscom_mwnd_button_disable");
9559 }
9560 if !syscom_feature_enabled_for_button(syscom, &obj.button, read_skip_runtime_enabled) {
9561 return Some("syscom_feature_disable");
9562 }
9563 None
9564}
9565
9566fn button_state_name(state: i64) -> &'static str {
9567 match state {
9568 TNM_BTN_STATE_NORMAL => "normal",
9569 TNM_BTN_STATE_HIT => "hit",
9570 TNM_BTN_STATE_PUSH => "push",
9571 TNM_BTN_STATE_SELECT => "select",
9572 TNM_BTN_STATE_DISABLE => "disable",
9573 _ => "unknown",
9574 }
9575}
9576
9577fn object_button_renderable_by_syscom(
9578 syscom: &globals::SyscomRuntimeState,
9579 obj: &globals::ObjectState,
9580) -> bool {
9581 !obj.button.enabled || button_syscom_mode_visible(syscom, &obj.button)
9582}
9583
9584fn button_real_state_for_visual(
9585 syscom: &globals::SyscomRuntimeState,
9586 st: &globals::StageFormState,
9587 stage_idx: i64,
9588 obj: &globals::ObjectState,
9589 mwnd_button_idx: Option<usize>,
9590 read_skip_runtime_enabled: bool,
9591) -> i64 {
9592 if let Some(reason) =
9593 button_disabled_reason(syscom, obj, mwnd_button_idx, read_skip_runtime_enabled)
9594 {
9595 if sg_debug_enabled() {
9596 eprintln!(
9597 "[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={}",
9598 reason,
9599 stage_idx,
9600 obj.file_name,
9601 mwnd_button_idx,
9602 obj.button.button_no,
9603 obj.button.group_no,
9604 obj.button.group_idx(),
9605 obj.button.action_no,
9606 obj.button.state,
9607 obj.button.enabled,
9608 obj.button.hit,
9609 obj.button.pushed,
9610 obj.button.sys_type,
9611 obj.button.sys_type_opt,
9612 obj.button.mode,
9613 syscom.mwnd_btn_touch_disable
9614 );
9615 }
9616 return TNM_BTN_STATE_DISABLE;
9617 }
9618 if obj.button.state == TNM_BTN_STATE_SELECT || obj.button.state == TNM_BTN_STATE_DISABLE {
9619 return obj.button.state;
9620 }
9621 if syscom.mwnd_btn_touch_disable {
9622 if sg_debug_enabled() && obj.button.enabled {
9623 eprintln!(
9624 "[SG_DEBUG][BUTTON_TRACE][VISUAL] real_state=normal reason=touch_disable stage={} file={:?} mwnd_button_idx={:?} button_no={} group_no={} action_no={}",
9625 stage_idx,
9626 obj.file_name,
9627 mwnd_button_idx,
9628 obj.button.button_no,
9629 obj.button.group_no,
9630 obj.button.action_no
9631 );
9632 }
9633 return TNM_BTN_STATE_NORMAL;
9634 }
9635 if let Some(gidx) = obj.button.group_idx() {
9636 if let Some(gl) = st
9637 .group_lists
9638 .get(&stage_idx)
9639 .and_then(|groups| groups.get(gidx))
9640 {
9641 if gl.decided_button_no == obj.button.button_no {
9642 return TNM_BTN_STATE_PUSH;
9643 }
9644 if gl.hit_button_no == obj.button.button_no {
9645 return TNM_BTN_STATE_HIT;
9646 }
9647 if gl.pushed_button_no == obj.button.button_no {
9648 return TNM_BTN_STATE_PUSH;
9649 }
9650 }
9651 } else if obj.button.pushed {
9652 return TNM_BTN_STATE_PUSH;
9653 } else if obj.button.hit {
9654 return TNM_BTN_STATE_HIT;
9655 }
9656 TNM_BTN_STATE_NORMAL
9657}
9658
9659fn collect_button_decided_action_by_runtime_slot_recursive(
9660 obj_idx: usize,
9661 obj: &globals::ObjectState,
9662 runtime_slot: usize,
9663 out: &mut Vec<globals::PendingButtonAction>,
9664) -> bool {
9665 if object_runtime_slot(obj_idx, obj) == runtime_slot {
9666 if obj.button.enabled && obj.button.action_no >= 0 {
9667 push_object_button_decided_action(obj, out);
9668 }
9669 return true;
9670 }
9671 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
9672 if collect_button_decided_action_by_runtime_slot_recursive(
9673 child_idx,
9674 child,
9675 runtime_slot,
9676 out,
9677 ) {
9678 return true;
9679 }
9680 }
9681 false
9682}
9683
9684fn button_event_enabled_by_runtime_slot_recursive(
9685 obj_idx: usize,
9686 obj: &globals::ObjectState,
9687 runtime_slot: usize,
9688 syscom: &globals::SyscomRuntimeState,
9689 read_skip_runtime_enabled: bool,
9690) -> Option<bool> {
9691 if object_runtime_slot(obj_idx, obj) == runtime_slot {
9692 return Some(button_event_enabled(
9693 syscom,
9694 obj,
9695 None,
9696 read_skip_runtime_enabled,
9697 ));
9698 }
9699 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
9700 if let Some(enabled) = button_event_enabled_by_runtime_slot_recursive(
9701 child_idx,
9702 child,
9703 runtime_slot,
9704 syscom,
9705 read_skip_runtime_enabled,
9706 ) {
9707 return Some(enabled);
9708 }
9709 }
9710 None
9711}
9712
9713fn button_event_enabled_in_list_by_runtime_slot(
9714 objs: &[globals::ObjectState],
9715 runtime_slot: usize,
9716 syscom: &globals::SyscomRuntimeState,
9717 read_skip_runtime_enabled: bool,
9718) -> bool {
9719 objs.iter()
9720 .enumerate()
9721 .find_map(|(obj_idx, obj)| {
9722 button_event_enabled_by_runtime_slot_recursive(
9723 obj_idx,
9724 obj,
9725 runtime_slot,
9726 syscom,
9727 read_skip_runtime_enabled,
9728 )
9729 })
9730 .unwrap_or(false)
9731}
9732
9733fn find_button_se_no_by_runtime_slot_recursive(
9734 obj_idx: usize,
9735 obj: &globals::ObjectState,
9736 runtime_slot: usize,
9737) -> Option<i64> {
9738 if object_runtime_slot(obj_idx, obj) == runtime_slot {
9739 return (obj.button.enabled && obj.button.action_no >= 0).then_some(obj.button.se_no);
9740 }
9741 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
9742 if let Some(se_no) =
9743 find_button_se_no_by_runtime_slot_recursive(child_idx, child, runtime_slot)
9744 {
9745 return Some(se_no);
9746 }
9747 }
9748 None
9749}
9750
9751fn find_button_se_no_in_list_by_runtime_slot(
9752 objs: &[globals::ObjectState],
9753 runtime_slot: usize,
9754) -> Option<i64> {
9755 for (obj_idx, obj) in objs.iter().enumerate() {
9756 if let Some(se_no) = find_button_se_no_by_runtime_slot_recursive(obj_idx, obj, runtime_slot)
9757 {
9758 return Some(se_no);
9759 }
9760 }
9761 None
9762}
9763
9764fn set_button_pushed_by_runtime_slot_recursive(
9765 obj_idx: usize,
9766 obj: &mut globals::ObjectState,
9767 runtime_slot: usize,
9768) -> bool {
9769 if object_runtime_slot(obj_idx, obj) == runtime_slot {
9770 if obj.button.enabled {
9771 obj.button.last_pushed = obj.button.pushed;
9772 obj.button.pushed = true;
9773 }
9774 return true;
9775 }
9776 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
9777 if set_button_pushed_by_runtime_slot_recursive(child_idx, child, runtime_slot) {
9778 return true;
9779 }
9780 }
9781 false
9782}
9783
9784fn pushed_group_button_recursive(
9785 obj_idx: usize,
9786 obj: &globals::ObjectState,
9787 group_idx: usize,
9788) -> Option<(i64, usize)> {
9789 if obj.button.enabled && obj.button.pushed && obj.button.group_idx() == Some(group_idx) {
9790 return Some((obj.button.button_no, object_runtime_slot(obj_idx, obj)));
9791 }
9792 obj.runtime
9793 .child_objects
9794 .iter()
9795 .enumerate()
9796 .find_map(|(idx, child)| pushed_group_button_recursive(idx, child, group_idx))
9797}
9798
9799fn object_button_push_keep_by_runtime_slot_recursive(
9800 obj_idx: usize,
9801 obj: &globals::ObjectState,
9802 runtime_slot: usize,
9803) -> bool {
9804 if object_runtime_slot(obj_idx, obj) == runtime_slot {
9805 return obj.button.enabled && obj.button.push_keep;
9806 }
9807 obj.runtime
9808 .child_objects
9809 .iter()
9810 .enumerate()
9811 .any(|(child_idx, child)| {
9812 object_button_push_keep_by_runtime_slot_recursive(child_idx, child, runtime_slot)
9813 })
9814}
9815
9816fn object_button_push_keep_in_list_by_runtime_slot(
9817 objs: &[globals::ObjectState],
9818 runtime_slot: usize,
9819) -> bool {
9820 objs.iter().enumerate().any(|(obj_idx, obj)| {
9821 object_button_push_keep_by_runtime_slot_recursive(obj_idx, obj, runtime_slot)
9822 })
9823}
9824
9825fn clear_button_hit_recursive(obj: &mut globals::ObjectState) {
9826 if obj.button.enabled {
9827 obj.button.last_hit = obj.button.hit;
9828 obj.button.hit = false;
9829 }
9830 for child in &mut obj.runtime.child_objects {
9831 clear_button_hit_recursive(child);
9832 }
9833}
9834
9835fn set_button_hit_by_runtime_slot_recursive(
9836 obj_idx: usize,
9837 obj: &mut globals::ObjectState,
9838 runtime_slot: usize,
9839) -> bool {
9840 if object_runtime_slot(obj_idx, obj) == runtime_slot {
9841 obj.button.hit = true;
9842 return true;
9843 }
9844 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
9845 if set_button_hit_by_runtime_slot_recursive(child_idx, child, runtime_slot) {
9846 return true;
9847 }
9848 }
9849 false
9850}
9851
9852fn set_button_pushed_recursive(obj: &mut globals::ObjectState, group_idx: usize, button_no: i64) {
9853 if obj.button.enabled
9854 && obj.button.group_idx() == Some(group_idx)
9855 && obj.button.button_no == button_no
9856 {
9857 obj.button.last_pushed = obj.button.pushed;
9858 obj.button.pushed = true;
9859 }
9860 for child in &mut obj.runtime.child_objects {
9861 set_button_pushed_recursive(child, group_idx, button_no);
9862 }
9863}
9864
9865fn mark_standalone_button_pushed_from_hit_recursive(
9866 _obj_idx: usize,
9867 obj: &mut globals::ObjectState,
9868 syscom: &globals::SyscomRuntimeState,
9869 read_skip_runtime_enabled: bool,
9870 mwnd_button_idx: Option<usize>,
9871) -> Option<i64> {
9872 if has_standalone_button_registration(obj) && obj.button.hit {
9873 let was_pushed = obj.button.pushed;
9874 obj.button.last_pushed = obj.button.pushed;
9875 obj.button.pushed = true;
9876 if !was_pushed
9877 && button_event_enabled(syscom, obj, mwnd_button_idx, read_skip_runtime_enabled)
9878 {
9879 return Some(obj.button.se_no);
9880 }
9881 }
9882 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
9883 if let Some(se_no) = mark_standalone_button_pushed_from_hit_recursive(
9884 child_idx,
9885 child,
9886 syscom,
9887 read_skip_runtime_enabled,
9888 mwnd_button_idx,
9889 ) {
9890 return Some(se_no);
9891 }
9892 }
9893 None
9894}
9895
9896fn registered_button_hit_recursive(obj: &globals::ObjectState) -> bool {
9897 if obj.button.enabled && obj.button.action_no >= 0 && obj.button.hit {
9898 return true;
9899 }
9900 obj.runtime
9901 .child_objects
9902 .iter()
9903 .any(registered_button_hit_recursive)
9904}
9905
9906fn standalone_button_hit_recursive(obj: &globals::ObjectState) -> bool {
9907 if has_standalone_button_registration(obj) && obj.button.hit {
9908 return true;
9909 }
9910 obj.runtime
9911 .child_objects
9912 .iter()
9913 .any(standalone_button_hit_recursive)
9914}
9915
9916fn standalone_button_pushed_recursive(obj: &globals::ObjectState) -> bool {
9917 if has_standalone_button_registration(obj) && obj.button.pushed {
9918 return true;
9919 }
9920 obj.runtime
9921 .child_objects
9922 .iter()
9923 .any(standalone_button_pushed_recursive)
9924}
9925
9926fn clear_button_pushed_recursive(obj: &mut globals::ObjectState) {
9927 if obj.button.enabled {
9928 obj.button.last_pushed = obj.button.pushed;
9929 obj.button.pushed = false;
9930 }
9931 for child in &mut obj.runtime.child_objects {
9932 clear_button_pushed_recursive(child);
9933 }
9934}
9935
9936fn object_button_push_keep_recursive(
9937 obj: &globals::ObjectState,
9938 group_idx: usize,
9939 button_no: i64,
9940) -> bool {
9941 if obj.button.enabled
9942 && obj.button.group_idx() == Some(group_idx)
9943 && obj.button.button_no == button_no
9944 && obj.button.push_keep
9945 {
9946 return true;
9947 }
9948 obj.runtime
9949 .child_objects
9950 .iter()
9951 .any(|child| object_button_push_keep_recursive(child, group_idx, button_no))
9952}
9953
9954fn hit_test_render_sprite(
9955 images: &mut ImageManager,
9956 sprite: &Sprite,
9957 mx: i32,
9958 my: i32,
9959 alpha_test: bool,
9960) -> bool {
9961 if !sprite.visible || sprite.tr == 0 {
9962 return false;
9963 }
9964 if let Some(clip) = sprite.dst_clip
9965 && (mx < clip.left || my < clip.top || mx >= clip.right || my >= clip.bottom)
9966 {
9967 return false;
9968 }
9969 let image = sprite
9970 .image_id
9971 .as_ref()
9972 .and_then(|img_id| images.get(img_id));
9973 let img = image.as_deref();
9974 let emote = sprite.emote_render.as_deref();
9975 if img.is_none() && emote.is_none() {
9976 return false;
9977 }
9978 let (intrinsic_w, intrinsic_h) = if let Some(img) = img {
9979 (img.width, img.height)
9980 } else if let Some(packet) = emote {
9981 (packet.width, packet.height)
9982 } else {
9983 return false;
9984 };
9985 let (w, h) = match sprite.size_mode {
9986 SpriteSizeMode::Intrinsic => (intrinsic_w as f32, intrinsic_h as f32),
9987 SpriteSizeMode::Explicit { width, height } => (width as f32, height as f32),
9988 };
9989 let (anchor_x, anchor_y) = match sprite.fit {
9990 SpriteFit::PixelRect => (sprite.x as f32, sprite.y as f32),
9991 SpriteFit::FullScreen => (0.0, 0.0),
9992 };
9993 if sprite.scale_x == 0.0 || sprite.scale_y == 0.0 {
9994 return false;
9995 }
9996 let (origin_x, origin_y) = if sprite.object_anchor {
9997 (anchor_x, anchor_y)
9998 } else {
9999 (anchor_x + sprite.pivot_x, anchor_y + sprite.pivot_y)
10000 };
10001 let mut px = mx as f32 - origin_x;
10002 let mut py = my as f32 - origin_y;
10003 if sprite.rotate != 0.0 {
10004 let (s, c) = (-sprite.rotate).sin_cos();
10005 let rx = px * c - py * s;
10006 let ry = px * s + py * c;
10007 px = rx;
10008 py = ry;
10009 }
10010 let (tex_center_x, tex_center_y) = if sprite.object_anchor {
10011 (sprite.texture_center_x, sprite.texture_center_y)
10012 } else {
10013 (0.0, 0.0)
10014 };
10015 let local_x = px / sprite.scale_x + sprite.pivot_x + tex_center_x;
10016 let local_y = py / sprite.scale_y + sprite.pivot_y + tex_center_y;
10017 if !(0.0 <= local_x && local_x < w && 0.0 <= local_y && local_y < h) {
10018 return false;
10019 }
10020 if alpha_test {
10021 let (sx, sy) = match sprite.src_clip {
10022 Some(src) => (
10023 src.left.saturating_add(local_x.floor() as i32),
10024 src.top.saturating_add(local_y.floor() as i32),
10025 ),
10026 None => (local_x.floor() as i32, local_y.floor() as i32),
10027 };
10028 let opaque = if let Some(img) = img {
10029 CommandContext::alpha_test_image(img, sx, sy)
10030 } else if let Some(packet) = emote {
10031 packet.alpha_hit_test(sx, sy)
10032 } else {
10033 false
10034 };
10035 if !opaque {
10036 return false;
10037 }
10038 }
10039 true
10040}
10041
10042fn hit_test_layer_sprite(
10043 images: &mut ImageManager,
10044 layers: &LayerManager,
10045 layer_id: LayerId,
10046 sprite_id: SpriteId,
10047 mx: i32,
10048 my: i32,
10049 alpha_test: bool,
10050) -> bool {
10051 let Some(spr) = layers.layer(layer_id).and_then(|l| l.sprite(sprite_id)) else {
10052 return false;
10053 };
10054 hit_test_render_sprite(images, spr, mx, my, alpha_test)
10055}
10056
10057fn object_button_sort_key(
10058 ids: &constants::RuntimeConstants,
10059 gfx: &graphics::GfxRuntime,
10060 stage_idx: i64,
10061 runtime_slot: usize,
10062 obj: &globals::ObjectState,
10063) -> ButtonSortKey {
10064 let embedded_tree_object = obj.nested_runtime_slot.is_some();
10065 let layer = obj
10066 .lookup_int_prop(ids, ids.obj_layer)
10067 .or_else(|| {
10068 if embedded_tree_object {
10069 None
10070 } else {
10071 gfx.object_peek_layer(stage_idx, runtime_slot as i64)
10072 }
10073 })
10074 .unwrap_or(obj.base.layer);
10075 let order = obj
10076 .lookup_int_prop(ids, ids.obj_order)
10077 .or_else(|| {
10078 if embedded_tree_object {
10079 None
10080 } else {
10081 gfx.object_peek_order(stage_idx, runtime_slot as i64)
10082 }
10083 })
10084 .unwrap_or(obj.base.order);
10085 ButtonSortKey { order, layer }
10086}
10087
10088fn button_sort_ge(lhs: ButtonSortKey, rhs: ButtonSortKey) -> bool {
10089 lhs.order > rhs.order || (lhs.order == rhs.order && lhs.layer >= rhs.layer)
10090}
10091
10092fn has_standalone_button_registration(obj: &globals::ObjectState) -> bool {
10093 obj.button.enabled && obj.button.group_idx().is_none() && obj.button.action_no >= 0
10097}
10098
10099fn has_standalone_button_action(obj: &globals::ObjectState) -> bool {
10100 has_standalone_button_registration(obj) && !obj.button.is_disabled()
10101}
10102
10103fn button_event_enabled(
10104 syscom: &globals::SyscomRuntimeState,
10105 obj: &globals::ObjectState,
10106 mwnd_button_idx: Option<usize>,
10107 read_skip_runtime_enabled: bool,
10108) -> bool {
10109 !syscom.mwnd_btn_touch_disable
10110 && !button_effective_disabled(syscom, obj, mwnd_button_idx, read_skip_runtime_enabled)
10111}
10112
10113fn merge_button_hit(
10114 best: &mut Option<ButtonHitCandidate>,
10115 tied: &mut bool,
10116 hit: ButtonHitCandidate,
10117) {
10118 match *best {
10119 None => {
10120 *best = Some(hit);
10121 *tied = false;
10122 }
10123 Some(prev) if button_sort_ge(hit.sort_key, prev.sort_key) => {
10124 *best = Some(hit);
10127 *tied = false;
10128 }
10129 _ => {}
10130 }
10131}
10132
10133fn object_event_value(
10134 ids: &constants::RuntimeConstants,
10135 obj: &globals::ObjectState,
10136 event_op: i32,
10137 current: i64,
10138) -> i64 {
10139 if event_op != 0 {
10140 obj.int_event_by_op(ids, event_op)
10141 .map(|ev| ev.get_total_value() as i64)
10142 .unwrap_or(current)
10143 } else {
10144 current
10145 }
10146}
10147
10148fn object_button_effective_gfx_hit(
10149 images: &mut ImageManager,
10150 layers: &LayerManager,
10151 gfx: &graphics::GfxRuntime,
10152 ids: &constants::RuntimeConstants,
10153 stage_idx: i64,
10154 runtime_slot: usize,
10155 obj: &globals::ObjectState,
10156 mx: i32,
10157 my: i32,
10158 parent_state: Option<ParentRenderState>,
10159) -> Option<ButtonSortKey> {
10160 let embedded_tree_object = obj.nested_runtime_slot.is_some();
10161 let disp = obj
10162 .lookup_int_prop(ids, ids.obj_disp)
10163 .or_else(|| {
10164 if embedded_tree_object {
10165 None
10166 } else {
10167 gfx.object_peek_disp(stage_idx, runtime_slot as i64)
10168 }
10169 })
10170 .unwrap_or(obj.base.disp);
10171 if disp == 0 {
10172 return None;
10173 }
10174
10175 let mut tr = obj.lookup_int_prop(ids, ids.obj_tr).unwrap_or(obj.base.tr);
10176 tr = object_event_value(ids, obj, ids.obj_tr_eve, tr);
10177 tr = obj
10178 .runtime
10179 .prop_event_lists
10180 .tr_rep
10181 .iter()
10182 .fold(tr, |acc, ev| {
10183 acc.saturating_mul(ev.get_total_value() as i64)
10184 .div_euclid(255)
10185 });
10186 if tr <= 0 {
10187 return None;
10188 }
10189
10190 if parent_state.is_none()
10191 && let Some((layer_id, sprite_id)) =
10192 gfx.object_sprite_binding(stage_idx, runtime_slot as i64)
10193 {
10194 if hit_test_layer_sprite(
10195 images,
10196 layers,
10197 layer_id,
10198 sprite_id,
10199 mx,
10200 my,
10201 obj.button.alpha_test,
10202 ) {
10203 return Some(object_button_sort_key(
10204 ids,
10205 gfx,
10206 stage_idx,
10207 runtime_slot,
10208 obj,
10209 ));
10210 }
10211 return None;
10212 }
10213
10214 let (base_x, base_y) = if embedded_tree_object {
10215 (obj.base.x, obj.base.y)
10216 } else {
10217 gfx.object_peek_pos(stage_idx, runtime_slot as i64)
10218 .unwrap_or((obj.base.x, obj.base.y))
10219 };
10220 let mut x = obj.lookup_int_prop(ids, ids.obj_x).unwrap_or(base_x);
10221 let mut y = obj.lookup_int_prop(ids, ids.obj_y).unwrap_or(base_y);
10222 x = object_event_value(ids, obj, ids.obj_x_eve, x);
10223 y = object_event_value(ids, obj, ids.obj_y_eve, y);
10224 x += obj
10225 .runtime
10226 .prop_event_lists
10227 .x_rep
10228 .iter()
10229 .map(|ev| ev.get_total_value() as i64)
10230 .sum::<i64>();
10231 y += obj
10232 .runtime
10233 .prop_event_lists
10234 .y_rep
10235 .iter()
10236 .map(|ev| ev.get_total_value() as i64)
10237 .sum::<i64>();
10238
10239 let mut scale_x = obj
10240 .lookup_int_prop(ids, ids.obj_scale_x)
10241 .unwrap_or(obj.base.scale_x);
10242 let mut scale_y = obj
10243 .lookup_int_prop(ids, ids.obj_scale_y)
10244 .unwrap_or(obj.base.scale_y);
10245 scale_x = object_event_value(ids, obj, ids.obj_scale_x_eve, scale_x);
10246 scale_y = object_event_value(ids, obj, ids.obj_scale_y_eve, scale_y);
10247 if scale_x == 0 || scale_y == 0 {
10248 return None;
10249 }
10250
10251 let mut patno = obj
10252 .lookup_int_prop(ids, ids.obj_patno)
10253 .or_else(|| gfx.object_peek_patno(stage_idx, runtime_slot as i64))
10254 .unwrap_or(obj.base.patno);
10255 patno = object_event_value(ids, obj, ids.obj_patno_eve, patno);
10256 patno = patno.saturating_add(obj.button.cut_no);
10257
10258 let file_name = obj.file_name.as_ref()?;
10259 let img_id = CommandContext::load_any_image_for_hit(images, file_name.as_str(), patno)?;
10260
10261 let mut sprite = Sprite {
10262 image_id: Some(img_id.clone()),
10263 ..Default::default()
10264 };
10265
10266 if let Some(img) = images.get(&img_id) {
10267 sprite.object_anchor = true;
10268 sprite.texture_center_x = img.center_x as f32;
10269 sprite.texture_center_y = img.center_y as f32;
10270 } else {
10271 sprite.object_anchor = false;
10272 sprite.texture_center_x = 0.0;
10273 sprite.texture_center_y = 0.0;
10274 }
10275 sprite.visible = true;
10276 let center_x = obj
10277 .lookup_int_prop(ids, ids.obj_center_x)
10278 .unwrap_or(obj.base.center_x);
10279 let center_y = obj
10280 .lookup_int_prop(ids, ids.obj_center_y)
10281 .unwrap_or(obj.base.center_y);
10282 let center_z = obj
10283 .lookup_int_prop(ids, ids.obj_center_z)
10284 .unwrap_or(obj.base.center_z);
10285 let center_rep_x = obj
10286 .lookup_int_prop(ids, ids.obj_center_rep_x)
10287 .unwrap_or(obj.base.center_rep_x);
10288 let center_rep_y = obj
10289 .lookup_int_prop(ids, ids.obj_center_rep_y)
10290 .unwrap_or(obj.base.center_rep_y);
10291 let center_rep_z = obj
10292 .lookup_int_prop(ids, ids.obj_center_rep_z)
10293 .unwrap_or(obj.base.center_rep_z);
10294 sprite.x = x as i32;
10295 sprite.y = y as i32;
10296 sprite.pivot_x = (center_x + center_rep_x) as f32;
10297 sprite.pivot_y = (center_y + center_rep_y) as f32;
10298 sprite.pivot_z = (center_z + center_rep_z) as f32;
10299 sprite.scale_x = scale_x as f32 / 1000.0;
10300 sprite.scale_y = scale_y as f32 / 1000.0;
10301 sprite.tr = tr.clamp(0, 255) as u8;
10302 if let Some(parent) = parent_state {
10303 let dummy = ObjectRenderInfo::default();
10304 apply_parent_render_state_to_sprite(&mut sprite, &dummy, &parent);
10305 }
10306 sprite.x = (sprite.x as i64 + center_rep_x).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
10307 sprite.y = (sprite.y as i64 + center_rep_y).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
10308 sprite.z += center_rep_z as f32;
10309
10310 if !hit_test_render_sprite(images, &sprite, mx, my, obj.button.alpha_test) {
10311 return None;
10312 }
10313
10314 Some(object_button_sort_key(
10315 ids,
10316 gfx,
10317 stage_idx,
10318 runtime_slot,
10319 obj,
10320 ))
10321}
10322
10323fn collect_standalone_button_decided_actions_recursive(
10324 obj: &globals::ObjectState,
10325 out: &mut Vec<globals::PendingButtonAction>,
10326 sounds: &mut Vec<i64>,
10327 syscom: &globals::SyscomRuntimeState,
10328 read_skip_runtime_enabled: bool,
10329 mwnd_button_idx: Option<usize>,
10330) {
10331 if has_standalone_button_registration(obj)
10332 && obj.button.pushed
10333 && (obj.button.hit || obj.button.push_keep)
10334 && button_event_enabled(syscom, obj, mwnd_button_idx, read_skip_runtime_enabled)
10335 {
10336 push_object_button_decided_action(obj, out);
10337 sounds.push(obj.button.se_no);
10338 }
10339 for child in &obj.runtime.child_objects {
10340 collect_standalone_button_decided_actions_recursive(
10341 child,
10342 out,
10343 sounds,
10344 syscom,
10345 read_skip_runtime_enabled,
10346 mwnd_button_idx,
10347 );
10348 }
10349}
10350
10351#[derive(Debug, Clone, Copy)]
10352struct ButtonObjectRenderInfo {
10353 disp: bool,
10354 x: i64,
10355 y: i64,
10356 z: i64,
10357 x_rep: i64,
10358 y_rep: i64,
10359 z_rep: i64,
10360 center_x: i64,
10361 center_y: i64,
10362 center_z: i64,
10363 center_rep_x: i64,
10364 center_rep_y: i64,
10365 center_rep_z: i64,
10366 scale_x: i64,
10367 scale_y: i64,
10368 scale_z: i64,
10369 rotate_x: i64,
10370 rotate_y: i64,
10371 rotate_z: i64,
10372 tr: i64,
10373 tr_rep: i64,
10374 world_no: i64,
10375 dst_clip: Option<ClipRect>,
10376}
10377
10378fn fetch_bound_render_sprites_for_hit(
10379 layers: &LayerManager,
10380 gfx: &graphics::GfxRuntime,
10381 stage_idx: i64,
10382 runtime_slot: usize,
10383 obj: &globals::ObjectState,
10384) -> Vec<RenderSprite> {
10385 fn push_one(layers: &LayerManager, lid: LayerId, sid: SpriteId, out: &mut Vec<RenderSprite>) {
10386 let Some(layer) = layers.layer(lid) else {
10387 return;
10388 };
10389 let Some(sprite) = layer.sprite(sid) else {
10390 return;
10391 };
10392 if sprite.image_id.is_none() && sprite.emote_render.is_none() {
10393 return;
10394 }
10395 out.push(RenderSprite::new(Some(lid), Some(sid), sprite.clone()));
10396 }
10397 let mut out = Vec::new();
10398 match &obj.backend {
10399 globals::ObjectBackend::Gfx => {
10400 if let Some((lid, sid)) = gfx.object_sprite_binding(stage_idx, runtime_slot as i64) {
10401 push_one(layers, lid, sid, &mut out);
10402 }
10403 }
10404 globals::ObjectBackend::None => {}
10405 backend => {
10406 for (layer_id, sprite_id) in layer_backed_object_sprite_bindings(backend) {
10407 push_one(layers, layer_id, sprite_id, &mut out);
10408 }
10409 }
10410 }
10411 out
10412}
10413
10414fn first_bound_render_sprite<'a>(
10418 layers: &'a LayerManager,
10419 gfx: &graphics::GfxRuntime,
10420 stage_idx: i64,
10421 runtime_slot: usize,
10422 obj: &globals::ObjectState,
10423) -> Option<&'a Sprite> {
10424 let pick = |lid: LayerId, sid: SpriteId| {
10425 layers
10426 .layer(lid)?
10427 .sprite(sid)
10428 .filter(|sprite| sprite.image_id.is_some() || sprite.emote_render.is_some())
10429 };
10430 match &obj.backend {
10431 globals::ObjectBackend::Gfx => gfx
10432 .object_sprite_binding(stage_idx, runtime_slot as i64)
10433 .and_then(|(lid, sid)| pick(lid, sid)),
10434 globals::ObjectBackend::None => None,
10435 backend => layer_backed_object_sprite_bindings(backend)
10436 .into_iter()
10437 .find_map(|(lid, sid)| pick(lid, sid)),
10438 }
10439}
10440
10441fn button_object_render_info(
10442 ids: &constants::RuntimeConstants,
10443 gfx: &graphics::GfxRuntime,
10444 stage_idx: i64,
10445 obj_idx: usize,
10446 obj: &globals::ObjectState,
10447) -> ButtonObjectRenderInfo {
10448 let runtime_slot = object_runtime_slot(obj_idx, obj);
10449 let embedded_tree_object = obj.nested_runtime_slot.is_some();
10450 let use_gfx_object_state =
10451 matches!(obj.backend, globals::ObjectBackend::Gfx) && !embedded_tree_object;
10452 let extra = |id: i32, default: i64| -> i64 {
10453 if id != 0 {
10454 obj.lookup_int_prop(ids, id).unwrap_or(default)
10455 } else {
10456 default
10457 }
10458 };
10459 let gfx_disp = || {
10460 if use_gfx_object_state {
10461 gfx.object_peek_disp(stage_idx, runtime_slot as i64)
10462 } else {
10463 None
10464 }
10465 };
10466 let gfx_pos = || {
10467 if use_gfx_object_state {
10468 gfx.object_peek_pos(stage_idx, runtime_slot as i64)
10469 } else {
10470 None
10471 }
10472 };
10473 let x_rep = obj
10474 .runtime
10475 .prop_event_lists
10476 .x_rep
10477 .iter()
10478 .map(|ev| ev.get_total_value() as i64)
10479 .sum::<i64>();
10480 let y_rep = obj
10481 .runtime
10482 .prop_event_lists
10483 .y_rep
10484 .iter()
10485 .map(|ev| ev.get_total_value() as i64)
10486 .sum::<i64>();
10487 let z_rep = obj
10488 .runtime
10489 .prop_event_lists
10490 .z_rep
10491 .iter()
10492 .map(|ev| ev.get_total_value() as i64)
10493 .sum::<i64>();
10494 let tr_rep = obj
10495 .runtime
10496 .prop_event_lists
10497 .tr_rep
10498 .iter()
10499 .fold(255i64, |acc, ev| {
10500 acc.saturating_mul(ev.get_total_value() as i64)
10501 .div_euclid(255)
10502 });
10503 let dst_clip = if extra(ids.obj_clip_use, obj.base.clip_use) != 0 {
10504 Some(ClipRect {
10505 left: extra(ids.obj_clip_left, obj.base.clip_left) as i32,
10506 top: extra(ids.obj_clip_top, obj.base.clip_top) as i32,
10507 right: extra(ids.obj_clip_right, obj.base.clip_right) as i32,
10508 bottom: extra(ids.obj_clip_bottom, obj.base.clip_bottom) as i32,
10509 })
10510 } else {
10511 None
10512 };
10513 ButtonObjectRenderInfo {
10514 disp: extra(ids.obj_disp, gfx_disp().unwrap_or(obj.base.disp)) != 0,
10515 x: object_event_value(
10516 ids,
10517 obj,
10518 ids.obj_x_eve,
10519 extra(ids.obj_x, gfx_pos().map(|v| v.0).unwrap_or(obj.base.x)),
10520 ),
10521 y: object_event_value(
10522 ids,
10523 obj,
10524 ids.obj_y_eve,
10525 extra(ids.obj_y, gfx_pos().map(|v| v.1).unwrap_or(obj.base.y)),
10526 ),
10527 z: object_event_value(ids, obj, ids.obj_z_eve, extra(ids.obj_z, obj.base.z)),
10528 x_rep,
10529 y_rep,
10530 z_rep,
10531 center_x: object_event_value(
10532 ids,
10533 obj,
10534 ids.obj_center_x_eve,
10535 extra(ids.obj_center_x, obj.base.center_x),
10536 ),
10537 center_y: object_event_value(
10538 ids,
10539 obj,
10540 ids.obj_center_y_eve,
10541 extra(ids.obj_center_y, obj.base.center_y),
10542 ),
10543 center_z: object_event_value(
10544 ids,
10545 obj,
10546 ids.obj_center_z_eve,
10547 extra(ids.obj_center_z, obj.base.center_z),
10548 ),
10549 center_rep_x: extra(ids.obj_center_rep_x, obj.base.center_rep_x),
10550 center_rep_y: extra(ids.obj_center_rep_y, obj.base.center_rep_y),
10551 center_rep_z: extra(ids.obj_center_rep_z, obj.base.center_rep_z),
10552 scale_x: object_event_value(
10553 ids,
10554 obj,
10555 ids.obj_scale_x_eve,
10556 extra(ids.obj_scale_x, obj.base.scale_x),
10557 ),
10558 scale_y: object_event_value(
10559 ids,
10560 obj,
10561 ids.obj_scale_y_eve,
10562 extra(ids.obj_scale_y, obj.base.scale_y),
10563 ),
10564 scale_z: object_event_value(
10565 ids,
10566 obj,
10567 ids.obj_scale_z_eve,
10568 extra(ids.obj_scale_z, obj.base.scale_z),
10569 ),
10570 rotate_x: object_event_value(
10571 ids,
10572 obj,
10573 ids.obj_rotate_x_eve,
10574 extra(ids.obj_rotate_x, obj.base.rotate_x),
10575 ),
10576 rotate_y: object_event_value(
10577 ids,
10578 obj,
10579 ids.obj_rotate_y_eve,
10580 extra(ids.obj_rotate_y, obj.base.rotate_y),
10581 ),
10582 rotate_z: object_event_value(
10583 ids,
10584 obj,
10585 ids.obj_rotate_z_eve,
10586 extra(ids.obj_rotate_z, obj.base.rotate_z),
10587 ),
10588 tr: object_event_value(ids, obj, ids.obj_tr_eve, extra(ids.obj_tr, obj.base.tr)),
10589 tr_rep,
10590 world_no: extra(ids.obj_world, obj.base.world),
10591 dst_clip,
10592 }
10593}
10594
10595fn apply_button_object_render_info_to_sprite(sprite: &mut Sprite, info: &ButtonObjectRenderInfo) {
10596 sprite.visible = info.disp;
10597 sprite.x = (info.x + info.x_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
10598 sprite.y = (info.y + info.y_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
10599 sprite.z = (info.z + info.z_rep) as f32;
10600 sprite.pivot_x = (info.center_x + info.center_rep_x) as f32;
10601 sprite.pivot_y = (info.center_y + info.center_rep_y) as f32;
10602 sprite.pivot_z = (info.center_z + info.center_rep_z) as f32;
10603 sprite.scale_x = info.scale_x as f32 / 1000.0;
10604 sprite.scale_y = info.scale_y as f32 / 1000.0;
10605 sprite.scale_z = info.scale_z as f32 / 1000.0;
10606 sprite.rotate = info.rotate_z as f32 * std::f32::consts::PI / 1800.0;
10607 sprite.rotate_x = info.rotate_x as f32 * std::f32::consts::PI / 1800.0;
10608 sprite.rotate_y = info.rotate_y as f32 * std::f32::consts::PI / 1800.0;
10609 sprite.tr = ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255).clamp(0, 255) as u8;
10610 sprite.dst_clip = info.dst_clip;
10611}
10612
10613fn finalize_button_object_center_rep_to_sprite(sprite: &mut Sprite, info: &ButtonObjectRenderInfo) {
10614 let x = (sprite.x as i64 + info.center_rep_x).clamp(i32::MIN as i64, i32::MAX as i64);
10615 let y = (sprite.y as i64 + info.center_rep_y).clamp(i32::MIN as i64, i32::MAX as i64);
10616 sprite.x = x as i32;
10617 sprite.y = y as i32;
10618 sprite.z += info.center_rep_z as f32;
10619}
10620
10621fn object_button_hit_sort_key_from_render(
10622 images: &mut ImageManager,
10623 layers: &LayerManager,
10624 gfx: &graphics::GfxRuntime,
10625 ids: &constants::RuntimeConstants,
10626 syscom: &globals::SyscomRuntimeState,
10627 read_skip_runtime_enabled: bool,
10628 stage_idx: i64,
10629 obj_idx: usize,
10630 obj: &globals::ObjectState,
10631 mx: i32,
10632 my: i32,
10633 parent_state: Option<ParentRenderState>,
10634) -> Option<ButtonSortKey> {
10635 if !object_button_renderable_by_syscom(syscom, obj) {
10639 if sg_debug_enabled() && obj.button.enabled {
10640 eprintln!(
10641 "[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={}",
10642 stage_idx,
10643 obj_idx,
10644 object_runtime_slot(obj_idx, obj),
10645 obj.file_name,
10646 mx,
10647 my,
10648 object_button_renderable_by_syscom(syscom, obj),
10649 button_disabled_reason(syscom, obj, None, read_skip_runtime_enabled),
10650 syscom.mwnd_btn_touch_disable,
10651 obj.button.button_no,
10652 obj.button.group_no,
10653 obj.button.group_idx(),
10654 obj.button.action_no,
10655 obj.button.state,
10656 obj.button.hit,
10657 obj.button.pushed,
10658 obj.button.alpha_test,
10659 obj.button.sys_type,
10660 obj.button.sys_type_opt,
10661 obj.button.mode
10662 );
10663 }
10664 return None;
10665 }
10666 let runtime_slot = object_runtime_slot(obj_idx, obj);
10667 let info = button_object_render_info(ids, gfx, stage_idx, obj_idx, obj);
10668 let mut bound = fetch_bound_render_sprites_for_hit(layers, gfx, stage_idx, runtime_slot, obj);
10669 for rs in &mut bound {
10670 apply_button_object_render_info_to_sprite(&mut rs.sprite, &info);
10671 if let Some(parent) = parent_state.clone() {
10672 let dummy = ObjectRenderInfo::default();
10673 apply_parent_render_state_to_sprite(&mut rs.sprite, &dummy, &parent);
10674 }
10675 finalize_button_object_center_rep_to_sprite(&mut rs.sprite, &info);
10676 if hit_test_render_sprite(images, &rs.sprite, mx, my, obj.button.alpha_test) {
10677 let sort_key = object_button_sort_key(ids, gfx, stage_idx, runtime_slot, obj);
10678 if sg_debug_enabled() {
10679 eprintln!(
10680 "[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={}",
10681 stage_idx,
10682 obj_idx,
10683 runtime_slot,
10684 obj.file_name,
10685 mx,
10686 my,
10687 obj.button.button_no,
10688 obj.button.group_no,
10689 obj.button.group_idx(),
10690 obj.button.action_no,
10691 obj.button.state,
10692 obj.button.hit,
10693 obj.button.pushed,
10694 obj.button.alpha_test,
10695 rs.layer_id,
10696 rs.sprite_id,
10697 rs.sprite.x,
10698 rs.sprite.y,
10699 rs.sprite.size_mode,
10700 sort_key.display_tuple()
10701 );
10702 }
10703 return Some(sort_key);
10704 }
10705 }
10706
10707 if bound.is_empty() {
10708 return object_button_effective_gfx_hit(
10709 images,
10710 layers,
10711 gfx,
10712 ids,
10713 stage_idx,
10714 runtime_slot,
10715 obj,
10716 mx,
10717 my,
10718 parent_state,
10719 );
10720 }
10721 None
10722}
10723
10724fn button_parent_render_state(
10725 layers: &LayerManager,
10726 gfx: &graphics::GfxRuntime,
10727 ids: &constants::RuntimeConstants,
10728 stage_idx: i64,
10729 obj_idx: usize,
10730 obj: &globals::ObjectState,
10731 parent_state: Option<ParentRenderState>,
10732) -> ParentRenderState {
10733 let runtime_slot = object_runtime_slot(obj_idx, obj);
10734 let info = button_object_render_info(ids, gfx, stage_idx, obj_idx, obj);
10735 let first = first_bound_render_sprite(layers, gfx, stage_idx, runtime_slot, obj);
10736 let mut cur = ParentRenderState {
10737 world_no: info.world_no,
10738 pos_x: (info.x + info.x_rep) as f32,
10739 pos_y: (info.y + info.y_rep) as f32,
10740 pos_z: (info.z + info.z_rep) as f32,
10741 center_rep_x: info.center_rep_x as f32,
10742 center_rep_y: info.center_rep_y as f32,
10743 center_rep_z: info.center_rep_z as f32,
10744 scale_x: info.scale_x as f32 / 1000.0,
10745 scale_y: info.scale_y as f32 / 1000.0,
10746 scale_z: info.scale_z as f32 / 1000.0,
10747 rotate_x: info.rotate_x as f32 * std::f32::consts::PI / 1800.0,
10748 rotate_y: info.rotate_y as f32 * std::f32::consts::PI / 1800.0,
10749 rotate_z: info.rotate_z as f32 * std::f32::consts::PI / 1800.0,
10750 tr: ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255) as i32,
10751 mono: 0,
10752 reverse: 0,
10753 bright: 0,
10754 dark: 0,
10755 color_rate: 0,
10756 color_r: 255,
10757 color_g: 255,
10758 color_b: 255,
10759 color_add_r: 0,
10760 color_add_g: 0,
10761 color_add_b: 0,
10762 blend: crate::layer::SpriteBlend::Normal,
10763 dst_clip: info.dst_clip,
10764 mask_image_id: first.and_then(|s| s.mask_image_id.clone()),
10765 mask_offset_x: first.map(|s| s.mask_offset_x).unwrap_or(0),
10766 mask_offset_y: first.map(|s| s.mask_offset_y).unwrap_or(0),
10767 tonecurve_image_id: first.and_then(|s| s.tonecurve_image_id.clone()),
10768 tonecurve_row: first.map(|s| s.tonecurve_row).unwrap_or(0.0),
10769 tonecurve_sat: first.map(|s| s.tonecurve_sat).unwrap_or(0.0),
10770 };
10771 if let Some(parent) = parent_state {
10772 cur = compose_parent_render_state(parent, cur);
10773 }
10774 cur
10775}
10776
10777fn subtree_has_standalone_button_owner(obj: &globals::ObjectState) -> bool {
10780 (has_standalone_button_registration(obj) && !obj.base.no_event_hint)
10781 || obj
10782 .runtime
10783 .child_objects
10784 .iter()
10785 .any(subtree_has_standalone_button_owner)
10786}
10787
10788fn hit_test_standalone_action_button_recursive(
10789 images: &mut ImageManager,
10790 layers: &LayerManager,
10791 gfx: &graphics::GfxRuntime,
10792 ids: &constants::RuntimeConstants,
10793 syscom: &globals::SyscomRuntimeState,
10794 read_skip_runtime_enabled: bool,
10795 mwnd_button_idx: Option<usize>,
10796 stage_idx: i64,
10797 mx: i32,
10798 my: i32,
10799 obj_idx: usize,
10800 obj: &mut globals::ObjectState,
10801 parent_state: Option<ParentRenderState>,
10802) -> Option<ButtonHitCandidate> {
10803 fn recurse(
10804 images: &mut ImageManager,
10805 layers: &LayerManager,
10806 gfx: &graphics::GfxRuntime,
10807 ids: &constants::RuntimeConstants,
10808 syscom: &globals::SyscomRuntimeState,
10809 read_skip_runtime_enabled: bool,
10810 mwnd_button_idx: Option<usize>,
10811 stage_idx: i64,
10812 mx: i32,
10813 my: i32,
10814 obj_idx: usize,
10815 obj: &mut globals::ObjectState,
10816 parent_state: Option<ParentRenderState>,
10817 inherited_owner: Option<ButtonOwnerInfo>,
10818 ) -> Option<ButtonHitCandidate> {
10819 let runtime_slot = object_runtime_slot(obj_idx, obj);
10820 let current_owner = if has_standalone_button_registration(obj) && !obj.base.no_event_hint {
10821 Some(ButtonOwnerInfo {
10822 button_no: obj.button.button_no,
10823 runtime_slot,
10824 se_no: obj.button.se_no,
10825 was_hit: obj.button.last_hit,
10826 event_enabled: button_event_enabled(
10827 syscom,
10828 obj,
10829 mwnd_button_idx,
10830 read_skip_runtime_enabled,
10831 ),
10832 })
10833 } else {
10834 None
10835 };
10836 let effective_owner = current_owner.or(inherited_owner);
10837
10838 let mut best = None;
10839 let mut tied = false;
10840 if let Some(owner) = effective_owner
10841 && !obj.base.no_event_hint
10842 && let Some(sort_key) = object_button_hit_sort_key_from_render(
10843 images,
10844 layers,
10845 gfx,
10846 ids,
10847 syscom,
10848 read_skip_runtime_enabled,
10849 stage_idx,
10850 obj_idx,
10851 obj,
10852 mx,
10853 my,
10854 parent_state.clone(),
10855 )
10856 {
10857 best = Some(ButtonHitCandidate {
10858 button_no: owner.button_no,
10859 sort_key,
10860 runtime_slot: owner.runtime_slot,
10861 se_no: owner.se_no,
10862 was_hit: owner.was_hit,
10863 event_enabled: owner.event_enabled,
10864 });
10865 }
10866 if obj.runtime.child_objects.is_empty()
10871 || (effective_owner.is_none()
10872 && !obj
10873 .runtime
10874 .child_objects
10875 .iter()
10876 .any(subtree_has_standalone_button_owner))
10877 {
10878 return if tied { None } else { best };
10879 }
10880 let cur_parent_state =
10881 button_parent_render_state(layers, gfx, ids, stage_idx, obj_idx, obj, parent_state);
10882 let parent_sort = object_button_sort_key(ids, gfx, stage_idx, runtime_slot, obj);
10883 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
10884 if effective_owner.is_none() && !subtree_has_standalone_button_owner(child) {
10885 continue;
10886 }
10887 if let Some(mut hit) = recurse(
10888 images,
10889 layers,
10890 gfx,
10891 ids,
10892 syscom,
10893 read_skip_runtime_enabled,
10894 mwnd_button_idx,
10895 stage_idx,
10896 mx,
10897 my,
10898 child_idx,
10899 child,
10900 Some(cur_parent_state.clone()),
10901 effective_owner,
10902 ) {
10903 hit.sort_key.order = hit.sort_key.order.saturating_add(parent_sort.order);
10905 hit.sort_key.layer = hit.sort_key.layer.saturating_add(parent_sort.layer);
10906 merge_button_hit(&mut best, &mut tied, hit);
10907 }
10908 }
10909 if tied { None } else { best }
10910 }
10911
10912 recurse(
10913 images,
10914 layers,
10915 gfx,
10916 ids,
10917 syscom,
10918 read_skip_runtime_enabled,
10919 mwnd_button_idx,
10920 stage_idx,
10921 mx,
10922 my,
10923 obj_idx,
10924 obj,
10925 parent_state,
10926 None,
10927 )
10928}
10929
10930fn hit_test_object_button_recursive(
10931 images: &mut ImageManager,
10932 layers: &LayerManager,
10933 gfx: &graphics::GfxRuntime,
10934 ids: &constants::RuntimeConstants,
10935 syscom: &globals::SyscomRuntimeState,
10936 read_skip_runtime_enabled: bool,
10937 stage_idx: i64,
10938 group_idx: usize,
10939 mx: i32,
10940 my: i32,
10941 obj_idx: usize,
10942 obj: &mut globals::ObjectState,
10943 parent_state: Option<ParentRenderState>,
10944) -> Option<ButtonHitCandidate> {
10945 fn recurse(
10946 images: &mut ImageManager,
10947 layers: &LayerManager,
10948 gfx: &graphics::GfxRuntime,
10949 ids: &constants::RuntimeConstants,
10950 syscom: &globals::SyscomRuntimeState,
10951 read_skip_runtime_enabled: bool,
10952 stage_idx: i64,
10953 group_idx: usize,
10954 mx: i32,
10955 my: i32,
10956 obj_idx: usize,
10957 obj: &mut globals::ObjectState,
10958 parent_state: Option<ParentRenderState>,
10959 inherited_owner: Option<ButtonOwnerInfo>,
10960 ) -> Option<ButtonHitCandidate> {
10961 let runtime_slot = object_runtime_slot(obj_idx, obj);
10962 let current_owner = if obj.button.enabled
10963 && !obj.base.no_event_hint
10964 && obj.button.action_no >= 0
10965 && obj.button.group_idx() == Some(group_idx)
10966 {
10967 Some(ButtonOwnerInfo {
10968 button_no: obj.button.button_no,
10969 runtime_slot,
10970 se_no: obj.button.se_no,
10971 was_hit: obj.button.last_hit,
10972 event_enabled: button_event_enabled(syscom, obj, None, read_skip_runtime_enabled),
10973 })
10974 } else {
10975 None
10976 };
10977 let effective_owner = current_owner.or(inherited_owner);
10978
10979 let mut best = None;
10980 let mut tied = false;
10981 if let Some(owner) = effective_owner
10982 && !obj.base.no_event_hint
10983 && let Some(sort_key) = object_button_hit_sort_key_from_render(
10984 images,
10985 layers,
10986 gfx,
10987 ids,
10988 syscom,
10989 read_skip_runtime_enabled,
10990 stage_idx,
10991 obj_idx,
10992 obj,
10993 mx,
10994 my,
10995 parent_state.clone(),
10996 )
10997 {
10998 best = Some(ButtonHitCandidate {
10999 button_no: owner.button_no,
11000 sort_key,
11001 runtime_slot: owner.runtime_slot,
11002 se_no: owner.se_no,
11003 was_hit: owner.was_hit,
11004 event_enabled: owner.event_enabled,
11005 });
11006 }
11007 let cur_parent_state =
11008 button_parent_render_state(layers, gfx, ids, stage_idx, obj_idx, obj, parent_state);
11009 let parent_sort = object_button_sort_key(ids, gfx, stage_idx, runtime_slot, obj);
11010 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
11011 if let Some(mut hit) = recurse(
11012 images,
11013 layers,
11014 gfx,
11015 ids,
11016 syscom,
11017 read_skip_runtime_enabled,
11018 stage_idx,
11019 group_idx,
11020 mx,
11021 my,
11022 child_idx,
11023 child,
11024 Some(cur_parent_state.clone()),
11025 effective_owner,
11026 ) {
11027 hit.sort_key.order = hit.sort_key.order.saturating_add(parent_sort.order);
11030 hit.sort_key.layer = hit.sort_key.layer.saturating_add(parent_sort.layer);
11031 merge_button_hit(&mut best, &mut tied, hit);
11032 }
11033 }
11034 if tied { None } else { best }
11035 }
11036
11037 recurse(
11038 images,
11039 layers,
11040 gfx,
11041 ids,
11042 syscom,
11043 read_skip_runtime_enabled,
11044 stage_idx,
11045 group_idx,
11046 mx,
11047 my,
11048 obj_idx,
11049 obj,
11050 parent_state,
11051 None,
11052 )
11053}
11054
11055fn find_object_by_runtime_slot_mut(
11056 mut objects: &mut [globals::ObjectState],
11057 runtime_slot: usize,
11058) -> Option<&mut globals::ObjectState> {
11059 let mut idx = 0usize;
11060 while let Some((obj, tail)) = objects.split_first_mut() {
11061 if obj.runtime_slot_or(idx) == runtime_slot {
11062 return Some(obj);
11063 }
11064 if let Some(found) =
11065 find_object_by_runtime_slot_mut(&mut obj.runtime.child_objects, runtime_slot)
11066 {
11067 return Some(found);
11068 }
11069 objects = tail;
11070 idx += 1;
11071 }
11072 None
11073}
11074
11075fn compose_clip_rect(
11076 parent_clip: Option<ClipRect>,
11077 child_clip: Option<ClipRect>,
11078) -> Option<ClipRect> {
11079 match (parent_clip, child_clip) {
11080 (Some(parent), Some(child)) => Some(ClipRect {
11081 left: child.left.min(parent.left),
11086 top: child.top.min(parent.top),
11087 right: child.right.max(parent.right),
11088 bottom: child.bottom.max(parent.bottom),
11089 }),
11090 (Some(parent), None) => Some(parent),
11091 (None, Some(child)) => Some(child),
11092 (None, None) => None,
11093 }
11094}
11095
11096fn compose_parent_render_state(
11097 parent: ParentRenderState,
11098 mut cur: ParentRenderState,
11099) -> ParentRenderState {
11100 if cur.world_no < 0 {
11101 cur.world_no = parent.world_no;
11102 }
11103
11104 let child_clip = cur.dst_clip;
11105
11106 cur.pos_x = (cur.pos_x - parent.center_rep_x) * parent.scale_x + parent.center_rep_x;
11107 cur.pos_y = (cur.pos_y - parent.center_rep_y) * parent.scale_y + parent.center_rep_y;
11108 cur.pos_z = (cur.pos_z - parent.center_rep_z) * parent.scale_z + parent.center_rep_z;
11109 {
11110 let tmp_x = cur.pos_x;
11111 let tmp_y = cur.pos_y;
11112 let (s, c) = parent.rotate_z.sin_cos();
11113 cur.pos_x = (tmp_x - parent.center_rep_x) * c - (tmp_y - parent.center_rep_y) * s
11114 + parent.center_rep_x;
11115 cur.pos_y = (tmp_x - parent.center_rep_x) * s
11116 + (tmp_y - parent.center_rep_y) * c
11117 + parent.center_rep_y;
11118 }
11119 cur.pos_x += parent.pos_x;
11120 cur.pos_y += parent.pos_y;
11121 cur.pos_z += parent.pos_z;
11122 cur.scale_x *= parent.scale_x;
11123 cur.scale_y *= parent.scale_y;
11124 cur.scale_z *= parent.scale_z;
11125 cur.rotate_x += parent.rotate_x;
11126 cur.rotate_y += parent.rotate_y;
11127 cur.rotate_z += parent.rotate_z;
11128
11129 cur.dst_clip = compose_clip_rect(parent.dst_clip, child_clip);
11130
11131 cur.tr = (cur.tr * parent.tr / 255).clamp(0, 255);
11132 cur.mono = combine_lerp(cur.mono as u8, parent.mono) as i32;
11133 cur.reverse = combine_lerp(cur.reverse as u8, parent.reverse) as i32;
11134 cur.bright = combine_lerp(cur.bright as u8, parent.bright) as i32;
11135 cur.dark = combine_lerp(cur.dark as u8, parent.dark) as i32;
11136 if cur.color_rate + parent.color_rate > 0 {
11137 let parent_rate = (parent.color_rate * 255 * 255)
11138 / (255 * 255 - (255 - cur.color_rate) * (255 - parent.color_rate)).max(1);
11139 cur.color_r = blend_color(cur.color_r as u8, parent.color_r, parent_rate) as i32;
11140 cur.color_g = blend_color(cur.color_g as u8, parent.color_g, parent_rate) as i32;
11141 cur.color_b = blend_color(cur.color_b as u8, parent.color_b, parent_rate) as i32;
11142 }
11143 cur.color_rate = combine_lerp(cur.color_rate as u8, parent.color_rate) as i32;
11144 cur.color_add_r = clamp_add(cur.color_add_r as u8, parent.color_add_r) as i32;
11145 cur.color_add_g = clamp_add(cur.color_add_g as u8, parent.color_add_g) as i32;
11146 cur.color_add_b = clamp_add(cur.color_add_b as u8, parent.color_add_b) as i32;
11147 if matches!(cur.blend, crate::layer::SpriteBlend::Normal) {
11148 cur.blend = parent.blend;
11149 }
11150 if cur.mask_image_id.is_none() {
11151 cur.mask_image_id = parent.mask_image_id;
11152 cur.mask_offset_x = parent.mask_offset_x;
11153 cur.mask_offset_y = parent.mask_offset_y;
11154 }
11155 if cur.tonecurve_image_id.is_none() {
11156 cur.tonecurve_image_id = parent.tonecurve_image_id;
11157 cur.tonecurve_row = parent.tonecurve_row;
11158 cur.tonecurve_sat = parent.tonecurve_sat;
11159 }
11160 cur
11161}
11162
11163fn apply_object_event_animations_recursive(
11164 ids: &constants::RuntimeConstants,
11165 gfx: &mut graphics::GfxRuntime,
11166 images: &mut ImageManager,
11167 layers: &mut LayerManager,
11168 stage_i64: i64,
11169 obj_i64: i64,
11170 obj: &mut globals::ObjectState,
11171) {
11172 if obj.any_event_active() {
11173 let read_ev = |op_id: i32, obj: &globals::ObjectState| -> Option<i64> {
11174 if op_id == 0 {
11175 None
11176 } else {
11177 obj.int_event_by_op(ids, op_id)
11178 .filter(|ev| ev.check_event())
11179 .map(|ev| ev.get_total_value() as i64)
11180 }
11181 };
11182 let read_list0 = |op_id: i32, obj: &globals::ObjectState| -> Option<i64> {
11183 if op_id == 0 {
11184 None
11185 } else {
11186 obj.int_event_list_by_op(ids, op_id)
11187 .and_then(|list| list.first())
11188 .filter(|ev| ev.check_event())
11189 .map(|ev| ev.get_total_value() as i64)
11190 }
11191 };
11192
11193 let x: Option<i64> = read_ev(ids.obj_x_eve, obj);
11194 let y: Option<i64> = read_ev(ids.obj_y_eve, obj);
11195 let x_rep: Option<i64> = read_list0(ids.obj_x_rep_eve, obj);
11196 let y_rep: Option<i64> = read_list0(ids.obj_y_rep_eve, obj);
11197 let z_rep: Option<i64> = read_list0(ids.obj_z_rep_eve, obj);
11198 let alpha: Option<i64> = None;
11199 let patno: Option<i64> = read_ev(ids.obj_patno_eve, obj);
11200 let order: Option<i64> = None;
11201 let layer_no: Option<i64> = None;
11202 let z: Option<i64> = read_ev(ids.obj_z_eve, obj);
11203 let center_x: Option<i64> = read_ev(ids.obj_center_x_eve, obj);
11204 let center_y: Option<i64> = read_ev(ids.obj_center_y_eve, obj);
11205 let center_z: Option<i64> = read_ev(ids.obj_center_z_eve, obj);
11206 let center_rep_x: Option<i64> = read_ev(ids.obj_center_rep_x_eve, obj);
11207 let center_rep_y: Option<i64> = read_ev(ids.obj_center_rep_y_eve, obj);
11208 let center_rep_z: Option<i64> = read_ev(ids.obj_center_rep_z_eve, obj);
11209 let scale_x: Option<i64> = read_ev(ids.obj_scale_x_eve, obj);
11210 let scale_y: Option<i64> = read_ev(ids.obj_scale_y_eve, obj);
11211 let scale_z: Option<i64> = read_ev(ids.obj_scale_z_eve, obj);
11212 let rotate_x: Option<i64> = read_ev(ids.obj_rotate_x_eve, obj);
11213 let rotate_y: Option<i64> = read_ev(ids.obj_rotate_y_eve, obj);
11214 let rotate_z: Option<i64> = read_ev(ids.obj_rotate_z_eve, obj);
11215 let clip_left: Option<i64> = read_ev(ids.obj_clip_left_eve, obj);
11216 let clip_top: Option<i64> = read_ev(ids.obj_clip_top_eve, obj);
11217 let clip_right: Option<i64> = read_ev(ids.obj_clip_right_eve, obj);
11218 let clip_bottom: Option<i64> = read_ev(ids.obj_clip_bottom_eve, obj);
11219 let src_clip_left: Option<i64> = read_ev(ids.obj_src_clip_left_eve, obj);
11220 let src_clip_top: Option<i64> = read_ev(ids.obj_src_clip_top_eve, obj);
11221 let src_clip_right: Option<i64> = read_ev(ids.obj_src_clip_right_eve, obj);
11222 let src_clip_bottom: Option<i64> = read_ev(ids.obj_src_clip_bottom_eve, obj);
11223 let tr: Option<i64> = read_ev(ids.obj_tr_eve, obj);
11224 let tr_rep: Option<i64> = read_list0(ids.obj_tr_rep_eve, obj);
11225 let mono: Option<i64> = read_ev(ids.obj_mono_eve, obj);
11226 let reverse: Option<i64> = read_ev(ids.obj_reverse_eve, obj);
11227 let bright: Option<i64> = read_ev(ids.obj_bright_eve, obj);
11228 let dark: Option<i64> = read_ev(ids.obj_dark_eve, obj);
11229 let color_rate: Option<i64> = read_ev(ids.obj_color_rate_eve, obj);
11230 let color_add_r: Option<i64> = read_ev(ids.obj_color_add_r_eve, obj);
11231 let color_add_g: Option<i64> = read_ev(ids.obj_color_add_g_eve, obj);
11232 let color_add_b: Option<i64> = read_ev(ids.obj_color_add_b_eve, obj);
11233 let color_r: Option<i64> = read_ev(ids.obj_color_r_eve, obj);
11234 let color_g: Option<i64> = read_ev(ids.obj_color_g_eve, obj);
11235 let color_b: Option<i64> = read_ev(ids.obj_color_b_eve, obj);
11236
11237 let mut set_extra_prop = |prop_id: i32, val: Option<i64>| {
11238 if prop_id != 0
11239 && let Some(v) = val
11240 {
11241 let old_value = obj.get_int_prop(ids, prop_id);
11242 trace_config_event_frame_prop_write(
11243 ids, stage_i64, obj_i64, obj, prop_id, old_value, v,
11244 );
11245 obj.set_int_prop_from_event_frame(ids, prop_id, v);
11246 }
11247 };
11248 set_extra_prop(ids.obj_x, x);
11249 set_extra_prop(ids.obj_y, y);
11250 set_extra_prop(ids.obj_alpha, alpha);
11254 set_extra_prop(ids.obj_patno, patno);
11255 set_extra_prop(ids.obj_order, order);
11256 set_extra_prop(ids.obj_layer, layer_no);
11257 set_extra_prop(ids.obj_z, z);
11258 set_extra_prop(ids.obj_center_x, center_x);
11259 set_extra_prop(ids.obj_center_y, center_y);
11260 set_extra_prop(ids.obj_center_z, center_z);
11261 set_extra_prop(ids.obj_center_rep_x, center_rep_x);
11262 set_extra_prop(ids.obj_center_rep_y, center_rep_y);
11263 set_extra_prop(ids.obj_center_rep_z, center_rep_z);
11264 set_extra_prop(ids.obj_scale_x, scale_x);
11265 set_extra_prop(ids.obj_scale_y, scale_y);
11266 set_extra_prop(ids.obj_scale_z, scale_z);
11267 set_extra_prop(ids.obj_rotate_x, rotate_x);
11268 set_extra_prop(ids.obj_rotate_y, rotate_y);
11269 set_extra_prop(ids.obj_rotate_z, rotate_z);
11270 set_extra_prop(ids.obj_clip_left, clip_left);
11271 set_extra_prop(ids.obj_clip_top, clip_top);
11272 set_extra_prop(ids.obj_clip_right, clip_right);
11273 set_extra_prop(ids.obj_clip_bottom, clip_bottom);
11274 set_extra_prop(ids.obj_src_clip_left, src_clip_left);
11275 set_extra_prop(ids.obj_src_clip_top, src_clip_top);
11276 set_extra_prop(ids.obj_src_clip_right, src_clip_right);
11277 set_extra_prop(ids.obj_src_clip_bottom, src_clip_bottom);
11278 set_extra_prop(ids.obj_tr, tr);
11279 set_extra_prop(ids.obj_mono, mono);
11281 set_extra_prop(ids.obj_reverse, reverse);
11282 set_extra_prop(ids.obj_bright, bright);
11283 set_extra_prop(ids.obj_dark, dark);
11284 set_extra_prop(ids.obj_color_rate, color_rate);
11285 set_extra_prop(ids.obj_color_add_r, color_add_r);
11286 set_extra_prop(ids.obj_color_add_g, color_add_g);
11287 set_extra_prop(ids.obj_color_add_b, color_add_b);
11288 set_extra_prop(ids.obj_color_r, color_r);
11289 set_extra_prop(ids.obj_color_g, color_g);
11290 set_extra_prop(ids.obj_color_b, color_b);
11291
11292 if !(x.is_none()
11293 && y.is_none()
11294 && x_rep.is_none()
11295 && y_rep.is_none()
11296 && z_rep.is_none()
11297 && alpha.is_none()
11298 && patno.is_none()
11299 && order.is_none()
11300 && layer_no.is_none()
11301 && z.is_none()
11302 && center_x.is_none()
11303 && center_y.is_none()
11304 && center_z.is_none()
11305 && center_rep_x.is_none()
11306 && center_rep_y.is_none()
11307 && center_rep_z.is_none()
11308 && scale_x.is_none()
11309 && scale_y.is_none()
11310 && scale_z.is_none()
11311 && rotate_x.is_none()
11312 && rotate_y.is_none()
11313 && rotate_z.is_none()
11314 && clip_left.is_none()
11315 && clip_top.is_none()
11316 && clip_right.is_none()
11317 && clip_bottom.is_none()
11318 && src_clip_left.is_none()
11319 && src_clip_top.is_none()
11320 && src_clip_right.is_none()
11321 && src_clip_bottom.is_none()
11322 && tr.is_none()
11323 && tr_rep.is_none()
11324 && mono.is_none()
11325 && reverse.is_none()
11326 && bright.is_none()
11327 && dark.is_none()
11328 && color_rate.is_none()
11329 && color_add_r.is_none()
11330 && color_add_g.is_none()
11331 && color_add_b.is_none()
11332 && color_r.is_none()
11333 && color_g.is_none()
11334 && color_b.is_none())
11335 {
11336 match &obj.backend {
11337 globals::ObjectBackend::Gfx => {
11338 if let Some(ax) = x {
11339 let _ = gfx.object_set_x(images, layers, stage_i64, obj_i64, ax);
11340 }
11341 if let Some(ay) = y {
11342 let _ = gfx.object_set_y(images, layers, stage_i64, obj_i64, ay);
11343 }
11344 if let Some(a) = alpha {
11345 let _ = gfx.object_set_alpha(images, layers, stage_i64, obj_i64, a);
11346 }
11347 if let Some(p) = patno {
11348 let _ = gfx.object_set_pat_no(images, layers, stage_i64, obj_i64, p);
11349 }
11350 if let Some(o) = order {
11351 let _ = gfx.object_set_order(images, layers, stage_i64, obj_i64, o);
11352 }
11353 if let Some(l) = layer_no {
11354 let _ = gfx.object_set_layer(images, layers, stage_i64, obj_i64, l);
11355 }
11356 if let Some(zv) = z {
11357 let _ = gfx.object_set_z(stage_i64, obj_i64, zv);
11358 }
11359 if center_x.is_some() || center_y.is_some() {
11360 let cx = center_x
11361 .or_else(|| {
11362 (ids.obj_center_x != 0)
11363 .then_some(obj.get_int_prop(ids, ids.obj_center_x))
11364 })
11365 .unwrap_or(0);
11366 let cy = center_y
11367 .or_else(|| {
11368 (ids.obj_center_y != 0)
11369 .then_some(obj.get_int_prop(ids, ids.obj_center_y))
11370 })
11371 .unwrap_or(0);
11372 let _ = gfx.object_set_center(images, layers, stage_i64, obj_i64, cx, cy);
11373 }
11374 if scale_x.is_some() || scale_y.is_some() {
11375 let sx = scale_x
11376 .or_else(|| {
11377 (ids.obj_scale_x != 0)
11378 .then_some(obj.get_int_prop(ids, ids.obj_scale_x))
11379 })
11380 .unwrap_or(1000);
11381 let sy = scale_y
11382 .or_else(|| {
11383 (ids.obj_scale_y != 0)
11384 .then_some(obj.get_int_prop(ids, ids.obj_scale_y))
11385 })
11386 .unwrap_or(1000);
11387 let _ = gfx.object_set_scale(images, layers, stage_i64, obj_i64, sx, sy);
11388 }
11389 if let Some(rz) = rotate_z {
11390 let _ = gfx.object_set_rotate(images, layers, stage_i64, obj_i64, rz);
11391 }
11392 if clip_left.is_some()
11393 || clip_top.is_some()
11394 || clip_right.is_some()
11395 || clip_bottom.is_some()
11396 {
11397 let use_flag = if ids.obj_clip_use != 0 {
11398 obj.get_int_prop(ids, ids.obj_clip_use)
11399 } else {
11400 0
11401 };
11402 let left = clip_left
11403 .or_else(|| {
11404 (ids.obj_clip_left != 0)
11405 .then_some(obj.get_int_prop(ids, ids.obj_clip_left))
11406 })
11407 .unwrap_or(0);
11408 let top = clip_top
11409 .or_else(|| {
11410 (ids.obj_clip_top != 0)
11411 .then_some(obj.get_int_prop(ids, ids.obj_clip_top))
11412 })
11413 .unwrap_or(0);
11414 let right = clip_right
11415 .or_else(|| {
11416 (ids.obj_clip_right != 0)
11417 .then_some(obj.get_int_prop(ids, ids.obj_clip_right))
11418 })
11419 .unwrap_or(0);
11420 let bottom = clip_bottom
11421 .or_else(|| {
11422 (ids.obj_clip_bottom != 0)
11423 .then_some(obj.get_int_prop(ids, ids.obj_clip_bottom))
11424 })
11425 .unwrap_or(0);
11426 let _ = gfx.object_set_clip(
11427 images, layers, stage_i64, obj_i64, use_flag, left, top, right, bottom,
11428 );
11429 }
11430 if src_clip_left.is_some()
11431 || src_clip_top.is_some()
11432 || src_clip_right.is_some()
11433 || src_clip_bottom.is_some()
11434 {
11435 let use_flag = if ids.obj_src_clip_use != 0 {
11436 obj.lookup_int_prop(ids, ids.obj_src_clip_use).unwrap_or(0)
11437 } else {
11438 0
11439 };
11440 let left = src_clip_left
11441 .or_else(|| {
11442 if ids.obj_src_clip_left != 0 {
11443 obj.lookup_int_prop(ids, ids.obj_src_clip_left)
11444 } else {
11445 None
11446 }
11447 })
11448 .unwrap_or(0);
11449 let top = src_clip_top
11450 .or_else(|| {
11451 if ids.obj_src_clip_top != 0 {
11452 obj.lookup_int_prop(ids, ids.obj_src_clip_top)
11453 } else {
11454 None
11455 }
11456 })
11457 .unwrap_or(0);
11458 let right = src_clip_right
11459 .or_else(|| {
11460 if ids.obj_src_clip_right != 0 {
11461 obj.lookup_int_prop(ids, ids.obj_src_clip_right)
11462 } else {
11463 None
11464 }
11465 })
11466 .unwrap_or(0);
11467 let bottom = src_clip_bottom
11468 .or_else(|| {
11469 if ids.obj_src_clip_bottom != 0 {
11470 obj.lookup_int_prop(ids, ids.obj_src_clip_bottom)
11471 } else {
11472 None
11473 }
11474 })
11475 .unwrap_or(0);
11476 let _ = gfx.object_set_src_clip(
11477 images, layers, stage_i64, obj_i64, use_flag, left, top, right, bottom,
11478 );
11479 }
11480 if let Some(v) = tr {
11481 let _ = gfx.object_set_tr(images, layers, stage_i64, obj_i64, v);
11482 }
11483 if let Some(v) = mono {
11484 let _ = gfx.object_set_mono(images, layers, stage_i64, obj_i64, v);
11485 }
11486 if let Some(v) = reverse {
11487 let _ = gfx.object_set_reverse(images, layers, stage_i64, obj_i64, v);
11488 }
11489 if let Some(v) = bright {
11490 let _ = gfx.object_set_bright(images, layers, stage_i64, obj_i64, v);
11491 }
11492 if let Some(v) = dark {
11493 let _ = gfx.object_set_dark(images, layers, stage_i64, obj_i64, v);
11494 }
11495 if let Some(v) = color_rate {
11496 let _ = gfx.object_set_color_rate(images, layers, stage_i64, obj_i64, v);
11497 }
11498 if color_add_r.is_some() || color_add_g.is_some() || color_add_b.is_some() {
11499 let r = color_add_r.unwrap_or_else(|| {
11500 if ids.obj_color_add_r != 0 {
11501 obj.get_int_prop(ids, ids.obj_color_add_r)
11502 } else {
11503 0
11504 }
11505 });
11506 let g = color_add_g.unwrap_or_else(|| {
11507 if ids.obj_color_add_g != 0 {
11508 obj.get_int_prop(ids, ids.obj_color_add_g)
11509 } else {
11510 0
11511 }
11512 });
11513 let b = color_add_b.unwrap_or_else(|| {
11514 if ids.obj_color_add_b != 0 {
11515 obj.get_int_prop(ids, ids.obj_color_add_b)
11516 } else {
11517 0
11518 }
11519 });
11520 let _ =
11521 gfx.object_set_color_add(images, layers, stage_i64, obj_i64, r, g, b);
11522 }
11523 if color_r.is_some() || color_g.is_some() || color_b.is_some() {
11524 let r = color_r.unwrap_or_else(|| {
11525 if ids.obj_color_r != 0 {
11526 obj.get_int_prop(ids, ids.obj_color_r)
11527 } else {
11528 0
11529 }
11530 });
11531 let g = color_g.unwrap_or_else(|| {
11532 if ids.obj_color_g != 0 {
11533 obj.get_int_prop(ids, ids.obj_color_g)
11534 } else {
11535 0
11536 }
11537 });
11538 let b = color_b.unwrap_or_else(|| {
11539 if ids.obj_color_b != 0 {
11540 obj.get_int_prop(ids, ids.obj_color_b)
11541 } else {
11542 0
11543 }
11544 });
11545 let _ = gfx.object_set_color(images, layers, stage_i64, obj_i64, r, g, b);
11546 }
11547 }
11548 backend => {
11549 for (layer_id, sprite_id) in layer_backed_object_sprite_bindings(backend) {
11550 let Some(sprite) = layers
11551 .layer_mut(layer_id)
11552 .and_then(|layer| layer.sprite_mut(sprite_id))
11553 else {
11554 continue;
11555 };
11556 if let Some(ax) = x {
11557 sprite.x = ax as i32;
11558 }
11559 if let Some(ay) = y {
11560 sprite.y = ay as i32;
11561 }
11562 if let Some(v) = alpha {
11563 sprite.alpha = v.clamp(0, 255) as u8;
11564 }
11565 if let Some(v) = order {
11566 sprite.order = v as i32;
11567 }
11568 if let Some(v) = tr {
11569 sprite.tr = v.clamp(0, 255) as u8;
11570 }
11571 }
11572 }
11573 }
11574 }
11575 }
11576
11577 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
11578 apply_object_event_animations_recursive(
11579 ids,
11580 gfx,
11581 images,
11582 layers,
11583 stage_i64,
11584 object_runtime_slot(child_idx, child) as i64,
11585 child,
11586 );
11587 }
11588}
11589
11590const WEATHER_APPEAR_MS: i64 = 1000;
11591const WEATHER_DISAPPEAR_MS: i64 = 1000;
11592const WEATHER_ANGLE_FULL: f64 = 3600.0;
11593
11594fn weather_alpha_for_state(state: i64, cur: i64, len: i64) -> u8 {
11595 match state {
11596 1 => ((cur.clamp(0, WEATHER_APPEAR_MS) * 255) / WEATHER_APPEAR_MS).clamp(0, 255) as u8,
11597 2 => 255,
11598 3 => {
11599 let len = if len <= 0 { WEATHER_DISAPPEAR_MS } else { len };
11600 ((len.saturating_sub(cur).clamp(0, len) * 255) / len).clamp(0, 255) as u8
11601 }
11602 _ => 0,
11603 }
11604}
11605
11606fn weather_wave(time: i64, period: i64, power: i64) -> i64 {
11607 if period == 0 || power == 0 {
11608 return 0;
11609 }
11610 let rad = (time as f64 / period.abs() as f64) * std::f64::consts::TAU;
11611 (rad.sin() * power as f64).round() as i64
11612}
11613
11614fn weather_pattern(obj: &mut globals::ObjectState, idx: usize) -> i64 {
11615 let p = obj.weather_param.clone();
11616 let first = p.pat_no_00.min(p.pat_no_01);
11617 let last = p.pat_no_00.max(p.pat_no_01);
11618 let span = (last - first + 1).max(1);
11619 match (p.pat_mode, p.pat_time) {
11620 (1, 1..) => {
11621 let t = obj
11622 .weather_work
11623 .sub
11624 .get(idx)
11625 .map(|s| s.move_cur_time.max(0) % p.pat_time)
11626 .unwrap_or(0);
11627 first + t.saturating_mul(span) / p.pat_time
11628 }
11629 (2, _) => first + obj.weather_work.rand_mod(span),
11630 _ => p.pat_no_00,
11631 }
11632}
11633
11634fn ensure_weather_sprites(
11635 layers: &mut LayerManager,
11636 obj: &mut globals::ObjectState,
11637) -> Option<(LayerId, Vec<SpriteId>)> {
11638 let required = obj.weather_sprite_count();
11639 let (layer_id, sprite_ids) = match &mut obj.backend {
11640 globals::ObjectBackend::Weather {
11641 layer_id,
11642 sprite_ids,
11643 } => (*layer_id, sprite_ids),
11644 _ => return None,
11645 };
11646 if let Some(layer) = layers.layer_mut(layer_id) {
11647 while sprite_ids.len() < required {
11648 let sid = layer.create_sprite();
11649 if let Some(sprite) = layer.sprite_mut(sid) {
11650 sprite.fit = SpriteFit::PixelRect;
11651 sprite.size_mode = SpriteSizeMode::Intrinsic;
11652 sprite.visible = false;
11653 sprite.image_id = None;
11654 }
11655 sprite_ids.push(sid);
11656 }
11657 }
11658 Some((layer_id, sprite_ids.clone()))
11659}
11660
11661fn set_weather_sprite(
11662 ids: &constants::RuntimeConstants,
11663 layers: &mut LayerManager,
11664 _images: &mut ImageManager,
11665 obj: &globals::ObjectState,
11666 layer_id: LayerId,
11667 sprite_id: SpriteId,
11668 image_id: Option<ImageHandle>,
11669 x: i64,
11670 y: i64,
11671 alpha: u8,
11672 scale_x: i64,
11673 scale_y: i64,
11674) {
11675 let Some(layer) = layers.layer_mut(layer_id) else {
11676 return;
11677 };
11678 let Some(sprite) = layer.sprite_mut(sprite_id) else {
11679 return;
11680 };
11681 sprite.image_id = image_id.clone();
11682 sprite.visible = image_id.is_some() && obj.get_int_prop(ids, ids.obj_disp) != 0 && alpha > 0;
11683 sprite.fit = SpriteFit::PixelRect;
11684 sprite.size_mode = SpriteSizeMode::Intrinsic;
11685 sprite.x = x.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
11688 sprite.y = y.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
11689 sprite.alpha = 255;
11690 sprite.tr = alpha;
11691 sprite.order = 0;
11692 sprite.scale_x = scale_x as f32 / 1000.0;
11693 sprite.scale_y = scale_y as f32 / 1000.0;
11694}
11695
11696fn weather_wrap_position(value: i64, extent: i64) -> i64 {
11697 if extent <= 0 {
11698 return value;
11699 }
11700 if value > 0 {
11702 value % extent
11703 } else {
11704 extent - ((-value) % extent)
11705 }
11706}
11707
11708fn sync_weather_object_recursive(
11709 ids: &constants::RuntimeConstants,
11710 layers: &mut LayerManager,
11711 images: &mut ImageManager,
11712 screen_w: i64,
11713 screen_h: i64,
11714 game_delta_ms: i32,
11715 real_delta_ms: i32,
11716 obj: &mut globals::ObjectState,
11717) {
11718 if obj.object_type == 4 && matches!(obj.weather_param.weather_type, 1 | 2) {
11719 obj.update_weather_time(game_delta_ms, real_delta_ms, screen_w, screen_h);
11720 let Some((layer_id, sprite_ids)) = ensure_weather_sprites(layers, obj) else {
11721 return;
11722 };
11723
11724 let file_name = obj.file_name.clone().unwrap_or_default();
11725 let cnt_max = obj.weather_work.cnt_max.min(obj.weather_work.sub.len());
11726 let mut used = 0usize;
11727 for idx in 0..cnt_max {
11728 let sub = obj.weather_work.sub[idx].clone();
11729 if sub.state == 0 {
11730 continue;
11731 }
11732 let pat_no = weather_pattern(obj, idx).max(0) as u32;
11733 let image_id = if file_name.is_empty() {
11734 None
11735 } else {
11736 images.load_g00(&file_name, pat_no).ok()
11737 };
11738 let alpha = weather_alpha_for_state(sub.state, sub.state_cur_time, sub.state_time_len);
11739
11740 if obj.weather_param.weather_type == 1 {
11741 let move_x = if sub.move_time_x == 0 {
11742 0
11743 } else {
11744 (1000.0 / sub.move_time_x as f64 * sub.move_cur_time.max(0) as f64) as i64
11745 };
11746 let move_y = if sub.move_time_y == 0 {
11747 0
11748 } else {
11749 (1000.0 / sub.move_time_y as f64 * sub.move_cur_time.max(0) as f64) as i64
11750 };
11751 let mut x = sub.move_start_pos_x
11752 + move_x
11753 + weather_wave(sub.sin_cur_time, sub.sin_time_x, sub.sin_power_x);
11754 let mut y = sub.move_start_pos_y
11755 + move_y
11756 + weather_wave(sub.sin_cur_time, sub.sin_time_y, sub.sin_power_y);
11757 x = weather_wrap_position(x, screen_w);
11758 y = weather_wrap_position(y, screen_h);
11759
11760 let (over_l, over_r, over_u, over_d) = image_id
11761 .as_ref()
11762 .and_then(|id| images.get(id))
11763 .map(|img| {
11764 let sx = sub.scale_x as f64 / 1000.0;
11765 let sy = sub.scale_y as f64 / 1000.0;
11766 let left = x as f64 - img.center_x as f64 * sx;
11767 let right = x as f64 + (img.width as i64 - img.center_x as i64) as f64 * sx;
11768 let top = y as f64 - img.center_y as f64 * sy;
11769 let bottom =
11770 y as f64 + (img.height as i64 - img.center_y as i64) as f64 * sy;
11771 (
11772 left < 0.0,
11773 right >= screen_w as f64,
11774 top < 0.0,
11775 bottom >= screen_h as f64,
11776 )
11777 })
11778 .unwrap_or((false, false, false, false));
11779 let wrap_x = if over_l {
11780 screen_w
11781 } else if over_r {
11782 -screen_w
11783 } else {
11784 0
11785 };
11786 let wrap_y = if over_u {
11787 screen_h
11788 } else if over_d {
11789 -screen_h
11790 } else {
11791 0
11792 };
11793 let offsets = [
11794 Some((0, 0)),
11795 (over_l || over_r).then_some((wrap_x, 0)),
11796 (over_u || over_d).then_some((0, wrap_y)),
11797 ((over_l || over_r) && (over_u || over_d)).then_some((wrap_x, wrap_y)),
11798 ];
11799 for offset in offsets.into_iter().flatten() {
11800 if let Some(&sid) = sprite_ids.get(used) {
11801 set_weather_sprite(
11802 ids,
11803 layers,
11804 images,
11805 obj,
11806 layer_id,
11807 sid,
11808 image_id.clone(),
11809 x + offset.0,
11810 y + offset.1,
11811 alpha,
11812 sub.scale_x,
11813 sub.scale_y,
11814 );
11815 }
11816 used += 1;
11817 }
11818 } else {
11819 let total_time =
11820 (WEATHER_APPEAR_MS + sub.active_time_len + WEATHER_DISAPPEAR_MS).max(1);
11821 let move_cur_time = if sub.move_time_x > 0 {
11822 sub.move_cur_time.max(0)
11823 } else {
11824 total_time.saturating_sub(sub.move_cur_time.max(0))
11825 };
11826 let mut rep_x = sub.move_start_distance as f64;
11827 let mut rep_y = 0.0f64;
11828 if sub.move_time_x != 0 {
11829 let mt = sub.move_time_x as f64;
11830 let t = move_cur_time as f64;
11831 rep_x += 1000.0 / mt / mt * t * t;
11832 }
11833 rep_y += weather_wave(sub.sin_cur_time, sub.sin_time_x, sub.sin_power_x) as f64;
11836 rep_x += weather_wave(sub.sin_cur_time, sub.sin_time_y, sub.sin_power_y) as f64;
11837
11838 let mut rad = (sub.move_start_degree as f64 / 10.0).to_radians();
11839 let theta_deg = rep_x * (sub.center_rotate as f64 / 10.0) / 1000.0;
11840 rad += theta_deg.to_radians();
11841 let x = obj.weather_param.center_x + (rep_x * rad.cos() - rep_y * rad.sin()) as i64;
11842 let y = obj.weather_param.center_y + (rep_x * rad.sin() + rep_y * rad.cos()) as i64;
11843 let process = (move_cur_time.saturating_mul(1000) / total_time).clamp(0, 1000);
11844 let zoom =
11845 sub.zoom_min + (sub.zoom_max - sub.zoom_min).saturating_mul(process) / 1000;
11846 if let Some(&sid) = sprite_ids.get(used) {
11847 set_weather_sprite(
11848 ids, layers, images, obj, layer_id, sid, image_id, x, y, alpha, zoom, zoom,
11849 );
11850 }
11851 used += 1;
11852 }
11853 }
11854
11855 if let Some(layer) = layers.layer_mut(layer_id) {
11856 for sid in sprite_ids.into_iter().skip(used) {
11857 if let Some(sprite) = layer.sprite_mut(sid) {
11858 sprite.visible = false;
11859 sprite.image_id = None;
11860 }
11861 }
11862 }
11863 }
11864
11865 for child in &mut obj.runtime.child_objects {
11866 sync_weather_object_recursive(
11867 ids,
11868 layers,
11869 images,
11870 screen_w,
11871 screen_h,
11872 game_delta_ms,
11873 real_delta_ms,
11874 child,
11875 );
11876 }
11877}
11878
11879fn install_object_movie_preview_if_missing(
11880 layers: &mut LayerManager,
11881 movie_mgr: &mut MovieManager,
11882 images: &mut ImageManager,
11883 obj: &mut globals::ObjectState,
11884 stage_idx: i64,
11885 obj_idx: i64,
11886 file: &str,
11887 trace: bool,
11888) {
11889 let globals::ObjectBackend::Movie {
11890 layer_id,
11891 sprite_id,
11892 image_id,
11893 width,
11894 height,
11895 } = &mut obj.backend
11896 else {
11897 return;
11898 };
11899
11900 if image_id.is_some() {
11901 return;
11902 }
11903
11904 match movie_mgr.ensure_omv_preview_frame(file) {
11905 Ok(frame) => {
11906 let img_id = images.insert_image_arc(frame.clone());
11907 *image_id = Some(img_id.clone());
11908 *width = frame.width;
11909 *height = frame.height;
11910 obj.movie.frame_image_ids[0] = Some(img_id.clone());
11911 obj.movie.frame_image_cursor = 0;
11912 if let Some(layer) = layers.layer_mut(*layer_id)
11913 && let Some(sprite) = layer.sprite_mut(*sprite_id)
11914 {
11915 sprite.image_id = Some(img_id.clone());
11916 sprite.object_anchor = true;
11917 sprite.texture_center_x = 0.0;
11918 sprite.texture_center_y = 0.0;
11919 }
11920 if trace || sg_debug_enabled() {
11921 eprintln!(
11922 "[SG_DEBUG][MOV] object_movie.preview_installed stage={} obj={} file={} image={:?} size={}x{}",
11923 stage_idx, obj_idx, file, img_id, frame.width, frame.height
11924 );
11925 }
11926 }
11927 Err(err) => {
11928 if trace || sg_debug_enabled() {
11929 eprintln!(
11930 "[SG_DEBUG][MOV] object_movie.preview_failed stage={} obj={} file={} err={:#}",
11931 stage_idx, obj_idx, file, err
11932 );
11933 }
11934 }
11935 }
11936}
11937
11938fn install_object_movie_stream_frame(
11939 layers: &mut LayerManager,
11940 images: &mut ImageManager,
11941 obj: &mut globals::ObjectState,
11942 stage_idx: i64,
11943 obj_idx: i64,
11944 file: &str,
11945 frame_idx: usize,
11946 frame: std::sync::Arc<crate::assets::RgbaImage>,
11947 source_size: Option<(u32, u32)>,
11948 trace: bool,
11949) {
11950 let globals::ObjectBackend::Movie {
11951 layer_id,
11952 sprite_id,
11953 image_id,
11954 width,
11955 height,
11956 } = &mut obj.backend
11957 else {
11958 return;
11959 };
11960
11961 let img_id = if let Some(ref id) = obj.movie.frame_image_ids[0] {
11967 let _ = images.replace_image_arc(id, frame.clone());
11968 id.clone()
11969 } else {
11970 let id = images.insert_image_arc(frame.clone());
11971 obj.movie.frame_image_ids[0] = Some(id.clone());
11972 id
11973 };
11974 obj.movie.frame_image_cursor = 0;
11975
11976 let (video_w, video_h) = source_size
11979 .filter(|&(w, h)| w > 0 && h > 0)
11980 .unwrap_or((frame.width, frame.height));
11981 *image_id = Some(img_id.clone());
11982 *width = video_w;
11983 *height = video_h;
11984 if let Some(layer) = layers.layer_mut(*layer_id)
11985 && let Some(sprite) = layer.sprite_mut(*sprite_id)
11986 {
11987 sprite.size_mode = if (video_w, video_h) == (frame.width, frame.height) {
11988 SpriteSizeMode::Intrinsic
11989 } else {
11990 SpriteSizeMode::Explicit {
11991 width: video_w,
11992 height: video_h,
11993 }
11994 };
11995 sprite.image_id = Some(img_id.clone());
11996 sprite.object_anchor = true;
11997 sprite.texture_center_x = 0.0;
11998 sprite.texture_center_y = 0.0;
11999 }
12000 if trace || sg_debug_enabled() {
12001 eprintln!(
12002 "[SG_DEBUG][MOV] object_movie.frame stage={} obj={} file={} frame={} image={:?} size={}x{} timer_ms={}",
12003 stage_idx,
12004 obj_idx,
12005 file,
12006 frame_idx,
12007 img_id,
12008 frame.width,
12009 frame.height,
12010 obj.movie.timer_ms
12011 );
12012 }
12013}
12014
12015fn sync_emote_object_recursive(
12016 layers: &mut LayerManager,
12017 obj: &mut globals::ObjectState,
12018 mouth_stop: bool,
12019 koe_playing: bool,
12020 koe_ex: bool,
12021 koe_chara_no: i64,
12022 live_mouth: f32,
12023) {
12024 if obj.object_type == 12 {
12025 let fallback = obj.emote.koe_mouth_volume as f32 / 1000.0;
12026 let mouth = if !mouth_stop
12027 && obj.emote.koe_chara_no >= 0
12028 && obj.emote.koe_chara_no == koe_chara_no
12029 && !koe_ex
12030 && koe_playing
12031 {
12032 live_mouth
12033 } else {
12034 fallback
12035 };
12036 if let Some(runtime) = obj.emote.runtime.as_mut()
12037 && let Err(err) = runtime.set_face_talk(mouth)
12038 {
12039 log::error!("Emote face_talk update failed: {err:#}");
12040 }
12041 if let globals::ObjectBackend::Rect {
12042 layer_id,
12043 sprite_id,
12044 width,
12045 height,
12046 } = obj.backend
12047 && let Some(sprite) = layers
12048 .layer_mut(layer_id)
12049 .and_then(|layer| layer.sprite_mut(sprite_id))
12050 {
12051 sprite.emote_render = obj.emote.runtime.as_ref().map(|runtime| {
12052 runtime.packet(
12053 obj.emote.width,
12054 obj.emote.height,
12055 obj.emote.rep_x,
12056 obj.emote.rep_y,
12057 obj.button.alpha_test,
12058 )
12059 });
12060 sprite.size_mode = SpriteSizeMode::Explicit { width, height };
12061 sprite.alpha_test = true;
12062 sprite.alpha_blend = true;
12063 }
12064 }
12065 for child in &mut obj.runtime.child_objects {
12066 sync_emote_object_recursive(
12067 layers,
12068 child,
12069 mouth_stop,
12070 koe_playing,
12071 koe_ex,
12072 koe_chara_no,
12073 live_mouth,
12074 );
12075 }
12076}
12077
12078fn sync_movie_object_recursive(
12079 ids: &constants::RuntimeConstants,
12080 layers: &mut LayerManager,
12081 movie_mgr: &mut MovieManager,
12082 audio: &mut AudioHub,
12083 gfx: &mut graphics::GfxRuntime,
12084 images: &mut ImageManager,
12085 stage_idx: i64,
12086 obj_idx: i64,
12087 obj: &mut globals::ObjectState,
12088 _decoded_any: &mut bool,
12089) {
12090 let trace = env_is_set!("SG_MOVIE_TRACE");
12091 if obj.object_type == 9
12092 && let Some(file_name) = obj.file_name.clone()
12093 {
12094 if trace {
12095 eprintln!(
12096 "[SG_MOVIE_TRACE] enter stage={} obj={} file={} playing={} pause={} backend={:?} children={}",
12097 stage_idx,
12098 obj_idx,
12099 file_name,
12100 obj.movie.playing,
12101 obj.movie.pause_flag,
12102 obj.backend,
12103 obj.runtime.child_objects.len()
12104 );
12105 }
12106 let file = file_name.as_str();
12107 if obj.movie.just_finished {
12108 if let Some(id) = obj.movie.audio_id.take() {
12109 movie_mgr.stop_audio(id);
12110 }
12111 obj.movie.just_finished = false;
12112 if obj.movie.auto_free_flag {
12113 if let globals::ObjectBackend::Movie {
12117 layer_id,
12118 sprite_id,
12119 ..
12120 } = obj.backend
12121 && let Some(layer) = layers.layer_mut(layer_id)
12122 && let Some(sprite) = layer.sprite_mut(sprite_id)
12123 {
12124 sprite.visible = false;
12125 sprite.image_id = None;
12126 }
12127 obj.init_type_like();
12128 }
12129 } else if !obj.movie.playing
12130 && let Some(id) = obj.movie.audio_id.take()
12131 {
12132 movie_mgr.stop_audio(id);
12133 }
12134
12135 if obj.object_type == 9 {
12136 let (layer_id, sprite_id) = if let globals::ObjectBackend::Movie {
12137 layer_id,
12138 sprite_id,
12139 ..
12140 } = &obj.backend
12141 {
12142 (*layer_id, *sprite_id)
12143 } else {
12144 let Some(layer_id) = gfx.ensure_stage_layer_id(layers, stage_idx) else {
12145 return;
12146 };
12147 let Some(layer) = layers.layer_mut(layer_id) else {
12148 return;
12149 };
12150 let sid = layer.create_sprite();
12151 if let Some(sprite) = layer.sprite_mut(sid) {
12152 sprite.visible = true;
12153 sprite.alpha = 255;
12154 sprite.fit = SpriteFit::PixelRect;
12155 sprite.size_mode = SpriteSizeMode::Intrinsic;
12156 sprite.object_anchor = true;
12157 sprite.texture_center_x = 0.0;
12158 sprite.texture_center_y = 0.0;
12159 sprite.x = 0;
12160 sprite.y = 0;
12161 sprite.order = 0;
12162 }
12163 obj.backend = globals::ObjectBackend::Movie {
12164 layer_id,
12165 sprite_id: sid,
12166 image_id: None,
12167 width: obj.movie.width,
12168 height: obj.movie.height,
12169 };
12170 (layer_id, sid)
12171 };
12172
12173 if let Some(layer) = layers.layer_mut(layer_id)
12174 && let Some(sprite) = layer.sprite_mut(sprite_id)
12175 {
12176 let render_info =
12177 button_object_render_info(ids, gfx, stage_idx, obj_idx as usize, obj);
12178 apply_button_object_render_info_to_sprite(sprite, &render_info);
12179 finalize_button_object_center_rep_to_sprite(sprite, &render_info);
12180 if ids.obj_alpha != 0 {
12181 sprite.alpha = obj
12182 .lookup_int_prop(ids, ids.obj_alpha)
12183 .unwrap_or(255)
12184 .clamp(0, 255) as u8;
12185 }
12186 if ids.obj_order != 0 {
12187 sprite.order = obj.lookup_int_prop(ids, ids.obj_order).unwrap_or(0) as i32;
12188 }
12189 sprite.blend = crate::layer::SpriteBlend::from_i64(
12190 obj.lookup_int_prop(ids, ids.obj_blend).unwrap_or(0),
12191 );
12192 sprite.object_anchor = true;
12196 sprite.texture_center_x = 0.0;
12197 sprite.texture_center_y = 0.0;
12198 }
12199
12200 install_object_movie_preview_if_missing(
12205 layers, movie_mgr, images, obj, stage_idx, obj_idx, file, trace,
12206 );
12207
12208 if obj.movie.seeked
12209 && let Some(id) = obj.movie.audio_id.take()
12210 {
12211 movie_mgr.stop_audio(id);
12212 }
12213 obj.movie.seeked = false;
12214 obj.movie.just_looped = false;
12215
12216 if let Some(id) = obj.movie.audio_id {
12217 if let Some(position_ms) = movie_mgr.audio_playback_position_ms(id) {
12218 obj.movie.timer_ms = position_ms;
12219 if movie_mgr.audio_playback_finished(id) {
12220 obj.movie.audio_id = None;
12221 if !obj.movie.loop_flag {
12222 if let Some(total_ms) = obj.movie.total_ms {
12223 obj.movie.timer_ms = total_ms;
12224 }
12225 obj.movie.playing = false;
12226 obj.movie.just_finished = true;
12227 }
12228 }
12229 } else {
12230 obj.movie.audio_id = None;
12233 obj.movie.audio_started_once = true;
12234 }
12235 }
12236
12237 if obj.movie.pause_flag {
12243 if let globals::ObjectBackend::Movie {
12244 layer_id,
12245 sprite_id,
12246 image_id,
12247 width,
12248 height,
12249 } = &mut obj.backend
12250 && image_id.is_none()
12251 {
12252 match movie_mgr.ensure_preview_frame(file) {
12253 Ok(frame) => {
12254 let img_id = images.insert_image_arc(frame.clone());
12255 *image_id = Some(img_id.clone());
12256 *width = frame.width;
12257 *height = frame.height;
12258 if let Some(layer) = layers.layer_mut(*layer_id)
12259 && let Some(sprite) = layer.sprite_mut(*sprite_id)
12260 {
12261 sprite.image_id = Some(img_id);
12262 sprite.object_anchor = true;
12263 sprite.texture_center_x = 0.0;
12264 sprite.texture_center_y = 0.0;
12265 }
12266 if trace {
12267 eprintln!(
12268 "[SG_MOVIE_TRACE] installed paused preview stage={} obj={} file={} size={}x{}",
12269 stage_idx, obj_idx, file, frame.width, frame.height,
12270 );
12271 }
12272 }
12273 Err(err) => {
12274 if trace {
12275 eprintln!(
12276 "[SG_MOVIE_TRACE] paused preview decode failed stage={} obj={} file={} err={:#}",
12277 stage_idx, obj_idx, file, err,
12278 );
12279 }
12280 }
12281 }
12282 }
12283 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12284 sync_movie_object_recursive(
12285 ids,
12286 layers,
12287 movie_mgr,
12288 audio,
12289 gfx,
12290 images,
12291 stage_idx,
12292 object_runtime_slot(child_idx, child) as i64,
12293 child,
12294 _decoded_any,
12295 );
12296 }
12297 return;
12298 }
12299
12300 if trace {
12301 eprintln!(
12302 "[SG_MOVIE_TRACE] poll_stream stage={} obj={} file={}",
12303 stage_idx, obj_idx, file
12304 );
12305 }
12306 let polled = match movie_mgr.poll_global_movie_frame_with_loop(
12307 file,
12308 obj.movie.timer_ms,
12309 obj.movie.loop_flag,
12310 ) {
12311 Ok(Some(frame)) => frame,
12312 Ok(None) => {
12313 if obj.movie.last_frame_idx.is_none() {
12314 obj.movie.timer_ms = 0;
12315 obj.movie.last_tick = Some(crate::platform_time::Instant::now());
12316 }
12317 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12318 sync_movie_object_recursive(
12319 ids,
12320 layers,
12321 movie_mgr,
12322 audio,
12323 gfx,
12324 images,
12325 stage_idx,
12326 object_runtime_slot(child_idx, child) as i64,
12327 child,
12328 _decoded_any,
12329 );
12330 }
12331 return;
12332 }
12333 Err(err) => {
12334 eprintln!(
12335 "[SG_MOVIE] object movie error stage={} obj={} file={}: {:#}",
12336 stage_idx, obj_idx, file, err
12337 );
12338 obj.movie.playing = false;
12339 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12340 sync_movie_object_recursive(
12341 ids,
12342 layers,
12343 movie_mgr,
12344 audio,
12345 gfx,
12346 images,
12347 stage_idx,
12348 object_runtime_slot(child_idx, child) as i64,
12349 child,
12350 _decoded_any,
12351 );
12352 }
12353 return;
12354 }
12355 };
12356 if obj.movie.total_ms.is_none() || polled.total_ms.is_some() {
12357 obj.movie.total_ms = polled.total_ms.or(obj.movie.total_ms);
12358 }
12359 let frame_idx = polled.frame_idx;
12360 let last_idx = obj.movie.last_frame_idx;
12361 let small_rewind = last_idx
12362 .is_some_and(|last| frame_idx < last && frame_idx.saturating_add(1) >= last);
12363 if last_idx != Some(frame_idx) && !small_rewind {
12364 obj.movie.last_frame_idx = Some(frame_idx);
12365 let frame = polled.frame.clone();
12366 install_object_movie_stream_frame(
12367 layers,
12368 images,
12369 obj,
12370 stage_idx,
12371 obj_idx,
12372 file,
12373 frame_idx,
12374 frame,
12375 polled.source_size,
12376 trace,
12377 );
12378 }
12379 if obj.movie.playing && obj.movie.audio_id.is_none() {
12387 if let Some(track) = polled.audio.as_ref() {
12388 if let Ok(id) =
12389 movie_mgr.start_audio(audio, track, obj.movie.timer_ms, obj.movie.loop_flag)
12390 {
12391 obj.movie.audio_id = Some(id);
12392 obj.movie.audio_started_once = true;
12393 }
12394 } else if polled.audio_ready {
12395 obj.movie.audio_started_once = true;
12398 }
12399 }
12400 }
12401 }
12402
12403 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12404 sync_movie_object_recursive(
12405 ids,
12406 layers,
12407 movie_mgr,
12408 audio,
12409 gfx,
12410 images,
12411 stage_idx,
12412 object_runtime_slot(child_idx, child) as i64,
12413 child,
12414 _decoded_any,
12415 );
12416 }
12417}
12418
12419fn apply_object_masks_recursive(
12420 ids: &constants::RuntimeConstants,
12421 gfx: &mut graphics::GfxRuntime,
12422 layers: &mut LayerManager,
12423 stage_i64: i64,
12424 obj_i64: i64,
12425 obj: &mut globals::ObjectState,
12426 mask_info: &[Option<(String, i32, i32)>],
12427 resolved_masks: &HashMap<String, ImageHandle>,
12428) {
12429 let mask_no = if ids.obj_mask_no != 0 {
12430 obj.lookup_int_prop(ids, ids.obj_mask_no).unwrap_or(-1)
12431 } else {
12432 -1
12433 };
12434 let mask_binding = usize::try_from(mask_no)
12435 .ok()
12436 .and_then(|mask_idx| mask_info.get(mask_idx))
12437 .and_then(|entry| entry.as_ref())
12438 .and_then(|(mask_name, mask_x, mask_y)| {
12439 resolved_masks
12440 .get(mask_name)
12441 .cloned()
12442 .map(|mask_image_id| (mask_image_id, *mask_x, *mask_y))
12443 });
12444
12445 let targets: Vec<(LayerId, SpriteId)> = match &obj.backend {
12446 globals::ObjectBackend::Gfx => gfx
12447 .object_sprite_binding(stage_i64, obj_i64)
12448 .into_iter()
12449 .collect(),
12450 backend => layer_backed_object_sprite_bindings(backend),
12451 };
12452
12453 for (layer_id, sprite_id) in targets {
12454 let Some(sprite) = layers
12455 .layer_mut(layer_id)
12456 .and_then(|layer| layer.sprite_mut(sprite_id))
12457 else {
12458 continue;
12459 };
12460 if let Some((mask_image_id, mask_x, mask_y)) = mask_binding.clone() {
12461 sprite.mask_image_id = Some(mask_image_id);
12462 sprite.mask_offset_x = mask_x;
12463 sprite.mask_offset_y = mask_y;
12464 } else {
12465 sprite.mask_image_id = None;
12466 sprite.mask_offset_x = 0;
12467 sprite.mask_offset_y = 0;
12468 }
12469 }
12470
12471 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12472 apply_object_masks_recursive(
12473 ids,
12474 gfx,
12475 layers,
12476 stage_i64,
12477 object_runtime_slot(child_idx, child) as i64,
12478 child,
12479 mask_info,
12480 resolved_masks,
12481 );
12482 }
12483}
12484
12485fn apply_object_tonecurves_recursive(
12486 ids: &constants::RuntimeConstants,
12487 gfx: &mut graphics::GfxRuntime,
12488 images: &mut ImageManager,
12489 layers: &mut LayerManager,
12490 tonecurve: &mut tonecurve::ToneCurveRuntime,
12491 stage_i64: i64,
12492 obj_i64: i64,
12493 obj: &mut globals::ObjectState,
12494) {
12495 let tonecurve_no = if ids.obj_tonecurve_no != 0 {
12496 obj.lookup_int_prop(ids, ids.obj_tonecurve_no).unwrap_or(-1)
12497 } else {
12498 -1
12499 };
12500 if tonecurve_no >= 0
12501 && let Some((tonecurve_image_id, tonecurve_row, tonecurve_sat)) =
12502 tonecurve.shader_binding(images, tonecurve_no as i32)
12503 {
12504 let targets: Vec<(LayerId, SpriteId)> = match &obj.backend {
12505 globals::ObjectBackend::Gfx => gfx
12506 .object_sprite_binding(stage_i64, obj_i64)
12507 .into_iter()
12508 .collect(),
12509 backend => layer_backed_object_sprite_bindings(backend),
12510 };
12511 for (layer_id, sprite_id) in targets {
12512 if let Some(sprite) = layers
12513 .layer_mut(layer_id)
12514 .and_then(|l| l.sprite_mut(sprite_id))
12515 {
12516 sprite.tonecurve_image_id = Some(tonecurve_image_id.clone());
12517 sprite.tonecurve_row = tonecurve_row;
12518 sprite.tonecurve_sat = tonecurve_sat;
12519 }
12520 }
12521 }
12522
12523 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12524 apply_object_tonecurves_recursive(
12525 ids,
12526 gfx,
12527 images,
12528 layers,
12529 tonecurve,
12530 stage_i64,
12531 object_runtime_slot(child_idx, child) as i64,
12532 child,
12533 );
12534 }
12535}
12536
12537fn apply_gan_effects_recursive(
12538 gfx: &mut graphics::GfxRuntime,
12539 images: &mut ImageManager,
12540 sprites: &mut Vec<RenderSprite>,
12541 index: &HashMap<(Option<LayerId>, Option<SpriteId>), usize>,
12542 stage_i64: i64,
12543 obj_i64: i64,
12544 obj: &mut globals::ObjectState,
12545) {
12546 if let Some(pat) = obj.gan.current_pat()
12547 && !(pat.pat_no == 0 && pat.x == 0 && pat.y == 0 && pat.tr == 255)
12548 {
12549 let keys: Vec<(LayerId, SpriteId)> = match &obj.backend {
12550 globals::ObjectBackend::Gfx => gfx
12551 .object_sprite_binding(stage_i64, obj_i64)
12552 .into_iter()
12553 .collect(),
12554 backend => layer_backed_object_sprite_bindings(backend),
12555 };
12556 let replacement_image = if pat.pat_no != 0 {
12557 gfx.object_peek_file(stage_i64, obj_i64).and_then(|file| {
12558 let base_pat = gfx.object_peek_patno(stage_i64, obj_i64).unwrap_or(0);
12559 let pat_no = (base_pat + pat.pat_no as i64).max(0) as u32;
12560 images.load_g00(&file, pat_no).ok()
12561 })
12562 } else {
12563 None
12564 };
12565 for (layer_id, sprite_id) in keys {
12566 let Some(&idx) = index.get(&(Some(layer_id), Some(sprite_id))) else {
12567 continue;
12568 };
12569 let sprite = &mut sprites[idx].sprite;
12570 if pat.x != 0 {
12571 sprite.x = sprite.x.saturating_add(pat.x);
12572 }
12573 if pat.y != 0 {
12574 sprite.y = sprite.y.saturating_add(pat.y);
12575 }
12576 if pat.tr != 255 {
12577 let tr = (sprite.tr as i64 * pat.tr as i64 / 255).clamp(0, 255) as u8;
12578 sprite.tr = tr;
12579 }
12580 if let Some(id) = replacement_image.clone() {
12581 sprite.image_id = Some(id);
12582 }
12583 }
12584 }
12585
12586 for (child_idx, child) in obj.runtime.child_objects.iter_mut().enumerate() {
12587 apply_gan_effects_recursive(
12588 gfx,
12589 images,
12590 sprites,
12591 index,
12592 stage_i64,
12593 object_runtime_slot(child_idx, child) as i64,
12594 child,
12595 );
12596 }
12597}
12598
12599fn build_parent_render_state(
12600 info: &ObjectRenderInfo,
12601 first_sprite: Option<&Sprite>,
12602) -> ParentRenderState {
12603 ParentRenderState {
12604 world_no: info.world_no,
12605 pos_x: (info.x + info.x_rep) as f32,
12606 pos_y: (info.y + info.y_rep) as f32,
12607 pos_z: (info.z + info.z_rep) as f32,
12608 center_rep_x: info.center_rep_x as f32,
12609 center_rep_y: info.center_rep_y as f32,
12610 center_rep_z: info.center_rep_z as f32,
12611 scale_x: info.scale_x as f32 / 1000.0,
12612 scale_y: info.scale_y as f32 / 1000.0,
12613 scale_z: info.scale_z as f32 / 1000.0,
12614 rotate_x: info.rotate_x as f32 * std::f32::consts::PI / 1800.0,
12615 rotate_y: info.rotate_y as f32 * std::f32::consts::PI / 1800.0,
12616 rotate_z: info.rotate_z as f32 * std::f32::consts::PI / 1800.0,
12617 tr: ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255) as i32,
12618 mono: info.mono.clamp(0, 255) as i32,
12619 reverse: info.reverse.clamp(0, 255) as i32,
12620 bright: info.bright.clamp(0, 255) as i32,
12621 dark: info.dark.clamp(0, 255) as i32,
12622 color_rate: info.color_rate.clamp(0, 255) as i32,
12623 color_r: info.color_r.clamp(0, 255) as i32,
12624 color_g: info.color_g.clamp(0, 255) as i32,
12625 color_b: info.color_b.clamp(0, 255) as i32,
12626 color_add_r: info.color_add_r.clamp(0, 255) as i32,
12627 color_add_g: info.color_add_g.clamp(0, 255) as i32,
12628 color_add_b: info.color_add_b.clamp(0, 255) as i32,
12629 blend: info.blend,
12630 dst_clip: info.dst_clip,
12631 mask_image_id: first_sprite.and_then(|s| s.mask_image_id.clone()),
12632 mask_offset_x: first_sprite.map(|s| s.mask_offset_x).unwrap_or(0),
12633 mask_offset_y: first_sprite.map(|s| s.mask_offset_y).unwrap_or(0),
12634 tonecurve_image_id: first_sprite.and_then(|s| s.tonecurve_image_id.clone()),
12635 tonecurve_row: first_sprite.map(|s| s.tonecurve_row).unwrap_or(0.0),
12636 tonecurve_sat: first_sprite.map(|s| s.tonecurve_sat).unwrap_or(0.0),
12637 }
12638}
12639
12640fn apply_parent_render_state_to_sprite(
12641 sprite: &mut Sprite,
12642 _info: &ObjectRenderInfo,
12643 state: &ParentRenderState,
12644) {
12645 let local_x = sprite.x as f32;
12646 let local_y = sprite.y as f32;
12647 let local_z = sprite.z;
12648
12649 let mut rel_x = local_x - state.center_rep_x;
12650 let mut rel_y = local_y - state.center_rep_y;
12651 rel_x *= state.scale_x;
12652 rel_y *= state.scale_y;
12653 let (sin_z, cos_z) = state.rotate_z.sin_cos();
12654 let rot_x = rel_x * cos_z - rel_y * sin_z;
12655 let rot_y = rel_x * sin_z + rel_y * cos_z;
12656
12657 sprite.x = (state.pos_x + state.center_rep_x + rot_x).round() as i32;
12658 sprite.y = (state.pos_y + state.center_rep_y + rot_y).round() as i32;
12659 sprite.z = state.pos_z + state.center_rep_z + local_z * state.scale_z;
12660 sprite.scale_x *= state.scale_x;
12661 sprite.scale_y *= state.scale_y;
12662 sprite.scale_z *= state.scale_z;
12663 sprite.rotate_x += state.rotate_x;
12664 sprite.rotate_y += state.rotate_y;
12665 sprite.rotate += state.rotate_z;
12666
12667 sprite.tr = ((sprite.tr as i32 * state.tr.clamp(0, 255)) / 255).clamp(0, 255) as u8;
12668 sprite.mono = combine_lerp(sprite.mono, state.mono);
12669 sprite.reverse = combine_lerp(sprite.reverse, state.reverse);
12670 sprite.bright = combine_lerp(sprite.bright, state.bright);
12671 sprite.dark = combine_lerp(sprite.dark, state.dark);
12672 if (sprite.color_rate as i32) + state.color_rate > 0 {
12673 let parent_rate = (state.color_rate * 255 * 255)
12674 / (255 * 255 - (255 - sprite.color_rate as i32) * (255 - state.color_rate)).max(1);
12675 sprite.color_r = blend_color(sprite.color_r, state.color_r, parent_rate);
12676 sprite.color_g = blend_color(sprite.color_g, state.color_g, parent_rate);
12677 sprite.color_b = blend_color(sprite.color_b, state.color_b, parent_rate);
12678 sprite.color_rate = combine_lerp(sprite.color_rate, state.color_rate);
12679 }
12680 sprite.color_add_r = sprite
12681 .color_add_r
12682 .saturating_add(state.color_add_r.clamp(0, 255) as u8);
12683 sprite.color_add_g = sprite
12684 .color_add_g
12685 .saturating_add(state.color_add_g.clamp(0, 255) as u8);
12686 sprite.color_add_b = sprite
12687 .color_add_b
12688 .saturating_add(state.color_add_b.clamp(0, 255) as u8);
12689 if matches!(sprite.blend, crate::layer::SpriteBlend::Normal) {
12690 sprite.blend = state.blend;
12691 }
12692 let child_clip = sprite.dst_clip;
12693 sprite.dst_clip = compose_clip_rect(state.dst_clip, child_clip);
12694 if state.dst_clip.is_some() && child_clip.is_some() && sprite.dst_clip.is_none() {
12695 sprite.tr = 0;
12696 }
12697 if sprite.mask_image_id.is_none() {
12698 sprite.mask_image_id = state.mask_image_id.clone();
12699 sprite.mask_offset_x = state.mask_offset_x;
12700 sprite.mask_offset_y = state.mask_offset_y;
12701 }
12702 if sprite.tonecurve_image_id.is_none() {
12703 sprite.tonecurve_image_id = state.tonecurve_image_id.clone();
12704 sprite.tonecurve_row = state.tonecurve_row;
12705 sprite.tonecurve_sat = state.tonecurve_sat;
12706 }
12707
12708 if state.world_no >= 0 {
12709 sprite.world_no = state.world_no as i32;
12710 }
12711}
12712
12713fn apply_world_camera_mode(
12714 sprite: &mut Sprite,
12715 worlds: Option<&Vec<globals::WorldState>>,
12716 screen_w: u32,
12717 screen_h: u32,
12718) {
12719 if sprite.world_no < 0 {
12720 return;
12721 }
12722 let Some(worlds) = worlds else {
12723 return;
12724 };
12725 let Some(world) = worlds.get(sprite.world_no as usize) else {
12726 return;
12727 };
12728
12729 let cam_eye = [
12730 world.camera_eye_x.get_total_value() as f32,
12731 world.camera_eye_y.get_total_value() as f32,
12732 world.camera_eye_z.get_total_value() as f32,
12733 ];
12734 let cam_target = [
12735 world.camera_pint_x.get_total_value() as f32,
12736 world.camera_pint_y.get_total_value() as f32,
12737 world.camera_pint_z.get_total_value() as f32,
12738 ];
12739 let cam_up = [
12740 world.camera_up_x.get_total_value() as f32,
12741 world.camera_up_y.get_total_value() as f32,
12742 world.camera_up_z.get_total_value() as f32,
12743 ];
12744 sprite.camera_view_angle_deg = (world.camera_view_angle as f32) / 10.0;
12745 if world.mono != 0 {
12746 let base = sprite.mono as i32;
12747 let parent = world.mono.clamp(0, 255);
12748 sprite.mono = (255 - (255 - base) * (255 - parent) / 255) as u8;
12749 }
12750
12751 if world.mode == 0 {
12752 let dz = sprite.z - cam_eye[2];
12753 if dz <= 0.0 {
12754 sprite.visible = false;
12755 return;
12756 }
12757 let camera_scale = 1000.0 / dz;
12758 let sw = screen_w as f32;
12759 let sh = screen_h as f32;
12760 sprite.x = (((sprite.x as f32) - cam_eye[0]) * camera_scale + sw * 0.5)
12761 .round()
12762 .clamp(i32::MIN as f32, i32::MAX as f32) as i32;
12763 sprite.y = (((sprite.y as f32) - cam_eye[1]) * camera_scale + sh * 0.5)
12764 .round()
12765 .clamp(i32::MIN as f32, i32::MAX as f32) as i32;
12766 sprite.scale_x *= camera_scale;
12767 sprite.scale_y *= camera_scale;
12768 sprite.z = 0.0;
12769 sprite.pivot_z = 0.0;
12770 sprite.scale_z = 1.0;
12771 sprite.rotate_x = 0.0;
12772 sprite.rotate_y = 0.0;
12773 sprite.billboard = false;
12774 sprite.culling = false;
12775 sprite.fog_use = false;
12776 sprite.light_no = -1;
12777 sprite.light_enabled = false;
12778 sprite.light_diffuse = [1.0, 1.0, 1.0, 1.0];
12779 sprite.light_ambient = [0.0, 0.0, 0.0, 1.0];
12780 sprite.light_specular = [0.0, 0.0, 0.0, 1.0];
12781 sprite.light_factor = 0.0;
12782 sprite.light_kind = -1;
12783 sprite.light_pos = [0.0, 0.0, 0.0, 0.0];
12784 sprite.light_dir = [0.0, 0.0, -1.0, 0.0];
12785 sprite.light_atten = [1.0, 0.0, 0.0, 5000.0];
12786 sprite.light_cone = [0.0, 0.0, 1.0, 0.0];
12787 sprite.fog_enabled = false;
12788 sprite.fog_color = [0.0, 0.0, 0.0, 1.0];
12789 sprite.fog_near = 0.0;
12790 sprite.fog_far = 0.0;
12791 sprite.fog_scroll_x = 0.0;
12792 sprite.fog_texture_image_id = None;
12793 sprite.camera_enabled = false;
12794 sprite.camera_eye = [0.0, 0.0, -1000.0];
12795 sprite.camera_target = [0.0, 0.0, 0.0];
12796 sprite.camera_up = [0.0, 1.0, 0.0];
12797 return;
12798 }
12799
12800 sprite.camera_enabled = true;
12801 sprite.camera_eye = cam_eye;
12802 sprite.camera_target = cam_target;
12803 sprite.camera_up = cam_up;
12804}
12805
12806fn fetch_bound_render_sprites(
12807 ctx: &CommandContext,
12808 stage_idx: i64,
12809 runtime_slot: usize,
12810 obj: &globals::ObjectState,
12811) -> Vec<RenderSprite> {
12812 fetch_bound_render_sprites_impl(ctx, stage_idx, runtime_slot, obj, false)
12817}
12818
12819fn fetch_bound_render_sprites_any(
12820 ctx: &CommandContext,
12821 stage_idx: i64,
12822 runtime_slot: usize,
12823 obj: &globals::ObjectState,
12824) -> Vec<RenderSprite> {
12825 fetch_bound_render_sprites_impl(ctx, stage_idx, runtime_slot, obj, false)
12826}
12827
12828fn fetch_bound_render_sprites_impl(
12829 ctx: &CommandContext,
12830 stage_idx: i64,
12831 runtime_slot: usize,
12832 obj: &globals::ObjectState,
12833 visible_only: bool,
12834) -> Vec<RenderSprite> {
12835 fn push_one(
12836 ctx: &CommandContext,
12837 lid: LayerId,
12838 sid: SpriteId,
12839 visible_only: bool,
12840 out: &mut Vec<RenderSprite>,
12841 ) {
12842 let Some(layer) = ctx.layers.layer(lid) else {
12843 return;
12844 };
12845 let Some(sprite) = layer.sprite(sid) else {
12846 return;
12847 };
12848 if visible_only && !sprite.visible {
12849 return;
12850 }
12851 if !sprite_has_render_payload(sprite) {
12852 return;
12853 }
12854 out.push(RenderSprite::new(Some(lid), Some(sid), sprite.clone()));
12855 }
12856
12857 let mut out = Vec::new();
12858 match &obj.backend {
12859 globals::ObjectBackend::Gfx => {
12860 if let Some((lid, sid)) = ctx
12861 .gfx
12862 .object_sprite_binding(stage_idx, runtime_slot as i64)
12863 {
12864 push_one(ctx, lid, sid, visible_only, &mut out);
12865 }
12866 }
12867 globals::ObjectBackend::None => {}
12868 globals::ObjectBackend::Number {
12869 layer_id,
12870 sprite_ids,
12871 } => {
12872 for (&sprite_id, offset) in sprite_ids.iter().zip(&obj.runtime.number_sprite_offsets) {
12875 if offset.is_some() {
12876 push_one(ctx, *layer_id, sprite_id, visible_only, &mut out);
12877 }
12878 }
12879 }
12880 backend => {
12881 for (layer_id, sprite_id) in layer_backed_object_sprite_bindings(backend) {
12882 push_one(ctx, layer_id, sprite_id, visible_only, &mut out);
12883 }
12884 }
12885 }
12886 out
12887}
12888
12889fn effective_object_info(
12890 ctx: &CommandContext,
12891 _stage_idx: i64,
12892 obj_idx: usize,
12893 obj: &globals::ObjectState,
12894) -> ObjectRenderInfo {
12895 let runtime_slot = object_runtime_slot(obj_idx, obj);
12896 let ids = &ctx.ids;
12897 let base = &obj.base;
12898 let ev = &obj.runtime.prop_events;
12899
12900 macro_rules! event_or_base {
12901 ($prop_id:expr, $event_id:expr, $field:ident) => {{
12902 if $prop_id != 0 || $event_id != 0 {
12903 ev.$field.get_total_value() as i64
12904 } else {
12905 base.$field
12906 }
12907 }};
12908 }
12909
12910 let dst_clip = if base.clip_use != 0 {
12916 Some(ClipRect {
12917 left: event_or_base!(ids.obj_clip_left, ids.obj_clip_left_eve, clip_left) as i32,
12918 top: event_or_base!(ids.obj_clip_top, ids.obj_clip_top_eve, clip_top) as i32,
12919 right: event_or_base!(ids.obj_clip_right, ids.obj_clip_right_eve, clip_right) as i32,
12920 bottom: event_or_base!(ids.obj_clip_bottom, ids.obj_clip_bottom_eve, clip_bottom)
12921 as i32,
12922 })
12923 } else {
12924 None
12925 };
12926
12927 let x_rep_total = obj
12928 .runtime
12929 .prop_event_lists
12930 .x_rep
12931 .iter()
12932 .map(|ev| ev.get_total_value() as i64)
12933 .sum::<i64>();
12934 let y_rep_total = obj
12935 .runtime
12936 .prop_event_lists
12937 .y_rep
12938 .iter()
12939 .map(|ev| ev.get_total_value() as i64)
12940 .sum::<i64>();
12941 let z_rep_total = obj
12942 .runtime
12943 .prop_event_lists
12944 .z_rep
12945 .iter()
12946 .map(|ev| ev.get_total_value() as i64)
12947 .sum::<i64>();
12948 let tr_rep_total = obj
12949 .runtime
12950 .prop_event_lists
12951 .tr_rep
12952 .iter()
12953 .fold(255i64, |acc, ev| {
12954 acc.saturating_mul(ev.get_total_value() as i64)
12955 .div_euclid(255)
12956 });
12957
12958 let info = ObjectRenderInfo {
12959 runtime_slot,
12960 used: obj.used,
12961 object_type: obj.object_type,
12962 disp: base.disp != 0,
12963 x: event_or_base!(ids.obj_x, ids.obj_x_eve, x),
12964 y: event_or_base!(ids.obj_y, ids.obj_y_eve, y),
12965 x_rep: x_rep_total,
12966 y_rep: y_rep_total,
12967 z_rep: z_rep_total,
12968 order: base.order,
12969 layer: base.layer,
12970 alpha: base.alpha,
12971 tr: event_or_base!(ids.obj_tr, ids.obj_tr_eve, tr),
12972 tr_rep: tr_rep_total,
12973 mono: event_or_base!(ids.obj_mono, ids.obj_mono_eve, mono),
12974 reverse: event_or_base!(ids.obj_reverse, ids.obj_reverse_eve, reverse),
12975 bright: event_or_base!(ids.obj_bright, ids.obj_bright_eve, bright),
12976 dark: event_or_base!(ids.obj_dark, ids.obj_dark_eve, dark),
12977 color_rate: event_or_base!(ids.obj_color_rate, ids.obj_color_rate_eve, color_rate),
12978 color_add_r: event_or_base!(ids.obj_color_add_r, ids.obj_color_add_r_eve, color_add_r),
12979 color_add_g: event_or_base!(ids.obj_color_add_g, ids.obj_color_add_g_eve, color_add_g),
12980 color_add_b: event_or_base!(ids.obj_color_add_b, ids.obj_color_add_b_eve, color_add_b),
12981 color_r: event_or_base!(ids.obj_color_r, ids.obj_color_r_eve, color_r),
12982 color_g: event_or_base!(ids.obj_color_g, ids.obj_color_g_eve, color_g),
12983 color_b: event_or_base!(ids.obj_color_b, ids.obj_color_b_eve, color_b),
12984 z: event_or_base!(ids.obj_z, ids.obj_z_eve, z),
12985 world_no: base.world,
12986 center_x: event_or_base!(ids.obj_center_x, ids.obj_center_x_eve, center_x),
12987 center_y: event_or_base!(ids.obj_center_y, ids.obj_center_y_eve, center_y),
12988 center_z: event_or_base!(ids.obj_center_z, ids.obj_center_z_eve, center_z),
12989 center_rep_x: event_or_base!(ids.obj_center_rep_x, ids.obj_center_rep_x_eve, center_rep_x),
12990 center_rep_y: event_or_base!(ids.obj_center_rep_y, ids.obj_center_rep_y_eve, center_rep_y),
12991 center_rep_z: event_or_base!(ids.obj_center_rep_z, ids.obj_center_rep_z_eve, center_rep_z),
12992 scale_x: event_or_base!(ids.obj_scale_x, ids.obj_scale_x_eve, scale_x),
12993 scale_y: event_or_base!(ids.obj_scale_y, ids.obj_scale_y_eve, scale_y),
12994 scale_z: event_or_base!(ids.obj_scale_z, ids.obj_scale_z_eve, scale_z),
12995 rotate_x: event_or_base!(ids.obj_rotate_x, ids.obj_rotate_x_eve, rotate_x),
12996 rotate_y: event_or_base!(ids.obj_rotate_y, ids.obj_rotate_y_eve, rotate_y),
12997 rotate_z: event_or_base!(ids.obj_rotate_z, ids.obj_rotate_z_eve, rotate_z),
12998 culling: base.culling != 0,
12999 alpha_test: base.alpha_test != 0,
13000 alpha_blend: base.alpha_blend != 0,
13001 fog_use: base.fog_use != 0,
13002 light_no: base.light_no,
13003 blend: crate::layer::SpriteBlend::from_i64(base.blend),
13004 child_sort_type: base.child_sort_type,
13005 dst_clip,
13006 billboard: obj.object_type == 7,
13007 file_name: obj.file_name.clone(),
13008 mesh_animation: obj.mesh_animation_state.clone(),
13009 };
13010
13011 info
13019}
13020
13021fn configure_sprite_3d(
13022 sprite: &mut crate::layer::Sprite,
13023 info: &ObjectRenderInfo,
13024 _worlds: Option<&Vec<globals::WorldState>>,
13025 _screen_w: u32,
13026 _screen_h: u32,
13027) {
13028 sprite.z = info.z as f32;
13029 sprite.pivot_z = info.center_z as f32;
13030 sprite.scale_z = info.scale_z as f32 / 1000.0;
13031 sprite.rotate_x = info.rotate_x as f32 * std::f32::consts::PI / 1800.0;
13032 sprite.rotate_y = info.rotate_y as f32 * std::f32::consts::PI / 1800.0;
13033 sprite.culling = info.culling;
13034 sprite.alpha_test = info.alpha_test;
13035 sprite.alpha_blend = object_alpha_blend_for_render(info.object_type, info.alpha_blend);
13039 sprite.fog_use = info.fog_use;
13040 sprite.light_no = info.light_no as i32;
13041 sprite.world_no = info.world_no as i32;
13042 sprite.billboard = info.billboard;
13043 sprite.mesh_file_name = if info.object_type == 6 {
13044 info.file_name.clone()
13045 } else {
13046 None
13047 };
13048 sprite.mesh_kind = if info.object_type == 6 { 1 } else { 0 };
13049 sprite.shadow_cast = sprite.mesh_kind != 0;
13050 sprite.shadow_receive = sprite.mesh_kind != 0;
13051 sprite.mesh_animation = info.mesh_animation.clone();
13052
13053 sprite.camera_enabled = matches!(info.object_type, 6 | 7) || info.billboard;
13056 sprite.camera_eye = [0.0, 0.0, -1000.0];
13057 sprite.camera_target = [0.0, 0.0, 0.0];
13058 sprite.camera_up = [0.0, 1.0, 0.0];
13059 sprite.camera_view_angle_deg = 45.0;
13060}
13061
13062fn object_alpha_blend_for_render(object_type: i64, requested: bool) -> bool {
13063 object_type != 6 && requested
13064}
13065
13066#[derive(Debug)]
13067enum ObjectMotionTraceFilter {
13068 Disabled,
13069 All,
13070 Files(Vec<String>),
13071}
13072
13073fn object_motion_trace_filter() -> &'static ObjectMotionTraceFilter {
13074 static FILTER: std::sync::OnceLock<ObjectMotionTraceFilter> = std::sync::OnceLock::new();
13075 FILTER.get_or_init(|| {
13076 let Some(raw) = std::env::var_os("SG_OBJECT_MOTION_TRACE") else {
13077 return ObjectMotionTraceFilter::Disabled;
13078 };
13079 let raw = raw.to_string_lossy();
13080 let raw = raw.trim();
13081 if raw.is_empty()
13082 || raw == "1"
13083 || raw.eq_ignore_ascii_case("true")
13084 || raw.eq_ignore_ascii_case("all")
13085 || raw == "*"
13086 {
13087 return ObjectMotionTraceFilter::All;
13088 }
13089
13090 let files = raw
13091 .split(',')
13092 .map(str::trim)
13093 .filter(|item| !item.is_empty())
13094 .map(|item| item.to_ascii_lowercase())
13095 .collect::<Vec<_>>();
13096 if files.is_empty() {
13097 ObjectMotionTraceFilter::All
13098 } else {
13099 ObjectMotionTraceFilter::Files(files)
13100 }
13101 })
13102}
13103
13104fn object_motion_trace_enabled() -> bool {
13105 !matches!(
13106 object_motion_trace_filter(),
13107 ObjectMotionTraceFilter::Disabled
13108 )
13109}
13110
13111fn object_motion_trace_object(obj: &globals::ObjectState) -> bool {
13112 match object_motion_trace_filter() {
13113 ObjectMotionTraceFilter::Disabled => false,
13114 ObjectMotionTraceFilter::All => true,
13115 ObjectMotionTraceFilter::Files(filters) => {
13116 let Some(file_name) = obj.file_name.as_deref() else {
13117 return false;
13118 };
13119 let normalized = file_name.replace('\\', "/").to_ascii_lowercase();
13120 let base = normalized.rsplit('/').next().unwrap_or(normalized.as_str());
13121 let stem = base.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(base);
13122 filters
13123 .iter()
13124 .any(|filter| filter == &normalized || filter == base || filter == stem)
13125 }
13126 }
13127}
13128
13129fn object_motion_trace_bind(
13130 ctx: &CommandContext,
13131 stage_idx: i64,
13132 info: &ObjectRenderInfo,
13133 obj: &globals::ObjectState,
13134) -> Option<(LayerId, SpriteId)> {
13135 match &obj.backend {
13136 globals::ObjectBackend::Gfx => ctx
13137 .gfx
13138 .object_sprite_binding(stage_idx, info.runtime_slot as i64),
13139 globals::ObjectBackend::Rect {
13140 layer_id,
13141 sprite_id,
13142 ..
13143 }
13144 | globals::ObjectBackend::String {
13145 layer_id,
13146 sprite_id,
13147 ..
13148 }
13149 | globals::ObjectBackend::Movie {
13150 layer_id,
13151 sprite_id,
13152 ..
13153 } => Some((*layer_id, *sprite_id)),
13154 globals::ObjectBackend::Number {
13155 layer_id,
13156 sprite_ids,
13157 }
13158 | globals::ObjectBackend::Weather {
13159 layer_id,
13160 sprite_ids,
13161 } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
13162 globals::ObjectBackend::None => None,
13163 }
13164}
13165
13166fn object_motion_trace_emit(
13167 ctx: &CommandContext,
13168 stage_idx: i64,
13169 obj_idx: usize,
13170 obj: &globals::ObjectState,
13171 info: &ObjectRenderInfo,
13172 parent_visible: bool,
13173 visible: bool,
13174 local_tr: i64,
13175 total_order: i64,
13176 total_layer: i64,
13177 bound_len: usize,
13178 bind_dbg: Option<(LayerId, SpriteId)>,
13179 emitted: bool,
13180 sprite: Option<&Sprite>,
13181) {
13182 let ev = &obj.runtime.prop_events;
13183 let sprite_desc = sprite
13184 .map(|s| {
13185 format!(
13186 "sprite_pos=({}, {}, {:.3}) sprite_order={} sprite_sorter=({}, {}) sprite_alpha={} sprite_tr={} image={:?}",
13187 s.x,
13188 s.y,
13189 s.z,
13190 s.order,
13191 unpack_legacy_sorter_key(s.order).0,
13192 unpack_legacy_sorter_key(s.order).1,
13193 s.alpha,
13194 s.tr,
13195 s.image_id
13196 )
13197 })
13198 .unwrap_or_else(|| "sprite_pos=(none) sprite_order=(none) sprite_sorter=(none) sprite_alpha=(none) sprite_tr=(none) image=None".to_string());
13199 eprintln!(
13200 "[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:{}/{} {}",
13201 ctx.globals.render_frame,
13202 stage_idx,
13203 obj_idx,
13204 info.runtime_slot,
13205 obj.file_name.as_deref().unwrap_or("-"),
13206 obj.used,
13207 obj.backend,
13208 parent_visible,
13209 visible,
13210 emitted,
13211 info.disp,
13212 info.x,
13213 info.y,
13214 info.z,
13215 info.x_rep,
13216 info.y_rep,
13217 info.z_rep,
13218 info.center_x,
13219 info.center_y,
13220 info.center_z,
13221 info.center_rep_x,
13222 info.center_rep_y,
13223 info.center_rep_z,
13224 info.x + info.x_rep + info.center_rep_x,
13225 info.y + info.y_rep + info.center_rep_y,
13226 info.z + info.z_rep + info.center_rep_z,
13227 info.order,
13228 info.layer,
13229 total_order,
13230 total_layer,
13231 info.alpha,
13232 info.tr,
13233 info.tr_rep,
13234 local_tr,
13235 bound_len,
13236 bind_dbg,
13237 ev.x.check_event(),
13238 ev.x.get_total_value(),
13239 ev.x.get_value(),
13240 ev.x.cur_time,
13241 ev.x.end_time,
13242 ev.y.check_event(),
13243 ev.y.get_total_value(),
13244 ev.y.get_value(),
13245 ev.y.cur_time,
13246 ev.y.end_time,
13247 ev.tr.check_event(),
13248 ev.tr.get_total_value(),
13249 ev.tr.get_value(),
13250 ev.tr.cur_time,
13251 ev.tr.end_time,
13252 sprite_desc,
13253 );
13254}
13255
13256fn object_tree_texture_key(
13257 ctx: &CommandContext,
13258 stage_idx: i64,
13259 obj_idx: usize,
13260 obj: &globals::ObjectState,
13261) -> (bool, u32) {
13262 let slot = object_runtime_slot(obj_idx, obj);
13263 let image = fetch_bound_render_sprites_any(ctx, stage_idx, slot, obj)
13264 .first()
13265 .and_then(|rs| rs.sprite.image_id.clone());
13266 (image.is_some(), image.map(|id| id.key().0).unwrap_or(0))
13267}
13268
13269fn object_tree_stored_axis(obj: &globals::ObjectState, axis: u8) -> i32 {
13270 match axis {
13274 0 => obj.runtime.prop_events.x.get_value(),
13275 1 => obj.runtime.prop_events.y.get_value(),
13276 _ => obj.runtime.prop_events.z.get_value(),
13277 }
13278}
13279
13280fn append_object_tree_nodes(
13281 ctx: &CommandContext,
13282 worlds: Option<&Vec<globals::WorldState>>,
13283 stage_idx: i64,
13284 obj_idx: usize,
13285 obj: &globals::ObjectState,
13286 precomputed_info: Option<ObjectRenderInfo>,
13287 parent_visible: bool,
13288 parent_order: i64,
13289 parent_layer: i64,
13290 parent_state: Option<ParentRenderState>,
13291 out: &mut Vec<SiglusRenderNode>,
13292 object_keys: &mut HashSet<(LayerId, SpriteId)>,
13293 debug_lines: &mut Vec<String>,
13294) {
13295 if !object_participates_in_tree(obj) {
13296 return;
13297 }
13298
13299 let debug_enabled = sg_render_tree_debug_enabled();
13300 let info =
13301 precomputed_info.unwrap_or_else(|| effective_object_info(ctx, stage_idx, obj_idx, obj));
13302 let local_tr = ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255).clamp(0, 255);
13303 let visible = parent_visible
13304 && info.disp
13305 && local_tr > 0
13306 && object_button_renderable_by_syscom(&ctx.globals.syscom, obj);
13307 let total_order = parent_order.saturating_add(info.order);
13308 let total_layer = parent_layer.saturating_add(info.layer);
13309 let mut own_sprites: Vec<RenderSprite> = Vec::new();
13310
13311 if debug_enabled {
13312 let bind_dbg = match &obj.backend {
13313 globals::ObjectBackend::Gfx => ctx
13314 .gfx
13315 .object_sprite_binding(stage_idx, info.runtime_slot as i64),
13316 globals::ObjectBackend::Rect {
13317 layer_id,
13318 sprite_id,
13319 ..
13320 }
13321 | globals::ObjectBackend::String {
13322 layer_id,
13323 sprite_id,
13324 ..
13325 }
13326 | globals::ObjectBackend::Movie {
13327 layer_id,
13328 sprite_id,
13329 ..
13330 } => Some((*layer_id, *sprite_id)),
13331 globals::ObjectBackend::Number {
13332 layer_id,
13333 sprite_ids,
13334 }
13335 | globals::ObjectBackend::Weather {
13336 layer_id,
13337 sprite_ids,
13338 } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
13339 globals::ObjectBackend::None => None,
13340 };
13341 debug_lines.push(format!(
13342 "[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={:?}",
13343 info.runtime_slot,
13344 obj.used,
13345 obj.object_type,
13346 obj.backend,
13347 obj.file_name.as_deref().unwrap_or("-"),
13348 info.disp,
13349 info.x,
13350 info.y,
13351 info.center_x,
13352 info.center_y,
13353 info.center_z,
13354 info.center_rep_x,
13355 info.center_rep_y,
13356 info.center_rep_z,
13357 info.x + info.x_rep + info.center_rep_x,
13358 info.y + info.y_rep + info.center_rep_y,
13359 info.z + info.z_rep + info.center_rep_z,
13360 info.order,
13361 info.layer,
13362 info.alpha,
13363 info.tr,
13364 info.z,
13365 info.child_sort_type,
13366 obj.get_int_prop(&ctx.ids, ctx.ids.obj_wipe_copy),
13367 obj.get_int_prop(&ctx.ids, ctx.ids.obj_wipe_erase),
13368 bind_dbg,
13369 ));
13370 }
13371
13372 if debug_enabled && obj.button.enabled {
13373 debug_lines.push(format!(
13374 "[SG_DEBUG] button enabled=true no={} group_no={} group_idx={:?} cut={} action={} se={} state={} hit={} pushed={} alpha_test={} call={}::{}/{}",
13375 obj.button.button_no,
13376 obj.button.group_no,
13377 obj.button.group_idx(),
13378 obj.button.cut_no,
13379 obj.button.action_no,
13380 obj.button.se_no,
13381 obj.button.state,
13382 obj.button.hit,
13383 obj.button.pushed,
13384 obj.button.alpha_test,
13385 obj.button.decided_action_scn_name,
13386 obj.button.decided_action_cmd_name,
13387 obj.button.decided_action_z_no,
13388 ));
13389 }
13390 if debug_enabled && (!obj.frame_action.cmd_name.is_empty() || obj.frame_action.end_flag) {
13391 debug_lines.push(format!(
13392 "[SG_DEBUG] frame_action cmd={}::{} count={} end_time={} real={} end_flag={} args={:?}",
13393 obj.frame_action.scn_name,
13394 obj.frame_action.cmd_name,
13395 obj.frame_action.counter.get_count(),
13396 obj.frame_action.end_time,
13397 obj.frame_action.real_time_flag,
13398 obj.frame_action.end_flag,
13399 obj.frame_action.args,
13400 ));
13401 }
13402 for (fa_idx, fa) in obj.frame_action_ch.iter().enumerate() {
13403 if debug_enabled && (!fa.cmd_name.is_empty() || fa.end_flag) {
13404 debug_lines.push(format!(
13405 "[SG_DEBUG] frame_action_ch[{}] cmd={}::{} count={} end_time={} real={} end_flag={} args={:?}",
13406 fa_idx,
13407 fa.scn_name,
13408 fa.cmd_name,
13409 fa.counter.get_count(),
13410 fa.end_time,
13411 fa.real_time_flag,
13412 fa.end_flag,
13413 fa.args,
13414 ));
13415 }
13416 }
13417 let ev = &obj.runtime.prop_events;
13418 if debug_enabled
13419 && (ev.color_rate.check_event()
13420 || ev.tr.check_event()
13421 || ev.x.check_event()
13422 || ev.y.check_event())
13423 {
13424 debug_lines.push(format!(
13425 "[SG_DEBUG] active_events x={}/{} t={}/{} y={}/{} t={}/{} tr={}/{} t={}/{} color_rate={}/{} t={}/{}",
13426 ev.x.get_total_value(), ev.x.get_value(), ev.x.cur_time, ev.x.end_time,
13427 ev.y.get_total_value(), ev.y.get_value(), ev.y.cur_time, ev.y.end_time,
13428 ev.tr.get_total_value(), ev.tr.get_value(), ev.tr.cur_time, ev.tr.end_time,
13429 ev.color_rate.get_total_value(), ev.color_rate.get_value(), ev.color_rate.cur_time, ev.color_rate.end_time,
13430 ));
13431 }
13432 if debug_enabled
13433 && (!obj.runtime.prop_event_lists.x_rep.is_empty()
13434 || !obj.runtime.prop_event_lists.y_rep.is_empty()
13435 || !obj.runtime.prop_event_lists.tr_rep.is_empty())
13436 {
13437 let fmt_list = |list: &Vec<crate::runtime::int_event::IntEvent>| -> Vec<String> {
13438 list.iter()
13439 .enumerate()
13440 .filter(|(_, ev)| {
13441 ev.check_event()
13442 || ev.get_total_value() != ev.def_value
13443 || ev.get_value() != ev.def_value
13444 })
13445 .map(|(idx, ev)| {
13446 format!(
13447 "{}:{}/{} t={}/{} active={}",
13448 idx,
13449 ev.get_total_value(),
13450 ev.get_value(),
13451 ev.cur_time,
13452 ev.end_time,
13453 ev.check_event()
13454 )
13455 })
13456 .collect()
13457 };
13458 let x_rep = fmt_list(&obj.runtime.prop_event_lists.x_rep);
13459 let y_rep = fmt_list(&obj.runtime.prop_event_lists.y_rep);
13460 let tr_rep = fmt_list(&obj.runtime.prop_event_lists.tr_rep);
13461 if !x_rep.is_empty() || !y_rep.is_empty() || !tr_rep.is_empty() {
13462 debug_lines.push(format!(
13463 "[SG_DEBUG] rep_events x={:?} y={:?} tr={:?}",
13464 x_rep, y_rep, tr_rep,
13465 ));
13466 }
13467 }
13468
13469 let mut bound = fetch_bound_render_sprites(ctx, stage_idx, info.runtime_slot, obj);
13470 let motion_trace = object_motion_trace_enabled() && object_motion_trace_object(obj);
13471 let motion_bind_dbg = if motion_trace {
13472 object_motion_trace_bind(ctx, stage_idx, &info, obj)
13473 } else {
13474 None
13475 };
13476 if motion_trace && (!visible || bound.is_empty()) {
13477 object_motion_trace_emit(
13478 ctx,
13479 stage_idx,
13480 obj_idx,
13481 obj,
13482 &info,
13483 parent_visible,
13484 visible,
13485 local_tr,
13486 total_order,
13487 total_layer,
13488 bound.len(),
13489 motion_bind_dbg,
13490 false,
13491 None,
13492 );
13493 }
13494 if config_button_trace_enabled() && config_button_trace_object(obj) {
13495 let bind_dbg = match &obj.backend {
13496 globals::ObjectBackend::Gfx => ctx
13497 .gfx
13498 .object_sprite_binding(stage_idx, info.runtime_slot as i64),
13499 globals::ObjectBackend::Rect {
13500 layer_id,
13501 sprite_id,
13502 ..
13503 }
13504 | globals::ObjectBackend::String {
13505 layer_id,
13506 sprite_id,
13507 ..
13508 }
13509 | globals::ObjectBackend::Movie {
13510 layer_id,
13511 sprite_id,
13512 ..
13513 } => Some((*layer_id, *sprite_id)),
13514 globals::ObjectBackend::Number {
13515 layer_id,
13516 sprite_ids,
13517 }
13518 | globals::ObjectBackend::Weather {
13519 layer_id,
13520 sprite_ids,
13521 } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
13522 globals::ObjectBackend::None => None,
13523 };
13524 let syscom_renderable = object_button_renderable_by_syscom(&ctx.globals.syscom, obj);
13525 debug_lines.push(format!(
13526 "[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={}",
13527 stage_idx,
13528 obj_idx,
13529 info.runtime_slot,
13530 obj.file_name.as_deref().unwrap_or("-"),
13531 obj.backend,
13532 object_participates_in_tree(obj),
13533 parent_visible,
13534 info.disp,
13535 local_tr,
13536 info.tr,
13537 info.tr_rep,
13538 syscom_renderable,
13539 visible,
13540 bound.len(),
13541 bind_dbg,
13542 info.order,
13543 info.layer,
13544 total_order,
13545 total_layer,
13546 obj.button.enabled,
13547 obj.button.state,
13548 obj.button.button_no,
13549 obj.button.group_no,
13550 obj.button.action_no,
13551 obj.button.hit,
13552 obj.button.pushed,
13553 button_disabled_reason(&ctx.globals.syscom, obj, None, ctx.runtime_read_skip_is_enable()),
13554 parent_state.is_some()
13555 ));
13556 }
13557 for rs in &bound {
13558 if let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) {
13559 object_keys.insert((lid, sid));
13560 }
13561 }
13562 let mut cur_parent_state = build_parent_render_state(&info, bound.first().map(|rs| &rs.sprite));
13563 if let Some(parent) = parent_state.clone() {
13564 cur_parent_state = compose_parent_render_state(parent, cur_parent_state);
13565 }
13566
13567 if visible {
13568 if obj.object_type == 4 {
13569 let out_len_before = own_sprites.len();
13570 append_weather_sprites(
13571 ctx,
13572 worlds,
13573 obj,
13574 &info,
13575 total_order,
13576 total_layer,
13577 &bound,
13578 &mut own_sprites,
13579 );
13580 for rs in own_sprites[out_len_before..].iter_mut() {
13581 if let Some(parent) = parent_state.clone() {
13582 apply_parent_render_state_to_sprite(&mut rs.sprite, &info, &parent);
13583 }
13584 finalize_object_center_rep_to_sprite(&mut rs.sprite, &info);
13585 apply_world_camera_mode(&mut rs.sprite, worlds, ctx.screen_w, ctx.screen_h);
13586 apply_runtime_light_and_fog(ctx, &mut rs.sprite);
13587 if motion_trace {
13588 object_motion_trace_emit(
13589 ctx,
13590 stage_idx,
13591 obj_idx,
13592 obj,
13593 &info,
13594 parent_visible,
13595 visible,
13596 local_tr,
13597 total_order,
13598 total_layer,
13599 bound.len(),
13600 motion_bind_dbg,
13601 rs.sprite.tr > 0,
13602 Some(&rs.sprite),
13603 );
13604 }
13605 }
13606 } else {
13607 let bound_len_for_trace = bound.len();
13608 for mut rs in bound.drain(..) {
13609 apply_object_render_info_to_sprite(&mut rs.sprite, &info);
13610 let (local_x, local_y) = object_backend_sprite_local_offset(obj, rs.sprite_id);
13611 if local_x != 0 || local_y != 0 {
13612 rs.sprite.x = (rs.sprite.x as i64 + local_x)
13613 .clamp(i32::MIN as i64, i32::MAX as i64)
13614 as i32;
13615 rs.sprite.y = (rs.sprite.y as i64 + local_y)
13616 .clamp(i32::MIN as i64, i32::MAX as i64)
13617 as i32;
13618 rs.sprite.pivot_x -= local_x as f32;
13622 rs.sprite.pivot_y -= local_y as f32;
13623 }
13624 let sprite_layer = total_layer.saturating_add(object_backend_sprite_layer_offset(
13625 ctx,
13626 &obj.backend,
13627 rs.sprite_id,
13628 ));
13629 rs.set_sorter(total_order, sprite_layer);
13630 rs.sprite.order = legacy_packed_sorter_key(total_order, sprite_layer);
13631 configure_sprite_3d(&mut rs.sprite, &info, worlds, ctx.screen_w, ctx.screen_h);
13632 if let Some(parent) = parent_state.clone() {
13633 apply_parent_render_state_to_sprite(&mut rs.sprite, &info, &parent);
13634 }
13635 finalize_object_center_rep_to_sprite(&mut rs.sprite, &info);
13636 apply_world_camera_mode(&mut rs.sprite, worlds, ctx.screen_w, ctx.screen_h);
13637 apply_runtime_light_and_fog(ctx, &mut rs.sprite);
13638 if motion_trace {
13639 object_motion_trace_emit(
13640 ctx,
13641 stage_idx,
13642 obj_idx,
13643 obj,
13644 &info,
13645 parent_visible,
13646 visible,
13647 local_tr,
13648 total_order,
13649 total_layer,
13650 bound_len_for_trace,
13651 motion_bind_dbg,
13652 rs.sprite.tr > 0,
13653 Some(&rs.sprite),
13654 );
13655 }
13656 if rs.sprite.tr > 0 {
13657 own_sprites.push(rs);
13658 }
13659 }
13660 }
13661 }
13662
13663 if config_button_trace_enabled() && config_button_trace_object(obj) {
13664 debug_lines.push(format!(
13665 "[SG_DEBUG][CONFIG_BUTTON_TRACE][EMIT_DONE] stage={} obj_idx={} runtime_slot={} file={} out_len_now={} visible={} child_count={}",
13666 stage_idx,
13667 obj_idx,
13668 info.runtime_slot,
13669 obj.file_name.as_deref().unwrap_or("-"),
13670 own_sprites.len(),
13671 visible,
13672 obj.runtime.child_objects.len()
13673 ));
13674 }
13675
13676 if debug_enabled && !obj.runtime.child_objects.is_empty() {
13677 debug_lines.push(format!(
13678 "[SG_DEBUG] child_list op=93 len={}",
13679 obj.runtime.child_objects.len()
13680 ));
13681 }
13682 if sg_mwnd_object_trace_enabled() && !obj.runtime.child_objects.is_empty() {
13683 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
13684 let child_info = effective_object_info(ctx, stage_idx, child_idx, child);
13685 let child_bind = match &child.backend {
13686 globals::ObjectBackend::Gfx => ctx
13687 .gfx
13688 .object_sprite_binding(stage_idx, child_info.runtime_slot as i64),
13689 globals::ObjectBackend::Rect {
13690 layer_id,
13691 sprite_id,
13692 ..
13693 }
13694 | globals::ObjectBackend::String {
13695 layer_id,
13696 sprite_id,
13697 ..
13698 }
13699 | globals::ObjectBackend::Movie {
13700 layer_id,
13701 sprite_id,
13702 ..
13703 } => Some((*layer_id, *sprite_id)),
13704 globals::ObjectBackend::Number {
13705 layer_id,
13706 sprite_ids,
13707 }
13708 | globals::ObjectBackend::Weather {
13709 layer_id,
13710 sprite_ids,
13711 } => sprite_ids.first().copied().map(|sid| (*layer_id, sid)),
13712 globals::ObjectBackend::None => None,
13713 };
13714 debug_lines.push(format!(
13715 "[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={}",
13716 info.runtime_slot,
13717 obj_idx,
13718 child_idx,
13719 child_info.runtime_slot,
13720 object_participates_in_tree(child),
13721 child.used,
13722 child.object_type,
13723 child.backend,
13724 child.file_name.as_deref().unwrap_or("-"),
13725 child_info.disp,
13726 child_info.x,
13727 child_info.y,
13728 child_info.center_x,
13729 child_info.center_y,
13730 child_info.center_z,
13731 child_info.center_rep_x,
13732 child_info.center_rep_y,
13733 child_info.center_rep_z,
13734 child_info.x + child_info.x_rep + child_info.center_rep_x,
13735 child_info.y + child_info.y_rep + child_info.center_rep_y,
13736 child_info.z + child_info.z_rep + child_info.center_rep_z,
13737 child_info.order,
13738 child_info.layer,
13739 child_info.alpha,
13740 child_info.tr,
13741 child.nested_runtime_slot,
13742 child_bind,
13743 child.runtime.child_objects.len()
13744 ));
13745 }
13746 }
13747
13748 let mut children: Vec<(usize, &globals::ObjectState)> = obj
13749 .runtime
13750 .child_objects
13751 .iter()
13752 .enumerate()
13753 .filter(|(_, child)| object_participates_in_tree(child))
13754 .collect();
13755
13756 let tree_sort_type = if matches!(obj.object_type, 3..=5) {
13757 0
13760 } else {
13761 info.child_sort_type
13762 };
13763 match (tree_sort_type, obj.object_type) {
13764 (0 | 1, _) => {
13765 }
13769 (2, _) => {
13770 children.sort_by(|(lhs_idx, lhs), (rhs_idx, rhs)| {
13771 object_tree_texture_key(ctx, stage_idx, *lhs_idx, lhs)
13772 .cmp(&object_tree_texture_key(ctx, stage_idx, *rhs_idx, rhs))
13773 });
13774 }
13775 (3, 0) => {
13776 children.sort_by_key(|(_, child)| object_tree_stored_axis(child, 0));
13777 }
13778 (4, 0) => {
13779 children.sort_by_key(|(_, child)| std::cmp::Reverse(object_tree_stored_axis(child, 0)));
13780 }
13781 (5, 0) => {
13782 children.sort_by_key(|(_, child)| object_tree_stored_axis(child, 1));
13783 }
13784 (6, 0) => {
13785 children.sort_by_key(|(_, child)| std::cmp::Reverse(object_tree_stored_axis(child, 1)));
13786 }
13787 (7, 0) => {
13788 children.sort_by_key(|(_, child)| object_tree_stored_axis(child, 2));
13789 }
13790 (8, 0) => {
13791 children.sort_by_key(|(_, child)| std::cmp::Reverse(object_tree_stored_axis(child, 2)));
13792 }
13793 (3..=8, _) => {
13794 children.clear();
13798 }
13799 _ => {}
13800 }
13801
13802 let recurse_children = visible;
13806 let mut child_nodes = Vec::new();
13807 for (child_idx, child) in children {
13808 append_object_tree_nodes(
13809 ctx,
13810 worlds,
13811 stage_idx,
13812 child_idx,
13813 child,
13814 None,
13815 recurse_children,
13816 total_order,
13817 total_layer,
13818 Some(cur_parent_state.clone()),
13819 &mut child_nodes,
13820 object_keys,
13821 debug_lines,
13822 );
13823 }
13824
13825 if !visible {
13826 return;
13827 }
13828
13829 match obj.object_type {
13830 3..=5 => {
13831 for rs in own_sprites {
13835 out.push(SiglusRenderNode::from_single_sprite(rs));
13836 }
13837 if !child_nodes.is_empty() {
13838 out.push(SiglusRenderNode::dummy(
13839 total_order,
13840 total_layer,
13841 true,
13842 child_nodes,
13843 ));
13844 }
13845 }
13846 0 => {
13847 if !child_nodes.is_empty() {
13850 out.push(SiglusRenderNode::dummy(
13851 total_order,
13852 total_layer,
13853 tree_sort_type == 0,
13854 child_nodes,
13855 ));
13856 }
13857 }
13858 _ => {
13859 if own_sprites.is_empty() && child_nodes.is_empty() {
13860 return;
13861 }
13862 let (sorter_order, sorter_layer) = own_sprites
13863 .first()
13864 .map(|rs| (rs.sorter_order, rs.sorter_layer))
13865 .unwrap_or((
13866 total_order.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
13867 total_layer.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
13868 ));
13869 out.push(SiglusRenderNode {
13870 sorter_order,
13871 sorter_layer,
13872 sprites: own_sprites,
13873 sort_children_default: tree_sort_type == 0,
13874 children: child_nodes,
13875 });
13876 }
13877 }
13878}
13879
13880fn append_weather_sprites(
13881 ctx: &CommandContext,
13882 worlds: Option<&Vec<globals::WorldState>>,
13883 _obj: &globals::ObjectState,
13884 info: &ObjectRenderInfo,
13885 total_order: i64,
13886 total_layer: i64,
13887 bound: &[RenderSprite],
13888 out: &mut Vec<RenderSprite>,
13889) {
13890 for template in bound {
13894 if !template.sprite.visible || template.sprite.image_id.is_none() {
13895 continue;
13896 }
13897 let mut rs = template.clone();
13898 let local_x = rs.sprite.x;
13899 let local_y = rs.sprite.y;
13900 let local_scale_x = rs.sprite.scale_x;
13901 let local_scale_y = rs.sprite.scale_y;
13902 let weather_tr = rs.sprite.tr;
13903
13904 apply_object_render_info_to_sprite(&mut rs.sprite, info);
13905 rs.sprite.x =
13906 (rs.sprite.x as i64 + local_x as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
13907 rs.sprite.y =
13908 (rs.sprite.y as i64 + local_y as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
13909 rs.sprite.scale_x *= local_scale_x;
13910 rs.sprite.scale_y *= local_scale_y;
13911 rs.sprite.tr = ((info.tr.clamp(0, 255) * weather_tr as i64) / 255).clamp(0, 255) as u8;
13912 rs.set_sorter(total_order, total_layer);
13913 rs.sprite.order = legacy_packed_sorter_key(total_order, total_layer);
13914 configure_sprite_3d(&mut rs.sprite, info, worlds, ctx.screen_w, ctx.screen_h);
13915 if rs.sprite.tr > 0 {
13916 out.push(rs);
13917 }
13918 }
13919}
13920
13921fn apply_object_render_info_to_sprite(sprite: &mut Sprite, info: &ObjectRenderInfo) {
13922 sprite.visible = info.disp;
13923 sprite.x = (info.x + info.x_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
13924 sprite.y = (info.y + info.y_rep).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
13925 sprite.z = (info.z + info.z_rep) as f32;
13926 sprite.pivot_x = (info.center_x + info.center_rep_x) as f32;
13927 sprite.pivot_y = (info.center_y + info.center_rep_y) as f32;
13928 sprite.pivot_z = (info.center_z + info.center_rep_z) as f32;
13929 sprite.scale_x = info.scale_x as f32 / 1000.0;
13930 sprite.scale_y = info.scale_y as f32 / 1000.0;
13931 sprite.scale_z = info.scale_z as f32 / 1000.0;
13932 sprite.rotate = info.rotate_z as f32 * std::f32::consts::PI / 1800.0;
13933 sprite.rotate_x = info.rotate_x as f32 * std::f32::consts::PI / 1800.0;
13934 sprite.rotate_y = info.rotate_y as f32 * std::f32::consts::PI / 1800.0;
13935 sprite.alpha = info.alpha.clamp(0, 255) as u8;
13936 sprite.tr = ((info.tr.clamp(0, 255) * info.tr_rep.clamp(0, 255)) / 255).clamp(0, 255) as u8;
13937 sprite.mono = info.mono.clamp(0, 255) as u8;
13938 sprite.reverse = info.reverse.clamp(0, 255) as u8;
13939 sprite.bright = info.bright.clamp(0, 255) as u8;
13940 sprite.dark = info.dark.clamp(0, 255) as u8;
13941 sprite.color_rate = info.color_rate.clamp(0, 255) as u8;
13942 sprite.color_add_r = info.color_add_r.clamp(0, 255) as u8;
13943 sprite.color_add_g = info.color_add_g.clamp(0, 255) as u8;
13944 sprite.color_add_b = info.color_add_b.clamp(0, 255) as u8;
13945 sprite.color_r = info.color_r.clamp(0, 255) as u8;
13946 sprite.color_g = info.color_g.clamp(0, 255) as u8;
13947 sprite.color_b = info.color_b.clamp(0, 255) as u8;
13948 sprite.blend = info.blend;
13949 sprite.dst_clip = info.dst_clip;
13950}
13951
13952fn finalize_object_center_rep_to_sprite(sprite: &mut Sprite, info: &ObjectRenderInfo) {
13953 let x = (sprite.x as i64 + info.center_rep_x).clamp(i32::MIN as i64, i32::MAX as i64);
13954 let y = (sprite.y as i64 + info.center_rep_y).clamp(i32::MIN as i64, i32::MAX as i64);
13955 sprite.x = x as i32;
13956 sprite.y = y as i32;
13957 sprite.z += info.center_rep_z as f32;
13958}
13959
13960fn object_participates_in_tree(obj: &globals::ObjectState) -> bool {
13961 if obj.object_type != 0 {
13965 return true;
13966 }
13967 if !obj.runtime.child_objects.is_empty() {
13968 return true;
13969 }
13970 !matches!(obj.backend, globals::ObjectBackend::None)
13971}
13972
13973fn mark_object_tree_sprite_keys(
13974 ctx: &CommandContext,
13975 stage_idx: i64,
13976 obj_idx: usize,
13977 obj: &globals::ObjectState,
13978 object_keys: &mut HashSet<(LayerId, SpriteId)>,
13979) {
13980 let runtime_slot = object_runtime_slot(obj_idx, obj);
13981 match &obj.backend {
13982 globals::ObjectBackend::Gfx => {
13983 if let Some(binding) = ctx
13984 .gfx
13985 .object_sprite_binding(stage_idx, runtime_slot as i64)
13986 {
13987 object_keys.insert(binding);
13988 }
13989 }
13990 globals::ObjectBackend::None => {}
13991 globals::ObjectBackend::Rect {
13992 layer_id,
13993 sprite_id,
13994 ..
13995 }
13996 | globals::ObjectBackend::Movie {
13997 layer_id,
13998 sprite_id,
13999 ..
14000 } => {
14001 object_keys.insert((*layer_id, *sprite_id));
14002 }
14003 globals::ObjectBackend::String {
14004 layer_id,
14005 shadow_sprite_id,
14006 fuchi_sprite_id,
14007 sprite_id,
14008 glyphs,
14009 ..
14010 } => {
14011 if glyphs.is_empty() {
14012 object_keys.insert((*layer_id, *shadow_sprite_id));
14013 object_keys.insert((*layer_id, *fuchi_sprite_id));
14014 object_keys.insert((*layer_id, *sprite_id));
14015 } else {
14016 for glyph in glyphs {
14017 object_keys.insert((*layer_id, glyph.shadow_sprite_id));
14018 object_keys.insert((*layer_id, glyph.fuchi_sprite_id));
14019 object_keys.insert((*layer_id, glyph.body_sprite_id));
14020 }
14021 }
14022 }
14023 globals::ObjectBackend::Number {
14024 layer_id,
14025 sprite_ids,
14026 }
14027 | globals::ObjectBackend::Weather {
14028 layer_id,
14029 sprite_ids,
14030 } => {
14031 for &sprite_id in sprite_ids {
14032 object_keys.insert((*layer_id, sprite_id));
14033 }
14034 }
14035 }
14036 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
14037 mark_object_tree_sprite_keys(ctx, stage_idx, child_idx, child, object_keys);
14038 }
14039}
14040
14041fn mark_mwnd_owned_sprite_keys(
14042 ctx: &CommandContext,
14043 stage_idx: i64,
14044 m: &globals::MwndState,
14045 object_keys: &mut HashSet<(LayerId, SpriteId)>,
14046) {
14047 for (idx, obj) in m.button_list.iter().enumerate() {
14048 mark_object_tree_sprite_keys(ctx, stage_idx, idx, obj, object_keys);
14049 }
14050 for (idx, obj) in m.face_list.iter().enumerate() {
14051 mark_object_tree_sprite_keys(ctx, stage_idx, idx, obj, object_keys);
14052 }
14053 for (idx, obj) in m.object_list.iter().enumerate() {
14054 mark_object_tree_sprite_keys(ctx, stage_idx, idx, obj, object_keys);
14055 }
14056}
14057
14058#[derive(Debug)]
14059struct SiglusRenderNode {
14060 sorter_order: i32,
14061 sorter_layer: i32,
14062 sprites: Vec<RenderSprite>,
14063 sort_children_default: bool,
14064 children: Vec<SiglusRenderNode>,
14065}
14066
14067impl SiglusRenderNode {
14068 fn from_single_sprite(rs: RenderSprite) -> Self {
14069 Self {
14070 sorter_order: rs.sorter_order,
14071 sorter_layer: rs.sorter_layer,
14072 sprites: vec![rs],
14073 sort_children_default: true,
14074 children: Vec::new(),
14075 }
14076 }
14077
14078 fn dummy(
14079 sorter_order: i64,
14080 sorter_layer: i64,
14081 sort_children_default: bool,
14082 children: Vec<SiglusRenderNode>,
14083 ) -> Self {
14084 Self {
14085 sorter_order: sorter_order.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
14086 sorter_layer: sorter_layer.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
14087 sprites: Vec::new(),
14088 sort_children_default,
14089 children,
14090 }
14091 }
14092
14093 fn set_stage_metadata(&mut self, stage_form_id: u32, wipe_order: i64, wipe_layer: i64) {
14094 for rs in &mut self.sprites {
14095 rs.set_wipe_sorter(wipe_order, wipe_layer);
14096 rs.set_stage_form_owner(stage_form_id);
14097 }
14098 for child in &mut self.children {
14099 child.set_stage_metadata(stage_form_id, wipe_order, wipe_layer);
14100 }
14101 }
14102
14103 fn sprite_count(&self) -> usize {
14104 self.sprites.len()
14105 + self
14106 .children
14107 .iter()
14108 .map(SiglusRenderNode::sprite_count)
14109 .sum::<usize>()
14110 }
14111
14112 fn flatten(mut self, out: &mut Vec<RenderSprite>) {
14113 out.extend(self.sprites);
14114 if self.sort_children_default {
14115 self.children.sort_by(siglus_render_node_cmp);
14116 }
14117 for child in self.children {
14118 child.flatten(out);
14119 }
14120 }
14121}
14122
14123fn siglus_render_node_cmp(lhs: &SiglusRenderNode, rhs: &SiglusRenderNode) -> std::cmp::Ordering {
14124 (lhs.sorter_order, lhs.sorter_layer).cmp(&(rhs.sorter_order, rhs.sorter_layer))
14125}
14126
14127fn collect_object_render_nodes(
14128 ctx: &CommandContext,
14129 stage_form_id: u32,
14130 worlds: Option<&Vec<globals::WorldState>>,
14131 stage_idx: i64,
14132 obj_idx: usize,
14133 obj: &globals::ObjectState,
14134 precomputed_info: Option<ObjectRenderInfo>,
14135 parent_visible: bool,
14136 parent_order: i64,
14137 parent_layer: i64,
14138 parent_state: Option<ParentRenderState>,
14139 wipe_order: i64,
14140 wipe_layer: i64,
14141 object_keys: &mut HashSet<(LayerId, SpriteId)>,
14142 debug: &mut Vec<String>,
14143) -> Vec<SiglusRenderNode> {
14144 if !object_participates_in_tree(obj) {
14145 return Vec::new();
14146 }
14147
14148 let mut nodes = Vec::new();
14149 append_object_tree_nodes(
14150 ctx,
14151 worlds,
14152 stage_idx,
14153 obj_idx,
14154 obj,
14155 precomputed_info,
14156 parent_visible,
14157 parent_order,
14158 parent_layer,
14159 parent_state,
14160 &mut nodes,
14161 object_keys,
14162 debug,
14163 );
14164 for node in &mut nodes {
14165 node.set_stage_metadata(stage_form_id, wipe_order, wipe_layer);
14166 }
14167 nodes
14168}
14169
14170fn mwnd_parent_render_state_at(
14171 m: &globals::MwndState,
14172 window_x: i64,
14173 window_y: i64,
14174) -> ParentRenderState {
14175 ParentRenderState {
14176 world_no: -1,
14184 pos_x: window_x as f32,
14185 pos_y: window_y as f32,
14186 pos_z: 0.0,
14187 center_rep_x: 0.0,
14188 center_rep_y: 0.0,
14189 center_rep_z: 0.0,
14190 scale_x: 1.0,
14191 scale_y: 1.0,
14192 scale_z: 1.0,
14193 rotate_x: 0.0,
14194 rotate_y: 0.0,
14195 rotate_z: 0.0,
14196 tr: 255,
14197 mono: 0,
14198 reverse: 0,
14199 bright: 0,
14200 dark: 0,
14201 color_rate: 0,
14202 color_r: 0,
14203 color_g: 0,
14204 color_b: 0,
14205 color_add_r: 0,
14206 color_add_g: 0,
14207 color_add_b: 0,
14208 blend: crate::layer::SpriteBlend::Normal,
14209 dst_clip: None,
14210 mask_image_id: None,
14211 mask_offset_x: 0,
14212 mask_offset_y: 0,
14213 tonecurve_image_id: None,
14214 tonecurve_row: 0.0,
14215 tonecurve_sat: 0.0,
14216 }
14217}
14218
14219fn mwnd_parent_render_state(m: &globals::MwndState) -> ParentRenderState {
14220 let (x, y) = m.window_pos.unwrap_or((0, 0));
14221 mwnd_parent_render_state_at(m, x, y)
14222}
14223
14224fn mwnd_window_rect_for_embedded(
14225 ctx: &CommandContext,
14226 m: &globals::MwndState,
14227) -> Option<(
14228 i64,
14229 i64,
14230 i64,
14231 i64,
14232 Option<crate::runtime::ui::MwndWindowRenderState>,
14233)> {
14234 let (x, y) = m.window_pos?;
14235 let (w, h) = m.window_size?;
14236 if w <= 0 || h <= 0 {
14237 return None;
14238 }
14239 let ui_state = ctx
14240 .ui
14241 .current_mwnd_window_render_state(ctx.screen_w, ctx.screen_h)
14242 .filter(|ui| ui.x as i64 == x && ui.y as i64 == y && ui.w as i64 == w && ui.h as i64 == h);
14243 Some((x, y, w, h, ui_state))
14244}
14245
14246fn mwnd_anim_parent_from_ui_state(
14247 m: &globals::MwndState,
14248 ui: crate::runtime::ui::MwndWindowRenderState,
14249) -> ParentRenderState {
14250 let mut parent = mwnd_parent_render_state_at(m, 0, 0);
14251 parent.pos_x = ui.dx as f32;
14252 parent.pos_y = ui.dy as f32;
14253 parent.center_rep_x = ui.pivot_abs_x - ui.dx as f32;
14254 parent.center_rep_y = ui.pivot_abs_y - ui.dy as f32;
14255 parent.scale_x = ui.scale_x;
14256 parent.scale_y = ui.scale_y;
14257 parent.rotate_z = ui.rotate;
14258 parent.tr = ui.alpha as i32;
14259 parent
14260}
14261
14262fn apply_mwnd_window_anim_parent(
14263 parent: ParentRenderState,
14264 anim_parent: Option<ParentRenderState>,
14265) -> ParentRenderState {
14266 match anim_parent {
14267 Some(anim) => compose_parent_render_state(anim, parent),
14268 None => parent,
14269 }
14270}
14271
14272fn append_mwnd_embedded_object_list_groups(
14273 ctx: &CommandContext,
14274 stage_form_id: u32,
14275 worlds: Option<&Vec<globals::WorldState>>,
14276 stage_idx: i64,
14277 list: &[globals::ObjectState],
14278 parent: ParentRenderState,
14279 parent_order: i64,
14280 parent_layer: i64,
14281 wipe_order: i64,
14282 wipe_layer: i64,
14283 groups: &mut Vec<SiglusRenderNode>,
14284 object_keys: &mut HashSet<(LayerId, SpriteId)>,
14285 debug: &mut Vec<String>,
14286) {
14287 for (obj_idx, obj) in list.iter().enumerate() {
14288 groups.extend(collect_object_render_nodes(
14289 ctx,
14290 stage_form_id,
14291 worlds,
14292 stage_idx,
14293 obj_idx,
14294 obj,
14295 None,
14296 true,
14297 parent_order,
14298 parent_layer,
14299 Some(parent.clone()),
14300 wipe_order,
14301 wipe_layer,
14302 object_keys,
14303 debug,
14304 ));
14305 }
14306}
14307
14308fn mwnd_button_parent_render_state(
14309 m: &globals::MwndState,
14310 button_idx: usize,
14311 window_x: i64,
14312 window_y: i64,
14313 window_w: i64,
14314 window_h: i64,
14315) -> ParentRenderState {
14316 let mut parent = mwnd_parent_render_state_at(m, window_x, window_y);
14317 let Some(&(pos_base, x, y)) = m.waku_button_layout.get(button_idx) else {
14318 return parent;
14319 };
14320 match pos_base {
14321 1 => {
14322 parent.pos_x += (window_w - x) as f32;
14323 parent.pos_y += y as f32;
14324 }
14325 2 => {
14326 parent.pos_x += x as f32;
14327 parent.pos_y += (window_h - y) as f32;
14328 }
14329 3 => {
14330 parent.pos_x += (window_w - x) as f32;
14331 parent.pos_y += (window_h - y) as f32;
14332 }
14333 _ => {
14334 parent.pos_x += x as f32;
14335 parent.pos_y += y as f32;
14336 }
14337 }
14338 parent
14339}
14340
14341fn mwnd_face_parent_render_state(
14342 m: &globals::MwndState,
14343 face_idx: usize,
14344 window_x: i64,
14345 window_y: i64,
14346) -> ParentRenderState {
14347 let mut parent = mwnd_parent_render_state_at(m, window_x, window_y);
14348 if let Some(&(x, y)) = m.waku_face_pos.get(face_idx) {
14349 parent.pos_x += x as f32;
14350 parent.pos_y += y as f32;
14351 }
14352 parent
14353}
14354
14355fn btnselitem_parent_render_state(item: &globals::BtnSelItemState) -> ParentRenderState {
14356 ParentRenderState {
14357 world_no: -1,
14358 pos_x: item.pos.0.saturating_add(item.animation_offset.0) as f32,
14359 pos_y: item.pos.1.saturating_add(item.animation_offset.1) as f32,
14360 pos_z: 0.0,
14361 center_rep_x: 0.0,
14362 center_rep_y: 0.0,
14363 center_rep_z: 0.0,
14364 scale_x: 1.0,
14365 scale_y: 1.0,
14366 scale_z: 1.0,
14367 rotate_x: 0.0,
14368 rotate_y: 0.0,
14369 rotate_z: 0.0,
14370 tr: if item.visible {
14371 item.animation_tr.unwrap_or(255).clamp(0, 255) as i32
14372 } else {
14373 0
14374 },
14375 mono: 0,
14376 reverse: 0,
14377 bright: 0,
14378 dark: 0,
14379 color_rate: 0,
14380 color_r: 0,
14381 color_g: 0,
14382 color_b: 0,
14383 color_add_r: 0,
14384 color_add_g: 0,
14385 color_add_b: 0,
14386 blend: crate::layer::SpriteBlend::Normal,
14387 dst_clip: None,
14388 mask_image_id: None,
14389 mask_offset_x: 0,
14390 mask_offset_y: 0,
14391 tonecurve_image_id: None,
14392 tonecurve_row: 0.0,
14393 tonecurve_sat: 0.0,
14394 }
14395}
14396
14397fn append_btnselitem_groups(
14398 ctx: &CommandContext,
14399 stage_form_id: u32,
14400 worlds: Option<&Vec<globals::WorldState>>,
14401 stage_idx: i64,
14402 items: &[globals::BtnSelItemState],
14403 groups: &mut Vec<SiglusRenderNode>,
14404 object_keys: &mut HashSet<(LayerId, SpriteId)>,
14405 debug: &mut Vec<String>,
14406) {
14407 let wipe_order = ctx.tables.mwnd_render.order;
14408 let wipe_layer = 0;
14409 for (item_idx, item) in items.iter().enumerate() {
14410 if !item.visible {
14411 continue;
14412 }
14413 let parent = btnselitem_parent_render_state(item);
14414 for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
14415 groups.extend(collect_object_render_nodes(
14416 ctx,
14417 stage_form_id,
14418 worlds,
14419 stage_idx,
14420 obj_idx,
14421 obj,
14422 None,
14423 true,
14424 wipe_order,
14425 0,
14426 Some(parent.clone()),
14427 wipe_order,
14428 wipe_layer,
14429 object_keys,
14430 debug,
14431 ));
14432 }
14433 for (obj_idx, obj) in item.object_list.iter().enumerate() {
14434 groups.extend(collect_object_render_nodes(
14435 ctx,
14436 stage_form_id,
14437 worlds,
14438 stage_idx,
14439 obj_idx,
14440 obj,
14441 None,
14442 true,
14443 wipe_order,
14444 0,
14445 Some(parent.clone()),
14446 wipe_order,
14447 wipe_layer,
14448 object_keys,
14449 debug,
14450 ));
14451 }
14452 if sg_render_tree_debug_enabled()
14453 && (item.generated_objects.len() + item.object_list.len()) == 0
14454 {
14455 debug.push(format!(
14456 "[SG_DEBUG] btnselitem[{item_idx}] text_len={} visible={} pos=({}, {}) size=({}, {}) no_objects",
14457 item.text.chars().count(),
14458 item.visible,
14459 item.pos.0,
14460 item.pos.1,
14461 item.size.0,
14462 item.size.1,
14463 ));
14464 }
14465 }
14466}
14467
14468#[derive(Clone)]
14469struct SelBtnSpriteVisual {
14470 component: u8,
14471 action_no: i64,
14472 state: i64,
14473 base_file: Option<String>,
14474 base_patno: i64,
14475 text_color: Option<(u8, u8, u8)>,
14476}
14477
14478fn collect_selbtn_sprite_visuals_recursive(
14479 obj: &globals::ObjectState,
14480 component: u8,
14481 action_no: i64,
14482 state: i64,
14483 text_color: Option<(u8, u8, u8)>,
14484 map: &mut HashMap<(LayerId, SpriteId), SelBtnSpriteVisual>,
14485) {
14486 match (&obj.backend, component) {
14487 (globals::ObjectBackend::Gfx | globals::ObjectBackend::None, _) => {}
14488 (
14489 globals::ObjectBackend::String {
14490 layer_id,
14491 shadow_sprite_id,
14492 fuchi_sprite_id,
14493 sprite_id,
14494 glyphs,
14495 ..
14496 },
14497 2,
14498 ) => {
14499 if glyphs.is_empty() {
14503 for (sid, text_component, color) in [
14504 (*shadow_sprite_id, 4u8, None),
14505 (*fuchi_sprite_id, 5u8, None),
14506 (*sprite_id, 2u8, text_color),
14507 ] {
14508 map.insert(
14509 (*layer_id, sid),
14510 SelBtnSpriteVisual {
14511 component: text_component,
14512 action_no,
14513 state,
14514 base_file: obj.file_name.clone(),
14515 base_patno: obj.base.patno,
14516 text_color: color,
14517 },
14518 );
14519 }
14520 } else {
14521 for glyph in glyphs {
14522 for (sid, text_component, color) in [
14523 (glyph.shadow_sprite_id, 4u8, None),
14524 (glyph.fuchi_sprite_id, 5u8, None),
14525 (glyph.body_sprite_id, 2u8, text_color),
14526 ] {
14527 map.insert(
14528 (*layer_id, sid),
14529 SelBtnSpriteVisual {
14530 component: text_component,
14531 action_no,
14532 state,
14533 base_file: obj.file_name.clone(),
14534 base_patno: obj.base.patno,
14535 text_color: color,
14536 },
14537 );
14538 }
14539 }
14540 }
14541 }
14542 _ => {
14543 for (layer_id, sprite_id) in layer_backed_object_sprite_bindings(&obj.backend) {
14544 map.insert(
14545 (layer_id, sprite_id),
14546 SelBtnSpriteVisual {
14547 component,
14548 action_no,
14549 state,
14550 base_file: obj.file_name.clone(),
14551 base_patno: obj.base.patno,
14552 text_color,
14553 },
14554 );
14555 }
14556 }
14557 }
14558
14559 for child in &obj.runtime.child_objects {
14560 collect_selbtn_sprite_visuals_recursive(
14561 child, component, action_no, state, text_color, map,
14562 );
14563 }
14564}
14565
14566fn apply_selbtn_item_visuals(ctx: &mut CommandContext, sprites: &mut [RenderSprite]) {
14567 let mut map: HashMap<(LayerId, SpriteId), SelBtnSpriteVisual> = HashMap::default();
14568 let template_no = ctx.globals.selbtn.template_no.max(0) as usize;
14569 let hit_color_no = ctx
14570 .globals
14571 .selbtn
14572 .saved_cur_param
14573 .map(|param| param[17])
14574 .or_else(|| {
14575 ctx.tables
14576 .sel_btn_templates
14577 .get(template_no)
14578 .map(|tmpl| tmpl.moji_hit_color)
14579 })
14580 .unwrap_or(-1);
14581 for st in ctx.globals.stage_forms.values() {
14582 for items in st.btnselitem_lists.values() {
14583 for item in items {
14584 if !item.visible {
14585 continue;
14586 }
14587 for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
14588 let component = match obj_idx {
14589 0 => 0,
14590 1 => 1,
14591 _ => 2,
14592 };
14593 let text_color_no = if component == 2
14594 && item.button_state != TNM_BTN_STATE_NORMAL
14595 && item.button_state != TNM_BTN_STATE_DISABLE
14596 && hit_color_no >= 0
14597 {
14598 hit_color_no
14599 } else {
14600 item.color
14601 };
14602 let text_color = if component == 2 {
14603 Some(ctx.gameexe_color(text_color_no))
14604 } else {
14605 None
14606 };
14607 collect_selbtn_sprite_visuals_recursive(
14608 obj,
14609 component,
14610 item.button_action_no,
14611 item.button_state,
14612 text_color,
14613 &mut map,
14614 );
14615 }
14616 for obj in &item.object_list {
14617 collect_selbtn_sprite_visuals_recursive(
14618 obj,
14619 3,
14620 item.button_action_no,
14621 item.button_state,
14622 None,
14623 &mut map,
14624 );
14625 }
14626 }
14627 }
14628 }
14629
14630 if map.is_empty() {
14631 return;
14632 }
14633
14634 for rs in sprites.iter_mut() {
14635 let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) else {
14636 continue;
14637 };
14638 let Some(visual) = map.get(&(lid, sid)).cloned() else {
14639 continue;
14640 };
14641 let pat = button_action_pattern(&ctx.tables, visual.action_no, visual.state);
14642
14643 rs.sprite.x = rs.sprite.x.saturating_add(pat.rep_pos_x as i32);
14644 rs.sprite.y = rs.sprite.y.saturating_add(pat.rep_pos_y as i32);
14645
14646 match visual.component {
14647 0 => {
14648 if let Some(file_name) = visual.base_file.as_deref().filter(|s| !s.is_empty()) {
14649 let patno = visual.base_patno.saturating_add(pat.rep_pat_no).max(0) as u32;
14650 let image_id = match ctx.images.load_g00(file_name, patno) {
14651 Ok(id) => Some(id),
14652 Err(_) => ctx.images.load_bg_frame(file_name, patno as usize).ok(),
14653 };
14654 if let Some(image_id) = image_id {
14655 rs.sprite.image_id = Some(image_id.clone());
14656 if let Some(img) = ctx.images.get(&image_id) {
14657 rs.sprite.object_anchor = true;
14658 rs.sprite.texture_center_x = img.center_x as f32;
14659 rs.sprite.texture_center_y = img.center_y as f32;
14660 }
14661 }
14662 }
14663 rs.sprite.tr =
14664 ((rs.sprite.tr as i64 * pat.rep_tr.clamp(0, 255)) / 255).clamp(0, 255) as u8;
14665 rs.sprite.bright = (rs.sprite.bright as i64 + pat.rep_bright).clamp(0, 255) as u8;
14666 rs.sprite.dark = (rs.sprite.dark as i64 + pat.rep_dark).clamp(0, 255) as u8;
14667 }
14668 1 => {
14669 let (cfg_r, cfg_g, cfg_b, cfg_a) = ctx.syscom_filter_config_rgba();
14670 rs.sprite.alpha = 255;
14671 rs.sprite.tr =
14672 ((cfg_a as i64 * pat.rep_tr.clamp(0, 255)) / 255).clamp(0, 255) as u8;
14673 rs.sprite.alpha_test = true;
14674 rs.sprite.alpha_blend = true;
14675 rs.sprite.color_rate = 0;
14676 rs.sprite.color_add_r = cfg_r;
14677 rs.sprite.color_add_g = cfg_g;
14678 rs.sprite.color_add_b = cfg_b;
14679 rs.sprite.color_r = 0;
14680 rs.sprite.color_g = 0;
14681 rs.sprite.color_b = 0;
14682 rs.sprite.bright = 0;
14683 rs.sprite.dark = 0;
14684 rs.sprite.mask_mode = 0;
14685 }
14686 2 => {
14687 if let Some((r, g, b)) = visual.text_color {
14688 rs.sprite.color_rate = 255;
14692 rs.sprite.color_r = r;
14693 rs.sprite.color_g = g;
14694 rs.sprite.color_b = b;
14695 rs.sprite.color_add_r = 0;
14696 rs.sprite.color_add_g = 0;
14697 rs.sprite.color_add_b = 0;
14698 rs.sprite.bright = 0;
14699 rs.sprite.dark = 0;
14700 }
14701 }
14702 _ => {}
14703 }
14704 }
14705}
14706
14707fn append_mwnd_embedded_groups(
14708 ctx: &CommandContext,
14709 stage_form_id: u32,
14710 worlds: Option<&Vec<globals::WorldState>>,
14711 stage_idx: i64,
14712 m: &globals::MwndState,
14713 groups: &mut Vec<SiglusRenderNode>,
14714 object_keys: &mut HashSet<(LayerId, SpriteId)>,
14715 debug: &mut Vec<String>,
14716) {
14717 if ctx.globals.script.mwnd_disp_off_flag
14718 || ctx.globals.syscom.hide_mwnd.onoff
14719 || ctx.globals.syscom.msg_back_open
14720 {
14721 if config_button_trace_enabled() {
14722 debug.push(format!(
14723 "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_SKIP] stage={} reason=hidden script_off={} sys_hide={} open={} buttons={} objects={} waku={} filter={} pos={:?} size={:?}",
14724 stage_idx,
14725 ctx.globals.script.mwnd_disp_off_flag,
14726 ctx.globals.syscom.hide_mwnd.onoff,
14727 m.open,
14728 m.button_list.len(),
14729 m.object_list.len(),
14730 if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
14731 if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
14732 m.window_pos,
14733 m.window_size
14734 ));
14735 }
14736 return;
14737 }
14738 let Some((window_x, window_y, window_w, window_h, ui_state)) =
14739 mwnd_window_rect_for_embedded(ctx, m)
14740 else {
14741 if config_button_trace_enabled() {
14742 debug.push(format!(
14743 "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_SKIP] stage={} reason=no_window_rect open={} buttons={} objects={} waku={} filter={} pos={:?} size={:?}",
14744 stage_idx,
14745 m.open,
14746 m.button_list.len(),
14747 m.object_list.len(),
14748 if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
14749 if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
14750 m.window_pos,
14751 m.window_size
14752 ));
14753 }
14754 return;
14755 };
14756 if !m.open && ui_state.is_none() {
14757 if config_button_trace_enabled() {
14758 debug.push(format!(
14759 "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_SKIP] stage={} reason=closed_no_anim open={} buttons={} objects={} waku={} filter={} rect=({}, {}, {}, {})",
14760 stage_idx,
14761 m.open,
14762 m.button_list.len(),
14763 m.object_list.len(),
14764 if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
14765 if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
14766 window_x, window_y, window_w, window_h
14767 ));
14768 }
14769 return;
14770 }
14771 let mwnd_order_source = if m.order <= 0 {
14772 ctx.tables.mwnd_render.order.max(1)
14773 } else {
14774 m.order
14775 };
14776 let mwnd_order = mwnd_order_source;
14777 let mwnd_layer = m.layer;
14778 let anim_parent = ui_state.map(|ui| mwnd_anim_parent_from_ui_state(m, ui));
14779 if config_button_trace_enabled() {
14780 debug.push(format!(
14781 "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_COLLECT] stage={} open={} buttons={} faces={} objects={} waku={} filter={} rect=({}, {}, {}, {}) ui_anim={} order={} layer={} hide_flags=(script:{},sys:{})",
14782 stage_idx,
14783 m.open,
14784 m.button_list.len(),
14785 m.face_list.len(),
14786 m.object_list.len(),
14787 if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
14788 if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
14789 window_x, window_y, window_w, window_h,
14790 anim_parent.is_some(),
14791 mwnd_order,
14792 mwnd_layer,
14793 ctx.globals.script.mwnd_disp_off_flag,
14794 ctx.globals.syscom.hide_mwnd.onoff
14795 ));
14796 }
14797 for (button_idx, obj) in m.button_list.iter().enumerate() {
14798 if !object_participates_in_tree(obj) {
14799 if config_button_trace_enabled() {
14800 debug.push(format!(
14801 "[SG_DEBUG][CONFIG_BUTTON_TRACE][MWND_BUTTON_SKIP] stage={} button_idx={} reason=not_participating file={} used={} type={} disp={} backend={:?}",
14802 stage_idx,
14803 button_idx,
14804 obj.file_name.as_deref().unwrap_or("-"),
14805 obj.used,
14806 obj.object_type,
14807 obj.base.disp,
14808 obj.backend
14809 ));
14810 }
14811 continue;
14812 }
14813 let local_parent =
14814 mwnd_button_parent_render_state(m, button_idx, window_x, window_y, window_w, window_h);
14815 let parent = apply_mwnd_window_anim_parent(local_parent, anim_parent.clone());
14816 if sg_render_tree_debug_enabled() {
14817 debug.push(format!(
14818 "[SG_DEBUG] mwnd_button_parent[{}] file={} pos=({}, {}) local_base={:?} order={} layer={}",
14819 button_idx,
14820 obj.file_name.as_deref().unwrap_or("-"),
14821 parent.pos_x,
14822 parent.pos_y,
14823 m.waku_button_layout.get(button_idx),
14824 mwnd_order,
14825 mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
14826 ));
14827 }
14828 groups.extend(collect_object_render_nodes(
14829 ctx,
14830 stage_form_id,
14831 worlds,
14832 stage_idx,
14833 button_idx,
14834 obj,
14835 None,
14836 true,
14837 mwnd_order,
14838 mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
14839 Some(parent),
14840 mwnd_order,
14841 mwnd_layer,
14842 object_keys,
14843 debug,
14844 ));
14845 }
14846 for (face_idx, obj) in m.face_list.iter().enumerate() {
14847 if !object_participates_in_tree(obj) {
14848 continue;
14849 }
14850 let parent = apply_mwnd_window_anim_parent(
14851 mwnd_face_parent_render_state(m, face_idx, window_x, window_y),
14852 anim_parent.clone(),
14853 );
14854 groups.extend(collect_object_render_nodes(
14855 ctx,
14856 stage_form_id,
14857 worlds,
14858 stage_idx,
14859 face_idx,
14860 obj,
14861 None,
14862 true,
14863 mwnd_order,
14864 mwnd_layer.saturating_add(ctx.tables.mwnd_render.face_layer_rep),
14865 Some(parent),
14866 mwnd_order,
14867 mwnd_layer,
14868 object_keys,
14869 debug,
14870 ));
14871 }
14872 let parent = apply_mwnd_window_anim_parent(
14873 mwnd_parent_render_state_at(m, window_x, window_y),
14874 anim_parent,
14875 );
14876 append_mwnd_embedded_object_list_groups(
14877 ctx,
14878 stage_form_id,
14879 worlds,
14880 stage_idx,
14881 &m.object_list,
14882 parent,
14883 mwnd_order,
14884 mwnd_layer,
14885 mwnd_order,
14886 mwnd_layer,
14887 groups,
14888 object_keys,
14889 debug,
14890 );
14891}
14892
14893fn mwnd_sort_base(ctx: &CommandContext, m: &globals::MwndState) -> (i64, i64) {
14894 let order = if m.order <= 0 {
14895 ctx.tables.mwnd_render.order.max(1)
14896 } else {
14897 m.order
14898 };
14899 (order, m.layer)
14900}
14901
14902fn selected_mwnd_sort_base(ctx: &CommandContext) -> Option<(i64, i64)> {
14903 if let Some((focused_form, focused_stage, focused_idx)) = ctx.globals.focused_stage_mwnd
14904 && let Some(m) = ctx
14905 .globals
14906 .stage_forms
14907 .get(&focused_form)
14908 .and_then(|st| st.mwnd_lists.get(&focused_stage))
14909 .and_then(|list| list.get(focused_idx))
14910 .filter(|m| m.open)
14911 {
14912 return Some(mwnd_sort_base(ctx, m));
14913 }
14914
14915 let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
14916 form_ids.sort_unstable();
14917 for form_id in form_ids {
14918 let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
14919 continue;
14920 };
14921 let mut stage_ids: Vec<i64> = st.mwnd_lists.keys().copied().collect();
14922 stage_ids.sort_unstable();
14923 for stage_idx in stage_ids {
14924 let Some(list) = st.mwnd_lists.get(&stage_idx) else {
14925 continue;
14926 };
14927 for m in list {
14928 if m.open {
14929 return Some(mwnd_sort_base(ctx, m));
14930 }
14931 }
14932 }
14933 }
14934 None
14935}
14936
14937fn normalize_mwnd_ui_sprite_sorter(
14938 ctx: &CommandContext,
14939 layer_id: Option<LayerId>,
14940 order: i32,
14941) -> (i32, i32) {
14942 if let Some(sorter) = ctx.ui.mwnd_sorter_for_ui_layer(
14943 layer_id,
14944 order,
14945 ctx.tables.mwnd_render.waku_layer_rep,
14946 ctx.tables.mwnd_render.filter_layer_rep,
14947 ctx.tables.mwnd_render.face_layer_rep,
14948 ctx.tables.mwnd_render.shadow_layer_rep,
14949 ctx.tables.mwnd_render.fuchi_layer_rep,
14950 ctx.tables.mwnd_render.moji_layer_rep,
14951 ) {
14952 return sorter;
14953 }
14954 let Some((mwnd_order, mwnd_layer)) = selected_mwnd_sort_base(ctx) else {
14955 return unpack_legacy_sorter_key(order);
14956 };
14957 let layer = match order {
14958 1_000_000 | 1_000_030 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.waku_layer_rep),
14959 1_000_005 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.filter_layer_rep),
14960 1_000_008 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.face_layer_rep),
14961 1_000_010 | 1_000_020 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.shadow_layer_rep),
14962 1_000_011 | 1_000_021 => mwnd_layer.saturating_add(ctx.tables.mwnd_render.fuchi_layer_rep),
14963 1_000_012 | 1_000_013 | 1_000_022 => {
14964 mwnd_layer.saturating_add(ctx.tables.mwnd_render.moji_layer_rep)
14965 }
14966 _ => return unpack_legacy_sorter_key(order),
14967 };
14968 (
14969 mwnd_order.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
14970 layer.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
14971 )
14972}
14973
14974const TNM_STAGE_FRONT_I64: i64 = 1;
14975const TNM_SEL_ITEM_TYPE_ON_I64: i64 = 1;
14976const TNM_SEL_ITEM_TYPE_READ_I64: i64 = 2;
14977const TNM_STAGE_NEXT_I64: i64 = 2;
14978const EXCALL_LOCAL_NS_XOR: u32 = 0x4000;
14979const INIDEF_EXCALL_ORDER: i64 = 20_000;
14980
14981fn excall_stage_form_id(ctx: &CommandContext) -> u32 {
14982 let normal = if ctx.ids.form_global_stage != 0 {
14983 ctx.ids.form_global_stage
14984 } else {
14985 crate::runtime::forms::codes::FORM_GLOBAL_STAGE
14986 };
14987 normal ^ EXCALL_LOCAL_NS_XOR
14988}
14989
14990fn is_excall_stage_form(ctx: &CommandContext, form_id: u32) -> bool {
14991 ctx.excall_state.ready && form_id == excall_stage_form_id(ctx)
14992}
14993
14994fn configured_excall_order(ctx: &CommandContext) -> i64 {
14995 ctx.tables
14996 .gameexe
14997 .as_ref()
14998 .and_then(|cfg| {
14999 cfg.get_i64("#EXCALL.ORDER")
15000 .or_else(|| cfg.get_i64("EXCALL.ORDER"))
15001 })
15002 .unwrap_or(INIDEF_EXCALL_ORDER)
15003}
15004
15005fn mark_all_stage_owned_sprite_keys(
15006 ctx: &CommandContext,
15007 object_keys: &mut HashSet<(LayerId, SpriteId)>,
15008) {
15009 for st in ctx.globals.stage_forms.values() {
15012 for (&stage_idx, list) in &st.object_lists {
15013 for (obj_idx, obj) in list.iter().enumerate() {
15014 mark_object_tree_sprite_keys(ctx, stage_idx, obj_idx, obj, object_keys);
15015 }
15016 }
15017 for (&stage_idx, mwnds) in &st.mwnd_lists {
15018 for m in mwnds {
15019 mark_mwnd_owned_sprite_keys(ctx, stage_idx, m, object_keys);
15020 }
15021 }
15022 for (&stage_idx, items) in &st.btnselitem_lists {
15023 for item in items {
15024 for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
15025 mark_object_tree_sprite_keys(ctx, stage_idx, obj_idx, obj, object_keys);
15026 }
15027 for (obj_idx, obj) in item.object_list.iter().enumerate() {
15028 mark_object_tree_sprite_keys(ctx, stage_idx, obj_idx, obj, object_keys);
15029 }
15030 }
15031 }
15032 }
15033}
15034
15035fn build_siglus_object_render_list(
15036 ctx: &CommandContext,
15037 base: &[RenderSprite],
15038 selected_stage: i64,
15039 object_keys: &mut HashSet<(LayerId, SpriteId)>,
15040) -> (Vec<RenderSprite>, Vec<String>) {
15041 let debug_enabled = sg_render_tree_debug_enabled();
15042 let focused_mwnd = ctx.globals.focused_stage_mwnd;
15043 let mut render_nodes: Vec<SiglusRenderNode> = Vec::new();
15044 let mut debug = Vec::new();
15045 if config_button_trace_enabled() {
15046 debug.push(format!(
15047 "[SG_DEBUG][CONFIG_BUTTON_TRACE][BUILD] selected_stage={} focused_mwnd={:?} base_len={} wipe_active={}",
15048 selected_stage,
15049 ctx.globals.focused_stage_mwnd,
15050 base.len(),
15051 ctx.globals.wipe.is_some()
15052 ));
15053 }
15054
15055 let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
15056 form_ids.sort_unstable();
15057 for form_id in form_ids {
15058 let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
15059 continue;
15060 };
15061 if debug_enabled {
15062 debug.push(format!("[SG_DEBUG] stage_form {}", form_id));
15063 }
15064 let mut stage_ids: Vec<i64> = st
15065 .object_lists
15066 .keys()
15067 .chain(st.mwnd_lists.keys())
15068 .chain(st.group_lists.keys())
15069 .chain(st.btnselitem_lists.keys())
15070 .chain(st.world_lists.keys())
15071 .chain(st.effect_lists.keys())
15072 .chain(st.quake_lists.keys())
15073 .copied()
15074 .collect();
15075 stage_ids.sort_unstable();
15076 stage_ids.dedup();
15077 for stage_idx in stage_ids {
15078 let worlds = st.world_lists.get(&stage_idx);
15079 if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
15080 for m in mwnds {
15081 mark_mwnd_owned_sprite_keys(ctx, stage_idx, m, object_keys);
15082 }
15083 }
15084
15085 let active_cnt = st
15086 .object_lists
15087 .get(&stage_idx)
15088 .map(|list| {
15089 list.iter()
15090 .enumerate()
15091 .filter(|(obj_idx, o)| {
15092 !st.is_embedded_object_slot(stage_idx, *obj_idx)
15093 && object_participates_in_tree(o)
15094 })
15095 .count()
15096 })
15097 .unwrap_or(0);
15098 let mwnd_embedded_cnt = st
15099 .mwnd_lists
15100 .get(&stage_idx)
15101 .map(|mwnds| {
15102 mwnds
15103 .iter()
15104 .map(|m| m.button_list.len() + m.face_list.len() + m.object_list.len())
15105 .sum::<usize>()
15106 })
15107 .unwrap_or(0);
15108 let group_cnt = st.group_lists.get(&stage_idx).map(|v| v.len()).unwrap_or(0);
15109 let btnselitem_cnt = st
15110 .btnselitem_lists
15111 .get(&stage_idx)
15112 .map(|v| v.len())
15113 .unwrap_or(0);
15114 let world_cnt = st.world_lists.get(&stage_idx).map(|v| v.len()).unwrap_or(0);
15115 let effect_cnt = st
15116 .effect_lists
15117 .get(&stage_idx)
15118 .map(|v| v.len())
15119 .unwrap_or(0);
15120 let quake_cnt = st.quake_lists.get(&stage_idx).map(|v| v.len()).unwrap_or(0);
15121 if active_cnt == 0
15122 && mwnd_embedded_cnt == 0
15123 && group_cnt == 0
15124 && btnselitem_cnt == 0
15125 && world_cnt == 0
15126 && effect_cnt == 0
15127 && quake_cnt == 0
15128 {
15129 continue;
15130 }
15131 if debug_enabled {
15132 debug.push(format!(
15133 "[SG_DEBUG] stage {} active_objects={} mwnd_embedded={} groups={} btnselitems={} worlds={} effects={} quakes={}",
15134 stage_idx, active_cnt, mwnd_embedded_cnt, group_cnt, btnselitem_cnt, world_cnt, effect_cnt, quake_cnt
15135 ));
15136 if let Some(effects) = st.effect_lists.get(&stage_idx) {
15137 for (effect_idx, effect) in effects.iter().enumerate() {
15138 debug.push(format!(
15139 "[SG_DEBUG] effect[{}] range=({},{})->({},{}) wipe_copy={} wipe_erase={} xy=({}, {}) color_rate={} bright={} dark={} tr-like-mono={}",
15140 effect_idx,
15141 effect.begin_order,
15142 effect.begin_layer,
15143 effect.end_order,
15144 effect.end_layer,
15145 effect.wipe_copy,
15146 effect.wipe_erase,
15147 effect.x.get_total_value(),
15148 effect.y.get_total_value(),
15149 effect.color_rate.get_total_value(),
15150 effect.bright.get_total_value(),
15151 effect.dark.get_total_value(),
15152 effect.mono.get_total_value(),
15153 ));
15154 }
15155 }
15156 if let Some(quakes) = st.quake_lists.get(&stage_idx) {
15157 for (quake_idx, quake) in quakes.iter().enumerate() {
15158 debug.push(format!(
15159 "[SG_DEBUG] quake[{}] type={} power={} vec={} center=({}, {}) order_range={}..{} active={}",
15160 quake_idx,
15161 quake.quake_type,
15162 quake.power,
15163 quake.vec,
15164 quake.center_x,
15165 quake.center_y,
15166 quake.begin_order,
15167 quake.end_order,
15168 quake.is_active(),
15169 ));
15170 }
15171 }
15172 }
15173 if stage_idx != selected_stage {
15174 if config_button_trace_enabled() {
15175 let mwnd_summary = st.mwnd_lists.get(&stage_idx).map(|mwnds| {
15176 mwnds.iter().enumerate().map(|(idx, m)| {
15177 format!(
15178 "{}:open={} buttons={} objects={} waku={} filter={} pos={:?} size={:?}",
15179 idx,
15180 m.open,
15181 m.button_list.len(),
15182 m.object_list.len(),
15183 if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
15184 if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
15185 m.window_pos,
15186 m.window_size
15187 )
15188 }).collect::<Vec<_>>()
15189 }).unwrap_or_default();
15190 debug.push(format!(
15191 "[SG_DEBUG][CONFIG_BUTTON_TRACE][STAGE_SKIP] form={} stage={} selected_stage={} active_objects={} mwnd_embedded={} mwnds={:?} focused_mwnd={:?}",
15192 form_id,
15193 stage_idx,
15194 selected_stage,
15195 active_cnt,
15196 mwnd_embedded_cnt,
15197 mwnd_summary,
15198 focused_mwnd
15199 ));
15200 }
15201 continue;
15207 }
15208 if let Some(list) = st.object_lists.get(&stage_idx) {
15209 let stage_parent_order = if is_excall_stage_form(ctx, form_id) {
15215 configured_excall_order(ctx)
15216 } else {
15217 0
15218 };
15219 for (obj_idx, obj) in list.iter().enumerate() {
15220 if st.is_embedded_object_slot(stage_idx, obj_idx)
15221 || !st.object_slot_is_used(stage_idx, obj_idx)
15222 || !object_participates_in_tree(obj)
15223 {
15224 continue;
15225 }
15226 let info = effective_object_info(ctx, stage_idx, obj_idx, obj);
15227 let top_order = stage_parent_order.saturating_add(info.order);
15228 let top_layer = info.layer;
15229 render_nodes.extend(collect_object_render_nodes(
15230 ctx,
15231 form_id,
15232 worlds,
15233 stage_idx,
15234 obj_idx,
15235 obj,
15236 Some(info),
15237 true,
15238 stage_parent_order,
15239 0,
15240 None,
15241 top_order,
15242 top_layer,
15243 object_keys,
15244 &mut debug,
15245 ));
15246 }
15247 }
15248 if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
15249 for (mwnd_idx, m) in mwnds.iter().enumerate() {
15250 if debug_enabled {
15251 let embedded_cnt =
15252 m.button_list.len() + m.face_list.len() + m.object_list.len();
15253 if m.open
15254 || embedded_cnt != 0
15255 || !m.msg_text.is_empty()
15256 || !m.name_text.is_empty()
15257 || m.selection.is_some()
15258 {
15259 debug.push(format!(
15260 "[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:{})",
15261 m.open,
15262 m.order,
15263 m.layer,
15264 m.world,
15265 m.msg_text.chars().count(),
15266 m.name_text.chars().count(),
15267 embedded_cnt,
15268 m.button_list.len(),
15269 m.face_list.len(),
15270 m.object_list.len(),
15271 if m.waku_file.is_empty() { "-" } else { m.waku_file.as_str() },
15272 if m.filter_file.is_empty() { "-" } else { m.filter_file.as_str() },
15273 if m.face_file.is_empty() { "-" } else { m.face_file.as_str() },
15274 m.open_anime_type,
15275 m.open_anime_time,
15276 m.close_anime_type,
15277 m.close_anime_time,
15278 m.selection.is_some(),
15279 ctx.globals.script.mwnd_disp_off_flag,
15280 ctx.globals.syscom.hide_mwnd.onoff,
15281 ));
15282 }
15283 }
15284 append_mwnd_embedded_groups(
15285 ctx,
15286 form_id,
15287 worlds,
15288 stage_idx,
15289 m,
15290 &mut render_nodes,
15291 object_keys,
15292 &mut debug,
15293 );
15294 }
15295 }
15296 if let Some(items) = st.btnselitem_lists.get(&stage_idx) {
15297 append_btnselitem_groups(
15298 ctx,
15299 form_id,
15300 worlds,
15301 stage_idx,
15302 items,
15303 &mut render_nodes,
15304 object_keys,
15305 &mut debug,
15306 );
15307 }
15308 }
15309 }
15310
15311 let mut bg = Vec::new();
15312 let mut rest = Vec::new();
15313 for rs in base {
15314 if let Some(mwnd_stage) = ctx.ui.mwnd_stage_for_ui_layer(rs.layer_id)
15315 && mwnd_stage != selected_stage
15316 {
15317 continue;
15318 }
15319 match (rs.layer_id, rs.sprite_id) {
15320 (Some(lid), Some(sid)) if object_keys.contains(&(lid, sid)) => {}
15321 (None, None) if render_sprite_visible_for_submit(rs) => bg.push(rs.clone()),
15322 (None, None) => {}
15323 _ if render_sprite_visible_for_submit(rs) => rest.push(rs.clone()),
15324 _ => {}
15325 }
15326 }
15327
15328 for mut rs in rest {
15329 let (order, layer) = normalize_mwnd_ui_sprite_sorter(ctx, rs.layer_id, rs.sprite.order);
15333 rs.set_sorter(order as i64, layer as i64);
15334 rs.sprite.order = legacy_packed_sorter_key(order as i64, layer as i64);
15335 if let Some((wipe_order, wipe_layer)) = ctx.ui.mwnd_base_sorter_for_ui_layer(rs.layer_id) {
15336 rs.set_wipe_sorter(wipe_order as i64, wipe_layer as i64);
15337 }
15338 if let Some((form_id, _)) = ctx.ui.mwnd_owner_for_ui_layer(rs.layer_id) {
15339 rs.set_stage_form_owner(form_id);
15340 }
15341 render_nodes.push(SiglusRenderNode::from_single_sprite(rs));
15342 }
15343
15344 render_nodes.sort_by(siglus_render_node_cmp);
15350 let sprite_count = render_nodes
15351 .iter()
15352 .map(SiglusRenderNode::sprite_count)
15353 .sum::<usize>();
15354 let mut final_list = Vec::with_capacity(bg.len() + sprite_count);
15355 final_list.extend(bg);
15356 for node in render_nodes {
15357 node.flatten(&mut final_list);
15358 }
15359 (final_list, debug)
15360}
15361
15362fn trace_codes_enabled() -> bool {
15363 static ENABLED: OnceLock<bool> = OnceLock::new();
15364 *ENABLED.get_or_init(|| env_is_set!("SIGLUS_TRACE_CODES"))
15365}
15366
15367pub fn dispatch_form_code(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
15368 let append_dir = ctx.globals.append_dir.as_str();
15369 ctx.images.set_current_append_dir_ref(append_dir);
15370 ctx.movie.set_current_append_dir_ref(append_dir);
15371 ctx.bgm.set_current_append_dir_ref(append_dir);
15372
15373 let code = opcode::OpCode::form(form_id);
15374 if trace_codes_enabled() {
15375 let chain = ctx
15376 .vm_call
15377 .as_ref()
15378 .map(|call| call.element.clone())
15379 .unwrap_or_default();
15380 eprintln!(
15381 "[TRACE code] form={} chain={chain:?} argc={} args={args:?}",
15382 form_id,
15383 args.len()
15384 );
15385 }
15386
15387 opcode::dispatch_code(ctx, code, args)
15388}
15389
15390pub fn dispatch(ctx: &mut CommandContext, cmd: &Command) -> Result<()> {
15392 let Some(code) = cmd.code else {
15393 anyhow::bail!(
15394 "name-only command dispatch is invalid: {}; Siglus user commands must run through SceneVm",
15395 cmd.name
15396 );
15397 };
15398
15399 if !dispatch_form_code(ctx, code.id, &cmd.args)? {
15400 anyhow::bail!("unhandled form code {}", code.id);
15401 }
15402 Ok(())
15403}
15404
15405fn apply_button_visuals(ctx: &mut CommandContext, sprites: &mut [RenderSprite]) {
15406 let mut map: HashMap<(LayerId, SpriteId), ButtonVisualState> = HashMap::default();
15407
15408 let mut form_ids: Vec<u32> = ctx.globals.stage_forms.keys().copied().collect();
15409 form_ids.sort_unstable();
15410 for form_id in form_ids {
15411 let Some(st) = ctx.globals.stage_forms.get(&form_id) else {
15412 continue;
15413 };
15414 let mut stage_ids: Vec<i64> = st
15415 .object_lists
15416 .keys()
15417 .chain(st.mwnd_lists.keys())
15418 .chain(st.btnselitem_lists.keys())
15419 .copied()
15420 .collect();
15421 stage_ids.sort_unstable();
15422 stage_ids.dedup();
15423 for stage_idx in stage_ids {
15424 if let Some(objs) = st.object_lists.get(&stage_idx) {
15425 for (obj_idx, obj) in objs.iter().enumerate() {
15426 collect_button_visuals_recursive(
15427 ctx, st, stage_idx, obj_idx, obj, &mut map, None,
15428 );
15429 }
15430 }
15431 if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
15432 for m in mwnds {
15433 for (obj_idx, obj) in m.button_list.iter().enumerate() {
15434 collect_button_visuals_recursive(
15435 ctx,
15436 st,
15437 stage_idx,
15438 obj_idx,
15439 obj,
15440 &mut map,
15441 Some(obj_idx),
15442 );
15443 }
15444 for (obj_idx, obj) in m.face_list.iter().enumerate() {
15445 collect_button_visuals_recursive(
15446 ctx, st, stage_idx, obj_idx, obj, &mut map, None,
15447 );
15448 }
15449 for (obj_idx, obj) in m.object_list.iter().enumerate() {
15450 collect_button_visuals_recursive(
15451 ctx, st, stage_idx, obj_idx, obj, &mut map, None,
15452 );
15453 }
15454 }
15455 }
15456 if let Some(items) = st.btnselitem_lists.get(&stage_idx) {
15457 for item in items {
15458 for (obj_idx, obj) in item.generated_objects.iter().enumerate() {
15459 collect_button_visuals_recursive(
15460 ctx, st, stage_idx, obj_idx, obj, &mut map, None,
15461 );
15462 }
15463 for (obj_idx, obj) in item.object_list.iter().enumerate() {
15464 collect_button_visuals_recursive(
15465 ctx, st, stage_idx, obj_idx, obj, &mut map, None,
15466 );
15467 }
15468 }
15469 }
15470 }
15471 }
15472
15473 if map.is_empty() {
15474 return;
15475 }
15476
15477 for rs in sprites.iter_mut() {
15478 let (Some(lid), Some(sid)) = (rs.layer_id, rs.sprite_id) else {
15479 continue;
15480 };
15481 let Some(visual) = map.get(&(lid, sid)).cloned() else {
15482 continue;
15483 };
15484 apply_button_state_visual(&ctx.tables, &mut ctx.images, &mut rs.sprite, visual);
15485 }
15486}
15487
15488fn collect_button_visuals_recursive(
15489 ctx: &CommandContext,
15490 st: &globals::StageFormState,
15491 stage_idx: i64,
15492 obj_idx: usize,
15493 obj: &globals::ObjectState,
15494 map: &mut HashMap<(LayerId, SpriteId), ButtonVisualState>,
15495 mwnd_button_idx: Option<usize>,
15496) {
15497 use globals::ObjectBackend;
15498
15499 let mut effective_visual = None;
15504 if obj.button.enabled || obj.button.state == TNM_BTN_STATE_DISABLE {
15505 if !button_syscom_mode_visible(&ctx.globals.syscom, &obj.button) {
15506 effective_visual = None;
15507 } else {
15508 let state = button_real_state_for_visual(
15509 &ctx.globals.syscom,
15510 st,
15511 stage_idx,
15512 obj,
15513 mwnd_button_idx,
15514 ctx.runtime_read_skip_is_enable(),
15515 );
15516 if sg_debug_enabled() {
15517 let runtime_slot = object_runtime_slot(obj_idx, obj);
15518 eprintln!(
15519 "[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={}::{}/{}",
15520 stage_idx,
15521 obj_idx,
15522 runtime_slot,
15523 obj.file_name,
15524 mwnd_button_idx,
15525 state,
15526 button_state_name(state),
15527 obj.button.state,
15528 obj.button.enabled,
15529 button_syscom_mode_visible(&ctx.globals.syscom, &obj.button),
15530 button_disabled_reason(
15531 &ctx.globals.syscom,
15532 obj,
15533 mwnd_button_idx,
15534 ctx.runtime_read_skip_is_enable()
15535 ),
15536 obj.button.button_no,
15537 obj.button.group_no,
15538 obj.button.group_idx(),
15539 obj.button.action_no,
15540 obj.button.cut_no,
15541 obj.button.hit,
15542 obj.button.pushed,
15543 obj.button.sys_type,
15544 obj.button.sys_type_opt,
15545 obj.button.mode,
15546 obj.button.decided_action_scn_name,
15547 obj.button.decided_action_cmd_name,
15548 obj.button.decided_action_z_no
15549 );
15550 }
15551 let base_patno = obj
15552 .lookup_int_prop(&ctx.ids, ctx.ids.obj_patno)
15553 .unwrap_or(obj.base.patno);
15554 effective_visual = Some(ButtonVisualState {
15555 state,
15556 action_no: obj.button.action_no,
15557 file_name: obj.file_name.clone(),
15558 base_patno,
15559 cut_no: obj.button.cut_no,
15560 });
15561 }
15562 }
15563
15564 if let Some(visual) = effective_visual.clone() {
15565 let runtime_slot = object_runtime_slot(obj_idx, obj);
15566 match &obj.backend {
15567 ObjectBackend::Gfx => {
15568 if let Some((lid, sid)) = ctx
15569 .gfx
15570 .object_sprite_binding(stage_idx, runtime_slot as i64)
15571 {
15572 map.insert((lid, sid), visual.clone());
15573 }
15574 }
15575 ObjectBackend::Rect {
15576 layer_id,
15577 sprite_id,
15578 ..
15579 } => {
15580 map.insert((*layer_id, *sprite_id), visual.clone());
15581 }
15582 ObjectBackend::String { .. } => {
15583 for binding in layer_backed_object_sprite_bindings(&obj.backend) {
15584 map.insert(binding, visual.clone());
15585 }
15586 }
15587 ObjectBackend::Movie {
15588 layer_id,
15589 sprite_id,
15590 ..
15591 } => {
15592 map.insert((*layer_id, *sprite_id), visual.clone());
15593 }
15594 ObjectBackend::Number {
15595 layer_id,
15596 sprite_ids,
15597 }
15598 | ObjectBackend::Weather {
15599 layer_id,
15600 sprite_ids,
15601 } => {
15602 for sid in sprite_ids {
15603 map.insert((*layer_id, *sid), visual.clone());
15604 }
15605 }
15606 ObjectBackend::None => {}
15607 }
15608 }
15609
15610 for (child_idx, child) in obj.runtime.child_objects.iter().enumerate() {
15611 collect_button_visuals_recursive(ctx, st, stage_idx, child_idx, child, map, None);
15612 }
15613}
15614
15615#[cfg(test)]
15616mod button_visual_ownership_tests {
15617 use super::*;
15618
15619 fn picture(sprite_id: SpriteId, file: &str) -> globals::ObjectState {
15620 globals::ObjectState {
15621 used: true,
15622 file_name: Some(file.to_owned()),
15623 backend: globals::ObjectBackend::Rect {
15624 layer_id: 0,
15625 sprite_id,
15626 width: 360,
15627 height: 48,
15628 },
15629 ..Default::default()
15630 }
15631 }
15632
15633 fn visuals(obj: &globals::ObjectState) -> HashMap<(LayerId, SpriteId), ButtonVisualState> {
15634 let ctx = CommandContext::new(PathBuf::from("."));
15635 let stage = globals::StageFormState::default();
15636 let mut map = HashMap::default();
15637 collect_button_visuals_recursive(&ctx, &stage, 0, 0, obj, &mut map, None);
15638 map
15639 }
15640
15641 #[test]
15642 fn save_slot_button_does_not_replace_child_text_digits_or_icons() {
15643 let mut slot = picture(0, "_s_data");
15644 slot.button.enabled = true;
15645 slot.button.action_no = 9;
15646
15647 let mut text = globals::ObjectState {
15648 used: true,
15649 backend: globals::ObjectBackend::String {
15650 layer_id: 0,
15651 shadow_sprite_id: 1,
15652 fuchi_sprite_id: 2,
15653 sprite_id: 3,
15654 shadow_image_id: None,
15655 fuchi_image_id: None,
15656 image_id: None,
15657 glyphs: Vec::new(),
15658 mwnd_layer_reps: false,
15659 width: 200,
15660 height: 22,
15661 },
15662 ..Default::default()
15663 };
15664
15665 let mut number = globals::ObjectState {
15666 used: true,
15667 backend: globals::ObjectBackend::Number {
15668 layer_id: 0,
15669 sprite_ids: vec![4, 5],
15670 },
15671 ..Default::default()
15672 };
15673
15674 let mut container = globals::ObjectState::default();
15675 container.runtime.child_objects.push(picture(6, "_sl_new"));
15676 slot.runtime.child_objects = vec![text, number, container];
15677
15678 for state in 0..=TNM_BTN_STATE_DISABLE {
15679 slot.button.state = state;
15680 slot.button.hit = state == TNM_BTN_STATE_HIT;
15681 slot.button.pushed = state == TNM_BTN_STATE_PUSH;
15682 let map = visuals(&slot);
15683 assert_eq!(map.len(), 1, "parent button state {state}");
15684 assert_eq!(map[&(0, 0)].file_name.as_deref(), Some("_s_data"));
15685 }
15686 }
15687
15688 #[test]
15689 fn nested_button_keeps_its_own_action_texture_and_cut() {
15690 let ctx = CommandContext::new(PathBuf::from("."));
15691 let mut slot = picture(0, "_l_data");
15692 slot.button.enabled = true;
15693 slot.button.action_no = 9;
15694 slot.button.state = TNM_BTN_STATE_DISABLE;
15695
15696 let mut icon = picture(1, "_sl_new");
15697 icon.button.enabled = true;
15698 icon.button.action_no = 2;
15699 icon.button.hit = true;
15700 icon.button.cut_no = 3;
15701 icon.set_int_prop(&ctx.ids, ctx.ids.obj_patno, 4);
15702 icon.runtime.child_objects.push(picture(2, "child_label"));
15703 slot.runtime.child_objects.push(icon);
15704
15705 let map = visuals(&slot);
15706 assert_eq!(map.len(), 2);
15707 let icon_visual = &map[&(0, 1)];
15708 assert_eq!(icon_visual.state, TNM_BTN_STATE_HIT);
15709 assert_eq!(icon_visual.action_no, 2);
15710 assert_eq!(icon_visual.file_name.as_deref(), Some("_sl_new"));
15711 assert_eq!(icon_visual.base_patno, 4);
15712 assert_eq!(icon_visual.cut_no, 3);
15713 }
15714}
15715
15716fn button_action_pattern(
15717 tables: &tables::AssetTables,
15718 action_no: i64,
15719 state: i64,
15720) -> tables::ButtonActionPattern {
15721 let state_idx = state.clamp(0, 4) as usize;
15722 if action_no >= 0
15723 && let Some(tpl) = tables.button_action_templates.get(action_no as usize)
15724 {
15725 return tpl.state[state_idx];
15726 }
15727 tables::ButtonActionTemplate::default().state[state_idx]
15728}
15729
15730fn apply_button_state_visual(
15731 tables: &tables::AssetTables,
15732 images: &mut ImageManager,
15733 sprite: &mut Sprite,
15734 visual: ButtonVisualState,
15735) {
15736 let pat = button_action_pattern(tables, visual.action_no, visual.state);
15737
15738 if let Some(file_name) = visual.file_name.as_deref().filter(|s| !s.is_empty()) {
15739 let patno = visual
15740 .base_patno
15741 .saturating_add(visual.cut_no)
15742 .saturating_add(pat.rep_pat_no)
15743 .max(0) as u32;
15744 let image_id = match images.load_g00(file_name, patno) {
15745 Ok(id) => Some(id),
15746 Err(_) => images.load_bg_frame(file_name, patno as usize).ok(),
15747 };
15748 if let Some(image_id) = image_id {
15749 sprite.image_id = Some(image_id.clone());
15750 if let Some(img) = images.get(&image_id) {
15751 sprite.object_anchor = true;
15752 sprite.texture_center_x = img.center_x as f32;
15753 sprite.texture_center_y = img.center_y as f32;
15754 } else {
15755 sprite.object_anchor = false;
15756 sprite.texture_center_x = 0.0;
15757 sprite.texture_center_y = 0.0;
15758 }
15759 }
15760 }
15761 sprite.x = sprite.x.saturating_add(pat.rep_pos_x as i32);
15762 sprite.y = sprite.y.saturating_add(pat.rep_pos_y as i32);
15763 sprite.tr = ((sprite.tr as i64 * pat.rep_tr.clamp(0, 255)) / 255).clamp(0, 255) as u8;
15764 sprite.bright = (sprite.bright as i64 + pat.rep_bright).clamp(0, 255) as u8;
15765 sprite.dark = (sprite.dark as i64 + pat.rep_dark).clamp(0, 255) as u8;
15766}
15767
15768fn unpack_legacy_sorter_key(order: i32) -> (i32, i32) {
15769 if order.abs() >= 1024 {
15770 (order.div_euclid(1024), order.rem_euclid(1024))
15771 } else {
15772 (0, order)
15773 }
15774}
15775
15776fn legacy_packed_sorter_key(order: i64, layer: i64) -> i32 {
15777 order
15778 .clamp(i32::MIN as i64 / 1024, i32::MAX as i64 / 1024)
15779 .saturating_mul(1024)
15780 .saturating_add(layer.clamp(-1023, 1023)) as i32
15781}
15782
15783fn sorter_key(order: i32, layer: i32) -> (i32, i32) {
15784 (order, layer)
15785}
15786
15787fn sprite_sorter_key(rs: &RenderSprite) -> (i32, i32) {
15788 (rs.sorter_order, rs.sorter_layer)
15789}
15790
15791fn quake_order_affects_sprite(quake: &globals::ScreenQuakeState, rs: &RenderSprite) -> bool {
15792 let order = rs.sorter_order;
15793 let (lo, hi) = if quake.begin_order <= quake.end_order {
15794 (quake.begin_order, quake.end_order)
15795 } else {
15796 (quake.end_order, quake.begin_order)
15797 };
15798 lo <= order && order <= hi
15799}
15800
15801fn apply_quake_transform(sprite: &mut Sprite, tr: globals::ScreenQuakeTransform) {
15802 sprite.x = sprite.x.saturating_add(tr.x);
15803 sprite.y = sprite.y.saturating_add(tr.y);
15804 sprite.pivot_x += tr.center_x as f32;
15805 sprite.pivot_y += tr.center_y as f32;
15806 sprite.scale_x *= tr.scale;
15807 sprite.scale_y *= tr.scale;
15808 sprite.rotate += tr.rotate_degrees * std::f32::consts::PI / 180.0;
15809}
15810
15811fn render_sprite_owned_by(rs: &RenderSprite, stage_form_id: u32) -> bool {
15812 rs.stage_form_id == Some(stage_form_id)
15813}
15814
15815fn collect_screen_shake(globals: &globals::GlobalState) -> (i32, i32) {
15816 let mut shake_x = 0i32;
15817 let mut shake_y = 0i32;
15818 let mut screen_form_ids: Vec<u32> = globals.screen_forms.keys().copied().collect();
15819 screen_form_ids.sort_unstable();
15820 for form_id in screen_form_ids {
15821 let Some(st) = globals.screen_forms.get(&form_id) else {
15822 continue;
15823 };
15824 if st.shake.is_active() {
15825 shake_x = shake_x.saturating_add(st.shake.cur_x);
15826 shake_y = shake_y.saturating_add(st.shake.cur_y);
15827 }
15828 }
15829 (shake_x, shake_y)
15830}
15831
15832fn collect_screen_quakes(globals: &globals::GlobalState) -> Vec<&globals::ScreenQuakeState> {
15833 if globals.script.quake_stop_flag {
15834 return Vec::new();
15835 }
15836 let mut out = Vec::new();
15837 let mut screen_form_ids: Vec<u32> = globals.screen_forms.keys().copied().collect();
15838 screen_form_ids.sort_unstable();
15839 for form_id in screen_form_ids {
15840 let Some(st) = globals.screen_forms.get(&form_id) else {
15841 continue;
15842 };
15843 out.extend(st.quake_list.iter().filter(|q| q.is_active()));
15844 }
15845 out
15846}
15847
15848fn collect_stage_quakes(
15849 globals: &globals::GlobalState,
15850 stage_form_id: u32,
15851 stage_idx: i64,
15852) -> Vec<&globals::ScreenQuakeState> {
15853 if globals.script.quake_stop_flag {
15854 return Vec::new();
15855 }
15856 globals
15857 .stage_forms
15858 .get(&stage_form_id)
15859 .and_then(|st| st.quake_lists.get(&stage_idx))
15860 .map(|quakes| quakes.iter().filter(|q| q.is_active()).collect())
15861 .unwrap_or_default()
15862}
15863
15864fn apply_quakes_to_owner(
15865 sprites: &mut [RenderSprite],
15866 owner_form_id: u32,
15867 quakes: &[&globals::ScreenQuakeState],
15868) {
15869 if quakes.is_empty() {
15870 return;
15871 }
15872 for rs in sprites {
15873 if !render_sprite_owned_by(rs, owner_form_id) {
15874 continue;
15875 }
15876 for quake in quakes {
15877 if quake_order_affects_sprite(quake, rs) {
15878 apply_quake_transform(&mut rs.sprite, quake.transform());
15879 }
15880 }
15881 }
15882}
15883
15884#[derive(Debug, Clone, Copy)]
15885struct EffectParam {
15886 x: i32,
15887 y: i32,
15888 z: i32,
15889 mono: i32,
15890 reverse: i32,
15891 bright: i32,
15892 dark: i32,
15893 color_r: i32,
15894 color_g: i32,
15895 color_b: i32,
15896 color_rate: i32,
15897 color_add_r: i32,
15898 color_add_g: i32,
15899 color_add_b: i32,
15900 begin_order: i32,
15901 begin_layer: i32,
15902 end_order: i32,
15903 end_layer: i32,
15904}
15905
15906fn apply_effects_to_owner(
15907 sprites: &mut [RenderSprite],
15908 owner_form_id: u32,
15909 effects: &[EffectParam],
15910) {
15911 if effects.is_empty() {
15912 return;
15913 }
15914 for effect in effects {
15915 for rs in sprites.iter_mut() {
15916 if !render_sprite_owned_by(rs, owner_form_id) || !in_sorter_range(rs, effect) {
15917 continue;
15918 }
15919 apply_effect_to_sprite(&mut rs.sprite, effect);
15920 }
15921 }
15922}
15923
15924fn read_effect_event(ev: &crate::runtime::int_event::IntEvent) -> i32 {
15925 ev.get_total_value()
15926}
15927
15928fn effect_param_from_state(effect: &globals::ScreenEffectState) -> EffectParam {
15929 EffectParam {
15930 x: read_effect_event(&effect.x),
15931 y: read_effect_event(&effect.y),
15932 z: read_effect_event(&effect.z),
15933 mono: read_effect_event(&effect.mono),
15934 reverse: read_effect_event(&effect.reverse),
15935 bright: read_effect_event(&effect.bright),
15936 dark: read_effect_event(&effect.dark),
15937 color_r: read_effect_event(&effect.color_r),
15938 color_g: read_effect_event(&effect.color_g),
15939 color_b: read_effect_event(&effect.color_b),
15940 color_rate: read_effect_event(&effect.color_rate),
15941 color_add_r: read_effect_event(&effect.color_add_r),
15942 color_add_g: read_effect_event(&effect.color_add_g),
15943 color_add_b: read_effect_event(&effect.color_add_b),
15944 begin_order: effect.begin_order,
15945 begin_layer: effect.begin_layer,
15946 end_order: effect.end_order,
15947 end_layer: effect.end_layer,
15948 }
15949}
15950
15951fn collect_global_screen_effects(globals: &globals::GlobalState) -> Vec<EffectParam> {
15952 let mut out = Vec::new();
15953 let mut screen_form_ids: Vec<u32> = globals.screen_forms.keys().copied().collect();
15954 screen_form_ids.sort_unstable();
15955 for form_id in screen_form_ids {
15956 let Some(st) = globals.screen_forms.get(&form_id) else {
15957 continue;
15958 };
15959 for effect in &st.effect_list {
15960 let rp = effect_param_from_state(effect);
15961 if effect_is_use(&rp) {
15962 out.push(rp);
15963 }
15964 }
15965 }
15966 out
15967}
15968
15969fn collect_stage_effects(
15970 globals: &globals::GlobalState,
15971 stage_form_id: u32,
15972 stage_idx: i64,
15973) -> Vec<EffectParam> {
15974 globals
15975 .stage_forms
15976 .get(&stage_form_id)
15977 .and_then(|st| st.effect_lists.get(&stage_idx))
15978 .map(|effects| {
15979 effects
15980 .iter()
15981 .map(effect_param_from_state)
15982 .filter(effect_is_use)
15983 .collect()
15984 })
15985 .unwrap_or_default()
15986}
15987
15988fn apply_stage_render_effects(
15989 globals: &globals::GlobalState,
15990 ids: &constants::RuntimeConstants,
15991 stage_idx: i64,
15992 sprites: &mut [RenderSprite],
15993) {
15994 let normal_stage_form = ids.form_global_stage;
15998 if normal_stage_form != 0 {
15999 let screen_effects = collect_global_screen_effects(globals);
16000 apply_effects_to_owner(sprites, normal_stage_form, &screen_effects);
16001
16002 let screen_quakes = collect_screen_quakes(globals);
16003 apply_quakes_to_owner(sprites, normal_stage_form, &screen_quakes);
16004 }
16005
16006 let mut stage_form_ids: Vec<u32> = globals.stage_forms.keys().copied().collect();
16007 stage_form_ids.sort_unstable();
16008 for stage_form_id in stage_form_ids {
16009 let effects = collect_stage_effects(globals, stage_form_id, stage_idx);
16010 apply_effects_to_owner(sprites, stage_form_id, &effects);
16011
16012 let quakes = collect_stage_quakes(globals, stage_form_id, stage_idx);
16013 apply_quakes_to_owner(sprites, stage_form_id, &quakes);
16014 }
16015
16016 let (shake_x, shake_y) = collect_screen_shake(globals);
16022 if shake_x != 0 || shake_y != 0 {
16023 for rs in sprites {
16024 rs.sprite.x = rs.sprite.x.saturating_add(shake_x);
16025 rs.sprite.y = rs.sprite.y.saturating_add(shake_y);
16026 }
16027 }
16028}
16029
16030fn effect_is_use(effect: &EffectParam) -> bool {
16031 effect.x != 0
16032 || effect.y != 0
16033 || effect.z != 0
16034 || effect.mono != 0
16035 || effect.reverse != 0
16036 || effect.bright != 0
16037 || effect.dark != 0
16038 || effect.color_r != 0
16039 || effect.color_g != 0
16040 || effect.color_b != 0
16041 || effect.color_rate != 0
16042 || effect.color_add_r != 0
16043 || effect.color_add_g != 0
16044 || effect.color_add_b != 0
16045}
16046
16047fn in_sorter_range(rs: &RenderSprite, effect: &EffectParam) -> bool {
16048 let key = sprite_sorter_key(rs);
16049 let begin = sorter_key(effect.begin_order, effect.begin_layer);
16050 let end = sorter_key(effect.end_order, effect.end_layer);
16051 if begin <= end {
16052 begin <= key && key <= end
16053 } else {
16054 end <= key && key <= begin
16055 }
16056}
16057
16058fn apply_effect_to_sprite(sprite: &mut Sprite, effect: &EffectParam) {
16059 sprite.x = sprite.x.saturating_add(effect.x);
16060 sprite.y = sprite.y.saturating_add(effect.y);
16061 sprite.z += effect.z as f32;
16062
16063 sprite.mono = combine_lerp(sprite.mono, effect.mono);
16064 sprite.reverse = combine_lerp(sprite.reverse, effect.reverse);
16065 sprite.bright = combine_lerp(sprite.bright, effect.bright);
16066 sprite.dark = combine_lerp(sprite.dark, effect.dark);
16067
16068 let sr = sprite.color_rate as i32;
16070 let pr = clamp_u8(effect.color_rate);
16071 if sr + pr > 0 {
16072 let parent_rate = (pr * 255 * 255) / (255 * 255 - (255 - sr) * (255 - pr));
16073 sprite.color_r = blend_color(sprite.color_r, effect.color_r, parent_rate);
16074 sprite.color_g = blend_color(sprite.color_g, effect.color_g, parent_rate);
16075 sprite.color_b = blend_color(sprite.color_b, effect.color_b, parent_rate);
16076 }
16077 sprite.color_rate = (255 - (255 - sr) * (255 - pr) / 255) as u8;
16078
16079 sprite.color_add_r = clamp_add(sprite.color_add_r, effect.color_add_r);
16080 sprite.color_add_g = clamp_add(sprite.color_add_g, effect.color_add_g);
16081 sprite.color_add_b = clamp_add(sprite.color_add_b, effect.color_add_b);
16082}
16083
16084fn combine_lerp(base: u8, parent: i32) -> u8 {
16085 let parent = clamp_u8(parent);
16086 (255 - (255 - base as i32) * (255 - parent) / 255) as u8
16087}
16088
16089fn blend_color(base: u8, parent: i32, rate: i32) -> u8 {
16090 let parent = clamp_u8(parent);
16091 let base = base as i32;
16092 ((base * (255 - rate) + parent * rate) / 255) as u8
16093}
16094
16095fn clamp_u8(v: i32) -> i32 {
16096 v.clamp(0, 255)
16097}
16098
16099fn clamp_add(base: u8, add: i32) -> u8 {
16100 let v = base as i32 + add;
16101 v.clamp(0, 255) as u8
16102}
16103
16104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16105enum WipePartition {
16106 Under,
16107 Target,
16108 Over,
16109}
16110
16111fn compose_basic_wipe_scene_inputs<T: Clone>(
16112 wipe_type: i32,
16113 under: &mut Vec<T>,
16114 front: &mut Vec<T>,
16115 next: &mut Vec<T>,
16116) {
16117 if !matches!(wipe_type, 0..=2) {
16118 return;
16119 }
16120
16121 let mut front_scene = Vec::with_capacity(under.len() + front.len());
16122 front_scene.extend(under.iter().cloned());
16123 front_scene.append(front);
16124
16125 let mut next_scene = Vec::with_capacity(under.len() + next.len());
16126 next_scene.extend(under.iter().cloned());
16127 next_scene.append(next);
16128
16129 under.clear();
16130 *front = front_scene;
16131 *next = next_scene;
16132}
16133
16134fn effective_wipe_render_order_bounds(
16135 ctx: &CommandContext,
16136 wipe: &globals::WipeState,
16137) -> (i32, i32) {
16138 if wipe.stage_form_id != excall_stage_form_id(ctx) {
16139 return (wipe.begin_order, wipe.end_order);
16140 }
16141
16142 let offset = configured_excall_order(ctx);
16146 let shift = |value: i32| -> i32 {
16147 if value == i32::MIN || value == i32::MAX {
16148 value
16149 } else {
16150 (value as i64)
16151 .saturating_add(offset)
16152 .clamp(i32::MIN as i64, i32::MAX as i64) as i32
16153 }
16154 };
16155 (shift(wipe.begin_order), shift(wipe.end_order))
16156}
16157
16158fn render_sprite_wipe_sorter(rs: &RenderSprite) -> (i32, i32) {
16159 (rs.wipe_sorter_order, rs.wipe_sorter_layer)
16163}
16164
16165fn classify_wipe_partition(
16166 rs: &RenderSprite,
16167 begin_layer: i32,
16168 end_layer: i32,
16169 begin_order: i32,
16170 end_order: i32,
16171 with_low: bool,
16172) -> WipePartition {
16173 let (order, layer) = render_sprite_wipe_sorter(rs);
16174 if layer < begin_layer {
16175 return WipePartition::Under;
16176 }
16177 if layer > end_layer {
16178 return WipePartition::Over;
16179 }
16180 let affected = if with_low {
16181 order <= end_order
16182 } else {
16183 order >= begin_order && order <= end_order
16184 };
16185 if affected {
16186 WipePartition::Target
16187 } else if order < begin_order {
16188 WipePartition::Under
16189 } else {
16190 WipePartition::Over
16191 }
16192}
16193
16194fn sprite_forward_dir(sprite: &Sprite) -> [f32; 3] {
16195 let (sx, cx) = sprite.rotate_x.sin_cos();
16196 let (sy, cy) = sprite.rotate_y.sin_cos();
16197
16198 let x = sy * cx;
16204 let y = -sx;
16205 let z = cy * cx;
16206 let len = (x * x + y * y + z * z).sqrt().max(1e-6);
16207 [x / len, y / len, z / len]
16208}
16209
16210fn apply_runtime_light_and_fog(ctx: &CommandContext, sprite: &mut Sprite) {
16211 sprite.light_enabled = false;
16212 sprite.mesh_runtime_lights.clear();
16213 sprite.light_diffuse = [1.0, 1.0, 1.0, 1.0];
16214 sprite.light_ambient = [0.0, 0.0, 0.0, 1.0];
16215 sprite.light_specular = [0.0, 0.0, 0.0, 1.0];
16216 sprite.light_factor = 0.0;
16217 sprite.light_kind = -1;
16218 sprite.light_pos = [0.0, 0.0, 0.0, 0.0];
16219 sprite.light_dir = [0.0, 0.0, -1.0, 0.0];
16220 sprite.light_atten = [1.0, 0.0, 0.0, 5000.0];
16221 sprite.light_cone = [0.0, 0.0, 1.0, 0.0];
16222 sprite.shadow_cast = sprite.mesh_kind != 0;
16223 sprite.shadow_receive = sprite.mesh_kind != 0;
16224 sprite.fog_enabled = false;
16225 sprite.fog_color = [0.0, 0.0, 0.0, 1.0];
16226 sprite.fog_near = 0.0;
16227 sprite.fog_far = 0.0;
16228 sprite.fog_scroll_x = 0.0;
16229 sprite.fog_texture_image_id = None;
16230
16231 if sprite.light_no >= 0 {
16232 let camera_default_light;
16233 let light = if let Some(light) = ctx.globals.lights.get(&sprite.light_no) {
16234 Some(light)
16235 } else if sprite.light_no == 0 {
16236 camera_default_light = siglus_default_camera_light(sprite);
16237 Some(&camera_default_light)
16238 } else {
16239 None
16240 };
16241 if let Some(light) = light {
16242 let n = if sprite.billboard {
16243 [0.0, 0.0, -1.0]
16244 } else {
16245 sprite_forward_dir(sprite)
16246 };
16247 let pos = [sprite.x as f32, sprite.y as f32, sprite.z];
16248 let mut ndotl = 0.0f32;
16249 let mut attenuation = 1.0f32;
16250 match light.kind {
16251 globals::LightType::Directional => {
16252 let l = [-light.dir[0], -light.dir[1], -light.dir[2]];
16253 let ll = (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt().max(1e-6);
16254 ndotl = (n[0] * (l[0] / ll) + n[1] * (l[1] / ll) + n[2] * (l[2] / ll)).max(0.0);
16255 }
16256 globals::LightType::Point
16257 | globals::LightType::Spot
16258 | globals::LightType::ShadowMapSpot => {
16259 let mut l = [
16260 light.pos[0] - pos[0],
16261 light.pos[1] - pos[1],
16262 light.pos[2] - pos[2],
16263 ];
16264 let dist = (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt().max(1e-6);
16265 l = [l[0] / dist, l[1] / dist, l[2] / dist];
16266 ndotl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
16267 attenuation = 1.0
16268 / (light.attenuation0
16269 + light.attenuation1 * dist
16270 + light.attenuation2 * dist * dist)
16271 .max(1.0);
16272 if light.range > 0.0 {
16273 attenuation *= (1.0 - dist / light.range).clamp(0.0, 1.0);
16274 }
16275 if matches!(
16276 light.kind,
16277 globals::LightType::Spot | globals::LightType::ShadowMapSpot
16278 ) {
16279 let spot_dir = [light.dir[0], light.dir[1], light.dir[2]];
16280 let sll = (spot_dir[0] * spot_dir[0]
16281 + spot_dir[1] * spot_dir[1]
16282 + spot_dir[2] * spot_dir[2])
16283 .sqrt()
16284 .max(1e-6);
16285 let cosang = (l[0] * (-spot_dir[0] / sll)
16286 + l[1] * (-spot_dir[1] / sll)
16287 + l[2] * (-spot_dir[2] / sll))
16288 .clamp(-1.0, 1.0);
16289 let cos_theta = (light.theta_deg.to_radians() * 0.5).cos();
16290 let cos_phi = (light.phi_deg.to_radians() * 0.5).cos();
16291 let spot = if cosang >= cos_theta {
16292 1.0
16293 } else if cosang <= cos_phi {
16294 0.0
16295 } else {
16296 ((cosang - cos_phi) / (cos_theta - cos_phi).max(1e-6))
16297 .powf(light.falloff.max(0.01))
16298 };
16299 attenuation *= spot;
16300 }
16301 }
16302 globals::LightType::None => {}
16303 }
16304 sprite.light_enabled = !matches!(light.kind, globals::LightType::None);
16305 sprite.light_diffuse = light.diffuse;
16306 sprite.light_ambient = light.ambient;
16307 sprite.light_specular = light.specular;
16308 sprite.light_factor = (ndotl * attenuation).clamp(0.0, 1.0);
16309 sprite.light_kind = light.kind as i32;
16310 sprite.light_pos = [light.pos[0], light.pos[1], light.pos[2], 1.0];
16311 let dir_len = (light.dir[0] * light.dir[0]
16312 + light.dir[1] * light.dir[1]
16313 + light.dir[2] * light.dir[2])
16314 .sqrt()
16315 .max(1e-6);
16316 sprite.light_dir = [
16317 light.dir[0] / dir_len,
16318 light.dir[1] / dir_len,
16319 light.dir[2] / dir_len,
16320 0.0,
16321 ];
16322 sprite.light_atten = [
16323 light.attenuation0,
16324 light.attenuation1,
16325 light.attenuation2,
16326 light.range,
16327 ];
16328 sprite.light_cone = [
16329 (light.theta_deg.to_radians() * 0.5).cos(),
16330 (light.phi_deg.to_radians() * 0.5).cos(),
16331 light.falloff,
16332 if matches!(light.kind, globals::LightType::ShadowMapSpot) {
16333 1.0
16334 } else {
16335 0.0
16336 },
16337 ];
16338 if matches!(light.kind, globals::LightType::ShadowMapSpot) {
16339 sprite.shadow_cast = sprite.mesh_kind != 0;
16340 sprite.shadow_receive = sprite.camera_enabled;
16341 }
16342 }
16343 }
16344
16345 if sprite.mesh_kind != 0 || sprite.camera_enabled {
16346 let mut ids: Vec<i32> = ctx.globals.lights.keys().copied().collect();
16347 if !ctx.globals.lights.contains_key(&0) {
16348 ids.push(0);
16349 }
16350 ids.sort_unstable();
16351 ids.dedup();
16352 for light_id in ids {
16353 let camera_default_light;
16354 let light = if let Some(light) = ctx.globals.lights.get(&light_id) {
16355 light
16356 } else if light_id == 0 {
16357 camera_default_light = siglus_default_camera_light(sprite);
16358 &camera_default_light
16359 } else {
16360 continue;
16361 };
16362 if matches!(light.kind, globals::LightType::None) {
16363 continue;
16364 }
16365 let dir_len = (light.dir[0] * light.dir[0]
16366 + light.dir[1] * light.dir[1]
16367 + light.dir[2] * light.dir[2])
16368 .sqrt()
16369 .max(1e-6);
16370 sprite.mesh_runtime_lights.push(SpriteRuntimeLight {
16371 id: light_id,
16372 kind: light.kind as i32,
16373 diffuse: light.diffuse,
16374 ambient: light.ambient,
16375 specular: light.specular,
16376 pos: [light.pos[0], light.pos[1], light.pos[2], 1.0],
16377 dir: [
16378 light.dir[0] / dir_len,
16379 light.dir[1] / dir_len,
16380 light.dir[2] / dir_len,
16381 0.0,
16382 ],
16383 atten: [
16384 light.attenuation0,
16385 light.attenuation1,
16386 light.attenuation2,
16387 light.range,
16388 ],
16389 cone: [
16390 (light.theta_deg.to_radians() * 0.5).cos(),
16391 (light.phi_deg.to_radians() * 0.5).cos(),
16392 light.falloff,
16393 if matches!(light.kind, globals::LightType::ShadowMapSpot) {
16394 1.0
16395 } else {
16396 0.0
16397 },
16398 ],
16399 });
16400 }
16401 }
16402
16403 if sprite.fog_use && sprite.camera_enabled && ctx.globals.fog_global.enabled {
16404 let fog = &ctx.globals.fog_global;
16405 sprite.fog_enabled = true;
16406 sprite.fog_color = fog.color;
16407 sprite.fog_near = fog.near;
16408 sprite.fog_far = fog.far;
16409 sprite.fog_scroll_x = fog.scroll_x;
16410 sprite.fog_texture_image_id = fog.texture_image_id.clone();
16411 }
16412}
16413
16414fn siglus_default_camera_light(sprite: &Sprite) -> globals::LightState {
16415 let mut light = globals::LightState::directional(0, [0.0, 1.0, 0.0]);
16416 light.pos = sprite.camera_eye;
16417 light.diffuse = [1.0, 1.0, 1.0, 1.0];
16418 light.ambient = [3.0, 3.0, 3.0, 1.0];
16419 light.specular = [0.0, 0.0, 0.0, 1.0];
16420 light
16421}
16422
16423fn render_sprite_visible_for_submit(rs: &RenderSprite) -> bool {
16424 rs.sprite.visible
16425 && sprite_has_render_payload(&rs.sprite)
16426 && rs.sprite.alpha > 0
16427 && rs.sprite.tr > 0
16428}
16429
16430fn sprite_has_render_payload(sprite: &Sprite) -> bool {
16431 sprite.image_id.is_some()
16432 || sprite.emote_render.is_some()
16433 || (sprite.mesh_kind != 0 && sprite.mesh_file_name.is_some())
16434}
16435
16436fn resolve_mask_path(project_dir: &Path, raw: &str) -> Option<PathBuf> {
16437 let norm = raw.replace('\\', "/");
16438 let mut candidates = Vec::new();
16439
16440 if !norm.contains('.') {
16441 for ext in ["png", "bmp", "jpg", "jpeg", "g00"] {
16442 candidates.push(project_dir.join(format!("{}.{}", norm, ext)));
16443 candidates.push(project_dir.join("dat").join(format!("{}.{}", norm, ext)));
16444 candidates.push(project_dir.join("mask").join(format!("{}.{}", norm, ext)));
16445 }
16446 }
16447
16448 candidates.push(project_dir.join(&norm));
16449 candidates.push(project_dir.join("dat").join(&norm));
16450 candidates.push(project_dir.join("mask").join(&norm));
16451
16452 for c in candidates {
16453 if let Some(path) = crate::resource::resolve_game_file(&c).ok().flatten() {
16454 return Some(path);
16455 }
16456 }
16457 None
16458}
16459
16460fn ensure_font_list(syscom: &mut globals::SyscomRuntimeState, project_dir: &Path) {
16461 if !syscom.font_list.is_empty() {
16462 return;
16463 }
16464
16465 let mut seen = HashSet::default();
16466 for dir in [project_dir.join("font"), project_dir.join("fonts")] {
16467 let Some(dir) = crate::resource::resolve_game_path(&dir).ok().flatten() else {
16468 continue;
16469 };
16470 let Ok(entries) = fs::read_dir(dir) else {
16471 continue;
16472 };
16473 for entry in entries.flatten() {
16474 let path = entry.path();
16475 if !path.is_file() {
16476 continue;
16477 }
16478 let ext = path
16479 .extension()
16480 .and_then(|s| s.to_str())
16481 .unwrap_or("")
16482 .to_ascii_lowercase();
16483 if (ext == "ttf" || ext == "otf" || ext == "ttc")
16484 && let Some(name) = path.file_stem().and_then(|s| s.to_str())
16485 && seen.insert(name.to_string())
16486 {
16487 syscom.font_list.push(name.to_string());
16488 }
16489 }
16490 }
16491
16492 for name in embedded_default_font_names() {
16493 if seen.insert((*name).to_string()) {
16494 syscom.font_list.push((*name).to_string());
16495 }
16496 }
16497 syscom.font_list.sort();
16498}
16499
16500#[cfg(test)]
16501mod skip_state_parity_tests {
16502 use super::*;
16503
16504 #[test]
16505 fn read_skip_and_script_trigger_obey_local_skip_disable() {
16506 let mut ctx = CommandContext::new(std::path::PathBuf::from("."));
16507
16508 ctx.globals.syscom.read_skip.onoff = true;
16509 assert!(ctx.runtime_is_skipping());
16510
16511 ctx.globals.script.skip_disable = true;
16512 assert!(!ctx.runtime_is_skipping());
16513
16514 ctx.globals.syscom.read_skip.onoff = false;
16515 ctx.skip_because_skip_trigger = true;
16516 assert!(!ctx.runtime_is_skipping());
16517
16518 ctx.globals.script.skip_disable = false;
16519 assert!(ctx.runtime_is_skipping());
16520 }
16521
16522 #[test]
16523 fn decide_down_that_stops_read_skip_cannot_advance_message_on_release() {
16524 let mut ctx = CommandContext::new(std::path::PathBuf::from("."));
16525 ctx.globals.syscom.read_skip.onoff = true;
16526 ctx.ui.begin_wait_message();
16527 ctx.wait.wait_message_reveal_then_key();
16528 assert!(ctx.wait.finish_message_reveal());
16529 assert!(ctx.wait.message_key_waiting());
16530
16531 ctx.on_mouse_down(input::VmMouseButton::Left);
16532 assert!(!ctx.globals.syscom.read_skip.onoff);
16533 ctx.on_mouse_up(input::VmMouseButton::Left);
16534
16535 assert!(ctx.wait.message_key_waiting());
16538 assert!(ctx.ui.message_waiting());
16539 }
16540
16541 #[test]
16542 fn active_read_skip_releases_message_key_wait_without_another_click() {
16543 let mut ctx = CommandContext::new(std::path::PathBuf::from("."));
16544 ctx.ui.begin_wait_message();
16545 ctx.wait.wait_message_reveal_then_key();
16546 assert!(ctx.wait.finish_message_reveal());
16547 ctx.globals.syscom.read_skip.onoff = true;
16548
16549 let generation = ctx.proc_generation();
16550 assert!(!ctx.wait_poll());
16551 assert!(!ctx.wait.message_key_waiting());
16552 assert!(!ctx.ui.message_waiting());
16553 assert_ne!(ctx.proc_generation(), generation);
16557 assert_eq!(ctx.last_proc_kind(), ProcKind::Disp);
16558 }
16559
16560 #[test]
16561 fn message_skip_without_vsync_wait_always_requests_a_draw() {
16562 let mut ctx = CommandContext::new(std::path::PathBuf::from("."));
16563 ctx.globals.script.wait_display_vsync_off_flag = true;
16564 ctx.globals.syscom.read_skip.onoff = true;
16565 ctx.disp_because_msg_wait_cnt_max = 3000;
16569 ctx.ui.begin_wait_message();
16570 ctx.wait.wait_message_reveal_then_key();
16571 assert!(ctx.wait.finish_message_reveal());
16572
16573 let generation = ctx.proc_generation();
16574 assert!(!ctx.wait_poll());
16575 assert_ne!(ctx.proc_generation(), generation);
16576 assert_eq!(ctx.last_proc_kind(), ProcKind::Disp);
16577 }
16578
16579 #[test]
16580 fn skip_draw_throttle_matches_original_adaptive_bounds() {
16581 let mut ctx = CommandContext::new(std::path::PathBuf::from("."));
16582 ctx.frame_rate_100msec_total = 60;
16583 ctx.disp_because_msg_wait_cnt = 0;
16584 ctx.disp_because_msg_wait_cnt_max = 0;
16585
16586 assert!(ctx.should_draw_after_message_key_wait_advance());
16587 assert_eq!(ctx.disp_because_msg_wait_cnt, 0);
16588 assert_eq!(ctx.disp_because_msg_wait_cnt_max, 31);
16589
16590 ctx.disp_because_msg_wait_cnt_max = 3000;
16591 ctx.disp_because_msg_wait_cnt = 29;
16592 assert!(ctx.should_draw_after_message_key_wait_advance());
16593 assert_eq!(ctx.disp_because_msg_wait_cnt, 0);
16594 assert_eq!(ctx.disp_because_msg_wait_cnt_max, 3000);
16595
16596 ctx.frame_rate_100msec_total = 20;
16597 ctx.disp_because_msg_wait_cnt_max = 50;
16598 assert!(ctx.should_draw_after_message_key_wait_advance());
16599 assert_eq!(ctx.disp_because_msg_wait_cnt_max, 0);
16600 }
16601
16602 #[test]
16603 fn read_skip_button_uses_full_runtime_eligibility() {
16604 let mut syscom = globals::SyscomRuntimeState::default();
16605 syscom.read_skip.enable = true;
16606 syscom.read_skip.exist = true;
16607 let mut button = globals::ObjectButtonState {
16608 sys_type: TNM_SYSCOM_TYPE_READ_SKIP,
16609 ..Default::default()
16610 };
16611
16612 assert!(!syscom_feature_enabled_for_button(&syscom, &button, false));
16613 assert!(syscom_feature_enabled_for_button(&syscom, &button, true));
16614 }
16615
16616 #[test]
16617 fn ctrl_precedes_skip_disable_but_ctrl_shift_is_message_only() {
16618 let mut ctx = CommandContext::new(std::path::PathBuf::from("."));
16619 ctx.globals.script.skip_disable = true;
16620
16621 ctx.input.on_key_down(input::VmKey::Control);
16622 assert!(ctx.runtime_is_skipping());
16623 assert!(ctx.runtime_is_skipping_msg());
16624
16625 ctx.input.on_key_down(input::VmKey::Shift);
16626 assert!(!ctx.runtime_is_skipping());
16627 assert!(ctx.runtime_is_skipping_msg());
16628
16629 ctx.globals.script.ctrl_disable = true;
16630 assert!(!ctx.runtime_is_skipping());
16631 assert!(!ctx.runtime_is_skipping_msg());
16632 }
16633}
16634
16635#[cfg(test)]
16636mod basic_wipe_scene_input_tests {
16637 use super::compose_basic_wipe_scene_inputs;
16638
16639 #[test]
16640 fn cross_fade_inputs_are_complete_front_and_next_scenes() {
16641 let mut under = vec![1, 2];
16642 let mut front = vec![3];
16643 let mut next = vec![4];
16644
16645 compose_basic_wipe_scene_inputs(0, &mut under, &mut front, &mut next);
16646
16647 assert!(under.is_empty());
16648 assert_eq!(front, vec![1, 2, 3]);
16649 assert_eq!(next, vec![1, 2, 4]);
16650 }
16651
16652 #[test]
16653 fn processed_wipes_keep_their_separate_under_input() {
16654 let mut under = vec![1, 2];
16655 let mut front = vec![3];
16656 let mut next = vec![4];
16657
16658 compose_basic_wipe_scene_inputs(50, &mut under, &mut front, &mut next);
16659
16660 assert_eq!(under, vec![1, 2]);
16661 assert_eq!(front, vec![3]);
16662 assert_eq!(next, vec![4]);
16663 }
16664}
16665
16666#[cfg(test)]
16667mod render_tree_fidelity_tests {
16668 use super::{
16669 EffectParam, SiglusRenderNode, WipePartition, apply_effects_to_owner,
16670 classify_wipe_partition, compose_clip_rect, object_alpha_blend_for_render,
16671 sprite_has_render_payload,
16672 };
16673 use crate::layer::{ClipRect, RenderSprite, Sprite};
16674
16675 fn sprite(order: i32, layer: i32, marker: i32) -> RenderSprite {
16676 let mut sprite = Sprite {
16677 x: marker,
16678 ..Default::default()
16679 };
16680
16681 RenderSprite::with_sorter(None, None, order, layer, sprite)
16682 }
16683
16684 #[test]
16685 fn mesh_payload_does_not_require_a_pct_image() {
16686 let mut mesh = Sprite {
16687 mesh_kind: 1,
16688 mesh_file_name: Some("room.x".to_owned()),
16689 ..Default::default()
16690 };
16691
16692 assert!(mesh.image_id.is_none());
16693 assert!(sprite_has_render_payload(&mesh));
16694 }
16695
16696 #[test]
16697 fn mesh_object_forces_opaque_submission_like_restruct_mesh() {
16698 assert!(!object_alpha_blend_for_render(6, true));
16699 assert!(object_alpha_blend_for_render(7, true));
16700 }
16701
16702 #[test]
16703 fn screen_icon_rotation_keeps_screen_coordinates() {
16704 let ctx = super::CommandContext::new(std::path::PathBuf::from("."));
16705 let mut obj = super::globals::ObjectState::default();
16706 obj.init_param_like();
16707 obj.object_type = 2;
16708 obj.set_int_prop(&ctx.ids, ctx.ids.obj_rotate_x, 3600);
16709 let info = super::effective_object_info(&ctx, 1, 0, &obj);
16710 let mut sprite = Sprite::default();
16711 super::configure_sprite_3d(&mut sprite, &info, None, 1920, 1080);
16712 assert!(!sprite.camera_enabled);
16713 let quad = crate::render_math::sprite_quad_points(
16714 &sprite, 950.0, 800.0, 132.0, 132.0, 1920.0, 1080.0,
16715 )
16716 .expect("rotated icon quad");
16717 assert!((quad[0].x - 950.0).abs() < 0.01);
16718 assert!((quad[0].y - 800.0).abs() < 0.01);
16719 assert!((quad[2].x - 1082.0).abs() < 0.01);
16720 assert!((quad[2].y - 932.0).abs() < 0.01);
16721 }
16722
16723 #[test]
16724 fn number_tree_preserves_digit_offsets_and_omits_unused_slots() {
16725 let mut ctx = super::CommandContext::new(std::path::PathBuf::from("."));
16726 let image_id = ctx.images.solid_rgba((255, 255, 255, 255));
16727 let layer_id = ctx.layers.create_layer();
16728 let layer = ctx.layers.layer_mut(layer_id).unwrap();
16729 let sprite_ids: Vec<_> = (0..3)
16730 .map(|_| {
16731 let id = layer.create_sprite();
16732 let sprite = layer.sprite_mut(id).unwrap();
16733 sprite.image_id = Some(image_id.clone());
16734 sprite.visible = false;
16736 id
16737 })
16738 .collect();
16739 let mut obj = super::globals::ObjectState::default();
16740 obj.init_param_like();
16741 obj.used = true;
16742 obj.object_type = 5;
16743 obj.backend = super::globals::ObjectBackend::Number {
16744 layer_id,
16745 sprite_ids,
16746 };
16747 obj.runtime.number_sprite_offsets = vec![Some(0), Some(30), None];
16748 obj.set_int_prop(&ctx.ids, ctx.ids.obj_disp, 1);
16749 obj.set_int_prop(&ctx.ids, ctx.ids.obj_y, 206);
16750 for x in [106, 200] {
16751 obj.set_int_prop(&ctx.ids, ctx.ids.obj_x, x);
16752 let mut nodes = Vec::new();
16753 super::append_object_tree_nodes(
16754 &ctx,
16755 None,
16756 1,
16757 0,
16758 &obj,
16759 None,
16760 true,
16761 0,
16762 0,
16763 None,
16764 &mut nodes,
16765 &mut std::collections::HashSet::default(),
16766 &mut Vec::new(),
16767 );
16768 let mut sprites = Vec::new();
16769 for node in nodes {
16770 node.flatten(&mut sprites);
16771 }
16772 assert_eq!(sprites.len(), 2);
16773 assert_eq!(sprites[0].sprite.x, x as i32);
16774 assert_eq!(sprites[1].sprite.x, x as i32 + 30);
16775 assert_eq!(sprites[1].sprite.y, 206);
16776 }
16777 }
16778
16779 #[test]
16780 fn root_sort_keeps_each_subtree_contiguous() {
16781 let child = SiglusRenderNode::from_single_sprite(sprite(100, 0, 11));
16782 let first = SiglusRenderNode {
16783 sorter_order: 10,
16784 sorter_layer: 0,
16785 sprites: vec![sprite(10, 0, 10)],
16786 sort_children_default: true,
16787 children: vec![child],
16788 };
16789 let second = SiglusRenderNode::from_single_sprite(sprite(20, 0, 20));
16790 let mut roots = vec![second, first];
16791 roots.sort_by(super::siglus_render_node_cmp);
16792
16793 let mut flattened = Vec::new();
16794 for root in roots {
16795 root.flatten(&mut flattened);
16796 }
16797 assert_eq!(
16798 flattened.iter().map(|rs| rs.sprite.x).collect::<Vec<_>>(),
16799 vec![10, 11, 20]
16800 );
16801 }
16802
16803 #[test]
16804 fn default_child_sort_sorts_expanded_root_nodes() {
16805 let parent = SiglusRenderNode {
16806 sorter_order: 0,
16807 sorter_layer: 0,
16808 sprites: vec![sprite(0, 0, 0)],
16809 sort_children_default: true,
16810 children: vec![
16811 SiglusRenderNode::from_single_sprite(sprite(30, 0, 30)),
16812 SiglusRenderNode::from_single_sprite(sprite(10, 0, 10)),
16813 ],
16814 };
16815 let mut flattened = Vec::new();
16816 parent.flatten(&mut flattened);
16817 assert_eq!(
16818 flattened.iter().map(|rs| rs.sprite.x).collect::<Vec<_>>(),
16819 vec![0, 10, 30]
16820 );
16821 }
16822
16823 #[test]
16824 fn none_child_sort_preserves_insertion_order() {
16825 let parent = SiglusRenderNode {
16826 sorter_order: 0,
16827 sorter_layer: 0,
16828 sprites: vec![sprite(0, 0, 0)],
16829 sort_children_default: false,
16830 children: vec![
16831 SiglusRenderNode::from_single_sprite(sprite(30, 0, 30)),
16832 SiglusRenderNode::from_single_sprite(sprite(10, 0, 10)),
16833 ],
16834 };
16835 let mut flattened = Vec::new();
16836 parent.flatten(&mut flattened);
16837 assert_eq!(
16838 flattened.iter().map(|rs| rs.sprite.x).collect::<Vec<_>>(),
16839 vec![0, 30, 10]
16840 );
16841 }
16842
16843 #[test]
16844 fn parent_trp_clip_composition_uses_original_union_rule() {
16845 let parent = ClipRect {
16846 left: 20,
16847 top: 30,
16848 right: 80,
16849 bottom: 90,
16850 };
16851 let child = ClipRect {
16852 left: 10,
16853 top: 40,
16854 right: 70,
16855 bottom: 100,
16856 };
16857 assert_eq!(
16858 compose_clip_rect(Some(parent), Some(child)),
16859 Some(ClipRect {
16860 left: 10,
16861 top: 30,
16862 right: 80,
16863 bottom: 100,
16864 })
16865 );
16866 }
16867
16868 #[test]
16869 fn stage_effects_do_not_cross_stage_form_owners() {
16870 let mut normal = sprite(0, 0, 0);
16871 normal.set_stage_form_owner(49);
16872 let mut excall = sprite(0, 0, 0);
16873 excall.set_stage_form_owner(65);
16874 let mut sprites = vec![normal, excall];
16875 let effect = EffectParam {
16876 x: 12,
16877 y: 0,
16878 z: 0,
16879 mono: 0,
16880 reverse: 0,
16881 bright: 0,
16882 dark: 0,
16883 color_r: 0,
16884 color_g: 0,
16885 color_b: 0,
16886 color_rate: 0,
16887 color_add_r: 0,
16888 color_add_g: 0,
16889 color_add_b: 0,
16890 begin_order: i32::MIN,
16891 begin_layer: i32::MIN,
16892 end_order: i32::MAX,
16893 end_layer: i32::MAX,
16894 };
16895 apply_effects_to_owner(&mut sprites, 49, &[effect]);
16896 assert_eq!(sprites[0].sprite.x, 12);
16897 assert_eq!(sprites[1].sprite.x, 0);
16898 }
16899
16900 #[test]
16901 fn wipe_range_uses_top_level_entity_sorter() {
16902 let mut child = sprite(900, 900, 0);
16903 child.set_wipe_sorter(10, 5);
16904 assert_eq!(
16905 classify_wipe_partition(&child, 0, 10, 0, 20, false),
16906 WipePartition::Target
16907 );
16908 }
16909}
16910
16911#[cfg(test)]
16912mod scene_metadata_cache_tests {
16913 use super::*;
16914
16915 #[test]
16916 fn active_append_change_invalidates_resident_scene_metadata() {
16917 let mut ctx =
16918 CommandContext::new(std::env::temp_dir().join("siglus-scene-metadata-cache-test"));
16919 ctx.set_active_append("append_a".to_string(), "A".to_string());
16920 *ctx.scene_metadata.get_mut() = Some((
16921 "append_a".to_string(),
16922 Arc::new(SceneMetadata::from_rows(vec![("scene_a".to_string(), 3)])),
16923 ));
16924 assert!(ctx.scene_metadata.get_mut().is_some());
16925
16926 ctx.set_active_append("append_b".to_string(), "B".to_string());
16927 assert!(ctx.scene_metadata.get_mut().is_none());
16928 }
16929}
16930
16931#[cfg(test)]
16932mod movie_menu_wait_tests {
16933 use super::*;
16934 use input::{VmKey, VmMouseButton};
16935
16936 #[test]
16937 fn menu_group_ignores_background_clicks_and_skip_until_a_decision() {
16938 let mut ctx = CommandContext::new(PathBuf::from("."));
16939 let form = ctx.ids.form_global_stage;
16940 let stage = ctx.globals.stage_forms.entry(form).or_default();
16941 stage.ensure_group_list(1, 1);
16942 let group = &mut stage.group_lists.get_mut(&1).unwrap()[0];
16943 group.start();
16944 group.wait_flag = true;
16945 ctx.globals.focused_stage_group = Some((form, 1, 0));
16946 ctx.wait.wait_group_selection(form, 1, 0);
16947 assert!(ctx.wait_poll());
16948
16949 ctx.on_mouse_move(900, 100);
16950 ctx.on_mouse_down(VmMouseButton::Left);
16951 ctx.input.next_frame();
16952 ctx.on_mouse_up(VmMouseButton::Left);
16953 assert!(
16954 ctx.wait_poll(),
16955 "a background click must not choose New Game"
16956 );
16957 assert!(ctx.stack.is_empty());
16958 ctx.on_key_down(VmKey::Control);
16959 assert!(ctx.wait_poll(), "Ctrl skip must not accept a menu choice");
16960 ctx.on_key_up(VmKey::Control);
16961 ctx.globals.syscom.read_skip.onoff = true;
16962 ctx.globals.syscom.auto_mode.onoff = true;
16963 assert!(ctx.wait_poll());
16964 ctx.globals.syscom.read_skip.onoff = false;
16965
16966 ctx.globals
16967 .stage_forms
16968 .get_mut(&form)
16969 .unwrap()
16970 .group_lists
16971 .get_mut(&1)
16972 .unwrap()[0]
16973 .hit_button_no = 5;
16974 ctx.on_key_down(VmKey::Enter);
16975 assert!(
16976 !ctx.wait_poll(),
16977 "a real decision must release the selection wait"
16978 );
16979 assert_eq!(ctx.stack.pop().and_then(|v| v.as_i64()), Some(5));
16980 }
16981
16982 #[test]
16983 fn movie_natural_finish_returns_zero_without_an_extra_click() {
16984 let mut ctx = CommandContext::new(PathBuf::from("."));
16985 ctx.globals.mov.playing = true;
16986 ctx.wait.wait_global_movie(true, true);
16987 assert!(ctx.wait_poll());
16988 ctx.globals.mov.playing = false;
16989 assert!(!ctx.wait_poll());
16990 assert_eq!(ctx.stack.pop().and_then(|v| v.as_i64()), Some(0));
16991 assert!(!ctx.wait_poll());
16992 assert!(ctx.stack.is_empty(), "completion must return exactly once");
16993 }
16994
16995 #[test]
16996 fn movie_click_skip_obeys_script_flag_and_requires_down_up() {
16997 for key_skip in [false, true] {
16998 let mut ctx = CommandContext::new(PathBuf::from("."));
16999 ctx.globals.mov.playing = true;
17000 ctx.wait.wait_global_movie(key_skip, key_skip);
17001 ctx.on_mouse_up(VmMouseButton::Left);
17002 assert!(ctx.wait_poll(), "a release alone must not skip the OP");
17003 ctx.on_mouse_down(VmMouseButton::Left);
17004 ctx.input.next_frame();
17005 assert!(ctx.wait_poll());
17006 ctx.on_mouse_up(VmMouseButton::Left);
17007 assert_eq!(ctx.wait_poll(), !key_skip);
17008 assert_eq!(ctx.globals.mov.playing, !key_skip);
17009 assert_eq!(
17010 ctx.stack.pop().and_then(|v| v.as_i64()),
17011 key_skip.then_some(1)
17012 );
17013 }
17014 }
17015
17016 #[test]
17017 fn object_movie_natural_finish_does_not_leave_an_input_wait() {
17018 let mut ctx = CommandContext::new(PathBuf::from("."));
17019 let form = ctx.ids.form_global_stage;
17020 ctx.wait.wait_object_movie(form, 1, 0, true, true);
17022 assert!(!ctx.wait_poll());
17023 assert_eq!(ctx.stack.pop().and_then(|v| v.as_i64()), Some(0));
17024 }
17025}
17026
17027#[cfg(test)]
17028mod msg_back_voice_tests {
17029 use super::*;
17030
17031 fn backlog_with_button(project: PathBuf, voices: &[(i64, i64)]) -> CommandContext {
17032 let mut ctx = CommandContext::new(project);
17033 let mut history = globals::MsgBackState::default();
17034 for &(voice, character) in voices {
17035 history.add_koe(voice, character, 0, 0);
17036 }
17037 history.add_msg("Backlog voice", "", 0, 0);
17038 ctx.globals
17039 .msgbk_forms
17040 .insert(ctx.ids.form_global_msgbk, history);
17041 ctx.globals.syscom.msg_back_open = true;
17042 let mut projection = ctx.build_msg_back_projection().unwrap();
17043 projection.window_x = 100;
17044 projection.window_y = 50;
17045 projection.window_w = 320;
17046 projection.window_h = 200;
17047 projection.disp_margin = (10, 10, 10, 10);
17048 projection.koe_buttons = vec![ui::MsgBackEntryButtonProjection {
17049 history_index: 0,
17050 file: None,
17051 x: 30,
17052 y: 5,
17053 }];
17054 ctx.ui.set_msg_back_projection(Some(projection));
17055 let image = ctx.images.solid_rgba((255, 255, 255, 255));
17056 ctx.ui.msg_back.koe_buttons = vec![ui::MsgBackButtonRuntime {
17057 image: Some(image),
17058 size: Some((20, 20)),
17059 center: Some((4, 3)),
17060 ..Default::default()
17061 }];
17062 ctx
17063 }
17064
17065 #[test]
17066 fn backlog_voice_click_cycles_clips_and_preserves_message_voice() {
17067 let mut ctx = backlog_with_button(
17068 std::env::temp_dir().join("siglus-backlog-click-test"),
17069 &[(10, 2), (11, 3)],
17070 );
17071 ctx.globals.script.cur_koe_no = 99;
17072 ctx.globals.script.cur_chr_no = 7;
17073 ctx.globals.syscom.replay_koe = Some((99, 7));
17074 assert_eq!(
17075 ctx.ui.msg_back_hit_action(128, 55),
17076 None,
17077 "clipped part of icon"
17078 );
17079 assert_eq!(
17080 ctx.ui.msg_back_hit_action(128, 62),
17081 Some(ui::MsgBackHitAction::ReplayKoe(0))
17082 );
17083 for (voice, character, next) in [(10, 2, 1), (11, 3, 2), (10, 2, 1)] {
17084 ctx.on_mouse_move(128, 62);
17085 ctx.on_mouse_down(input::VmMouseButton::Left);
17086 ctx.on_mouse_up(input::VmMouseButton::Left);
17087 assert!(
17088 ctx.koe.is_playing_any(),
17089 "click must request voice decoding"
17090 );
17091 assert_eq!(ctx.globals.sound_routing.koe_chara_no, character);
17092 assert!(ctx.globals.sound_routing.koe_ex_flag);
17093 assert_eq!(
17094 ctx.globals.int_props[&(constants::fm::GLOBAL as u32)]
17095 [&constants::elm_value::GLOBAL_KOE_CHECK_GET_KOE_NO],
17096 voice
17097 );
17098 assert_eq!(ctx.msg_back_state().unwrap().history[0].koe_play_no, next);
17099 assert_eq!(ctx.globals.script.cur_koe_no, 99);
17100 assert_eq!(ctx.globals.script.cur_chr_no, 7);
17101 assert_eq!(ctx.globals.syscom.replay_koe, Some((99, 7)));
17102 assert!(!ctx.globals.syscom.msg_back_content_dragging);
17103 }
17104 ctx.ui
17106 .msg_back
17107 .projection
17108 .as_mut()
17109 .unwrap()
17110 .koe_buttons
17111 .clear();
17112 assert_eq!(ctx.ui.msg_back_hit_action(128, 62), None);
17113 }
17114
17115 #[test]
17116 #[ignore = "requires audio output, SIGLUS_BACKLOG_TEST_GAME and SIGLUS_BACKLOG_TEST_KOE"]
17117 fn backlog_voice_click_reaches_audio_output() {
17118 let project = PathBuf::from(std::env::var("SIGLUS_BACKLOG_TEST_GAME").unwrap());
17119 let voice: i64 = std::env::var("SIGLUS_BACKLOG_TEST_KOE")
17120 .unwrap()
17121 .parse()
17122 .unwrap();
17123 let mut ctx = backlog_with_button(project, &[(voice, 2)]);
17124 ctx.on_mouse_move(128, 62);
17125 ctx.on_mouse_down(input::VmMouseButton::Left);
17126 ctx.on_mouse_up(input::VmMouseButton::Left);
17127 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
17128 while ctx.koe.current_koe_no() != voice && std::time::Instant::now() < deadline {
17129 ctx.koe.tick(&mut ctx.audio);
17130 std::thread::sleep(std::time::Duration::from_millis(10));
17131 }
17132 assert_eq!(
17133 ctx.koe.current_koe_no(),
17134 voice,
17135 "voice must decode and start"
17136 );
17137 assert!(
17138 ctx.audio.is_enabled(),
17139 "requires a working audio output device"
17140 );
17141 assert!(ctx.koe.is_playing_any());
17142 std::thread::sleep(std::time::Duration::from_millis(150));
17143 assert!(
17144 ctx.koe.current_play_pos_ms() > 0,
17145 "audio playback must advance"
17146 );
17147 }
17148}
17149
17150#[cfg(test)]
17151mod selbtn_continuous_frame_tests {
17152 use super::*;
17153 use std::path::PathBuf;
17154
17155 #[test]
17156 fn selector_animation_keeps_redraws_alive_during_selbtn_wait() {
17157 let mut ctx = CommandContext::new(PathBuf::from("."));
17158
17159 ctx.globals.selbtn.processing_flag_0 = true;
17162 ctx.globals.selbtn.sync_type = 0;
17163 ctx.wait.wait_selbtn();
17164 assert!(!ctx.wait.needs_continuous_frame());
17165 assert!(!ctx.needs_continuous_frame());
17166
17167 ctx.globals.selbtn.started = true;
17171 ctx.globals.selbtn.appear_flag = true;
17172 ctx.globals.selbtn.open_anime_type = 6;
17173 assert!(ctx.needs_continuous_frame());
17174
17175 ctx.globals.selbtn.open_anime_type = 0;
17177 assert!(!ctx.needs_continuous_frame());
17178
17179 ctx.globals.selbtn.started = false;
17182 ctx.globals.selbtn.decide_anime_type = 2;
17183 assert!(ctx.needs_continuous_frame());
17184 ctx.globals.selbtn.decide_anime_type = 0;
17185 ctx.globals.selbtn.close_anime_type = 6;
17186 assert!(ctx.needs_continuous_frame());
17187 ctx.globals.selbtn.close_anime_type = 0;
17188
17189 ctx.globals.selbtn.capture_now_flag = true;
17192 assert!(ctx.needs_continuous_frame());
17193 ctx.globals.selbtn.capture_now_flag = false;
17194 assert!(!ctx.needs_continuous_frame());
17195 }
17196}
17197
17198#[cfg(test)]
17199mod emote_continuous_frame_tests {
17200 use super::*;
17201
17202 #[test]
17203 #[ignore = "requires SIGLUS_EMOTE_TEST_PROJECT pointing to 神待少女纱波"]
17204 fn live_emote_keeps_idle_dialogue_and_child_objects_ticking() {
17205 let project = PathBuf::from(std::env::var_os("SIGLUS_EMOTE_TEST_PROJECT").unwrap());
17206 let sources = [
17207 "bup_sn01_03制服+エプロン.psb",
17208 "bup_sn01_頭部.psb",
17209 "bup_共通tl.psb",
17210 ]
17211 .map(|name| std::fs::read(project.join("dat").join(name)).unwrap());
17212 let key = siglus_assets::key_toml::load_emote_key_from_project_dir(&project).unwrap();
17213 let runtime = crate::emote::SiglusEmoteRuntime::from_psb_sources(
17214 &sources.iter().map(Vec::as_slice).collect::<Vec<_>>(),
17215 key,
17216 )
17217 .unwrap();
17218 let mut ctx = CommandContext::new(project);
17220 let form = ctx.ids.form_global_stage;
17221 assert!(!ctx.needs_continuous_frame());
17222 let mut obj = globals::ObjectState::default();
17223 obj.used = true;
17224 obj.object_type = 12;
17225 obj.emote.runtime = Some(runtime);
17226 ctx.globals
17227 .stage_forms
17228 .entry(form)
17229 .or_default()
17230 .object_lists
17231 .insert(1, vec![obj]);
17232 assert!(ctx.needs_continuous_frame());
17233
17234 let child = ctx
17235 .globals
17236 .stage_forms
17237 .get_mut(&form)
17238 .unwrap()
17239 .object_lists
17240 .get_mut(&1)
17241 .unwrap()
17242 .pop()
17243 .unwrap();
17244 let mut parent = globals::ObjectState::default();
17245 parent.used = true;
17246 parent.runtime.child_objects.push(child);
17247 ctx.globals
17248 .stage_forms
17249 .get_mut(&form)
17250 .unwrap()
17251 .object_lists
17252 .insert(1, vec![parent]);
17253 assert!(ctx.needs_continuous_frame());
17254 ctx.globals
17255 .stage_forms
17256 .get_mut(&form)
17257 .unwrap()
17258 .object_lists
17259 .get_mut(&1)
17260 .unwrap()[0]
17261 .runtime
17262 .child_objects[0]
17263 .emote
17264 .clear_runtime();
17265 assert!(!ctx.needs_continuous_frame());
17266 }
17267}