all_is_cubes/space.rs
1//! [`Space`] and related types; the physical space of All is Cubes.
2
3use alloc::borrow::Cow;
4use alloc::boxed::Box;
5use alloc::vec::Vec;
6use core::fmt;
7use core::ops;
8use core::time::Duration;
9
10use bevy_ecs::prelude as ecs;
11use hashbrown::HashSet as HbHashSet;
12use manyfmt::Fmt;
13
14use crate::behavior::BehaviorSet;
15use crate::behavior::BehaviorSetStepInfo;
16use crate::block::{AIR, AIR_EVALUATED_REF, Block, EvaluatedBlock, Resolution};
17use crate::character::Spawn;
18use crate::drawing::DrawingPlane;
19use crate::fluff::Fluff;
20use crate::listen::{self, Listen, Notifier};
21use crate::math::{Cube, GridAab, Gridgid, Vol};
22use crate::time::TimeStats;
23use crate::universe::{self, HandleVisitor, ReadTicket, SealedMember as _, VisitHandles};
24use crate::util::{ConciseDebug, Refmt as _, StatusText};
25
26#[cfg(doc)]
27use crate::{
28 block::BlockDef,
29 character::Character,
30 universe::{Handle, Universe},
31};
32
33// -------------------------------------------------------------------------------------------------
34
35mod behaviors;
36#[expect(clippy::module_name_repetitions)] // TODO: consider renaming
37pub use behaviors::{ActivatableRegion, SpaceBehaviorAttachment};
38
39pub mod builder;
40pub use builder::Builder;
41
42#[cfg(not(feature = "_special_testing"))]
43mod light;
44#[cfg(feature = "_special_testing")]
45#[doc(hidden)]
46pub mod light;
47
48#[doc(hidden)] // pub only for debug visualization by all-is-cubes-gpu
49pub use light::LightUpdateCubeInfo;
50pub(crate) use light::{LightStatus, LightUpdateRequest};
51use light::{LightStorage, LightUpdateQueue, PackedLightScalar};
52pub use light::{LightUpdatesInfo, PackedLight};
53
54mod palette;
55pub use palette::PaletteError;
56#[expect(clippy::module_name_repetitions)] // TODO: consider renaming
57pub use palette::SpaceBlockData;
58use palette::{Palette, PendingEvaluation};
59
60mod physics;
61pub use physics::*;
62
63mod sky;
64pub use sky::*;
65
66pub(crate) mod step;
67
68mod space_txn;
69pub use space_txn::*;
70
71#[cfg(test)]
72mod tests;
73
74// -------------------------------------------------------------------------------------------------
75
76/// Number used to identify distinct blocks within a [`Space`].
77pub type BlockIndex = u16;
78
79/// A physical space consisting mostly of [`Block`]s arranged in a grid.
80/// The main “game world” data structure.
81///
82/// A `Space` consists of:
83///
84/// * A bounding box outside of which there is only emptiness.
85/// Set using [`Builder::bounds()`] and read using [`Space::bounds()`].
86/// * [`Block`]s for each [cube][Cube] within the bounds.
87/// For efficiency, identical blocks are deduplicated.
88/// Set using [`Space::mutate()`] or [`SpaceTransaction`]; and
89/// read using [the indexing operator](#impl-Index%3CT%3E-for-Space), [`Space::get_evaluated()`], [`Space::get_block_index()`],
90/// and [`Space::extract()`].
91/// * Information about light passing through the space and falling on each block.
92/// Light can be emitted by blocks and by the surrounding “sky”.
93/// Updated automatically and read using [`Space::get_lighting()`] or [`Space::extract()`].
94/// * [`SpacePhysics`] defining global properties of the space.
95/// Set using [`Space::set_physics()`] and read using [`Space::physics()`].
96/// * A default [`Spawn`] location for characters entering the space.
97/// Set using [`Space::set_spawn()`] and read using [`Space::spawn()`].
98/// * A [`BehaviorSet`].
99/// Set using [`SpaceTransaction::behaviors()`].
100///
101#[doc = include_str!("save/serde-warning.md")]
102pub struct Space {
103 palette: Palette,
104
105 /// The blocks in the space, stored as indices into [`Self::palette`].
106 ///
107 /// This field also stores the bounds of the space.
108 //---
109 // TODO: Consider making this use different integer types depending on how
110 // many blocks there are, so we can save memory in simple spaces but not have
111 // a cap on complex ones.
112 contents: Vol<Box<[BlockIndex]>>,
113
114 /// The light reflected from or emitted by each cube,
115 /// and the information for continuously updating it.
116 light: LightStorage,
117
118 /// Global characteristics such as the behavior of light and gravity.
119 physics: SpacePhysics,
120
121 // TODO: Replace this with something that has a spatial index so we can
122 // search for behaviors in specific regions
123 behaviors: BehaviorSet<Space>,
124
125 ticks: Ticks,
126
127 spawn: Spawn,
128
129 notifiers: Notifiers,
130}
131
132/// Component of [`Space`] storing which block is in each cube within the bounds.
133///
134/// Also serves as the component that requires other components not included in the bundle.
135#[derive(ecs::Component)]
136#[require(Notifiers, Ticks)]
137pub(crate) struct Contents(Vol<Box<[BlockIndex]>>);
138
139/// Component of [`Space`] storing which block is in each cube within the bounds.
140#[derive(Debug, Default, ecs::Component)]
141pub(crate) struct Notifiers {
142 /// Notifier of changes to Space data.
143 change_notifier: Notifier<SpaceChange>,
144
145 /// Notifier which delivers [`Fluff`] (events that happen in the space but are not changes).
146 fluff_notifier: Notifier<SpaceFluff>,
147}
148
149/// Component of [`Space`] storing where characters spawn by default.
150#[derive(Debug, ecs::Component)]
151pub(crate) struct DefaultSpawn(Spawn);
152
153#[derive(Debug, Default, ecs::Component)]
154pub(crate) struct Ticks {
155 /// Cubes that should be checked for `tick_action`s on the next step.
156 ///
157 /// TODO: Need to track which *phase* the action applies to.
158 cubes_wanting_ticks: HbHashSet<Cube>,
159}
160
161/// Read access to a [`Space`] that may be currently in a [`Universe`].
162///
163/// Obtain this using [`Handle::read()`] or [`Space::read()`].
164#[derive(Clone, Copy)]
165pub struct Read<'ticket> {
166 palette: &'ticket Palette,
167 contents: Vol<&'ticket [BlockIndex]>,
168 light: &'ticket LightStorage,
169 physics: &'ticket SpacePhysics,
170 behaviors: &'ticket BehaviorSet<Space>,
171 default_spawn: &'ticket Spawn,
172 notifiers: &'ticket Notifiers,
173}
174
175// -------------------------------------------------------------------------------------------------
176
177impl fmt::Debug for Space {
178 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
179 let Self {
180 palette,
181 contents,
182 light: _,
183 physics,
184 behaviors,
185 ticks: _,
186 spawn,
187 notifiers: _,
188 } = self;
189 // Make the assumption that a Space is too big to print in its entirety.
190 // (What's left out is kind of arbitrary, though.)
191 fmt.debug_struct("Space")
192 .field("bounds", &contents.bounds())
193 .field("palette", palette)
194 .field("physics", physics)
195 .field("behaviors", behaviors)
196 .field("default_spawn", spawn)
197 .finish_non_exhaustive()
198 }
199}
200
201impl fmt::Debug for Read<'_> {
202 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
203 let Self {
204 palette,
205 contents,
206 light: _,
207 physics,
208 behaviors,
209 default_spawn,
210 notifiers: _,
211 } = self;
212 fmt.debug_struct("space::Read")
213 .field("bounds", &contents.bounds())
214 .field("palette", palette)
215 .field("physics", physics)
216 .field("behaviors", behaviors)
217 .field("default_spawn", default_spawn)
218 .finish_non_exhaustive()
219 }
220}
221
222impl Space {
223 /// Returns a space [`Builder`] configured for a [recursive] block,
224 /// which may be used to construct a new [`Space`].
225 ///
226 /// This means that its bounds are as per [`GridAab::for_block()`], and its
227 /// [`physics`](Self::physics) is [`SpacePhysics::DEFAULT_FOR_BLOCK`].
228 ///
229 /// [recursive]: crate::block::Primitive::Recur
230 pub fn for_block(resolution: Resolution) -> Builder<'static, Vol<()>> {
231 Builder::new()
232 .bounds(GridAab::for_block(resolution))
233 .physics(SpacePhysics::DEFAULT_FOR_BLOCK)
234 }
235
236 /// Returns a [`space::Builder`](Builder) with the given bounds and all default values,
237 /// which may be used to construct a new [`Space`].
238 ///
239 /// Panics if `bounds` has a volume exceeding `usize::MAX`.
240 /// (But there will likely be a memory allocation failure well below that point.)
241 #[track_caller] // used for ReadTicket debugging
242 pub fn builder(bounds: GridAab) -> Builder<'static, Vol<()>> {
243 Builder::new().bounds(bounds)
244 }
245
246 /// Constructs a [`Space`] that is entirely filled with [`AIR`].
247 ///
248 /// Equivalent to `Space::builder(bounds).build()`
249 pub fn empty(bounds: GridAab) -> Space {
250 Space::builder(bounds).build()
251 }
252
253 /// Implementation of [`Builder`]'s terminal methods.
254 #[inline(never)] // complex, expensive, infrequent
255 fn new_from_builder(builder: Builder<'_, Vol<()>>) -> Result<Self, builder::Error> {
256 let Builder {
257 read_ticket,
258 bounds,
259 spawn,
260 physics,
261 behaviors,
262 contents,
263 } = builder;
264
265 let (palette, contents, light) = match contents {
266 builder::Fill::Block(block) => {
267 let volume = bounds.volume();
268 let palette = Palette::new(read_ticket, block, volume);
269 let opacity = palette.all_block_opacities_as_category();
270
271 // Vec::try_reserve is roundabout, but currently the only stable way to get fallible
272 // memory allocation for a slice.
273 let mut contents_buffer = Vec::new();
274 contents_buffer
275 .try_reserve_exact(volume)
276 .map_err(|_| builder::Error::OutOfMemory {})?;
277 contents_buffer.resize(volume, 0);
278
279 (
280 palette,
281 Box::from(contents_buffer),
282 LightStorage::new(
283 &physics,
284 physics.light.initialize_lighting(bounds, opacity)?,
285 LightUpdateQueue::new(), // TODO: nonempty if opacity is partial
286 ),
287 )
288 }
289 builder::Fill::Data {
290 palette,
291 contents,
292 light,
293 } => {
294 let light_st = match light {
295 Some(light) if physics.light != LightPhysics::None => {
296 // Fill the light update queue with each block whose light is known invalid.
297 // TODO: Also register a low-priority "update everything" in case data is
298 // from an old version.
299 let mut queue = LightUpdateQueue::new();
300 for (cube, cube_light) in light.iter() {
301 match cube_light.status() {
302 LightStatus::Uninitialized => queue.insert(LightUpdateRequest {
303 priority: light::Priority::UNINIT,
304 cube,
305 }),
306 LightStatus::NoRays
307 | LightStatus::Opaque
308 | LightStatus::Visible => {}
309 }
310 }
311 LightStorage::new(&physics, light, queue)
312 }
313 _ => LightStorage::new(
314 &physics,
315 physics.light.initialize_lighting(
316 bounds,
317 palette.all_block_opacities_as_category(),
318 )?,
319 LightUpdateQueue::new(), // TODO: nonempty if needed
320 ),
321 };
322
323 (palette, contents.into_elements(), light_st)
324 }
325 };
326
327 Ok(Space {
328 palette,
329 contents: bounds.with_elements(contents).unwrap(),
330 light,
331
332 physics,
333 behaviors,
334 spawn: spawn.unwrap_or_else(|| Spawn::default_for_new_space(bounds.bounds())),
335 ticks: Ticks::default(),
336 notifiers: Notifiers::default(),
337 })
338 }
339
340 /// Constructs a `Space` that is entirely empty and whose coordinate system
341 /// is in the +X+Y+Z octant. This is a shorthand intended mainly for tests.
342 ///
343 /// Panics if the volume is greater than [`usize::MAX`], if any dimension is greater than
344 /// [`i32::MAX`].
345 #[track_caller]
346 pub fn empty_positive<S>(wx: S, wy: S, wz: S) -> Space
347 where
348 S: Copy + num_traits::NumCast,
349 {
350 Space::empty(GridAab::from_lower_size(
351 [0, 0, 0],
352 euclid::Size3D::new(wx, wy, wz).cast(),
353 ))
354 }
355
356 /// Returns the [`GridAab`] describing the bounds of this space; no blocks may exist
357 /// outside it.
358 pub fn bounds(&self) -> GridAab {
359 self.contents.bounds()
360 }
361
362 /// Returns the internal unstable numeric ID for the block at the given position,
363 /// which may be mapped to a [`Block`] by [`Space::block_data`].
364 /// If you are looking for *simple* access, use `space[position]` (the
365 /// [`core::ops::Index`] trait) instead.
366 ///
367 /// These IDs may be used to perform efficient processing of many blocks, but they
368 /// may be renumbered after any mutation.
369 #[inline(always)]
370 pub fn get_block_index(&self, position: impl Into<Cube>) -> Option<BlockIndex> {
371 self.contents.get(position.into()).copied()
372 }
373
374 /// Copy data out of a portion of the space in a caller-chosen format.
375 ///
376 /// The given `bounds` must be fully contained within `self.bounds()`.
377 pub fn extract<C, V>(&self, bounds: GridAab, extractor: impl FnMut(Extract<'_>) -> V) -> Vol<C>
378 where
379 C: ops::Deref<Target = [V]> + FromIterator<V>,
380 {
381 Space::read_from_standalone(self).extract(bounds, extractor)
382 }
383
384 /// Returns the [`EvaluatedBlock`] of the block in this space at the given position.
385 ///
386 /// If out of bounds, returns the evaluation of [`AIR`].
387 #[inline(always)]
388 pub fn get_evaluated(&self, position: impl Into<Cube>) -> &EvaluatedBlock {
389 if let Some(block_index) = self.get_block_index(position.into()) {
390 self.palette.entry(block_index).evaluated()
391 } else {
392 AIR_EVALUATED_REF
393 }
394 }
395
396 /// Returns the light occupying the given cube.
397 ///
398 /// This value may be considered as representing the average of the light reflecting
399 /// off of all surfaces within, or immediately adjacent to and facing toward, this cube.
400 /// If there are no such surfaces, or if the given position is out of bounds, the result
401 /// is arbitrary. If the position is within an opaque block, the result is black.
402 ///
403 /// Lighting is updated asynchronously after modifications, so all above claims about
404 /// the meaning of this value are actually “will eventually be, if no more changes are
405 /// made”.
406 #[inline(always)]
407 pub fn get_lighting(&self, cube: impl Into<Cube>) -> PackedLight {
408 self.light.get(cube.into())
409 }
410
411 #[cfg_attr(not(test), allow(unused))]
412 pub(crate) fn in_light_update_queue(&self, cube: Cube) -> bool {
413 self.light.in_light_update_queue(cube)
414 }
415
416 /// Implementation of replacing the block in a single cube, as in [`Mutation::set()`].
417 /// Monomorphic to keep codegen costs low.
418 ///
419 /// `prepared_entry`, if `Some`, is the new palette entry already created by a
420 /// [`SpaceTransaction`] check.
421 fn set_impl(
422 m: &mut Mutation<'_, '_>,
423 position: Cube,
424 block: &Block,
425 prepared_entry: Option<PendingEvaluation>,
426 ) -> Result<bool, SetCubeError> {
427 let mut evaluation_method = match prepared_entry {
428 None => palette::EvaluationMethod::Ticket(m.read_ticket),
429 Some(entry) => palette::EvaluationMethod::AlreadyEvaluated(Some(entry)),
430 };
431
432 if let Some(contents_index) = m.contents.index(position) {
433 let old_block_index = m.contents.as_linear()[contents_index];
434 let old_block = m.palette.entry(old_block_index).block();
435 if *old_block == *block {
436 // No change.
437 return Ok(false);
438 }
439
440 // Replacing one unique block with a new one.
441 //
442 // This special case is worth having because it means that if a unique block is
443 // *modified* (read-modify-write) then the entry is preserved, and rendering
444 // may be able to optimize that case.
445 //
446 // It also means that the externally observable block index behavior is easier
447 // to characterize and won't create unnecessary holes.
448 if m.palette.try_replace_unique(
449 &mut evaluation_method,
450 old_block_index,
451 block,
452 m.change_buffer,
453 ) {
454 Self::side_effects_of_set(
455 m,
456 old_block_index,
457 old_block_index,
458 position,
459 contents_index,
460 );
461 return Ok(true);
462 }
463
464 // Find or allocate index for new block. This must be done before other mutations since it can fail.
465 let new_block_index =
466 m.palette.ensure_index(&mut evaluation_method, block, m.change_buffer, true)?;
467
468 // Update counts
469 m.palette.decrement_maybe_free(old_block_index);
470 m.palette.increment(new_block_index);
471
472 // Write actual space change.
473 m.contents.as_linear_mut()[contents_index] = new_block_index;
474
475 Self::side_effects_of_set(
476 m,
477 old_block_index,
478 new_block_index,
479 position,
480 contents_index,
481 );
482 Ok(true)
483 } else {
484 Err(SetCubeError::OutOfBounds {
485 modification: GridAab::single_cube(position),
486 space_bounds: m.contents.bounds(),
487 })
488 }
489 }
490
491 /// Implement the consequences of changing what block occupies a cube.
492 ///
493 /// `contents_index` is redundant with `position` but saves computation.
494 #[inline]
495 fn side_effects_of_set(
496 m: &mut Mutation<'_, '_>,
497 old_block_index: BlockIndex,
498 new_block_index: BlockIndex,
499 cube: Cube,
500 contents_index: usize,
501 ) {
502 let evaluated = &m.palette.entry(new_block_index).evaluated;
503
504 if evaluated.attributes().tick_action.is_some() {
505 m.cubes_wanting_ticks.insert(cube);
506 }
507 // We could also *remove* the cube from `cubes_wanting_ticks` if it has no action,
508 // but that would be frequently wasted. Instead, let the tick come around and remove
509 // it then.
510
511 m.light.modified_cube_needs_update(
512 light::UpdateCtx {
513 contents: m.contents.as_ref(),
514 palette: m.palette,
515 },
516 m.change_buffer,
517 cube,
518 evaluated,
519 contents_index,
520 );
521
522 m.change_buffer.push(SpaceChange::CubeBlock {
523 cube,
524 old_block_index,
525 new_block_index,
526 });
527 }
528
529 /// Converts `&Space` into [`Read`] for when you need it.
530 pub fn read(&self) -> Read<'_> {
531 Self::read_from_standalone(self)
532 }
533
534 /// Begins a batch of mutations to the contents of this space.
535 ///
536 /// `read_ticket` should be a [`ReadTicket`] obtained from the [`Universe`] which contains
537 /// the [`BlockDef`]s used and will eventually contain this [`Space`] too.
538 ///
539 /// The returned [`Mutation`] contains methods to perform various mutations.
540 //---
541 // Design note: The reason this is a function with callback, rather than returning a
542 // `Mutation`, is so that the `Mutation` is guaranteed to be dropped and deliver its
543 // notifications.
544 //
545 // In the future, there may also be ways in which the space can be in a temporarily invalid
546 // state (e.g. allocating a block index before it is used anywhere).
547 #[inline] // a non-inlined monomorphization of this function will basically never be a benefit
548 pub fn mutate<'space, R>(
549 &'space mut self,
550 read_ticket: ReadTicket<'space>,
551 f: impl FnOnce(&mut Mutation<'_, 'space>) -> R,
552 ) -> R {
553 f(&mut Mutation {
554 read_ticket,
555 palette: &mut self.palette,
556 contents: self.contents.as_mut(),
557 light: &mut self.light,
558 behaviors: &mut self.behaviors,
559 spawn: &mut self.spawn,
560 change_buffer: &mut self.notifiers.change_notifier.buffer(),
561 fluff_buffer: &mut self.notifiers.fluff_notifier.buffer(),
562 cubes_wanting_ticks: &mut self.ticks.cubes_wanting_ticks,
563 })
564 }
565
566 /// Returns all distinct block types found in the space.
567 ///
568 /// TODO: This was invented for testing the indexing of blocks and should
569 /// be replaced with something else *if* it only gets used for testing.
570 pub fn distinct_blocks(&self) -> Vec<Block> {
571 let d = self.block_data();
572 let mut blocks = Vec::with_capacity(d.len());
573 for data in d {
574 if data.count() > 0 {
575 blocks.push(data.block.clone());
576 }
577 }
578 blocks
579 }
580
581 /// Returns data about all the blocks assigned internal IDs (indices) in the space,
582 /// as well as placeholder data for any deallocated indices.
583 ///
584 /// The indices of this slice correspond to the results of [`Space::get_block_index`].
585 pub fn block_data(&self) -> &[SpaceBlockData] {
586 self.palette.entries()
587 }
588
589 /// Returns the source of [fluff](Fluff) occurring in this space.
590 pub fn fluff(
591 &self,
592 ) -> impl Listen<Msg = SpaceFluff, Listener = listen::DynListener<SpaceFluff>> + '_ {
593 &self.notifiers.fluff_notifier
594 }
595
596 /// Returns the current [`SpacePhysics`] data, which determines global characteristics
597 /// such as the behavior of light and gravity.
598 pub fn physics(&self) -> &SpacePhysics {
599 &self.physics
600 }
601
602 /// Sets the physics parameters, as per [`physics`](Self::physics).
603 ///
604 /// This may cause immediate recomputation of lighting.
605 pub fn set_physics(&mut self, physics: SpacePhysics) {
606 if physics == self.physics {
607 return;
608 }
609
610 self.physics = physics;
611 // We could avoid this clone by putting `&self.physics` in `UpdateCtx`, but it’s not worth
612 // it for the common cases.
613 // TODO: But maybe add it to the return value of `borrow_light_update_context`?
614 let new_physics = self.physics.clone();
615
616 let (light, uc, mut change_buffer) = self.borrow_light_update_context();
617 light.maybe_reinitialize_for_physics_change(
618 uc,
619 &new_physics,
620 uc.palette.all_block_opacities_as_category(),
621 );
622
623 // TODO: We should notify specifically whether the light changed,
624 // but there isn't a message for that.
625 change_buffer.push(SpaceChange::Physics);
626 }
627
628 /// Returns the current default [`Spawn`], which determines where new [`Character`]s
629 /// are placed in the space if no alternative applies.
630 pub fn spawn(&self) -> &Spawn {
631 &self.spawn
632 }
633
634 /// Sets the default [`Spawn`], which determines where new [`Character`]s are placed
635 /// in the space if no alternative applies.
636 pub fn set_spawn(&mut self, spawn: Spawn) {
637 self.spawn = spawn;
638 }
639
640 /// Returns the [`BehaviorSet`] of behaviors attached to this space.
641 pub fn behaviors(&self) -> &BehaviorSet<Space> {
642 &self.behaviors
643 }
644
645 /// Produce split borrows of `self` to run light updating functions.
646 fn borrow_light_update_context(
647 &mut self,
648 ) -> (&mut LightStorage, light::UpdateCtx<'_>, ChangeBuffer<'_>) {
649 (
650 &mut self.light,
651 light::UpdateCtx {
652 contents: self.contents.as_ref(),
653 palette: &self.palette,
654 },
655 self.notifiers.change_notifier.buffer(),
656 )
657 }
658
659 /// Transform ECS components into what light updating functions need.
660 fn borrow_light_update_context_ecs<'w, 'n>(
661 palette: &'w Palette,
662 contents: &'w mut Contents,
663 notifiers: &'n Notifiers,
664 ) -> (light::UpdateCtx<'w>, ChangeBuffer<'n>) {
665 (
666 light::UpdateCtx {
667 contents: contents.0.as_ref(),
668 palette,
669 },
670 notifiers.change_notifier.buffer(),
671 )
672 }
673
674 #[cfg(test)]
675 #[track_caller]
676 pub(crate) fn consistency_check(&self) {
677 self.palette.consistency_check(self.contents.as_linear());
678 self.light.consistency_check();
679 }
680}
681
682impl Read<'_> {
683 // TODO(ecs): Read should have a complete set of getter functions and does not yet
684
685 /// Returns the [`GridAab`] describing the bounds of this space; no blocks may exist
686 /// outside it.
687 pub fn bounds(&self) -> GridAab {
688 self.contents.bounds()
689 }
690
691 /// Returns the internal unstable numeric ID for the block at the given position,
692 /// which may be mapped to a [`Block`] by [`Read::block_data()`].
693 /// If you are looking for *simple* access, use `space[position]` (the
694 /// [`core::ops::Index`] trait) instead.
695 ///
696 /// These IDs may be used to perform efficient processing of many blocks, but they
697 /// may be renumbered after any mutation.
698 #[inline(always)]
699 pub fn get_block_index(&self, position: impl Into<Cube>) -> Option<BlockIndex> {
700 self.contents.get(position.into()).copied()
701 }
702
703 /// Returns the [`EvaluatedBlock`] of the block in this space at the given position.
704 ///
705 /// If out of bounds, returns the evaluation of [`AIR`].
706 #[inline(always)]
707 pub fn get_evaluated(&self, position: impl Into<Cube>) -> &EvaluatedBlock {
708 if let Some(block_index) = self.get_block_index(position.into()) {
709 self.palette.entry(block_index).evaluated()
710 } else {
711 AIR_EVALUATED_REF
712 }
713 }
714
715 /// Returns the light occupying the given cube.
716 ///
717 /// This value may be considered as representing the average of the light reflecting
718 /// off of all surfaces within, or immediately adjacent to and facing toward, this cube.
719 /// If there are no such surfaces, or if the given position is out of bounds, the result
720 /// is arbitrary. If the position is within an opaque block, the result is black.
721 ///
722 /// Lighting is updated asynchronously after modifications, so all above claims about
723 /// the meaning of this value are actually “will eventually be, if no more changes are
724 /// made”.
725 #[inline(always)]
726 pub fn get_lighting(&self, cube: impl Into<Cube>) -> PackedLight {
727 self.light.get(cube.into())
728 }
729
730 /// Copy data out of a portion of the space in a caller-chosen format.
731 ///
732 /// The given `bounds` must be fully contained within `self.bounds()`.
733 pub fn extract<'s, C, V>(
734 &'s self,
735 bounds: GridAab,
736 mut extractor: impl FnMut(Extract<'s>) -> V,
737 ) -> Vol<C>
738 where
739 C: ops::Deref<Target = [V]> + FromIterator<V>,
740 {
741 assert!(self.bounds().contains_box(bounds));
742
743 // TODO: Implement an iterator over the indexes (which is not just
744 // interior_iter().enumerate() because it's a sub-region) so that we don't
745 // have to run independent self.bounds.index() calculations per cube.
746 // (But before that, we can optimize the case given bounds are the whole space.)
747 Vol::from_fn(bounds, |cube| {
748 extractor(Extract {
749 space: self,
750 cube,
751 cube_index: self.contents.index(cube).unwrap(),
752 block_index: Default::default(),
753 })
754 })
755 }
756
757 /// Returns data about all the blocks assigned internal IDs (indices) in the space,
758 /// as well as placeholder data for any deallocated indices.
759 ///
760 /// The indices of this slice correspond to the results of [`Space::get_block_index()`].
761 pub fn block_data(&self) -> &[SpaceBlockData] {
762 self.palette.entries()
763 }
764
765 /// Returns the source of [fluff](Fluff) occurring in this space.
766 pub fn fluff(
767 &self,
768 ) -> impl Listen<Msg = SpaceFluff, Listener = listen::DynListener<SpaceFluff>> + '_ {
769 &self.notifiers.fluff_notifier
770 }
771
772 pub(crate) fn fluff_notifier(&self) -> &Notifier<SpaceFluff> {
773 &self.notifiers.fluff_notifier
774 }
775
776 /// Returns the current [`SpacePhysics`] data, which determines global characteristics
777 /// such as the behavior of light and gravity.
778 pub fn physics(&self) -> &SpacePhysics {
779 self.physics
780 }
781
782 /// Returns the current default [`Spawn`], which determines where new [`Character`]s
783 /// are placed in the space if no alternative applies.
784 pub fn spawn(&self) -> &Spawn {
785 self.default_spawn
786 }
787
788 /// Returns the [`BehaviorSet`] of behaviors attached to this space.
789 pub fn behaviors(&self) -> &BehaviorSet<Space> {
790 self.behaviors
791 }
792
793 #[cfg_attr(not(feature = "save"), allow(unused))]
794 pub(crate) fn in_light_update_queue(&self, cube: Cube) -> bool {
795 self.light.in_light_update_queue(cube)
796 }
797
798 /// Compute the new light value for a cube.
799 ///
800 /// The returned vector of points lists those cubes which the computed value depends on
801 /// (imprecisely; empty cubes passed through are not listed).
802 #[doc(hidden)] // pub to be used by all-is-cubes-gpu for debugging visualization
803 pub fn compute_light<D>(&self, cube: Cube) -> light::ComputedLight<D>
804 where
805 D: light::LightComputeOutput,
806 {
807 // Unlike borrow_light_update_context(), this returns only references
808 let (light, uc) = {
809 (
810 &self.light,
811 light::UpdateCtx {
812 contents: self.contents.as_ref(),
813 palette: self.palette,
814 },
815 )
816 };
817 light.compute_lighting(uc, cube)
818 }
819
820 #[doc(hidden)] // pub to be used by all-is-cubes-gpu for debugging visualization
821 pub fn last_light_updates(&self) -> impl ExactSizeIterator<Item = Cube> + '_ {
822 self.light.last_light_updates.iter().copied()
823 }
824}
825
826impl<T: Into<Cube>> ops::Index<T> for Space {
827 type Output = Block;
828
829 /// Gets a reference to the block in this space at the given position.
830 ///
831 /// If the position is out of bounds, returns [`AIR`].
832 ///
833 /// Note that [`Space`] does not implement [`IndexMut`](core::ops::IndexMut);
834 /// use [`Mutation::set()`] or [`Mutation::fill()`] to modify blocks.
835 #[inline(always)]
836 fn index(&self, position: T) -> &Self::Output {
837 if let Some(&block_index) = self.contents.get(position.into()) {
838 self.palette.entry(block_index).block()
839 } else {
840 &AIR
841 }
842 }
843}
844
845impl<T: Into<Cube>> ops::Index<T> for Read<'_> {
846 type Output = Block;
847
848 /// Gets a reference to the block in this space at the given position.
849 ///
850 /// If the position is out of bounds, returns [`AIR`].
851 ///
852 /// There is no corresponding [`IndexMut`](core::ops::IndexMut) implementation;
853 /// use [`Mutation::set()`] or [`Mutation::fill()`] to modify blocks.
854 #[inline(always)]
855 fn index(&self, position: T) -> &Self::Output {
856 if let Some(&block_index) = self.contents.get(position.into()) {
857 self.palette.entry(block_index).block()
858 } else {
859 &AIR
860 }
861 }
862}
863
864impl universe::SealedMember for Space {
865 type Bundle = (
866 Palette,
867 Contents,
868 LightStorage,
869 SpacePhysics,
870 BehaviorSet<Space>,
871 Ticks,
872 DefaultSpawn,
873 Notifiers,
874 );
875 type ReadQueryData = (
876 &'static Palette,
877 &'static Contents,
878 &'static LightStorage,
879 &'static SpacePhysics,
880 &'static BehaviorSet<Space>,
881 &'static DefaultSpawn,
882 &'static Notifiers,
883 );
884
885 fn register_all_member_components(world: &mut ecs::World) {
886 universe::VisitableComponents::register::<Palette>(world);
887 // no handles in Contents
888 // no handles in LightStorage
889 // no handles in SpacePhysics
890 universe::VisitableComponents::register::<BehaviorSet<Space>>(world);
891 universe::VisitableComponents::register::<DefaultSpawn>(world);
892 // no relevant handles in Notifiers
893 }
894
895 fn read_from_standalone(value: &Self) -> <Self as universe::UniverseMember>::Read<'_> {
896 Read {
897 palette: &value.palette,
898 contents: value.contents.as_ref(),
899 light: &value.light,
900 physics: &value.physics,
901 behaviors: &value.behaviors,
902 default_spawn: &value.spawn,
903 notifiers: &value.notifiers,
904 }
905 }
906 fn read_from_query(
907 data: <Self::ReadQueryData as ::bevy_ecs::query::QueryData>::Item<'_>,
908 ) -> <Self as universe::UniverseMember>::Read<'_> {
909 let (palette, contents, light, physics, behaviors, default_spawn, notifiers) = data;
910 Read {
911 palette,
912 contents: contents.0.as_ref(),
913 light,
914 physics,
915 behaviors,
916 default_spawn: &default_spawn.0,
917 notifiers,
918 }
919 }
920 fn read_from_entity_ref(
921 entity: ::bevy_ecs::world::EntityRef<'_>,
922 ) -> Option<<Self as universe::UniverseMember>::Read<'_>> {
923 Some(Read {
924 palette: entity.get()?,
925 contents: entity.get::<Contents>()?.0.as_ref(),
926 light: entity.get()?,
927 physics: entity.get()?,
928 behaviors: entity.get()?,
929 default_spawn: &entity.get::<DefaultSpawn>()?.0,
930 notifiers: entity.get()?,
931 })
932 }
933 fn into_bundle(value: Box<Self>) -> Self::Bundle {
934 let Self {
935 palette,
936 contents,
937 light,
938 physics,
939 behaviors,
940 spawn,
941 ticks,
942 notifiers,
943 } = *value;
944 (
945 palette,
946 Contents(contents),
947 light,
948 physics,
949 behaviors,
950 ticks,
951 DefaultSpawn(spawn),
952 notifiers,
953 )
954 }
955}
956impl universe::UniverseMember for Space {
957 type Read<'ticket> = Read<'ticket>;
958}
959
960impl VisitHandles for Space {
961 fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
962 let Space {
963 palette,
964 contents: _,
965 light: _,
966 physics: _,
967 behaviors,
968 ticks: _,
969 spawn,
970 notifiers: _,
971 } = self;
972 palette.visit_handles(visitor);
973 behaviors.visit_handles(visitor);
974 spawn.visit_handles(visitor);
975 }
976}
977
978impl VisitHandles for DefaultSpawn {
979 fn visit_handles(&self, visitor: &mut dyn HandleVisitor) {
980 let Self(spawn) = self;
981 spawn.visit_handles(visitor);
982 }
983}
984
985/// Registers a listener for mutations of this space.
986impl Listen for Space {
987 type Msg = SpaceChange;
988 type Listener = <Notifier<Self::Msg> as Listen>::Listener;
989 fn listen_raw(&self, listener: Self::Listener) {
990 self.notifiers.change_notifier.listen_raw(listener)
991 }
992}
993
994/// Registers a listener for mutations of this space.
995impl Listen for Read<'_> {
996 type Msg = SpaceChange;
997 type Listener = <Notifier<Self::Msg> as Listen>::Listener;
998 fn listen_raw(&self, listener: Self::Listener) {
999 self.notifiers.change_notifier.listen_raw(listener)
1000 }
1001}
1002
1003// -------------------------------------------------------------------------------------------------
1004
1005/// Ways that [`Mutation::set()`] can fail to make a change.
1006///
1007/// Note that "already contained the given block" is considered a success.
1008#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1009#[non_exhaustive]
1010pub enum SetCubeError {
1011 /// The given cube or region is not within the bounds of this Space.
1012 OutOfBounds {
1013 /// The cube or region where modification was attempted.
1014 modification: GridAab,
1015 /// The bounds of the space.
1016 space_bounds: GridAab,
1017 },
1018
1019 /// More distinct blocks were added than currently supported.
1020 TooManyBlocks(),
1021}
1022
1023impl core::error::Error for SetCubeError {}
1024
1025impl fmt::Display for SetCubeError {
1026 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1027 match self {
1028 SetCubeError::OutOfBounds {
1029 modification,
1030 space_bounds,
1031 } => write!(
1032 f,
1033 "{modification:?} is outside of the Space bounds {space_bounds:?}"
1034 ),
1035 SetCubeError::TooManyBlocks() => write!(
1036 f,
1037 "more than {} unique blocks in a Space is not yet supported",
1038 BlockIndex::MAX as usize + 1
1039 ),
1040 }
1041 }
1042}
1043
1044// -------------------------------------------------------------------------------------------------
1045
1046/// Description of a change to a [`Space`].
1047///
1048/// This message type may be received via [`Space::listen()`].
1049#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1050#[expect(
1051 clippy::exhaustive_enums,
1052 reason = "any change will probably be breaking anyway"
1053)]
1054#[expect(clippy::module_name_repetitions)] // TODO: consider renaming all *Change types
1055pub enum SpaceChange {
1056 /// The block occupying the specified cube was replaced.
1057 CubeBlock {
1058 /// The cube whose contents changed.
1059 cube: Cube,
1060 /// The index within [`Space::block_data()`] that the space contained prior to this message.
1061 old_block_index: BlockIndex,
1062 /// The index within [`Space::block_data()`] that the space contains after this message.
1063 ///
1064 /// Note that it may be the case that `old_block_index == new_block_index`.
1065 /// This does not mean that a block is replaced with itself
1066 /// (that would not produce any notifications),
1067 /// but rather that a block that occurred exactly once in the space was replaced with a
1068 /// different block. In this situation, a [`SpaceChange::BlockIndex`] message is also sent.
1069 new_block_index: BlockIndex,
1070 },
1071
1072 /// The light level value at the given location changed.
1073 CubeLight {
1074 /// The cube whose light level changed.
1075 cube: Cube,
1076 },
1077
1078 /// The given block index number was reassigned and now refers to a different
1079 /// [`Block`] value.
1080 BlockIndex(BlockIndex),
1081
1082 /// The evaluation of the block referred to by the given block index number has
1083 /// changed. The result of [`Space::get_evaluated()`] for that index may differ, but
1084 /// the [`Block`] value remains equal.
1085 BlockEvaluation(BlockIndex),
1086
1087 /// The space contents were completely overwritten in some way.
1088 /// This should be understood as equivalent to [`SpaceChange::CubeBlock`] for every cube
1089 /// and [`SpaceChange::BlockIndex`] for every index.
1090 EveryBlock,
1091
1092 /// The associated [`SpacePhysics`] was changed.
1093 Physics,
1094}
1095
1096// -------------------------------------------------------------------------------------------------
1097
1098/// [`Fluff`] happening at a point in space.
1099#[derive(Debug, Clone, Hash, Eq, PartialEq)]
1100#[non_exhaustive]
1101#[expect(clippy::module_name_repetitions)] // TODO: consider renaming all Space* types
1102pub struct SpaceFluff {
1103 /// Cube at which it was emitted.
1104 /// TODO: we're going to want rotation and fine positioning eventually
1105 pub position: Cube,
1106 #[allow(missing_docs)]
1107 pub fluff: Fluff,
1108}
1109
1110// -------------------------------------------------------------------------------------------------
1111
1112/// Performance data about stepping a [`Space`].
1113///
1114/// The exact contents of this structure
1115/// are unstable; use only `Debug` formatting to examine its contents unless you have
1116/// a specific need for one of the values.
1117#[derive(Clone, Debug, Default, PartialEq)]
1118#[non_exhaustive]
1119#[expect(clippy::module_name_repetitions)] // TODO: consider renaming all Space* types and all *StepInfo types
1120pub struct SpaceStepInfo {
1121 /// Number of spaces whose updates were aggregated into this value.
1122 pub spaces: usize,
1123
1124 /// Time and count of block re-evaluations.
1125 ///
1126 /// Note that this does not count evaluations resulting from modifications
1127 /// that add new blocks to the space.
1128 pub evaluations: TimeStats,
1129
1130 /// Number of individual cubes processed (`tick_action`).
1131 cube_ticks: usize,
1132
1133 /// Time spent on processing individual cube updates
1134 /// (measured as a whole because transaction conflict checking is needed),
1135 cube_time: Duration,
1136
1137 behaviors: BehaviorSetStepInfo,
1138
1139 /// Performance data about light updates within the space.
1140 pub light: LightUpdatesInfo,
1141}
1142impl ops::AddAssign<SpaceStepInfo> for SpaceStepInfo {
1143 fn add_assign(&mut self, other: Self) {
1144 if other == Self::default() {
1145 // Specifically don't count those that did nothing.
1146 return;
1147 }
1148 let Self {
1149 spaces,
1150 evaluations,
1151 cube_ticks,
1152 cube_time,
1153 behaviors,
1154 light,
1155 } = self;
1156 *spaces += other.spaces;
1157 *evaluations += other.evaluations;
1158 *cube_ticks += other.cube_ticks;
1159 *cube_time += other.cube_time;
1160 *behaviors += other.behaviors;
1161 *light += other.light;
1162 }
1163}
1164impl Fmt<StatusText> for SpaceStepInfo {
1165 fn fmt(&self, fmt: &mut fmt::Formatter<'_>, fopt: &StatusText) -> fmt::Result {
1166 let Self {
1167 spaces,
1168 evaluations,
1169 cube_ticks,
1170 cube_time,
1171 behaviors,
1172 light,
1173 } = self;
1174 if self.spaces > 0 {
1175 let light = light.refmt(fopt);
1176 let cube_time = cube_time.refmt(&ConciseDebug);
1177 let behaviors = behaviors.refmt(fopt);
1178 write!(
1179 fmt,
1180 "\
1181 {spaces} spaces' steps:\n\
1182 Block reeval: {evaluations}\n\
1183 Cubes: {cube_ticks:3} cubes ticked in {cube_time}\n\
1184 Behaviors: {behaviors}\n\
1185 Light: {light}\
1186 "
1187 )?;
1188 } else {
1189 write!(fmt, "No spaces stepped")?;
1190 }
1191
1192 Ok(())
1193 }
1194}
1195
1196// -------------------------------------------------------------------------------------------------
1197
1198/// Access to data of a single cube of a [`Space`], provided by [`Space::extract()`].
1199///
1200/// Methods of this type are optimized to not perform redundant computation between each
1201/// other but not if called more than once.
1202#[derive(Clone, Debug)]
1203pub struct Extract<'s> {
1204 space: &'s Read<'s>,
1205 cube: Cube,
1206 cube_index: usize,
1207 block_index: core::cell::OnceCell<BlockIndex>,
1208}
1209
1210impl<'s> Extract<'s> {
1211 /// Returns the cube being processed.
1212 #[allow(unused, reason = "currently only used on feature=save")]
1213 pub(crate) fn cube(&self) -> Cube {
1214 self.cube
1215 }
1216
1217 /// Returns the block index; the index within [`Space::block_data()`] where the block
1218 /// present in this cube can be found.
1219 #[inline]
1220 pub fn block_index(&self) -> BlockIndex {
1221 *self
1222 .block_index
1223 .get_or_init(|| self.space.contents.as_linear()[self.cube_index])
1224 }
1225
1226 /// Returns the [`SpaceBlockData`] for the block present in this cube.
1227 #[inline]
1228 pub fn block_data(&self) -> &'s SpaceBlockData {
1229 self.space.palette.entry(self.block_index())
1230 }
1231
1232 /// Returns the data for the light present in this cube.
1233 #[inline]
1234 pub fn light(&self) -> PackedLight {
1235 match self.space.physics.light {
1236 LightPhysics::None => PackedLight::ONE,
1237 LightPhysics::Rays { .. } => self.space.light.contents.as_linear()[self.cube_index],
1238 }
1239 }
1240}
1241
1242// -------------------------------------------------------------------------------------------------
1243
1244// TODO: Tune this buffer size parameter, and validate it isn't overly large on the stack.
1245type ChangeBuffer<'notifier> =
1246 listen::Buffer<'notifier, SpaceChange, listen::DynListener<SpaceChange>, 16>;
1247
1248/// Access to a [`Space`]’s contents to perform several modifications.
1249///
1250/// Obtain this using [`Space::mutate()`].
1251#[allow(missing_debug_implementations, reason = "TODO")]
1252pub struct Mutation<'m, 'space> {
1253 /// Used for evaluating blocks that are added.
1254 read_ticket: ReadTicket<'m>,
1255
1256 contents: Vol<&'m mut [BlockIndex]>,
1257 light: &'m mut LightStorage,
1258 palette: &'m mut Palette,
1259 cubes_wanting_ticks: &'m mut HbHashSet<Cube>,
1260 spawn: &'m mut Spawn,
1261 behaviors: &'m mut BehaviorSet<Space>,
1262
1263 // Buffers outgoing notifications; flushed as needed and on drop.
1264 change_buffer: &'m mut ChangeBuffer<'space>,
1265 fluff_buffer: &'m mut listen::Buffer<'space, SpaceFluff, listen::DynListener<SpaceFluff>, 16>,
1266}
1267
1268#[allow(missing_docs, reason = "TODO")]
1269impl<'space> Mutation<'_, 'space> {
1270 pub(crate) fn with_write_query<Out>(
1271 read_ticket: ReadTicket<'space>,
1272 q: bevy_ecs::query::QueryItem<
1273 'space,
1274 <SpaceTransaction as universe::TransactionOnEcs>::WriteQueryData,
1275 >,
1276 f: impl FnOnce(&mut Mutation<'_, 'space>) -> Out,
1277 ) -> Out {
1278 let (mut palette, mut contents, mut light, mut behaviors, mut spawn, notifiers, mut ticks) =
1279 q;
1280 f(&mut Mutation {
1281 read_ticket,
1282 palette: &mut palette,
1283 contents: contents.0.as_mut(),
1284 light: &mut light,
1285 behaviors: &mut behaviors,
1286 spawn: &mut spawn.0,
1287 change_buffer: &mut notifiers.change_notifier.buffer(),
1288 fluff_buffer: &mut notifiers.fluff_notifier.buffer(),
1289 cubes_wanting_ticks: &mut ticks.cubes_wanting_ticks,
1290 })
1291 }
1292
1293 /// Same as [`Space::bounds()`].
1294 pub fn bounds(&self) -> GridAab {
1295 self.contents.bounds()
1296 }
1297
1298 /// Returns the [`EvaluatedBlock`] of the block in this space at the given position.
1299 ///
1300 /// If out of bounds, returns the evaluation of [`AIR`].
1301 #[inline(always)]
1302 pub fn get_evaluated(&self, position: impl Into<Cube>) -> &EvaluatedBlock {
1303 if let Some(block_index) = self.contents.get(position.into()).copied() {
1304 self.palette.entry(block_index).evaluated()
1305 } else {
1306 AIR_EVALUATED_REF
1307 }
1308 }
1309
1310 /// Replace the block in this space at the given position.
1311 ///
1312 /// If the position is out of bounds, there is no effect.
1313 ///
1314 /// # Returns
1315 ///
1316 /// Returns `Ok(true)` if the change was made, `Ok(false)` if the same block was
1317 /// already present, and `Err(_)` if the replacement could not be made; see
1318 /// [`SetCubeError`] for possible errors.
1319 ///
1320 /// ```
1321 /// use all_is_cubes::block;
1322 /// use all_is_cubes::math::Rgba;
1323 /// use all_is_cubes::space::Space;
1324 /// use all_is_cubes::universe::ReadTicket;
1325 ///
1326 /// let mut space = Space::empty_positive(1, 1, 1);
1327 /// let a_block = block::from_color!(1.0, 0.0, 0.0, 1.0);
1328 ///
1329 /// space.mutate(ReadTicket::stub(), |m| {
1330 /// m.set([0, 0, 0], &a_block)
1331 /// }).unwrap();
1332 ///
1333 /// assert_eq!(space[[0, 0, 0]], a_block);
1334 /// ```
1335 pub fn set<'block>(
1336 &mut self,
1337 position: impl Into<Cube>,
1338 block: impl Into<Cow<'block, Block>>,
1339 ) -> Result<bool, SetCubeError> {
1340 Space::set_impl(self, position.into(), &block.into(), None)
1341 }
1342
1343 /// Replace blocks in `region` with a block computed by the function.
1344 ///
1345 /// The function may return a reference to a block or a block. If it returns [`None`],
1346 /// the existing block is left unchanged.
1347 ///
1348 /// The operation will stop on the first error, potentially leaving some blocks
1349 /// replaced. (Exception: If the `region` extends outside of
1350 /// [`self.bounds()`](Self::bounds), that will always be rejected before any changes
1351 /// are made.)
1352 ///
1353 /// ```
1354 /// use all_is_cubes::block;
1355 /// use all_is_cubes::math::{GridAab, Rgba};
1356 /// use all_is_cubes::space::Space;
1357 /// use all_is_cubes::universe::ReadTicket;
1358 ///
1359 /// let mut space = Space::empty_positive(10, 10, 10);
1360 /// let a_block: block::Block = block::from_color!(1.0, 0.0, 0.0, 1.0);
1361 ///
1362 /// space.mutate(ReadTicket::stub(), |m| {
1363 /// m.fill(GridAab::from_lower_size([0, 0, 0], [2, 1, 1]), |_point| Some(&a_block))
1364 /// }).unwrap();
1365 ///
1366 /// assert_eq!(space[[0, 0, 0]], a_block);
1367 /// assert_eq!(space[[1, 0, 0]], a_block);
1368 /// assert_eq!(space[[0, 1, 0]], block::AIR);
1369 /// ```
1370 ///
1371 /// TODO: Support providing the previous block as a parameter (take cues from `extract`).
1372 ///
1373 /// See also [`Mutation::fill_uniform()`] for filling a region with one block.
1374 pub fn fill<F, B>(&mut self, region: GridAab, mut function: F) -> Result<(), SetCubeError>
1375 where
1376 F: FnMut(Cube) -> Option<B>,
1377 B: core::borrow::Borrow<Block>,
1378 {
1379 if !self.bounds().contains_box(region) {
1380 return Err(SetCubeError::OutOfBounds {
1381 modification: region,
1382 space_bounds: self.bounds(),
1383 });
1384 }
1385
1386 for cube in region.interior_iter() {
1387 if let Some(block) = function(cube) {
1388 // TODO: Optimize side effect processing by batching lighting updates for
1389 // when we know what's now opaque or not.
1390 Space::set_impl(self, cube, block.borrow(), None)?;
1391 }
1392 }
1393 Ok(())
1394 }
1395
1396 /// As [`Mutation::fill()`], but fills the entire space instead of a specified region.
1397 pub fn fill_all<F, B>(&mut self, function: F) -> Result<(), SetCubeError>
1398 where
1399 F: FnMut(Cube) -> Option<B>,
1400 B: core::borrow::Borrow<Block>,
1401 {
1402 self.fill(self.bounds(), function)
1403 }
1404
1405 /// Replace blocks in `region` with the given block.
1406 ///
1407 /// TODO: Document error behavior
1408 ///
1409 /// ```
1410 /// use all_is_cubes::block;
1411 /// use all_is_cubes::math::{GridAab, Rgba};
1412 /// use all_is_cubes::space::Space;
1413 /// use all_is_cubes::universe::ReadTicket;
1414 ///
1415 /// let mut space = Space::empty_positive(10, 10, 10);
1416 /// let a_block: block::Block = block::from_color!(1.0, 0.0, 0.0, 1.0);
1417 ///
1418 /// space.mutate(ReadTicket::stub(), |m| {
1419 /// m.fill_uniform(GridAab::from_lower_size([0, 0, 0], [2, 1, 1]), &a_block)
1420 /// }).unwrap();
1421 ///
1422 /// assert_eq!(&space[[0, 0, 0]], &a_block);
1423 /// assert_eq!(&space[[1, 0, 0]], &a_block);
1424 /// assert_eq!(&space[[0, 1, 0]], &block::AIR);
1425 /// ```
1426 ///
1427 /// See also [`Mutation::fill()`] for non-uniform fill and bulk copies.
1428 pub fn fill_uniform(&mut self, region: GridAab, block: &Block) -> Result<(), SetCubeError> {
1429 if !self.bounds().contains_box(region) {
1430 Err(SetCubeError::OutOfBounds {
1431 modification: region,
1432 space_bounds: self.bounds(),
1433 })
1434 } else if self.bounds() == region {
1435 // We're overwriting the entire space, so we might as well re-initialize it.
1436 {
1437 let linear = self.contents.as_linear_mut();
1438 let volume = linear.len();
1439 *self.palette = Palette::new(self.read_ticket, block.clone(), volume);
1440 linear.fill(/* block index = */ 0);
1441 }
1442 // TODO: if opaque, don't schedule updates
1443 self.light.light_needs_update_in_region(region, light::Priority::UNINIT);
1444 // TODO: also need to activate tick_action if present.
1445 // And see if we can share more of the logic of this with new_from_builder().
1446 self.change_buffer.push(SpaceChange::EveryBlock);
1447 Ok(())
1448 } else {
1449 // Fall back to the generic strategy.
1450 self.fill(region, |_| Some(block))
1451 }
1452 }
1453
1454 /// As [`Mutation::fill_uniform()`], but fills the entire space instead of a specified region.
1455 pub fn fill_all_uniform(&mut self, block: &Block) -> Result<(), SetCubeError> {
1456 self.fill_uniform(self.bounds(), block)
1457 }
1458
1459 /// Provides an [`DrawTarget`](embedded_graphics::prelude::DrawTarget)
1460 /// adapter for 2.5D drawing.
1461 ///
1462 /// For more information on how to use this, see
1463 /// [`all_is_cubes::drawing`](crate::drawing).
1464 pub fn draw_target<C>(&mut self, transform: Gridgid) -> DrawingPlane<'_, Self, C> {
1465 DrawingPlane::new(self, transform)
1466 }
1467
1468 /// Perform lighting updates until there are none left to do. Returns the number of
1469 /// updates performed.
1470 ///
1471 /// This may take a while. It is appropriate for when the goal is to
1472 /// render a fully lit scene non-interactively.
1473 ///
1474 /// `epsilon` specifies a threshold at which to stop doing updates.
1475 /// Zero means to run to full completion; one is the smallest unit of light level
1476 /// difference; and so on.
1477 pub fn evaluate_light(
1478 &mut self,
1479 epsilon: u8,
1480 mut progress_callback: impl FnMut(LightUpdatesInfo),
1481 ) -> usize {
1482 let (light, uc, change_buffer) = self.borrow_light_update_context();
1483 let epsilon = light::Priority::from_difference(epsilon);
1484
1485 let mut total = 0;
1486 loop {
1487 let info = light.update_lighting_from_queue(
1488 uc,
1489 change_buffer,
1490 Some(Duration::from_secs_f32(0.25)),
1491 );
1492
1493 progress_callback(info);
1494
1495 let LightUpdatesInfo {
1496 update_count,
1497 max_queue_priority,
1498 ..
1499 } = info;
1500 total += update_count;
1501 if max_queue_priority <= epsilon {
1502 // Stop when we have nothing worth updating as decided by epsilon
1503 // (or if the queue is empty).
1504 break;
1505 }
1506 }
1507 total
1508 }
1509
1510 /// Clear and recompute light data and update queue, in a way which gets fast approximate
1511 /// results suitable for flat landscapes mostly lit from above (the +Y axis).
1512 ///
1513 /// This function is useful immeduately after filling a [`Space`] with its initial contents.
1514 ///
1515 /// TODO: Revisit whether this is a good public API.
1516 pub fn fast_evaluate_light(&mut self) {
1517 let (light, uc, _change_buffer) = self.borrow_light_update_context();
1518 light.fast_evaluate_light(uc);
1519
1520 // TODO: change_buffer.push(SpaceChange::EveryBlock), or something
1521 }
1522
1523 #[doc(hidden)] // kludge used by session for tool usage
1524 pub fn evaluate_light_for_time(&mut self, budget: Duration) -> LightUpdatesInfo {
1525 let (light, uc, change_buffer) = self.borrow_light_update_context();
1526 light.update_lighting_from_queue(uc, change_buffer, Some(budget))
1527 }
1528
1529 /// Produce a bundle of borrows to run light updating functions.
1530 fn borrow_light_update_context<'this>(
1531 &'this mut self,
1532 ) -> (
1533 &'this mut LightStorage,
1534 light::UpdateCtx<'this>,
1535 &'this mut ChangeBuffer<'space>,
1536 ) {
1537 (
1538 &mut *self.light,
1539 light::UpdateCtx {
1540 contents: self.contents.as_ref(),
1541 palette: self.palette,
1542 },
1543 &mut self.change_buffer,
1544 )
1545 }
1546
1547 /// Returns the current default [`Spawn`], which determines where new [`Character`]s
1548 /// are placed in the space if no alternative applies.
1549 pub fn spawn(&self) -> &Spawn {
1550 self.spawn
1551 }
1552
1553 /// Sets the default [`Spawn`], which determines where new [`Character`]s are placed
1554 /// in the space if no alternative applies.
1555 pub fn set_spawn(&mut self, spawn: Spawn) {
1556 *self.spawn = spawn;
1557 }
1558}
1559
1560impl<T: Into<Cube>> ops::Index<T> for Mutation<'_, '_> {
1561 type Output = Block;
1562
1563 #[inline(always)]
1564 fn index(&self, position: T) -> &Self::Output {
1565 if let Some(&block_index) = self.contents.get(position.into()) {
1566 self.palette.entry(block_index).block()
1567 } else {
1568 &AIR
1569 }
1570 }
1571}