siglus_scene_vm/runtime/forms/
str_list.rs1use 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(¤t, 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 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}