use crate::scene::{Budget, random, scene_seed};
use crate::{
Color, Document, Error, ErrorCode, Image, Limits, Position, Rect, Result, Sprite, Surface,
Tilemap, Tileset,
};
use std::sync::Arc;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Ratio {
num: i32,
den: u32,
}
impl Ratio {
pub fn new(num: i32, den: u32) -> Result<Self> {
if den == 0 {
return Err(Error::invalid("Parallax denominator must be positive"));
}
let (mut a, mut b) = (num.unsigned_abs(), den);
while b != 0 {
(a, b) = (b, a % b);
}
let divisor = a.max(1);
Ok(Self {
num: (num as i64 / divisor as i64) as i32,
den: den / divisor,
})
}
pub fn numerator(self) -> i32 {
self.num
}
pub fn denominator(self) -> u32 {
self.den
}
pub fn offset(self, camera: i32) -> i64 {
(camera as i64 * self.num as i64).div_euclid(self.den as i64)
}
}
impl Default for Ratio {
fn default() -> Self {
Self { num: 1, den: 1 }
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Repeat {
#[default]
None,
X,
Y,
Both,
}
impl Repeat {
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::X => "x",
Self::Y => "y",
Self::Both => "both",
}
}
pub(crate) fn x(self) -> bool {
matches!(self, Self::X | Self::Both)
}
pub(crate) fn y(self) -> bool {
matches!(self, Self::Y | Self::Both)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Anchor {
#[default]
TopLeft,
BottomLeft,
Center,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum SourceRef {
Sprite(String),
Surface(String),
Tilemap(String),
Background {
name: String,
layer: Option<String>,
},
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct BackgroundRenderOptions {
pub camera: (i32, i32),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LayerOptions {
pub origin: (i32, i32),
pub repeat: Repeat,
pub parallax: (Ratio, Ratio),
pub clip: Option<Rect>,
}
impl Default for LayerOptions {
fn default() -> Self {
Self {
origin: (0, 0),
repeat: Repeat::None,
parallax: (Ratio::default(), Ratio::default()),
clip: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct PlacementOptions {
pub at: (i32, i32),
pub anchor: Anchor,
pub crop: Option<Rect>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BackgroundPlacement {
pub(crate) source: SourceRef,
pub(crate) sprite: Option<Arc<Sprite>>,
pub(crate) options: PlacementOptions,
pub(crate) position: Position,
}
impl BackgroundPlacement {
pub fn source(&self) -> &SourceRef {
&self.source
}
pub fn options(&self) -> PlacementOptions {
self.options
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BackgroundLayer {
pub(crate) name: String,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) options: LayerOptions,
pub(crate) items: Vec<BackgroundPlacement>,
pub(crate) position: Position,
}
impl BackgroundLayer {
pub fn name(&self) -> &str {
&self.name
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn options(&self) -> &LayerOptions {
&self.options
}
pub fn placements(&self) -> &[BackgroundPlacement] {
&self.items
}
pub fn sample(&self, x: u32, y: u32, camera: (i32, i32)) -> Option<(u32, u32)> {
let u = x as i64 - self.options.origin.0 as i64 + self.options.parallax.0.offset(camera.0);
let v = y as i64 - self.options.origin.1 as i64 + self.options.parallax.1.offset(camera.1);
let axis = |v: i64, n: u32, repeat: bool| {
if repeat {
Some(v.rem_euclid(n as i64) as u32)
} else {
(v >= 0 && v < n as i64).then_some(v as u32)
}
};
Some((
axis(u, self.width, self.options.repeat.x())?,
axis(v, self.height, self.options.repeat.y())?,
))
}
pub(crate) fn native(
&self,
doc: &Document,
limits: &Limits,
b: &mut Budget,
depth: usize,
) -> Result<Image> {
limits.check_image(self.width, self.height)?;
b.pixels(self.width as u64 * self.height as u64)?;
let mut image = Image::new_with(self.width, self.height, limits)?;
for item in &self.items {
let source = item.view(doc)?;
let placed = match &source {
SourceView::Background(bg, layer) => {
Some(bg.placed_image(doc, *layer, limits, b, depth + 1)?)
}
_ => None,
};
let lookups = source.lookups(doc);
let crop = item.crop(source.size()?)?;
let (x, y) = item.top_left(crop);
let (l, t, r, d) = crate::screen::placement_bounds(
image.width(),
image.height(),
crop.width,
crop.height,
x,
y,
);
b.work(
((r - l) as u64 * (d - t) as u64)
.checked_mul(source.cost())
.ok_or_else(|| Error::limit("Source work overflow"))?,
)?;
for py in t..d {
for px in l..r {
let (sx, sy) = (crop.x + (px - x) as u32, crop.y + (py - y) as u32);
let color = match &placed {
Some(placed) => placed.pixel(sx, sy).unwrap(),
None => source.pixel(&lookups, sx, sy),
};
let below = image.pixel(px as u32, py as u32).unwrap();
image.set_pixel(px as u32, py as u32, color.over(below));
}
}
}
if let Some(clip) = self.options.clip {
b.work(self.width as u64 * self.height as u64)?;
for y in 0..self.height {
for x in 0..self.width {
if x < clip.x
|| y < clip.y
|| x >= clip.x + clip.width
|| y >= clip.y + clip.height
{
image.set_pixel(x, y, Color::TRANSPARENT);
}
}
}
}
Ok(image)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[doc=include_str!("../docs/backgrounds.md")]
pub struct Background {
pub(crate) commands: usize,
pub(crate) name: String,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) layers: Vec<BackgroundLayer>,
pub(crate) seed: u64,
}
impl Background {
pub fn name(&self) -> &str {
&self.name
}
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn layers(&self) -> &[BackgroundLayer] {
&self.layers
}
pub fn render(&self, doc: &Document, options: &BackgroundRenderOptions) -> Result<Image> {
self.render_with(doc, options, &Limits::default())
}
pub fn render_with(
&self,
doc: &Document,
options: &BackgroundRenderOptions,
limits: &Limits,
) -> Result<Image> {
let mut b = Budget::new(limits);
let mut shared = std::collections::HashSet::new();
b.pixels(doc.scene_storage_seen(&mut shared))?;
if !doc.backgrounds.iter().any(|bg| std::ptr::eq(bg, self)) {
b.pixels(self.storage(&mut shared))?;
}
let layers = self.native_layers(doc, limits, &mut b)?;
self.flatten(&layers, options, limits, &mut b)
}
pub(crate) fn native_layers(
&self,
doc: &Document,
limits: &Limits,
b: &mut Budget,
) -> Result<Vec<Image>> {
self.native_layers_at(doc, limits, b, 0)
}
fn native_layers_at(
&self,
doc: &Document,
limits: &Limits,
b: &mut Budget,
depth: usize,
) -> Result<Vec<Image>> {
if self.layers.len() > limits.max_layers {
return Err(Error::limit("Too many background layers"));
}
if depth > limits.max_nesting {
return Err(Error::limit(format!(
"Backgrounds placed in backgrounds more than the limit of {} deep",
limits.max_nesting
)));
}
self.layers
.iter()
.map(|l| l.native(doc, limits, b, depth))
.collect()
}
fn placed_image(
&self,
doc: &Document,
layer: Option<usize>,
limits: &Limits,
b: &mut Budget,
depth: usize,
) -> Result<Image> {
let options = BackgroundRenderOptions::default();
match layer {
None => {
let layers = self.native_layers_at(doc, limits, b, depth)?;
self.flatten(&layers, &options, limits, b)
}
Some(i) => {
if depth > limits.max_nesting {
return Err(Error::limit(format!(
"Backgrounds placed in backgrounds more than the limit of {} deep",
limits.max_nesting
)));
}
let native = self.layers[i].native(doc, limits, b, depth)?;
self.compose(
std::iter::once((&self.layers[i], &native)),
1,
&options,
limits,
b,
)
}
}
}
pub(crate) fn storage(&self, sprites: &mut std::collections::HashSet<*const Sprite>) -> u64 {
self.layers
.iter()
.map(|l| {
l.items.len() as u64
+ l.items
.iter()
.filter_map(|p| p.sprite.as_ref())
.filter(|s| sprites.insert(Arc::as_ptr(s)))
.map(|s| s.width as u64 * s.height as u64 * s.layers.len() as u64)
.sum::<u64>()
})
.sum()
}
pub(crate) fn flatten(
&self,
layers: &[Image],
options: &BackgroundRenderOptions,
limits: &Limits,
b: &mut Budget,
) -> Result<Image> {
self.compose(
self.layers.iter().zip(layers),
self.layers.len(),
options,
limits,
b,
)
}
fn compose<'l>(
&self,
layers: impl Iterator<Item = (&'l BackgroundLayer, &'l Image)>,
count: usize,
options: &BackgroundRenderOptions,
limits: &Limits,
b: &mut Budget,
) -> Result<Image> {
b.pixels(self.width as u64 * self.height as u64)?;
let mut image = Image::new_with(self.width, self.height, limits)?;
b.work(
(self.width as u64 * self.height as u64)
.checked_mul(count as u64)
.ok_or_else(|| Error::limit("View work overflow"))?,
)?;
for (layer, native) in layers {
for y in 0..self.height {
for x in 0..self.width {
if let Some((u, v)) = layer.sample(x, y, options.camera) {
let above = native.pixel(u, v).unwrap();
let below = image.pixel(x, y).unwrap();
image.set_pixel(x, y, above.over(below));
}
}
}
}
Ok(image)
}
}
impl Document {
pub fn surfaces(&self) -> &[Surface] {
&self.surfaces
}
pub fn surface(&self, name: &str) -> Option<&Surface> {
self.surfaces.iter().find(|s| s.name() == name)
}
pub fn backgrounds(&self) -> &[Background] {
&self.backgrounds
}
pub fn background(&self, name: &str) -> Option<&Background> {
self.backgrounds.iter().find(|s| s.name() == name)
}
pub(crate) fn chain_base(&self, name: &str) -> Option<&Sprite> {
self.sprite(name).or_else(|| {
let (set, tile) = name.split_once('.')?;
let tileset = self.tileset(set)?;
tileset.tile(tileset.index(tile)?)
})
}
pub(crate) fn scene_storage(&self) -> u64 {
self.scene_storage_seen(&mut std::collections::HashSet::new())
}
pub(crate) fn scene_commands(&self) -> usize {
self.surfaces.iter().map(|s| s.commands).sum::<usize>()
+ self.backgrounds.iter().map(|b| b.commands).sum::<usize>()
}
pub(crate) fn scene_storage_seen(
&self,
sprites: &mut std::collections::HashSet<*const Sprite>,
) -> u64 {
self.surfaces.iter().map(|s| s.storage()).sum::<u64>()
+ self
.backgrounds
.iter()
.map(|b| b.storage(sprites))
.sum::<u64>()
}
}
pub(crate) enum SourceView<'a> {
Sprite(&'a Sprite),
Surface(&'a Surface),
Tilemap(&'a Tilemap, &'a Tileset),
Background(&'a Background, Option<usize>),
}
impl SourceView<'_> {
pub fn size(&self) -> Result<(u32, u32)> {
match self {
Self::Background(bg, _) => Ok(bg.size()),
Self::Sprite(s) => Ok((s.width, s.height)),
Self::Surface(s) => Ok(s.size()),
Self::Tilemap(m, t) => Ok((
m.width
.checked_mul(t.tile_width)
.ok_or_else(|| Error::limit("Map width overflow"))?,
m.height
.checked_mul(t.tile_height)
.ok_or_else(|| Error::limit("Map height overflow"))?,
)),
}
}
fn cost(&self) -> u64 {
match self {
Self::Background(..) => 1,
Self::Sprite(s) => s.layers.len() as u64,
Self::Surface(_) => 1,
Self::Tilemap(_, t) => t
.tiles
.iter()
.map(|s| s.layers.len() as u64)
.max()
.unwrap_or(1),
}
}
fn lookups<'a>(&'a self, doc: &'a Document) -> Vec<crate::sprite::ColorLookup<'a>> {
let current = |p: &'a crate::Palette| {
crate::sprite::ColorLookup::new(doc.palette(p.name()).unwrap_or(p))
};
match self {
Self::Sprite(s) => s.layer_palettes().map(current).collect(),
Self::Surface(s) => vec![current(&s.palette)],
Self::Tilemap(_, t) => vec![current(&t.palette)],
Self::Background(..) => Vec::new(),
}
}
fn pixel(&self, lookups: &[crate::sprite::ColorLookup<'_>], x: u32, y: u32) -> Color {
match self {
Self::Background(..) => Color::TRANSPARENT,
Self::Sprite(s) => sprite_pixel(s, lookups, x, y),
Self::Surface(s) => s
.key(x, y)
.map(|k| lookups[0].get(k))
.unwrap_or(Color::TRANSPARENT),
Self::Tilemap(m, t) => m
.tile(x / t.tile_width, y / t.tile_height)
.and_then(|i| i.checked_sub(1))
.and_then(|i| t.tiles.get(i as usize))
.map(|s| sprite_pixel(s, lookups, x % t.tile_width, y % t.tile_height))
.unwrap_or(Color::TRANSPARENT),
}
}
}
fn sprite_pixel(s: &Sprite, lookups: &[crate::sprite::ColorLookup<'_>], x: u32, y: u32) -> Color {
let i = y as usize * s.width as usize + x as usize;
s.layers
.iter()
.enumerate()
.fold(Color::TRANSPARENT, |c, (n, l)| {
lookups[if lookups.len() == 1 { 0 } else { n }]
.get(l.keys[i])
.over(c)
})
}
impl BackgroundPlacement {
pub(crate) fn view<'a>(&'a self, doc: &'a Document) -> Result<SourceView<'a>> {
let missing = || Error::new(ErrorCode::UnknownName, "Unknown background source");
match &self.source {
SourceRef::Sprite(name) => self
.sprite
.as_deref()
.or_else(|| doc.sprite(name))
.map(SourceView::Sprite)
.ok_or_else(missing),
SourceRef::Surface(name) => doc
.surface(name)
.map(SourceView::Surface)
.ok_or_else(missing),
SourceRef::Tilemap(name) => {
let m = doc.tilemap(name).ok_or_else(missing)?;
let t = doc.tileset(m.tileset()).ok_or_else(missing)?;
Ok(SourceView::Tilemap(m, t))
}
SourceRef::Background { name, layer } => {
let bg = doc.background(name).ok_or_else(missing)?;
let layer = match layer {
None => None,
Some(layer) => Some(
bg.layers
.iter()
.position(|l| &l.name == layer)
.ok_or_else(|| {
Error::new(
ErrorCode::UnknownName,
format!("Background `{name}` has no layer `{layer}`"),
)
})?,
),
};
Ok(SourceView::Background(bg, layer))
}
}
}
pub(crate) fn crop(&self, size: (u32, u32)) -> Result<Rect> {
let crop = self.options.crop.unwrap_or(Rect {
x: 0,
y: 0,
width: size.0,
height: size.1,
});
check_rect(crop, size)?;
Ok(crop)
}
pub(crate) fn top_left(&self, crop: Rect) -> (i64, i64) {
let (x, y) = (self.options.at.0 as i64, self.options.at.1 as i64);
match self.options.anchor {
Anchor::TopLeft => (x, y),
Anchor::BottomLeft => (x, y - crop.height as i64),
Anchor::Center => (x - crop.width as i64 / 2, y - crop.height as i64 / 2),
}
}
}
pub(crate) fn check_rect(rect: Rect, size: (u32, u32)) -> Result<()> {
if rect.width == 0
|| rect.height == 0
|| rect.x as u64 + rect.width as u64 > size.0 as u64
|| rect.y as u64 + rect.height as u64 > size.1 as u64
{
return Err(Error::invalid(
"Crop/clip must be a positive in-bounds rectangle",
));
}
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ScatterAmount {
Count(u64),
Density { num: u32, den: u32 },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ScatterOptions {
pub amount: ScatterAmount,
pub spacing: u32,
pub attempts: Option<u64>,
pub seed: u64,
pub mask: Option<(String, char)>,
pub baseline: Option<(String, String)>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WeightedVariant {
pub source: SourceRef,
pub weight: u32,
}
pub(crate) struct ScatterContext<'a> {
pub background: &'a str,
pub layer: &'a BackgroundLayer,
pub operation: &'a str,
pub owner_seed: u64,
pub position: Position,
}
pub(crate) fn scatter(
doc: &Document,
c: ScatterContext<'_>,
options: &ScatterOptions,
variants: &[(WeightedVariant, Option<Arc<Sprite>>)],
b: &mut Budget,
) -> Result<Vec<BackgroundPlacement>> {
let layer = c.layer;
let mask = options
.mask
.as_ref()
.map(|(name, key)| {
let s = doc
.surface(name)
.ok_or_else(|| Error::new(ErrorCode::UnknownName, "Unknown scatter mask"))?;
s.check_key(*key)?;
Ok::<_, Error>((s, *key))
})
.transpose()?;
let baseline = options
.baseline
.as_ref()
.map(|(name, profile)| {
let s = doc
.surface(name)
.ok_or_else(|| Error::new(ErrorCode::UnknownName, "Unknown baseline surface"))?;
let p = s
.profile(profile)
.ok_or_else(|| Error::new(ErrorCode::UnknownName, "Unknown baseline profile"))?;
Ok::<_, Error>((s, p))
})
.transpose()?;
if mask.is_some_and(|(s, _)| s.size() != layer.size())
|| baseline.is_some_and(|(s, _)| s.size() != layer.size())
{
return Err(Error::new(
ErrorCode::SizeMismatch,
"Scatter mask/profile must match native layer size",
));
}
if variants.is_empty()
|| variants
.iter()
.any(|(v, _)| v.weight == 0 || !matches!(v.source, SourceRef::Sprite(_)))
{
return Err(Error::invalid(
"Scatter needs positive weighted sprite variants",
));
}
let weights = variants.iter().try_fold(0u64, |sum, (v, _)| {
sum.checked_add(v.weight as u64)
.ok_or_else(|| Error::limit("Variant weight overflow"))
})?;
let y_at = |x: u32, y: u32| baseline.map_or(y, |(_, p)| p.heights()[x as usize]);
let eligible =
|x: u32, y: u32| mask.is_none_or(|(s, k)| s.key(x, y.min(layer.height - 1)) == Some(k));
let count = match options.amount {
ScatterAmount::Count(n) => n,
ScatterAmount::Density { num, den } => {
if den == 0 || num > den {
return Err(Error::invalid(
"Scatter density must be in 0..=1 with positive denominator",
));
}
let rows = if baseline.is_some() { 1 } else { layer.height };
b.work(layer.width as u64 * rows as u64)?;
let mut n = 0u64;
for y in 0..rows {
for x in 0..layer.width {
n += u64::from(eligible(x, y_at(x, y)));
}
}
((n as u128 * num as u128) / den as u128) as u64
}
};
if options.attempts == Some(0) {
return Err(Error::invalid("Scatter attempts must be positive"));
}
if count == 0 {
return Ok(Vec::new());
}
let attempts = options.attempts.unwrap_or(
count
.checked_mul(32)
.ok_or_else(|| Error::limit("Scatter attempts overflow"))?
.max(64),
);
if count > isize::MAX as u64 / std::mem::size_of::<BackgroundPlacement>() as u64 {
return Err(Error::limit(
"Scatter records exceed platform allocation limit",
));
}
b.pixels(count)?;
let seed = scene_seed(
&["scatter", c.background, &layer.name, c.operation],
&[c.owner_seed, options.seed],
);
let mut items: Vec<BackgroundPlacement> = Vec::new();
for attempt in 0..attempts {
b.attempts(1)?;
b.work(1)?;
let x = random(seed, attempt, 0, layer.width as u64) as u32;
let y = y_at(x, random(seed, attempt, 1, layer.height as u64) as u32);
let mut draw = random(seed, attempt, 2, weights);
let (variant, sprite) = variants
.iter()
.find(|(v, _)| {
if draw < v.weight as u64 {
true
} else {
draw -= v.weight as u64;
false
}
})
.unwrap();
if !eligible(x, y) {
continue;
}
let mut close = false;
for prior in &items {
b.work(1)?;
let (mut dx, mut dy) = (
(prior.options.at.0 as i64 - x as i64).unsigned_abs(),
(prior.options.at.1 as i64 - y as i64).unsigned_abs(),
);
if layer.options.repeat.x() {
dx = dx.min(layer.width as u64 - dx);
}
if layer.options.repeat.y() {
dy = dy.min(layer.height as u64 - dy);
}
if (dx == 0 && dy == 0)
|| dx * dx + dy * dy < (options.spacing as u64) * (options.spacing as u64)
{
close = true;
break;
}
}
if close {
continue;
}
items.push(BackgroundPlacement {
source: variant.source.clone(),
sprite: sprite.clone(),
options: PlacementOptions {
at: (x as i32, y as i32),
anchor: Anchor::BottomLeft,
crop: None,
},
position: c.position,
});
if items.len() as u64 == count {
return Ok(items);
}
}
Err(Error::invalid(format!(
"Scatter target {count}, accepted {}, attempts {attempts}; increase attempts, reduce count/spacing, or widen the mask",
items.len()
)))
}