1use super::shared::{read_i32_le, read_script_name_9};
2use crate::import::soup_def::SoupDef;
3use std::fmt::Write;
4
5const RAHD_ITEM_SIZE: usize = 165;
6const COMPARE_OPS: [&str; 6] = ["=", "!=", "<", ">", "<=", ">="];
7const FORMULA_OPS: [&str; 12] = [
8 "", "+", "-", "*", "/", "<<", ">>", "&", "|", "^", "++", "--",
9];
10
11const DIALOGUE_FUNCTIONS: [&str; 9] = [
12 "ACTIVATE",
13 "RTX",
14 "rtxAnim",
15 "RTXp",
16 "RTXpAnim",
17 "AddLog",
18 "AmbientRtx",
19 "menuAddItem",
20 "TorchActivate",
21];
22
23const ATTRIBUTE_FUNCTIONS: [&str; 4] = ["SetAttribute", "GetAttribute", "SetMyAttr", "GetMyAttr"];
24
25const ANIM_GROUP_FUNCTIONS: [&str; 6] = [
26 "PlayAnimation",
27 "PushAnimation",
28 "PushControlAnimation",
29 "WaitAnimFrame",
30 "WaitPlayerAnimFrame",
31 "SyncWithGroup",
32];
33
34const AXIS_FUNCTIONS: [&str; 8] = [
35 "MoveByAxis",
36 "MoveAlongAxis",
37 "RotateByAxis",
38 "RotateToAxis",
39 "sMoveByAxis",
40 "sMoveAlongAxis",
41 "sRotateByAxis",
42 "sRotateToAxis",
43];
44
45const AXIS_NAMES: [&str; 3] = ["axis_x", "axis_y", "axis_z"];
46
47#[derive(Debug, Clone)]
48pub struct ActorScript {
49 pub header_index: usize,
51 pub instances: u16,
52 pub instance_variables: Vec<Vec<i32>>,
54 pub bytecode: Vec<u8>,
56 pub script_length: i32,
57 pub script_data_offset: i32,
58 pub script_pc: i32,
59 pub num_strings: i32,
60 pub num_variables: i32,
61 pub strings: Vec<String>,
62 pub variables: Vec<i32>,
63 pub instructions: Vec<ScriptInstruction>,
64}
65
66#[derive(Debug, Clone)]
67pub struct ScriptInstruction {
68 pub addr: usize,
69 pub indent: usize,
70 pub text: String,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74enum ValueMode {
75 Main,
76 Lhs,
77 Rhs,
78 Parameter,
79 Formula,
80}
81
82struct Decoder<'a> {
83 code: &'a [u8],
84 pc: usize,
85 strings: &'a [String],
86 soup_def: Option<&'a SoupDef>,
87 instructions: Vec<ScriptInstruction>,
88 indent: usize,
89}
90
91impl<'a> Decoder<'a> {
92 fn new(
93 code: &'a [u8],
94 script_pc: i32,
95 strings: &'a [String],
96 soup_def: Option<&'a SoupDef>,
97 ) -> Self {
98 let pc = usize::try_from(script_pc.max(0)).unwrap_or(0);
99 Self {
100 code,
101 pc,
102 strings,
103 soup_def,
104 instructions: Vec::new(),
105 indent: 0,
106 }
107 }
108
109 fn read_u8(&mut self) -> Result<u8, String> {
110 let value = self
111 .code
112 .get(self.pc)
113 .copied()
114 .ok_or_else(|| "unexpected end of script".to_string())?;
115 self.pc += 1;
116 Ok(value)
117 }
118
119 fn read_u16(&mut self) -> Result<u16, String> {
120 let chunk: [u8; 2] = self
121 .code
122 .get(self.pc..self.pc + 2)
123 .ok_or_else(|| "unexpected end of script".to_string())?
124 .try_into()
125 .map_err(|_| "invalid u16 read".to_string())?;
126 self.pc += 2;
127 Ok(u16::from_le_bytes(chunk))
128 }
129
130 fn read_i32(&mut self) -> Result<i32, String> {
131 let chunk: [u8; 4] = self
132 .code
133 .get(self.pc..self.pc + 4)
134 .ok_or_else(|| "unexpected end of script".to_string())?
135 .try_into()
136 .map_err(|_| "invalid i32 read".to_string())?;
137 self.pc += 4;
138 Ok(i32::from_le_bytes(chunk))
139 }
140
141 fn skip(&mut self, count: usize) -> Result<(), String> {
142 if self.pc + count > self.code.len() {
143 return Err("unexpected end of script".to_string());
144 }
145 self.pc += count;
146 Ok(())
147 }
148
149 fn function_name(&self, func_id: u16) -> String {
150 self.soup_def
151 .and_then(|def| def.functions.get(usize::from(func_id)))
152 .map_or_else(|| format!("func_{func_id}"), |f| f.name.clone())
153 }
154
155 fn flag_name(&self, flag_id: u16) -> String {
156 self.soup_def
157 .and_then(|def| def.flags.get(usize::from(flag_id)))
158 .map_or_else(|| format!("Flag{flag_id}"), |f| f.name.clone())
159 }
160
161 fn ref_name(&self, ref_id: u16) -> String {
162 let idx = usize::from(ref_id & 0x00FF);
163 self.soup_def
164 .and_then(|def| def.references.get(idx))
165 .cloned()
166 .unwrap_or_else(|| format!("ref_{idx}"))
167 }
168
169 fn label_name(offset: i32) -> String {
170 format!("{:04X}", u32::from_le_bytes(offset.to_le_bytes()))
171 }
172
173 fn decode_ref(&mut self) -> Result<String, String> {
174 let ref_id = self.read_u16()?;
175 Ok(self.ref_name(ref_id))
176 }
177
178 fn decode_object(&mut self) -> Result<String, String> {
179 let obj_type = self.read_u8()?;
180 match obj_type {
181 0 => {
182 self.skip(1)?;
183 Ok("Me".to_string())
184 }
185 1 => {
186 self.skip(1)?;
187 Ok("Player".to_string())
188 }
189 2 => {
190 self.skip(1)?;
191 Ok("Camera".to_string())
192 }
193 4 => {
194 let str_idx = usize::from(self.read_u8()?);
195 Ok(self
196 .strings
197 .get(str_idx)
198 .cloned()
199 .unwrap_or_else(|| format!("strings[{str_idx}]")))
200 }
201 10 => {
202 let var_idx = self.read_u8()?;
203 Ok(format!("var{var_idx}"))
204 }
205 _ => Ok(format!("ObjectType{obj_type}")),
206 }
207 }
208
209 fn decode_formula(&mut self) -> Result<String, String> {
210 let mut out = String::from(" = ");
211 loop {
212 out.push_str(&self.decode_value(ValueMode::Formula, None)?);
213 let op = self.read_u8()?;
214 if op == 10 {
215 out.push_str(FORMULA_OPS[10]);
216 self.skip(1)?;
217 break;
218 }
219 if op == 11 {
220 out.push_str(FORMULA_OPS[11]);
221 self.skip(1)?;
222 break;
223 }
224 if (1..=9).contains(&op) {
225 out.push(' ');
226 out.push_str(FORMULA_OPS[usize::from(op)]);
227 out.push(' ');
228 continue;
229 }
230 break;
231 }
232 Ok(out)
233 }
234
235 fn decode_if(&mut self) -> Result<String, String> {
236 let mut out = String::from("if ");
237 loop {
238 let lhs = self.decode_value(ValueMode::Lhs, None)?;
239 let cmp = usize::from(self.read_u8()?);
240 let rhs = self.decode_value(ValueMode::Rhs, None)?;
241 let conj = self.read_u8()?;
242 let cmp_text = COMPARE_OPS.get(cmp).copied().unwrap_or("?");
243 out.push_str(&lhs);
244 out.push(' ');
245 out.push_str(cmp_text);
246 out.push(' ');
247 out.push_str(&rhs);
248 match conj {
249 1 => out.push_str(" and "),
250 2 => out.push_str(" or "),
251 _ => break,
252 }
253 }
254
255 let end_offset = self.read_i32()?;
256 let block_end = usize::try_from(end_offset.max(0)).unwrap_or(self.code.len());
257 let target_end = block_end.min(self.code.len());
258 if target_end > self.pc {
259 let mut nested = Vec::new();
260 while self.pc < target_end {
261 let nested_start = self.pc;
262 match self.decode_value(ValueMode::Main, None) {
263 Ok(text) => nested.push(format!("{nested_start}:{text}")),
264 Err(err) => {
265 nested.push(format!("{nested_start}:<error: {err}>"));
266 self.pc = target_end;
267 break;
268 }
269 }
270 }
271 if !nested.is_empty() {
272 out.push_str(" { ");
273 out.push_str(&nested.join("; "));
274 out.push_str(" }");
275 }
276 }
277 Ok(out)
278 }
279
280 fn decode_if_block(&mut self, addr: usize) -> Result<(), String> {
281 let mut condition = String::from("if ");
282 loop {
283 let lhs = self.decode_value(ValueMode::Lhs, None)?;
284 let cmp = usize::from(self.read_u8()?);
285 let rhs = self.decode_value(ValueMode::Rhs, None)?;
286 let conj = self.read_u8()?;
287 let cmp_text = COMPARE_OPS.get(cmp).copied().unwrap_or("?");
288 condition.push_str(&lhs);
289 condition.push(' ');
290 condition.push_str(cmp_text);
291 condition.push(' ');
292 condition.push_str(&rhs);
293 match conj {
294 1 => condition.push_str(" and "),
295 2 => condition.push_str(" or "),
296 _ => break,
297 }
298 }
299
300 let end_offset = self.read_i32()?;
301 let block_end = usize::try_from(end_offset.max(0))
302 .unwrap_or(self.code.len())
303 .min(self.code.len());
304
305 self.instructions.push(ScriptInstruction {
306 addr,
307 indent: self.indent,
308 text: condition,
309 });
310
311 self.indent += 1;
312 while self.pc < block_end {
313 if let Err(err) = self.decode_statement() {
314 self.instructions.push(ScriptInstruction {
315 addr: self.pc,
316 indent: self.indent,
317 text: format!("<error: {err}>"),
318 });
319 self.pc = block_end;
320 break;
321 }
322 }
323 self.indent -= 1;
324
325 Ok(())
326 }
327
328 fn decode_statement(&mut self) -> Result<(), String> {
329 let addr = self.pc;
330 let opcode = *self
331 .code
332 .get(self.pc)
333 .ok_or_else(|| "unexpected end of script".to_string())?;
334
335 if opcode == 0x03 {
336 self.pc += 1;
337 self.decode_if_block(addr)?;
338 } else if opcode == 0x1E {
339 self.pc += 1;
340 let rv = self.read_u8()?;
341 let condition = format!("if <ScriptRv> = {rv}");
342 self.instructions.push(ScriptInstruction {
343 addr,
344 indent: self.indent,
345 text: condition,
346 });
347
348 let block_end = self.pc.saturating_add(4).min(self.code.len());
349 self.indent += 1;
350 while self.pc < block_end {
351 if let Err(err) = self.decode_statement() {
352 self.instructions.push(ScriptInstruction {
353 addr: self.pc,
354 indent: self.indent,
355 text: format!("<error: {err}>"),
356 });
357 self.pc = block_end;
358 break;
359 }
360 }
361 self.indent -= 1;
362 } else {
363 let text = self.decode_value(ValueMode::Main, None)?;
364 self.instructions.push(ScriptInstruction {
365 addr,
366 indent: self.indent,
367 text,
368 });
369 }
370
371 Ok(())
372 }
373
374 fn decode_call(&mut self, call_type: u8) -> Result<String, String> {
375 let func_id = self.read_u16()?;
376 let name = self.function_name(func_id);
377 let param_count = if func_id == 0 {
378 0
379 } else {
380 usize::from(self.read_u8()?)
381 };
382 let mut params = Vec::with_capacity(param_count);
383 for param_idx in 0..param_count {
384 params.push(self.decode_value(ValueMode::Parameter, Some((&name, param_idx)))?);
385 }
386 let prefix = if call_type == 1 { "@" } else { "" };
387 Ok(format!("{prefix}{name}({})", params.join(", ")))
388 }
389
390 fn decode_numeric_parameter(&self, value: i32, context: Option<(&str, usize)>) -> String {
391 let Some((function_name, param_index)) = context else {
392 return value.to_string();
393 };
394 if param_index != 0 {
395 return value.to_string();
396 }
397 if DIALOGUE_FUNCTIONS
398 .iter()
399 .any(|name| name.eq_ignore_ascii_case(function_name))
400 {
401 let chars = value.to_le_bytes();
402 if chars.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
403 return format!(
404 "\"{}\"",
405 chars
406 .iter()
407 .map(|b| char::from(*b))
408 .collect::<String>()
409 .trim_end_matches('\0')
410 );
411 }
412 }
413 if ATTRIBUTE_FUNCTIONS
414 .iter()
415 .any(|name| name.eq_ignore_ascii_case(function_name))
416 && let Some(attr_name) = self
417 .soup_def
418 .and_then(|def| def.attributes.get(usize::try_from(value).ok()?))
419 {
420 return attr_name.clone();
421 }
422 if ANIM_GROUP_FUNCTIONS
423 .iter()
424 .any(|name| name.eq_ignore_ascii_case(function_name))
425 && let Some(anim_name) = self
426 .soup_def
427 .and_then(|def| def.anim_groups.get(usize::try_from(value).ok()?))
428 {
429 return anim_name.clone();
430 }
431 if AXIS_FUNCTIONS
432 .iter()
433 .any(|name| name.eq_ignore_ascii_case(function_name))
434 && let Some(axis_name) =
435 AXIS_NAMES.get(usize::try_from(value).ok().unwrap_or(usize::MAX))
436 {
437 return (*axis_name).to_string();
438 }
439 value.to_string()
440 }
441
442 fn decode_value(
443 &mut self,
444 mode: ValueMode,
445 param_context: Option<(&str, usize)>,
446 ) -> Result<String, String> {
447 let opcode = self.read_u8()?;
448 match opcode {
449 0x00 => self.decode_call(0),
450 0x01 => self.decode_call(1),
451 0x02 => self.decode_call(2),
452 0x03 => self.decode_if(),
453 0x04 => Ok(format!("Goto {}", Self::label_name(self.read_i32()?))),
454 0x05 => {
455 let target = self.read_i32()?;
456 if target == 0 {
457 Ok("End".to_string())
458 } else {
459 Ok(format!("End {}", Self::label_name(target)))
460 }
461 }
462 0x06 => {
463 let flag_id = self.read_u16()?;
464 let mut out = self.flag_name(flag_id);
465 match mode {
466 ValueMode::Main => out.push_str(&self.decode_formula()?),
467 ValueMode::Lhs | ValueMode::Rhs => {
468 let _ = self.read_u8()?;
469 }
470 ValueMode::Parameter => {
471 self.skip(2)?;
472 }
473 ValueMode::Formula => {}
474 }
475 Ok(out)
476 }
477 0x07 | 0x16 => {
478 let value = self.read_i32()?;
479 if mode == ValueMode::Parameter {
480 Ok(self.decode_numeric_parameter(value, param_context))
481 } else {
482 Ok(value.to_string())
483 }
484 }
485 0x0A => {
486 let idx = self.read_u8()?;
487 let mut out = format!("var{idx}");
488 match mode {
489 ValueMode::Main => out.push_str(&self.decode_formula()?),
490 ValueMode::Lhs | ValueMode::Rhs => {
491 let _ = self.read_u8()?;
492 }
493 ValueMode::Parameter => {
494 self.skip(3)?;
495 }
496 ValueMode::Formula => {}
497 }
498 Ok(out)
499 }
500 0x0F => Ok(format!(
501 "{}.{}++",
502 self.decode_object()?,
503 self.decode_ref()?
504 )),
505 0x10 => Ok(format!(
506 "{}.{}--",
507 self.decode_object()?,
508 self.decode_ref()?
509 )),
510 0x11 => Ok(format!("Gosub {}", Self::label_name(self.read_i32()?))),
511 0x12 => Ok("Return".to_string()),
512 0x13 => Ok("Endint".to_string()),
513 0x14 => {
514 let mut out = format!("{}.{}", self.decode_object()?, self.decode_ref()?);
515 match mode {
516 ValueMode::Main => out.push_str(&self.decode_formula()?),
517 ValueMode::Lhs => {
518 let _ = self.read_u8()?;
519 }
520 ValueMode::Rhs | ValueMode::Parameter | ValueMode::Formula => {}
521 }
522 Ok(out)
523 }
524 0x15 => {
525 let idx = usize::try_from(self.read_i32()?.max(0)).unwrap_or(0);
526 let value = self
527 .strings
528 .get(idx)
529 .map_or_else(|| format!("strings[{idx}]"), |s| format!("\"{s}\""));
530 Ok(value)
531 }
532 0x17 => Ok(format!("<Anchor>={}", self.read_u8()?)),
533 0x19 | 0x1A => {
534 let object = self.decode_object()?;
535 if mode == ValueMode::Main {
536 let inner = self.decode_value(ValueMode::Formula, None)?;
537 Ok(format!("{object}.{inner}"))
538 } else {
539 Ok(format!("{object}.{}", self.decode_call(2)?))
540 }
541 }
542 0x1B => Ok(format!(
543 "<TaskStop({})>",
544 Self::label_name(self.read_i32()?)
545 )),
546 0x1E => {
547 let rv = self.read_u8()?;
548 let b0 = self.read_u8()?;
549 let b1 = self.read_u8()?;
550 let b2 = self.read_u8()?;
551 let b3 = self.read_u8()?;
552 Ok(format!(
553 "if <ScriptRv> = {rv} [block {:02X} {:02X} {:02X} {:02X}]",
554 b0, b1, b2, b3
555 ))
556 }
557 _ => Err(format!("unknown opcode 0x{opcode:02X}")),
558 }
559 }
560}
561
562fn read_null_terminated_string(bytes: &[u8], offset: usize) -> Option<String> {
563 let slice = bytes.get(offset..)?;
564 let end = slice.iter().position(|b| *b == 0).unwrap_or(slice.len());
565 Some(String::from_utf8_lossy(&slice[..end]).to_string())
566}
567
568fn read_actor_strings(
569 num_strings: i32,
570 string_offsets_index: i32,
571 rast: &[u8],
572 rasb: &[u8],
573) -> Vec<String> {
574 let mut out = Vec::new();
575 if num_strings <= 0 || string_offsets_index < 0 {
576 return out;
577 }
578
579 let base = usize::try_from(string_offsets_index).unwrap_or(0);
580 let count = usize::try_from(num_strings).unwrap_or(0);
581 for i in 0..count {
582 let offset = base + i * 4;
583 let Some(str_off) = read_i32_le(rasb, offset) else {
584 break;
585 };
586 let Ok(str_off) = usize::try_from(str_off.max(0)) else {
587 out.push(String::new());
588 continue;
589 };
590 out.push(read_null_terminated_string(rast, str_off).unwrap_or_default());
591 }
592 out
593}
594
595fn read_actor_variables(num_vars: i32, variable_offset: i32, rava: &[u8]) -> Vec<i32> {
596 let mut out = Vec::new();
597 if num_vars <= 0 || variable_offset < 0 {
598 return out;
599 }
600
601 let Ok(base_index) = usize::try_from(variable_offset / 4) else {
602 return out;
603 };
604 let count = usize::try_from(num_vars).unwrap_or(0);
605 for i in 0..count {
606 let byte_offset = (base_index + i) * 4;
607 let Some(value) = read_i32_le(rava, byte_offset) else {
608 break;
609 };
610 out.push(value);
611 }
612 out
613}
614
615fn disassemble_script(
616 code: &[u8],
617 script_pc: i32,
618 strings: &[String],
619 soup_def: Option<&SoupDef>,
620) -> Vec<ScriptInstruction> {
621 let mut decoder = Decoder::new(code, script_pc, strings, soup_def);
622
623 if decoder.pc > code.len() {
624 decoder.instructions.push(ScriptInstruction {
625 addr: code.len(),
626 indent: 0,
627 text: format!("<error: script_pc {} out of bounds>", script_pc),
628 });
629 return decoder.instructions;
630 }
631
632 while decoder.pc < code.len() {
633 if let Err(err) = decoder.decode_statement() {
634 decoder.instructions.push(ScriptInstruction {
635 addr: decoder.pc,
636 indent: decoder.indent,
637 text: format!("<error: {err}>"),
638 });
639 break;
640 }
641 }
642
643 decoder.instructions
644}
645
646#[must_use]
647pub fn disassemble_actor_scripts(
648 rahd_data: &[u8],
649 rasc_data: &[u8],
650 rast_data: &[u8],
651 rasb_data: &[u8],
652 rava_data: &[u8],
653 soup_def: Option<&SoupDef>,
654) -> Vec<(String, ActorScript)> {
655 let mut out = Vec::new();
656 if rahd_data.len() < 8 {
657 return out;
658 }
659
660 let count =
661 u32::from_le_bytes([rahd_data[0], rahd_data[1], rahd_data[2], rahd_data[3]]) as usize;
662
663 for i in 0..count {
664 let rec_off = 8 + i * RAHD_ITEM_SIZE;
665 if rec_off + RAHD_ITEM_SIZE > rahd_data.len() {
666 break;
667 }
668 let item = &rahd_data[rec_off..rec_off + RAHD_ITEM_SIZE];
669 let instances = u16::from_le_bytes([item[0x0D], item[0x0E]]);
670
671 let script_name = read_script_name_9(item, 4).unwrap_or_default();
672 if script_name.is_empty() {
673 continue;
674 }
675
676 let num_strings = read_i32_le(item, 0x41).unwrap_or(0);
677 let string_offsets_index = read_i32_le(item, 0x49).unwrap_or(0);
678 let script_length = read_i32_le(item, 0x4D).unwrap_or(0);
679 let script_data_offset = read_i32_le(item, 0x51).unwrap_or(0);
680 let script_pc = read_i32_le(item, 0x55).unwrap_or(0);
681 let num_variables = read_i32_le(item, 0x75).unwrap_or(0);
682 let variable_offset = read_i32_le(item, 0x7D).unwrap_or(0);
683
684 if script_length <= 0 {
685 continue;
686 }
687
688 let strings = read_actor_strings(num_strings, string_offsets_index, rast_data, rasb_data);
689 let variables = read_actor_variables(num_variables, variable_offset, rava_data);
690 let instance_variables = (0..i32::from(instances))
691 .map(|instance| {
692 instance
693 .checked_mul(num_variables)
694 .and_then(|offset| offset.checked_mul(4))
695 .and_then(|offset| variable_offset.checked_add(offset))
696 .map(|offset| read_actor_variables(num_variables, offset, rava_data))
697 .unwrap_or_default()
698 })
699 .collect();
700
701 let mut instructions = Vec::new();
702 let mut bytecode = Vec::new();
703 let start = usize::try_from(script_data_offset.max(0)).unwrap_or(0);
704 let Ok(script_len) = usize::try_from(script_length.max(0)) else {
705 continue;
706 };
707 let end = start.saturating_add(script_len);
708 if start >= rasc_data.len() || end > rasc_data.len() {
709 instructions.push(ScriptInstruction {
710 addr: 0,
711 indent: 0,
712 text: format!(
713 "<error: script slice out of bounds offset={} length={}>",
714 script_data_offset, script_length
715 ),
716 });
717 } else {
718 let code = &rasc_data[start..end];
719 bytecode.extend_from_slice(code);
720 instructions = disassemble_script(code, script_pc, &strings, soup_def);
721 }
722
723 out.push((
724 script_name,
725 ActorScript {
726 header_index: i,
727 instances,
728 instance_variables,
729 bytecode,
730 script_length,
731 script_data_offset,
732 script_pc,
733 num_strings,
734 num_variables,
735 strings,
736 variables,
737 instructions,
738 },
739 ));
740 }
741
742 out
743}
744
745#[must_use]
746pub fn format_soup_text(
747 name: &str,
748 source_file: &str,
749 script: &ActorScript,
750 rtx_labels: Option<&std::collections::HashMap<String, String>>,
751) -> String {
752 let mut out = String::new();
753
754 let _ = writeln!(out, "; SOUP script: {name}");
755 let _ = writeln!(out, "; Source: {source_file}");
756 out.push('\n');
757
758 for inst in &script.instructions {
759 let indent_str = " ".repeat(inst.indent);
760 let _ = write!(out, "{:04X}:\t{indent_str}{}", inst.addr, inst.text);
761 if let Some(labels) = rtx_labels {
762 for rtx_key in extract_rtx_keys(&inst.text) {
763 if let Some(label) = labels.get(&rtx_key) {
764 let truncated = if label.len() > 80 {
765 format!("{}...", &label[..77])
766 } else {
767 label.clone()
768 };
769 let _ = write!(out, "\t; {truncated}");
770 break;
771 }
772 }
773 }
774 out.push('\n');
775 }
776
777 out
778}
779
780fn extract_rtx_keys(text: &str) -> Vec<String> {
781 let mut keys = Vec::new();
782 let mut chars = text.char_indices().peekable();
783 while let Some((i, ch)) = chars.next() {
784 if ch == '"' {
785 let start = i + 1;
786 let mut end = start;
787 for (j, c) in chars.by_ref() {
788 if c == '"' {
789 end = j;
790 break;
791 }
792 }
793 if end > start && end - start <= 4 {
794 keys.push(text[start..end].to_string());
795 }
796 }
797 }
798 keys
799}
800
801#[must_use]
802pub fn script_metadata_json(script: &ActorScript) -> serde_json::Value {
803 serde_json::json!({
804 "script_length": script.script_length,
805 "script_data_offset": script.script_data_offset,
806 "script_pc": script.script_pc,
807 "num_variables": script.num_variables,
808 "variables": script.variables,
809 "num_strings": script.num_strings,
810 "strings": script.strings,
811 })
812}