all_is_cubes/space/
builder.rs

1//! Lesser-used helpers for [`Builder`].
2
3#![expect(clippy::elidable_lifetime_names, reason = "names for clarity")]
4
5use alloc::boxed::Box;
6
7use crate::behavior::BehaviorSet;
8use crate::block::{AIR, Block};
9use crate::character::Spawn;
10use crate::math::{FreePoint, Rgb, Vol};
11use crate::space::{
12    BlockIndex, GridAab, LightPhysics, PackedLight, Palette, PaletteError, SetCubeError, Sky,
13    Space, SpacePhysics,
14};
15use crate::universe::ReadTicket;
16
17/// Builder of [`Space`]s.
18///
19/// To create one, call [`Space::builder()`](Space::builder) or [`Builder::default()`].
20///
21/// # Type parameters
22///
23/// * `B` is either `()` or `Vol<()>` according to whether the bounds have been specified.
24#[derive(Clone, Debug)]
25#[must_use]
26pub struct Builder<'universe, B> {
27    pub(super) read_ticket: ReadTicket<'universe>,
28    pub(super) bounds: B,
29    pub(super) spawn: Option<Spawn>,
30    pub(super) physics: SpacePhysics,
31    pub(super) behaviors: BehaviorSet<Space>,
32    pub(super) contents: Fill,
33}
34
35#[derive(Clone, Debug)]
36pub(super) enum Fill {
37    Block(Block),
38    Data {
39        /// Note: this palette has its block counts already set to match contents
40        palette: Palette,
41        contents: Vol<Box<[BlockIndex]>>,
42        light: Option<Vol<Box<[PackedLight]>>>,
43    },
44}
45
46impl<'universe, B> Builder<'universe, B> {
47    /// Sets the [`ReadTicket`] that will be used by builder operations that evaluate blocks.
48    ///
49    /// The default is [`ReadTicket::stub()`].
50    pub fn read_ticket<'u2>(self, read_ticket: ReadTicket<'u2>) -> Builder<'u2, B> {
51        Builder {
52            read_ticket,
53            bounds: self.bounds,
54            spawn: self.spawn,
55            physics: self.physics,
56            behaviors: self.behaviors,
57            contents: self.contents,
58        }
59    }
60
61    /// Sets the [`Block`] that the space's volume will be filled with.
62    ///
63    /// Calling this method will replace any previous specification of the contents,
64    /// such as [`palette_and_contents()`](Self::palette_and_contents()).
65    pub fn filled_with(mut self, block: Block) -> Self {
66        self.contents = Fill::Block(block);
67        self
68    }
69
70    /// Sets the value for [`Space::physics`], which determines global characteristics
71    /// of gravity and light in the space.
72    pub fn physics(mut self, physics: SpacePhysics) -> Self {
73        self.physics = physics;
74        self
75    }
76
77    /// Sets the value of [`SpacePhysics::sky`] for the space.
78    pub fn sky(mut self, sky: Sky) -> Self {
79        self.physics.sky = sky;
80        self
81    }
82
83    /// Sets the value of [`SpacePhysics::sky`] for the space to a uniform color.
84    pub fn sky_color(self, color: Rgb) -> Self {
85        self.sky(Sky::Uniform(color))
86    }
87
88    /// Sets the value of [`SpacePhysics::light`] for the space, which determines the
89    /// behavior of light within the space.
90    pub fn light_physics(mut self, light_physics: LightPhysics) -> Self {
91        self.physics.light = light_physics;
92        self
93    }
94
95    /// Sets the value for [`Space::spawn`], which determines the default circumstances of
96    /// new characters.
97    ///
98    /// If not set, the default spawn position will be [0, 0, 0].
99    /// (TODO: Improve this and document it centrally.)
100    pub fn spawn(mut self, spawn: Spawn) -> Self {
101        self.spawn = Some(spawn);
102        self
103    }
104
105    /// TODO: not sure if this is good public API
106    #[allow(unused, reason = "currently only used on feature=save")]
107    pub(crate) fn behaviors(mut self, behaviors: BehaviorSet<Space>) -> Self {
108        self.behaviors = behaviors;
109        self
110    }
111}
112
113impl<'universe, B: Bounds> Builder<'universe, B> {
114    /// Set the bounds unless they have already been set.
115    pub fn bounds_if_not_set(
116        self,
117        bounds_fn: impl FnOnce() -> GridAab,
118    ) -> Builder<'universe, Vol<()>> {
119        // Delegate to the trait. (This method exists so the trait need not be imported.)
120        Bounds::bounds_if_not_set(self, bounds_fn)
121    }
122}
123
124impl<'universe> Builder<'universe, ()> {
125    /// Use [`Builder::default()`] as the public way to call this.
126    #[track_caller] // used for ReadTicket debugging
127    pub(super) fn new() -> Self {
128        Self {
129            read_ticket: ReadTicket::stub(),
130            bounds: (),
131            spawn: None,
132            physics: SpacePhysics::DEFAULT,
133            behaviors: BehaviorSet::new(),
134            contents: Fill::Block(AIR),
135        }
136    }
137
138    /// Set the bounds of the space, outside which no blocks may be placed.
139    ///
140    /// Panics if `bounds` has a volume exceeding `usize::MAX`.
141    /// (But there will likely be a memory allocation failure well below that point.)
142    pub fn bounds(self, bounds: GridAab) -> Builder<'universe, Vol<()>> {
143        Builder {
144            read_ticket: self.read_ticket,
145            bounds: bounds.to_vol().unwrap(),
146            spawn: self.spawn,
147            physics: self.physics,
148            behaviors: self.behaviors,
149            contents: self.contents,
150        }
151    }
152}
153
154impl Builder<'_, Vol<()>> {
155    /// Sets the default spawn location of new characters.
156    ///
157    /// Panics if any of the given coordinates is infinite or NaN.
158    #[track_caller]
159    pub fn spawn_position(mut self, position: FreePoint) -> Self {
160        assert!(
161            position.to_vector().square_length().is_finite(),
162            "spawn_position must be finite"
163        );
164
165        let mut spawn =
166            self.spawn.unwrap_or_else(|| Spawn::default_for_new_space(self.bounds.bounds()));
167        spawn.set_eye_position(position);
168        self.spawn = Some(spawn);
169        self
170    }
171
172    /// Sets the initial contents of the space using a palette (numbered list of blocks)
173    /// and indices into that palette for every in-bounds cube.
174    ///
175    /// The input data must meet all of these requirements, or a [`PaletteError`] will be
176    /// returned:
177    ///
178    /// * `palette` must have no more than `BlockIndex::MAX + 1` elements.
179    /// * `contents` must have the same bounds as were set for this space.
180    /// * `contents` must contain no elements that are out of bounds of the `palette`.
181    /// * `light`, if specified, must have the same bounds as were set for this space.
182    ///
183    /// The `palette` is allowed to contain duplicate elements, but they will be combined.
184    /// In general, the produced [`Space`] will not necessarily have the same indices
185    /// as were provided.
186    ///
187    /// Calling this method will replace any previous specification of the contents,
188    /// such as [`filled_with()`](Self::filled_with()).
189    pub fn palette_and_contents<P>(
190        self,
191        palette: P,
192        contents: Vol<Box<[BlockIndex]>>,
193        light: Option<Vol<Box<[PackedLight]>>>,
194    ) -> Result<Self, PaletteError>
195    where
196        P: IntoIterator<IntoIter: ExactSizeIterator<Item = Block>>,
197    {
198        let ticket = self.read_ticket;
199        self.palette_and_contents_impl(ticket, &mut palette.into_iter(), contents, light)
200    }
201
202    fn palette_and_contents_impl(
203        mut self,
204        read_ticket: ReadTicket<'_>,
205        palette: &mut dyn ExactSizeIterator<Item = Block>,
206        mut contents: Vol<Box<[BlockIndex]>>,
207        light: Option<Vol<Box<[PackedLight]>>>,
208    ) -> Result<Self, PaletteError> {
209        // Validate palette.
210        let (mut palette, remapping) = Palette::from_blocks(read_ticket, palette)?;
211
212        // Validate bounds.
213        if contents.bounds() != self.bounds {
214            return Err(PaletteError::WrongDataBounds {
215                expected: self.bounds.bounds(),
216                actual: contents.bounds(),
217            });
218        }
219        if let Some(light) = light.as_ref()
220            && light.bounds() != self.bounds
221        {
222            return Err(PaletteError::WrongDataBounds {
223                expected: self.bounds.bounds(),
224                actual: light.bounds(),
225            });
226        }
227
228        // Validate data and update palette contents
229        let palette_len = palette.entries().len();
230        for (cube, contents_block_index) in contents.iter_mut() {
231            if let Some(&new_block_index) = remapping.get(contents_block_index) {
232                // Remap indices in the case where the palette contained duplicates
233                *contents_block_index = new_block_index;
234            } else if usize::from(*contents_block_index) >= palette_len {
235                // If the index was not remapped and is out of range then it's invalid.
236                return Err(PaletteError::Index {
237                    index: *contents_block_index,
238                    cube,
239                    palette_len,
240                });
241            }
242
243            palette.increment(*contents_block_index);
244        }
245
246        palette.free_all_zero_counts();
247
248        // Store data
249        self.contents = Fill::Data {
250            palette,
251            contents,
252            light,
253        };
254
255        Ok(self)
256    }
257
258    /// Construct a [`Space`] with the contents and settings from this builder.
259    ///
260    /// The builder must have had bounds specified, or it will not be possible to call this method.
261    ///
262    /// Panics if insufficient memory is available is available for the [`Space`]’s data arrays.
263    //---
264    // TODO: Consider offering only a `Result`-returning `build()` instead of this.
265    #[track_caller]
266    pub fn build(self) -> Space {
267        Space::new_from_builder(self).unwrap()
268    }
269
270    /// Construct a [`Space`] with the contents and settings from this builder.
271    ///
272    /// The builder must have had bounds specified, or it will not be possible to call this method.
273    ///
274    /// Returns an error if insufficient memory is available is available for the [`Space`]’s
275    /// data arrays.
276    pub fn try_build(self) -> Result<Space, Error> {
277        Space::new_from_builder(self)
278    }
279
280    /// Convenience combination of [`Builder::build()`] and [`Space::mutate()`].
281    ///
282    /// Use this when [`Builder::filled_with()`] is not enough control over the initial contents
283    /// of the space.
284    ///
285    /// # Errors
286    ///
287    /// * Returns [`Error::OutOfMemory`] if insufficient memory is available for the [`Space`]’s
288    ///   data arrays.
289    /// * Returns [`Error::Mutate`] if `f` returns an error.
290    ///   If an error type within the mutation function other than [`SetCubeError`] is needed, use
291    ///   [`Space::mutate()`] instead of this function.
292    pub fn build_and_mutate(
293        self,
294        f: impl FnOnce(&mut super::Mutation<'_, '_>) -> Result<(), SetCubeError>,
295    ) -> Result<Space, Error> {
296        let read_ticket = self.read_ticket;
297        let mut space = self.try_build()?;
298        space.mutate(read_ticket, f).map_err(Error::Mutate)?;
299        Ok(space)
300    }
301}
302
303impl Default for Builder<'_, ()> {
304    #[track_caller] // used for ReadTicket debugging
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310/// Helper for [`Builder::bounds_if_not_set()`]. Do not call or implement this trait.
311pub trait Bounds: sealed::Sealed + Sized {
312    /// Set the bounds unless they have already been set.
313    ///
314    /// This function is an implementation detail; call
315    /// [`Builder::bounds_if_not_set()`] instead.
316    #[doc(hidden)]
317    fn bounds_if_not_set<'u>(
318        builder: Builder<'u, Self>,
319        bounds_fn: impl FnOnce() -> GridAab,
320    ) -> Builder<'u, Vol<()>>;
321}
322
323impl Bounds for () {
324    fn bounds_if_not_set<'u>(
325        builder: Builder<'u, Self>,
326        bounds_fn: impl FnOnce() -> GridAab,
327    ) -> Builder<'u, Vol<()>> {
328        builder.bounds(bounds_fn())
329    }
330}
331
332impl Bounds for Vol<()> {
333    fn bounds_if_not_set<'u>(
334        builder: Builder<'u, Self>,
335        _bounds_fn: impl FnOnce() -> GridAab,
336    ) -> Builder<'u, Vol<()>> {
337        builder
338    }
339}
340
341/// Module for [`Bounds`] sealed trait
342mod sealed {
343    use super::*;
344    #[doc(hidden)]
345    #[expect(unnameable_types)]
346    pub trait Sealed {}
347    impl Sealed for () {}
348    impl Sealed for Vol<()> {}
349}
350
351#[cfg(feature = "arbitrary")]
352#[mutants::skip]
353impl<'a> arbitrary::Arbitrary<'a> for Space {
354    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
355        use crate::content::make_some_blocks;
356
357        // TODO: Should be reusing Vol as Arbitrary for this.
358
359        // Generate some blocks to put in the space
360        let mut blocks = alloc::vec::Vec::from(make_some_blocks::<2>()); // TODO: generate arbitrary blocks with attributes
361        #[expect(clippy::same_item_push)]
362        for _ in 0..6 {
363            // Make it probable that blocks are AIR
364            blocks.push(AIR);
365        }
366
367        let mut failure = None;
368
369        let bounds = Vol::<()>::arbitrary_with_max_volume(u, 2048)?;
370        let space = Space::builder(bounds.bounds()) // TODO: builder should accept Vol
371            .physics(u.arbitrary()?)
372            .spawn(u.arbitrary()?)
373            .build_and_mutate(|m| {
374                // Fill space with blocks
375                // TODO: use palette mechanism instead now that we have it
376                m.fill_all(|_| {
377                    match u.choose(&blocks) {
378                        Ok(block) => Some(block),
379                        Err(e) => {
380                            // We can't abort a space.fill() early unless we resort to catch_unwind.
381                            failure = Some(e);
382                            None
383                        }
384                    }
385                })
386            })
387            .unwrap();
388
389        if let Some(e) = failure {
390            return Err(e);
391        }
392
393        Ok(space)
394    }
395}
396
397// -------------------------------------------------------------------------------------------------
398
399/// Errors that may occur when constructing a [`Space`].
400#[derive(Clone, Debug, displaydoc::Display)]
401#[non_exhaustive]
402pub enum Error {
403    /// insufficient memory available to allocate Space
404    #[non_exhaustive]
405    OutOfMemory {},
406
407    /// A mutation performed during [`Builder::build_and_mutate()`] failed.
408    Mutate(SetCubeError),
409}
410
411impl core::error::Error for Error {
412    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
413        match self {
414            Error::OutOfMemory {} => None,
415            Error::Mutate(e) => Some(e),
416        }
417    }
418}
419
420// -------------------------------------------------------------------------------------------------
421
422#[cfg(test)]
423mod tests {
424    use crate::block;
425    use crate::content::make_some_blocks;
426    use crate::math::{Cube, Rgba};
427
428    use super::*;
429
430    #[test]
431    fn defaults() {
432        let bounds = GridAab::from_lower_size([1, 2, 3], [1, 1, 1]);
433        let space = Space::builder(bounds).build();
434        space.consistency_check();
435        assert_eq!(space.bounds(), bounds);
436        assert_eq!(space[bounds.lower_bounds()], AIR);
437        assert_eq!(space.physics(), &SpacePhysics::default());
438        assert_eq!(space.spawn(), &Spawn::default_for_new_space(bounds));
439    }
440
441    #[test]
442    fn filled_with() {
443        let bounds = GridAab::from_lower_size([1, 2, 3], [1, 1, 1]);
444        let block = block::from_color!(Rgba::WHITE);
445        let space = Space::builder(bounds).filled_with(block.clone()).build();
446        space.consistency_check();
447        assert_eq!(space[bounds.lower_bounds()], block);
448    }
449
450    #[test]
451    fn bounds_if_not_set_when_not_set() {
452        let bounds = GridAab::from_lower_size([1, 2, 3], [1, 1, 1]);
453        assert_eq!(
454            Builder::new().bounds_if_not_set(|| bounds).build().bounds(),
455            bounds
456        );
457    }
458
459    #[test]
460    fn bounds_if_not_set_when_already_set() {
461        let first_bounds = GridAab::from_lower_size([1, 2, 3], [1, 1, 1]);
462        let ignored_bounds = GridAab::from_lower_size([100, 2, 3], [1, 1, 1]);
463        assert_eq!(
464            Space::builder(first_bounds)
465                .bounds_if_not_set(|| ignored_bounds)
466                .build()
467                .bounds(),
468            first_bounds
469        );
470    }
471
472    #[test]
473    fn palette_err_too_long() {
474        let bounds = GridAab::ORIGIN_CUBE;
475        assert_eq!(
476            Space::builder(bounds)
477                .palette_and_contents(vec![AIR; 65537], Vol::from_element(2), None,)
478                .unwrap_err(),
479            PaletteError::PaletteTooLarge { len: 65537 }
480        );
481    }
482
483    #[test]
484    fn palette_err_too_short_for_contents() {
485        let bounds = GridAab::ORIGIN_CUBE;
486        assert_eq!(
487            Space::builder(bounds)
488                .palette_and_contents([AIR], Vol::from_element(2), None,)
489                .unwrap_err(),
490            PaletteError::Index {
491                index: 2,
492                cube: Cube::new(0, 0, 0),
493                palette_len: 1
494            }
495        );
496    }
497
498    #[test]
499    fn palette_err_contents_wrong_bounds() {
500        assert_eq!(
501            Space::builder(GridAab::single_cube(Cube::new(1, 0, 0)))
502                .palette_and_contents([AIR], Vol::from_element(0), None)
503                .unwrap_err(),
504            PaletteError::WrongDataBounds {
505                expected: GridAab::single_cube(Cube::new(1, 0, 0)),
506                actual: GridAab::ORIGIN_CUBE,
507            }
508        );
509    }
510
511    /// Duplicate blocks are permitted in the input palette even though `Space` doesn't
512    /// allow duplicates in its own palette. This is because deserialized/imported input
513    /// might have duplicates it did not intend, once the foreign or old blocks are
514    /// converted into specific [`Block`] instances.
515    #[test]
516    fn palette_with_duplicate_entries() {
517        let bounds = GridAab::from_lower_size([0, 0, 0], [3, 1, 1]);
518        let [block0, block1] = make_some_blocks();
519        let space = Space::builder(bounds)
520            .palette_and_contents(
521                [block0.clone(), block1.clone(), block0.clone()],
522                Vol::from_elements(bounds, [0, 1, 2]).unwrap(),
523                None,
524            )
525            .unwrap()
526            .build();
527
528        space.consistency_check();
529
530        // We do not require the new space to have exactly the same indices as the input,
531        // but the blocks should match.
532        assert_eq!(space[[0, 0, 0]], block0);
533        assert_eq!(space[[1, 0, 0]], block1);
534        assert_eq!(space[[2, 0, 0]], block0);
535    }
536
537    /// Unused entries in a palette should be converted to canonical tombstone entries.
538    #[test]
539    fn palette_with_unused_entries() {
540        let bounds = GridAab::from_lower_size([0, 0, 0], [2, 1, 1]);
541        let blocks = make_some_blocks::<3>();
542        let space = Space::builder(bounds)
543            .palette_and_contents(
544                blocks.clone(),
545                Vol::from_elements(bounds, [0, 2]).unwrap(),
546                None,
547            )
548            .unwrap()
549            .build();
550
551        space.consistency_check();
552
553        // blocks[1] was not used so it should not be in the palette.
554        let found = space.block_data().iter().find(|entry| entry.block == blocks[1]);
555        assert!(found.is_none(), "{found:?}");
556    }
557
558    // TODO: test and implement initial fill that has a tick_action that needs to be
559    // activated properly
560
561    // TODO: test all builder features
562}