From 5260c82c4aa500c85ec0c2e956bb9a486e4df11b Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 19 Jan 2024 18:05:56 +0000 Subject: [PATCH] Overhauled sprite representation to support many more sprites and attributes --- common/examples/sprite_debug.rs | 22 ++ common/src/terrain/block.rs | 174 ++++++++---- common/src/terrain/sprite.rs | 436 +++++++++++++++-------------- common/src/terrain/sprite/magic.rs | 208 ++++++++++++++ voxygen/src/scene/terrain.rs | 12 +- 5 files changed, 584 insertions(+), 268 deletions(-) create mode 100644 common/examples/sprite_debug.rs create mode 100644 common/src/terrain/sprite/magic.rs diff --git a/common/examples/sprite_debug.rs b/common/examples/sprite_debug.rs new file mode 100644 index 0000000000..1e8d141196 --- /dev/null +++ b/common/examples/sprite_debug.rs @@ -0,0 +1,22 @@ +use veloren_common::terrain::sprite::{Attributes, Category, SpriteKind}; + +fn main() { + for cat in Category::all() { + println!( + "Category::{cat:?} (value = 0x{:02X}, sprite_id_mask: {:032b}, sprite_id_size: {})", + *cat as u16, + cat.sprite_id_mask(), + cat.sprite_id_size() + ); + for attr in Attributes::all() { + println!( + " - {attr:?} offset = {:?}", + cat.attr_offsets()[*attr as usize] + ); + } + } + + for sprite in SpriteKind::all() { + println!("SpriteKind::{sprite:?} (value = 0x{:04X})", *sprite as u16); + } +} diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 8e76e336b7..6f5eeb8996 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -1,4 +1,4 @@ -use super::SpriteKind; +use super::{sprite, SpriteKind}; use crate::{ comp::{fluid_dynamics::LiquidKind, tool::ToolKind}, consts::FRIC_GROUND, @@ -114,10 +114,29 @@ impl BlockKind { } } +/// # Format +/// +/// ``` +/// BBBBBBBB CCCCCCCC AAAAAIII IIIIIIII +/// ``` +/// - `0..8` : BlockKind +/// - `8..16` : Category +/// - `16..N` : Attributes (many fields) +/// - `N..32` : Sprite ID +/// +/// `N` is per-category. You can match on the category byte to find the length +/// of the ID field. +/// +/// Attributes are also per-category. Each category specifies its own list of +/// attribute fields. +/// +/// Why is the sprite ID at the end? Simply put, it makes masking faster and +/// easier, which is important because extracting the `SpriteKind` is a more +/// commonly performed operation than extracting attributes. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct Block { kind: BlockKind, - attr: [u8; 3], + data: [u8; 3], } impl FilledVox for Block { @@ -135,12 +154,21 @@ impl Deref for Block { impl Block { pub const MAX_HEIGHT: f32 = 3.0; + /* Constructors */ + #[inline] pub const fn new(kind: BlockKind, color: Rgb) -> Self { + // TODO: we should probably assert this, overwriting the data fields with a + // colour is bad news + /* + #[cfg(debug_assertions)] + assert!(kind.is_filled()); + */ + Self { kind, // Colours are only valid for non-fluids - attr: if kind.is_filled() { + data: if kind.is_filled() { [color.r, color.g, color.b] } else { [0; 3] @@ -148,61 +176,117 @@ impl Block { } } + // Only valid if `block_kind` is unfilled, so this is just a private utility + // method #[inline] - pub const fn air(sprite: SpriteKind) -> Self { + const fn unfilled(kind: BlockKind, sprite: SpriteKind) -> Self { + #[cfg(debug_assertions)] + assert!(!kind.is_filled()); + + let sprite_bytes = (sprite as u32).to_be_bytes(); + Self { - kind: BlockKind::Air, - attr: [sprite as u8, 0, 0], + kind, + data: [sprite_bytes[1], sprite_bytes[2], sprite_bytes[3]], } } + #[inline] + pub const fn air(sprite: SpriteKind) -> Self { Self::unfilled(BlockKind::Air, sprite) } + #[inline] pub const fn lava(sprite: SpriteKind) -> Self { - Self { - kind: BlockKind::Lava, - attr: [sprite as u8, 0, 0], - } + // TODO: Is this valid? I don't think so, lava is filled. Debug panic will catch + // it if not though. + Self::unfilled(BlockKind::Lava, sprite) } #[inline] pub const fn empty() -> Self { Self::air(SpriteKind::Empty) } - /// TODO: See if we can generalize this somehow. #[inline] - pub const fn water(sprite: SpriteKind) -> Self { - Self { - kind: BlockKind::Water, - attr: [sprite as u8, 0, 0], + pub const fn water(sprite: SpriteKind) -> Self { Self::unfilled(BlockKind::Water, sprite) } + + /* Sprite decoding */ + + #[inline(always)] + pub const fn get_sprite(&self) -> Option { + if !self.kind.is_filled() { + SpriteKind::from_block(*self) + } else { + None } } + #[inline(always)] + pub(super) const fn sprite_category_byte(&self) -> u8 { self.data[0] } + + #[inline(always)] + pub const fn sprite_category(&self) -> Option { + if self.kind.is_filled() { + None + } else { + sprite::Category::from_block(*self) + } + } + + /// Build this block with the given sprite attribute set. + #[inline] + pub fn with_attr( + mut self, + attr: A, + ) -> Result> { + match self.sprite_category() { + Some(category) => category.write_attr(&mut self, attr)?, + None => return Err(sprite::AttributeError::NotPresent), + } + Ok(self) + } + + /// Set the given attribute of this block's sprite. + #[inline] + pub fn set_attr( + &mut self, + attr: A, + ) -> Result<(), sprite::AttributeError> { + match self.sprite_category() { + Some(category) => category.write_attr(self, attr), + None => Err(sprite::AttributeError::NotPresent), + } + } + + /// Get the given attribute of this block's sprite. + #[inline] + pub fn get_attr(&self) -> Result> { + match self.sprite_category() { + Some(category) => category.read_attr(*self), + None => return Err(sprite::AttributeError::NotPresent), + } + } + + #[inline(always)] + pub(super) const fn with_data(mut self, data: [u8; 3]) -> Self { + self.data = data; + self + } + + #[inline(always)] + pub(super) const fn to_be_u32(&self) -> u32 { + u32::from_be_bytes([self.kind as u8, self.data[0], self.data[1], self.data[2]]) + } + #[inline] pub fn get_color(&self) -> Option> { if self.has_color() { - Some(self.attr.into()) + Some(self.data.into()) } else { None } } + // TODO: phase out use of this method in favour of `block.get_attr::()` #[inline] - pub fn get_sprite(&self) -> Option { - if !self.is_filled() { - SpriteKind::from_u8(self.attr[0]) - } else { - None - } - } - - #[inline] - pub fn get_ori(&self) -> Option { - if self.get_sprite()?.has_ori() { - // TODO: Formalise this a bit better - Some(self.attr[1] & 0b111) - } else { - None - } - } + pub fn get_ori(&self) -> Option { self.get_attr::().ok().map(|ori| ori.0) } /// Returns the rtsim resource, if any, that this block corresponds to. If /// you want the scarcity of a block to change with rtsim's resource @@ -547,7 +631,7 @@ impl Block { #[must_use] pub fn with_sprite(mut self, sprite: SpriteKind) -> Self { if !self.is_filled() { - self.attr[0] = sprite as u8; + self = Self::unfilled(self.kind, sprite); } self } @@ -555,14 +639,7 @@ impl Block { /// If this block can have orientation, give it a new orientation. #[inline] #[must_use] - pub fn with_ori(mut self, ori: u8) -> Option { - if self.get_sprite().map(|s| s.has_ori()).unwrap_or(false) { - self.attr[1] = (self.attr[1] & !0b111) | (ori & 0b111); - Some(self) - } else { - None - } - } + pub fn with_ori(self, ori: u8) -> Option { self.with_attr(sprite::Ori(ori)).ok() } /// Remove the terrain sprite or solid aspects of a block #[inline] @@ -584,27 +661,24 @@ impl Block { let [bk, r, g, b] = x.to_le_bytes(); Some(Self { kind: BlockKind::from_u8(bk)?, - attr: [r, g, b], + data: [r, g, b], }) } #[inline] pub fn to_u32(&self) -> u32 { - u32::from_le_bytes([self.kind as u8, self.attr[0], self.attr[1], self.attr[2]]) + u32::from_le_bytes([self.kind as u8, self.data[0], self.data[1], self.data[2]]) } } +const _: () = assert!(core::mem::size_of::() == 1); +const _: () = assert!(core::mem::size_of::() == 4); + #[cfg(test)] mod tests { use super::*; use strum::IntoEnumIterator; - #[test] - fn block_size() { - assert_eq!(std::mem::size_of::(), 1); - assert_eq!(std::mem::size_of::(), 4); - } - #[test] fn convert_u32() { for bk in BlockKind::iter() { diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 95b9f52d66..896a5b5998 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -1,10 +1,50 @@ +//! Here's the deal. +//! +//! Blocks are always 4 bytes. The first byte is the [`BlockKind`]. For filled +//! blocks, the remaining 3 sprites are the block colour. For unfilled sprites +//! (air, water, etc.) the remaining 3 bytes correspond to sprite data. That's +//! not a lot to work with! As a result, we're pulling every rabbit out of the +//! bit-twiddling hat to squash as much information as possible into those 3 +//! bytes. +//! +//! Fundamentally, sprites are composed of one or more elements: the +//! [`SpriteKind`], which tells us what the sprite *is*, and a list of +//! attributes that define extra properties that the sprite has. Some examples +//! of attributes might include: +//! +//! - the orientation of the sprite (with respect to the volume it sits within) +//! - whether the sprite has snow cover on it +//! - a 'variation seed' that allows frontends to pseudorandomly customise the +//! appearance of the sprite in a manner that's consistent across clients +//! - Whether doors are open, closed, or permanently locked +//! - The stage of growth of a plant +//! - The kind of plant that sits in pots/planters/vessels +//! - The colour of the sprite +//! - The material of the sprite +//! +//! # Category +//! +//! The first of the three bytes is the sprite 'category'. As much as possible, +//! we should try to have the properties of each sprite within a category be +//! consistent with others in the category, to improve performance. +//! +//! Since a single byte is not enough to disambiguate the [`SpriteKind`] (we +//! have more than 256 kinds, so there's not enough space), the category also +//! corresponds to a 'kind mask': a bitmask that, when applied to the first two +//! of the three bytes gives us the [`SpriteKind`]. + +mod magic; + +pub use self::magic::{Attribute, AttributeError}; + use crate::{ + attributes, comp::{ item::{ItemDefinitionId, ItemDefinitionIdOwned}, tool::ToolKind, }, lottery::LootSpec, - make_case_elim, + make_case_elim, sprites, terrain::Block, }; use common_i18n::Content; @@ -16,37 +56,18 @@ use std::{convert::TryFrom, fmt}; use strum::EnumIter; use vek::*; -make_case_elim!( - sprite_kind, - #[derive( - Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, EnumIter, FromPrimitive, - )] - #[repr(u8)] - pub enum SpriteKind { - // Note that the values of these should be linearly contiguous to allow for quick - // bounds-checking when casting to a u8. - Empty = 0x00, - BarrelCactus = 0x01, - RoundCactus = 0x02, - ShortCactus = 0x03, - MedFlatCactus = 0x04, - ShortFlatCactus = 0x05, - BlueFlower = 0x06, - PinkFlower = 0x07, - PurpleFlower = 0x08, - RedFlower = 0x09, - WhiteFlower = 0x0A, - YellowFlower = 0x0B, - Sunflower = 0x0C, - LongGrass = 0x0D, - MediumGrass = 0x0E, - ShortGrass = 0x0F, +sprites! { + Void = 0 { + Empty = 0, + }, + // Generic collection of sprites, no attributes but anything goes + // Also used as a 'dumping ground' for old-style sprites without orientation until we recategorise them. + Misc = 1 { Apple = 0x10, Mushroom = 0x11, Liana = 0x12, Velorite = 0x13, VeloriteFrag = 0x14, - Chest = 0x15, Pumpkin = 0x16, Welwitch = 0x17, LingonBerry = 0x18, @@ -65,52 +86,19 @@ make_case_elim!( Radish = 0x25, Coconut = 0x26, Turnip = 0x27, - Window1 = 0x28, - Window2 = 0x29, - Window3 = 0x2A, - Window4 = 0x2B, Scarecrow = 0x2C, StreetLamp = 0x2D, StreetLampTall = 0x2E, - Door = 0x2F, - Bed = 0x30, - Bench = 0x31, - ChairSingle = 0x32, - ChairDouble = 0x33, - CoatRack = 0x34, - Crate = 0x35, - DrawerLarge = 0x36, - DrawerMedium = 0x37, - DrawerSmall = 0x38, - DungeonWallDecor = 0x39, - HangingBasket = 0x3A, - HangingSign = 0x3B, - WallLamp = 0x3C, - Planter = 0x3D, - Shelf = 0x3E, - TableSide = 0x3F, - TableDining = 0x40, - TableDouble = 0x41, - WardrobeSingle = 0x42, - WardrobeDouble = 0x43, LargeGrass = 0x44, - Pot = 0x45, Stones = 0x46, Twigs = 0x47, - DropGate = 0x48, - DropGateBottom = 0x49, GrassSnow = 0x4A, Reed = 0x4B, - Beehive = 0x4C, LargeCactus = 0x4D, - VialEmpty = 0x4E, - PotionMinor = 0x4F, GrassBlue = 0x50, ChestBuried = 0x51, Mud = 0x52, - FireBowlGround = 0x53, CaveMushroom = 0x54, - Bowl = 0x55, SavannaGrass = 0x56, TallSavannaGrass = 0x57, RedSavannaGrass = 0x58, @@ -127,8 +115,6 @@ make_case_elim!( RubySmall = 0x63, EmeraldSmall = 0x64, SapphireSmall = 0x65, - WallLampSmall = 0x66, - WallSconce = 0x67, StonyCoral = 0x68, SoftCoral = 0x69, SeaweedTemperate = 0x6A, @@ -143,20 +129,6 @@ make_case_elim!( Seagrass = 0x73, RedAlgae = 0x74, UnderwaterVent = 0x75, - Lantern = 0x76, - CraftingBench = 0x77, - Forge = 0x78, - Cauldron = 0x79, - Anvil = 0x7A, - CookingPot = 0x7B, - DungeonChest0 = 0x7C, - DungeonChest1 = 0x7D, - DungeonChest2 = 0x7E, - DungeonChest3 = 0x7F, - DungeonChest4 = 0x80, - DungeonChest5 = 0x81, - Loom = 0x82, - SpinningWheel = 0x83, CrystalHigh = 0x84, Bloodstone = 0x85, Coal = 0x86, @@ -169,7 +141,6 @@ make_case_elim!( Cotton = 0x8D, Moonbell = 0x8E, Pyrebloom = 0x8F, - TanningRack = 0x90, WildFlax = 0x91, CrystalLow = 0x92, CeilingMushroom = 0x93, @@ -183,32 +154,17 @@ make_case_elim!( CavernGrassBlueLong = 0x9B, CavernLillypadBlue = 0x9C, CavernMycelBlue = 0x9D, - DismantlingBench = 0x9E, JungleFern = 0x9F, LillyPads = 0xA0, JungleLeafyPlant = 0xA1, JungleRedGrass = 0xA2, Bomb = 0xA3, - ChristmasOrnament = 0xA4, - ChristmasWreath = 0xA5, EnsnaringWeb = 0xA6, - WindowArabic = 0xA7, - MelonCut = 0xA8, - BookshelfArabic = 0xA9, DecorSetArabic = 0xAA, SepareArabic = 0xAB, CushionArabic = 0xAC, - JugArabic = 0xAD, TableArabicSmall = 0xAE, - TableArabicLarge = 0xAF, - CanapeArabic = 0xB0, - CupboardArabic = 0xB1, - WallTableArabic = 0xB2, - JugAndBowlArabic = 0xB3, - OvenArabic = 0xB4, FountainArabic = 0xB5, - Hearth = 0xB6, - ForgeTools = 0xB7, CliffDecorBlock = 0xB8, Wood = 0xB9, Bamboo = 0xBA, @@ -218,60 +174,169 @@ make_case_elim!( Eldwood = 0xBE, SeaUrchin = 0xBF, GlassBarrier = 0xC0, - CoralChest = 0xC1, SeaDecorChain = 0xC2, SeaDecorBlock = 0xC3, SeaDecorWindowHor = 0xC4, - SeaDecorWindowVer = 0xC5, - SeaDecorEmblem = 0xC6, SeaDecorPillar = 0xC7, SeashellLantern = 0xC8, Rope = 0xC9, IceSpike = 0xCA, - Bedroll = 0xCB, BedrollSnow = 0xCC, BedrollPirate = 0xCD, - Tent = 0xCE, - Grave = 0xCF, - Gravestone = 0xD0, - PotionDummy = 0xD1, - DoorDark = 0xD2, - MagicalBarrier = 0xD3, MagicalSeal = 0xD4, WallLampWizard = 0xD5, Candle = 0xD6, Keyhole = 0xD7, KeyDoor = 0xD8, CommonLockedChest = 0xD9, - RepairBench = 0xDA, - Helm = 0xDB, - DoorWide = 0xDC, - BoneKeyhole = 0xDD, - BoneKeyDoor = 0xDE, // FireBlock for Burning Buff FireBlock = 0xDF, - IceCrystal = 0xE0, - GlowIceCrystal = 0xE1, - OneWayWall = 0xE2, GlassKeyhole = 0xE3, TallCactus = 0xE4, - Sign = 0xE5, DoorBars = 0xE6, KeyholeBars = 0xE7, - WoodBarricades = 0xE8, SewerMushroom = 0xE9, DiamondLight = 0xEA, Mine = 0xEB, - SmithingTable = 0xEC, - Forge0 = 0xED, - GearWheel0 = 0xEE, - Quench0 = 0xEF, IronSpike = 0xF0, HotSurface = 0xF1, Barrel = 0xF2, CrateBlock = 0xF3, - } -); + }, + // 'Dumping ground' for old-style sprites with orientation until we recategorise them. + MiscWithOri = 2 has Ori { + Window1 = 0, + Window2 = 1, + Window3 = 2, + Window4 = 3, + Bed = 4, + Bench = 5, + ChairSingle = 6, + ChairDouble = 7, + CoatRack = 8, + Crate = 9, + DrawerLarge = 10, + DrawerMedium = 11, + DrawerSmall = 12, + DungeonWallDecor = 13, + HangingBasket = 14, + HangingSign = 15, + WallLamp = 16, + WallLampSmall = 17, + WallSconce = 18, + Planter = 19, + Shelf = 20, + TableSide = 21, + TableDining = 22, + TableDouble = 23, + WardrobeSingle = 24, + WardrobeDouble = 25, + Pot = 26, + Chest = 27, + DungeonChest0 = 28, + DungeonChest1 = 29, + DungeonChest2 = 30, + DungeonChest3 = 31, + DungeonChest4 = 32, + DungeonChest5 = 33, + CoralChest = 34, + SeaDecorWindowVer = 35, + SeaDecorEmblem = 36, + DropGate = 37, + DropGateBottom = 38, + Door = 39, + DoorDark = 40, + Beehive = 41, + PotionMinor = 42, + PotionDummy = 43, + Bowl = 44, + VialEmpty = 45, + FireBowlGround = 46, + Lantern = 47, + CraftingBench = 48, + Forge = 49, + Cauldron = 50, + Anvil = 51, + CookingPot = 52, + SpinningWheel = 53, + TanningRack = 54, + Loom = 55, + DismantlingBench = 56, + RepairBench = 57, + ChristmasOrnament = 58, + ChristmasWreath = 59, + WindowArabic = 60, + BookshelfArabic = 61, + TableArabicLarge = 62, + CanapeArabic = 63, + CupboardArabic = 64, + WallTableArabic = 65, + JugAndBowlArabic = 66, + JugArabic = 67, + MelonCut = 68, + OvenArabic = 69, + Hearth = 70, + ForgeTools = 71, + Tent = 72, + Bedroll = 73, + Grave = 74, + Gravestone = 75, + MagicalBarrier = 76, + Helm = 77, + DoorWide = 78, + BoneKeyhole = 79, + BoneKeyDoor = 80, + IceCrystal = 81, + OneWayWall = 82, + GlowIceCrystal = 83, + Sign = 84, + WoodBarricades = 85, + SmithingTable = 86, + Forge0 = 87, + GearWheel0 = 88, + Quench0 = 89, + }, + // Furniture. In the future, we might add an attribute to customise material + Furniture = 3 has Ori { + // TODO: add stuff to this + }, + // Sprites representing plants that may grow over time (this does not include plant parts, like fruit). + Plant = 4 has Ori, Growth { + // Cacti + BarrelCactus = 0x00, + RoundCactus = 0x01, + ShortCactus = 0x02, + MedFlatCactus = 0x03, + ShortFlatCactus = 0x04, + // Flowers + BlueFlower = 0x10, + PinkFlower = 0x11, + PurpleFlower = 0x12, + RedFlower = 0x13, + WhiteFlower = 0x14, + YellowFlower = 0x15, + Sunflower = 0x16, + // Grasses + LongGrass = 0x20, + MediumGrass = 0x21, + ShortGrass = 0x22, + }, +} + +use core::convert::Infallible; + +attributes! { + Ori { bits: 4, err: Infallible, from: |bits| Ok(Self(bits as u8)), into: |Ori(x)| x as u16 }, + Growth { bits: 4, err: Infallible, from: |bits| Ok(Self(bits as u8)), into: |Growth(x)| x as u16 }, +} + +// The orientation of the sprite, 0..8 +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Ori(pub u8); + +// The growth of the plant, 0..16 +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Growth(pub u8); impl SpriteKind { #[inline] @@ -656,102 +721,9 @@ impl SpriteKind { cfg.and_then(|cfg| cfg.content) } + // TODO: phase out use of this method in favour of `sprite.has_attr::()` #[inline] - pub fn has_ori(&self) -> bool { - matches!( - self, - SpriteKind::Window1 - | SpriteKind::Window2 - | SpriteKind::Window3 - | SpriteKind::Window4 - | SpriteKind::Bed - | SpriteKind::Bench - | SpriteKind::ChairSingle - | SpriteKind::ChairDouble - | SpriteKind::CoatRack - | SpriteKind::Crate - | SpriteKind::DrawerLarge - | SpriteKind::DrawerMedium - | SpriteKind::DrawerSmall - | SpriteKind::DungeonWallDecor - | SpriteKind::HangingBasket - | SpriteKind::HangingSign - | SpriteKind::WallLamp - | SpriteKind::WallLampSmall - | SpriteKind::WallSconce - | SpriteKind::Planter - | SpriteKind::Shelf - | SpriteKind::TableSide - | SpriteKind::TableDining - | SpriteKind::TableDouble - | SpriteKind::WardrobeSingle - | SpriteKind::WardrobeDouble - | SpriteKind::Pot - | SpriteKind::Chest - | SpriteKind::DungeonChest0 - | SpriteKind::DungeonChest1 - | SpriteKind::DungeonChest2 - | SpriteKind::DungeonChest3 - | SpriteKind::DungeonChest4 - | SpriteKind::DungeonChest5 - | SpriteKind::CoralChest - | SpriteKind::SeaDecorWindowVer - | SpriteKind::SeaDecorEmblem - | SpriteKind::DropGate - | SpriteKind::DropGateBottom - | SpriteKind::Door - | SpriteKind::DoorDark - | SpriteKind::Beehive - | SpriteKind::PotionMinor - | SpriteKind::PotionDummy - | SpriteKind::Bowl - | SpriteKind::VialEmpty - | SpriteKind::FireBowlGround - | SpriteKind::Lantern - | SpriteKind::CraftingBench - | SpriteKind::Forge - | SpriteKind::Cauldron - | SpriteKind::Anvil - | SpriteKind::CookingPot - | SpriteKind::SpinningWheel - | SpriteKind::TanningRack - | SpriteKind::Loom - | SpriteKind::DismantlingBench - | SpriteKind::RepairBench - | SpriteKind::ChristmasOrnament - | SpriteKind::ChristmasWreath - | SpriteKind::WindowArabic - | SpriteKind::BookshelfArabic - | SpriteKind::TableArabicLarge - | SpriteKind::CanapeArabic - | SpriteKind::CupboardArabic - | SpriteKind::WallTableArabic - | SpriteKind::JugAndBowlArabic - | SpriteKind::JugArabic - | SpriteKind::MelonCut - | SpriteKind::OvenArabic - | SpriteKind::Hearth - | SpriteKind::ForgeTools - | SpriteKind::Tent - | SpriteKind::Bedroll - | SpriteKind::Grave - | SpriteKind::Gravestone - | SpriteKind::MagicalBarrier - | SpriteKind::Helm - | SpriteKind::DoorWide - | SpriteKind::BoneKeyhole - | SpriteKind::BoneKeyDoor - | SpriteKind::IceCrystal - | SpriteKind::OneWayWall - | SpriteKind::GlowIceCrystal - | SpriteKind::Sign - | SpriteKind::WoodBarricades - | SpriteKind::SmithingTable - | SpriteKind::Forge0 - | SpriteKind::GearWheel0 - | SpriteKind::Quench0 - ) - } + pub fn has_ori(&self) -> bool { self.category().has_attr::() } } impl fmt::Display for SpriteKind { @@ -791,3 +763,41 @@ pub struct SpriteCfg { pub unlock: Option, pub content: Option, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sprite_conv_kind() { + for sprite in SpriteKind::all() { + let block = Block::air(*sprite); + assert_eq!(block.sprite_category(), Some(sprite.category())); + assert_eq!(block.get_sprite(), Some(*sprite)); + } + } + + #[test] + fn sprite_attr() { + for category in Category::all() { + if category.has_attr::() { + for sprite in category.all_sprites() { + for i in 0..4 { + let block = Block::air(*sprite).with_attr(Ori(i)).unwrap(); + assert_eq!(block.get_attr::().unwrap(), Ori(i)); + assert_eq!(block.get_sprite(), Some(*sprite)); + } + } + } + if category.has_attr::() { + for sprite in category.all_sprites() { + for i in 0..16 { + let block = Block::air(*sprite).with_attr(Growth(i)).unwrap(); + assert_eq!(block.get_attr::().unwrap(), Growth(i)); + assert_eq!(block.get_sprite(), Some(*sprite)); + } + } + } + } + } +} diff --git a/common/src/terrain/sprite/magic.rs b/common/src/terrain/sprite/magic.rs new file mode 100644 index 0000000000..5ebd2a2687 --- /dev/null +++ b/common/src/terrain/sprite/magic.rs @@ -0,0 +1,208 @@ +#[macro_export] +macro_rules! sprites { + ( + $($category_name:ident = $category_disc:literal $(has $($attr:ident),* $(,)?)? { + $($sprite_name:ident = $sprite_id:literal),* $(,)? + }),* $(,)? + ) => { + make_case_elim!( + category, + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, EnumIter, FromPrimitive)] + #[repr(u32)] + pub enum Category { + $($category_name = $category_disc,)* + } + ); + + impl Category { + #[inline] pub const fn all() -> &'static [Self] { + &[$(Self::$category_name,)*] + } + + #[cfg(test)] + #[inline] const fn all_sprites(&self) -> &'static [SpriteKind] { + match self { + $(Self::$category_name => &[$(SpriteKind::$sprite_name,)*],)* + } + } + + // Size, in bits, of the sprite ID + #[inline] pub const fn sprite_id_mask(&self) -> u32 { + match self { + $(Self::$category_name => ((0u32 $(| $sprite_id)*) + 1).next_power_of_two() - 1,)* + } + } + + // Size, in bits, of the sprite ID + #[inline] pub const fn sprite_id_size(&self) -> u32 { self.sprite_id_mask().count_ones() } + + // The mask that, when applied to the block data, yields the sprite kind + #[inline(always)] pub const fn sprite_kind_mask(&self) -> u32 { 0x00FF0000 | self.sprite_id_mask() } + + /// Note that this function assumes that the `BlockKind` of `block` permits sprite inhabitants + /// (i.e: is unfilled). + #[allow(non_upper_case_globals)] + #[inline] pub(super) const fn from_block(block: Block) -> Option { + $(const $category_name: u8 = Category::$category_name as u8;)* + match block.sprite_category_byte() { + $($category_name => Some(Self::$category_name),)* + _ => None, + } + } + + // TODO: It would be nice to use `NonZeroU8` here for the space saving, but `0` is a valid + // offset for categories with only one SpriteKind (i.e: the sprite ID is zero-length and so + // attributes can go right up to the end of the block data). However, we could decide that an + // offset of, say, 0xFF (which would obviously be far out of bounds anyway) represents 'this + // attribute has no presence in this category'. + #[inline] pub const fn attr_offsets(&self) -> &[Option; Attributes::all().len()] { + match self { + $(Self::$category_name => { + #[allow(unused_mut, unused_variables, unused_assignments)] + const fn gen_attr_offsets() -> [Option; Attributes::all().len()] { + let mut lut = [None; Attributes::all().len()]; + // Don't take up space used by the sprite ID + let mut offset = Category::$category_name.sprite_id_size(); + $($({ + // Perform basic checks + if offset + $attr::BITS as u32 > 16 { + panic!("Sprite category has an attribute set that will not fit in the block data"); + } else if lut[$attr::INDEX].is_some() { + panic!("Sprite category cannot have more than one instance of an attribute"); + } else if offset > (!0u8) as u32 { + panic!("Uhhh"); + } + lut[$attr::INDEX] = Some(offset as u8); + offset += $attr::BITS as u32; + })*)* + lut + } + const ATTR_OFFSETS: [Option; Attributes::all().len()] = gen_attr_offsets(); + &ATTR_OFFSETS + },)* + } + } + + /// Returns `true` if this category of sprite has the given attribute. + #[inline] pub fn has_attr(&self) -> bool { + self.attr_offsets()[A::INDEX].is_some() + } + + /// Read an attribute from the given block. + /// + /// Note that this function assumes that the category of `self` matches that of the block, but does + /// not validate this. + #[inline] pub(super) fn read_attr(&self, block: Block) -> Result> { + let offset = match self.attr_offsets()[A::INDEX] { + Some(offset) => offset, + None => return Err(AttributeError::NotPresent), + }; + let bits = (block.to_be_u32() >> offset as u32) & ((1 << A::BITS as u32) - 1); + A::from_bits(bits as u16).map_err(AttributeError::Attribute) + } + + /// Write an attribute to the given block. + /// + /// Note that this function assumes that the category of `self` matches that of the block, but does + /// not validate this. + #[inline] pub(super) fn write_attr(&self, block: &mut Block, attr: A) -> Result<(), AttributeError> { + let offset = match self.attr_offsets()[A::INDEX] { + Some(offset) => offset, + None => return Err(AttributeError::NotPresent), + }; + let bits = attr.into_bits() as u32; + #[cfg(debug_assertions)] + assert!(bits < (1 << A::BITS as u32), "The bit representation of the attribute {} must fit within {} bits, but the representation was {:0b}", core::any::type_name::(), A::BITS, bits); + let data = ((block.to_be_u32() & (!(((1 << A::BITS as u32) - 1) << offset as u32))) | (bits << offset as u32)).to_be_bytes(); + *block = block.with_data([data[1], data[2], data[3]]); + Ok(()) + } + } + + #[inline] const fn gen_discriminant(category: Category, id: u16) -> u32 { + (category as u32) << 16 | id as u32 + } + + make_case_elim!( + sprite_kind, + #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, EnumIter, FromPrimitive)] + #[repr(u32)] + pub enum SpriteKind { + $($($sprite_name = crate::terrain::sprite::gen_discriminant(crate::terrain::sprite::Category::$category_name, $sprite_id),)*)* + } + ); + + impl SpriteKind { + #[inline] pub const fn all() -> &'static [Self] { + &[$($(Self::$sprite_name,)*)*] + } + + #[inline] pub const fn category(&self) -> Category { + match self { + $($(Self::$sprite_name => Category::$category_name,)*)* + } + } + + /// Note that this function assumes that the category of `self` matches that of the block data, but does + /// not validate this. + #[allow(non_upper_case_globals)] + #[inline] pub(super) const fn from_block(block: Block) -> Option { + match block.sprite_category() { + None => None, + $(Some(category @ Category::$category_name) => { + $(const $sprite_name: u32 = SpriteKind::$sprite_name as u32;)* + match block.to_be_u32() & category.sprite_kind_mask() { + $($sprite_name => Some(Self::$sprite_name),)* + _ => None, + } + },)* + } + } + } + }; +} + +#[derive(Debug)] +pub enum AttributeError { + /// The attribute was not present for the given block data's category. + NotPresent, + /// An attribute-specific error occurred when performing extraction. + Attribute(E), +} + +pub trait Attribute: Sized { + /// The unique index assigned to this attribute, used to index offset arrays + const INDEX: usize; + /// The number of bits required to represent this attribute + const BITS: u8; + type Error; + fn from_bits(bits: u16) -> Result; + fn into_bits(self) -> u16; +} + +#[macro_export] +macro_rules! attributes { + ($( + $name:ident { bits: $bits:literal, err: $err:path, from: $from_bits:expr, into: $into_bits:expr $(,)? } + ),* $(,)?) => { + #[derive(Copy, Clone, Debug)] + #[repr(u16)] + pub enum Attributes { + $($name,)* + } + + impl Attributes { + #[inline] pub const fn all() -> &'static [Self] { + &[$(Self::$name,)*] + } + } + + $(impl Attribute for $name { + const INDEX: usize = Attributes::$name as usize; + const BITS: u8 = $bits; + type Error = $err; + #[inline(always)] fn from_bits(bits: u16) -> Result { $from_bits(bits) } + #[inline(always)] fn into_bits(self) -> u16 { $into_bits(self) } + })* + }; +} diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index ed3a8708d4..1e4705e72a 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -183,13 +183,11 @@ struct SpriteConfig { /// NOTE: Model is an asset path to the appropriate sprite .vox model. #[derive(Deserialize)] #[serde(try_from = "HashMap>>")] -pub struct SpriteSpec([Option>; 256]); +pub struct SpriteSpec(HashMap>>); impl SpriteSpec { fn get(&self, kind: SpriteKind) -> Option<&SpriteConfig> { - const _: () = assert!(core::mem::size_of::() == 1); - // NOTE: This will never be out of bounds since `SpriteKind` is `repr(u8)` - self.0[kind as usize].as_ref() + self.0.get(&kind).and_then(Option::as_ref) } } @@ -216,7 +214,10 @@ impl TryFrom>>> for SpriteSpec { fn try_from( mut map: HashMap>>, ) -> Result { - let mut array = [(); 256].map(|()| None); + Ok(Self(map)) + + /* + let mut array = [(); 65536].map(|()| None); let sprites_missing = SpriteKind::iter() .filter(|kind| match map.remove(kind) { Some(config) => { @@ -232,6 +233,7 @@ impl TryFrom>>> for SpriteSpec { } else { Err(SpritesMissing(sprites_missing)) } + */ } }