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;