Skip to main content

tilewright_source/
transitions.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Terrain transitions (`docs/spec/FORMAT.md` section 13): the profile's
5//! rules that choose the edge, corner and variant tiles around painted
6//! terrain, and the deterministic resolver that applies them.
7//!
8//! A world's source stores only the terrain a brush paints.
9//! [`Transitions::resolve_chunk`] derives a chunk's ground tiles from its
10//! terrain and its neighbours', across chunk edges, so the compiler, the
11//! editor and every other tool draw the same tiles.
12
13mod read;
14mod resolve;
15
16pub(crate) use read::read_transitions;
17pub use resolve::{ResolvedChunk, TerrainSource, variant_hash};
18
19use std::{collections::BTreeMap, ops::Range};
20
21/// The `format` of a transitions document.
22pub const TRANSITIONS_FORMAT: &str = "tilewright.transitions/1";
23
24/// Most terrains a transitions document declares.
25pub const MAXIMUM_TERRAINS: usize = 1_024;
26
27/// Most prototypes one terrain lists.
28pub const MAXIMUM_TERRAIN_PROTOS: usize = 1_024;
29
30/// Most transitions one terrain has.
31pub const MAXIMUM_TERRAIN_TRANSITIONS: usize = 8;
32
33/// Most transitions a transitions document has in all.
34pub const MAXIMUM_TRANSITIONS: usize = 256;
35
36/// Most transitions resolving one tile evaluates, however large the
37/// document: the transitions of the tile's own terrain and of the terrains
38/// of its eight neighbours, since an outside transition draws only next to
39/// its terrain.
40pub const MAXIMUM_TILE_TRANSITIONS: usize = 9 * MAXIMUM_TERRAIN_TRANSITIONS;
41
42/// Most variants one pattern lists.
43pub const MAXIMUM_VARIANTS: usize = 256;
44
45/// Largest weight of a variant.
46pub const MAXIMUM_WEIGHT: u32 = 65_535;
47
48/// Where a transition draws.
49#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum Draw {
51    /// On tiles of the transition's own terrain, showing where their
52    /// neighbours differ.
53    Inside,
54    /// On tiles of the terrains it blends with, showing its terrain
55    /// reaching in from their neighbours.
56    Outside,
57}
58
59impl Draw {
60    #[must_use]
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::Inside => "inside",
64            Self::Outside => "outside",
65        }
66    }
67
68    /// The draw a document spells `text`.
69    #[must_use]
70    pub fn parse(text: &str) -> Option<Self> {
71        match text {
72            "inside" => Some(Self::Inside),
73            "outside" => Some(Self::Outside),
74            _ => None,
75        }
76    }
77}
78
79/// The neighbourhood a transition reads, and the patterns it maps to tiles.
80#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub enum Model {
82    /// No neighbours: every tile has pattern 0. Inside only; for variants.
83    None,
84    /// The four sides, N 1, E 2, S 4, W 8: 16 patterns.
85    Sides,
86    /// The four corners, NE 1, SE 2, SW 4, NW 8: the 16 patterns of a
87    /// corner (Wang or marching-squares) set.
88    Corners,
89    /// Sides and corners, N 1, NE 2, E 4, SE 8, S 16, SW 32, W 64, NW 128,
90    /// with each corner counted only where its sides allow: 47 patterns.
91    Blob,
92}
93
94/// A side bit of the blob model and the corners next to it, clockwise from
95/// north: `(corner, side before it, side after it)`.
96const BLOB_CORNERS: [(u8, u8, u8); 4] = [(2, 1, 4), (8, 4, 16), (32, 16, 64), (128, 64, 1)];
97
98impl Model {
99    #[must_use]
100    pub const fn as_str(self) -> &'static str {
101        match self {
102            Self::None => "none",
103            Self::Sides => "sides",
104            Self::Corners => "corners",
105            Self::Blob => "blob",
106        }
107    }
108
109    /// The model a document spells `text`.
110    #[must_use]
111    pub fn parse(text: &str) -> Option<Self> {
112        match text {
113            "none" => Some(Self::None),
114            "sides" => Some(Self::Sides),
115            "corners" => Some(Self::Corners),
116            "blob" => Some(Self::Blob),
117            _ => None,
118        }
119    }
120
121    /// The pattern of a tile whose neighbours, N, NE, E, SE, S, SW, W and
122    /// NW, show the transition's terrain where `shows` is `true`.
123    #[must_use]
124    pub fn pattern(self, draw: Draw, shows: [bool; 8]) -> u8 {
125        let [n, ne, e, se, s, sw, w, nw] = shows;
126        let bit = |on: bool, value: u8| if on { value } else { 0 };
127        match self {
128            Self::None => 0,
129            Self::Sides => bit(n, 1) | bit(e, 2) | bit(s, 4) | bit(w, 8),
130            Self::Corners => {
131                // Inside, a corner shows the terrain where everything
132                // around it does; outside, where anything does.
133                let corner = |a: bool, diagonal: bool, b: bool| match draw {
134                    Draw::Inside => a && diagonal && b,
135                    Draw::Outside => a || diagonal || b,
136                };
137                bit(corner(n, ne, e), 1)
138                    | bit(corner(e, se, s), 2)
139                    | bit(corner(s, sw, w), 4)
140                    | bit(corner(w, nw, n), 8)
141            }
142            Self::Blob => {
143                // Inside, a corner counts only where both its sides
144                // continue; outside, only where neither side already
145                // draws the edge over it.
146                let corner = |a: bool, diagonal: bool, b: bool| match draw {
147                    Draw::Inside => a && diagonal && b,
148                    Draw::Outside => diagonal && !a && !b,
149                };
150                bit(n, 1)
151                    | bit(corner(n, ne, e), 2)
152                    | bit(e, 4)
153                    | bit(corner(e, se, s), 8)
154                    | bit(s, 16)
155                    | bit(corner(s, sw, w), 32)
156                    | bit(w, 64)
157                    | bit(corner(w, nw, n), 128)
158            }
159        }
160    }
161
162    /// Whether this model produces `pattern` when drawing `draw`: the keys
163    /// a transition's `tiles` may have. An outside transition never draws
164    /// pattern 0, where no neighbour shows its terrain.
165    #[must_use]
166    pub fn produces(self, draw: Draw, pattern: u8) -> bool {
167        if draw == Draw::Outside && pattern == 0 {
168            return false;
169        }
170        match self {
171            Self::None => pattern == 0,
172            Self::Sides | Self::Corners => pattern < 16,
173            Self::Blob => BLOB_CORNERS.iter().all(|&(corner, a, b)| {
174                pattern & corner == 0
175                    || match draw {
176                        Draw::Inside => pattern & a != 0 && pattern & b != 0,
177                        Draw::Outside => pattern & a == 0 && pattern & b == 0,
178                    }
179            }),
180        }
181    }
182}
183
184/// One variant of a pattern's tile.
185#[derive(Clone, Debug, Eq, PartialEq)]
186pub struct Variant {
187    /// A `proto` definition.
188    pub proto: String,
189    /// From 1 to [`MAXIMUM_WEIGHT`].
190    pub weight: u32,
191}
192
193/// The variants of one pattern, at least one, with the running sum of their
194/// weights.
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub struct Pattern {
197    variants: Vec<Variant>,
198    /// For each variant, the sum of its weight and those before it; the
199    /// last is the total.
200    cumulative: Vec<u64>,
201}
202
203impl Pattern {
204    /// The variants in document order.
205    #[must_use]
206    pub fn variants(&self) -> &[Variant] {
207        &self.variants
208    }
209
210    pub(crate) fn new(variants: Vec<Variant>) -> Self {
211        let cumulative = variants
212            .iter()
213            .scan(0_u64, |sum, variant| {
214                *sum += u64::from(variant.weight);
215                Some(*sum)
216            })
217            .collect();
218        Self {
219            variants,
220            cumulative,
221        }
222    }
223
224    /// The variant a tile with variant hash `hash` draws: the first at
225    /// which the running sum of weights exceeds `hash` modulo their total.
226    /// A binary search finds it, so a pick costs time in proportion to the
227    /// logarithm of the number of variants.
228    #[must_use]
229    pub fn pick(&self, hash: u64) -> &str {
230        let total = self.cumulative.last().copied().unwrap_or(0).max(1);
231        let point = hash % total;
232        let index = self.cumulative.partition_point(|sum| *sum <= point);
233        // A pattern always has a variant, and the point is below the total.
234        self.variants
235            .get(index)
236            .or_else(|| self.variants.last())
237            .map_or("", |variant| variant.proto.as_str())
238    }
239}
240
241/// One transition of a terrain.
242#[derive(Clone, Debug, Eq, PartialEq)]
243pub struct Transition {
244    terrain: usize,
245    draw: Draw,
246    model: Model,
247    layer: usize,
248    /// Whether it blends with each terrain, by index.
249    blends: Vec<bool>,
250    seed: u32,
251    tiles: BTreeMap<u8, Pattern>,
252    /// For each neighbourhood, the 8 bits of which neighbours show the
253    /// terrain (N 1, NE 2, ... NW 128), the pattern it draws, if `tiles`
254    /// has one for it: resolving a tile looks a transition up here instead
255    /// of computing its pattern.
256    draws: Box<[Option<u8>; 256]>,
257}
258
259impl Transition {
260    pub(crate) fn new(
261        (terrain, draw, model, layer): (usize, Draw, Model, usize),
262        blends: Vec<bool>,
263        seed: u32,
264        tiles: BTreeMap<u8, Pattern>,
265    ) -> Self {
266        let mut draws = Box::new([None; 256]);
267        for (bits, entry) in (0..=255_u8).zip(draws.iter_mut()) {
268            let shows = std::array::from_fn(|bit| bits & (1 << bit) != 0);
269            let pattern = model.pattern(draw, shows);
270            if (draw == Draw::Inside || pattern != 0) && tiles.contains_key(&pattern) {
271                *entry = Some(pattern);
272            }
273        }
274        Self {
275            terrain,
276            draw,
277            model,
278            layer,
279            blends,
280            seed,
281            tiles,
282            draws,
283        }
284    }
285
286    /// The tile this transition draws on a tile whose neighbours, as bits
287    /// N 1, NE 2, ... NW 128, show its terrain where set, for variant hash
288    /// `hash`; `None` where its table has no tile for the pattern.
289    #[must_use]
290    pub fn draw_at(&self, shows: u8, hash: u64) -> Option<&str> {
291        let pattern = self.draws.get(usize::from(shows)).copied().flatten()?;
292        Some(self.tiles.get(&pattern)?.pick(hash))
293    }
294
295    /// Whether this transition draws anything on a tile whose neighbours
296    /// show its terrain as the bits of `shows` do.
297    #[must_use]
298    pub fn draws(&self, shows: u8) -> bool {
299        self.draws
300            .get(usize::from(shows))
301            .is_some_and(Option::is_some)
302    }
303
304    /// The index of its terrain in [`Transitions::terrains`].
305    #[must_use]
306    pub const fn terrain(&self) -> usize {
307        self.terrain
308    }
309
310    #[must_use]
311    pub const fn draw(&self) -> Draw {
312        self.draw
313    }
314
315    #[must_use]
316    pub const fn model(&self) -> Model {
317        self.model
318    }
319
320    /// The index in the profile's ground layers of the layer its tiles go
321    /// into: never the terrain layer, which keeps the painted terrain.
322    #[must_use]
323    pub const fn layer(&self) -> usize {
324        self.layer
325    }
326
327    /// Whether it blends with the terrain at index `terrain`: the terrains
328    /// an outside transition draws onto, or the neighbours an inside one
329    /// draws an edge against.
330    #[must_use]
331    pub fn blends_with(&self, terrain: usize) -> bool {
332        self.blends.get(terrain).copied().unwrap_or(false)
333    }
334
335    #[must_use]
336    pub const fn seed(&self) -> u32 {
337        self.seed
338    }
339
340    /// The tile of each pattern it draws.
341    #[must_use]
342    pub const fn tiles(&self) -> &BTreeMap<u8, Pattern> {
343        &self.tiles
344    }
345}
346
347/// One terrain: the prototypes that are it when painted.
348#[derive(Clone, Debug, Eq, PartialEq)]
349pub struct Terrain {
350    pub id: String,
351    /// At least one `proto` definition; a brush paints the first.
352    pub protos: Vec<String>,
353    /// Its transitions, as indexes into [`Transitions::transitions`].
354    pub transitions: Range<usize>,
355}
356
357/// A profile's terrain transitions, as read and checked by the loader.
358///
359/// Every `Transitions` comes from a load, so its terrains, prototypes,
360/// layers and patterns are consistent; its parts are read through
361/// accessors.
362#[derive(Clone, Debug, Eq, PartialEq)]
363pub struct Transitions {
364    /// Source path of the document, relative to the root.
365    path: String,
366    terrain_layer: usize,
367    ground_layers: usize,
368    terrains: Vec<Terrain>,
369    /// Every transition, in the order they run.
370    list: Vec<Transition>,
371    /// The terrain of each member prototype.
372    by_proto: BTreeMap<String, usize>,
373}
374
375impl Transitions {
376    /// Source path of the document, relative to the root.
377    #[must_use]
378    pub fn path(&self) -> &str {
379        &self.path
380    }
381
382    /// The index in the profile's ground layers of the layer that holds
383    /// painted terrain.
384    #[must_use]
385    pub const fn terrain_layer(&self) -> usize {
386        self.terrain_layer
387    }
388
389    /// How many ground layers the profile has: the grids a resolved chunk
390    /// holds.
391    #[must_use]
392    pub const fn ground_layers(&self) -> usize {
393        self.ground_layers
394    }
395
396    /// Terrains from lowest to highest precedence.
397    #[must_use]
398    pub fn terrains(&self) -> &[Terrain] {
399        &self.terrains
400    }
401
402    /// Every transition, in the order they run: terrains in order, each
403    /// terrain's transitions in order.
404    #[must_use]
405    pub fn transitions(&self) -> &[Transition] {
406        &self.list
407    }
408
409    /// The index of the terrain `id`.
410    #[must_use]
411    pub fn terrain(&self, id: &str) -> Option<usize> {
412        self.terrains.iter().position(|terrain| terrain.id == id)
413    }
414
415    /// The index of the terrain `proto` paints, if it paints one.
416    #[must_use]
417    pub fn terrain_of(&self, proto: &str) -> Option<usize> {
418        self.by_proto.get(proto).copied()
419    }
420
421    /// Every prototype the document names, with repeats: the terrains'
422    /// prototypes and every variant a transition may draw. Resolved tiles
423    /// hold only these and the source's own prototypes.
424    pub fn protos(&self) -> impl Iterator<Item = &str> + '_ {
425        let members = self
426            .terrains
427            .iter()
428            .flat_map(|terrain| terrain.protos.iter().map(String::as_str));
429        let tiles = self.list.iter().flat_map(|transition| {
430            transition.tiles.values().flat_map(|pattern| {
431                pattern
432                    .variants
433                    .iter()
434                    .map(|variant| variant.proto.as_str())
435            })
436        });
437        members.chain(tiles)
438    }
439
440    /// The prototype a brush writes into the terrain layer to paint the
441    /// terrain at index `terrain`: its first.
442    #[must_use]
443    pub fn paint(&self, terrain: usize) -> Option<&str> {
444        self.terrains
445            .get(terrain)?
446            .protos
447            .first()
448            .map(String::as_str)
449    }
450}
451
452#[cfg(test)]
453mod tests;