feat: set camera for spectators

This commit is contained in:
Alexander Medvedev 2026-06-22 17:57:36 +02:00
parent 65fe9f0af0
commit 0c1cb25c05
11 changed files with 277 additions and 32 deletions

View File

@ -916,6 +916,8 @@ impl DataComponentCodec<Self> for UseCooldownImpl {
fn deserialize_item_stack_template<'a, A: SeqAccess<'a>>(
seq: &mut A,
) -> Result<pumpkin_data::item_stack::ItemStack, A::Error> {
const MAX_COMPONENTS: i32 = 256;
let item_id = seq
.next_element::<VarInt>()?
.ok_or_else(|| de::Error::custom("Missing item_id in ItemStackTemplate"))?
@ -933,7 +935,6 @@ fn deserialize_item_stack_template<'a, A: SeqAccess<'a>>(
return Err(de::Error::custom("Negative component count"));
}
const MAX_COMPONENTS: i32 = 256;
let total_components = num_to_add
.checked_add(num_to_remove)
.ok_or_else(|| de::Error::custom("Component count overflow"))?;

View File

@ -68,6 +68,7 @@ mod set_border_lerp_size;
mod set_border_size;
mod set_border_warning_delay;
mod set_border_warning_distance;
mod set_camera;
mod set_container_content;
mod set_container_property;
mod set_container_slot;
@ -173,6 +174,7 @@ pub use set_border_lerp_size::*;
pub use set_border_size::*;
pub use set_border_warning_delay::*;
pub use set_border_warning_distance::*;
pub use set_camera::*;
pub use set_container_content::*;
pub use set_container_property::*;
pub use set_container_slot::*;

View File

@ -0,0 +1,23 @@
use pumpkin_data::packet::clientbound::PLAY_SET_CAMERA;
use pumpkin_util::version::JavaMinecraftVersion;
use serde::Serialize;
use crate::{VarInt, packet::MultiVersionJavaPacket};
#[derive(Serialize)]
pub struct CSetCamera {
pub camera_id: VarInt,
}
impl CSetCamera {
#[must_use]
pub const fn new(camera_id: VarInt) -> Self {
Self { camera_id }
}
}
impl MultiVersionJavaPacket for CSetCamera {
fn to_id(version: JavaMinecraftVersion) -> i32 {
PLAY_SET_CAMERA.to_id(version)
}
}

View File

@ -43,6 +43,7 @@ mod set_creative_slot;
mod set_held_item;
mod set_jigsaw_block;
mod swing_arm;
mod teleport_to_entity;
mod update_sign;
mod use_item;
mod use_item_on;
@ -92,6 +93,13 @@ pub use set_creative_slot::*;
pub use set_held_item::*;
pub use set_jigsaw_block::*;
pub use swing_arm::*;
pub use teleport_to_entity::*;
pub use update_sign::*;
pub use use_item::*;
pub use use_item_on::*;
mod test_instance_block_action;
pub use test_instance_block_action::*;
mod set_test_block;
pub use set_test_block::*;

View File

@ -0,0 +1,19 @@
use pumpkin_data::packet::serverbound::PLAY_SET_TEST_BLOCK;
use pumpkin_macros::java_packet;
use pumpkin_util::math::position::BlockPos;
#[derive(serde::Deserialize)]
#[java_packet(PLAY_SET_TEST_BLOCK)]
pub struct SSetTestBlock {
pub position: BlockPos,
pub mode: TestBlockMode,
pub message: String,
}
#[derive(serde::Deserialize, Clone, Copy, Debug)]
pub enum TestBlockMode {
Start,
Log,
Fail,
Accept,
}

View File

@ -0,0 +1,10 @@
use pumpkin_data::packet::serverbound::PLAY_TELEPORT_TO_ENTITY;
use pumpkin_macros::java_packet;
use serde::Deserialize;
#[derive(Deserialize)]
#[java_packet(PLAY_TELEPORT_TO_ENTITY)]
pub struct STeleportToEntity {
#[serde(with = "uuid::serde::compact")]
pub target: uuid::Uuid,
}

View File

@ -0,0 +1,49 @@
use pumpkin_data::packet::serverbound::PLAY_TEST_INSTANCE_BLOCK_ACTION;
use pumpkin_macros::java_packet;
use pumpkin_util::math::position::BlockPos;
use crate::VarInt;
#[derive(serde::Deserialize)]
#[java_packet(PLAY_TEST_INSTANCE_BLOCK_ACTION)]
pub struct STestInstanceBlockAction {
pub pos: BlockPos,
pub action: TestInstanceBlockAction,
pub data: TestInstanceBlockData,
}
#[derive(serde::Deserialize, Clone, Copy, Debug)]
pub enum TestInstanceBlockAction {
Init,
Query,
Set,
Reset,
Save,
Export,
Run,
}
#[derive(serde::Deserialize)]
pub struct VarIntVector3 {
pub x: VarInt,
pub y: VarInt,
pub z: VarInt,
}
#[derive(serde::Deserialize)]
pub struct TestInstanceBlockData {
pub test: Option<String>,
pub size: VarIntVector3,
pub rotation: pumpkin_data::block_rotation::Rotation,
pub ignore_entities: bool,
pub status: TestInstanceBlockStatus,
pub error_message: Option<String>,
}
#[derive(serde::Deserialize, Clone, Copy, Debug)]
pub enum TestInstanceBlockStatus {
Cleared,
Running,
Success,
Failed,
}

View File

@ -67,9 +67,9 @@ use pumpkin_protocol::java::client::play::{
CCloseContainer, CCombatDeath, CCustomPayload, CDisguisedChatMessage, CEntityAnimation,
CEntityPositionSync, CGameEvent, CItemCooldown, CMapItemData, COpenScreen, CParticle,
CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, CPlayerSpawnPosition, CRespawn,
CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem, CSetEquipment,
CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect, CStopSound,
CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk,
CSetCamera, CSetContainerContent, CSetContainerProperty, CSetContainerSlot, CSetCursorItem,
CSetEquipment, CSetExperience, CSetHealth, CSetPlayerInventory, CSetSelectedSlot, CSoundEffect,
CStopSound, CSubtitle, CSystemChatMessage, CTabList, CTitleAnimation, CTitleText, CUnloadChunk,
CUpdateMobEffect, CUpdateTime, GameEvent, MapIcon, MapPatch, Metadata, PlayerAction,
PlayerInfoFlags, PlayerSpawnData, PreviousMessage, Statistic,
};
@ -422,6 +422,8 @@ pub struct Player {
pub gamemode: AtomicCell<GameMode>,
/// The player's previous gamemode
pub previous_gamemode: AtomicCell<Option<GameMode>>,
/// The entity ID of the entity that the player is currently spectating/camera targeting.
pub camera_target_id: AtomicCell<Option<i32>>,
/// The player's spawnpoint
pub respawn_point: Mutex<Option<RespawnPoint>>,
/// The player's sleep status
@ -656,6 +658,7 @@ impl Player {
stats: Mutex::new(statistics::Statistics::default()),
gamemode: AtomicCell::new(gamemode),
previous_gamemode: AtomicCell::new(None),
camera_target_id: AtomicCell::new(None),
// TODO: Send the CPlayerSpawnPosition packet when the client connects with proper values
respawn_point: Mutex::new(None),
sleeping_since: AtomicCell::new(None),
@ -1859,6 +1862,32 @@ impl Player {
// TODO Abstract the chunk sending
#[expect(clippy::too_many_lines)]
pub async fn tick(self: &Arc<Self>, server: &Server) {
if let Some(camera_id) = self.camera_target_id.load() {
if camera_id == self.entity_id() {
self.camera_target_id.store(None);
} else {
let world = self.world();
let target = world
.get_player_by_id(camera_id)
.map(|p| Arc::clone(&p) as Arc<dyn EntityBase>)
.or_else(|| world.get_entity_by_id(camera_id));
if let Some(target) = target {
let target_pos = target.get_entity().pos.load();
let player_pos = self.living_entity.entity.pos.load();
if player_pos != target_pos {
self.living_entity.entity.set_pos(target_pos);
crate::world::chunker::update_position(self).await;
}
} else {
// Target no longer exists, reset camera back to player
self.camera_target_id.store(None);
self.client
.send_packet_now(&CSetCamera::new(self.entity_id().into()))
.await;
}
}
}
self.current_screen_handler
.lock()
.await
@ -2855,6 +2884,13 @@ impl Player {
}
}
if gamemode != GameMode::Spectator && self.camera_target_id.load().is_some() {
self.camera_target_id.store(None);
self.client.send_packet_now(&CSetCamera::new(
self.entity_id().into()
)).await;
}
self.living_entity.entity.invulnerable.store(
matches!(gamemode, GameMode::Creative | GameMode::Spectator),
Ordering::Relaxed,

View File

@ -31,20 +31,19 @@ impl ItemBehaviour for BundleItem {
matched = true;
if let Some(bundle_contents) =
held_item.get_data_component_mut::<BundleContentsImpl>()
&& let Some(extracted_stack) = bundle_contents.try_extract()
{
if let Some(extracted_stack) = bundle_contents.try_extract() {
let position = player.position();
player.world().play_sound(
Sound::ItemBundleRemoveOne,
pumpkin_data::sound::SoundCategory::Players,
&position,
);
let updated_bundle = held_item.clone();
drop(held_item);
let position = player.position();
player.world().play_sound(
Sound::ItemBundleRemoveOne,
pumpkin_data::sound::SoundCategory::Players,
&position,
);
let updated_bundle = held_item.clone();
drop(held_item);
player.drop_item(extracted_stack).await;
player.sync_hand_slot(used_slot_index, updated_bundle).await;
}
player.drop_item(extracted_stack).await;
player.sync_hand_slot(used_slot_index, updated_bundle).await;
}
}
@ -55,20 +54,19 @@ impl ItemBehaviour for BundleItem {
used_slot_index = 40; // OFF_HAND_SLOT
if let Some(bundle_contents) =
off_hand_item.get_data_component_mut::<BundleContentsImpl>()
&& let Some(extracted_stack) = bundle_contents.try_extract()
{
if let Some(extracted_stack) = bundle_contents.try_extract() {
let position = player.position();
player.world().play_sound(
Sound::ItemBundleRemoveOne,
pumpkin_data::sound::SoundCategory::Players,
&position,
);
let updated_bundle = off_hand_item.clone();
drop(off_hand_item);
let position = player.position();
player.world().play_sound(
Sound::ItemBundleRemoveOne,
pumpkin_data::sound::SoundCategory::Players,
&position,
);
let updated_bundle = off_hand_item.clone();
drop(off_hand_item);
player.drop_item(extracted_stack).await;
player.sync_hand_slot(used_slot_index, updated_bundle).await;
}
player.drop_item(extracted_stack).await;
player.sync_hand_slot(used_slot_index, updated_bundle).await;
}
}
}

View File

@ -21,7 +21,8 @@ use pumpkin_protocol::java::server::play::{
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
SRecipeBookSeenRecipe, SRenameItem, SSelectTrade, SSetCommandBlock, SSetCreativeSlot,
SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
SSetHeldItem, SSetJigsawBlock, SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity,
STestInstanceBlockAction, SUpdateSign, SUseItem, SUseItemOn,
};
use pumpkin_protocol::packet::MultiVersionJavaPacket;
use pumpkin_protocol::{
@ -923,6 +924,14 @@ impl JavaClient {
self.handle_attack(player, SAttack::read(payload, &version)?, server)
.await;
}
id if id == STeleportToEntity::to_id(version) => {
self.handle_teleport_to_entity(
player,
STeleportToEntity::read(payload, &version)?,
server,
)
.await;
}
id if id == pumpkin_protocol::java::server::play::SKeepAlive::to_id(version) => {
self.handle_keep_alive(
player,
@ -933,6 +942,15 @@ impl JavaClient {
id if id == SClientTickEnd::to_id(version) => {
// TODO
}
id if id == STestInstanceBlockAction::to_id(version) => {
self.handle_test_instance_block_action(
player,
&STestInstanceBlockAction::read(payload, &version)?,
);
}
id if id == SSetTestBlock::to_id(version) => {
self.handle_set_test_block(player, &SSetTestBlock::read(payload, &version)?);
}
id if id == SPlayerPosition::to_id(version) => {
self.handle_position(player, server, SPlayerPosition::read(payload, &version)?)
.await;

View File

@ -61,8 +61,9 @@ use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_ulong::VarULong;
use pumpkin_protocol::java::client::play::{
CBlockUpdate, CCommandSuggestions, CEntityPositionSync, CHeadRot, COpenSignEditor,
CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetSelectedSlot, CSystemChatMessage,
CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat, PlayerAction,
CPingResponse, CPlayerInfoUpdate, CPlayerPosition, CSetCamera, CSetSelectedSlot,
CSystemChatMessage, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, InitChat,
PlayerAction,
};
use pumpkin_protocol::java::server::play::{
Action, ActionType, CommandBlockMode, FLAG_ON_GROUND, SAttack, SBundleItemSelected,
@ -73,7 +74,8 @@ use pumpkin_protocol::java::server::play::{
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition, SPlayerPositionRotation,
SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings, SRecipeBookSeenRecipe,
SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetJigsawBlock,
SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status,
SSetPlayerGround, SSetTestBlock, SSwingArm, STeleportToEntity, STestInstanceBlockAction,
SUpdateSign, SUseItem, SUseItemOn, Status,
};
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::vector3::Vector3;
@ -987,6 +989,17 @@ impl JavaClient {
player.last_input.store(input.input, Ordering::Relaxed);
let sneak = input.input & SPlayerInput::SNEAK != 0;
if sneak
&& player.gamemode.load() == GameMode::Spectator
&& player.camera_target_id.load().is_some()
{
player.camera_target_id.store(None);
player
.client
.send_packet_now(&CSetCamera::new(player.entity_id().into()))
.await;
}
if player.get_entity().is_sneaking() != sneak {
send_cancellable! {{
server;
@ -1699,6 +1712,7 @@ impl JavaClient {
player.attack(target).await;
}
#[expect(clippy::too_many_lines)]
pub async fn handle_interact(
&self,
player: &Arc<Player>,
@ -1730,6 +1744,14 @@ impl JavaClient {
.or_else(|| world.get_entity_by_id(entity_id.0));
if let Some(target) = target {
if player.gamemode.load() == GameMode::Spectator {
player.camera_target_id.store(Some(entity_id.0));
player
.client
.send_packet_now(&CSetCamera::new(entity_id))
.await;
return;
}
send_cancellable! {{
server;
PlayerInteractEntityEvent::new(
@ -2942,4 +2964,63 @@ impl JavaClient {
packet.slot_id.0, selected_item_index
);
}
pub async fn handle_teleport_to_entity(
&self,
player: &Arc<Player>,
packet: STeleportToEntity,
server: &Server,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
if player.gamemode.load() != GameMode::Spectator {
return;
}
if let Some(target_player) = server.get_player_by_uuid(packet.target) {
let target_pos = target_player.living_entity.entity.pos.load();
let target_yaw = target_player.living_entity.entity.yaw.load();
let target_pitch = target_player.living_entity.entity.pitch.load();
let target_id = target_player.living_entity.entity.entity_id;
player.camera_target_id.store(Some(target_id));
player
.client
.send_packet_now(&CSetCamera::new(target_id.into()))
.await;
player
.request_teleport(target_pos, target_yaw, target_pitch)
.await;
}
}
pub fn handle_test_instance_block_action(
&self,
player: &Arc<Player>,
packet: &STestInstanceBlockAction,
) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
debug!(
"Test instance block action at {:?}: action={:?}",
packet.pos, packet.action
);
}
pub fn handle_set_test_block(&self, player: &Arc<Player>, packet: &SSetTestBlock) {
if !player.has_client_loaded() {
return;
}
player.update_last_action_time();
debug!(
"Set test block at {:?}: mode={:?}, message={}",
packet.position, packet.mode, packet.message
);
}
}