fix: many non event firing

This commit is contained in:
Alexander Medvedev 2026-09-03 19:11:20 +02:00
parent feba9acb25
commit f8d1627697
37 changed files with 952 additions and 195 deletions

View File

@ -359,19 +359,24 @@ impl ClientPacket for CSpawnLivingEntity {
self.velocity.write_legacy(&mut write)?; self.velocity.write_legacy(&mut write)?;
if !v1_15 { if !v1_15 {
if v1_9 {
if let Some(metadata) = &self.metadata { if let Some(metadata) = &self.metadata {
write.write_slice(metadata)?; write.write_slice(metadata)?;
} else if v1_9 {
write.write_u8(0xFF)?;
} else { } else {
// In <= 1.8 (specifically 1.7.10), DataWatcher requires at least one entry write.write_u8(0xFF)?;
// so that `readWatchedObjectsFromPacketBuffer` returns a non-null List, }
// avoiding a NullPointerException in `S0FPacketSpawnMob.func_149027_c()`. } else if let Some(metadata) = &self.metadata
// (0 << 5) | 0 = 0 (type: byte, index: 0 flags), 0 (value), 127 (terminator) && metadata.last().copied() == Some(127)
write.write_slice(&[0x00, 0x00, 127])?; {
write.write_slice(metadata)?;
} else {
// In <= 1.8, write the 127 (0x7F) terminator byte for the DataWatcher list,
// matching vanilla and PacketEvents.
write.write_u8(127)?;
} }
} }
Ok(()) Ok(())
} }
} }

View File

@ -57,7 +57,20 @@ impl BlockBehaviour for FarmlandBlock {
// TODO: add rain check. Remember to check which one is most optimized. // TODO: add rain check. Remember to check which one is most optimized.
if is_water_nearby(args.world, args.position) { if is_water_nearby(args.world, args.position) {
let mut props = FarmlandProperties::default(args.block); let mut props = FarmlandProperties::default(args.block);
props.moisture = 7; let mut new_moisture = 7;
if let Some(server) = args.world.server.upgrade() {
let mut event = crate::plugin::api::events::block::moisture_change::MoistureChangeEvent::new(
*args.position,
args.world.clone(),
new_moisture,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
new_moisture = event.new_moisture;
}
props.moisture = new_moisture.clamp(0, 7) as u8;
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),
@ -80,7 +93,20 @@ impl BlockBehaviour for FarmlandBlock {
); );
} }
} else { } else {
props.moisture = (props.moisture as i32 - 1).clamp(0, 7) as u8; let mut new_moisture = (props.moisture as i32 - 1).clamp(0, 7);
if let Some(server) = args.world.server.upgrade() {
let mut event = crate::plugin::api::events::block::moisture_change::MoistureChangeEvent::new(
*args.position,
args.world.clone(),
new_moisture,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
new_moisture = event.new_moisture;
}
props.moisture = new_moisture.clamp(0, 7) as u8;
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),

View File

@ -144,6 +144,17 @@ impl FireBlock {
let block = world.get_block(pos); let block = world.get_block(pos);
let odds = Self::get_burn_odds(block); let odds = Self::get_burn_odds(block);
if rand::rng().random_range(0..chance) < odds { if rand::rng().random_range(0..chance) < odds {
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::block_burn::BlockBurnEvent {
igniting_block: &Block::FIRE,
block,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
let old_block = block; let old_block = block;
if rand::rng().random_range(0..(age + 10) as i32) < 5 if rand::rng().random_range(0..(age + 10) as i32) < 5
&& !Self::is_near_rain(world.as_ref(), pos) && !Self::is_near_rain(world.as_ref(), pos)
@ -416,10 +427,24 @@ impl BlockBehaviour for FireBlock {
let mut new_fire_props = let mut new_fire_props =
FireProperties::from_state_id(fire_state_id); FireProperties::from_state_id(fire_state_id);
new_fire_props.age = spread_age; new_fire_props.age = spread_age;
let new_state_id = new_fire_props.to_state_id(&Block::FIRE);
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::block_spread::BlockSpreadEvent::new(
*pos,
offset_pos,
world.clone(),
new_state_id,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
continue;
}
}
world.set_block_state( world.set_block_state(
&offset_pos, &offset_pos,
new_fire_props.to_state_id(&Block::FIRE), new_state_id,
BlockFlags::NOTIFY_NEIGHBORS, BlockFlags::NOTIFY_NEIGHBORS,
); );
} }

View File

@ -101,6 +101,16 @@ impl BlockBehaviour for LeavesBlock {
let state_id = args.world.get_block_state_id(args.position); let state_id = args.world.get_block_state_id(args.position);
let props = OakLeavesLikeProperties::from_state_id(state_id); let props = OakLeavesLikeProperties::from_state_id(state_id);
if !props.persistent && props.distance == DECAY_DISTANCE { if !props.persistent && props.distance == DECAY_DISTANCE {
if let Some(server) = args.world.server.upgrade() {
let mut event = crate::plugin::api::events::block::leaves_decay::LeavesDecayEvent::new(
*args.position,
args.world.clone(),
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
args.world args.world
.break_block(args.position, None, BlockFlags::empty()); .break_block(args.position, None, BlockFlags::empty());
} }

View File

@ -73,10 +73,26 @@ 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);
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::block_grow::BlockGrowEvent::new(
world.clone(),
block,
state,
block,
new_state_id,
*pos,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
world.set_block_state(pos, event.new_state_id, BlockFlags::NOTIFY_LISTENERS);
} else {
world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS); world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS);
} }
} }
} }
}
} }
pub fn get_available_moisture(world: &World, pos: &BlockPos, block: &Block) -> f32 { pub fn get_available_moisture(world: &World, pos: &BlockPos, block: &Block) -> f32 {

View File

@ -37,12 +37,28 @@ impl BlockBehaviour for SweetBerryBushBlock {
match props.age { match props.age {
2 | 3 => { 2 | 3 => {
let index = props.age; let index = props.age;
props.age = 1;
let count: u8 = rand::rng().random_range((index - 1)..=(index)); let count: u8 = rand::rng().random_range((index - 1)..=(index));
for _ in 0..count { let mut drops = vec![ItemStack::new(count, &Item::SWEET_BERRIES)];
if let Some(player_arc) = args.world.get_player_by_uuid(args.player.gameprofile.id)
&& let Some(server) = args.world.server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_harvest_block::PlayerHarvestBlockEvent {
player: player_arc,
block_pos: *args.position,
harvested_items: drops.clone(),
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return BlockActionResult::Pass;
}
drops = event.harvested_items;
}
props.age = 1;
for stack in drops {
args.world.drop_stack( args.world.drop_stack(
args.position, args.position,
ItemStack::new(1, &Item::SWEET_BERRIES), // stack,
); );
} }
args.world.set_block_state( args.world.set_block_state(

View File

@ -133,6 +133,18 @@ impl CrafterBlock {
world.set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_LISTENERS); world.set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_LISTENERS);
let mut result_stack = ItemStack::new(recipe_result.count, item); let mut result_stack = ItemStack::new(recipe_result.count, item);
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::crafter_craft::CrafterCraftEvent::new(
*pos,
world.clone(),
result_stack.clone(),
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
result_stack = event.result;
}
Self::dispense_item(world, pos, crafter, &mut result_stack, props.orientation); Self::dispense_item(world, pos, crafter, &mut result_stack, props.orientation);
for i in 0..CrafterBlockEntity::INVENTORY_SIZE { for i in 0..CrafterBlockEntity::INVENTORY_SIZE {

View File

@ -106,6 +106,20 @@ impl BrewingStandBlockEntity {
let ingredient_id = ingredient.get_item().id; let ingredient_id = ingredient.get_item().id;
// Fire BrewEvent before mutating items
if let Some(server) = world.server.upgrade() {
let mut brew_event = crate::plugin::api::events::inventory::brew::BrewEvent::new(
self.position,
self.fuel.load(Ordering::Relaxed) as u8,
);
server
.plugin_manager
.fire_blocking(&server, &mut brew_event);
if brew_event.cancelled {
return;
}
}
// Brew potion slots // Brew potion slots
let mut ingredient_used = false; let mut ingredient_used = false;
{ {
@ -171,17 +185,6 @@ impl BrewingStandBlockEntity {
return; return;
} }
// Fire BrewEvent
if let Some(server) = world.server.upgrade() {
let mut brew_event = crate::plugin::api::events::inventory::brew::BrewEvent::new(
self.position,
self.fuel.load(Ordering::Relaxed) as u8,
);
server
.plugin_manager
.fire_blocking(&server, &mut brew_event);
}
// Check if remaining ingredient matches or clear it // Check if remaining ingredient matches or clear it
if let Ok(items) = self.items.read() { if let Ok(items) = self.items.read() {
let remaining = &items[3]; let remaining = &items[3];
@ -441,6 +444,7 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity {
self self
} }
#[allow(clippy::too_many_lines)]
fn tick(&self, world: &Arc<crate::world::World>) { fn tick(&self, world: &Arc<crate::world::World>) {
// Refill fuel counter from fuel item if needed // Refill fuel counter from fuel item if needed
let fuel_refilled = self.fuel.load(Ordering::Relaxed) <= 0 let fuel_refilled = self.fuel.load(Ordering::Relaxed) <= 0
@ -458,10 +462,18 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity {
server server
.plugin_manager .plugin_manager
.fire_blocking(&server, &mut fuel_event); .fire_blocking(&server, &mut fuel_event);
if fuel_event.cancelled {
false
} else {
self.fuel.store(fuel_event.fuel_power as i32, Ordering::Relaxed);
items[4].decrement(1);
true
} }
} else {
self.fuel.store(20, Ordering::Relaxed); self.fuel.store(20, Ordering::Relaxed);
items[4].decrement(1); items[4].decrement(1);
true true
}
} else { } else {
false false
}; };
@ -493,9 +505,24 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity {
self.mark_dirty(); self.mark_dirty();
} }
} else if brewable && self.fuel.load(Ordering::Relaxed) > 0 { } else if brewable && self.fuel.load(Ordering::Relaxed) > 0 {
let brew_time = if let Some(server) = world.server.upgrade() {
let mut start_event = crate::plugin::api::events::block::brewing_start::BrewingStartEvent::new(
self.position,
world.clone(),
400,
);
server.plugin_manager.fire_blocking(&server, &mut start_event);
if start_event.cancelled {
return;
}
start_event.brewing_time
} else {
400
};
// Start new brewing cycle // Start new brewing cycle
self.fuel.fetch_sub(1, Ordering::Relaxed); self.fuel.fetch_sub(1, Ordering::Relaxed);
self.brew_time.store(400, Ordering::Relaxed); self.brew_time.store(brew_time, Ordering::Relaxed);
*self *self
.ingredient_item .ingredient_item
.lock() .lock()

View File

@ -108,7 +108,22 @@ impl BlockEntity for CampfireBlockEntity {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
if *total <= 0 { if *total <= 0 {
*total = recipe.cookingtime; let mut cooking_time = recipe.cookingtime;
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::campfire_start::CampfireStartEvent::new(
self.position,
world.clone(),
stack.clone(),
slot as u8,
cooking_time,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
continue;
}
cooking_time = event.cooking_time;
}
*total = cooking_time;
} }
*total *total
}; };
@ -130,7 +145,20 @@ impl BlockEntity for CampfireBlockEntity {
continue; continue;
}; };
let result = ItemStack::new(recipe.result.count, result_item); let mut result = ItemStack::new(recipe.result.count, result_item);
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::block_cook::BlockCookEvent::new(
self.position,
world.clone(),
stack.clone(),
result.clone(),
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
continue;
}
result = event.result;
}
Self::spawn_cooked_item(world, self.position, result); Self::spawn_cooked_item(world, self.position, result);
*self.items[slot] *self.items[slot]

View File

@ -398,6 +398,8 @@ macro_rules! impl_block_entity_for_cooking {
base_fuel_ticks base_fuel_ticks
}; };
let mut burn_ticks = adjusted_fuel_ticks;
let mut burn_cancelled = false;
if let Some(server) = world.server.upgrade() { if let Some(server) = world.server.upgrade() {
let mut burn_event = $crate::plugin::api::events::inventory::furnace_burn::FurnaceBurnEvent::new( let mut burn_event = $crate::plugin::api::events::inventory::furnace_burn::FurnaceBurnEvent::new(
self.position, self.position,
@ -405,9 +407,15 @@ macro_rules! impl_block_entity_for_cooking {
adjusted_fuel_ticks as u32, adjusted_fuel_ticks as u32,
); );
server.plugin_manager.fire_blocking(&server, &mut burn_event); server.plugin_manager.fire_blocking(&server, &mut burn_event);
if burn_event.cancelled {
burn_cancelled = true;
} else {
burn_ticks = burn_event.burn_time as u16;
} }
self.set_lit_time_remaining(adjusted_fuel_ticks); }
self.set_lit_total_time(adjusted_fuel_ticks); if !burn_cancelled {
self.set_lit_time_remaining(burn_ticks);
self.set_lit_total_time(burn_ticks);
if self.is_burning() { if self.is_burning() {
is_dirty = true; is_dirty = true;
@ -428,8 +436,10 @@ macro_rules! impl_block_entity_for_cooking {
} }
} }
} }
}
if self.is_burning() && can_accept_output { if self.is_burning() && can_accept_output {
let mut start_cancelled = false;
if self.get_cooking_time_spent() == 0 { if self.get_cooking_time_spent() == 0 {
if let Some(server) = world.server.upgrade() { if let Some(server) = world.server.upgrade() {
let mut start_event = $crate::plugin::api::events::inventory::furnace_start_smelt::FurnaceStartSmeltEvent::new( let mut start_event = $crate::plugin::api::events::inventory::furnace_start_smelt::FurnaceStartSmeltEvent::new(
@ -438,8 +448,14 @@ macro_rules! impl_block_entity_for_cooking {
self.get_cooking_total_time() as u32, self.get_cooking_total_time() as u32,
); );
server.plugin_manager.fire_blocking(&server, &mut start_event); server.plugin_manager.fire_blocking(&server, &mut start_event);
if start_event.cancelled {
start_cancelled = true;
} else {
self.set_cooking_total_time(start_event.cooking_time as u16);
} }
} }
}
if !start_cancelled {
self.cooking_time_spent.fetch_add(1, Ordering::Relaxed); self.cooking_time_spent.fetch_add(1, Ordering::Relaxed);
if self.get_cooking_time_spent() == self.get_cooking_total_time() { if self.get_cooking_time_spent() == self.get_cooking_total_time() {
@ -448,6 +464,7 @@ macro_rules! impl_block_entity_for_cooking {
let cooking_total_time = cooking_recipe.cookingtime; let cooking_total_time = cooking_recipe.cookingtime;
self.set_cooking_total_time(cooking_total_time as u16); self.set_cooking_total_time(cooking_total_time as u16);
let mut smelt_cancelled = false;
if let Some(server) = world.server.upgrade() { if let Some(server) = world.server.upgrade() {
let mut smelt_event = $crate::plugin::api::events::inventory::furnace_smelt::FurnaceSmeltEvent::new( let mut smelt_event = $crate::plugin::api::events::inventory::furnace_smelt::FurnaceSmeltEvent::new(
self.position, self.position,
@ -455,11 +472,17 @@ macro_rules! impl_block_entity_for_cooking {
cooking_recipe.result.id.to_string(), cooking_recipe.result.id.to_string(),
); );
server.plugin_manager.fire_blocking(&server, &mut smelt_event); server.plugin_manager.fire_blocking(&server, &mut smelt_event);
if smelt_event.cancelled {
smelt_cancelled = true;
} }
}
if !smelt_cancelled {
self.craft_recipe(Some(cooking_recipe)); self.craft_recipe(Some(cooking_recipe));
is_dirty = true; is_dirty = true;
} }
} }
}
}
} else { } else {
self.set_cooking_time_spent(0); self.set_cooking_time_spent(0);
} }

View File

@ -526,6 +526,18 @@ impl BlockRegistry {
return false; return false;
} }
if behaviour.is_bonemeal_success(args) { if behaviour.is_bonemeal_success(args) {
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::block::block_fertilize::BlockFertilizeEvent::new(
*position,
world.clone(),
None,
vec![],
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return false;
}
}
behaviour.perform_bonemeal(args); behaviour.perform_bonemeal(args);
} }
true true

View File

@ -155,6 +155,9 @@ impl Goal for BreakDoorGoal {
); );
if let Some(server) = world.server.upgrade() { if let Some(server) = world.server.upgrade() {
server.plugin_manager.fire_blocking(&server, &mut event); server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
} }
world.set_block_state(&door_pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL); world.set_block_state(&door_pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL);
world.sync_world_event( world.sync_world_event(

View File

@ -58,14 +58,11 @@ impl BreathManager {
} }
let in_water = Self::is_eye_in_water(player); let in_water = Self::is_eye_in_water(player);
let prev = self.air_supply.load(Ordering::Relaxed);
if in_water { if in_water {
let prev = self let mut new_air = (prev - AIR_DEPLETION_RATE).max(0);
.air_supply
.fetch_sub(AIR_DEPLETION_RATE, Ordering::Relaxed);
let new_air = (prev - AIR_DEPLETION_RATE).max(0);
if new_air != prev { if new_air != prev {
self.air_supply.store(new_air, Ordering::Relaxed);
let server = player.world().server.upgrade(); let server = player.world().server.upgrade();
if let Some(server) = server { if let Some(server) = server {
let mut event = crate::plugin::api::events::entity::entity_air_change::EntityAirChangeEvent::new( let mut event = crate::plugin::api::events::entity::entity_air_change::EntityAirChangeEvent::new(
@ -73,7 +70,12 @@ impl BreathManager {
new_air, new_air,
); );
server.plugin_manager.fire_blocking(&server, &mut event); server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
} }
new_air = event.amount.clamp(0, MAX_AIR);
}
self.air_supply.store(new_air, Ordering::Relaxed);
self.send_air_supply(player); self.send_air_supply(player);
} }
@ -88,9 +90,20 @@ impl BreathManager {
} }
} }
} else { } else {
let prev = self.air_supply.load(Ordering::Relaxed); let mut new_air = (prev + AIR_RECOVERY_RATE).min(MAX_AIR);
let new_air = (prev + AIR_RECOVERY_RATE).min(MAX_AIR);
if new_air != prev { if new_air != prev {
let server = player.world().server.upgrade();
if let Some(server) = server {
let mut event = crate::plugin::api::events::entity::entity_air_change::EntityAirChangeEvent::new(
player.entity_id(),
new_air,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
new_air = event.amount.clamp(0, MAX_AIR);
}
self.air_supply.store(new_air, Ordering::Relaxed); self.air_supply.store(new_air, Ordering::Relaxed);
self.send_air_supply(player); self.send_air_supply(player);
} }

View File

@ -407,6 +407,7 @@ impl CombatTracker {
pub fn get_death_message( pub fn get_death_message(
&self, &self,
victim_name: pumpkin_util::text::TextComponent, victim_name: pumpkin_util::text::TextComponent,
kill_credit_name: Option<pumpkin_util::text::TextComponent>,
) -> pumpkin_util::text::TextComponent { ) -> pumpkin_util::text::TextComponent {
if self.entries.is_empty() { if self.entries.is_empty() {
return pumpkin_util::text::TextComponent::translate_cross( return pumpkin_util::text::TextComponent::translate_cross(
@ -456,7 +457,7 @@ impl CombatTracker {
) )
} }
pumpkin_data::damage::DeathMessageType::Default => { pumpkin_data::damage::DeathMessageType::Default => {
self.get_default_death_message(victim_name, killing_blow) self.get_default_death_message(victim_name, killing_blow, kill_credit_name)
} }
} }
} }
@ -585,6 +586,7 @@ impl CombatTracker {
&self, &self,
victim_name: pumpkin_util::text::TextComponent, victim_name: pumpkin_util::text::TextComponent,
killing_blow: &CombatEntry, killing_blow: &CombatEntry,
kill_credit_name: Option<pumpkin_util::text::TextComponent>,
) -> pumpkin_util::text::TextComponent { ) -> pumpkin_util::text::TextComponent {
let damage_type = killing_blow.damage_type; let damage_type = killing_blow.damage_type;
let msg_id = damage_type.message_id; let msg_id = damage_type.message_id;
@ -598,18 +600,18 @@ impl CombatTracker {
) )
} else { } else {
pumpkin_util::text::TextComponent::translate_cross( pumpkin_util::text::TextComponent::translate_cross(
format!("death.attack.{msg_id}.player"), format!("death.attack.{msg_id}"),
format!("death.attack.{msg_id}.player"), format!("death.attack.{msg_id}"),
[victim_name, attacker_name.clone()], [victim_name, attacker_name.clone()],
) )
} }
} else if let Some(killer) = self.get_killer_entry() } else if let Some(killer_name) = kill_credit_name.or_else(|| {
&& let Some(killer_name) = &killer.attacker_name self.get_killer_entry().and_then(|k| k.attacker_name.clone())
{ }) {
pumpkin_util::text::TextComponent::translate_cross( pumpkin_util::text::TextComponent::translate_cross(
format!("death.attack.{msg_id}.player"), format!("death.attack.{msg_id}.player"),
format!("death.attack.{msg_id}.player"), format!("death.attack.{msg_id}.player"),
[victim_name, killer_name.clone()], [victim_name, killer_name],
) )
} else { } else {
pumpkin_util::text::TextComponent::translate_cross( pumpkin_util::text::TextComponent::translate_cross(

View File

@ -14,10 +14,9 @@ use pumpkin_util::Hand;
use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::position::BlockPos;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::sync::atomic::{ use std::sync::atomic::{
AtomicBool, AtomicU8, AtomicBool, AtomicI32, AtomicI64, AtomicU8,
Ordering::{Relaxed, SeqCst}, Ordering::{Relaxed, SeqCst},
}; };
use tracing::warn; use tracing::warn;
@ -119,6 +118,15 @@ pub struct LivingEntity {
/// Tracks combat entries, assisted falls, kill credit, and death messages. /// Tracks combat entries, assisted falls, kill credit, and death messages.
pub combat_tracker: std::sync::Mutex<CombatTracker>, pub combat_tracker: std::sync::Mutex<CombatTracker>,
/// The ID of the player that last hurt this entity.
pub last_hurt_by_player_id: AtomicI32,
/// The tick at which this entity was last hurt by a player.
pub last_hurt_by_player_time: AtomicI64,
/// The ID of the mob/entity that last hurt this entity.
pub last_hurt_by_mob_id: AtomicI32,
/// The tick at which this entity was last hurt by a mob/entity.
pub last_hurt_by_mob_time: AtomicI64,
water_movement_speed_multiplier: f32, water_movement_speed_multiplier: f32,
livings_flags: AtomicU8, livings_flags: AtomicU8,
@ -231,12 +239,56 @@ impl LivingEntity {
last_attacking_id: AtomicI32::new(0), last_attacking_id: AtomicI32::new(0),
last_attack_time: AtomicI32::new(0), last_attack_time: AtomicI32::new(0),
combat_tracker: std::sync::Mutex::new(CombatTracker::new()), combat_tracker: std::sync::Mutex::new(CombatTracker::new()),
last_hurt_by_player_id: AtomicI32::new(0),
last_hurt_by_player_time: AtomicI64::new(0),
last_hurt_by_mob_id: AtomicI32::new(0),
last_hurt_by_mob_time: AtomicI64::new(0),
movement_input: AtomicCell::new(Vector3::default()), movement_input: AtomicCell::new(Vector3::default()),
water_movement_speed_multiplier, water_movement_speed_multiplier,
last_block_pos: AtomicCell::new(None), last_block_pos: AtomicCell::new(None),
} }
} }
/// Returns the entity that should receive kill credit for this entity's death.
/// Following vanilla Java logic (`LivingEntity.getKillCredit`):
/// 1. Prioritize `last_hurt_by_player` if hurt within the last 100 ticks (5 seconds).
/// 2. Then `last_hurt_by_mob` if hurt within the last 100 ticks.
/// 3. Fall back to combat tracker's killer entry if available.
pub fn get_kill_credit(&self) -> Option<Arc<dyn EntityBase>> {
let world = self.entity.world.load();
let current_tick = world.level_info.load().day_time;
let player_id = self.last_hurt_by_player_id.load(Relaxed);
let player_time = self.last_hurt_by_player_time.load(Relaxed);
if player_id != 0
&& (current_tick - player_time).abs() <= 100
&& let Some(player) = world.get_entity_by_id(player_id)
{
return Some(player);
}
let mob_id = self.last_hurt_by_mob_id.load(Relaxed);
let mob_time = self.last_hurt_by_mob_time.load(Relaxed);
if mob_id != 0
&& (current_tick - mob_time).abs() <= 100
&& let Some(mob) = world.get_entity_by_id(mob_id)
{
return Some(mob);
}
let tracker = self
.combat_tracker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(killer) = tracker.get_killer_entry()
&& let Some(killer_id) = killer.attacker_id
{
return world.get_entity_by_id(killer_id);
}
None
}
/// Triggers location-based enchantment effects (e.g. Frost Walker) when the entity's block position changes. /// Triggers location-based enchantment effects (e.g. Frost Walker) when the entity's block position changes.
pub fn on_changed_block(&self, caller: &dyn EntityBase, _pos: BlockPos) { pub fn on_changed_block(&self, caller: &dyn EntityBase, _pos: BlockPos) {
let pos_f64 = self.entity.pos.load(); let pos_f64 = self.entity.pos.load();
@ -1607,28 +1659,40 @@ impl LivingEntity {
} }
} }
#[allow(clippy::redundant_closure_for_method_calls)]
pub fn get_death_message( pub fn get_death_message(
dyn_self: &dyn EntityBase, dyn_self: &dyn EntityBase,
damage_type: DamageType, damage_type: DamageType,
source: Option<&dyn EntityBase>, source: Option<&dyn EntityBase>,
cause: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>,
) -> TextComponent { ) -> TextComponent {
let kill_credit = dyn_self
.get_living_entity()
.and_then(Self::get_kill_credit);
let kill_credit_name = kill_credit.as_ref().map(|c| c.get_display_name());
if let Some(living) = dyn_self.get_living_entity() { if let Some(living) = dyn_self.get_living_entity() {
let tracker = living let tracker = living
.combat_tracker .combat_tracker
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
return tracker.get_death_message(dyn_self.get_display_name()); return tracker.get_death_message(dyn_self.get_display_name(), kill_credit_name);
} }
if let Some(cause) = cause if let Some(cause) = cause
&& source.is_some() && source.is_some()
{ {
TextComponent::translate_cross( TextComponent::translate_cross(
format!("death.attack.{}.player", damage_type.message_id), format!("death.attack.{}", damage_type.message_id),
format!("death.attack.{}.player", damage_type.message_id), format!("death.attack.{}", damage_type.message_id),
[dyn_self.get_display_name(), cause.get_display_name()], [dyn_self.get_display_name(), cause.get_display_name()],
) )
} else if let Some(killer) = cause.or(source).map(|c| c.get_display_name()).or(kill_credit_name) {
TextComponent::translate_cross(
format!("death.attack.{}.player", damage_type.message_id),
format!("death.attack.{}.player", damage_type.message_id),
[dyn_self.get_display_name(), killer],
)
} else { } else {
TextComponent::translate_cross( TextComponent::translate_cross(
format!("death.attack.{}", damage_type.message_id), format!("death.attack.{}", damage_type.message_id),
@ -1656,7 +1720,10 @@ impl LivingEntity {
self.movement_input.store(Vector3::default()); self.movement_input.store(Vector3::default());
self.jumping.store(false, Relaxed); self.jumping.store(false, Relaxed);
self.update_death_stats(&*dyn_self, cause.or(source)); let kill_credit = self.get_kill_credit();
let killer = cause.or(source).or(kill_credit.as_deref());
self.update_death_stats(&*dyn_self, killer);
// Plays the death sound // Plays the death sound
world.send_entity_status(&self.entity, EntityStatus::Death, Some(ActorEventID::Death)); world.send_entity_status(&self.entity, EntityStatus::Death, Some(ActorEventID::Death));
@ -1686,7 +1753,7 @@ impl LivingEntity {
let is_thundering = world.is_thundering(); let is_thundering = world.is_thundering();
let has_player_kill = let has_player_kill =
cause.is_some_and(|c| c.get_entity().entity_type == &EntityType::PLAYER) || { killer.is_some_and(|c| c.get_entity().entity_type == &EntityType::PLAYER) || {
let tracker = self let tracker = self
.combat_tracker .combat_tracker
.lock() .lock()
@ -1697,7 +1764,7 @@ impl LivingEntity {
let params = LootContextParameters { let params = LootContextParameters {
killed_by_player: Some(has_player_kill), killed_by_player: Some(has_player_kill),
this_entity: Some(self.entity.entity_type), this_entity: Some(self.entity.entity_type),
killer_entity: cause.map(|c| c.get_entity().entity_type), killer_entity: killer.map(|c| c.get_entity().entity_type),
direct_killer_entity: source.map(|s| s.get_entity().entity_type), direct_killer_entity: source.map(|s| s.get_entity().entity_type),
position: Some(self.entity.pos.load()), position: Some(self.entity.pos.load()),
world_time: world.level_info.load().day_time as u64, world_time: world.level_info.load().day_time as u64,
@ -1721,7 +1788,7 @@ impl LivingEntity {
if params.killed_by_player.unwrap_or(false) if params.killed_by_player.unwrap_or(false)
&& world.level_info.load().game_rules.mob_drops && world.level_info.load().game_rules.mob_drops
{ {
let amount = dyn_self.get_experience_reward(cause); let amount = dyn_self.get_experience_reward(killer);
if amount > 0 { if amount > 0 {
ExperienceOrbEntity::spawn(&world, self.entity.pos.load(), amount); ExperienceOrbEntity::spawn(&world, self.entity.pos.load(), amount);
} }
@ -1813,14 +1880,42 @@ impl LivingEntity {
) { ) {
let world = self.entity.world.load(); let world = self.entity.world.load();
let show_death_messages = { world.level_info.load().game_rules.show_death_messages }; let show_death_messages = { world.level_info.load().game_rules.show_death_messages };
if self.entity.entity_type == &EntityType::PLAYER && show_death_messages { if self.entity.entity_type == &EntityType::PLAYER {
//TODO: KillCredit
let death_message = Self::get_death_message(dyn_self, damage_type, source, cause); let death_message = Self::get_death_message(dyn_self, damage_type, source, cause);
if let Some(server) = world.server.upgrade() { let mut final_death_message = death_message;
if let Some(player) = dyn_self.get_player() {
if let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = world.server.upgrade()
{
let mut event = crate::plugin::api::events::entity::entity_death::PlayerDeathEvent::new(
player_arc,
final_death_message.clone(),
0,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
final_death_message = event.death_message;
}
player.handle_killed(&final_death_message);
}
if show_death_messages
&& let Some(server) = world.server.upgrade()
{
for player in server.get_all_players() { for player in server.get_all_players() {
player.send_system_message(&death_message); player.send_system_message(&final_death_message);
} }
} }
} else if self.entity.custom_name.load().is_some() {
let death_message = Self::get_death_message(dyn_self, damage_type, source, cause);
tracing::info!(
"Named entity {} died: {}",
dyn_self.get_display_name().to_pretty_console(),
death_message.to_pretty_console()
);
} }
} }
@ -1894,11 +1989,6 @@ impl LivingEntity {
); );
} }
} }
killer_player.increment_stat(
StatisticCategory::Killed,
self.entity.entity_type.id as i32,
1,
);
} }
} }
@ -2521,6 +2611,54 @@ impl LivingEntity {
} }
amount = damage_event.damage; amount = damage_event.damage;
if let Some(damager) = source.or(cause) {
let mut by_entity_event =
crate::plugin::api::events::entity::entity_damage_by_entity::EntityDamageByEntityEvent {
entity_id: self.entity.entity_id,
damager_id: damager.get_entity().entity_id,
damage: amount,
cause: format!("{damage_type:?}"),
cancelled: false,
};
if let Some(server) = self.entity.world.load().server.upgrade() {
server
.plugin_manager
.fire_blocking(&server, &mut by_entity_event);
}
if by_entity_event.cancelled {
return false;
}
amount = by_entity_event.damage;
} else if position.is_some()
|| matches!(
damage_type,
DamageType::CACTUS
| DamageType::SWEET_BERRY_BUSH
| DamageType::CAMPFIRE
| DamageType::HOT_FLOOR
| DamageType::STALAGMITE
)
{
let damager_pos = position.map(|p| BlockPos(Vector3::new(p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32)));
let mut by_block_event =
crate::plugin::api::events::entity::entity_damage_by_block::EntityDamageByBlockEvent {
entity_id: self.entity.entity_id,
damager_pos,
damage: amount,
cause: format!("{damage_type:?}"),
cancelled: false,
};
if let Some(server) = self.entity.world.load().server.upgrade() {
server
.plugin_manager
.fire_blocking(&server, &mut by_block_event);
}
if by_block_event.cancelled {
return false;
}
amount = by_block_event.damage;
}
let world = self.entity.world.load(); let world = self.entity.world.load();
let is_fire_damage = damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE); let is_fire_damage = damage_type.has_tag(&tag::DamageType::MINECRAFT_IS_FIRE);
@ -2758,10 +2896,20 @@ impl LivingEntity {
} }
if let Some(attacker) = cause.or(source) { if let Some(attacker) = cause.or(source) {
let attacker_id = attacker.get_entity().entity_id;
self.last_attacker_id self.last_attacker_id
.store(attacker.get_entity().entity_id, Relaxed); .store(attacker_id, Relaxed);
self.last_attacked_time self.last_attacked_time
.store(self.entity.age.load(Relaxed), Relaxed); .store(self.entity.age.load(Relaxed), Relaxed);
let current_tick = world.level_info.load().day_time;
if attacker.get_player().is_some() {
self.last_hurt_by_player_id.store(attacker_id, Relaxed);
self.last_hurt_by_player_time.store(current_tick, Relaxed);
} else if attacker.get_living_entity().is_some() {
self.last_hurt_by_mob_id.store(attacker_id, Relaxed);
self.last_hurt_by_mob_time.store(current_tick, Relaxed);
}
} }
} }

View File

@ -305,6 +305,14 @@ pub trait EntityBase: Send + Sync + std::any::Any {
} }
fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> { fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
if version < JavaMinecraftVersion::V_1_9 {
let entity = self.get_entity();
let shared_flags = entity.flags.load(Ordering::Relaxed);
return (shared_flags != 0).then(|| {
// (0 << 5) | 0 = 0 (type: byte, index: 0 flags), value, 127 (terminator)
Box::<[u8]>::from([0x00u8, shared_flags as u8, 127u8])
});
}
self.get_mob().map_or_else( self.get_mob().map_or_else(
|| { || {
let entity = self.get_entity(); let entity = self.get_entity();
@ -382,7 +390,9 @@ 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(meta) = metadata { if let Some(meta) = metadata
&& (version >= JavaMinecraftVersion::V_1_9 || meta.last().copied() == Some(127))
{
let meta_packet = CSetEntityMetadata::new(entity.entity_id.into(), meta); let meta_packet = CSetEntityMetadata::new(entity.entity_id.into(), meta);
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);
@ -3356,6 +3366,23 @@ impl Entity {
pub fn leash_to(&self, holder: Arc<dyn EntityBase>) { pub fn leash_to(&self, holder: Arc<dyn EntityBase>) {
let holder_entity_id = holder.get_entity().entity_id; let holder_entity_id = holder.get_entity().entity_id;
let world = self.world.load();
if let Some(server) = world.server.upgrade()
&& let Some(player) = holder.get_player()
&& let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id)
{
let mut event = crate::plugin::api::events::player::player_leash_entity::PlayerLeashEntityEvent {
player: player_arc,
entity_id: self.entity_id,
holder_id: holder_entity_id,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
*self *self
.leashed_to .leashed_to
.lock() .lock()
@ -3387,6 +3414,18 @@ impl Entity {
} }
pub fn unleash(&self) { pub fn unleash(&self) {
let world = self.world.load();
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::entity::entity_unleash::EntityUnleashEvent::new(
self.entity_id,
"unleashed".to_string(),
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
let old_holder = self let old_holder = self
.leashed_to .leashed_to
.lock() .lock()
@ -3396,6 +3435,19 @@ impl Entity {
return; return;
} }
if let Some(holder) = &old_holder
&& let Some(server) = world.server.upgrade()
&& let Some(player) = holder.get_player()
&& let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id)
{
let mut event = crate::plugin::api::events::player::player_unleash_entity::PlayerUnleashEntityEvent {
player: player_arc,
entity_id: self.entity_id,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
}
let je_packet = let je_packet =
pumpkin_protocol::java::client::play::CSetEntityLink::new(self.entity_id, -1, true); pumpkin_protocol::java::client::play::CSetEntityLink::new(self.entity_id, -1, true);
let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink { let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink {

View File

@ -296,9 +296,21 @@ impl Mob for AxolotlEntity {
let item = item_stack.get_item(); let item = item_stack.get_item();
if item == &Item::WATER_BUCKET { if item == &Item::WATER_BUCKET {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
let entity = self.get_entity(); let entity = self.get_entity();
let world = entity.world.load(); let world = entity.world.load();
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::player::player_bucket_entity::PlayerBucketEntityEvent {
player: player.clone(),
entity_id: entity.entity_id,
bucket_item: "axolotl_bucket".to_string(),
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return false;
}
}
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
let pos = entity.pos.load(); let pos = entity.pos.load();
world.play_sound(Sound::ItemBucketFillAxolotl, SoundCategory::Neutral, &pos); world.play_sound(Sound::ItemBucketFillAxolotl, SoundCategory::Neutral, &pos);
entity.remove(); entity.remove();

View File

@ -127,9 +127,21 @@ impl Mob for TadpoleEntity {
} }
if item_stack.get_item() == &Item::WATER_BUCKET { if item_stack.get_item() == &Item::WATER_BUCKET {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
let entity = self.get_entity(); let entity = self.get_entity();
let world = entity.world.load(); let world = entity.world.load();
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::player::player_bucket_entity::PlayerBucketEntityEvent {
player: player.clone(),
entity_id: entity.entity_id,
bucket_item: "tadpole_bucket".to_string(),
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return false;
}
}
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
let pos = entity.pos.load(); let pos = entity.pos.load();
world.play_sound(Sound::ItemBucketFillTadpole, SoundCategory::Neutral, &pos); world.play_sound(Sound::ItemBucketFillTadpole, SoundCategory::Neutral, &pos);
entity.remove(); entity.remove();

View File

@ -2141,6 +2141,9 @@ impl Mob for VillagerEntity {
} }
fn mob_java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> { fn mob_java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option<Box<[u8]>> {
if version < JavaMinecraftVersion::V_1_9 {
return None;
}
let mut metadata = Vec::new(); let mut metadata = Vec::new();
Metadata::new( Metadata::new(
tracked_data::villager::VILLAGER_DATA, tracked_data::villager::VILLAGER_DATA,

View File

@ -1603,19 +1603,38 @@ impl Player {
return false; return false;
} }
let mut final_block_pos = block_pos;
if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id)
&& let Some(server) = self.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_spawn_change::PlayerSpawnChangeEvent {
player: player_arc,
new_spawn: Some(block_pos),
forced,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return false;
}
if let Some(pos) = event.new_spawn {
final_block_pos = pos;
}
}
let bedrock_dimension = match dimension.minecraft_name { let bedrock_dimension = match dimension.minecraft_name {
"minecraft:the_nether" => 1, "minecraft:the_nether" => 1,
"minecraft:the_end" => 2, "minecraft:the_end" => 2,
_ => 0, _ => 0,
}; };
self.client.try_enqueue_packet_editioned( self.client.try_enqueue_packet_editioned(
&CPlayerSpawnPosition::new(block_pos, yaw, pitch, dimension.minecraft_name.to_owned()), &CPlayerSpawnPosition::new(final_block_pos, yaw, pitch, dimension.minecraft_name.to_owned()),
&pumpkin_protocol::bedrock::client::CSetSpawnPosition { &pumpkin_protocol::bedrock::client::CSetSpawnPosition {
spawn_position_type: spawn_position_type:
pumpkin_protocol::bedrock::client::SpawnPositionType::PlayerRespawn, pumpkin_protocol::bedrock::client::SpawnPositionType::PlayerRespawn,
block_position: block_pos, block_position: final_block_pos,
dimension_type: bedrock_dimension.into(), dimension_type: bedrock_dimension.into(),
spawn_block_pos: block_pos, spawn_block_pos: final_block_pos,
}, },
); );
@ -1624,7 +1643,7 @@ impl Player {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RespawnPoint { .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RespawnPoint {
dimension, dimension,
position: block_pos, position: final_block_pos,
yaw, yaw,
force: forced, force: forced,
}); });
@ -2293,11 +2312,39 @@ impl Player {
} }
pub fn open_sign_editor(&self, location: BlockPos, is_front_text: bool) { pub fn open_sign_editor(&self, location: BlockPos, is_front_text: bool) {
if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id)
&& let Some(server) = self.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_open_sign::PlayerOpenSignEvent {
player: player_arc,
block_pos: location,
is_front: is_front_text,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
let packet = COpenSignEditor::new(location, is_front_text); let packet = COpenSignEditor::new(location, is_front_text);
self.try_send_client_packet(&packet); self.try_send_client_packet(&packet);
} }
pub fn set_velocity(&self, velocity: Vector3<f64>) { pub fn set_velocity(&self, mut velocity: Vector3<f64>) {
if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id)
&& let Some(server) = self.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_velocity::PlayerVelocityEvent {
player: player_arc,
velocity,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
velocity = event.velocity;
}
self.living_entity.entity.set_velocity(velocity); self.living_entity.entity.set_velocity(velocity);
self.try_send_client_packet(&CEntityVelocity::new(self.entity_id().into(), velocity)); self.try_send_client_packet(&CEntityVelocity::new(self.entity_id().into(), velocity));
} }
@ -2340,6 +2387,19 @@ impl Player {
/// Sends custom server links to the player (displayed in the client Esc pause menu). /// Sends custom server links to the player (displayed in the client Esc pause menu).
pub fn set_server_links(&self, links: &[pumpkin_protocol::Link<'_>]) { pub fn set_server_links(&self, links: &[pumpkin_protocol::Link<'_>]) {
if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id)
&& let Some(server) = self.world().server.upgrade()
{
let link_strings = links.iter().map(|l| l.url.clone()).collect();
let mut event = crate::plugin::api::events::player::player_links_send::PlayerLinksSendEvent::new(
player_arc,
link_strings,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
let packet = CPlayServerLinks::new(links); let packet = CPlayServerLinks::new(links);
self.try_send_client_packet(&packet); self.try_send_client_packet(&packet);
} }
@ -3029,8 +3089,25 @@ impl Player {
} }
pub fn increment_stat(&self, category: statistics::StatisticCategory, stat: i32, amount: i32) { pub fn increment_stat(&self, category: statistics::StatisticCategory, stat: i32, amount: i32) {
let final_amount = if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id)
&& let Some(server) = self.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_statistic_increment::PlayerStatisticIncrementEvent {
player: player_arc,
statistic_id: format!("{category:?}:{stat}"),
amount,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
event.amount
} else {
amount
};
if let Ok(mut stats) = self.stats.try_lock() { if let Ok(mut stats) = self.stats.try_lock() {
stats.increment(category, stat, amount); stats.increment(category, stat, final_amount);
} }
} }
@ -3729,6 +3806,14 @@ impl Player {
} }
player.request_teleport(position, yaw, pitch); player.request_teleport(position, yaw, pitch);
let mut changed_world_event = crate::plugin::api::events::player::player_changed_world::PlayerChangedWorldEvent {
player: player.clone(),
from_world: current_world,
to_world: new_world,
cancelled: false,
};
server.plugin_manager.fire(&server, &mut changed_world_event).await;
} }
}} }}
} }
@ -4364,8 +4449,19 @@ impl Player {
} }
} }
#[allow(dead_code)] pub fn send_combat_death(&self, death_msg: &TextComponent) {
async fn handle_killed(&self, death_msg: TextComponent) { self.try_enqueue_packet_editioned(
&CCombatDeath::new(self.entity_id().into(), death_msg),
&SActorEvent {
target_runtime_id: VarULong(self.entity_id() as u64),
event_id: ActorEventID::Death,
data: VarInt(0),
fire_at_position: None,
},
);
}
pub fn handle_killed(&self, death_msg: &TextComponent) {
self.trigger_advancement( self.trigger_advancement(
crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilled, crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilled,
); );
@ -4402,17 +4498,7 @@ impl Player {
if matches!(self.client.as_ref(), ClientPlatform::Java(_)) { if matches!(self.client.as_ref(), ClientPlatform::Java(_)) {
self.set_client_loaded(false); self.set_client_loaded(false);
} }
self.client self.send_combat_death(death_msg);
.send_packet_now_editioned(
&CCombatDeath::new(self.entity_id().into(), &death_msg),
&SActorEvent {
target_runtime_id: VarULong(self.entity_id() as u64),
event_id: ActorEventID::Death,
data: VarInt(0),
fire_at_position: None,
},
)
.await;
self.send_health(); self.send_health();
self.send_bedrock_respawn_state(RespawnState::SearchingForSpawn); self.send_bedrock_respawn_state(RespawnState::SearchingForSpawn);
} }
@ -5137,10 +5223,27 @@ impl Player {
return xp; return xp;
} }
let xp_used = (repaired + 1) / 2;
if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id)
&& let Some(server) = self.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_item_mend::PlayerItemMendEvent {
player: player_arc,
item_name: stack.item.registry_key.to_string(),
repair_amount: repaired,
exp_consumed: xp_used,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return xp;
}
}
let updated_stack = stack.clone(); let updated_stack = stack.clone();
self.inventory.set_slot(slot_index, updated_stack.clone()); self.inventory.set_slot(slot_index, updated_stack.clone());
let xp_used = (repaired + 1) / 2;
xp = xp.saturating_sub(xp_used); xp = xp.saturating_sub(xp_used);
self.try_send_slot_set_packet(&CSetPlayerInventory::new( self.try_send_slot_set_packet(&CSetPlayerInventory::new(

View File

@ -1013,6 +1013,19 @@ impl EntityBase for ArrowEntity {
_ => {} _ => {}
} }
if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = player.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_pickup_arrow::PlayerPickupArrowEvent::new(
player_arc,
self.entity.entity_id,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
// Try to insert an arrow into the player's inventory // Try to insert an arrow into the player's inventory
let item_stack = self let item_stack = self
.item_stack .item_stack

View File

@ -70,6 +70,16 @@ impl FireworkRocketEntity {
pub fn explode_and_remove(&self, world: &World) { pub fn explode_and_remove(&self, world: &World) {
let entity = self.get_entity(); let entity = self.get_entity();
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::entity::firework_explode::FireworkExplodeEvent {
entity_id: entity.entity_id,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
world.send_entity_status( world.send_entity_status(
entity, entity,
EntityStatus::FireworksExplode, EntityStatus::FireworksExplode,
@ -97,6 +107,21 @@ impl EntityBase for FireworkRocketEntity {
let shooter = shooter.get_entity(); let shooter = shooter.get_entity();
if shooter.is_fall_flying() { if shooter.is_fall_flying() {
let mut boost_cancelled = false;
if let Some(player) = world.get_player_by_id(shooter_id)
&& let Some(server) = world.server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_elytra_boost::PlayerElytraBoostEvent {
player,
firework_id: entity.entity_id,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
boost_cancelled = true;
}
}
if !boost_cancelled {
let rotation = shooter.rotation().to_f64(); let rotation = shooter.rotation().to_f64();
let shooter_vel = shooter.velocity.load(); let shooter_vel = shooter.velocity.load();
@ -109,6 +134,7 @@ impl EntityBase for FireworkRocketEntity {
entity.set_velocity(new_shooter_vel); entity.set_velocity(new_shooter_vel);
} }
} }
}
} else { } else {
velocity.x *= 1.15; velocity.x *= 1.15;
velocity.z *= 1.15; velocity.z *= 1.15;

View File

@ -182,6 +182,18 @@ impl EntityBase for LingeringPotionEntity {
// Spawn and configure an `AreaEffectCloud` entity // Spawn and configure an `AreaEffectCloud` entity
extinguish_fire_if_water_potion(&world, hit_pos, &stack); extinguish_fire_if_water_potion(&world, hit_pos, &stack);
if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::entity::lingering_potion_splash::LingeringPotionSplashEvent::new(
self.get_entity().entity_id,
block_pos,
stack.item.registry_key.to_string(),
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
let cloud_entity = crate::entity::Entity::from_uuid( let cloud_entity = crate::entity::Entity::from_uuid(
Uuid::new_v4(), Uuid::new_v4(),
world.clone(), world.clone(),

View File

@ -132,6 +132,7 @@ impl EntityBase for SplashPotionEntity {
self self
} }
#[allow(clippy::too_many_lines)]
fn on_hit(&self, hit: crate::entity::projectile::ProjectileHit) { fn on_hit(&self, hit: crate::entity::projectile::ProjectileHit) {
let world = self.get_entity().world.load(); let world = self.get_entity().world.load();
let hit_pos = hit.hit_pos(); let hit_pos = hit.hit_pos();
@ -228,20 +229,38 @@ impl EntityBase for SplashPotionEntity {
candidates.push(p.clone() as Arc<dyn EntityBase>); candidates.push(p.clone() as Arc<dyn EntityBase>);
} }
let mut affected: Vec<(Arc<dyn EntityBase>, f32)> = Vec::new();
for cand in candidates { for cand in candidates {
if let Some(living) = cand.get_living_entity() { if cand.get_living_entity().is_some() {
let pos = cand.get_entity().pos.load(); let pos = cand.get_entity().pos.load();
let dx = pos.x - hit_pos.x; let dx = pos.x - hit_pos.x;
let dy = pos.y - hit_pos.y; let dy = pos.y - hit_pos.y;
let dz = pos.z - hit_pos.z; let dz = pos.z - hit_pos.z;
let dist = (dx * dx + dy * dy + dz * dz).sqrt(); let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist > radius { if dist <= radius {
continue; let scale = (1.0f32 - (dist as f32 / radius as f32)).max(0.0);
affected.push((cand, scale));
}
}
} }
// Distance scaling let affected_ids: Vec<i32> = affected.iter().map(|(c, _)| c.get_entity().entity_id).collect();
let scale = (1.0f32 - (dist as f32 / radius as f32)).max(0.0); if let Some(server) = world.server.upgrade() {
let mut event = crate::plugin::api::events::entity::potion_splash::PotionSplashEvent::new(
self.get_entity().entity_id,
block_pos,
stack.item.registry_key.to_string(),
affected_ids,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
affected.retain(|(c, _)| event.affected_entities.contains(&c.get_entity().entity_id));
}
for (cand, scale) in affected {
if let Some(living) = cand.get_living_entity() {
crate::item::potion::PotionContents::apply_effects_to( crate::item::potion::PotionContents::apply_effects_to(
living, living,
effects.clone(), effects.clone(),

View File

@ -117,6 +117,21 @@ impl ItemBehaviour for BrushItem {
); );
if is_sand || is_gravel { if is_sand || is_gravel {
if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = player.world().server.upgrade()
{
let mut event = crate::plugin::api::events::block::block_brush::BlockBrushEvent::new(
location,
world.clone(),
player_arc,
player.inventory().held_item(),
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
let current_state_id = world.get_block_state_id(&location); let current_state_id = world.get_block_state_id(&location);
let current_stage = get_dusted_stage(block, current_state_id); let current_stage = get_dusted_stage(block, current_state_id);

View File

@ -76,6 +76,20 @@ impl ItemBehaviour for ShearsItem {
.downcast_ref::<crate::entity::passive::sheep::SheepEntity>() .downcast_ref::<crate::entity::passive::sheep::SheepEntity>()
&& !sheep.is_sheared() && !sheep.is_sheared()
{ {
if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = player.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_shear_entity::PlayerShearEntityEvent {
player: player_arc,
entity_id: sheep.mob_entity.living_entity.entity.entity_id,
hand: 0,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
}
sheep.set_sheared(true); sheep.set_sheared(true);
let world = player.world(); let world = player.world();
let pos = sheep.mob_entity.living_entity.entity.pos.load(); let pos = sheep.mob_entity.living_entity.entity.pos.load();
@ -176,6 +190,23 @@ fn handle_beehive(
}); });
if let Some(new_state_id) = action { if let Some(new_state_id) = action {
let mut drops = vec![ItemStack::new(3, &Item::HONEYCOMB)];
if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = player.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_harvest_block::PlayerHarvestBlockEvent {
player: player_arc,
block_pos: *location,
harvested_items: drops.clone(),
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return false;
}
drops = event.harvested_items;
}
world.set_block_state(location, new_state_id, BlockFlags::NOTIFY_ALL); world.set_block_state(location, new_state_id, BlockFlags::NOTIFY_ALL);
world.play_sound( world.play_sound(
Sound::BlockBeehiveShear, Sound::BlockBeehiveShear,
@ -188,11 +219,13 @@ fn handle_beehive(
f64::from(location.0.y) + 0.5, f64::from(location.0.y) + 0.5,
f64::from(location.0.z) + 0.5, f64::from(location.0.z) + 0.5,
); );
for item in drops {
let item_entity = Arc::new(ItemEntity::new( let item_entity = Arc::new(ItemEntity::new(
Entity::new(world.clone(), drop_pos, &EntityType::ITEM), Entity::new(world.clone(), drop_pos, &EntityType::ITEM),
ItemStack::new(3, &Item::HONEYCOMB), item,
)); ));
world.spawn_entity(item_entity); world.spawn_entity(item_entity);
}
player.damage_held_item(1); player.damage_held_item(1);
return true; return true;
} }

View File

@ -65,6 +65,21 @@ impl ItemBehaviour for TridentItem {
return; return;
} }
if let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = world.server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_riptide::PlayerRiptideEvent {
player: player_arc,
item_name: "trident".to_string(),
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
player.living_entity.clear_active_hand();
return;
}
}
let f = f64::from(riptide_level); let f = f64::from(riptide_level);
let (yaw, pitch) = player.rotation(); let (yaw, pitch) = player.rotation();
let f_yaw = f32::to_radians(yaw); let f_yaw = f32::to_radians(yaw);

View File

@ -160,7 +160,9 @@ impl ChunkSender {
epoch: u32, epoch: u32,
version: JavaMinecraftVersion, version: JavaMinecraftVersion,
) -> Option<PreparedBatch> { ) -> Option<PreparedBatch> {
if self.in_flight_batches >= self.max_in_flight { if version >= JavaMinecraftVersion::V_1_20_2
&& self.in_flight_batches >= self.max_in_flight
{
return None; return None;
} }
@ -294,9 +296,9 @@ impl ChunkSender {
&& let ClientPlatform::Java(java_client) = client && let ClientPlatform::Java(java_client) = client
{ {
java_client.try_send_packet(&CChunkBatchEnd::new(sent_count as u16)); java_client.try_send_packet(&CChunkBatchEnd::new(sent_count as u16));
self.in_flight_batches = self.in_flight_batches.saturating_add(1);
} }
self.in_flight_batches = self.in_flight_batches.saturating_add(1);
self.send_quota -= sent_count as f32; self.send_quota -= sent_count as f32;
} }

View File

@ -29,7 +29,7 @@ impl PendingConnection {
pub async fn handle_encryption_response( pub async fn handle_encryption_response(
&mut self, &mut self,
server: &Server, server: &Arc<Server>,
encryption_response: SEncryptionResponse, encryption_response: SEncryptionResponse,
) -> Option<PacketHandlerResult> { ) -> Option<PacketHandlerResult> {
debug!("Handling encryption"); debug!("Handling encryption");
@ -146,9 +146,22 @@ impl PendingConnection {
pub(super) async fn finish_login( pub(super) async fn finish_login(
&mut self, &mut self,
server: &Server, server: &Arc<Server>,
profile: &GameProfile, profile: &GameProfile,
) -> Option<PacketHandlerResult> { ) -> Option<PacketHandlerResult> {
let mut pre_login_event = crate::plugin::api::events::player::async_player_pre_login::AsyncPlayerPreLoginEvent {
player_name: profile.name.clone(),
player_uuid: profile.id,
ip_address: self.address,
kick_message: TextComponent::text("Disconnected"),
cancelled: false,
};
server.plugin_manager.fire(server, &mut pre_login_event).await;
if pre_login_event.cancelled {
self.kick(pre_login_event.kick_message).await;
return Some(PacketHandlerResult::Stop);
}
let props = profile.properties.load(); let props = profile.properties.load();
let packet = CLoginSuccess::new( let packet = CLoginSuccess::new(
&profile.id, &profile.id,

View File

@ -4,7 +4,7 @@ use super::*;
impl PendingConnection { impl PendingConnection {
pub async fn handle_login_start( pub async fn handle_login_start(
&mut self, &mut self,
server: &Server, server: &Arc<Server>,
login_start: SLoginStart, login_start: SLoginStart,
) -> Option<PacketHandlerResult> { ) -> Option<PacketHandlerResult> {
debug!("login start"); debug!("login start");

View File

@ -4,7 +4,7 @@ use super::*;
impl PendingConnection { impl PendingConnection {
pub async fn handle_plugin_response( pub async fn handle_plugin_response(
&mut self, &mut self,
server: &Server, server: &Arc<Server>,
plugin_response: SLoginPluginResponse, plugin_response: SLoginPluginResponse,
) -> Option<PacketHandlerResult> { ) -> Option<PacketHandlerResult> {
debug!("Handling plugin"); debug!("Handling plugin");

View File

@ -1148,12 +1148,45 @@ impl JavaClient {
} }
id if id == SCustomPayload::to_id(version) => { id if id == SCustomPayload::to_id(version) => {
let payload = SCustomPayload::read(&mut payload, &version)?; let payload = SCustomPayload::read(&mut payload, &version)?;
let channel_str = payload.channel.to_string();
let mut event = PlayerCustomPayloadEvent::new( let mut event = PlayerCustomPayloadEvent::new(
player.clone(), player.clone(),
payload.channel.to_string(), channel_str.clone(),
Bytes::copy_from_slice(payload.data), Bytes::copy_from_slice(payload.data),
); );
server.plugin_manager.fire_blocking(server, &mut event); server.plugin_manager.fire_blocking(server, &mut event);
if channel_str == "minecraft:register" {
if let Ok(channels_data) = std::str::from_utf8(payload.data) {
for ch in channels_data.split('\0') {
if !ch.is_empty() {
let mut reg_event = crate::plugin::api::events::player::player_register_channel::PlayerRegisterChannelEvent::new(
player.clone(),
ch.to_string(),
);
server.plugin_manager.fire_blocking(server, &mut reg_event);
let mut ch_event = crate::plugin::api::events::player::player_channel::PlayerChannelEvent {
player: player.clone(),
channel: ch.to_string(),
cancelled: false,
};
server.plugin_manager.fire_blocking(server, &mut ch_event);
}
}
}
} else if channel_str == "minecraft:unregister"
&& let Ok(channels_data) = std::str::from_utf8(payload.data)
{
for ch in channels_data.split('\0') {
if !ch.is_empty() {
let mut unreg_event = crate::plugin::api::events::player::player_unregister_channel::PlayerUnregisterChannelEvent::new(
player.clone(),
ch.to_string(),
);
server.plugin_manager.fire_blocking(server, &mut unreg_event);
}
}
}
} }
id if id == SRecipeBookChangeSettings::to_id(version) => { id if id == SRecipeBookChangeSettings::to_id(version) => {
self.handle_recipe_book_change_settings( self.handle_recipe_book_change_settings(

View File

@ -313,7 +313,7 @@ impl PendingConnection {
async fn handle_login_packet( async fn handle_login_packet(
&mut self, &mut self,
server: &Server, server: &Arc<Server>,
packet: &RawPacket, packet: &RawPacket,
) -> Result<Option<PacketHandlerResult>, ReadingError> { ) -> Result<Option<PacketHandlerResult>, ReadingError> {
debug!("Handling login group"); debug!("Handling login group");

View File

@ -8,12 +8,34 @@ impl JavaClient {
return; return;
} }
let pages: Vec<String> = packet.pages.iter().map(|p| (*p).to_string()).collect(); let mut pages: Vec<String> = packet.pages.iter().map(|p| (*p).to_string()).collect();
let mut title = packet.title.map(std::string::ToString::to_string);
let signing = title.is_some();
let slot = player.inventory().get_selected_slot() as u32;
if let Some(title) = packet.title { if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id)
&& let Some(server) = player.world().server.upgrade()
{
let mut event = crate::plugin::api::events::player::player_edit_book::PlayerEditBookEvent {
player: player_arc,
slot,
pages: pages.clone(),
title: title.clone(),
signing,
cancelled: false,
};
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return;
}
pages = event.pages;
title = event.title;
}
if let Some(title) = title {
let mut written_book = ItemStack::new(1, &Item::WRITTEN_BOOK); let mut written_book = ItemStack::new(1, &Item::WRITTEN_BOOK);
let content = WrittenBookContentImpl { let content = WrittenBookContentImpl {
title: title.to_string(), title,
author: player.gameprofile.name.clone(), author: player.gameprofile.name.clone(),
pages, pages,
}; };

View File

@ -160,9 +160,20 @@ impl JavaClient {
self.update_sequence(player_action.sequence.0); self.update_sequence(player_action.sequence.0);
return; return;
} }
player.mining.store(false, Ordering::Relaxed);
let entity = &player.get_entity(); let entity = &player.get_entity();
entity.world.load().set_block_breaking( let world = entity.world.load_full();
if let Some(server_arc) = world.server.upgrade() {
let mut abort_event = crate::plugin::api::events::block::block_damage_abort::BlockDamageAbortEvent::new(
player.clone(),
player_action.position,
world.clone(),
player.inventory().held_item(),
);
server_arc.plugin_manager.fire_blocking(&server_arc, &mut abort_event);
}
player.mining.store(false, Ordering::Relaxed);
world.set_block_breaking(
entity, entity,
player_action.position, player_action.position,
BlockBreakingProgress::Stop, BlockBreakingProgress::Stop,

View File

@ -79,22 +79,7 @@ impl HostEntity for PluginHostState {
) -> wasmtime::Result<entity_types::EntityType> { ) -> wasmtime::Result<entity_types::EntityType> {
let entity = entity_from_resource(self, &entity)?; let entity = entity_from_resource(self, &entity)?;
let original_name = entity.get_entity().entity_type.resource_name; let original_name = entity.get_entity().entity_type.resource_name;
to_wit_entity_type(original_name)
let mut names: Vec<String> = serde_json::from_str::<
std::collections::BTreeMap<String, serde_json::Value>,
>(&std::fs::read_to_string("assets/entities.json")?)?
.keys()
.cloned()
.collect();
names.sort();
let index = names
.iter()
.position(|n| n == original_name)
.ok_or_else(|| wasmtime::Error::msg(format!("Unknown entity type: {original_name}")))?;
// SAFETY: The WIT enum is generated from the sorted keys of assets/entities.json.
Ok(unsafe { std::mem::transmute::<u8, entity_types::EntityType>(index as u8) })
} }
async fn get_position(&mut self, entity: Resource<Entity>) -> wasmtime::Result<Position> { async fn get_position(&mut self, entity: Resource<Entity>) -> wasmtime::Result<Position> {
@ -925,3 +910,24 @@ impl HostEntity for PluginHostState {
Ok(()) Ok(())
} }
} }
pub(crate) fn to_wit_entity_type(name: &str) -> wasmtime::Result<entity_types::EntityType> {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
let index = pumpkin_data::entity::EntityType::ALL
.binary_search_by_key(&name, |e| e.resource_name)
.map_err(|_| wasmtime::Error::msg(format!("Unknown entity type: {name}")))?;
// SAFETY: The WIT enum is generated with variants matching EntityType::ALL in alphabetical order.
Ok(unsafe { std::mem::transmute::<u8, entity_types::EntityType>(index as u8) })
}
pub(crate) fn from_wit_entity_type(
entity_type: entity_types::EntityType,
) -> wasmtime::Result<&'static pumpkin_data::entity::EntityType> {
let index = entity_type as usize;
pumpkin_data::entity::EntityType::ALL
.get(index)
.copied()
.ok_or_else(|| wasmtime::Error::msg(format!("Invalid entity type index: {index}")))
}

View File

@ -281,21 +281,7 @@ impl PluginHostState {
fn get_wit_biome( fn get_wit_biome(
biome: &pumpkin_data::biome::Biome, biome: &pumpkin_data::biome::Biome,
) -> wasmtime::Result<pumpkin::plugin::biomes::Biome> { ) -> wasmtime::Result<pumpkin::plugin::biomes::Biome> {
let mut names: Vec<String> = serde_json::from_str::< to_wit_biome(biome)
std::collections::BTreeMap<String, serde_json::Value>,
>(&std::fs::read_to_string("assets/biome.json")?)?
.keys()
.cloned()
.collect();
names.sort();
let index = names
.iter()
.position(|n| n.strip_prefix("minecraft:").unwrap_or(n) == biome.registry_id)
.ok_or_else(|| wasmtime::Error::msg(format!("Unknown biome: {}", biome.registry_id)))?;
// SAFETY: The WIT enum is generated from the sorted keys of assets/biome.json.
Ok(unsafe { std::mem::transmute::<u8, pumpkin::plugin::biomes::Biome>(index as u8) })
} }
fn get_wit_block_entity( fn get_wit_block_entity(
@ -1038,20 +1024,8 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
let world_ref = self.get_world_res(&world)?; let world_ref = self.get_world_res(&world)?;
let world_provider = world_ref.provider.clone(); let world_provider = world_ref.provider.clone();
let mut names: Vec<String> = serde_json::from_str::< let internal_type =
std::collections::BTreeMap<String, serde_json::Value>, super::entity::from_wit_entity_type(entity_type)?;
>(&std::fs::read_to_string("assets/entities.json")?)?
.keys()
.cloned()
.collect();
names.sort();
let type_name = names.get(entity_type as usize).ok_or_else(|| {
wasmtime::Error::msg(format!("Invalid entity type index: {}", entity_type as u8))
})?;
let internal_type = pumpkin_data::entity::EntityType::from_name(type_name)
.ok_or_else(|| wasmtime::Error::msg(format!("Invalid entity type: {type_name}")))?;
let internal_pos = pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2); let internal_pos = pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2);
let entity = crate::entity::r#type::from_type( let entity = crate::entity::r#type::from_type(
@ -2340,9 +2314,24 @@ pub const fn from_wit_sound_category(
} }
} }
pub(crate) fn to_wit_biome(
biome: &pumpkin_data::biome::Biome,
) -> wasmtime::Result<pumpkin::plugin::biomes::Biome> {
let name = biome
.registry_id
.strip_prefix("minecraft:")
.unwrap_or(biome.registry_id);
let index = pumpkin_data::biome::Biome::ALL
.binary_search_by_key(&name, |b| b.registry_id)
.map_err(|_| wasmtime::Error::msg(format!("Unknown biome: {}", biome.registry_id)))?;
// SAFETY: The WIT enum is generated with variants matching Biome::ALL in alphabetical order.
Ok(unsafe { std::mem::transmute::<u8, pumpkin::plugin::biomes::Biome>(index as u8) })
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::pumpkin; use super::*;
#[test] #[test]
fn wit_particle_ids_match_internal_particle_ids() { fn wit_particle_ids_match_internal_particle_ids() {