fix: anvil
This commit is contained in:
parent
39886a6c55
commit
fdb6da2b0d
134
Cargo.lock
generated
134
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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"] }
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>()
|
||||||
|
|||||||
@ -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;
|
|
||||||
let mut cost = 0;
|
|
||||||
|
|
||||||
// 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) {
|
pub fn set_repair_cost(&mut self, cost: i32) {
|
||||||
self.repair_cost = cost;
|
self.repair_cost.store(cost, Ordering::Relaxed);
|
||||||
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,16 +497,32 @@ 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 => {
|
||||||
if !self.insert_item(&mut slot_stack, 3, 39, true) {
|
// Result slot: from anvil to player
|
||||||
return ItemStack::EMPTY.clone();
|
if !slot.can_take_items(player) {
|
||||||
|
return ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
if !self.insert_item(&mut slot_stack, 3, 39, true) {
|
||||||
|
return ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
slot.on_quick_move_crafted(slot_stack.clone(), stack_left.clone());
|
||||||
|
slot.on_take_item(player, &stack_left);
|
||||||
|
self.create_result(player.has_infinite_materials());
|
||||||
|
self.send_content_updates();
|
||||||
|
return stack_left;
|
||||||
}
|
}
|
||||||
slot.on_quick_move_crafted(slot_stack.clone(), stack_left.clone());
|
std::cmp::Ordering::Less => {
|
||||||
} else {
|
// Input slots (0, 1): from anvil to player
|
||||||
// From player to anvil
|
if !self.insert_item(&mut slot_stack, 3, 39, false) {
|
||||||
if !self.insert_item(&mut slot_stack, 0, 2, false) {
|
return ItemStack::EMPTY.clone();
|
||||||
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) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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)))
|
||||||
});
|
});
|
||||||
writer.write_var_int(&remapped_state)?;
|
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||||
|
writer.write_var_int(&remapped_state)?;
|
||||||
|
} else {
|
||||||
|
writer.write_i32(remapped_state.0)?;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,9 +263,13 @@ 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> {
|
||||||
writer.write_i32(*self)
|
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||||
|
writer.write_var_int(&VarInt(*self))
|
||||||
|
} else {
|
||||||
|
writer.write_i32(*self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,9 +277,55 @@ 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> {
|
||||||
writer.write_u32(*self)
|
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||||
|
writer.write_var_int(&VarInt(*self as i32))
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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> {
|
||||||
writer.write_block_pos(self, version)
|
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||||
|
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);
|
||||||
writer.write_var_int(&VarInt(val))
|
if *version >= JavaMinecraftVersion::V_1_9 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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| {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,65 +65,92 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
Self::prime(args.world, args.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn explode(&self, args: ExplodeArgs<'_>) {
|
fn on_projectile_hit(&self, args: OnProjectileHitArgs<'_>) {
|
||||||
{
|
if args.projectile.get_entity().is_on_fire() {
|
||||||
let entity = Entity::new(args.world.clone(), args.position.to_f64(), &EntityType::TNT);
|
Self::prime(args.world, args.position);
|
||||||
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 fuse = rand::rng().random_range(0..DEFAULT_FUSE / 4) + DEFAULT_FUSE / 8;
|
|
||||||
let tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, fuse));
|
|
||||||
args.world.spawn_entity(tnt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn explode(&self, args: ExplodeArgs<'_>) {
|
||||||
|
if !args.world.level_info.load().game_rules.tnt_explodes {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 tnt = Arc::new(TNTEntity::new(entity, DEFAULT_POWER, fuse));
|
||||||
|
args.world.spawn_entity(tnt);
|
||||||
|
}
|
||||||
|
|
||||||
fn should_drop_items_on_explosion(&self) -> bool {
|
fn should_drop_items_on_explosion(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,68 +86,146 @@ 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();
|
||||||
|
|
||||||
for x in min_x..=max_x {
|
let min_chunk_x = min_x >> 4;
|
||||||
for y in min_y..=max_y {
|
let max_chunk_x = max_x >> 4;
|
||||||
for z in min_z..=max_z {
|
let min_chunk_z = min_z >> 4;
|
||||||
let pos = BlockPos(Vector3::new(x, y, z));
|
let max_chunk_z = max_z >> 4;
|
||||||
let current_block = world.get_block(&pos);
|
|
||||||
|
|
||||||
match mode {
|
for chunk_x in min_chunk_x..=max_chunk_x {
|
||||||
FillMode::Keep => {
|
for chunk_z in min_chunk_z..=max_chunk_z {
|
||||||
if !current_block.is_air() {
|
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 y in min_y..=max_y {
|
||||||
|
let y_rel = y - min_y_chunk;
|
||||||
|
if y_rel < 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
let rel_y = y_rel as usize;
|
||||||
_ => {
|
let section_index = rel_y / pumpkin_world::chunk::CHUNK_WIDTH;
|
||||||
if let Some(f) = filter
|
let sub_y = rel_y % pumpkin_world::chunk::CHUNK_WIDTH;
|
||||||
&& !f.test(current_block)
|
|
||||||
{
|
let Some(section) = block_sections.get(section_index) else {
|
||||||
continue;
|
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 {
|
||||||
|
FillMode::Keep => {
|
||||||
|
if !current_block.is_air() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(f) = filter
|
||||||
|
&& !f.test(current_block)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_edge = global_x == min_x
|
||||||
|
|| global_x == max_x
|
||||||
|
|| y == min_y
|
||||||
|
|| y == max_y
|
||||||
|
|| global_z == min_z
|
||||||
|
|| global_z == max_z;
|
||||||
|
|
||||||
|
let block_to_place = match mode {
|
||||||
|
FillMode::Outline => is_edge.then_some(target_state_id),
|
||||||
|
FillMode::Hollow => {
|
||||||
|
if is_edge {
|
||||||
|
Some(target_state_id)
|
||||||
|
} else {
|
||||||
|
Some(BlockStateId::AIR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FillMode::Destroy => {
|
||||||
|
if current_state_id != target_state_id {
|
||||||
|
world.break_block(
|
||||||
|
&pos,
|
||||||
|
None,
|
||||||
|
BlockFlags::SKIP_DROPS
|
||||||
|
| BlockFlags::NOTIFY_ALL
|
||||||
|
| BlockFlags::FORCE_STATE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(target_state_id)
|
||||||
|
}
|
||||||
|
FillMode::Replace | FillMode::Keep => Some(target_state_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(state_id) = block_to_place {
|
||||||
|
if current_block.default_state.block_entity_type != u16::MAX {
|
||||||
|
block_entities_to_remove.push(pos);
|
||||||
|
}
|
||||||
|
updates_for_chunk.push((rel_x, y, rel_z, state_id, pos));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_edge = x == min_x
|
if updates_for_chunk.is_empty() {
|
||||||
|| x == max_x
|
return (Vec::new(), block_entities_to_remove);
|
||||||
|| y == min_y
|
|
||||||
|| y == max_y
|
|
||||||
|| z == min_z
|
|
||||||
|| z == max_z;
|
|
||||||
|
|
||||||
let block_to_place = match mode {
|
|
||||||
FillMode::Outline => is_edge.then_some(target_state_id),
|
|
||||||
FillMode::Hollow => {
|
|
||||||
if is_edge {
|
|
||||||
Some(target_state_id)
|
|
||||||
} else {
|
|
||||||
Some(BlockStateId::AIR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FillMode::Destroy => {
|
|
||||||
world.break_block(
|
|
||||||
&pos,
|
|
||||||
None,
|
|
||||||
BlockFlags::SKIP_DROPS | BlockFlags::FORCE_STATE,
|
|
||||||
);
|
|
||||||
Some(target_state_id)
|
|
||||||
}
|
|
||||||
FillMode::Replace | FillMode::Keep => Some(target_state_id),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(state_id) = block_to_place {
|
|
||||||
world.set_block_state(&pos, state_id, BlockFlags::FORCE_STATE);
|
|
||||||
count += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
19
crates/pumpkin/src/enchantment/effects/add_value.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/add_value.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
crates/pumpkin/src/enchantment/effects/all_of.rs
Normal file
14
crates/pumpkin/src/enchantment/effects/all_of.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crates/pumpkin/src/enchantment/effects/damage_entity.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/damage_entity.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
crates/pumpkin/src/enchantment/effects/explode.rs
Normal file
68
crates/pumpkin/src/enchantment/effects/explode.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/pumpkin/src/enchantment/effects/ignite.rs
Normal file
23
crates/pumpkin/src/enchantment/effects/ignite.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/pumpkin/src/enchantment/effects/mod.rs
Normal file
21
crates/pumpkin/src/enchantment/effects/mod.rs
Normal 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;
|
||||||
19
crates/pumpkin/src/enchantment/effects/multiply_value.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/multiply_value.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
crates/pumpkin/src/enchantment/effects/play_sound.rs
Normal file
12
crates/pumpkin/src/enchantment/effects/play_sound.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
24
crates/pumpkin/src/enchantment/effects/remove_binomial.rs
Normal file
24
crates/pumpkin/src/enchantment/effects/remove_binomial.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
crates/pumpkin/src/enchantment/effects/replace_block.rs
Normal file
33
crates/pumpkin/src/enchantment/effects/replace_block.rs
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crates/pumpkin/src/enchantment/effects/set_value.rs
Normal file
19
crates/pumpkin/src/enchantment/effects/set_value.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
169
crates/pumpkin/src/enchantment/helper.rs
Normal file
169
crates/pumpkin/src/enchantment/helper.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
crates/pumpkin/src/enchantment/mod.rs
Normal file
6
crates/pumpkin/src/enchantment/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
pub mod effects;
|
||||||
|
pub mod helper;
|
||||||
|
|
||||||
|
pub use effects::*;
|
||||||
|
pub use helper::EnchantmentHelper;
|
||||||
|
pub use pumpkin_data::enchantment::*;
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)]
|
||||||
|
|||||||
@ -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,17 +685,55 @@ 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::Crit,
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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,12 +808,26 @@ 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 {
|
||||||
if self.has_hit.swap(true, Ordering::SeqCst) {
|
ProjectileHit::Block { .. } => {
|
||||||
return;
|
if self.has_hit.swap(true, Ordering::SeqCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
caller.on_hit(h);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
{
|
.enchantment
|
||||||
has_infinity = enchantments
|
.iter()
|
||||||
.enchantment
|
.any(|(e, _)| **e == pumpkin_data::Enchantment::INFINITY)
|
||||||
.iter()
|
});
|
||||||
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,61 +50,53 @@ 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| {
|
||||||
},
|
let inventory = player.inventory();
|
||||||
|slot| {
|
inventory.get_slot(slot).copy_with_count(1)
|
||||||
let inventory = player.inventory();
|
},
|
||||||
|
|
||||||
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 {
|
|
||||||
stack.patch.push((
|
|
||||||
DataComponent::ChargedProjectiles,
|
|
||||||
Some(Box::new(ChargedProjectilesImpl {
|
|
||||||
projectiles: vec![arrow_nbt],
|
|
||||||
})),
|
|
||||||
));
|
|
||||||
player.inventory().set_held_item(stack);
|
|
||||||
|
|
||||||
if player.gamemode.load() != GameMode::Creative {
|
|
||||||
player.consume_arrow(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
player.world().play_sound(
|
|
||||||
Sound::ItemCrossbowLoadingEnd,
|
|
||||||
SoundCategory::Players,
|
|
||||||
&player.position(),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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((
|
||||||
|
DataComponent::ChargedProjectiles,
|
||||||
|
Some(Box::new(ChargedProjectilesImpl {
|
||||||
|
projectiles: charged_nbts,
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
player.inventory().set_held_item(stack);
|
||||||
|
|
||||||
|
if let Some(slot) = arrow_slot
|
||||||
|
&& !is_creative
|
||||||
|
{
|
||||||
|
player.consume_arrow(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.world().play_sound(
|
||||||
|
Sound::ItemCrossbowLoadingEnd,
|
||||||
|
SoundCategory::Players,
|
||||||
|
&player.position(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.living_entity.clear_active_hand();
|
player.living_entity.clear_active_hand();
|
||||||
@ -121,66 +112,45 @@ 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 {
|
|
||||||
let world = player.world();
|
|
||||||
world.play_sound(
|
|
||||||
Sound::ItemCrossbowShoot,
|
|
||||||
SoundCategory::Players,
|
|
||||||
&player.position(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let (yaw, pitch) = player.rotation();
|
|
||||||
|
|
||||||
|
if let Some(charged) = charged_opt {
|
||||||
|
let mut projectiles = Vec::new();
|
||||||
for projectile_nbt in charged.projectiles {
|
for projectile_nbt in charged.projectiles {
|
||||||
let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) else {
|
if let Some(projectile) = ItemStack::read_item_stack(&projectile_nbt) {
|
||||||
continue;
|
projectiles.push(projectile);
|
||||||
};
|
|
||||||
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(),
|
|
||||||
&projectile,
|
|
||||||
pickup,
|
|
||||||
);
|
|
||||||
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
|
if !projectiles.is_empty() {
|
||||||
.retain(|(id, _)| *id != DataComponent::ChargedProjectiles);
|
let world = player.world();
|
||||||
player.inventory().set_held_item(held);
|
world.play_sound(
|
||||||
player.damage_held_item(1);
|
Sound::ItemCrossbowShoot,
|
||||||
|
SoundCategory::Players,
|
||||||
|
&player.position(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_creative = player.gamemode.load() == GameMode::Creative;
|
||||||
|
ProjectileWeaponItem::shoot_projectiles(
|
||||||
|
&world,
|
||||||
|
player.get_entity(),
|
||||||
|
&held,
|
||||||
|
&projectiles,
|
||||||
|
Self::ARROW_POWER,
|
||||||
|
1.0,
|
||||||
|
false,
|
||||||
|
is_creative,
|
||||||
|
);
|
||||||
|
|
||||||
|
held.patch
|
||||||
|
.retain(|(id, _)| *id != DataComponent::ChargedProjectiles);
|
||||||
|
player.inventory().set_held_item(held);
|
||||||
|
player.damage_held_item(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
172
crates/pumpkin/src/item/items/projectile_weapon.rs
Normal file
172
crates/pumpkin/src/item/items/projectile_weapon.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -646,10 +646,21 @@ pub(crate) fn build() -> TokenStream {
|
|||||||
fn registry_key(&self) -> &str;
|
fn registry_key(&self) -> &str;
|
||||||
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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user