Skip to content

Survival block breaking #510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion feather/base/src/anvil/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion feather/blocks/generator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
2 changes: 1 addition & 1 deletion feather/blocks/generator/src/load.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand Down
167 changes: 167 additions & 0 deletions feather/common/src/block_break.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
use anyhow::Context;
use base::{inventory::SLOT_HOTBAR_OFFSET, BlockKind, ItemStack, ValidBlockPosition};
use ecs::{Entity, SysResult, SystemExecutor};
use libcraft_items::EnchantmentKind;
use quill_common::components::Instabreak;

pub struct DestroyStateChange(pub ValidBlockPosition, pub u8);

use crate::{entities::player::HotbarSlot, Game, Window, World};

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,
position: ValidBlockPosition,
) -> anyhow::Result<bool> {
if game.ecs.get::<Instabreak>(player)?.0 {
game.break_block(position);
} else {
let breaker = {
let window = game.ecs.get::<Window>(player)?;
let hotbar_slot = game.ecs.get::<HotbarSlot>(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")?
};
game.ecs.insert(player, breaker)?;
}
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,
position: ValidBlockPosition,
) -> anyhow::Result<bool> {
if game.ecs.get::<ActiveBreaker>(player).is_err() {
return Ok(false);
}
game.ecs.remove::<ActiveBreaker>(player)?;
game.ecs
.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,
position: ValidBlockPosition,
) -> anyhow::Result<bool> {
if game.ecs.get::<Instabreak>(player)?.0 {
return Ok(true);
}
let success = if let Ok(breaker) = game.ecs.get::<ActiveBreaker>(player) {
breaker.can_break()
} else {
false
};
if success {
let pos = game.ecs.get::<ActiveBreaker>(player)?.position;
game.break_block(pos); // TODO: drop an item
game.ecs.remove::<ActiveBreaker>(player)?;
}
game.ecs
.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,
pub drop_item: bool,
pub progress: f32,
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;
let after = self.destroy_stage();
let break_block = self.can_break();
let change_stage = break_block || before != after;
(break_block, change_stage)
}
/// Check if the block has been damaged enough to break.
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>,
) -> anyhow::Result<Self> {
// https://minecraft.fandom.com/wiki/Breaking
let block = world
.block_at(block_pos)
.context("Block is not loaded")?
.kind();
if !block.diggable() || block == BlockKind::Air {
anyhow::bail!("Block is not diggable")
}
let harvestable = match (block.harvest_tools(), equipped_item) {
(None, None | Some(_)) => true,
(Some(_), None) => false,
(Some(valid_tools), Some(equipped)) => valid_tools.contains(&equipped.item()),
};
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))
})
.unwrap_or(1.0);
let effi_level = equipped_item
.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
} else {
1.0 / block.hardness() / 100.0
};
Ok(Self {
position: block_pos,
drop_item: harvestable,
progress: damage,
damage,
})
}
/// Get the destroying progress.
pub fn destroy_stage(&self) -> u8 {
(self.progress * 9.0).round() as u8
}
pub fn destroy_change_event(&self) -> DestroyStateChange {
DestroyStateChange(self.position, self.destroy_stage())
}
}

pub fn register(systems: &mut SystemExecutor<Game>) {
systems.add_system(process_block_breaking);
}

fn process_block_breaking(game: &mut Game) -> SysResult {
let mut update_queue = vec![];
for (entity, breaker) in game.ecs.query::<&mut ActiveBreaker>().iter() {
let (_, update_stage) = breaker.tick();
if update_stage {
update_queue.push(entity);
}
}
for entity in update_queue {
let event = game
.ecs
.get_mut::<ActiveBreaker>(entity)?
.destroy_change_event();
game.ecs.insert_entity_event(entity, event)?;
}
Ok(())
}
9 changes: 7 additions & 2 deletions feather/common/src/game.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions feather/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Game>) {
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);
}
2 changes: 1 addition & 1 deletion feather/datapacks/src/id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion feather/protocol/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ impl<'a> Readable for LengthInferredVecU8<'a> {

impl<'a> Writeable for LengthInferredVecU8<'a> {
fn write(&self, buffer: &mut Vec<u8>, _version: ProtocolVersion) -> anyhow::Result<()> {
buffer.extend_from_slice(&*self.0);
buffer.extend_from_slice(&self.0);
Ok(())
}
}
Expand Down
33 changes: 31 additions & 2 deletions feather/server/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use common::{
use libcraft_items::InventorySlot;
use packets::server::{Particle, SetSlot, SpawnLivingEntity, UpdateLight, WindowConfirmation};
use protocol::packets::server::{
ChangeGameState, EntityPosition, EntityPositionAndRotation, EntityTeleport, GameStateChange,
HeldItemChange, PlayerAbilities,
AcknowledgePlayerDigging, BlockBreakAnimation, ChangeGameState, EntityPosition,
EntityPositionAndRotation, EntityTeleport, GameStateChange, HeldItemChange, PlayerAbilities,
PlayerDiggingStatus,
};
use protocol::{
packets::{
Expand Down Expand Up @@ -611,6 +612,34 @@ 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: u32,
position: ValidBlockPosition,
destroy_stage: u8,
) {
self.send_packet(BlockBreakAnimation {
entity_id: i32::from_le_bytes(entity_id.to_le_bytes()),
position,
destroy_stage,
})
}

pub fn change_gamemode(&self, gamemode: Gamemode) {
self.send_packet(ChangeGameState {
state_change: GameStateChange::ChangeGamemode { gamemode },
Expand Down
2 changes: 1 addition & 1 deletion feather/server/src/initial_handler/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 12 additions & 4 deletions feather/server/src/packet_handlers.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -132,8 +132,16 @@ fn handle_animation(
Ok(())
}

fn handle_chat_message(game: &Game, player: EntityRef, packet: client::ChatMessage) -> SysResult {
let name = player.get::<Name>()?;
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::<Name>(player)?;
let message = Text::translate_with("chat.type.text", vec![name.to_string(), packet.message]);
game.broadcast_chat(ChatKind::PlayerChat, message);
Ok(())
Expand Down
Loading