feat(bedrock): add basic combat

This commit is contained in:
Alexander Medvedev 2026-05-25 13:52:26 +02:00
parent e1967bed2b
commit fa5e07f8da
8 changed files with 339 additions and 2 deletions

View File

@ -0,0 +1,89 @@
use crate::serial::PacketWrite;
use std::io::{Error, Write};
use pumpkin_macros::packet;
use pumpkin_util::math::vector3::Vector3;
use crate::codec::var_int::VarInt;
#[derive(Debug, PacketWrite)]
#[packet(27)]
pub struct SActorEvent {
pub entity_runtime_id: VarInt,
pub event_type: ActorEventType,
pub event_data: VarInt,
pub fire_at_position: Option<Vector3<f32>>,
}
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum ActorEventType {
Jump = 1,
Hurt = 2,
Death = 3,
StartAttacking = 4,
StopAttacking = 5,
TamingFailed = 6,
TamingSucceeded = 7,
ShakeWetness = 8,
UseItem = 9,
EatGrass = 10,
FishhookBubble = 11,
FishhookFishPosition = 12,
FishhookHookTime = 13,
FishhookTease = 14,
SquidFleeing = 15,
ZombieConverting = 16,
PlayAmbient = 17,
SpawnAlive = 18,
StartOfferFlower = 19,
StopOfferFlower = 20,
LoveHearts = 21,
VillagerAngry = 22,
VillagerHappy = 23,
WitchHatMagic = 24,
FireworksExplode = 25,
InLoveHearts = 26,
SilverfishMergeAnimation = 27,
GuardianAttackSound = 28,
DrinkPotion = 29,
ThrowPotion = 30,
CartWithPrimeTNT = 31,
PrimeCreeper = 32,
AirSupply = 33,
AddPlayerLevels = 34,
GuardianMiningFatigue = 35,
AgentSwingArm = 36,
DragonStartDeathAnim = 37,
GroundDust = 38,
Shake = 39,
Feed = 57,
BabyEat = 60,
InstantDeath = 61,
NotifyTrade = 62,
LeashDestroyed = 63,
CaravanUpdated = 64,
TalismanActivate = 65,
UpdateStructureFeature = 66,
PlayerSpawnedMob = 67,
Puke = 68,
UpdateStackSize = 69,
StartSwimming = 70,
BalloonPop = 71,
TreasureHunt = 72,
SummonAgent = 73,
FinishedChargingItem = 74,
LandedOnGround = 75,
ActorGrowUp = 76,
VibrationDetected = 77,
DrinkMilk = 78,
WetnessStop = 79,
KineticDamageDealt = 80,
HurtWithoutReceivingDamage = 81,
}
impl PacketWrite for ActorEventType {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
(*self as u8).write(writer)
}
}

View File

@ -19,6 +19,7 @@ pub struct SInteraction {
pub enum Action {
Invalid = 0,
Interact = 1,
// No longer used in newer versions
Attack = 2,
StopRiding = 3,
InteractUpdate = 4,

View File

@ -0,0 +1,179 @@
use std::io::{Error, ErrorKind, Read};
use pumpkin_macros::packet;
use pumpkin_util::math::position::BlockPos;
use crate::{
codec::{var_int::VarInt, var_uint::VarUInt, var_ulong::VarULong},
serial::PacketRead,
};
pub const WINDOW_ID_INVENTORY: i32 = 0;
pub const WINDOW_ID_OFF_HAND: i32 = 119;
pub const WINDOW_ID_ARMOUR: i32 = 120;
pub const WINDOW_ID_UI: i32 = 124;
#[derive(Debug, PartialEq, Eq)]
pub enum InventoryActionSource {
Container,
World,
Creative,
Todo,
Unknown(u32),
}
impl From<u32> for InventoryActionSource {
fn from(value: u32) -> Self {
match value {
0 => Self::Container,
2 => Self::World,
3 => Self::Creative,
99999 => Self::Todo,
_ => Self::Unknown(value),
}
}
}
#[derive(Debug)]
pub enum TransactionData {
Normal(NormalTransactionData),
Mismatch(MismatchTransactionData),
UseItem(UseItemTransactionData),
UseItemOnEntity(UseItemOnEntityTransactionData),
ReleaseItem(ReleaseItemTransactionData),
}
#[derive(Debug, PacketRead)]
pub struct LegacySetItemSlot {
pub container_id: u8,
pub slots: Vec<u8>,
}
#[derive(Debug)]
pub struct InventoryAction {
pub source_type: u32,
pub window_id: Option<i32>,
pub source_flags: Option<u32>,
pub inventory_slot: u32,
// TODO
pub old_item: (),
pub new_item: (),
}
impl PacketRead for InventoryAction {
fn read<R: Read>(buf: &mut R) -> Result<Self, Error> {
let source_type = VarULong::read(buf)?.0 as u32;
let mut window_id = None;
let mut source_flags = None;
match InventoryActionSource::from(source_type) {
InventoryActionSource::Container | InventoryActionSource::Todo => {
window_id = Some(VarInt::read(buf)?.0);
}
InventoryActionSource::World => {
source_flags = Some(VarULong::read(buf)?.0 as u32);
}
_ => {}
}
let inventory_slot = VarULong::read(buf)?.0 as u32;
// let old_item = ItemStack::read(buf)?;
// let new_item = ItemStack::read(buf)?;
Ok(Self {
source_type,
window_id,
source_flags,
inventory_slot,
old_item: (),
new_item: (),
})
}
}
#[derive(Debug, PacketRead)]
pub struct NormalTransactionData;
#[derive(Debug, PacketRead)]
pub struct MismatchTransactionData;
#[derive(Debug, PacketRead)]
pub struct UseItemTransactionData {
pub action_type: VarULong,
pub trigger_type: VarULong,
pub block_position: BlockPos,
pub block_face: VarInt,
pub hot_bar_slot: VarInt,
// TODO
}
#[derive(Debug, PacketRead)]
pub struct UseItemOnEntityTransactionData {
pub target_entity_runtime_id: VarULong,
pub action_type: VarULong,
pub hot_bar_slot: VarInt,
// TODO
}
#[derive(Debug, PacketRead)]
pub struct ReleaseItemTransactionData {
pub action_type: VarULong,
pub hot_bar_slot: VarInt,
// TODO
}
#[derive(Debug)]
#[packet(30)]
pub struct SInventoryTransaction {
pub legacy_request_id: VarInt,
pub legacy_set_item_slots: Vec<LegacySetItemSlot>,
pub actions: Vec<InventoryAction>,
pub transaction_type: VarUInt,
pub transaction_data: TransactionData,
}
impl PacketRead for SInventoryTransaction {
fn read<R: Read>(buf: &mut R) -> Result<Self, Error> {
let legacy_request_id = VarInt::read(buf)?;
let mut legacy_set_item_slots = Vec::new();
if legacy_request_id.0 != 0 {
let len = VarUInt::read(buf)?.0;
for _ in 0..len {
legacy_set_item_slots.push(LegacySetItemSlot::read(buf)?);
}
}
let transaction_type = VarUInt::read(buf)?;
let actions_len = VarUInt::read(buf)?.0;
let mut actions = Vec::new();
for _ in 0..actions_len {
actions.push(InventoryAction::read(buf)?);
}
let transaction_data = match transaction_type.0 {
0 => TransactionData::Normal(NormalTransactionData::read(buf)?),
1 => TransactionData::Mismatch(MismatchTransactionData::read(buf)?),
2 => TransactionData::UseItem(UseItemTransactionData::read(buf)?),
3 => TransactionData::UseItemOnEntity(UseItemOnEntityTransactionData::read(buf)?),
4 => TransactionData::ReleaseItem(ReleaseItemTransactionData::read(buf)?),
_ => {
return Err(Error::new(
ErrorKind::InvalidData,
format!("Unknown inventory transaction type: {}", transaction_type.0),
));
}
};
Ok(Self {
legacy_request_id,
legacy_set_item_slots,
actions,
transaction_type,
transaction_data,
})
}
}

View File

@ -1,8 +1,10 @@
pub mod actor_event;
pub mod animate;
pub mod client_cache_status;
pub mod command_request;
pub mod container_close;
pub mod interaction;
pub mod inventory_transaction;
pub mod loading_screen;
pub mod login;
pub mod modal_form_response;

View File

@ -6,6 +6,7 @@ use pumpkin_data::tracked_data::{TrackedData, TrackedId};
use pumpkin_inventory::build_equipment_slots;
use pumpkin_inventory::player::player_inventory::PlayerInventory;
use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_protocol::bedrock::server::actor_event::{ActorEventType, SActorEvent};
use pumpkin_util::GameMode;
use pumpkin_util::Hand;
use pumpkin_util::math::position::BlockPos;
@ -2025,7 +2026,15 @@ impl EntityBase for LivingEntity {
(src.z - tgt.z).atan2(src.x - tgt.x).to_degrees() as f32
- self.entity.yaw.load()
});
world.broadcast_packet_all(&CHurtAnimation::new(entity_id, hurt_yaw));
let hurt_event = SActorEvent {
entity_runtime_id: entity_id,
event_type: ActorEventType::Hurt,
event_data: VarInt(0),
fire_at_position: None,
};
world
.broadcast_editioned(&CHurtAnimation::new(entity_id, hurt_yaw), &hurt_event)
.await;
}
world.broadcast_packet_all(&CDamageEvent::new(

View File

@ -32,6 +32,7 @@ use pumpkin_data::{
sound::{Sound, SoundCategory},
};
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_protocol::bedrock::client::CSetActorMotion;
use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot};
use pumpkin_protocol::{
PositionFlag,
@ -701,9 +702,14 @@ impl Entity {
pub fn send_velocity(&self) {
let velocity = self.velocity.load();
let chunk_pos = self.chunk_pos.load();
self.world.load().broadcast_to_chunk(
self.world.load().broadcast_to_chunk_editioned_sync(
chunk_pos,
&CEntityVelocity::new(self.entity_id.into(), velocity),
&CSetActorMotion::new(
VarULong(self.entity_id as u64),
Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32),
VarULong(0),
),
);
}

View File

@ -32,6 +32,7 @@ use pumpkin_protocol::{
command_request::SCommandRequest,
container_close::SContainerClose,
interaction::SInteraction,
inventory_transaction::SInventoryTransaction,
loading_screen::SLoadingScreen,
login::SLogin,
player_action::SPlayerAction,
@ -906,6 +907,9 @@ impl BedrockClient {
self.handle_request_chunk_radius(player, SRequestChunkRadius::read(reader)?)
.await;
}
SInventoryTransaction::PACKET_ID => {
self.handle_inventory_action(player, SInventoryTransaction::read(reader)?).await;
}
SInteraction::PACKET_ID => {
self.handle_interaction(player, SInteraction::read(reader)?)
.await;

View File

@ -12,6 +12,7 @@ use pumpkin_protocol::{
command_request::SCommandRequest,
container_close::SContainerClose,
interaction::{Action, SInteraction},
inventory_transaction::{SInventoryTransaction, TransactionData},
player_action::{Action as PlayerAction, SPlayerAction},
player_auth_input::{InputData, SPlayerAuthInput},
request_chunk_radius::SRequestChunkRadius,
@ -337,6 +338,51 @@ impl BedrockClient {
}
}
pub async fn handle_inventory_action(
&self,
player: &Arc<Player>,
packet: SInventoryTransaction,
) {
match packet.transaction_data {
TransactionData::Normal(_data) => {
// TODO
}
TransactionData::Mismatch(_data) => {
// TODO
}
TransactionData::UseItem(_data) => {
// TODO
}
TransactionData::UseItemOnEntity(data) => {
let target_runtime_id = data.target_entity_runtime_id.0 as i32;
// TODO: replace with consts, i'm too lazy
match data.action_type.0 {
// Interact
0 => {
// TODO
}
// Attack
1 => {
let world = player.world();
if let Some(target) = world.get_entity_by_id(target_runtime_id) {
player.attack(target).await;
}
}
_ => {
tracing::warn!(
"invalid UseItemOnEntity action type {}",
data.action_type.0
);
// Kick?
}
}
}
TransactionData::ReleaseItem(_data) => {
// TODO
}
}
}
pub async fn handle_interaction(&self, player: &Arc<Player>, packet: SInteraction) {
match packet.action {
Action::OpenInventory => {
@ -348,6 +394,7 @@ impl BedrockClient {
})
.await;
}
// No longer used in newer versions
Action::Attack => {
let target_runtime_id = packet.target_runtime_id.0 as i32;
let world = player.world();