1use crate::prelude::*;
27
28use alloc::collections::BTreeMap;
29use core::fmt;
30use core::marker::PhantomData;
31
32use serde::de::DeserializeOwned;
33use serde::{Deserialize, Serialize};
34use shuyi_core::{Grid, Pos};
35
36use crate::error::{ContentError, codes};
37use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
38use crate::ron::parse_ron;
39
40pub const SEPARATOR: &str = "---";
42pub const DEFAULT_TILES: &str = "outdoor";
44pub const DEFAULT_LIGHT: &str = "noon";
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct MapFormat<'a> {
52 pub looks: &'a [&'a str],
55 pub max_size: usize,
57}
58
59impl<'a> MapFormat<'a> {
60 #[must_use]
63 pub const fn new(looks: &'a [&'a str], max_size: usize) -> Self {
64 Self { looks, max_size }
65 }
66}
67
68pub trait MapFeature: Serialize + DeserializeOwned {
72 fn problem(&self) -> Option<String> {
74 None
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80pub enum NoFeature {}
81
82impl MapFeature for NoFeature {}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct MapLook {
90 #[serde(default = "default_tiles")]
93 pub tiles: String,
94 #[serde(default = "default_light", skip_serializing_if = "is_default_light")]
96 pub light: String,
97}
98
99fn default_tiles() -> String {
100 DEFAULT_TILES.to_owned()
101}
102
103fn default_light() -> String {
104 DEFAULT_LIGHT.to_owned()
105}
106
107#[allow(clippy::ptr_arg)] fn is_default_light(light: &String) -> bool {
109 light == DEFAULT_LIGHT
110}
111
112impl Default for MapLook {
113 fn default() -> Self {
114 Self {
115 tiles: default_tiles(),
116 light: default_light(),
117 }
118 }
119}
120
121impl MapLook {
122 pub fn is_default(&self) -> bool {
125 *self == Self::default()
126 }
127
128 #[must_use]
131 pub fn lit(mut self, light: Option<&str>) -> Self {
132 if let Some(light) = light {
133 light.clone_into(&mut self.light);
134 }
135 self
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct Legend<T> {
143 pub names: BTreeMap<char, String>,
145 pub terrains: BTreeMap<char, T>,
147}
148
149impl<T> Default for Legend<T> {
150 fn default() -> Self {
151 Self {
152 names: BTreeMap::new(),
153 terrains: BTreeMap::new(),
154 }
155 }
156}
157
158impl<T: Copy + PartialEq> Legend<T> {
159 pub fn resolve(
162 names: BTreeMap<char, String>,
163 terrain: impl Fn(&str) -> Option<T>,
164 ) -> Option<Self> {
165 let terrains = names
166 .iter()
167 .map(|(&c, id)| Some((c, terrain(id)?)))
168 .collect::<Option<_>>()?;
169 Some(Self { names, terrains })
170 }
171
172 pub fn char_for(&self, t: T) -> Option<char> {
174 self.terrains
175 .iter()
176 .find(|&(_, &u)| u == t)
177 .map(|(&c, _)| c)
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct GridMap<T, F> {
185 pub name: String,
187 pub tiles: Grid<T>,
189 pub features: BTreeMap<Pos, F>,
191 pub legend: Legend<T>,
193 #[serde(with = "look_in_full")]
195 pub look: MapLook,
196}
197
198mod look_in_full {
202 use alloc::string::String;
203
204 use serde::{Deserialize, Deserializer, Serialize, Serializer};
205
206 use super::MapLook;
207
208 pub(super) fn serialize<S: Serializer>(
209 look: &MapLook,
210 serializer: S,
211 ) -> Result<S::Ok, S::Error> {
212 (&look.tiles, &look.light).serialize(serializer)
213 }
214
215 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
216 deserializer: D,
217 ) -> Result<MapLook, D::Error> {
218 let (tiles, light) = <(String, String)>::deserialize(deserializer)?;
219 Ok(MapLook { tiles, light })
220 }
221}
222
223impl<T: 'static, F: 'static> AssetKind for GridMap<T, F> {
224 const NAME: &'static str = "map";
225}
226
227type Terrain<T> = Box<dyn Fn(&str) -> Option<T> + Send + Sync>;
229
230pub struct MapText<T, F = NoFeature> {
234 format: MapFormat<'static>,
235 terrain: Terrain<T>,
236 feature: PhantomData<fn() -> F>,
237}
238
239impl<T, F> MapText<T, F> {
240 pub fn new(
242 format: MapFormat<'static>,
243 terrain: impl Fn(&str) -> Option<T> + Send + Sync + 'static,
244 ) -> Self {
245 Self {
246 format,
247 terrain: Box::new(terrain),
248 feature: PhantomData,
249 }
250 }
251}
252
253impl<T, F> fmt::Debug for MapText<T, F> {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 f.debug_struct("MapText")
256 .field("format", &self.format)
257 .finish_non_exhaustive()
258 }
259}
260
261impl<T: Copy + Default + 'static, F: MapFeature + 'static> Format for MapText<T, F> {
262 type Asset = GridMap<T, F>;
263
264 fn id(&self) -> &'static str {
265 "map"
266 }
267
268 fn extensions(&self) -> &'static [&'static str] {
269 &["map"]
270 }
271
272 fn max_bytes(&self) -> usize {
273 TEXT_MAX_BYTES
274 }
275
276 fn decode(
277 &self,
278 file: &AssetPath,
279 bytes: &[u8],
280 _cx: &DecodeCx<'_>,
281 ) -> Result<GridMap<T, F>, Vec<ContentError>> {
282 parse(
283 file.shown(),
284 text(file, bytes)?,
285 &self.format,
286 &self.terrain,
287 )
288 }
289}
290
291#[derive(Serialize, Deserialize)]
292#[serde(deny_unknown_fields)]
293#[serde(bound(serialize = "F: Serialize", deserialize = "F: DeserializeOwned"))]
294struct Header<F> {
295 name: String,
296 legend: BTreeMap<char, String>,
297 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
298 features: BTreeMap<(i32, i32), F>,
299 #[serde(default, skip_serializing_if = "MapLook::is_default")]
300 look: MapLook,
301}
302
303pub fn parse<T, F>(
307 file: &str,
308 source: &str,
309 format: &MapFormat<'_>,
310 terrain: impl Fn(&str) -> Option<T>,
311) -> Result<GridMap<T, F>, Vec<ContentError>>
312where
313 T: Copy + Default,
314 F: MapFeature,
315{
316 let lines: Vec<&str> = source
317 .split('\n')
318 .map(|l| l.strip_suffix('\r').unwrap_or(l))
319 .collect();
320 let Some(sep) = lines.iter().position(|&l| l == SEPARATOR) else {
321 return Err(vec![ContentError::new(
322 file,
323 format!("missing the \"{SEPARATOR}\" line between the header and the tiles"),
324 )]);
325 };
326 let header_text = lines[..sep].join("\n");
327 let header: Header<F> = parse_ron(file, &header_text).map_err(|e| vec![e])?;
328
329 let mut errors = Vec::new();
330 let mut terrains = BTreeMap::new();
331 for (&c, id) in &header.legend {
332 if let Some(t) = terrain(id) {
333 terrains.insert(c, t);
334 } else {
335 let (line, col) = legend_position(&lines[..sep], c, id);
336 errors.push(
337 ContentError::new(file, format!("legend '{c}': unknown terrain \"{id}\""))
338 .at(line, Some(col))
339 .with_code(codes::UNKNOWN_NAME),
340 );
341 }
342 }
343
344 let tiles_look = header.look.tiles.as_str();
345 if !format.looks.contains(&tiles_look) {
346 let known: Vec<String> = format.looks.iter().map(|l| format!("\"{l}\"")).collect();
347 let message = format!(
348 "look: tiles \"{tiles_look}\" is not a look; the looks are {}",
349 known.join(", ")
350 );
351 let (line, col) =
352 header_position(&lines[..sep], &format!("\"{tiles_look}\"")).unwrap_or((1, 1));
353 errors.push(ContentError::new(file, message).at(line, Some(col)));
354 }
355
356 let rows = Rows {
357 file,
358 legend: &header.legend,
359 terrains: &terrains,
360 max_size: format.max_size,
361 };
362 let tiles = rows.parse(&lines[sep + 1..], sep + 1, &mut errors);
363 let mut features = BTreeMap::new();
364 for ((x, y), feature) in header.features {
365 let pos = Pos::new(x, y);
366 let at = || format!("feature at ({x}, {y})");
367 if tiles.as_ref().is_some_and(|t| t.get(pos).is_none()) {
368 errors.push(ContentError::new(
369 file,
370 format!("{}: outside the map", at()),
371 ));
372 }
373 if let Some(problem) = feature.problem() {
374 errors.push(ContentError::new(file, format!("{}: {problem}", at())));
375 }
376 features.insert(pos, feature);
377 }
378 match tiles {
379 Some(tiles) if errors.is_empty() => Ok(GridMap {
380 name: header.name,
381 tiles,
382 features,
383 legend: Legend {
384 names: header.legend,
385 terrains,
386 },
387 look: header.look,
388 }),
389 _ => Err(errors),
390 }
391}
392
393struct Rows<'a, T> {
395 file: &'a str,
396 legend: &'a BTreeMap<char, String>,
397 terrains: &'a BTreeMap<char, T>,
398 max_size: usize,
399}
400
401impl<T: Copy + Default> Rows<'_, T> {
402 fn parse(
407 &self,
408 rows: &[&str],
409 first: usize,
410 errors: &mut Vec<ContentError>,
411 ) -> Option<Grid<T>> {
412 let file = self.file;
413 let max = self.max_size;
414 let mut rows: Vec<(u32, Vec<char>)> = rows
415 .iter()
416 .enumerate()
417 .map(|(i, l)| (line_number(first + i), l.chars().collect()))
418 .collect();
419 while rows.last().is_some_and(|(_, r)| r.is_empty()) {
421 rows.pop();
422 }
423 let first_line = line_number(first);
424 let Some((_, first_row)) = rows.first() else {
425 errors.push(ContentError::new(file, "map has no tiles").at(first_line, Some(1)));
426 return None;
427 };
428 let width = first_row.len();
429 if width == 0 {
430 errors.push(ContentError::new(file, "first tile row is empty").at(first_line, Some(1)));
431 return None;
432 }
433 if width > max {
434 errors.push(
435 ContentError::new(
436 file,
437 format!("map is {width} tiles wide; the maximum is {max}"),
438 )
439 .at(first_line, Some(column_number(max)))
440 .with_code(codes::OUT_OF_RANGE),
441 );
442 }
443 if rows.len() > max {
444 errors.push(
445 ContentError::new(
446 file,
447 format!("map is {} tiles tall; the maximum is {max}", rows.len()),
448 )
449 .at(rows[max].0, Some(1))
450 .with_code(codes::OUT_OF_RANGE),
451 );
452 }
453 let mut cells = Vec::with_capacity(width * rows.len());
454 for (line, row) in &rows {
455 if row.len() != width {
456 errors.push(
457 ContentError::new(
458 file,
459 format!(
460 "row is {} tiles wide; expected {width} like the first row",
461 row.len()
462 ),
463 )
464 .at(*line, Some(column_number(row.len().min(width)))),
465 );
466 }
467 for (i, &c) in row.iter().enumerate() {
468 match self.terrains.get(&c) {
469 Some(&t) => cells.push(t),
470 None if self.legend.contains_key(&c) => cells.push(T::default()),
472 None => errors.push(
473 ContentError::new(file, format!("'{c}' is not in the legend"))
474 .at(*line, Some(column_number(i)))
475 .with_code(codes::UNKNOWN_NAME),
476 ),
477 }
478 }
479 }
480 let w = u16::try_from(width).ok()?;
481 let h = u16::try_from(rows.len()).ok()?;
482 Grid::from_cells(w, h, cells)
483 }
484}
485
486pub fn print<T, F>(map: &GridMap<T, F>) -> Option<String>
490where
491 T: Copy + PartialEq,
492 F: MapFeature + Clone,
493{
494 let header = Header {
495 name: map.name.clone(),
496 legend: map.legend.names.clone(),
497 features: map
498 .features
499 .iter()
500 .map(|(p, f)| ((p.x, p.y), f.clone()))
501 .collect(),
502 look: map.look.clone(),
503 };
504 let mut out = ::ron::to_string(&header).ok()?;
505 out.push('\n');
506 out.push_str(SEPARATOR);
507 out.push('\n');
508 let width = usize::from(map.tiles.width());
509 for row in map.tiles.cells().chunks(width.max(1)) {
510 for &t in row {
511 out.push(map.legend.char_for(t)?);
512 }
513 out.push('\n');
514 }
515 Some(out)
516}
517
518pub fn header_lines(source: &str) -> Vec<&str> {
521 let lines: Vec<&str> = source.lines().collect();
522 let header = lines.iter().position(|&l| l == SEPARATOR);
523 lines[..header.unwrap_or(lines.len())].to_vec()
524}
525
526pub fn legend_position(header: &[&str], c: char, id: &str) -> (u32, u32) {
529 let needles = [format!("'{c}'"), format!("\"{id}\"")];
530 let found = needles.iter().find_map(|n| header_position(header, n));
531 found.unwrap_or((1, 1))
532}
533
534pub fn header_position(header: &[&str], needle: &str) -> Option<(u32, u32)> {
536 header.iter().enumerate().find_map(|(i, line)| {
537 let byte = line.find(needle)?;
538 Some((line_number(i), column_number(line[..byte].chars().count())))
539 })
540}
541
542pub fn line_number(i: usize) -> u32 {
544 u32::try_from(i + 1).unwrap_or(u32::MAX)
545}
546
547pub fn column_number(i: usize) -> u32 {
549 u32::try_from(i + 1).unwrap_or(u32::MAX)
550}
551
552#[cfg(test)]
553mod tests;