Skip to main content

siglus_scene_vm/runtime/forms/
pcmevent.rs

1//! Global PCMEVENT form.
2//!
3//! This is a direct port of `C_elm_pcm_event`: an event owns a line scheduler
4//! and may target either the independent global PCM player (`pcm_buf_no == -1`)
5//! or one PCMCH channel.
6
7use 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                // cmd_sound.cpp initializes this to true and exposes no named
175                // argument that changes it.
176                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                    // The C++ special case avoids stopping the global PCM
202                    // player when this event itself is already inactive.
203                    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
409/// Advance all PCMEVENT schedulers once, matching `C_elm_pcm_event::frame`.
410pub(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            // Valid scripts provide a positive interval or include the sound
436            // length. Keep a guard so malformed zero-interval LOOP data cannot
437            // lock the native frame forever.
438            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}