mod read;
mod resolve;
pub(crate) use read::read_transitions;
pub use resolve::{ResolvedChunk, TerrainSource, variant_hash};
use std::{collections::BTreeMap, ops::Range};
pub const TRANSITIONS_FORMAT: &str = "tilewright.transitions/1";
pub const MAXIMUM_TERRAINS: usize = 1_024;
pub const MAXIMUM_TERRAIN_PROTOS: usize = 1_024;
pub const MAXIMUM_TERRAIN_TRANSITIONS: usize = 8;
pub const MAXIMUM_TRANSITIONS: usize = 256;
pub const MAXIMUM_TILE_TRANSITIONS: usize = 9 * MAXIMUM_TERRAIN_TRANSITIONS;
pub const MAXIMUM_VARIANTS: usize = 256;
pub const MAXIMUM_WEIGHT: u32 = 65_535;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Draw {
Inside,
Outside,
}
impl Draw {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Inside => "inside",
Self::Outside => "outside",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
match text {
"inside" => Some(Self::Inside),
"outside" => Some(Self::Outside),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Model {
None,
Sides,
Corners,
Blob,
}
const BLOB_CORNERS: [(u8, u8, u8); 4] = [(2, 1, 4), (8, 4, 16), (32, 16, 64), (128, 64, 1)];
impl Model {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Sides => "sides",
Self::Corners => "corners",
Self::Blob => "blob",
}
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
match text {
"none" => Some(Self::None),
"sides" => Some(Self::Sides),
"corners" => Some(Self::Corners),
"blob" => Some(Self::Blob),
_ => None,
}
}
#[must_use]
pub fn pattern(self, draw: Draw, shows: [bool; 8]) -> u8 {
let [n, ne, e, se, s, sw, w, nw] = shows;
let bit = |on: bool, value: u8| if on { value } else { 0 };
match self {
Self::None => 0,
Self::Sides => bit(n, 1) | bit(e, 2) | bit(s, 4) | bit(w, 8),
Self::Corners => {
let corner = |a: bool, diagonal: bool, b: bool| match draw {
Draw::Inside => a && diagonal && b,
Draw::Outside => a || diagonal || b,
};
bit(corner(n, ne, e), 1)
| bit(corner(e, se, s), 2)
| bit(corner(s, sw, w), 4)
| bit(corner(w, nw, n), 8)
}
Self::Blob => {
let corner = |a: bool, diagonal: bool, b: bool| match draw {
Draw::Inside => a && diagonal && b,
Draw::Outside => diagonal && !a && !b,
};
bit(n, 1)
| bit(corner(n, ne, e), 2)
| bit(e, 4)
| bit(corner(e, se, s), 8)
| bit(s, 16)
| bit(corner(s, sw, w), 32)
| bit(w, 64)
| bit(corner(w, nw, n), 128)
}
}
}
#[must_use]
pub fn produces(self, draw: Draw, pattern: u8) -> bool {
if draw == Draw::Outside && pattern == 0 {
return false;
}
match self {
Self::None => pattern == 0,
Self::Sides | Self::Corners => pattern < 16,
Self::Blob => BLOB_CORNERS.iter().all(|&(corner, a, b)| {
pattern & corner == 0
|| match draw {
Draw::Inside => pattern & a != 0 && pattern & b != 0,
Draw::Outside => pattern & a == 0 && pattern & b == 0,
}
}),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Variant {
pub proto: String,
pub weight: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Pattern {
variants: Vec<Variant>,
cumulative: Vec<u64>,
}
impl Pattern {
#[must_use]
pub fn variants(&self) -> &[Variant] {
&self.variants
}
pub(crate) fn new(variants: Vec<Variant>) -> Self {
let cumulative = variants
.iter()
.scan(0_u64, |sum, variant| {
*sum += u64::from(variant.weight);
Some(*sum)
})
.collect();
Self {
variants,
cumulative,
}
}
#[must_use]
pub fn pick(&self, hash: u64) -> &str {
let total = self.cumulative.last().copied().unwrap_or(0).max(1);
let point = hash % total;
let index = self.cumulative.partition_point(|sum| *sum <= point);
self.variants
.get(index)
.or_else(|| self.variants.last())
.map_or("", |variant| variant.proto.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Transition {
terrain: usize,
draw: Draw,
model: Model,
layer: usize,
blends: Vec<bool>,
seed: u32,
tiles: BTreeMap<u8, Pattern>,
draws: Box<[Option<u8>; 256]>,
}
impl Transition {
pub(crate) fn new(
(terrain, draw, model, layer): (usize, Draw, Model, usize),
blends: Vec<bool>,
seed: u32,
tiles: BTreeMap<u8, Pattern>,
) -> Self {
let mut draws = Box::new([None; 256]);
for (bits, entry) in (0..=255_u8).zip(draws.iter_mut()) {
let shows = std::array::from_fn(|bit| bits & (1 << bit) != 0);
let pattern = model.pattern(draw, shows);
if (draw == Draw::Inside || pattern != 0) && tiles.contains_key(&pattern) {
*entry = Some(pattern);
}
}
Self {
terrain,
draw,
model,
layer,
blends,
seed,
tiles,
draws,
}
}
#[must_use]
pub fn draw_at(&self, shows: u8, hash: u64) -> Option<&str> {
let pattern = self.draws.get(usize::from(shows)).copied().flatten()?;
Some(self.tiles.get(&pattern)?.pick(hash))
}
#[must_use]
pub fn draws(&self, shows: u8) -> bool {
self.draws
.get(usize::from(shows))
.is_some_and(Option::is_some)
}
#[must_use]
pub const fn terrain(&self) -> usize {
self.terrain
}
#[must_use]
pub const fn draw(&self) -> Draw {
self.draw
}
#[must_use]
pub const fn model(&self) -> Model {
self.model
}
#[must_use]
pub const fn layer(&self) -> usize {
self.layer
}
#[must_use]
pub fn blends_with(&self, terrain: usize) -> bool {
self.blends.get(terrain).copied().unwrap_or(false)
}
#[must_use]
pub const fn seed(&self) -> u32 {
self.seed
}
#[must_use]
pub const fn tiles(&self) -> &BTreeMap<u8, Pattern> {
&self.tiles
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Terrain {
pub id: String,
pub protos: Vec<String>,
pub transitions: Range<usize>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Transitions {
path: String,
terrain_layer: usize,
ground_layers: usize,
terrains: Vec<Terrain>,
list: Vec<Transition>,
by_proto: BTreeMap<String, usize>,
}
impl Transitions {
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub const fn terrain_layer(&self) -> usize {
self.terrain_layer
}
#[must_use]
pub const fn ground_layers(&self) -> usize {
self.ground_layers
}
#[must_use]
pub fn terrains(&self) -> &[Terrain] {
&self.terrains
}
#[must_use]
pub fn transitions(&self) -> &[Transition] {
&self.list
}
#[must_use]
pub fn terrain(&self, id: &str) -> Option<usize> {
self.terrains.iter().position(|terrain| terrain.id == id)
}
#[must_use]
pub fn terrain_of(&self, proto: &str) -> Option<usize> {
self.by_proto.get(proto).copied()
}
pub fn protos(&self) -> impl Iterator<Item = &str> + '_ {
let members = self
.terrains
.iter()
.flat_map(|terrain| terrain.protos.iter().map(String::as_str));
let tiles = self.list.iter().flat_map(|transition| {
transition.tiles.values().flat_map(|pattern| {
pattern
.variants
.iter()
.map(|variant| variant.proto.as_str())
})
});
members.chain(tiles)
}
#[must_use]
pub fn paint(&self, terrain: usize) -> Option<&str> {
self.terrains
.get(terrain)?
.protos
.first()
.map(String::as_str)
}
}
#[cfg(test)]
mod tests;