Skip to main content

siglus_scene_vm/runtime/forms/
pcmch.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{bail, Context, Result};
4use siglus_assets::gameexe::{decode_gameexe_dat_bytes, GameexeConfig, GameexeDecodeOptions};
5
6use crate::audio::bgm::decode_bgm_to_wav_bytes;
7use crate::runtime::{CommandContext, Value};
8
9use super::codes;
10
11fn store_or_push_pcmch_prop(ctx: &mut CommandContext, ch: usize, op: i32, args: &[Value]) {
12    let form_key = if ctx.ids.form_global_pcmch != 0 {
13        ctx.ids.form_global_pcmch
14    } else {
15        super::codes::FORM_GLOBAL_PCMCH
16    };
17    let prop = (((ch as i32) & 0x7fff) << 16) ^ ((op as i32) & 0xffff);
18    if let Some(v) = args.get(0).cloned() {
19        match v {
20            Value::Str(s) => {
21                ctx.globals
22                    .str_props
23                    .entry(form_key)
24                    .or_default()
25                    .insert(prop, s);
26            }
27            Value::Int(n) => {
28                ctx.globals
29                    .int_props
30                    .entry(form_key)
31                    .or_default()
32                    .insert(prop, n);
33            }
34            _ => {}
35        }
36        ctx.push(Value::Int(0));
37        return;
38    }
39    if let Some(s) = ctx
40        .globals
41        .str_props
42        .get(&form_key)
43        .and_then(|m| m.get(&prop))
44        .cloned()
45    {
46        ctx.push(Value::Str(s));
47        return;
48    }
49    let v = ctx
50        .globals
51        .int_props
52        .get(&form_key)
53        .and_then(|m| m.get(&prop).copied())
54        .unwrap_or(0);
55    ctx.push(Value::Int(v));
56}
57
58fn arg_str<'a>(args: &'a [Value], idx: usize) -> Option<&'a str> {
59    args.get(idx).and_then(|v| v.as_str())
60}
61
62fn arg_int(args: &[Value], idx: usize) -> Option<i64> {
63    args.get(idx).and_then(|v| v.as_i64())
64}
65
66fn named_str<'a>(args: &'a [Value], id: i32) -> Option<&'a str> {
67    args.iter().find_map(|v| match v {
68        Value::NamedArg { id: nid, value } if *nid == id => value.as_str(),
69        _ => None,
70    })
71}
72
73fn named_int(args: &[Value], id: i32) -> Option<i64> {
74    args.iter().find_map(|v| match v {
75        Value::NamedArg { id: nid, value } if *nid == id => value.as_i64(),
76        _ => None,
77    })
78}
79
80fn parse_channel_from_chain(
81    form_id: u32,
82    ctx: &CommandContext,
83    chain: &[i32],
84) -> Option<(usize, i32)> {
85    if chain.len() < 4 {
86        return None;
87    }
88    if chain[0] as u32 != form_id {
89        return None;
90    }
91    let elm_array = ctx.ids.elm_array;
92    if elm_array >= 0 {
93        if chain[1] != elm_array {
94            return None;
95        }
96    } else if chain[1] == 0 {
97        return None;
98    }
99    let ch = chain[2];
100    if ch < 0 {
101        return None;
102    }
103    let op = *chain.last()?;
104    Some((ch as usize, op))
105}
106
107fn resolve_numeric_candidates(n: i64) -> Vec<String> {
108    if n < 0 {
109        return vec![n.to_string()];
110    }
111    vec![
112        format!("{:05}", n),
113        format!("{:04}", n),
114        format!("{:03}", n),
115        n.to_string(),
116    ]
117}
118
119fn resolve_subdir_path(
120    project_dir: &Path,
121    current_append_dir: &str,
122    subdir: &str,
123    file_name: &str,
124) -> Option<PathBuf> {
125    crate::resource::find_audio_path_with_append_dir(
126        project_dir,
127        current_append_dir,
128        subdir,
129        file_name,
130    )
131    .ok()
132    .map(|(path, _ty)| path)
133}
134
135fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
136    const CANDIDATES: &[&str] = &[
137        "Gameexe.dat",
138        "Gameexe.ini",
139        "gameexe.dat",
140        "gameexe.ini",
141        "GameexeEN.dat",
142        "GameexeEN.ini",
143        "GameexeZH.dat",
144        "GameexeZH.ini",
145        "GameexeZHTW.dat",
146        "GameexeZHTW.ini",
147        "GameexeDE.dat",
148        "GameexeDE.ini",
149        "GameexeES.dat",
150        "GameexeES.ini",
151        "GameexeFR.dat",
152        "GameexeFR.ini",
153        "GameexeID.dat",
154        "GameexeID.ini",
155    ];
156    for name in CANDIDATES {
157        let p = project_dir.join(name);
158        if p.is_file() {
159            return Some(p);
160        }
161    }
162    None
163}
164
165fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
166    let path = find_gameexe_path(project_dir)?;
167    let raw = std::fs::read(&path).ok()?;
168    if path
169        .extension()
170        .and_then(|s| s.to_str())
171        .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
172    {
173        let text = String::from_utf8(raw).ok()?;
174        return Some(GameexeConfig::from_text(&text));
175    }
176    let opt = GameexeDecodeOptions::from_project_dir(project_dir).ok()?;
177    let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
178    Some(GameexeConfig::from_text(&text))
179}
180
181fn lookup_gameexe_bgm_file_name(project_dir: &Path, regist_name: &str) -> Option<String> {
182    let cfg = load_gameexe_config(project_dir)?;
183    let target = regist_name.trim().to_ascii_lowercase();
184    let cnt = cfg.indexed_count("BGM");
185    for i in 0..cnt {
186        let Some(key_name) = cfg.get_indexed_item_unquoted("BGM", i, 0) else {
187            continue;
188        };
189        if key_name.trim().to_ascii_lowercase() != target {
190            continue;
191        }
192        let Some(file_name) = cfg.get_indexed_item_unquoted("BGM", i, 1) else {
193            continue;
194        };
195        if !file_name.trim().is_empty() {
196            return Some(file_name.trim().to_string());
197        }
198    }
199    None
200}
201
202fn play_path_on_pcm_slot(
203    ctx: &mut CommandContext,
204    ch: usize,
205    display_name: &str,
206    path: &Path,
207    loop_flag: bool,
208) -> Result<()> {
209    let decoded = decode_bgm_to_wav_bytes(path, None)
210        .with_context(|| format!("decode audio: {}", path.display()))?;
211    let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
212    pcm.play_decoded_wav_in_slot(audio, ch, display_name, decoded.wav_bytes, loop_flag)
213}
214
215fn play_named_source(
216    ctx: &mut CommandContext,
217    ch: usize,
218    pcm_name: Option<&str>,
219    bgm_name: Option<&str>,
220    koe_no: Option<i64>,
221    se_no: Option<i64>,
222    loop_flag: bool,
223) -> Result<bool> {
224    if let Some(name) = pcm_name.filter(|s| !s.is_empty()) {
225        let ok = {
226            let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
227            pcm.play_in_slot(audio, ch, name, loop_flag).is_ok()
228        };
229        if !ok {
230            ctx.unknown
231                .record_note(&format!("pcmch.play_pcm.failed:{ch}:{name}"));
232        }
233        return Ok(true);
234    }
235
236    if let Some(name) = bgm_name.filter(|s| !s.is_empty()) {
237        if let Some(mapped_name) = lookup_gameexe_bgm_file_name(&ctx.project_dir, name) {
238            if let Some(path) = resolve_subdir_path(
239                &ctx.project_dir,
240                &ctx.globals.append_dir,
241                "bgm",
242                &mapped_name,
243            ) {
244                if play_path_on_pcm_slot(ctx, ch, &format!("bgm:{name}"), &path, loop_flag).is_err()
245                {
246                    ctx.unknown
247                        .record_note(&format!("pcmch.play_bgm.failed:{ch}:{name}"));
248                }
249                return Ok(true);
250            }
251        }
252        if let Some(path) =
253            resolve_subdir_path(&ctx.project_dir, &ctx.globals.append_dir, "bgm", name)
254        {
255            if play_path_on_pcm_slot(ctx, ch, &format!("bgm:{name}"), &path, loop_flag).is_err() {
256                ctx.unknown
257                    .record_note(&format!("pcmch.play_bgm.failed:{ch}:{name}"));
258            }
259            return Ok(true);
260        }
261        ctx.unknown
262            .record_note(&format!("pcmch.bgm.missing:{ch}:{name}"));
263        return Ok(true);
264    }
265
266    if let Some(no) = koe_no {
267        let ok = {
268            let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
269            pcm.play_koe_no_in_slot(audio, ch, no, loop_flag).is_ok()
270        };
271        if !ok {
272            ctx.unknown
273                .record_note(&format!("pcmch.play_koe.failed:{ch}:{no}"));
274        }
275        return Ok(true);
276    }
277
278    if let Some(no) = se_no {
279        let Some(name) = ctx
280            .tables
281            .se_file_names
282            .get(no as usize)
283            .and_then(|v| v.as_deref())
284            .filter(|s| !s.is_empty())
285            .map(|s| s.to_string())
286        else {
287            ctx.unknown
288                .record_note(&format!("pcmch.se.table.missing:{ch}:{no}"));
289            return Ok(true);
290        };
291        let ok = {
292            let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
293            pcm.play_in_slot(audio, ch, &name, loop_flag).is_ok()
294        };
295        if !ok {
296            ctx.unknown
297                .record_note(&format!("pcmch.play_se.failed:{ch}:{no}:{name}"));
298        }
299        return Ok(true);
300    }
301
302    Ok(false)
303}
304
305pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
306    let vm_call = match ctx.vm_call.as_ref() {
307        Some(v) => v,
308        None => return Ok(false),
309    };
310    let Some((ch, op)) = parse_channel_from_chain(form_id, ctx, &vm_call.element) else {
311        return Ok(false);
312    };
313    dispatch_inner(ctx, ch, op, args, Some(vm_call.ret_form))
314}
315
316fn dispatch_inner(
317    ctx: &mut CommandContext,
318    ch: usize,
319    op: i32,
320    args: &[Value],
321    ret_form: Option<i64>,
322) -> Result<bool> {
323    match op {
324        codes::pcmch_op::PLAY
325        | codes::pcmch_op::PLAY_LOOP
326        | codes::pcmch_op::PLAY_WAIT
327        | codes::pcmch_op::READY => {
328            let default_loop = op == codes::pcmch_op::PLAY_LOOP;
329            let loop_flag = named_int(args, 0).map(|v| v != 0).unwrap_or(default_loop);
330            let wait_flag = named_int(args, 1)
331                .map(|v| v != 0)
332                .unwrap_or(op == codes::pcmch_op::PLAY_WAIT);
333            let _fade_in_time = named_int(args, 2).or_else(|| arg_int(args, 1)).unwrap_or(0);
334            let pcm_name = named_str(args, 7).or_else(|| arg_str(args, 0));
335            let koe_no = named_int(args, 8);
336            let se_no = named_int(args, 9);
337            let bgm_name = named_str(args, 10);
338
339            let played = play_named_source(ctx, ch, pcm_name, bgm_name, koe_no, se_no, loop_flag)?;
340            if !played {
341                store_or_push_pcmch_prop(ctx, ch, op, args);
342                return Ok(true);
343            }
344
345            if wait_flag && !loop_flag {
346                ctx.wait
347                    .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), false);
348            }
349            Ok(true)
350        }
351
352        // Siglus PCMCH has no standalone PLAY_BY_SE_NO / PLAY_BY_KOE_NO opcodes.
353        // SE/KOE numeric sources are selected through PCMCH PLAY/READY named args
354        // (`se_no` / `koe_no`) in the compiler ELEMENT table.
355        codes::pcmch_op::STOP => {
356            let fade = arg_int(args, 0);
357            ctx.pcm.stop_slot(ch, fade)?;
358            Ok(true)
359        }
360        codes::pcmch_op::PAUSE => {
361            ctx.pcm.stop_slot(ch, arg_int(args, 0))?;
362            Ok(true)
363        }
364        codes::pcmch_op::RESUME | codes::pcmch_op::RESUME_WAIT => {
365            let _fade_time = arg_int(args, 0);
366            let _delay_time = named_int(args, 0);
367            if op == codes::pcmch_op::RESUME_WAIT {
368                ctx.wait
369                    .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), false);
370            }
371            Ok(true)
372        }
373        codes::pcmch_op::WAIT => {
374            ctx.wait
375                .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), false);
376            if ret_form.unwrap_or(0) != 0 {
377                ctx.push(Value::Int(0));
378            }
379            Ok(true)
380        }
381        codes::pcmch_op::WAIT_KEY => {
382            ctx.wait
383                .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), true);
384            if ret_form.unwrap_or(0) != 0 {
385                ctx.push(Value::Int(0));
386            }
387            Ok(true)
388        }
389        codes::pcmch_op::WAIT_FADE | codes::pcmch_op::WAIT_FADE_KEY => {
390            let key = op == codes::pcmch_op::WAIT_FADE_KEY;
391            ctx.wait
392                .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), key);
393            if ret_form.unwrap_or(0) != 0 {
394                ctx.push(Value::Int(0));
395            }
396            Ok(true)
397        }
398        codes::pcmch_op::CHECK => {
399            let playing = ctx.pcm.is_playing_slot(ch);
400            ctx.push(Value::Int(if playing { 1 } else { 0 }));
401            Ok(true)
402        }
403        codes::pcmch_op::SET_VOLUME => {
404            let vol = match arg_int(args, 0) {
405                Some(v) => v.clamp(0, 255) as u8,
406                None => {
407                    store_or_push_pcmch_prop(ctx, ch, op, args);
408                    return Ok(true);
409                }
410            };
411            let fade_time = arg_int(args, 1).unwrap_or(0);
412            let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
413            pcm.set_volume_raw_fade(audio, vol, fade_time)?;
414            Ok(true)
415        }
416        codes::pcmch_op::SET_VOLUME_MAX => {
417            let fade_time = arg_int(args, 0).unwrap_or(0);
418            let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
419            pcm.set_volume_raw_fade(audio, 255, fade_time)?;
420            Ok(true)
421        }
422        codes::pcmch_op::SET_VOLUME_MIN => {
423            let fade_time = arg_int(args, 0).unwrap_or(0);
424            let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
425            pcm.set_volume_raw_fade(audio, 0, fade_time)?;
426            Ok(true)
427        }
428        codes::pcmch_op::GET_VOLUME => {
429            let v = ctx.pcm.volume_raw() as i64;
430            ctx.push(Value::Int(v));
431            Ok(true)
432        }
433        _ => {
434            store_or_push_pcmch_prop(ctx, ch, op, args);
435            Ok(true)
436        }
437    }
438}