1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result, bail};
4use siglus_assets::gameexe::{GameexeConfig, decode_gameexe_dat_bytes};
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 & 0xffff);
18 if let Some(v) = args.first().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 positional_arg(args: &[Value], idx: usize) -> Option<&Value> {
59 args.iter()
60 .filter(|v| !matches!(v, Value::NamedArg { .. }))
61 .nth(idx)
62}
63
64fn arg_str(args: &[Value], idx: usize) -> Option<&str> {
65 positional_arg(args, idx).and_then(Value::as_str)
66}
67
68fn arg_int(args: &[Value], idx: usize) -> Option<i64> {
69 positional_arg(args, idx).and_then(Value::as_i64)
70}
71
72fn named_str(args: &[Value], id: i32) -> Option<&str> {
73 args.iter().find_map(|v| match v {
74 Value::NamedArg { id: nid, value } if *nid == id => value.as_str(),
75 _ => None,
76 })
77}
78
79fn named_int(args: &[Value], id: i32) -> Option<i64> {
80 args.iter().find_map(|v| match v {
81 Value::NamedArg { id: nid, value } if *nid == id => value.as_i64(),
82 _ => None,
83 })
84}
85
86fn parse_channel_from_chain(
87 form_id: u32,
88 ctx: &CommandContext,
89 chain: &[i32],
90) -> Option<(usize, i32)> {
91 if chain.len() < 4 {
92 return None;
93 }
94 if chain[0] as u32 != form_id {
95 return None;
96 }
97 let elm_array = ctx.ids.elm_array;
98 if elm_array >= 0 {
99 if chain[1] != elm_array {
100 return None;
101 }
102 } else if chain[1] == 0 {
103 return None;
104 }
105 let ch = chain[2];
106 if ch < 0 {
107 return None;
108 }
109 let op = *chain.last()?;
110 Some((ch as usize, op))
111}
112
113fn resolve_numeric_candidates(n: i64) -> Vec<String> {
114 if n < 0 {
115 return vec![n.to_string()];
116 }
117 vec![
118 format!("{:05}", n),
119 format!("{:04}", n),
120 format!("{:03}", n),
121 n.to_string(),
122 ]
123}
124
125fn resolve_subdir_path(
126 project_dir: &Path,
127 current_append_dir: &str,
128 subdir: &str,
129 file_name: &str,
130) -> Option<PathBuf> {
131 crate::resource::find_audio_path_with_append_dir(
132 project_dir,
133 current_append_dir,
134 subdir,
135 file_name,
136 )
137 .ok()
138 .map(|(path, _ty)| path)
139}
140
141fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
142 const CANDIDATES: &[&str] = &[
143 "Gameexe.dat",
144 "Gameexe.ini",
145 "gameexe.dat",
146 "gameexe.ini",
147 "GameexeEN.dat",
148 "GameexeEN.ini",
149 "GameexeZH.dat",
150 "GameexeZH.ini",
151 "GameexeZHTW.dat",
152 "GameexeZHTW.ini",
153 "GameexeDE.dat",
154 "GameexeDE.ini",
155 "GameexeES.dat",
156 "GameexeES.ini",
157 "GameexeFR.dat",
158 "GameexeFR.ini",
159 "GameexeID.dat",
160 "GameexeID.ini",
161 ];
162 for name in CANDIDATES {
163 let p = project_dir.join(name);
164 if let Some(path) = crate::resource::resolve_game_file(&p).ok().flatten() {
165 return Some(path);
166 }
167 }
168 None
169}
170
171fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
172 let path = find_gameexe_path(project_dir)?;
173 let raw = crate::resource::read_file_bytes(&path).ok()?;
174 if path
175 .extension()
176 .and_then(|s| s.to_str())
177 .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
178 {
179 let text = String::from_utf8(raw).ok()?;
180 return Some(GameexeConfig::from_text(&text));
181 }
182 let opt = crate::resource::load_gameexe_decode_options(project_dir).ok()?;
183 let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
184 Some(GameexeConfig::from_text(&text))
185}
186
187fn lookup_gameexe_bgm_file_name(project_dir: &Path, regist_name: &str) -> Option<String> {
188 let cfg = load_gameexe_config(project_dir)?;
189 let target = regist_name.trim().to_ascii_lowercase();
190 let cnt = cfg.indexed_count("BGM");
191 for i in 0..cnt {
192 let Some(key_name) = cfg.get_indexed_item_unquoted("BGM", i, 0) else {
193 continue;
194 };
195 if key_name.trim().to_ascii_lowercase() != target {
196 continue;
197 }
198 let Some(file_name) = cfg.get_indexed_item_unquoted("BGM", i, 1) else {
199 continue;
200 };
201 if !file_name.trim().is_empty() {
202 return Some(file_name.trim().to_string());
203 }
204 }
205 None
206}
207
208fn play_path_on_pcm_slot(
209 ctx: &mut CommandContext,
210 ch: usize,
211 display_name: &str,
212 path: &Path,
213 loop_flag: bool,
214 fade_in_ms: i64,
215 ready_only: bool,
216) -> Result<()> {
217 let decoded = decode_bgm_to_wav_bytes(path, None)
218 .with_context(|| format!("decode audio: {}", path.display()))?;
219 let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
220 pcm.play_decoded_wav_in_slot_with_options(
221 audio,
222 ch,
223 display_name,
224 decoded.wav_bytes,
225 loop_flag,
226 fade_in_ms,
227 ready_only,
228 )
229}
230
231fn play_named_source(
232 ctx: &mut CommandContext,
233 ch: usize,
234 pcm_name: Option<&str>,
235 bgm_name: Option<&str>,
236 koe_no: Option<i64>,
237 se_no: Option<i64>,
238 loop_flag: bool,
239 fade_in_ms: i64,
240 ready_only: bool,
241) -> Result<bool> {
242 if let Some(name) = pcm_name.filter(|s| !s.is_empty()) {
243 let ok = {
244 let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
245 pcm.play_in_slot_with_options(audio, ch, name, loop_flag, fade_in_ms, ready_only)
246 .is_ok()
247 };
248 if !ok {
249 ctx.unknown
250 .record_note(&format!("pcmch.play_pcm.failed:{ch}:{name}"));
251 }
252 return Ok(true);
253 }
254
255 if let Some(name) = bgm_name.filter(|s| !s.is_empty()) {
256 if let Some(mapped_name) = lookup_gameexe_bgm_file_name(&ctx.project_dir, name)
257 && let Some(path) = resolve_subdir_path(
258 &ctx.project_dir,
259 &ctx.globals.append_dir,
260 "bgm",
261 &mapped_name,
262 )
263 {
264 if play_path_on_pcm_slot(
265 ctx,
266 ch,
267 &format!("bgm:{name}"),
268 &path,
269 loop_flag,
270 fade_in_ms,
271 ready_only,
272 )
273 .is_err()
274 {
275 ctx.unknown
276 .record_note(&format!("pcmch.play_bgm.failed:{ch}:{name}"));
277 }
278 return Ok(true);
279 }
280 if let Some(path) =
281 resolve_subdir_path(&ctx.project_dir, &ctx.globals.append_dir, "bgm", name)
282 {
283 if play_path_on_pcm_slot(
284 ctx,
285 ch,
286 &format!("bgm:{name}"),
287 &path,
288 loop_flag,
289 fade_in_ms,
290 ready_only,
291 )
292 .is_err()
293 {
294 ctx.unknown
295 .record_note(&format!("pcmch.play_bgm.failed:{ch}:{name}"));
296 }
297 return Ok(true);
298 }
299 ctx.unknown
300 .record_note(&format!("pcmch.bgm.missing:{ch}:{name}"));
301 return Ok(true);
302 }
303
304 if let Some(no) = koe_no {
305 let ok = {
306 let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
307 pcm.play_koe_no_in_slot_with_options(audio, ch, no, loop_flag, 0, ready_only)
308 .is_ok()
309 };
310 if !ok {
311 ctx.unknown
312 .record_note(&format!("pcmch.play_koe.failed:{ch}:{no}"));
313 }
314 return Ok(true);
315 }
316
317 if let Some(no) = se_no {
318 let Some(name) = ctx
319 .tables
320 .se_file_names
321 .get(no as usize)
322 .and_then(|v| v.as_deref())
323 .filter(|s| !s.is_empty())
324 .map(|s| s.to_string())
325 else {
326 ctx.unknown
327 .record_note(&format!("pcmch.se.table.missing:{ch}:{no}"));
328 return Ok(true);
329 };
330 let ok = {
331 let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
332 pcm.play_in_slot_with_options(audio, ch, &name, false, 0, ready_only)
333 .is_ok()
334 };
335 if !ok {
336 ctx.unknown
337 .record_note(&format!("pcmch.play_se.failed:{ch}:{no}:{name}"));
338 }
339 return Ok(true);
340 }
341
342 Ok(false)
343}
344
345fn ensure_persistent_channel(
346 ctx: &mut CommandContext,
347 ch: usize,
348) -> &mut crate::runtime::globals::PcmChPersistentState {
349 if ctx.globals.pcmch_persistent.len() <= ch {
350 ctx.globals.pcmch_persistent.resize_with(
351 ch + 1,
352 crate::runtime::globals::PcmChPersistentState::default,
353 );
354 }
355 &mut ctx.globals.pcmch_persistent[ch]
356}
357
358fn persistent_state_for_play(
359 ctx: &CommandContext,
360 ch: usize,
361 pcm_name: Option<&str>,
362 bgm_name: Option<&str>,
363 koe_no: Option<i64>,
364 se_no: Option<i64>,
365 args: &[Value],
366 loop_flag: bool,
367 fade_in_time: i64,
368 ready_only: bool,
369) -> crate::runtime::globals::PcmChPersistentState {
370 let mut state = ctx
371 .globals
372 .pcmch_persistent
373 .get(ch)
374 .cloned()
375 .unwrap_or_default();
376 state.pcm_name = pcm_name.unwrap_or("").to_string();
377 state.bgm_name = bgm_name.unwrap_or("").to_string();
378 state.koe_no = koe_no.unwrap_or(-1);
379 state.se_no = se_no.unwrap_or(-1);
380 state.volume_type = named_int(args, 3).unwrap_or(2);
381 state.bgm_fade_target_flag = named_int(args, 4).unwrap_or(0) != 0;
382 state.bgm_fade2_target_flag = named_int(args, 5).unwrap_or(0) != 0;
383 state.chara_no = named_int(args, 6).unwrap_or(-1);
384 state.bgm_fade_source_flag = named_int(args, 11).unwrap_or(0) != 0;
385 state.volume = ctx.pcm.slot_volume_raw(ch) as i64;
386 state.delay_time = 0;
387 state.fade_in_time = fade_in_time;
388 state.loop_flag = loop_flag;
389 state.ready_flag = ready_only;
390 state
391}
392
393pub(crate) fn play_event_line(
396 ctx: &mut CommandContext,
397 ch: usize,
398 file_name: &str,
399 volume_type: i32,
400 chara_no: i32,
401 bgm_fade_target_flag: bool,
402 bgm_fade2_target_flag: bool,
403 bgm_fade_source_flag: bool,
404) -> Result<u64> {
405 let played = {
406 let (pcm, audio) = (&mut ctx.pcm, &mut ctx.audio);
407 pcm.play_in_slot_with_options(audio, ch, file_name, false, 0, false)
408 .is_ok()
409 };
410 if !played {
411 return Ok(0);
412 }
413
414 let mut state = ctx
415 .globals
416 .pcmch_persistent
417 .get(ch)
418 .cloned()
419 .unwrap_or_default();
420 state.pcm_name = file_name.to_string();
421 state.bgm_name.clear();
422 state.koe_no = -1;
423 state.se_no = -1;
424 state.volume_type = volume_type as i64;
425 state.chara_no = chara_no as i64;
426 state.bgm_fade_target_flag = bgm_fade_target_flag;
427 state.bgm_fade2_target_flag = bgm_fade2_target_flag;
428 state.bgm_fade_source_flag = bgm_fade_source_flag;
429 state.delay_time = 0;
430 state.fade_in_time = 0;
431 state.loop_flag = false;
432 state.ready_flag = false;
433 state.volume = ctx.pcm.slot_volume_raw(ch) as i64;
434 *ensure_persistent_channel(ctx, ch) = state;
435 crate::runtime::forms::syscom::update_audio_routing(ctx, 0, true);
436 Ok(ctx.pcm.slot_duration_ms(ch))
437}
438
439pub(crate) fn restore_persistent_channel(
444 ctx: &mut CommandContext,
445 ch: usize,
446 state: crate::runtime::globals::PcmChPersistentState,
447) -> Result<()> {
448 ctx.pcm
449 .set_slot_volume_raw_fade(ch, state.volume.clamp(0, 255) as u8, 0)?;
450
451 if state.loop_flag || state.ready_flag || state.delay_time > 0 {
452 if !state.pcm_name.is_empty() {
453 let _ = play_named_source(
454 ctx,
455 ch,
456 Some(&state.pcm_name),
457 None,
458 None,
459 None,
460 state.loop_flag,
461 0,
462 state.ready_flag,
463 )?;
464 }
465 if !state.bgm_name.is_empty() {
466 let _ = play_named_source(
467 ctx,
468 ch,
469 None,
470 Some(&state.bgm_name),
471 None,
472 None,
473 state.loop_flag,
474 0,
475 state.ready_flag,
476 )?;
477 } else if state.koe_no >= 0 {
478 let _ = play_named_source(
479 ctx,
480 ch,
481 None,
482 None,
483 Some(state.koe_no),
484 None,
485 state.loop_flag,
486 0,
487 state.ready_flag,
488 )?;
489 } else if state.se_no >= 0 {
490 let _ = play_named_source(
491 ctx,
492 ch,
493 None,
494 None,
495 None,
496 Some(state.se_no),
497 state.loop_flag,
498 0,
499 state.ready_flag,
500 )?;
501 }
502 }
503
504 let slot = ensure_persistent_channel(ctx, ch);
505 *slot = state;
506 crate::runtime::forms::syscom::update_audio_routing(ctx, 0, true);
507 Ok(())
508}
509
510pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
511 let vm_call = match ctx.vm_call.as_ref() {
512 Some(v) => v,
513 None => return Ok(false),
514 };
515 let Some((ch, op)) = parse_channel_from_chain(form_id, ctx, &vm_call.element) else {
516 return Ok(false);
517 };
518 dispatch_inner(ctx, ch, op, args, Some(vm_call.ret_form))
519}
520
521fn dispatch_inner(
522 ctx: &mut CommandContext,
523 ch: usize,
524 op: i32,
525 args: &[Value],
526 ret_form: Option<i64>,
527) -> Result<bool> {
528 match op {
529 codes::pcmch_op::PLAY
530 | codes::pcmch_op::PLAY_LOOP
531 | codes::pcmch_op::PLAY_WAIT
532 | codes::pcmch_op::READY
533 | codes::pcmch_op::READY_LOOP => {
534 let ready_only = matches!(op, codes::pcmch_op::READY | codes::pcmch_op::READY_LOOP);
535 let default_loop =
536 matches!(op, codes::pcmch_op::PLAY_LOOP | codes::pcmch_op::READY_LOOP);
537 let loop_flag = named_int(args, 0).map(|v| v != 0).unwrap_or(default_loop);
538 let wait_flag = if ready_only {
539 false
540 } else {
541 named_int(args, 1)
542 .map(|v| v != 0)
543 .unwrap_or(op == codes::pcmch_op::PLAY_WAIT)
544 };
545 let fade_in_time = if op == codes::pcmch_op::READY {
546 0
547 } else {
548 named_int(args, 2).or_else(|| arg_int(args, 1)).unwrap_or(0)
549 };
550 let pcm_name = named_str(args, 7).or_else(|| arg_str(args, 0));
551 let koe_no = named_int(args, 8).filter(|v| *v >= 0);
552 let se_no = named_int(args, 9).filter(|v| *v >= 0);
553 let bgm_name = named_str(args, 10);
554
555 let persistent = persistent_state_for_play(
556 ctx,
557 ch,
558 pcm_name,
559 bgm_name,
560 koe_no,
561 se_no,
562 args,
563 loop_flag,
564 fade_in_time,
565 ready_only,
566 );
567 let played = play_named_source(
568 ctx,
569 ch,
570 pcm_name,
571 bgm_name,
572 koe_no,
573 se_no,
574 loop_flag,
575 fade_in_time,
576 ready_only,
577 )?;
578 if !played {
579 store_or_push_pcmch_prop(ctx, ch, op, args);
580 return Ok(true);
581 }
582
583 *ensure_persistent_channel(ctx, ch) = persistent;
584 crate::runtime::forms::syscom::update_audio_routing(ctx, 0, true);
585
586 if wait_flag {
587 ctx.wait
588 .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), false);
589 }
590 Ok(true)
591 }
592
593 codes::pcmch_op::STOP => {
597 let fade = arg_int(args, 0);
598 ctx.pcm.stop_slot(ch, fade)?;
599 ensure_persistent_channel(ctx, ch).loop_flag = false;
601 crate::runtime::forms::syscom::update_audio_routing(ctx, 0, true);
602 Ok(true)
603 }
604 codes::pcmch_op::PAUSE => {
605 ctx.pcm.pause_slot(ch, arg_int(args, 0))?;
606 Ok(true)
607 }
608 codes::pcmch_op::RESUME | codes::pcmch_op::RESUME_WAIT => {
609 let fade_time = arg_int(args, 0).unwrap_or(0);
610 let delay_time = if op == codes::pcmch_op::RESUME {
613 named_int(args, 0).unwrap_or(0)
614 } else {
615 0
616 };
617 ctx.pcm.resume_slot(ch, fade_time, delay_time)?;
618 {
619 let state = ensure_persistent_channel(ctx, ch);
620 state.ready_flag = false;
621 state.delay_time = delay_time.max(0);
622 state.fade_in_time = fade_time.max(0);
623 }
624 if op == codes::pcmch_op::RESUME_WAIT {
625 ctx.wait
626 .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), false);
627 }
628 Ok(true)
629 }
630 codes::pcmch_op::WAIT => {
631 ctx.wait
632 .wait_audio(crate::runtime::wait::AudioWait::PcmSlot(ch as u8), false);
633 if ret_form.unwrap_or(0) != 0 {
634 ctx.push(Value::Int(0));
635 }
636 Ok(true)
637 }
638 codes::pcmch_op::WAIT_KEY => {
639 ctx.wait.wait_audio_with_return(
640 crate::runtime::wait::AudioWait::PcmSlot(ch as u8),
641 true,
642 ret_form.unwrap_or(0) != 0,
643 );
644 Ok(true)
645 }
646 codes::pcmch_op::WAIT_FADE | codes::pcmch_op::WAIT_FADE_KEY => {
647 let key = op == codes::pcmch_op::WAIT_FADE_KEY;
648 ctx.wait.wait_audio_with_return(
649 crate::runtime::wait::AudioWait::PcmSlotFade(ch as u8),
650 key,
651 ret_form.unwrap_or(0) != 0,
652 );
653 Ok(true)
654 }
655 codes::pcmch_op::CHECK => {
656 let playing = ctx.pcm.is_playing_slot(ch);
657 ctx.push(Value::Int(if playing { 1 } else { 0 }));
658 Ok(true)
659 }
660 codes::pcmch_op::SET_VOLUME => {
661 let vol = match arg_int(args, 0) {
662 Some(v) => v.clamp(0, 255) as u8,
663 None => {
664 store_or_push_pcmch_prop(ctx, ch, op, args);
665 return Ok(true);
666 }
667 };
668 let fade_time = arg_int(args, 1).unwrap_or(0);
669 ctx.pcm.set_slot_volume_raw_fade(ch, vol, fade_time)?;
670 ensure_persistent_channel(ctx, ch).volume = vol as i64;
671 Ok(true)
672 }
673 codes::pcmch_op::SET_VOLUME_MAX => {
674 let fade_time = arg_int(args, 0).unwrap_or(0);
675 ctx.pcm.set_slot_volume_raw_fade(ch, 255, fade_time)?;
676 ensure_persistent_channel(ctx, ch).volume = 255;
677 Ok(true)
678 }
679 codes::pcmch_op::SET_VOLUME_MIN => {
680 let fade_time = arg_int(args, 0).unwrap_or(0);
681 ctx.pcm.set_slot_volume_raw_fade(ch, 0, fade_time)?;
682 ensure_persistent_channel(ctx, ch).volume = 0;
683 Ok(true)
684 }
685 codes::pcmch_op::GET_VOLUME => {
686 let v = ctx.pcm.slot_volume_raw(ch) as i64;
687 ctx.push(Value::Int(v));
688 Ok(true)
689 }
690 _ => {
691 store_or_push_pcmch_prop(ctx, ch, op, args);
692 Ok(true)
693 }
694 }
695}