Skip to main content

siglus_scene_vm/runtime/forms/
int_list.rs

1use anyhow::Result;
2
3use crate::runtime::{CommandContext, Value};
4
5use super::codes::{self, intlist_op, intlistref_op};
6use super::prop_access;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9enum IntListTarget {
10    Root,
11    Index { width: u32, index: i64 },
12    Command { width: u32, op: i32 },
13}
14
15fn selector_width(op: i32) -> Option<u32> {
16    if op == intlist_op::BIT || op == intlistref_op::BIT {
17        Some(1)
18    } else if op == intlist_op::BIT2 || op == intlistref_op::BIT2 {
19        Some(2)
20    } else if op == intlist_op::BIT4 || op == intlistref_op::BIT4 {
21        Some(4)
22    } else if op == intlist_op::BIT8 || op == intlistref_op::BIT8 {
23        Some(8)
24    } else if op == intlist_op::BIT16 || op == intlistref_op::BIT16 {
25        Some(16)
26    } else {
27        None
28    }
29}
30
31fn parse_target(ctx: &CommandContext, chain: &[i32]) -> Option<IntListTarget> {
32    if chain.is_empty() {
33        return None;
34    }
35    if chain.len() == 1 {
36        return Some(IntListTarget::Root);
37    }
38
39    let mut pos = 1usize;
40    let mut width = 32u32;
41    if let Some(selected) = selector_width(chain[pos]) {
42        width = selected;
43        pos += 1;
44    }
45    if pos >= chain.len() {
46        return Some(IntListTarget::Root);
47    }
48
49    let op = chain[pos];
50    if op == ctx.ids.elm_array || op == codes::ELM_ARRAY {
51        let index = i64::from(*chain.get(pos + 1)?);
52        Some(IntListTarget::Index { width, index })
53    } else {
54        Some(IntListTarget::Command { width, op })
55    }
56}
57
58fn configured_count_info(ctx: &CommandContext, global: bool) -> (usize, bool) {
59    let keys = if global {
60        ["#GLOBAL_FLAG.CNT", "GLOBAL_FLAG.CNT"]
61    } else {
62        ["#FLAG.CNT", "FLAG.CNT"]
63    };
64    let configured = ctx
65        .tables
66        .gameexe
67        .as_ref()
68        .and_then(|cfg| keys.into_iter().find_map(|key| cfg.get_usize(key)));
69    (configured.unwrap_or(1000).min(10000), configured.is_some())
70}
71
72fn configured_count(ctx: &CommandContext, global: bool) -> usize {
73    configured_count_info(ctx, global).0
74}
75
76pub(super) fn fixed_default_len(ctx: &CommandContext, form_id: u32) -> Option<usize> {
77    let local = [
78        codes::ELM_GLOBAL_A as u32,
79        codes::ELM_GLOBAL_B as u32,
80        codes::ELM_GLOBAL_C as u32,
81        codes::ELM_GLOBAL_D as u32,
82        codes::ELM_GLOBAL_E as u32,
83        codes::ELM_GLOBAL_F as u32,
84        codes::ELM_GLOBAL_X as u32,
85    ];
86    if local.contains(&form_id) {
87        return Some(configured_count(ctx, false));
88    }
89
90    let global = [codes::ELM_GLOBAL_G as u32, codes::ELM_GLOBAL_Z as u32];
91    if global.contains(&form_id) {
92        return Some(configured_count(ctx, true));
93    }
94
95    None
96}
97
98fn fixed_count_is_explicit(ctx: &CommandContext, form_id: u32) -> bool {
99    let local = [
100        codes::ELM_GLOBAL_A as u32,
101        codes::ELM_GLOBAL_B as u32,
102        codes::ELM_GLOBAL_C as u32,
103        codes::ELM_GLOBAL_D as u32,
104        codes::ELM_GLOBAL_E as u32,
105        codes::ELM_GLOBAL_F as u32,
106        codes::ELM_GLOBAL_X as u32,
107    ];
108    if local.contains(&form_id) {
109        return configured_count_info(ctx, false).1;
110    }
111    let global = [codes::ELM_GLOBAL_G as u32, codes::ELM_GLOBAL_Z as u32];
112    global.contains(&form_id) && configured_count_info(ctx, true).1
113}
114
115fn list_mut(ctx: &mut CommandContext, form_id: u32) -> &mut Vec<i64> {
116    let fixed_len = fixed_default_len(ctx, form_id);
117    let initial_len = fixed_len.unwrap_or(0);
118    let list = ctx
119        .globals
120        .int_lists
121        .entry(form_id)
122        .or_insert_with(|| vec![0; initial_len]);
123    // Old saves and the previous compatibility layer could leave these lists at
124    // 32 words. The original engine always restores the configured fixed size.
125    if let Some(fixed_len) = fixed_len
126        && list.len() < fixed_len
127    {
128        list.resize(fixed_len, 0);
129    }
130    list
131}
132
133fn required_storage_words(width: u32, index: i64) -> Option<usize> {
134    if index < 0 || !matches!(width, 1 | 2 | 4 | 8 | 16 | 32) {
135        return None;
136    }
137    let bits = (index as u64)
138        .checked_add(1)?
139        .checked_mul(u64::from(width))?;
140    let words = bits.checked_add(31)? / 32;
141    usize::try_from(words).ok()
142}
143
144fn ensure_compatible_access_capacity(
145    ctx: &mut CommandContext,
146    form_id: u32,
147    width: u32,
148    index: i64,
149) {
150    if fixed_default_len(ctx, form_id).is_none() || fixed_count_is_explicit(ctx, form_id) {
151        return;
152    }
153    let Some(required) = required_storage_words(width, index) else {
154        return;
155    };
156    // FLAG.CNT/GLOBAL_FLAG.CNT are capped at 10000 storage words in the
157    // original engine. When Gameexe was unavailable, grow only inside that
158    // legal envelope so valid titles are not constrained by the fallback 1000.
159    if required <= 10000 {
160        let list = list_mut(ctx, form_id);
161        if list.len() < required {
162            log::warn!(
163                "Gameexe flag count unavailable; extending INTLIST compatibility storage: form_id={} old_words={} new_words={}",
164                form_id,
165                list.len(),
166                required
167            );
168            list.resize(required, 0);
169        }
170    }
171}
172
173pub(super) fn ensure_fixed_direct_index(ctx: &mut CommandContext, form_id: u32, index: usize) {
174    if let Ok(index) = i64::try_from(index) {
175        ensure_compatible_access_capacity(ctx, form_id, 32, index);
176    }
177    let _ = list_mut(ctx, form_id);
178}
179
180fn is_fixed(ctx: &CommandContext, form_id: u32) -> bool {
181    fixed_default_len(ctx, form_id).is_some()
182}
183
184fn i32_value(value: i64) -> i64 {
185    i64::from(value as i32)
186}
187
188fn bit_location(list_len: usize, width: u32, index: i64) -> Option<(usize, u32)> {
189    if index < 0 || !matches!(width, 1 | 2 | 4 | 8 | 16 | 32) {
190        return None;
191    }
192    let bit_index = (index as u64).checked_mul(u64::from(width))?;
193    let word = bit_index / 32;
194    if word >= list_len as u64 {
195        return None;
196    }
197    Some((word as usize, (bit_index % 32) as u32))
198}
199
200fn log_out_of_range(form_id: u32, width: u32, index: i64, words: usize) {
201    log::error!(
202        "INTLIST index out of range: form_id={} width={} index={} storage_words={}",
203        form_id,
204        width,
205        index,
206        words
207    );
208}
209
210pub(super) fn bit_get(form_id: u32, list: &[i64], width: u32, index: i64) -> i64 {
211    let Some((word, shift)) = bit_location(list.len(), width, index) else {
212        log_out_of_range(form_id, width, index, list.len());
213        return 0;
214    };
215
216    if width == 32 {
217        return i64::from(list[word] as i32);
218    }
219    let mask = (1u32 << width) - 1;
220    i64::from(((list[word] as i32 as u32) >> shift) & mask)
221}
222
223pub(super) fn bit_set(form_id: u32, list: &mut [i64], width: u32, index: i64, value: i64) {
224    let Some((word, shift)) = bit_location(list.len(), width, index) else {
225        log_out_of_range(form_id, width, index, list.len());
226        return;
227    };
228
229    if width == 32 {
230        list[word] = i32_value(value);
231        return;
232    }
233
234    let value_mask = (1u32 << width) - 1;
235    let field_mask = value_mask << shift;
236    let raw = list[word] as i32 as u32;
237    let next = (raw & !field_mask) | (((value as u32) & value_mask) << shift);
238    list[word] = i64::from(next as i32);
239}
240
241pub(super) fn logical_size(storage_words: usize, width: u32) -> i64 {
242    let per_word = 32usize / width as usize;
243    storage_words.saturating_mul(per_word) as i64
244}
245
246fn resize_list(ctx: &mut CommandContext, form_id: u32, requested: i64) {
247    if is_fixed(ctx, form_id) {
248        log::error!(
249            "INTLIST.RESIZE rejected for fixed list: form_id={} requested={}",
250            form_id,
251            requested
252        );
253        return;
254    }
255    let Ok(new_len) = usize::try_from(requested.max(0)) else {
256        log::error!(
257            "INTLIST.RESIZE size is not representable: form_id={} requested={}",
258            form_id,
259            requested
260        );
261        return;
262    };
263    list_mut(ctx, form_id).resize(new_len, 0);
264}
265
266fn reinit_list(ctx: &mut CommandContext, form_id: u32) {
267    if let Some(default_len) = fixed_default_len(ctx, form_id) {
268        let list = list_mut(ctx, form_id);
269        list.resize(default_len, 0);
270        list.fill(0);
271    } else {
272        // The original extendable INTLIST instances used here (for example EXCALL.F)
273        // have a default size of zero.
274        list_mut(ctx, form_id).clear();
275    }
276}
277
278fn params(args: &[Value], chain_pos: usize) -> &[Value] {
279    prop_access::script_args(args, chain_pos.min(args.len()))
280}
281
282/// Implements the original `tnm_command_proc_int_list` behavior.
283pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
284    let Some((chain_pos, chain_ref)) = prop_access::parse_element_chain_ctx(ctx, form_id, args)
285        .or_else(|| prop_access::parse_element_chain(form_id, args))
286    else {
287        ctx.push(Value::Int(0));
288        return Ok(true);
289    };
290    let chain = chain_ref.to_vec();
291    let target = parse_target(ctx, &chain);
292    let script_params = params(args, chain_pos);
293    let (al_id, ret_form) = prop_access::current_vm_meta(ctx);
294    let al_id = al_id.unwrap_or(0);
295
296    match target {
297        Some(IntListTarget::Root) => {
298            // Returning an element reference is represented by the VM's element chain,
299            // not by a scalar stack value. Keep the historical neutral result here.
300            ctx.push(Value::Int(0));
301        }
302        Some(IntListTarget::Index { width, index }) => {
303            ensure_compatible_access_capacity(ctx, form_id, width, index);
304            if al_id == 1 {
305                let value = args.first().and_then(Value::as_i64).unwrap_or(0);
306                let list = list_mut(ctx, form_id);
307                bit_set(form_id, list.as_mut_slice(), width, index, value);
308                ctx.push(Value::Int(0));
309            } else {
310                let value = {
311                    let list = list_mut(ctx, form_id);
312                    bit_get(form_id, list.as_slice(), width, index)
313                };
314                ctx.push(Value::Int(value));
315            }
316        }
317        Some(IntListTarget::Command { width, op }) => match op {
318            intlist_op::INIT => {
319                reinit_list(ctx, form_id);
320                ctx.push(Value::Int(0));
321            }
322            intlist_op::RESIZE => {
323                let requested = script_params.first().and_then(Value::as_i64).unwrap_or(0);
324                resize_list(ctx, form_id, requested);
325                ctx.push(Value::Int(0));
326            }
327            intlist_op::GET_SIZE => {
328                let size = {
329                    let list = list_mut(ctx, form_id);
330                    logical_size(list.len(), width)
331                };
332                ctx.push(Value::Int(size));
333            }
334            intlist_op::CLEAR => {
335                let start = script_params.first().and_then(Value::as_i64).unwrap_or(0);
336                let end = script_params
337                    .get(1)
338                    .and_then(Value::as_i64)
339                    .unwrap_or(start);
340                let value = if al_id == 0 {
341                    0
342                } else {
343                    script_params.get(2).and_then(Value::as_i64).unwrap_or(0)
344                };
345                if start <= end {
346                    ensure_compatible_access_capacity(ctx, form_id, width, end);
347                    let list = list_mut(ctx, form_id);
348                    for index in start..=end {
349                        bit_set(form_id, list.as_mut_slice(), width, index, value);
350                    }
351                }
352                ctx.push(Value::Int(0));
353            }
354            intlist_op::SETS => {
355                let start = script_params.first().and_then(Value::as_i64).unwrap_or(0);
356                if let Some(last_offset) = script_params.len().checked_sub(2)
357                    && let Some(last_index) = start.checked_add(last_offset as i64)
358                {
359                    ensure_compatible_access_capacity(ctx, form_id, width, last_index);
360                }
361                let list = list_mut(ctx, form_id);
362                for (offset, value) in script_params.iter().skip(1).enumerate() {
363                    let Some(index) = start.checked_add(offset as i64) else {
364                        log::error!(
365                            "INTLIST.SETS index overflow: form_id={} start={}",
366                            form_id,
367                            start
368                        );
369                        break;
370                    };
371                    bit_set(
372                        form_id,
373                        list.as_mut_slice(),
374                        width,
375                        index,
376                        value.as_i64().unwrap_or(0),
377                    );
378                }
379                ctx.push(Value::Int(0));
380            }
381            _ => {
382                log::error!(
383                    "unsupported INTLIST command: form_id={} op={} width={} ret_form={:?}",
384                    form_id,
385                    op,
386                    width,
387                    ret_form
388                );
389                ctx.push(Value::Int(0));
390            }
391        },
392        None => {
393            log::error!(
394                "malformed INTLIST element chain: form_id={} chain={:?}",
395                form_id,
396                chain
397            );
398            ctx.push(Value::Int(0));
399        }
400    }
401
402    Ok(true)
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn packed_access_uses_signed_i32_storage() {
411        let mut words = vec![0_i64];
412        bit_set(25, &mut words, 8, 3, 0xAB);
413        assert_eq!(bit_get(25, &words, 8, 3), 0xAB);
414        assert_eq!(words[0], i64::from(0xAB00_0000u32 as i32));
415
416        bit_set(25, &mut words, 32, 0, 0xFFFF_FFFFu32 as i64);
417        assert_eq!(words[0], -1);
418        assert_eq!(bit_get(25, &words, 32, 0), -1);
419    }
420
421    #[test]
422    fn packed_access_does_not_grow_or_alias() {
423        let mut words = vec![0x1234_i64];
424        let before = words.clone();
425        bit_set(25, &mut words, 8, 4, 0xCD);
426        bit_set(25, &mut words, 16, -1, 0xFFFF);
427        assert_eq!(words, before);
428        assert_eq!(bit_get(25, &words, 8, 4), 0);
429        assert_eq!(bit_get(25, &words, 16, -1), 0);
430    }
431
432    #[test]
433    fn logical_sizes_match_cpp_views() {
434        assert_eq!(logical_size(7, 1), 224);
435        assert_eq!(logical_size(7, 2), 112);
436        assert_eq!(logical_size(7, 4), 56);
437        assert_eq!(logical_size(7, 8), 28);
438        assert_eq!(logical_size(7, 16), 14);
439        assert_eq!(logical_size(7, 32), 7);
440    }
441}