feat: set camera for spectators
This commit is contained in:
parent
65fe9f0af0
commit
0c1cb25c05
@ -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"))?;
|
||||
|
||||
@ -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::*;
|
||||
|
||||
23
pumpkin-protocol/src/java/client/play/set_camera.rs
Normal file
23
pumpkin-protocol/src/java/client/play/set_camera.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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::*;
|
||||
|
||||
19
pumpkin-protocol/src/java/server/play/set_test_block.rs
Normal file
19
pumpkin-protocol/src/java/server/play/set_test_block.rs
Normal 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,
|
||||
}
|
||||
10
pumpkin-protocol/src/java/server/play/teleport_to_entity.rs
Normal file
10
pumpkin-protocol/src/java/server/play/teleport_to_entity.rs
Normal 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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user