siglus_scene_vm/runtime/forms/
pcmevent.rs1use anyhow::Result;
8
9use crate::runtime::globals::{
10 PCM_EVENT_TYPE_LOOP, PCM_EVENT_TYPE_NONE, PCM_EVENT_TYPE_ONESHOT, PCM_EVENT_TYPE_RANDOM,
11 PcmEventLine, PcmEventState,
12};
13use crate::runtime::{CommandContext, Value};
14
15#[derive(Clone, Copy)]
16enum PcmEventOp {
17 StartOneShot,
18 StartLoop,
19 StartRandom,
20 Stop,
21 Check,
22 Wait,
23 WaitKey,
24 Unknown,
25}
26
27fn resolve_pcm_event_op(op: i32) -> PcmEventOp {
28 match op {
29 crate::runtime::constants::PCMEVENT_START_ONESHOT => PcmEventOp::StartOneShot,
30 crate::runtime::constants::PCMEVENT_START_LOOP => PcmEventOp::StartLoop,
31 crate::runtime::constants::PCMEVENT_START_RANDOM => PcmEventOp::StartRandom,
32 crate::runtime::constants::PCMEVENT_STOP => PcmEventOp::Stop,
33 crate::runtime::constants::PCMEVENT_CHECK => PcmEventOp::Check,
34 crate::runtime::constants::PCMEVENT_WAIT_KEY => PcmEventOp::WaitKey,
35 crate::runtime::constants::PCMEVENT_WAIT => PcmEventOp::Wait,
36 _ => PcmEventOp::Unknown,
37 }
38}
39
40fn named_int(args: &[Value], id: i32) -> Option<i64> {
41 args.iter().find_map(|v| match v {
42 Value::NamedArg { id: nid, value } if *nid == id => value.as_i64(),
43 _ => None,
44 })
45}
46
47fn positional_pcm_buf_no(args: &[Value]) -> i32 {
48 args.first()
49 .and_then(|value| match value {
50 Value::Int(value) => Some(*value as i32),
51 _ => None,
52 })
53 .unwrap_or(-1)
54}
55
56fn collect_lines(args: &[Value], random: bool) -> Vec<PcmEventLine> {
57 let mut out = Vec::new();
58 for v in args {
59 match v {
60 Value::Str(s) => out.push(PcmEventLine {
61 file_name: s.clone(),
62 probability: if random { 1 } else { 0 },
63 min_time: 0,
64 max_time: 0,
65 }),
66 Value::List(items) if !items.is_empty() => {
67 let file_name = items
68 .first()
69 .and_then(Value::as_str)
70 .unwrap_or("")
71 .to_string();
72 if file_name.is_empty() {
73 continue;
74 }
75 let mut line = PcmEventLine {
76 file_name,
77 probability: if random { 1 } else { 0 },
78 min_time: 0,
79 max_time: 0,
80 };
81 if random {
82 if let Some(v) = items.get(1).and_then(Value::as_i64) {
83 line.probability = v as i32;
84 }
85 if let Some(v) = items.get(2).and_then(Value::as_i64) {
86 line.min_time = v as i32;
87 line.max_time = v as i32;
88 }
89 if let Some(v) = items.get(3).and_then(Value::as_i64) {
90 line.max_time = v as i32;
91 }
92 } else {
93 if let Some(v) = items.get(1).and_then(Value::as_i64) {
94 line.min_time = v as i32;
95 line.max_time = v as i32;
96 }
97 if let Some(v) = items.get(2).and_then(Value::as_i64) {
98 line.max_time = v as i32;
99 }
100 }
101 out.push(line);
102 }
103 _ => {}
104 }
105 }
106 out
107}
108
109pub fn dispatch(ctx: &mut CommandContext, args: &[Value]) -> Result<bool> {
110 let form_global_pcm_event = ctx.ids.form_global_pcm_event;
111 let elm_array = ctx.ids.elm_array;
112 let Some((chain_pos, chain)) = crate::runtime::forms::prop_access::parse_element_chain_ctx(
113 ctx,
114 form_global_pcm_event,
115 args,
116 ) else {
117 return Ok(false);
118 };
119 let chain = chain.to_vec();
120 if chain.len() < 3 || chain[1] != elm_array {
121 return Ok(false);
122 }
123 let idx = chain[2].max(0) as usize;
124
125 {
126 let list = ctx
127 .globals
128 .pcm_event_lists
129 .entry(form_global_pcm_event)
130 .or_default();
131 if list.len() <= idx {
132 list.resize(idx + 1, PcmEventState::default());
133 }
134 }
135
136 if chain.len() == 3 {
137 return Ok(true);
138 }
139
140 let op = resolve_pcm_event_op(chain[3]);
141 let script_args = if chain_pos == args.len() {
142 crate::runtime::forms::prop_access::script_args(args, chain_pos)
143 } else {
144 &args[..chain_pos]
145 };
146 match op {
147 PcmEventOp::StartOneShot | PcmEventOp::StartLoop | PcmEventOp::StartRandom => {
148 let event_type = match op {
149 PcmEventOp::StartOneShot => PCM_EVENT_TYPE_ONESHOT,
150 PcmEventOp::StartLoop => PCM_EVENT_TYPE_LOOP,
151 PcmEventOp::StartRandom => PCM_EVENT_TYPE_RANDOM,
152 _ => unreachable!(),
153 };
154 let random = event_type == PCM_EVENT_TYPE_RANDOM;
155 let lines = collect_lines(script_args, random);
156 let pcm_buf_no = positional_pcm_buf_no(script_args);
157 let volume_type = named_int(script_args, 3)
158 .filter(|value| (-1..32).contains(value))
159 .unwrap_or(2) as i32;
160 let chara_no = named_int(script_args, 6).unwrap_or(-1) as i32;
161 let bgm_fade_target = named_int(script_args, 4).unwrap_or(0) != 0;
162 let bgm_fade2_target = named_int(script_args, 5).unwrap_or(0) != 0;
163 let time_type = named_int(script_args, 11).unwrap_or(0) != 0;
164 let bgm_fade_source = named_int(script_args, 12).unwrap_or(0) != 0;
165
166 if let Some(st) = ctx
167 .globals
168 .pcm_event_lists
169 .get_mut(&form_global_pcm_event)
170 .and_then(|v| v.get_mut(idx))
171 {
172 st.reinit();
173 st.lines = lines;
174 st.start(
177 event_type,
178 pcm_buf_no,
179 volume_type,
180 chara_no,
181 bgm_fade_target,
182 bgm_fade2_target,
183 bgm_fade_source,
184 true,
185 time_type,
186 );
187 }
188 Ok(true)
189 }
190 PcmEventOp::Stop => {
191 let stop_pcm = script_args.first().and_then(Value::as_i64).unwrap_or(0) != 0;
192 let (event_active, pcm_buf_no) = ctx
193 .globals
194 .pcm_event_lists
195 .get(&form_global_pcm_event)
196 .and_then(|v| v.get(idx))
197 .map(|st| (st.is_active(), st.pcm_buf_no))
198 .unwrap_or((false, -1));
199 if stop_pcm {
200 if pcm_buf_no == -1 {
201 if event_active {
204 let _ = ctx.pcm.stop(None);
205 }
206 } else if pcm_buf_no >= 0 {
207 let _ = ctx.pcm.stop_slot(pcm_buf_no as usize, None);
208 }
209 }
210 if let Some(st) = ctx
211 .globals
212 .pcm_event_lists
213 .get_mut(&form_global_pcm_event)
214 .and_then(|v| v.get_mut(idx))
215 {
216 st.reinit();
217 }
218 crate::runtime::forms::syscom::update_audio_routing(ctx, 0, true);
219 Ok(true)
220 }
221 PcmEventOp::Check => {
222 let active = ctx
223 .globals
224 .pcm_event_lists
225 .get(&form_global_pcm_event)
226 .and_then(|v| v.get(idx))
227 .map(PcmEventState::is_active)
228 .unwrap_or(false);
229 ctx.push(Value::Int(if active { 1 } else { 0 }));
230 Ok(true)
231 }
232 PcmEventOp::Wait | PcmEventOp::WaitKey => {
233 let oneshot = ctx
234 .globals
235 .pcm_event_lists
236 .get(&form_global_pcm_event)
237 .and_then(|v| v.get(idx))
238 .is_some_and(|st| st.event_type == PCM_EVENT_TYPE_ONESHOT);
239 if oneshot {
240 let key = matches!(op, PcmEventOp::WaitKey);
241 ctx.wait
242 .wait_pcm_event(form_global_pcm_event, idx, key, key);
243 }
244 Ok(true)
245 }
246 PcmEventOp::Unknown => Ok(false),
247 }
248}
249
250#[derive(Debug, Clone)]
251struct PendingEventPlay {
252 pcm_buf_no: i32,
253 file_name: String,
254 volume_type: i32,
255 chara_no: i32,
256 bgm_fade_target_flag: bool,
257 bgm_fade2_target_flag: bool,
258 bgm_fade_source_flag: bool,
259 time_type: bool,
260 min_time: i32,
261 max_time: i32,
262}
263
264fn next_random(state: &mut u32) -> u32 {
265 let mut value = if *state == 0 { 0x1234_5678 } else { *state };
266 value ^= value << 13;
267 value ^= value >> 17;
268 value ^= value << 5;
269 *state = value;
270 value
271}
272
273fn random_exclusive(state: &mut u32, min_value: i64, max_value: i64) -> i64 {
274 if max_value <= min_value {
275 return min_value;
276 }
277 let span = (max_value - min_value) as u64;
278 min_value + (u64::from(next_random(state)) % span) as i64
279}
280
281fn select_random_line(st: &mut PcmEventState, rng_state: &mut u32) -> Option<usize> {
282 let line_count = st.lines.len();
283 if line_count == 0 {
284 return None;
285 }
286 st.last_line_no = st.cur_line_no;
287 let total: i64 = st
288 .lines
289 .iter()
290 .enumerate()
291 .map(|(index, line)| {
292 let probability = i64::from(line.probability.max(0));
293 if index as i32 == st.last_line_no {
294 probability
295 } else {
296 probability.saturating_mul(2)
297 }
298 })
299 .sum();
300 if total <= 0 {
301 st.cur_line_no = 0;
302 return Some(0);
303 }
304 let pos = random_exclusive(rng_state, 0, total);
305 let mut current = 0i64;
306 for (index, line) in st.lines.iter().enumerate() {
307 let probability = i64::from(line.probability.max(0));
308 current += if index as i32 == st.last_line_no {
309 probability
310 } else {
311 probability.saturating_mul(2)
312 };
313 if pos < current {
314 st.cur_line_no = index as i32;
315 return Some(index);
316 }
317 }
318 st.cur_line_no = (line_count - 1) as i32;
319 Some(line_count - 1)
320}
321
322fn prepare_next_play(st: &mut PcmEventState, rng_state: &mut u32) -> Option<PendingEventPlay> {
323 if !st.is_active() || st.cur_time - st.next_time < 0 {
324 return None;
325 }
326 let index = match st.event_type {
327 PCM_EVENT_TYPE_ONESHOT => {
328 st.cur_line_no += 1;
329 if st.cur_line_no < 0 || st.cur_line_no as usize >= st.lines.len() {
330 st.reinit();
331 return None;
332 }
333 st.cur_line_no as usize
334 }
335 PCM_EVENT_TYPE_LOOP => {
336 if st.lines.is_empty() {
337 st.reinit();
338 return None;
339 }
340 st.cur_line_no = (st.cur_line_no + 1).rem_euclid(st.lines.len() as i32);
341 st.cur_line_no as usize
342 }
343 PCM_EVENT_TYPE_RANDOM => match select_random_line(st, rng_state) {
344 Some(index) => index,
345 None => {
346 st.reinit();
347 return None;
348 }
349 },
350 _ => return None,
351 };
352 st.cur_time = 0;
353 let line = st.lines[index].clone();
354 Some(PendingEventPlay {
355 pcm_buf_no: st.pcm_buf_no,
356 file_name: line.file_name,
357 volume_type: st.volume_type,
358 chara_no: st.chara_no,
359 bgm_fade_target_flag: st.bgm_fade_target_flag,
360 bgm_fade2_target_flag: st.bgm_fade2_target_flag,
361 bgm_fade_source_flag: st.bgm_fade_source_flag,
362 time_type: st.time_type,
363 min_time: line.min_time,
364 max_time: line.max_time,
365 })
366}
367
368fn play_pending(ctx: &mut CommandContext, pending: &PendingEventPlay) -> u64 {
369 if pending.pcm_buf_no == -1 {
370 let result = {
371 let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
372 pcm.play_file_name(audio, &pending.file_name)
373 };
374 if let Err(error) = result {
375 log::error!(
376 "PCMEVENT failed to play global PCM {:?}: {error:#}",
377 pending.file_name
378 );
379 0
380 } else {
381 ctx.pcm.global_duration_ms()
382 }
383 } else if pending.pcm_buf_no >= 0 {
384 match crate::runtime::forms::pcmch::play_event_line(
385 ctx,
386 pending.pcm_buf_no as usize,
387 &pending.file_name,
388 pending.volume_type,
389 pending.chara_no,
390 pending.bgm_fade_target_flag,
391 pending.bgm_fade2_target_flag,
392 pending.bgm_fade_source_flag,
393 ) {
394 Ok(duration) => duration,
395 Err(error) => {
396 log::error!(
397 "PCMEVENT failed to play PCMCH[{}] {:?}: {error:#}",
398 pending.pcm_buf_no,
399 pending.file_name
400 );
401 0
402 }
403 }
404 } else {
405 0
406 }
407}
408
409pub(crate) fn tick_all(ctx: &mut CommandContext, game_delta_ms: i32, real_delta_ms: i32) {
411 let form_ids: Vec<u32> = ctx.globals.pcm_event_lists.keys().copied().collect();
412 for form_id in form_ids {
413 let event_count = ctx
414 .globals
415 .pcm_event_lists
416 .get(&form_id)
417 .map(Vec::len)
418 .unwrap_or(0);
419 for index in 0..event_count {
420 if let Some(st) = ctx
421 .globals
422 .pcm_event_lists
423 .get_mut(&form_id)
424 .and_then(|events| events.get_mut(index))
425 && st.is_active()
426 {
427 let delta = if st.real_flag {
428 real_delta_ms
429 } else {
430 game_delta_ms
431 };
432 st.cur_time = st.cur_time.saturating_add(delta.max(0) as i64);
433 }
434
435 for _ in 0..1024 {
439 let pending = {
440 let (events, rng_state) =
441 (&mut ctx.globals.pcm_event_lists, &mut ctx.globals.rng_state);
442 events
443 .get_mut(&form_id)
444 .and_then(|events| events.get_mut(index))
445 .and_then(|st| prepare_next_play(st, rng_state))
446 };
447 let Some(pending) = pending else {
448 break;
449 };
450 let duration_ms = play_pending(ctx, &pending) as i64;
451 let min_time = i64::from(pending.min_time.min(pending.max_time));
452 let max_time = i64::from(pending.min_time.max(pending.max_time));
453 let mut next_time =
454 random_exclusive(&mut ctx.globals.rng_state, min_time, max_time);
455 if min_time == max_time {
456 next_time = max_time;
457 }
458 if pending.time_type {
459 next_time = next_time.saturating_add(duration_ms);
460 }
461 if let Some(st) = ctx
462 .globals
463 .pcm_event_lists
464 .get_mut(&form_id)
465 .and_then(|events| events.get_mut(index))
466 {
467 st.next_time = next_time;
468 }
469 }
470 }
471 }
472}