use std::collections::{HashMap, HashSet};
use super::lines::Canvas;
use super::{Detail, Found, Index, Kind, LintCode, Sink, shading};
use crate::{Color, Document, Position, Sprite};
#[derive(Clone, Copy)]
struct Measure {
silhouette: bool,
outline: Option<Color>,
scale: Option<i64>,
}
pub(super) fn check(document: &Document, index: &Index, sink: &mut Sink) {
let mut measured: HashMap<(Option<&str>, &str), Measure> = HashMap::new();
let tiles: HashMap<(&str, &str), &Sprite> = document
.tilesets
.iter()
.flat_map(|t| {
t.tiles
.iter()
.map(move |s| ((t.name.as_str(), s.name.as_str()), s))
})
.collect();
for set in &document.sources.sets {
let mut members: Vec<(&Sprite, Position)> = Vec::new();
let mut sources: Vec<&[String]> = Vec::new();
let mut seen: HashSet<&str> = HashSet::new();
for member in &set.members {
if !seen.insert(&member.name) {
continue;
}
let sprite = match set.kind {
Kind::Tileset => tiles
.get(&(set.name.as_str(), member.name.as_str()))
.copied(),
_ => document.sprite(&member.name),
};
if let Some(sprite) = sprite {
members.push((sprite, member.pos));
sources.push(&member.sources);
}
}
if members.len() < 2 {
continue;
}
let tileset = (set.kind == Kind::Tileset).then_some(set.name.as_str());
let measures: Vec<Measure> = members
.iter()
.map(|(s, _)| {
*measured
.entry((tileset, s.name.as_str()))
.or_insert_with(|| {
let canvas = Canvas::new(s);
Measure {
silhouette: canvas.has_silhouette(),
outline: canvas.outline_color(),
scale: shading::scale(&canvas),
}
})
})
.collect();
let what = match set.kind {
Kind::Animation => "animation",
Kind::Tileset => "tileset",
_ => "group",
};
let report = |sink: &mut Sink, code, i: usize, message: String| {
let (sprite, pos) = members[i];
if sources[i]
.iter()
.any(|source| sink.allowed_by_definition(code, Kind::Sprite, source))
{
return;
}
let found = Found {
code,
kind: set.kind,
name: &set.name,
position: Some(pos),
sprite: Some(&sprite.name),
pixels: Vec::new(),
};
sink.push(found, || message);
};
let set_name = &set.name;
if set.kind != Kind::Tileset {
let sizes: Vec<Option<(u32, u32)>> = members
.iter()
.map(|(s, _)| Some((s.width, s.height)))
.collect();
let (usual, odd) = minority(&sizes);
for i in odd {
let (s, (w, h)) = (members[i].0, (members[i].0.width, members[i].0.height));
let (uw, uh) = usual.unwrap_or_default();
report(
sink,
LintCode::InconsistentSize,
i,
format!(
"Sprite `{}` is {w}x{h}, unlike the {uw}x{uh} of most of {what} \
`{set_name}`",
s.name
),
);
}
}
let outlines: Vec<Option<Option<Color>>> = measures
.iter()
.map(|m| m.silhouette.then_some(m.outline))
.collect();
let presence: Vec<Option<bool>> = outlines.iter().map(|o| o.map(|c| c.is_some())).collect();
let (usual, odd) = minority(&presence);
for &i in &odd {
let name = &members[i].0.name;
let message = if usual == Some(true) {
format!("Sprite `{name}` has no outline, unlike most of {what} `{set_name}`")
} else {
format!("Sprite `{name}` has an outline, unlike most of {what} `{set_name}`")
};
report(sink, LintCode::InconsistentOutline, i, message);
}
let mut palettes: Vec<&str> = members
.iter()
.map(|(s, _)| s.palette.name.as_str())
.collect();
palettes.dedup();
palettes.sort_unstable();
palettes.dedup();
for palette in palettes {
let colors: Vec<Option<Color>> = outlines
.iter()
.enumerate()
.map(|(i, o)| {
let same = members[i].0.palette.name == palette && !odd.contains(&i);
o.flatten().filter(|_| same)
})
.collect();
let (usual, recolored) = minority(&colors);
for i in recolored {
let (name, this) = (&members[i].0.name, colors[i].unwrap_or_default());
let usual = usual.unwrap_or_default();
report(
sink,
LintCode::InconsistentOutline,
i,
format!(
"Sprite `{name}` is outlined in {}, unlike most of {what} `{set_name}` \
drawn with palette `{palette}`, outlined in {}",
hex(this),
hex(usual)
),
);
}
}
if set.kind != Kind::Tileset {
let roots: Vec<Option<String>> = members
.iter()
.map(|(s, _)| Some(root(index, &s.palette.name)))
.collect();
let (usual, odd) = minority(&roots);
for i in odd {
let sprite = members[i].0;
let usual = usual.clone().unwrap_or_default();
report(
sink,
LintCode::InconsistentPalette,
i,
format!(
"Sprite `{}` uses palette `{}`, which is not a copy of `{usual}`, \
used by most of {what} `{set_name}`",
sprite.name, sprite.palette.name
),
);
}
}
let scales: Vec<Option<i64>> = measures.iter().map(|m| m.scale).collect();
let (usual, odd) = minority(&scales);
for i in odd {
let name = &members[i].0.name;
let (this, usual) = (scales[i].unwrap_or(1), usual.unwrap_or(1));
report(
sink,
LintCode::InconsistentScale,
i,
format!(
"Sprite `{name}` looks drawn at {this}x, unlike most of {what} \
`{set_name}`, drawn at {usual}x"
),
);
}
}
}
fn minority<T: PartialEq + Clone>(values: &[Option<T>]) -> (Option<T>, Vec<usize>) {
let mut counts: Vec<(T, usize)> = Vec::new();
for v in values.iter().flatten() {
match counts.iter_mut().find(|(c, _)| c == v) {
Some((_, n)) => *n += 1,
None => counts.push((v.clone(), 1)),
}
}
let Some(best) = counts.iter().map(|&(_, n)| n).max() else {
return (None, Vec::new());
};
let usual = counts.into_iter().find(|&(_, n)| n == best).map(|(v, _)| v);
let odd = values
.iter()
.enumerate()
.filter(|(_, v)| v.is_some() && **v != usual)
.map(|(i, _)| i)
.collect();
(usual, odd)
}
fn root(index: &Index, palette: &str) -> String {
let mut name = palette;
for _ in 0..=index.len() {
let detail = index.get(&(Kind::Palette, name)).map(|d| &d.detail);
match detail {
Some(Detail::Palette {
from: Some(from), ..
}) => name = from,
_ => break,
}
}
name.to_owned()
}
fn hex(c: Color) -> String {
if c.a == 255 {
format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
} else {
format!("#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a)
}
}