Skip to main content

siglus_scene_vm/runtime/forms/
bgm_table.rs

1use 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(&regist_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    // C++ tnm_command_proc_bgm_table() receives the operation from elm_begin[0]
91    // and the script arguments separately through p_ai->al_begin[].  The VM now
92    // carries that element chain in VmCallMeta; args contains only script args.
93    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}