mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Overhauled sprite representation to support many more sprites and attributes
This commit is contained in:
parent
a852298010
commit
5260c82c4a
22
common/examples/sprite_debug.rs
Normal file
22
common/examples/sprite_debug.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
208
common/src/terrain/sprite/magic.rs
Normal file
208
common/src/terrain/sprite/magic.rs
Normal 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) }
|
||||
})*
|
||||
};
|
||||
}
|
@ -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))
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user