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}