From cb6c3f6369b2ef5a98ef70eaa73aee4d36273366 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Thu, 30 Dec 2021 22:50:55 +0100 Subject: [PATCH 01/15] First commit Gamemode hardcoded to survival for testing --- feather/common/src/block_break.rs | 66 +++++++++++++++++++ feather/common/src/lib.rs | 3 + feather/server/src/client.rs | 19 +++++- .../server/src/packet_handlers/interaction.rs | 35 +++++++++- feather/server/src/systems/player_join.rs | 4 +- libcraft/items/src/inventory_slot.rs | 14 ++++ 6 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 feather/common/src/block_break.rs diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs new file mode 100644 index 000000000..f3bef13a7 --- /dev/null +++ b/feather/common/src/block_break.rs @@ -0,0 +1,66 @@ +use base::{ValidBlockPosition, ItemStack}; +use ecs::{SystemExecutor, SysResult, EntityBuilder}; +use quill_common::entity_init::EntityInit; + +use crate::{Game, World}; + +pub type BlockBreaker = Option; +pub struct ActiveBlockBreaker { + pub position: ValidBlockPosition, + pub drop_item: bool, + pub ticks_remaining: u32, +} +impl ActiveBlockBreaker { + pub fn tick(&mut self) -> bool { + self.ticks_remaining = self.ticks_remaining.saturating_sub(1); + self.ticks_remaining == 0 + } + pub fn break_block(self, game: &mut Game) -> SysResult { + let target_block = match game.block(self.position) { + Some(b) => b, + None => anyhow::bail!("cannot break unloaded block") + }; + game.break_block(self.position); + if let Some(_item_drop) = base::Item::from_name(target_block.kind().name()) { + if !self.drop_item { + return Ok(()) + } + let mut item_entity = EntityBuilder::new(); + crate::entities::item::build_default(&mut item_entity); + let builder = game.create_entity_builder(self.position.position(), EntityInit::Item); + game.spawn_entity(builder); + } + Ok(()) + } + pub fn new_player(_world: &mut World, block_pos: ValidBlockPosition, _mainhand: Option<&ItemStack>, _offhand: Option<&ItemStack>) -> Option { + // TODO + Some(Self { + position: block_pos, + drop_item: true, + ticks_remaining: 20, + }) + } +} + +pub fn register(systems: &mut SystemExecutor) { + systems.add_system(process_block_breaking); +} + +fn process_block_breaking(game: &mut Game) -> SysResult { + let mut break_queue = vec![]; + for (entity, breaker) in game.ecs.query::<&mut BlockBreaker>().iter() { + if let Some(active) = breaker { + if active.tick() { + break_queue.push(entity); + } + } + } + for entity in break_queue.into_iter() { + // Set block breakers to None + let breaker = { + game.ecs.get_mut::(entity)?.take().unwrap() + }; + breaker.break_block(game)?; + } + Ok(()) +} \ No newline at end of file diff --git a/feather/common/src/lib.rs b/feather/common/src/lib.rs index 9f9e7348a..bc91035b6 100644 --- a/feather/common/src/lib.rs +++ b/feather/common/src/lib.rs @@ -32,12 +32,15 @@ pub mod entities; pub mod interactable; +pub mod block_break; + /// Registers gameplay systems with the given `Game` and `SystemExecutor`. pub fn register(game: &mut Game, systems: &mut SystemExecutor) { view::register(game, systems); chunk::loading::register(game, systems); chunk::entities::register(systems); interactable::register(game); + block_break::register(systems); game.add_entity_spawn_callback(entities::add_entity_components); } diff --git a/feather/server/src/client.rs b/feather/server/src/client.rs index 20211f39b..c8808ae34 100644 --- a/feather/server/src/client.rs +++ b/feather/server/src/client.rs @@ -20,7 +20,7 @@ use common::{ use libcraft_items::InventorySlot; use packets::server::{Particle, SetSlot, SpawnLivingEntity, UpdateLight, WindowConfirmation}; use protocol::packets::server::{ - EntityPosition, EntityPositionAndRotation, EntityTeleport, HeldItemChange, PlayerAbilities, + EntityPosition, EntityPositionAndRotation, EntityTeleport, HeldItemChange, PlayerAbilities, PlayerDiggingStatus, AcknowledgePlayerDigging, BlockBreakAnimation, }; use protocol::{ packets::{ @@ -602,6 +602,23 @@ impl Client { self.send_packet(HeldItemChange { slot }); } + pub fn acknowledge_player_digging(&self, position: ValidBlockPosition, block: BlockId, status: PlayerDiggingStatus, successful: bool) { + self.send_packet(AcknowledgePlayerDigging { + position, + block, + status, + successful, + }) + } + + pub fn block_break_animation(&self, entity_id: i32, position: ValidBlockPosition, destroy_stage: u8) { + self.send_packet(BlockBreakAnimation { + entity_id, + position, + destroy_stage, + }) + } + fn register_entity(&self, network_id: NetworkId) { self.sent_entities.borrow_mut().insert(network_id); } diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 1c6f69d9c..65bf54c83 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -1,5 +1,7 @@ use crate::{ClientId, NetworkId, Server}; +use base::Gamemode; use base::inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; +use common::block_break::{BlockBreaker, ActiveBlockBreaker}; use common::entities::player::HotbarSlot; use common::interactable::InteractableRegistry; use common::{Game, Window}; @@ -119,8 +121,32 @@ pub fn handle_player_digging( ) -> SysResult { log::trace!("Got player digging with status {:?}", packet.status); match packet.status { - PlayerDiggingStatus::StartDigging | PlayerDiggingStatus::CancelDigging => { - game.break_block(packet.position); + PlayerDiggingStatus::StartDigging => { + if matches!(*game.ecs.get::(player)?, Gamemode::Creative | Gamemode::Spectator) { + game.break_block(packet.position); + } else { + let mut breaker = game.ecs.get_mut::(player)?; + let window = game.ecs.get::(player)?; + let hotbar_slot = game.ecs.get::(player)?.get(); + let main = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; + let offh = window.item(SLOT_OFFHAND)?; + let _ = breaker.insert(ActiveBlockBreaker::new_player(&mut game.world, packet.position, main.item_stack(), offh.item_stack()).unwrap()); + } + + Ok(()) + }, + PlayerDiggingStatus::CancelDigging => { + game.ecs.get_mut::(player)?.take(); + let client = server.clients.get(*game.ecs.get(player)?).unwrap(); + let block = match game.block(packet.position) { Some(s)=>s,None=>return Ok(())}; + client.acknowledge_player_digging(packet.position, block, protocol::packets::server::PlayerDiggingStatus::Cancelled, false); + Ok(()) + }, + PlayerDiggingStatus::FinishDigging => { + let success = game.ecs.get::(player)?.is_some(); + let client = server.clients.get(*game.ecs.get(player)?).unwrap(); + let block = match game.block(packet.position) { Some(s)=>s,None=>return Ok(())}; + client.acknowledge_player_digging(packet.position, block, protocol::packets::server::PlayerDiggingStatus::Finished, success); Ok(()) } PlayerDiggingStatus::SwapItemInHand => { @@ -145,7 +171,10 @@ pub fn handle_player_digging( Ok(()) } - _ => Ok(()), + PlayerDiggingStatus::DropItemStack => Ok(()), + PlayerDiggingStatus::DropItem => Ok(()), + PlayerDiggingStatus::ShootArrow => Ok(()), + } } diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index 24ddf090d..9a7334195 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -1,3 +1,4 @@ +use common::block_break::BlockBreaker; use libcraft_items::InventorySlot; use log::debug; @@ -108,7 +109,7 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) Position::default().chunk(), server.options.view_distance, )) - .add(gamemode) + .add(Gamemode::Survival) .add(previous_gamemode) .add(Name::new(client.username())) .add(client.uuid()) @@ -123,6 +124,7 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) .map(|data| data.animal.health) .unwrap_or(20.0), )) + .add(BlockBreaker::None) .add(abilities.walk_speed) .add(abilities.fly_speed) .add(abilities.is_flying) diff --git a/libcraft/items/src/inventory_slot.rs b/libcraft/items/src/inventory_slot.rs index bdaee8fe4..86523c871 100644 --- a/libcraft/items/src/inventory_slot.rs +++ b/libcraft/items/src/inventory_slot.rs @@ -188,6 +188,20 @@ impl InventorySlot { InventorySlot::Empty => None, } } + + pub fn item_stack(&self) -> Option<&ItemStack> { + match self { + InventorySlot::Filled(f) => Some(f), + InventorySlot::Empty => None, + } + } + + pub fn item_stack_mut(&mut self) -> Option<&mut ItemStack> { + match self { + InventorySlot::Filled(f) => Some(f), + InventorySlot::Empty => None, + } + } } #[cfg(test)] From 3e5d7e8b18a898a369d4c7a8129477755eb022e8 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Fri, 31 Dec 2021 00:01:35 +0100 Subject: [PATCH 02/15] Block breaking progress sent to client --- feather/common/src/block_break.rs | 56 +++++++++++++++---- feather/server/src/client.rs | 20 +++++-- .../server/src/packet_handlers/interaction.rs | 55 ++++++++++++++---- feather/server/src/systems/block.rs | 14 ++++- 4 files changed, 116 insertions(+), 29 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index f3bef13a7..ece08c9ce 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -1,29 +1,35 @@ -use base::{ValidBlockPosition, ItemStack}; -use ecs::{SystemExecutor, SysResult, EntityBuilder}; +use base::{ItemStack, ValidBlockPosition}; +use ecs::{EntityBuilder, SysResult, SystemExecutor}; use quill_common::entity_init::EntityInit; +pub struct DestroyStateChange(pub ValidBlockPosition, pub u8); + use crate::{Game, World}; pub type BlockBreaker = Option; +#[derive(Clone, Copy)] pub struct ActiveBlockBreaker { pub position: ValidBlockPosition, pub drop_item: bool, + pub total_ticks: u32, pub ticks_remaining: u32, } impl ActiveBlockBreaker { - pub fn tick(&mut self) -> bool { + pub fn tick(&mut self) -> (bool, bool) { + let before = self.destroy_stage(); self.ticks_remaining = self.ticks_remaining.saturating_sub(1); - self.ticks_remaining == 0 + let after = self.destroy_stage(); + (self.ticks_remaining == 0, before != after) } pub fn break_block(self, game: &mut Game) -> SysResult { let target_block = match game.block(self.position) { Some(b) => b, - None => anyhow::bail!("cannot break unloaded block") + None => anyhow::bail!("cannot break unloaded block"), }; game.break_block(self.position); if let Some(_item_drop) = base::Item::from_name(target_block.kind().name()) { if !self.drop_item { - return Ok(()) + return Ok(()); } let mut item_entity = EntityBuilder::new(); crate::entities::item::build_default(&mut item_entity); @@ -32,14 +38,23 @@ impl ActiveBlockBreaker { } Ok(()) } - pub fn new_player(_world: &mut World, block_pos: ValidBlockPosition, _mainhand: Option<&ItemStack>, _offhand: Option<&ItemStack>) -> Option { + pub fn new_player( + _world: &mut World, + block_pos: ValidBlockPosition, + _mainhand: Option<&ItemStack>, + _offhand: Option<&ItemStack>, + ) -> Option { // TODO Some(Self { position: block_pos, drop_item: true, + total_ticks: 20, ticks_remaining: 20, }) } + pub fn destroy_stage(&self) -> u8 { + 9 - (self.ticks_remaining as f32 / self.total_ticks as f32 * 9.0).round() as u8 + } } pub fn register(systems: &mut SystemExecutor) { @@ -47,20 +62,39 @@ pub fn register(systems: &mut SystemExecutor) { } fn process_block_breaking(game: &mut Game) -> SysResult { - let mut break_queue = vec![]; + let mut break_queue = vec![]; + let mut update_queue = vec![]; for (entity, breaker) in game.ecs.query::<&mut BlockBreaker>().iter() { if let Some(active) = breaker { - if active.tick() { + let (break_block, update_stage) = active.tick(); + if update_stage { + update_queue.push(entity); + } + if break_block { break_queue.push(entity); } } } + for entity in update_queue { + let breaker = { game.ecs.get_mut::(entity).unwrap().unwrap() }; + game.ecs.insert_entity_event( + entity, + DestroyStateChange(breaker.position, breaker.destroy_stage()), + )?; + } for entity in break_queue.into_iter() { // Set block breakers to None let breaker = { - game.ecs.get_mut::(entity)?.take().unwrap() + game.ecs + .get_mut::(entity) + .unwrap() + .take() + .unwrap() }; + game.ecs + .insert_entity_event(entity, DestroyStateChange(breaker.position, 10)) + .unwrap(); breaker.break_block(game)?; } Ok(()) -} \ No newline at end of file +} diff --git a/feather/server/src/client.rs b/feather/server/src/client.rs index c8808ae34..e8318092e 100644 --- a/feather/server/src/client.rs +++ b/feather/server/src/client.rs @@ -20,7 +20,8 @@ use common::{ use libcraft_items::InventorySlot; use packets::server::{Particle, SetSlot, SpawnLivingEntity, UpdateLight, WindowConfirmation}; use protocol::packets::server::{ - EntityPosition, EntityPositionAndRotation, EntityTeleport, HeldItemChange, PlayerAbilities, PlayerDiggingStatus, AcknowledgePlayerDigging, BlockBreakAnimation, + AcknowledgePlayerDigging, BlockBreakAnimation, EntityPosition, EntityPositionAndRotation, + EntityTeleport, HeldItemChange, PlayerAbilities, PlayerDiggingStatus, }; use protocol::{ packets::{ @@ -602,7 +603,13 @@ impl Client { self.send_packet(HeldItemChange { slot }); } - pub fn acknowledge_player_digging(&self, position: ValidBlockPosition, block: BlockId, status: PlayerDiggingStatus, successful: bool) { + pub fn acknowledge_player_digging( + &self, + position: ValidBlockPosition, + block: BlockId, + status: PlayerDiggingStatus, + successful: bool, + ) { self.send_packet(AcknowledgePlayerDigging { position, block, @@ -611,9 +618,14 @@ impl Client { }) } - pub fn block_break_animation(&self, entity_id: i32, position: ValidBlockPosition, destroy_stage: u8) { + pub fn block_break_animation( + &self, + entity_id: u32, + position: ValidBlockPosition, + destroy_stage: u8, + ) { self.send_packet(BlockBreakAnimation { - entity_id, + entity_id: i32::from_le_bytes(entity_id.to_le_bytes()), position, destroy_stage, }) diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 65bf54c83..32dad1dfe 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -1,7 +1,7 @@ use crate::{ClientId, NetworkId, Server}; -use base::Gamemode; use base::inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; -use common::block_break::{BlockBreaker, ActiveBlockBreaker}; +use base::Gamemode; +use common::block_break::{ActiveBlockBreaker, BlockBreaker, DestroyStateChange}; use common::entities::player::HotbarSlot; use common::interactable::InteractableRegistry; use common::{Game, Window}; @@ -122,7 +122,10 @@ pub fn handle_player_digging( log::trace!("Got player digging with status {:?}", packet.status); match packet.status { PlayerDiggingStatus::StartDigging => { - if matches!(*game.ecs.get::(player)?, Gamemode::Creative | Gamemode::Spectator) { + if matches!( + *game.ecs.get::(player)?, + Gamemode::Creative | Gamemode::Spectator + ) { game.break_block(packet.position); } else { let mut breaker = game.ecs.get_mut::(player)?; @@ -130,23 +133,52 @@ pub fn handle_player_digging( let hotbar_slot = game.ecs.get::(player)?.get(); let main = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; let offh = window.item(SLOT_OFFHAND)?; - let _ = breaker.insert(ActiveBlockBreaker::new_player(&mut game.world, packet.position, main.item_stack(), offh.item_stack()).unwrap()); + let _ = breaker.insert( + ActiveBlockBreaker::new_player( + &mut game.world, + packet.position, + main.item_stack(), + offh.item_stack(), + ) + .unwrap(), + ); } Ok(()) - }, + } PlayerDiggingStatus::CancelDigging => { - game.ecs.get_mut::(player)?.take(); + let breaker = game.ecs.get_mut::(player)?.take(); + if let Some(s) = breaker { + game.ecs + .insert_entity_event(player, DestroyStateChange(s.position, 10))?; + } + let client = server.clients.get(*game.ecs.get(player)?).unwrap(); - let block = match game.block(packet.position) { Some(s)=>s,None=>return Ok(())}; - client.acknowledge_player_digging(packet.position, block, protocol::packets::server::PlayerDiggingStatus::Cancelled, false); + let block = match game.block(packet.position) { + Some(s) => s, + None => return Ok(()), + }; + client.acknowledge_player_digging( + packet.position, + block, + protocol::packets::server::PlayerDiggingStatus::Cancelled, + false, + ); Ok(()) - }, + } PlayerDiggingStatus::FinishDigging => { let success = game.ecs.get::(player)?.is_some(); let client = server.clients.get(*game.ecs.get(player)?).unwrap(); - let block = match game.block(packet.position) { Some(s)=>s,None=>return Ok(())}; - client.acknowledge_player_digging(packet.position, block, protocol::packets::server::PlayerDiggingStatus::Finished, success); + let block = match game.block(packet.position) { + Some(s) => s, + None => return Ok(()), + }; + client.acknowledge_player_digging( + packet.position, + block, + protocol::packets::server::PlayerDiggingStatus::Finished, + success, + ); Ok(()) } PlayerDiggingStatus::SwapItemInHand => { @@ -174,7 +206,6 @@ pub fn handle_player_digging( PlayerDiggingStatus::DropItemStack => Ok(()), PlayerDiggingStatus::DropItem => Ok(()), PlayerDiggingStatus::ShootArrow => Ok(()), - } } diff --git a/feather/server/src/systems/block.rs b/feather/server/src/systems/block.rs index 5544da2bd..50f75b7ad 100644 --- a/feather/server/src/systems/block.rs +++ b/feather/server/src/systems/block.rs @@ -15,7 +15,7 @@ use ahash::AHashMap; use base::{chunk::SECTION_VOLUME, position, ChunkPosition, CHUNK_WIDTH}; -use common::{events::BlockChangeEvent, Game}; +use common::{block_break::DestroyStateChange, events::BlockChangeEvent, Game}; use ecs::{SysResult, SystemExecutor}; use crate::Server; @@ -23,7 +23,17 @@ use crate::Server; pub fn register(systems: &mut SystemExecutor) { systems .group::() - .add_system(broadcast_block_changes); + .add_system(broadcast_block_changes) + .add_system(broadcast_block_destroy_stage_change); +} + +fn broadcast_block_destroy_stage_change(game: &mut Game, server: &mut Server) -> SysResult { + for (entity, event) in game.ecs.query::<&DestroyStateChange>().iter() { + server.broadcast_nearby_with(event.0.position(), |client| { + client.block_break_animation(entity.id(), event.0, event.1); + }); + } + Ok(()) } fn broadcast_block_changes(game: &mut Game, server: &mut Server) -> SysResult { From 316f4901b8e61cfe9f703ba5493413f58801a1e5 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Fri, 31 Dec 2021 01:39:48 +0100 Subject: [PATCH 03/15] Block breaking speed calculation --- feather/common/src/block_break.rs | 59 +++++++++++++++++-- .../server/src/packet_handlers/interaction.rs | 6 +- libcraft/items/src/item_stack.rs | 10 ++++ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index ece08c9ce..8cdd62704 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -1,5 +1,6 @@ use base::{ItemStack, ValidBlockPosition}; use ecs::{EntityBuilder, SysResult, SystemExecutor}; +use libcraft_items::EnchantmentKind; use quill_common::entity_init::EntityInit; pub struct DestroyStateChange(pub ValidBlockPosition, pub u8); @@ -39,17 +40,63 @@ impl ActiveBlockBreaker { Ok(()) } pub fn new_player( - _world: &mut World, + world: &mut World, block_pos: ValidBlockPosition, - _mainhand: Option<&ItemStack>, - _offhand: Option<&ItemStack>, + main_hand: Option<&ItemStack>, ) -> Option { - // TODO + let block = world.block_at(block_pos)?.kind(); + if !block.diggable() { + return None; + } + let harvestable = match (block.harvest_tools(), main_hand) { + (None, None | Some(_)) => true, + (Some(_), None) => false, + (Some(tools), Some(tool)) => tools.contains(&tool.item()), + }; + let dig_multiplier = block + .dig_multipliers() + .iter() + .find_map(|(item, speed)| { + main_hand + .map(|e| { + if e.item() == *item { + Some(*speed) + } else { + None + } + }) + .flatten() + }) + .unwrap_or(1.0); + let effi_level = main_hand + .map(ItemStack::metadata) + .flatten() + .map(|meta| { + meta.enchantments().iter().find_map(|ench| { + if ench.kind() == EnchantmentKind::Efficiency { + Some(ench.level()) + } else { + None + } + }) + }) + .flatten(); + let effi_speed = effi_level.map(|level| level * level + 1).unwrap_or(0) as f32; + let damage = if harvestable { + (dig_multiplier + effi_speed) / block.hardness() / 30.0 + } else { + 1.0 / block.hardness() / 100.0 + }; + let ticks = if damage > 1.0 { + 0 + } else { + (1.0 / damage).ceil() as u32 - 4 + }; Some(Self { position: block_pos, drop_item: true, - total_ticks: 20, - ticks_remaining: 20, + total_ticks: ticks, + ticks_remaining: ticks, }) } pub fn destroy_stage(&self) -> u8 { diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 32dad1dfe..3dbb1d79a 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -131,14 +131,12 @@ pub fn handle_player_digging( let mut breaker = game.ecs.get_mut::(player)?; let window = game.ecs.get::(player)?; let hotbar_slot = game.ecs.get::(player)?.get(); - let main = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; - let offh = window.item(SLOT_OFFHAND)?; + let main_hand = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; let _ = breaker.insert( ActiveBlockBreaker::new_player( &mut game.world, packet.position, - main.item_stack(), - offh.item_stack(), + main_hand.item_stack(), ) .unwrap(), ); diff --git a/libcraft/items/src/item_stack.rs b/libcraft/items/src/item_stack.rs index 146af7243..325cd9cbe 100644 --- a/libcraft/items/src/item_stack.rs +++ b/libcraft/items/src/item_stack.rs @@ -324,6 +324,10 @@ impl ItemStack { pub fn stack_size(&self) -> u32 { self.item.stack_size() } + + pub fn metadata(&self) -> Option<&ItemStackMeta> { + self.meta.as_ref() + } } /// An error type that may be returned when performing @@ -355,6 +359,12 @@ impl ItemStackMeta { enchantments: vec![], } } + pub fn enchantments(&self) -> &[Enchantment] { + &self.enchantments + } + pub fn enchantments_mut(&mut self) -> &mut Vec { + &mut self.enchantments + } } pub struct ItemStackBuilder { From c36fe98e5a59d581d7d36bd9f3a8a896e5129988 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Sun, 2 Jan 2022 18:05:06 +0100 Subject: [PATCH 04/15] WIP Breaking blocks in survival works 90% of the time Instamine and creative mostly untested Time to mine blocks off by 20% for unknown reasons Netherite tools are not included in `BlockKind::harvest_tools` nor in `BlockKind::dig_multipliers` --- feather/common/src/block_break.rs | 195 ++++++++++++++---- .../server/src/packet_handlers/interaction.rs | 65 ++++-- feather/server/src/systems/player_join.rs | 2 +- 3 files changed, 199 insertions(+), 63 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 8cdd62704..93cf37958 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -1,28 +1,106 @@ use base::{ItemStack, ValidBlockPosition}; use ecs::{EntityBuilder, SysResult, SystemExecutor}; use libcraft_items::EnchantmentKind; -use quill_common::entity_init::EntityInit; +use quill_common::{entities::Player, entity_init::EntityInit}; pub struct DestroyStateChange(pub ValidBlockPosition, pub u8); use crate::{Game, World}; -pub type BlockBreaker = Option; -#[derive(Clone, Copy)] -pub struct ActiveBlockBreaker { - pub position: ValidBlockPosition, - pub drop_item: bool, - pub total_ticks: u32, - pub ticks_remaining: u32, +#[derive(Clone)] +pub enum BlockBreaker { + Active(ActiveBreaker), + Finished(FinishedBreaker), + Inactive, } -impl ActiveBlockBreaker { +impl BlockBreaker { + pub fn new( + world: &mut World, + block_pos: ValidBlockPosition, + equipped_item: Option<&ItemStack>, + ) -> Self { + ActiveBreaker::new(world, block_pos, equipped_item) + .map(Self::Active) + .unwrap_or(Self::Inactive) + } + pub fn destroy_change_event(&self) -> Option { + Some(DestroyStateChange(self.position()?, self.destroy_stage())) + } + pub fn position(&self) -> Option { + match self { + BlockBreaker::Active(a) => Some(a.position), + BlockBreaker::Finished(f) => Some(f.position), + BlockBreaker::Inactive => None, + } + } + pub fn active(&self) -> Option<&ActiveBreaker> { + match self { + Self::Active(a) => Some(a), + _ => None, + } + } + pub fn finished(&self) -> Option<&FinishedBreaker> { + match self { + Self::Finished(f) => Some(f), + _ => None, + } + } pub fn tick(&mut self) -> (bool, bool) { - let before = self.destroy_stage(); - self.ticks_remaining = self.ticks_remaining.saturating_sub(1); - let after = self.destroy_stage(); - (self.ticks_remaining == 0, before != after) + let (block_break, stage_update) = if let Self::Active(breaker) = self { + breaker.tick() + } else { + (false, false) + }; + if block_break { + let fin = match self { + Self::Active(a) => a.clone().finish(), + _ => unreachable!(), + }; + *self = Self::Finished(fin); + } + (block_break, stage_update) } - pub fn break_block(self, game: &mut Game) -> SysResult { + pub fn destroy_stage(&self) -> u8 { + match self { + BlockBreaker::Active(a) => a.destroy_stage(), + _ => 10, + } + } + pub fn cancel(&mut self) { + *self = Self::Inactive; + } + pub fn matches_position(&self, pos: ValidBlockPosition) -> bool { + match self { + BlockBreaker::Active(a) => a.position == pos, + BlockBreaker::Finished(f) => f.position == pos, + BlockBreaker::Inactive => true, + } + } + pub fn try_finish(&mut self) -> Option { + let this = self.clone(); + match this { + BlockBreaker::Active(a) => { + if a.ticks_remaining == 1 { + let fin = a.finish(); + *self = Self::Finished(fin.clone()); + Some(fin) + } else { + None + } + } + BlockBreaker::Finished(f) => Some(f), + BlockBreaker::Inactive => None, + } + } +} +#[derive(Clone)] +pub struct FinishedBreaker { + pub position: ValidBlockPosition, + pub drop_item: bool, + pub fake_finished: bool, +} +impl FinishedBreaker { + pub fn break_block(&self, game: &mut Game) -> SysResult { let target_block = match game.block(self.position) { Some(b) => b, None => anyhow::bail!("cannot break unloaded block"), @@ -39,16 +117,34 @@ impl ActiveBlockBreaker { } Ok(()) } - pub fn new_player( +} +#[derive(Clone)] +pub struct ActiveBreaker { + pub position: ValidBlockPosition, + pub drop_item: bool, + pub fake_finished: bool, + pub total_ticks: u32, + pub ticks_remaining: u32, +} +impl ActiveBreaker { + pub fn tick(&mut self) -> (bool, bool) { + let before = self.destroy_stage(); + self.ticks_remaining = self.ticks_remaining.saturating_sub(1); + let after = self.destroy_stage(); + let break_block = self.ticks_remaining == 0; + let change_stage = before != after || break_block; + (break_block, change_stage) + } + pub fn new( world: &mut World, block_pos: ValidBlockPosition, - main_hand: Option<&ItemStack>, + equipped_item: Option<&ItemStack>, ) -> Option { let block = world.block_at(block_pos)?.kind(); if !block.diggable() { return None; } - let harvestable = match (block.harvest_tools(), main_hand) { + let harvestable = match (block.harvest_tools(), equipped_item) { (None, None | Some(_)) => true, (Some(_), None) => false, (Some(tools), Some(tool)) => tools.contains(&tool.item()), @@ -57,7 +153,7 @@ impl ActiveBlockBreaker { .dig_multipliers() .iter() .find_map(|(item, speed)| { - main_hand + equipped_item .map(|e| { if e.item() == *item { Some(*speed) @@ -68,7 +164,7 @@ impl ActiveBlockBreaker { .flatten() }) .unwrap_or(1.0); - let effi_level = main_hand + let effi_level = equipped_item .map(ItemStack::metadata) .flatten() .map(|meta| { @@ -90,17 +186,37 @@ impl ActiveBlockBreaker { let ticks = if damage > 1.0 { 0 } else { - (1.0 / damage).ceil() as u32 - 4 + (1.0 / damage / 1.2).ceil() as u32 }; + println!( + "Mining {} with {} takes {} ticks", + block.display_name(), + equipped_item + .map(|e| e.get_item().item().display_name()) + .unwrap_or("bare hands"), + ticks + ); Some(Self { position: block_pos, drop_item: true, + fake_finished: false, total_ticks: ticks, ticks_remaining: ticks, }) } pub fn destroy_stage(&self) -> u8 { - 9 - (self.ticks_remaining as f32 / self.total_ticks as f32 * 9.0).round() as u8 + if self.fake_finished { + 10 + } else { + 9 - (self.ticks_remaining as f32 / self.total_ticks as f32 * 9.0).round() as u8 + } + } + pub fn finish(self) -> FinishedBreaker { + FinishedBreaker { + position: self.position, + drop_item: self.drop_item, + fake_finished: self.fake_finished, + } } } @@ -112,35 +228,28 @@ fn process_block_breaking(game: &mut Game) -> SysResult { let mut break_queue = vec![]; let mut update_queue = vec![]; for (entity, breaker) in game.ecs.query::<&mut BlockBreaker>().iter() { - if let Some(active) = breaker { - let (break_block, update_stage) = active.tick(); - if update_stage { - update_queue.push(entity); - } - if break_block { + let (break_block, update_stage) = breaker.tick(); + if update_stage { + update_queue.push(entity); + } + // Break block when client requests to finish in order to prevent desyncs + if break_block { + if breaker.finished().unwrap().fake_finished || !game.ecs.get::(entity).is_ok() + { break_queue.push(entity); } } } for entity in update_queue { - let breaker = { game.ecs.get_mut::(entity).unwrap().unwrap() }; - game.ecs.insert_entity_event( - entity, - DestroyStateChange(breaker.position, breaker.destroy_stage()), - )?; + let event = game + .ecs + .get_mut::(entity)? + .destroy_change_event() + .unwrap(); + game.ecs.insert_entity_event(entity, event)?; } for entity in break_queue.into_iter() { - // Set block breakers to None - let breaker = { - game.ecs - .get_mut::(entity) - .unwrap() - .take() - .unwrap() - }; - game.ecs - .insert_entity_event(entity, DestroyStateChange(breaker.position, 10)) - .unwrap(); + let breaker = game.ecs.get::(entity)?.finished().unwrap().clone(); breaker.break_block(game)?; } Ok(()) diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 3dbb1d79a..f30e3182f 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -1,7 +1,7 @@ use crate::{ClientId, NetworkId, Server}; use base::inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; use base::Gamemode; -use common::block_break::{ActiveBlockBreaker, BlockBreaker, DestroyStateChange}; +use common::block_break::{ActiveBreaker, BlockBreaker, DestroyStateChange}; use common::entities::player::HotbarSlot; use common::interactable::InteractableRegistry; use common::{Game, Window}; @@ -120,6 +120,7 @@ pub fn handle_player_digging( player: Entity, ) -> SysResult { log::trace!("Got player digging with status {:?}", packet.status); + let client = server.clients.get(*game.ecs.get(player)?).unwrap(); match packet.status { PlayerDiggingStatus::StartDigging => { if matches!( @@ -132,26 +133,32 @@ pub fn handle_player_digging( let window = game.ecs.get::(player)?; let hotbar_slot = game.ecs.get::(player)?.get(); let main_hand = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; - let _ = breaker.insert( - ActiveBlockBreaker::new_player( - &mut game.world, - packet.position, - main_hand.item_stack(), - ) - .unwrap(), + *breaker = BlockBreaker::Active( + ActiveBreaker::new(&mut game.world, packet.position, main_hand.item_stack()) + .unwrap(), ); } - + let block = match game.block(packet.position) { + Some(s) => s, + None => return Ok(()), + }; + client.acknowledge_player_digging( + packet.position, + block, + protocol::packets::server::PlayerDiggingStatus::Started, + true, + ); Ok(()) } PlayerDiggingStatus::CancelDigging => { - let breaker = game.ecs.get_mut::(player)?.take(); - if let Some(s) = breaker { - game.ecs - .insert_entity_event(player, DestroyStateChange(s.position, 10))?; - } - - let client = server.clients.get(*game.ecs.get(player)?).unwrap(); + let success = { + let mut breaker = game.ecs.get_mut::(player)?; + let a = breaker.matches_position(packet.position); + breaker.cancel(); + a + }; + game.ecs + .insert_entity_event(player, DestroyStateChange(packet.position, 10))?; let block = match game.block(packet.position) { Some(s) => s, None => return Ok(()), @@ -160,13 +167,33 @@ pub fn handle_player_digging( packet.position, block, protocol::packets::server::PlayerDiggingStatus::Cancelled, - false, + success, ); Ok(()) } PlayerDiggingStatus::FinishDigging => { - let success = game.ecs.get::(player)?.is_some(); - let client = server.clients.get(*game.ecs.get(player)?).unwrap(); + let mut finished = None; + let success = { + let mut breaker = game.ecs.get_mut::(player)?; + let success = if let Some(f) = breaker.try_finish() { + finished = Some(f); + breaker.cancel(); + true + } else { + if let BlockBreaker::Active(a) = &mut *breaker { + a.fake_finished = true; + println!("{} ticks remaining", a.ticks_remaining); + } + false + }; + success && breaker.matches_position(packet.position) + }; + if success { + println!("confirm"); + finished.unwrap().break_block(game)?; + } + game.ecs + .insert_entity_event(player, DestroyStateChange(packet.position, 10))?; let block = match game.block(packet.position) { Some(s) => s, None => return Ok(()), diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index 9a7334195..a3ccf0d8e 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -124,7 +124,7 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) .map(|data| data.animal.health) .unwrap_or(20.0), )) - .add(BlockBreaker::None) + .add(BlockBreaker::Inactive) .add(abilities.walk_speed) .add(abilities.fly_speed) .add(abilities.is_flying) From bc78067fb387f420403e39c82d2ffe48eff23ab0 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Sun, 2 Jan 2022 22:14:29 +0100 Subject: [PATCH 05/15] Please formatter and clippy --- feather/common/src/block_break.rs | 20 ++++++++++++-------- feather/server/src/systems/player_join.rs | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 93cf37958..1a633444b 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -1,4 +1,4 @@ -use base::{ItemStack, ValidBlockPosition}; +use base::{BlockKind, ItemStack, ValidBlockPosition}; use ecs::{EntityBuilder, SysResult, SystemExecutor}; use libcraft_items::EnchantmentKind; use quill_common::{entities::Player, entity_init::EntityInit}; @@ -141,7 +141,7 @@ impl ActiveBreaker { equipped_item: Option<&ItemStack>, ) -> Option { let block = world.block_at(block_pos)?.kind(); - if !block.diggable() { + if !block.diggable() || block == BlockKind::Air { return None; } let harvestable = match (block.harvest_tools(), equipped_item) { @@ -233,11 +233,10 @@ fn process_block_breaking(game: &mut Game) -> SysResult { update_queue.push(entity); } // Break block when client requests to finish in order to prevent desyncs - if break_block { - if breaker.finished().unwrap().fake_finished || !game.ecs.get::(entity).is_ok() - { - break_queue.push(entity); - } + if break_block && breaker.finished().unwrap().fake_finished + || game.ecs.get::(entity).is_err() + { + break_queue.push(entity); } } for entity in update_queue { @@ -249,7 +248,12 @@ fn process_block_breaking(game: &mut Game) -> SysResult { game.ecs.insert_entity_event(entity, event)?; } for entity in break_queue.into_iter() { - let breaker = game.ecs.get::(entity)?.finished().unwrap().clone(); + let breaker = game + .ecs + .get::(entity)? + .finished() + .unwrap() + .clone(); breaker.break_block(game)?; } Ok(()) diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index a3ccf0d8e..e05daa56f 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -109,7 +109,7 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) Position::default().chunk(), server.options.view_distance, )) - .add(Gamemode::Survival) + .add(Gamemode::Creative) .add(previous_gamemode) .add(Name::new(client.username())) .add(client.uuid()) From 43538c695b8a24d0afb46a0ec00733ae984d29ff Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Sun, 23 Jan 2022 16:56:23 +0100 Subject: [PATCH 06/15] Merge origin/main --- .gitignore | 1 + Cargo.lock | 15 +- Cargo.toml | 1 + README.md | 4 +- feather/common/src/chunk/entities.rs | 6 +- feather/common/src/chunk/loading.rs | 7 +- feather/common/src/events.rs | 15 -- feather/common/src/game.rs | 3 +- feather/common/src/view.rs | 6 +- feather/plugin-host/Cargo.toml | 1 + feather/plugin-host/src/context.rs | 14 ++ feather/plugin-host/src/context/wasm/bump.rs | 19 +- feather/plugin-host/src/host_calls/entity.rs | 11 +- feather/protocol/src/packets/server/play.rs | 146 +++++++++-- feather/server/config.toml | 2 +- feather/server/src/chunk_subscriptions.rs | 7 +- feather/server/src/client.rs | 25 +- feather/server/src/logging.rs | 2 + feather/server/src/main.rs | 3 +- feather/server/src/systems.rs | 2 + feather/server/src/systems/effects.rs | 226 ++++++++++++++++++ .../server/src/systems/entity/spawn_packet.rs | 3 +- feather/server/src/systems/gamemode.rs | 194 +++++++++++++++ feather/server/src/systems/player_join.rs | 3 + feather/server/src/systems/tablist.rs | 20 +- feather/worldgen/src/lib.rs | 8 +- libcraft/effects/src/effects.rs | 47 ++++ libcraft/effects/src/lib.rs | 4 + libcraft/generators/python/effects.py | 25 ++ libcraft/text/src/title.rs | 5 +- quill/api/Cargo.toml | 2 + quill/api/plugin-macro/Cargo.toml | 11 + quill/api/plugin-macro/src/lib.rs | 108 +++++++++ quill/api/src/entity.rs | 7 +- quill/api/src/lib.rs | 97 +------- quill/common/src/component.rs | 18 +- quill/common/src/components_effects.rs | 112 +++++++++ quill/common/src/events.rs | 13 +- quill/common/src/events/change.rs | 24 +- quill/common/src/events/entity.rs | 16 ++ quill/example-plugins/block-access/src/lib.rs | 3 +- quill/example-plugins/block-place/src/lib.rs | 3 +- .../src/lib.rs | 3 +- .../particle-example/src/lib.rs | 3 +- .../example-plugins/plugin-message/src/lib.rs | 3 +- .../example-plugins/query-entities/src/lib.rs | 3 +- quill/example-plugins/simple/src/lib.rs | 3 +- quill/example-plugins/titles/src/lib.rs | 7 +- quill/sys/src/lib.rs | 2 +- 49 files changed, 1042 insertions(+), 221 deletions(-) create mode 100644 feather/server/src/systems/effects.rs create mode 100644 feather/server/src/systems/gamemode.rs create mode 100644 libcraft/effects/src/effects.rs create mode 100644 libcraft/effects/src/lib.rs create mode 100644 libcraft/generators/python/effects.py create mode 100644 quill/api/plugin-macro/Cargo.toml create mode 100644 quill/api/plugin-macro/src/lib.rs create mode 100644 quill/common/src/components_effects.rs create mode 100644 quill/common/src/events/entity.rs diff --git a/.gitignore b/.gitignore index 79c1a3956..d0a8c67d2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ world/ /config.toml +plugins/ # Python cache files (libcraft) **/__pycache__/ diff --git a/Cargo.lock b/Cargo.lock index 728d312fc..3fe12ed98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,6 +912,7 @@ dependencies = [ "quill-common", "quill-plugin-format", "serde", + "serde_json", "tempfile", "vec-arena", "wasmer", @@ -1955,6 +1956,14 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +[[package]] +name = "plugin-macro" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "plugin-message" version = "0.1.0" @@ -2068,8 +2077,10 @@ dependencies = [ "libcraft-core", "libcraft-particles", "libcraft-text", + "plugin-macro", "quill-common", "quill-sys", + "serde_json", "thiserror", "uuid", ] @@ -2807,9 +2818,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c4b46578e..6bf58f42b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "quill/sys", "quill/common", "quill/api", + "quill/api/plugin-macro", "quill/plugin-format", "quill/cargo-quill", diff --git a/README.md b/README.md index d6aa149ce..aa6cfef37 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ In the long term, Feather could be used on larger, more survival-like servers, w ### Ecosystem The Feather ecosystem consists of several repositories: -* [`libcraft`](https://github.com/feather-rs/libcraft), a set of Rust crates providing Minecraft functionality. -* [`quill`](https://github.com/feather-rs/quill), our work-in-progress plugin API. Quill plugins are written in Rust and compiled to WebAssembly. Feather runs them in a sandboxed WebAssembly VM. +* [`libcraft`](https://github.com/feather-rs/feather/tree/main/libcraft), a set of Rust crates providing Minecraft functionality. +* [`quill`](https://github.com/feather-rs/feather/tree/main/quill), our work-in-progress plugin API. Quill plugins are written in Rust and compiled to WebAssembly. Feather runs them in a sandboxed WebAssembly VM. * `feather`, the server software built on top of `libcraft` and `quill`. ### Performance diff --git a/feather/common/src/chunk/entities.rs b/feather/common/src/chunk/entities.rs index 0b666a392..02a2b0db4 100644 --- a/feather/common/src/chunk/entities.rs +++ b/feather/common/src/chunk/entities.rs @@ -1,12 +1,10 @@ use ahash::AHashMap; use base::{ChunkPosition, Position}; use ecs::{Entity, SysResult, SystemExecutor}; +use quill_common::events::{EntityCreateEvent, EntityRemoveEvent}; use utils::vec_remove_item; -use crate::{ - events::{ChunkCrossEvent, EntityCreateEvent, EntityRemoveEvent}, - Game, -}; +use crate::{events::ChunkCrossEvent, Game}; pub fn register(systems: &mut SystemExecutor) { systems.add_system(update_chunk_entities); diff --git a/feather/common/src/chunk/loading.rs b/feather/common/src/chunk/loading.rs index a6bb9971e..7e52d37fa 100644 --- a/feather/common/src/chunk/loading.rs +++ b/feather/common/src/chunk/loading.rs @@ -9,13 +9,10 @@ use std::{ use ahash::AHashMap; use base::ChunkPosition; use ecs::{Entity, SysResult, SystemExecutor}; +use quill_common::events::EntityRemoveEvent; use utils::vec_remove_item; -use crate::{ - chunk::worker::LoadRequest, - events::{EntityRemoveEvent, ViewUpdateEvent}, - Game, -}; +use crate::{chunk::worker::LoadRequest, events::ViewUpdateEvent, Game}; pub fn register(game: &mut Game, systems: &mut SystemExecutor) { game.insert_resource(ChunkLoadState::default()); diff --git a/feather/common/src/events.rs b/feather/common/src/events.rs index 2426c2896..114540089 100644 --- a/feather/common/src/events.rs +++ b/feather/common/src/events.rs @@ -8,10 +8,6 @@ mod plugin_message; pub use block_change::BlockChangeEvent; pub use plugin_message::PluginMessageEvent; -/// Triggered when a player joins the `Game`. -#[derive(Debug)] -pub struct PlayerJoinEvent; - /// Event triggered when a player changes their `View`, /// meaning they crossed into a new chunk. #[derive(Debug)] @@ -62,14 +58,3 @@ pub struct ChunkLoadEvent { pub struct ChunkLoadFailEvent { pub position: ChunkPosition, } - -/// Triggered when an entity is removed from the world. -/// -/// The entity will remain alive for one tick after it is -/// destroyed to allow systems to observe this event. -#[derive(Debug)] -pub struct EntityRemoveEvent; - -/// Triggered when an entity is added into the world. -#[derive(Debug)] -pub struct EntityCreateEvent; diff --git a/feather/common/src/game.rs b/feather/common/src/game.rs index aa61f52cc..64e6bf87d 100644 --- a/feather/common/src/game.rs +++ b/feather/common/src/game.rs @@ -5,12 +5,13 @@ use ecs::{ Ecs, Entity, EntityBuilder, HasEcs, HasResources, NoSuchEntity, Resources, SysResult, SystemExecutor, }; +use quill_common::events::{EntityCreateEvent, EntityRemoveEvent, PlayerJoinEvent}; use quill_common::{entities::Player, entity_init::EntityInit}; use crate::{ chat::{ChatKind, ChatMessage}, chunk::entities::ChunkEntities, - events::{BlockChangeEvent, EntityCreateEvent, EntityRemoveEvent, PlayerJoinEvent}, + events::BlockChangeEvent, ChatBox, World, }; diff --git a/feather/common/src/view.rs b/feather/common/src/view.rs index 325b27a8f..52c404f5b 100644 --- a/feather/common/src/view.rs +++ b/feather/common/src/view.rs @@ -3,11 +3,9 @@ use base::{ChunkPosition, Position}; use ecs::{SysResult, SystemExecutor}; use itertools::Either; use quill_common::components::Name; +use quill_common::events::PlayerJoinEvent; -use crate::{ - events::{PlayerJoinEvent, ViewUpdateEvent}, - Game, -}; +use crate::{events::ViewUpdateEvent, Game}; /// Registers systems to update the `View` of a player. pub fn register(_game: &mut Game, systems: &mut SystemExecutor) { diff --git a/feather/plugin-host/Cargo.toml b/feather/plugin-host/Cargo.toml index 03a13037f..b25394923 100644 --- a/feather/plugin-host/Cargo.toml +++ b/feather/plugin-host/Cargo.toml @@ -25,6 +25,7 @@ tempfile = "3" vec-arena = "1" wasmer = { version = "2", default-features = false, features = [ "jit" ] } wasmer-wasi = { version = "2", default-features = false, features = [ "host-fs", "sys" ] } +serde_json = "1" [features] llvm = [ "wasmer/llvm" ] diff --git a/feather/plugin-host/src/context.rs b/feather/plugin-host/src/context.rs index b52b59671..3e014ddcd 100644 --- a/feather/plugin-host/src/context.rs +++ b/feather/plugin-host/src/context.rs @@ -328,6 +328,20 @@ impl PluginContext { } } + /// Accesses a `json`-encoded value in the plugin's memory space. + pub fn read_json( + &self, + ptr: PluginPtr, + len: u32, + ) -> anyhow::Result { + // SAFETY: we do not return a reference to these + // bytes. + unsafe { + let bytes = self.deref_bytes(ptr.cast(), len)?; + serde_json::from_slice(bytes).map_err(From::from) + } + } + /// Deserializes a component value in the plugin's memory space. pub fn read_component(&self, ptr: PluginPtr, len: u32) -> anyhow::Result { // SAFETY: we do not return a reference to these diff --git a/feather/plugin-host/src/context/wasm/bump.rs b/feather/plugin-host/src/context/wasm/bump.rs index 054725956..6c97db69a 100644 --- a/feather/plugin-host/src/context/wasm/bump.rs +++ b/feather/plugin-host/src/context/wasm/bump.rs @@ -40,8 +40,6 @@ impl WasmBump { deallocate_function, chunks: Vec::new(), }; - let initial_chunk = this.allocate_chunk(None, None)?; - this.chunks.push(initial_chunk); Ok(this) } @@ -94,7 +92,6 @@ impl WasmBump { .context("chunk overflows usize")?, None => INITIAL_CHUNK_SIZE, }; - let mut align = CHUNK_ALIGN; if let Some(min_layout) = min_layout { align = align.max(min_layout.align()); @@ -102,17 +99,13 @@ impl WasmBump { round_up_to(min_layout.size(), align).context("allocation too large")?; new_size = new_size.max(requested_size); } - assert_eq!(align % CHUNK_ALIGN, 0); assert_eq!(new_size % CHUNK_ALIGN, 0); let layout = Layout::from_size_align(new_size, align).context("size or align is 0")?; - assert!(new_size >= previous_size.unwrap_or(0) * 2); - let start = self .allocate_function .call(layout.size() as u32, layout.align() as u32)?; - Ok(Chunk { start, layout, @@ -124,7 +117,7 @@ impl WasmBump { /// all allocated memory. pub fn reset(&mut self) -> anyhow::Result<()> { // Free all but the last chunk. - for chunk in self.chunks.drain(..self.chunks.len() - 1) { + for chunk in self.chunks.drain(..self.chunks.len()) { self.deallocate_function.call( chunk.start, chunk.layout.size() as u32, @@ -132,13 +125,9 @@ impl WasmBump { )?; } - assert_eq!(self.chunks.len(), 1); - - // Reset the last chunk's data pointer. - let last_chunk = self.chunks.last_mut().unwrap(); - last_chunk - .ptr - .set(last_chunk.start + last_chunk.layout.size() as u32); + // Allocate initial chunk + let chunk = self.allocate_chunk(None, None)?; + self.chunks.push(chunk); Ok(()) } diff --git a/feather/plugin-host/src/host_calls/entity.rs b/feather/plugin-host/src/host_calls/entity.rs index f6d443476..0224f6a0c 100644 --- a/feather/plugin-host/src/host_calls/entity.rs +++ b/feather/plugin-host/src/host_calls/entity.rs @@ -17,12 +17,11 @@ pub fn entity_send_message( message_ptr: PluginPtr, message_len: u32, ) -> anyhow::Result<()> { - let message = cx.read_string(message_ptr, message_len)?; + let message = cx.read_json(message_ptr, message_len)?; let entity = Entity::from_bits(entity); - let _ = cx.game_mut().send_message( - entity, - ChatMessage::new(ChatKind::System, Text::from(message)), - ); + let _ = cx + .game_mut() + .send_message(entity, ChatMessage::new(ChatKind::System, message)); Ok(()) } @@ -33,7 +32,7 @@ pub fn entity_send_title( title_ptr: PluginPtr, title_len: u32, ) -> anyhow::Result<()> { - let title = cx.read_bincode(title_ptr, title_len)?; + let title = cx.read_json(title_ptr, title_len)?; let entity = Entity::from_bits(entity); cx.game_mut().send_title(entity, title); Ok(()) diff --git a/feather/protocol/src/packets/server/play.rs b/feather/protocol/src/packets/server/play.rs index 01c83b03f..e52266b58 100644 --- a/feather/protocol/src/packets/server/play.rs +++ b/feather/protocol/src/packets/server/play.rs @@ -1,4 +1,6 @@ -use anyhow::bail; +use std::io::Cursor; + +use anyhow::{anyhow, bail}; use base::{ BlockState, EntityMetadata, Gamemode, ParticleKind, ProfileProperty, ValidBlockPosition, @@ -7,7 +9,7 @@ pub use chunk_data::{ChunkData, ChunkDataKind}; use quill_common::components::PreviousGamemode; pub use update_light::UpdateLight; -use crate::{io::VarLong, Readable, Writeable}; +use crate::{io::VarLong, ProtocolVersion, Readable, Writeable}; use super::*; @@ -334,8 +336,7 @@ packets! { } ChangeGameState { - reason StateReason; - value f32; + state_change GameStateChange; } OpenHorseWindow { @@ -356,22 +357,131 @@ packets! { } } -def_enum! { - StateReason (i8) { - 0 = NoRespawnBlock, - 1 = EndRaining, - 2 = BeginningRain, - 3 = ChangeGameMode, - 4 = WinGame, - 5 = DemoEvent, - 6 = ArrowHitPlayer, - 7 = RainLevelChange, - 8 = ThunderLevelChange, - 9 = PufferfishSting, - 10 = ElderGuardianAppearance, - 11 = EnableRespawnScreen, +#[derive(Debug, Clone)] +pub enum GameStateChange { + /// Sends block.minecraft.spawn.not_valid to client + SendNoRespawnBlockAvailableMessage, + EndRaining, + BeginRaining, + ChangeGamemode { + gamemode: Gamemode, + }, + /// Sent when the player enters an end portal from minecraft:the_end to minecraft:overworld + WinGame { + show_credits: bool, + }, + /// See https://help.minecraft.net/hc/en-us/articles/4408948974989-Minecraft-Java-Edition-Demo-Mode- + DemoEvent(DemoEventType), + /// Sent when any player is struck by an arrow. + ArrowHitAnyPlayer, + /// Seems to change both skycolor and lightning. + RainLevelChange { + /// Possible values are from 0 to 1 + rain_level: f32, + }, + /// Seems to change both skycolor and lightning (same as Rain level change, but doesn't start rain). + /// It also requires rain to render by notchian client. + ThunderLevelChange { + /// Possible values are from 0 to 1 + thunder_level: f32, + }, + PlayPufferfishStingSound, + PlayElderGuardianAppearance, + /// Send when doImmediateRespawn gamerule changes. + EnableRespawnScreen { + enable: bool, + }, +} + +#[derive(Debug, Clone)] +pub enum DemoEventType { + ShowWelcomeToDemoScreen, + TellMovementControls, + TellJumpControl, + TellInventoryControl, + TellDemoIsOver, +} + +impl Writeable for GameStateChange { + fn write(&self, buffer: &mut Vec, version: ProtocolVersion) -> anyhow::Result<()> { + // Reason + match self { + GameStateChange::SendNoRespawnBlockAvailableMessage => 0u8, + GameStateChange::EndRaining => 1, + GameStateChange::BeginRaining => 2, + GameStateChange::ChangeGamemode { .. } => 3, + GameStateChange::WinGame { .. } => 4, + GameStateChange::DemoEvent(_) => 5, + GameStateChange::ArrowHitAnyPlayer => 6, + GameStateChange::RainLevelChange { .. } => 7, + GameStateChange::ThunderLevelChange { .. } => 8, + GameStateChange::PlayPufferfishStingSound => 9, + GameStateChange::PlayElderGuardianAppearance => 10, + GameStateChange::EnableRespawnScreen { .. } => 11, + } + .write(buffer, version)?; + + // Value + match self { + GameStateChange::ChangeGamemode { gamemode } => *gamemode as u8 as f32, + GameStateChange::WinGame { show_credits } => *show_credits as u8 as f32, + GameStateChange::DemoEvent(DemoEventType::ShowWelcomeToDemoScreen) => 0.0, + GameStateChange::DemoEvent(DemoEventType::TellMovementControls) => 101.0, + GameStateChange::DemoEvent(DemoEventType::TellJumpControl) => 102.0, + GameStateChange::DemoEvent(DemoEventType::TellInventoryControl) => 103.0, + GameStateChange::DemoEvent(DemoEventType::TellDemoIsOver) => 104.0, + GameStateChange::RainLevelChange { rain_level } => *rain_level, + GameStateChange::ThunderLevelChange { thunder_level } => *thunder_level, + GameStateChange::EnableRespawnScreen { enable } => !enable as u8 as f32, + _ => 0.0, + } + .write(buffer, version)?; + + Ok(()) } } + +impl Readable for GameStateChange { + fn read(buffer: &mut Cursor<&[u8]>, version: ProtocolVersion) -> anyhow::Result + where + Self: Sized, + { + let reason = u8::read(buffer, version)?; + let value = f32::read(buffer, version)?; + Ok(match reason { + 0 => GameStateChange::SendNoRespawnBlockAvailableMessage, + 1 => GameStateChange::EndRaining, + 2 => GameStateChange::BeginRaining, + 3 => GameStateChange::ChangeGamemode { + gamemode: Gamemode::from_id(value as u8) + .ok_or(anyhow!("Unsupported gamemode ID"))?, + }, + 4 => GameStateChange::WinGame { + show_credits: value as u8 != 0, + }, + 5 => GameStateChange::DemoEvent(match value as u8 { + 0 => DemoEventType::ShowWelcomeToDemoScreen, + 101 => DemoEventType::TellMovementControls, + 102 => DemoEventType::TellJumpControl, + 103 => DemoEventType::TellInventoryControl, + 104 => DemoEventType::TellDemoIsOver, + other => bail!("Invalid demo event type: {}", other), + }), + 6 => GameStateChange::ArrowHitAnyPlayer, + 7 => GameStateChange::RainLevelChange { rain_level: value }, + 8 => GameStateChange::ThunderLevelChange { + thunder_level: value, + }, + 9 => GameStateChange::PlayPufferfishStingSound, + 10 => GameStateChange::PlayElderGuardianAppearance, + 11 => GameStateChange::EnableRespawnScreen { + enable: value as u8 == 0, + }, + other => bail!("Invalid game state change reason: {}", other), + }) + } +} + packets! { JoinGame { entity_id i32; diff --git a/feather/server/config.toml b/feather/server/config.toml index b0206bc8a..5e9efff7a 100644 --- a/feather/server/config.toml +++ b/feather/server/config.toml @@ -31,7 +31,7 @@ hash = "" # The name of the directory containing the world. name = "world" # The generator to use if the world does not exist. -# Implemented values are: default, flat +# Implemented values are: default, flat, void generator = "default" # The seed to use if the world does not exist. # Leaving this value empty will generate a random seed. diff --git a/feather/server/src/chunk_subscriptions.rs b/feather/server/src/chunk_subscriptions.rs index ccec7a780..fc7484a0f 100644 --- a/feather/server/src/chunk_subscriptions.rs +++ b/feather/server/src/chunk_subscriptions.rs @@ -1,11 +1,8 @@ use ahash::AHashMap; use base::ChunkPosition; -use common::{ - events::{EntityRemoveEvent, ViewUpdateEvent}, - view::View, - Game, -}; +use common::{events::ViewUpdateEvent, view::View, Game}; use ecs::{SysResult, SystemExecutor}; +use quill_common::events::EntityRemoveEvent; use utils::vec_remove_item; use crate::{ClientId, Server}; diff --git a/feather/server/src/client.rs b/feather/server/src/client.rs index e8318092e..d1f00d07a 100644 --- a/feather/server/src/client.rs +++ b/feather/server/src/client.rs @@ -7,6 +7,7 @@ use std::{ use ahash::AHashSet; use flume::{Receiver, Sender}; +use slab::Slab; use uuid::Uuid; use base::{ @@ -20,8 +21,9 @@ use common::{ use libcraft_items::InventorySlot; use packets::server::{Particle, SetSlot, SpawnLivingEntity, UpdateLight, WindowConfirmation}; use protocol::packets::server::{ - AcknowledgePlayerDigging, BlockBreakAnimation, EntityPosition, EntityPositionAndRotation, - EntityTeleport, HeldItemChange, PlayerAbilities, PlayerDiggingStatus, + AcknowledgePlayerDigging, BlockBreakAnimation, ChangeGameState, EntityPosition, + EntityPositionAndRotation, EntityTeleport, GameStateChange, HeldItemChange, PlayerAbilities, + PlayerDiggingStatus, }; use protocol::{ packets::{ @@ -43,7 +45,6 @@ use crate::{ network_id_registry::NetworkId, Options, }; -use slab::Slab; /// Max number of chunks to send to a client per tick. const MAX_CHUNKS_PER_TICK: usize = 10; @@ -332,6 +333,10 @@ impl Client { self.send_packet(PlayerInfo::RemovePlayers(vec![uuid])); } + pub fn change_player_tablist_gamemode(&self, uuid: Uuid, gamemode: Gamemode) { + self.send_packet(PlayerInfo::UpdateGamemodes(vec![(uuid, gamemode)])); + } + pub fn unload_entity(&self, id: NetworkId) { log::trace!("Unloading {:?} on {}", id, self.username); self.sent_entities.borrow_mut().remove(&id); @@ -481,11 +486,15 @@ impl Client { self.send_packet(Title::Reset); } else { if let Some(main_title) = title.title { - self.send_packet(Title::SetTitle { text: main_title }); + self.send_packet(Title::SetTitle { + text: main_title.to_string(), + }); } if let Some(sub_title) = title.sub_title { - self.send_packet(Title::SetSubtitle { text: sub_title }) + self.send_packet(Title::SetSubtitle { + text: sub_title.to_string(), + }) } self.send_packet(Title::SetTimesAndDisplay { @@ -631,6 +640,12 @@ impl Client { }) } + pub fn change_gamemode(&self, gamemode: Gamemode) { + self.send_packet(ChangeGameState { + state_change: GameStateChange::ChangeGamemode { gamemode }, + }) + } + fn register_entity(&self, network_id: NetworkId) { self.sent_entities.borrow_mut().insert(network_id); } diff --git a/feather/server/src/logging.rs b/feather/server/src/logging.rs index 83ef7232b..0dd93e45a 100644 --- a/feather/server/src/logging.rs +++ b/feather/server/src/logging.rs @@ -39,6 +39,8 @@ pub fn init(level: LevelFilter) { // cranelift_codegen spams debug-level logs .level_for("cranelift_codegen", LevelFilter::Info) .level_for("regalloc", LevelFilter::Off) + .level_for("wasmer_wasi::syscalls", LevelFilter::Info) + .level_for("wasmer_compiler_cranelift::translator", LevelFilter::Warn) .chain(std::io::stdout()) .apply() .unwrap(); diff --git a/feather/server/src/main.rs b/feather/server/src/main.rs index 615857028..c3bf7ea16 100644 --- a/feather/server/src/main.rs +++ b/feather/server/src/main.rs @@ -6,7 +6,7 @@ use common::{Game, TickLoop, World}; use ecs::SystemExecutor; use feather_server::{config::Config, Server}; use plugin_host::PluginManager; -use worldgen::{ComposableGenerator, SuperflatWorldGenerator, WorldGenerator}; +use worldgen::{ComposableGenerator, SuperflatWorldGenerator, VoidWorldGenerator, WorldGenerator}; mod logging; @@ -70,6 +70,7 @@ fn init_world_source(game: &mut Game, config: &Config) { "flat" => Arc::new(SuperflatWorldGenerator::new( SuperflatGeneratorOptions::default(), )), + "void" => Arc::new(VoidWorldGenerator), _ => Arc::new(ComposableGenerator::default_with_seed(seed)), }; game.world = World::with_gen_and_path(generator, config.world.name.clone()); diff --git a/feather/server/src/systems.rs b/feather/server/src/systems.rs index ee7e85b37..e364dc591 100644 --- a/feather/server/src/systems.rs +++ b/feather/server/src/systems.rs @@ -3,6 +3,7 @@ mod block; mod chat; mod entity; +mod gamemode; mod particle; mod player_join; mod player_leave; @@ -36,6 +37,7 @@ pub fn register(server: Server, game: &mut Game, systems: &mut SystemExecutor().add_system(tick_clients); } diff --git a/feather/server/src/systems/effects.rs b/feather/server/src/systems/effects.rs new file mode 100644 index 000000000..4a255452b --- /dev/null +++ b/feather/server/src/systems/effects.rs @@ -0,0 +1,226 @@ +use base::{Particle, ParticleKind, TPS}; +use common::Game; +use ecs::{Entity, SysResult, SystemExecutor}; +use libcraft_core::Position; +use libcraft_effects::effect::Effect; +use quill_common::components_effects::WalkEffectModifier; +use quill_common::entity_init::EntityInit; +use std::collections::HashMap; + +use crate::{Client, ClientId, NetworkId, Server}; + +pub fn register(_game: &mut Game, systems: &mut SystemExecutor) { + systems + .group::() + .add_system(add_start_tick_to_speed_effects) + .add_system(speed_effect) + .add_system(walk_effect_modifier_cleaner) + .add_system(effect_remover); +} + +fn speed_effect(game: &mut Game, _server: &mut Server) -> SysResult { + let mut new_walk_speed = HashMap::new(); + for (entity, speed) in game.ecs.query::<&mut SpeedEffect>().iter() { + if speed.0.is_empty() { + continue; + } + + if let Some(effect_ref) = speed.active_effect() { + let modifier = 20 * (effect_ref.amplifier + 1) as i32; + new_walk_speed.insert(entity, modifier); + }; + } + + for (entity, modifier) in new_walk_speed { + change_modifier(game, entity, modifier)?; + } + + Ok(()) +} + +fn effect_remover(game: &mut Game, server: &mut Server) -> SysResult { + let mut new_walk_speed = HashMap::new(); + for (entity, (&client_id, speed, &network_id)) in game + .ecs + .query::<(&ClientId, &mut SpeedEffect, &NetworkId)>() + .iter() + { + if speed.0.is_empty() { + continue; + } + + let end_effect = speed.ended_on_tick(game.tick_count); + + for effect in end_effect.iter() { + if let Some(client) = server.clients.get(client_id) { + client.send_remove_entity_effect(network_id, Effect::SpeedEffect.id() as u8); + } + + if speed.0.remove(effect) { + log::debug!("speed effect was removed with params {:?}", effect) + } + new_walk_speed.insert(entity, 0); + } + + if !end_effect.is_empty() { + if let Some(active_effect) = speed.active_effect() { + if let Some(client) = server.clients.get(client_id) { + let duration = active_effect.duration as u64 + - (game.tick_count - active_effect.start_tick); + + send_effect_to_client( + Effect::SpeedEffect.id() as u8, + network_id, + active_effect, + client, + duration, + ); + } + } + } + } + + for (entity, modifier) in new_walk_speed { + change_modifier(game, entity, modifier)?; + } + + Ok(()) +} + +fn walk_effect_modifier_cleaner(game: &mut Game, _server: &mut Server) -> SysResult { + let mut rem_comp = vec![]; + + for (entity, wm) in game.ecs.query::<&mut WalkEffectModifier>().iter() { + let mut rem_wm = vec![]; + + for (effect, modifier) in wm.0.iter() { + if *modifier == 0 { + rem_wm.push(*effect); + } + } + + for effect in rem_wm { + wm.0.remove(&effect); + } + + if wm.0.is_empty() { + rem_comp.push(entity); + } + } + + for entity in rem_comp { + game.ecs.remove::(entity)?; + } + + Ok(()) +} + +fn change_modifier(game: &mut Game, entity: Entity, new_modifier: i32) -> SysResult { + if game.ecs.get::(entity).is_err() { + game.ecs.insert(entity, WalkEffectModifier::new())?; + } + + let mut walk_speed_modifier = game.ecs.get_mut::(entity)?; + if walk_speed_modifier.0.contains_key(&Effect::SpeedEffect) + && new_modifier + != *walk_speed_modifier + .0 + .get(&Effect::SpeedEffect) + .unwrap_or(&0) + { + walk_speed_modifier + .0 + .insert(Effect::SpeedEffect, new_modifier); + } + Ok(()) +} + +fn send_effect_to_client( + effect_id: u8, + network_id: NetworkId, + active_effect: &EffectApplication, + client: &Client, + duration: u64, +) { + if duration == 0 { + return; + } + + client.send_entity_effect( + network_id, + effect_id, + active_effect.amplifier as i8, + duration as i32, + active_effect.flags, + ); +} + +// todo change particle color +fn add_particles(game: &mut Game, entity: Entity, _effect_kind: Effect) -> SysResult { + if game.tick_count % (TPS * 2) as u64 == 0 { + let position = *game.ecs.get::(entity)?; + + let mut entity_builder = game.create_entity_builder(position, EntityInit::AreaEffectCloud); + + entity_builder.add(position); + entity_builder.add(Particle { + kind: ParticleKind::Effect, + offset_x: 0.0, + offset_y: 0.0, + offset_z: 0.0, + count: 5, + }); + game.spawn_entity(entity_builder); + } + Ok(()) +} + +/// Set start_tick to all effects in effects_bucket and spawn particles +macro_rules! add_start_tick_to_effects { + ($fn_name:ident,$type:ident) => { + fn $fn_name(game: &mut Game, server: &mut Server) -> SysResult { + let mut entities = vec![]; + for (entity, (&client_id, effects_bucket, &network_id)) in game + .ecs + .query::<(&ClientId, &mut $type, &NetworkId)>() + .iter() + { + if effects_bucket.0.is_empty() { + continue; + } + + if let Some(active_effect) = effects_bucket.active_effect() { + if active_effect.flags.particle { + entities.push((entity, Effect::$type)); + } + } + + let not_started = effects_bucket.not_started(); + + for mut effect in not_started { + effect.start_tick = game.tick_count; + + if let Some(client) = server.clients.get(client_id) { + send_effect_to_client( + Effect::$type.id() as u8, + network_id, + &effect, + client, + effect.duration as u64, + ); + } + + effects_bucket.0.replace(effect); + } + } + + for (entity, effect_kind) in entities { + add_particles(game, entity, effect_kind)?; + } + + Ok(()) + } + }; +} + +add_start_tick_to_effects!(add_start_tick_to_speed_effects, SpeedEffect); diff --git a/feather/server/src/systems/entity/spawn_packet.rs b/feather/server/src/systems/entity/spawn_packet.rs index fcb4ea03a..23cb0b523 100644 --- a/feather/server/src/systems/entity/spawn_packet.rs +++ b/feather/server/src/systems/entity/spawn_packet.rs @@ -2,10 +2,11 @@ use ahash::AHashSet; use anyhow::Context; use base::Position; use common::{ - events::{ChunkCrossEvent, EntityCreateEvent, EntityRemoveEvent, ViewUpdateEvent}, + events::{ChunkCrossEvent, ViewUpdateEvent}, Game, }; use ecs::{SysResult, SystemExecutor}; +use quill_common::events::{EntityCreateEvent, EntityRemoveEvent}; use crate::{entities::SpawnPacketSender, ClientId, NetworkId, Server}; diff --git a/feather/server/src/systems/gamemode.rs b/feather/server/src/systems/gamemode.rs new file mode 100644 index 000000000..8c9581bd1 --- /dev/null +++ b/feather/server/src/systems/gamemode.rs @@ -0,0 +1,194 @@ +use base::anvil::player::PlayerAbilities; +use base::Gamemode; +use common::Game; +use ecs::{SysResult, SystemExecutor}; +use quill_common::components::{ + CanBuild, CanCreativeFly, CreativeFlying, CreativeFlyingSpeed, Instabreak, Invulnerable, + PreviousGamemode, WalkSpeed, +}; +use quill_common::events::{ + BuildingAbilityEvent, CreativeFlyingEvent, FlyingAbilityEvent, GamemodeEvent, InstabreakEvent, + InvulnerabilityEvent, +}; + +use crate::{ClientId, Server}; + +pub fn register(systems: &mut SystemExecutor) { + systems.group::().add_system(gamemode_change); +} + +fn gamemode_change(game: &mut Game, server: &mut Server) -> SysResult { + let mut may_fly_changes = Vec::new(); + let mut fly_changes = Vec::new(); + let mut instabreak_changes = Vec::new(); + let mut build_changes = Vec::new(); + let mut invulnerability_changes = Vec::new(); + for ( + entity, + ( + event, + &client_id, + &walk_speed, + &fly_speed, + mut may_fly, + mut is_flying, + mut instabreak, + mut may_build, + mut invulnerable, + gamemode, + prev_gamemode, + ), + ) in game + .ecs + .query::<( + &GamemodeEvent, + &ClientId, + &WalkSpeed, + &CreativeFlyingSpeed, + &mut CanCreativeFly, + &mut CreativeFlying, + &mut Instabreak, + &mut CanBuild, + &mut Invulnerable, + &mut Gamemode, + &mut PreviousGamemode, + )>() + .iter() + { + if **event == *gamemode { + continue; + } + *prev_gamemode = PreviousGamemode(Some(*gamemode)); + *gamemode = **event; + match gamemode { + Gamemode::Creative => { + if !**instabreak { + instabreak_changes.push((entity, true)); + instabreak.0 = true; + } + if !**may_fly { + may_fly_changes.push((entity, true)); + may_fly.0 = true; + } + if !**may_build { + build_changes.push((entity, true)); + may_build.0 = true; + } + if !**invulnerable { + invulnerability_changes.push((entity, true)); + invulnerable.0 = true; + } + } + Gamemode::Spectator => { + if !**is_flying { + fly_changes.push((entity, true)); + is_flying.0 = true; + } + if **instabreak { + instabreak_changes.push((entity, false)); + instabreak.0 = false; + } + if !**may_fly { + may_fly_changes.push((entity, true)); + may_fly.0 = true; + } + if **may_build { + build_changes.push((entity, false)); + may_build.0 = false; + } + if !**invulnerable { + invulnerability_changes.push((entity, true)); + invulnerable.0 = true; + } + } + Gamemode::Survival => { + if **is_flying { + fly_changes.push((entity, false)); + is_flying.0 = false; + } + if **instabreak { + instabreak_changes.push((entity, false)); + instabreak.0 = false; + } + if **may_fly { + may_fly_changes.push((entity, false)); + may_fly.0 = false; + } + if !**may_build { + build_changes.push((entity, true)); + may_build.0 = true; + } + if **invulnerable { + invulnerability_changes.push((entity, false)); + invulnerable.0 = false; + } + } + Gamemode::Adventure => { + if **is_flying { + fly_changes.push((entity, false)); + is_flying.0 = false; + } + if **instabreak { + instabreak_changes.push((entity, false)); + instabreak.0 = false; + } + if **may_fly { + may_fly_changes.push((entity, false)); + may_fly.0 = false; + } + if **may_build { + build_changes.push((entity, false)); + may_build.0 = false; + } + if **invulnerable { + invulnerability_changes.push((entity, false)); + invulnerable.0 = false; + } + } + } + server + .clients + .get(client_id) + .unwrap() + .change_gamemode(**event); + server + .clients + .get(client_id) + .unwrap() + .send_abilities(&PlayerAbilities { + walk_speed, + fly_speed, + may_fly: *may_fly, + is_flying: *is_flying, + may_build: *may_build, + instabreak: *instabreak, + invulnerable: *invulnerable, + }); + } + for (entity, flying) in fly_changes { + game.ecs + .insert_entity_event(entity, CreativeFlyingEvent::new(flying)) + .unwrap(); + } + for (entity, instabreak) in instabreak_changes { + game.ecs + .insert_entity_event(entity, InstabreakEvent(instabreak)) + .unwrap(); + } + for (entity, may_fly) in may_fly_changes { + game.ecs + .insert_entity_event(entity, FlyingAbilityEvent(may_fly)) + .unwrap(); + } + for (entity, build) in build_changes { + game.ecs + .insert_entity_event(entity, BuildingAbilityEvent(build)) + .unwrap(); + } + for (entity, invulnerable) in invulnerability_changes { + game.ecs + .insert_entity_event(entity, InvulnerabilityEvent(invulnerable)) + .unwrap(); + } + Ok(()) +} diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index e05daa56f..eea536b90 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -16,6 +16,7 @@ use quill_common::components::{ CanBuild, CanCreativeFly, CreativeFlying, CreativeFlyingSpeed, Health, Instabreak, Invulnerable, PreviousGamemode, WalkSpeed, }; +use quill_common::events::GamemodeEvent; use quill_common::{components::Name, entity_init::EntityInit}; use crate::{ClientId, NetworkId, Server}; @@ -133,6 +134,8 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) .add(abilities.instabreak) .add(abilities.invulnerable); + builder.add(GamemodeEvent(gamemode)); + game.spawn_entity(builder); broadcast_player_join(game, client.username()); diff --git a/feather/server/src/systems/tablist.rs b/feather/server/src/systems/tablist.rs index 2f38b6aa9..047ac19c6 100644 --- a/feather/server/src/systems/tablist.rs +++ b/feather/server/src/systems/tablist.rs @@ -1,13 +1,12 @@ //! Sends tablist info to clients via the Player Info packet. +use uuid::Uuid; + use base::{Gamemode, ProfileProperty}; -use common::{ - events::{EntityRemoveEvent, PlayerJoinEvent}, - Game, -}; +use common::Game; use ecs::{SysResult, SystemExecutor}; +use quill_common::events::{EntityRemoveEvent, GamemodeEvent, PlayerJoinEvent}; use quill_common::{components::Name, entities::Player}; -use uuid::Uuid; use crate::{ClientId, Server}; @@ -15,7 +14,8 @@ pub fn register(systems: &mut SystemExecutor) { systems .group::() .add_system(remove_tablist_players) - .add_system(add_tablist_players); + .add_system(add_tablist_players) + .add_system(change_tablist_player_gamemode); } fn remove_tablist_players(game: &mut Game, server: &mut Server) -> SysResult { @@ -62,3 +62,11 @@ fn add_tablist_players(game: &mut Game, server: &mut Server) -> SysResult { } Ok(()) } + +fn change_tablist_player_gamemode(game: &mut Game, server: &mut Server) -> SysResult { + for (_, (event, &uuid)) in game.ecs.query::<(&GamemodeEvent, &Uuid)>().iter() { + // Change this player's gamemode in players' tablists + server.broadcast_with(|client| client.change_player_tablist_gamemode(uuid, **event)); + } + Ok(()) +} diff --git a/feather/worldgen/src/lib.rs b/feather/worldgen/src/lib.rs index 7737747e2..2e55c7ae7 100644 --- a/feather/worldgen/src/lib.rs +++ b/feather/worldgen/src/lib.rs @@ -41,9 +41,9 @@ pub trait WorldGenerator: Send + Sync { fn generate_chunk(&self, position: ChunkPosition) -> Chunk; } -pub struct EmptyWorldGenerator {} +pub struct VoidWorldGenerator; -impl WorldGenerator for EmptyWorldGenerator { +impl WorldGenerator for VoidWorldGenerator { fn generate_chunk(&self, position: ChunkPosition) -> Chunk { Chunk::new(position) } @@ -408,9 +408,9 @@ mod tests { } #[test] - pub fn test_worldgen_empty() { + pub fn test_worldgen_void() { let chunk_pos = ChunkPosition { x: 1, z: 2 }; - let generator = EmptyWorldGenerator {}; + let generator = VoidWorldGenerator; let chunk = generator.generate_chunk(chunk_pos); // No sections have been generated diff --git a/libcraft/effects/src/effects.rs b/libcraft/effects/src/effects.rs new file mode 100644 index 000000000..a269e676f --- /dev/null +++ b/libcraft/effects/src/effects.rs @@ -0,0 +1,47 @@ +use std::cmp::Ordering; + +use serde::{Serialize, Deserialize}; + +use crate::Effect; + +/// Storing effects info. +#[derive(Copy, Clone, Debug, Hash, Serialize, PartialEq, Eq, Deserialize)] +pub struct EffectApplication { + /// Effect kind + pub kind: Effect, + /// Effect intensity, up to 255. + pub amplifier: u8, + /// Duration of the effect in ticks. + pub duration: u32, + /// Effect flags + pub flags: EffectFlags, + + /// Store when effect was added, if start_tick == 0 effect not yet sent to client + pub start_tick: u64, +} +/// Flags that define how an effect is presented. +#[derive(Copy, Clone, Debug, Hash, Serialize, PartialEq, Eq, Deserialize)] +pub struct EffectFlags { + /// `true` if there should be visible particles. + pub particle: bool, + /// `true` when caused by a beacon. + pub ambient: bool, + /// `true` if an icon should be shown to the player. + pub icon: bool, +} +impl Ord for EffectApplication { + fn cmp(&self, other: &Self) -> Ordering { + if self.amplifier > other.amplifier || self.duration > other.duration { + Ordering::Greater + } else if self.amplifier == other.amplifier || self.duration == other.duration { + Ordering::Equal + } else { + Ordering::Less + } + } +} +impl PartialOrd for EffectApplication { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} \ No newline at end of file diff --git a/libcraft/effects/src/lib.rs b/libcraft/effects/src/lib.rs new file mode 100644 index 000000000..89f2cf0c7 --- /dev/null +++ b/libcraft/effects/src/lib.rs @@ -0,0 +1,4 @@ +pub mod effect; +pub use effect::Effect; +pub mod effects; +pub use effects::EffectApplication; diff --git a/libcraft/generators/python/effects.py b/libcraft/generators/python/effects.py new file mode 100644 index 000000000..024708deb --- /dev/null +++ b/libcraft/generators/python/effects.py @@ -0,0 +1,25 @@ +from common import load_minecraft_json, camel_case, generate_enum, generate_enum_property, output + +effects = [] +ids = {} +names = {} +display_names = {} +is_good = {} + +for effect in load_minecraft_json("effects.json", "1.16.1"): + variant = effect['name'] + effects.append(variant) + ids[variant] = effect['id'] + names[variant] = effect['name'] + display_names[variant] = effect['displayName'] + is_good[variant] = True if effect['type'] == "good" else False + +enumName = "Effect" + +output_data = generate_enum(enumName, effects, ["serde::Deserialize", "serde::Serialize"]) +output_data += generate_enum_property(enumName, "id", "u8", ids, True) +output_data += generate_enum_property(enumName, "name", "&str", names, True, "&'static str") +output_data += generate_enum_property(enumName, "display_name", "&str", display_names, True, "&'static str") +output_data += generate_enum_property(enumName, "is_good", "bool", is_good) + +output("effects/src/effect.rs", output_data) diff --git a/libcraft/text/src/title.rs b/libcraft/text/src/title.rs index 38c995352..a77c4782f 100644 --- a/libcraft/text/src/title.rs +++ b/libcraft/text/src/title.rs @@ -1,10 +1,11 @@ +use crate::Text; use serde::{Deserialize, Serialize}; // Based on https://wiki.vg/index.php?title=Protocol&oldid=16459#Title #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct Title { - pub title: Option, - pub sub_title: Option, + pub title: Option, + pub sub_title: Option, pub fade_in: u32, pub stay: u32, pub fade_out: u32, diff --git a/quill/api/Cargo.toml b/quill/api/Cargo.toml index 54f2abf6e..33dfdc59c 100644 --- a/quill/api/Cargo.toml +++ b/quill/api/Cargo.toml @@ -5,6 +5,7 @@ authors = ["caelunshun "] edition = "2018" [dependencies] +plugin-macro = { path = "./plugin-macro" } libcraft-core = { path = "../../libcraft/core" } libcraft-particles = { path = "../../libcraft/particles" } libcraft-blocks = { path = "../../libcraft/blocks" } @@ -16,4 +17,5 @@ quill-common = { path = "../common" } thiserror = "1" uuid = "0.8" itertools = "0.10.0" +serde_json = "1" diff --git a/quill/api/plugin-macro/Cargo.toml b/quill/api/plugin-macro/Cargo.toml new file mode 100644 index 000000000..b16c64f64 --- /dev/null +++ b/quill/api/plugin-macro/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "plugin-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "1.0.86", features = ["full"] } +quote = "1.0.14" \ No newline at end of file diff --git a/quill/api/plugin-macro/src/lib.rs b/quill/api/plugin-macro/src/lib.rs new file mode 100644 index 000000000..15ef5b4ed --- /dev/null +++ b/quill/api/plugin-macro/src/lib.rs @@ -0,0 +1,108 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Item}; + +/// Invoke this macro in your plugin's main.rs. +/// +/// Give it the name of your struct implementing `Plugin`. +/// +/// # Example +/// ```ignore +/// // main.rs +/// use quill::{Plugin, Setup, Game}; +/// +/// #[quill::plugin] +/// pub struct MyPlugin { +/// // plugin state goes here +/// } +/// +/// impl Plugin for MyPlugin { +/// fn enable(game: &mut Game, setup: &mut Setup) -> Self { +/// // Initialize plugin state... +/// Self {} +/// } +/// +/// fn disable(self, game: &mut Game) { +/// // Clean up... +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn plugin(_attr: TokenStream, mut item: TokenStream) -> TokenStream { + let cloned_item = item.clone(); + let input = parse_macro_input!(cloned_item as Item); + + let name = match input { + Item::Enum(itm_enum) => itm_enum.ident, + Item::Struct(itm_str) => itm_str.ident, + _ => panic!("Only structs or enums can be #[quill::plugin]!"), + }; + let res = quote! { + // `static mut` can be used without synchronization because the host + // guarantees it will not invoke plugin systems outside of the main thread. + static mut PLUGIN: Option<#name> = None; + + // Exports to the host required for all plugins + #[no_mangle] + #[doc(hidden)] + #[cfg(target_arch = "wasm32")] + pub unsafe extern "C" fn quill_setup() { + let plugin: #name = + quill::Plugin::enable(&mut ::quill::Game::new(), &mut ::quill::Setup::new()); + PLUGIN = Some(plugin); + } + + #[no_mangle] + #[doc(hidden)] + #[cfg(not(target_arch = "wasm32"))] + pub unsafe extern "C" fn quill_setup( + context: *const (), + vtable_ptr: *const u8, + vtable_len: usize, + ) { + // Set up vtable and host context for quill_sys. + let vtable_bytes = ::std::slice::from_raw_parts(vtable_ptr, vtable_len); + let vtable: ::std::collections::HashMap<&str, usize> = + ::quill::bincode::deserialize(vtable_bytes).expect("invalid vtable"); + + ::quill::sys::init_host_context(context); + ::quill::sys::init_host_vtable(&vtable) + .expect("invalid vtable (check that the plugin and host are up to date)"); + + let plugin: #name = + quill::Plugin::enable(&mut ::quill::Game::new(), &mut ::quill::Setup::new()); + PLUGIN = Some(plugin); + } + + #[no_mangle] + #[doc(hidden)] + pub unsafe extern "C" fn quill_allocate(size: usize, align: usize) -> *mut u8 { + std::alloc::alloc(std::alloc::Layout::from_size_align_unchecked(size, align)) + } + + #[no_mangle] + #[doc(hidden)] + pub unsafe extern "C" fn quill_deallocate(ptr: *mut u8, size: usize, align: usize) { + std::alloc::dealloc( + ptr, + std::alloc::Layout::from_size_align_unchecked(size, align), + ) + } + + #[no_mangle] + #[doc(hidden)] + pub unsafe extern "C" fn quill_run_system(data: *mut u8) { + let system = &mut *data.cast::>(); + let plugin = PLUGIN.as_mut().expect("quill_setup never called"); + system(plugin, &mut ::quill::Game::new()); + } + + /// Never called by Quill, but this is needed + /// to avoid linker errors with WASI. + #[doc(hidden)] + fn main() {} + }; + item.extend(TokenStream::from(res)); + + item +} diff --git a/quill/api/src/entity.rs b/quill/api/src/entity.rs index e5bc9bf92..cd6217e09 100644 --- a/quill/api/src/entity.rs +++ b/quill/api/src/entity.rs @@ -1,3 +1,4 @@ +use libcraft_text::Text; use std::{marker::PhantomData, ptr}; use quill_common::{Component, Pointer, PointerMut}; @@ -92,8 +93,8 @@ impl Entity { /// /// The message sends as a "system" message. /// See [the wiki](https://wiki.vg/Chat) for more details. - pub fn send_message(&self, message: impl AsRef) { - let message = message.as_ref(); + pub fn send_message(&self, message: impl Into) { + let message = message.into().to_string(); unsafe { quill_sys::entity_send_message(self.id.0, message.as_ptr().into(), message.len() as u32) } @@ -101,7 +102,7 @@ impl Entity { /// Sends the given title to this entity. pub fn send_title(&self, title: &libcraft_text::Title) { - let title = bincode::serialize(title).expect("failed to serialize Title"); + let title = serde_json::to_string(title).expect("failed to serialize Title"); unsafe { quill_sys::entity_send_title(self.id.0, title.as_ptr().into(), title.len() as u32); } diff --git a/quill/api/src/lib.rs b/quill/api/src/lib.rs index 23b3a9418..3334382ad 100644 --- a/quill/api/src/lib.rs +++ b/quill/api/src/lib.rs @@ -32,6 +32,8 @@ pub extern crate bincode; #[doc(hidden)] pub extern crate quill_sys as sys; +pub use plugin_macro::plugin; + /// Implement this trait for your plugin's struct. pub trait Plugin: Sized { /// Invoked when the plugin is enabled. @@ -55,98 +57,3 @@ pub trait Plugin: Sized { /// the server is shutting down when this method is called. fn disable(self, game: &mut Game); } - -/// Invoke this macro in your plugin's main.rs. -/// -/// Give it the name of your struct implementing `Plugin`. -/// -/// # Example -/// ```no_run -/// // main.rs -/// use quill::{Plugin, Setup, Game}; -/// -/// quill::plugin!(MyPlugin); -/// -/// pub struct MyPlugin { -/// // plugin state goes here -/// } -/// -/// impl Plugin for MyPlugin { -/// fn enable(game: &mut Game, setup: &mut Setup) -> Self { -/// // Initialize plugin state... -/// Self {} -/// } -/// -/// fn disable(self, game: &mut Game) { -/// // Clean up... -/// } -/// } -/// ``` -#[macro_export] -macro_rules! plugin { - ($plugin:ident) => { - // `static mut` can be used without synchronization because the host - // guarantees it will not invoke plugin systems outside of the main thread. - static mut PLUGIN: Option<$plugin> = None; - - // Exports to the host required for all plugins - #[no_mangle] - #[doc(hidden)] - #[cfg(target_arch = "wasm32")] - pub unsafe extern "C" fn quill_setup() { - let plugin: $plugin = - quill::Plugin::enable(&mut $crate::Game::new(), &mut $crate::Setup::new()); - PLUGIN = Some(plugin); - } - - #[no_mangle] - #[doc(hidden)] - #[cfg(not(target_arch = "wasm32"))] - pub unsafe extern "C" fn quill_setup( - context: *const (), - vtable_ptr: *const u8, - vtable_len: usize, - ) { - // Set up vtable and host context for quill_sys. - let vtable_bytes = ::std::slice::from_raw_parts(vtable_ptr, vtable_len); - let vtable: ::std::collections::HashMap<&str, usize> = - $crate::bincode::deserialize(vtable_bytes).expect("invalid vtable"); - - $crate::sys::init_host_context(context); - $crate::sys::init_host_vtable(&vtable) - .expect("invalid vtable (check that the plugin and host are up to date)"); - - let plugin: $plugin = - quill::Plugin::enable(&mut $crate::Game::new(), &mut $crate::Setup::new()); - PLUGIN = Some(plugin); - } - - #[no_mangle] - #[doc(hidden)] - pub unsafe extern "C" fn quill_allocate(size: usize, align: usize) -> *mut u8 { - std::alloc::alloc(std::alloc::Layout::from_size_align_unchecked(size, align)) - } - - #[no_mangle] - #[doc(hidden)] - pub unsafe extern "C" fn quill_deallocate(ptr: *mut u8, size: usize, align: usize) { - std::alloc::dealloc( - ptr, - std::alloc::Layout::from_size_align_unchecked(size, align), - ) - } - - #[no_mangle] - #[doc(hidden)] - pub unsafe extern "C" fn quill_run_system(data: *mut u8) { - let system = &mut *data.cast::>(); - let plugin = PLUGIN.as_mut().expect("quill_setup never called"); - system(plugin, &mut $crate::Game::new()); - } - - /// Never called by Quill, but this is needed - /// to avoid linker errors with WASI. - #[doc(hidden)] - fn main() {} - }; -} diff --git a/quill/common/src/component.rs b/quill/common/src/component.rs index f113fa29a..11e4c4651 100644 --- a/quill/common/src/component.rs +++ b/quill/common/src/component.rs @@ -198,8 +198,14 @@ host_component_enum! { CanBuild = 1020, Instabreak = 1021, Invulnerable = 1022, - - + PlayerJoinEvent = 1023, + EntityRemoveEvent = 1024, + EntityCreateEvent = 1025, + GamemodeEvent = 1026, + InstabreakEvent = 1027, + FlyingAbilityEvent = 1028, + BuildingAbilityEvent = 1029, + InvulnerabilityEvent = 1030, } } @@ -363,3 +369,11 @@ bincode_component_impl!(BlockInteractEvent); bincode_component_impl!(CreativeFlyingEvent); bincode_component_impl!(SneakEvent); bincode_component_impl!(SprintEvent); +bincode_component_impl!(PlayerJoinEvent); +bincode_component_impl!(EntityRemoveEvent); +bincode_component_impl!(EntityCreateEvent); +bincode_component_impl!(GamemodeEvent); +bincode_component_impl!(InstabreakEvent); +bincode_component_impl!(FlyingAbilityEvent); +bincode_component_impl!(BuildingAbilityEvent); +bincode_component_impl!(InvulnerabilityEvent); diff --git a/quill/common/src/components_effects.rs b/quill/common/src/components_effects.rs new file mode 100644 index 000000000..9f2f0f907 --- /dev/null +++ b/quill/common/src/components_effects.rs @@ -0,0 +1,112 @@ +use libcraft_effects::{effects::EffectApplication, Effect}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Default, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub struct Effects(pub BTreeSet); +impl Effects { + pub fn new() -> Self { + Self::default() + } + pub fn add_effect(&mut self, effect: EffectApplication) { + self.0.insert(effect); + } + pub fn strongest_active(&self, kind: Effect) -> Option<&EffectApplication> { + self.0.iter().filter(|e| e.kind == kind).last() + } +} +bincode_component_impl!(Effects); +/* +macro_rules! impl_effect { + ($ident:ident) => { + #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] + pub struct $ident(pub BTreeSet); + impl $ident { + pub fn new() -> $ident { + $ident { 0: BTreeSet::new() } + } + pub fn add_effect(&mut self, effect: EffectApplication) -> bool { + self.0.insert(effect) + } + pub fn not_started(&mut self) -> Vec { + self.0 + .iter() + .filter(|effect| effect.start_tick == 0) + .cloned() + .collect::>() + } + pub fn ended_on_tick(&mut self, tick: u64) -> Vec { + self.0 + .iter() + .filter(|effect| { + tick >= effect.start_tick + effect.duration as u64 && effect.start_tick != 0 + }) + .cloned() + .collect::>() + } + pub fn active_effect(&mut self) -> Option<&EffectApplication> { + self.0.iter().last() + } + } + impl Default for $ident { + fn default() -> Self { + $ident::new() + } + } + bincode_component_impl!($ident); + }; +} + + +impl_effect!(SpeedEffect); +impl_effect!(SlownessEffect); +impl_effect!(HasteEffect); +impl_effect!(MiningFatigueEffect); +impl_effect!(StrengthEffect); +impl_effect!(InstantHealthEffect); +impl_effect!(InstantDamageEffect); +impl_effect!(JumpBoostEffect); +impl_effect!(NauseaEffect); +impl_effect!(RegenerationEffect); +impl_effect!(ResistanceEffect); +impl_effect!(FireResistanceEffect); +impl_effect!(WaterBreathingEffect); +impl_effect!(InvisibilityEffect); +impl_effect!(BlindnessEffect); +impl_effect!(NightVisionEffect); +impl_effect!(HungerEffect); +impl_effect!(WeaknessEffect); +impl_effect!(PoisonEffect); +impl_effect!(WitherEffect); +impl_effect!(HealthBoostEffect); +impl_effect!(AbsorptionEffect); +impl_effect!(SaturationEffect); +impl_effect!(GlowingEffect); +impl_effect!(LevitationEffect); +impl_effect!(LuckEffect); +impl_effect!(BadLuckEffect); +impl_effect!(SlowFallingEffect); +impl_effect!(ConduitPowerEffect); +impl_effect!(DolphinsGraceEffect); +impl_effect!(BadOmenEffect); +impl_effect!(HeroOfTheVillageEffect); +*/ +/// A walk speed modifier in percent +#[derive( + Clone, Debug, PartialEq, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut, +)] +pub struct WalkEffectModifier(pub BTreeMap); + +impl WalkEffectModifier { + pub fn new() -> WalkEffectModifier { + WalkEffectModifier { 0: BTreeMap::new() } + } +} + +impl Default for WalkEffectModifier { + fn default() -> Self { + WalkEffectModifier::new() + } +} + +bincode_component_impl!(WalkEffectModifier); diff --git a/quill/common/src/events.rs b/quill/common/src/events.rs index 734d572cb..e253da176 100644 --- a/quill/common/src/events.rs +++ b/quill/common/src/events.rs @@ -1,7 +1,12 @@ +pub use block_interact::{BlockInteractEvent, BlockPlacementEvent}; +pub use change::{ + BuildingAbilityEvent, CreativeFlyingEvent, FlyingAbilityEvent, GamemodeEvent, InstabreakEvent, + InvulnerabilityEvent, SneakEvent, SprintEvent, +}; +pub use entity::{EntityCreateEvent, EntityRemoveEvent, PlayerJoinEvent}; +pub use interact_entity::InteractEntityEvent; + mod block_interact; mod change; +mod entity; mod interact_entity; - -pub use block_interact::{BlockInteractEvent, BlockPlacementEvent}; -pub use change::{CreativeFlyingEvent, SneakEvent, SprintEvent}; -pub use interact_entity::InteractEntityEvent; diff --git a/quill/common/src/events/change.rs b/quill/common/src/events/change.rs index f6bca77ac..266846160 100644 --- a/quill/common/src/events/change.rs +++ b/quill/common/src/events/change.rs @@ -1,7 +1,9 @@ /* -All events in this file are triggerd when there is a change in a certain value. +All events in this file are triggered when there is a change in a certain value. */ +use derive_more::Deref; +use libcraft_core::Gamemode; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] @@ -42,3 +44,23 @@ impl SprintEvent { } } } + +/// This event is called when a player's gamemode is changed and every time the player joins. +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct GamemodeEvent(pub Gamemode); + +/// This event is called when player's ability to instantly break blocks changes. +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct InstabreakEvent(pub bool); + +/// This event is called when player's ability to fly changes. +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct FlyingAbilityEvent(pub bool); + +/// This event is called when player's ability to place or break blocks changes. +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct BuildingAbilityEvent(pub bool); + +/// This event is called when player's invulnerability property changes. +#[derive(Debug, Serialize, Deserialize, Clone, Deref)] +pub struct InvulnerabilityEvent(pub bool); diff --git a/quill/common/src/events/entity.rs b/quill/common/src/events/entity.rs new file mode 100644 index 000000000..073feb61a --- /dev/null +++ b/quill/common/src/events/entity.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +/// Triggered when a player joins the `Game`. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PlayerJoinEvent; + +/// Triggered when an entity is removed from the world. +/// +/// The entity will remain alive for one tick after it is +/// destroyed to allow systems to observe this event. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EntityRemoveEvent; + +/// Triggered when an entity is added into the world. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EntityCreateEvent; diff --git a/quill/example-plugins/block-access/src/lib.rs b/quill/example-plugins/block-access/src/lib.rs index 33f3ad798..9813d2b36 100644 --- a/quill/example-plugins/block-access/src/lib.rs +++ b/quill/example-plugins/block-access/src/lib.rs @@ -2,8 +2,7 @@ use quill::{entities::Player, BlockState, Game, Plugin, Position}; -quill::plugin!(BlockAccess); - +#[quill::plugin] pub struct BlockAccess; impl Plugin for BlockAccess { diff --git a/quill/example-plugins/block-place/src/lib.rs b/quill/example-plugins/block-place/src/lib.rs index fa1be6692..19913eb7f 100644 --- a/quill/example-plugins/block-place/src/lib.rs +++ b/quill/example-plugins/block-place/src/lib.rs @@ -2,8 +2,7 @@ use quill::{events::BlockPlacementEvent, Game, Plugin}; -quill::plugin!(BlockPlace); - +#[quill::plugin] pub struct BlockPlace; impl Plugin for BlockPlace { diff --git a/quill/example-plugins/observe-creativemode-flight-event/src/lib.rs b/quill/example-plugins/observe-creativemode-flight-event/src/lib.rs index 98d28c272..9d84f159d 100644 --- a/quill/example-plugins/observe-creativemode-flight-event/src/lib.rs +++ b/quill/example-plugins/observe-creativemode-flight-event/src/lib.rs @@ -9,8 +9,7 @@ use quill::{ Game, Plugin, Setup, }; -quill::plugin!(FlightPlugin); - +#[quill::plugin] struct FlightPlugin {} impl Plugin for FlightPlugin { diff --git a/quill/example-plugins/particle-example/src/lib.rs b/quill/example-plugins/particle-example/src/lib.rs index e35c35f3c..bd84bb58d 100644 --- a/quill/example-plugins/particle-example/src/lib.rs +++ b/quill/example-plugins/particle-example/src/lib.rs @@ -1,7 +1,6 @@ use quill::{Game, Particle, ParticleKind, Plugin, Position}; -quill::plugin!(ParticleExample); - +#[quill::plugin] struct ParticleExample {} impl Plugin for ParticleExample { diff --git a/quill/example-plugins/plugin-message/src/lib.rs b/quill/example-plugins/plugin-message/src/lib.rs index 5efb7ea77..9b66f8c7a 100644 --- a/quill/example-plugins/plugin-message/src/lib.rs +++ b/quill/example-plugins/plugin-message/src/lib.rs @@ -2,8 +2,7 @@ //! send a player to a server named lobby. use quill::{entities::Player, BlockPosition, Game, Plugin, Position}; -quill::plugin!(PluginMessage); - +#[quill::plugin] pub struct PluginMessage; impl Plugin for PluginMessage { diff --git a/quill/example-plugins/query-entities/src/lib.rs b/quill/example-plugins/query-entities/src/lib.rs index 9ea8247b1..527fd7546 100644 --- a/quill/example-plugins/query-entities/src/lib.rs +++ b/quill/example-plugins/query-entities/src/lib.rs @@ -4,8 +4,7 @@ use quill::{entities::PiglinBrute, EntityInit, Game, Plugin, Position}; use rand::Rng; -quill::plugin!(QueryEntities); - +#[quill::plugin] struct QueryEntities { tick_counter: u64, } diff --git a/quill/example-plugins/simple/src/lib.rs b/quill/example-plugins/simple/src/lib.rs index d3d006137..d6ac19dae 100644 --- a/quill/example-plugins/simple/src/lib.rs +++ b/quill/example-plugins/simple/src/lib.rs @@ -5,8 +5,7 @@ use quill::{ }; use rand::Rng; -quill::plugin!(SimplePlugin); - +#[quill::plugin] struct SimplePlugin { tick_counter: u64, } diff --git a/quill/example-plugins/titles/src/lib.rs b/quill/example-plugins/titles/src/lib.rs index f7ab938ce..634f5b003 100644 --- a/quill/example-plugins/titles/src/lib.rs +++ b/quill/example-plugins/titles/src/lib.rs @@ -1,7 +1,6 @@ use quill::{entities::Player, Game, Plugin, Text, TextComponent, TextComponentBuilder, Title}; -quill::plugin!(TitleExample); - +#[quill::plugin] struct TitleExample { tick_count: u32, title_active: bool, @@ -28,8 +27,8 @@ fn title_system(plugin: &mut TitleExample, game: &mut Game) { // Create a title to send to the player let title = Title { - title: Some(Text::from(title_component).to_string()), - sub_title: Some(Text::from(component).to_string()), + title: Some(Text::from(title_component)), + sub_title: Some(Text::from(component)), fade_in: 5, stay: 400, fade_out: 5, diff --git a/quill/sys/src/lib.rs b/quill/sys/src/lib.rs index 14e6e6176..e33eebb15 100644 --- a/quill/sys/src/lib.rs +++ b/quill/sys/src/lib.rs @@ -97,7 +97,7 @@ extern "C" { /// The given `Title` should contain at least a `title` or a `sub_title` /// /// Does nothing if the entity does not exist or if it does not have the `Chat` component. - pub fn entity_send_title(entity: EntityId, title_ptr: Pointer, title_len: u32); + pub fn entity_send_title(entity: EntityId, title_json_ptr: Pointer, title_len: u32); /// Creates an empty entity builder. /// From 53050710c824b3da441074e81e6df0d16078df12 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Sun, 23 Jan 2022 16:59:13 +0100 Subject: [PATCH 07/15] Improve documentation --- feather/common/src/block_break.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 1a633444b..c3e4bbf9a 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -1,3 +1,5 @@ +use std::mem; + use base::{BlockKind, ItemStack, ValidBlockPosition}; use ecs::{EntityBuilder, SysResult, SystemExecutor}; use libcraft_items::EnchantmentKind; @@ -14,6 +16,7 @@ pub enum BlockBreaker { Inactive, } impl BlockBreaker { + /// Create a new active instance pointing to `block_pos`. Calculates the time needed using `world.block_at(block_pos)` and `equipped_item`. pub fn new( world: &mut World, block_pos: ValidBlockPosition, @@ -23,9 +26,11 @@ impl BlockBreaker { .map(Self::Active) .unwrap_or(Self::Inactive) } + /// If active, produces a `DestroyStateChange` event for the adequate position. pub fn destroy_change_event(&self) -> Option { Some(DestroyStateChange(self.position()?, self.destroy_stage())) } + /// If active or finished, returns the pointed to position. pub fn position(&self) -> Option { match self { BlockBreaker::Active(a) => Some(a.position), @@ -33,18 +38,22 @@ impl BlockBreaker { BlockBreaker::Inactive => None, } } + /// If active, returns the underlying `ActiveBreaker`. pub fn active(&self) -> Option<&ActiveBreaker> { match self { Self::Active(a) => Some(a), _ => None, } } + /// If finished, returns the underlying `FinishedBreaker`. pub fn finished(&self) -> Option<&FinishedBreaker> { match self { Self::Finished(f) => Some(f), _ => None, } } + /// Progresses block breaking. Returns a (newly_finished, do_destry_state_change) tuple. + /// If this operation finishes block breaking, this turns `self` into `Self::Finished` with the same position. pub fn tick(&mut self) -> (bool, bool) { let (block_break, stage_update) = if let Self::Active(breaker) = self { breaker.tick() @@ -52,23 +61,26 @@ impl BlockBreaker { (false, false) }; if block_break { - let fin = match self { - Self::Active(a) => a.clone().finish(), + let fin = match mem::take(self) { + Self::Active(a) => a.finish(), _ => unreachable!(), }; *self = Self::Finished(fin); } (block_break, stage_update) } + /// Returns the block destroying progress in a range of 0 - 9. When inactive or finished, returns 10. pub fn destroy_stage(&self) -> u8 { match self { BlockBreaker::Active(a) => a.destroy_stage(), _ => 10, } } + /// Set `self` to `Self::Inactive`. pub fn cancel(&mut self) { *self = Self::Inactive; } + /// Check if the breaker points to `pos`. Returns `true` when `self` is `Self::Inactive`. pub fn matches_position(&self, pos: ValidBlockPosition) -> bool { match self { BlockBreaker::Active(a) => a.position == pos, @@ -76,6 +88,7 @@ impl BlockBreaker { BlockBreaker::Inactive => true, } } + /// Attempts to finish breaking the target block, optionally turning `self` into `Self::Finished`. pub fn try_finish(&mut self) -> Option { let this = self.clone(); match this { @@ -93,6 +106,11 @@ impl BlockBreaker { } } } +impl Default for BlockBreaker { + fn default() -> Self { + Self::Inactive + } +} #[derive(Clone)] pub struct FinishedBreaker { pub position: ValidBlockPosition, @@ -100,6 +118,7 @@ pub struct FinishedBreaker { pub fake_finished: bool, } impl FinishedBreaker { + /// Breaks the targeted block and spawns its drops. TODO: make drops work. pub fn break_block(&self, game: &mut Game) -> SysResult { let target_block = match game.block(self.position) { Some(b) => b, From d6dd7085acea7cfadd6eeee99ffc47b5473d5834 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Sun, 23 Jan 2022 17:45:12 +0100 Subject: [PATCH 08/15] Switch to float based break progress --- feather/common/src/block_break.rs | 35 ++++++++----------- .../server/src/packet_handlers/interaction.rs | 2 -- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index c3e4bbf9a..73274c79b 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -93,7 +93,7 @@ impl BlockBreaker { let this = self.clone(); match this { BlockBreaker::Active(a) => { - if a.ticks_remaining == 1 { + if a.can_break() { let fin = a.finish(); *self = Self::Finished(fin.clone()); Some(fin) @@ -142,18 +142,23 @@ pub struct ActiveBreaker { pub position: ValidBlockPosition, pub drop_item: bool, pub fake_finished: bool, - pub total_ticks: u32, - pub ticks_remaining: u32, + pub progress: f32, + pub damage: f32, } impl ActiveBreaker { pub fn tick(&mut self) -> (bool, bool) { let before = self.destroy_stage(); - self.ticks_remaining = self.ticks_remaining.saturating_sub(1); + self.progress += self.damage; let after = self.destroy_stage(); - let break_block = self.ticks_remaining == 0; + let break_block = self.can_break(); let change_stage = before != after || break_block; (break_block, change_stage) } + /// Check if the block has been damaged enough to break. + pub fn can_break(&self) -> bool { + // Comparing to 0.7 ensures good feeling in the client + self.progress >= 0.7 - self.damage / 2.0 + } pub fn new( world: &mut World, block_pos: ValidBlockPosition, @@ -202,32 +207,20 @@ impl ActiveBreaker { } else { 1.0 / block.hardness() / 100.0 }; - let ticks = if damage > 1.0 { - 0 - } else { - (1.0 / damage / 1.2).ceil() as u32 - }; - println!( - "Mining {} with {} takes {} ticks", - block.display_name(), - equipped_item - .map(|e| e.get_item().item().display_name()) - .unwrap_or("bare hands"), - ticks - ); Some(Self { position: block_pos, drop_item: true, fake_finished: false, - total_ticks: ticks, - ticks_remaining: ticks, + progress: 0.0, + damage, }) } + /// Get the destroying progress. pub fn destroy_stage(&self) -> u8 { if self.fake_finished { 10 } else { - 9 - (self.ticks_remaining as f32 / self.total_ticks as f32 * 9.0).round() as u8 + (self.progress * 9.0).round() as u8 } } pub fn finish(self) -> FinishedBreaker { diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index f30e3182f..01ef51425 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -182,14 +182,12 @@ pub fn handle_player_digging( } else { if let BlockBreaker::Active(a) = &mut *breaker { a.fake_finished = true; - println!("{} ticks remaining", a.ticks_remaining); } false }; success && breaker.matches_position(packet.position) }; if success { - println!("confirm"); finished.unwrap().break_block(game)?; } game.ecs From becec55cd4050ee646c288f94f279c1f84fd3eaa Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Mon, 24 Jan 2022 16:25:59 +0100 Subject: [PATCH 09/15] Fix block breaking progress --- feather/common/src/block_break.rs | 8 +++++--- feather/server/src/systems/player_join.rs | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 73274c79b..9f3b919b1 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -9,6 +9,9 @@ pub struct DestroyStateChange(pub ValidBlockPosition, pub u8); use crate::{Game, World}; +// Comparing to 0.7 ensures good feeling in the client +pub const BREAK_THRESHOLD: f32 = 0.7; + #[derive(Clone)] pub enum BlockBreaker { Active(ActiveBreaker), @@ -156,8 +159,7 @@ impl ActiveBreaker { } /// Check if the block has been damaged enough to break. pub fn can_break(&self) -> bool { - // Comparing to 0.7 ensures good feeling in the client - self.progress >= 0.7 - self.damage / 2.0 + self.progress >= BREAK_THRESHOLD - self.damage / 2.0 } pub fn new( world: &mut World, @@ -220,7 +222,7 @@ impl ActiveBreaker { if self.fake_finished { 10 } else { - (self.progress * 9.0).round() as u8 + (self.progress / BREAK_THRESHOLD * 9.0).round() as u8 } } pub fn finish(self) -> FinishedBreaker { diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index eea536b90..3682bac13 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -104,13 +104,15 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) client.send_window_items(&window); + let gamemode = Gamemode::Survival; + builder .add(client_id) .add(View::new( Position::default().chunk(), server.options.view_distance, )) - .add(Gamemode::Creative) + .add(gamemode) .add(previous_gamemode) .add(Name::new(client.username())) .add(client.uuid()) From bbfd69b2c102f4ba798974a29aec0d69ea133f2b Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Sun, 28 Aug 2022 22:03:10 +0200 Subject: [PATCH 10/15] Make clippy happy --- feather/base/src/anvil/level.rs | 2 +- feather/blocks/generator/src/load.rs | 2 +- feather/common/src/block_break.rs | 25 +++++-------------- feather/datapacks/src/id.rs | 2 +- feather/protocol/src/io.rs | 2 +- feather/server/src/initial_handler/proxy.rs | 2 +- .../server/src/packet_handlers/interaction.rs | 2 +- .../server/src/packet_handlers/inventory.rs | 2 +- libcraft/items/src/item_stack.rs | 3 +++ libcraft/text/src/text.rs | 4 +-- libcraft/text/src/text/markdown/lexer.rs | 10 ++++---- libcraft/text/src/text/markdown/parser.rs | 2 +- .../text/src/text/markdown/parser/events.rs | 4 +-- quill/api/src/setup.rs | 1 + quill/common/src/components.rs | 4 +-- quill/plugin-format/src/lib.rs | 2 +- quill/plugin-format/src/metadata.rs | 2 +- 17 files changed, 31 insertions(+), 40 deletions(-) diff --git a/feather/base/src/anvil/level.rs b/feather/base/src/anvil/level.rs index b312e5de3..5411093a4 100644 --- a/feather/base/src/anvil/level.rs +++ b/feather/base/src/anvil/level.rs @@ -149,7 +149,7 @@ pub struct SuperflatLayer { } /// The type of world generator for a level. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum LevelGeneratorType { Default, Flat, diff --git a/feather/blocks/generator/src/load.rs b/feather/blocks/generator/src/load.rs index 10bd98909..390e0bbee 100644 --- a/feather/blocks/generator/src/load.rs +++ b/feather/blocks/generator/src/load.rs @@ -133,7 +133,7 @@ impl PropertyStore { fn update_name(name: &str) -> &str { match NAME_OVERRIDES.get(&name) { - Some(x) => *x, + Some(x) => x, None => name, } } diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 9f3b919b1..4a7adaf3a 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -180,29 +180,16 @@ impl ActiveBreaker { .iter() .find_map(|(item, speed)| { equipped_item - .map(|e| { - if e.item() == *item { - Some(*speed) - } else { - None - } + .and_then(|e| { + bool::then_some(e.item() == *item, *speed) }) - .flatten() }) .unwrap_or(1.0); let effi_level = equipped_item - .map(ItemStack::metadata) - .flatten() - .map(|meta| { - meta.enchantments().iter().find_map(|ench| { - if ench.kind() == EnchantmentKind::Efficiency { - Some(ench.level()) - } else { - None - } - }) - }) - .flatten(); + .and_then(ItemStack::metadata) + .and_then(|meta| { + meta.get_enchantment_level(EnchantmentKind::Efficiency) + }); let effi_speed = effi_level.map(|level| level * level + 1).unwrap_or(0) as f32; let damage = if harvestable { (dig_multiplier + effi_speed) / block.hardness() / 30.0 diff --git a/feather/datapacks/src/id.rs b/feather/datapacks/src/id.rs index e0e9beebb..7f6213385 100644 --- a/feather/datapacks/src/id.rs +++ b/feather/datapacks/src/id.rs @@ -32,7 +32,7 @@ impl NamespacedId { } /// Error returned when a namespaced ID was formatted incorrectly. -#[derive(Debug, thiserror::Error, PartialEq)] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum ParseError { #[error("'{0}' is not a valid character for namespaces")] InvalidNamespaceChar(char), diff --git a/feather/protocol/src/io.rs b/feather/protocol/src/io.rs index 4c9a51be6..fbe598fd0 100644 --- a/feather/protocol/src/io.rs +++ b/feather/protocol/src/io.rs @@ -480,7 +480,7 @@ impl<'a> Readable for LengthInferredVecU8<'a> { impl<'a> Writeable for LengthInferredVecU8<'a> { fn write(&self, buffer: &mut Vec, _version: ProtocolVersion) -> anyhow::Result<()> { - buffer.extend_from_slice(&*self.0); + buffer.extend_from_slice(&self.0); Ok(()) } } diff --git a/feather/server/src/initial_handler/proxy.rs b/feather/server/src/initial_handler/proxy.rs index f2a2775bb..4a590b036 100644 --- a/feather/server/src/initial_handler/proxy.rs +++ b/feather/server/src/initial_handler/proxy.rs @@ -10,7 +10,7 @@ mod bungeecord; mod velocity; /// IP forwarding data received from the proxy. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub struct ProxyData { /// IP address of the proxy. pub host: String, diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index 01ef51425..f17671f6c 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -134,7 +134,7 @@ pub fn handle_player_digging( let hotbar_slot = game.ecs.get::(player)?.get(); let main_hand = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; *breaker = BlockBreaker::Active( - ActiveBreaker::new(&mut game.world, packet.position, main_hand.item_stack()) + ActiveBreaker::new(&mut game.world, packet.position, main_hand.option_ref()) .unwrap(), ); } diff --git a/feather/server/src/packet_handlers/inventory.rs b/feather/server/src/packet_handlers/inventory.rs index 48a397197..f8723b51c 100644 --- a/feather/server/src/packet_handlers/inventory.rs +++ b/feather/server/src/packet_handlers/inventory.rs @@ -56,7 +56,7 @@ pub fn handle_click_window( } client.set_cursor_slot(window.cursor_item()); - client.send_window_items(&*window); + client.send_window_items(&window); result } diff --git a/libcraft/items/src/item_stack.rs b/libcraft/items/src/item_stack.rs index fe227964a..09fc7c142 100644 --- a/libcraft/items/src/item_stack.rs +++ b/libcraft/items/src/item_stack.rs @@ -352,6 +352,7 @@ impl ItemStack { self.item.stack_size() } + #[must_use] pub fn metadata(&self) -> Option<&ItemStackMeta> { self.meta.as_ref() } @@ -403,9 +404,11 @@ impl ItemStackMeta { self.enchantments.push(Enchantment::new(ench, level)); } } + #[must_use] pub fn enchantments(&self) -> &[Enchantment] { &self.enchantments } + #[must_use] pub fn enchantments_mut(&mut self) -> &mut Vec { &mut self.enchantments } diff --git a/libcraft/text/src/text.rs b/libcraft/text/src/text.rs index 90af776de..ea5108e3f 100644 --- a/libcraft/text/src/text.rs +++ b/libcraft/text/src/text.rs @@ -16,7 +16,7 @@ pub enum TextConversionError { InvalidStyle(String), } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Color { DarkRed, @@ -304,7 +304,7 @@ where } } -impl<'a> From<&Translate> for String { +impl From<&Translate> for String { fn from(translate: &Translate) -> Self { match translate { Translate::ChatTypeText => "chat.type.text", diff --git a/libcraft/text/src/text/markdown/lexer.rs b/libcraft/text/src/text/markdown/lexer.rs index 27e003e3b..899357bc8 100644 --- a/libcraft/text/src/text/markdown/lexer.rs +++ b/libcraft/text/src/text/markdown/lexer.rs @@ -13,7 +13,7 @@ use std::slice::Iter; pub type Span<'a> = LocatedSpan<&'a str>; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct LexToken<'a> { pub tok: LexTokenType<'a>, pub span: Span<'a>, @@ -34,7 +34,7 @@ pub enum LexTokenType<'a> { Word(&'a str), } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct Tokens<'a> { pub tok: &'a [LexToken<'a>], start: usize, @@ -159,7 +159,7 @@ pub fn lex_control_word(input: Span) -> IResult IResult> { map(space1, |s: Span| { - LexToken::new(s, LexTokenType::Space(*s.fragment())) + LexToken::new(s, LexTokenType::Space(s.fragment())) })(input) } @@ -173,13 +173,13 @@ pub fn valid_word(input: Span) -> IResult> { pub fn lex_word(input: Span) -> IResult> { map(valid_word, |s: Span| { - LexToken::new(s, LexTokenType::Word(*s.fragment())) + LexToken::new(s, LexTokenType::Word(s.fragment())) })(input) } pub fn lex_color_code(input: Span) -> IResult> { map(preceded(peek(tag("#")), take(7usize)), |code: Span| { - LexToken::new(code, LexTokenType::Word(*code.fragment())) + LexToken::new(code, LexTokenType::Word(code.fragment())) })(input) } diff --git a/libcraft/text/src/text/markdown/parser.rs b/libcraft/text/src/text/markdown/parser.rs index 60ba2d9c5..52ed26bd4 100644 --- a/libcraft/text/src/text/markdown/parser.rs +++ b/libcraft/text/src/text/markdown/parser.rs @@ -9,7 +9,7 @@ use nom::{Err, IResult}; pub mod events; -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct DynamicSpan { pub fragment: String, pub col: usize, diff --git a/libcraft/text/src/text/markdown/parser/events.rs b/libcraft/text/src/text/markdown/parser/events.rs index 73932a840..d3e933318 100644 --- a/libcraft/text/src/text/markdown/parser/events.rs +++ b/libcraft/text/src/text/markdown/parser/events.rs @@ -4,13 +4,13 @@ pub enum EventParseError<'a> { InvalidEventAction(&'a str), } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum EventType { OnHover, OnClick, } -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum EventAction { ShowText, OpenUrl, diff --git a/quill/api/src/setup.rs b/quill/api/src/setup.rs index 0b555b25f..07fdebf96 100644 --- a/quill/api/src/setup.rs +++ b/quill/api/src/setup.rs @@ -24,6 +24,7 @@ impl Setup { /// /// The function should take as parameters your /// plugin instance and an `&mut Game` and return nothing. + #[allow(clippy::type_complexity)] pub fn add_system(&mut self, system: T) -> &mut Self { let system: Box = Box::new(system); let system_data = Box::leak(Box::new(system)) as *mut Box<_> as *mut u8; diff --git a/quill/common/src/components.rs b/quill/common/src/components.rs index 352c45b69..b0fecea22 100644 --- a/quill/common/src/components.rs +++ b/quill/common/src/components.rs @@ -45,7 +45,7 @@ impl Name { } pub fn as_str(&self) -> &str { - &*self + self } } @@ -73,7 +73,7 @@ impl CustomName { } pub fn as_str(&self) -> &str { - &*self + self } pub fn as_mut_str(&mut self) -> &mut str { diff --git a/quill/plugin-format/src/lib.rs b/quill/plugin-format/src/lib.rs index 20a55c417..0c7ae0c53 100644 --- a/quill/plugin-format/src/lib.rs +++ b/quill/plugin-format/src/lib.rs @@ -43,7 +43,7 @@ impl<'a> PluginFile<'a> { /// this is the contents of the shared library /// containing the plugin. pub fn module(&self) -> &[u8] { - &*self.module + &self.module } pub fn metadata(&self) -> &PluginMetadata { diff --git a/quill/plugin-format/src/metadata.rs b/quill/plugin-format/src/metadata.rs index 66cd70eb5..b72b3e1a7 100644 --- a/quill/plugin-format/src/metadata.rs +++ b/quill/plugin-format/src/metadata.rs @@ -3,7 +3,7 @@ use serde_with::{serde_as, DisplayFromStr}; use target_lexicon::Triple; /// A plugin's metadata, stored alongside its WASM module. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PluginMetadata { /// Plugin name, no spaces pub name: String, From 42e78d0fb8bb8460928b87668b7de5a2a106e22e Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Mon, 29 Aug 2022 01:23:08 +0200 Subject: [PATCH 11/15] Fix mining animation Greatly simplify code Add an ad hoc gamemode changing system --- feather/common/src/block_break.rs | 239 +++++------------- feather/common/src/game.rs | 9 +- feather/server/src/packet_handlers.rs | 16 +- .../server/src/packet_handlers/interaction.rs | 75 +----- feather/server/src/systems/player_join.rs | 2 - 5 files changed, 99 insertions(+), 242 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 4a7adaf3a..3cb9ed7fc 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -1,150 +1,74 @@ -use std::mem; - -use base::{BlockKind, ItemStack, ValidBlockPosition}; -use ecs::{EntityBuilder, SysResult, SystemExecutor}; +use anyhow::Context; +use base::{inventory::SLOT_HOTBAR_OFFSET, BlockKind, ItemStack, ValidBlockPosition}; +use ecs::{Entity, SysResult, SystemExecutor}; use libcraft_items::EnchantmentKind; -use quill_common::{entities::Player, entity_init::EntityInit}; +use quill_common::components::Instabreak; pub struct DestroyStateChange(pub ValidBlockPosition, pub u8); -use crate::{Game, World}; +use crate::{entities::player::HotbarSlot, Game, Window, World}; // Comparing to 0.7 ensures good feeling in the client pub const BREAK_THRESHOLD: f32 = 0.7; -#[derive(Clone)] -pub enum BlockBreaker { - Active(ActiveBreaker), - Finished(FinishedBreaker), - Inactive, -} -impl BlockBreaker { - /// Create a new active instance pointing to `block_pos`. Calculates the time needed using `world.block_at(block_pos)` and `equipped_item`. - pub fn new( - world: &mut World, - block_pos: ValidBlockPosition, - equipped_item: Option<&ItemStack>, - ) -> Self { - ActiveBreaker::new(world, block_pos, equipped_item) - .map(Self::Active) - .unwrap_or(Self::Inactive) - } - /// If active, produces a `DestroyStateChange` event for the adequate position. - pub fn destroy_change_event(&self) -> Option { - Some(DestroyStateChange(self.position()?, self.destroy_stage())) - } - /// If active or finished, returns the pointed to position. - pub fn position(&self) -> Option { - match self { - BlockBreaker::Active(a) => Some(a.position), - BlockBreaker::Finished(f) => Some(f.position), - BlockBreaker::Inactive => None, - } - } - /// If active, returns the underlying `ActiveBreaker`. - pub fn active(&self) -> Option<&ActiveBreaker> { - match self { - Self::Active(a) => Some(a), - _ => None, - } - } - /// If finished, returns the underlying `FinishedBreaker`. - pub fn finished(&self) -> Option<&FinishedBreaker> { - match self { - Self::Finished(f) => Some(f), - _ => None, - } - } - /// Progresses block breaking. Returns a (newly_finished, do_destry_state_change) tuple. - /// If this operation finishes block breaking, this turns `self` into `Self::Finished` with the same position. - pub fn tick(&mut self) -> (bool, bool) { - let (block_break, stage_update) = if let Self::Active(breaker) = self { - breaker.tick() - } else { - (false, false) +pub fn start_digging( + game: &mut Game, + player: Entity, + position: ValidBlockPosition, +) -> anyhow::Result { + if game.ecs.get::(player)?.0 { + game.break_block(position); + } else { + let breaker = { + let window = game.ecs.get::(player)?; + let hotbar_slot = game.ecs.get::(player)?.get(); + let main_hand = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; + ActiveBreaker::new(&mut game.world, position, main_hand.option_ref()) + .context("Cannot mine this block")? }; - if block_break { - let fin = match mem::take(self) { - Self::Active(a) => a.finish(), - _ => unreachable!(), - }; - *self = Self::Finished(fin); - } - (block_break, stage_update) - } - /// Returns the block destroying progress in a range of 0 - 9. When inactive or finished, returns 10. - pub fn destroy_stage(&self) -> u8 { - match self { - BlockBreaker::Active(a) => a.destroy_stage(), - _ => 10, - } - } - /// Set `self` to `Self::Inactive`. - pub fn cancel(&mut self) { - *self = Self::Inactive; - } - /// Check if the breaker points to `pos`. Returns `true` when `self` is `Self::Inactive`. - pub fn matches_position(&self, pos: ValidBlockPosition) -> bool { - match self { - BlockBreaker::Active(a) => a.position == pos, - BlockBreaker::Finished(f) => f.position == pos, - BlockBreaker::Inactive => true, - } - } - /// Attempts to finish breaking the target block, optionally turning `self` into `Self::Finished`. - pub fn try_finish(&mut self) -> Option { - let this = self.clone(); - match this { - BlockBreaker::Active(a) => { - if a.can_break() { - let fin = a.finish(); - *self = Self::Finished(fin.clone()); - Some(fin) - } else { - None - } - } - BlockBreaker::Finished(f) => Some(f), - BlockBreaker::Inactive => None, - } - } -} -impl Default for BlockBreaker { - fn default() -> Self { - Self::Inactive + game.ecs.insert(player, breaker)?; } + Ok(true) } -#[derive(Clone)] -pub struct FinishedBreaker { - pub position: ValidBlockPosition, - pub drop_item: bool, - pub fake_finished: bool, +pub fn cancel_digging( + game: &mut Game, + player: Entity, + position: ValidBlockPosition, +) -> anyhow::Result { + if game.ecs.get::(player).is_err() { + return Ok(false); + } + game.ecs.remove::(player)?; + game.ecs + .insert_entity_event(player, DestroyStateChange(position, 10))?; + Ok(true) } -impl FinishedBreaker { - /// Breaks the targeted block and spawns its drops. TODO: make drops work. - pub fn break_block(&self, game: &mut Game) -> SysResult { - let target_block = match game.block(self.position) { - Some(b) => b, - None => anyhow::bail!("cannot break unloaded block"), - }; - game.break_block(self.position); - if let Some(_item_drop) = base::Item::from_name(target_block.kind().name()) { - if !self.drop_item { - return Ok(()); - } - let mut item_entity = EntityBuilder::new(); - crate::entities::item::build_default(&mut item_entity); - let builder = game.create_entity_builder(self.position.position(), EntityInit::Item); - game.spawn_entity(builder); - } - Ok(()) - } +pub fn finish_digging( + game: &mut Game, + player: Entity, + position: ValidBlockPosition, +) -> anyhow::Result { + if game.ecs.get::(player)?.0 { + return Ok(true); + } + let success = if let Ok(breaker) = game.ecs.get::(player) { + breaker.can_break() + } else { + false + }; + if success { + let pos = game.ecs.get::(player)?.position; + game.break_block(pos); // TODO: drop an item + game.ecs.remove::(player)?; + } + game.ecs + .insert_entity_event(player, DestroyStateChange(position, 10))?; + Ok(success) } #[derive(Clone)] pub struct ActiveBreaker { pub position: ValidBlockPosition, pub drop_item: bool, - pub fake_finished: bool, pub progress: f32, pub damage: f32, } @@ -154,7 +78,7 @@ impl ActiveBreaker { self.progress += self.damage; let after = self.destroy_stage(); let break_block = self.can_break(); - let change_stage = before != after || break_block; + let change_stage = break_block || before != after; (break_block, change_stage) } /// Check if the block has been damaged enough to break. @@ -175,21 +99,16 @@ impl ActiveBreaker { (Some(_), None) => false, (Some(tools), Some(tool)) => tools.contains(&tool.item()), }; - let dig_multiplier = block + let dig_multiplier = block // TODO: calculate with Haste effect .dig_multipliers() .iter() .find_map(|(item, speed)| { - equipped_item - .and_then(|e| { - bool::then_some(e.item() == *item, *speed) - }) + equipped_item.and_then(|e| bool::then_some(e.item() == *item, *speed)) }) .unwrap_or(1.0); let effi_level = equipped_item .and_then(ItemStack::metadata) - .and_then(|meta| { - meta.get_enchantment_level(EnchantmentKind::Efficiency) - }); + .and_then(|meta| meta.get_enchantment_level(EnchantmentKind::Efficiency)); let effi_speed = effi_level.map(|level| level * level + 1).unwrap_or(0) as f32; let damage = if harvestable { (dig_multiplier + effi_speed) / block.hardness() / 30.0 @@ -199,25 +118,16 @@ impl ActiveBreaker { Some(Self { position: block_pos, drop_item: true, - fake_finished: false, - progress: 0.0, + progress: damage, damage, }) } /// Get the destroying progress. pub fn destroy_stage(&self) -> u8 { - if self.fake_finished { - 10 - } else { - (self.progress / BREAK_THRESHOLD * 9.0).round() as u8 - } + (self.progress * 9.0).round() as u8 } - pub fn finish(self) -> FinishedBreaker { - FinishedBreaker { - position: self.position, - drop_item: self.drop_item, - fake_finished: self.fake_finished, - } + pub fn destroy_change_event(&self) -> DestroyStateChange { + DestroyStateChange(self.position, self.destroy_stage()) } } @@ -226,36 +136,19 @@ pub fn register(systems: &mut SystemExecutor) { } fn process_block_breaking(game: &mut Game) -> SysResult { - let mut break_queue = vec![]; let mut update_queue = vec![]; - for (entity, breaker) in game.ecs.query::<&mut BlockBreaker>().iter() { - let (break_block, update_stage) = breaker.tick(); + for (entity, breaker) in game.ecs.query::<&mut ActiveBreaker>().iter() { + let (_, update_stage) = breaker.tick(); if update_stage { update_queue.push(entity); } - // Break block when client requests to finish in order to prevent desyncs - if break_block && breaker.finished().unwrap().fake_finished - || game.ecs.get::(entity).is_err() - { - break_queue.push(entity); - } } for entity in update_queue { let event = game .ecs - .get_mut::(entity)? - .destroy_change_event() - .unwrap(); + .get_mut::(entity)? + .destroy_change_event(); game.ecs.insert_entity_event(entity, event)?; } - for entity in break_queue.into_iter() { - let breaker = game - .ecs - .get::(entity)? - .finished() - .unwrap() - .clone(); - breaker.break_block(game)?; - } Ok(()) } diff --git a/feather/common/src/game.rs b/feather/common/src/game.rs index 64e6bf87d..0ab71fd95 100644 --- a/feather/common/src/game.rs +++ b/feather/common/src/game.rs @@ -1,11 +1,11 @@ use std::{cell::RefCell, mem, rc::Rc, sync::Arc}; -use base::{BlockId, ChunkPosition, Position, Text, Title, ValidBlockPosition}; +use base::{BlockId, ChunkPosition, Gamemode, Position, Text, Title, ValidBlockPosition}; use ecs::{ Ecs, Entity, EntityBuilder, HasEcs, HasResources, NoSuchEntity, Resources, SysResult, SystemExecutor, }; -use quill_common::events::{EntityCreateEvent, EntityRemoveEvent, PlayerJoinEvent}; +use quill_common::events::{EntityCreateEvent, EntityRemoveEvent, GamemodeEvent, PlayerJoinEvent}; use quill_common::{entities::Player, entity_init::EntityInit}; use crate::{ @@ -230,6 +230,11 @@ impl Game { pub fn break_block(&mut self, pos: ValidBlockPosition) -> bool { self.set_block(pos, BlockId::air()) } + + pub fn set_gamemode(&mut self, player: Entity, new: Gamemode) -> SysResult { + self.ecs.insert_entity_event(player, GamemodeEvent(new))?; + Ok(()) + } } impl HasResources for Game { diff --git a/feather/server/src/packet_handlers.rs b/feather/server/src/packet_handlers.rs index b0452c96a..019495530 100644 --- a/feather/server/src/packet_handlers.rs +++ b/feather/server/src/packet_handlers.rs @@ -1,4 +1,4 @@ -use base::{Position, Text}; +use base::{Gamemode, Position, Text}; use common::{chat::ChatKind, Game}; use ecs::{Entity, EntityRef, SysResult}; use interaction::{ @@ -45,7 +45,7 @@ pub fn handle_packet( ClientPlayPacket::Animation(packet) => handle_animation(server, player, packet), - ClientPlayPacket::ChatMessage(packet) => handle_chat_message(game, player, packet), + ClientPlayPacket::ChatMessage(packet) => handle_chat_message(game, player_id, packet), ClientPlayPacket::PlayerDigging(packet) => { handle_player_digging(game, server, packet, player_id) @@ -132,8 +132,16 @@ fn handle_animation( Ok(()) } -fn handle_chat_message(game: &Game, player: EntityRef, packet: client::ChatMessage) -> SysResult { - let name = player.get::()?; +fn handle_chat_message(game: &mut Game, player: Entity, packet: client::ChatMessage) -> SysResult { + if let m @ ("c" | "s") = &*packet.message { + if m == "c" { + game.set_gamemode(player, Gamemode::Creative)?; + } else { + game.set_gamemode(player, Gamemode::Survival)?; + } + } + + let name = game.ecs.get::(player)?; let message = Text::translate_with("chat.type.text", vec![name.to_string(), packet.message]); game.broadcast_chat(ChatKind::PlayerChat, message); Ok(()) diff --git a/feather/server/src/packet_handlers/interaction.rs b/feather/server/src/packet_handlers/interaction.rs index f17671f6c..a01b3621a 100644 --- a/feather/server/src/packet_handlers/interaction.rs +++ b/feather/server/src/packet_handlers/interaction.rs @@ -1,7 +1,5 @@ use crate::{ClientId, NetworkId, Server}; use base::inventory::{SLOT_HOTBAR_OFFSET, SLOT_OFFHAND}; -use base::Gamemode; -use common::block_break::{ActiveBreaker, BlockBreaker, DestroyStateChange}; use common::entities::player::HotbarSlot; use common::interactable::InteractableRegistry; use common::{Game, Window}; @@ -121,84 +119,39 @@ pub fn handle_player_digging( ) -> SysResult { log::trace!("Got player digging with status {:?}", packet.status); let client = server.clients.get(*game.ecs.get(player)?).unwrap(); + use PlayerDiggingStatus::*; + if matches!(packet.status, StartDigging | CancelDigging | FinishDigging) + && game.block(packet.position).is_none() + { + client.disconnect("Cannot interact with unloaded block!"); + anyhow::bail!("Cannot interact with unloaded block!") + } match packet.status { PlayerDiggingStatus::StartDigging => { - if matches!( - *game.ecs.get::(player)?, - Gamemode::Creative | Gamemode::Spectator - ) { - game.break_block(packet.position); - } else { - let mut breaker = game.ecs.get_mut::(player)?; - let window = game.ecs.get::(player)?; - let hotbar_slot = game.ecs.get::(player)?.get(); - let main_hand = window.item(SLOT_HOTBAR_OFFSET + hotbar_slot)?; - *breaker = BlockBreaker::Active( - ActiveBreaker::new(&mut game.world, packet.position, main_hand.option_ref()) - .unwrap(), - ); - } - let block = match game.block(packet.position) { - Some(s) => s, - None => return Ok(()), - }; + let success = common::block_break::start_digging(game, player, packet.position)?; client.acknowledge_player_digging( packet.position, - block, + game.block(packet.position).unwrap(), protocol::packets::server::PlayerDiggingStatus::Started, - true, + success, ); Ok(()) } PlayerDiggingStatus::CancelDigging => { - let success = { - let mut breaker = game.ecs.get_mut::(player)?; - let a = breaker.matches_position(packet.position); - breaker.cancel(); - a - }; - game.ecs - .insert_entity_event(player, DestroyStateChange(packet.position, 10))?; - let block = match game.block(packet.position) { - Some(s) => s, - None => return Ok(()), - }; + let success = common::block_break::cancel_digging(game, player, packet.position)?; client.acknowledge_player_digging( packet.position, - block, + game.block(packet.position).unwrap(), protocol::packets::server::PlayerDiggingStatus::Cancelled, success, ); Ok(()) } PlayerDiggingStatus::FinishDigging => { - let mut finished = None; - let success = { - let mut breaker = game.ecs.get_mut::(player)?; - let success = if let Some(f) = breaker.try_finish() { - finished = Some(f); - breaker.cancel(); - true - } else { - if let BlockBreaker::Active(a) = &mut *breaker { - a.fake_finished = true; - } - false - }; - success && breaker.matches_position(packet.position) - }; - if success { - finished.unwrap().break_block(game)?; - } - game.ecs - .insert_entity_event(player, DestroyStateChange(packet.position, 10))?; - let block = match game.block(packet.position) { - Some(s) => s, - None => return Ok(()), - }; + let success = common::block_break::finish_digging(game, player, packet.position)?; client.acknowledge_player_digging( packet.position, - block, + game.block(packet.position).unwrap(), protocol::packets::server::PlayerDiggingStatus::Finished, success, ); diff --git a/feather/server/src/systems/player_join.rs b/feather/server/src/systems/player_join.rs index 3682bac13..31cb2af3e 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -1,4 +1,3 @@ -use common::block_break::BlockBreaker; use libcraft_items::InventorySlot; use log::debug; @@ -127,7 +126,6 @@ fn accept_new_player(game: &mut Game, server: &mut Server, client_id: ClientId) .map(|data| data.animal.health) .unwrap_or(20.0), )) - .add(BlockBreaker::Inactive) .add(abilities.walk_speed) .add(abilities.fly_speed) .add(abilities.is_flying) From cbc495097cc365a6f76fa11a5f72cfd1bbad7e21 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:46:56 +0200 Subject: [PATCH 12/15] Fix CI --- feather/blocks/generator/src/lib.rs | 3 ++- quill/common/src/component.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/feather/blocks/generator/src/lib.rs b/feather/blocks/generator/src/lib.rs index b5b8fbd84..bd48b2ff3 100644 --- a/feather/blocks/generator/src/lib.rs +++ b/feather/blocks/generator/src/lib.rs @@ -369,7 +369,8 @@ fn generate_block_fns(blocks: &Blocks) -> TokenStream { } for (name, value) in default_state { - doc.push_str(&format!("* `{}`: {}\n", name, value)); + use core::fmt::Write as _; + let _ = writeln!(doc, "* `{}`: {}", name, value); } fns.push(quote! { diff --git a/quill/common/src/component.rs b/quill/common/src/component.rs index 11e4c4651..a79655d82 100644 --- a/quill/common/src/component.rs +++ b/quill/common/src/component.rs @@ -327,7 +327,7 @@ pod_component_impl!(Position); /** If you are using this macro and you get the error: -``` +```norun error[E0599]: no variant or associated item named `...` found for enum `HostComponent` in the current scope. ``` Then you need to go to the top of the file were this macro is defined. There you find the HostCompoent enum, that From d9914f1be895223dbc768d7918035b83f161d44b Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:23:18 +0200 Subject: [PATCH 13/15] Add rudimentary documentation --- feather/common/src/block_break.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 3cb9ed7fc..34eba9eda 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -8,9 +8,9 @@ pub struct DestroyStateChange(pub ValidBlockPosition, pub u8); use crate::{entities::player::HotbarSlot, Game, Window, World}; -// Comparing to 0.7 ensures good feeling in the client pub const BREAK_THRESHOLD: f32 = 0.7; +/// Handle a player's request to start digging the block at `position`. Adds an `ActiveBreaker` component to `player`. pub fn start_digging( game: &mut Game, player: Entity, @@ -30,6 +30,7 @@ pub fn start_digging( } Ok(true) } +/// Handle a player's request to stop digging the block at `position`. Removes `ActiveBreaker` from `player`. pub fn cancel_digging( game: &mut Game, player: Entity, @@ -43,6 +44,10 @@ pub fn cancel_digging( .insert_entity_event(player, DestroyStateChange(position, 10))?; Ok(true) } +/// Handle a player's request to finish digging the block at `position`. +/// This is called when the block breaking finishes at the player's side. +/// +/// Will return `false` if the system hasn't finished breaking the block yet. pub fn finish_digging( game: &mut Game, player: Entity, @@ -65,6 +70,8 @@ pub fn finish_digging( .insert_entity_event(player, DestroyStateChange(position, 10))?; Ok(success) } +/// Main component for the block breaking system. +/// Tracks the position of the block being mined, whether it will drop an item and the current progress. #[derive(Clone)] pub struct ActiveBreaker { pub position: ValidBlockPosition, @@ -73,6 +80,7 @@ pub struct ActiveBreaker { pub damage: f32, } impl ActiveBreaker { + /// Advance block breaking by one tick. pub fn tick(&mut self) -> (bool, bool) { let before = self.destroy_stage(); self.progress += self.damage; @@ -85,19 +93,23 @@ impl ActiveBreaker { pub fn can_break(&self) -> bool { self.progress >= BREAK_THRESHOLD - self.damage / 2.0 } + /// Start breaking a block. Returns an error if the block at `block_pos` is unloaded or not diggable. pub fn new( world: &mut World, block_pos: ValidBlockPosition, equipped_item: Option<&ItemStack>, - ) -> Option { - let block = world.block_at(block_pos)?.kind(); + ) -> anyhow::Result { + let block = world + .block_at(block_pos) + .context("Block is not loaded")? + .kind(); if !block.diggable() || block == BlockKind::Air { - return None; + anyhow::bail!("Block is not diggable") } let harvestable = match (block.harvest_tools(), equipped_item) { (None, None | Some(_)) => true, (Some(_), None) => false, - (Some(tools), Some(tool)) => tools.contains(&tool.item()), + (Some(valid_tools), Some(equipped)) => valid_tools.contains(&equipped.item()), }; let dig_multiplier = block // TODO: calculate with Haste effect .dig_multipliers() @@ -115,9 +127,9 @@ impl ActiveBreaker { } else { 1.0 / block.hardness() / 100.0 }; - Some(Self { + Ok(Self { position: block_pos, - drop_item: true, + drop_item: harvestable, progress: damage, damage, }) From 9ce3652ba057d46afb9716f57eb3c48b0f9d5186 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:02:34 +0200 Subject: [PATCH 14/15] Change `norun` to `no_run` Remove effects system --- feather/common/src/block_break.rs | 1 + feather/server/src/systems/effects.rs | 226 ------------------------- libcraft/effects/src/effects.rs | 47 ----- libcraft/effects/src/lib.rs | 4 - libcraft/generators/python/effects.py | 25 --- quill/common/src/component.rs | 2 +- quill/common/src/components_effects.rs | 112 ------------ 7 files changed, 2 insertions(+), 415 deletions(-) delete mode 100644 feather/server/src/systems/effects.rs delete mode 100644 libcraft/effects/src/effects.rs delete mode 100644 libcraft/effects/src/lib.rs delete mode 100644 libcraft/generators/python/effects.py delete mode 100644 quill/common/src/components_effects.rs diff --git a/feather/common/src/block_break.rs b/feather/common/src/block_break.rs index 34eba9eda..0de68fa5f 100644 --- a/feather/common/src/block_break.rs +++ b/feather/common/src/block_break.rs @@ -99,6 +99,7 @@ impl ActiveBreaker { block_pos: ValidBlockPosition, equipped_item: Option<&ItemStack>, ) -> anyhow::Result { + // https://minecraft.fandom.com/wiki/Breaking let block = world .block_at(block_pos) .context("Block is not loaded")? diff --git a/feather/server/src/systems/effects.rs b/feather/server/src/systems/effects.rs deleted file mode 100644 index 4a255452b..000000000 --- a/feather/server/src/systems/effects.rs +++ /dev/null @@ -1,226 +0,0 @@ -use base::{Particle, ParticleKind, TPS}; -use common::Game; -use ecs::{Entity, SysResult, SystemExecutor}; -use libcraft_core::Position; -use libcraft_effects::effect::Effect; -use quill_common::components_effects::WalkEffectModifier; -use quill_common::entity_init::EntityInit; -use std::collections::HashMap; - -use crate::{Client, ClientId, NetworkId, Server}; - -pub fn register(_game: &mut Game, systems: &mut SystemExecutor) { - systems - .group::() - .add_system(add_start_tick_to_speed_effects) - .add_system(speed_effect) - .add_system(walk_effect_modifier_cleaner) - .add_system(effect_remover); -} - -fn speed_effect(game: &mut Game, _server: &mut Server) -> SysResult { - let mut new_walk_speed = HashMap::new(); - for (entity, speed) in game.ecs.query::<&mut SpeedEffect>().iter() { - if speed.0.is_empty() { - continue; - } - - if let Some(effect_ref) = speed.active_effect() { - let modifier = 20 * (effect_ref.amplifier + 1) as i32; - new_walk_speed.insert(entity, modifier); - }; - } - - for (entity, modifier) in new_walk_speed { - change_modifier(game, entity, modifier)?; - } - - Ok(()) -} - -fn effect_remover(game: &mut Game, server: &mut Server) -> SysResult { - let mut new_walk_speed = HashMap::new(); - for (entity, (&client_id, speed, &network_id)) in game - .ecs - .query::<(&ClientId, &mut SpeedEffect, &NetworkId)>() - .iter() - { - if speed.0.is_empty() { - continue; - } - - let end_effect = speed.ended_on_tick(game.tick_count); - - for effect in end_effect.iter() { - if let Some(client) = server.clients.get(client_id) { - client.send_remove_entity_effect(network_id, Effect::SpeedEffect.id() as u8); - } - - if speed.0.remove(effect) { - log::debug!("speed effect was removed with params {:?}", effect) - } - new_walk_speed.insert(entity, 0); - } - - if !end_effect.is_empty() { - if let Some(active_effect) = speed.active_effect() { - if let Some(client) = server.clients.get(client_id) { - let duration = active_effect.duration as u64 - - (game.tick_count - active_effect.start_tick); - - send_effect_to_client( - Effect::SpeedEffect.id() as u8, - network_id, - active_effect, - client, - duration, - ); - } - } - } - } - - for (entity, modifier) in new_walk_speed { - change_modifier(game, entity, modifier)?; - } - - Ok(()) -} - -fn walk_effect_modifier_cleaner(game: &mut Game, _server: &mut Server) -> SysResult { - let mut rem_comp = vec![]; - - for (entity, wm) in game.ecs.query::<&mut WalkEffectModifier>().iter() { - let mut rem_wm = vec![]; - - for (effect, modifier) in wm.0.iter() { - if *modifier == 0 { - rem_wm.push(*effect); - } - } - - for effect in rem_wm { - wm.0.remove(&effect); - } - - if wm.0.is_empty() { - rem_comp.push(entity); - } - } - - for entity in rem_comp { - game.ecs.remove::(entity)?; - } - - Ok(()) -} - -fn change_modifier(game: &mut Game, entity: Entity, new_modifier: i32) -> SysResult { - if game.ecs.get::(entity).is_err() { - game.ecs.insert(entity, WalkEffectModifier::new())?; - } - - let mut walk_speed_modifier = game.ecs.get_mut::(entity)?; - if walk_speed_modifier.0.contains_key(&Effect::SpeedEffect) - && new_modifier - != *walk_speed_modifier - .0 - .get(&Effect::SpeedEffect) - .unwrap_or(&0) - { - walk_speed_modifier - .0 - .insert(Effect::SpeedEffect, new_modifier); - } - Ok(()) -} - -fn send_effect_to_client( - effect_id: u8, - network_id: NetworkId, - active_effect: &EffectApplication, - client: &Client, - duration: u64, -) { - if duration == 0 { - return; - } - - client.send_entity_effect( - network_id, - effect_id, - active_effect.amplifier as i8, - duration as i32, - active_effect.flags, - ); -} - -// todo change particle color -fn add_particles(game: &mut Game, entity: Entity, _effect_kind: Effect) -> SysResult { - if game.tick_count % (TPS * 2) as u64 == 0 { - let position = *game.ecs.get::(entity)?; - - let mut entity_builder = game.create_entity_builder(position, EntityInit::AreaEffectCloud); - - entity_builder.add(position); - entity_builder.add(Particle { - kind: ParticleKind::Effect, - offset_x: 0.0, - offset_y: 0.0, - offset_z: 0.0, - count: 5, - }); - game.spawn_entity(entity_builder); - } - Ok(()) -} - -/// Set start_tick to all effects in effects_bucket and spawn particles -macro_rules! add_start_tick_to_effects { - ($fn_name:ident,$type:ident) => { - fn $fn_name(game: &mut Game, server: &mut Server) -> SysResult { - let mut entities = vec![]; - for (entity, (&client_id, effects_bucket, &network_id)) in game - .ecs - .query::<(&ClientId, &mut $type, &NetworkId)>() - .iter() - { - if effects_bucket.0.is_empty() { - continue; - } - - if let Some(active_effect) = effects_bucket.active_effect() { - if active_effect.flags.particle { - entities.push((entity, Effect::$type)); - } - } - - let not_started = effects_bucket.not_started(); - - for mut effect in not_started { - effect.start_tick = game.tick_count; - - if let Some(client) = server.clients.get(client_id) { - send_effect_to_client( - Effect::$type.id() as u8, - network_id, - &effect, - client, - effect.duration as u64, - ); - } - - effects_bucket.0.replace(effect); - } - } - - for (entity, effect_kind) in entities { - add_particles(game, entity, effect_kind)?; - } - - Ok(()) - } - }; -} - -add_start_tick_to_effects!(add_start_tick_to_speed_effects, SpeedEffect); diff --git a/libcraft/effects/src/effects.rs b/libcraft/effects/src/effects.rs deleted file mode 100644 index a269e676f..000000000 --- a/libcraft/effects/src/effects.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::cmp::Ordering; - -use serde::{Serialize, Deserialize}; - -use crate::Effect; - -/// Storing effects info. -#[derive(Copy, Clone, Debug, Hash, Serialize, PartialEq, Eq, Deserialize)] -pub struct EffectApplication { - /// Effect kind - pub kind: Effect, - /// Effect intensity, up to 255. - pub amplifier: u8, - /// Duration of the effect in ticks. - pub duration: u32, - /// Effect flags - pub flags: EffectFlags, - - /// Store when effect was added, if start_tick == 0 effect not yet sent to client - pub start_tick: u64, -} -/// Flags that define how an effect is presented. -#[derive(Copy, Clone, Debug, Hash, Serialize, PartialEq, Eq, Deserialize)] -pub struct EffectFlags { - /// `true` if there should be visible particles. - pub particle: bool, - /// `true` when caused by a beacon. - pub ambient: bool, - /// `true` if an icon should be shown to the player. - pub icon: bool, -} -impl Ord for EffectApplication { - fn cmp(&self, other: &Self) -> Ordering { - if self.amplifier > other.amplifier || self.duration > other.duration { - Ordering::Greater - } else if self.amplifier == other.amplifier || self.duration == other.duration { - Ordering::Equal - } else { - Ordering::Less - } - } -} -impl PartialOrd for EffectApplication { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} \ No newline at end of file diff --git a/libcraft/effects/src/lib.rs b/libcraft/effects/src/lib.rs deleted file mode 100644 index 89f2cf0c7..000000000 --- a/libcraft/effects/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod effect; -pub use effect::Effect; -pub mod effects; -pub use effects::EffectApplication; diff --git a/libcraft/generators/python/effects.py b/libcraft/generators/python/effects.py deleted file mode 100644 index 024708deb..000000000 --- a/libcraft/generators/python/effects.py +++ /dev/null @@ -1,25 +0,0 @@ -from common import load_minecraft_json, camel_case, generate_enum, generate_enum_property, output - -effects = [] -ids = {} -names = {} -display_names = {} -is_good = {} - -for effect in load_minecraft_json("effects.json", "1.16.1"): - variant = effect['name'] - effects.append(variant) - ids[variant] = effect['id'] - names[variant] = effect['name'] - display_names[variant] = effect['displayName'] - is_good[variant] = True if effect['type'] == "good" else False - -enumName = "Effect" - -output_data = generate_enum(enumName, effects, ["serde::Deserialize", "serde::Serialize"]) -output_data += generate_enum_property(enumName, "id", "u8", ids, True) -output_data += generate_enum_property(enumName, "name", "&str", names, True, "&'static str") -output_data += generate_enum_property(enumName, "display_name", "&str", display_names, True, "&'static str") -output_data += generate_enum_property(enumName, "is_good", "bool", is_good) - -output("effects/src/effect.rs", output_data) diff --git a/quill/common/src/component.rs b/quill/common/src/component.rs index a79655d82..dc93d84f4 100644 --- a/quill/common/src/component.rs +++ b/quill/common/src/component.rs @@ -327,7 +327,7 @@ pod_component_impl!(Position); /** If you are using this macro and you get the error: -```norun +```no_run error[E0599]: no variant or associated item named `...` found for enum `HostComponent` in the current scope. ``` Then you need to go to the top of the file were this macro is defined. There you find the HostCompoent enum, that diff --git a/quill/common/src/components_effects.rs b/quill/common/src/components_effects.rs deleted file mode 100644 index 9f2f0f907..000000000 --- a/quill/common/src/components_effects.rs +++ /dev/null @@ -1,112 +0,0 @@ -use libcraft_effects::{effects::EffectApplication, Effect}; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; - -#[derive(Default, Serialize, Deserialize, Eq, PartialEq, Hash)] -pub struct Effects(pub BTreeSet); -impl Effects { - pub fn new() -> Self { - Self::default() - } - pub fn add_effect(&mut self, effect: EffectApplication) { - self.0.insert(effect); - } - pub fn strongest_active(&self, kind: Effect) -> Option<&EffectApplication> { - self.0.iter().filter(|e| e.kind == kind).last() - } -} -bincode_component_impl!(Effects); -/* -macro_rules! impl_effect { - ($ident:ident) => { - #[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] - pub struct $ident(pub BTreeSet); - impl $ident { - pub fn new() -> $ident { - $ident { 0: BTreeSet::new() } - } - pub fn add_effect(&mut self, effect: EffectApplication) -> bool { - self.0.insert(effect) - } - pub fn not_started(&mut self) -> Vec { - self.0 - .iter() - .filter(|effect| effect.start_tick == 0) - .cloned() - .collect::>() - } - pub fn ended_on_tick(&mut self, tick: u64) -> Vec { - self.0 - .iter() - .filter(|effect| { - tick >= effect.start_tick + effect.duration as u64 && effect.start_tick != 0 - }) - .cloned() - .collect::>() - } - pub fn active_effect(&mut self) -> Option<&EffectApplication> { - self.0.iter().last() - } - } - impl Default for $ident { - fn default() -> Self { - $ident::new() - } - } - bincode_component_impl!($ident); - }; -} - - -impl_effect!(SpeedEffect); -impl_effect!(SlownessEffect); -impl_effect!(HasteEffect); -impl_effect!(MiningFatigueEffect); -impl_effect!(StrengthEffect); -impl_effect!(InstantHealthEffect); -impl_effect!(InstantDamageEffect); -impl_effect!(JumpBoostEffect); -impl_effect!(NauseaEffect); -impl_effect!(RegenerationEffect); -impl_effect!(ResistanceEffect); -impl_effect!(FireResistanceEffect); -impl_effect!(WaterBreathingEffect); -impl_effect!(InvisibilityEffect); -impl_effect!(BlindnessEffect); -impl_effect!(NightVisionEffect); -impl_effect!(HungerEffect); -impl_effect!(WeaknessEffect); -impl_effect!(PoisonEffect); -impl_effect!(WitherEffect); -impl_effect!(HealthBoostEffect); -impl_effect!(AbsorptionEffect); -impl_effect!(SaturationEffect); -impl_effect!(GlowingEffect); -impl_effect!(LevitationEffect); -impl_effect!(LuckEffect); -impl_effect!(BadLuckEffect); -impl_effect!(SlowFallingEffect); -impl_effect!(ConduitPowerEffect); -impl_effect!(DolphinsGraceEffect); -impl_effect!(BadOmenEffect); -impl_effect!(HeroOfTheVillageEffect); -*/ -/// A walk speed modifier in percent -#[derive( - Clone, Debug, PartialEq, Serialize, Deserialize, derive_more::Deref, derive_more::DerefMut, -)] -pub struct WalkEffectModifier(pub BTreeMap); - -impl WalkEffectModifier { - pub fn new() -> WalkEffectModifier { - WalkEffectModifier { 0: BTreeMap::new() } - } -} - -impl Default for WalkEffectModifier { - fn default() -> Self { - WalkEffectModifier::new() - } -} - -bincode_component_impl!(WalkEffectModifier); From accc62e09d2167ab83eafbbb5764b279b960c269 Mon Sep 17 00:00:00 2001 From: koskja <87826472+koskja@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:10:02 +0200 Subject: [PATCH 15/15] Fix doctest attribute --- quill/common/src/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quill/common/src/component.rs b/quill/common/src/component.rs index dc93d84f4..009efba9d 100644 --- a/quill/common/src/component.rs +++ b/quill/common/src/component.rs @@ -327,7 +327,7 @@ pod_component_impl!(Position); /** If you are using this macro and you get the error: -```no_run +```text error[E0599]: no variant or associated item named `...` found for enum `HostComponent` in the current scope. ``` Then you need to go to the top of the file were this macro is defined. There you find the HostCompoent enum, that