From ec4813f3f79cffe3f1267fe15ac8d228ac2f8532 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sat, 17 Aug 2024 22:16:10 -0700 Subject: [PATCH] Move `BlockAttributes` storage out of `Primitive`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: * Many blocks have all default attributes. This way, the primitive need not store all attributes (is smaller in memory), is conceptually simpler, and leans less heavily on “default values aren’t serialized”. * Sometimes you want to set attributes on an already-modified block, like a `Composite` of several more basic parts. * `BlockAttributes` itself is an inelegant bundle of many different concerns, which has needed to change incompatibly. (For example, light emission used to be an attribute and now is strictly per-voxel.) After this change, we can, if we choose, completely hide or remove `BlockAttributes`; we would replace creating it with a bunch of modifiers which override individual attributes, and replace reading it with individual methods on `EvaluatedBlock`. --- CHANGELOG.md | 7 +- all-is-cubes-content/src/blocks.rs | 17 +-- all-is-cubes-content/src/city.rs | 5 +- all-is-cubes-content/src/city/exhibits.rs | 10 +- all-is-cubes-content/src/menu.rs | 4 +- all-is-cubes-ui/src/editor.rs | 11 +- all-is-cubes-ui/src/vui/page.rs | 4 +- all-is-cubes-ui/src/vui/widgets/voxels.rs | 37 +++---- all-is-cubes/src/block.rs | 107 ++++++++++--------- all-is-cubes/src/block/attributes.rs | 12 ++- all-is-cubes/src/block/builder.rs | 74 +++++++------ all-is-cubes/src/block/eval/evaluated.rs | 5 + all-is-cubes/src/block/eval/tests.rs | 10 +- all-is-cubes/src/block/modifier.rs | 40 +++++-- all-is-cubes/src/block/modifier/move.rs | 4 +- all-is-cubes/src/block/modifier/zoom.rs | 4 +- all-is-cubes/src/block/tests.rs | 14 +-- all-is-cubes/src/content.rs | 24 ++--- all-is-cubes/src/inv/tool.rs | 14 ++- all-is-cubes/src/save/conversion.rs | 79 +++++++++----- all-is-cubes/src/save/schema.rs | 14 ++- all-is-cubes/src/save/tests.rs | 123 ++++++++++++---------- all-is-cubes/src/space/tests.rs | 46 +++----- all-is-cubes/src/universe/universe_txn.rs | 12 ++- 24 files changed, 379 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18313f4dd..a5046e35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - `all-is-cubes` library: - Block inventories are now more functional. - `block::Block::with_inventory` attaches inventory to a block. - - `inv::InvInBlock`, stored in `block::BlockAtributes::inventory`, describes the size and rendering such inventories should have. + - `inv::InvInBlock`, stored in `block::BlockAttributes::inventory`, describes the size and rendering such inventories should have. + - `block::Modifier::Attributes` allows overriding block attributes. ### Changed @@ -22,6 +23,10 @@ ### Removed +- `all-is-cubes` library: + - `block::Primitive` no longer contains `BlockAttributes` in any of its variants. + The new means of specifying attributes is `block::Modifier::Attributes`. + ## 0.8.0 (2024-07-08) ### Added diff --git a/all-is-cubes-content/src/blocks.rs b/all-is-cubes-content/src/blocks.rs index ebf57df91..ba4c70f8c 100644 --- a/all-is-cubes-content/src/blocks.rs +++ b/all-is-cubes-content/src/blocks.rs @@ -9,7 +9,7 @@ use exhaust::Exhaust; use rand::{Rng as _, SeedableRng as _}; use all_is_cubes::block::{ - self, AnimationHint, Atom, Block, BlockCollision, BlockDefTransaction, Primitive, + self, AnimationHint, Block, BlockCollision, BlockDefTransaction, Primitive, Resolution::*, RotationPlacementRule, TickAction, AIR, }; use all_is_cubes::color_block; @@ -500,10 +500,7 @@ pub async fn install_demo_blocks( // Join up blinker blocks for state in bool::exhaust() { modify_def(&provider_for_patch[BecomeBlinker(state)], |block| { - let Primitive::Atom(Atom { attributes, .. }) = block.primitive_mut() else { - panic!("blinker not atom"); - }; - attributes.tick_action = Some(TickAction { + block.freezing_get_attributes_mut().tick_action = Some(TickAction { operation: Operation::Become(provider_for_patch[BecomeBlinker(!state)].clone()), schedule: time::Schedule::from_period(NonZeroU16::new(60).unwrap()), }); @@ -514,10 +511,7 @@ pub async fn install_demo_blocks( for state in bool::exhaust() { for ctor in [Lamp, Sconce] { modify_def(&provider_for_patch[ctor(state)], |block| { - let Primitive::Recur { attributes, .. } = block.primitive_mut() else { - panic!("lamp not recur"); - }; - attributes.activation_action = + block.freezing_get_attributes_mut().activation_action = Some(Operation::Become(provider_for_patch[ctor(!state)].clone())); }); } @@ -526,9 +520,6 @@ pub async fn install_demo_blocks( // Join up explosion blocks for i in i8::exhaust() { modify_def(&provider_for_patch[Explosion(i)], |block| { - let Primitive::Recur { attributes, .. } = block.primitive_mut() else { - panic!("explosion not atom"); - }; let neighbor_ops: Arc<[(Cube, Operation)]> = if i > 22 { // Expire because we're invisible by now [(Cube::ORIGIN, Operation::Become(AIR))].into() @@ -574,7 +565,7 @@ pub async fn install_demo_blocks( [(Cube::ORIGIN, next.clone())].into() } }; - attributes.tick_action = Some(TickAction { + block.freezing_get_attributes_mut().tick_action = Some(TickAction { operation: Operation::Neighbors(neighbor_ops), schedule: time::Schedule::from_period(NonZeroU16::new(2).unwrap()), }); diff --git a/all-is-cubes-content/src/city.rs b/all-is-cubes-content/src/city.rs index dbed186bf..d7de2eb05 100644 --- a/all-is-cubes-content/src/city.rs +++ b/all-is-cubes-content/src/city.rs @@ -534,10 +534,11 @@ fn place_one_exhibit( bounds_for_info_voxels, universe.insert_anonymous(exhibit_info_space), info_resolution, - BlockAttributes { + [BlockAttributes { display_name: "Exhibit Name".into(), ..Default::default() - }, + } + .into()], ))), ], }); diff --git a/all-is-cubes-content/src/city/exhibits.rs b/all-is-cubes-content/src/city/exhibits.rs index 224cf73d6..799af6519 100644 --- a/all-is-cubes-content/src/city/exhibits.rs +++ b/all-is-cubes-content/src/city/exhibits.rs @@ -284,11 +284,13 @@ fn KNOT(ctx: Context<'_>) { })?; let space = space_to_blocks( resolution, - BlockAttributes { - display_name: ctx.exhibit.name.into(), - ..BlockAttributes::default() - }, txn.insert_anonymous(drawing_space), + &mut |block| { + block.with_modifier(BlockAttributes { + display_name: ctx.exhibit.name.into(), + ..BlockAttributes::default() + }) + }, )?; Ok((space, txn)) } diff --git a/all-is-cubes-content/src/menu.rs b/all-is-cubes-content/src/menu.rs index a1a5e96c7..3549d03a7 100644 --- a/all-is-cubes-content/src/menu.rs +++ b/all-is-cubes-content/src/menu.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use strum::IntoEnumIterator; use all_is_cubes::arcstr; -use all_is_cubes::block::{BlockAttributes, Resolution}; +use all_is_cubes::block::Resolution; use all_is_cubes::character::Spawn; use all_is_cubes::content::palette; use all_is_cubes::euclid::Vector3D; @@ -39,7 +39,7 @@ pub(crate) async fn template_menu( logo_text_space.bounds(), universe.insert_anonymous(logo_text_space), Resolution::R8, - BlockAttributes::default(), + [], ); let mut vertical_widgets: Vec = Vec::with_capacity(10); diff --git a/all-is-cubes-ui/src/editor.rs b/all-is-cubes-ui/src/editor.rs index 21c4ea89d..00c66f132 100644 --- a/all-is-cubes-ui/src/editor.rs +++ b/all-is-cubes-ui/src/editor.rs @@ -60,7 +60,6 @@ fn inspect_primitive(primitive: &block::Primitive) -> vui::WidgetTree { let (name, details): (ArcStr, vui::WidgetTree) = match primitive { block::Primitive::Indirect(block_def) => (literal!("Indirect"), inspect_handle(block_def)), block::Primitive::Atom(block::Atom { - attributes, color, emission, collision, @@ -72,14 +71,12 @@ fn inspect_primitive(primitive: &block::Primitive) -> vui::WidgetTree { "\ Color: {color:?}\n\ Emission: {emission:?}\n\ - Collision: {collision:?}\n\ - Attributes: {attributes:#?}\ + Collision: {collision:?}\ " ))], }), ), block::Primitive::Recur { - attributes, space, offset, resolution, @@ -92,8 +89,7 @@ fn inspect_primitive(primitive: &block::Primitive) -> vui::WidgetTree { paragraph(arcstr::format!( "\ Resolution: {resolution}\n\ - Offset: {offset:?}\n\ - Attributes: {attributes:#?}\ + Offset: {offset:?}\ " )), ], @@ -142,6 +138,9 @@ fn inspect_modifier(block: &Block, modifier_index: usize) -> vui::WidgetTree { .truncate(modifier_index + 1); let (name, details) = match modifier { + block::Modifier::Attributes(a) => { + (literal!("Attributes"), paragraph(arcstr::format!("{a:?}"))) + } block::Modifier::Quote(q) => (literal!("Quote"), paragraph(arcstr::format!("{q:?}"))), block::Modifier::Rotate(rotation) => ( literal!("Rotate"), diff --git a/all-is-cubes-ui/src/vui/page.rs b/all-is-cubes-ui/src/vui/page.rs index 06a869a93..a60ec724b 100644 --- a/all-is-cubes-ui/src/vui/page.rs +++ b/all-is-cubes-ui/src/vui/page.rs @@ -2,7 +2,7 @@ use alloc::sync::Arc; use all_is_cubes::arcstr::ArcStr; use all_is_cubes::block::{text, AIR}; -use all_is_cubes::block::{Block, BlockAttributes, Resolution}; +use all_is_cubes::block::{Block, Resolution}; use all_is_cubes::color_block; use all_is_cubes::content::palette; use all_is_cubes::euclid::{size2, Size2D}; @@ -312,7 +312,7 @@ pub(crate) mod parts { space.bounds(), universe.insert_anonymous(space), resolution, - BlockAttributes::default(), + [], ))) } diff --git a/all-is-cubes-ui/src/vui/widgets/voxels.rs b/all-is-cubes-ui/src/vui/widgets/voxels.rs index 8443608f9..53ba816ab 100644 --- a/all-is-cubes-ui/src/vui/widgets/voxels.rs +++ b/all-is-cubes-ui/src/vui/widgets/voxels.rs @@ -1,6 +1,6 @@ use alloc::sync::Arc; -use all_is_cubes::block::{Block, BlockAttributes, Primitive, Resolution}; +use all_is_cubes::block::{self, Block, Primitive, Resolution}; use all_is_cubes::euclid::Size3D; use all_is_cubes::math::{GridAab, GridMatrix, GridPoint, GridSizeCoord, GridVector}; use all_is_cubes::space::{Space, SpaceTransaction}; @@ -17,7 +17,7 @@ pub struct Voxels { space: Handle, region: GridAab, scale: Resolution, - block_attributes: BlockAttributes, + modifiers: Vec, } impl Voxels { @@ -26,11 +26,11 @@ impl Voxels { /// When the `region`'s dimensions are not multiples of `scale`, their alignment within the /// block grid will be determined by the layout gravity. Note that areas of `space` outside of /// `region` may be displayed in that case. - pub const fn new( + pub fn new( region: GridAab, space: Handle, scale: Resolution, - block_attributes: BlockAttributes, + modifiers: impl IntoIterator, ) -> Self { // Design note: We could take `region` from `space` but that'd require locking it, // and the caller is very likely to already have that information. @@ -38,7 +38,7 @@ impl Voxels { space, region, scale, - block_attributes, + modifiers: modifiers.into_iter().collect(), } } } @@ -92,15 +92,15 @@ impl vui::Widget for Voxels { let mut txn = SpaceTransaction::default(); for cube in position.bounds.interior_iter() { - txn.at(cube) - .overwrite(Block::from_primitive(Primitive::Recur { - attributes: self.block_attributes.clone(), - offset: block_to_voxels_transform - .transform_cube(cube) - .lower_bounds(), - resolution: self.scale, - space: self.space.clone(), - })); + let mut block = Block::from_primitive(Primitive::Recur { + offset: block_to_voxels_transform + .transform_cube(cube) + .lower_bounds(), + resolution: self.scale, + space: self.space.clone(), + }); + block.modifiers_mut().clone_from(&self.modifiers); + txn.at(cube).overwrite(block); } super::OneshotController::new(txn) } @@ -119,7 +119,6 @@ mod tests { fn test_voxels_widget( voxel_space_bounds: GridAab, grant: vui::LayoutGrant, - attributes: BlockAttributes, ) -> (Option, Space) { let mut universe = Universe::new(); instantiate_widget( @@ -128,7 +127,7 @@ mod tests { voxel_space_bounds, universe.insert_anonymous(Space::builder(voxel_space_bounds).build()), R8, - attributes, + [], ), ) } @@ -141,7 +140,6 @@ mod tests { let _output = test_voxels_widget( v_space_bounds, vui::LayoutGrant::new(GridAab::single_cube(output_origin)), - BlockAttributes::default(), ); // TODO assertions about resulting blocks } @@ -158,8 +156,7 @@ mod tests { bounds: GridAab::from_lower_size(output_origin, [3, 3, 3]), gravity: Vector3D::new(Align::Low, Align::Center, Align::High), }; - let (output_bounds, output) = - test_voxels_widget(v_space_bounds, grant, BlockAttributes::default()); + let (output_bounds, output) = test_voxels_widget(v_space_bounds, grant); assert_eq!( output_bounds.unwrap(), GridAab::from_lower_size([100, 101, 101], [1, 1, 2]) @@ -167,7 +164,6 @@ mod tests { // Expect two adjacent recursive blocks match *output[[100, 101, 101]].primitive() { Primitive::Recur { - attributes: _, offset, resolution, space: _, @@ -179,7 +175,6 @@ mod tests { } match *output[[100, 101, 102]].primitive() { Primitive::Recur { - attributes: _, offset, resolution, space: _, diff --git a/all-is-cubes/src/block.rs b/all-is-cubes/src/block.rs index 765485f2e..eb0c63fc8 100644 --- a/all-is-cubes/src/block.rs +++ b/all-is-cubes/src/block.rs @@ -16,7 +16,7 @@ use crate::math::{GridAab, GridCoordinate, GridPoint, GridRotation, GridVector, use crate::space::{SetCubeError, Space, SpaceChange}; use crate::universe::{Handle, HandleVisitor, VisitHandles}; -/// Construct a [`Block`] with the given reflectance color, and default attributes. +/// Construct a [`Block`] with the given reflectance color. /// /// This is equivalent to calling `Block::from()`, except that: /// @@ -168,9 +168,6 @@ pub enum Primitive { /// A block that is composed of smaller blocks, defined by the referenced [`Space`]. Recur { - #[allow(missing_docs)] - attributes: BlockAttributes, - /// The space from which voxels are taken. space: Handle, @@ -217,9 +214,6 @@ pub enum Primitive { #[derive(Clone, Eq, Hash, PartialEq)] #[allow(clippy::exhaustive_structs)] pub struct Atom { - #[allow(missing_docs)] - pub attributes: BlockAttributes, - /// The color exhibited by diffuse reflection from this block. /// /// The RGB components of this color are the *[reflectance]:* the fraction of incoming light @@ -438,6 +432,24 @@ impl Block { .all(|m| m.does_not_introduce_asymmetry()) } + /// Add a [`Modifier::Attributes`] if there isn't one already. + /// Evaluates the block if needed to get existing attributes. + /// + /// TODO: bad API probably, because it overwrites/freezes attributes; this was added in a hurry + /// to tidy up the attributes-is-a-modifer refactor. The proper API is more like with_modifier() + /// for a single attribute, but we don't have single attribute override modifiers yet. + #[doc(hidden)] + pub fn freezing_get_attributes_mut(&mut self) -> &mut BlockAttributes { + if !matches!(self.modifiers().last(), Some(Modifier::Attributes(_))) { + let attr_modifier = self.evaluate().unwrap().attributes.into(); + self.modifiers_mut().push(attr_modifier); + } + let Some(Modifier::Attributes(a)) = self.modifiers_mut().last_mut() else { + unreachable!(); + }; + Arc::make_mut(a) + } + /// Rotates this block by the specified rotation. /// /// Compared to direct use of [`Modifier::Rotate`], this will: @@ -456,7 +468,8 @@ impl Block { /// use all_is_cubes::universe::Universe; /// /// let mut universe = Universe::new(); - /// let [block] = make_some_voxel_blocks(&mut universe); + /// let [mut block] = make_some_voxel_blocks(&mut universe); + /// block.modifiers_mut().clear(); /// let clockwise = GridRotation::CLOCKWISE; /// /// // Basic rotation @@ -628,16 +641,15 @@ impl Block { Primitive::Indirect(ref def_handle) => def_handle.read()?.evaluate_impl(filter)?, Primitive::Atom(Atom { - ref attributes, color, emission, collision, }) => MinEval::new( - attributes.clone(), + BlockAttributes::default(), Evoxels::from_one(Evoxel { color, emission, - selectable: attributes.selectable, + selectable: true, collision, }), ), @@ -645,7 +657,6 @@ impl Block { Primitive::Air => AIR_EVALUATED_MIN, Primitive::Recur { - ref attributes, offset, resolution, space: ref space_handle, @@ -721,13 +732,12 @@ impl Block { BlockAttributes { // Translate the voxels' animation hints into their effect on // the outer block. - animation_hint: attributes.animation_hint - | AnimationHint { - redefinition: voxels_animation_hint.redefinition - | voxels_animation_hint.replacement, - replacement: AnimationChange::None, - }, - ..attributes.clone() + animation_hint: AnimationHint { + redefinition: voxels_animation_hint.redefinition + | voxels_animation_hint.replacement, + replacement: AnimationChange::None, + }, + ..BlockAttributes::default() }, Evoxels::from_many(resolution, voxels), ) @@ -871,14 +881,12 @@ mod arbitrary_block { Ok(match u.int_in_range(0..=4)? { 0 => Primitive::Air, 1 => Primitive::Atom(Atom { - attributes: BlockAttributes::arbitrary(u)?, color: Rgba::arbitrary(u)?, emission: Rgb::arbitrary(u)?, collision: BlockCollision::arbitrary(u)?, }), 2 => Primitive::Indirect(Handle::arbitrary(u)?), 3 => Primitive::Recur { - attributes: BlockAttributes::arbitrary(u)?, offset: GridPoint::from(<[i32; 3]>::arbitrary(u)?), resolution: Resolution::arbitrary(u)?, space: Handle::arbitrary(u)?, @@ -896,7 +904,8 @@ mod arbitrary_block { // `Primitive` is arbitrarily recursive because it can contain `Block`s // (via `Indirect` and `Text`). Therefore, the size hint calculation will always hit // the depth limit, and we should skip it for efficiency. - // The lower bound is 2 because we need at least one byte to make a choice of primitive. + // The lower bound is 1 because we need at least one byte to make a choice of primitive, + // but if that primitive is `AIR` then we need no more bytes. (1, None) } } @@ -926,33 +935,40 @@ impl Primitive { /// under all possible conditions of the rest of the universe. pub(in crate::block) fn rotationally_symmetric(&self) -> bool { match self { - Primitive::Indirect(_) => false, - Primitive::Atom(Atom { - attributes, - color: _, - emission: _, - collision: _, - }) => attributes.rotationally_symmetric(), - Primitive::Recur { .. } => false, + Primitive::Indirect(_) => false, // could point to anything + Primitive::Atom(atom) => atom.rotationally_symmetric(), + Primitive::Recur { .. } => false, // could point to anything Primitive::Air => true, - Primitive::Text { .. } => false, + Primitive::Text { .. } => false, // always asymmetric unless it's trivial } } } +impl Atom { + fn rotationally_symmetric(&self) -> bool { + let Self { + color: _, + emission: _, + collision: _, + } = self; + // I'm planning to eventually have non-uniform collision behaviors + // or visual effects such as normal mapping, + // at which point this will be sometimes false. + true + } +} + impl fmt::Debug for Primitive { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Indirect(def) => f.debug_tuple("Indirect").field(def).finish(), Self::Atom(atom) => atom.fmt(f), Self::Recur { - attributes, space, offset, resolution, } => f .debug_struct("Recur") - .field("attributes", attributes) .field("space", space) .field("offset", offset) .field("resolution", resolution) @@ -970,15 +986,11 @@ impl fmt::Debug for Primitive { impl fmt::Debug for Atom { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let &Self { - ref attributes, color, emission, collision, } = self; let mut s = f.debug_struct("Atom"); - if attributes != &BlockAttributes::default() { - s.field("attributes", &attributes); - } s.field("color", &color); if emission != Rgb::ZERO { s.field("emission", &emission); @@ -996,12 +1008,10 @@ impl VisitHandles for Primitive { Primitive::Air => {} Primitive::Recur { space, - attributes, offset: _, resolution: _, } => { visitor.visit(space); - attributes.visit_handles(visitor); } Primitive::Text { text, offset: _ } => text.visit_handles(visitor), } @@ -1009,14 +1019,12 @@ impl VisitHandles for Primitive { } impl VisitHandles for Atom { - fn visit_handles(&self, visitor: &mut dyn HandleVisitor) { + fn visit_handles(&self, _: &mut dyn HandleVisitor) { let Self { - attributes, color: _, emission: _, collision: _, } = self; - attributes.visit_handles(visitor); } } @@ -1024,13 +1032,12 @@ mod conversions_for_atom { use super::*; impl Atom { - /// Construct an [`Atom`] with the given reflectance color, and default attributes. + /// Construct an [`Atom`] with the given reflectance color. /// /// This is identical to `From::from()` except that it is a `const fn`. // TODO: public API? pub(crate) const fn from_color(color: Rgba) -> Self { Atom { - attributes: BlockAttributes::default(), color, emission: Rgb::ZERO, collision: BlockCollision::DEFAULT_FOR_FROM_COLOR, @@ -1108,12 +1115,13 @@ impl BlockChange { /// /// Panics if the `Space` cannot be accessed, and returns /// [`SetCubeError::TooManyBlocks`] if the space volume is too large. -/// -/// TODO: add doc test for this +//--- +// TODO: add doc test for this +// TODO: This is only used once ... is it really a good public API? pub fn space_to_blocks( resolution: Resolution, - attributes: BlockAttributes, space_handle: Handle, + block_transform: &mut dyn FnMut(Block) -> Block, ) -> Result { let resolution_g: GridCoordinate = resolution.into(); let source_bounds = space_handle @@ -1124,12 +1132,11 @@ pub fn space_to_blocks( let mut destination_space = Space::empty(destination_bounds); destination_space.fill(destination_bounds, move |cube| { - Some(Block::from_primitive(Primitive::Recur { - attributes: attributes.clone(), + Some(block_transform(Block::from_primitive(Primitive::Recur { offset: (cube.lower_bounds().to_vector() * resolution_g).to_point(), resolution, space: space_handle.clone(), - })) + }))) })?; Ok(destination_space) } diff --git a/all-is-cubes/src/block/attributes.rs b/all-is-cubes/src/block/attributes.rs index f27cabfe7..11fa78d73 100644 --- a/all-is-cubes/src/block/attributes.rs +++ b/all-is-cubes/src/block/attributes.rs @@ -8,15 +8,16 @@ use crate::inv::InvInBlock; use crate::math::{Face6, GridRotation}; use crate::op::Operation; +use crate::block::Modifier; use crate::time; #[cfg(doc)] use crate::{ - block::{Block, BlockDef, Modifier, Primitive}, + block::{Block, BlockDef, Primitive}, space::Space, time::TickSchedule, }; -/// Collection of miscellaneous attribute data for blocks that doesn't come in variants. +/// Miscellaneous properties of blocks that are not the block’s voxels. /// /// `BlockAttributes::default()` will produce a reasonable set of defaults for “ordinary” /// blocks. @@ -237,6 +238,13 @@ impl crate::universe::VisitHandles for BlockAttributes { } } +impl From for Modifier { + /// Converts [`BlockAttributes`] to a modifier that applies them to a block. + fn from(value: BlockAttributes) -> Self { + Modifier::Attributes(alloc::sync::Arc::new(value)) + } +} + /// Specifies the effect on a [`Body`](crate::physics::Body) of colliding with a /// [`Primitive::Atom`] block or voxel having this property. // diff --git a/all-is-cubes/src/block/builder.rs b/all-is-cubes/src/block/builder.rs index cf16f2bda..68001652f 100644 --- a/all-is-cubes/src/block/builder.rs +++ b/all-is-cubes/src/block/builder.rs @@ -257,17 +257,29 @@ impl BlockBuilder { where P: BuildPrimitive, { - let primitive = self.primitive_builder.build_primitive(self.attributes); - let block = if matches!(primitive, Primitive::Air) && self.modifiers.is_empty() { + let Self { + attributes, + primitive_builder, + mut modifiers, + transaction, + } = self; + let primitive = primitive_builder.build_primitive(); + + if attributes != BlockAttributes::default() { + modifiers.insert(0, Modifier::Attributes(Arc::new(attributes))); + } + + let block = if matches!(primitive, Primitive::Air) && modifiers.is_empty() { // Avoid allocating an Arc. AIR } else { Block(BlockPtr::Owned(Arc::new(BlockParts { primitive, - modifiers: self.modifiers, + modifiers, }))) }; - (block, self.transaction) + + (block, transaction) } } @@ -370,9 +382,12 @@ impl From for BlockBuilder { pub struct NeedsPrimitive; /// Something that a parameterized [`BlockBuilder`] can use to construct a block's primitive. +/// +/// TODO: This is not currently necessary; we can replace the BuildPrimitive types with the +/// primitive itself. (But will that remain true?) #[doc(hidden)] pub trait BuildPrimitive { - fn build_primitive(self, attributes: BlockAttributes) -> Primitive; + fn build_primitive(self) -> Primitive; } /// Parameter type for [`BlockBuilder::color`], building [`Primitive::Atom`]. @@ -383,9 +398,8 @@ pub struct BlockBuilderAtom { collision: BlockCollision, } impl BuildPrimitive for BlockBuilderAtom { - fn build_primitive(self, attributes: BlockAttributes) -> Primitive { + fn build_primitive(self) -> Primitive { Primitive::Atom(Atom { - attributes, color: self.color, emission: self.emission, collision: self.collision, @@ -402,9 +416,8 @@ pub struct BlockBuilderVoxels { offset: GridPoint, } impl BuildPrimitive for BlockBuilderVoxels { - fn build_primitive(self, attributes: BlockAttributes) -> Primitive { + fn build_primitive(self) -> Primitive { Primitive::Recur { - attributes, offset: self.offset, resolution: self.resolution, space: self.space, @@ -418,7 +431,7 @@ mod tests { use crate::block::{self, Resolution::*, TickAction}; use crate::content::palette; - use crate::math::{Face6, Vol}; + use crate::math::{Face6, GridRotation, Vol}; use crate::op::Operation; use crate::space::SpacePhysics; use crate::transaction::Transactional as _; @@ -431,7 +444,6 @@ mod tests { assert_eq!( Block::builder().color(color).build(), Block::from(Atom { - attributes: BlockAttributes::default(), color, emission: Rgb::ZERO, collision: BlockCollision::Hard, @@ -467,21 +479,23 @@ mod tests { .tick_action(tick_action.clone()) .activation_action(activation_action.clone()) .animation_hint(AnimationHint::replacement(block::AnimationChange::Shape)) + .modifier(Modifier::Rotate(GridRotation::CLOCKWISE)) .build(), Block::from(Atom { - attributes: BlockAttributes { - display_name: "hello world".into(), - selectable: false, - inventory, - rotation_rule, - tick_action, - activation_action, - animation_hint: AnimationHint::replacement(block::AnimationChange::Shape), - }, color, emission, collision: BlockCollision::None, - }), + }) + .with_modifier(BlockAttributes { + display_name: "hello world".into(), + selectable: false, + inventory, + rotation_rule, + tick_action, + activation_action, + animation_hint: AnimationHint::replacement(block::AnimationChange::Shape), + }) + .with_modifier(Modifier::Rotate(GridRotation::CLOCKWISE)) ); } @@ -496,14 +510,14 @@ mod tests { .voxels_handle(R2, space_handle.clone()) .build(), Block::from_primitive(Primitive::Recur { - attributes: BlockAttributes { - display_name: "hello world".into(), - ..BlockAttributes::default() - }, offset: GridPoint::origin(), resolution: R2, // not same as space size space: space_handle - }), + }) + .with_modifier(Modifier::Attributes(Arc::new(BlockAttributes { + display_name: "hello world".into(), + ..BlockAttributes::default() + }))), ); } @@ -530,13 +544,13 @@ mod tests { assert_eq!( block, Block::from_primitive(Primitive::Recur { - attributes: BlockAttributes { - display_name: "hello world".into(), - ..BlockAttributes::default() - }, offset: GridPoint::origin(), resolution, space: space_handle.clone() + }) + .with_modifier(BlockAttributes { + display_name: "hello world".into(), + ..BlockAttributes::default() }), ); diff --git a/all-is-cubes/src/block/eval/evaluated.rs b/all-is-cubes/src/block/eval/evaluated.rs index 726c124f8..800bf2799 100644 --- a/all-is-cubes/src/block/eval/evaluated.rs +++ b/all-is-cubes/src/block/eval/evaluated.rs @@ -450,6 +450,11 @@ impl MinEval { &self.voxels } + pub(crate) fn set_attributes(&mut self, attributes: BlockAttributes) { + self.derived = None; + self.attributes = attributes; + } + #[cfg(test)] pub(crate) fn has_derived(&self) -> bool { self.derived.is_some() diff --git a/all-is-cubes/src/block/eval/tests.rs b/all-is-cubes/src/block/eval/tests.rs index 0ccc72fdc..42299e44d 100644 --- a/all-is-cubes/src/block/eval/tests.rs +++ b/all-is-cubes/src/block/eval/tests.rs @@ -77,9 +77,6 @@ fn evaluated_block_debug_complex() { EvaluatedBlock { block: Block { primitive: Recur { - attributes: BlockAttributes { - display_name: "hello", - }, space: Handle([anonymous #0]), offset: ( 0, @@ -88,6 +85,11 @@ fn evaluated_block_debug_complex() { ), resolution: 2, }, + modifiers: [ + BlockAttributes { + display_name: "hello", + }, + ], }, attributes: BlockAttributes { display_name: "hello", @@ -105,7 +107,7 @@ fn evaluated_block_debug_complex() { .. }, cost: Cost { - components: 1, + components: 2, voxels: 8, recursion: 0, }, diff --git a/all-is-cubes/src/block/modifier.rs b/all-is-cubes/src/block/modifier.rs index 3a8e9cce4..50534e790 100644 --- a/all-is-cubes/src/block/modifier.rs +++ b/all-is-cubes/src/block/modifier.rs @@ -1,10 +1,12 @@ -use crate::block::{self, Block, Evoxels, MinEval}; +use alloc::sync::Arc; +use alloc::vec::Vec; + +use crate::block::{self, Block, BlockAttributes, Evoxels, MinEval}; use crate::inv; use crate::math::{GridRotation, Vol}; use crate::universe::{HandleVisitor, VisitHandles}; mod composite; -use alloc::vec::Vec; pub use composite::*; mod r#move; pub use r#move::*; @@ -53,6 +55,12 @@ pub use zoom::*; #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] pub enum Modifier { + /// Sets or overrides the [attributes](BlockAttributes) of the block. + //--- + // Design note: Indirection is used here to keep `Modifier` small. + // `Arc` specifically is used so cloning does not allocate. + Attributes(Arc), + /// Suppresses all behaviors of the [`Block`] that might affect the space around it, /// (or itself). Quote(Quote), @@ -84,6 +92,7 @@ impl core::fmt::Debug for Modifier { // Print most modifiers’ data without the enum variant, because their struct names // are identifying enough. match self { + Self::Attributes(a) => a.fmt(f), Self::Quote(q) => q.fmt(f), Self::Rotate(r) => write!(f, "Rotate({r:?})"), Self::Composite(c) => c.fmt(f), @@ -108,12 +117,19 @@ impl Modifier { &self, block: &Block, this_modifier_index: usize, - value: MinEval, + mut value: MinEval, filter: &block::EvalFilter, ) -> Result { block::Budget::decrement_components(&filter.budget)?; Ok(match *self { + // TODO: Eventually, we want to be able to override individual attributes. + // We will need a new schema (possibly a set of individual modifiers) for that. + Modifier::Attributes(ref attributes) => { + value.set_attributes(BlockAttributes::clone(attributes)); + value + } + Modifier::Quote(ref quote) => quote.evaluate(value, filter)?, Modifier::Rotate(rotation) => { @@ -169,6 +185,8 @@ impl Modifier { pub(crate) fn unspecialize(&self, block: &Block) -> ModifierUnspecialize { // When modifying this match, update the public documentation of `Block::unspecialize` too. match self { + Modifier::Attributes(_) => ModifierUnspecialize::Keep, + Modifier::Quote(_) => ModifierUnspecialize::Keep, Modifier::Rotate(_) => ModifierUnspecialize::Pop, @@ -203,6 +221,7 @@ impl Modifier { /// had none”. pub(crate) fn does_not_introduce_asymmetry(&self) -> bool { match self { + Modifier::Attributes(attr) => attr.rotationally_symmetric(), // Quote has no asymmetry Modifier::Quote(_) => true, // Rotate may change existing asymmetry but does not introduce it @@ -223,6 +242,7 @@ impl Modifier { impl VisitHandles for Modifier { fn visit_handles(&self, visitor: &mut dyn HandleVisitor) { match self { + Modifier::Attributes(a) => a.visit_handles(visitor), Modifier::Quote(m) => m.visit_handles(visitor), Modifier::Rotate(_) => {} Modifier::Composite(m) => m.visit_handles(visitor), @@ -273,6 +293,11 @@ mod tests { #[test] fn modifier_debug() { let modifiers: Vec = vec![ + BlockAttributes { + display_name: arcstr::literal!("hello"), + ..Default::default() + } + .into(), Modifier::Quote(Quote::new()), Modifier::Rotate(GridRotation::RXyZ), Modifier::Composite(Composite::new(block::AIR, CompositeOperator::Over)), @@ -281,7 +306,10 @@ mod tests { assert_eq!( format!("{modifiers:#?}"), indoc::indoc! { - r"[ + r#"[ + BlockAttributes { + display_name: "hello", + }, Quote { suppress_ambient: false, }, @@ -297,7 +325,7 @@ mod tests { Inventory { slots: [], }, - ]" + ]"# } ); } @@ -365,7 +393,7 @@ mod tests { ..BlockAttributes::default() }, cost: block::Cost { - components: 2, + components: 3, // Primitive + display_name + Rotate voxels: 2u32.pow(3) * 2, // original + rotation recursion: 0 }, diff --git a/all-is-cubes/src/block/modifier/move.rs b/all-is-cubes/src/block/modifier/move.rs index d8e43c0d2..0f2614694 100644 --- a/all-is-cubes/src/block/modifier/move.rs +++ b/all-is-cubes/src/block/modifier/move.rs @@ -1,10 +1,8 @@ -use all_is_cubes_base::math::GridRotation; - use crate::block::TickAction; use crate::block::{ self, Block, BlockAttributes, Evoxel, Evoxels, MinEval, Modifier, Resolution::R16, AIR, }; -use crate::math::{Face6, GridAab, GridCoordinate, GridVector, Vol}; +use crate::math::{Face6, GridAab, GridCoordinate, GridRotation, GridVector, Vol}; use crate::op::Operation; use crate::time; use crate::universe; diff --git a/all-is-cubes/src/block/modifier/zoom.rs b/all-is-cubes/src/block/modifier/zoom.rs index 7c46c5c19..3d62336ea 100644 --- a/all-is-cubes/src/block/modifier/zoom.rs +++ b/all-is-cubes/src/block/modifier/zoom.rs @@ -216,7 +216,7 @@ mod tests { ev_original.attributes.clone(), Evoxels::from_one(Evoxel::from_color(Rgba::TRANSPARENT)), block::Cost { - components: 2, + components: 3, // Primitive + display_name + Zoom voxels: 16u32.pow(3), // counts evaluation of Recur recursion: 0, }, @@ -236,7 +236,7 @@ mod tests { }), ), block::Cost { - components: 2, + components: 3, voxels: 16u32.pow(3) + 8u32.pow(3), // Recur + Zoom recursion: 0, }, diff --git a/all-is-cubes/src/block/tests.rs b/all-is-cubes/src/block/tests.rs index 3f3ad78e0..581c149d1 100644 --- a/all-is-cubes/src/block/tests.rs +++ b/all-is-cubes/src/block/tests.rs @@ -128,19 +128,13 @@ mod eval { #[test] fn opaque_atom_and_attributes() { let color = Rgba::new(1.0, 2.0, 3.0, 1.0); - let attributes = BlockAttributes { - display_name: arcstr::literal!("hello world"), - selectable: false, - ..BlockAttributes::default() - }; let block = Block::from(Atom { - attributes: attributes.clone(), color, emission: Rgb::ONE, collision: BlockCollision::None, }); let e = block.evaluate().unwrap(); - assert_eq!(e.attributes, attributes); + assert_eq!(e.attributes(), BlockAttributes::DEFAULT_REF); assert_eq!(e.color(), color); assert_eq!(e.face_colors(), FaceMap::repeat(color)); assert_eq!(e.light_emission(), Rgb::ONE); @@ -149,7 +143,7 @@ mod eval { Evoxels::from_one(Evoxel { color, emission: Rgb::ONE, - selectable: false, + selectable: true, collision: BlockCollision::None, }) ); @@ -274,7 +268,7 @@ mod eval { assert_eq!( e.cost, Cost { - components: 1, + components: 2, voxels: 8, recursion: 0 } @@ -473,7 +467,6 @@ mod eval { .unwrap(); let space_handle = universe.insert_anonymous(space); let block_at_offset = Block::from_primitive(Primitive::Recur { - attributes: BlockAttributes::default(), offset: offset.to_point(), resolution, space: space_handle.clone(), @@ -507,7 +500,6 @@ mod eval { )) .build(); let block_at_offset = Block::from_primitive(Primitive::Recur { - attributes: BlockAttributes::default(), offset: GridPoint::new(-414232629, -2147483648, -13697025), resolution: R128, space: universe.insert_anonymous(space), diff --git a/all-is-cubes/src/content.rs b/all-is-cubes/src/content.rs index a14a046b0..e26ff41f0 100644 --- a/all-is-cubes/src/content.rs +++ b/all-is-cubes/src/content.rs @@ -270,13 +270,13 @@ mod tests { assert_eq!( make_some_blocks::<1>(), [Block::from(Atom { - attributes: BlockAttributes { - display_name: "0".into(), - ..BlockAttributes::default() - }, color: Rgba::new(0.5, 0.5, 0.5, 1.0), emission: Rgb::ZERO, collision: BlockCollision::Hard, + }) + .with_modifier(BlockAttributes { + display_name: "0".into(), + ..BlockAttributes::default() })] ); } @@ -287,23 +287,23 @@ mod tests { make_some_blocks::<2>(), [ Block::from(Atom { - attributes: BlockAttributes { - display_name: "0".into(), - ..BlockAttributes::default() - }, color: Rgba::new(0.0, 0.0, 0.0, 1.0), emission: Rgb::ZERO, collision: BlockCollision::Hard, + }) + .with_modifier(BlockAttributes { + display_name: "0".into(), + ..BlockAttributes::default() }), Block::from(Atom { - attributes: BlockAttributes { - display_name: "1".into(), - ..BlockAttributes::default() - }, color: Rgba::new(1.0, 1.0, 1.0, 1.0), emission: Rgb::ZERO, collision: BlockCollision::Hard, }) + .with_modifier(BlockAttributes { + display_name: "1".into(), + ..BlockAttributes::default() + }) ] ); } diff --git a/all-is-cubes/src/inv/tool.rs b/all-is-cubes/src/inv/tool.rs index 14ac27290..68d3a776f 100644 --- a/all-is-cubes/src/inv/tool.rs +++ b/all-is-cubes/src/inv/tool.rs @@ -870,14 +870,12 @@ mod tests { // Make a block with a rotation rule let [mut tool_block] = make_some_voxel_blocks(&mut tester.universe); - if let Primitive::Recur { - ref mut attributes, .. - } = tool_block.primitive_mut() - { - attributes.rotation_rule = RotationPlacementRule::Attach { by: Face6::NZ }; - } else { - unreachable!(); - } + tool_block + .modifiers_mut() + .push(block::Modifier::from(block::BlockAttributes { + rotation_rule: RotationPlacementRule::Attach { by: Face6::NZ }, + ..block::BlockAttributes::default() + })); // TODO: For more thorough testing, we need to be able to control ToolTester's choice of ray let transaction = tester diff --git a/all-is-cubes/src/save/conversion.rs b/all-is-cubes/src/save/conversion.rs index 3cebf4f85..9c6bf5898 100644 --- a/all-is-cubes/src/save/conversion.rs +++ b/all-is-cubes/src/save/conversion.rs @@ -102,10 +102,18 @@ mod block { primitive, modifiers, } => { - let mut block = Block::from_primitive(primitive.into()); + let (primitive, attributes) = primitive_from_schema(primitive); + let mut block = Block::from_primitive(primitive); + + let attr_mod_iter = attributes + .filter(|a| a != BlockAttributes::DEFAULT_REF) + .map(Modifier::from) + .into_iter(); + let general_mod_iter = modifiers.into_iter().map(Modifier::from); + block .modifiers_mut() - .extend(modifiers.into_iter().map(Modifier::from)); + .extend(attr_mod_iter.chain(general_mod_iter)); block } }) @@ -119,23 +127,25 @@ mod block { definition: definition.clone(), }, &Primitive::Atom(Atom { - ref attributes, color, emission, collision, }) => schema::PrimitiveSer::AtomV1 { + // attributes on the primitive are no longer used, but still supported + // by deserialization. + attributes: BlockAttributes::DEFAULT_REF.into(), color: color.into(), light_emission: emission.into(), - attributes: attributes.into(), collision: collision.into(), }, &Primitive::Recur { - ref attributes, ref space, offset, resolution, } => schema::PrimitiveSer::RecurV1 { - attributes: attributes.into(), + // attributes on the primitive are no longer used, but still supported + // by deserialization. + attributes: BlockAttributes::DEFAULT_REF.into(), space: space.clone(), offset: offset.into(), resolution, @@ -149,38 +159,47 @@ mod block { } } - impl<'a> From> for Primitive { - fn from(value: schema::PrimitiveSer<'a>) -> Self { - match value { - schema::PrimitiveSer::IndirectV1 { definition } => Primitive::Indirect(definition), - schema::PrimitiveSer::AtomV1 { - attributes, - color, - light_emission: emission, - collision, - } => Primitive::Atom(Atom { - attributes: BlockAttributes::from(attributes), + fn primitive_from_schema( + value: schema::PrimitiveSer<'_>, + ) -> (Primitive, Option) { + match value { + schema::PrimitiveSer::IndirectV1 { definition } => { + (Primitive::Indirect(definition), None) + } + schema::PrimitiveSer::AtomV1 { + attributes, + color, + light_emission: emission, + collision, + } => ( + Primitive::Atom(Atom { color: Rgba::from(color), emission: Rgb::from(emission), collision: collision.into(), }), - schema::PrimitiveSer::RecurV1 { - attributes, - space, - offset, - resolution, - } => Primitive::Recur { - attributes: attributes.into(), + Some(attributes.into()), + ), + schema::PrimitiveSer::RecurV1 { + attributes, + space, + offset, + resolution, + } => ( + Primitive::Recur { space, offset: offset.into(), resolution, }, - schema::PrimitiveSer::AirV1 => Primitive::Air, - schema::PrimitiveSer::TextPrimitiveV1 { text, offset } => Primitive::Text { + Some(attributes.into()), + ), + schema::PrimitiveSer::AirV1 => (Primitive::Air, None), + schema::PrimitiveSer::TextPrimitiveV1 { text, offset } => ( + Primitive::Text { text: text.into(), offset: offset.into(), }, - } + None, + ), } } @@ -341,6 +360,9 @@ mod block { impl<'a> From<&'a Modifier> for ModifierSer<'a> { fn from(value: &'a Modifier) -> Self { match *value { + Modifier::Attributes(ref attributes) => ModifierSer::AttributesV1 { + attributes: (&**attributes).into(), + }, Modifier::Quote(Quote { suppress_ambient }) => { ModifierSer::QuoteV1 { suppress_ambient } } @@ -368,6 +390,9 @@ mod block { impl From> for Modifier { fn from(value: ModifierSer<'_>) -> Self { match value { + ModifierSer::AttributesV1 { attributes } => { + Modifier::Attributes(Arc::new(attributes.into())) + } ModifierSer::QuoteV1 { suppress_ambient } => { Modifier::Quote(Quote { suppress_ambient }) } diff --git a/all-is-cubes/src/save/schema.rs b/all-is-cubes/src/save/schema.rs index 9c8f45414..61a956516 100644 --- a/all-is-cubes/src/save/schema.rs +++ b/all-is-cubes/src/save/schema.rs @@ -79,12 +79,14 @@ pub(crate) enum PrimitiveSer<'a> { color: RgbaSer, #[serde(default, skip_serializing_if = "is_default")] light_emission: RgbSer, + /// Note: Attributes stored on the primitive are no longer used, and supported only for deserialization. #[serde(flatten)] attributes: BlockAttributesV1Ser<'a>, #[serde(default, skip_serializing_if = "is_default")] collision: BlockCollisionSer, }, RecurV1 { + /// Note: Attributes stored on the primitive are no longer used, and supported only for deserialization. #[serde(flatten)] attributes: BlockAttributesV1Ser<'a>, space: Handle, @@ -101,7 +103,7 @@ pub(crate) enum PrimitiveSer<'a> { }, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct BlockAttributesV1Ser<'a> { #[serde(default, skip_serializing_if = "str::is_empty")] pub display_name: ArcStr, @@ -136,7 +138,7 @@ pub(crate) enum BlockCollisionSer { NoneV1, } -#[derive(Debug, Default, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] #[serde(tag = "type")] pub(crate) enum RotationPlacementRuleSer { #[default] @@ -147,7 +149,7 @@ pub(crate) enum RotationPlacementRuleSer { } /// Unversioned because it's versioned by the parent struct -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct TickActionSer<'a> { pub operation: Cow<'a, op::Operation>, pub schedule: Schedule, @@ -179,6 +181,10 @@ pub(crate) enum AnimationChangeV1Ser { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub(crate) enum ModifierSer<'a> { + AttributesV1 { + #[serde(flatten)] + attributes: BlockAttributesV1Ser<'a>, + }, QuoteV1 { suppress_ambient: bool, }, @@ -333,7 +339,7 @@ pub(crate) enum ToolSer { CustomV1 { op: op::Operation, icon: Block }, } -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[serde(tag = "type")] pub(crate) enum InvInBlockSer { InvInBlockV1 { diff --git a/all-is-cubes/src/save/tests.rs b/all-is-cubes/src/save/tests.rs index be8a4965e..a0e345730 100644 --- a/all-is-cubes/src/save/tests.rs +++ b/all-is-cubes/src/save/tests.rs @@ -189,59 +189,64 @@ fn block_atom_with_all_attributes() { "primitive": { "type": "AtomV1", "color": [1.0, 0.5, 0.0, 0.5], + "light_emission": [1.0, 0.0, 10.0], "collision": "NoneV1", - "display_name": "foo", - "selectable": false, - "inventory": { - "type": "InvInBlockV1", - "size": 1, - "icon_scale": 4, - "icon_resolution": 16, - "icon_rows": [ - { - "count": 3, - "first_slot": 0, - "origin": [1, 1, 1], - "stride": [5, 0, 0], - }, - { - "count": 3, - "first_slot": 3, - "origin": [1, 1, 6], - "stride": [5, 0, 0], - }, - { - "count": 3, - "first_slot": 6, - "origin": [1, 1, 11], - "stride": [5, 0, 0], - }, - ], - }, - "rotation_rule": { - "type": "AttachV1", - "by": "PX", - }, - "tick_action": { - "schedule": { - "type": "ScheduleV1", - "period": 3, + }, + "modifiers": [ + { + "type": "AttributesV1", + "display_name": "foo", + "selectable": false, + "inventory": { + "type": "InvInBlockV1", + "size": 1, + "icon_scale": 4, + "icon_resolution": 16, + "icon_rows": [ + { + "count": 3, + "first_slot": 0, + "origin": [1, 1, 1], + "stride": [5, 0, 0], + }, + { + "count": 3, + "first_slot": 3, + "origin": [1, 1, 6], + "stride": [5, 0, 0], + }, + { + "count": 3, + "first_slot": 6, + "origin": [1, 1, 11], + "stride": [5, 0, 0], + }, + ], + }, + "rotation_rule": { + "type": "AttachV1", + "by": "PX", }, - "operation": { - "type": "BecomeV1", - "block": { - "type": "BlockV1", - "primitive": {"type": "AirV1"}, + "tick_action": { + "schedule": { + "type": "ScheduleV1", + "period": 3, + }, + "operation": { + "type": "BecomeV1", + "block": { + "type": "BlockV1", + "primitive": {"type": "AirV1"}, + } } - } - }, - "light_emission": [1.0, 0.0, 10.0], - "animation_hint": { - "type": "AnimationHintV1", - "redefinition": "ColorSameCategory", - "replacement": "Shape", - }, - }, + }, + "animation_hint": { + "type": "AnimationHintV1", + "redefinition": "ColorSameCategory", + "replacement": "Shape", + }, + } + ] }), ); } @@ -674,9 +679,14 @@ fn space_success() { "type": "BlockV1", "primitive": { "type": "AtomV1", - "color": [0.5, 0.5, 0.5, 1.0], - "display_name": "0", + "color": [0.5, 0.5, 0.5, 1.0] }, + "modifiers": [ + { + "type": "AttributesV1", + "display_name": "0" + } + ] }, ], "contents": space_contents_json([ @@ -837,8 +847,13 @@ fn universe_with_one_of_each_json() -> serde_json::Value { "primitive": { "type": "AtomV1", "color": [0.5, 0.5, 0.5, 1.0], - "display_name": "0", - } + }, + "modifiers": [ + { + "type": "AttributesV1", + "display_name": "0", + } + ] } }, { diff --git a/all-is-cubes/src/space/tests.rs b/all-is-cubes/src/space/tests.rs index bbb7129e9..c2c48093d 100644 --- a/all-is-cubes/src/space/tests.rs +++ b/all-is-cubes/src/space/tests.rs @@ -10,9 +10,7 @@ use core::num::NonZeroU16; use euclid::Vector3D; use indoc::indoc; -use crate::block::{ - self, Atom, Block, BlockDef, BlockDefTransaction, Primitive, Resolution::*, TickAction, AIR, -}; +use crate::block::{self, Block, BlockDef, BlockDefTransaction, Resolution::*, TickAction, AIR}; use crate::color_block; use crate::content::make_some_blocks; use crate::fluff::{self, Fluff}; @@ -606,14 +604,10 @@ fn block_tick_action_timing() { // Hook them up to turn into each other fn connect(from: &mut Block, to: &Block) { - if let Primitive::Atom(Atom { attributes, .. }) = from.primitive_mut() { - attributes.tick_action = Some(TickAction { - operation: Operation::Become(to.clone()), - schedule: time::Schedule::from_period(NonZeroU16::new(2).unwrap()), - }); - } else { - panic!(); - } + from.freezing_get_attributes_mut().tick_action = Some(TickAction { + operation: Operation::Become(to.clone()), + schedule: time::Schedule::from_period(NonZeroU16::new(2).unwrap()), + }); } connect(&mut block2, &block3); connect(&mut block1, &block2); @@ -656,24 +650,18 @@ fn block_tick_action_conflict() { // Create an active block. let [mut modifies_px_neighbor, output1, mut modifies_nx_neighbor, output2] = make_some_blocks(); fn connect(from: &mut Block, to: &Block, face: Face6) { - if let Primitive::Atom(Atom { attributes, .. }) = from.primitive_mut() { - attributes.tick_action = Some({ - TickAction { - // TODO: replace this with a better-behaved neighbor-modifying operation, - // once we have one - operation: Operation::Neighbors( - [( - Cube::from(face.normal_vector().to_point()), - Operation::Become(to.clone()), - )] - .into(), - ), - schedule: time::Schedule::from_period(NonZeroU16::new(1).unwrap()), - } - }); - } else { - panic!(); - } + from.freezing_get_attributes_mut().tick_action = Some(TickAction { + // TODO: replace this with a better-behaved neighbor-modifying operation, + // once we have one + operation: Operation::Neighbors( + [( + Cube::from(face.normal_vector().to_point()), + Operation::Become(to.clone()), + )] + .into(), + ), + schedule: time::Schedule::from_period(NonZeroU16::new(1).unwrap()), + }); } connect(&mut modifies_px_neighbor, &output1, Face6::PX); connect(&mut modifies_nx_neighbor, &output2, Face6::NX); diff --git a/all-is-cubes/src/universe/universe_txn.rs b/all-is-cubes/src/universe/universe_txn.rs index f7c78ad83..653a4e5ad 100644 --- a/all-is-cubes/src/universe/universe_txn.rs +++ b/all-is-cubes/src/universe/universe_txn.rs @@ -1037,7 +1037,7 @@ mod tests { println!("{transaction:#?}"); pretty_assertions::assert_str_eq!( format!("{transaction:#?}\n"), - indoc! {" + indoc! {r#" UniverseTransaction { [anonymous #0]: SpaceTransaction { (+0, +0, +0): CubeTransaction { @@ -1045,12 +1045,14 @@ mod tests { new: Some( Block { primitive: Atom { - attributes: BlockAttributes { - display_name: \"0\", - }, color: Rgba(0.5, 0.5, 0.5, 1.0), collision: Hard, }, + modifiers: [ + BlockAttributes { + display_name: "0", + }, + ], }, ), conserved: true, @@ -1076,7 +1078,7 @@ mod tests { ], }, } - "} + "#} .to_string() ); }