siglus_scene_vm/runtime/forms/
bgm_table.rs1use anyhow::{Result, bail};
2
3use crate::runtime::{CommandContext, Value};
4
5use super::codes::bgm_table_op;
6
7fn arg_str(args: &[Value], idx: usize) -> Option<&str> {
8 match args.get(idx) {
9 Some(Value::Str(s)) => Some(s.as_str()),
10 Some(Value::NamedArg { value, .. }) => value.as_str(),
11 _ => None,
12 }
13}
14
15fn arg_int(args: &[Value], idx: usize) -> Option<i64> {
16 args.get(idx).and_then(|v| v.as_i64())
17}
18
19fn normalize_name(name: &str) -> String {
20 name.trim().to_ascii_lowercase()
21}
22
23fn bgm_declared_count(ctx: &CommandContext) -> usize {
24 ctx.tables
25 .gameexe
26 .as_ref()
27 .map(|cfg| {
28 cfg.get_usize("BGM.CNT")
29 .unwrap_or_else(|| cfg.indexed_count("BGM"))
30 })
31 .unwrap_or(0)
32}
33
34fn bgm_count(ctx: &CommandContext) -> usize {
35 bgm_declared_count(ctx)
36 .max(ctx.globals.bgm_table_flags.len())
37 .max(ctx.globals.bgm_table_listened.len())
38}
39
40fn ensure_bgm_flags_size(ctx: &mut CommandContext) {
41 let count = bgm_declared_count(ctx);
42 if ctx.globals.bgm_table_flags.len() < count {
43 ctx.globals
44 .bgm_table_flags
45 .resize(count, ctx.globals.bgm_table_all_flag);
46 }
47}
48
49fn bgm_regist_name(ctx: &CommandContext, index: usize) -> Option<String> {
50 let cfg = ctx.tables.gameexe.as_ref()?;
51 cfg.get_indexed_item_unquoted("BGM", index, 0)
52 .or_else(|| cfg.get_indexed_field_unquoted("BGM", index, "REGIST_NAME"))
53 .or_else(|| cfg.get_indexed_unquoted("BGM", index))
54 .map(|s| s.to_string())
55}
56
57fn bgm_no_by_regist_name(ctx: &CommandContext, name: &str) -> Option<usize> {
58 let needle = normalize_name(name);
59 let count = bgm_declared_count(ctx);
60 for i in 0..count {
61 let Some(regist_name) = bgm_regist_name(ctx, i) else {
62 continue;
63 };
64 if normalize_name(®ist_name) == needle {
65 return Some(i);
66 }
67 }
68 None
69}
70
71pub(crate) fn mark_listened_by_name(ctx: &mut CommandContext, name: &str, listened: bool) -> bool {
72 ensure_bgm_flags_size(ctx);
73 let key = normalize_name(name);
74 let index = bgm_no_by_regist_name(ctx, name);
75 if let Some(index) = index {
76 if ctx.globals.bgm_table_flags.len() <= index {
77 ctx.globals
78 .bgm_table_flags
79 .resize(index + 1, ctx.globals.bgm_table_all_flag);
80 }
81 ctx.globals.bgm_table_flags[index] = listened;
82 ctx.globals.bgm_table_listened.insert(key, listened);
83 true
84 } else {
85 false
86 }
87}
88
89pub fn dispatch(ctx: &mut CommandContext, args: &[Value]) -> Result<bool> {
90 let Some(op) = crate::runtime::forms::prop_access::current_op_from_ctx_or_args(ctx, args)
94 else {
95 bail!("BGMTABLE form expects an element opcode");
96 };
97 let args = crate::runtime::forms::prop_access::params_without_op(ctx, args);
98
99 match op {
100 bgm_table_op::GET_COUNT => {
101 ctx.push(Value::Int(bgm_count(ctx) as i64));
102 Ok(true)
103 }
104 bgm_table_op::GET_LISTEN_BY_NAME => {
105 let Some(name) = arg_str(args, 0) else {
106 ctx.push(Value::Int(-1));
107 return Ok(true);
108 };
109 ensure_bgm_flags_size(ctx);
110 let res = if let Some(index) = bgm_no_by_regist_name(ctx, name) {
111 ctx.globals
112 .bgm_table_flags
113 .get(index)
114 .copied()
115 .unwrap_or(ctx.globals.bgm_table_all_flag) as i64
116 } else {
117 -1
118 };
119 ctx.push(Value::Int(res));
120 Ok(true)
121 }
122 bgm_table_op::SET_LISTEN_CURRENT => {
123 let Some(name) = arg_str(args, 0) else {
124 return Ok(true);
125 };
126 let listened = arg_int(args, 1).unwrap_or(0) != 0;
127 let _ = mark_listened_by_name(ctx, name, listened);
128 Ok(true)
129 }
130 bgm_table_op::SET_ALL_FLAG => {
131 let listened = arg_int(args, 0).unwrap_or(0) != 0;
132 ctx.globals.bgm_table_all_flag = listened;
133 ensure_bgm_flags_size(ctx);
134 ctx.globals.bgm_table_flags.fill(listened);
135 for v in ctx.globals.bgm_table_listened.values_mut() {
136 *v = listened;
137 }
138 Ok(true)
139 }
140 _ => bail!("invalid BGMTABLE command opcode {op}"),
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::runtime::VmCallMeta;
148 use std::path::PathBuf;
149
150 fn setup_call(ctx: &mut CommandContext, op: i32, ret_form: i64) {
151 let form = if ctx.ids.form_global_bgm_table != 0 {
152 ctx.ids.form_global_bgm_table
153 } else {
154 crate::runtime::forms::codes::FORM_GLOBAL_BGM_TABLE
155 };
156 ctx.vm_call = Some(VmCallMeta {
157 element: vec![form as i32, op],
158 al_id: 0,
159 ret_form,
160 });
161 }
162
163 fn test_context() -> CommandContext {
164 let mut ctx = CommandContext::new(PathBuf::from("."));
165 ctx.tables.gameexe = Some(crate::formats::gameexe::GameexeConfig::from_text(
166 "#BGM.000=\"BGM079\",\"bgm079\"\n",
167 ));
168 ctx.globals.bgm_table_flags = vec![true];
169 ctx
170 }
171
172 #[test]
173 fn get_listen_by_name_takes_opcode_from_vm_call() {
174 let mut ctx = test_context();
175 setup_call(&mut ctx, bgm_table_op::GET_LISTEN_BY_NAME, 10);
176
177 assert!(dispatch(&mut ctx, &[Value::Str("bgm079".into())]).unwrap());
178 assert_eq!(ctx.stack.pop().and_then(|value| value.as_i64()), Some(1));
179 }
180
181 #[test]
182 fn set_listen_by_name_uses_script_arguments_without_stack_result() {
183 let mut ctx = test_context();
184 ctx.globals.bgm_table_flags[0] = false;
185 setup_call(&mut ctx, bgm_table_op::SET_LISTEN_CURRENT, 0);
186
187 assert!(dispatch(&mut ctx, &[Value::Str("BGM079".into()), Value::Int(1)],).unwrap());
188 assert!(ctx.globals.bgm_table_flags[0]);
189 assert!(ctx.stack.is_empty());
190 }
191}