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