mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
commit
88a4d0898f
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::{
|
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() {
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
211
common/src/terrain/sprite/magic.rs
Normal file
211
common/src/terrain/sprite/magic.rs
Normal 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) }
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user