Skip to main content

lua2hcb_compiler/
meta.rs

1use anyhow::{anyhow, bail, Context, Result};
2use serde_yaml::Value;
3use std::collections::{BTreeMap, HashMap};
4use std::path::Path;
5
6#[derive(Clone, Copy, Debug)]
7pub enum Nls {
8    Utf8,
9    ShiftJis,
10    Gb18030,
11}
12
13impl Nls {
14    pub fn parse(s: &str) -> Result<Self> {
15        let ss = s.trim().to_ascii_lowercase();
16        match ss.as_str() {
17            "utf8" | "utf-8" => Ok(Nls::Utf8),
18            "sjis" | "shiftjis" | "shift_jis" | "shift-jis" => Ok(Nls::ShiftJis),
19            "gbk" | "gb18030" => Ok(Nls::Gb18030),
20            other => bail!("unsupported nls: {other}"),
21        }
22    }
23}
24
25#[derive(Clone, Debug)]
26pub struct Syscall {
27    pub id: u16,
28    pub args: u8,
29    pub name: String,
30}
31
32#[derive(Clone, Debug)]
33pub struct Meta {
34    pub nls: Nls,
35    pub game_title: String,
36    pub game_mode: u8,
37    pub game_mode_reserved: u8,
38    pub non_volatile_global_count: u16,
39    pub volatile_global_count: u16,
40    pub custom_syscall_count: u16,
41    pub syscalls: Vec<Syscall>,
42
43    name_to_id: HashMap<String, u16>,
44    id_to_args: HashMap<u16, u8>,
45}
46
47impl Meta {
48    pub fn syscall_id_by_name(&self, name: &str) -> Option<u16> {
49        self.name_to_id.get(name).copied()
50    }
51
52    pub fn syscall_args_by_id(&self, id: u16) -> Option<u8> {
53        self.id_to_args.get(&id).copied()
54    }
55
56    pub fn syscall_count(&self) -> u16 {
57        self.syscalls.len() as u16
58    }
59}
60
61fn as_u16(v: &Value, key: &str) -> Result<u16> {
62    match v {
63        Value::Number(n) => n
64            .as_u64()
65            .and_then(|x| u16::try_from(x).ok())
66            .ok_or_else(|| anyhow!("{key} must be a u16")),
67        _ => bail!("{key} must be a number"),
68    }
69}
70
71fn as_u8(v: &Value, key: &str) -> Result<u8> {
72    match v {
73        Value::Number(n) => n
74            .as_u64()
75            .and_then(|x| u8::try_from(x).ok())
76            .ok_or_else(|| anyhow!("{key} must be a u8")),
77        _ => bail!("{key} must be a number"),
78    }
79}
80
81fn as_u32_opt(v: Option<&Value>) -> Result<Option<u32>> {
82    if let Some(Value::Number(n)) = v {
83        return Ok(Some(
84            n.as_u64()
85                .and_then(|x| u32::try_from(x).ok())
86                .ok_or_else(|| anyhow!("value must be a u32"))?,
87        ));
88    }
89    Ok(None)
90}
91
92fn as_str(v: &Value, key: &str) -> Result<String> {
93    match v {
94        Value::String(s) => Ok(s.clone()),
95        _ => bail!("{key} must be a string"),
96    }
97}
98
99pub fn load_meta(path: &Path) -> Result<Meta> {
100    let txt =
101        std::fs::read_to_string(path).with_context(|| format!("read meta: {}", path.display()))?;
102    let doc: Value = serde_yaml::from_str(&txt).context("parse yaml")?;
103    let map = doc
104        .as_mapping()
105        .ok_or_else(|| anyhow!("meta must be a mapping"))?;
106
107    let get = |k: &str| -> Option<&Value> { map.get(&Value::String(k.to_string())) };
108
109    let nls = if let Some(v) = get("nls") {
110        Nls::parse(&as_str(v, "nls")?)?
111    } else {
112        Nls::ShiftJis
113    };
114
115    let game_title = if let Some(v) = get("game_title") {
116        as_str(v, "game_title")?
117    } else {
118        String::new()
119    };
120
121    let game_mode = if let Some(v) = get("game_mode") {
122        as_u8(v, "game_mode")?
123    } else {
124        0
125    };
126
127    let game_mode_reserved = if let Some(v) = get("game_mode_reserved") {
128        as_u8(v, "game_mode_reserved")?
129    } else {
130        0
131    };
132
133    let non_volatile_global_count = if let Some(v) = get("non_volatile_global_count") {
134        as_u16(v, "non_volatile_global_count")?
135    } else {
136        0
137    };
138
139    let volatile_global_count = if let Some(v) = get("volatile_global_count") {
140        as_u16(v, "volatile_global_count")?
141    } else {
142        0
143    };
144
145    let custom_syscall_count = if let Some(v) = get("custom_syscall_count") {
146        as_u16(v, "custom_syscall_count")?
147    } else {
148        0
149    };
150
151    // Accept legacy fields but ignore them (must be recomputed).
152    let _sys_desc_offset = as_u32_opt(get("sys_desc_offset")).unwrap_or(None);
153    let _entry_point = as_u32_opt(get("entry_point")).unwrap_or(None);
154
155    // Parse syscalls: either a sequence (legacy) or a mapping id -> {name,args} (new).
156    let syscalls_v = get("syscalls").ok_or_else(|| anyhow!("meta.syscalls is required"))?;
157
158    let mut syscalls: Vec<Syscall> = Vec::new();
159
160    match syscalls_v {
161        Value::Sequence(seq) => {
162            // Legacy: list of {name, args} objects, IDs are implicit 0..N-1.
163            for (i, it) in seq.iter().enumerate() {
164                let m = it
165                    .as_mapping()
166                    .ok_or_else(|| anyhow!("meta.syscalls[{i}] must be a mapping"))?;
167                let name = m
168                    .get(&Value::String("name".to_string()))
169                    .ok_or_else(|| anyhow!("meta.syscalls[{i}].name missing"))?;
170                let args = m.get(Value::String("args".to_string())).unwrap();
171
172                let name = as_str(name, "name")?;
173                let argc = as_u16(args, "args")?;
174                let argc_u8 =
175                    u8::try_from(argc).map_err(|_| anyhow!("syscall args must fit u8"))?;
176
177                syscalls.push(Syscall {
178                    id: u16::try_from(i).unwrap(),
179                    args: argc_u8,
180                    name,
181                });
182            }
183        }
184        Value::Mapping(m) => {
185            // New: id -> {name,args}
186            // Use BTreeMap for deterministic ordering.
187            let mut tmp: BTreeMap<u16, Syscall> = BTreeMap::new();
188            for (k, v) in m.iter() {
189                let id = match k {
190                    Value::Number(n) => n
191                        .as_u64()
192                        .and_then(|x| u16::try_from(x).ok())
193                        .ok_or_else(|| anyhow!("syscalls key must fit u16"))?,
194                    Value::String(s) => s
195                        .parse::<u16>()
196                        .map_err(|_| anyhow!("syscalls key must be integer"))?,
197                    _ => bail!("syscalls key must be integer"),
198                };
199
200                let vm = v
201                    .as_mapping()
202                    .ok_or_else(|| anyhow!("syscalls[{id}] must be a mapping"))?;
203
204                let name_v = vm
205                    .get(&Value::String("name".to_string()))
206                    .ok_or_else(|| anyhow!("syscalls[{id}].name missing"))?;
207                let args_v = vm
208                    .get(&Value::String("args".to_string()))
209                    .ok_or_else(|| anyhow!("syscalls[{id}].args missing"))?;
210
211                let name = as_str(name_v, "name")?;
212                let argc = as_u16(args_v, "args")?;
213                let argc_u8 =
214                    u8::try_from(argc).map_err(|_| anyhow!("syscall args must fit u8"))?;
215
216                if tmp.contains_key(&id) {
217                    bail!("duplicate syscall id: {id}");
218                }
219                tmp.insert(
220                    id,
221                    Syscall {
222                        id,
223                        args: argc_u8,
224                        name,
225                    },
226                );
227            }
228
229            // Validate contiguity if syscall_count exists.
230            let declared_count = get("syscall_count").and_then(|v| as_u16(v, "syscall_count").ok());
231            let count = if let Some(dc) = declared_count {
232                dc
233            } else {
234                // infer count as max_id+1
235                tmp.keys().next_back().map(|x| x + 1).unwrap_or(0)
236            };
237
238            for id in 0..count {
239                if !tmp.contains_key(&id) {
240                    bail!("missing syscall id {id} in meta.syscalls");
241                }
242            }
243
244            syscalls = tmp.into_values().collect();
245
246            if declared_count.is_some() {
247                if syscalls.len() != usize::from(count) {
248                    bail!(
249                        "syscall_count mismatch: declared {count}, found {}",
250                        syscalls.len()
251                    );
252                }
253            }
254        }
255        _ => bail!("meta.syscalls must be a sequence or mapping"),
256    }
257
258    let mut name_to_id: HashMap<String, u16> = HashMap::new();
259    let mut id_to_args: HashMap<u16, u8> = HashMap::new();
260    for sc in &syscalls {
261        if name_to_id.insert(sc.name.clone(), sc.id).is_some() {
262            bail!("duplicate syscall name: {}", sc.name);
263        }
264        id_to_args.insert(sc.id, sc.args);
265    }
266
267    Ok(Meta {
268        nls,
269        game_title,
270        game_mode,
271        game_mode_reserved,
272        non_volatile_global_count,
273        volatile_global_count,
274        custom_syscall_count,
275        syscalls,
276        name_to_id,
277        id_to_args,
278    })
279}