all_is_cubes_content/
template.rs

1//! First-run game content.
2
3#![allow(
4    clippy::module_name_repetitions,
5    reason = "false positive; TODO: remove after Rust 1.84 is released"
6)]
7
8use alloc::boxed::Box;
9use alloc::string::{String, ToString as _};
10use alloc::sync::Arc;
11use core::error::Error;
12
13use macro_rules_attribute::macro_rules_derive;
14use paste::paste;
15
16use all_is_cubes::block::Block;
17use all_is_cubes::character::{Character, Spawn};
18use all_is_cubes::euclid::{Point3D, Size3D};
19use all_is_cubes::linking::{BlockProvider, GenError, InGenError};
20use all_is_cubes::math::{
21    Face6, FaceMap, FreeCoordinate, GridAab, GridCoordinate, GridSize, GridSizeCoord, GridVector,
22    Rgb, Rgba,
23};
24use all_is_cubes::save::WhenceUniverse;
25use all_is_cubes::space::{LightPhysics, Space};
26use all_is_cubes::transaction::Transaction as _;
27use all_is_cubes::universe::{Handle, Name, Universe, UniverseTransaction};
28use all_is_cubes::util::YieldProgress;
29use all_is_cubes::{time, transaction};
30
31use crate::fractal::menger_sponge;
32use crate::{atrium::atrium, demo_city, dungeon::demo_dungeon, install_demo_blocks};
33use crate::{free_editing_starter_inventory, palette};
34use crate::{wavy_landscape, LandscapeBlocks};
35
36/// Generate a `#[test]` function for each element of [`UniverseTemplate`].
37/// This macro is used as a derive macro via [`macro_rules_derive`].
38macro_rules! generate_template_test {
39    (
40        $(#[$type_meta:meta])*
41        $vis:vis enum $enum_name:ident {
42            $(
43                $( #[doc = $doc:literal] )*
44                $( #[cfg($variant_cfg:meta)] )*
45                $variant_name:ident
46            ),* $(,)?
47        }
48    ) => {
49        $(
50            paste! {
51                $( #[cfg($variant_cfg)] )*
52                #[cfg(test)]
53                #[tokio::test]
54                #[allow(non_snake_case)]
55                async fn [< template_ $variant_name >] () {
56                    tests::check_universe_template($enum_name::$variant_name).await;
57                }
58            }
59        )*
60    }
61}
62
63/// Selection of initial content for constructing a new [`Universe`].
64//
65// TODO: Stop using strum, because we will eventually want parameterized templates.
66#[derive(
67    Clone,
68    Debug,
69    Eq,
70    Hash,
71    PartialEq,
72    strum::Display,
73    strum::EnumString,
74    strum::EnumIter,
75    strum::IntoStaticStr,
76)]
77#[strum(serialize_all = "kebab-case")]
78#[non_exhaustive]
79#[macro_rules_derive(generate_template_test!)]
80pub enum UniverseTemplate {
81    /// Provides an interactive menu of other templates.
82    Menu,
83
84    /// New universe with no contents at all.
85    Blank,
86
87    /// Always produces an error, for testing error-handling functionality.
88    Fail,
89
90    /// Space with assorted “exhibits” demonstrating or testing various features of All is Cubes.
91    DemoCity,
92
93    /// Randomly generated connected rooms.
94    /// Someday this might have challenges or become a tutorial.
95    Dungeon,
96
97    /// Large space with separate floating islands.
98    Islands,
99
100    /// A procedural voxel version of the classic [Sponza] Atrium rendering test scene.
101    ///
102    /// [Sponza]: https://en.wikipedia.org/wiki/Sponza_Palace
103    Atrium,
104
105    /// A procedural voxel version of the classic [Cornell Box] rendering test scene.
106    ///
107    /// [Cornell Box]: https://en.wikipedia.org/wiki/Cornell_box
108    CornellBox,
109
110    /// A [Menger sponge] fractal.
111    ///
112    /// [Menger sponge]: https://en.wikipedia.org/wiki/Menger_sponge
113    MengerSponge,
114
115    /// A test scene containing various shapes and colors to exercise the lighting algorithm.
116    LightingBench,
117
118    /// Use entirely random choices.
119    ///
120    /// TODO: This doesn't yet produce anything even visible — we need more sanity constraints.
121    #[cfg(feature = "arbitrary")]
122    Random,
123    // TODO: add an "nothing, you get a blank editor" option once we have enough editing support.
124}
125
126impl UniverseTemplate {
127    /// Whether the template should be shown to users.
128    /// (This does not control )
129    pub fn include_in_lists(&self) -> bool {
130        use UniverseTemplate::*;
131        match self {
132            DemoCity | Dungeon | Atrium | Islands | CornellBox | MengerSponge | LightingBench => {
133                true
134            }
135
136            // Itself a list of templates!
137            Menu => false,
138
139            // More testing than interesting demos.
140            Blank | Fail => false,
141
142            #[cfg(feature = "arbitrary")]
143            Random => false,
144        }
145    }
146
147    /// Create a new [`Universe`] based on this template's specifications.
148    pub async fn build<I: time::Instant>(
149        self,
150        p: YieldProgress,
151        params: TemplateParameters,
152    ) -> Result<Universe, GenError> {
153        let mut universe = Universe::new();
154
155        // TODO: Later we want a "module loading" system that can lazily bring in content.
156        // For now, unconditionally add all these blocks.
157        let [demo_blocks_progress, p] = p.split(0.1);
158        {
159            let mut install_txn = UniverseTransaction::default();
160            install_demo_blocks(&mut install_txn, demo_blocks_progress).await?;
161            install_txn.execute(&mut universe, &mut transaction::no_outputs)?;
162        }
163        p.progress(0.).await;
164
165        let default_space_name: Name = "space".into();
166
167        let maybe_space = {
168            let params = params.clone();
169            let mut p = Some(p);
170            use UniverseTemplate::*;
171            let maybe_space: Option<Result<Space, InGenError>> = match self {
172                Menu => Some(
173                    crate::menu::template_menu_space(
174                        &mut universe,
175                        p.take().unwrap(),
176                        Arc::new(|_| {}),
177                    )
178                    .await,
179                ),
180                Blank => None,
181                Fail => Some(Err(InGenError::Other(
182                    "the Fail template always fails to generate".into(),
183                ))),
184                DemoCity => Some(demo_city::<I>(&mut universe, p.take().unwrap(), params).await),
185                Dungeon => Some(demo_dungeon(&mut universe, p.take().unwrap(), params).await),
186                Islands => Some(islands(&mut universe, p.take().unwrap(), params).await),
187                Atrium => Some(atrium(&mut universe, p.take().unwrap()).await),
188                CornellBox => Some(cornell_box()),
189                MengerSponge => Some(menger_sponge(&mut universe, 4)),
190                LightingBench => Some(
191                    all_is_cubes::content::testing::lighting_bench_space(
192                        &mut universe,
193                        p.take().unwrap(),
194                        params.size.unwrap_or(GridSize::new(54, 16, 54)),
195                    )
196                    .await,
197                ),
198                #[cfg(feature = "arbitrary")]
199                Random => Some(
200                    arbitrary_space(&mut universe, p.take().unwrap(), params.seed.unwrap_or(0))
201                        .await,
202                ),
203            };
204
205            if let Some(p) = p {
206                p.finish().await;
207            }
208
209            maybe_space
210        };
211
212        // Insert the space and generate the initial character.
213        if let Some(space_result) = maybe_space {
214            let space_handle =
215                insert_generated_space(&mut universe, default_space_name, space_result)?;
216
217            // TODO: "character" is a special default name used for finding the character the
218            // player actually uses, and we should replace that or handle it more formally.
219            universe.insert("character".into(), Character::spawn_default(space_handle))?;
220        }
221
222        universe.whence = Arc::new(TemplateAndParameters {
223            template: self.clone(),
224            parameters: params,
225        });
226
227        Ok(universe)
228    }
229}
230
231impl Default for UniverseTemplate {
232    fn default() -> Self {
233        Self::DemoCity
234    }
235}
236
237/// TODO: This should be a general Universe tool for "insert a generated value or report an error"
238/// but for now was written to help out `UniverseTemplate::build`
239fn insert_generated_space(
240    universe: &mut Universe,
241    name: Name,
242    result: Result<Space, InGenError>,
243) -> Result<Handle<Space>, GenError> {
244    match result {
245        Ok(space) => Ok(universe.insert(name, space)?),
246        Err(e) => Err(GenError::failure(e, name)),
247    }
248}
249
250/// Configuration for exactly what a [`UniverseTemplate`] should produce.
251///
252/// Pass this structure to [`UniverseTemplate::build()`].
253#[derive(Clone, Debug, Default, Eq, PartialEq)]
254#[expect(clippy::exhaustive_structs)]
255pub struct TemplateParameters {
256    /// Seed for any randomization which the template performs.
257    /// Not all templates have random elements.
258    ///
259    /// The seed is optional so that user input processing can distinguish whether the
260    /// seed was explicitly specified. If a template receives a seed of `None`, (TODO
261    /// define what should happen. Fail? Treat equal to `Some(0)`?)
262    //---
263    // Design note: u64 was chosen because both `std::hash::Hasher` and `rand::SeedableRng`
264    // agree on this many bits for seeds.
265    pub seed: Option<u64>,
266
267    /// Dimensions of the primary space of the universe, if there is such a thing.
268    ///
269    /// If the space cannot be constructed in approximately this size, building the
270    /// template should return an error.
271    pub size: Option<GridSize>,
272}
273
274#[derive(Clone, Debug, Default, Eq, PartialEq)]
275struct TemplateAndParameters {
276    template: UniverseTemplate,
277    parameters: TemplateParameters,
278}
279
280impl WhenceUniverse for TemplateAndParameters {
281    fn document_name(&self) -> Option<String> {
282        Some(self.template.to_string())
283    }
284
285    fn can_load(&self) -> bool {
286        false
287    }
288
289    fn load(
290        &self,
291        progress: YieldProgress,
292    ) -> futures_core::future::BoxFuture<'static, Result<Universe, Box<dyn Error + Send + Sync>>>
293    {
294        let ingredients = self.clone();
295        Box::pin(async move {
296            ingredients
297                .template
298                // TODO: don't use placeholder time
299                .build::<time::NoTime>(progress, ingredients.parameters)
300                .await
301                .map_err(From::from)
302        })
303    }
304
305    fn can_save(&self) -> bool {
306        false
307    }
308
309    fn save(
310        &self,
311        universe: &Universe,
312        progress: YieldProgress,
313    ) -> futures_core::future::BoxFuture<'static, Result<(), Box<dyn Error + Send + Sync>>> {
314        // Delegate to the same error as () would produce. TODO: Have an error enum instead
315        <() as WhenceUniverse>::save(&(), universe, progress)
316    }
317}
318
319// -- Specific templates below this point ---
320
321async fn islands(
322    universe: &mut Universe,
323    p: YieldProgress,
324    params: TemplateParameters,
325) -> Result<Space, InGenError> {
326    let landscape_blocks = BlockProvider::<LandscapeBlocks>::using(universe)?;
327
328    let TemplateParameters { size, seed: _ } = params;
329    let size = size.unwrap_or(GridSize::new(1000, 400, 1000));
330
331    // Set up dimensions
332    #[expect(clippy::cast_possible_wrap, reason = "big numbers will break anyway")]
333    let bounds = GridAab::checked_from_lower_size(
334        [
335            -((size.width / 2) as i32),
336            -((size.height / 2) as i32),
337            size.depth as i32,
338        ],
339        size,
340    )
341    .map_err(InGenError::other)?; // TODO: add automatic error conversion?
342
343    let mut space = Space::builder(bounds)
344        .sky_color(palette::DAY_SKY_COLOR)
345        .spawn({
346            let mut spawn = Spawn::default_for_new_space(bounds);
347            spawn.set_inventory(free_editing_starter_inventory(true));
348            spawn.set_eye_position(bounds.center());
349            // TODO: Make this tidier by having a "shrink to centermost point or cube" operation on GridAab
350            let cp = bounds.center().map(|c| c as GridCoordinate);
351            spawn.set_bounds(GridAab::from_lower_size(
352                cp - GridVector::new(30, 30, 30),
353                [60, 60, 60],
354            ));
355            spawn
356        })
357        .build();
358
359    // Set up grid in which islands are placed
360    let island_stride = 50;
361    let island_grid = bounds.divide(island_stride);
362
363    for (i, island_pos) in island_grid.interior_iter().enumerate() {
364        let cell_bounds = GridAab::from_lower_size(
365            (island_pos.lower_bounds().to_vector() * island_stride).to_point(),
366            Size3D::splat(island_stride).to_u32(),
367        )
368        .intersection_cubes(bounds)
369        .expect("island outside space bounds");
370        // TODO: randomize island location in cell?
371        let margin = 10;
372        // TODO: non-panicking expand() will be a better solution than this conditional here
373        if cell_bounds.size().width >= margin * 2
374            && cell_bounds.size().height >= margin + 25
375            && cell_bounds.size().depth >= margin * 2
376        {
377            let occupied_bounds = cell_bounds
378                .shrink(FaceMap::splat(10).with(Face6::PY, 25))
379                .unwrap();
380            wavy_landscape(occupied_bounds, &mut space, &landscape_blocks, 0.5)?;
381        }
382        p.progress(i as f32 / island_grid.volume_f64() as f32).await;
383    }
384
385    Ok(space)
386}
387
388#[rustfmt::skip]
389fn cornell_box() -> Result<Space, InGenError> {
390    // Coordinates are set up based on this dimension because, being blocks, we're not
391    // going to *exactly* replicate the original data, but we might want to adjust the
392    // scale to something else entirely.
393    let box_size: GridSizeCoord = 55;
394    let box_size_c: GridCoordinate = 55;
395
396    // Add one block to all sides for wall thickness.
397    let bounds = GridAab::from_lower_size(
398        [-1, -1, -1],
399        GridSize::splat(box_size + 2),
400    );
401    let mut space = Space::builder(bounds)
402        // There shall be no light but that which we make for ourselves!
403        .sky_color(Rgb::ZERO)
404        .light_physics(LightPhysics::Rays {
405            maximum_distance: (box_size * 2).try_into().unwrap_or(u8::MAX),
406        })
407        .spawn({
408            let mut spawn = Spawn::default_for_new_space(bounds);
409            spawn.set_inventory(free_editing_starter_inventory(true));
410            spawn.set_eye_position(Point3D::<FreeCoordinate, _>::new(0.5, 0.5, 1.6)
411                * FreeCoordinate::from(box_size));
412            spawn
413        })
414        .build();
415
416    let white: Block = Rgba::new(1.0, 1.0, 1.0, 1.0).into();
417    let red: Block = Rgba::new(0.57, 0.025, 0.025, 1.0).into();
418    let green: Block = Rgba::new(0.025, 0.236, 0.025, 1.0).into();
419    let light: Block = Block::builder()
420        .display_name("Light")
421        .color(Rgba::new(1.0, 1.0, 1.0, 1.0))
422        .light_emission(Rgb::ONE * 8.0)
423        .build();
424
425    // Floor.
426    space.fill_uniform(GridAab::from_lower_size([0, -1, 0], [box_size, 1, box_size]), &white)?;
427    // Ceiling.
428    space.fill_uniform(GridAab::from_lower_size([0, box_size_c, 0], [box_size, 1, box_size]), &white)?;
429    // Light in ceiling.
430    space.fill_uniform(GridAab::from_lower_upper([21, box_size_c, 23], [34, box_size_c + 1, 33]), &light)?;
431    // Back wall.
432    space.fill_uniform(GridAab::from_lower_size([0, 0, -1], [box_size, box_size, 1]), &white)?;
433    // Right wall (green).
434    space.fill_uniform(GridAab::from_lower_size([box_size_c, 0, 0], [1, box_size, box_size]), &green)?;
435    // Left wall (red).
436    space.fill_uniform(GridAab::from_lower_size([-1, 0, 0], [1, box_size, box_size]), &red)?;
437
438    // Block #1
439    space.fill_uniform(GridAab::from_lower_size([29, 0, 36], [16, 16, 15]), &white)?;
440    // Block #2
441    space.fill_uniform(GridAab::from_lower_size([10, 0, 13], [18, 33, 15]), &white)?;
442
443    // This won't figure out the correct light values, but it will reset everything to
444    // uninitialized, which will help the updater get going faster.
445    space.fast_evaluate_light();
446
447    Ok(space)
448}
449
450#[cfg(feature = "arbitrary")]
451async fn arbitrary_space(
452    _: &mut Universe,
453    mut progress: YieldProgress,
454    seed: u64,
455) -> Result<Space, InGenError> {
456    use all_is_cubes::euclid::Vector3D;
457    use arbitrary::{Arbitrary, Error, Unstructured};
458    use rand::{RngCore, SeedableRng};
459
460    let mut rng = rand_xoshiro::Xoshiro256Plus::seed_from_u64(seed);
461    let mut bytes = vec![0u8; 16384];
462    let mut attempt = 0;
463    loop {
464        attempt += 1;
465        rng.fill_bytes(&mut bytes);
466        let r: Result<Space, _> = Arbitrary::arbitrary(&mut Unstructured::new(&bytes));
467        match r {
468            Ok(mut space) => {
469                // Patch spawn position to be reasonable
470                let bounds = space.bounds();
471                let mut spawn = space.spawn().clone();
472                spawn.set_bounds(bounds.expand(FaceMap::splat(20)));
473                spawn.set_eye_position(bounds.center());
474                space.set_spawn(spawn);
475
476                // Patch physics to be reasonable
477                let mut p = space.physics().clone();
478                p.gravity = Vector3D::zero(); // won't be a floor
479                space.set_physics(p);
480
481                // TODO: These patches are still not enough to get a good result.
482
483                return Ok(space);
484            }
485            Err(Error::IncorrectFormat) => {
486                if attempt >= 1000000 {
487                    return Err(InGenError::Other("Out of attempts".into()));
488                }
489            }
490            Err(e) => panic!("{}", e),
491        }
492        progress = progress.finish_and_cut(0.1).await; // mostly nonsense but we do want to yield
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use all_is_cubes::block;
500    use all_is_cubes::util::yield_progress_for_testing;
501    use futures_core::future::BoxFuture;
502
503    #[expect(clippy::let_underscore_future)]
504    fn _test_build_future_is_send() {
505        let _: BoxFuture<'_, _> = Box::pin(UniverseTemplate::Atrium.build::<std::time::Instant>(
506            yield_progress_for_testing(),
507            TemplateParameters::default(),
508        ));
509    }
510
511    /// Test one template.
512    /// This function is called by the [`generate_template_test!`] macro.
513    pub(super) async fn check_universe_template(template: UniverseTemplate) {
514        let params = if let UniverseTemplate::Islands = template {
515            // Kludge: the islands template is known to be very slow.
516            // We should work on making what it does faster, but for now, let's
517            // run a much smaller instance of it for the does-it-succeed test.
518            TemplateParameters {
519                seed: Some(0x7f16dfe65954583e),
520                size: Some(GridSize::new(100, 50, 100)),
521            }
522        } else {
523            TemplateParameters {
524                seed: Some(0x7f16dfe65954583e),
525                size: None,
526            }
527        };
528
529        let result = template
530            .clone()
531            .build::<std::time::Instant>(yield_progress_for_testing(), params)
532            .await;
533
534        if matches!(template, UniverseTemplate::Fail) {
535            // The Fail template _should_ return an error
536            result.unwrap_err();
537        } else {
538            let mut u = result.unwrap();
539
540            if template != UniverseTemplate::Blank {
541                let _ = u.get_default_character().unwrap().read().unwrap();
542            }
543            u.step(false, time::DeadlineNt::Asap);
544
545            check_block_spaces(&u);
546        }
547
548        // Test that asking for an impossibly huge size does not panic, but returns an error or
549        // ignores the size.
550        //
551        // (This shouldn't ever actually hog memory because it'd be too much to succeed
552        // in allocating, regardless of platform, unless the template clamps all but exactly one
553        // dimension.)
554        // TODO: This test doesn't pass but it should.
555        if false {
556            println!(
557                "too-big result: {:?}",
558                template
559                    .clone()
560                    .build::<std::time::Instant>(
561                        yield_progress_for_testing(),
562                        TemplateParameters {
563                            seed: Some(0),
564                            size: Some(GridSize::splat(GridSizeCoord::MAX)),
565                        }
566                    )
567                    .await
568            );
569        }
570    }
571
572    /// Assert that every `Space` used in a `Block` has appropriate properties;
573    /// in particular, that it is not unnecessarily having lighting computations.
574    fn check_block_spaces(universe: &Universe) {
575        // TODO: also check blocks that are found in `Composite` and directly in `Space`, etc.
576        // Use case for `VisitHandles` being more general?
577        for (block_def_name, block_def_handle) in universe.iter_by_type::<block::BlockDef>() {
578            let block_def = &*block_def_handle.read().unwrap();
579            if let block::Primitive::Recur {
580                space: space_handle,
581                ..
582            } = block_def.block().primitive()
583            {
584                assert_eq!(
585                    space_handle.read().unwrap().physics().light,
586                    LightPhysics::None,
587                    "block {block_def_name} has space {space_name} \
588                        whose light physics are not disabled",
589                    space_name = space_handle.name()
590                );
591            }
592        }
593    }
594}