Merge branch 'making-things-sprite' into 'master'

Overhauled sprite representation to support many more sprites and attributes

See merge request veloren/veloren!4259
This commit is contained in:
Joshua Barretto 2024-01-20 11:16:18 +00:00
commit 88a4d0898f
5 changed files with 755 additions and 412 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::{ use crate::{
comp::{fluid_dynamics::LiquidKind, tool::ToolKind}, comp::{fluid_dynamics::LiquidKind, tool::ToolKind},
consts::FRIC_GROUND, consts::FRIC_GROUND,
@ -114,10 +114,29 @@ impl BlockKind {
} }
} }
/// # Format
///
/// ```ignore
/// 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)] #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Block { pub struct Block {
kind: BlockKind, kind: BlockKind,
attr: [u8; 3], data: [u8; 3],
} }
impl FilledVox for Block { impl FilledVox for Block {
@ -135,12 +154,22 @@ impl Deref for Block {
impl Block { impl Block {
pub const MAX_HEIGHT: f32 = 3.0; pub const MAX_HEIGHT: f32 = 3.0;
/* Constructors */
// TODO: Rename to `filled`
#[inline] #[inline]
pub const fn new(kind: BlockKind, color: Rgb<u8>) -> Self { 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 { Self {
kind, kind,
// Colours are only valid for non-fluids // Colours are only valid for non-fluids
attr: if kind.is_filled() { data: if kind.is_filled() {
[color.r, color.g, color.b] [color.r, color.g, color.b]
} else { } else {
[0; 3] [0; 3]
@ -148,61 +177,110 @@ impl Block {
} }
} }
// Only valid if `block_kind` is unfilled, so this is just a private utility
// method
#[inline] #[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 { Self {
kind: BlockKind::Air, kind,
attr: [sprite as u8, 0, 0], data: [sprite_bytes[1], sprite_bytes[2], sprite_bytes[3]],
} }
} }
#[inline] #[inline]
pub const fn lava(sprite: SpriteKind) -> Self { pub const fn air(sprite: SpriteKind) -> Self { Self::unfilled(BlockKind::Air, sprite) }
Self {
kind: BlockKind::Lava,
attr: [sprite as u8, 0, 0],
}
}
#[inline] #[inline]
pub const fn empty() -> Self { Self::air(SpriteKind::Empty) } pub const fn empty() -> Self { Self::air(SpriteKind::Empty) }
/// TODO: See if we can generalize this somehow.
#[inline] #[inline]
pub const fn water(sprite: SpriteKind) -> Self { pub const fn water(sprite: SpriteKind) -> Self { Self::unfilled(BlockKind::Water, sprite) }
Self {
kind: BlockKind::Water, /* Sprite decoding */
attr: [sprite as u8, 0, 0],
#[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 => 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] #[inline]
pub fn get_color(&self) -> Option<Rgb<u8>> { pub fn get_color(&self) -> Option<Rgb<u8>> {
if self.has_color() { if self.has_color() {
Some(self.attr.into()) Some(self.data.into())
} else { } else {
None None
} }
} }
// TODO: phase out use of this method in favour of `block.get_attr::<Ori>()`
#[inline] #[inline]
pub fn get_sprite(&self) -> Option<SpriteKind> { pub fn get_ori(&self) -> Option<u8> { self.get_attr::<sprite::Ori>().ok().map(|ori| ori.0) }
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
}
}
/// Returns the rtsim resource, if any, that this block corresponds to. If /// 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 /// you want the scarcity of a block to change with rtsim's resource
@ -547,7 +625,7 @@ impl Block {
#[must_use] #[must_use]
pub fn with_sprite(mut self, sprite: SpriteKind) -> Self { pub fn with_sprite(mut self, sprite: SpriteKind) -> Self {
if !self.is_filled() { if !self.is_filled() {
self.attr[0] = sprite as u8; self = Self::unfilled(self.kind, sprite);
} }
self self
} }
@ -555,14 +633,7 @@ impl Block {
/// If this block can have orientation, give it a new orientation. /// If this block can have orientation, give it a new orientation.
#[inline] #[inline]
#[must_use] #[must_use]
pub fn with_ori(mut self, ori: u8) -> Option<Self> { pub fn with_ori(self, ori: u8) -> Option<Self> { self.with_attr(sprite::Ori(ori)).ok() }
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
}
}
/// Remove the terrain sprite or solid aspects of a block /// Remove the terrain sprite or solid aspects of a block
#[inline] #[inline]
@ -584,27 +655,24 @@ impl Block {
let [bk, r, g, b] = x.to_le_bytes(); let [bk, r, g, b] = x.to_le_bytes();
Some(Self { Some(Self {
kind: BlockKind::from_u8(bk)?, kind: BlockKind::from_u8(bk)?,
attr: [r, g, b], data: [r, g, b],
}) })
} }
#[inline] #[inline]
pub fn to_u32(&self) -> u32 { 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
#[test]
fn block_size() {
assert_eq!(std::mem::size_of::<BlockKind>(), 1);
assert_eq!(std::mem::size_of::<Block>(), 4);
}
#[test] #[test]
fn convert_u32() { fn convert_u32() {
for bk in BlockKind::iter() { 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::{ use crate::{
attributes,
comp::{ comp::{
item::{ItemDefinitionId, ItemDefinitionIdOwned}, item::{ItemDefinitionId, ItemDefinitionIdOwned},
tool::ToolKind, tool::ToolKind,
}, },
lottery::LootSpec, lottery::LootSpec,
make_case_elim, make_case_elim, sprites,
terrain::Block, terrain::Block,
}; };
use common_i18n::Content; use common_i18n::Content;
@ -12,266 +52,318 @@ use hashbrown::HashMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use num_derive::FromPrimitive; use num_derive::FromPrimitive;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fmt}; use std::{
convert::{Infallible, TryFrom},
fmt,
};
use strum::EnumIter; use strum::EnumIter;
use vek::*; use vek::*;
make_case_elim!( sprites! {
sprite_kind, Void = 0 {
#[derive( Empty = 0,
Copy, Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize, EnumIter, FromPrimitive, },
)] // Generic collection of sprites, no attributes but anything goes
#[repr(u8)] Misc = 1 {
pub enum SpriteKind { Ember = 0x00,
// Note that the values of these should be linearly contiguous to allow for quick SmokeDummy = 0x01,
// bounds-checking when casting to a u8. Bomb = 0x02,
Empty = 0x00, FireBlock = 0x03, // FireBlock for Burning Buff
BarrelCactus = 0x01, Mine = 0x04,
RoundCactus = 0x02, HotSurface = 0x05,
ShortCactus = 0x03, },
MedFlatCactus = 0x04, // Furniture. In the future, we might add an attribute to customise material
ShortFlatCactus = 0x05, // TODO: Remove sizes and variants, represent with attributes
BlueFlower = 0x06, Furniture = 2 has Ori {
PinkFlower = 0x07, // Indoor
PurpleFlower = 0x08, CoatRack = 0x00,
RedFlower = 0x09, Bed = 0x01,
WhiteFlower = 0x0A, Bench = 0x02,
YellowFlower = 0x0B, ChairSingle = 0x03,
Sunflower = 0x0C, ChairDouble = 0x04,
LongGrass = 0x0D, DrawerLarge = 0x05,
MediumGrass = 0x0E, DrawerMedium = 0x06,
ShortGrass = 0x0F, DrawerSmall = 0x07,
Apple = 0x10, TableSide = 0x08,
Mushroom = 0x11, TableDining = 0x09,
Liana = 0x12, TableDouble = 0x0A,
Velorite = 0x13, WardrobeSingle = 0x0B,
VeloriteFrag = 0x14, WardrobeDouble = 0x0C,
Chest = 0x15, BookshelfArabic = 0x0D,
Pumpkin = 0x16, WallTableArabic = 0x0E,
Welwitch = 0x17, TableArabicLarge = 0x0F,
LingonBerry = 0x18, TableArabicSmall = 0x10,
LeafyPlant = 0x19, CupboardArabic = 0x11,
Fern = 0x1A, OvenArabic = 0x12,
DeadBush = 0x1B, CushionArabic = 0x13,
Blueberry = 0x1C, CanapeArabic = 0x14,
Ember = 0x1D, Shelf = 0x15,
Corn = 0x1E, Planter = 0x16,
WheatYellow = 0x1F, Pot = 0x17,
WheatGreen = 0x20, // Crafting
Cabbage = 0x21, CraftingBench = 0x20,
Flax = 0x22, Forge = 0x21,
Carrot = 0x23, Cauldron = 0x22,
Tomato = 0x24, Anvil = 0x23,
Radish = 0x25, CookingPot = 0x24,
Coconut = 0x26, SpinningWheel = 0x25,
Turnip = 0x27, TanningRack = 0x26,
Window1 = 0x28, Loom = 0x27,
Window2 = 0x29, DismantlingBench = 0x28,
Window3 = 0x2A, RepairBench = 0x29,
Window4 = 0x2B, // Containers
Scarecrow = 0x2C, Chest = 0x30,
StreetLamp = 0x2D, DungeonChest0 = 0x31,
StreetLampTall = 0x2E, DungeonChest1 = 0x32,
Door = 0x2F, DungeonChest2 = 0x33,
Bed = 0x30, DungeonChest3 = 0x34,
Bench = 0x31, DungeonChest4 = 0x35,
ChairSingle = 0x32, DungeonChest5 = 0x36,
ChairDouble = 0x33, CoralChest = 0x37,
CoatRack = 0x34, CommonLockedChest = 0x38,
Crate = 0x35, ChestBuried = 0x39,
DrawerLarge = 0x36, Crate = 0x3A,
DrawerMedium = 0x37, Barrel = 0x3B,
DrawerSmall = 0x38, CrateBlock = 0x3C,
DungeonWallDecor = 0x39, // Standalone lights
HangingBasket = 0x3A, Lantern = 0x40,
HangingSign = 0x3B, StreetLamp = 0x41,
WallLamp = 0x3C, StreetLampTall = 0x42,
Planter = 0x3D, SeashellLantern = 0x43,
Shelf = 0x3E, FireBowlGround = 0x44,
TableSide = 0x3F, // Wall
TableDining = 0x40, HangingBasket = 0x50,
TableDouble = 0x41, HangingSign = 0x51,
WardrobeSingle = 0x42, ChristmasOrnament = 0x52,
WardrobeDouble = 0x43, ChristmasWreath = 0x53,
LargeGrass = 0x44, WallLampWizard = 0x54,
Pot = 0x45, WallLamp = 0x55,
Stones = 0x46, WallLampSmall = 0x56,
Twigs = 0x47, WallSconce = 0x57,
DropGate = 0x48, DungeonWallDecor = 0x58,
DropGateBottom = 0x49, // Outdoor
GrassSnow = 0x4A, Tent = 0x60,
Reed = 0x4B, Bedroll = 0x61,
Beehive = 0x4C, BedrollSnow = 0x62,
LargeCactus = 0x4D, BedrollPirate = 0x63,
VialEmpty = 0x4E, Sign = 0x64,
PotionMinor = 0x4F, Helm = 0x65,
GrassBlue = 0x50, // Misc
ChestBuried = 0x51, Scarecrow = 0x70,
Mud = 0x52, FountainArabic = 0x71,
FireBowlGround = 0x53, Hearth = 0x72,
CaveMushroom = 0x54, },
Bowl = 0x55, // Sprites representing plants that may grow over time (this does not include plant parts, like fruit).
SavannaGrass = 0x56, Plant = 3 has Growth {
TallSavannaGrass = 0x57, // Cacti
RedSavannaGrass = 0x58, BarrelCactus = 0x00,
SavannaBush = 0x59, RoundCactus = 0x01,
Amethyst = 0x5A, ShortCactus = 0x02,
Ruby = 0x5B, MedFlatCactus = 0x03,
Sapphire = 0x5C, ShortFlatCactus = 0x04,
Emerald = 0x5D, LargeCactus = 0x05,
Topaz = 0x5E, TallCactus = 0x06,
Diamond = 0x5F, // Flowers
AmethystSmall = 0x60, BlueFlower = 0x10,
TopazSmall = 0x61, PinkFlower = 0x11,
DiamondSmall = 0x62, PurpleFlower = 0x12,
RubySmall = 0x63, RedFlower = 0x13,
EmeraldSmall = 0x64, WhiteFlower = 0x14,
SapphireSmall = 0x65, YellowFlower = 0x15,
WallLampSmall = 0x66, Sunflower = 0x16,
WallSconce = 0x67, Moonbell = 0x17,
StonyCoral = 0x68, Pyrebloom = 0x18,
SoftCoral = 0x69, // Grasses, ferns, and other 'wild' plants/fungi
SeaweedTemperate = 0x6A, // TODO: remove sizes, make part of the `Growth` attribute
SeaweedTropical = 0x6B, LongGrass = 0x20,
GiantKelp = 0x6C, MediumGrass = 0x21,
BullKelp = 0x6D, ShortGrass = 0x22,
WavyAlgae = 0x6E, Fern = 0x23,
SeaGrapes = 0x6F, LargeGrass = 0x24,
MermaidsFan = 0x70, GrassSnow = 0x25,
SeaAnemone = 0x71, Reed = 0x26,
Seashells = 0x72, GrassBlue = 0x27,
Seagrass = 0x73, SavannaGrass = 0x28,
RedAlgae = 0x74, TallSavannaGrass = 0x29,
UnderwaterVent = 0x75, RedSavannaGrass = 0x2A,
Lantern = 0x76, SavannaBush = 0x2B,
CraftingBench = 0x77, Welwitch = 0x2C,
Forge = 0x78, LeafyPlant = 0x2D,
Cauldron = 0x79, DeadBush = 0x2E,
Anvil = 0x7A, JungleFern = 0x2F,
CookingPot = 0x7B, CavernGrassBlueShort = 0x30,
DungeonChest0 = 0x7C, CavernGrassBlueMedium = 0x31,
DungeonChest1 = 0x7D, CavernGrassBlueLong = 0x32,
DungeonChest2 = 0x7E, CavernLillypadBlue = 0x33,
DungeonChest3 = 0x7F, EnsnaringVines = 0x34,
DungeonChest4 = 0x80, LillyPads = 0x35,
DungeonChest5 = 0x81, JungleLeafyPlant = 0x36,
Loom = 0x82, JungleRedGrass = 0x37,
SpinningWheel = 0x83, // Crops, berries, and fungi
CrystalHigh = 0x84, Corn = 0x40,
Bloodstone = 0x85, WheatYellow = 0x41,
Coal = 0x86, WheatGreen = 0x42, // TODO: Remove `WheatGreen`, make part of the `Growth` attribute
Cobalt = 0x87, LingonBerry = 0x43,
Copper = 0x88, Blueberry = 0x44,
Iron = 0x89, Cabbage = 0x45,
Tin = 0x8A, Pumpkin = 0x46,
Silver = 0x8B, Carrot = 0x47,
Gold = 0x8C, Tomato = 0x48,
Cotton = 0x8D, Radish = 0x49,
Moonbell = 0x8E, Turnip = 0x4A,
Pyrebloom = 0x8F, Flax = 0x4B,
TanningRack = 0x90, Mushroom = 0x4C,
WildFlax = 0x91, CaveMushroom = 0x4D,
CrystalLow = 0x92, Cotton = 0x4E,
CeilingMushroom = 0x93, WildFlax = 0x4F,
Orb = 0x94, SewerMushroom = 0x50,
EnsnaringVines = 0x95, // Seaweeds, corals, and other underwater plants
WitchWindow = 0x96, StonyCoral = 0x60,
SmokeDummy = 0x97, SoftCoral = 0x61,
Bones = 0x98, SeaweedTemperate = 0x62,
CavernGrassBlueShort = 0x99, SeaweedTropical = 0x63,
CavernGrassBlueMedium = 0x9A, GiantKelp = 0x64,
CavernGrassBlueLong = 0x9B, BullKelp = 0x65,
CavernLillypadBlue = 0x9C, WavyAlgae = 0x66,
CavernMycelBlue = 0x9D, SeaGrapes = 0x67,
DismantlingBench = 0x9E, MermaidsFan = 0x68,
JungleFern = 0x9F, SeaAnemone = 0x69,
LillyPads = 0xA0, Seagrass = 0x6A,
JungleLeafyPlant = 0xA1, RedAlgae = 0x6B,
JungleRedGrass = 0xA2, // Danglying ceiling plants/fungi
Bomb = 0xA3, Liana = 0x70,
ChristmasOrnament = 0xA4, CavernMycelBlue = 0x71,
ChristmasWreath = 0xA5, CeilingMushroom = 0x72,
EnsnaringWeb = 0xA6, },
WindowArabic = 0xA7, // Solid resources
MelonCut = 0xA8, // TODO: Remove small variants, make deposit size be an attribute
BookshelfArabic = 0xA9, Resources = 4 {
DecorSetArabic = 0xAA, // Gems and ores
SepareArabic = 0xAB, Amethyst = 0x00,
CushionArabic = 0xAC, AmethystSmall = 0x01,
JugArabic = 0xAD, Ruby = 0x02,
TableArabicSmall = 0xAE, RubySmall = 0x03,
TableArabicLarge = 0xAF, Sapphire = 0x04,
CanapeArabic = 0xB0, SapphireSmall = 0x05,
CupboardArabic = 0xB1, Emerald = 0x06,
WallTableArabic = 0xB2, EmeraldSmall = 0x07,
JugAndBowlArabic = 0xB3, Topaz = 0x08,
OvenArabic = 0xB4, TopazSmall = 0x09,
FountainArabic = 0xB5, Diamond = 0x0A,
Hearth = 0xB6, DiamondSmall = 0x0B,
ForgeTools = 0xB7, Bloodstone = 0x0C,
CliffDecorBlock = 0xB8, Coal = 0x0D,
Wood = 0xB9, Cobalt = 0x0E,
Bamboo = 0xBA, Copper = 0x0F,
Hardwood = 0xBB, Iron = 0x10,
Ironwood = 0xBC, Tin = 0x11,
Frostwood = 0xBD, Silver = 0x12,
Eldwood = 0xBE, Gold = 0x13,
SeaUrchin = 0xBF, Velorite = 0x14,
GlassBarrier = 0xC0, VeloriteFrag = 0x15,
CoralChest = 0xC1, // Woods and twigs
SeaDecorChain = 0xC2, Twigs = 0x20,
SeaDecorBlock = 0xC3, Wood = 0x21,
SeaDecorWindowHor = 0xC4, Bamboo = 0x22,
SeaDecorWindowVer = 0xC5, Hardwood = 0x23,
SeaDecorEmblem = 0xC6, Ironwood = 0x24,
SeaDecorPillar = 0xC7, Frostwood = 0x25,
SeashellLantern = 0xC8, Eldwood = 0x26,
Rope = 0xC9, // Other
IceSpike = 0xCA, Apple = 0x30,
Bedroll = 0xCB, Coconut = 0x31,
BedrollSnow = 0xCC, Stones = 0x32,
BedrollPirate = 0xCD, Seashells = 0x33,
Tent = 0xCE, Beehive = 0x34,
Grave = 0xCF, Bowl = 0x35,
Gravestone = 0xD0, PotionMinor = 0x36,
PotionDummy = 0xD1, PotionDummy = 0x37,
DoorDark = 0xD2, VialEmpty = 0x38,
MagicalBarrier = 0xD3, },
MagicalSeal = 0xD4, // Structural elements including doors and building parts
WallLampWizard = 0xD5, Structural = 5 has Ori {
Candle = 0xD6, // Doors and keyholes
Keyhole = 0xD7, Door = 0x00,
KeyDoor = 0xD8, DoorDark = 0x01,
CommonLockedChest = 0xD9, DoorWide = 0x02,
RepairBench = 0xDA, BoneKeyhole = 0x03,
Helm = 0xDB, BoneKeyDoor = 0x04,
DoorWide = 0xDC, Keyhole = 0x05,
BoneKeyhole = 0xDD, KeyDoor = 0x06,
BoneKeyDoor = 0xDE, GlassKeyhole = 0x07,
// FireBlock for Burning Buff KeyholeBars = 0x08,
FireBlock = 0xDF, // Windows
IceCrystal = 0xE0, Window1 = 0x10,
GlowIceCrystal = 0xE1, Window2 = 0x11,
OneWayWall = 0xE2, Window3 = 0x12,
GlassKeyhole = 0xE3, Window4 = 0x13,
TallCactus = 0xE4, WitchWindow = 0x14,
Sign = 0xE5, WindowArabic = 0x15,
DoorBars = 0xE6, // Walls
KeyholeBars = 0xE7, GlassBarrier = 0x20,
WoodBarricades = 0xE8, SeaDecorBlock = 0x21,
SewerMushroom = 0xE9, CliffDecorBlock = 0x22,
DiamondLight = 0xEA, MagicalBarrier = 0x23,
Mine = 0xEB, OneWayWall = 0x24,
SmithingTable = 0xEC, // Gates and grates
Forge0 = 0xED, SeaDecorWindowHor = 0x30,
GearWheel0 = 0xEE, SeaDecorWindowVer = 0x31,
Quench0 = 0xEF, DropGate = 0x32,
IronSpike = 0xF0, DropGateBottom = 0x33,
HotSurface = 0xF1, WoodBarricades = 0x34,
Barrel = 0xF2, // Misc
CrateBlock = 0xF3, Rope = 0x40,
SeaDecorChain = 0x41,
IronSpike = 0x42,
DoorBars = 0x43,
},
// Decorative items, both natural and artificial
Decor = 6 has Ori {
// Natural
Bones = 0x00,
IceCrystal = 0x01,
GlowIceCrystal = 0x02,
CrystalHigh = 0x03,
CrystalLow = 0x04,
UnderwaterVent = 0x05,
SeaUrchin = 0x06,
IceSpike = 0x07,
Mud = 0x08,
Orb = 0x09,
EnsnaringWeb = 0x0A,
DiamondLight = 0x0B,
// Artificial
Grave = 0x10,
Gravestone = 0x11,
MelonCut = 0x12,
ForgeTools = 0x13,
JugAndBowlArabic = 0x14,
JugArabic = 0x15,
DecorSetArabic = 0x16,
SepareArabic = 0x17,
Candle = 0x18,
SmithingTable = 0x19,
Forge0 = 0x1A,
GearWheel0 = 0x1B,
Quench0 = 0x1C,
SeaDecorEmblem = 0x1D,
SeaDecorPillar = 0x1E,
MagicalSeal = 0x1F,
},
} }
);
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 { impl SpriteKind {
#[inline] #[inline]
@ -656,102 +748,9 @@ impl SpriteKind {
cfg.and_then(|cfg| cfg.content) cfg.and_then(|cfg| cfg.content)
} }
// TODO: phase out use of this method in favour of `sprite.has_attr::<Ori>()`
#[inline] #[inline]
pub fn has_ori(&self) -> bool { pub fn has_ori(&self) -> bool { self.category().has_attr::<Ori>() }
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
)
}
} }
impl fmt::Display for SpriteKind { impl fmt::Display for SpriteKind {
@ -791,3 +790,41 @@ pub struct SpriteCfg {
pub unlock: Option<UnlockKind>, pub unlock: Option<UnlockKind>,
pub content: Option<Content>, 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,211 @@
#[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,)*]
}
}
$(
#[allow(clippy::all)]
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

@ -181,15 +181,16 @@ struct SpriteConfig<Model> {
/// Configuration data for all sprite models. /// Configuration data for all sprite models.
/// ///
/// NOTE: Model is an asset path to the appropriate sprite .vox model. /// NOTE: Model is an asset path to the appropriate sprite .vox model.
// TODO: Overhaul this entirely to work with the new sprite attribute system. We'll probably be
// wanting a way to specify inexact mappings between sprite models and sprite configurations. For
// example, the ability to use a model for a range of plant growth states.
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(try_from = "HashMap<SpriteKind, Option<SpriteConfig<String>>>")] #[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 { impl SpriteSpec {
fn get(&self, kind: SpriteKind) -> Option<&SpriteConfig<String>> { fn get(&self, kind: SpriteKind) -> Option<&SpriteConfig<String>> {
const _: () = assert!(core::mem::size_of::<SpriteKind>() == 1); self.0.get(&kind).and_then(Option::as_ref)
// NOTE: This will never be out of bounds since `SpriteKind` is `repr(u8)`
self.0[kind as usize].as_ref()
} }
} }
@ -214,9 +215,12 @@ impl TryFrom<HashMap<SpriteKind, Option<SpriteConfig<String>>>> for SpriteSpec {
type Error = SpritesMissing; type Error = SpritesMissing;
fn try_from( fn try_from(
mut map: HashMap<SpriteKind, Option<SpriteConfig<String>>>, map: HashMap<SpriteKind, Option<SpriteConfig<String>>>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
let mut array = [(); 256].map(|()| None); Ok(Self(map))
/*
let mut array = [(); 65536].map(|()| None);
let sprites_missing = SpriteKind::iter() let sprites_missing = SpriteKind::iter()
.filter(|kind| match map.remove(kind) { .filter(|kind| match map.remove(kind) {
Some(config) => { Some(config) => {
@ -232,6 +236,7 @@ impl TryFrom<HashMap<SpriteKind, Option<SpriteConfig<String>>>> for SpriteSpec {
} else { } else {
Err(SpritesMissing(sprites_missing)) Err(SpritesMissing(sprites_missing))
} }
*/
} }
} }