Parse Items at Compile time

This commit is contained in:
Alexander Medvedev 2025-02-08 19:23:59 +01:00
parent 5c5bc67b3a
commit 8592d095f3
43 changed files with 463 additions and 272 deletions

1
assets/spawn_egg.json Normal file
View File

@ -0,0 +1 @@
{"1130":"wither_skeleton","1071":"creeper","1094":"ocelot","1114":"snow_golem","1125":"vindicator","1090":"llama","1129":"wither","1134":"zombie","1080":"fox","1085":"guardian","1076":"ender_dragon","1102":"polar_bear","1081":"frog","1131":"wolf","1070":"cow","1111":"skeleton_horse","1122":"turtle","1096":"parrot","1104":"rabbit","1093":"mule","1092":"mooshroom","1106":"salmon","1082":"ghast","1126":"wandering_trader","1109":"silverfish","1132":"zoglin","1065":"cat","1062":"blaze","1068":"chicken","1072":"dolphin","1103":"pufferfish","1108":"shulker","1119":"tadpole","1059":"axolotl","1069":"cod","1112":"slime","1113":"sniffer","1137":"zombified_piglin","1087":"horse","1105":"ravager","1110":"skeleton","1083":"glow_squid","1066":"camel","1067":"cave_spider","1057":"armadillo","1086":"hoglin","1118":"strider","1077":"enderman","1124":"villager","1128":"witch","1107":"sheep","1063":"bogged","1120":"trader_llama","1074":"drowned","1064":"breeze","1075":"elder_guardian","1115":"spider","1097":"phantom","1116":"squid","1101":"pillager","1127":"warden","1078":"endermite","1061":"bee","1089":"iron_golem","1098":"pig","1135":"zombie_horse","1117":"stray","1133":"creaking","1073":"donkey","1095":"panda","1136":"zombie_villager","1091":"magma_cube","1099":"piglin","1100":"piglin_brute","1084":"goat","1058":"allay","1121":"tropical_fish","1060":"bat","1079":"evoker","1088":"husk","1123":"vex"}

View File

@ -5,7 +5,10 @@ edition.workspace = true
build = "build/build.rs"
[dependencies]
pumpkin-util = { path = "../pumpkin-util" }
serde.workspace = true
serde_json.workspace = true
[build-dependencies]
pumpkin-util = { path = "../pumpkin-util" }

View File

@ -7,8 +7,8 @@ pub(crate) fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/biome.json");
let biomes: Vec<String> = serde_json::from_str(include_str!("../../assets/biome.json"))
.expect("Failed to parse entity_pose.json");
let variants = array_to_tokenstream(biomes);
.expect("Failed to parse biome.json");
let variants = array_to_tokenstream(&biomes);
quote! {
#[derive(Clone, Copy)]

View File

@ -10,6 +10,7 @@ mod damage_type;
mod entity_pose;
mod entity_type;
mod game_event;
mod item;
mod message_type;
mod noise_parameter;
mod packet;
@ -18,6 +19,7 @@ mod scoreboard_slot;
mod screen;
mod sound;
mod sound_category;
mod spawn_egg;
mod world_event;
pub fn main() {
@ -36,9 +38,11 @@ pub fn main() {
write_generated_file(biome::build(), "biome.rs");
write_generated_file(damage_type::build(), "damage_type.rs");
write_generated_file(message_type::build(), "message_type.rs");
write_generated_file(spawn_egg::build(), "spawn_egg.rs");
write_generated_file(item::build(), "item.rs");
}
pub fn array_to_tokenstream(array: Vec<String>) -> TokenStream {
pub fn array_to_tokenstream(array: &[String]) -> TokenStream {
let mut variants = TokenStream::new();
for item in array.iter() {

View File

@ -3,7 +3,7 @@ use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use serde::Deserialize;
use std::collections::HashMap;
use syn::LitInt;
use syn::{Ident, LitInt};
#[derive(Deserialize)]
struct DamageTypeEntry {
@ -13,10 +13,26 @@ struct DamageTypeEntry {
#[derive(Deserialize)]
pub struct DamageTypeData {
death_message_type: Option<String>,
death_message_type: Option<DeathMessageType>,
exhaustion: f32,
message_id: String,
scaling: String,
scaling: DamageScaling,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum DamageScaling {
Never,
WhenCausedByLivingNonPlayer,
Always,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum DeathMessageType {
Default,
FallVariants,
IntentionalGameDesign,
}
pub(crate) fn build() -> TokenStream {
@ -36,13 +52,20 @@ pub(crate) fn build() -> TokenStream {
let data = &entry.components;
let death_message_type = match &data.death_message_type {
Some(msg) => quote! { Some(#msg) },
Some(msg) => {
let msg_ident = Ident::new(&format!("{:?}", msg), proc_macro2::Span::call_site());
quote! { Some(DeathMessageType::#msg_ident) }
}
None => quote! { None },
};
let exhaustion = data.exhaustion;
let message_id = &data.message_id;
let scaling = &data.scaling;
let scaling_ident = Ident::new(
&format!("{:?}", data.scaling),
proc_macro2::Span::call_site(),
);
let scaling = quote! {DamageScaling::#scaling_ident};
let id_lit = LitInt::new(&entry.id.to_string(), proc_macro2::Span::call_site());
constants.push(quote! {
@ -68,13 +91,27 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DamageType {
pub death_message_type: Option<&'static str>,
pub death_message_type: Option<DeathMessageType>,
pub exhaustion: f32,
pub message_id: &'static str,
pub scaling: &'static str,
pub scaling: DamageScaling,
pub id: u32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DeathMessageType {
Default,
FallVariants,
IntentionalGameDesign,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DamageScaling {
Never,
WhenCausedByLivingNonPlayer,
Always,
}
impl DamageType {
#(#constants)*

View File

@ -8,7 +8,7 @@ pub(crate) fn build() -> TokenStream {
let poses: Vec<String> = serde_json::from_str(include_str!("../../assets/entity_pose.json"))
.expect("Failed to parse entity_pose.json");
let variants = array_to_tokenstream(poses);
let variants = array_to_tokenstream(&poses);
quote! {
#[derive(Clone, Copy)]

View File

@ -9,7 +9,7 @@ pub(crate) fn build() -> TokenStream {
let game_events: Vec<String> =
serde_json::from_str(include_str!("../../assets/game_event.json"))
.expect("Failed to parse game_event.json");
let variants = array_to_tokenstream(game_events);
let variants = array_to_tokenstream(&game_events);
quote! {
pub enum GameEvent {

239
pumpkin-data/build/item.rs Normal file
View File

@ -0,0 +1,239 @@
use heck::ToShoutySnakeCase;
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use serde::Deserialize;
use std::collections::HashMap;
use syn::{Ident, LitInt, LitStr};
#[derive(Deserialize, Clone, Debug)]
pub struct Item {
pub id: u16,
pub components: ItemComponents,
}
#[derive(Deserialize, Clone, Debug)]
pub struct ItemComponents {
#[serde(rename = "minecraft:item_name")]
// TODO: TextComponent
pub item_name: Option<String>,
#[serde(rename = "minecraft:max_stack_size")]
pub max_stack_size: u8,
#[serde(rename = "minecraft:jukebox_playable")]
pub jukebox_playable: Option<JukeboxPlayable>,
#[serde(rename = "minecraft:damage")]
pub damage: Option<u16>,
#[serde(rename = "minecraft:max_damage")]
pub max_damage: Option<u16>,
#[serde(rename = "minecraft:attribute_modifiers")]
pub attribute_modifiers: Option<AttributeModifiers>,
}
impl ToTokens for ItemComponents {
fn to_tokens(&self, tokens: &mut TokenStream) {
let max_stack_size = LitInt::new(&self.max_stack_size.to_string(), Span::call_site());
let jukebox_playable = match &self.jukebox_playable {
Some(playable) => {
let song = LitStr::new(&playable.song, Span::call_site());
quote! { Some(JukeboxPlayable { song: #song }) }
}
None => quote! { None },
};
let item_name = match &self.item_name {
Some(d) => {
let item_name = LitStr::new(d, Span::call_site());
quote! { Some(#item_name) }
}
None => quote! { None },
};
let damage = match self.damage {
Some(d) => {
let damage_lit = LitInt::new(&d.to_string(), Span::call_site());
quote! { Some(#damage_lit) }
}
None => quote! { None },
};
let max_damage = match self.max_damage {
Some(md) => {
let max_damage_lit = LitInt::new(&md.to_string(), Span::call_site());
quote! { Some(#max_damage_lit) }
}
None => quote! { None },
};
let attribute_modifiers = match &self.attribute_modifiers {
Some(modifiers) => {
let modifier_code = modifiers.modifiers.iter().map(|modifier| {
let r#type = LitStr::new(&modifier.r#type, Span::call_site());
let id = LitStr::new(&modifier.id, Span::call_site());
let amount = modifier.amount;
let operation =
Ident::new(&format!("{:?}", modifier.operation), Span::call_site());
let slot = LitStr::new(&modifier.slot, Span::call_site());
quote! {
Modifier {
r#type: #r#type,
id: #id,
amount: #amount,
operation: Operation::#operation,
slot: #slot,
}
}
});
quote! { Some(AttributeModifiers { modifiers: &[#(#modifier_code),*] }) }
}
None => quote! { None },
};
tokens.extend(quote! {
ItemComponents {
item_name: #item_name,
max_stack_size: #max_stack_size,
jukebox_playable: #jukebox_playable,
damage: #damage,
max_damage: #max_damage,
attribute_modifiers: #attribute_modifiers,
}
});
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct JukeboxPlayable {
pub song: String,
}
#[derive(Deserialize, Clone, Debug)]
pub struct AttributeModifiers {
pub modifiers: Vec<Modifier>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct Modifier {
pub r#type: String,
pub id: String,
pub amount: f64,
pub operation: Operation,
// TODO: Make this an enum
pub slot: String,
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::enum_variant_names)]
pub enum Operation {
AddValue,
AddMultipliedBase,
AddMultipliedTotal,
}
pub(crate) fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/items.json");
let items: HashMap<String, Item> =
serde_json::from_str(include_str!("../../assets/items.json"))
.expect("Failed to parse items.json");
let mut type_from_raw_id_arms = TokenStream::new();
let mut type_from_name = TokenStream::new();
let mut constants = TokenStream::new();
for (name, item) in items {
let const_ident = format_ident!("{}", name.to_shouty_snake_case());
let components = &item.components;
let components_tokens = components.to_token_stream();
let id_lit = LitInt::new(&item.id.to_string(), proc_macro2::Span::call_site());
constants.extend(quote! {
pub const #const_ident: Item = Item {
id: #id_lit,
components: #components_tokens
};
});
type_from_raw_id_arms.extend(quote! {
#id_lit => Some(Self::#const_ident),
});
type_from_name.extend(quote! {
#name => Some(Self::#const_ident),
});
}
quote! {
use pumpkin_util::text::TextComponent;
#[derive(Clone, Copy, Debug)]
pub struct Item {
pub id: u16,
pub components: ItemComponents,
}
#[derive(Clone, Copy, Debug)]
pub struct ItemComponents {
pub item_name: Option<&'static str>,
pub max_stack_size: u8,
pub jukebox_playable: Option<JukeboxPlayable>,
pub damage: Option<u16>,
pub max_damage: Option<u16>,
pub attribute_modifiers: Option<AttributeModifiers>,
}
#[derive(Clone, Copy, Debug)]
pub struct JukeboxPlayable {
pub song: &'static str,
}
#[derive(Clone, Copy, Debug)]
pub struct AttributeModifiers {
pub modifiers: &'static [Modifier],
}
#[derive(Clone, Copy, Debug)]
pub struct Modifier {
pub r#type: &'static str,
pub id: &'static str,
pub amount: f64,
pub operation: Operation,
// TODO: Make this an enum
pub slot: &'static str,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Operation {
AddValue,
AddMultipliedBase,
AddMultipliedTotal,
}
impl Item {
#constants
pub fn translated_name(&self) -> TextComponent {
serde_json::from_str(self.components.item_name.unwrap()).expect("Could not parse item name.")
}
#[doc = r" Try to parse a Item from a resource location string"]
pub fn from_name(name: &str) -> Option<Self> {
match name {
#type_from_name
_ => None
}
}
#[doc = r" Try to parse a Item from a raw id"]
pub const fn from_id(id: u16) -> Option<Self> {
match id {
#type_from_raw_id_arms
_ => None
}
}
}
}
}

View File

@ -8,7 +8,7 @@ pub(crate) fn build() -> TokenStream {
let particle: Vec<String> = serde_json::from_str(include_str!("../../assets/particles.json"))
.expect("Failed to parse particles.json");
let variants = array_to_tokenstream(particle);
let variants = array_to_tokenstream(&particle);
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View File

@ -9,7 +9,7 @@ pub(crate) fn build() -> TokenStream {
let sound_categories: Vec<String> =
serde_json::from_str(include_str!("../../assets/scoreboard_display_slot.json"))
.expect("Failed to parse scoreboard_display_slot.json");
let variants = array_to_tokenstream(sound_categories);
let variants = array_to_tokenstream(&sound_categories);
quote! {
pub enum ScoreboardDisplaySlot {

View File

@ -8,7 +8,7 @@ pub(crate) fn build() -> TokenStream {
let screens: Vec<String> = serde_json::from_str(include_str!("../../assets/screens.json"))
.expect("Failed to parse screens.json");
let variants = array_to_tokenstream(screens);
let variants = array_to_tokenstream(&screens);
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View File

@ -9,7 +9,7 @@ pub(crate) fn build() -> TokenStream {
let sound: Vec<String> = serde_json::from_str(include_str!("../../assets/sounds.json"))
.expect("Failed to parse sounds.json");
let variants = array_to_tokenstream(sound.clone());
let variants = array_to_tokenstream(&sound);
let type_from_name = &sound
.iter()

View File

@ -9,7 +9,7 @@ pub(crate) fn build() -> TokenStream {
let sound_categories: Vec<String> =
serde_json::from_str(include_str!("../../assets/sound_category.json"))
.expect("Failed to parse sound_category.json");
let variants = array_to_tokenstream(sound_categories);
let variants = array_to_tokenstream(&sound_categories);
quote! {
#[derive(Clone, Copy)]

View File

@ -0,0 +1,29 @@
use std::collections::HashMap;
use heck::ToShoutySnakeCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
pub(crate) fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/spawn_egg.json");
let eggs: HashMap<u16, String> =
serde_json::from_str(include_str!("../../assets/spawn_egg.json"))
.expect("Failed to parse spawn_egg.json");
let mut names = TokenStream::new();
for (egg, entity) in &eggs {
let entity = entity.to_shouty_snake_case();
let entity = format_ident!("{}", entity);
names.extend(quote! { #egg => Some(EntityType::#entity), });
}
quote! {
pub fn entity_from_egg(id: u16) -> Option<EntityType> {
match id {
#names
_ => None
}
}
}
}

View File

@ -1,3 +1,7 @@
pub mod item {
include!(concat!(env!("OUT_DIR"), "/item.rs"));
}
pub mod packet {
include!(concat!(env!("OUT_DIR"), "/packet.rs"));
}
@ -26,6 +30,7 @@ pub mod game_event {
}
pub mod entity {
include!(concat!(env!("OUT_DIR"), "/spawn_egg.rs"));
include!(concat!(env!("OUT_DIR"), "/entity_type.rs"));
include!(concat!(env!("OUT_DIR"), "/entity_pose.rs"));
}

View File

@ -1,7 +1,7 @@
use pumpkin_data::item::Item;
use pumpkin_registry::{
flatten_3x3, get_tag_values, IngredientSlot, IngredientType, RecipeResult, TagCategory, RECIPES,
};
use pumpkin_world::item::registry::get_item;
use pumpkin_world::item::ItemStack;
use rayon::prelude::*;
@ -17,7 +17,9 @@ fn check_ingredient_type(ingredient_type: &IngredientType, input: ItemStack) ->
.iter()
.any(|tag| check_ingredient_type(&tag.to_ingredient_type(), input))
}
IngredientType::Item(item) => get_item(item).is_some_and(|item| item.id == input.item_id),
IngredientType::Item(item) => {
Item::from_name(item).is_some_and(|item| item.id == input.item.id)
}
}
}
@ -52,11 +54,11 @@ pub fn check_if_matches_crafting(input: [[Option<ItemStack>; 3]; 3]) -> Option<I
})
.map(|recipe| match recipe.result() {
RecipeResult::Single { id, .. } => Some(ItemStack {
item_id: get_item(id).unwrap().id,
item: Item::from_name(id).unwrap(),
item_count: 1,
}),
RecipeResult::Many { id, count, .. } => Some(ItemStack {
item_id: get_item(id).unwrap().id,
item: Item::from_name(id).unwrap(),
item_count: *count,
}),
RecipeResult::Special => None,

View File

@ -91,7 +91,7 @@ impl DragHandler {
single_item.item_count = 1;
let changing_slots =
drag.possibly_changing_slots(&slots_cloned, carried_item.item_id);
drag.possibly_changing_slots(&slots_cloned, carried_item.item.id);
changing_slots.for_each(|slot| {
if carried_item.item_count != 0 {
carried_item.item_count -= 1;
@ -117,7 +117,7 @@ impl DragHandler {
// In that specific case, follow MouseDragType::Right behaviours instead!
let changing_slots =
drag.possibly_changing_slots(&slots_cloned, carried_item.item_id);
drag.possibly_changing_slots(&slots_cloned, carried_item.item.id);
let amount_of_slots = changing_slots.clone().count();
let (amount_per_slot, remainder) = if amount_of_slots == 0 {
// TODO: please work lol
@ -160,7 +160,7 @@ impl Drag {
match slot {
Some(item_slot) => {
if item_slot.item_id == carried_item_id {
if item_slot.item.id == carried_item_id {
Some(*slot_index)
} else {
None

View File

@ -36,7 +36,7 @@ pub trait Container: Sync + Send {
if taking_crafted {
match (all_slots[slot].as_mut(), carried_item.as_mut()) {
(Some(s1), Some(s2)) => {
if s1.item_id == s2.item_id {
if s1.item.id == s2.item.id {
handle_item_change(all_slots[slot], carried_item, mouse_click);
}
}
@ -152,7 +152,7 @@ pub fn handle_item_change(
match (current_slot.as_mut(), carried_slot.as_mut()) {
// Swap or combine current and carried
(Some(current), Some(carried)) => {
if current.item_id == carried.item_id {
if current.item.id == carried.item.id {
combine_stacks(carried_slot, current, mouse_click);
} else if mouse_click == MouseClick::Left {
let carried = *carried;

View File

@ -135,7 +135,7 @@ impl PlayerInventory {
pub fn get_slot_with_item(&self, item_id: u16, max_stack: u8) -> Option<usize> {
for slot in 9..=44 {
match &self.items[slot - 9] {
Some(item) if item.item_id == item_id && item.item_count <= max_stack => {
Some(item) if item.item.id == item_id && item.item_count <= max_stack => {
return Some(slot)
}
_ => continue,

View File

@ -7,6 +7,8 @@ edition.workspace = true
proc-macro = true
[dependencies]
pumpkin-data = { path = "../pumpkin-data" }
serde.workspace = true
serde_json.workspace = true

View File

@ -1,4 +1,5 @@
use proc_macro::TokenStream;
use pumpkin_data::item::Item;
use quote::quote;
use syn::{
parse::{Nothing, Parser},
@ -222,18 +223,15 @@ pub fn pumpkin_item(input: TokenStream, item: TokenStream) -> TokenStream {
let input_string = input.to_string();
let packet_name = input_string.trim_matches('"');
let packet_name_split: Vec<&str> = packet_name.split(":").collect();
let namespace = packet_name_split[0];
let id = packet_name_split[1];
let item_id = Item::from_name(packet_name).unwrap();
let id = item_id.id;
let item: proc_macro2::TokenStream = item.into();
let gen = quote! {
#item
impl #impl_generics crate::item::pumpkin_item::ItemMetadata for #name #ty_generics {
const NAMESPACE: &'static str = #namespace;
const ID: &'static str = #id;
const ID: u16 = #id;
}
};

View File

@ -1,4 +1,5 @@
use crate::VarInt;
use pumpkin_data::item::Item;
use pumpkin_world::item::ItemStack;
use serde::ser::SerializeSeq;
use serde::{
@ -131,8 +132,9 @@ impl Serialize for Slot {
impl Slot {
pub fn to_item(self) -> Option<ItemStack> {
let item_id = self.item_id?.0.try_into().unwrap();
let item = Item::from_id(item_id)?;
Some(ItemStack {
item_id,
item,
item_count: self.item_count.0.try_into().unwrap(),
})
}
@ -153,7 +155,7 @@ impl From<&ItemStack> for Slot {
fn from(item: &ItemStack) -> Self {
Slot {
item_count: item.item_count.into(),
item_id: Some(VarInt(item.item_id as i32)),
item_id: Some(VarInt(item.item.id as i32)),
// TODO: add these
num_components_to_add: None,
num_components_to_remove: None,

View File

@ -10,7 +10,7 @@ impl ItemStack {
838, // Diamond
843, // Netherite
]
.contains(&self.item_id)
.contains(&self.item.id)
}
pub fn is_helmet(&self) -> bool {
@ -24,7 +24,7 @@ impl ItemStack {
872, // Iron
864,
]
.contains(&self.item_id)
.contains(&self.item.id)
}
pub fn is_chestplate(&self) -> bool {
@ -38,7 +38,7 @@ impl ItemStack {
865, // Elytra
773,
]
.contains(&self.item_id)
.contains(&self.item.id)
}
pub fn is_leggings(&self) -> bool {
@ -51,7 +51,7 @@ impl ItemStack {
874, // Iron
866,
]
.contains(&self.item_id)
.contains(&self.item.id)
}
pub fn is_boots(&self) -> bool {
@ -64,6 +64,6 @@ impl ItemStack {
875, // Iron
867,
]
.contains(&self.item_id)
.contains(&self.item.id)
}
}

View File

@ -1,6 +1,6 @@
use pumpkin_data::item::Item;
mod categories;
pub mod registry;
pub use registry::ITEMS;
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
/// Item Rarity
@ -13,23 +13,18 @@ pub enum Rarity {
#[derive(Clone, Copy, Debug)]
pub struct ItemStack {
// This ID is the numerical protocol ID, not the usual minecraft::block ID.
pub item_id: u16,
pub item_count: u8,
// TODO: Add Item Components
pub item: Item,
}
impl PartialEq for ItemStack {
fn eq(&self, other: &Self) -> bool {
self.item_id == other.item_id
self.item.id == other.item.id
}
}
impl ItemStack {
pub fn new(item_count: u8, item_id: u16) -> Self {
Self {
item_count,
item_id,
}
pub fn new(item_count: u8, item: Item) -> Self {
Self { item_count, item }
}
}

View File

@ -1,98 +0,0 @@
use std::collections::HashMap;
use std::sync::LazyLock;
use pumpkin_util::text::TextComponent;
use serde::Deserialize;
const ITEMS_JSON: &str = include_str!("../../../assets/items.json");
pub static ITEMS: LazyLock<HashMap<String, Item>> = LazyLock::new(|| {
serde_json::from_str(ITEMS_JSON).expect("Could not parse items.json registry.")
});
pub static ITEMS_REGISTRY_NAME_BY_ID: LazyLock<HashMap<u16, String>> = LazyLock::new(|| {
let mut map = HashMap::new();
for item in ITEMS.clone() {
map.insert(item.1.id, item.0.clone());
}
map
});
pub fn get_item(name: &str) -> Option<&Item> {
ITEMS.get(&name.replace("minecraft:", ""))
}
pub fn get_name_by_id<'a>(item_id: u16) -> Option<&'a String> {
ITEMS_REGISTRY_NAME_BY_ID.get(&item_id)
}
pub fn get_item_by_id<'a>(item_id: u16) -> Option<&'a Item> {
ITEMS.values().find(|&item| item.id == item_id)
}
pub fn get_spawn_egg(item_id: u16) -> Option<String> {
if let Some(item_name) = ITEMS_REGISTRY_NAME_BY_ID.get(&item_id) {
if item_name.ends_with("_spawn_egg") {
if let Some(res) = item_name.strip_suffix("_spawn_egg") {
return Some(res.to_owned());
}
}
};
None
}
#[derive(Deserialize, Clone, Debug)]
pub struct Item {
pub id: u16,
pub components: ItemComponents,
}
impl Item {
pub fn translated_name(&self) -> TextComponent {
serde_json::from_str(&self.components.item_name).expect("Could not parse item name.")
}
}
#[derive(Deserialize, Clone, Debug)]
pub struct ItemComponents {
#[serde(rename = "minecraft:item_name")]
pub item_name: String,
#[serde(rename = "minecraft:max_stack_size")]
pub max_stack_size: u8,
#[serde(rename = "minecraft:jukebox_playable")]
pub jukebox_playable: Option<JukeboxPlayable>,
#[serde(rename = "minecraft:damage")]
pub damage: Option<u16>,
#[serde(rename = "minecraft:max_damage")]
pub max_damage: Option<u16>,
#[serde(rename = "minecraft:attribute_modifiers")]
pub attribute_modifiers: Option<AttributeModifiers>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct JukeboxPlayable {
pub song: String,
}
#[derive(Deserialize, Clone, Debug)]
pub struct AttributeModifiers {
pub modifiers: Vec<Modifier>,
}
#[derive(Deserialize, Clone, Debug)]
pub struct Modifier {
pub r#type: String,
pub id: String,
pub amount: f64,
pub operation: Operation,
// TODO: Make this an enum
pub slot: String,
}
#[derive(Deserialize, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Operation {
AddValue,
AddMultipliedBase,
AddMultipliedTotal,
}

View File

@ -1,4 +1,5 @@
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_data::{
screen::WindowType,
sound::{Sound, SoundCategory},
@ -7,10 +8,7 @@ use pumpkin_inventory::{Chest, OpenContainer};
use pumpkin_macros::pumpkin_block;
use pumpkin_protocol::{client::play::CBlockAction, codec::var_int::VarInt};
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::{
block::registry::{get_block, Block},
item::registry::Item,
};
use pumpkin_world::block::registry::{get_block, Block};
use crate::{
block::{pumpkin_block::PumpkinBlock, registry::BlockActionResult},

View File

@ -3,11 +3,12 @@ use crate::block::registry::BlockActionResult;
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_data::screen::WindowType;
use pumpkin_inventory::{CraftingTable, OpenContainer};
use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::{block::registry::Block, item::registry::Item};
use pumpkin_world::block::registry::Block;
#[pumpkin_block("minecraft:crafting_table")]
pub struct CraftingTableBlock;

View File

@ -1,12 +1,12 @@
use crate::block::registry::BlockActionResult;
use crate::entity::player::Player;
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_data::screen::WindowType;
use pumpkin_inventory::Furnace;
use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::Item;
use crate::{block::pumpkin_block::PumpkinBlock, server::Server};

View File

@ -3,11 +3,11 @@ use crate::block::registry::BlockActionResult;
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_macros::pumpkin_block;
use pumpkin_registry::SYNCED_REGISTRIES;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::Item;
#[pumpkin_block("minecraft:jukebox")]
pub struct JukeboxBlock;

View File

@ -2,6 +2,7 @@ use blocks::chest::ChestBlock;
use blocks::furnace::FurnaceBlock;
use properties::BlockPropertiesManager;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::block::registry::Block;
@ -43,7 +44,10 @@ pub async fn drop_loot(server: &Server, world: &Arc<World>, block: &Block, pos:
);
let entity = server.add_entity(pos, EntityType::ITEM, world);
let item_entity = Arc::new(ItemEntity::new(entity, &ItemStack::new(1, block.item_id)));
let item_entity = Arc::new(ItemEntity::new(
entity,
&ItemStack::new(1, Item::from_id(block.item_id).unwrap()),
));
world.spawn_entity(item_entity.clone()).await;
item_entity.send_meta_packet().await;
}

View File

@ -2,10 +2,10 @@ use crate::block::registry::BlockActionResult;
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_inventory::OpenContainer;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::Item;
pub trait BlockMetadata {
const NAMESPACE: &'static str;

View File

@ -1,10 +1,10 @@
use crate::block::pumpkin_block::{BlockMetadata, PumpkinBlock};
use crate::entity::player::Player;
use crate::server::Server;
use pumpkin_data::item::Item;
use pumpkin_inventory::OpenContainer;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::Item;
use std::collections::HashMap;
use std::sync::Arc;

View File

@ -52,7 +52,7 @@ impl ArgumentConsumer for DamageTypeArgumentConsumer {
impl DefaultNameArgConsumer for DamageTypeArgumentConsumer {
fn default_name(&self) -> &'static str {
"damage_type"
"damageType"
}
}

View File

@ -1,6 +1,6 @@
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders};
use pumpkin_world::item::registry::{self, Item};
use crate::{command::dispatcher::CommandError, server::Server};
@ -53,11 +53,11 @@ impl DefaultNameArgConsumer for ItemArgumentConsumer {
}
impl<'a> FindArg<'a> for ItemArgumentConsumer {
type Data = (&'a str, &'a Item);
type Data = (&'a str, Item);
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Item(name)) => registry::get_item(name).map_or_else(
Some(Arg::Item(name)) => Item::from_name(&name.replace("minecraft:", "")).map_or_else(
|| {
Err(CommandError::GeneralCommandIssue(format!(
"Item {name} does not exist."

View File

@ -56,7 +56,6 @@ impl CommandExecutor for GiveExecutor {
for target in targets {
target.give_items(item, item_count as u32).await;
}
let msg = if targets.len() == 1 {
TextComponent::translate(
"commands.give.success.single",

View File

@ -22,7 +22,7 @@ impl ItemEntity {
let slot = Slot::from(stack);
Self {
entity,
id: stack.item_id,
id: stack.item.id,
count: stack.item_count,
item: slot,
pickup_delay: AtomicI8::new(10), // Vanilla

View File

@ -13,6 +13,7 @@ use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG};
use pumpkin_data::{
damage::DamageType,
entity::EntityType,
item::Operation,
sound::{Sound, SoundCategory},
};
use pumpkin_inventory::player::PlayerInventory;
@ -57,13 +58,7 @@ use pumpkin_util::{
text::TextComponent,
GameMode,
};
use pumpkin_world::{
cylindrical_chunk_iterator::Cylindrical,
item::{
registry::{get_item_by_id, Operation},
ItemStack,
},
};
use pumpkin_world::{cylindrical_chunk_iterator::Cylindrical, item::ItemStack};
use tokio::sync::{Mutex, Notify, RwLock};
use super::{
@ -294,17 +289,15 @@ impl Player {
// get attack damage
if let Some(item_stack) = item_slot {
if let Some(item) = get_item_by_id(item_stack.item_id) {
// TODO: this should be cached in memory
if let Some(modifiers) = &item.components.attribute_modifiers {
for item_mod in &modifiers.modifiers {
if item_mod.operation == Operation::AddValue {
if item_mod.id == "minecraft:base_attack_damage" {
add_damage = item_mod.amount;
}
if item_mod.id == "minecraft:base_attack_speed" {
add_speed = item_mod.amount;
}
// TODO: this should be cached in memory
if let Some(modifiers) = &item_stack.item.components.attribute_modifiers {
for item_mod in modifiers.modifiers {
if item_mod.operation == Operation::AddValue {
if item_mod.id == "minecraft:base_attack_damage" {
add_damage = item_mod.amount;
}
if item_mod.id == "minecraft:base_attack_speed" {
add_speed = item_mod.amount;
}
}
}

View File

@ -6,10 +6,11 @@ use crate::item::pumpkin_item::PumpkinItem;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::sound::Sound;
use pumpkin_macros::pumpkin_item;
use pumpkin_world::item::registry::Item;
#[pumpkin_item("minecraft:egg")]
#[pumpkin_item("egg")]
pub struct EggItem;
const POWER: f32 = 1.5;

View File

@ -6,10 +6,11 @@ use crate::item::pumpkin_item::PumpkinItem;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::sound::Sound;
use pumpkin_macros::pumpkin_item;
use pumpkin_world::item::registry::Item;
#[pumpkin_item("minecraft:snowball")]
#[pumpkin_item("snowball")]
pub struct SnowBallItem;
const POWER: f32 = 1.5;

View File

@ -1,16 +1,12 @@
use crate::entity::player::Player;
use crate::server::Server;
use async_trait::async_trait;
use pumpkin_data::item::Item;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::Item;
pub trait ItemMetadata {
const NAMESPACE: &'static str;
const ID: &'static str;
fn name(&self) -> String {
format!("{}:{}", Self::NAMESPACE, Self::ID)
}
const ID: u16;
}
#[async_trait]

View File

@ -1,8 +1,8 @@
use crate::entity::player::Player;
use crate::server::Server;
use pumpkin_data::item::Item;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::Item;
use std::collections::HashMap;
use std::sync::Arc;
@ -10,16 +10,16 @@ use super::pumpkin_item::{ItemMetadata, PumpkinItem};
#[derive(Default)]
pub struct ItemRegistry {
blocks: HashMap<String, Arc<dyn PumpkinItem>>,
items: HashMap<u16, Arc<dyn PumpkinItem>>,
}
impl ItemRegistry {
pub fn register<T: PumpkinItem + ItemMetadata + 'static>(&mut self, block: T) {
self.blocks.insert(block.name(), Arc::new(block));
pub fn register<T: PumpkinItem + ItemMetadata + 'static>(&mut self, item: T) {
self.items.insert(T::ID, Arc::new(item));
}
pub async fn on_use(&self, item_name: &str, item: &Item, player: &Player, server: &Server) {
let pumpkin_block = self.get_pumpkin_item(item_name);
pub async fn on_use(&self, item: &Item, player: &Player, server: &Server) {
let pumpkin_block = self.get_pumpkin_item(item.id);
if let Some(pumpkin_block) = pumpkin_block {
pumpkin_block.normal_use(item, player, server).await;
}
@ -27,23 +27,22 @@ impl ItemRegistry {
pub async fn use_on_block(
&self,
item_name: &str,
item: &Item,
player: &Player,
location: BlockPos,
block: &Block,
server: &Server,
) {
let pumpkin_block = self.get_pumpkin_item(item_name);
if let Some(pumpkin_block) = pumpkin_block {
return pumpkin_block
let pumpkin_item = self.get_pumpkin_item(item.id);
if let Some(pumpkin_item) = pumpkin_item {
return pumpkin_item
.use_on_block(item, player, location, block, server)
.await;
}
}
#[must_use]
pub fn get_pumpkin_item(&self, item_name: &str) -> Option<&Arc<dyn PumpkinItem>> {
self.blocks.get(format!("minecraft:{item_name}").as_str())
pub fn get_pumpkin_item(&self, item_id: u16) -> Option<&Arc<dyn PumpkinItem>> {
self.items.get(&item_id)
}
}

View File

@ -1,5 +1,6 @@
use crate::entity::player::Player;
use crate::server::Server;
use pumpkin_data::item::Item;
use pumpkin_data::screen::WindowType;
use pumpkin_inventory::container_click::{
Click, ClickType, KeyClick, MouseClick, MouseDragState, MouseDragType,
@ -16,7 +17,6 @@ use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::server::play::SClickContainer;
use pumpkin_util::text::TextComponent;
use pumpkin_util::GameMode;
use pumpkin_world::item::registry::Item;
use pumpkin_world::item::ItemStack;
use std::sync::Arc;
@ -323,7 +323,7 @@ impl Player {
let find_condition = |(slot_number, slot): (usize, &mut Option<ItemStack>)| {
// TODO: Check for max item count here
match slot {
Some(item) => (item.item_id == item_in_pressed_slot.item_id
Some(item) => (item.item.id == item_in_pressed_slot.item.id
&& item.item_count != 64)
.then_some(slot_number),
None => Some(slot_number),
@ -411,7 +411,7 @@ impl Player {
**item = None;
for slot in slots.iter_mut().filter_map(|slot| slot.as_mut()) {
if slot.item_id == carried_item.item_id {
if slot.item.id == carried_item.item.id {
// TODO: Check for max stack size
if slot.item_count + carried_item.item_count <= 64 {
slot.item_count = 0;
@ -553,14 +553,14 @@ impl Player {
}
}
async fn pickup_items(&self, item: &Item, mut amount: u32) {
async fn pickup_items(&self, item: Item, mut amount: u32) {
let max_stack = item.components.max_stack_size;
let mut inventory = self.inventory().lock().await;
let slots = inventory.slots_with_hotbar_first();
let matching_slots = slots.filter_map(|slot| {
if let Some(item_slot) = slot.as_mut() {
(item_slot.item_id == item.id && item_slot.item_count < max_stack).then(|| {
(item_slot.item.id == item.id && item_slot.item_count < max_stack).then(|| {
let item_count = item_slot.item_count;
(item_slot, item_count)
})
@ -577,12 +577,12 @@ impl Player {
if let Some(amount_left) = amount.checked_sub(u32::from(amount_to_add)) {
amount = amount_left;
*slot = ItemStack {
item_id: item.id,
item,
item_count: item.components.max_stack_size,
};
} else {
*slot = ItemStack {
item_id: item.id,
item,
item_count: max_stack - (amount_to_add - amount as u8),
};
return;
@ -599,12 +599,12 @@ impl Player {
if let Some(remaining_amount) = amount.checked_sub(u32::from(max_stack)) {
amount = remaining_amount;
*slot = Some(ItemStack {
item_id: item.id,
item,
item_count: max_stack,
});
} else {
*slot = Some(ItemStack {
item_id: item.id,
item,
item_count: amount as u8,
});
return;
@ -618,7 +618,7 @@ impl Player {
/// Add items to inventory if there's space, else drop them to the ground.
///
/// This method automatically syncs changes with the client.
pub async fn give_items(&self, item: &Item, amount: u32) {
pub async fn give_items(&self, item: Item, amount: u32) {
self.pickup_items(item, amount).await;
self.set_container_content(None).await;
}

View File

@ -13,7 +13,8 @@ use crate::{
world::chunker,
};
use pumpkin_config::ADVANCED_CONFIG;
use pumpkin_data::entity::{EntityPose, EntityType};
use pumpkin_data::entity::{entity_from_egg, EntityPose, EntityType};
use pumpkin_data::item::Item;
use pumpkin_data::world::CHAT;
use pumpkin_inventory::player::PlayerInventory;
use pumpkin_inventory::InventoryError;
@ -49,12 +50,8 @@ use pumpkin_util::{
use pumpkin_world::block::interactive::sign::Sign;
use pumpkin_world::block::registry::get_block_collision_shapes;
use pumpkin_world::block::registry::Block;
use pumpkin_world::item::registry::{get_item_by_id, get_name_by_id};
use pumpkin_world::block::{registry::get_block_by_item, BlockDirection};
use pumpkin_world::item::ItemStack;
use pumpkin_world::{
block::{registry::get_block_by_item, BlockDirection},
item::registry::get_spawn_egg,
};
use pumpkin_world::{WORLD_LOWEST_Y, WORLD_MAX_Y};
use thiserror::Error;
@ -464,7 +461,7 @@ impl Player {
}
None if self.gamemode.load() == GameMode::Creative => {
// Case where item is not present, if in creative mode create the item
let item_stack = ItemStack::new(1, block.item_id);
let item_stack = ItemStack::new(1, Item::from_id(block.item_id).unwrap());
let slot_data = Slot::from(&item_stack);
self.update_single_slot(&mut inventory, dest_slot as i16 + 36, slot_data)
.await;
@ -975,11 +972,6 @@ impl Player {
}
return Ok(());
};
let Some(item) = get_item_by_id(stack.item_id) else {
// If there is an item stack, there should be an item
return Err(BlockPlacingError::InventoryInvalid.into());
};
if !self
.living_entity
.entity
@ -988,7 +980,7 @@ impl Player {
{
let action_result = server
.block_registry
.use_with_item(block, self, location, item, server)
.use_with_item(block, self, location, &stack.item, server)
.await;
match action_result {
BlockActionResult::Continue => {}
@ -998,16 +990,16 @@ impl Player {
}
}
// check if item is a block, Because Not every item can be placed :D
if let Some(block) = get_block_by_item(stack.item_id) {
if let Some(block) = get_block_by_item(stack.item.id) {
should_try_decrement = self
.run_is_block_place(block.clone(), server, use_item_on, location, &face)
.await?;
}
// check if item is a spawn egg
if let Some(item_t) = get_spawn_egg(stack.item_id) {
should_try_decrement = self
.run_is_spawn_egg(item_t, server, location, &face)
.await?;
if let Some(entity) = entity_from_egg(stack.item.id) {
self.spawn_entity_from_egg(entity, server, location, &face)
.await;
should_try_decrement = true;
};
if should_try_decrement {
@ -1062,11 +1054,7 @@ impl Player {
return;
}
if let Some(held) = self.inventory().lock().await.held_item() {
// this is so ugly and slow brooo...
if let Some(item) = get_item_by_id(held.item_id) {
let name = get_name_by_id(item.id).unwrap();
server.item_registry.on_use(name, item, self, server).await;
}
server.item_registry.on_use(&held.item, self, server).await;
}
}
@ -1170,48 +1158,40 @@ impl Player {
);
}
async fn run_is_spawn_egg(
async fn spawn_entity_from_egg(
&self,
item_t: String,
entity_type: EntityType,
server: &Server,
location: BlockPos,
face: &BlockDirection,
) -> Result<bool, Box<dyn PumpkinError>> {
// checks if spawn egg has a corresponding entity name
if let Some(entity_type) = EntityType::from_name(&item_t) {
let world_pos = BlockPos(location.0 + face.to_offset());
// align position like Vanilla does
let pos = Vector3::new(
f64::from(world_pos.0.x) + 0.5,
f64::from(world_pos.0.y),
f64::from(world_pos.0.z) + 0.5,
);
// create rotation like Vanilla
let yaw = wrap_degrees(rand::random::<f32>() * 360.0) % 360.0;
) {
let world_pos = BlockPos(location.0 + face.to_offset());
// align position like Vanilla does
let pos = Vector3::new(
f64::from(world_pos.0.x) + 0.5,
f64::from(world_pos.0.y),
f64::from(world_pos.0.z) + 0.5,
);
// create rotation like Vanilla
let yaw = wrap_degrees(rand::random::<f32>() * 360.0) % 360.0;
let world = self.world();
// create new mob and uuid based on spawn egg id
let mob = mob::from_type(
EntityType::from_raw(entity_type.id).unwrap(),
server,
pos,
world,
)
.await;
let world = self.world();
// create new mob and uuid based on spawn egg id
let mob = mob::from_type(
EntityType::from_raw(entity_type.id).unwrap(),
server,
pos,
world,
)
.await;
// set the rotation
mob.get_entity().set_rotation(yaw, 0.0);
// set the rotation
mob.get_entity().set_rotation(yaw, 0.0);
// broadcast new mob to all players
world.spawn_entity(mob).await;
// broadcast new mob to all players
world.spawn_entity(mob).await;
// TODO: send/configure additional commands/data based on type of entity (horse, slime, etc)
} else {
// TODO: maybe include additional error types
return Ok(false);
};
Ok(true)
// TODO: send/configure additional commands/data based on type of entity (horse, slime, etc)
}
fn get_player_direction(&self) -> Direction {