1#![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
36macro_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#[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 Menu,
83
84 Blank,
86
87 Fail,
89
90 DemoCity,
92
93 Dungeon,
96
97 Islands,
99
100 Atrium,
104
105 CornellBox,
109
110 MengerSponge,
114
115 LightingBench,
117
118 #[cfg(feature = "arbitrary")]
122 Random,
123 }
125
126impl UniverseTemplate {
127 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 Menu => false,
138
139 Blank | Fail => false,
141
142 #[cfg(feature = "arbitrary")]
143 Random => false,
144 }
145 }
146
147 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 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 if let Some(space_result) = maybe_space {
214 let space_handle =
215 insert_generated_space(&mut universe, default_space_name, space_result)?;
216
217 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
237fn 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#[derive(Clone, Debug, Default, Eq, PartialEq)]
254#[expect(clippy::exhaustive_structs)]
255pub struct TemplateParameters {
256 pub seed: Option<u64>,
266
267 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 .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 <() as WhenceUniverse>::save(&(), universe, progress)
316 }
317}
318
319async 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 #[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)?; 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 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 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 let margin = 10;
372 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 let box_size: GridSizeCoord = 55;
394 let box_size_c: GridCoordinate = 55;
395
396 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 .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 space.fill_uniform(GridAab::from_lower_size([0, -1, 0], [box_size, 1, box_size]), &white)?;
427 space.fill_uniform(GridAab::from_lower_size([0, box_size_c, 0], [box_size, 1, box_size]), &white)?;
429 space.fill_uniform(GridAab::from_lower_upper([21, box_size_c, 23], [34, box_size_c + 1, 33]), &light)?;
431 space.fill_uniform(GridAab::from_lower_size([0, 0, -1], [box_size, box_size, 1]), &white)?;
433 space.fill_uniform(GridAab::from_lower_size([box_size_c, 0, 0], [1, box_size, box_size]), &green)?;
435 space.fill_uniform(GridAab::from_lower_size([-1, 0, 0], [1, box_size, box_size]), &red)?;
437
438 space.fill_uniform(GridAab::from_lower_size([29, 0, 36], [16, 16, 15]), &white)?;
440 space.fill_uniform(GridAab::from_lower_size([10, 0, 13], [18, 33, 15]), &white)?;
442
443 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 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 let mut p = space.physics().clone();
478 p.gravity = Vector3D::zero(); space.set_physics(p);
480
481 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; }
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 pub(super) async fn check_universe_template(template: UniverseTemplate) {
514 let params = if let UniverseTemplate::Islands = template {
515 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 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 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 fn check_block_spaces(universe: &Universe) {
575 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}