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 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 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 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 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 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 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}