Skip to main content

rgpre/import/rgm/
script_program.rs

1//! Typed SOUP instructions and expressions from the documented bytecode contract.
2use crate::import::soup_def::SoupDef;
3use serde::{Deserialize, Serialize};
4
5/// A single decoded statement, with offsets relative to its actor's script slice.
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct Step {
8    pub next: u32,
9    pub operation: Operation,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum Operation {
15    Call {
16        call: Call,
17    },
18    ObjectCall {
19        call: Call,
20    },
21    Assign {
22        destination: Place,
23        formula: Formula,
24    },
25    Increment {
26        destination: Place,
27        delta: i32,
28    },
29    If {
30        conditions: Vec<Condition>,
31        target: u32,
32    },
33    Goto {
34        target: u32,
35    },
36    End {
37        target: u32,
38    },
39    Gosub {
40        target: u32,
41    },
42    Return,
43    Endint,
44    Anchor {
45        value: u8,
46    },
47    Yield,
48    TaskStop {
49        label: u32,
50    },
51    TaskPause {
52        label: u32,
53    },
54    TaskResume {
55        label: u32,
56    },
57    ScriptRv {
58        expected: u8,
59        target: u32,
60        save_return: bool,
61    },
62    Unsupported {
63        reason: String,
64    },
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct Call {
69    /// Original index in the installed SOUP386.DEF, including its synthetic null entry.
70    pub function_id: u16,
71    pub kind: CallKind,
72    pub target: Object,
73    pub function: String,
74    pub arguments: Vec<Value>,
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
78#[serde(rename_all = "snake_case")]
79pub enum CallKind {
80    Task,
81    Multitask,
82    Function,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(tag = "type", rename_all = "snake_case")]
87pub enum Object {
88    Me,
89    Player,
90    Camera,
91    Named { string: u32 },
92    Local { index: u8 },
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(tag = "type", rename_all = "snake_case")]
97pub enum Place {
98    Flag { index: u16 },
99    Local { index: u8 },
100    Property { object: Object, reference: u8 },
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
104#[serde(tag = "type", rename_all = "snake_case")]
105pub enum Value {
106    Integer { value: i32 },
107    String { index: u32 },
108    Read { place: Place },
109    Update { place: Place, delta: i32 },
110    Call { call: Box<Call> },
111}
112
113/// Formula operators retain their byte values; evaluation is owned by the consumer.
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
115pub struct Formula {
116    pub first: Value,
117    pub rest: Vec<(u8, Value)>,
118}
119
120/// Conjunction joins this comparison to the next: zero ends, one AND, two OR.
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub struct Condition {
123    pub lhs: Value,
124    pub comparison: u8,
125    pub rhs: Value,
126    pub conjunction: u8,
127}
128
129#[derive(Clone, Copy)]
130enum Mode {
131    Lhs,
132    Rhs,
133    Parameter,
134    Formula,
135}
136
137struct Decoder<'a> {
138    bytes: &'a [u8],
139    cursor: usize,
140    definition: &'a SoupDef,
141    depth: usize,
142}
143
144impl Decoder<'_> {
145    fn read(&mut self, count: usize) -> Result<&[u8], String> {
146        let end = self
147            .cursor
148            .checked_add(count)
149            .ok_or("operand offset overflow")?;
150        let bytes = self
151            .bytes
152            .get(self.cursor..end)
153            .ok_or_else(|| format!("truncated operand at {}", self.cursor))?;
154        self.cursor = end;
155        Ok(bytes)
156    }
157    fn byte(&mut self) -> Result<u8, String> {
158        Ok(self.read(1)?[0])
159    }
160    fn word(&mut self) -> Result<u16, String> {
161        Ok(u16::from_le_bytes(self.read(2)?.try_into().unwrap()))
162    }
163    fn dword(&mut self) -> Result<u32, String> {
164        Ok(u32::from_le_bytes(self.read(4)?.try_into().unwrap()))
165    }
166    fn object(&mut self) -> Result<Object, String> {
167        let kind = self.byte()?;
168        let operand = self.byte()?;
169        Ok(match kind {
170            0 => Object::Me,
171            1 => Object::Player,
172            2 => Object::Camera,
173            4 => Object::Named {
174                string: u32::from(operand),
175            },
176            10 => Object::Local { index: operand },
177            _ => return Err(format!("unsupported object kind {kind}")),
178        })
179    }
180    fn place(&mut self, opcode: u8) -> Result<Place, String> {
181        Ok(match opcode {
182            6 => Place::Flag {
183                index: self.word()?,
184            },
185            10 => Place::Local {
186                index: self.byte()?,
187            },
188            20 => Place::Property {
189                object: self.object()?,
190                reference: self.word()? as u8,
191            },
192            _ => return Err(format!("unsupported assignment opcode {opcode}")),
193        })
194    }
195    fn call(&mut self, target: Object, kind: CallKind) -> Result<Call, String> {
196        let id = self.word()?;
197        let count = if id == 0 { 0 } else { self.byte()? };
198        self.call_arguments(id, count, target, kind)
199    }
200    fn call_arguments(
201        &mut self,
202        id: u16,
203        count: u8,
204        target: Object,
205        kind: CallKind,
206    ) -> Result<Call, String> {
207        let function = self
208            .definition
209            .functions
210            .get(usize::from(id))
211            .ok_or_else(|| format!("SOUP386.DEF function {id} absent"))?;
212        if count != function.param_count {
213            return Err(format!(
214                "function {} requires {} arguments, found {count}",
215                function.name, function.param_count
216            ));
217        }
218        let name = function.name.clone();
219        let mut arguments = Vec::with_capacity(usize::from(count));
220        for _ in 0..count {
221            arguments.push(self.value(Mode::Parameter)?);
222        }
223        Ok(Call {
224            function_id: id,
225            target,
226            kind,
227            function: name,
228            arguments,
229        })
230    }
231    fn kind(&mut self) -> Result<CallKind, String> {
232        match self.byte()? {
233            0 => Ok(CallKind::Task),
234            1 => Ok(CallKind::Multitask),
235            2 => Ok(CallKind::Function),
236            byte => Err(format!("unsupported call kind {byte}")),
237        }
238    }
239    fn value(&mut self, mode: Mode) -> Result<Value, String> {
240        // Depth is a malformed-input limit, never a simulated yield.
241        if self.depth == 128 {
242            return Err("expression nesting limit exceeded".into());
243        }
244        self.depth += 1;
245        let result = self.value_inner(mode);
246        self.depth -= 1;
247        result
248    }
249    fn value_inner(&mut self, mode: Mode) -> Result<Value, String> {
250        let opcode = self.byte()?;
251        if matches!(mode, Mode::Parameter) && matches!(opcode, 2 | 25 | 26) {
252            return Err("unsupported native call in parameter mode".into());
253        }
254        Ok(match opcode {
255            7 => Value::Integer {
256                value: self.dword()? as i32,
257            },
258            22 => {
259                let value = self.dword()?;
260                Value::Integer {
261                    value: if matches!(mode, Mode::Parameter) {
262                        i32::from(value as u16)
263                    } else {
264                        value as i32
265                    },
266                }
267            }
268            21 => Value::String {
269                index: {
270                    let value = self.dword()?;
271                    if matches!(mode, Mode::Parameter) {
272                        u32::from(value as u8)
273                    } else {
274                        value
275                    }
276                },
277            },
278            6 | 10 | 20 => {
279                let place = self.place(opcode)?;
280                let extra = match (opcode, mode) {
281                    (6 | 10 | 20, Mode::Lhs | Mode::Rhs) => 1,
282                    (6, Mode::Parameter) => 2,
283                    (10, Mode::Parameter) => 3,
284                    _ => 0,
285                };
286                let trailing = self.read(extra)?;
287                if matches!(mode, Mode::Lhs | Mode::Rhs) {
288                    match trailing[0] {
289                        0 => Value::Read { place },
290                        10 => Value::Update { place, delta: 1 },
291                        11 => Value::Update { place, delta: -1 },
292                        value => return Err(format!("unsupported conditional update {value}")),
293                    }
294                } else {
295                    Value::Read { place }
296                }
297            }
298            2 => Value::Call {
299                call: Box::new(self.call(Object::Me, CallKind::Function)?),
300            },
301            25 | 26 => {
302                let target = self.object()?;
303                Value::Call {
304                    call: Box::new(self.call(target, CallKind::Function)?),
305                }
306            }
307            _ => return Err(format!("unsupported value opcode {opcode}")),
308        })
309    }
310    fn formula(&mut self) -> Result<Formula, String> {
311        let first = self.value(Mode::Formula)?;
312        let mut rest = Vec::new();
313        loop {
314            let operator = self.byte()?;
315            match operator {
316                0 => break,
317                1..=9 => rest.push((operator, self.value(Mode::Formula)?)),
318                10 | 11 => return Err(format!("invalid formula operator {operator}")),
319                _ => return Err(format!("invalid formula operator {operator}")),
320            }
321        }
322        Ok(Formula { first, rest })
323    }
324    fn statement(&mut self) -> Result<Operation, String> {
325        let opcode = self.byte()?;
326        Ok(match opcode {
327            0..=2 => {
328                let kind =
329                    [CallKind::Task, CallKind::Multitask, CallKind::Function][usize::from(opcode)];
330                Operation::Call {
331                    call: self.call(Object::Me, kind)?,
332                }
333            }
334            3 => {
335                let mut conditions = Vec::new();
336                loop {
337                    let lhs = self.value(Mode::Lhs)?;
338                    let comparison = self.byte()?;
339                    if comparison > 5 {
340                        return Err(format!("invalid comparison {comparison}"));
341                    }
342                    let rhs = self.value(Mode::Rhs)?;
343                    let conjunction = self.byte()?;
344                    if conjunction > 2 {
345                        return Err(format!("invalid conjunction {conjunction}"));
346                    }
347                    conditions.push(Condition {
348                        lhs,
349                        comparison,
350                        rhs,
351                        conjunction,
352                    });
353                    if conjunction == 0 {
354                        break;
355                    }
356                }
357                Operation::If {
358                    conditions,
359                    target: self.dword()?,
360                }
361            }
362            4 => Operation::Goto {
363                target: self.dword()?,
364            },
365            5 => Operation::End {
366                target: self.dword()?,
367            },
368            6 | 10 | 20 => Operation::Assign {
369                destination: self.place(opcode)?,
370                formula: self.formula()?,
371            },
372            15 | 16 => Operation::Increment {
373                destination: Place::Property {
374                    object: self.object()?,
375                    reference: self.word()? as u8,
376                },
377                delta: if opcode == 15 { 1 } else { -1 },
378            },
379            17 => Operation::Gosub {
380                target: self.dword()?,
381            },
382            18 => Operation::Return,
383            19 => Operation::Endint,
384            23 => Operation::Anchor {
385                value: self.byte()?,
386            },
387            24 => Operation::Yield,
388            25 | 26 => {
389                let target = self.object()?;
390                let kind = self.kind()?;
391                Operation::ObjectCall {
392                    call: self.call(target, kind)?,
393                }
394            }
395            27 => Operation::TaskStop {
396                label: self.dword()?,
397            },
398            28 => Operation::TaskPause {
399                label: self.dword()?,
400            },
401            29 => Operation::TaskResume {
402                label: self.dword()?,
403            },
404            30 => {
405                let expected = self.byte()?;
406                let target = u32::try_from(self.cursor + 5)
407                    .map_err(|_| "ScriptRV continuation exceeds u32")?;
408                let opcode = *self
409                    .bytes
410                    .get(self.cursor)
411                    .ok_or("truncated ScriptRV body")?;
412                if opcode == 18 {
413                    self.read(4)?;
414                }
415                Operation::ScriptRv {
416                    expected,
417                    target,
418                    save_return: opcode == 17,
419                }
420            }
421            _ => return Err(format!("unsupported opcode {opcode}")),
422        })
423    }
424}
425
426/// Decode one actor's RAHK native hook, independently of its RASC continuations.
427/// A zero RAHD hook offset means no call; nonzero offsets are relative to RAHK.
428/// The stored record uses a function index, count byte and five-byte parameters.
429///
430/// # Errors
431/// Returns a contextual error for absent/truncated sections, invalid offsets,
432/// definition mismatches or unsupported parameter encodings.
433pub fn decode_native_hook(
434    rgm: &super::RgmFile,
435    header_index: usize,
436    definition: &SoupDef,
437) -> Result<Option<Call>, String> {
438    let decode = || -> Result<Option<Call>, String> {
439        let headers = rgm
440            .sections
441            .iter()
442            .find_map(|section| match section {
443                super::RgmSection::Rahd(_, data) => Some(data.as_slice()),
444                _ => None,
445            })
446            .ok_or("RAHD section absent")?;
447        let count = u32::from_le_bytes(
448            headers
449                .get(..4)
450                .ok_or("RAHD count truncated")?
451                .try_into()
452                .unwrap(),
453        );
454        if header_index >= count as usize {
455            return Err("RAHD index outside record count".into());
456        }
457        let start = header_index
458            .checked_mul(165)
459            .and_then(|offset| offset.checked_add(8))
460            .ok_or("RAHD record offset overflow")?;
461        let end = start.checked_add(165).ok_or("RAHD record end overflow")?;
462        let header = headers.get(start..end).ok_or("RAHD record truncated")?;
463        let offset = super::shared::read_i32_le(header, 0x5d).ok_or("RAHD hook offset absent")?;
464        if offset == 0 {
465            return Ok(None);
466        }
467        let offset = usize::try_from(offset).map_err(|_| "negative RAHK offset")?;
468        let bytes = rgm
469            .sections
470            .iter()
471            .find_map(|section| match section {
472                super::RgmSection::Rahk(_, data) => Some(data.as_slice()),
473                _ => None,
474            })
475            .ok_or("RAHK section absent")?;
476        let mut decoder = Decoder {
477            bytes,
478            cursor: offset,
479            definition,
480            depth: 0,
481        };
482        let function = decoder.word()?;
483        let count = decoder.byte()?;
484        if count > 8 {
485            return Err(format!("RAHK argument count {count} exceeds eight"));
486        }
487        let end = decoder
488            .cursor
489            .checked_add(usize::from(count) * 5)
490            .ok_or("RAHK record end overflow")?;
491        decoder.bytes = bytes.get(..end).ok_or("RAHK arguments truncated")?;
492        let call = decoder.call_arguments(function, count, Object::Me, CallKind::Function)?;
493        if decoder.cursor != end {
494            return Err("RAHK arguments do not occupy five bytes each".into());
495        }
496        Ok(Some(call))
497    };
498    decode().map_err(|error| format!("actor header {header_index} native hook: {error}"))
499}
500
501/// Decodes one statement without traversing branches or executing native handlers.
502/// Unsupported encodings and malformed inputs retain the actor-relative instruction.
503pub fn decode_script_step(
504    bytes: &[u8],
505    offset: usize,
506    definition: &SoupDef,
507) -> Result<Step, String> {
508    let mut decoder = Decoder {
509        bytes,
510        cursor: offset,
511        definition,
512        depth: 0,
513    };
514    let operation = decoder
515        .statement()
516        .map_err(|error| format!("script instruction {offset}: {error}"))?;
517    let next = u32::try_from(decoder.cursor)
518        .map_err(|_| format!("script instruction {offset}: continuation exceeds u32"))?;
519    Ok(Step { next, operation })
520}
521
522/// Follows all explicit continuations from the main and optional interrupt entries.
523/// Unknown instructions remain terminal unsupported records. Malformed known encodings
524/// and branches into operands fail instead of inventing instruction boundaries.
525pub fn decode_script_program(
526    bytes: &[u8],
527    entry: u32,
528    definition: &SoupDef,
529) -> Result<std::collections::BTreeMap<u32, Step>, String> {
530    use std::collections::{BTreeMap, BTreeSet};
531    let mut pending = BTreeSet::from([0, entry]);
532    let mut steps: BTreeMap<u32, Step> = BTreeMap::new();
533    while let Some(offset) = pending.pop_first() {
534        if steps.contains_key(&offset) {
535            continue;
536        }
537        if offset as usize >= bytes.len() {
538            return Err(format!(
539                "script instruction {offset}: target outside script"
540            ));
541        }
542        if let Some((&start, step)) = steps.range(..offset).next_back()
543            && offset < step.next
544        {
545            return Err(format!(
546                "script instruction {offset}: target inside instruction {start}"
547            ));
548        }
549        let step = match decode_script_step(bytes, offset as usize, definition) {
550            Ok(step) => step,
551            Err(reason) if reason.contains(": unsupported ") => Step {
552                next: offset,
553                operation: Operation::Unsupported { reason },
554            },
555            Err(reason) => return Err(reason),
556        };
557        if let Some((&start, _)) = steps.range(offset..step.next).next() {
558            return Err(format!(
559                "script instruction {offset}: overlaps instruction {start}"
560            ));
561        }
562        match &step.operation {
563            Operation::Goto { target } | Operation::End { target } => {
564                pending.insert(*target);
565            }
566            Operation::If { target, .. }
567            | Operation::Gosub { target }
568            | Operation::ScriptRv { target, .. } => {
569                pending.insert(*target);
570                pending.insert(step.next);
571            }
572            Operation::Return | Operation::Endint | Operation::Unsupported { .. } => {}
573            _ => {
574                pending.insert(step.next);
575            }
576        }
577        steps.insert(offset, step);
578    }
579    Ok(steps)
580}