From f8d1627697f039abc75bbfaede827b4e50739713 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 3 Sep 2026 19:11:20 +0200 Subject: [PATCH] fix: many non event firing --- .../java/client/play/spawn_living_entity.rs | 21 +- crates/pumpkin/src/block/blocks/farmland.rs | 30 ++- crates/pumpkin/src/block/blocks/fire/fire.rs | 27 ++- crates/pumpkin/src/block/blocks/leaves.rs | 10 + .../src/block/blocks/plant/crop/mod.rs | 18 +- .../blocks/plant/crop/sweet_berry_bush.rs | 22 ++- .../src/block/blocks/redstone/crafter.rs | 12 ++ .../src/block/entities/brewing_stand.rs | 57 ++++-- crates/pumpkin/src/block/entities/campfire.rs | 32 ++- .../entities/furnace_like_block_entity.rs | 85 +++++--- crates/pumpkin/src/block/registry.rs | 12 ++ .../pumpkin/src/entity/ai/goal/break_door.rs | 3 + crates/pumpkin/src/entity/breath.rs | 27 ++- crates/pumpkin/src/entity/combat.rs | 16 +- crates/pumpkin/src/entity/living.rs | 186 ++++++++++++++++-- crates/pumpkin/src/entity/mod.rs | 54 ++++- crates/pumpkin/src/entity/passive/axolotl.rs | 14 +- crates/pumpkin/src/entity/passive/tadpole.rs | 14 +- .../src/entity/passive/villager/mod.rs | 3 + crates/pumpkin/src/entity/player.rs | 143 ++++++++++++-- crates/pumpkin/src/entity/projectile/arrow.rs | 13 ++ .../src/entity/projectile/firework_rocket.rs | 40 +++- .../src/entity/projectile/lingering_potion.rs | 12 ++ .../src/entity/projectile/splash_potion.rs | 29 ++- crates/pumpkin/src/item/items/brush.rs | 15 ++ crates/pumpkin/src/item/items/shears.rs | 43 +++- crates/pumpkin/src/item/items/trident.rs | 15 ++ crates/pumpkin/src/net/chunk_sender.rs | 6 +- .../src/net/java/login/encryption_response.rs | 17 +- .../pumpkin/src/net/java/login/login_start.rs | 2 +- .../src/net/java/login/plugin_response.rs | 2 +- crates/pumpkin/src/net/java/mod.rs | 35 +++- crates/pumpkin/src/net/java/pending.rs | 2 +- crates/pumpkin/src/net/java/play/edit_book.rs | 28 ++- .../src/net/java/play/player_action.rs | 15 +- .../loader/wasm/wasm_host/wit/v0_1/entity.rs | 38 ++-- .../loader/wasm/wasm_host/wit/v0_1/world.rs | 49 ++--- 37 files changed, 952 insertions(+), 195 deletions(-) diff --git a/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs b/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs index b8c743bc2..62763d92a 100644 --- a/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs +++ b/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs @@ -359,19 +359,24 @@ impl ClientPacket for CSpawnLivingEntity { self.velocity.write_legacy(&mut write)?; if !v1_15 { - if let Some(metadata) = &self.metadata { + if v1_9 { + if let Some(metadata) = &self.metadata { + write.write_slice(metadata)?; + } else { + write.write_u8(0xFF)?; + } + } else if let Some(metadata) = &self.metadata + && metadata.last().copied() == Some(127) + { write.write_slice(metadata)?; - } else if v1_9 { - write.write_u8(0xFF)?; } else { - // In <= 1.8 (specifically 1.7.10), DataWatcher requires at least one entry - // so that `readWatchedObjectsFromPacketBuffer` returns a non-null List, - // avoiding a NullPointerException in `S0FPacketSpawnMob.func_149027_c()`. - // (0 << 5) | 0 = 0 (type: byte, index: 0 flags), 0 (value), 127 (terminator) - write.write_slice(&[0x00, 0x00, 127])?; + // In <= 1.8, write the 127 (0x7F) terminator byte for the DataWatcher list, + // matching vanilla and PacketEvents. + write.write_u8(127)?; } } Ok(()) } } + diff --git a/crates/pumpkin/src/block/blocks/farmland.rs b/crates/pumpkin/src/block/blocks/farmland.rs index 8e888dcfa..c271327c2 100644 --- a/crates/pumpkin/src/block/blocks/farmland.rs +++ b/crates/pumpkin/src/block/blocks/farmland.rs @@ -57,7 +57,20 @@ impl BlockBehaviour for FarmlandBlock { // TODO: add rain check. Remember to check which one is most optimized. if is_water_nearby(args.world, args.position) { 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.position, props.to_state_id(args.block), @@ -80,7 +93,20 @@ impl BlockBehaviour for FarmlandBlock { ); } } 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.position, props.to_state_id(args.block), diff --git a/crates/pumpkin/src/block/blocks/fire/fire.rs b/crates/pumpkin/src/block/blocks/fire/fire.rs index f5bf705f0..6baf74ad2 100644 --- a/crates/pumpkin/src/block/blocks/fire/fire.rs +++ b/crates/pumpkin/src/block/blocks/fire/fire.rs @@ -144,6 +144,17 @@ impl FireBlock { let block = world.get_block(pos); let odds = Self::get_burn_odds(block); 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; if rand::rng().random_range(0..(age + 10) as i32) < 5 && !Self::is_near_rain(world.as_ref(), pos) @@ -416,10 +427,24 @@ impl BlockBehaviour for FireBlock { let mut new_fire_props = FireProperties::from_state_id(fire_state_id); 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( &offset_pos, - new_fire_props.to_state_id(&Block::FIRE), + new_state_id, BlockFlags::NOTIFY_NEIGHBORS, ); } diff --git a/crates/pumpkin/src/block/blocks/leaves.rs b/crates/pumpkin/src/block/blocks/leaves.rs index 1f20db3a1..13e463778 100644 --- a/crates/pumpkin/src/block/blocks/leaves.rs +++ b/crates/pumpkin/src/block/blocks/leaves.rs @@ -101,6 +101,16 @@ impl BlockBehaviour for LeavesBlock { let state_id = args.world.get_block_state_id(args.position); let props = OakLeavesLikeProperties::from_state_id(state_id); 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 .break_block(args.position, None, BlockFlags::empty()); } diff --git a/crates/pumpkin/src/block/blocks/plant/crop/mod.rs b/crates/pumpkin/src/block/blocks/plant/crop/mod.rs index 02676538f..2155c846b 100644 --- a/crates/pumpkin/src/block/blocks/plant/crop/mod.rs +++ b/crates/pumpkin/src/block/blocks/plant/crop/mod.rs @@ -73,7 +73,23 @@ trait CropBlockBase: PlantBlockBase { let f = get_available_moisture(world, pos, block); 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); - world.set_block_state(pos, new_state_id, BlockFlags::NOTIFY_LISTENERS); + 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); + } } } } diff --git a/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs b/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs index 834bce03a..cf4841b42 100644 --- a/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs +++ b/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs @@ -37,12 +37,28 @@ impl BlockBehaviour for SweetBerryBushBlock { match props.age { 2 | 3 => { let index = props.age; - props.age = 1; 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.position, - ItemStack::new(1, &Item::SWEET_BERRIES), // + stack, ); } args.world.set_block_state( diff --git a/crates/pumpkin/src/block/blocks/redstone/crafter.rs b/crates/pumpkin/src/block/blocks/redstone/crafter.rs index ac99442e1..4a8f68efc 100644 --- a/crates/pumpkin/src/block/blocks/redstone/crafter.rs +++ b/crates/pumpkin/src/block/blocks/redstone/crafter.rs @@ -133,6 +133,18 @@ impl CrafterBlock { world.set_block_state(pos, props.to_state_id(block), BlockFlags::NOTIFY_LISTENERS); 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); for i in 0..CrafterBlockEntity::INVENTORY_SIZE { diff --git a/crates/pumpkin/src/block/entities/brewing_stand.rs b/crates/pumpkin/src/block/entities/brewing_stand.rs index e5c38144f..a28832d31 100644 --- a/crates/pumpkin/src/block/entities/brewing_stand.rs +++ b/crates/pumpkin/src/block/entities/brewing_stand.rs @@ -106,6 +106,20 @@ impl BrewingStandBlockEntity { 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 let mut ingredient_used = false; { @@ -171,17 +185,6 @@ impl BrewingStandBlockEntity { 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 if let Ok(items) = self.items.read() { let remaining = &items[3]; @@ -441,6 +444,7 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity { self } + #[allow(clippy::too_many_lines)] fn tick(&self, world: &Arc) { // Refill fuel counter from fuel item if needed let fuel_refilled = self.fuel.load(Ordering::Relaxed) <= 0 @@ -458,10 +462,18 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity { server .plugin_manager .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); + items[4].decrement(1); + true } - self.fuel.store(20, Ordering::Relaxed); - items[4].decrement(1); - true } else { false }; @@ -493,9 +505,24 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity { self.mark_dirty(); } } 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 self.fuel.fetch_sub(1, Ordering::Relaxed); - self.brew_time.store(400, Ordering::Relaxed); + self.brew_time.store(brew_time, Ordering::Relaxed); *self .ingredient_item .lock() diff --git a/crates/pumpkin/src/block/entities/campfire.rs b/crates/pumpkin/src/block/entities/campfire.rs index 1e76636b9..480976ad4 100644 --- a/crates/pumpkin/src/block/entities/campfire.rs +++ b/crates/pumpkin/src/block/entities/campfire.rs @@ -108,7 +108,22 @@ impl BlockEntity for CampfireBlockEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); 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 }; @@ -130,7 +145,20 @@ impl BlockEntity for CampfireBlockEntity { 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.items[slot] diff --git a/crates/pumpkin/src/block/entities/furnace_like_block_entity.rs b/crates/pumpkin/src/block/entities/furnace_like_block_entity.rs index 821a17e3d..495bcbb93 100644 --- a/crates/pumpkin/src/block/entities/furnace_like_block_entity.rs +++ b/crates/pumpkin/src/block/entities/furnace_like_block_entity.rs @@ -398,6 +398,8 @@ macro_rules! impl_block_entity_for_cooking { base_fuel_ticks }; + let mut burn_ticks = adjusted_fuel_ticks; + let mut burn_cancelled = false; if let Some(server) = world.server.upgrade() { let mut burn_event = $crate::plugin::api::events::inventory::furnace_burn::FurnaceBurnEvent::new( self.position, @@ -405,24 +407,31 @@ macro_rules! impl_block_entity_for_cooking { adjusted_fuel_ticks as u32, ); 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() { - is_dirty = true; - if let Ok(mut items_guard) = self.items.try_write() { - if !items_guard[1].is_empty() { - items_guard[1].decrement(1); - if let Some(remainder_id) = - pumpkin_data::recipe_remainder::get_recipe_remainder_id( - items_guard[1].item.id, - ) - && items_guard[1].is_empty() - && let Some(remainder_item) = - pumpkin_data::item::Item::from_id(remainder_id) - { - items_guard[1] = ItemStack::new(1, remainder_item); + if self.is_burning() { + is_dirty = true; + if let Ok(mut items_guard) = self.items.try_write() { + if !items_guard[1].is_empty() { + items_guard[1].decrement(1); + if let Some(remainder_id) = + pumpkin_data::recipe_remainder::get_recipe_remainder_id( + items_guard[1].item.id, + ) + && items_guard[1].is_empty() + && let Some(remainder_item) = + pumpkin_data::item::Item::from_id(remainder_id) + { + items_guard[1] = ItemStack::new(1, remainder_item); + } } } } @@ -430,6 +439,7 @@ macro_rules! impl_block_entity_for_cooking { } if self.is_burning() && can_accept_output { + let mut start_cancelled = false; if self.get_cooking_time_spent() == 0 { if let Some(server) = world.server.upgrade() { let mut start_event = $crate::plugin::api::events::inventory::furnace_start_smelt::FurnaceStartSmeltEvent::new( @@ -438,26 +448,39 @@ macro_rules! impl_block_entity_for_cooking { self.get_cooking_total_time() as u32, ); 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); + } } } - self.cooking_time_spent.fetch_add(1, Ordering::Relaxed); + if !start_cancelled { + self.cooking_time_spent.fetch_add(1, Ordering::Relaxed); - if self.get_cooking_time_spent() == self.get_cooking_total_time() { - self.set_cooking_time_spent(0); - if let Some(cooking_recipe) = furnace_recipe { - let cooking_total_time = cooking_recipe.cookingtime; - self.set_cooking_total_time(cooking_total_time as u16); + if self.get_cooking_time_spent() == self.get_cooking_total_time() { + self.set_cooking_time_spent(0); + if let Some(cooking_recipe) = furnace_recipe { + let cooking_total_time = cooking_recipe.cookingtime; + self.set_cooking_total_time(cooking_total_time as u16); - if let Some(server) = world.server.upgrade() { - let mut smelt_event = $crate::plugin::api::events::inventory::furnace_smelt::FurnaceSmeltEvent::new( - self.position, - top_item.item.registry_key.to_string(), - cooking_recipe.result.id.to_string(), - ); - server.plugin_manager.fire_blocking(&server, &mut smelt_event); + let mut smelt_cancelled = false; + if let Some(server) = world.server.upgrade() { + let mut smelt_event = $crate::plugin::api::events::inventory::furnace_smelt::FurnaceSmeltEvent::new( + self.position, + top_item.item.registry_key.to_string(), + cooking_recipe.result.id.to_string(), + ); + 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)); + is_dirty = true; + } } - self.craft_recipe(Some(cooking_recipe)); - is_dirty = true; } } } else { diff --git a/crates/pumpkin/src/block/registry.rs b/crates/pumpkin/src/block/registry.rs index 6f95ad39d..16c2ad667 100644 --- a/crates/pumpkin/src/block/registry.rs +++ b/crates/pumpkin/src/block/registry.rs @@ -526,6 +526,18 @@ impl BlockRegistry { return false; } 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); } true diff --git a/crates/pumpkin/src/entity/ai/goal/break_door.rs b/crates/pumpkin/src/entity/ai/goal/break_door.rs index a49793fae..e118d506c 100644 --- a/crates/pumpkin/src/entity/ai/goal/break_door.rs +++ b/crates/pumpkin/src/entity/ai/goal/break_door.rs @@ -155,6 +155,9 @@ impl Goal for BreakDoorGoal { ); if let Some(server) = world.server.upgrade() { server.plugin_manager.fire_blocking(&server, &mut event); + if event.cancelled { + return; + } } world.set_block_state(&door_pos, BlockStateId::AIR, BlockFlags::NOTIFY_ALL); world.sync_world_event( diff --git a/crates/pumpkin/src/entity/breath.rs b/crates/pumpkin/src/entity/breath.rs index 84eb19b55..60d6298a2 100644 --- a/crates/pumpkin/src/entity/breath.rs +++ b/crates/pumpkin/src/entity/breath.rs @@ -58,14 +58,11 @@ impl BreathManager { } let in_water = Self::is_eye_in_water(player); + let prev = self.air_supply.load(Ordering::Relaxed); if in_water { - let prev = self - .air_supply - .fetch_sub(AIR_DEPLETION_RATE, Ordering::Relaxed); - let new_air = (prev - AIR_DEPLETION_RATE).max(0); + let mut new_air = (prev - AIR_DEPLETION_RATE).max(0); if new_air != prev { - self.air_supply.store(new_air, Ordering::Relaxed); let server = player.world().server.upgrade(); if let Some(server) = server { let mut event = crate::plugin::api::events::entity::entity_air_change::EntityAirChangeEvent::new( @@ -73,7 +70,12 @@ impl BreathManager { 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.send_air_supply(player); } @@ -88,9 +90,20 @@ impl BreathManager { } } } else { - let prev = self.air_supply.load(Ordering::Relaxed); - let new_air = (prev + AIR_RECOVERY_RATE).min(MAX_AIR); + let mut new_air = (prev + AIR_RECOVERY_RATE).min(MAX_AIR); 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.send_air_supply(player); } diff --git a/crates/pumpkin/src/entity/combat.rs b/crates/pumpkin/src/entity/combat.rs index ffeb8cf18..9581e139f 100644 --- a/crates/pumpkin/src/entity/combat.rs +++ b/crates/pumpkin/src/entity/combat.rs @@ -407,6 +407,7 @@ impl CombatTracker { pub fn get_death_message( &self, victim_name: pumpkin_util::text::TextComponent, + kill_credit_name: Option, ) -> pumpkin_util::text::TextComponent { if self.entries.is_empty() { return pumpkin_util::text::TextComponent::translate_cross( @@ -456,7 +457,7 @@ impl CombatTracker { ) } 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, victim_name: pumpkin_util::text::TextComponent, killing_blow: &CombatEntry, + kill_credit_name: Option, ) -> pumpkin_util::text::TextComponent { let damage_type = killing_blow.damage_type; let msg_id = damage_type.message_id; @@ -598,18 +600,18 @@ impl CombatTracker { ) } else { pumpkin_util::text::TextComponent::translate_cross( - format!("death.attack.{msg_id}.player"), - format!("death.attack.{msg_id}.player"), + format!("death.attack.{msg_id}"), + format!("death.attack.{msg_id}"), [victim_name, attacker_name.clone()], ) } - } else if let Some(killer) = self.get_killer_entry() - && let Some(killer_name) = &killer.attacker_name - { + } else if let Some(killer_name) = kill_credit_name.or_else(|| { + self.get_killer_entry().and_then(|k| k.attacker_name.clone()) + }) { pumpkin_util::text::TextComponent::translate_cross( format!("death.attack.{msg_id}.player"), format!("death.attack.{msg_id}.player"), - [victim_name, killer_name.clone()], + [victim_name, killer_name], ) } else { pumpkin_util::text::TextComponent::translate_cross( diff --git a/crates/pumpkin/src/entity/living.rs b/crates/pumpkin/src/entity/living.rs index 87433e1ae..01e27da25 100644 --- a/crates/pumpkin/src/entity/living.rs +++ b/crates/pumpkin/src/entity/living.rs @@ -14,10 +14,9 @@ use pumpkin_util::Hand; use pumpkin_util::math::position::BlockPos; use rustc_hash::FxHashMap; use std::sync::Arc; -use std::sync::atomic::AtomicI32; use std::sync::atomic::Ordering; use std::sync::atomic::{ - AtomicBool, AtomicU8, + AtomicBool, AtomicI32, AtomicI64, AtomicU8, Ordering::{Relaxed, SeqCst}, }; use tracing::warn; @@ -119,6 +118,15 @@ pub struct LivingEntity { /// Tracks combat entries, assisted falls, kill credit, and death messages. pub combat_tracker: std::sync::Mutex, + /// 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, livings_flags: AtomicU8, @@ -231,12 +239,56 @@ impl LivingEntity { last_attacking_id: AtomicI32::new(0), last_attack_time: AtomicI32::new(0), 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()), water_movement_speed_multiplier, 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> { + 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. pub fn on_changed_block(&self, caller: &dyn EntityBase, _pos: BlockPos) { 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( dyn_self: &dyn EntityBase, damage_type: DamageType, source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) -> 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() { let tracker = living .combat_tracker .lock() .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 && source.is_some() { TextComponent::translate_cross( - format!("death.attack.{}.player", damage_type.message_id), - format!("death.attack.{}.player", damage_type.message_id), + format!("death.attack.{}", damage_type.message_id), + format!("death.attack.{}", damage_type.message_id), [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 { TextComponent::translate_cross( format!("death.attack.{}", damage_type.message_id), @@ -1656,7 +1720,10 @@ impl LivingEntity { self.movement_input.store(Vector3::default()); 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 world.send_entity_status(&self.entity, EntityStatus::Death, Some(ActorEventID::Death)); @@ -1686,7 +1753,7 @@ impl LivingEntity { let is_thundering = world.is_thundering(); 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 .combat_tracker .lock() @@ -1697,7 +1764,7 @@ impl LivingEntity { let params = LootContextParameters { killed_by_player: Some(has_player_kill), 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), position: Some(self.entity.pos.load()), world_time: world.level_info.load().day_time as u64, @@ -1721,7 +1788,7 @@ impl LivingEntity { if params.killed_by_player.unwrap_or(false) && 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 { ExperienceOrbEntity::spawn(&world, self.entity.pos.load(), amount); } @@ -1813,14 +1880,42 @@ impl LivingEntity { ) { let world = self.entity.world.load(); let show_death_messages = { world.level_info.load().game_rules.show_death_messages }; - if self.entity.entity_type == &EntityType::PLAYER && show_death_messages { - //TODO: KillCredit + if self.entity.entity_type == &EntityType::PLAYER { 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() { - 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; + 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 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) { + let attacker_id = attacker.get_entity().entity_id; self.last_attacker_id - .store(attacker.get_entity().entity_id, Relaxed); + .store(attacker_id, Relaxed); self.last_attacked_time .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); + } } } diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 9646180eb..1bdfabdc3 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -305,6 +305,14 @@ pub trait EntityBase: Send + Sync + std::any::Any { } fn java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option> { + 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( || { 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) { 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); if let Ok(meta_data) = client.serialize_packet(&meta_packet) { client.try_enqueue_packet(meta_data); @@ -3356,6 +3366,23 @@ impl Entity { pub fn leash_to(&self, holder: Arc) { 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 .leashed_to .lock() @@ -3387,6 +3414,18 @@ impl Entity { } 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 .leashed_to .lock() @@ -3396,6 +3435,19 @@ impl Entity { 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 = pumpkin_protocol::java::client::play::CSetEntityLink::new(self.entity_id, -1, true); let be_packet = pumpkin_protocol::bedrock::client::CSetActorLink { diff --git a/crates/pumpkin/src/entity/passive/axolotl.rs b/crates/pumpkin/src/entity/passive/axolotl.rs index 97e15ed99..80d344355 100644 --- a/crates/pumpkin/src/entity/passive/axolotl.rs +++ b/crates/pumpkin/src/entity/passive/axolotl.rs @@ -296,9 +296,21 @@ impl Mob for AxolotlEntity { let item = item_stack.get_item(); if item == &Item::WATER_BUCKET { - item_stack.decrement_unless_creative(player.gamemode.load(), 1); let entity = self.get_entity(); 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(); world.play_sound(Sound::ItemBucketFillAxolotl, SoundCategory::Neutral, &pos); entity.remove(); diff --git a/crates/pumpkin/src/entity/passive/tadpole.rs b/crates/pumpkin/src/entity/passive/tadpole.rs index a42980470..9fe675381 100644 --- a/crates/pumpkin/src/entity/passive/tadpole.rs +++ b/crates/pumpkin/src/entity/passive/tadpole.rs @@ -127,9 +127,21 @@ impl Mob for TadpoleEntity { } if item_stack.get_item() == &Item::WATER_BUCKET { - item_stack.decrement_unless_creative(player.gamemode.load(), 1); let entity = self.get_entity(); 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(); world.play_sound(Sound::ItemBucketFillTadpole, SoundCategory::Neutral, &pos); entity.remove(); diff --git a/crates/pumpkin/src/entity/passive/villager/mod.rs b/crates/pumpkin/src/entity/passive/villager/mod.rs index 6fb671d12..bf6bd17bd 100644 --- a/crates/pumpkin/src/entity/passive/villager/mod.rs +++ b/crates/pumpkin/src/entity/passive/villager/mod.rs @@ -2141,6 +2141,9 @@ impl Mob for VillagerEntity { } fn mob_java_spawn_metadata(&self, version: JavaMinecraftVersion) -> Option> { + if version < JavaMinecraftVersion::V_1_9 { + return None; + } let mut metadata = Vec::new(); Metadata::new( tracked_data::villager::VILLAGER_DATA, diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 6707168b9..7f71e563f 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -1603,19 +1603,38 @@ impl Player { 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 { "minecraft:the_nether" => 1, "minecraft:the_end" => 2, _ => 0, }; 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 { spawn_position_type: pumpkin_protocol::bedrock::client::SpawnPositionType::PlayerRespawn, - block_position: block_pos, + block_position: final_block_pos, dimension_type: bedrock_dimension.into(), - spawn_block_pos: block_pos, + spawn_block_pos: final_block_pos, }, ); @@ -1624,7 +1643,7 @@ impl Player { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(RespawnPoint { dimension, - position: block_pos, + position: final_block_pos, yaw, force: forced, }); @@ -2293,11 +2312,39 @@ impl Player { } 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); self.try_send_client_packet(&packet); } - pub fn set_velocity(&self, velocity: Vector3) { + pub fn set_velocity(&self, mut velocity: Vector3) { + 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.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). 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); self.try_send_client_packet(&packet); } @@ -3029,8 +3089,25 @@ impl Player { } 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() { - stats.increment(category, stat, amount); + stats.increment(category, stat, final_amount); } } @@ -3729,6 +3806,14 @@ impl Player { } 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)] - async fn handle_killed(&self, death_msg: TextComponent) { + pub fn send_combat_death(&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( crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilled, ); @@ -4402,17 +4498,7 @@ impl Player { if matches!(self.client.as_ref(), ClientPlatform::Java(_)) { self.set_client_loaded(false); } - self.client - .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_combat_death(death_msg); self.send_health(); self.send_bedrock_respawn_state(RespawnState::SearchingForSpawn); } @@ -5137,10 +5223,27 @@ impl Player { 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(); self.inventory.set_slot(slot_index, updated_stack.clone()); - let xp_used = (repaired + 1) / 2; xp = xp.saturating_sub(xp_used); self.try_send_slot_set_packet(&CSetPlayerInventory::new( diff --git a/crates/pumpkin/src/entity/projectile/arrow.rs b/crates/pumpkin/src/entity/projectile/arrow.rs index b043c002b..fc4e8be41 100644 --- a/crates/pumpkin/src/entity/projectile/arrow.rs +++ b/crates/pumpkin/src/entity/projectile/arrow.rs @@ -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 let item_stack = self .item_stack diff --git a/crates/pumpkin/src/entity/projectile/firework_rocket.rs b/crates/pumpkin/src/entity/projectile/firework_rocket.rs index 807a66801..b22b8f119 100644 --- a/crates/pumpkin/src/entity/projectile/firework_rocket.rs +++ b/crates/pumpkin/src/entity/projectile/firework_rocket.rs @@ -70,6 +70,16 @@ impl FireworkRocketEntity { pub fn explode_and_remove(&self, world: &World) { 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( entity, EntityStatus::FireworksExplode, @@ -97,16 +107,32 @@ impl EntityBase for FireworkRocketEntity { let shooter = shooter.get_entity(); if shooter.is_fall_flying() { - let rotation = shooter.rotation().to_f64(); - let shooter_vel = shooter.velocity.load(); + 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 shooter_vel = shooter.velocity.load(); - let new_shooter_vel = - shooter_vel + (rotation * 0.1 + (rotation * 1.5 - shooter_vel) * 0.5); + let new_shooter_vel = + shooter_vel + (rotation * 0.1 + (rotation * 1.5 - shooter_vel) * 0.5); - shooter.set_velocity(new_shooter_vel); + shooter.set_velocity(new_shooter_vel); - entity.set_pos(shooter.pos.load()); - entity.set_velocity(new_shooter_vel); + entity.set_pos(shooter.pos.load()); + entity.set_velocity(new_shooter_vel); + } } } } else { diff --git a/crates/pumpkin/src/entity/projectile/lingering_potion.rs b/crates/pumpkin/src/entity/projectile/lingering_potion.rs index d09b4c66d..b0295a0ff 100644 --- a/crates/pumpkin/src/entity/projectile/lingering_potion.rs +++ b/crates/pumpkin/src/entity/projectile/lingering_potion.rs @@ -182,6 +182,18 @@ impl EntityBase for LingeringPotionEntity { // Spawn and configure an `AreaEffectCloud` entity 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( Uuid::new_v4(), world.clone(), diff --git a/crates/pumpkin/src/entity/projectile/splash_potion.rs b/crates/pumpkin/src/entity/projectile/splash_potion.rs index 17c2eb5e0..f0cd0d627 100644 --- a/crates/pumpkin/src/entity/projectile/splash_potion.rs +++ b/crates/pumpkin/src/entity/projectile/splash_potion.rs @@ -132,6 +132,7 @@ impl EntityBase for SplashPotionEntity { self } + #[allow(clippy::too_many_lines)] fn on_hit(&self, hit: crate::entity::projectile::ProjectileHit) { let world = self.get_entity().world.load(); let hit_pos = hit.hit_pos(); @@ -228,20 +229,38 @@ impl EntityBase for SplashPotionEntity { candidates.push(p.clone() as Arc); } + let mut affected: Vec<(Arc, f32)> = Vec::new(); 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 dx = pos.x - hit_pos.x; let dy = pos.y - hit_pos.y; let dz = pos.z - hit_pos.z; let dist = (dx * dx + dy * dy + dz * dz).sqrt(); - if dist > radius { - continue; + if dist <= radius { + let scale = (1.0f32 - (dist as f32 / radius as f32)).max(0.0); + affected.push((cand, scale)); } + } + } - // Distance scaling - let scale = (1.0f32 - (dist as f32 / radius as f32)).max(0.0); + let affected_ids: Vec = affected.iter().map(|(c, _)| c.get_entity().entity_id).collect(); + 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( living, effects.clone(), diff --git a/crates/pumpkin/src/item/items/brush.rs b/crates/pumpkin/src/item/items/brush.rs index 048409b89..d78025365 100644 --- a/crates/pumpkin/src/item/items/brush.rs +++ b/crates/pumpkin/src/item/items/brush.rs @@ -117,6 +117,21 @@ impl ItemBehaviour for BrushItem { ); 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_stage = get_dusted_stage(block, current_state_id); diff --git a/crates/pumpkin/src/item/items/shears.rs b/crates/pumpkin/src/item/items/shears.rs index f0579c8f4..fbcfcc14e 100644 --- a/crates/pumpkin/src/item/items/shears.rs +++ b/crates/pumpkin/src/item/items/shears.rs @@ -76,6 +76,20 @@ impl ItemBehaviour for ShearsItem { .downcast_ref::() && !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); let world = player.world(); let pos = sheep.mob_entity.living_entity.entity.pos.load(); @@ -176,6 +190,23 @@ fn handle_beehive( }); 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.play_sound( Sound::BlockBeehiveShear, @@ -188,11 +219,13 @@ fn handle_beehive( f64::from(location.0.y) + 0.5, f64::from(location.0.z) + 0.5, ); - let item_entity = Arc::new(ItemEntity::new( - Entity::new(world.clone(), drop_pos, &EntityType::ITEM), - ItemStack::new(3, &Item::HONEYCOMB), - )); - world.spawn_entity(item_entity); + for item in drops { + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), drop_pos, &EntityType::ITEM), + item, + )); + world.spawn_entity(item_entity); + } player.damage_held_item(1); return true; } diff --git a/crates/pumpkin/src/item/items/trident.rs b/crates/pumpkin/src/item/items/trident.rs index ba6331be1..ebcf5d550 100644 --- a/crates/pumpkin/src/item/items/trident.rs +++ b/crates/pumpkin/src/item/items/trident.rs @@ -65,6 +65,21 @@ impl ItemBehaviour for TridentItem { 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 (yaw, pitch) = player.rotation(); let f_yaw = f32::to_radians(yaw); diff --git a/crates/pumpkin/src/net/chunk_sender.rs b/crates/pumpkin/src/net/chunk_sender.rs index 8481f97a7..0f023f00f 100644 --- a/crates/pumpkin/src/net/chunk_sender.rs +++ b/crates/pumpkin/src/net/chunk_sender.rs @@ -160,7 +160,9 @@ impl ChunkSender { epoch: u32, version: JavaMinecraftVersion, ) -> Option { - 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; } @@ -294,9 +296,9 @@ impl ChunkSender { && let ClientPlatform::Java(java_client) = client { 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; } diff --git a/crates/pumpkin/src/net/java/login/encryption_response.rs b/crates/pumpkin/src/net/java/login/encryption_response.rs index b87e30ab1..d7525ec72 100644 --- a/crates/pumpkin/src/net/java/login/encryption_response.rs +++ b/crates/pumpkin/src/net/java/login/encryption_response.rs @@ -29,7 +29,7 @@ impl PendingConnection { pub async fn handle_encryption_response( &mut self, - server: &Server, + server: &Arc, encryption_response: SEncryptionResponse, ) -> Option { debug!("Handling encryption"); @@ -146,9 +146,22 @@ impl PendingConnection { pub(super) async fn finish_login( &mut self, - server: &Server, + server: &Arc, profile: &GameProfile, ) -> Option { + 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 packet = CLoginSuccess::new( &profile.id, diff --git a/crates/pumpkin/src/net/java/login/login_start.rs b/crates/pumpkin/src/net/java/login/login_start.rs index 0978500c5..a500c9aa2 100644 --- a/crates/pumpkin/src/net/java/login/login_start.rs +++ b/crates/pumpkin/src/net/java/login/login_start.rs @@ -4,7 +4,7 @@ use super::*; impl PendingConnection { pub async fn handle_login_start( &mut self, - server: &Server, + server: &Arc, login_start: SLoginStart, ) -> Option { debug!("login start"); diff --git a/crates/pumpkin/src/net/java/login/plugin_response.rs b/crates/pumpkin/src/net/java/login/plugin_response.rs index f178d3ce5..a5e94afe5 100644 --- a/crates/pumpkin/src/net/java/login/plugin_response.rs +++ b/crates/pumpkin/src/net/java/login/plugin_response.rs @@ -4,7 +4,7 @@ use super::*; impl PendingConnection { pub async fn handle_plugin_response( &mut self, - server: &Server, + server: &Arc, plugin_response: SLoginPluginResponse, ) -> Option { debug!("Handling plugin"); diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index edfda72c1..ab2db05b0 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -1148,12 +1148,45 @@ impl JavaClient { } id if id == SCustomPayload::to_id(version) => { let payload = SCustomPayload::read(&mut payload, &version)?; + let channel_str = payload.channel.to_string(); let mut event = PlayerCustomPayloadEvent::new( player.clone(), - payload.channel.to_string(), + channel_str.clone(), Bytes::copy_from_slice(payload.data), ); 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) => { self.handle_recipe_book_change_settings( diff --git a/crates/pumpkin/src/net/java/pending.rs b/crates/pumpkin/src/net/java/pending.rs index 7bb64b37b..2522573e0 100644 --- a/crates/pumpkin/src/net/java/pending.rs +++ b/crates/pumpkin/src/net/java/pending.rs @@ -313,7 +313,7 @@ impl PendingConnection { async fn handle_login_packet( &mut self, - server: &Server, + server: &Arc, packet: &RawPacket, ) -> Result, ReadingError> { debug!("Handling login group"); diff --git a/crates/pumpkin/src/net/java/play/edit_book.rs b/crates/pumpkin/src/net/java/play/edit_book.rs index b6928e358..d9f67916f 100644 --- a/crates/pumpkin/src/net/java/play/edit_book.rs +++ b/crates/pumpkin/src/net/java/play/edit_book.rs @@ -8,12 +8,34 @@ impl JavaClient { return; } - let pages: Vec = packet.pages.iter().map(|p| (*p).to_string()).collect(); + let mut pages: Vec = 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 content = WrittenBookContentImpl { - title: title.to_string(), + title, author: player.gameprofile.name.clone(), pages, }; diff --git a/crates/pumpkin/src/net/java/play/player_action.rs b/crates/pumpkin/src/net/java/play/player_action.rs index 2ee4b29fa..4e53ffa47 100644 --- a/crates/pumpkin/src/net/java/play/player_action.rs +++ b/crates/pumpkin/src/net/java/play/player_action.rs @@ -160,9 +160,20 @@ impl JavaClient { self.update_sequence(player_action.sequence.0); return; } - player.mining.store(false, Ordering::Relaxed); 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, player_action.position, BlockBreakingProgress::Stop, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs index 206ff1513..f3d79da3f 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs @@ -79,22 +79,7 @@ impl HostEntity for PluginHostState { ) -> wasmtime::Result { let entity = entity_from_resource(self, &entity)?; let original_name = entity.get_entity().entity_type.resource_name; - - let mut names: Vec = serde_json::from_str::< - std::collections::BTreeMap, - >(&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::(index as u8) }) + to_wit_entity_type(original_name) } async fn get_position(&mut self, entity: Resource) -> wasmtime::Result { @@ -925,3 +910,24 @@ impl HostEntity for PluginHostState { Ok(()) } } + +pub(crate) fn to_wit_entity_type(name: &str) -> wasmtime::Result { + 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::(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}"))) +} + diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs index b78c61c5f..02f31dca5 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs @@ -281,21 +281,7 @@ impl PluginHostState { fn get_wit_biome( biome: &pumpkin_data::biome::Biome, ) -> wasmtime::Result { - let mut names: Vec = serde_json::from_str::< - std::collections::BTreeMap, - >(&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::(index as u8) }) + to_wit_biome(biome) } 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_provider = world_ref.provider.clone(); - let mut names: Vec = serde_json::from_str::< - std::collections::BTreeMap, - >(&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_type = + super::entity::from_wit_entity_type(entity_type)?; let internal_pos = pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2); 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 { + 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::(index as u8) }) +} + #[cfg(test)] mod tests { - use super::pumpkin; + use super::*; #[test] fn wit_particle_ids_match_internal_particle_ids() {