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 = std::fs::read_to_string(path).with_context(|| format!("read meta: {}", path.display()))?;
101    let doc: Value = serde_yaml::from_str(&txt).context("parse yaml")?;
102    let map = doc
103        .as_mapping()
104        .ok_or_else(|| anyhow!("meta must be a mapping"))?;
105
106    let get = |k: &str| -> Option<&Value> { map.get(&Value::String(k.to_string())) };
107
108    let nls = if let Some(v) = get("nls") {
109        Nls::parse(&as_str(v, "nls")?)?
110    } else {
111        Nls::ShiftJis
112    };
113
114    let game_title = if let Some(v) = get("game_title") {
115        as_str(v, "game_title")?
116    } else {
117        String::new()
118    };
119
120    let game_mode = if let Some(v) = get("game_mode") {
121        as_u8(v, "game_mode")?
122    } else {
123        0
124    };
125
126    let game_mode_reserved = if let Some(v) = get("game_mode_reserved") {
127        as_u8(v, "game_mode_reserved")?
128    } else {
129        0
130    };
131
132    let non_volatile_global_count = if let Some(v) = get("non_volatile_global_count") {
133        as_u16(v, "non_volatile_global_count")?
134    } else {
135        0
136    };
137
138    let volatile_global_count = if let Some(v) = get("volatile_global_count") {
139        as_u16(v, "volatile_global_count")?
140    } else {
141        0
142    };
143
144    let custom_syscall_count = if let Some(v) = get("custom_syscall_count") {
145        as_u16(v, "custom_syscall_count")?
146    } else {
147        0
148    };
149
150    // Accept legacy fields but ignore them (must be recomputed).
151    let _sys_desc_offset = as_u32_opt(get("sys_desc_offset")).unwrap_or(None);
152    let _entry_point = as_u32_opt(get("entry_point")).unwrap_or(None);
153
154    // Parse syscalls: either a sequence (legacy) or a mapping id -> {name,args} (new).
155    let syscalls_v = get("syscalls").ok_or_else(|| anyhow!("meta.syscalls is required"))?;
156
157    let mut syscalls: Vec<Syscall> = Vec::new();
158
159    match syscalls_v {
160        Value::Sequence(seq) => {
161            // Legacy: list of {name, args} objects, IDs are implicit 0..N-1.
162            for (i, it) in seq.iter().enumerate() {
163                let m = it
164                    .as_mapping()
165                    .ok_or_else(|| anyhow!("meta.syscalls[{i}] must be a mapping"))?;
166                let name = m
167                    .get(&Value::String("name".to_string()))
168                    .ok_or_else(|| anyhow!("meta.syscalls[{i}].name missing"))?;
169                let args = m
170                    .get(Value::String("args".to_string()))
171                    .unwrap();
172
173                let name = as_str(name, "name")?;
174                let argc = as_u16(args, "args")?;
175                let argc_u8 = 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 = u8::try_from(argc).map_err(|_| anyhow!("syscall args must fit u8"))?;
214
215                if tmp.contains_key(&id) {
216                    bail!("duplicate syscall id: {id}");
217                }
218                tmp.insert(
219                    id,
220                    Syscall {
221                        id,
222                        args: argc_u8,
223                        name,
224                    },
225                );
226            }
227
228            // Validate contiguity if syscall_count exists.
229            let declared_count = get("syscall_count")
230                .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!("syscall_count mismatch: declared {count}, found {}", syscalls.len());
249                }
250            }
251        }
252        _ => bail!("meta.syscalls must be a sequence or mapping"),
253    }
254
255    let mut name_to_id: HashMap<String, u16> = HashMap::new();
256    let mut id_to_args: HashMap<u16, u8> = HashMap::new();
257    for sc in &syscalls {
258        if name_to_id.insert(sc.name.clone(), sc.id).is_some() {
259            bail!("duplicate syscall name: {}", sc.name);
260        }
261        id_to_args.insert(sc.id, sc.args);
262    }
263
264    Ok(Meta {
265        nls,
266        game_title,
267        game_mode,
268        game_mode_reserved,
269        non_volatile_global_count,
270        volatile_global_count,
271        custom_syscall_count,
272        syscalls,
273        name_to_id,
274        id_to_args,
275    })
276}