all_is_cubes/content/
testing.rs

1use euclid::vec2;
2use num_traits::Euclid as _;
3use rand::{Rng as _, SeedableRng as _};
4use rand_xoshiro::Xoshiro256Plus;
5
6use crate::block::{self, AIR, Block};
7use crate::character::Spawn;
8use crate::content::{free_editing_starter_inventory, palette};
9use crate::linking::InGenError;
10use crate::math::{Face6, FaceMap, GridAab, GridCoordinate, GridSize, GridSizeCoord, Rgba};
11use crate::space::{LightPhysics, Space, SpacePhysics};
12use crate::universe::Universe;
13use crate::util::YieldProgress;
14
15/// Test space for the `all-is-cubes/benches/light` benchmark, designed to exercise a variety of
16/// geometric and color circumstances for the lighting algorithm.
17///
18/// (Since being created, it has found uses in many other tests as a fast-to-create yet
19/// nontrivial space).
20///
21/// TODO: Once we have the ability to write save files, give the benchmark code an option
22/// to do that instead, so this can just live in the benchmark instead of indirect.
23#[doc(hidden)]
24pub async fn lighting_bench_space(
25    universe: &mut Universe,
26    progress: YieldProgress,
27    requested_space_size: GridSize,
28) -> Result<Space, InGenError> {
29    let layout = LightingBenchLayout::new(requested_space_size)?;
30
31    let mut space = Space::builder(layout.space_bounds())
32        .light_physics(LightPhysics::None)
33        .read_ticket(universe.read_ticket())
34        .spawn({
35            let mut spawn = Spawn::looking_at_space(layout.space_bounds(), [0., 0.5, 1.]);
36            spawn.set_inventory(free_editing_starter_inventory(true));
37            spawn
38        })
39        .build_and_mutate(|m| {
40            // Ground level
41            m.fill_uniform(
42                m.bounds().shrink(FaceMap::default().with(Face6::PY, layout.yup())).unwrap(),
43                &block::from_color!(0.5, 0.5, 0.5),
44            )
45        })
46        .unwrap();
47
48    let progress = progress.finish_and_cut(0.25).await;
49
50    // Individual test sections (buildings/caves)
51    let section_iter = {
52        let i = layout.section_iter();
53        progress.split_evenly(i.len()).zip(i)
54    };
55    #[expect(
56        clippy::shadow_unrelated,
57        reason = "https://github.com/rust-lang/rust-clippy/issues/11827"
58    )]
59    for (progress, (sx, sz)) in section_iter {
60        {
61            // This block ensures its variables are dropped before the await.
62
63            // Independent RNG for each section, so that the number of values used doesn't
64            // affect the next section.
65            let mut rng = Xoshiro256Plus::seed_from_u64(
66                (sx + sz * i32::from(layout.array_side_lengths.x)) as u64,
67            );
68            let section_bounds = layout.section_bounds(sx, sz);
69            let color = Block::from(Rgba::new(
70                rng.random_range(0.0..=1.0),
71                rng.random_range(0.0..=1.0),
72                rng.random_range(0.0..=1.0),
73                if rng.random_bool(0.125) { 0.5 } else { 1.0 },
74            ));
75            space.mutate(universe.read_ticket(), |m| match rng.random_range(0..3) {
76                0 => {
77                    m.fill_uniform(section_bounds, &color).unwrap();
78                }
79                1 => {
80                    m.fill_uniform(
81                        section_bounds
82                            .shrink(FaceMap::default().with(Face6::PY, layout.yup()))
83                            .unwrap(),
84                        &color,
85                    )
86                    .unwrap();
87                    m.fill_uniform(
88                        section_bounds
89                            .shrink(FaceMap {
90                                nx: 1,
91                                ny: 0,
92                                nz: 1,
93                                px: 1,
94                                py: 0,
95                                pz: 1,
96                            })
97                            .unwrap(),
98                        &AIR,
99                    )
100                    .unwrap();
101                }
102                2 => {
103                    m.fill(section_bounds, |_| {
104                        if rng.random_bool(0.25) {
105                            Some(&color)
106                        } else {
107                            Some(&AIR)
108                        }
109                    })
110                    .unwrap();
111                }
112                _ => unreachable!("rng range"),
113            })
114        }
115        progress.finish().await;
116    }
117
118    space.set_physics(SpacePhysics {
119        light: LightPhysics::Rays {
120            maximum_distance: space.bounds().size().width.max(space.bounds().size().depth) as _,
121        },
122        sky: {
123            let sky_ground = palette::ALMOST_BLACK.to_rgb();
124            let sky_bright = palette::DAY_SKY_COLOR * 2.0;
125            let sky_dim = palette::DAY_SKY_COLOR * 0.5;
126            crate::space::Sky::Octants([
127                //       Y down ------------------ Y up
128                // Z forward - Z back
129                sky_ground, sky_ground, sky_bright, sky_bright, // X left
130                sky_ground, sky_ground, sky_dim, sky_dim, // X right
131            ])
132        },
133        ..SpacePhysics::default()
134    });
135    Ok(space)
136}
137
138/// Layout calculations for [`lighting_bench_space()`].
139///
140/// Expressing them as this bundle of functions avoids putting many local variables into
141/// the `async fn` future and increasing its overall size.
142///
143/// TODO: all of the functions are sloppily named because they used to be simple variables.
144struct LightingBenchLayout {
145    array_side_lengths: euclid::default::Vector2D<u8>,
146    height: u8,
147}
148
149impl LightingBenchLayout {
150    fn new(requested_space_size: GridSize) -> Result<LightingBenchLayout, InGenError> {
151        let layout = LightingBenchLayout {
152            array_side_lengths: vec2(
153                (requested_space_size.width - u32::from(Self::MARGIN))
154                    / u32::from(Self::SECTION_SPACING),
155                (requested_space_size.depth - u32::from(Self::MARGIN))
156                    / u32::from(Self::SECTION_SPACING),
157            )
158            .map(saturating_cast),
159            height: saturating_cast(requested_space_size.height),
160        };
161
162        if layout.section_height() < 2 {
163            return Err(InGenError::Other("height too small".into()));
164        }
165
166        Ok(layout)
167    }
168
169    // Constant sizes
170    const SECTION_WIDTH: u8 = 6;
171    const MARGIN: u8 = 4;
172    const SECTION_SPACING: u8 = Self::SECTION_WIDTH + Self::MARGIN;
173
174    fn space_bounds(&self) -> GridAab {
175        // These bounds should be a rounded version of requested_space_size.
176        GridAab::from_lower_upper(
177            [0, -self.ydown() - 1, 0],
178            [
179                i32::from(Self::SECTION_SPACING) * i32::from(self.array_side_lengths.x)
180                    + i32::from(Self::MARGIN),
181                1i32.saturating_add_unsigned(self.yup()),
182                i32::from(Self::SECTION_SPACING) * i32::from(self.array_side_lengths.y)
183                    + i32::from(Self::MARGIN),
184            ],
185        )
186    }
187
188    fn section_height(&self) -> u8 {
189        // Subtract 2 so that there can be air and non-section ground space at the top and bottom.
190        self.height.saturating_sub(2)
191    }
192
193    /// Height above y=0 (ground) that the sections extend.
194    fn yup(&self) -> GridSizeCoord {
195        GridSizeCoord::from(self.section_height()) * 4 / 14
196    }
197    /// Depth below y=0 (ground) that the sections extend.
198    fn ydown(&self) -> GridCoordinate {
199        GridCoordinate::from(self.section_height()).saturating_sub_unsigned(self.yup())
200    }
201
202    fn section_iter(
203        &self,
204    ) -> impl ExactSizeIterator<Item = (GridCoordinate, GridCoordinate)> + use<> {
205        let size = self.array_side_lengths;
206        let size_z = GridCoordinate::from(size.y);
207        let total = GridCoordinate::from(size.x) * size_z;
208        (0..total).map(move |i| i.div_rem_euclid(&size_z))
209    }
210
211    // Bounds in which one of the sections should be drawn.
212    fn section_bounds(&self, sx: GridCoordinate, sz: GridCoordinate) -> GridAab {
213        GridAab::from_lower_size(
214            [
215                i32::from(Self::MARGIN) + sx * i32::from(Self::SECTION_SPACING),
216                -self.ydown() + 1,
217                i32::from(Self::MARGIN) + sz * i32::from(Self::SECTION_SPACING),
218            ],
219            [
220                Self::SECTION_WIDTH.into(),
221                self.section_height().into(),
222                Self::SECTION_WIDTH.into(),
223            ],
224        )
225    }
226}
227
228fn saturating_cast(input: u32) -> u8 {
229    u8::try_from(input).unwrap_or(u8::MAX)
230}