1use anyhow::{Result, bail};
2
3use crate::runtime::forms::codes::{ELM_ARRAY, FM_OBJECTEVENT, FM_OBJECTEVENTLIST, elm_value};
4use crate::runtime::globals::{ObjectEventTarget, ObjectState, StageFormState};
5use crate::runtime::{CommandContext, Value};
6
7use super::prop_access;
8
9fn as_i64(v: &Value) -> Option<i64> {
10 v.as_i64()
11}
12
13fn default_push(ctx: &mut CommandContext) {
14 ctx.push(Value::Int(0));
15}
16
17fn anim_skip_trace_enabled() -> bool {
18 env_is_set!("SG_DEBUG")
19}
20
21fn anim_skip_trace(ctx: &CommandContext, msg: impl AsRef<str>) {
22 if anim_skip_trace_enabled() {
23 let scene = ctx.current_scene_name.as_deref().unwrap_or("<none>");
24 let scene_no = ctx
25 .current_scene_no
26 .map(|v| v.to_string())
27 .unwrap_or_else(|| "-".to_string());
28 eprintln!(
29 "[SG_DEBUG][ANIM_SKIP_TRACE][OBJECTEVENT] scene={} scene_no={} line={} {}",
30 scene,
31 scene_no,
32 ctx.current_line_no,
33 msg.as_ref()
34 );
35 }
36}
37
38fn int_event_state(ev: &crate::runtime::int_event::IntEvent) -> String {
39 format!(
40 "value={} cur={} start={} end={} cur_time={} end_time={} delay={} loop_type={} speed={} real={} active={}",
41 ev.value,
42 ev.cur_value,
43 ev.start_value,
44 ev.end_value,
45 ev.cur_time,
46 ev.end_time,
47 ev.delay_time,
48 ev.loop_type,
49 ev.speed_type,
50 ev.real_flag,
51 ev.check_event()
52 )
53}
54
55fn parse_chain<'a>(ctx: &'a CommandContext, args: &'a [Value]) -> Option<(usize, &'a [i32])> {
56 prop_access::parse_element_chain_ctx(ctx, FM_OBJECTEVENT as u32, args)
57}
58
59fn object_runtime_slot(idx: usize, obj: &ObjectState) -> usize {
60 obj.runtime_slot_or(idx)
61}
62
63fn find_object_by_runtime_slot(
64 objects: &[ObjectState],
65 runtime_slot: usize,
66) -> Option<&ObjectState> {
67 for (idx, obj) in objects.iter().enumerate() {
68 if object_runtime_slot(idx, obj) == runtime_slot {
69 return Some(obj);
70 }
71 if let Some(found) = find_object_by_runtime_slot(&obj.runtime.child_objects, runtime_slot) {
72 return Some(found);
73 }
74 }
75 None
76}
77
78fn find_object_by_runtime_slot_mut(
79 mut objects: &mut [ObjectState],
80 runtime_slot: usize,
81) -> Option<&mut ObjectState> {
82 let mut idx = 0usize;
83 while let Some((obj, tail)) = objects.split_first_mut() {
84 if object_runtime_slot(idx, obj) == runtime_slot {
85 return Some(obj);
86 }
87 if let Some(found) =
88 find_object_by_runtime_slot_mut(&mut obj.runtime.child_objects, runtime_slot)
89 {
90 return Some(found);
91 }
92 objects = tail;
93 idx += 1;
94 }
95 None
96}
97
98fn object_by_runtime_slot(
99 st: &StageFormState,
100 stage_idx: i64,
101 runtime_slot: usize,
102) -> Option<&ObjectState> {
103 if let Some(obj) = st
104 .object_lists
105 .get(&stage_idx)
106 .and_then(|list| find_object_by_runtime_slot(list, runtime_slot))
107 {
108 return Some(obj);
109 }
110
111 if let Some(mwnds) = st.mwnd_lists.get(&stage_idx) {
112 for mwnd in mwnds {
113 if let Some(obj) = find_object_by_runtime_slot(&mwnd.button_list, runtime_slot) {
114 return Some(obj);
115 }
116 if let Some(obj) = find_object_by_runtime_slot(&mwnd.face_list, runtime_slot) {
117 return Some(obj);
118 }
119 if let Some(obj) = find_object_by_runtime_slot(&mwnd.object_list, runtime_slot) {
120 return Some(obj);
121 }
122 }
123 }
124
125 if let Some(items) = st.btnselitem_lists.get(&stage_idx) {
126 for item in items {
127 if let Some(obj) = find_object_by_runtime_slot(&item.object_list, runtime_slot) {
128 return Some(obj);
129 }
130 }
131 }
132
133 None
134}
135
136fn object_by_runtime_slot_mut(
137 st: &mut StageFormState,
138 stage_idx: i64,
139 runtime_slot: usize,
140) -> Option<&mut ObjectState> {
141 if let Some(list) = st.object_lists.get_mut(&stage_idx)
142 && let Some(obj) = find_object_by_runtime_slot_mut(list, runtime_slot)
143 {
144 return Some(obj);
145 }
146
147 if let Some(mwnds) = st.mwnd_lists.get_mut(&stage_idx) {
148 for mwnd in mwnds {
149 if let Some(obj) = find_object_by_runtime_slot_mut(&mut mwnd.button_list, runtime_slot)
150 {
151 return Some(obj);
152 }
153 if let Some(obj) = find_object_by_runtime_slot_mut(&mut mwnd.face_list, runtime_slot) {
154 return Some(obj);
155 }
156 if let Some(obj) = find_object_by_runtime_slot_mut(&mut mwnd.object_list, runtime_slot)
157 {
158 return Some(obj);
159 }
160 }
161 }
162
163 if let Some(items) = st.btnselitem_lists.get_mut(&stage_idx) {
164 for item in items {
165 if let Some(obj) = find_object_by_runtime_slot_mut(&mut item.object_list, runtime_slot)
166 {
167 return Some(obj);
168 }
169 }
170 }
171
172 None
173}
174
175fn target_for_set_loop_turn_stop_wait(op: i32) -> Option<ObjectEventTarget> {
176 match op {
177 elm_value::OBJECTEVENT_SET_X
178 | elm_value::OBJECTEVENT_LOOP_X
179 | elm_value::OBJECTEVENT_TURN_X
180 | elm_value::OBJECTEVENT_STOP_X
181 | elm_value::OBJECTEVENT_WAIT_X => Some(ObjectEventTarget::X),
182 elm_value::OBJECTEVENT_SET_Y
183 | elm_value::OBJECTEVENT_LOOP_Y
184 | elm_value::OBJECTEVENT_TURN_Y
185 | elm_value::OBJECTEVENT_STOP_Y
186 | elm_value::OBJECTEVENT_WAIT_Y => Some(ObjectEventTarget::Y),
187 elm_value::OBJECTEVENT_SET_Z
188 | elm_value::OBJECTEVENT_LOOP_Z
189 | elm_value::OBJECTEVENT_TURN_Z
190 | elm_value::OBJECTEVENT_STOP_Z
191 | elm_value::OBJECTEVENT_WAIT_Z => Some(ObjectEventTarget::Z),
192 elm_value::OBJECTEVENT_SET_SCALE_X
193 | elm_value::OBJECTEVENT_STOP_SCALE_X
194 | elm_value::OBJECTEVENT_WAIT_SCALE_X => Some(ObjectEventTarget::ScaleX),
195 elm_value::OBJECTEVENT_SET_SCALE_Y
196 | elm_value::OBJECTEVENT_STOP_SCALE_Y
197 | elm_value::OBJECTEVENT_WAIT_SCALE_Y => Some(ObjectEventTarget::ScaleY),
198 elm_value::OBJECTEVENT_SET_SCALE_Z
199 | elm_value::OBJECTEVENT_STOP_SCALE_Z
200 | elm_value::OBJECTEVENT_WAIT_SCALE_Z => Some(ObjectEventTarget::ScaleZ),
201 elm_value::OBJECTEVENT_SET_ROTATE_X
202 | elm_value::OBJECTEVENT_STOP_ROTATE_X
203 | elm_value::OBJECTEVENT_WAIT_ROTATE_X => Some(ObjectEventTarget::RotateX),
204 elm_value::OBJECTEVENT_SET_ROTATE_Y
205 | elm_value::OBJECTEVENT_STOP_ROTATE_Y
206 | elm_value::OBJECTEVENT_WAIT_ROTATE_Y => Some(ObjectEventTarget::RotateY),
207 elm_value::OBJECTEVENT_SET_ROTATE_Z
208 | elm_value::OBJECTEVENT_STOP_ROTATE_Z
209 | elm_value::OBJECTEVENT_WAIT_ROTATE_Z => Some(ObjectEventTarget::RotateZ),
210 elm_value::OBJECTEVENT_SET_TR
211 | elm_value::OBJECTEVENT_LOOP_TR
212 | elm_value::OBJECTEVENT_TURN_TR
213 | elm_value::OBJECTEVENT_STOP_TR
214 | elm_value::OBJECTEVENT_WAIT_TR => Some(ObjectEventTarget::Tr),
215 _ => None,
216 }
217}
218
219fn event_prop_for_target(ctx: &CommandContext, target: ObjectEventTarget) -> i32 {
220 match target {
221 ObjectEventTarget::X => ctx.ids.obj_x_eve,
222 ObjectEventTarget::Y => ctx.ids.obj_y_eve,
223 ObjectEventTarget::Z => ctx.ids.obj_z_eve,
224 ObjectEventTarget::ScaleX => ctx.ids.obj_scale_x_eve,
225 ObjectEventTarget::ScaleY => ctx.ids.obj_scale_y_eve,
226 ObjectEventTarget::ScaleZ => ctx.ids.obj_scale_z_eve,
227 ObjectEventTarget::RotateX => ctx.ids.obj_rotate_x_eve,
228 ObjectEventTarget::RotateY => ctx.ids.obj_rotate_y_eve,
229 ObjectEventTarget::RotateZ => ctx.ids.obj_rotate_z_eve,
230 ObjectEventTarget::Tr => ctx.ids.obj_tr_eve,
231 _ => 0,
232 }
233}
234
235fn is_set_op(op: i32) -> bool {
236 matches!(
237 op,
238 elm_value::OBJECTEVENT_SET_X
239 | elm_value::OBJECTEVENT_SET_Y
240 | elm_value::OBJECTEVENT_SET_Z
241 | elm_value::OBJECTEVENT_SET_SCALE_X
242 | elm_value::OBJECTEVENT_SET_SCALE_Y
243 | elm_value::OBJECTEVENT_SET_SCALE_Z
244 | elm_value::OBJECTEVENT_SET_ROTATE_X
245 | elm_value::OBJECTEVENT_SET_ROTATE_Y
246 | elm_value::OBJECTEVENT_SET_ROTATE_Z
247 | elm_value::OBJECTEVENT_SET_TR
248 )
249}
250
251fn is_loop_op(op: i32) -> bool {
252 matches!(
253 op,
254 elm_value::OBJECTEVENT_LOOP_X
255 | elm_value::OBJECTEVENT_LOOP_Y
256 | elm_value::OBJECTEVENT_LOOP_Z
257 | elm_value::OBJECTEVENT_LOOP_TR
258 )
259}
260
261fn is_turn_op(op: i32) -> bool {
262 matches!(
263 op,
264 elm_value::OBJECTEVENT_TURN_X
265 | elm_value::OBJECTEVENT_TURN_Y
266 | elm_value::OBJECTEVENT_TURN_Z
267 | elm_value::OBJECTEVENT_TURN_TR
268 )
269}
270
271fn is_stop_op(op: i32) -> bool {
272 matches!(
273 op,
274 elm_value::OBJECTEVENT_STOP_X
275 | elm_value::OBJECTEVENT_STOP_Y
276 | elm_value::OBJECTEVENT_STOP_Z
277 | elm_value::OBJECTEVENT_STOP_SCALE_X
278 | elm_value::OBJECTEVENT_STOP_SCALE_Y
279 | elm_value::OBJECTEVENT_STOP_SCALE_Z
280 | elm_value::OBJECTEVENT_STOP_ROTATE_X
281 | elm_value::OBJECTEVENT_STOP_ROTATE_Y
282 | elm_value::OBJECTEVENT_STOP_ROTATE_Z
283 | elm_value::OBJECTEVENT_STOP_TR
284 )
285}
286
287fn is_wait_op(op: i32) -> bool {
288 matches!(
289 op,
290 elm_value::OBJECTEVENT_WAIT_X
291 | elm_value::OBJECTEVENT_WAIT_Y
292 | elm_value::OBJECTEVENT_WAIT_Z
293 | elm_value::OBJECTEVENT_WAIT_SCALE_X
294 | elm_value::OBJECTEVENT_WAIT_SCALE_Y
295 | elm_value::OBJECTEVENT_WAIT_SCALE_Z
296 | elm_value::OBJECTEVENT_WAIT_ROTATE_X
297 | elm_value::OBJECTEVENT_WAIT_ROTATE_Y
298 | elm_value::OBJECTEVENT_WAIT_ROTATE_Z
299 | elm_value::OBJECTEVENT_WAIT_TR
300 )
301}
302
303fn dispatch_object_event_on_runtime_slot(
304 ctx: &mut CommandContext,
305 stage_idx: i64,
306 runtime_slot: usize,
307 op: i32,
308 script_args: &[Value],
309) -> Result<bool> {
310 if op == elm_value::OBJECTEVENT_WAIT_ALL {
311 let active = {
312 let stage_form = ctx.ids.form_global_stage;
313 ctx.globals
314 .stage_forms
315 .get(&stage_form)
316 .and_then(|st| object_by_runtime_slot(st, stage_idx, runtime_slot))
317 .map(|o| o.any_event_active())
318 .unwrap_or(false)
319 };
320 anim_skip_trace(
321 ctx,
322 format!(
323 "OBJECTEVENT.WAIT_ALL stage={} slot={} active={}",
324 stage_idx, runtime_slot, active
325 ),
326 );
327 if active {
328 ctx.wait.wait_object_all_events(
329 ctx.ids.form_global_stage,
330 stage_idx,
331 runtime_slot,
332 false,
333 );
334 }
335 default_push(ctx);
336 return Ok(true);
337 }
338
339 if op == elm_value::OBJECTEVENT_STOP_ALL {
340 anim_skip_trace(
341 ctx,
342 format!(
343 "OBJECTEVENT.STOP_ALL stage={} slot={}",
344 stage_idx, runtime_slot
345 ),
346 );
347 let stage_form = ctx.ids.form_global_stage;
348 if let Some(st) = ctx.globals.stage_forms.get_mut(&stage_form)
349 && let Some(obj) = object_by_runtime_slot_mut(st, stage_idx, runtime_slot)
350 {
351 obj.end_all_events();
352 }
353 default_push(ctx);
354 return Ok(true);
355 }
356
357 let Some(target) = target_for_set_loop_turn_stop_wait(op) else {
358 bail!("unsupported OBJECTEVENT op {}", op);
359 };
360 let event_prop = event_prop_for_target(ctx, target);
361 if event_prop == 0 {
362 bail!("OBJECTEVENT op {} has no mapped object event property", op);
363 }
364
365 if is_wait_op(op) {
366 let active = {
367 let stage_form = ctx.ids.form_global_stage;
368 ctx.globals
369 .stage_forms
370 .get(&stage_form)
371 .and_then(|st| object_by_runtime_slot(st, stage_idx, runtime_slot))
372 .and_then(|obj| obj.runtime.prop_events.get(target))
373 .map(|ev| ev.check_event())
374 .unwrap_or(false)
375 };
376 anim_skip_trace(
377 ctx,
378 format!(
379 "OBJECTEVENT.WAIT target={:?} stage={} slot={} op={} event_prop={} active={}",
380 target, stage_idx, runtime_slot, op, event_prop, active
381 ),
382 );
383 if active {
384 ctx.wait.wait_object_event(
385 ctx.ids.form_global_stage,
386 stage_idx,
387 runtime_slot,
388 event_prop,
389 false,
390 false,
391 );
392 }
393 default_push(ctx);
394 return Ok(true);
395 }
396
397 let stage_form = ctx.ids.form_global_stage;
398 let st: &mut StageFormState = ctx.globals.stage_forms.entry(stage_form).or_default();
399 let Some(obj) = object_by_runtime_slot_mut(st, stage_idx, runtime_slot) else {
400 return Ok(false);
401 };
402 let Some(ev) = obj.runtime.prop_events.get_mut(target) else {
403 bail!(
404 "OBJECTEVENT target {:?} is not backed by an object IntEvent",
405 target
406 );
407 };
408
409 if is_set_op(op) {
410 let value = script_args.first().and_then(as_i64).unwrap_or(0) as i32;
411 let total_time = script_args.get(1).and_then(as_i64).unwrap_or(0) as i32;
412 let delay_time = script_args.get(2).and_then(as_i64).unwrap_or(0) as i32;
413 let speed_type = script_args.get(3).and_then(as_i64).unwrap_or(0) as i32;
414 ev.set_event(value, total_time, delay_time, speed_type, 0);
415 if anim_skip_trace_enabled() {
416 eprintln!(
417 "[SG_DEBUG][ANIM_SKIP_TRACE][OBJECTEVENT] OBJECTEVENT.SET target={:?} stage={} slot={} value={} total_time={} delay={} speed={} state=[{}]",
418 target,
419 stage_idx,
420 runtime_slot,
421 value,
422 total_time,
423 delay_time,
424 speed_type,
425 int_event_state(ev)
426 );
427 }
428 default_push(ctx);
429 return Ok(true);
430 }
431
432 if is_loop_op(op) {
433 let start_value = script_args.first().and_then(as_i64).unwrap_or(0) as i32;
434 let end_value = script_args.get(1).and_then(as_i64).unwrap_or(0) as i32;
435 let loop_time = script_args.get(2).and_then(as_i64).unwrap_or(0) as i32;
436 let delay_time = script_args.get(3).and_then(as_i64).unwrap_or(0) as i32;
437 ev.loop_event(start_value, end_value, loop_time, delay_time, 0, 0);
438 if anim_skip_trace_enabled() {
439 eprintln!(
440 "[SG_DEBUG][ANIM_SKIP_TRACE][OBJECTEVENT] OBJECTEVENT.LOOP target={:?} stage={} slot={} start={} end={} loop_time={} delay={} state=[{}]",
441 target,
442 stage_idx,
443 runtime_slot,
444 start_value,
445 end_value,
446 loop_time,
447 delay_time,
448 int_event_state(ev)
449 );
450 }
451 default_push(ctx);
452 return Ok(true);
453 }
454
455 if is_turn_op(op) {
456 let start_value = script_args.first().and_then(as_i64).unwrap_or(0) as i32;
457 let end_value = script_args.get(1).and_then(as_i64).unwrap_or(0) as i32;
458 let loop_time = script_args.get(2).and_then(as_i64).unwrap_or(0) as i32;
459 let delay_time = script_args.get(3).and_then(as_i64).unwrap_or(0) as i32;
460 ev.turn_event(start_value, end_value, loop_time, delay_time, 0, 0);
461 if anim_skip_trace_enabled() {
462 eprintln!(
463 "[SG_DEBUG][ANIM_SKIP_TRACE][OBJECTEVENT] OBJECTEVENT.TURN target={:?} stage={} slot={} start={} end={} loop_time={} delay={} state=[{}]",
464 target,
465 stage_idx,
466 runtime_slot,
467 start_value,
468 end_value,
469 loop_time,
470 delay_time,
471 int_event_state(ev)
472 );
473 }
474 default_push(ctx);
475 return Ok(true);
476 }
477
478 if is_stop_op(op) {
479 if anim_skip_trace_enabled() {
480 eprintln!(
481 "[SG_DEBUG][ANIM_SKIP_TRACE][OBJECTEVENT] OBJECTEVENT.STOP before target={:?} stage={} slot={} state=[{}]",
482 target,
483 stage_idx,
484 runtime_slot,
485 int_event_state(ev)
486 );
487 }
488 ev.end_event();
489 if anim_skip_trace_enabled() {
490 eprintln!(
491 "[SG_DEBUG][ANIM_SKIP_TRACE][OBJECTEVENT] OBJECTEVENT.STOP after target={:?} stage={} slot={} state=[{}]",
492 target,
493 stage_idx,
494 runtime_slot,
495 int_event_state(ev)
496 );
497 }
498 default_push(ctx);
499 return Ok(true);
500 }
501
502 bail!("unsupported OBJECTEVENT op {}", op)
503}
504
505fn object_runtime_slot_by_stage_index(
506 st: &StageFormState,
507 stage_idx: i64,
508 object_idx: usize,
509) -> Option<usize> {
510 st.object_lists
511 .get(&stage_idx)
512 .and_then(|list| list.get(object_idx))
513 .map(|obj| obj.runtime_slot_or(object_idx))
514}
515
516pub fn dispatch(ctx: &mut CommandContext, args: &[Value]) -> Result<bool> {
517 let Some((chain_pos, chain)) = parse_chain(ctx, args) else {
518 return Ok(false);
519 };
520 if chain.len() < 2 {
521 return Ok(false);
522 }
523 let op = chain[1];
524 let script_args = prop_access::script_args(args, chain_pos);
525 let Some((stage_idx, runtime_slot)) = ctx.globals.current_stage_object else {
526 return Ok(false);
527 };
528
529 dispatch_object_event_on_runtime_slot(ctx, stage_idx, runtime_slot, op, script_args)
530}
531
532pub fn dispatch_list(ctx: &mut CommandContext, args: &[Value]) -> Result<bool> {
533 let Some((chain_pos, chain)) =
534 prop_access::parse_element_chain_ctx(ctx, FM_OBJECTEVENTLIST as u32, args)
535 else {
536 return Ok(false);
537 };
538 if chain.len() < 3 {
539 bail!("OBJECTEVENTLIST.ARRAY requires an index");
540 }
541 if chain[1] != ELM_ARRAY && chain[1] != elm_value::OBJECTEVENTLIST_ARRAY {
542 bail!("unsupported OBJECTEVENTLIST op {}", chain[1]);
543 }
544
545 if chain.len() == 3 {
546 ctx.push(Value::Element(chain.to_vec()));
547 return Ok(true);
548 }
549
550 if chain[2] < 0 {
551 bail!(
552 "OBJECTEVENTLIST.ARRAY index must be non-negative: {}",
553 chain[2]
554 );
555 }
556
557 let Some((stage_idx, _ambient_runtime_slot)) = ctx.globals.current_stage_object else {
558 return Ok(false);
559 };
560 let object_idx = chain[2] as usize;
561 let op = chain[3];
562 let script_args = prop_access::script_args(args, chain_pos);
563
564 let runtime_slot = {
565 let stage_form = ctx.ids.form_global_stage;
566 let Some(st) = ctx.globals.stage_forms.get(&stage_form) else {
567 return Ok(false);
568 };
569 let Some(runtime_slot) = object_runtime_slot_by_stage_index(st, stage_idx, object_idx)
570 else {
571 bail!(
572 "OBJECTEVENTLIST.ARRAY[{}] has no object in stage {}",
573 object_idx,
574 stage_idx
575 );
576 };
577 runtime_slot
578 };
579
580 dispatch_object_event_on_runtime_slot(ctx, stage_idx, runtime_slot, op, script_args)
581}