fix: anvil

This commit is contained in:
Alexander Medvedev 2026-08-31 16:47:23 +02:00
parent 39886a6c55
commit fdb6da2b0d
72 changed files with 27985 additions and 5446 deletions

134
Cargo.lock generated
View File

@ -1786,10 +1786,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1799,11 +1797,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"rand_core 0.10.1", "rand_core 0.10.1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -2062,22 +2058,6 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-rustls"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]] [[package]]
name = "hyper-timeout" name = "hyper-timeout"
version = "0.5.2" version = "0.5.2"
@ -2466,12 +2446,6 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lz4-java-wrc" name = "lz4-java-wrc"
version = "0.2.0" version = "0.2.0"
@ -3591,62 +3565,6 @@ version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "quinn"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp 0.5.15",
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
dependencies = [
"bytes",
"getrandom 0.4.3",
"lru-slab",
"rand",
"rand_pcg",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"tracing",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "quinn-udp" name = "quinn-udp"
version = "0.6.1" version = "0.6.1"
@ -3710,15 +3628,6 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_pcg"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
dependencies = [
"rand_core 0.10.1",
]
[[package]] [[package]]
name = "rayon" name = "rayon"
version = "1.12.0" version = "1.12.0"
@ -3828,35 +3737,28 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.28" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls",
"tower", "tower",
"tower-http", "tower-http",
"tower-service", "tower-service",
@ -3864,7 +3766,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots",
] ]
[[package]] [[package]]
@ -4309,7 +4210,6 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@ -4355,12 +4255,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888" checksum = "a252f5e20f038fe7b4ea53e073e65398d652c864cc162fc77c56c2f13717b888"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -4484,18 +4378,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serdect" name = "serdect"
version = "0.4.3" version = "0.4.3"
@ -5778,16 +5660,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.9" version = "1.0.9"
@ -5810,7 +5682,7 @@ dependencies = [
"event-listener", "event-listener",
"futures", "futures",
"log", "log",
"quinn-udp 0.6.1", "quinn-udp",
"rtc", "rtc",
"tokio", "tokio",
] ]

View File

@ -209,7 +209,7 @@ time = { version = "0.3", default-features = false, features = ["std"] }
arc-swap = { version = "1.9", default-features = false } arc-swap = { version = "1.9", default-features = false }
tokio-util = { version = "0.7.19", default-features = false } tokio-util = { version = "0.7.19", default-features = false }
toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] } toml = { version = "1.1", default-features = false, features = ["parse", "display", "serde"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json", "http2"] } reqwest = { version = "0.13.4", default-features = false, features = ["json"] }
notify = { version = "8.2.0", default-features = false, features = ["macos_fsevent"] } notify = { version = "8.2.0", default-features = false, features = ["macos_fsevent"] }
sysinfo = { version = "0.39", default-features = false, features = ["system"] } sysinfo = { version = "0.39", default-features = false, features = ["system"] }

View File

@ -207,9 +207,77 @@ impl DataComponentImpl for LoreImpl {
default_impl!(Lore); default_impl!(Lore);
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct RarityImpl; pub enum Rarity {
#[default]
Common = 0,
Uncommon = 1,
Rare = 2,
Epic = 3,
}
impl Rarity {
#[must_use]
pub fn from_id(id: i32) -> Option<Self> {
match id {
0 => Some(Self::Common),
1 => Some(Self::Uncommon),
2 => Some(Self::Rare),
3 => Some(Self::Epic),
_ => None,
}
}
#[must_use]
pub fn to_id(self) -> i32 {
self as i32
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"common" => Some(Self::Common),
"uncommon" => Some(Self::Uncommon),
"rare" => Some(Self::Rare),
"epic" => Some(Self::Epic),
_ => None,
}
}
#[must_use]
pub fn to_name(self) -> &'static str {
match self {
Self::Common => "common",
Self::Uncommon => "uncommon",
Self::Rare => "rare",
Self::Epic => "epic",
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct RarityImpl {
pub rarity: Rarity,
}
impl RarityImpl {
pub fn read_data(data: &NbtTag) -> Option<Self> {
let name = data.extract_string()?;
Some(Self {
rarity: Rarity::from_name(name)?,
})
}
}
impl DataComponentImpl for RarityImpl { impl DataComponentImpl for RarityImpl {
fn write_data(&self) -> NbtTag {
NbtTag::String(self.rarity.to_name().into())
}
fn get_hash(&self) -> i32 {
crate::data_component_impl::get_i32_hash(self.rarity.to_id()) as i32
}
default_impl!(Rarity); default_impl!(Rarity);
} }

View File

@ -7,7 +7,10 @@ use crate::data_component_impl::{
get_idset_hash, get_str_hash, put_idor, get_idset_hash, get_str_hash, put_idor,
}; };
use crate::entity_type::EntityType; use crate::entity_type::EntityType;
use crate::item::Item;
use crate::item_stack::ItemStack;
use crate::sound::Sound; use crate::sound::Sound;
use crate::tag::Taggable;
use crc_fast::CrcAlgorithm::Crc32Iscsi; use crc_fast::CrcAlgorithm::Crc32Iscsi;
use crc_fast::Digest; use crc_fast::Digest;
use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::compound::NbtCompound;
@ -134,9 +137,23 @@ impl DataComponentImpl for CanBreakImpl {
default_impl!(CanBreak); default_impl!(CanBreak);
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
pub struct RepairCostImpl; pub struct RepairCostImpl {
pub cost: i32,
}
impl RepairCostImpl {
pub const DEFAULT: Self = Self { cost: 0 };
pub fn read_data(data: &NbtTag) -> Option<Self> {
Some(Self {
cost: data.extract_int()?,
})
}
}
impl DataComponentImpl for RepairCostImpl { impl DataComponentImpl for RepairCostImpl {
fn write_data(&self) -> NbtTag {
NbtTag::Int(self.cost)
}
default_impl!(RepairCost); default_impl!(RepairCost);
} }
@ -608,9 +625,49 @@ impl DataComponentImpl for EquippableImpl {
default_impl!(Equippable); default_impl!(Equippable);
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq)]
pub struct RepairableImpl; pub struct RepairableImpl {
pub items: IDSet<Item>,
}
impl Hash for RepairableImpl {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
get_idset_hash(&self.items).hash(state);
}
}
impl RepairableImpl {
#[must_use]
pub fn is_valid_repair_item(&self, repair_item: &ItemStack) -> bool {
if repair_item.is_empty() {
return false;
}
match &self.items {
IDSet::Tag(tag) => repair_item.item.is_tagged_with(tag).unwrap_or(false),
IDSet::IDs(items) => items.iter().any(|item| item.id == repair_item.item.id),
}
}
pub fn read_data(data: &NbtTag) -> Option<Self> {
if let NbtTag::Compound(c) = data {
let items_tag = c.get("items")?;
let items = IDSet::read(items_tag)?;
Some(Self { items })
} else if let Some(items) = IDSet::read(data) {
Some(Self { items })
} else {
None
}
}
}
impl DataComponentImpl for RepairableImpl { impl DataComponentImpl for RepairableImpl {
fn write_data(&self) -> NbtTag {
let mut compound = NbtCompound::new();
self.items.write(&mut compound, "items");
NbtTag::Compound(compound)
}
default_impl!(Repairable); default_impl!(Repairable);
} }
@ -652,9 +709,102 @@ impl DataComponentImpl for KineticWeaponImpl {
default_impl!(KineticWeapon); default_impl!(KineticWeapon);
} }
#[derive(Clone, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct SwingAnimationImpl; pub enum SwingAnimationType {
#[default]
Whack = 0,
Stab = 1,
None = 2,
}
impl SwingAnimationType {
#[must_use]
pub fn from_id(id: i32) -> Option<Self> {
match id {
0 => Some(Self::Whack),
1 => Some(Self::Stab),
2 => Some(Self::None),
_ => None,
}
}
#[must_use]
pub fn to_id(&self) -> i32 {
*self as i32
}
#[must_use]
pub fn to_name(&self) -> &'static str {
match self {
Self::Whack => "whack",
Self::Stab => "stab",
Self::None => "none",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"whack" => Some(Self::Whack),
"stab" => Some(Self::Stab),
"none" => Some(Self::None),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct SwingAnimationImpl {
pub animation_type: SwingAnimationType,
pub duration: i32,
}
impl Default for SwingAnimationImpl {
fn default() -> Self {
Self::DEFAULT
}
}
impl SwingAnimationImpl {
pub const DEFAULT: Self = Self {
animation_type: SwingAnimationType::Whack,
duration: 6,
};
pub fn read_data(data: &NbtTag) -> Option<Self> {
match data {
NbtTag::Compound(compound) => {
let animation_type = compound
.get("type")
.and_then(|tag| match tag {
NbtTag::String(name) => SwingAnimationType::from_name(name),
NbtTag::Int(id) => SwingAnimationType::from_id(*id),
NbtTag::Byte(id) => SwingAnimationType::from_id(*id as i32),
_ => None,
})
.unwrap_or(Self::DEFAULT.animation_type);
let duration = compound
.get("duration")
.and_then(|tag| tag.extract_int())
.unwrap_or(Self::DEFAULT.duration);
Some(Self {
animation_type,
duration,
})
}
_ => Some(Self::DEFAULT),
}
}
}
impl DataComponentImpl for SwingAnimationImpl { impl DataComponentImpl for SwingAnimationImpl {
fn write_data(&self) -> NbtTag {
let mut compound = NbtCompound::new();
compound.put_string("type", self.animation_type.to_name().to_string());
compound.put_int("duration", self.duration);
NbtTag::Compound(compound)
}
default_impl!(SwingAnimation); default_impl!(SwingAnimation);
} }

View File

@ -221,6 +221,25 @@ impl IDSetContent for Block {
} }
} }
impl IDSetContent for crate::item::Item {
fn registry_id(&self) -> u16 {
self.id
}
fn from_id(id: u16) -> Option<&'static Self> {
crate::item::Item::from_id(id)
}
fn from_str(name: &str) -> Option<&'static Self> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
crate::item::Item::from_registry_key(name)
}
fn to_string(&self) -> String {
self.registry_key.to_string()
}
}
#[derive(Clone, Hash, PartialEq, Debug)] #[derive(Clone, Hash, PartialEq, Debug)]
pub enum IDSet<T: IDSetContent + 'static> { pub enum IDSet<T: IDSetContent + 'static> {
Tag(Cow<'static, str>), Tag(Cow<'static, str>),
@ -548,6 +567,8 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
Some(StoredEnchantmentsImpl::read_data(data)?.to_dyn()) Some(StoredEnchantmentsImpl::read_data(data)?.to_dyn())
} }
DataComponent::UseCooldown => Some(UseCooldownImpl::read_data(data)?.to_dyn()), DataComponent::UseCooldown => Some(UseCooldownImpl::read_data(data)?.to_dyn()),
DataComponent::RepairCost => Some(RepairCostImpl::read_data(data)?.to_dyn()),
DataComponent::Repairable => Some(RepairableImpl::read_data(data)?.to_dyn()),
DataComponent::MapId => Some(MapIdImpl::read_data(data)?.to_dyn()), DataComponent::MapId => Some(MapIdImpl::read_data(data)?.to_dyn()),
DataComponent::ChargedProjectiles => { DataComponent::ChargedProjectiles => {
Some(ChargedProjectilesImpl::read_data(data)?.to_dyn()) Some(ChargedProjectilesImpl::read_data(data)?.to_dyn())
@ -620,6 +641,8 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
DataComponent::Trim => Some(TrimImpl::read_data(data)?.to_dyn()), DataComponent::Trim => Some(TrimImpl::read_data(data)?.to_dyn()),
DataComponent::CanPlaceOn => Some(CanPlaceOnImpl::read_data(data)?.to_dyn()), DataComponent::CanPlaceOn => Some(CanPlaceOnImpl::read_data(data)?.to_dyn()),
DataComponent::CanBreak => Some(CanBreakImpl::read_data(data)?.to_dyn()), DataComponent::CanBreak => Some(CanBreakImpl::read_data(data)?.to_dyn()),
DataComponent::SwingAnimation => Some(SwingAnimationImpl::read_data(data)?.to_dyn()),
DataComponent::Rarity => Some(RarityImpl::read_data(data)?.to_dyn()),
_ => None, _ => None,
} }
} }
@ -845,4 +868,16 @@ mod tests {
assert!(enc2.enchantment[0].0 == &crate::Enchantment::SHARPNESS); assert!(enc2.enchantment[0].0 == &crate::Enchantment::SHARPNESS);
assert_eq!(enc2.enchantment[0].1, 2); assert_eq!(enc2.enchantment[0].1, 2);
} }
#[test]
fn swing_animation_round_trip() {
assert_round_trip(
SwingAnimationImpl {
animation_type: SwingAnimationType::Stab,
duration: 19,
},
SwingAnimationImpl::read_data,
);
assert_round_trip(SwingAnimationImpl::DEFAULT, SwingAnimationImpl::read_data);
}
} }

View File

@ -1,6 +1,6 @@
/* This file is generated. Do not edit manually. */ /* This file is generated. Do not edit manually. */
use crate::tag::{RegistryKey, Tag, Taggable}; use crate::tag::{RegistryKey, Tag, Taggable};
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub struct DamageType { pub struct DamageType {
pub death_message_type: DeathMessageType, pub death_message_type: DeathMessageType,
pub exhaustion: f32, pub exhaustion: f32,
@ -9,13 +9,13 @@ pub struct DamageType {
pub scaling: DamageScaling, pub scaling: DamageScaling,
pub id: u8, pub id: u8,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum DeathMessageType { pub enum DeathMessageType {
Default, Default,
FallVariants, FallVariants,
IntentionalGameDesign, IntentionalGameDesign,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum DamageEffects { pub enum DamageEffects {
Hurt, Hurt,
Thorns, Thorns,
@ -24,7 +24,7 @@ pub enum DamageEffects {
Poking, Poking,
Freezing, Freezing,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum DamageScaling { pub enum DamageScaling {
Never, Never,
WhenCausedByLivingNonPlayer, WhenCausedByLivingNonPlayer,

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
/* This file is generated. Do not edit manually. */ /* This file is generated. Do not edit manually. */
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GameEvent { pub enum GameEvent {
BlockActivate, BlockActivate,
BlockAttach, BlockAttach,
@ -62,3 +63,78 @@ pub enum GameEvent {
Resonate14, Resonate14,
Resonate15, Resonate15,
} }
impl GameEvent {
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
"block_activate" | "minecraft:block_activate" => Some(Self::BlockActivate),
"block_attach" | "minecraft:block_attach" => Some(Self::BlockAttach),
"block_change" | "minecraft:block_change" => Some(Self::BlockChange),
"block_close" | "minecraft:block_close" => Some(Self::BlockClose),
"block_deactivate" | "minecraft:block_deactivate" => Some(Self::BlockDeactivate),
"block_destroy" | "minecraft:block_destroy" => Some(Self::BlockDestroy),
"block_detach" | "minecraft:block_detach" => Some(Self::BlockDetach),
"block_open" | "minecraft:block_open" => Some(Self::BlockOpen),
"block_place" | "minecraft:block_place" => Some(Self::BlockPlace),
"bounce" | "minecraft:bounce" => Some(Self::Bounce),
"container_close" | "minecraft:container_close" => Some(Self::ContainerClose),
"container_open" | "minecraft:container_open" => Some(Self::ContainerOpen),
"drink" | "minecraft:drink" => Some(Self::Drink),
"eat" | "minecraft:eat" => Some(Self::Eat),
"elytra_glide" | "minecraft:elytra_glide" => Some(Self::ElytraGlide),
"entity_damage" | "minecraft:entity_damage" => Some(Self::EntityDamage),
"entity_die" | "minecraft:entity_die" => Some(Self::EntityDie),
"entity_dismount" | "minecraft:entity_dismount" => Some(Self::EntityDismount),
"entity_interact" | "minecraft:entity_interact" => Some(Self::EntityInteract),
"entity_mount" | "minecraft:entity_mount" => Some(Self::EntityMount),
"entity_place" | "minecraft:entity_place" => Some(Self::EntityPlace),
"entity_action" | "minecraft:entity_action" => Some(Self::EntityAction),
"equip" | "minecraft:equip" => Some(Self::Equip),
"explode" | "minecraft:explode" => Some(Self::Explode),
"flap" | "minecraft:flap" => Some(Self::Flap),
"fluid_pickup" | "minecraft:fluid_pickup" => Some(Self::FluidPickup),
"fluid_place" | "minecraft:fluid_place" => Some(Self::FluidPlace),
"hit_ground" | "minecraft:hit_ground" => Some(Self::HitGround),
"instrument_play" | "minecraft:instrument_play" => Some(Self::InstrumentPlay),
"item_interact_finish" | "minecraft:item_interact_finish" => {
Some(Self::ItemInteractFinish)
}
"item_interact_start" | "minecraft:item_interact_start" => {
Some(Self::ItemInteractStart)
}
"jukebox_play" | "minecraft:jukebox_play" => Some(Self::JukeboxPlay),
"jukebox_stop_play" | "minecraft:jukebox_stop_play" => Some(Self::JukeboxStopPlay),
"lightning_strike" | "minecraft:lightning_strike" => Some(Self::LightningStrike),
"note_block_play" | "minecraft:note_block_play" => Some(Self::NoteBlockPlay),
"prime_fuse" | "minecraft:prime_fuse" => Some(Self::PrimeFuse),
"projectile_land" | "minecraft:projectile_land" => Some(Self::ProjectileLand),
"projectile_shoot" | "minecraft:projectile_shoot" => Some(Self::ProjectileShoot),
"sculk_sensor_tendrils_clicking" | "minecraft:sculk_sensor_tendrils_clicking" => {
Some(Self::SculkSensorTendrilsClicking)
}
"shear" | "minecraft:shear" => Some(Self::Shear),
"shriek" | "minecraft:shriek" => Some(Self::Shriek),
"splash" | "minecraft:splash" => Some(Self::Splash),
"step" | "minecraft:step" => Some(Self::Step),
"swim" | "minecraft:swim" => Some(Self::Swim),
"teleport" | "minecraft:teleport" => Some(Self::Teleport),
"unequip" | "minecraft:unequip" => Some(Self::Unequip),
"resonate_1" | "minecraft:resonate_1" => Some(Self::Resonate1),
"resonate_2" | "minecraft:resonate_2" => Some(Self::Resonate2),
"resonate_3" | "minecraft:resonate_3" => Some(Self::Resonate3),
"resonate_4" | "minecraft:resonate_4" => Some(Self::Resonate4),
"resonate_5" | "minecraft:resonate_5" => Some(Self::Resonate5),
"resonate_6" | "minecraft:resonate_6" => Some(Self::Resonate6),
"resonate_7" | "minecraft:resonate_7" => Some(Self::Resonate7),
"resonate_8" | "minecraft:resonate_8" => Some(Self::Resonate8),
"resonate_9" | "minecraft:resonate_9" => Some(Self::Resonate9),
"resonate_10" | "minecraft:resonate_10" => Some(Self::Resonate10),
"resonate_11" | "minecraft:resonate_11" => Some(Self::Resonate11),
"resonate_12" | "minecraft:resonate_12" => Some(Self::Resonate12),
"resonate_13" | "minecraft:resonate_13" => Some(Self::Resonate13),
"resonate_14" | "minecraft:resonate_14" => Some(Self::Resonate14),
"resonate_15" | "minecraft:resonate_15" => Some(Self::Resonate15),
_ => None,
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -17777,8 +17777,19 @@ pub trait Taggable {
fn registry_id(&self) -> u16; fn registry_id(&self) -> u16;
#[must_use] #[must_use]
fn is_tagged_with(&self, tag: &str) -> Option<bool> { fn is_tagged_with(&self, tag: &str) -> Option<bool> {
let tag = tag.strip_prefix("#").unwrap_or(tag); let tag = tag.strip_prefix('#').unwrap_or(tag);
let items = get_tag_ids(Self::tag_key(), tag)?; let items = get_tag_ids(Self::tag_key(), tag).or_else(|| {
if !tag.contains(':') {
let mut full = String::with_capacity(10 + tag.len());
full.push_str("minecraft:");
full.push_str(tag);
get_tag_ids(Self::tag_key(), &full)
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
get_tag_ids(Self::tag_key(), stripped)
} else {
None
}
})?;
Some(items.contains(&self.registry_id())) Some(items.contains(&self.registry_id()))
} }
#[must_use] #[must_use]
@ -17787,7 +17798,18 @@ pub trait Taggable {
} }
#[must_use] #[must_use]
fn get_tag_values(tag: &str) -> Option<&'static [&'static str]> { fn get_tag_values(tag: &str) -> Option<&'static [&'static str]> {
let tag = tag.strip_prefix("#").unwrap_or(tag); let tag = tag.strip_prefix('#').unwrap_or(tag);
get_tag_values(Self::tag_key(), tag) get_tag_values(Self::tag_key(), tag).or_else(|| {
if !tag.contains(':') {
let mut full = String::with_capacity(10 + tag.len());
full.push_str("minecraft:");
full.push_str(tag);
get_tag_values(Self::tag_key(), &full)
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
get_tag_values(Self::tag_key(), stripped)
} else {
None
}
})
} }
} }

View File

@ -2,9 +2,10 @@ use crate::data_component::DataComponent;
use crate::data_component::DataComponent::Enchantments; use crate::data_component::DataComponent::Enchantments;
use crate::data_component_impl::{ use crate::data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, CustomDataImpl, DamageImpl, DataComponentImpl, BlocksAttacksImpl, ConsumableImpl, CustomDataImpl, DamageImpl, DataComponentImpl,
EnchantmentsImpl, IDSet, MaxDamageImpl, MaxStackSizeImpl, ToolImpl, UnbreakableImpl, EnchantmentsImpl, IDSet, MaxDamageImpl, MaxStackSizeImpl, Rarity, RarityImpl,
UseCooldownImpl, get, get_mut, read_data, SwingAnimationImpl, ToolImpl, UnbreakableImpl, UseCooldownImpl, get, get_mut, read_data,
}; };
use crate::item::Item; use crate::item::Item;
use crate::recipes::RecipeResultStruct; use crate::recipes::RecipeResultStruct;
use crate::tag::Taggable; use crate::tag::Taggable;
@ -187,6 +188,21 @@ impl ItemStack {
None None
} }
#[must_use]
pub fn has_data_component(&self, to_get_id: DataComponent) -> bool {
for (id, component) in &self.patch {
if *id == to_get_id {
return component.is_some();
}
}
for (id, _) in self.item.components {
if *id == to_get_id {
return true;
}
}
false
}
pub fn has_enchantments(&self) -> bool { pub fn has_enchantments(&self) -> bool {
self.get_data_component::<EnchantmentsImpl>() self.get_data_component::<EnchantmentsImpl>()
.is_some_and(|e| !e.enchantment.is_empty()) .is_some_and(|e| !e.enchantment.is_empty())
@ -446,6 +462,81 @@ impl ItemStack {
} }
} }
#[must_use]
pub fn has_custom_name(&self) -> bool {
self.get_data_component::<crate::data_component_impl::CustomNameImpl>()
.is_some()
}
#[must_use]
pub fn get_custom_name(&self) -> Option<&pumpkin_util::text::TextComponent> {
self.get_data_component::<crate::data_component_impl::CustomNameImpl>()
.map(|c| &c.name)
}
pub fn remove_custom_name(&mut self) {
self.patch
.retain(|(id, _)| *id != DataComponent::CustomName);
}
#[must_use]
pub fn get_hover_name(&self) -> String {
if let Some(custom_name) =
self.get_data_component::<crate::data_component_impl::CustomNameImpl>()
{
return custom_name.name.clone().get_text();
}
if let Some(item_name) =
self.get_data_component::<crate::data_component_impl::ItemNameImpl>()
{
return item_name.name.to_string();
}
self.item.registry_key.to_string()
}
#[must_use]
pub fn get_repair_cost(&self) -> i32 {
self.get_data_component::<crate::data_component_impl::RepairCostImpl>()
.map_or(0, |value| value.cost)
}
pub fn set_repair_cost(&mut self, cost: i32) {
if cost <= 0 {
self.patch
.retain(|(id, _)| *id != DataComponent::RepairCost);
return;
}
self.set_data_component(crate::data_component_impl::RepairCostImpl { cost });
}
#[must_use]
pub fn is_valid_repair_item(&self, repair_item: &ItemStack) -> bool {
let repairable = self.get_data_component::<crate::data_component_impl::RepairableImpl>();
repairable.is_some_and(|r| r.is_valid_repair_item(repair_item))
}
#[must_use]
pub fn get_swing_animation(&self) -> SwingAnimationImpl {
self.get_data_component::<SwingAnimationImpl>()
.copied()
.unwrap_or(SwingAnimationImpl::DEFAULT)
}
#[must_use]
pub fn get_rarity(&self) -> Rarity {
let base = self
.get_data_component::<RarityImpl>()
.map_or(Rarity::Common, |r| r.rarity);
if !self.has_enchantments() {
return base;
}
match base {
Rarity::Common | Rarity::Uncommon => Rarity::Rare,
Rarity::Rare => Rarity::Epic,
Rarity::Epic => Rarity::Epic,
}
}
#[must_use] #[must_use]
pub fn custom_data_compound(&self) -> Option<&NbtCompound> { pub fn custom_data_compound(&self) -> Option<&NbtCompound> {
self.get_data_component::<CustomDataImpl>() self.get_data_component::<CustomDataImpl>()

View File

@ -1,21 +1,184 @@
use std::any::Any; use std::any::Any;
use std::borrow::Cow;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering};
use pumpkin_data::{item_stack::ItemStack, screen::WindowType}; use pumpkin_data::Enchantment;
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{EnchantmentsImpl, StoredEnchantmentsImpl};
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::screen::WindowType;
use pumpkin_world::inventory::Inventory; use pumpkin_world::inventory::Inventory;
use crate::{ use crate::{
player::player_inventory::PlayerInventory, player::player_inventory::PlayerInventory,
screen_handler::{InventoryPlayer, ScreenHandler, ScreenHandlerBehaviour, offer_or_drop_stack}, screen_handler::{InventoryPlayer, ScreenHandler, ScreenHandlerBehaviour, offer_or_drop_stack},
slot::NormalSlot, slot::{NormalSlot, Slot},
window_property::{Anvil, WindowProperty}, window_property::{Anvil, WindowProperty},
}; };
pub struct AnvilResultSlot {
pub inventory: Arc<dyn Inventory>,
pub index: usize,
pub id: AtomicU8,
pub cost: Arc<AtomicI32>,
pub repair_item_count_cost: Arc<AtomicI32>,
pub only_renaming: Arc<AtomicBool>,
}
impl AnvilResultSlot {
#[must_use]
pub fn new(
inventory: Arc<dyn Inventory>,
index: usize,
cost: Arc<AtomicI32>,
repair_item_count_cost: Arc<AtomicI32>,
only_renaming: Arc<AtomicBool>,
) -> Self {
Self {
inventory,
index,
id: AtomicU8::new(0),
cost,
repair_item_count_cost,
only_renaming,
}
}
}
impl Slot for AnvilResultSlot {
fn get_inventory(&self) -> Arc<dyn Inventory> {
self.inventory.clone()
}
fn get_index(&self) -> usize {
self.index
}
fn set_id(&self, id: usize) {
self.id.store(id as u8, Ordering::Relaxed);
}
fn can_insert(&self, _stack: &ItemStack) -> bool {
false
}
fn can_take_items(&self, player: &dyn InventoryPlayer) -> bool {
let cost = self.cost.load(Ordering::Relaxed);
(player.has_infinite_materials() || player.experience_level() >= cost) && cost > 0
}
fn on_take_item(&self, player: &dyn InventoryPlayer, _stack: &ItemStack) {
let cost = self.cost.load(Ordering::Relaxed);
if !player.has_infinite_materials() {
player.add_experience_levels(-cost);
}
let repair_item_count_cost = self.repair_item_count_cost.load(Ordering::Relaxed);
if repair_item_count_cost > 0 {
let mut addition = self.inventory.get_stack(1);
if !addition.is_empty() && addition.item_count > repair_item_count_cost as u8 {
addition.decrement(repair_item_count_cost as u8);
self.inventory.set_stack(1, addition);
} else {
self.inventory.set_stack(1, ItemStack::EMPTY.clone());
}
} else if !self.only_renaming.load(Ordering::Relaxed) {
self.inventory.set_stack(1, ItemStack::EMPTY.clone());
}
self.cost.store(0, Ordering::Relaxed);
self.inventory.set_stack(0, ItemStack::EMPTY.clone());
player.use_anvil();
self.mark_dirty();
}
fn mark_dirty(&self) {
self.inventory.mark_dirty();
}
}
pub struct AnvilScreenHandler { pub struct AnvilScreenHandler {
pub inventory: Arc<dyn Inventory>, pub inventory: Arc<dyn Inventory>,
behaviour: ScreenHandlerBehaviour, behaviour: ScreenHandlerBehaviour,
pub rename_text: String, pub item_name: Option<String>,
pub repair_cost: i16, pub repair_cost: Arc<AtomicI32>,
pub repair_item_count_cost: Arc<AtomicI32>,
pub only_renaming: Arc<AtomicBool>,
}
#[must_use]
pub fn validate_name(name: &str) -> Option<String> {
let filtered: String = name.chars().filter(|&c| c >= ' ' && c != '\x7F').collect();
(filtered.chars().count() <= 50).then_some(filtered)
}
#[must_use]
pub const fn calculate_increased_repair_cost(base_cost: i32) -> i32 {
let doubled = (base_cost as i64) * 2 + 1;
if doubled > i32::MAX as i64 {
i32::MAX
} else {
doubled as i32
}
}
#[must_use]
pub fn get_component_type(stack: &ItemStack) -> DataComponent {
if stack.item == &Item::ENCHANTED_BOOK {
DataComponent::StoredEnchantments
} else {
DataComponent::Enchantments
}
}
#[must_use]
pub fn can_store_enchantments(stack: &ItemStack) -> bool {
if stack.is_empty() {
return false;
}
stack.has_data_component(get_component_type(stack))
}
#[must_use]
pub fn get_enchantments_for_crafting(stack: &ItemStack) -> Vec<(&'static Enchantment, i32)> {
if stack.is_empty() {
return Vec::new();
}
if stack.item == &Item::ENCHANTED_BOOK {
if let Some(stored) = stack.get_data_component::<StoredEnchantmentsImpl>() {
return stored
.enchantment
.iter()
.map(|(e, lvl)| (*e, *lvl))
.collect();
}
} else if let Some(ench) = stack.get_data_component::<EnchantmentsImpl>() {
return ench.enchantment.iter().map(|(e, lvl)| (*e, *lvl)).collect();
}
Vec::new()
}
pub fn set_enchantments(result: &mut ItemStack, enchantments: Vec<(&'static Enchantment, i32)>) {
if result.is_empty() {
return;
}
if result.item == &Item::ENCHANTED_BOOK
|| result
.get_data_component::<StoredEnchantmentsImpl>()
.is_some()
{
let stored = StoredEnchantmentsImpl {
enchantment: Cow::Owned(enchantments),
};
result.set_data_component(stored);
} else {
let ench = EnchantmentsImpl {
enchantment: Cow::Owned(enchantments),
};
result.set_data_component(ench);
}
} }
impl AnvilScreenHandler { impl AnvilScreenHandler {
@ -25,17 +188,31 @@ impl AnvilScreenHandler {
player_inventory: &Arc<PlayerInventory>, player_inventory: &Arc<PlayerInventory>,
inventory: Arc<dyn Inventory>, inventory: Arc<dyn Inventory>,
) -> Self { ) -> Self {
let cost = Arc::new(AtomicI32::new(0));
let repair_item_count_cost = Arc::new(AtomicI32::new(0));
let only_renaming = Arc::new(AtomicBool::new(false));
let mut handler = Self { let mut handler = Self {
inventory: inventory.clone(), inventory: inventory.clone(),
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Anvil)), behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Anvil)),
rename_text: String::new(), item_name: None,
repair_cost: 0, repair_cost: cost.clone(),
repair_item_count_cost: repair_item_count_cost.clone(),
only_renaming: only_renaming.clone(),
}; };
// Anvil specific slots: 2 input, 1 output // Slot 0: Input slot
for i in 0..3 { handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), 0)));
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), i))); // Slot 1: Additional slot
} handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), 1)));
// Slot 2: Result slot
handler.add_slot(Arc::new(AnvilResultSlot::new(
inventory.clone(),
2,
cost,
repair_item_count_cost,
only_renaming,
)));
let player_inventory: Arc<dyn Inventory> = player_inventory.clone(); let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
handler.add_player_slots(&player_inventory); handler.add_player_slots(&player_inventory);
@ -43,46 +220,42 @@ impl AnvilScreenHandler {
handler handler
} }
pub fn update_item_name(&mut self, name: String) { pub fn set_item_name(&mut self, name: &str, has_infinite_materials: bool) -> bool {
self.rename_text = name; let validated_name = validate_name(name);
self.update_result_slot(); if let Some(validated_name) = validated_name
self.send_content_updates(); && self.item_name.as_deref() != Some(&validated_name)
{
self.item_name = Some(validated_name.clone());
let result_stack = self.inventory.get_stack(2);
if !result_stack.is_empty() {
let mut updated_stack = result_stack;
if validated_name.trim().is_empty() {
updated_stack.remove_custom_name();
} else {
updated_stack.set_custom_name(validated_name);
}
self.inventory.set_stack(2, updated_stack);
}
self.create_result(has_infinite_materials);
return true;
}
false
}
pub fn update_item_name(&mut self, name: &str) {
self.set_item_name(name, false);
} }
pub fn update_result_slot(&mut self) { pub fn update_result_slot(&mut self) {
let input_a = self.inventory.get_stack(0); self.create_result(false);
if input_a.is_empty() {
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
self.set_repair_cost(0);
return;
} }
let mut result_item = input_a; pub fn set_repair_cost(&mut self, cost: i32) {
let mut cost = 0; self.repair_cost.store(cost, Ordering::Relaxed);
// Basic renaming logic for now
if !self.rename_text.is_empty() {
result_item.set_custom_name(self.rename_text.clone());
cost += 1;
}
// If combining with another item... we'll skip complex anvil logic for now
// and just support renaming.
if cost > 0 {
self.inventory.set_stack(2, result_item);
self.set_repair_cost(cost);
} else {
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
self.set_repair_cost(0);
}
}
pub fn set_repair_cost(&mut self, cost: i16) {
self.repair_cost = cost;
if let Some(sync_handler) = self.behaviour.sync_handler.as_ref() { if let Some(sync_handler) = self.behaviour.sync_handler.as_ref() {
let (property_id, property_value) = let (property_id, property_value) =
WindowProperty::new(Anvil::RepairCost, cost).into_tuple(); WindowProperty::new(Anvil::RepairCost, cost.clamp(0, i16::MAX as i32) as i16)
.into_tuple();
sync_handler.update_property( sync_handler.update_property(
&self.behaviour, &self.behaviour,
property_id as i32, property_id as i32,
@ -90,6 +263,200 @@ impl AnvilScreenHandler {
); );
} }
} }
#[must_use]
pub fn get_cost(&self) -> i32 {
self.repair_cost.load(Ordering::Relaxed)
}
#[allow(clippy::too_many_lines)]
pub fn create_result(&mut self, has_infinite_materials: bool) {
let input = self.inventory.get_stack(0);
self.only_renaming.store(false, Ordering::Relaxed);
self.set_repair_cost(1);
let mut price: i32 = 0;
let mut tax: i64 = 0;
let mut naming_cost: i32 = 0;
if !input.is_empty() && can_store_enchantments(&input) {
let mut result = input.clone();
let addition = self.inventory.get_stack(1);
let mut enchantments = get_enchantments_for_crafting(&result);
tax += input.get_repair_cost() as i64 + addition.get_repair_cost() as i64;
self.repair_item_count_cost.store(0, Ordering::Relaxed);
if !addition.is_empty() {
let using_book = addition.item == &Item::ENCHANTED_BOOK
|| addition
.get_data_component::<StoredEnchantmentsImpl>()
.is_some_and(|s| !s.enchantment.is_empty());
if result.is_damageable() && input.is_valid_repair_item(&addition) {
let max_damage = result.get_max_damage().unwrap_or(0);
let mut repair_amount = result.get_damage().min(max_damage / 4);
if repair_amount <= 0 {
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
self.set_repair_cost(0);
return;
}
let mut count = 0;
while repair_amount > 0 && count < addition.item_count as i32 {
let result_damage = result.get_damage() - repair_amount;
result.set_damage(result_damage);
price += 1;
repair_amount = result.get_damage().min(max_damage / 4);
count += 1;
}
self.repair_item_count_cost.store(count, Ordering::Relaxed);
} else {
if !using_book && (result.item != addition.item || !result.is_damageable()) {
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
self.set_repair_cost(0);
return;
}
if result.is_damageable() && !using_book {
let max_damage = result.get_max_damage().unwrap_or(0);
let remaining1 = max_damage - input.get_damage();
let remaining2 =
addition.get_max_damage().unwrap_or(0) - addition.get_damage();
let additional = remaining2 + max_damage * 12 / 100;
let remaining = remaining1 + additional;
let mut result_damage = max_damage - remaining;
if result_damage < 0 {
result_damage = 0;
}
if result_damage < result.get_damage() {
result.set_damage(result_damage);
price += 2;
}
}
let additional_enchantments = get_enchantments_for_crafting(&addition);
let mut is_any_enchantment_compatible = false;
let mut is_any_enchantment_not_compatible = false;
for (enchantment, entry_level) in additional_enchantments {
let current = enchantments
.iter()
.find(|(e, _)| *e == enchantment)
.map_or(0, |(_, lvl)| *lvl);
let mut level = if current == entry_level {
entry_level + 1
} else {
entry_level.max(current)
};
let mut compatible =
if has_infinite_materials || input.item == &Item::ENCHANTED_BOOK {
true
} else {
enchantment.can_enchant(input.item)
};
for (other, _) in &enchantments {
if *other != enchantment && !other.are_compatible(enchantment) {
compatible = false;
price += 1;
}
}
if compatible {
is_any_enchantment_compatible = true;
if level > enchantment.max_level {
level = enchantment.max_level;
}
if let Some(entry) =
enchantments.iter_mut().find(|(e, _)| *e == enchantment)
{
entry.1 = level;
} else {
enchantments.push((enchantment, level));
}
let mut fee = enchantment.anvil_cost as i32;
if using_book {
fee = 1.max(fee / 2);
}
price += fee * level;
if input.item_count > 1 {
price = 40;
}
} else {
is_any_enchantment_not_compatible = true;
}
}
if is_any_enchantment_not_compatible && !is_any_enchantment_compatible {
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
self.set_repair_cost(0);
return;
}
}
}
let input_hover_name = input.get_hover_name();
if let Some(item_name) = &self.item_name
&& !item_name.trim().is_empty()
{
if item_name != &input_hover_name {
naming_cost = 1;
price += naming_cost;
result.set_custom_name(item_name.clone());
}
} else if input.has_custom_name() {
naming_cost = 1;
price += naming_cost;
result.remove_custom_name();
}
let final_price = if price <= 0 {
0
} else {
(tax + price as i64).clamp(0, i32::MAX as i64) as i32
};
self.set_repair_cost(final_price);
if price <= 0 {
result = ItemStack::EMPTY.clone();
}
if naming_cost == price && naming_cost > 0 {
if self.repair_cost.load(Ordering::Relaxed) >= 40 {
self.set_repair_cost(39);
}
self.only_renaming.store(true, Ordering::Relaxed);
}
if self.repair_cost.load(Ordering::Relaxed) >= 40 && !has_infinite_materials {
result = ItemStack::EMPTY.clone();
}
if !result.is_empty() {
let mut base_cost = result.get_repair_cost();
if base_cost < addition.get_repair_cost() {
base_cost = addition.get_repair_cost();
}
if naming_cost != price || naming_cost == 0 {
base_cost = calculate_increased_repair_cost(base_cost);
}
result.set_repair_cost(base_cost);
set_enchantments(&mut result, enchantments);
}
self.inventory.set_stack(2, result);
self.send_content_updates();
} else {
self.inventory.set_stack(2, ItemStack::EMPTY.clone());
self.set_repair_cost(0);
}
}
} }
impl ScreenHandler for AnvilScreenHandler { impl ScreenHandler for AnvilScreenHandler {
@ -130,18 +497,34 @@ impl ScreenHandler for AnvilScreenHandler {
let mut slot_stack = slot.get_stack(); let mut slot_stack = slot.get_stack();
stack_left = slot_stack.clone(); stack_left = slot_stack.clone();
if slot_index < 3 { match slot_index.cmp(&2) {
// From anvil to player std::cmp::Ordering::Equal => {
// Result slot: from anvil to player
if !slot.can_take_items(player) {
return ItemStack::EMPTY.clone();
}
if !self.insert_item(&mut slot_stack, 3, 39, true) { if !self.insert_item(&mut slot_stack, 3, 39, true) {
return ItemStack::EMPTY.clone(); return ItemStack::EMPTY.clone();
} }
slot.on_quick_move_crafted(slot_stack.clone(), stack_left.clone()); slot.on_quick_move_crafted(slot_stack.clone(), stack_left.clone());
} else { slot.on_take_item(player, &stack_left);
// From player to anvil self.create_result(player.has_infinite_materials());
self.send_content_updates();
return stack_left;
}
std::cmp::Ordering::Less => {
// Input slots (0, 1): from anvil to player
if !self.insert_item(&mut slot_stack, 3, 39, false) {
return ItemStack::EMPTY.clone();
}
}
std::cmp::Ordering::Greater => {
// From player to anvil input slots (0..2)
if !self.insert_item(&mut slot_stack, 0, 2, false) { if !self.insert_item(&mut slot_stack, 0, 2, false) {
return ItemStack::EMPTY.clone(); return ItemStack::EMPTY.clone();
} }
} }
}
if slot_stack.item_count == stack_left.item_count { if slot_stack.item_count == stack_left.item_count {
return ItemStack::EMPTY.clone(); return ItemStack::EMPTY.clone();
@ -150,6 +533,8 @@ impl ScreenHandler for AnvilScreenHandler {
slot.set_stack_prev(slot_stack.clone(), stack_left.clone()); slot.set_stack_prev(slot_stack.clone(), stack_left.clone());
slot.on_take_item(player, &slot_stack); slot.on_take_item(player, &slot_stack);
slot.mark_dirty(); slot.mark_dirty();
self.create_result(player.has_infinite_materials());
self.send_content_updates();
} }
stack_left stack_left
@ -162,35 +547,284 @@ impl ScreenHandler for AnvilScreenHandler {
action_type: pumpkin_protocol::java::server::play::SlotActionType, action_type: pumpkin_protocol::java::server::play::SlotActionType,
player: &dyn InventoryPlayer, player: &dyn InventoryPlayer,
) { ) {
if slot_index == 2 {
// Taking from output slot
let result_slot = self.get_behaviour().slots[2].clone();
if result_slot.has_stack() {
let result_stack = result_slot.get_cloned_stack();
if !result_stack.is_empty() {
if player.experience_level() >= self.repair_cost as i32 || player.is_creative()
{
// Consume experience
if !player.is_creative() {
player.add_experience_levels(-(self.repair_cost as i32));
}
// Consume inputs
self.inventory.set_stack(0, ItemStack::EMPTY.clone());
self.get_behaviour().slots[0].mark_dirty();
} else {
// Cancel click
self.send_content_updates();
return;
}
}
}
}
self.internal_on_slot_click(slot_index, button, action_type, player); self.internal_on_slot_click(slot_index, button, action_type, player);
if slot_index == 0 || slot_index == 1 || slot_index == 2 { if slot_index == 0 || slot_index == 1 || slot_index == 2 {
self.update_result_slot(); self.create_result(player.has_infinite_materials());
self.send_content_updates(); self.send_content_updates();
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::entity_equipment::EntityEquipment;
use pumpkin_protocol::java::client::play::{
CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem,
CSetPlayerInventory, CSetSelectedSlot,
};
use pumpkin_world::inventory::SimpleInventory;
use std::sync::Mutex;
use std::sync::atomic::AtomicI32;
struct DummyPlayer {
pub exp_level: AtomicI32,
pub creative: bool,
pub inventory: Arc<PlayerInventory>,
}
impl DummyPlayer {
fn new(exp_level: i32, creative: bool) -> Self {
Self {
exp_level: AtomicI32::new(exp_level),
creative,
inventory: Arc::new(PlayerInventory::new(
Arc::new(Mutex::new(EntityEquipment::new())),
Arc::new(rustc_hash::FxHashMap::default()),
)),
}
}
}
impl InventoryPlayer for DummyPlayer {
fn as_any(&self) -> &dyn Any {
self
}
fn drop_item(&self, _item: ItemStack, _retain_ownership: bool) {}
fn is_creative(&self) -> bool {
self.creative
}
fn has_infinite_materials(&self) -> bool {
self.creative
}
fn experience_level(&self) -> i32 {
self.exp_level.load(Ordering::Relaxed)
}
fn add_experience_levels(&self, levels: i32) {
self.exp_level.fetch_add(levels, Ordering::Relaxed);
}
fn enchantment_seed(&self) -> i32 {
0
}
fn set_enchantment_seed(&self, _seed: i32) {}
fn get_inventory(&self) -> Arc<PlayerInventory> {
self.inventory.clone()
}
fn enqueue_inventory_packet(
&self,
_packet: &CSetContainerContent,
_window_type: Option<WindowType>,
) {
}
fn enqueue_slot_packet(
&self,
_packet: &CSetContainerSlot,
_window_type: Option<WindowType>,
_total_slots: usize,
) {
}
fn enqueue_cursor_packet(&self, _packet: &CSetCursorItem) {}
fn enqueue_property_packet(&self, _packet: &CSetContainerProperty) {}
fn enqueue_slot_set_packet(&self, _packet: &CSetPlayerInventory) {}
fn enqueue_set_held_item_packet(&self, _packet: &CSetSelectedSlot) {}
fn enqueue_equipment_change(
&self,
_slot: &pumpkin_data::data_component_impl::EquipmentSlot,
_stack: &ItemStack,
) {
}
fn award_experience(&self, _amount: i32) {}
fn increment_stat(
&self,
_category: pumpkin_data::statistic::StatisticCategory,
_stat_id: i32,
_amount: i32,
) {
}
}
#[test]
fn rename_item() {
let player = DummyPlayer::new(10, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
anvil_inv.set_stack(0, sword);
handler.set_item_name("Excalibur", false);
assert_eq!(handler.get_cost(), 1);
let result = anvil_inv.get_stack(2);
assert_eq!(result.get_hover_name(), "Excalibur");
assert_eq!(result.get_repair_cost(), 0); // Only renaming does not increase repair cost
}
#[test]
fn material_repair() {
let player = DummyPlayer::new(10, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let mut damaged_sword = ItemStack::new(1, &Item::IRON_SWORD);
let max_damage = damaged_sword.get_max_damage().unwrap();
// 250 / 4 = 62 per ingot. 125 damage requires 3 ingots (62 + 62 + 1).
damaged_sword.set_damage(max_damage / 2);
anvil_inv.set_stack(0, damaged_sword);
let ingots = ItemStack::new(4, &Item::IRON_INGOT);
anvil_inv.set_stack(1, ingots);
handler.create_result(false);
assert_eq!(handler.get_cost(), 3);
let result = anvil_inv.get_stack(2);
assert_eq!(result.get_damage(), 0);
assert_eq!(result.get_repair_cost(), 1); // calculateIncreasedRepairCost(0) = 1
assert_eq!(handler.repair_item_count_cost.load(Ordering::Relaxed), 3);
}
#[test]
fn tool_combine_durability() {
let player = DummyPlayer::new(10, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let mut sword1 = ItemStack::new(1, &Item::DIAMOND_SWORD);
let max_damage = sword1.get_max_damage().unwrap();
sword1.set_damage(500);
anvil_inv.set_stack(0, sword1);
let mut sword2 = ItemStack::new(1, &Item::DIAMOND_SWORD);
sword2.set_damage(500);
anvil_inv.set_stack(1, sword2);
handler.create_result(false);
assert_eq!(handler.get_cost(), 2);
let result = anvil_inv.get_stack(2);
// Durability combine: (max - 500) + (max - 500) + max * 12 / 100
let expected_remaining = (max_damage - 500) + (max_damage - 500) + max_damage * 12 / 100;
let expected_damage = (max_damage - expected_remaining).max(0);
assert_eq!(result.get_damage(), expected_damage);
assert_eq!(result.get_repair_cost(), 1);
}
#[test]
fn enchanted_book_combine() {
let player = DummyPlayer::new(10, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
anvil_inv.set_stack(0, sword);
let mut book = ItemStack::new(1, &Item::ENCHANTED_BOOK);
let stored = StoredEnchantmentsImpl {
enchantment: Cow::Owned(vec![(&Enchantment::SHARPNESS, 3)]),
};
book.set_data_component(stored);
anvil_inv.set_stack(1, book);
handler.create_result(false);
// Sharpness anvil cost = 1. Fee using book = max(1, 1/2) = 1. Price = 1 * 3 = 3.
assert_eq!(handler.get_cost(), 3);
let result = anvil_inv.get_stack(2);
let enchs = get_enchantments_for_crafting(&result);
assert_eq!(enchs.len(), 1);
assert_eq!(enchs[0].0.id, Enchantment::SHARPNESS.id);
assert_eq!(enchs[0].1, 3);
assert_eq!(result.get_repair_cost(), 1);
}
#[test]
fn enchantment_level_upgrade() {
let player = DummyPlayer::new(10, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let mut sword1 = ItemStack::new(1, &Item::DIAMOND_SWORD);
let ench1 = EnchantmentsImpl {
enchantment: Cow::Owned(vec![(&Enchantment::SHARPNESS, 3)]),
};
sword1.set_data_component(ench1);
anvil_inv.set_stack(0, sword1);
let mut sword2 = ItemStack::new(1, &Item::DIAMOND_SWORD);
let ench2 = EnchantmentsImpl {
enchantment: Cow::Owned(vec![(&Enchantment::SHARPNESS, 3)]),
};
sword2.set_data_component(ench2);
anvil_inv.set_stack(1, sword2);
handler.create_result(false);
let result = anvil_inv.get_stack(2);
let enchs = get_enchantments_for_crafting(&result);
assert_eq!(enchs.len(), 1);
assert_eq!(enchs[0].0.id, Enchantment::SHARPNESS.id);
assert_eq!(enchs[0].1, 4);
}
#[test]
fn too_expensive_limit() {
let player = DummyPlayer::new(50, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let mut sword1 = ItemStack::new(1, &Item::DIAMOND_SWORD);
sword1.set_repair_cost(35);
sword1.set_damage(500);
anvil_inv.set_stack(0, sword1);
let mut sword2 = ItemStack::new(1, &Item::DIAMOND_SWORD);
sword2.set_repair_cost(35);
sword2.set_damage(500);
anvil_inv.set_stack(1, sword2);
handler.create_result(false);
// Tax is 35 + 35 = 70. Combining damaged tools adds 2. Total = 72 >= 40.
assert_eq!(handler.get_cost(), 72);
assert!(anvil_inv.get_stack(2).is_empty());
// But in creative mode, result is available
handler.create_result(true);
assert!(!anvil_inv.get_stack(2).is_empty());
}
#[test]
fn rename_only_caps_at_39() {
let player = DummyPlayer::new(50, false);
let anvil_inv = Arc::new(SimpleInventory::new(3));
let mut handler = AnvilScreenHandler::new(1, &player.inventory, anvil_inv.clone());
let mut sword = ItemStack::new(1, &Item::DIAMOND_SWORD);
sword.set_repair_cost(45);
anvil_inv.set_stack(0, sword);
handler.set_item_name("Legendary", false);
// Renaming only is capped at 39 even with high repair cost
assert_eq!(handler.get_cost(), 39);
assert!(!anvil_inv.get_stack(2).is_empty());
}
}

View File

@ -208,6 +208,9 @@ pub trait InventoryPlayer: Send + Sync {
/// Closes the player's current handled screen. /// Closes the player's current handled screen.
fn close_screen_handler(&self) {} fn close_screen_handler(&self) {}
/// Performs anvil block damage and plays anvil sound events.
fn use_anvil(&self) {}
} }
/// Gives a stack to the player or drops it if inventory is full. /// Gives a stack to the player or drops it if inventory is full.

View File

@ -13,12 +13,14 @@ use pumpkin_data::data_component_impl::{
HorseVariantImpl, IDSet, IDSetContent, IdOr, ItemModelImpl, ItemNameImpl, LlamaVariantImpl, HorseVariantImpl, IDSet, IDSetContent, IdOr, ItemModelImpl, ItemNameImpl, LlamaVariantImpl,
LoreImpl, MapIdImpl, MaxStackSizeImpl, MooshroomVariantImpl, PaintingVariantImpl, LoreImpl, MapIdImpl, MaxStackSizeImpl, MooshroomVariantImpl, PaintingVariantImpl,
ParrotVariantImpl, PigSoundVariantImpl, PigVariantImpl, PotionContentsImpl, RabbitVariantImpl, ParrotVariantImpl, PigSoundVariantImpl, PigVariantImpl, PotionContentsImpl, RabbitVariantImpl,
SalmonSizeImpl, SheepColorImpl, ShulkerColorImpl, SoundEvent, StatusEffectInstance, Rarity, RarityImpl, RepairCostImpl, RepairableImpl, SalmonSizeImpl, SheepColorImpl,
StoredEnchantmentsImpl, SuspiciousStewEffect, SuspiciousStewEffectsImpl, ShulkerColorImpl, SoundEvent, StatusEffectInstance, StoredEnchantmentsImpl,
SuspiciousStewEffect, SuspiciousStewEffectsImpl, SwingAnimationImpl, SwingAnimationType,
TropicalFishBaseColorImpl, TropicalFishPatternColorImpl, TropicalFishPatternImpl, TropicalFishBaseColorImpl, TropicalFishPatternColorImpl, TropicalFishPatternImpl,
UnbreakableImpl, UseCooldownImpl, VillagerVariantImpl, WolfCollarImpl, WolfSoundVariantImpl, UnbreakableImpl, UseCooldownImpl, VillagerVariantImpl, WolfCollarImpl, WolfSoundVariantImpl,
WolfVariantImpl, ZombieNautilusVariantImpl, get, WolfVariantImpl, ZombieNautilusVariantImpl, get,
}; };
use pumpkin_data::effect::StatusEffect; use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::Sound; use pumpkin_data::sound::Sound;
@ -262,6 +264,16 @@ impl DataComponentCodec<Self> for DamageImpl {
} }
} }
impl DataComponentCodec<Self> for RepairCostImpl {
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
seq.write_var_int(&VarInt::from(self.cost))
}
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
let cost = seq.get_var_int()?.0;
Ok(Self { cost })
}
}
impl DataComponentCodec<Self> for EnchantmentsImpl { impl DataComponentCodec<Self> for EnchantmentsImpl {
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> { fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
seq.write_var_int(&VarInt::from(self.enchantment.len() as i32))?; seq.write_var_int(&VarInt::from(self.enchantment.len() as i32))?;
@ -860,6 +872,50 @@ impl DataComponentCodec<Self> for StoredEnchantmentsImpl {
} }
} }
impl DataComponentCodec<Self> for RepairableImpl {
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
serialize_idset(&self.items, seq)
}
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
Ok(Self {
items: deserialize_idset(seq)?,
})
}
}
impl DataComponentCodec<Self> for SwingAnimationImpl {
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
seq.write_var_int(&VarInt::from(self.animation_type.to_id()))?;
seq.write_var_int(&VarInt::from(self.duration))
}
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
let type_id = seq.get_var_int()?.0;
let animation_type = SwingAnimationType::from_id(type_id).ok_or_else(|| {
ReadingError::Message(format!("Invalid SwingAnimationType id {type_id}"))
})?;
let duration = seq.get_var_int()?.0;
Ok(Self {
animation_type,
duration,
})
}
}
impl DataComponentCodec<Self> for RarityImpl {
fn serialize(&self, seq: &mut impl NetworkWriteExt) -> Result<(), WritingError> {
seq.write_var_int(&VarInt::from(self.rarity.to_id()))
}
fn deserialize(seq: &mut impl NetworkReadExt) -> Result<Self, ReadingError> {
let id = seq.get_var_int()?.0;
let rarity = Rarity::from_id(id)
.ok_or_else(|| ReadingError::Message(format!("Invalid Rarity id {id}")))?;
Ok(Self { rarity })
}
}
pub fn deserialize( pub fn deserialize(
id: DataComponent, id: DataComponent,
seq: &mut impl NetworkReadExt, seq: &mut impl NetworkReadExt,
@ -885,6 +941,10 @@ pub fn deserialize(
DataComponent::Consumable => Ok(ConsumableImpl::deserialize(seq)?.to_dyn()), DataComponent::Consumable => Ok(ConsumableImpl::deserialize(seq)?.to_dyn()),
DataComponent::Equippable => Ok(EquippableImpl::deserialize(seq)?.to_dyn()), DataComponent::Equippable => Ok(EquippableImpl::deserialize(seq)?.to_dyn()),
DataComponent::StoredEnchantments => Ok(StoredEnchantmentsImpl::deserialize(seq)?.to_dyn()), DataComponent::StoredEnchantments => Ok(StoredEnchantmentsImpl::deserialize(seq)?.to_dyn()),
DataComponent::RepairCost => Ok(RepairCostImpl::deserialize(seq)?.to_dyn()),
DataComponent::Repairable => Ok(RepairableImpl::deserialize(seq)?.to_dyn()),
DataComponent::SwingAnimation => Ok(SwingAnimationImpl::deserialize(seq)?.to_dyn()),
DataComponent::Rarity => Ok(RarityImpl::deserialize(seq)?.to_dyn()),
DataComponent::UseCooldown => Ok(UseCooldownImpl::deserialize(seq)?.to_dyn()), DataComponent::UseCooldown => Ok(UseCooldownImpl::deserialize(seq)?.to_dyn()),
DataComponent::MapId => Ok(MapIdImpl::deserialize(seq)?.to_dyn()), DataComponent::MapId => Ok(MapIdImpl::deserialize(seq)?.to_dyn()),
DataComponent::BundleContents => Ok(BundleContentsImpl::deserialize(seq)?.to_dyn()), DataComponent::BundleContents => Ok(BundleContentsImpl::deserialize(seq)?.to_dyn()),
@ -916,6 +976,10 @@ pub fn serialize(
DataComponent::Consumable => get::<ConsumableImpl>(value).serialize(seq), DataComponent::Consumable => get::<ConsumableImpl>(value).serialize(seq),
DataComponent::Equippable => get::<EquippableImpl>(value).serialize(seq), DataComponent::Equippable => get::<EquippableImpl>(value).serialize(seq),
DataComponent::StoredEnchantments => get::<StoredEnchantmentsImpl>(value).serialize(seq), DataComponent::StoredEnchantments => get::<StoredEnchantmentsImpl>(value).serialize(seq),
DataComponent::RepairCost => get::<RepairCostImpl>(value).serialize(seq),
DataComponent::Repairable => get::<RepairableImpl>(value).serialize(seq),
DataComponent::SwingAnimation => get::<SwingAnimationImpl>(value).serialize(seq),
DataComponent::Rarity => get::<RarityImpl>(value).serialize(seq),
DataComponent::UseCooldown => get::<UseCooldownImpl>(value).serialize(seq), DataComponent::UseCooldown => get::<UseCooldownImpl>(value).serialize(seq),
DataComponent::MapId => get::<MapIdImpl>(value).serialize(seq), DataComponent::MapId => get::<MapIdImpl>(value).serialize(seq),
DataComponent::BundleContents => get::<BundleContentsImpl>(value).serialize(seq), DataComponent::BundleContents => get::<BundleContentsImpl>(value).serialize(seq),

View File

@ -12,6 +12,7 @@ use pumpkin_util::version::JavaMinecraftVersion;
use crate::{ use crate::{
ClientPacket, VarInt, ClientPacket, VarInt,
codec::var_long::VarLong,
ser::{NetworkWriteExt, WritingError}, ser::{NetworkWriteExt, WritingError},
}; };
@ -65,8 +66,7 @@ impl ClientPacket for CSetEntityMetadata {
mut write: impl Write, mut write: impl Write,
version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
// 1. Entity ID if *version < JavaMinecraftVersion::V_1_8 {
if *version <= JavaMinecraftVersion::V_1_7_6 {
write.write_i32_be(self.entity_id.0)?; write.write_i32_be(self.entity_id.0)?;
} else { } else {
write.write_var_int(&self.entity_id)?; write.write_var_int(&self.entity_id)?;
@ -141,7 +141,9 @@ impl<T> Metadata<T> {
writer.write_u8(resolved_index)?; writer.write_u8(resolved_index)?;
writer.write_var_int(&VarInt(remapped_type_id))?; writer.write_var_int(&VarInt(remapped_type_id))?;
if self.r#type == MetaDataType::BLOCK_STATE { if self.r#type == MetaDataType::BLOCK_STATE
|| self.r#type == MetaDataType::OPTIONAL_BLOCK_STATE
{
let mut serialized_value = Vec::new(); let mut serialized_value = Vec::new();
self.value.write_metadata(&mut serialized_value, version)?; self.value.write_metadata(&mut serialized_value, version)?;
@ -152,7 +154,11 @@ impl<T> Metadata<T> {
let remapped_state = u16::try_from(decoded_state.0).map_or(decoded_state, |state_id| { let remapped_state = u16::try_from(decoded_state.0).map_or(decoded_state, |state_id| {
VarInt(i32::from(remap_block_state_for_version(state_id, *version))) VarInt(i32::from(remap_block_state_for_version(state_id, *version)))
}); });
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_int(&remapped_state)?; writer.write_var_int(&remapped_state)?;
} else {
writer.write_i32(remapped_state.0)?;
}
return Ok(()); return Ok(());
} }
@ -257,21 +263,71 @@ impl MetadataSerializer for i32 {
fn write_metadata( fn write_metadata(
&self, &self,
writer: &mut impl std::io::Write, writer: &mut impl std::io::Write,
_version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_int(&VarInt(*self))
} else {
writer.write_i32(*self) writer.write_i32(*self)
} }
} }
}
impl MetadataSerializer for u32 { impl MetadataSerializer for u32 {
fn write_metadata( fn write_metadata(
&self, &self,
writer: &mut impl std::io::Write, writer: &mut impl std::io::Write,
_version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_int(&VarInt(*self as i32))
} else {
writer.write_u32(*self) writer.write_u32(*self)
} }
} }
}
impl MetadataSerializer for i64 {
fn write_metadata(
&self,
writer: &mut impl std::io::Write,
version: &JavaMinecraftVersion,
) -> Result<(), WritingError> {
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_long(&VarLong(*self))
} else {
writer.write_i64_be(*self)
}
}
}
impl MetadataSerializer for u64 {
fn write_metadata(
&self,
writer: &mut impl std::io::Write,
version: &JavaMinecraftVersion,
) -> Result<(), WritingError> {
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_long(&VarLong(*self as i64))
} else {
writer.write_u64_be(*self)
}
}
}
impl MetadataSerializer for VarLong {
fn write_metadata(
&self,
writer: &mut impl std::io::Write,
version: &JavaMinecraftVersion,
) -> Result<(), WritingError> {
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_long(self)
} else {
writer.write_i64_be(self.0)
}
}
}
impl MetadataSerializer for f32 { impl MetadataSerializer for f32 {
fn write_metadata( fn write_metadata(
@ -360,13 +416,44 @@ impl MetadataSerializer for Option<String> {
} }
} }
impl MetadataSerializer for pumpkin_util::math::vector3::Vector3<f32> {
fn write_metadata(
&self,
writer: &mut impl std::io::Write,
_version: &JavaMinecraftVersion,
) -> Result<(), WritingError> {
writer.write_f32(self.x)?;
writer.write_f32(self.y)?;
writer.write_f32(self.z)
}
}
impl MetadataSerializer for [f32; 4] {
fn write_metadata(
&self,
writer: &mut impl std::io::Write,
_version: &JavaMinecraftVersion,
) -> Result<(), WritingError> {
writer.write_f32(self[0])?;
writer.write_f32(self[1])?;
writer.write_f32(self[2])?;
writer.write_f32(self[3])
}
}
impl MetadataSerializer for pumpkin_util::math::position::BlockPos { impl MetadataSerializer for pumpkin_util::math::position::BlockPos {
fn write_metadata( fn write_metadata(
&self, &self,
writer: &mut impl std::io::Write, writer: &mut impl std::io::Write,
version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_block_pos(self, version) writer.write_block_pos(self, version)
} else {
writer.write_i32(self.0.x)?;
writer.write_i32(self.0.y)?;
writer.write_i32(self.0.z)
}
} }
} }
@ -378,7 +465,7 @@ impl MetadataSerializer for Option<pumpkin_util::math::position::BlockPos> {
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
if let Some(pos) = self { if let Some(pos) = self {
writer.write_bool(true)?; writer.write_bool(true)?;
writer.write_block_pos(pos, version)?; pos.write_metadata(writer, version)?;
} else { } else {
writer.write_bool(false)?; writer.write_bool(false)?;
} }
@ -390,10 +477,14 @@ impl MetadataSerializer for crate::codec::optional_int::OptionalInt {
fn write_metadata( fn write_metadata(
&self, &self,
writer: &mut impl std::io::Write, writer: &mut impl std::io::Write,
_version: &JavaMinecraftVersion, version: &JavaMinecraftVersion,
) -> Result<(), WritingError> { ) -> Result<(), WritingError> {
let val = self.0.map_or(0, |id| id + 1); let val = self.0.map_or(0, |id| id + 1);
if *version >= JavaMinecraftVersion::V_1_9 {
writer.write_var_int(&VarInt(val)) writer.write_var_int(&VarInt(val))
} else {
writer.write_i32(val)
}
} }
} }
@ -490,4 +581,38 @@ mod tests {
assert_eq!(particle_id, VarInt(29)); assert_eq!(particle_id, VarInt(29));
assert_eq!(data, [0x12, 0x34, 0x56, 0x78]); assert_eq!(data, [0x12, 0x34, 0x56, 0x78]);
} }
#[test]
fn int_metadata_encodes_varint_for_1_9_plus_and_i32_for_legacy() {
let mut buf_modern = Vec::new();
(-1i32)
.write_metadata(&mut buf_modern, &JavaMinecraftVersion::V_1_21)
.unwrap();
// -1 as VarInt is 5 bytes: 0xFF, 0xFF, 0xFF, 0xFF, 0x0F
assert_eq!(buf_modern, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
let mut buf_legacy = Vec::new();
(-1i32)
.write_metadata(&mut buf_legacy, &JavaMinecraftVersion::V_1_8)
.unwrap();
// -1 as i32 BE is 4 bytes: 0xFF, 0xFF, 0xFF, 0xFF
assert_eq!(buf_legacy, vec![0xFF, 0xFF, 0xFF, 0xFF]);
}
#[test]
fn block_pos_metadata_encodes_packed_for_1_9_plus_and_three_ints_for_legacy() {
let pos = pumpkin_util::math::position::BlockPos(
pumpkin_util::math::vector3::Vector3::new(10, 20, 30),
);
let mut buf_modern = Vec::new();
pos.write_metadata(&mut buf_modern, &JavaMinecraftVersion::V_1_21)
.unwrap();
assert_eq!(buf_modern.len(), 8); // packed i64
let mut buf_legacy = Vec::new();
pos.write_metadata(&mut buf_legacy, &JavaMinecraftVersion::V_1_8)
.unwrap();
assert_eq!(buf_legacy.len(), 12); // 3 * i32 (12 bytes)
}
} }

View File

@ -8,7 +8,7 @@ use pumpkin_data::tag::Block::MINECRAFT_LEAVES;
use pumpkin_data::{Block, BlockState, BlockStateId}; use pumpkin_data::{Block, BlockState, BlockStateId};
use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use rustc_hash::FxHashMap; use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::RwLock; use std::sync::RwLock;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@ -651,6 +651,117 @@ impl ChunkData {
old old
} }
/// Sets multiple blocks in the chunk at absolute Y coordinates in a single batch.
///
/// This acquires section write locks and heightmap locks once across all updates,
/// significantly speeding up bulk block modifications.
///
/// Returns a list of `(relative_x, y, relative_z, replaced_block_state_id)`.
pub fn set_blocks_batch(
&self,
updates: impl IntoIterator<Item = (usize, i32, usize, BlockStateId)>,
) -> Vec<(usize, i32, usize, BlockStateId)> {
let min_y = self.section.min_y;
let mut results = Vec::new();
let mut sections = self
.section
.block_sections
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut random_tick_sections_guard = self
.section
.random_tick_sections
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut changed_columns = FxHashSet::default();
let mut modified = false;
for (rel_x, y, rel_z, new_state_id) in updates {
let y_rel = y - min_y;
if y_rel < 0 {
continue;
}
let rel_y = y_rel as usize;
let section_index = rel_y / BlockPalette::SIZE;
let sub_y = rel_y % BlockPalette::SIZE;
if let Some(section) = sections.get_mut(section_index) {
let replaced_id = section.set(rel_x, sub_y, rel_z, new_state_id);
if replaced_id != new_state_id {
modified = true;
changed_columns.insert((rel_x, rel_z));
if (has_random_ticks(new_state_id) || has_random_ticking_fluid(new_state_id))
&& random_tick_sections_guard.is_none()
{
let new_cache = vec![RandomTickSectionCache::default(); self.section.count]
.into_boxed_slice();
*random_tick_sections_guard = Some(new_cache);
}
if let Some(random_tick_sections) = random_tick_sections_guard.as_mut() {
let random_tick_cache = &mut random_tick_sections[section_index];
if has_random_ticks(replaced_id) {
random_tick_cache.random_ticking_block_count = random_tick_cache
.random_ticking_block_count
.saturating_sub(1);
}
if has_random_ticking_fluid(replaced_id) {
random_tick_cache.random_ticking_fluid_count = random_tick_cache
.random_ticking_fluid_count
.saturating_sub(1);
}
if has_random_ticks(new_state_id) {
random_tick_cache.random_ticking_block_count = random_tick_cache
.random_ticking_block_count
.saturating_add(1);
}
if has_random_ticking_fluid(new_state_id) {
random_tick_cache.random_ticking_fluid_count = random_tick_cache
.random_ticking_fluid_count
.saturating_add(1);
}
let mut mask = self
.section
.randomly_ticking_mask
.load(std::sync::atomic::Ordering::Relaxed);
if random_tick_cache.is_randomly_ticking() {
mask |= 1 << section_index;
} else {
mask &= !(1 << section_index);
}
self.section
.randomly_ticking_mask
.store(mask, std::sync::atomic::Ordering::Relaxed);
}
}
results.push((rel_x, y, rel_z, replaced_id));
}
}
if modified {
self.dirty.store(true, std::sync::atomic::Ordering::Relaxed);
}
drop(sections);
drop(random_tick_sections_guard);
if !changed_columns.is_empty() {
let mut heightmap = self
.heightmap
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let highest_non_empty_subchunk = self.get_highest_non_empty_subchunk();
for (x, z) in changed_columns {
self.populate_heightmaps(&mut heightmap, highest_non_empty_subchunk, x, z);
}
}
results
}
fn update_heightmap( fn update_heightmap(
&self, &self,
relative_x: usize, relative_x: usize,

View File

@ -5,9 +5,9 @@ use crate::block::{
PlacedArgs, PlacedArgs,
}; };
use pumpkin_data::BlockStateId;
use pumpkin_data::block_properties::{BlockProperties, WallTorchLikeProperties}; use pumpkin_data::block_properties::{BlockProperties, WallTorchLikeProperties};
use pumpkin_data::translation; use pumpkin_data::translation;
use pumpkin_data::{Block, BlockStateId};
use pumpkin_inventory::anvil::AnvilScreenHandler; use pumpkin_inventory::anvil::AnvilScreenHandler;
use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_inventory::screen_handler::{ use pumpkin_inventory::screen_handler::{
@ -22,6 +22,22 @@ use std::sync::Mutex;
#[pumpkin_block_from_tag("minecraft:anvil")] #[pumpkin_block_from_tag("minecraft:anvil")]
pub struct AnvilBlock; pub struct AnvilBlock;
impl AnvilBlock {
#[must_use]
pub fn damage(state_id: BlockStateId) -> Option<BlockStateId> {
let block = Block::from_state_id(state_id);
let next_block = if block == &Block::ANVIL {
&Block::CHIPPED_ANVIL
} else if block == &Block::CHIPPED_ANVIL {
&Block::DAMAGED_ANVIL
} else {
return None;
};
let props = WallTorchLikeProperties::from_state_id(state_id, block);
Some(props.to_state_id(next_block))
}
}
impl BlockBehaviour for AnvilBlock { impl BlockBehaviour for AnvilBlock {
fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult { fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult {
args.player.increment_stat( args.player.increment_stat(

View File

@ -32,7 +32,7 @@ impl BlockBehaviour for CoralBlock {
}; };
let dead_block_state_id = dead_block.default_state.id; let dead_block_state_id = dead_block.default_state.id;
args.world args.world
.set_block_state(args.position, dead_block_state_id, BlockFlags::empty()); .set_block_state(args.position, dead_block_state_id, BlockFlags::NOTIFY_ALL);
} }
} }
} }

View File

@ -126,7 +126,7 @@ impl BlockBehaviour for CoralFanBlock {
}; };
args.world args.world
.set_block_state(args.position, dead_block_state_id, BlockFlags::empty()); .set_block_state(args.position, dead_block_state_id, BlockFlags::NOTIFY_ALL);
} }
} }

View File

@ -46,7 +46,7 @@ impl BlockBehaviour for CoralPlantBlock {
props.to_state_id(get_dead_type(args.block.id).unwrap_or_default().to_block()) props.to_state_id(get_dead_type(args.block.id).unwrap_or_default().to_block())
}; };
args.world args.world
.set_block_state(args.position, dead_block_state_id, BlockFlags::empty()); .set_block_state(args.position, dead_block_state_id, BlockFlags::NOTIFY_ALL);
} }
} }
fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool {

View File

@ -163,13 +163,9 @@ impl FireBlock {
let mut fire_props = FireProperties::from_state_id(state_id, &Block::FIRE); let mut fire_props = FireProperties::from_state_id(state_id, &Block::FIRE);
fire_props.age = new_age; fire_props.age = new_age;
let new_state_id = fire_props.to_state_id(&Block::FIRE); let new_state_id = fire_props.to_state_id(&Block::FIRE);
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_ALL);
} else { } else {
world.set_block_state( world.set_block_state(pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL);
pos,
Block::AIR.default_state.id,
BlockFlags::NOTIFY_NEIGHBORS,
);
} }
if old_block == &Block::TNT { if old_block == &Block::TNT {
@ -308,7 +304,7 @@ impl BlockBehaviour for FireBlock {
if new_age != age { if new_age != age {
fire_props.age = new_age; fire_props.age = new_age;
let new_state_id = fire_props.to_state_id(&Block::FIRE); let new_state_id = fire_props.to_state_id(&Block::FIRE);
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_ALL);
} }
if !infiniburn { if !infiniburn {
@ -316,11 +312,7 @@ impl BlockBehaviour for FireBlock {
if !Self::are_blocks_around_flammable(world.as_ref(), pos) { if !Self::are_blocks_around_flammable(world.as_ref(), pos) {
let block_below_state = world.get_block_state(&pos.down()); let block_below_state = world.get_block_state(&pos.down());
if !block_below_state.is_side_solid(BlockDirection::Up) || new_age > 3 { if !block_below_state.is_side_solid(BlockDirection::Up) || new_age > 3 {
world.set_block_state( world.set_block_state(pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL);
pos,
Block::AIR.default_state.id,
BlockFlags::NOTIFY_NEIGHBORS,
);
return; return;
} }
} }
@ -330,11 +322,7 @@ impl BlockBehaviour for FireBlock {
&& rand::rng().random_range(0..4) == 0 && rand::rng().random_range(0..4) == 0
&& !Self::is_flammable(world.get_block_state(&pos.down())) && !Self::is_flammable(world.get_block_state(&pos.down()))
{ {
world.set_block_state( world.set_block_state(pos, Block::AIR.default_state.id, BlockFlags::NOTIFY_ALL);
pos,
Block::AIR.default_state.id,
BlockFlags::NOTIFY_NEIGHBORS,
);
return; return;
} }
} }

View File

@ -221,7 +221,7 @@ impl PistonBlock {
world.set_block_state( world.set_block_state(
pos, pos,
props.to_state_id(&Block::MOVING_PISTON), props.to_state_id(&Block::MOVING_PISTON),
BlockFlags::FORCE_STATE, BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
); );
let mut props = PistonProps::default(block); let mut props = PistonProps::default(block);
@ -242,7 +242,7 @@ impl PistonBlock {
world.set_block_state( world.set_block_state(
&extended_pos, &extended_pos,
Block::AIR.default_state.id, Block::AIR.default_state.id,
BlockFlags::FORCE_STATE, BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
); );
world.update_neighbors(pos, None); world.update_neighbors(pos, None);
@ -415,7 +415,11 @@ fn move_piston(
props.facing = dir.to_facing(); props.facing = dir.to_facing();
let state = props.to_state_id(&Block::MOVING_PISTON); let state = props.to_state_id(&Block::MOVING_PISTON);
world.set_block_state(&target_pos, state, BlockFlags::MOVED); world.set_block_state(
&target_pos,
state,
BlockFlags::NOTIFY_ALL | BlockFlags::MOVED,
);
if let Some(moved_state) = moved_block_states.get(moved_blocks.len() - 1 - index) { if let Some(moved_state) = moved_block_states.get(moved_blocks.len() - 1 - index) {
world.add_block_entity(Arc::new(PistonBlockEntity { world.add_block_entity(Arc::new(PistonBlockEntity {
@ -444,7 +448,7 @@ fn move_piston(
world.set_block_state( world.set_block_state(
&extended_pos, &extended_pos,
props.to_state_id(&Block::MOVING_PISTON), props.to_state_id(&Block::MOVING_PISTON),
BlockFlags::MOVED, BlockFlags::NOTIFY_ALL | BlockFlags::MOVED,
); );
let mut props = PistonHeadLikeProperties::default(&Block::PISTON_HEAD); let mut props = PistonHeadLikeProperties::default(&Block::PISTON_HEAD);
props.facing = dir.to_facing(); props.facing = dir.to_facing();

View File

@ -72,7 +72,7 @@ trait CropBlockBase: PlantBlockBase {
let f = get_available_moisture(world, pos, block); let f = get_available_moisture(world, pos, block);
if rand::rng().random_range(0..=(25.0 / f).floor() as i64) == 0 { if rand::rng().random_range(0..=(25.0 / f).floor() as i64) == 0 {
let new_state_id = self.state_with_age(block, state, age + 1); let new_state_id = self.state_with_age(block, state, age + 1);
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS);
} }
} }
} }

View File

@ -23,7 +23,7 @@ impl BlockBehaviour for SugarCaneBlock {
fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) { fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) {
if !can_place_at(args.world.as_ref(), args.position) { if !can_place_at(args.world.as_ref(), args.position) {
args.world args.world
.break_block(args.position, None, BlockFlags::empty()); .break_block(args.position, None, BlockFlags::NOTIFY_ALL);
} }
} }
@ -36,19 +36,19 @@ impl BlockBehaviour for SugarCaneBlock {
let age = CactusLikeProperties::from_state_id(state_id, args.block).age; let age = CactusLikeProperties::from_state_id(state_id, args.block).age;
if age == 15 { if age == 15 {
args.world args.world
.set_block_state(&args.position.up(), state_id, BlockFlags::empty()); .set_block_state(&args.position.up(), state_id, BlockFlags::NOTIFY_ALL);
let props = CactusLikeProperties { age: 0 }; let props = CactusLikeProperties { age: 0 };
args.world.set_block_state( args.world.set_block_state(
args.position, args.position,
props.to_state_id(args.block), props.to_state_id(args.block),
BlockFlags::empty(), BlockFlags::NOTIFY_LISTENERS,
); );
} else { } else {
let props = CactusLikeProperties { age: age + 1 }; let props = CactusLikeProperties { age: age + 1 };
args.world.set_block_state( args.world.set_block_state(
args.position, args.position,
props.to_state_id(args.block), props.to_state_id(args.block),
BlockFlags::empty(), BlockFlags::NOTIFY_LISTENERS,
); );
} }
} }

View File

@ -243,7 +243,7 @@ impl ComparatorBlock {
}; };
let state_id = props.to_state_id(block); let state_id = props.to_state_id(block);
world.set_block_state(&block_pos, state_id, BlockFlags::empty()); world.set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_ALL);
self.update(world, block_pos, BlockState::from_id(state_id), block); self.update(world, block_pos, BlockState::from_id(state_id), block);
} }

View File

@ -341,6 +341,7 @@ impl DispenserBlock {
); );
let arrow = let arrow =
ArrowEntity::new_with_item(arrow_entity, None, &projectile, ArrowPickup::Allowed); ArrowEntity::new_with_item(arrow_entity, None, &projectile, ArrowPickup::Allowed);
arrow.apply_on_projectile_spawned(&projectile);
arrow.set_velocity( arrow.set_velocity(
facing.x, facing.x,
@ -572,15 +573,18 @@ impl DispenserBlock {
&EntityType::SMALL_FIREBALL, &EntityType::SMALL_FIREBALL,
); );
let fireball = SmallFireballEntity::new(entity); let fireball = SmallFireballEntity::new(entity);
// Vanilla aims fire charges straight along the facing axis, without the +0.1 Y bias
// other projectiles get.
let facing = to_normal(ctx.facing); let facing = to_normal(ctx.facing);
fireball.thrown.set_velocity( let dir = Vector3::new(
facing.x, triangle(&mut rng(), facing.x, 0.114_850_000_000_000_01),
facing.y, triangle(&mut rng(), facing.y, 0.114_850_000_000_000_01),
facing.z, triangle(&mut rng(), facing.z, 0.114_850_000_000_000_01),
Self::FIREBALL_PROJECTILE_POWER, )
Self::FIREBALL_PROJECTILE_UNCERTAINTY, .normalize();
fireball.get_entity().set_velocity(dir);
let len = dir.horizontal_length();
fireball.get_entity().set_rotation(
dir.x.atan2(dir.z) as f32 * 57.295_776,
dir.y.atan2(len) as f32 * 57.295_776,
); );
Self::finish_projectile_launch(ctx, Arc::new(fireball), WorldEvent::SoundBlazeFireball); Self::finish_projectile_launch(ctx, Arc::new(fireball), WorldEvent::SoundBlazeFireball);
} }
@ -721,8 +725,7 @@ impl DispenserBlock {
let front_block = ctx.world.get_block(&front); let front_block = ctx.world.get_block(&front);
let ignited = if front_block == &Block::TNT { let ignited = if front_block == &Block::TNT {
TNTBlock::prime(ctx.world, &front); TNTBlock::prime(ctx.world, &front)
true
} else { } else {
Ignition::ignite_block( Ignition::ignite_block(
|world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| { |world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| {

View File

@ -289,7 +289,7 @@ pub fn update_power_strength(world: &Arc<World>, pos: &BlockPos) {
wire.power = target_strength; wire.power = target_strength;
let new_state_id = wire.to_state_id(&Block::REDSTONE_WIRE); let new_state_id = wire.to_state_id(&Block::REDSTONE_WIRE);
world.set_block_state(pos, new_state_id, BlockFlags::empty()); world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS);
let mut to_update = Vec::with_capacity(7); let mut to_update = Vec::with_capacity(7);
to_update.push(*pos); to_update.push(*pos);

View File

@ -219,7 +219,7 @@ impl RepeaterBlock {
let mut props = props; let mut props = props;
props.delay = if props.delay == 4 { 1 } else { props.delay + 1 }; props.delay = if props.delay == 4 { 1 } else { props.delay + 1 };
let state = props.to_state_id(block); let state = props.to_state_id(block);
world.set_block_state(&block_pos, state, BlockFlags::empty()); world.set_block_state(&block_pos, state, BlockFlags::NOTIFY_ALL);
} }
fn is_locked( fn is_locked(

View File

@ -1,29 +1,41 @@
use pumpkin_data::Block;
use pumpkin_data::BlockStateId;
use pumpkin_data::block_properties::{BlockProperties, TntLikeProperties};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::sound::SoundCategory;
use pumpkin_data::translation;
use pumpkin_macros::pumpkin_block;
use pumpkin_util::GameMode;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::text::TextComponent;
use pumpkin_world::world::BlockFlags;
use rand::RngExt;
use std::sync::Arc; use std::sync::Arc;
use super::redstone::block_receives_redstone_power;
use crate::block::registry::BlockActionResult; use crate::block::registry::BlockActionResult;
use crate::block::{ use crate::block::{
BlockBehaviour, ExplodeArgs, OnNeighborUpdateArgs, PlacedArgs, UseWithItemArgs, BlockBehaviour, BrokenArgs, ExplodeArgs, OnNeighborUpdateArgs, OnProjectileHitArgs, PlacedArgs,
UseWithItemArgs,
}; };
use crate::entity::Entity; use crate::entity::Entity;
use crate::entity::tnt::TNTEntity; use crate::entity::tnt::TNTEntity;
use crate::world::World; use crate::world::World;
use pumpkin_data::BlockStateId;
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::SoundCategory;
use pumpkin_macros::pumpkin_block;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::BlockFlags;
use rand::RngExt;
use super::redstone::block_receives_redstone_power;
#[pumpkin_block("minecraft:tnt")] #[pumpkin_block("minecraft:tnt")]
pub struct TNTBlock; pub struct TNTBlock;
const DEFAULT_FUSE: u32 = 80;
const DEFAULT_POWER: f32 = 4.0;
impl TNTBlock { impl TNTBlock {
pub fn prime(world: &Arc<World>, location: &BlockPos) { pub fn prime(world: &Arc<World>, location: &BlockPos) -> bool {
if !world.level_info.load().game_rules.tnt_explodes {
return false;
}
let mut event = crate::plugin::api::events::block::tnt_prime::TNTPrimeEvent::new( let mut event = crate::plugin::api::events::block::tnt_prime::TNTPrimeEvent::new(
*location, *location,
"REDSTONE".to_string(), "REDSTONE".to_string(),
@ -32,10 +44,15 @@ impl TNTBlock {
server.plugin_manager.fire_blocking(&server, &mut event); server.plugin_manager.fire_blocking(&server, &mut event);
} }
if event.cancelled { if event.cancelled {
return; return false;
} }
let entity = Entity::new(world.clone(), location.to_f64(), &EntityType::TNT); let spawn_pos = Vector3::new(
location.0.x as f64 + 0.5,
location.0.y as f64,
location.0.z as f64 + 0.5,
);
let entity = Entity::new(world.clone(), spawn_pos, &EntityType::TNT);
let mut prime_event = let mut prime_event =
crate::plugin::api::events::entity::explosion_prime::ExplosionPrimeEvent::new( crate::plugin::api::events::entity::explosion_prime::ExplosionPrimeEvent::new(
entity.entity_id, entity.entity_id,
@ -48,64 +65,91 @@ impl TNTBlock {
.fire_blocking(&server, &mut prime_event); .fire_blocking(&server, &mut prime_event);
} }
if prime_event.cancelled { if prime_event.cancelled {
return; return false;
} }
let pos = entity.pos.load();
let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, DEFAULT_FUSE)); let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, DEFAULT_FUSE));
world.spawn_entity(tnt); world.spawn_entity(tnt);
world.play_sound( world.play_sound(
pumpkin_data::sound::Sound::EntityTntPrimed, pumpkin_data::sound::Sound::EntityTntPrimed,
SoundCategory::Blocks, SoundCategory::Blocks,
&pos, &spawn_pos,
); );
world.set_block_state(location, BlockStateId::AIR, BlockFlags::NOTIFY_ALL); world.set_block_state(location, BlockStateId::AIR, BlockFlags::NOTIFY_ALL);
true
} }
} }
const DEFAULT_FUSE: u32 = 80;
const DEFAULT_POWER: f32 = 4.0;
impl BlockBehaviour for TNTBlock { impl BlockBehaviour for TNTBlock {
fn use_with_item(&self, args: UseWithItemArgs<'_>) -> BlockActionResult { fn use_with_item(&self, args: UseWithItemArgs<'_>) -> BlockActionResult {
{ let item_id = args.item_stack.item.id;
let item = args.item_stack.item; if item_id != Item::FLINT_AND_STEEL.id && item_id != Item::FIRE_CHARGE.id {
if item != &Item::FLINT_AND_STEEL || item == &Item::FIRE_CHARGE { return BlockActionResult::PassToDefaultBlockAction;
return BlockActionResult::Pass;
} }
let world = args.player.world();
Self::prime(&world, args.position);
BlockActionResult::Consume if Self::prime(args.world, args.position) {
if args.player.gamemode.load() != GameMode::Creative {
if item_id == Item::FLINT_AND_STEEL.id {
let _ = args.item_stack.damage_item(1);
} else {
args.item_stack.decrement(1);
}
}
BlockActionResult::Success
} else if !args.world.level_info.load().game_rules.tnt_explodes {
args.player.send_system_message_raw(
&TextComponent::translate(translation::java::BLOCK_MINECRAFT_TNT_DISABLED, []),
true,
);
BlockActionResult::Pass
} else {
BlockActionResult::Success
} }
} }
fn placed(&self, args: PlacedArgs<'_>) { fn placed(&self, args: PlacedArgs<'_>) {
if args.block != Block::from_state_id(args.old_state_id)
&& block_receives_redstone_power(args.world, args.position)
{ {
if block_receives_redstone_power(args.world, args.position) {
Self::prime(args.world, args.position); Self::prime(args.world, args.position);
} }
} }
}
fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) { fn on_neighbor_update(&self, args: OnNeighborUpdateArgs<'_>) {
{
if block_receives_redstone_power(args.world, args.position) { if block_receives_redstone_power(args.world, args.position) {
Self::prime(args.world, args.position); Self::prime(args.world, args.position);
} }
} }
fn broken(&self, args: BrokenArgs<'_>) {
if args.player.gamemode.load() != GameMode::Creative {
let props = TntLikeProperties::from_state_id(args.state.id, args.block);
if props.r#unstable {
Self::prime(args.world, args.position);
}
}
}
fn on_projectile_hit(&self, args: OnProjectileHitArgs<'_>) {
if args.projectile.get_entity().is_on_fire() {
Self::prime(args.world, args.position);
}
} }
fn explode(&self, args: ExplodeArgs<'_>) { fn explode(&self, args: ExplodeArgs<'_>) {
{ if !args.world.level_info.load().game_rules.tnt_explodes {
let entity = Entity::new(args.world.clone(), args.position.to_f64(), &EntityType::TNT); return;
let angle = rand::random::<f64>() * std::f64::consts::TAU; }
entity.set_velocity(Vector3::new(-angle.sin() * 0.02, 0.2, -angle.cos() * 0.02)); let spawn_pos = Vector3::new(
args.position.0.x as f64 + 0.5,
args.position.0.y as f64,
args.position.0.z as f64 + 0.5,
);
let entity = Entity::new(args.world.clone(), spawn_pos, &EntityType::TNT);
let fuse = rand::rng().random_range(0..DEFAULT_FUSE / 4) + DEFAULT_FUSE / 8; let fuse = rand::rng().random_range(0..DEFAULT_FUSE / 4) + DEFAULT_FUSE / 8;
let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, fuse)); let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, fuse));
args.world.spawn_entity(tnt); args.world.spawn_entity(tnt);
} }
}
fn should_drop_items_on_explosion(&self) -> bool { fn should_drop_items_on_explosion(&self) -> bool {
false false

View File

@ -82,6 +82,8 @@ pub trait BlockBehaviour: Send + Sync {
fn on_entity_collision(&self, _args: OnEntityCollisionArgs<'_>) {} fn on_entity_collision(&self, _args: OnEntityCollisionArgs<'_>) {}
fn on_projectile_hit(&self, _args: OnProjectileHitArgs<'_>) {}
/// Called when an entity is standing on / walking over the top face of this block. /// Called when an entity is standing on / walking over the top face of this block.
fn on_entity_step(&self, _args: OnEntityStepArgs<'_>) {} fn on_entity_step(&self, _args: OnEntityStepArgs<'_>) {}
@ -227,6 +229,16 @@ pub struct OnEntityCollisionArgs<'a> {
pub entity: &'a dyn EntityBase, pub entity: &'a dyn EntityBase,
} }
pub struct OnProjectileHitArgs<'a> {
pub server: &'a Server,
pub world: &'a Arc<World>,
pub block: &'a Block,
pub state: &'a BlockState,
pub position: &'a BlockPos,
pub projectile: &'a dyn EntityBase,
pub hit_pos: &'a Vector3<f64>,
}
pub struct OnEntityStepArgs<'a> { pub struct OnEntityStepArgs<'a> {
pub world: &'a Arc<World>, pub world: &'a Arc<World>,
pub block: &'a Block, pub block: &'a Block,

View File

@ -159,8 +159,8 @@ use crate::block::fluid::lava::FlowingLava;
use crate::block::fluid::water::FlowingWater; use crate::block::fluid::water::FlowingWater;
use crate::block::{ use crate::block::{
BlockBehaviour, BlockHitResult, BlockMetadata, BonemealArgs, FluidMetadata, BlockBehaviour, BlockHitResult, BlockMetadata, BonemealArgs, FluidMetadata,
GetInsideCollisionShapeArgs, OnEntityCollisionArgs, OnLandedUponArgs, GetInsideCollisionShapeArgs, OnEntityCollisionArgs, OnEntityStepArgs, OnLandedUponArgs,
UpdateEntityMovementAfterFallOnArgs, stop_vertical_movement_after_fall, OnProjectileHitArgs, UpdateEntityMovementAfterFallOnArgs, stop_vertical_movement_after_fall,
}; };
use crate::entity::EntityBase; use crate::entity::EntityBase;
use crate::entity::player::Player; use crate::entity::player::Player;
@ -176,6 +176,7 @@ use pumpkin_data::{Block, BlockDirection, BlockId, BlockState};
use pumpkin_protocol::java::server::play::SUseItemOn; use pumpkin_protocol::java::server::play::SUseItemOn;
use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::{BlockAccessor, BlockFlags}; use pumpkin_world::world::{BlockAccessor, BlockFlags};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::sync::Arc; use std::sync::Arc;
@ -190,7 +191,6 @@ use super::{
OnPlaceArgs, OnStateReplacedArgs, OnSyncedBlockEventArgs, PlacedArgs, PlayerPlacedArgs, OnPlaceArgs, OnStateReplacedArgs, OnSyncedBlockEventArgs, PlacedArgs, PlayerPlacedArgs,
PrepareArgs, UseWithItemArgs, PrepareArgs, UseWithItemArgs,
}; };
use crate::block::OnEntityStepArgs;
use crate::block::blocks::blast_furnace::BlastFurnaceBlock; use crate::block::blocks::blast_furnace::BlastFurnaceBlock;
use crate::block::blocks::chain::ChainBlock; use crate::block::blocks::chain::ChainBlock;
use crate::block::blocks::cobweb::CobwebBlock; use crate::block::blocks::cobweb::CobwebBlock;
@ -818,6 +818,30 @@ impl BlockRegistry {
} }
} }
#[allow(clippy::too_many_arguments)]
pub fn on_projectile_hit(
&self,
block: &Block,
world: &Arc<World>,
projectile: &dyn EntityBase,
position: &BlockPos,
state: &BlockState,
hit_pos: &Vector3<f64>,
server: &Server,
) {
if let Some(pumpkin_block) = self.get_pumpkin_block(block.id) {
pumpkin_block.on_projectile_hit(OnProjectileHitArgs {
server,
world,
block,
state,
position,
projectile,
hit_pos,
});
}
}
pub fn on_entity_collision_fluid(&self, fluid: &Fluid, entity: &dyn EntityBase) { pub fn on_entity_collision_fluid(&self, fluid: &Fluid, entity: &dyn EntityBase) {
let pumpkin_fluid = self.get_pumpkin_fluid(fluid.id); let pumpkin_fluid = self.get_pumpkin_fluid(fluid.id);
if let Some(pumpkin_fluid) = pumpkin_fluid { if let Some(pumpkin_fluid) = pumpkin_fluid {

View File

@ -238,6 +238,8 @@ impl CommandExecutor for CloneExecutor {
} }
} }
world.flush_block_updates();
context.source.send_feedback( context.source.send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_CLONE_SUCCESS, translation::java::COMMANDS_CLONE_SUCCESS,

View File

@ -2,6 +2,7 @@ use pumpkin_data::translation;
use pumpkin_data::{Block, BlockStateId}; use pumpkin_data::{Block, BlockStateId};
use pumpkin_util::PermissionLvl; use pumpkin_util::PermissionLvl;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
@ -49,6 +50,7 @@ enum FilterMode {
KeepAir, KeepAir,
} }
#[expect(clippy::too_many_lines)]
fn fill_blocks( fn fill_blocks(
source: &CommandSource, source: &CommandSource,
from: BlockPos, from: BlockPos,
@ -84,13 +86,54 @@ fn fill_blocks(
} }
let target_state_id = target_block.default_state.id; let target_state_id = target_block.default_state.id;
let mut count = 0; let mut changed_positions = Vec::new();
let min_chunk_x = min_x >> 4;
let max_chunk_x = max_x >> 4;
let min_chunk_z = min_z >> 4;
let max_chunk_z = max_z >> 4;
for chunk_x in min_chunk_x..=max_chunk_x {
for chunk_z in min_chunk_z..=max_chunk_z {
let chunk_pos = Vector2::new(chunk_x, chunk_z);
let local_start_x = (min_x - (chunk_x << 4)).clamp(0, 15) as usize;
let local_end_x = (max_x - (chunk_x << 4)).clamp(0, 15) as usize;
let local_start_z = (min_z - (chunk_z << 4)).clamp(0, 15) as usize;
let local_end_z = (max_z - (chunk_z << 4)).clamp(0, 15) as usize;
let result = world.level.read_chunk_sync(&chunk_pos, |chunk| {
let mut updates_for_chunk = Vec::new();
let mut block_entities_to_remove = Vec::new();
let min_y_chunk = chunk.section.min_y;
{
let block_sections = chunk
.section
.block_sections
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for x in min_x..=max_x {
for y in min_y..=max_y { for y in min_y..=max_y {
for z in min_z..=max_z { let y_rel = y - min_y_chunk;
let pos = BlockPos(Vector3::new(x, y, z)); if y_rel < 0 {
let current_block = world.get_block(&pos); continue;
}
let rel_y = y_rel as usize;
let section_index = rel_y / pumpkin_world::chunk::CHUNK_WIDTH;
let sub_y = rel_y % pumpkin_world::chunk::CHUNK_WIDTH;
let Some(section) = block_sections.get(section_index) else {
continue;
};
for rel_z in local_start_z..=local_end_z {
for rel_x in local_start_x..=local_end_x {
let global_x = (chunk_x << 4) + rel_x as i32;
let global_z = (chunk_z << 4) + rel_z as i32;
let pos = BlockPos(Vector3::new(global_x, y, global_z));
let current_state_id = section.get(rel_x, sub_y, rel_z);
let current_block = Block::from_state_id(current_state_id);
match mode { match mode {
FillMode::Keep => { FillMode::Keep => {
@ -107,12 +150,12 @@ fn fill_blocks(
} }
} }
let is_edge = x == min_x let is_edge = global_x == min_x
|| x == max_x || global_x == max_x
|| y == min_y || y == min_y
|| y == max_y || y == max_y
|| z == min_z || global_z == min_z
|| z == max_z; || global_z == max_z;
let block_to_place = match mode { let block_to_place = match mode {
FillMode::Outline => is_edge.then_some(target_state_id), FillMode::Outline => is_edge.then_some(target_state_id),
@ -124,28 +167,65 @@ fn fill_blocks(
} }
} }
FillMode::Destroy => { FillMode::Destroy => {
if current_state_id != target_state_id {
world.break_block( world.break_block(
&pos, &pos,
None, None,
BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE, BlockFlags::SKIP_DROPS
| BlockFlags::NOTIFY_ALL
| BlockFlags::FORCE_STATE,
); );
}
Some(target_state_id) Some(target_state_id)
} }
FillMode::Replace | FillMode::Keep => Some(target_state_id), FillMode::Replace | FillMode::Keep => Some(target_state_id),
}; };
if let Some(state_id) = block_to_place { if let Some(state_id) = block_to_place {
world.set_block_state(&pos, state_id, BlockFlags::FORCE_STATE); if current_block.default_state.block_entity_type != u16::MAX {
count += 1; block_entities_to_remove.push(pos);
}
updates_for_chunk.push((rel_x, y, rel_z, state_id, pos));
}
} }
} }
} }
} }
if updates_for_chunk.is_empty() {
return (Vec::new(), block_entities_to_remove);
}
let batch_inputs = updates_for_chunk
.iter()
.map(|&(rx, y, rz, sid, _)| (rx, y, rz, sid));
chunk.set_blocks_batch(batch_inputs);
let chunk_changed = updates_for_chunk
.into_iter()
.map(|(_, _, _, new_id, pos)| (pos, new_id))
.collect::<Vec<_>>();
(chunk_changed, block_entities_to_remove)
});
if let Some((chunk_changed, be_to_remove)) = result {
for pos in be_to_remove {
world.remove_block_entity(&pos);
}
changed_positions.extend(chunk_changed);
}
}
}
let count = changed_positions.len() as i32;
if count == 0 { if count == 0 {
return Err(ERROR_FAILED.create_without_context()); return Err(ERROR_FAILED.create_without_context());
} }
world.queue_block_updates(&changed_positions);
world.flush_block_updates();
source.send_feedback( source.send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
translation::java::COMMANDS_FILL_SUCCESS, translation::java::COMMANDS_FILL_SUCCESS,

View File

@ -47,11 +47,15 @@ impl CommandExecutor for SetBlockExecutor {
let success = match mode { let success = match mode {
Mode::Destroy => { Mode::Destroy => {
world.break_block(&pos, None, BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE); world.break_block(
&pos,
None,
BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
);
world.set_block_state( world.set_block_state(
&pos, &pos,
block_state_id, block_state_id,
BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS, BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
); );
true true
} }
@ -59,7 +63,7 @@ impl CommandExecutor for SetBlockExecutor {
world.set_block_state( world.set_block_state(
&pos, &pos,
block_state_id, block_state_id,
BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS, BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
); );
true true
} }
@ -69,7 +73,7 @@ impl CommandExecutor for SetBlockExecutor {
world.set_block_state( world.set_block_state(
&pos, &pos,
block_state_id, block_state_id,
BlockFlags::FORCE_STATE | BlockFlags::NOTIFY_NEIGHBORS, BlockFlags::NOTIFY_ALL | BlockFlags::FORCE_STATE,
); );
true true
} else { } else {
@ -77,12 +81,19 @@ impl CommandExecutor for SetBlockExecutor {
} }
} }
Mode::Strict => { Mode::Strict => {
world.set_block_state(&pos, block_state_id, BlockFlags::SKIP_BLOCK_ADDED_CALLBACK); world.set_block_state(
&pos,
block_state_id,
BlockFlags::NOTIFY_LISTENERS
| BlockFlags::SKIP_BLOCK_ADDED_CALLBACK
| BlockFlags::FORCE_STATE,
);
true true
} }
}; };
if success { if success {
world.flush_block_updates();
context.source.send_feedback( context.source.send_feedback(
TextComponent::translate_cross( TextComponent::translate_cross(
pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS, pumpkin_data::translation::java::COMMANDS_SETBLOCK_SUCCESS,

View File

@ -0,0 +1,19 @@
use pumpkin_data::enchantment::LevelBasedValue;
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.AddValue`.
#[derive(Clone, Debug, PartialEq)]
pub struct AddValue {
pub value: LevelBasedValue,
}
impl AddValue {
#[must_use]
pub const fn new(value: LevelBasedValue) -> Self {
Self { value }
}
#[must_use]
pub fn process(&self, level: i32, current_value: f32) -> f32 {
current_value + self.value.calculate(level)
}
}

View File

@ -0,0 +1,14 @@
use pumpkin_data::enchantment::EnchantmentEntityEffect;
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.AllOf`.
#[derive(Clone, Debug, PartialEq)]
pub struct AllOf<'a> {
pub effects: &'a [EnchantmentEntityEffect],
}
impl<'a> AllOf<'a> {
#[must_use]
pub const fn new(effects: &'a [EnchantmentEntityEffect]) -> Self {
Self { effects }
}
}

View File

@ -0,0 +1,19 @@
use pumpkin_data::enchantment::LevelBasedValue;
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.DamageEntity`.
#[derive(Clone, Debug, PartialEq)]
pub struct DamageEntity {
pub damage: LevelBasedValue,
}
impl DamageEntity {
#[must_use]
pub const fn new(damage: LevelBasedValue) -> Self {
Self { damage }
}
#[must_use]
pub fn calculate_damage(&self, level: i32) -> f32 {
self.damage.calculate(level)
}
}

View File

@ -0,0 +1,68 @@
use pumpkin_data::damage::DamageType;
use pumpkin_data::enchantment::LevelBasedValue;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::Sound;
use pumpkin_util::math::vector3::Vector3;
use crate::world::ExplosionInteraction;
/// Enchantment entity effect that produces an explosion at an offset position.
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.ExplodeEffect`.
#[derive(Clone, Debug, PartialEq)]
pub struct ExplodeEffect {
pub attribute_to_user: bool,
pub damage_type: Option<&'static DamageType>,
pub knockback_multiplier: Option<LevelBasedValue>,
pub immune_blocks: Option<&'static str>,
pub offset: Vector3<f64>,
pub radius: LevelBasedValue,
pub create_fire: bool,
pub block_interaction: ExplosionInteraction,
pub small_particle: Option<Particle>,
pub large_particle: Option<Particle>,
pub sound: Option<Sound>,
}
impl ExplodeEffect {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub const fn new(
attribute_to_user: bool,
damage_type: Option<&'static DamageType>,
knockback_multiplier: Option<LevelBasedValue>,
immune_blocks: Option<&'static str>,
offset: Vector3<f64>,
radius: LevelBasedValue,
create_fire: bool,
block_interaction: ExplosionInteraction,
small_particle: Option<Particle>,
large_particle: Option<Particle>,
sound: Option<Sound>,
) -> Self {
Self {
attribute_to_user,
damage_type,
knockback_multiplier,
immune_blocks,
offset,
radius,
create_fire,
block_interaction,
small_particle,
large_particle,
sound,
}
}
#[must_use]
pub fn calculate_radius(&self, level: i32) -> f32 {
self.radius.calculate(level).max(0.0)
}
#[must_use]
pub fn calculate_knockback(&self, level: i32) -> Option<f32> {
self.knockback_multiplier
.as_ref()
.map(|kb| kb.calculate(level))
}
}

View File

@ -0,0 +1,23 @@
use crate::entity::{Entity, EntityBase};
use pumpkin_data::enchantment::LevelBasedValue;
/// Enchantment entity effect that sets an entity on fire for a duration calculated from the level.
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.Ignite`.
#[derive(Clone, Debug, PartialEq)]
pub struct Ignite {
pub duration: LevelBasedValue,
}
impl Ignite {
#[must_use]
pub const fn new(duration: LevelBasedValue) -> Self {
Self { duration }
}
/// Applies the ignite effect to an entity for the given enchantment level.
pub fn apply(&self, level: i32, entity: &Entity) {
let seconds = self.duration.calculate(level);
entity.set_on_fire_for(seconds);
entity.set_on_fire(true);
}
}

View File

@ -0,0 +1,21 @@
pub mod add_value;
pub mod all_of;
pub mod damage_entity;
pub mod explode;
pub mod ignite;
pub mod multiply_value;
pub mod play_sound;
pub mod remove_binomial;
pub mod replace_block;
pub mod set_value;
pub use add_value::AddValue;
pub use all_of::AllOf;
pub use damage_entity::DamageEntity;
pub use explode::ExplodeEffect;
pub use ignite::Ignite;
pub use multiply_value::MultiplyValue;
pub use play_sound::PlaySound;
pub use remove_binomial::RemoveBinomial;
pub use replace_block::ReplaceBlock;
pub use set_value::SetValue;

View File

@ -0,0 +1,19 @@
use pumpkin_data::enchantment::LevelBasedValue;
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.MultiplyValue`.
#[derive(Clone, Debug, PartialEq)]
pub struct MultiplyValue {
pub factor: LevelBasedValue,
}
impl MultiplyValue {
#[must_use]
pub const fn new(factor: LevelBasedValue) -> Self {
Self { factor }
}
#[must_use]
pub fn process(&self, level: i32, current_value: f32) -> f32 {
current_value * self.factor.calculate(level)
}
}

View File

@ -0,0 +1,12 @@
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.PlaySound`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PlaySound {
pub sound: &'static str,
}
impl PlaySound {
#[must_use]
pub const fn new(sound: &'static str) -> Self {
Self { sound }
}
}

View File

@ -0,0 +1,24 @@
use pumpkin_data::enchantment::LevelBasedValue;
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.RemoveBinomial`.
#[derive(Clone, Debug, PartialEq)]
pub struct RemoveBinomial {
pub chance: LevelBasedValue,
}
impl RemoveBinomial {
#[must_use]
pub const fn new(chance: LevelBasedValue) -> Self {
Self { chance }
}
#[must_use]
pub fn process(&self, level: i32, current_value: f32) -> f32 {
let prob = self.chance.calculate(level);
if rand::random::<f32>() < prob {
0.0
} else {
current_value
}
}
}

View File

@ -0,0 +1,33 @@
use pumpkin_data::game_event::GameEvent;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
/// Enchantment entity effect that replaces a block at an offset position.
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.ReplaceBlock`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ReplaceBlock {
pub offset: Vector3<i32>,
pub trigger_game_event: Option<GameEvent>,
}
impl ReplaceBlock {
#[must_use]
pub const fn new(offset: Vector3<i32>, trigger_game_event: Option<GameEvent>) -> Self {
Self {
offset,
trigger_game_event,
}
}
#[must_use]
pub const fn target_position(&self, origin: Vector3<f64>) -> BlockPos {
let base_x = origin.x.floor() as i32;
let base_y = origin.y.floor() as i32;
let base_z = origin.z.floor() as i32;
BlockPos::new(
base_x + self.offset.x,
base_y + self.offset.y,
base_z + self.offset.z,
)
}
}

View File

@ -0,0 +1,19 @@
use pumpkin_data::enchantment::LevelBasedValue;
/// Matches vanilla `net.minecraft.world.item.enchantment.effects.SetValue`.
#[derive(Clone, Debug, PartialEq)]
pub struct SetValue {
pub value: LevelBasedValue,
}
impl SetValue {
#[must_use]
pub const fn new(value: LevelBasedValue) -> Self {
Self { value }
}
#[must_use]
pub fn process(&self, level: i32) -> f32 {
self.value.calculate(level)
}
}

View File

@ -0,0 +1,169 @@
use crate::enchantment::effects::Ignite;
use crate::entity::Entity;
use crate::entity::projectile::arrow::ArrowEntity;
use pumpkin_data::data_component_impl::EnchantmentsImpl;
use pumpkin_data::enchantment::{Enchantment, EnchantmentEntityEffect, EnchantmentTarget};
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
/// Data-driven helper for enchantment effects matching vanilla `EnchantmentHelper`.
pub struct EnchantmentHelper;
impl EnchantmentHelper {
/// Iterates through enchantments on an item stack, matching vanilla `runIterationOnItem`.
pub fn run_iteration_on_item<F>(item_stack: &ItemStack, mut visitor: F)
where
F: FnMut(&'static Enchantment, i32),
{
if let Some(enchantments) = item_stack.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
visitor(enchantment, *level);
}
}
}
/// Applies projectile-spawned enchantment effects (e.g. Flame ignites the projectile for 100s).
pub fn on_projectile_spawned(
weapon: &ItemStack,
projectile_entity: &Entity,
arrow: Option<&ArrowEntity>,
) {
Self::run_iteration_on_item(weapon, |enchantment, level| {
for conditional_effect in enchantment.effects.projectile_spawned {
if let EnchantmentEntityEffect::Ignite { duration } = &conditional_effect.effect {
let ignite = Ignite::new(duration.clone());
ignite.apply(level, projectile_entity);
if let Some(arrow) = arrow {
arrow.set_flame(true);
}
}
}
});
}
/// Applies post-attack enchantment effects (e.g. Fire Aspect ignites victim).
pub fn on_post_attack(_attacker: &Entity, victim: &Entity, weapon: &ItemStack) {
Self::run_iteration_on_item(weapon, |enchantment, level| {
for targeted_effect in enchantment.effects.post_attack {
if targeted_effect.affected == Some(EnchantmentTarget::Victim)
&& let EnchantmentEntityEffect::Ignite { duration } = &targeted_effect.effect
{
let ignite = Ignite::new(duration.clone());
ignite.apply(level, victim);
}
}
});
}
/// Computes projectile spread angle using data-driven projectile spread effects.
#[must_use]
pub fn process_projectile_spread(weapon: &ItemStack, base_spread: f32) -> f32 {
let mut spread = base_spread;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_projectile_spread(*level, &mut spread);
}
}
spread
}
/// Computes projectile count using data-driven projectile count effects.
#[must_use]
pub fn process_projectile_count(weapon: &ItemStack, base_count: usize) -> usize {
let mut count = base_count as f32;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_projectile_count(*level, &mut count);
}
}
count as usize
}
/// Computes projectile piercing level using data-driven piercing effects.
#[must_use]
pub fn process_projectile_piercing(weapon: &ItemStack, base_piercing: u8) -> u8 {
let mut piercing = f32::from(base_piercing);
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_piercing_count(*level, &mut piercing);
}
}
piercing as u8
}
/// Computes ammo use using data-driven ammo use effects (e.g. Infinity).
#[must_use]
pub fn process_ammo_use(weapon: &ItemStack, projectile: &ItemStack, base_ammo: i32) -> i32 {
let mut ammo = base_ammo as f32;
if projectile.item.id == Item::ARROW.id
&& let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>()
{
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_ammo_count(*level, &mut ammo);
}
}
ammo as i32
}
/// Modifies damage using data-driven damage effects (e.g. Power / Sharpness).
#[must_use]
pub fn modify_damage(weapon: &ItemStack, base_damage: f64) -> f64 {
let mut damage = base_damage;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_damage(*level, &mut damage);
}
}
damage
}
/// Modifies smash / fall-based damage using data-driven effects (e.g. Density).
#[must_use]
pub fn modify_fall_based_damage(weapon: &ItemStack, base_damage: f64) -> f64 {
let mut damage = base_damage;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_fall_based_damage(*level, &mut damage);
}
}
damage
}
/// Modifies knockback using data-driven knockback effects (e.g. Punch / Knockback).
#[must_use]
pub fn modify_knockback(weapon: &ItemStack, base_knockback: f32) -> f32 {
let mut knockback = base_knockback;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_knockback(*level, &mut knockback);
}
}
knockback
}
/// Modifies armor effectiveness using data-driven effects (e.g. Breach).
#[must_use]
pub fn modify_armor_effectiveness(weapon: &ItemStack, base_effectiveness: f32) -> f32 {
let mut effectiveness = base_effectiveness;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
enchantment.modify_armor_effectiveness(*level, &mut effectiveness);
}
}
effectiveness
}
/// Modifies crossbow charge time using data-driven charge time effects (e.g. Quick Charge).
#[must_use]
pub fn modify_crossbow_charge_time(weapon: &ItemStack, base_ticks: i32) -> i32 {
let mut charge_time = base_ticks as f32;
if let Some(enchantments) = weapon.get_data_component::<EnchantmentsImpl>() {
for (enchantment, level) in enchantments.enchantment.iter() {
let mut change_sec = 0.0f32;
enchantment.modify_crossbow_charge_time(*level, &mut change_sec);
charge_time += change_sec * 20.0;
}
}
(charge_time as i32).max(0)
}
}

View File

@ -0,0 +1,6 @@
pub mod effects;
pub mod helper;
pub use effects::*;
pub use helper::EnchantmentHelper;
pub use pumpkin_data::enchantment::*;

View File

@ -75,7 +75,31 @@ impl BowAttackGoal {
let arrow_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::ARROW); let arrow_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::ARROW);
let projectile = ItemStack::new(1, &Item::ARROW); let projectile = ItemStack::new(1, &Item::ARROW);
let arrow = ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed); let bow_item = Self::main_hand_item(mob);
let arrow = ArrowEntity::new_shot_with_weapon(
arrow_entity,
entity,
&projectile,
&bow_item,
ArrowPickup::Disallowed,
);
// Vanilla scales base damage with power and world difficulty
let difficulty = world.level_info.load().difficulty as i32;
arrow.set_base_damage_from_mob(Self::ARROW_SPEED, difficulty);
arrow.set_base_damage(crate::enchantment::EnchantmentHelper::modify_damage(
&bow_item,
arrow.get_base_damage(),
));
arrow.punch_level.store(
crate::enchantment::EnchantmentHelper::modify_knockback(&bow_item, 0.0) as u8,
std::sync::atomic::Ordering::Relaxed,
);
arrow.apply_on_projectile_spawned(&projectile);
if entity.is_on_fire() {
arrow.set_flame(true);
}
let mob_pos = entity.pos.load(); let mob_pos = entity.pos.load();
let target_entity = target.get_entity(); let target_entity = target.get_entity();
@ -88,7 +112,6 @@ impl BowAttackGoal {
let horizontal_distance = dx.hypot(dz); let horizontal_distance = dx.hypot(dz);
// Vanilla scales the spread with the world difficulty: 14 - difficulty * 4. // Vanilla scales the spread with the world difficulty: 14 - difficulty * 4.
let difficulty = world.level_info.load().difficulty as i32;
let divergence = f64::from(14 - difficulty * 4); let divergence = f64::from(14 - difficulty * 4);
arrow.set_velocity( arrow.set_velocity(

View File

@ -75,9 +75,39 @@ impl RangedCrossbowAttackGoal {
let target_entity = target.get_entity(); let target_entity = target.get_entity();
let target_pos = target_entity.pos.load(); let target_pos = target_entity.pos.load();
let equipment_guard = mob
.get_mob_entity()
.living_entity
.entity_equipment
.try_lock();
let weapon_opt = equipment_guard
.as_ref()
.ok()
.map(|eq| eq.get(&EquipmentSlot::MAIN_HAND));
let arrow_entity = Entity::new(world.clone(), mob_pos, &EntityType::ARROW); let arrow_entity = Entity::new(world.clone(), mob_pos, &EntityType::ARROW);
let projectile = ItemStack::new(1, &Item::ARROW); let projectile = ItemStack::new(1, &Item::ARROW);
let arrow = ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed); let arrow = if let Some(ref weapon) = weapon_opt {
ArrowEntity::new_shot_with_weapon(
arrow_entity,
entity,
&projectile,
weapon,
ArrowPickup::Allowed,
)
} else {
ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed)
};
if let Some(ref item) = weapon_opt {
arrow.set_pierce_level(
crate::enchantment::EnchantmentHelper::process_projectile_piercing(item, 0),
);
}
arrow.apply_on_projectile_spawned(&projectile);
if entity.is_on_fire() {
arrow.set_flame(true);
}
let dx = target_pos.x - mob_pos.x; let dx = target_pos.x - mob_pos.x;
let dy = (target_pos.y + f64::from(target_entity.entity_dimension.load().height) / 3.0) let dy = (target_pos.y + f64::from(target_entity.entity_dimension.load().height) / 3.0)

View File

@ -27,7 +27,7 @@ use pumpkin_util::version::JavaMinecraftVersion;
use rand::RngExt; use rand::RngExt;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering};
use uuid::Uuid; use uuid::Uuid;
pub mod bat; pub mod bat;
@ -82,6 +82,7 @@ pub struct MobEntity {
pub love_ticks: AtomicI32, pub love_ticks: AtomicI32,
pub breeding_cooldown: AtomicI32, pub breeding_cooldown: AtomicI32,
pub breeder: AtomicCell<Option<Uuid>>, pub breeder: AtomicCell<Option<Uuid>>,
pub persistence_required: AtomicBool,
mob_flags: AtomicU8, mob_flags: AtomicU8,
last_sent_yaw: AtomicU8, last_sent_yaw: AtomicU8,
last_sent_pitch: AtomicU8, last_sent_pitch: AtomicU8,
@ -178,6 +179,7 @@ impl MobEntity {
love_ticks: AtomicI32::new(0), love_ticks: AtomicI32::new(0),
breeding_cooldown: AtomicI32::new(0), breeding_cooldown: AtomicI32::new(0),
breeder: AtomicCell::new(None), breeder: AtomicCell::new(None),
persistence_required: AtomicBool::new(false),
mob_flags: AtomicU8::new(0), mob_flags: AtomicU8::new(0),
last_sent_yaw: AtomicU8::new(0), last_sent_yaw: AtomicU8::new(0),
last_sent_pitch: AtomicU8::new(0), last_sent_pitch: AtomicU8::new(0),
@ -265,6 +267,9 @@ impl MobEntity {
if self.can_pick_up_loot() { if self.can_pick_up_loot() {
nbt.put_bool("CanPickUpLoot", true); nbt.put_bool("CanPickUpLoot", true);
} }
if self.persistence_required.load(Relaxed) {
nbt.put_bool("PersistenceRequired", true);
}
} }
pub fn read_mob_nbt(&self, nbt: &NbtCompound) { pub fn read_mob_nbt(&self, nbt: &NbtCompound) {
@ -277,6 +282,10 @@ impl MobEntity {
if let Some(can_pick_up_loot) = nbt.get_bool("CanPickUpLoot") { if let Some(can_pick_up_loot) = nbt.get_bool("CanPickUpLoot") {
self.set_can_pick_up_loot(can_pick_up_loot); self.set_can_pick_up_loot(can_pick_up_loot);
} }
if let Some(persistence_required) = nbt.get_bool("PersistenceRequired") {
self.persistence_required
.store(persistence_required, Relaxed);
}
} }
pub fn add_goal<G: crate::entity::ai::goal::Goal + 'static>(&self, priority: u8, goal: G) { pub fn add_goal<G: crate::entity::ai::goal::Goal + 'static>(&self, priority: u8, goal: G) {
@ -559,6 +568,48 @@ impl MobEntity {
false false
} }
pub fn check_despawn(&self, mob: &dyn Mob) {
let entity = &self.living_entity.entity;
if self.persistence_required.load(Relaxed) {
return;
}
if (**entity.custom_name.load()).is_some() {
return;
}
let world = entity.world.load();
let pos = entity.pos.load();
let players = world.players.load();
let nearest_dist_sq = players
.iter()
.filter(|p| p.gamemode.load() != pumpkin_util::GameMode::Spectator)
.map(|p| {
let pp = p.get_entity().pos.load();
let dx = pp.x - pos.x;
let dy = pp.y - pos.y;
let dz = pp.z - pos.z;
dx * dx + dy * dy + dz * dz
})
.fold(f64::MAX, f64::min);
if nearest_dist_sq == f64::MAX {
mob.get_entity().remove();
return;
}
if nearest_dist_sq > 128.0 * 128.0 {
mob.get_entity().remove();
return;
}
if nearest_dist_sq > 32.0 * 32.0 && rand::random::<i32>().wrapping_abs() % 800 == 0 {
mob.get_entity().remove();
}
}
} }
pub trait Mob: EntityBase + Send + Sync { pub trait Mob: EntityBase + Send + Sync {
@ -1032,6 +1083,8 @@ impl<T: Mob + Send + 'static> EntityBase for T {
} }
} }
mob_entity.check_despawn(self);
self.mob_tick(caller); self.mob_tick(caller);
let age = mob_entity.living_entity.entity.age.load(Relaxed); let age = mob_entity.living_entity.entity.age.load(Relaxed);

View File

@ -301,6 +301,26 @@ pub trait EntityBase: Send + Sync + std::any::Any {
None None
} }
fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
self.get_mob().map_or_else(
|| {
let entity = self.get_entity();
let shared_flags = entity.flags.load(Ordering::Relaxed);
(shared_flags != 0).then(|| {
let mut buf = Vec::new();
let _ = Metadata::new(
pumpkin_data::tracked_data::entity::DATA_SHARED_FLAGS_ID,
shared_flags,
)
.write(&mut buf, &version);
buf.put_u8(255);
buf.into_boxed_slice()
})
},
|mob| mob.mob_java_spawn_metadata(version),
)
}
fn send_bedrock_spawn_packet(&self, client: &BedrockClient) { fn send_bedrock_spawn_packet(&self, client: &BedrockClient) {
let entity = self.get_entity(); let entity = self.get_entity();
let runtime_id = entity.entity_id as u64; let runtime_id = entity.entity_id as u64;
@ -340,10 +360,8 @@ pub trait EntityBase: Send + Sync + std::any::Any {
let entity = self.get_entity(); let entity = self.get_entity();
let version = client.version.load(); let version = client.version.load();
let is_mob = entity.entity_type.mob || self.get_mob().is_some(); let is_mob = entity.entity_type.mob || self.get_mob().is_some();
let metadata = self.java_spawn_metadata(version);
if version < JavaMinecraftVersion::V_1_19 && is_mob { if version < JavaMinecraftVersion::V_1_19 && is_mob {
let metadata = self
.get_mob()
.and_then(|mob| mob.mob_java_spawn_metadata(version));
let spawn_packet = entity.create_spawn_living_packet(metadata.clone()); let spawn_packet = entity.create_spawn_living_packet(metadata.clone());
if let Ok(data) = client.serialize_packet(&spawn_packet) { if let Ok(data) = client.serialize_packet(&spawn_packet) {
client.try_enqueue_packet(data); client.try_enqueue_packet(data);
@ -361,10 +379,8 @@ pub trait EntityBase: Send + Sync + std::any::Any {
if let Ok(data) = client.serialize_packet(&spawn_packet) { if let Ok(data) = client.serialize_packet(&spawn_packet) {
client.try_enqueue_packet(data); client.try_enqueue_packet(data);
} }
if let Some(mob) = self.get_mob() if let Some(meta) = metadata {
&& let Some(metadata) = mob.mob_java_spawn_metadata(version) let meta_packet = CSetEntityMetadata::new(entity.entity_id.into(), meta);
{
let meta_packet = CSetEntityMetadata::new(entity.entity_id.into(), metadata);
if let Ok(meta_data) = client.serialize_packet(&meta_packet) { if let Ok(meta_data) = client.serialize_packet(&meta_packet) {
client.try_enqueue_packet(meta_data); client.try_enqueue_packet(meta_data);
} }
@ -2808,6 +2824,11 @@ impl Entity {
} }
} }
#[must_use]
pub fn is_on_fire(&self) -> bool {
self.fire_ticks.load(Ordering::Relaxed) > 0 || self.has_visual_fire.load(Ordering::Relaxed)
}
pub fn get_horizontal_facing(&self) -> HorizontalFacing { pub fn get_horizontal_facing(&self) -> HorizontalFacing {
let yaw = self.yaw.load(); let yaw = self.yaw.load();
// Use vanilla's formula: floor(angle / 90.0 + 0.5) & 3 // Use vanilla's formula: floor(angle / 90.0 + 0.5) & 3

View File

@ -2444,8 +2444,8 @@ mod tests {
.iter() .iter()
.find(|trade| trade.modifier == VillagerTradeModifier::EnchantRandomly) .find(|trade| trade.modifier == VillagerTradeModifier::EnchantRandomly)
.unwrap(); .unwrap();
assert!(enchanted_book.wants.item == &Item::EMERALD); assert_eq!(enchanted_book.wants.item, &Item::EMERALD);
assert!(enchanted_book.wants_b.unwrap().item == &Item::BOOK); assert_eq!(enchanted_book.wants_b.unwrap().item, &Item::BOOK);
assert_eq!(enchanted_book.price_multiplier, 0.2); assert_eq!(enchanted_book.price_multiplier, 0.2);
let cartographer = VillagerProfession::Cartographer.trade_set(2).unwrap(); let cartographer = VillagerProfession::Cartographer.trade_set(2).unwrap();

View File

@ -1277,7 +1277,7 @@ mod tests {
#[test] #[test]
fn invisibility_potion_item_stack_creation() { fn invisibility_potion_item_stack_creation() {
let potion = create_invisibility_potion(); let potion = create_invisibility_potion();
assert!(potion.item == &Item::POTION); assert_eq!(potion.item, &Item::POTION);
assert_eq!(potion.item_count, 1); assert_eq!(potion.item_count, 1);
assert!(!potion.patch.is_empty()); assert!(!potion.patch.is_empty());
} }

View File

@ -5158,7 +5158,7 @@ impl Player {
.as_any_mut() .as_any_mut()
.downcast_mut::<pumpkin_inventory::anvil::AnvilScreenHandler>() .downcast_mut::<pumpkin_inventory::anvil::AnvilScreenHandler>()
{ {
anvil_handler.update_item_name(packet.item_name.to_string()); anvil_handler.set_item_name(packet.item_name, self.has_infinite_materials());
} }
} }
@ -7363,6 +7363,47 @@ impl InventoryPlayer for Player {
fn close_screen_handler(&self) { fn close_screen_handler(&self) {
self.close_handled_screen(); self.close_handled_screen();
} }
fn use_anvil(&self) {
if let Some(pos) = self.open_container_pos.load() {
let world = self.world();
let state = world.get_block_state(&pos);
let block = pumpkin_data::Block::from_state_id(state.id);
if block.has_tag(&pumpkin_data::tag::Block::MINECRAFT_ANVIL) {
if !self.has_infinite_materials() && rand::random::<f32>() < 0.12 {
if let Some(new_state) =
crate::block::blocks::anvil::AnvilBlock::damage(state.id)
{
world.set_block_state(
&pos,
new_state,
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
);
world.sync_world_event(
pumpkin_data::world::WorldEvent::SoundAnvilUsed,
pos,
0,
);
} else {
world.set_block_state(
&pos,
pumpkin_data::BlockStateId::AIR,
pumpkin_world::world::BlockFlags::NOTIFY_ALL,
);
world.sync_world_event(
pumpkin_data::world::WorldEvent::SoundAnvilBroken,
pos,
0,
);
}
} else {
world.sync_world_event(pumpkin_data::world::WorldEvent::SoundAnvilUsed, pos, 0);
}
} else {
world.sync_world_event(pumpkin_data::world::WorldEvent::SoundAnvilUsed, pos, 0);
}
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,12 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use std::sync::RwLock; use std::sync::RwLock;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
use crate::entity::projectile::ProjectileHit; use crate::entity::projectile::ProjectileHit;
use crate::{ use crate::{
entity::{Entity, EntityBase, living::LivingEntity, player::Player}, entity::{Entity, EntityBase, living::LivingEntity, player::Player},
server::Server, server::Server,
}; };
use bytes::BufMut;
use pumpkin_data::damage::DamageType; use pumpkin_data::damage::DamageType;
use pumpkin_data::data_component_impl::PotionDurationScaleImpl; use pumpkin_data::data_component_impl::PotionDurationScaleImpl;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
@ -15,11 +16,11 @@ use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::particle::Particle; use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_protocol::IdOr; use pumpkin_protocol::IdOr;
use pumpkin_protocol::java::client::play::CEntityVelocity; use pumpkin_protocol::java::client::play::{CEntityVelocity, CSoundEffect, Metadata};
use pumpkin_protocol::java::client::play::CSoundEffect;
use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::version::JavaMinecraftVersion;
/// Represents the pickup rules for arrows /// Represents the pickup rules for arrows
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -53,9 +54,10 @@ pub struct ArrowEntity {
pub entity: Entity, pub entity: Entity,
pub owner_id: Option<i32>, pub owner_id: Option<i32>,
pub item_stack: RwLock<ItemStack>, pub item_stack: RwLock<ItemStack>,
pub base_damage: f64, pub base_damage: AtomicU64,
pub pickup: ArrowPickup, pub pickup: ArrowPickup,
pub is_critical: AtomicBool, pub is_critical: AtomicBool,
pub no_physics: AtomicBool,
pub pierce_level: AtomicU8, pub pierce_level: AtomicU8,
pub punch_level: AtomicU8, pub punch_level: AtomicU8,
pub is_flame: AtomicBool, pub is_flame: AtomicBool,
@ -65,6 +67,8 @@ pub struct ArrowEntity {
pub shake_time: AtomicU8, pub shake_time: AtomicU8,
pub has_hit: AtomicBool, pub has_hit: AtomicBool,
pub last_block_pos: Arc<std::sync::RwLock<Option<BlockPos>>>, pub last_block_pos: Arc<std::sync::RwLock<Option<BlockPos>>>,
pub pierced_entities: Arc<RwLock<Vec<i32>>>,
pub weapon: RwLock<Option<ItemStack>>,
} }
impl ArrowEntity { impl ArrowEntity {
@ -89,9 +93,10 @@ impl ArrowEntity {
entity, entity,
owner_id, owner_id,
item_stack: RwLock::new(item_stack.copy_with_count(1)), item_stack: RwLock::new(item_stack.copy_with_count(1)),
base_damage: Self::ARROW_BASE_DAMAGE, base_damage: AtomicU64::new(Self::ARROW_BASE_DAMAGE.to_bits()),
pickup, pickup,
is_critical: AtomicBool::new(false), is_critical: AtomicBool::new(false),
no_physics: AtomicBool::new(false),
pierce_level: AtomicU8::new(0), pierce_level: AtomicU8::new(0),
punch_level: AtomicU8::new(0), punch_level: AtomicU8::new(0),
is_flame: AtomicBool::new(false), is_flame: AtomicBool::new(false),
@ -101,6 +106,8 @@ impl ArrowEntity {
shake_time: AtomicU8::new(0), shake_time: AtomicU8::new(0),
has_hit: AtomicBool::new(false), has_hit: AtomicBool::new(false),
last_block_pos: Arc::new(std::sync::RwLock::new(None)), last_block_pos: Arc::new(std::sync::RwLock::new(None)),
pierced_entities: Arc::new(RwLock::new(Vec::new())),
weapon: RwLock::new(None),
} }
} }
@ -128,9 +135,10 @@ impl ArrowEntity {
entity, entity,
owner_id: Some(shooter.entity_id), owner_id: Some(shooter.entity_id),
item_stack: RwLock::new(item_stack.copy_with_count(1)), item_stack: RwLock::new(item_stack.copy_with_count(1)),
base_damage: Self::ARROW_BASE_DAMAGE, base_damage: AtomicU64::new(Self::ARROW_BASE_DAMAGE.to_bits()),
pickup, pickup,
is_critical: AtomicBool::new(false), is_critical: AtomicBool::new(false),
no_physics: AtomicBool::new(false),
pierce_level: AtomicU8::new(0), pierce_level: AtomicU8::new(0),
punch_level: AtomicU8::new(0), punch_level: AtomicU8::new(0),
is_flame: AtomicBool::new(false), is_flame: AtomicBool::new(false),
@ -140,9 +148,39 @@ impl ArrowEntity {
shake_time: AtomicU8::new(0), shake_time: AtomicU8::new(0),
has_hit: AtomicBool::new(false), has_hit: AtomicBool::new(false),
last_block_pos: Arc::new(std::sync::RwLock::new(None)), last_block_pos: Arc::new(std::sync::RwLock::new(None)),
pierced_entities: Arc::new(RwLock::new(Vec::new())),
weapon: RwLock::new(None),
} }
} }
pub fn new_shot_with_weapon(
entity: Entity,
shooter: &Entity,
item_stack: &ItemStack,
weapon: &ItemStack,
pickup: ArrowPickup,
) -> Self {
let mut arrow = Self::new_shot(entity, shooter, item_stack, pickup);
arrow.weapon = RwLock::new(Some(weapon.copy_with_count(1)));
arrow
}
#[must_use]
pub fn get_weapon_item(&self) -> Option<ItemStack> {
self.weapon.read().ok().and_then(|w| w.clone())
}
/// Applies projectile-spawned enchantment effects matching vanilla `Projectile::applyOnProjectileSpawned`.
pub fn apply_on_projectile_spawned(&self, pickup_item_stack: &ItemStack) {
let weapon = self.weapon.read().ok().and_then(|w| w.clone());
super::apply_on_projectile_spawned(
self.get_entity(),
pickup_item_stack,
weapon.as_ref(),
Some(self),
);
}
#[must_use] #[must_use]
pub const fn entity_type_for_item(item: &'static Item) -> &'static EntityType { pub const fn entity_type_for_item(item: &'static Item) -> &'static EntityType {
if item.id == Item::SPECTRAL_ARROW.id { if item.id == Item::SPECTRAL_ARROW.id {
@ -193,6 +231,40 @@ impl ArrowEntity {
damage_succeeded damage_succeeded
} }
#[must_use]
pub fn get_effect_color(item_stack: &ItemStack) -> i32 {
if let Some(pc) =
item_stack.get_data_component::<pumpkin_data::data_component_impl::PotionContentsImpl>()
{
if let Some(color) = pc.custom_color {
return color;
}
let effects = crate::item::potion::PotionContents::read_potion_effects(item_stack);
if effects.is_empty() {
return -1;
}
let mut r = 0;
let mut g = 0;
let mut b = 0;
let mut total = 0;
for (effect, _, amplifier, _, _, _) in effects {
let color = effect.color;
let weight = (amplifier as i32) + 1;
r += weight * ((color >> 16) & 0xFF);
g += weight * ((color >> 8) & 0xFF);
b += weight * (color & 0xFF);
total += weight;
}
if total == 0 {
-1
} else {
((r / total) << 16) | ((g / total) << 8) | (b / total)
}
} else {
-1
}
}
pub fn set_velocity_from_rotation( pub fn set_velocity_from_rotation(
&self, &self,
pitch: f32, pitch: f32,
@ -240,16 +312,88 @@ impl ArrowEntity {
); );
} }
fn get_flags(&self) -> u8 {
let mut flags = 0u8;
if self.is_critical.load(Ordering::Relaxed) {
flags |= 0x01;
}
if self.no_physics.load(Ordering::Relaxed) {
flags |= 0x02;
}
flags
}
pub fn set_critical(&self, critical: bool) { pub fn set_critical(&self, critical: bool) {
self.is_critical.store(critical, Ordering::Relaxed); self.is_critical.store(critical, Ordering::Relaxed);
let flags = self.get_flags();
self.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::abstract_arrow::ID_FLAGS,
flags,
)],
None,
);
}
pub fn set_no_physics(&self, no_physics: bool) {
self.no_physics.store(no_physics, Ordering::Relaxed);
let flags = self.get_flags();
self.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::abstract_arrow::ID_FLAGS,
flags,
)],
None,
);
}
#[must_use]
pub fn is_no_physics(&self) -> bool {
self.no_physics.load(Ordering::Relaxed)
}
pub fn set_base_damage_from_mob(&self, power: f64, difficulty_id: i32) {
fn next_triangular(mode: f64, deviation: f64) -> f64 {
deviation.mul_add(rand::random::<f64>() - rand::random::<f64>(), mode)
}
let diff_factor = f64::from(difficulty_id) * 0.11;
let base = power * 2.0 + next_triangular(diff_factor, 0.57425);
self.set_base_damage(base);
} }
pub fn set_pierce_level(&self, level: u8) { pub fn set_pierce_level(&self, level: u8) {
self.pierce_level.store(level, Ordering::Relaxed); self.pierce_level.store(level, Ordering::Relaxed);
self.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::abstract_arrow::PIERCE_LEVEL,
level,
)],
None,
);
} }
pub const fn set_base_damage(&self, _damage: f64) { #[must_use]
// TODO: implement this pub fn get_base_damage(&self) -> f64 {
f64::from_bits(self.base_damage.load(Ordering::Relaxed))
}
pub fn set_base_damage(&self, damage: f64) {
self.base_damage.store(damage.to_bits(), Ordering::Relaxed);
}
pub fn set_flame(&self, flame: bool) {
self.is_flame.store(flame, Ordering::Relaxed);
if flame {
self.entity.set_on_fire_for(100.0);
self.entity.set_on_fire(true);
} else {
self.entity.extinguish();
}
}
#[must_use]
pub fn is_on_fire(&self) -> bool {
self.entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed)
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -275,6 +419,16 @@ impl EntityBase for ArrowEntity {
.read() .read()
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
Self::write_item_stack_nbt(&item_stack, nbt); Self::write_item_stack_nbt(&item_stack, nbt);
nbt.put_bool("crit", self.is_critical.load(Ordering::Relaxed));
nbt.put_double("damage", self.get_base_damage());
nbt.put_bool("inGround", self.in_ground.load(Ordering::Relaxed));
nbt.put_int("life", self.life.load(Ordering::Relaxed) as i32);
nbt.put_byte("shake", self.shake_time.load(Ordering::Relaxed) as i8);
nbt.put_byte(
"PierceLevel",
self.pierce_level.load(Ordering::Relaxed) as i8,
);
nbt.put_byte("pickup", self.pickup.to_byte() as i8);
} }
fn read_custom_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) { fn read_custom_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) {
@ -284,12 +438,183 @@ impl EntityBase for ArrowEntity {
.write() .write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = item_stack; .unwrap_or_else(std::sync::PoisonError::into_inner) = item_stack;
} }
if let Some(crit) = nbt.get_bool("crit") {
self.is_critical.store(crit, Ordering::Relaxed);
} }
if let Some(damage) = nbt.get_double("damage") {
self.set_base_damage(damage);
}
if let Some(in_ground) = nbt
.get_bool("inGround")
.or_else(|| nbt.get_byte("inGround").map(|b| b != 0))
{
self.in_ground.store(in_ground, Ordering::Relaxed);
}
if let Some(life) = nbt
.get_int("life")
.or_else(|| nbt.get_short("life").map(i32::from))
{
self.life.store(life.max(0) as u32, Ordering::Relaxed);
}
if let Some(shake) = nbt.get_byte("shake") {
self.shake_time.store(shake.max(0) as u8, Ordering::Relaxed);
}
if let Some(pierce) = nbt.get_byte("PierceLevel") {
self.pierce_level
.store(pierce.max(0) as u8, Ordering::Relaxed);
}
}
fn init_data_tracker(&self) {
let entity = self.get_entity();
let flags = self.get_flags();
let pierce = self.pierce_level.load(Ordering::Relaxed);
let in_ground = self.in_ground.load(Ordering::Relaxed);
if entity.entity_type.id == EntityType::SPECTRAL_ARROW.id {
entity.send_meta_data(
&[
Metadata::new(pumpkin_data::tracked_data::spectral_arrow::ID_FLAGS, flags),
Metadata::new(
pumpkin_data::tracked_data::spectral_arrow::PIERCE_LEVEL,
pierce,
),
],
None,
);
entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::spectral_arrow::IN_GROUND,
in_ground,
)],
None,
);
} else {
let item_stack = self
.item_stack
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let color = Self::get_effect_color(&item_stack);
entity.send_meta_data(
&[
Metadata::new(pumpkin_data::tracked_data::arrow::ID_FLAGS, flags),
Metadata::new(pumpkin_data::tracked_data::arrow::PIERCE_LEVEL, pierce),
],
None,
);
entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::arrow::IN_GROUND,
in_ground,
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::arrow::ID_EFFECT_COLOR,
color,
)],
None,
);
}
if self.is_on_fire() {
entity.set_on_fire(true);
}
}
fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
let entity = self.get_entity();
let flags = self.get_flags();
let pierce = self.pierce_level.load(Ordering::Relaxed);
let in_ground = self.in_ground.load(Ordering::Relaxed);
let shared_flags = entity.flags.load(Ordering::Relaxed);
let mut buf = Vec::new();
if shared_flags != 0 {
let _ = Metadata::new(
pumpkin_data::tracked_data::entity::DATA_SHARED_FLAGS_ID,
shared_flags,
)
.write(&mut buf, &version);
}
if entity.entity_type.id == EntityType::SPECTRAL_ARROW.id {
let _ = Metadata::new(pumpkin_data::tracked_data::spectral_arrow::ID_FLAGS, flags)
.write(&mut buf, &version);
let _ = Metadata::new(
pumpkin_data::tracked_data::spectral_arrow::PIERCE_LEVEL,
pierce,
)
.write(&mut buf, &version);
let _ = Metadata::new(
pumpkin_data::tracked_data::spectral_arrow::IN_GROUND,
in_ground,
)
.write(&mut buf, &version);
} else {
let item_stack = self
.item_stack
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let color = Self::get_effect_color(&item_stack);
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::ID_FLAGS, flags)
.write(&mut buf, &version);
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::PIERCE_LEVEL, pierce)
.write(&mut buf, &version);
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::IN_GROUND, in_ground)
.write(&mut buf, &version);
if color != -1 {
let _ = Metadata::new(pumpkin_data::tracked_data::arrow::ID_EFFECT_COLOR, color)
.write(&mut buf, &version);
}
}
(!buf.is_empty()).then(|| {
buf.put_u8(255);
buf.into_boxed_slice()
})
}
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn tick(&self, caller: &dyn EntityBase, _server: &Server) { fn tick(&self, caller: &dyn EntityBase, _server: &Server) {
let entity = self.get_entity(); let entity = self.get_entity();
let world = entity.world.load(); let world = entity.world.load();
// Fire & Extinguish logic
let mut fire_ticks = entity.fire_ticks.load(Ordering::Relaxed);
let touching_water = entity.touching_water.load(Ordering::Relaxed);
let in_water = touching_water || entity.is_in_water();
let block_pos = entity.block_pos.load();
let in_rain = world.is_raining_at(&block_pos);
if in_water || in_rain {
if entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed) {
entity.extinguish();
self.is_flame.store(false, Ordering::Relaxed);
}
} else if fire_ticks > 0 {
fire_ticks -= 1;
entity.fire_ticks.store(fire_ticks, Ordering::Relaxed);
if fire_ticks <= 0 && self.is_flame.load(Ordering::Relaxed) {
self.is_flame.store(false, Ordering::Relaxed);
entity.set_on_fire(false);
}
}
// Check if arrow enters lava or fire block
let current_block = world.get_block(&block_pos);
if current_block == &pumpkin_data::Block::LAVA {
entity.set_on_fire_for(15.0);
self.is_flame.store(true, Ordering::Relaxed);
} else if current_block == &pumpkin_data::Block::FIRE
|| current_block == &pumpkin_data::Block::SOUL_FIRE
{
entity.set_on_fire_for(8.0);
self.is_flame.store(true, Ordering::Relaxed);
}
let is_on_fire = entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed);
entity.set_on_fire(is_on_fire);
// Handle shake time // Handle shake time
let shake = self.shake_time.load(Ordering::Relaxed); let shake = self.shake_time.load(Ordering::Relaxed);
if shake > 0 { if shake > 0 {
@ -297,6 +622,30 @@ impl EntityBase for ArrowEntity {
} }
if self.in_ground.load(Ordering::Relaxed) { if self.in_ground.load(Ordering::Relaxed) {
// Check if the block we are stuck into was broken / turned to air
let last_pos = *self
.last_block_pos
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(pos) = last_pos {
let block = world.get_block(&pos);
if block.is_air() {
self.in_ground.store(false, Ordering::Relaxed);
entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::abstract_arrow::IN_GROUND,
false,
)],
None,
);
let mut vel = entity.velocity.load();
vel.x *= rand::random::<f64>() * 0.2;
vel.y *= rand::random::<f64>() * 0.2;
vel.z *= rand::random::<f64>() * 0.2;
entity.velocity.store(vel);
}
}
// Increment in-ground time and life // Increment in-ground time and life
let _in_ground_time = self.in_ground_time.fetch_add(1, Ordering::Relaxed); let _in_ground_time = self.in_ground_time.fetch_add(1, Ordering::Relaxed);
let life = self.life.fetch_add(1, Ordering::Relaxed); let life = self.life.fetch_add(1, Ordering::Relaxed);
@ -316,7 +665,7 @@ impl EntityBase for ArrowEntity {
velocity.y -= Self::GRAVITY; velocity.y -= Self::GRAVITY;
// Apply inertia (air resistance or water drag) // Apply inertia (air resistance or water drag)
let inertia = if entity.touching_water.load(Ordering::Relaxed) { let inertia = if in_water {
Self::WATER_INERTIA Self::WATER_INERTIA
} else { } else {
Self::AIR_INERTIA Self::AIR_INERTIA
@ -336,16 +685,54 @@ impl EntityBase for ArrowEntity {
let new_pos = start_pos.add(&velocity); let new_pos = start_pos.add(&velocity);
entity.set_pos(new_pos); entity.set_pos(new_pos);
// Spawn critical particle trail while arrow is flying and critical // Spawn particles while arrow is flying
if self.is_critical.load(Ordering::Relaxed) { if in_water {
for i in 0..4 {
let factor = 0.25 * f64::from(i);
world.spawn_particle(
Vector3::new(
new_pos.x - velocity.x * factor,
new_pos.y - velocity.y * factor,
new_pos.z - velocity.z * factor,
),
Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32),
0.0,
1,
Particle::Bubble,
);
}
}
if is_on_fire {
world.spawn_particle( world.spawn_particle(
entity.pos.load(), entity.pos.load(),
Vector3::new(0.0f32, 0.0f32, 0.0f32), Vector3::new(0.0f32, 0.0f32, 0.0f32),
0.0, 0.0,
1, 1,
Particle::Flame,
);
}
if self.is_critical.load(Ordering::Relaxed) {
for i in 0..4 {
let factor = f64::from(i) / 4.0;
world.spawn_particle(
Vector3::new(
start_pos.x + velocity.x * factor,
start_pos.y + velocity.y * factor,
start_pos.z + velocity.z * factor,
),
Vector3::new(
-velocity.x as f32,
(-velocity.y + 0.2) as f32,
-velocity.z as f32,
),
0.0,
1,
Particle::Crit, Particle::Crit,
); );
} }
}
// Broadcast velocity update // Broadcast velocity update
let packet = CEntityVelocity::new(entity.entity_id.into(), velocity); let packet = CEntityVelocity::new(entity.entity_id.into(), velocity);
@ -421,13 +808,27 @@ impl EntityBase for ArrowEntity {
// Handle hit // Handle hit
if let Some(h) = hit { if let Some(h) = hit {
// Ensure hit is only processed once match h {
ProjectileHit::Block { .. } => {
if self.has_hit.swap(true, Ordering::SeqCst) { if self.has_hit.swap(true, Ordering::SeqCst) {
return; return;
} }
caller.on_hit(h); caller.on_hit(h);
} }
ProjectileHit::Entity { .. } => {
let pierce = self.pierce_level.load(Ordering::Relaxed);
let pierced_len = self
.pierced_entities
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
if pierced_len >= pierce as usize && self.has_hit.swap(true, Ordering::SeqCst) {
return;
}
caller.on_hit(h);
}
}
}
} }
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
@ -469,6 +870,13 @@ impl EntityBase for ArrowEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pos); .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pos);
let block = world.get_block(&pos); let block = world.get_block(&pos);
let state = world.get_block_state(&pos);
if let Some(server) = world.server.upgrade() {
world
.block_registry
.on_projectile_hit(block, &world, self, &pos, state, &hit_pos, &server);
}
if block == &pumpkin_data::Block::TARGET if block == &pumpkin_data::Block::TARGET
&& let Some(player) = self.owner_id.and_then(|id| world.get_player_by_id(id)) && let Some(player) = self.owner_id.and_then(|id| world.get_player_by_id(id))
{ {
@ -477,83 +885,122 @@ impl EntityBase for ArrowEntity {
); );
} }
// Stop the arrow // Stop the arrow with slight position offset backwards
let velocity = entity.velocity.load();
let norm_dir = Vector3::new(
velocity.x.signum(),
velocity.y.signum(),
velocity.z.signum(),
);
let offset = norm_dir.multiply(0.05, 0.05, 0.05);
entity.set_pos(hit_pos.sub(&offset));
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
entity.set_pos(hit_pos);
// Play sound // Notify client that arrow is in ground
entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::abstract_arrow::IN_GROUND,
true,
)],
None,
);
// Play sound with vanilla pitch formula
let sound_pitch = 1.2 / (rand::random::<f32>() * 0.2 + 0.9);
let sound_packet = CSoundEffect::new( let sound_packet = CSoundEffect::new(
IdOr::Id(Sound::EntityArrowHit as u16), IdOr::Id(Sound::EntityArrowHit as u16),
SoundCategory::Neutral, SoundCategory::Neutral,
&hit_pos, &hit_pos,
1.0, 1.0,
1.0, sound_pitch,
0.0, 0.0,
); );
let chunk_pos = entity.chunk_pos.load(); let chunk_pos = entity.chunk_pos.load();
world.broadcast_to_chunk(chunk_pos, &sound_packet); world.broadcast_to_chunk(chunk_pos, &sound_packet);
// Reset critical flag // Reset critical flag and pierce level
self.is_critical.store(false, Ordering::Relaxed); self.set_critical(false);
self.set_pierce_level(0);
self.pierced_entities
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
} }
ProjectileHit::Entity { ProjectileHit::Entity {
entity: target, entity: target,
hit_pos, hit_pos,
.. ..
} => { } => {
let target_entity_id = target.get_entity().entity_id;
self.pierced_entities
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(target_entity_id);
// Calculate damage // Calculate damage
let velocity = entity.velocity.load(); let velocity = entity.velocity.load();
let power = velocity.length(); let power = velocity.length();
let mut damage = (power * self.base_damage).ceil() as i32; let mut damage = (power * self.get_base_damage()).ceil() as i32;
// Apply critical hit bonus // Apply critical hit bonus
if self.is_critical.load(Ordering::Relaxed) { if self.is_critical.load(Ordering::Relaxed) {
let bonus = (rand::random::<u32>() % (damage / 2 + 2) as u32) as i32; let bonus = (rand::random::<u32>() % (damage.max(0) / 2 + 2) as u32) as i32;
damage = damage.saturating_add(bonus); damage = damage.saturating_add(bonus);
} }
if self.is_flame.load(Ordering::Relaxed) {
target.get_entity().set_on_fire_for_ticks(100); let is_enderman =
target.get_entity().entity_type == &pumpkin_data::entity::EntityType::ENDERMAN;
let is_on_fire = entity.is_on_fire() || self.is_flame.load(Ordering::Relaxed);
if is_on_fire && !is_enderman {
target.get_entity().set_on_fire_for(5.0);
} }
let punch = self.punch_level.load(Ordering::Relaxed); let punch = self.punch_level.load(Ordering::Relaxed);
let is_spectral = entity.entity_type.id == EntityType::SPECTRAL_ARROW.id; let is_spectral = entity.entity_type.id == EntityType::SPECTRAL_ARROW.id;
let entity_type: &'static EntityType = entity.entity_type;
let owner_id = self.owner_id; let owner_id = self.owner_id;
let pierce = self.pierce_level.load(Ordering::Relaxed); let pierce = self.pierce_level.load(Ordering::Relaxed);
let owner_entity = owner_id.and_then(|id| world.get_entity_by_id(id));
let damage_succeeded = target.damage_with_context( let damage_succeeded = target.damage_with_context(
target.as_ref(), target.as_ref(),
damage as f32, damage as f32,
DamageType::ARROW, DamageType::ARROW,
Some(hit_pos), Some(hit_pos),
None, owner_entity.as_deref(),
None, None,
); );
if let Some(living) = target.get_living_entity() { if let Some(living) = target.get_living_entity() {
if punch > 0 if punch > 0 {
&& let Some(owner_id) = owner_id let norm = Vector3::new(velocity.x, 0.0, velocity.z).normalize();
&& let Some(owner_entity) = world.get_entity_by_id(owner_id) let push_scale = f64::from(punch) * 0.6;
{ target.get_entity().velocity.store(
crate::entity::combat::handle_knockback( target.get_entity().velocity.load().add(&Vector3::new(
owner_entity.get_entity(), norm.x * push_scale,
target.as_ref(), 0.1,
f64::from(punch) * 0.6, norm.z * push_scale,
)),
); );
} }
// Play hit sound // Play hit sound
let sound_pitch = 1.2 / (rand::random::<f32>() * 0.2 + 0.9);
let sound_packet = CSoundEffect::new( let sound_packet = CSoundEffect::new(
IdOr::Id(Sound::EntityArrowHit as u16), IdOr::Id(Sound::EntityArrowHit as u16),
SoundCategory::Neutral, SoundCategory::Neutral,
&hit_pos, &hit_pos,
1.0, 1.0,
1.0, sound_pitch,
0.0, 0.0,
); );
world.broadcast_packet_all(&sound_packet); world.broadcast_packet_all(&sound_packet);
if Self::should_apply_post_hurt_effects(damage_succeeded) { if Self::should_apply_post_hurt_effects(damage_succeeded) {
let item_stack = ItemStack::new(1, Self::default_item(entity_type)); let item_stack = self
.item_stack
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let scale = item_stack let scale = item_stack
.get_data_component::<PotionDurationScaleImpl>() .get_data_component::<PotionDurationScaleImpl>()
.map_or(1.0, |component| component.scale); .map_or(1.0, |component| component.scale);
@ -571,8 +1018,12 @@ impl EntityBase for ArrowEntity {
} }
// Check pierce level // Check pierce level
if pierce == 0 { let pierced_count = self
// No piercing - remove arrow .pierced_entities
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
if pierced_count > pierce as usize {
entity.remove(); entity.remove();
} }
} }
@ -638,11 +1089,27 @@ impl ArrowEntity {
return true; return true;
} }
// Skip other arrows, item entities, and falling block entities // Skip already pierced entities
if self
.pierced_entities
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.contains(&other_ent.entity_id)
{
return true;
}
// Skip dead entities
if !other_ent.is_alive() {
return true;
}
// Skip other arrows, item entities, falling block entities, and area effect clouds
if (other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW if (other_ent.entity_type == &pumpkin_data::entity::EntityType::ARROW
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::SPECTRAL_ARROW) || other_ent.entity_type == &pumpkin_data::entity::EntityType::SPECTRAL_ARROW)
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::ITEM || other_ent.entity_type == &pumpkin_data::entity::EntityType::ITEM
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK || other_ent.entity_type == &pumpkin_data::entity::EntityType::FALLING_BLOCK
|| other_ent.entity_type == &pumpkin_data::entity::EntityType::AREA_EFFECT_CLOUD
{ {
return true; return true;
} }
@ -793,4 +1260,24 @@ mod tests {
assert!(!ArrowEntity::should_apply_post_hurt_effects(false)); assert!(!ArrowEntity::should_apply_post_hurt_effects(false));
assert!(ArrowEntity::should_apply_post_hurt_effects(true)); assert!(ArrowEntity::should_apply_post_hurt_effects(true));
} }
#[test]
fn tipped_arrow_effect_color_uses_custom_color_or_effects() {
let tipped = tipped_payload(1);
let color = ArrowEntity::get_effect_color(&tipped);
assert_eq!(color, 0x123456);
let normal = ItemStack::new(1, &Item::ARROW);
let normal_color = ArrowEntity::get_effect_color(&normal);
assert_eq!(normal_color, -1);
}
#[test]
fn power_enchantment_damage_formula_matches_vanilla() {
let base_damage = 2.0;
let power_1 = base_damage + 1.0 * 0.5 + 0.5;
let power_5 = base_damage + 5.0 * 0.5 + 0.5;
assert_eq!(power_1, 3.0);
assert_eq!(power_5, 5.0);
}
} }

View File

@ -26,6 +26,8 @@ pub mod trident;
pub mod wind_charge; pub mod wind_charge;
pub mod wither_skull; pub mod wither_skull;
use pumpkin_data::item_stack::ItemStack;
#[must_use] #[must_use]
pub fn is_projectile(entity_type: &EntityType) -> bool { pub fn is_projectile(entity_type: &EntityType) -> bool {
*entity_type == EntityType::ARROW *entity_type == EntityType::ARROW
@ -45,6 +47,30 @@ pub fn is_projectile(entity_type: &EntityType) -> bool {
|| *entity_type == EntityType::LLAMA_SPIT || *entity_type == EntityType::LLAMA_SPIT
} }
/// Helper to apply projectile spawned enchantment effects matching vanilla `Projectile::applyOnProjectileSpawned`.
pub fn apply_on_projectile_spawned(
projectile_entity: &Entity,
pickup_item_stack: &ItemStack,
weapon: Option<&ItemStack>,
arrow: Option<&arrow::ArrowEntity>,
) {
crate::enchantment::EnchantmentHelper::on_projectile_spawned(
pickup_item_stack,
projectile_entity,
arrow,
);
if let Some(weapon) = weapon
&& weapon.item_count > 0
&& weapon.item.id != pickup_item_stack.item.id
{
crate::enchantment::EnchantmentHelper::on_projectile_spawned(
weapon,
projectile_entity,
arrow,
);
}
}
pub struct ThrownItemEntity { pub struct ThrownItemEntity {
pub entity: Entity, pub entity: Entity,
pub owner_id: Option<i32>, pub owner_id: Option<i32>,
@ -212,6 +238,16 @@ impl ThrownItemEntity {
return; return;
} }
if let ProjectileHit::Block { pos, hit_pos, .. } = &h {
let block = world.get_block(pos);
let state = world.get_block_state(pos);
if let Some(server) = world.server.upgrade() {
world
.block_registry
.on_projectile_hit(block, &world, caller, pos, state, hit_pos, &server);
}
}
// Just trigger hit effects and remove // Just trigger hit effects and remove
caller.on_hit(h); caller.on_hit(h);
entity.remove(); entity.remove();

View File

@ -78,6 +78,11 @@ impl TridentEntity {
} }
} }
/// Applies projectile-spawned enchantment effects matching vanilla `Projectile::applyOnProjectileSpawned`.
pub fn apply_on_projectile_spawned(&self, pickup_item_stack: &ItemStack) {
super::apply_on_projectile_spawned(self.get_entity(), pickup_item_stack, None, None);
}
pub fn set_velocity_from_rotation( pub fn set_velocity_from_rotation(
&self, &self,
pitch: f32, pitch: f32,
@ -304,6 +309,14 @@ impl EntityBase for TridentEntity {
.write() .write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pos); .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(pos);
let block = world.get_block(&pos);
let state = world.get_block_state(&pos);
if let Some(server) = world.server.upgrade() {
world
.block_registry
.on_projectile_hit(block, &world, self, &pos, state, &hit_pos, &server);
}
// Stop the trident // Stop the trident
entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); entity.velocity.store(Vector3::new(0.0, 0.0, 0.0));
entity.set_pos(hit_pos); entity.set_pos(hit_pos);

View File

@ -1,10 +1,10 @@
use std::any::Any; use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::entity::EntityBase;
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup}; use crate::entity::projectile::arrow::ArrowEntity;
use crate::entity::{Entity, EntityBase}; use crate::item::items::projectile_weapon::ProjectileWeaponItem;
use crate::item::{ItemBehaviour, ItemMetadata}; use crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
@ -41,8 +41,8 @@ impl ItemBehaviour for BowItem {
.set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION); .set_active_hand(pumpkin_util::Hand::Right, stack, Self::USE_DURATION);
} }
fn on_stopped_using(&self, _stack: &ItemStack, player: &Player) { fn on_stopped_using(&self, stack: &ItemStack, player: &Player) {
Self::release_bow(player); Self::release_bow(player, stack);
} }
fn get_use_duration(&self) -> i32 { fn get_use_duration(&self) -> i32 {
@ -58,10 +58,10 @@ impl BowItem {
/// The maximum number of ticks a bow can be drawn for /// The maximum number of ticks a bow can be drawn for
pub const USE_DURATION: i32 = 72000; pub const USE_DURATION: i32 = 72000;
const MAX_DRAW_DURATION: f32 = 20.0; const MAX_DRAW_DURATION: f32 = 20.0;
const ARROW_SPEED_MULTIPLIER: f32 = 3.0; pub const ARROW_SPEED_MULTIPLIER: f32 = 3.0;
/// Called when the player releases the bow /// Called when the player releases the bow
pub fn release_bow(player: &Player) { pub fn release_bow(player: &Player, weapon: &ItemStack) {
// Get the used ticks // Get the used ticks
let use_ticks = player.living_entity.item_use_time.load(Ordering::Relaxed); let use_ticks = player.living_entity.item_use_time.load(Ordering::Relaxed);
let use_ticks = Self::USE_DURATION - use_ticks; let use_ticks = Self::USE_DURATION - use_ticks;
@ -92,18 +92,24 @@ impl BowItem {
let power = Self::get_power_for_time(use_ticks); let power = Self::get_power_for_time(use_ticks);
// Check for Infinity enchantment // Check for Infinity enchantment
let mut has_infinity = false; let has_infinity = weapon
let held = player.inventory().held_item(); .get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>()
if let Some(enchantments) = .is_some_and(|enchantments| {
held.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>() enchantments
{
has_infinity = enchantments
.enchantment .enchantment
.iter() .iter()
.any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY); .any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY)
} });
Self::fire_arrow(player, power, &projectile); let is_crit = (power - 1.0).abs() < f32::EPSILON;
Self::shoot(
player,
weapon,
std::slice::from_ref(&projectile),
power,
1.0,
is_crit,
);
// Consume arrow (if not creative and no Infinity) // Consume arrow (if not creative and no Infinity)
if let Some(slot) = arrow_slot if let Some(slot) = arrow_slot
@ -133,70 +139,58 @@ impl BowItem {
power power
} }
/// Fire an arrow from the bow /// Creates projectile matching vanilla `ProjectileWeaponItem::createProjectile`.
pub fn fire_arrow(player: &Player, power: f32, projectile: &ItemStack) { pub fn create_projectile(
if power < 0.1 { player: &Player,
return; // Not enough charge weapon: &ItemStack,
projectile: &ItemStack,
is_crit: bool,
) -> ArrowEntity {
let world = player.world();
let is_creative = player.gamemode.load() == GameMode::Creative;
ProjectileWeaponItem::create_projectile(
world,
player.get_entity(),
weapon,
projectile,
is_crit,
is_creative,
)
}
/// Shoot projectile matching vanilla `ProjectileWeaponItem::shoot`.
pub fn shoot(
player: &Player,
weapon: &ItemStack,
projectiles: &[ItemStack],
power: f32,
uncertainty: f32,
is_crit: bool,
) {
if power < 0.1 || projectiles.is_empty() {
return;
} }
let world = player.world(); let world = player.world();
let position = player.position(); let is_creative = player.gamemode.load() == GameMode::Creative;
let speed = power * ProjectileWeaponItem::ARROW_SPEED_MULTIPLIER;
// Create arrow entity ProjectileWeaponItem::shoot_projectiles(
let arrow_entity = Entity::new( &world,
world.clone(), player.get_entity(),
position, weapon,
ArrowEntity::entity_type_for_item(projectile.item), projectiles,
speed,
uncertainty,
is_crit,
is_creative,
); );
// Determine pickup mode based on gamemode
let gamemode = player.gamemode.load();
let pickup = if gamemode == GameMode::Creative {
ArrowPickup::CreativeOnly
} else {
ArrowPickup::Allowed
};
let mut arrow =
ArrowEntity::new_shot(arrow_entity, player.get_entity(), projectile, pickup);
// Read enchantments of the held item (bow)
let stack = player.inventory().held_item();
if let Some(enchantments) =
stack.get_data_component::<pumpkin_data::data_component_impl::EnchantmentsImpl>()
{
for (enchantment, level) in enchantments.enchantment.iter() {
if **enchantment == pumpkin_data::Enchantment::POWER {
arrow.base_damage *= 1.0 + 0.25 * (f64::from(*level) + 1.0);
} else if **enchantment == pumpkin_data::Enchantment::PUNCH {
arrow.punch_level.store(*level as u8, Ordering::Relaxed);
} else if **enchantment == pumpkin_data::Enchantment::FLAME {
arrow.is_flame.store(true, Ordering::Relaxed);
}
}
}
drop(stack);
// Set velocity based on player's look direction and power
let (yaw, pitch) = player.rotation();
let speed = power * Self::ARROW_SPEED_MULTIPLIER;
arrow.set_velocity_from_rotation(pitch, yaw, 0.0, speed, 1.0);
// Set critical if fully charged
if power >= 1.0 {
arrow.set_critical(true);
}
// Spawn the arrow entity in the world
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
world.spawn_entity(arrow_arc);
// Play bow shoot sound
let sound_pitch = 1.0 / (rand::random::<f32>() * 0.4 + 1.2) + power * 0.5; let sound_pitch = 1.0 / (rand::random::<f32>() * 0.4 + 1.2) + power * 0.5;
let sound_packet = CSoundEffect::new( let sound_packet = CSoundEffect::new(
IdOr::Id(Sound::EntityArrowShoot as u16), IdOr::Id(Sound::EntityArrowShoot as u16),
SoundCategory::Neutral, SoundCategory::Neutral,
&position, &player.position(),
1.0, 1.0,
sound_pitch, sound_pitch,
0.0, 0.0,
@ -204,4 +198,27 @@ impl BowItem {
let chunk_pos = player.get_entity().chunk_pos.load(); let chunk_pos = player.get_entity().chunk_pos.load();
world.broadcast_to_chunk(chunk_pos, &sound_packet); world.broadcast_to_chunk(chunk_pos, &sound_packet);
} }
/// Fire an arrow from the bow with explicit critical flag
pub fn fire_arrow_with_crit(
player: &Player,
power: f32,
projectile: &ItemStack,
is_crit: bool,
) {
let held = player.inventory().held_item();
Self::shoot(
player,
&held,
std::slice::from_ref(projectile),
power,
1.0,
is_crit,
);
}
/// Fire an arrow from the bow
pub fn fire_arrow(player: &Player, power: f32, projectile: &ItemStack) {
Self::fire_arrow_with_crit(player, power, projectile, power >= 1.0);
}
} }

View File

@ -156,17 +156,17 @@ pub(crate) fn try_pickup_fluid_at(
if is_waterlogged(block, state) { if is_waterlogged(block, state) {
let state_id = set_waterlogged(block, state, false); let state_id = set_waterlogged(block, state, false);
world.set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(&block_pos, state_id, BlockFlags::NOTIFY_ALL);
world.schedule_fluid_tick(&Fluid::WATER, block_pos, 5, TickPriority::Normal); world.schedule_fluid_tick(&Fluid::WATER, block_pos, 5, TickPriority::Normal);
return Some(&Item::WATER_BUCKET); return Some(&Item::WATER_BUCKET);
} }
if state == Block::LAVA.default_state.id || state == Block::WATER.default_state.id { if state == Block::LAVA.default_state.id || state == Block::WATER.default_state.id {
world.break_block(&block_pos, None, BlockFlags::NOTIFY_NEIGHBORS); world.break_block(&block_pos, None, BlockFlags::NOTIFY_ALL);
world.set_block_state( world.set_block_state(
&block_pos, &block_pos,
Block::AIR.default_state.id, Block::AIR.default_state.id,
BlockFlags::NOTIFY_NEIGHBORS, BlockFlags::NOTIFY_ALL,
); );
return Some(if state == Block::LAVA.default_state.id { return Some(if state == Block::LAVA.default_state.id {
&Item::LAVA_BUCKET &Item::LAVA_BUCKET
@ -191,7 +191,7 @@ fn try_pickup_bucket_item(
let (block, state) = world.get_block_and_state_id(&target_pos); let (block, state) = world.get_block_and_state_id(&target_pos);
if waterlogged_check(block, state).is_some() { if waterlogged_check(block, state).is_some() {
let state_id = set_waterlogged(block, state, false); let state_id = set_waterlogged(block, state, false);
world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_ALL);
world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal); world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal);
return Some(&Item::WATER_BUCKET); return Some(&Item::WATER_BUCKET);
} }
@ -247,7 +247,7 @@ pub(crate) fn try_place_filled_bucket(
if is_waterlogged(block, state.id) && item.id == Item::WATER_BUCKET.id { if is_waterlogged(block, state.id) && item.id == Item::WATER_BUCKET.id {
let state_id = set_waterlogged(block, state.id, true); let state_id = set_waterlogged(block, state.id, true);
world.set_block_state(&pos, state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(&pos, state_id, BlockFlags::NOTIFY_ALL);
world.schedule_fluid_tick(&Fluid::WATER, pos, 5, TickPriority::Normal); world.schedule_fluid_tick(&Fluid::WATER, pos, 5, TickPriority::Normal);
return true; return true;
} }
@ -260,7 +260,7 @@ pub(crate) fn try_place_filled_bucket(
return false; return false;
} }
let state_id = set_waterlogged(block, state.id, true); let state_id = set_waterlogged(block, state.id, true);
world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_NEIGHBORS); world.set_block_state(&target_pos, state_id, BlockFlags::NOTIFY_ALL);
world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal); world.schedule_fluid_tick(&Fluid::WATER, target_pos, 5, TickPriority::Normal);
return true; return true;
} }
@ -273,7 +273,7 @@ pub(crate) fn try_place_filled_bucket(
} else { } else {
Block::WATER.default_state.id Block::WATER.default_state.id
}, },
BlockFlags::NOTIFY_NEIGHBORS, BlockFlags::NOTIFY_ALL,
); );
return true; return true;
} }

View File

@ -1,13 +1,12 @@
use std::any::Any; use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use crate::entity::EntityBase;
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup}; use crate::item::items::projectile_weapon::ProjectileWeaponItem;
use crate::entity::{Entity, EntityBase};
use crate::item::{ItemBehaviour, ItemMetadata}; use crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::data_component::DataComponent; use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{ChargedProjectilesImpl, EnchantmentsImpl}; use pumpkin_data::data_component_impl::ChargedProjectilesImpl;
use pumpkin_data::item::Item; use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::sound::{Sound, SoundCategory};
@ -51,53 +50,44 @@ impl ItemBehaviour for CrossbowItem {
let use_ticks = player.living_entity.item_use_time.load(Ordering::Relaxed); let use_ticks = player.living_entity.item_use_time.load(Ordering::Relaxed);
let use_ticks = 72000 - use_ticks; let use_ticks = 72000 - use_ticks;
let mut charge_time = 25;
let mut stack = player.inventory().held_item(); let mut stack = player.inventory().held_item();
let charge_time =
if let Some(enchantments) = stack.get_data_component::<EnchantmentsImpl>() { crate::enchantment::EnchantmentHelper::modify_crossbow_charge_time(&stack, 25);
for (enchantment, level) in enchantments.enchantment.iter() {
if **enchantment == pumpkin_data::Enchantment::QUICK_CHARGE {
charge_time -= 5 * level;
}
}
}
charge_time = charge_time.max(0);
if use_ticks >= charge_time { if use_ticks >= charge_time {
let arrow_slot = player.find_arrow(); let arrow_slot = player.find_arrow();
let (arrow_nbt_wrapper, slot) = arrow_slot.map_or_else( let gamemode = player.gamemode.load();
|| { let is_creative = gamemode == GameMode::Creative;
if player.gamemode.load() == GameMode::Creative {
let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new();
let arrow_stack = ItemStack::new(1, &Item::ARROW);
arrow_stack.write_item_stack(&mut arrow_nbt);
(Some(arrow_nbt), 0) if arrow_slot.is_some() || is_creative {
} else { let projectile = arrow_slot.map_or_else(
(None, 0) || ItemStack::new(1, &Item::ARROW),
}
},
|slot| { |slot| {
let inventory = player.inventory(); let inventory = player.inventory();
inventory.get_slot(slot).copy_with_count(1)
let arrow_stack = inventory.get_slot(slot);
let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new();
arrow_stack
.copy_with_count(1)
.write_item_stack(&mut arrow_nbt);
(Some(arrow_nbt), slot)
}, },
); );
if let Some(arrow_nbt) = arrow_nbt_wrapper {
let drawn = ProjectileWeaponItem::draw(&stack, &projectile, is_creative);
if !drawn.is_empty() {
let mut charged_nbts = Vec::new();
for item in drawn {
let mut arrow_nbt = pumpkin_nbt::compound::NbtCompound::new();
item.write_item_stack(&mut arrow_nbt);
charged_nbts.push(arrow_nbt);
}
stack.patch.push(( stack.patch.push((
DataComponent::ChargedProjectiles, DataComponent::ChargedProjectiles,
Some(Box::new(ChargedProjectilesImpl { Some(Box::new(ChargedProjectilesImpl {
projectiles: vec![arrow_nbt], projectiles: charged_nbts,
})), })),
)); ));
player.inventory().set_held_item(stack); player.inventory().set_held_item(stack);
if player.gamemode.load() != GameMode::Creative { if let Some(slot) = arrow_slot
&& !is_creative
{
player.consume_arrow(slot); player.consume_arrow(slot);
} }
@ -108,6 +98,7 @@ impl ItemBehaviour for CrossbowItem {
); );
} }
} }
}
player.living_entity.clear_active_hand(); player.living_entity.clear_active_hand();
} }
@ -121,19 +112,21 @@ impl ItemBehaviour for CrossbowItem {
} }
impl CrossbowItem { impl CrossbowItem {
pub const ARROW_POWER: f32 = 3.15;
fn fire_projectiles(player: &Player) { fn fire_projectiles(player: &Player) {
let mut held = player.inventory().held_item(); let mut held = player.inventory().held_item();
let projectiles = held.get_data_component::<ChargedProjectilesImpl>().cloned(); let charged_opt = held.get_data_component::<ChargedProjectilesImpl>().cloned();
let has_multishot =
held.get_data_component::<EnchantmentsImpl>()
.is_some_and(|enchantments| {
enchantments
.enchantment
.iter()
.any(|(e, _)| **e == pumpkin_data::Enchantment::MULTISHOT)
});
if let Some(charged) = projectiles { if let Some(charged) = charged_opt {
let mut projectiles = Vec::new();
for projectile_nbt in charged.projectiles {
if let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) {
projectiles.push(projectile);
}
}
if !projectiles.is_empty() {
let world = player.world(); let world = player.world();
world.play_sound( world.play_sound(
Sound::ItemCrossbowShoot, Sound::ItemCrossbowShoot,
@ -141,41 +134,17 @@ impl CrossbowItem {
&player.position(), &player.position(),
); );
let (yaw, pitch) = player.rotation(); let is_creative = player.gamemode.load() == GameMode::Creative;
ProjectileWeaponItem::shoot_projectiles(
for projectile_nbt in charged.projectiles { &world,
let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) else {
continue;
};
let yaws = if has_multishot {
vec![yaw - 10.0, yaw, yaw + 10.0]
} else {
vec![yaw]
};
for t_yaw in yaws {
let arrow_entity = Entity::new(
world.clone(),
player.position(),
ArrowEntity::entity_type_for_item(projectile.item),
);
let pickup = if player.gamemode.load() == GameMode::Creative {
ArrowPickup::CreativeOnly
} else {
ArrowPickup::Allowed
};
let arrow = ArrowEntity::new_shot(
arrow_entity,
player.get_entity(), player.get_entity(),
&projectile, &held,
pickup, &projectiles,
Self::ARROW_POWER,
1.0,
false,
is_creative,
); );
arrow.set_velocity_from_rotation(pitch, t_yaw, 0.0, 3.15, 1.0);
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
world.spawn_entity(arrow_arc);
}
}
held.patch held.patch
.retain(|(id, _)| *id != DataComponent::ChargedProjectiles); .retain(|(id, _)| *id != DataComponent::ChargedProjectiles);
@ -184,3 +153,4 @@ impl CrossbowItem {
} }
} }
} }
}

View File

@ -7,11 +7,13 @@ use pumpkin_data::{Block, BlockStateId};
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3; use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::BlockFlags; use pumpkin_world::world::BlockFlags;
use rand::{RngExt, rng};
use std::sync::Arc; use std::sync::Arc;
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::item::items::ignite::ignition::Ignition; use crate::item::items::ignite::ignition::Ignition;
use crate::item::{ItemBehaviour, ItemMetadata}; use crate::item::{ItemBehaviour, ItemMetadata};
use crate::plugin::api::events::world::portal_create::{PortalCreateEvent, PortalType};
use crate::server::Server; use crate::server::Server;
use crate::world::World; use crate::world::World;
@ -26,7 +28,7 @@ impl ItemMetadata for FireChargeItem {
impl ItemBehaviour for FireChargeItem { impl ItemBehaviour for FireChargeItem {
fn use_on_block( fn use_on_block(
&self, &self,
_item: &mut ItemStack, item: &mut ItemStack,
player: &Player, player: &Player,
location: BlockPos, location: BlockPos,
face: BlockDirection, face: BlockDirection,
@ -35,17 +37,48 @@ impl ItemBehaviour for FireChargeItem {
_server: &Server, _server: &Server,
) { ) {
let world = player.world(); let world = player.world();
Ignition::ignite_block( let server_ref = world.server.upgrade();
if let Some(server_ref) = server_ref {
let player_arc = world.get_player_by_uuid(player.gameprofile.id);
let mut event = crate::plugin::api::events::block::block_ignite::BlockIgniteEvent::new(
location,
&pumpkin_data::Block::FIRE,
player_arc,
);
server_ref
.plugin_manager
.fire_blocking(&server_ref, &mut event);
let mut portal_event = PortalCreateEvent::new(location, PortalType::Nether);
server_ref
.plugin_manager
.fire_blocking(&server_ref, &mut portal_event);
if event.cancelled || portal_event.cancelled {
return;
}
}
let ignited = Ignition::ignite_block(
|world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| { |world: Arc<World>, pos: BlockPos, new_state_id: BlockStateId| {
world.set_block_state(&pos, new_state_id, BlockFlags::NOTIFY_ALL); world.set_block_state(&pos, new_state_id, BlockFlags::NOTIFY_ALL);
world.play_block_sound(Sound::ItemFirechargeUse, SoundCategory::Blocks, pos); let pitch = (rng().random::<f32>() - rng().random::<f32>()).mul_add(0.2, 1.0);
world.play_sound_fine(
Sound::ItemFirechargeUse,
SoundCategory::Blocks,
&pos.to_centered_f64(),
1.0,
pitch,
);
}, },
&world, &world,
location, location,
location.offset(face.to_offset()), location.offset(face.to_offset()),
block, block,
); );
if ignited && player.gamemode.load() != pumpkin_util::GameMode::Creative {
item.decrement(1);
}
} }
fn as_any(&self) -> &dyn std::any::Any { fn as_any(&self) -> &dyn std::any::Any {

View File

@ -32,6 +32,7 @@ pub mod minecart;
pub mod name_tag; pub mod name_tag;
pub mod on_a_stick; pub mod on_a_stick;
pub mod potions; pub mod potions;
pub mod projectile_weapon;
pub mod saddle; pub mod saddle;
pub mod shears; pub mod shears;
pub mod shovel; pub mod shovel;

View File

@ -0,0 +1,172 @@
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::enchantment::EnchantmentHelper;
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
use crate::entity::{Entity, EntityBase};
use crate::world::World;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
/// Abstraction for projectile-firing weapons (Bow, Crossbow), matching vanilla `ProjectileWeaponItem`.
pub struct ProjectileWeaponItem;
impl ProjectileWeaponItem {
pub const ARROW_SPEED_MULTIPLIER: f32 = 3.0;
#[must_use]
pub const fn is_arrow(item: &Item) -> bool {
item.id == Item::ARROW.id
|| item.id == Item::TIPPED_ARROW.id
|| item.id == Item::SPECTRAL_ARROW.id
}
#[must_use]
pub const fn is_arrow_or_firework(item: &Item) -> bool {
Self::is_arrow(item) || item.id == Item::FIREWORK_ROCKET.id
}
/// Creates a projectile matching vanilla `ProjectileWeaponItem::createProjectile`.
pub fn create_projectile(
world: Arc<World>,
shooter: &Entity,
weapon: &ItemStack,
projectile: &ItemStack,
is_crit: bool,
is_creative: bool,
) -> ArrowEntity {
let arrow_entity = Entity::new(
world,
shooter.pos.load(),
ArrowEntity::entity_type_for_item(projectile.item),
);
let pickup = if is_creative {
ArrowPickup::CreativeOnly
} else {
ArrowPickup::Allowed
};
let arrow =
ArrowEntity::new_shot_with_weapon(arrow_entity, shooter, projectile, weapon, pickup);
// Apply data-driven weapon enchantment modifications
arrow.set_base_damage(EnchantmentHelper::modify_damage(
weapon,
arrow.get_base_damage(),
));
arrow.punch_level.store(
EnchantmentHelper::modify_knockback(weapon, 0.0) as u8,
Ordering::Relaxed,
);
arrow.set_pierce_level(EnchantmentHelper::process_projectile_piercing(weapon, 0));
// Data-driven PROJECTILE_SPAWNED effects (e.g. Flame enchantment -> Ignite effect)
arrow.apply_on_projectile_spawned(projectile);
if is_crit {
arrow.set_critical(true);
}
arrow
}
/// Draws ammo for projectile weapon matching vanilla `ProjectileWeaponItem::draw`.
#[must_use]
pub fn draw(weapon: &ItemStack, projectile: &ItemStack, is_creative: bool) -> Vec<ItemStack> {
if projectile.item_count == 0 {
return Vec::new();
}
let num_projectiles = EnchantmentHelper::process_projectile_count(weapon, 1);
let mut drawn = Vec::with_capacity(num_projectiles);
let projectile_copy = projectile.copy_with_count(1);
for i in 0..num_projectiles {
let drawn_stack = Self::use_ammo(
weapon,
if i == 0 { projectile } else { &projectile_copy },
is_creative,
i > 0,
);
if drawn_stack.item_count > 0 {
drawn.push(drawn_stack);
}
}
drawn
}
/// Handles ammo usage logic matching vanilla `ProjectileWeaponItem::useAmmo`.
#[must_use]
pub fn use_ammo(
weapon: &ItemStack,
projectile: &ItemStack,
is_creative: bool,
force_infinite: bool,
) -> ItemStack {
let ammo_to_use = if force_infinite || is_creative {
0
} else {
EnchantmentHelper::process_ammo_use(weapon, projectile, 1)
};
if ammo_to_use > projectile.item_count as i32 {
return ItemStack::EMPTY.clone();
}
let mut copy = projectile.copy_with_count(1);
if ammo_to_use == 0 {
copy.set_data_component(pumpkin_data::data_component_impl::IntangibleProjectileImpl);
}
copy
}
/// Shoots projectiles matching vanilla `ProjectileWeaponItem::shoot`.
#[allow(clippy::too_many_arguments)]
pub fn shoot_projectiles(
world: &Arc<World>,
shooter: &Entity,
weapon: &ItemStack,
projectiles: &[ItemStack],
power: f32,
uncertainty: f32,
is_crit: bool,
is_creative: bool,
) {
if projectiles.is_empty() {
return;
}
let max_angle = EnchantmentHelper::process_projectile_spread(weapon, 0.0);
let angle_step = if projectiles.len() == 1 {
0.0
} else {
2.0 * max_angle / (projectiles.len() as f32 - 1.0)
};
let angle_offset = ((projectiles.len() - 1) % 2) as f32 * angle_step / 2.0;
let mut direction = 1.0f32;
let (yaw, pitch) = (shooter.yaw.load(), shooter.pitch.load());
for (i, projectile) in projectiles.iter().enumerate() {
if projectile.item_count == 0 {
continue;
}
let angle = angle_offset + direction * (i.div_ceil(2) as f32) * angle_step;
direction = -direction;
let arrow = Self::create_projectile(
world.clone(),
shooter,
weapon,
projectile,
is_crit,
is_creative,
);
arrow.set_velocity_from_rotation(pitch, yaw + angle, 0.0, power, uncertainty);
let arrow_arc: Arc<dyn EntityBase> = Arc::new(arrow);
world.spawn_entity(arrow_arc);
}
}
}

View File

@ -95,10 +95,11 @@ impl ItemBehaviour for TridentItem {
let trident = TridentEntity::new_shot( let trident = TridentEntity::new_shot(
entity, entity,
player.get_entity(), player.get_entity(),
stack_guard, stack_guard.clone(),
ArrowPickup::Allowed, ArrowPickup::Allowed,
); );
trident.set_velocity_from_rotation(pitch, yaw, 0.0, 2.5, 1.0); trident.set_velocity_from_rotation(pitch, yaw, 0.0, 2.5, 1.0);
trident.apply_on_projectile_spawned(&stack_guard);
world.spawn_entity(Arc::new(trident)); world.spawn_entity(Arc::new(trident));
world.play_sound( world.play_sound(

View File

@ -52,6 +52,7 @@ pub mod block;
pub mod command; pub mod command;
pub mod crash; pub mod crash;
pub mod data; pub mod data;
pub mod enchantment;
pub mod entity; pub mod entity;
pub mod error; pub mod error;
pub mod item; pub mod item;

View File

@ -44,7 +44,7 @@ impl JavaClient {
player.world().set_block_state( player.world().set_block_state(
&command.pos, &command.pos,
new_state_id, new_state_id,
BlockFlags::SKIP_BLOCK_ADDED_CALLBACK, BlockFlags::NOTIFY_ALL | BlockFlags::SKIP_BLOCK_ADDED_CALLBACK,
); );
let mut cmd = command.command; let mut cmd = command.command;

View File

@ -134,7 +134,7 @@ pub fn build() -> TokenStream {
quote! { quote! {
use crate::tag::{RegistryKey, Tag, Taggable}; use crate::tag::{RegistryKey, Tag, Taggable};
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub struct DamageType { pub struct DamageType {
pub death_message_type: DeathMessageType, pub death_message_type: DeathMessageType,
pub exhaustion: f32, pub exhaustion: f32,
@ -144,14 +144,14 @@ pub fn build() -> TokenStream {
pub id: u8, pub id: u8,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum DeathMessageType { pub enum DeathMessageType {
Default, Default,
FallVariants, FallVariants,
IntentionalGameDesign, IntentionalGameDesign,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum DamageEffects { pub enum DamageEffects {
Hurt, Hurt,
Thorns, Thorns,
@ -161,7 +161,7 @@ pub fn build() -> TokenStream {
Freezing, Freezing,
} }
#[derive(Clone, Copy, PartialEq)] #[derive(Clone, Copy, PartialEq, Debug)]
pub enum DamageScaling { pub enum DamageScaling {
Never, Never,
WhenCausedByLivingNonPlayer, WhenCausedByLivingNonPlayer,

View File

@ -1,6 +1,6 @@
use std::{collections::BTreeMap, fs}; use std::{collections::BTreeMap, fs};
use heck::ToShoutySnakeCase; use heck::{ToPascalCase, ToShoutySnakeCase};
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use pumpkin_util::text::TextContent::Translate; use pumpkin_util::text::TextContent::Translate;
@ -24,13 +24,16 @@ pub struct Enchantment {
/// Maximum level this enchantment can reach. /// Maximum level this enchantment can reach.
pub max_level: i32, pub max_level: i32,
/// Equipment slots this enchantment's attribute modifiers apply to. /// Equipment slots this enchantment's attribute modifiers apply to.
pub slots: Vec<AttributeModifierSlot>, // TODO: add more pub slots: Vec<AttributeModifierSlot>,
/// The weight of this enchantment (used for random selection). /// The weight of this enchantment (used for random selection).
pub weight: i32, pub weight: i32,
/// The minimum cost to get this enchantment. /// The minimum cost to get this enchantment.
pub min_cost: Cost, pub min_cost: Cost,
/// The maximum cost to get this enchantment. /// The maximum cost to get this enchantment.
pub max_cost: Cost, pub max_cost: Cost,
/// Data-driven effects map.
#[serde(default)]
pub effects: serde_json::Map<String, serde_json::Value>,
} }
#[derive(Deserialize, Clone, Copy)] #[derive(Deserialize, Clone, Copy)]
@ -43,27 +46,16 @@ pub struct Cost {
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AttributeModifierSlot { pub enum AttributeModifierSlot {
/// Applies in any equipment slot.
Any, Any,
/// Applies only when held in the main hand.
MainHand, MainHand,
/// Applies only when held in the offhand.
OffHand, OffHand,
/// Applies when held in either hand.
Hand, Hand,
/// Applies when worn on the feet.
Feet, Feet,
/// Applies when worn on the legs.
Legs, Legs,
/// Applies when worn on the chest.
Chest, Chest,
/// Applies when worn on the head.
Head, Head,
/// Applies when worn on armor.
Armor, Armor,
/// Applies when worn on body.
Body, Body,
/// Applies when equipped as saddle.
Saddle, Saddle,
} }
@ -86,8 +78,417 @@ impl AttributeModifierSlot {
} }
} }
fn parse_level_based_value(val: &serde_json::Value) -> TokenStream {
if let Some(num) = val.as_f64() {
let num_f32 = num as f32;
return quote! { LevelBasedValue::Constant(#num_f32) };
}
if let Some(obj) = val.as_object() {
let val_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
match val_type {
"minecraft:linear" => {
let base = obj.get("base").and_then(|b| b.as_f64()).unwrap_or(0.0) as f32;
let per_level = obj
.get("per_level_above_first")
.and_then(|p| p.as_f64())
.unwrap_or(0.0) as f32;
quote! {
LevelBasedValue::Linear {
base: #base,
per_level_above_first: #per_level,
}
}
}
"minecraft:clamped" => {
let inner =
parse_level_based_value(obj.get("value").unwrap_or(&serde_json::Value::Null));
let min = obj.get("min").and_then(|m| m.as_f64()).unwrap_or(0.0) as f32;
let max = obj.get("max").and_then(|m| m.as_f64()).unwrap_or(0.0) as f32;
quote! {
LevelBasedValue::Clamped {
value: &#inner,
min: #min,
max: #max,
}
}
}
"minecraft:fraction" => {
let num = parse_level_based_value(
obj.get("numerator").unwrap_or(&serde_json::Value::Null),
);
let denom = parse_level_based_value(
obj.get("denominator").unwrap_or(&serde_json::Value::Null),
);
quote! {
LevelBasedValue::Fraction {
numerator: &#num,
denominator: &#denom,
}
}
}
"minecraft:levels_squared" => {
let added = obj.get("added").and_then(|a| a.as_f64()).unwrap_or(0.0) as f32;
quote! {
LevelBasedValue::LevelsSquared { added: #added }
}
}
"minecraft:lookup" => {
let values: Vec<f32> = obj
.get("values")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_f64().map(|f| f as f32))
.collect()
})
.unwrap_or_default();
let fallback = parse_level_based_value(
obj.get("fallback").unwrap_or(&serde_json::Value::Null),
);
quote! {
LevelBasedValue::Lookup {
values: &[#(#values),*],
fallback: &#fallback,
}
}
}
_ => quote! { LevelBasedValue::Constant(0.0) },
}
} else {
quote! { LevelBasedValue::Constant(0.0) }
}
}
fn parse_entity_effect(val: &serde_json::Value) -> TokenStream {
if let Some(obj) = val.as_object() {
let eff_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
match eff_type {
"minecraft:ignite" => {
let dur = parse_level_based_value(
obj.get("duration").unwrap_or(&serde_json::Value::Null),
);
quote! { EnchantmentEntityEffect::Ignite { duration: #dur } }
}
"minecraft:damage_entity" => {
let dmg = parse_level_based_value(
obj.get("damage")
.or_else(|| obj.get("min_damage"))
.unwrap_or(&serde_json::Value::Null),
);
quote! { EnchantmentEntityEffect::DamageEntity { damage: #dmg } }
}
"minecraft:play_sound" => {
let sound = obj.get("sound").and_then(|s| s.as_str()).unwrap_or("");
quote! { EnchantmentEntityEffect::PlaySound { sound: #sound } }
}
"minecraft:replace_block" => {
let offset_x = obj
.get("offset")
.and_then(|v| v.get(0))
.and_then(|n| n.as_i64())
.unwrap_or(0) as i32;
let offset_y = obj
.get("offset")
.and_then(|v| v.get(1))
.and_then(|n| n.as_i64())
.unwrap_or(0) as i32;
let offset_z = obj
.get("offset")
.and_then(|v| v.get(2))
.and_then(|n| n.as_i64())
.unwrap_or(0) as i32;
let event = obj.get("trigger_game_event").and_then(|s| s.as_str());
let event_token = match event {
Some(ev) => {
let ev_raw = ev.strip_prefix("minecraft:").unwrap_or(ev);
let ev_ident = format_ident!("{}", ev_raw.to_pascal_case());
quote! { Some(crate::game_event::GameEvent::#ev_ident) }
}
None => quote! { None },
};
quote! {
EnchantmentEntityEffect::ReplaceBlock {
offset_x: #offset_x,
offset_y: #offset_y,
offset_z: #offset_z,
trigger_game_event: #event_token,
}
}
}
"minecraft:explode" => {
let attribute_to_user = obj
.get("attribute_to_user")
.and_then(|b| b.as_bool())
.unwrap_or(false);
let damage_type = obj.get("damage_type").and_then(|s| s.as_str());
let damage_type_token = match damage_type {
Some(dt) => {
let dt_raw = dt.strip_prefix("minecraft:").unwrap_or(dt);
let dt_ident = format_ident!("{}", dt_raw.to_shouty_snake_case());
quote! { Some(&crate::damage::DamageType::#dt_ident) }
}
None => quote! { None },
};
let knockback_multiplier =
obj.get("knockback_multiplier").map(parse_level_based_value);
let knockback_token = match knockback_multiplier {
Some(kb) => quote! { Some(#kb) },
None => quote! { None },
};
let immune_blocks = obj.get("immune_blocks").and_then(|s| s.as_str());
let immune_blocks_token = match immune_blocks {
Some(ib) => quote! { Some(#ib) },
None => quote! { None },
};
let offset_x = obj
.get("offset")
.and_then(|v| v.get(0))
.and_then(|n| n.as_f64())
.unwrap_or(0.0);
let offset_y = obj
.get("offset")
.and_then(|v| v.get(1))
.and_then(|n| n.as_f64())
.unwrap_or(0.0);
let offset_z = obj
.get("offset")
.and_then(|v| v.get(2))
.and_then(|n| n.as_f64())
.unwrap_or(0.0);
let radius =
parse_level_based_value(obj.get("radius").unwrap_or(&serde_json::Value::Null));
let create_fire = obj
.get("create_fire")
.and_then(|b| b.as_bool())
.unwrap_or(false);
let block_interaction = obj
.get("block_interaction")
.and_then(|s| s.as_str())
.unwrap_or("none");
let small_particle = obj.get("small_particle").and_then(|p| {
p.get("type")
.and_then(|t| t.as_str())
.or_else(|| p.as_str())
});
let small_particle_token = match small_particle {
Some(p) => {
let p_raw = p.strip_prefix("minecraft:").unwrap_or(p);
let p_ident = format_ident!("{}", p_raw.to_pascal_case());
quote! { Some(crate::particle::Particle::#p_ident) }
}
None => quote! { None },
};
let large_particle = obj.get("large_particle").and_then(|p| {
p.get("type")
.and_then(|t| t.as_str())
.or_else(|| p.as_str())
});
let large_particle_token = match large_particle {
Some(p) => {
let p_raw = p.strip_prefix("minecraft:").unwrap_or(p);
let p_ident = format_ident!("{}", p_raw.to_pascal_case());
quote! { Some(crate::particle::Particle::#p_ident) }
}
None => quote! { None },
};
let sound = obj.get("sound").and_then(|s| s.as_str());
let sound_token = match sound {
Some(s) => {
let s_raw = s.strip_prefix("minecraft:").unwrap_or(s);
let s_ident = format_ident!("{}", s_raw.to_pascal_case());
quote! { Some(crate::sound::Sound::#s_ident) }
}
None => quote! { None },
};
quote! {
EnchantmentEntityEffect::Explode {
attribute_to_user: #attribute_to_user,
damage_type: #damage_type_token,
knockback_multiplier: #knockback_token,
immune_blocks: #immune_blocks_token,
offset_x: #offset_x,
offset_y: #offset_y,
offset_z: #offset_z,
radius: #radius,
create_fire: #create_fire,
block_interaction: #block_interaction,
small_particle: #small_particle_token,
large_particle: #large_particle_token,
sound: #sound_token,
}
}
}
"minecraft:all_of" => {
let inner: Vec<TokenStream> = obj
.get("effects")
.and_then(|arr| arr.as_array())
.map(|arr| arr.iter().map(parse_entity_effect).collect())
.unwrap_or_default();
quote! { EnchantmentEntityEffect::AllOf(&[#(#inner),*]) }
}
_ => quote! { EnchantmentEntityEffect::Other },
}
} else {
quote! { EnchantmentEntityEffect::Other }
}
}
fn parse_value_effect(val: &serde_json::Value) -> TokenStream {
if let Some(obj) = val.as_object() {
let eff_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
match eff_type {
"minecraft:add" => {
let v =
parse_level_based_value(obj.get("value").unwrap_or(&serde_json::Value::Null));
quote! { EnchantmentValueEffect::Add(#v) }
}
"minecraft:multiply" => {
let v = parse_level_based_value(
obj.get("factor")
.or_else(|| obj.get("value"))
.unwrap_or(&serde_json::Value::Null),
);
quote! { EnchantmentValueEffect::Multiply(#v) }
}
"minecraft:set" => {
let v =
parse_level_based_value(obj.get("value").unwrap_or(&serde_json::Value::Null));
quote! { EnchantmentValueEffect::Set(#v) }
}
"minecraft:remove_binomial" => {
let v =
parse_level_based_value(obj.get("chance").unwrap_or(&serde_json::Value::Null));
quote! { EnchantmentValueEffect::RemoveBinomial(#v) }
}
_ => quote! { EnchantmentValueEffect::Other },
}
} else {
quote! { EnchantmentValueEffect::Other }
}
}
fn parse_conditional_entity_effects(val: Option<&serde_json::Value>) -> TokenStream {
let effects: Vec<TokenStream> = val
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|item| {
let eff = item.get("effect").unwrap_or(item);
let eff_tokens = parse_entity_effect(eff);
quote! { ConditionalEffect { effect: #eff_tokens } }
})
.collect()
})
.unwrap_or_default();
quote! { &[#(#effects),*] }
}
fn parse_targeted_conditional_entity_effects(val: Option<&serde_json::Value>) -> TokenStream {
let effects: Vec<TokenStream> = val
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|item| {
let eff = item.get("effect").unwrap_or(item);
let eff_tokens = parse_entity_effect(eff);
let enchanted = match item.get("enchanted").and_then(|e| e.as_str()) {
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
Some("damaging_entity") => {
quote! { Some(EnchantmentTarget::DamagingEntity) }
}
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
_ => quote! { None },
};
let affected = match item.get("affected").and_then(|e| e.as_str()) {
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
Some("damaging_entity") => {
quote! { Some(EnchantmentTarget::DamagingEntity) }
}
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
_ => quote! { None },
};
quote! {
TargetedConditionalEffect {
enchanted: #enchanted,
affected: #affected,
effect: #eff_tokens,
}
}
})
.collect()
})
.unwrap_or_default();
quote! { &[#(#effects),*] }
}
fn parse_conditional_value_effects(val: Option<&serde_json::Value>) -> TokenStream {
let effects: Vec<TokenStream> = val
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|item| {
let eff = item.get("effect").unwrap_or(item);
let eff_tokens = parse_value_effect(eff);
quote! { ConditionalEffect { effect: #eff_tokens } }
})
.collect()
})
.unwrap_or_default();
quote! { &[#(#effects),*] }
}
fn parse_targeted_conditional_value_effects(val: Option<&serde_json::Value>) -> TokenStream {
let effects: Vec<TokenStream> = val
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|item| {
let eff = item.get("effect").unwrap_or(item);
let eff_tokens = parse_value_effect(eff);
let enchanted = match item.get("enchanted").and_then(|e| e.as_str()) {
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
Some("damaging_entity") => {
quote! { Some(EnchantmentTarget::DamagingEntity) }
}
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
_ => quote! { None },
};
let affected = match item.get("affected").and_then(|e| e.as_str()) {
Some("attacker") => quote! { Some(EnchantmentTarget::Attacker) },
Some("damaging_entity") => {
quote! { Some(EnchantmentTarget::DamagingEntity) }
}
Some("victim") => quote! { Some(EnchantmentTarget::Victim) },
_ => quote! { None },
};
quote! {
TargetedConditionalEffect {
enchanted: #enchanted,
affected: #affected,
effect: #eff_tokens,
}
}
})
.collect()
})
.unwrap_or_default();
quote! { &[#(#effects),*] }
}
fn parse_optional_value_effect(val: Option<&serde_json::Value>) -> TokenStream {
if let Some(v) = val {
let eff = v.get("effect").unwrap_or(v);
let eff_tokens = parse_value_effect(eff);
quote! { Some(#eff_tokens) }
} else {
quote! { None }
}
}
/// Generates the `TokenStream` for the `Enchantment` struct, `AttributeModifierSlot` enum, /// Generates the `TokenStream` for the `Enchantment` struct, `AttributeModifierSlot` enum,
/// and `from_name`/`from_id` lookup methods. /// data-driven effect types, and `from_name`/`from_id` lookup methods.
pub fn build() -> TokenStream { pub fn build() -> TokenStream {
let dir = std::path::Path::new("../../assets/datapacks/26_2/data/minecraft/enchantment"); let dir = std::path::Path::new("../../assets/datapacks/26_2/data/minecraft/enchantment");
let mut enchantments: BTreeMap<String, Enchantment> = BTreeMap::new(); let mut enchantments: BTreeMap<String, Enchantment> = BTreeMap::new();
@ -152,6 +553,80 @@ pub fn build() -> TokenStream {
}; };
let translate = translate.to_string(); let translate = translate.to_string();
let effects_map = enchantment.effects;
let projectile_spawned =
parse_conditional_entity_effects(effects_map.get("minecraft:projectile_spawned"));
let post_attack =
parse_targeted_conditional_entity_effects(effects_map.get("minecraft:post_attack"));
let projectile_count =
parse_conditional_value_effects(effects_map.get("minecraft:projectile_count"));
let projectile_spread =
parse_conditional_value_effects(effects_map.get("minecraft:projectile_spread"));
let projectile_piercing =
parse_conditional_value_effects(effects_map.get("minecraft:projectile_piercing"));
let ammo_use = parse_conditional_value_effects(effects_map.get("minecraft:ammo_use"));
let damage = parse_conditional_value_effects(effects_map.get("minecraft:damage"));
let knockback = parse_conditional_value_effects(effects_map.get("minecraft:knockback"));
let armor_effectiveness =
parse_conditional_value_effects(effects_map.get("minecraft:armor_effectiveness"));
let damage_protection =
parse_conditional_value_effects(effects_map.get("minecraft:damage_protection"));
let hit_block = parse_conditional_entity_effects(effects_map.get("minecraft:hit_block"));
let item_damage = parse_conditional_value_effects(effects_map.get("minecraft:item_damage"));
let equipment_drops =
parse_targeted_conditional_value_effects(effects_map.get("minecraft:equipment_drops"));
let fishing_time_reduction =
parse_conditional_value_effects(effects_map.get("minecraft:fishing_time_reduction"));
let fishing_luck_bonus =
parse_conditional_value_effects(effects_map.get("minecraft:fishing_luck_bonus"));
let block_experience =
parse_conditional_value_effects(effects_map.get("minecraft:block_experience"));
let mob_experience =
parse_conditional_value_effects(effects_map.get("minecraft:mob_experience"));
let repair_with_xp =
parse_conditional_value_effects(effects_map.get("minecraft:repair_with_xp"));
let smash_damage_per_fallen_block = parse_conditional_value_effects(
effects_map.get("minecraft:smash_damage_per_fallen_block"),
);
let trident_return_acceleration = parse_conditional_value_effects(
effects_map.get("minecraft:trident_return_acceleration"),
);
let trident_spin_attack_strength =
parse_optional_value_effect(effects_map.get("minecraft:trident_spin_attack_strength"));
let crossbow_charge_time =
parse_optional_value_effect(effects_map.get("minecraft:crossbow_charge_time"));
let prevent_armor_change = effects_map.contains_key("minecraft:prevent_armor_change");
let prevent_equipment_drop = effects_map.contains_key("minecraft:prevent_equipment_drop");
let effects_tokens = quote! {
EnchantmentEffects {
projectile_spawned: #projectile_spawned,
post_attack: #post_attack,
projectile_count: #projectile_count,
projectile_spread: #projectile_spread,
projectile_piercing: #projectile_piercing,
ammo_use: #ammo_use,
damage: #damage,
knockback: #knockback,
armor_effectiveness: #armor_effectiveness,
damage_protection: #damage_protection,
hit_block: #hit_block,
item_damage: #item_damage,
equipment_drops: #equipment_drops,
fishing_time_reduction: #fishing_time_reduction,
fishing_luck_bonus: #fishing_luck_bonus,
block_experience: #block_experience,
mob_experience: #mob_experience,
repair_with_xp: #repair_with_xp,
smash_damage_per_fallen_block: #smash_damage_per_fallen_block,
trident_return_acceleration: #trident_return_acceleration,
trident_spin_attack_strength: #trident_spin_attack_strength,
crossbow_charge_time: #crossbow_charge_time,
prevent_armor_change: #prevent_armor_change,
prevent_equipment_drop: #prevent_equipment_drop,
}
};
if let Some(exclusive_set) = &enchantment.exclusive_set { if let Some(exclusive_set) = &enchantment.exclusive_set {
let exclusive_set = format_ident!( let exclusive_set = format_ident!(
"{}", "{}",
@ -181,6 +656,7 @@ pub fn build() -> TokenStream {
base: #max_cost_base, base: #max_cost_base,
per_level_above_first: #max_cost_per_level, per_level_above_first: #max_cost_per_level,
}, },
effects: #effects_tokens,
}; };
}]); }]);
} else { } else {
@ -204,6 +680,7 @@ pub fn build() -> TokenStream {
base: #max_cost_base, base: #max_cost_base,
per_level_above_first: #max_cost_per_level, per_level_above_first: #max_cost_per_level,
}, },
effects: #effects_tokens,
}; };
}]); }]);
} }
@ -223,6 +700,176 @@ pub fn build() -> TokenStream {
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::slice::Iter; use std::slice::Iter;
#[derive(Clone, Debug, PartialEq)]
pub enum LevelBasedValue {
Constant(f32),
Linear {
base: f32,
per_level_above_first: f32,
},
Clamped {
value: &'static Self,
min: f32,
max: f32,
},
Fraction {
numerator: &'static Self,
denominator: &'static Self,
},
LevelsSquared {
added: f32,
},
Lookup {
values: &'static [f32],
fallback: &'static Self,
},
}
impl LevelBasedValue {
#[must_use]
pub fn calculate(&self, level: i32) -> f32 {
match self {
Self::Constant(val) => *val,
Self::Linear {
base,
per_level_above_first,
} => base + (level.max(1) - 1) as f32 * per_level_above_first,
Self::Clamped { value, min, max } => value.calculate(level).clamp(*min, *max),
Self::Fraction {
numerator,
denominator,
} => {
let denom = denominator.calculate(level);
if denom == 0.0 {
0.0
} else {
numerator.calculate(level) / denom
}
}
Self::LevelsSquared { added } => ((level * level) as f32) + added,
Self::Lookup { values, fallback } => {
let idx = (level - 1) as usize;
values
.get(idx)
.copied()
.unwrap_or_else(|| fallback.calculate(level))
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EnchantmentTarget {
Attacker,
DamagingEntity,
Victim,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TargetedConditionalEffect<T> {
pub enchanted: Option<EnchantmentTarget>,
pub affected: Option<EnchantmentTarget>,
pub effect: T,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConditionalEffect<T> {
pub effect: T,
}
#[derive(Clone, Debug, PartialEq)]
pub enum EnchantmentEntityEffect {
Ignite {
duration: LevelBasedValue,
},
DamageEntity {
damage: LevelBasedValue,
},
PlaySound {
sound: &'static str,
},
ReplaceBlock {
offset_x: i32,
offset_y: i32,
offset_z: i32,
trigger_game_event: Option<crate::game_event::GameEvent>,
},
Explode {
attribute_to_user: bool,
damage_type: Option<&'static crate::damage::DamageType>,
knockback_multiplier: Option<LevelBasedValue>,
immune_blocks: Option<&'static str>,
offset_x: f64,
offset_y: f64,
offset_z: f64,
radius: LevelBasedValue,
create_fire: bool,
block_interaction: &'static str,
small_particle: Option<crate::particle::Particle>,
large_particle: Option<crate::particle::Particle>,
sound: Option<crate::sound::Sound>,
},
AllOf(&'static [EnchantmentEntityEffect]),
Other,
}
#[derive(Clone, Debug, PartialEq)]
pub enum EnchantmentValueEffect {
Add(LevelBasedValue),
Multiply(LevelBasedValue),
Set(LevelBasedValue),
RemoveBinomial(LevelBasedValue),
Other,
}
impl EnchantmentValueEffect {
#[must_use]
pub fn process(&self, level: i32, current_value: f32) -> f32 {
match self {
Self::Add(val) => current_value + val.calculate(level),
Self::Multiply(val) => current_value * val.calculate(level),
Self::Set(val) => val.calculate(level),
Self::RemoveBinomial(val) => {
let prob = val.calculate(level);
if rand::random::<f32>() < prob {
0.0
} else {
current_value
}
}
Self::Other => current_value,
}
}
}
#[derive(Clone, Debug)]
pub struct EnchantmentEffects {
pub projectile_spawned: &'static [ConditionalEffect<EnchantmentEntityEffect>],
pub post_attack: &'static [TargetedConditionalEffect<EnchantmentEntityEffect>],
pub projectile_count: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub projectile_spread: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub projectile_piercing: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub ammo_use: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub damage: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub knockback: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub armor_effectiveness: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub damage_protection: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub hit_block: &'static [ConditionalEffect<EnchantmentEntityEffect>],
pub item_damage: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub equipment_drops: &'static [TargetedConditionalEffect<EnchantmentValueEffect>],
pub fishing_time_reduction: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub fishing_luck_bonus: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub block_experience: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub mob_experience: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub repair_with_xp: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub smash_damage_per_fallen_block: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub trident_return_acceleration: &'static [ConditionalEffect<EnchantmentValueEffect>],
pub trident_spin_attack_strength: Option<EnchantmentValueEffect>,
pub crossbow_charge_time: Option<EnchantmentValueEffect>,
pub prevent_armor_change: bool,
pub prevent_equipment_drop: bool,
}
pub struct Enchantment { pub struct Enchantment {
pub id: u8, pub id: u8,
pub name: &'static str, pub name: &'static str,
@ -236,7 +883,7 @@ pub fn build() -> TokenStream {
pub weight: i32, pub weight: i32,
pub min_cost: Cost, pub min_cost: Cost,
pub max_cost: Cost, pub max_cost: Cost,
// TODO: add more pub effects: EnchantmentEffects,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -350,6 +997,118 @@ pub fn build() -> TokenStream {
} }
ret ret
} }
pub fn modify_damage_protection(&self, level: i32, protection: &mut f32) {
for effect in self.effects.damage_protection {
*protection = effect.effect.process(level, *protection);
}
}
pub fn modify_damage(&self, level: i32, amount: &mut f64) {
for effect in self.effects.damage {
*amount = f64::from(effect.effect.process(level, *amount as f32));
}
}
pub fn modify_fall_based_damage(&self, level: i32, amount: &mut f64) {
for effect in self.effects.smash_damage_per_fallen_block {
*amount = f64::from(effect.effect.process(level, *amount as f32));
}
}
pub fn modify_knockback(&self, level: i32, amount: &mut f32) {
for effect in self.effects.knockback {
*amount = effect.effect.process(level, *amount);
}
}
pub fn modify_armor_effectiveness(&self, level: i32, amount: &mut f32) {
for effect in self.effects.armor_effectiveness {
*amount = effect.effect.process(level, *amount);
}
}
pub fn modify_durability_change(&self, level: i32, change: &mut f32) {
for effect in self.effects.item_damage {
*change = effect.effect.process(level, *change);
}
}
pub fn modify_ammo_count(&self, level: i32, change: &mut f32) {
for effect in self.effects.ammo_use {
*change = effect.effect.process(level, *change);
}
}
pub fn modify_piercing_count(&self, level: i32, count: &mut f32) {
for effect in self.effects.projectile_piercing {
*count = effect.effect.process(level, *count);
}
}
pub fn modify_block_experience(&self, level: i32, count: &mut f32) {
for effect in self.effects.block_experience {
*count = effect.effect.process(level, *count);
}
}
pub fn modify_mob_experience(&self, level: i32, experience: &mut f32) {
for effect in self.effects.mob_experience {
*experience = effect.effect.process(level, *experience);
}
}
pub fn modify_durability_to_repair_from_xp(&self, level: i32, change: &mut f32) {
for effect in self.effects.repair_with_xp {
*change = effect.effect.process(level, *change);
}
}
pub fn modify_trident_return_to_owner_acceleration(&self, level: i32, count: &mut f32) {
for effect in self.effects.trident_return_acceleration {
*count = effect.effect.process(level, *count);
}
}
pub fn modify_trident_spin_attack_strength(&self, level: i32, strength: &mut f32) {
if let Some(effect) = &self.effects.trident_spin_attack_strength {
*strength = effect.process(level, *strength);
}
}
pub fn modify_fishing_time_reduction(&self, level: i32, time_reduction: &mut f32) {
for effect in self.effects.fishing_time_reduction {
*time_reduction = effect.effect.process(level, *time_reduction);
}
}
pub fn modify_fishing_luck_bonus(&self, level: i32, luck: &mut f32) {
for effect in self.effects.fishing_luck_bonus {
*luck = effect.effect.process(level, *luck);
}
}
pub fn modify_projectile_count(&self, level: i32, count: &mut f32) {
for effect in self.effects.projectile_count {
*count = effect.effect.process(level, *count);
}
}
pub fn modify_projectile_spread(&self, level: i32, angle: &mut f32) {
for effect in self.effects.projectile_spread {
*angle = effect.effect.process(level, *angle);
}
}
pub fn modify_crossbow_charge_time(&self, level: i32, time: &mut f32) {
if let Some(effect) = &self.effects.crossbow_charge_time {
*time = effect.process(level, *time);
}
}
pub fn get_projectile_spawned_effects(&self) -> &'static [ConditionalEffect<EnchantmentEntityEffect>] {
self.effects.projectile_spawned
}
} }
} }
} }

View File

@ -1,20 +1,43 @@
use std::fs; use std::fs;
use heck::ToPascalCase;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::quote; use quote::{format_ident, quote};
use crate::array_to_tokenstream;
/// Generates the `TokenStream` for the `GameEvent` enum. /// Generates the `TokenStream` for the `GameEvent` enum.
pub fn build() -> TokenStream { pub fn build() -> TokenStream {
let game_events: Vec<String> = let game_events: Vec<String> =
serde_json::from_str(&fs::read_to_string("../../assets/game_event.json").unwrap()) serde_json::from_str(&fs::read_to_string("../../assets/game_event.json").unwrap())
.expect("Failed to parse game_event.json"); .expect("Failed to parse game_event.json");
let variants = array_to_tokenstream(&game_events);
let mut variants = TokenStream::new();
let mut from_name_match = TokenStream::new();
for event in &game_events {
let ident = format_ident!("{}", event.to_pascal_case());
let namespaced = format!("minecraft:{event}");
variants.extend(quote! {
#ident,
});
from_name_match.extend(quote! {
#event | #namespaced => Some(Self::#ident),
});
}
quote! { quote! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GameEvent { pub enum GameEvent {
#variants #variants
} }
impl GameEvent {
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name {
#from_name_match
_ => None,
}
}
}
} }
} }

View File

@ -139,19 +139,19 @@ pub struct ItemComponents {
#[serde(rename = "minecraft:provides_trim_material")] #[serde(rename = "minecraft:provides_trim_material")]
pub provides_trim_material: Option<serde_json::Value>, pub provides_trim_material: Option<serde_json::Value>,
#[serde(rename = "minecraft:rarity")] #[serde(rename = "minecraft:rarity")]
pub rarity: Option<serde_json::Value>, pub rarity: Option<String>,
#[serde(rename = "minecraft:recipes")] #[serde(rename = "minecraft:recipes")]
pub recipes: Option<serde_json::Value>, pub recipes: Option<serde_json::Value>,
#[serde(rename = "minecraft:repair_cost")] #[serde(rename = "minecraft:repair_cost")]
pub repair_cost: Option<serde_json::Value>, pub repair_cost: Option<i32>,
#[serde(rename = "minecraft:repairable")] #[serde(rename = "minecraft:repairable")]
pub repairable: Option<serde_json::Value>, pub repairable: Option<RepairableComponent>,
#[serde(rename = "minecraft:stored_enchantments")] #[serde(rename = "minecraft:stored_enchantments")]
pub stored_enchantments: Option<serde_json::Value>, pub stored_enchantments: Option<serde_json::Value>,
#[serde(rename = "minecraft:suspicious_stew_effects")] #[serde(rename = "minecraft:suspicious_stew_effects")]
pub suspicious_stew_effects: Option<serde_json::Value>, pub suspicious_stew_effects: Option<serde_json::Value>,
#[serde(rename = "minecraft:swing_animation")] #[serde(rename = "minecraft:swing_animation")]
pub swing_animation: Option<serde_json::Value>, pub swing_animation: Option<SwingAnimationComponent>,
#[serde(rename = "minecraft:tooltip_display")] #[serde(rename = "minecraft:tooltip_display")]
pub tooltip_display: Option<serde_json::Value>, pub tooltip_display: Option<serde_json::Value>,
#[serde(rename = "minecraft:use_effects")] #[serde(rename = "minecraft:use_effects")]
@ -162,6 +162,22 @@ pub struct ItemComponents {
pub writable_book_content: Option<serde_json::Value>, pub writable_book_content: Option<serde_json::Value>,
} }
fn default_swing_animation_type() -> String {
"whack".to_string()
}
const fn default_swing_animation_duration() -> i32 {
6
}
#[derive(Deserialize, Clone)]
pub struct SwingAnimationComponent {
#[serde(default = "default_swing_animation_type")]
pub r#type: String,
#[serde(default = "default_swing_animation_duration")]
pub duration: i32,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct EnchantableComponent { pub struct EnchantableComponent {
pub value: i32, pub value: i32,
@ -845,17 +861,50 @@ impl ToTokens for ItemComponents {
if self.provides_trim_material.is_some() { if self.provides_trim_material.is_some() {
tokens.extend(quote! { (ProvidesTrimMaterial, &ProvidesTrimMaterialImpl), }); tokens.extend(quote! { (ProvidesTrimMaterial, &ProvidesTrimMaterialImpl), });
} }
if self.rarity.is_some() { if let Some(rarity_str) = &self.rarity {
tokens.extend(quote! { (Rarity, &RarityImpl), }); let rarity_variant = match rarity_str.as_str() {
"uncommon" => quote! { crate::data_component_impl::Rarity::Uncommon },
"rare" => quote! { crate::data_component_impl::Rarity::Rare },
"epic" => quote! { crate::data_component_impl::Rarity::Epic },
_ => quote! { crate::data_component_impl::Rarity::Common },
};
tokens.extend(quote! { (Rarity, &RarityImpl { rarity: #rarity_variant }), });
} }
if self.recipes.is_some() { if self.recipes.is_some() {
tokens.extend(quote! { (Recipes, &RecipesImpl), }); tokens.extend(quote! { (Recipes, &RecipesImpl), });
} }
if self.repair_cost.is_some() { if let Some(cost) = self.repair_cost {
tokens.extend(quote! { (RepairCost, &RepairCostImpl), }); tokens.extend(quote! { (RepairCost, &RepairCostImpl { cost: #cost }), });
} }
if self.repairable.is_some() { if let Some(repairable) = &self.repairable {
tokens.extend(quote! { (Repairable, &RepairableImpl), }); let mut items_tokens = TokenStream::new();
match &repairable.items {
StringOrList::String(str) => {
if let Some(formatted) = str.strip_prefix('#') {
items_tokens.extend(quote! {
IDSet::Tag(Cow::Borrowed(#formatted))
});
} else {
let item_name = str.strip_prefix("minecraft:").unwrap_or(str);
let ident = format_ident!("{}", item_name.to_shouty_snake_case());
items_tokens.extend(quote! {
IDSet::IDs(Cow::Borrowed(&[&Self::#ident]))
});
}
}
StringOrList::List(items) => {
let mut ids = TokenStream::new();
for x in items {
let item_name = x.strip_prefix("minecraft:").unwrap_or(x);
let ident = format_ident!("{}", item_name.to_shouty_snake_case());
ids.extend(quote! { &Self::#ident, });
}
items_tokens.extend(quote! {
IDSet::IDs(Cow::Borrowed(&[#ids]))
});
}
}
tokens.extend(quote! { (Repairable, &RepairableImpl { items: #items_tokens }), });
} }
if self.stored_enchantments.is_some() { if self.stored_enchantments.is_some() {
tokens.extend(quote! { (StoredEnchantments, &StoredEnchantmentsImpl { enchantment: Cow::Borrowed(&[]) }), }); tokens.extend(quote! { (StoredEnchantments, &StoredEnchantmentsImpl { enchantment: Cow::Borrowed(&[]) }), });
@ -863,8 +912,23 @@ impl ToTokens for ItemComponents {
if self.suspicious_stew_effects.is_some() { if self.suspicious_stew_effects.is_some() {
tokens.extend(quote! { (SuspiciousStewEffects, &SuspiciousStewEffectsImpl::EMPTY), }); tokens.extend(quote! { (SuspiciousStewEffects, &SuspiciousStewEffectsImpl::EMPTY), });
} }
if self.swing_animation.is_some() { if let Some(swing) = &self.swing_animation {
tokens.extend(quote! { (SwingAnimation, &SwingAnimationImpl), }); let anim_type = match swing.r#type.as_str() {
"whack" => quote! { SwingAnimationType::Whack },
"stab" => quote! { SwingAnimationType::Stab },
"none" => quote! { SwingAnimationType::None },
_ => quote! { SwingAnimationType::Whack },
};
let duration = swing.duration;
tokens.extend(quote! {
(
SwingAnimation,
&SwingAnimationImpl {
animation_type: #anim_type,
duration: #duration,
},
),
});
} }
if self.tooltip_display.is_some() { if self.tooltip_display.is_some() {
tokens.extend(quote! { (TooltipDisplay, &TooltipDisplayImpl), }); tokens.extend(quote! { (TooltipDisplay, &TooltipDisplayImpl), });
@ -1034,6 +1098,11 @@ pub enum StringOrList {
List(Vec<String>), List(Vec<String>),
} }
#[derive(Deserialize, Clone)]
pub struct RepairableComponent {
pub items: StringOrList,
}
/// Deserialized equippable component describing how an item is worn or equipped. /// Deserialized equippable component describing how an item is worn or equipped.
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct EquippableComponent { pub struct EquippableComponent {
@ -1550,6 +1619,15 @@ pub fn build() -> TokenStream {
} }
} }
impl std::fmt::Debug for Item {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Item")
.field("id", &self.id)
.field("registry_key", &self.registry_key)
.finish()
}
}
#[derive(Clone)] #[derive(Clone)]
pub enum BedrockItemVersion { pub enum BedrockItemVersion {
Legacy, Legacy,

View File

@ -648,8 +648,19 @@ pub(crate) fn build() -> TokenStream {
#[must_use] #[must_use]
fn is_tagged_with(&self, tag: &str) -> Option<bool> { fn is_tagged_with(&self, tag: &str) -> Option<bool> {
let tag = tag.strip_prefix("#").unwrap_or(tag); let tag = tag.strip_prefix('#').unwrap_or(tag);
let items = get_tag_ids(Self::tag_key(), tag)?; let items = get_tag_ids(Self::tag_key(), tag).or_else(|| {
if !tag.contains(':') {
let mut full = String::with_capacity(10 + tag.len());
full.push_str("minecraft:");
full.push_str(tag);
get_tag_ids(Self::tag_key(), &full)
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
get_tag_ids(Self::tag_key(), stripped)
} else {
None
}
})?;
Some(items.contains(&self.registry_id())) Some(items.contains(&self.registry_id()))
} }
@ -660,8 +671,19 @@ pub(crate) fn build() -> TokenStream {
#[must_use] #[must_use]
fn get_tag_values(tag: &str) -> Option<&'static [&'static str]> { fn get_tag_values(tag: &str) -> Option<&'static [&'static str]> {
let tag = tag.strip_prefix("#").unwrap_or(tag); let tag = tag.strip_prefix('#').unwrap_or(tag);
get_tag_values(Self::tag_key(), tag) get_tag_values(Self::tag_key(), tag).or_else(|| {
if !tag.contains(':') {
let mut full = String::with_capacity(10 + tag.len());
full.push_str("minecraft:");
full.push_str(tag);
get_tag_values(Self::tag_key(), &full)
} else if let Some(stripped) = tag.strip_prefix("minecraft:") {
get_tag_values(Self::tag_key(), stripped)
} else {
None
}
})
} }
} }
} }