1use std::{
2 collections::{hash_map::Entry, HashMap},
3 error::Error,
4 fmt,
5 mem,
6};
7
8#[derive(Default)]
9pub struct Config {
10 pub global_names: Vec<Option<String>>,
11 pub global_types: Vec<Option<Type>>,
12 pub scripts: Vec<Script>,
13 pub rooms: Vec<Room>,
14 pub enums: Vec<Enum>,
15 pub enum_names: HashMap<String, EnumId>,
16 pub assocs: Vec<Assoc>,
17 pub assoc_names: HashMap<String, AssocId>,
18 pub assocs_pending_parse: Vec<String>, pub suppress_preamble: bool,
20 pub aside: bool,
21}
22
23#[derive(Default)]
24pub struct Room {
25 pub vars: Vec<Var>,
26 pub scripts: Vec<Script>,
27}
28
29#[derive(Default)]
30pub struct Script {
31 pub name: Option<String>,
32 pub params: Option<u16>,
33 pub locals: Vec<Var>,
34 pub skip_do_blocks: bool,
35}
36
37#[derive(Default)]
38pub struct Var {
39 pub name: Option<String>,
40 pub ty: Option<Type>,
41}
42
43pub struct Enum {
44 pub name: String,
45 pub values: HashMap<i32, String>,
46}
47
48pub struct Assoc {
49 pub types: Vec<Type>,
50}
51
52pub type EnumId = usize;
53pub type AssocId = usize;
54
55pub enum Type {
56 Any,
57 Char,
58 Enum(EnumId),
59 Script,
60 Array {
61 item: Box<Type>,
62 y: Box<Type>,
63 x: Box<Type>,
64 },
65 AssocArray {
66 assoc: AssocId,
67 y: Box<Type>,
68 x: Box<Type>,
69 },
70}
71
72impl Config {
73 pub fn from_ini(ini: &str) -> Result<Self, Box<dyn Error>> {
74 let mut result = Self {
75 global_names: Vec::with_capacity(1024),
76 global_types: Vec::with_capacity(1024),
77 scripts: Vec::with_capacity(512),
78 rooms: Vec::with_capacity(64),
79 enums: Vec::with_capacity(64),
80 enum_names: HashMap::with_capacity(64),
81 assocs: Vec::with_capacity(64),
82 assoc_names: HashMap::with_capacity(64),
83 assocs_pending_parse: Vec::with_capacity(64),
84 suppress_preamble: false,
85 aside: false,
86 };
87 for (i, line) in ini.lines().enumerate() {
88 let cx = &ParseContext::LineIndex(i);
89 let line = line.split_once(';').map_or(line, |(a, _)| a); let line = line.trim();
91 if line.is_empty() {
92 continue;
93 }
94 let (lhs, rhs) = line.split_once('=').ok_or_else(|| parse_err(cx))?;
95 let key = lhs.trim();
96 let value = rhs.trim();
97 let mut dots = key.split('.');
98 match dots.next() {
99 Some("enum") => {
100 handle_enum_key(cx, &mut dots, value, &mut result)?;
101 }
102 Some("assoc") => {
103 handle_assoc_key(&mut dots, value, &mut result, cx)?;
104 }
105 Some("global") => {
106 let id = it_final(&mut dots, cx)?;
107 let id: usize = id.parse().map_err(|_| parse_err(cx))?;
108 let (name, type_) = parse_var_name_type(value, &result, cx)?;
109 extend(&mut result.global_names, id);
110 result.global_names[id] = Some(name.to_string());
111 extend(&mut result.global_types, id);
112 result.global_types[id] = type_;
113 }
114 Some("script") => {
115 handle_script_key(cx, &mut dots, value, &mut result, |c| &mut c.scripts)?;
116 }
117 Some("room") => {
118 let room = it_next(&mut dots, cx)?;
119 let room: usize = room.parse().map_err(|_| parse_err(cx))?;
120 extend(&mut result.rooms, room);
121 match it_next(&mut dots, cx)? {
122 "var" => {
123 let var = it_final(&mut dots, cx)?;
124 let var: usize = var.parse().map_err(|_| parse_err(cx))?;
125 let (name, ty) = parse_var_name_type(value, &result, cx)?;
126 extend(&mut result.rooms[room].vars, var);
127 result.rooms[room].vars[var].name = Some(name.to_string());
128 result.rooms[room].vars[var].ty = ty;
129 }
130 "script" => {
131 handle_script_key(cx, &mut dots, value, &mut result, |c| {
132 &mut c.rooms[room].scripts
133 })?;
134 }
135 _ => {
136 return Err(parse_err(cx));
137 }
138 }
139 }
140 _ => {
141 return Err(parse_err(cx));
142 }
143 }
144 }
145
146 for comma_sep in mem::take(&mut result.assocs_pending_parse) {
147 let cx = &ParseContext::NearString(&comma_sep);
148 let types: Result<Vec<_>, _> = comma_sep
149 .split(',')
150 .map(|s| parse_type_or_empty_any(s.trim(), &result, cx))
151 .collect();
152 let types = types?;
153 result.assocs.push(Assoc { types });
154 }
155
156 check_conflicting_names(&result)?;
157
158 Ok(result)
159 }
160}
161
162fn handle_script_key<'a>(
163 cx: &ParseContext,
164 dots: &mut impl Iterator<Item = &'a str>,
165 mut value: &str,
166 config: &mut Config,
167 scripts_vec: impl Fn(&mut Config) -> &mut Vec<Script>,
168) -> Result<(), Box<dyn Error>> {
169 let script = it_next(dots, cx)?;
170 let script: usize = script.parse().map_err(|_| parse_err(cx))?;
171 let scripts = scripts_vec(config);
173 extend(scripts, script);
174 match dots.next() {
175 None => {
176 if let Some(paren) = value.find('(') {
178 if *value.as_bytes().last().unwrap() != b')' {
179 return Err(parse_err(cx));
180 }
181 let params = &value[paren + 1..value.len() - 1];
182 let params: u16 = params.parse().map_err(|_| parse_err(cx))?;
183 scripts[script].params = Some(params);
184 value = &value[..paren];
185 }
186 scripts[script].name = Some(value.to_string());
187 }
188 Some("local") => {
189 let local = it_final(dots, cx)?;
190 let local: usize = local.parse().map_err(|_| parse_err(cx))?;
191 let (name, ty) = parse_var_name_type(value, config, cx)?;
192 let scripts = scripts_vec(config);
193 extend(&mut scripts[script].locals, local);
194 scripts[script].locals[local].name = Some(name.to_string());
195 scripts[script].locals[local].ty = ty;
196 }
197 Some("disable_do_blocks") => {
198 it_end(dots, cx)?;
199 if value != "all" {
200 return Err(parse_err(cx));
201 }
202 scripts[script].skip_do_blocks = true;
203 }
204 Some(_) => {
205 return Err(parse_err(cx));
206 }
207 }
208 Ok(())
209}
210
211fn handle_enum_key<'a>(
212 cx: &ParseContext,
213 dots: &mut impl Iterator<Item = &'a str>,
214 value: &str,
215 config: &mut Config,
216) -> Result<(), Box<dyn Error>> {
217 let enum_name = it_next(dots, cx)?;
218 let const_value: i32 = it_final(dots, cx)?.parse().map_err(|_| parse_err(cx))?;
219 let const_name = value.to_string();
220
221 let enum_id = config
222 .enum_names
223 .get(enum_name)
224 .copied()
225 .unwrap_or_else(|| {
226 let id = config.enums.len();
227 config.enums.push(Enum {
228 name: enum_name.to_string(),
229 values: HashMap::new(),
230 });
231 config.enum_names.insert(enum_name.to_string(), id);
232 id
233 });
234 config.enums[enum_id].values.insert(const_value, const_name);
235 Ok(())
236}
237
238fn handle_assoc_key<'a>(
239 dots: &mut impl Iterator<Item = &'a str>,
240 value: &str,
241 config: &mut Config,
242 cx: &ParseContext,
243) -> Result<(), Box<dyn Error>> {
244 let name = it_next(dots, cx)?;
245 let id = config.assocs_pending_parse.len();
246 config.assocs_pending_parse.push(value.to_string());
247 config.assoc_names.insert(name.to_string(), id);
248 Ok(())
249}
250
251fn parse_type(s: &str, config: &Config, cx: &ParseContext) -> Result<Type, Box<dyn Error>> {
252 if s == "string" {
253 return Ok(Type::Array {
254 item: Box::new(Type::Char),
255 y: Box::new(Type::Any),
256 x: Box::new(Type::Any),
257 });
258 }
259 if s == "char" {
260 return Ok(Type::Char);
261 }
262 if s == "script" {
263 return Ok(Type::Script);
264 }
265 if let Some((item, y, x)) = parse_array(s) {
266 let y = Box::new(parse_type_or_empty_any(y.unwrap_or(""), config, cx)?);
267 let x = Box::new(parse_type_or_empty_any(x, config, cx)?);
268
269 if let Some(&assoc) = config.assoc_names.get(item) {
270 return Ok(Type::AssocArray { assoc, y, x });
271 }
272
273 let item = Box::new(parse_type_or_empty_any(item, config, cx)?);
274 return Ok(Type::Array { item, y, x });
275 }
276 if let Some(&enum_id) = config.enum_names.get(s) {
277 return Ok(Type::Enum(enum_id));
278 }
279 return Err(type_err(cx));
280}
281
282fn parse_type_or_empty_any(
283 s: &str,
284 config: &Config,
285 cx: &ParseContext,
286) -> Result<Type, Box<dyn Error>> {
287 if s.is_empty() {
288 return Ok(Type::Any);
289 }
290 parse_type(s, config, cx)
291}
292
293fn parse_array(s: &str) -> Option<(&str, Option<&str>, &str)> {
294 let (s, x) = parse_array_level(s)?;
295 match parse_array_level(s) {
296 None => Some((s, None, x)),
297 Some((s, y)) => Some((s, Some(y), x)),
298 }
299}
300
301fn parse_array_level(s: &str) -> Option<(&str, &str)> {
302 if !s.ends_with(']') {
304 return None;
305 }
306 let s = &s[..s.len() - 1];
307 let (o, i) = s.rsplit_once('[')?;
308 Some((o.trim_end(), i.trim()))
309}
310
311fn it_next<T>(it: &mut impl Iterator<Item = T>, cx: &ParseContext) -> Result<T, Box<dyn Error>> {
312 it.next().ok_or_else(|| parse_err(cx))
313}
314
315fn it_end<T>(it: &mut impl Iterator<Item = T>, cx: &ParseContext) -> Result<(), Box<dyn Error>> {
316 match it.next() {
317 Some(_) => return Err(parse_err(cx)),
318 None => Ok(()),
319 }
320}
321
322fn it_final<T>(it: &mut impl Iterator<Item = T>, cx: &ParseContext) -> Result<T, Box<dyn Error>> {
323 let result = it_next(it, cx);
324 it_end(it, cx)?;
325 result
326}
327
328fn extend<T: Default>(xs: &mut Vec<T>, upto: usize) {
329 if xs.len() < upto + 1 {
330 xs.resize_with(upto + 1, T::default);
331 }
332}
333
334enum ParseContext<'a> {
335 LineIndex(usize),
336 NearString(&'a str),
337}
338
339impl fmt::Display for ParseContext<'_> {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 match self {
342 Self::LineIndex(index) => {
343 let num = index + 1;
344 write!(f, "on line {num}")?;
345 }
346 Self::NearString(string) => {
347 write!(f, "near {string:?}")?;
348 }
349 }
350 Ok(())
351 }
352}
353
354fn parse_err(cx: &ParseContext) -> Box<dyn Error> {
355 format!("bad config {cx}").into()
356}
357
358fn type_err(cx: &ParseContext) -> Box<dyn Error> {
359 format!("bad type {cx}").into()
360}
361
362fn parse_var_name_type<'a>(
363 s: &'a str,
364 config: &Config,
365 cx: &ParseContext,
366) -> Result<(&'a str, Option<Type>), Box<dyn Error>> {
367 match s.split_once(':') {
368 None => Ok((s, None)),
369 Some((name, ty)) => {
370 let name = name.trim_end();
371 let ty = ty.trim_start();
372 let ty = parse_type(ty, config, cx)?;
373 Ok((name, Some(ty)))
374 }
375 }
376}
377
378fn check_conflicting_names(config: &Config) -> Result<(), Box<dyn Error>> {
379 let mut parents = Vec::new();
380 let mut global_seen = HashMap::with_capacity(1 << 10);
381 let mut room_seen = HashMap::with_capacity(64);
382 let mut script_seen = HashMap::with_capacity(16);
383
384 for (i, s) in config.scripts.iter().enumerate() {
385 if let Some(name) = &s.name {
386 check_dup(
387 &parents,
388 &mut global_seen,
389 name,
390 Some(i.try_into().unwrap()),
391 )?;
392 }
393 }
394
395 for name in config.global_names.iter().flatten() {
396 check_dup(&parents, &mut global_seen, name, None)?;
397 }
398
399 for enum_ in &config.enums {
400 for (&value, name) in &enum_.values {
401 check_dup(&parents, &mut global_seen, name, Some(value))?;
402 }
403 }
404
405 for script in &config.scripts {
406 script_seen.clear();
407 for var in &script.locals {
408 if let Some(name) = &var.name {
409 check_dup(&parents, &mut script_seen, name, None)?;
410 }
411 }
412 }
413
414 parents.push(&global_seen);
415
416 for room in &config.rooms {
417 room_seen.clear();
418
419 for var in &room.vars {
420 if let Some(name) = &var.name {
421 check_dup(&parents, &mut room_seen, name, None)?;
422 }
423 }
424
425 parents.push(unsafe { transmute_lifetime(&room_seen) });
428
429 for script in &room.scripts {
430 script_seen.clear();
431 for var in &script.locals {
432 if let Some(name) = &var.name {
433 check_dup(&parents, &mut script_seen, name, None)?;
434 }
435 }
436 }
437
438 parents.pop();
439 }
440
441 Ok(())
442}
443
444unsafe fn transmute_lifetime<'a, T>(x: &T) -> &'a T {
445 mem::transmute::<&T, &T>(x)
446}
447
448fn check_dup<'a>(
449 parents: &[&HashMap<&str, Option<i32>>],
450 seen: &mut HashMap<&'a str, Option<i32>>,
451 name: &'a str,
452 value: Option<i32>,
453) -> Result<(), Box<dyn Error>> {
454 for parent in parents {
455 if parent.contains_key(name) {
456 return Err(format!("conflicting name {name:?}").into());
457 }
458 }
459 match seen.entry(name) {
460 Entry::Vacant(e) => {
461 e.insert(value);
462 }
463 Entry::Occupied(e) => {
464 let equal = e.get().is_some() && *e.get() == value;
465 if !equal {
466 return Err(format!("conflicting name {name:?}").into());
467 }
468 }
469 }
470 Ok(())
471}