Skip to main content

siglus_scene_vm/runtime/
unknown.rs

1//! Runtime recorder for unmapped numeric codes, names, and load-time notes.
2//!
3//! During reverse-engineering and incremental porting, it is common to encounter
4//! numeric form codes that are not mapped to a known handler yet.
5//! We also keep non-fatal load-time notes here so the runtime can continue.
6
7use super::opcode::OpCode;
8use std::collections::BTreeMap;
9
10#[derive(Debug, Default)]
11pub struct UnknownOpRecorder {
12    /// Count unknown numeric codes.
13    pub codes: BTreeMap<OpCode, u64>,
14    /// Count unknown named commands.
15    pub names: BTreeMap<String, u64>,
16    /// Count non-fatal runtime or load-time notes.
17    pub notes: BTreeMap<String, u64>,
18
19    /// Observed element-chain signatures (e.g. "135:-1:0:2:-1:1:38").
20    ///
21    /// This is specifically useful when the project is missing a complete set
22    /// of element / command id constants.
23    pub element_chains: BTreeMap<String, u64>,
24}
25
26impl UnknownOpRecorder {
27    pub fn record_code(&mut self, code: OpCode) {
28        *self.codes.entry(code).or_insert(0) += 1;
29    }
30
31    pub fn record_name(&mut self, name: &str) {
32        *self.names.entry(name.to_string()).or_insert(0) += 1;
33    }
34
35    pub fn record_note(&mut self, tag: &str) {
36        *self.notes.entry(tag.to_string()).or_insert(0) += 1;
37    }
38
39    pub fn record_element_chain(&mut self, form_id: u32, chain: &[i32], note: &str) {
40        let mut s = String::new();
41        s.push_str(&form_id.to_string());
42        for c in chain {
43            s.push(':');
44            s.push_str(&c.to_string());
45        }
46        if !note.is_empty() {
47            s.push_str(" @");
48            s.push_str(note);
49        }
50        *self.element_chains.entry(s).or_insert(0) += 1;
51    }
52
53    /// A compact human-readable summary.
54    pub fn summary_string(&self, max_items: usize) -> String {
55        let mut out = String::new();
56
57        if !self.codes.is_empty() {
58            out.push_str("unknown numeric codes:\n");
59            for (i, (k, v)) in self.codes.iter().enumerate() {
60                if i >= max_items {
61                    out.push_str("  ...\n");
62                    break;
63                }
64                out.push_str(&format!("  form {} x{}\n", k.id, v));
65            }
66        }
67
68        if !self.notes.is_empty() {
69            out.push_str("runtime notes:\n");
70            for (i, (k, v)) in self.notes.iter().enumerate() {
71                if i >= max_items {
72                    out.push_str("  ...\n");
73                    break;
74                }
75                out.push_str(&format!("  {k} x{v}\n"));
76            }
77        }
78
79        if !self.names.is_empty() {
80            out.push_str("unknown named commands:\n");
81            for (i, (k, v)) in self.names.iter().enumerate() {
82                if i >= max_items {
83                    out.push_str("  ...\n");
84                    break;
85                }
86                out.push_str(&format!("  {k} x{v}\n"));
87            }
88        }
89
90        if !self.element_chains.is_empty() {
91            out.push_str("unknown element chains:\n");
92            for (i, (k, v)) in self.element_chains.iter().enumerate() {
93                if i >= max_items {
94                    out.push_str("  ...\n");
95                    break;
96                }
97                out.push_str(&format!("  {k} x{v}\n"));
98            }
99        }
100
101        if out.is_empty() {
102            out.push_str("(no unknown ops recorded)\n");
103        }
104
105        out
106    }
107}