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