Skip to main content

delvewright_dsl/siteplan/
ground.rs

1//! **What the whole writes where no place does** (spec-0098 §2b, §2c): the
2//! site's `fill`, resolved once into the one answer every reader asks of it —
3//! how high the terrain stands in a column, and which block a cell of the whole
4//! holds.
5//!
6//! The claim (`claim.rs`), the blockout derivation and the handout all read
7//! this, so the ground a piece is handed, the ground the derivation lays and the
8//! ground the crack check measures are one computation.
9
10use super::*;
11
12/// The site plan's heightmap as a loader read it.
13///
14/// The DSL reads no file: the loader decodes the image the plan names and
15/// attaches it to the [`Campaign`] (`Campaign::heightmap`), and everything here
16/// reads that. Grey values only, row-major over `z` then `x`, so
17/// `grey[pz * width + px]` is column `(region.min.x + px, region.min.z + pz)`.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum HeightmapRead {
20    /// The decoded image.
21    Read {
22        /// The path the plan named, as read.
23        path: String,
24        /// Pixels along `x`.
25        width: u32,
26        /// Pixels along `z`.
27        depth: u32,
28        /// One grey value per pixel.
29        grey: Vec<u8>,
30    },
31    /// The image could not be read or decoded; the loader's words.
32    Unreadable {
33        /// The path the plan named.
34        path: String,
35        /// Why it could not be read.
36        why: String,
37    },
38}
39
40/// The fill, resolved: what the whole holds wherever no place claims a cell.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Ground {
43    kind: GroundKind,
44    region_min: [i64; 3],
45    region_max: [i64; 3],
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
49enum GroundKind {
50    /// No plan, or a plan whose fill does not parse — nothing is written.
51    None,
52    Solid {
53        block: String,
54    },
55    Open {
56        surface: String,
57        below: String,
58        tops: Tops,
59    },
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63enum Tops {
64    /// One surface `y` everywhere.
65    Flat(i64),
66    /// A decoded heightmap: `tops[pz * width + px]`.
67    Map {
68        width: i64,
69        depth: i64,
70        tops: Vec<i64>,
71    },
72    /// A terrain this campaign cannot resolve — an undeclared datum (`DW0112`)
73    /// or a heightmap not read or not region-shaped (`DW0826`). Refused at
74    /// validation; anything asked of it answers as though there were no
75    /// terrain, which only a refused plan ever sees.
76    Unresolved,
77}
78
79/// The surface `y` a heightmap pixel stands for: `base_y + value × range / 255`,
80/// integer division.
81#[must_use]
82pub fn pixel_top(base_y: i64, range: u32, value: u8) -> i64 {
83    base_y + (i64::from(value) * i64::from(range)) / 255
84}
85
86impl Ground {
87    /// The ground a campaign's site plan declares.
88    #[must_use]
89    pub fn of(c: &Campaign) -> Ground {
90        let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
91            return Ground {
92                kind: GroundKind::None,
93                region_min: [0; 3],
94                region_max: [-1; 3],
95            };
96        };
97        let region_min = plan.region.min;
98        let region_max = plan.region.max();
99        let kind = match &plan.fill {
100            Fill::Solid { block } => GroundKind::Solid {
101                block: block.clone(),
102            },
103            Fill::Open {
104                terrain,
105                surface,
106                below,
107            } => GroundKind::Open {
108                surface: surface.clone(),
109                below: below.clone(),
110                tops: Self::tops(plan, terrain, c.heightmap.as_ref()),
111            },
112        };
113        Ground {
114            kind,
115            region_min,
116            region_max,
117        }
118    }
119
120    fn tops(plan: &SitePlanContent, terrain: &Terrain, read: Option<&HeightmapRead>) -> Tops {
121        match terrain {
122            Terrain::Flat { datum } => plan
123                .datums
124                .iter()
125                .find(|d| &d.id == datum)
126                .map_or(Tops::Unresolved, |d| Tops::Flat(d.y - 1)),
127            Terrain::Heightmap {
128                heightmap,
129                base_y,
130                range,
131            } => {
132                let Some(HeightmapRead::Read {
133                    path,
134                    width,
135                    depth,
136                    grey,
137                }) = read
138                else {
139                    return Tops::Unresolved;
140                };
141                let (w, d) = (
142                    i64::from(plan.region.extent[0].get()),
143                    i64::from(plan.region.extent[2].get()),
144                );
145                if path != heightmap || i64::from(*width) != w || i64::from(*depth) != d {
146                    return Tops::Unresolved;
147                }
148                Tops::Map {
149                    width: w,
150                    depth: d,
151                    tops: grey
152                        .iter()
153                        .map(|v| pixel_top(*base_y, *range, *v))
154                        .collect(),
155                }
156            }
157        }
158    }
159
160    /// A `solid` site of `block`, standing alone — for a caller that has no
161    /// campaign (a test, a probe).
162    #[must_use]
163    pub fn solid(block: &str) -> Ground {
164        Ground {
165            kind: GroundKind::Solid {
166                block: block.to_string(),
167            },
168            region_min: [i64::MIN / 4; 3],
169            region_max: [i64::MAX / 4; 3],
170        }
171    }
172
173    /// An `open` site over one flat surface `y`, standing alone.
174    #[must_use]
175    pub fn open_flat(top: i64, surface: &str, below: &str) -> Ground {
176        Ground {
177            kind: GroundKind::Open {
178                surface: surface.to_string(),
179                below: below.to_string(),
180                tops: Tops::Flat(top),
181            },
182            region_min: [i64::MIN / 4; 3],
183            region_max: [i64::MAX / 4; 3],
184        }
185    }
186
187    /// True for an `open` site.
188    #[must_use]
189    pub fn is_open(&self) -> bool {
190        matches!(self.kind, GroundKind::Open { .. })
191    }
192
193    /// True when the plan declares a fill this ground answers for.
194    #[must_use]
195    pub fn is_declared(&self) -> bool {
196        !matches!(self.kind, GroundKind::None)
197    }
198
199    /// The terrain's surface `y` in column `(x, z)`, on an `open` site whose
200    /// terrain resolved. A column outside the region reads the nearest edge
201    /// column, so a ring standing one cell past the region's rim still meets
202    /// the ground the rim has (`DW0826` refuses a claim that leaves the region).
203    #[must_use]
204    pub fn top(&self, x: i64, z: i64) -> Option<i64> {
205        let GroundKind::Open { tops, .. } = &self.kind else {
206            return None;
207        };
208        match tops {
209            Tops::Flat(y) => Some(*y),
210            Tops::Map { width, depth, tops } => {
211                let px = (x - self.region_min[0]).clamp(0, width - 1);
212                let pz = (z - self.region_min[2]).clamp(0, depth - 1);
213                tops.get(usize::try_from(pz * width + px).ok()?).copied()
214            }
215            Tops::Unresolved => None,
216        }
217    }
218
219    /// The block the whole holds at `cell` where nothing — no place, no volume
220    /// — claims it; `None` is air.
221    #[must_use]
222    pub fn fill_block(&self, cell: [i64; 3]) -> Option<&str> {
223        match &self.kind {
224            GroundKind::None => None,
225            GroundKind::Solid { block } => Some(block),
226            GroundKind::Open { surface, below, .. } => {
227                let top = self.top(cell[0], cell[2])?;
228                match cell[1].cmp(&top) {
229                    std::cmp::Ordering::Greater => None,
230                    std::cmp::Ordering::Equal => Some(surface),
231                    std::cmp::Ordering::Less => Some(below),
232                }
233            }
234        }
235    }
236
237    /// The block a **fixed ground** cell holds (spec-0098 §2 rule 0) — a ring
238    /// cell at or under its ground height `g`: the `surface` block at `g`,
239    /// `below` under it on an `open` site; the `solid` fill's block on a solid
240    /// one. Volumes never reach the ring's ground: it is the terrain continued
241    /// to the plot's edge, block for block.
242    #[must_use]
243    pub fn ground_block(&self, cell: [i64; 3], g: i64) -> &str {
244        match &self.kind {
245            GroundKind::None => "minecraft:air",
246            GroundKind::Solid { block } => block,
247            GroundKind::Open { surface, below, .. } => {
248                if cell[1] >= g {
249                    surface
250                } else {
251                    below
252                }
253            }
254        }
255    }
256
257    /// The block a volume of `role` is made of when it names none: the fill's
258    /// own block of that kind (spec-0098 §2b). `None` is air.
259    #[must_use]
260    pub fn volume_block(&self, role: VolumeRole, cell: [i64; 3], vol_top: i64) -> Option<&str> {
261        match (role, &self.kind) {
262            (VolumeRole::Clearance, _) | (_, GroundKind::None) => None,
263            (_, GroundKind::Solid { block }) => Some(block),
264            (VolumeRole::Massif, GroundKind::Open { below, .. }) => Some(below),
265            (VolumeRole::Ground, GroundKind::Open { surface, below, .. }) => {
266                Some(if cell[1] == vol_top { surface } else { below })
267            }
268        }
269    }
270
271    /// The region's inclusive corners.
272    #[must_use]
273    pub fn region(&self) -> ([i64; 3], [i64; 3]) {
274        (self.region_min, self.region_max)
275    }
276}