1use crate::prelude::*;
22
23use alloc::collections::{BTreeMap, BTreeSet};
24use core::fmt;
25
26use serde::{Deserialize, Deserializer, Serialize, Serializer};
27
28use crate::error::ContentError;
29use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
30use crate::palette::{PaletteDef, format_hex};
31use crate::ron::parse_ron_implicit_some;
32
33pub const BAYER: [[u8; 8]; 8] = [
36 [0, 32, 8, 40, 2, 34, 10, 42],
37 [48, 16, 56, 24, 50, 18, 58, 26],
38 [12, 44, 4, 36, 14, 46, 6, 38],
39 [60, 28, 52, 20, 62, 30, 54, 22],
40 [3, 35, 11, 43, 1, 33, 9, 41],
41 [51, 19, 59, 27, 49, 17, 57, 25],
42 [15, 47, 7, 39, 13, 45, 5, 37],
43 [63, 31, 55, 23, 61, 29, 53, 21],
44];
45
46pub type Rows = [u8; 8];
49
50pub fn dither(darkness: u8) -> Rows {
55 rows_where(|x, y| 4 * BAYER[y][x] + 2 < darkness)
57}
58
59fn rows_where(black: impl Fn(usize, usize) -> bool) -> Rows {
61 let mut rows = [0; 8];
62 for (y, row) in rows.iter_mut().enumerate() {
63 for x in 0..8 {
64 if !black(x, y) {
65 *row |= 0x80 >> x;
66 }
67 }
68 }
69 rows
70}
71
72#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
75pub enum Base {
76 Black,
78 White,
80 Dither25,
82 Dither50,
84 Dither75,
86 DiagonalUp,
88 DiagonalDown,
90 HLines,
92 VLines,
94 Checker2,
96}
97
98impl Base {
99 pub const ALL: [Self; 10] = [
101 Self::Black,
102 Self::White,
103 Self::Dither25,
104 Self::Dither50,
105 Self::Dither75,
106 Self::DiagonalUp,
107 Self::DiagonalDown,
108 Self::HLines,
109 Self::VLines,
110 Self::Checker2,
111 ];
112
113 pub const fn name(self) -> &'static str {
115 match self {
116 Self::Black => "Black",
117 Self::White => "White",
118 Self::Dither25 => "Dither25",
119 Self::Dither50 => "Dither50",
120 Self::Dither75 => "Dither75",
121 Self::DiagonalUp => "DiagonalUp",
122 Self::DiagonalDown => "DiagonalDown",
123 Self::HLines => "HLines",
124 Self::VLines => "VLines",
125 Self::Checker2 => "Checker2",
126 }
127 }
128
129 pub const fn black_pixels(self) -> u32 {
131 match self {
132 Self::Black => 64,
133 Self::White => 0,
134 Self::Dither25
135 | Self::DiagonalUp
136 | Self::DiagonalDown
137 | Self::HLines
138 | Self::VLines => 16,
139 Self::Dither50 | Self::Checker2 => 32,
140 Self::Dither75 => 48,
141 }
142 }
143
144 const fn is_black(self, x: usize, y: usize) -> bool {
146 match self {
147 Self::Black => true,
148 Self::White => false,
149 Self::Dither25 => BAYER[y][x] < 16,
150 Self::Dither50 => BAYER[y][x] < 32,
151 Self::Dither75 => BAYER[y][x] < 48,
152 Self::DiagonalUp => (x + y).is_multiple_of(4),
153 Self::DiagonalDown => (x + 8 - y).is_multiple_of(4),
154 Self::HLines => y.is_multiple_of(4),
155 Self::VLines => x.is_multiple_of(4),
156 Self::Checker2 => (x / 2 + y / 2).is_multiple_of(2),
157 }
158 }
159
160 pub fn rows(self) -> Rows {
162 rows_where(|x, y| self.is_black(x, y))
163 }
164}
165
166#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
170pub struct Pattern {
171 base: Base,
172 inverted: bool,
173}
174
175impl Pattern {
176 pub const BLACK: Self = Self::new(Base::Black);
178 pub const WHITE: Self = Self::new(Base::White);
180
181 pub const fn new(base: Base) -> Self {
183 Self {
184 base,
185 inverted: false,
186 }
187 }
188
189 #[must_use]
191 pub const fn inverted(self) -> Self {
192 Self {
193 base: self.base,
194 inverted: !self.inverted,
195 }
196 }
197
198 pub const fn base(self) -> Base {
200 self.base
201 }
202
203 pub const fn is_inverted(self) -> bool {
205 self.inverted
206 }
207
208 pub fn rows(self) -> Rows {
211 let rows = self.base.rows();
212 if self.inverted {
213 rows.map(|row| !row)
214 } else {
215 rows
216 }
217 }
218
219 pub const fn black_pixels(self) -> u32 {
221 if self.inverted {
222 64 - self.base.black_pixels()
223 } else {
224 self.base.black_pixels()
225 }
226 }
227
228 pub const fn is_solid(self) -> bool {
230 matches!(self.base, Base::Black | Base::White)
231 }
232
233 pub fn looks_like(self, other: Self) -> bool {
237 let (a, b) = (self.rows(), other.rows());
238 (0..8).any(|dy| (0..8u32).any(|dx| (0..8).all(|y| a[y] == b[(y + dy) % 8].rotate_left(dx))))
239 }
240}
241
242impl From<Base> for Pattern {
243 fn from(base: Base) -> Self {
244 Self::new(base)
245 }
246}
247
248impl fmt::Display for Pattern {
249 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250 if self.inverted {
251 write!(f, "Inverted({})", self.base.name())
252 } else {
253 f.write_str(self.base.name())
254 }
255 }
256}
257
258#[derive(Serialize, Deserialize)]
260#[serde(rename = "Pattern")]
261enum Spelled {
262 Black,
263 White,
264 Dither25,
265 Dither50,
266 Dither75,
267 DiagonalUp,
268 DiagonalDown,
269 HLines,
270 VLines,
271 Checker2,
272 Inverted(Box<Spelled>),
273}
274
275impl Spelled {
276 fn of(pattern: Pattern) -> Self {
277 let plain = match pattern.base {
278 Base::Black => Self::Black,
279 Base::White => Self::White,
280 Base::Dither25 => Self::Dither25,
281 Base::Dither50 => Self::Dither50,
282 Base::Dither75 => Self::Dither75,
283 Base::DiagonalUp => Self::DiagonalUp,
284 Base::DiagonalDown => Self::DiagonalDown,
285 Base::HLines => Self::HLines,
286 Base::VLines => Self::VLines,
287 Base::Checker2 => Self::Checker2,
288 };
289 if pattern.inverted {
290 Self::Inverted(Box::new(plain))
291 } else {
292 plain
293 }
294 }
295
296 fn pattern(self) -> Pattern {
298 let base = match self {
299 Self::Inverted(inner) => return inner.pattern().inverted(),
300 Self::Black => Base::Black,
301 Self::White => Base::White,
302 Self::Dither25 => Base::Dither25,
303 Self::Dither50 => Base::Dither50,
304 Self::Dither75 => Base::Dither75,
305 Self::DiagonalUp => Base::DiagonalUp,
306 Self::DiagonalDown => Base::DiagonalDown,
307 Self::HLines => Base::HLines,
308 Self::VLines => Base::VLines,
309 Self::Checker2 => Base::Checker2,
310 };
311 Pattern::new(base)
312 }
313}
314
315impl Serialize for Pattern {
316 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
317 Spelled::of(*self).serialize(serializer)
318 }
319}
320
321impl<'de> Deserialize<'de> for Pattern {
322 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
323 Spelled::deserialize(deserializer).map(Spelled::pattern)
324 }
325}
326
327#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
331#[serde(deny_unknown_fields)]
332pub struct RolePatterns {
333 pub fg: Pattern,
335 #[serde(default)]
337 pub bg: Option<Pattern>,
338}
339
340impl RolePatterns {
341 pub const fn new(fg: Pattern, bg: Pattern) -> Self {
343 Self { fg, bg: Some(bg) }
344 }
345
346 pub fn background(&self) -> Pattern {
348 self.bg.unwrap_or(self.fg)
349 }
350}
351
352impl AssetKind for PatternMapDef {
353 const NAME: &'static str = "pattern map";
354}
355
356#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(deny_unknown_fields)]
361pub struct PatternMapDef {
362 pub roles: BTreeMap<String, RolePatterns>,
364 #[serde(default)]
368 pub distinct: Vec<Vec<String>>,
369 #[serde(default)]
372 pub text: Vec<String>,
373}
374
375impl PatternMapDef {
376 pub fn get(&self, role: &str) -> Option<&RolePatterns> {
378 self.roles.get(role)
379 }
380
381 pub fn from_source(file: &str, source: &str) -> Result<Self, Vec<ContentError>> {
385 let map: Self = parse_ron_implicit_some(file, source).map_err(|e| vec![e])?;
386 let mut errors = Vec::new();
387 for (i, group) in map.distinct.iter().enumerate() {
388 let shown = group.join(", ");
389 if group.len() < 2 {
390 errors.push(ContentError::new(
391 file,
392 format!("distinct group {} [{shown}] needs two roles or more", i + 1),
393 ));
394 }
395 let mut seen = BTreeSet::new();
396 for role in group.iter().filter(|role| !seen.insert(role.as_str())) {
397 errors.push(ContentError::new(
398 file,
399 format!("distinct group {} names \"{role}\" twice", i + 1),
400 ));
401 }
402 }
403 if errors.is_empty() {
404 Ok(map)
405 } else {
406 Err(errors)
407 }
408 }
409}
410
411#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
414pub struct PatternMapRon;
415
416impl Format for PatternMapRon {
417 type Asset = PatternMapDef;
418
419 fn id(&self) -> &'static str {
420 "pattern-map"
421 }
422
423 fn extensions(&self) -> &'static [&'static str] {
424 &["ron"]
425 }
426
427 fn max_bytes(&self) -> usize {
428 TEXT_MAX_BYTES
429 }
430
431 fn decode(
432 &self,
433 file: &AssetPath,
434 bytes: &[u8],
435 _cx: &DecodeCx<'_>,
436 ) -> Result<PatternMapDef, Vec<ContentError>> {
437 PatternMapDef::from_source(file.shown(), text(file, bytes)?)
438 }
439}
440
441pub fn map_path(palette: &str) -> String {
444 let path = AssetPath::new(palette);
445 path.sibling(&format!("{}@onebit.ron", path.stem()))
446}
447
448#[derive(Copy, Clone, Debug, PartialEq, Eq)]
450pub enum Severity {
451 Error,
453 Warning,
455}
456
457#[derive(Copy, Clone, Debug, PartialEq, Eq)]
459pub enum Rule {
460 Missing,
462 Unknown,
464 SharedColour,
467 Distinct,
469 Text,
471}
472
473#[derive(Clone, Debug, PartialEq, Eq)]
475pub struct Problem {
476 pub severity: Severity,
478 pub rule: Rule,
480 pub roles: Vec<String>,
482 pub message: String,
484}
485
486impl Problem {
487 fn error(rule: Rule, roles: Vec<String>, message: String) -> Self {
488 Self {
489 severity: Severity::Error,
490 rule,
491 roles,
492 message,
493 }
494 }
495}
496
497pub fn check(palette: &PaletteDef, map: &PatternMapDef) -> Vec<Problem> {
508 let mut out = Vec::new();
509 for role in palette
510 .colors
511 .keys()
512 .filter(|r| !map.roles.contains_key(*r))
513 {
514 out.push(Problem::error(
515 Rule::Missing,
516 vec![role.clone()],
517 format!("role \"{role}\" has no patterns"),
518 ));
519 }
520 let named = map.roles.keys().map(|role| (role, "the map"));
521 let grouped = map
522 .distinct
523 .iter()
524 .flatten()
525 .map(|role| (role, "a distinct group"));
526 let text = map.text.iter().map(|role| (role, "the text roles"));
527 let mut unknown = BTreeSet::new();
528 for (role, place) in named.chain(grouped).chain(text) {
529 if !palette.colors.contains_key(role) && unknown.insert((role, place)) {
530 out.push(Problem::error(
531 Rule::Unknown,
532 vec![role.clone()],
533 format!("{place} names role \"{role}\", which the palette doesn't have"),
534 ));
535 }
536 }
537 let mut by_colour: BTreeMap<[u8; 3], Vec<(&String, &RolePatterns)>> = BTreeMap::new();
538 for (role, rgb) in &palette.colors {
539 if let Some(patterns) = map.roles.get(role) {
540 by_colour.entry(*rgb).or_default().push((role, patterns));
541 }
542 }
543 for (rgb, roles) in &by_colour {
544 let Some(&(first, patterns)) = roles.first() else {
545 continue;
546 };
547 let drawn = |p: &RolePatterns| (p.fg, p.background());
548 for &(other, theirs) in &roles[1..] {
549 if drawn(theirs) != drawn(patterns) {
550 out.push(Problem::error(
551 Rule::SharedColour,
552 vec![first.clone(), other.clone()],
553 format!(
554 "roles \"{first}\" and \"{other}\" are both {} in the palette, so the \
555 frame can't tell them apart, but their patterns differ: give them the \
556 same patterns or different colours",
557 format_hex(*rgb)
558 ),
559 ));
560 }
561 }
562 }
563 for group in &map.distinct {
564 let known: Vec<(&String, Pattern)> = group
565 .iter()
566 .filter_map(|role| map.roles.get(role).map(|p| (role, p.fg)))
567 .collect();
568 for (i, &(a, pa)) in known.iter().enumerate() {
569 for &(b, pb) in &known[i + 1..] {
570 if pa.looks_like(pb) {
571 let how = if pa == pb {
572 format!("both {pa}")
573 } else {
574 format!("{pa} and {pb} are the same pattern moved")
575 };
576 out.push(Problem::error(
577 Rule::Distinct,
578 vec![a.clone(), b.clone()],
579 format!(
580 "roles \"{a}\" and \"{b}\" must stay distinct, but their foregrounds \
581 look the same in one bit ({how})"
582 ),
583 ));
584 }
585 }
586 }
587 }
588 let solid_pair = |p: &RolePatterns| {
589 let pair = (p.fg, p.background());
590 pair == (Pattern::BLACK, Pattern::WHITE) || pair == (Pattern::WHITE, Pattern::BLACK)
591 };
592 for role in &map.text {
593 if let Some(patterns) = map.roles.get(role).filter(|p| !solid_pair(p)) {
594 out.push(Problem {
595 severity: Severity::Warning,
596 rule: Rule::Text,
597 roles: vec![role.clone()],
598 message: format!(
599 "text role \"{role}\" is {} on {}: text reads best Black on White or White \
600 on Black",
601 patterns.fg,
602 patterns.background()
603 ),
604 });
605 }
606 }
607 out
608}
609
610#[cfg(test)]
611mod tests;