Skip to main content

siglus_scene_vm/runtime/forms/
str_list.rs

1use anyhow::Result;
2
3use crate::runtime::{CommandContext, Value};
4
5use super::codes::{self, str_list_op, str_op};
6use super::prop_access;
7use crate::runtime::string_semantics as tstr;
8
9fn configured_count_info(ctx: &CommandContext, global: bool) -> (usize, bool) {
10    let keys = if global {
11        ["#GLOBAL_FLAG.CNT", "GLOBAL_FLAG.CNT"]
12    } else {
13        ["#FLAG.CNT", "FLAG.CNT"]
14    };
15    let configured = ctx
16        .tables
17        .gameexe
18        .as_ref()
19        .and_then(|cfg| keys.into_iter().find_map(|key| cfg.get_usize(key)));
20    (configured.unwrap_or(1000).min(10000), configured.is_some())
21}
22
23fn configured_count(ctx: &CommandContext, global: bool) -> usize {
24    configured_count_info(ctx, global).0
25}
26
27pub(super) fn fixed_default_len(ctx: &CommandContext, form_id: u32) -> Option<usize> {
28    if form_id == codes::ELM_GLOBAL_S as u32 {
29        Some(configured_count(ctx, false))
30    } else if form_id == codes::ELM_GLOBAL_M as u32 {
31        Some(configured_count(ctx, true))
32    } else if form_id == codes::ELM_GLOBAL_NAMAE_LOCAL as u32
33        || form_id == codes::ELM_GLOBAL_NAMAE_GLOBAL as u32
34    {
35        Some(26 + 26 * 26)
36    } else {
37        None
38    }
39}
40
41fn fixed_count_is_explicit(ctx: &CommandContext, form_id: u32) -> bool {
42    if form_id == codes::ELM_GLOBAL_S as u32 {
43        configured_count_info(ctx, false).1
44    } else if form_id == codes::ELM_GLOBAL_M as u32 {
45        configured_count_info(ctx, true).1
46    } else {
47        true
48    }
49}
50
51fn list_mut(ctx: &mut CommandContext, form_id: u32) -> &mut Vec<String> {
52    let fixed_len = fixed_default_len(ctx, form_id);
53    let initial_len = fixed_len.unwrap_or(0);
54    let list = ctx
55        .globals
56        .str_lists
57        .entry(form_id)
58        .or_insert_with(|| vec![String::new(); initial_len]);
59    if let Some(fixed_len) = fixed_len
60        && list.len() < fixed_len
61    {
62        list.resize_with(fixed_len, String::new);
63    }
64    list
65}
66
67fn ensure_compatible_index(ctx: &mut CommandContext, form_id: u32, index: usize) {
68    let _ = list_mut(ctx, form_id);
69    if fixed_default_len(ctx, form_id).is_some()
70        && !fixed_count_is_explicit(ctx, form_id)
71        && index < 10000
72    {
73        let list = list_mut(ctx, form_id);
74        if list.len() <= index {
75            log::warn!(
76                "Gameexe string flag count unavailable; extending STRLIST compatibility storage: form_id={} old_size={} new_size={}",
77                form_id,
78                list.len(),
79                index + 1
80            );
81            list.resize_with(index + 1, String::new);
82        }
83    }
84}
85
86pub(super) fn ensure_fixed_direct_index(ctx: &mut CommandContext, form_id: u32, index: usize) {
87    ensure_compatible_index(ctx, form_id, index);
88}
89
90fn default_for_ret_form(ret_form: i64) -> Value {
91    if prop_access::ret_form_is_string(ret_form) {
92        Value::Str(String::new())
93    } else {
94        Value::Int(0)
95    }
96}
97
98fn execute_str_op(current: &str, op: i32, params: &[Value], al_id: i64) -> Value {
99    match op {
100        str_op::UPPER => Value::Str(tstr::ascii_upper(current)),
101        str_op::LOWER => Value::Str(tstr::ascii_lower(current)),
102        str_op::CNT => Value::Int(tstr::utf16_len(current) as i64),
103        str_op::LEN => Value::Int(tstr::display_width(current) as i64),
104        str_op::LEFT => {
105            let len = params.first().and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
106            Value::Str(tstr::utf16_left(current, len))
107        }
108        str_op::LEFT_LEN => {
109            let len = params.first().and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
110            Value::Str(tstr::left_by_display_width(current, len))
111        }
112        str_op::RIGHT => {
113            let len = params.first().and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
114            Value::Str(tstr::utf16_right(current, len))
115        }
116        str_op::RIGHT_LEN => {
117            let len = params.first().and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
118            Value::Str(tstr::right_by_display_width(current, len))
119        }
120        str_op::MID => {
121            let start = params.first().and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
122            if al_id == 0 || params.len() <= 1 {
123                Value::Str(tstr::utf16_slice(current, start, None))
124            } else {
125                let len = params.get(1).and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
126                Value::Str(tstr::utf16_slice(current, start, Some(len)))
127            }
128        }
129        str_op::MID_LEN => {
130            let start = params.first().and_then(Value::as_i64).unwrap_or(0).max(0) as usize;
131            let len = if al_id == 0 || params.len() <= 1 {
132                None
133            } else {
134                Some(params.get(1).and_then(Value::as_i64).unwrap_or(0).max(0) as usize)
135            };
136            Value::Str(tstr::mid_by_display_width(current, start, len))
137        }
138        str_op::SEARCH => {
139            let needle = params.first().and_then(Value::as_str).unwrap_or("");
140            Value::Int(
141                tstr::search_ascii_case_insensitive(current, needle)
142                    .map(|v| v as i64)
143                    .unwrap_or(-1),
144            )
145        }
146        str_op::SEARCH_LAST => {
147            let needle = params.first().and_then(Value::as_str).unwrap_or("");
148            Value::Int(
149                tstr::rsearch_ascii_case_insensitive(current, needle)
150                    .map(|v| v as i64)
151                    .unwrap_or(-1),
152            )
153        }
154        str_op::GET_CODE => {
155            let pos = params.first().and_then(Value::as_i64).unwrap_or(0);
156            Value::Int(
157                usize::try_from(pos)
158                    .ok()
159                    .and_then(|pos| tstr::utf16_code_unit(current, pos))
160                    .map(i64::from)
161                    .unwrap_or(-1),
162            )
163        }
164        str_op::TONUM => Value::Int(current.parse::<i64>().unwrap_or(0)),
165        _ => {
166            log::error!("unsupported STR command in STRLIST element: op={op}");
167            Value::Str(current.to_string())
168        }
169    }
170}
171
172fn log_out_of_range(form_id: u32, index: i64, len: usize) {
173    log::error!(
174        "STRLIST index out of range: form_id={} index={} size={}",
175        form_id,
176        index,
177        len
178    );
179}
180
181pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
182    let Some((chain_pos, chain_ref)) = prop_access::parse_element_chain_ctx(ctx, form_id, args)
183        .or_else(|| prop_access::parse_element_chain(form_id, args))
184    else {
185        ctx.push(Value::Str(String::new()));
186        return Ok(true);
187    };
188    let chain = chain_ref.to_vec();
189    let params = prop_access::script_args(args, chain_pos.min(args.len()));
190    let (al_id, ret_form) = prop_access::current_vm_meta(ctx);
191    let al_id = al_id.unwrap_or(0);
192    let ret_form = ret_form.unwrap_or(prop_access::FM_STR);
193
194    if chain.len() >= 3 && (chain[1] == ctx.ids.elm_array || chain[1] == codes::ELM_ARRAY) {
195        let index = i64::from(chain[2]);
196        let Some(index_usize) = usize::try_from(index).ok() else {
197            let len = list_mut(ctx, form_id).len();
198            log_out_of_range(form_id, index, len);
199            ctx.push(default_for_ret_form(ret_form));
200            return Ok(true);
201        };
202
203        ensure_compatible_index(ctx, form_id, index_usize);
204        let in_range = index_usize < list_mut(ctx, form_id).len();
205        if !in_range {
206            let len = list_mut(ctx, form_id).len();
207            log_out_of_range(form_id, index, len);
208            ctx.push(default_for_ret_form(ret_form));
209            return Ok(true);
210        }
211
212        if chain.len() == 3 {
213            if al_id == 1 {
214                let rhs = args
215                    .first()
216                    .and_then(Value::as_str)
217                    .unwrap_or("")
218                    .to_string();
219                list_mut(ctx, form_id)[index_usize] = rhs;
220                ctx.push(Value::Int(0));
221            } else {
222                let value = list_mut(ctx, form_id)[index_usize].clone();
223                ctx.push(Value::Str(value));
224            }
225        } else {
226            let op = chain[3];
227            let current = list_mut(ctx, form_id)[index_usize].clone();
228            ctx.push(execute_str_op(&current, op, params, al_id));
229        }
230        return Ok(true);
231    }
232
233    if chain.len() >= 2 {
234        match chain[1] {
235            str_list_op::INIT => {
236                if let Some(default_len) = fixed_default_len(ctx, form_id) {
237                    let list = list_mut(ctx, form_id);
238                    list.resize_with(default_len, String::new);
239                    list.fill(String::new());
240                } else {
241                    list_mut(ctx, form_id).clear();
242                }
243                ctx.push(Value::Int(0));
244                return Ok(true);
245            }
246            str_list_op::RESIZE => {
247                if fixed_default_len(ctx, form_id).is_some() {
248                    let requested = params.first().and_then(Value::as_i64).unwrap_or(0);
249                    log::error!(
250                        "STRLIST.RESIZE rejected for fixed list: form_id={} requested={}",
251                        form_id,
252                        requested
253                    );
254                } else {
255                    let requested = params.first().and_then(Value::as_i64).unwrap_or(0).max(0);
256                    if let Ok(new_len) = usize::try_from(requested) {
257                        list_mut(ctx, form_id).resize_with(new_len, String::new);
258                    }
259                }
260                ctx.push(Value::Int(0));
261                return Ok(true);
262            }
263            str_list_op::GET_SIZE => {
264                let len = list_mut(ctx, form_id).len() as i64;
265                ctx.push(Value::Int(len));
266                return Ok(true);
267            }
268            str_list_op::SETS => {
269                // Present in the element table, but not implemented by the original
270                // `tnm_command_proc_str_list` in this engine version.
271                log::error!(
272                    "STRLIST.SETS is unsupported by the original engine path: form_id={form_id}"
273                );
274                ctx.push(Value::Int(0));
275                return Ok(true);
276            }
277            op => {
278                log::error!("unsupported STRLIST command: form_id={} op={}", form_id, op);
279            }
280        }
281    }
282
283    ctx.push(default_for_ret_form(ret_form));
284    Ok(true)
285}