Overhauled sprite representation to support many more sprites and attributes

This commit is contained in:
Joshua Barretto 2024-01-19 18:05:56 +00:00
parent a852298010
commit 5260c82c4a
5 changed files with 584 additions and 268 deletions

View File

@ -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);
}
}

View File

@ -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<u8>) -> 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<SpriteKind> {
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<sprite::Category> {
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<A: sprite::Attribute>(
mut self,
attr: A,
) -> Result<Self, sprite::AttributeError<core::convert::Infallible>> {
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<A: sprite::Attribute>(
&mut self,
attr: A,
) -> Result<(), sprite::AttributeError<core::convert::Infallible>> {
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<A: sprite::Attribute>(&self) -> Result<A, sprite::AttributeError<A::Error>> {
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<Rgb<u8>> {
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::<Ori>()`
#[inline]
pub fn get_sprite(&self) -> Option<SpriteKind> {
if !self.is_filled() {
SpriteKind::from_u8(self.attr[0])
} else {
None
}
}
#[inline]
pub fn get_ori(&self) -> Option<u8> {
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<u8> { self.get_attr::<sprite::Ori>().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<Self> {
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> { 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::<BlockKind>() == 1);
const _: () = assert!(core::mem::size_of::<Block>() == 4);
#[cfg(test)]
mod tests {
use super::*;
use strum::IntoEnumIterator;
#[test]
fn block_size() {
assert_eq!(std::mem::size_of::<BlockKind>(), 1);
assert_eq!(std::mem::size_of::<Block>(), 4);
}
#[test]
fn convert_u32() {
for bk in BlockKind::iter() {

View File

@ -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::<Ori>()`
#[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::<Ori>() }
}
impl fmt::Display for SpriteKind {
@ -791,3 +763,41 @@ pub struct SpriteCfg {
pub unlock: Option<UnlockKind>,
pub content: Option<Content>,
}
#[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::<Ori>() {
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::<Ori>().unwrap(), Ori(i));
assert_eq!(block.get_sprite(), Some(*sprite));
}
}
}
if category.has_attr::<Growth>() {
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::<Growth>().unwrap(), Growth(i));
assert_eq!(block.get_sprite(), Some(*sprite));
}
}
}
}
}
}

View File

@ -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<Self> {
$(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<u8>; Attributes::all().len()] {
match self {
$(Self::$category_name => {
#[allow(unused_mut, unused_variables, unused_assignments)]
const fn gen_attr_offsets() -> [Option<u8>; 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<u8>; Attributes::all().len()] = gen_attr_offsets();
&ATTR_OFFSETS
},)*
}
}
/// Returns `true` if this category of sprite has the given attribute.
#[inline] pub fn has_attr<A: Attribute>(&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<A: Attribute>(&self, block: Block) -> Result<A, AttributeError<A::Error>> {
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<A: Attribute>(&self, block: &mut Block, attr: A) -> Result<(), AttributeError<core::convert::Infallible>> {
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>(), 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<Self> {
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<E> {
/// 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<Self, Self::Error>;
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<Self, Self::Error> { $from_bits(bits) }
#[inline(always)] fn into_bits(self) -> u16 { $into_bits(self) }
})*
};
}

View File

@ -183,13 +183,11 @@ struct SpriteConfig<Model> {
/// NOTE: Model is an asset path to the appropriate sprite .vox model.
#[derive(Deserialize)]
#[serde(try_from = "HashMap<SpriteKind, Option<SpriteConfig<String>>>")]
pub struct SpriteSpec([Option<SpriteConfig<String>>; 256]);
pub struct SpriteSpec(HashMap<SpriteKind, Option<SpriteConfig<String>>>);
impl SpriteSpec {
fn get(&self, kind: SpriteKind) -> Option<&SpriteConfig<String>> {
const _: () = assert!(core::mem::size_of::<SpriteKind>() == 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<HashMap<SpriteKind, Option<SpriteConfig<String>>>> for SpriteSpec {
fn try_from(
mut map: HashMap<SpriteKind, Option<SpriteConfig<String>>>,
) -> Result<Self, Self::Error> {
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<HashMap<SpriteKind, Option<SpriteConfig<String>>>> for SpriteSpec {
} else {
Err(SpritesMissing(sprites_missing))
}
*/
}
}