Skip to main content

rgpre/import/rgm/
script.rs

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    /// Index in authored RAHD order; names are not instance identifiers.
50    pub header_index: usize,
51    pub instances: u16,
52    /// Separate authored RAVA slices in instance allocation order.
53    pub instance_variables: Vec<Vec<i32>>,
54    /// The exact actor-local RASC slice. Empty when the stored slice is invalid.
55    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}