diff --git a/azalea-client/src/client.rs b/azalea-client/src/client.rs index 0304e7b0..d9fec523 100644 --- a/azalea-client/src/client.rs +++ b/azalea-client/src/client.rs @@ -51,7 +51,7 @@ use crate::{ connection::RawConnection, disconnect::DisconnectEvent, events::Event, - interact::CurrentSequenceNumber, + interact::BlockStatePredictionHandler, inventory::Inventory, join::{ConnectOpts, StartJoinServerEvent}, local_player::{Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList}, @@ -586,7 +586,7 @@ pub struct JoinedClientBundle { pub physics_state: PhysicsState, pub inventory: Inventory, pub tab_list: TabList, - pub current_sequence_number: CurrentSequenceNumber, + pub current_sequence_number: BlockStatePredictionHandler, pub last_sent_direction: LastSentLookDirection, pub abilities: PlayerAbilities, pub permission_level: PermissionLevel, diff --git a/azalea-client/src/plugins/interact.rs b/azalea-client/src/plugins/interact.rs index 2e2b039f..31b5acf4 100644 --- a/azalea-client/src/plugins/interact.rs +++ b/azalea-client/src/plugins/interact.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use azalea_block::BlockState; use azalea_core::{ direction::Direction, @@ -96,17 +98,95 @@ impl Client { } } -/// A component that contains the number of changes this client has made to -/// blocks. -#[derive(Component, Copy, Clone, Debug, Default, Deref)] -pub struct CurrentSequenceNumber(u32); +/// A component that contains information about our local block state +/// predictions. +#[derive(Component, Clone, Debug, Default)] +pub struct BlockStatePredictionHandler { + /// The total number of changes that this client has made to blocks. + seq: u32, + server_state: HashMap, +} +#[derive(Clone, Debug)] +struct ServerVerifiedState { + seq: u32, + block_state: BlockState, + /// Used for teleporting the player back if we're colliding with the block + /// that got placed back. + #[allow(unused)] + player_pos: Vec3, +} -impl CurrentSequenceNumber { +impl BlockStatePredictionHandler { /// Get the next sequence number that we're going to use and increment the /// value. - pub fn get_next(&mut self) -> u32 { - self.0 += 1; - self.0 + pub fn start_predicting(&mut self) -> u32 { + self.seq += 1; + self.seq + } + + /// Should be called right before the client updates a block with its + /// prediction. + /// + /// This is used to make sure that we can rollback to this state if the + /// server acknowledges the sequence number (with + /// [`ClientboundBlockChangedAck`]) without having sent a block update. + /// + /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck + pub fn retain_known_server_state( + &mut self, + pos: BlockPos, + old_state: BlockState, + player_pos: Vec3, + ) { + self.server_state + .entry(pos) + .and_modify(|s| s.seq = self.seq) + .or_insert(ServerVerifiedState { + seq: self.seq, + block_state: old_state, + player_pos: player_pos, + }); + } + + /// Save this update as the correct server state so when the server sends a + /// [`ClientboundBlockChangedAck`] we don't roll back this new update. + /// + /// This should be used when we receive a block update from the server. + /// + /// [`ClientboundBlockChangedAck`]: azalea_protocol::packets::game::ClientboundBlockChangedAck + pub fn update_known_server_state(&mut self, pos: BlockPos, state: BlockState) -> bool { + if let Some(s) = self.server_state.get_mut(&pos) { + s.block_state = state; + true + } else { + false + } + } + + pub fn end_prediction_up_to(&mut self, seq: u32, world: &Instance) { + let mut to_remove = Vec::new(); + for (pos, state) in &self.server_state { + if state.seq > seq { + continue; + } + to_remove.push(*pos); + + // syncBlockState + let client_block_state = world.get_block_state(*pos).unwrap_or_default(); + let server_block_state = state.block_state; + if client_block_state == server_block_state { + continue; + } + world.set_block_state(*pos, server_block_state); + // TODO: implement these two functions + // if is_colliding(player, *pos, server_block_state) { + // abs_snap_to(state.player_pos); + // } + } + + for pos in to_remove { + self.server_state.remove(&pos); + } } } @@ -163,13 +243,15 @@ pub fn handle_start_use_item_queued( query: Query<( Entity, &StartUseItemQueued, - &mut CurrentSequenceNumber, + &mut BlockStatePredictionHandler, &HitResultComponent, &LookDirection, Option<&Mining>, )>, ) { - for (entity, start_use_item, mut sequence_number, hit_result, look_direction, mining) in query { + for (entity, start_use_item, mut prediction_handler, hit_result, look_direction, mining) in + query + { commands.entity(entity).remove::(); if mining.is_some() { @@ -203,12 +285,13 @@ pub fn handle_start_use_item_queued( match &hit_result { HitResult::Block(block_hit_result) => { + let seq = prediction_handler.start_predicting(); if block_hit_result.miss { commands.trigger(SendPacketEvent::new( entity, ServerboundUseItem { hand: start_use_item.hand, - sequence: sequence_number.get_next(), + seq, x_rot: look_direction.x_rot, y_rot: look_direction.y_rot, }, @@ -219,7 +302,7 @@ pub fn handle_start_use_item_queued( ServerboundUseItemOn { hand: start_use_item.hand, block_hit: block_hit_result.into(), - sequence: sequence_number.get_next(), + seq, }, )); // TODO: depending on the result of useItemOn, this might diff --git a/azalea-client/src/plugins/mining.rs b/azalea-client/src/plugins/mining.rs index 51bb5529..88bd3be8 100644 --- a/azalea-client/src/plugins/mining.rs +++ b/azalea-client/src/plugins/mining.rs @@ -1,6 +1,6 @@ use azalea_block::{BlockState, BlockTrait, fluid_state::FluidState}; use azalea_core::{direction::Direction, game_type::GameMode, position::BlockPos, tick::GameTick}; -use azalea_entity::{FluidOnEyes, Physics, mining::get_mine_progress}; +use azalea_entity::{FluidOnEyes, Physics, Position, mining::get_mine_progress}; use azalea_inventory::ItemStack; use azalea_physics::{PhysicsSet, collision::BlockWithShape}; use azalea_protocol::packets::game::s_player_action::{self, ServerboundPlayerAction}; @@ -13,7 +13,7 @@ use tracing::trace; use crate::{ Client, interact::{ - CurrentSequenceNumber, HitResultComponent, SwingArmEvent, can_use_game_master_blocks, + BlockStatePredictionHandler, HitResultComponent, SwingArmEvent, can_use_game_master_blocks, check_is_interaction_restricted, }, inventory::{Inventory, InventorySet}, @@ -216,7 +216,7 @@ fn handle_mining_queued( &FluidOnEyes, &Physics, Option<&Mining>, - &mut CurrentSequenceNumber, + &mut BlockStatePredictionHandler, &mut MineDelay, &mut MineProgress, &mut MineTicks, @@ -280,7 +280,7 @@ fn handle_mining_queued( pos: current_mining_pos .expect("IsMining is true so MineBlockPos must be present"), direction: mining_queued.direction, - sequence: 0, + seq: 0, }, )); } @@ -345,7 +345,7 @@ fn handle_mining_queued( action: s_player_action::Action::StartDestroyBlock, pos: mining_queued.position, direction: mining_queued.direction, - sequence: sequence_number.get_next(), + seq: sequence_number.start_predicting(), }, )); // vanilla really does send two swing arm packets @@ -440,14 +440,22 @@ pub fn handle_finish_mining_block_observer( &Inventory, &PlayerAbilities, &PermissionLevel, - &mut CurrentSequenceNumber, + &Position, + &mut BlockStatePredictionHandler, )>, instances: Res, ) { let event = trigger.event(); - let (instance_name, game_mode, inventory, abilities, permission_level, _sequence_number) = - query.get_mut(trigger.target()).unwrap(); + let ( + instance_name, + game_mode, + inventory, + abilities, + permission_level, + player_pos, + mut prediction_handler, + ) = query.get_mut(trigger.target()).unwrap(); let instance_lock = instances.get(instance_name).unwrap(); let instance = instance_lock.read(); if check_is_interaction_restricted(&instance, event.position, &game_mode.current, inventory) { @@ -469,7 +477,8 @@ pub fn handle_finish_mining_block_observer( return; }; - let registry_block = Box::::from(block_state).as_registry_block(); + let registry_block: azalea_registry::Block = + Box::::from(block_state).as_registry_block(); if !can_use_game_master_blocks(abilities, permission_level) && matches!( registry_block, @@ -485,7 +494,10 @@ pub fn handle_finish_mining_block_observer( // when we break a waterlogged block we want to keep the water there let fluid_state = FluidState::from(block_state); let block_state_for_fluid = BlockState::from(fluid_state); - instance.set_block_state(event.position, block_state_for_fluid); + let old_state = instance + .set_block_state(event.position, block_state_for_fluid) + .unwrap_or_default(); + prediction_handler.retain_known_server_state(event.position, old_state, **player_pos); } /// Abort mining a block. @@ -510,7 +522,7 @@ pub fn handle_stop_mining_block_event( action: s_player_action::Action::AbortDestroyBlock, pos: mine_block_pos, direction: Direction::Down, - sequence: 0, + seq: 0, }, )); commands.entity(event.entity).remove::(); @@ -538,7 +550,7 @@ pub fn continue_mining_block( &mut MineDelay, &mut MineProgress, &mut MineTicks, - &mut CurrentSequenceNumber, + &mut BlockStatePredictionHandler, )>, mut commands: Commands, mut mine_block_progress_events: EventWriter, @@ -557,7 +569,7 @@ pub fn continue_mining_block( mut mine_delay, mut mine_progress, mut mine_ticks, - mut sequence_number, + mut prediction_handler, ) in query.iter_mut() { if **mine_delay > 0 { @@ -580,7 +592,7 @@ pub fn continue_mining_block( action: s_player_action::Action::StartDestroyBlock, pos: mining.pos, direction: mining.dir, - sequence: sequence_number.get_next(), + seq: prediction_handler.start_predicting(), }, )); commands.trigger(SwingArmEvent { entity }); @@ -634,7 +646,7 @@ pub fn continue_mining_block( action: s_player_action::Action::StopDestroyBlock, pos: mining.pos, direction: mining.dir, - sequence: sequence_number.get_next(), + seq: prediction_handler.start_predicting(), }, )); **mine_progress = 0.; diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index a49a0209..b2a4abc4 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -25,6 +25,7 @@ use crate::{ connection::RawConnection, declare_packet_handlers, disconnect::DisconnectEvent, + interact::BlockStatePredictionHandler, inventory::{ ClientSideCloseContainerEvent, Inventory, MenuOpenedEvent, SetContainerContentEvent, }, @@ -1061,13 +1062,17 @@ impl GamePacketHandler<'_> { pub fn block_update(&mut self, p: &ClientboundBlockUpdate) { debug!("Got block update packet {p:?}"); - as_system::>(self.ecs, |mut query| { - let local_player = query.get_mut(self.player).unwrap(); + as_system::>( + self.ecs, + |mut query| { + let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap(); - let world = local_player.instance.write(); - - world.chunks.set_block_state(p.pos, p.block_state); - }); + let world = local_player.instance.read(); + if !prediction_handler.update_known_server_state(p.pos, p.block_state) { + world.chunks.set_block_state(p.pos, p.block_state); + } + }, + ); } pub fn animate(&mut self, p: &ClientboundAnimate) { @@ -1077,15 +1082,19 @@ impl GamePacketHandler<'_> { pub fn section_blocks_update(&mut self, p: &ClientboundSectionBlocksUpdate) { debug!("Got section blocks update packet {p:?}"); - as_system::>(self.ecs, |mut query| { - let local_player = query.get_mut(self.player).unwrap(); - let world = local_player.instance.write(); - for state in &p.states { - world - .chunks - .set_block_state(p.section_pos + state.pos, state.state); - } - }); + as_system::>( + self.ecs, + |mut query| { + let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap(); + let world = local_player.instance.read(); + for new_state in &p.states { + let pos = p.section_pos + new_state.pos; + if !prediction_handler.update_known_server_state(pos, new_state.state) { + world.chunks.set_block_state(pos, new_state.state); + } + } + }, + ); } pub fn game_event(&mut self, p: &ClientboundGameEvent) { @@ -1125,7 +1134,16 @@ impl GamePacketHandler<'_> { pub fn award_stats(&mut self, _p: &ClientboundAwardStats) {} - pub fn block_changed_ack(&mut self, _p: &ClientboundBlockChangedAck) {} + pub fn block_changed_ack(&mut self, p: &ClientboundBlockChangedAck) { + as_system::>( + self.ecs, + |mut query| { + let (local_player, mut prediction_handler) = query.get_mut(self.player).unwrap(); + let world = local_player.instance.read(); + prediction_handler.end_prediction_up_to(p.seq, &world); + }, + ); + } pub fn block_destruction(&mut self, _p: &ClientboundBlockDestruction) {} diff --git a/azalea-client/src/test_utils/simulation.rs b/azalea-client/src/test_utils/simulation.rs index caf63113..e7ac8d5b 100644 --- a/azalea-client/src/test_utils/simulation.rs +++ b/azalea-client/src/test_utils/simulation.rs @@ -6,7 +6,7 @@ use azalea_buf::AzaleaWrite; use azalea_core::{ delta::PositionDelta8, game_type::{GameMode, OptionalGameType}, - position::{ChunkPos, Vec3}, + position::{BlockPos, ChunkPos, Vec3}, resource_location::ResourceLocation, tick::GameTick, }; @@ -102,6 +102,9 @@ impl Simulation { raw_conn.injected_clientbound_packets.push(buf); }); } + pub fn send_event(&mut self, event: impl bevy_ecs::event::Event) { + self.app.world_mut().send_event(event); + } pub fn tick(&mut self) { tick_app(&mut self.app); @@ -151,6 +154,12 @@ impl Simulation { .chunks .get(&chunk_pos) } + pub fn get_block_state(&self, pos: BlockPos) -> Option { + self.component::() + .instance + .read() + .get_block_state(pos) + } pub fn disconnect(&mut self) { // send DisconnectEvent diff --git a/azalea-client/tests/mine_block_rollback.rs b/azalea-client/tests/mine_block_rollback.rs new file mode 100644 index 00000000..e9b46d13 --- /dev/null +++ b/azalea-client/tests/mine_block_rollback.rs @@ -0,0 +1,49 @@ +use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*}; +use azalea_core::{ + position::{BlockPos, ChunkPos}, + resource_location::ResourceLocation, +}; +use azalea_protocol::packets::{ + ConnectionProtocol, + game::{ClientboundBlockChangedAck, ClientboundBlockUpdate}, +}; +use azalea_registry::{Block, DataRegistry, DimensionType}; + +#[test] +fn test_mine_block_rollback() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Game); + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), + ResourceLocation::new("azalea:overworld"), + )); + + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16)); + simulation.tick(); + + let pos = BlockPos::new(1, 2, 3); + simulation.receive_packet(ClientboundBlockUpdate { + pos, + // tnt is used for this test because it's insta-mineable so we don't have to waste ticks + // waiting + block_state: Block::Tnt.into(), + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Tnt.into())); + println!("set serverside tnt"); + + simulation.send_event(StartMiningBlockEvent { + entity: simulation.entity, + position: pos, + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Air.into())); + println!("set clientside air"); + + // server didn't send the new block, so the change should be rolled back + simulation.receive_packet(ClientboundBlockChangedAck { seq: 1 }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Tnt.into())); + println!("reset serverside tnt"); +} diff --git a/azalea-client/tests/mine_block_without_rollback.rs b/azalea-client/tests/mine_block_without_rollback.rs new file mode 100644 index 00000000..02fb1a77 --- /dev/null +++ b/azalea-client/tests/mine_block_without_rollback.rs @@ -0,0 +1,51 @@ +use azalea_client::{mining::StartMiningBlockEvent, test_utils::prelude::*}; +use azalea_core::{ + position::{BlockPos, ChunkPos}, + resource_location::ResourceLocation, +}; +use azalea_protocol::packets::{ + ConnectionProtocol, + game::{ClientboundBlockChangedAck, ClientboundBlockUpdate}, +}; +use azalea_registry::{Block, DataRegistry, DimensionType}; + +#[test] +fn test_mine_block_without_rollback() { + init_tracing(); + + let mut simulation = Simulation::new(ConnectionProtocol::Game); + simulation.receive_packet(make_basic_login_packet( + DimensionType::new_raw(0), + ResourceLocation::new("azalea:overworld"), + )); + + simulation.receive_packet(make_basic_empty_chunk(ChunkPos::new(0, 0), (384 + 64) / 16)); + simulation.tick(); + + let pos = BlockPos::new(1, 2, 3); + simulation.receive_packet(ClientboundBlockUpdate { + pos, + // tnt is used for this test because it's insta-mineable so we don't have to waste ticks + // waiting + block_state: Block::Tnt.into(), + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Tnt.into())); + + simulation.send_event(StartMiningBlockEvent { + entity: simulation.entity, + position: pos, + }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Air.into())); + + // server acknowledged our change by sending a BlockUpdate + BlockChangedAck, so + // no rollback + simulation.receive_packet(ClientboundBlockUpdate { + pos, + block_state: Block::Air.into(), + }); + simulation.receive_packet(ClientboundBlockChangedAck { seq: 1 }); + simulation.tick(); + assert_eq!(simulation.get_block_state(pos), Some(Block::Air.into())); +} diff --git a/azalea-protocol/src/packets/game/c_block_changed_ack.rs b/azalea-protocol/src/packets/game/c_block_changed_ack.rs index ebb303c0..c477c1f2 100644 --- a/azalea-protocol/src/packets/game/c_block_changed_ack.rs +++ b/azalea-protocol/src/packets/game/c_block_changed_ack.rs @@ -4,5 +4,5 @@ use azalea_protocol_macros::ClientboundGamePacket; #[derive(Clone, Debug, AzBuf, ClientboundGamePacket)] pub struct ClientboundBlockChangedAck { #[var] - pub sequence: i32, + pub seq: u32, } diff --git a/azalea-protocol/src/packets/game/s_player_action.rs b/azalea-protocol/src/packets/game/s_player_action.rs index 5b65b746..cffec2bd 100644 --- a/azalea-protocol/src/packets/game/s_player_action.rs +++ b/azalea-protocol/src/packets/game/s_player_action.rs @@ -1,6 +1,5 @@ use azalea_buf::AzBuf; -use azalea_core::direction::Direction; -use azalea_core::position::BlockPos; +use azalea_core::{direction::Direction, position::BlockPos}; use azalea_protocol_macros::ServerboundGamePacket; #[derive(Clone, Debug, AzBuf, ServerboundGamePacket)] @@ -9,7 +8,7 @@ pub struct ServerboundPlayerAction { pub pos: BlockPos, pub direction: Direction, #[var] - pub sequence: u32, + pub seq: u32, } #[derive(AzBuf, Clone, Copy, Debug)] diff --git a/azalea-protocol/src/packets/game/s_use_item.rs b/azalea-protocol/src/packets/game/s_use_item.rs index d6ccca87..3e9e1a2f 100644 --- a/azalea-protocol/src/packets/game/s_use_item.rs +++ b/azalea-protocol/src/packets/game/s_use_item.rs @@ -7,7 +7,7 @@ use crate::packets::game::s_interact::InteractionHand; pub struct ServerboundUseItem { pub hand: InteractionHand, #[var] - pub sequence: u32, + pub seq: u32, pub y_rot: f32, pub x_rot: f32, } diff --git a/azalea-protocol/src/packets/game/s_use_item_on.rs b/azalea-protocol/src/packets/game/s_use_item_on.rs index 11e32b2c..1c08112d 100644 --- a/azalea-protocol/src/packets/game/s_use_item_on.rs +++ b/azalea-protocol/src/packets/game/s_use_item_on.rs @@ -15,7 +15,7 @@ pub struct ServerboundUseItemOn { pub hand: InteractionHand, pub block_hit: BlockHit, #[var] - pub sequence: u32, + pub seq: u32, } #[derive(Clone, Debug)] diff --git a/azalea-world/src/palette/container.rs b/azalea-world/src/palette/container.rs index 2f3d9238..0ddac6c2 100644 --- a/azalea-world/src/palette/container.rs +++ b/azalea-world/src/palette/container.rs @@ -192,19 +192,10 @@ impl PalettedContainer { /// Sets the id at the given coordinates and return the previous id pub fn get_and_set(&mut self, pos: S::SectionPos, value: S) -> S { let paletted_value = self.id_for(value); - let block_state_id = self + let old_paletted_value = self .storage .get_and_set(self.index_from_coords(pos), paletted_value as u64); - // error in debug mode - #[cfg(debug_assertions)] - if block_state_id > BlockState::MAX_STATE.into() { - warn!( - "Old block state from get_and_set {block_state_id} was greater than max state {}", - BlockState::MAX_STATE - ); - } - - S::try_from(block_state_id as u32).unwrap_or_default() + self.palette.value_for(old_paletted_value as usize) } /// Sets the id at the given index and return the previous id. You probably diff --git a/azalea/src/pathfinder/simulation.rs b/azalea/src/pathfinder/simulation.rs index 337efda7..5c548aa2 100644 --- a/azalea/src/pathfinder/simulation.rs +++ b/azalea/src/pathfinder/simulation.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use azalea_client::{ - PhysicsState, interact::CurrentSequenceNumber, inventory::Inventory, + PhysicsState, interact::BlockStatePredictionHandler, inventory::Inventory, local_player::LocalGameMode, mining::MineBundle, packet::game::SendPacketEvent, }; use azalea_core::{ @@ -113,7 +113,7 @@ fn create_simulation_player_complete_bundle( Inventory::default(), LocalGameMode::from(GameMode::Survival), MineBundle::default(), - CurrentSequenceNumber::default(), + BlockStatePredictionHandler::default(), azalea_client::local_player::PermissionLevel::default(), azalea_client::local_player::PlayerAbilities::default(), )