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 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}