diff --git a/feather/protocol/src/packets/server/play.rs b/feather/protocol/src/packets/server/play.rs index 01c83b03f..0116b363a 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: f64, + }, + /// 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: f64, + }, + 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 f64, + GameStateChange::WinGame { show_credits } => *show_credits as u8 as f64, + 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 f64, + _ => 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 = f64::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/src/client.rs b/feather/server/src/client.rs index 20211f39b..fd7c3c4d9 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,7 +21,8 @@ use common::{ use libcraft_items::InventorySlot; use packets::server::{Particle, SetSlot, SpawnLivingEntity, UpdateLight, WindowConfirmation}; use protocol::packets::server::{ - EntityPosition, EntityPositionAndRotation, EntityTeleport, HeldItemChange, PlayerAbilities, + ChangeGameState, EntityPosition, EntityPositionAndRotation, EntityTeleport, GameStateChange, + HeldItemChange, PlayerAbilities, }; use protocol::{ packets::{ @@ -42,7 +44,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; @@ -331,6 +332,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); @@ -602,6 +607,12 @@ impl Client { self.send_packet(HeldItemChange { slot }); } + 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/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/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 24ddf090d..f8e01b195 100644 --- a/feather/server/src/systems/player_join.rs +++ b/feather/server/src/systems/player_join.rs @@ -15,6 +15,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}; @@ -131,6 +132,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 2cfc37100..047ac19c6 100644 --- a/feather/server/src/systems/tablist.rs +++ b/feather/server/src/systems/tablist.rs @@ -1,11 +1,12 @@ //! Sends tablist info to clients via the Player Info packet. +use uuid::Uuid; + use base::{Gamemode, ProfileProperty}; use common::Game; use ecs::{SysResult, SystemExecutor}; -use quill_common::events::{EntityRemoveEvent, PlayerJoinEvent}; +use quill_common::events::{EntityRemoveEvent, GamemodeEvent, PlayerJoinEvent}; use quill_common::{components::Name, entities::Player}; -use uuid::Uuid; use crate::{ClientId, Server}; @@ -13,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 { @@ -60,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/quill/common/src/component.rs b/quill/common/src/component.rs index 3169f8ba1..11e4c4651 100644 --- a/quill/common/src/component.rs +++ b/quill/common/src/component.rs @@ -201,6 +201,11 @@ host_component_enum! { PlayerJoinEvent = 1023, EntityRemoveEvent = 1024, EntityCreateEvent = 1025, + GamemodeEvent = 1026, + InstabreakEvent = 1027, + FlyingAbilityEvent = 1028, + BuildingAbilityEvent = 1029, + InvulnerabilityEvent = 1030, } } @@ -367,3 +372,8 @@ 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/events.rs b/quill/common/src/events.rs index f6aa3baab..e253da176 100644 --- a/quill/common/src/events.rs +++ b/quill/common/src/events.rs @@ -1,9 +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 entity::{EntityCreateEvent, EntityRemoveEvent, PlayerJoinEvent}; -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);