From 295678e92dead23c6849042491df9d4f316d816a Mon Sep 17 00:00:00 2001 From: jivvy Date: Thu, 29 Dec 2022 02:26:19 -0600 Subject: [PATCH] Add Particles (#150) Implements the clientbound particle packet. Co-authored-by: Ryan Johnson --- README.md | 2 +- examples/particles.rs | 312 +++++++++++++++++++++ packet_inspector/src/main.rs | 6 +- src/client.rs | 20 ++ src/lib.rs | 1 + valence_protocol/src/lib.rs | 1 + valence_protocol/src/packets/s2c.rs | 15 +- valence_protocol/src/particle.rs | 416 ++++++++++++++++++++++++++++ valence_protocol/src/text.rs | 6 +- 9 files changed, 761 insertions(+), 18 deletions(-) create mode 100644 examples/particles.rs create mode 100644 valence_protocol/src/particle.rs diff --git a/README.md b/README.md index f816418..c2446b3 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ place. Here are some noteworthy achievements: - [x] A Fabric mod for extracting data from the game into JSON files. These files are processed by a build script to generate Rust code for the project. The JSON files can be used in other projects as well. - [x] Items +- [x] Particles - [ ] Inventory - [ ] Block entities - [x] Proxy support ([Velocity](https://velocitypowered.com/), [Bungeecord](https://www.spigotmc.org/wiki/bungeecord/) and [Waterfall](https://docs.papermc.io/waterfall)) -- [ ] Sounds, particles, etc. - [ ] Utilities for continuous collision detection Here is a [short video](https://www.youtube.com/watch?v=6P072lKE01s) showing the examples and some of its current diff --git a/examples/particles.rs b/examples/particles.rs new file mode 100644 index 0000000..0162c05 --- /dev/null +++ b/examples/particles.rs @@ -0,0 +1,312 @@ +use std::fmt; +use std::net::SocketAddr; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use valence::prelude::*; + +pub fn main() -> ShutdownResult { + tracing_subscriber::fmt().init(); + + valence::start_server( + Game { + player_count: AtomicUsize::new(0), + }, + ServerState { + player_list: None, + particle_list: create_particle_vec(), + particle_idx: 0, + }, + ) +} + +struct Game { + player_count: AtomicUsize, +} + +struct ServerState { + player_list: Option, + particle_list: Vec, + particle_idx: usize, +} + +const MAX_PLAYERS: usize = 10; + +const SPAWN_POS: BlockPos = BlockPos::new(0, 100, 0); + +#[async_trait] +impl Config for Game { + type ServerState = ServerState; + type ClientState = EntityId; + type EntityState = (); + type WorldState = (); + type ChunkState = (); + type PlayerListState = (); + type InventoryState = (); + + async fn server_list_ping( + &self, + _server: &SharedServer, + _remote_addr: SocketAddr, + _protocol_version: i32, + ) -> ServerListPing { + ServerListPing::Respond { + online_players: self.player_count.load(Ordering::SeqCst) as i32, + max_players: MAX_PLAYERS as i32, + player_sample: Default::default(), + description: "Hello Valence!".color(Color::AQUA), + favicon_png: Some(include_bytes!("../assets/logo-64x64.png").as_slice().into()), + } + } + + fn init(&self, server: &mut Server) { + let (_, world) = server.worlds.insert(DimensionId::default(), ()); + server.state.player_list = Some(server.player_lists.insert(()).0); + + let size = 5; + for z in -size..size { + for x in -size..size { + world.chunks.insert([x, z], UnloadedChunk::default(), ()); + } + } + + world.chunks.set_block_state(SPAWN_POS, BlockState::BEDROCK); + } + + fn update(&self, server: &mut Server) { + let (world_id, _) = server.worlds.iter_mut().next().expect("missing world"); + + server.clients.retain(|_, client| { + if client.created_this_tick() { + if self + .player_count + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| { + (count < MAX_PLAYERS).then_some(count + 1) + }) + .is_err() + { + client.disconnect("The server is full!".color(Color::RED)); + return false; + } + + match server + .entities + .insert_with_uuid(EntityKind::Player, client.uuid(), ()) + { + Some((id, _)) => client.state = id, + None => { + client.disconnect("Conflicting UUID"); + return false; + } + } + + client.respawn(world_id); + client.set_flat(true); + client.set_game_mode(GameMode::Creative); + client.teleport( + [ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 1.0, + SPAWN_POS.z as f64 + 0.5, + ], + 0.0, + 0.0, + ); + client.set_player_list(server.state.player_list.clone()); + client.send_message("Sneak to speed up the cycling of particles"); + + if let Some(id) = &server.state.player_list { + server.player_lists.get_mut(id).insert( + client.uuid(), + client.username(), + client.textures().cloned(), + client.game_mode(), + 0, + None, + true, + ); + } + } + + if client.is_disconnected() { + self.player_count.fetch_sub(1, Ordering::SeqCst); + if let Some(id) = &server.state.player_list { + server.player_lists[id].remove(client.uuid()); + } + server.entities[client.state].set_deleted(true); + + return false; + } + + let entity = server + .entities + .get_mut(client.state) + .expect("missing player entity"); + + while let Some(event) = client.next_event() { + event.handle_default(client, entity); + } + + true + }); + + let players_are_sneaking = server.clients.iter().any(|(_, client)| -> bool { + let player = &server.entities[client.state]; + if let TrackedData::Player(data) = player.data() { + return data.get_pose() == Pose::Sneaking; + } + false + }); + + let cycle_time = if players_are_sneaking { 5 } else { 30 }; + + if !server.clients.is_empty() && server.current_tick() % cycle_time == 0 { + if server.state.particle_idx == server.state.particle_list.len() { + server.state.particle_idx = 0; + } + + let pos = [ + SPAWN_POS.x as f64 + 0.5, + SPAWN_POS.y as f64 + 2.0, + SPAWN_POS.z as f64 + 5.5, + ]; + let offset = [0.5, 0.5, 0.5]; + let particle = &server.state.particle_list[server.state.particle_idx]; + + server.clients.iter_mut().for_each(|(_, client)| { + client.set_title( + "", + dbg_name(particle).bold(), + SetTitleAnimationTimes { + fade_in: 0, + stay: 100, + fade_out: 2, + }, + ); + client.play_particle(particle, true, pos, offset, 0.1, 100); + }); + server.state.particle_idx += 1; + } + } +} + +fn dbg_name(dbg: &impl fmt::Debug) -> String { + let string = format!("{dbg:?}"); + + string + .split_once(|ch: char| !ch.is_ascii_alphabetic()) + .map(|(fst, _)| fst.to_owned()) + .unwrap_or(string) +} + +fn create_particle_vec() -> Vec { + vec![ + Particle::AmbientEntityEffect, + Particle::AngryVillager, + Particle::Block(BlockState::OAK_PLANKS), + Particle::BlockMarker(BlockState::GOLD_BLOCK), + Particle::Bubble, + Particle::Cloud, + Particle::Crit, + Particle::DamageIndicator, + Particle::DragonBreath, + Particle::DrippingLava, + Particle::FallingLava, + Particle::LandingLava, + Particle::DrippingWater, + Particle::FallingWater, + Particle::Dust { + rgb: [1.0, 1.0, 0.0], + scale: 2.0, + }, + Particle::DustColorTransition { + from_rgb: [1.0, 0.0, 0.0], + scale: 2.0, + to_rgb: [0.0, 1.0, 0.0], + }, + Particle::Effect, + Particle::ElderGuardian, + Particle::EnchantedHit, + Particle::Enchant, + Particle::EndRod, + Particle::EntityEffect, + Particle::ExplosionEmitter, + Particle::Explosion, + Particle::SonicBoom, + Particle::FallingDust(BlockState::RED_SAND), + Particle::Firework, + Particle::Fishing, + Particle::Flame, + Particle::SculkSoul, + Particle::SculkCharge { roll: 1.0 }, + Particle::SculkChargePop, + Particle::SoulFireFlame, + Particle::Soul, + Particle::Flash, + Particle::HappyVillager, + Particle::Composter, + Particle::Heart, + Particle::InstantEffect, + Particle::Item(None), + Particle::Item(Some(ItemStack::new(ItemKind::IronPickaxe, 1, None))), + Particle::VibrationBlock { + block_pos: SPAWN_POS, + ticks: 50, + }, + Particle::VibrationEntity { + entity_id: 0, + entity_eye_height: 1.0, + ticks: 50, + }, + Particle::ItemSlime, + Particle::ItemSnowball, + Particle::LargeSmoke, + Particle::Lava, + Particle::Mycelium, + Particle::Note, + Particle::Poof, + Particle::Portal, + Particle::Rain, + Particle::Smoke, + Particle::Sneeze, + Particle::Spit, + Particle::SquidInk, + Particle::SweepAttack, + Particle::TotemOfUndying, + Particle::Underwater, + Particle::Splash, + Particle::Witch, + Particle::BubblePop, + Particle::CurrentDown, + Particle::BubbleColumnUp, + Particle::Nautilus, + Particle::Dolphin, + Particle::CampfireCosySmoke, + Particle::CampfireSignalSmoke, + Particle::DrippingHoney, + Particle::FallingHoney, + Particle::LandingHoney, + Particle::FallingNectar, + Particle::FallingSporeBlossom, + Particle::Ash, + Particle::CrimsonSpore, + Particle::WarpedSpore, + Particle::SporeBlossomAir, + Particle::DrippingObsidianTear, + Particle::FallingObsidianTear, + Particle::LandingObsidianTear, + Particle::ReversePortal, + Particle::WhiteAsh, + Particle::SmallFlame, + Particle::Snowflake, + Particle::DrippingDripstoneLava, + Particle::FallingDripstoneLava, + Particle::DrippingDripstoneWater, + Particle::FallingDripstoneWater, + Particle::GlowSquidInk, + Particle::Glow, + Particle::WaxOn, + Particle::WaxOff, + Particle::ElectricSpark, + Particle::Scrape, + ] +} diff --git a/packet_inspector/src/main.rs b/packet_inspector/src/main.rs index 01e7e46..c86d470 100644 --- a/packet_inspector/src/main.rs +++ b/packet_inspector/src/main.rs @@ -85,7 +85,11 @@ impl State { self.buf.clear(); write!(&mut self.buf, "{pkt:?}")?; - let packet_name = self.buf.split_ascii_whitespace().next().unwrap(); + let packet_name = self + .buf + .split_once(|ch: char| !ch.is_ascii_alphabetic()) + .map(|(fst, _)| fst) + .unwrap_or(&self.buf); if let Some(r) = &self.cli.include_regex { if !r.is_match(packet_name) { diff --git a/src/client.rs b/src/client.rs index da286cb..de8e6ef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -22,6 +22,7 @@ use valence_protocol::packets::s2c::play::{ SetSubtitleText, SetTitleAnimationTimes, SetTitleText, SynchronizePlayerPosition, SystemChatMessage, UnloadChunk, UpdateAttributes, UpdateTime, }; +use valence_protocol::particle::{Particle, ParticleS2c}; use valence_protocol::types::{ AttributeProperty, DisplayedSkinParts, GameMode, GameStateChangeReason, SyncPlayerPosLookFlags, }; @@ -575,6 +576,25 @@ impl Client { }); } + pub fn play_particle( + &mut self, + particle: &Particle, + long_distance: bool, + position: impl Into>, + offset: impl Into>, + max_speed: f32, + count: i32, + ) { + self.queue_packet(&ParticleS2c { + particle: particle.clone(), + long_distance, + position: position.into().into_array(), + offset: offset.into().into_array(), + max_speed, + count, + }) + } + /// Sets the title this client sees. /// /// A title is a large piece of text displayed in the center of the screen diff --git a/src/lib.rs b/src/lib.rs index 1932b23..c43a9a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,6 +136,7 @@ pub mod prelude { pub use valence_protocol::entity_meta::Pose; pub use valence_protocol::ident::IdentError; pub use valence_protocol::packets::s2c::play::SetTitleAnimationTimes; + pub use valence_protocol::particle::Particle; pub use valence_protocol::text::Color; pub use valence_protocol::types::{GameMode, Hand, SoundCategory}; pub use valence_protocol::{ diff --git a/valence_protocol/src/lib.rs b/valence_protocol/src/lib.rs index 77c3617..c2b5cae 100644 --- a/valence_protocol/src/lib.rs +++ b/valence_protocol/src/lib.rs @@ -109,6 +109,7 @@ mod impls; mod inventory; mod item; pub mod packets; +pub mod particle; pub mod player_list; mod raw_bytes; pub mod text; diff --git a/valence_protocol/src/packets/s2c.rs b/valence_protocol/src/packets/s2c.rs index ae10e07..2909f20 100644 --- a/valence_protocol/src/packets/s2c.rs +++ b/valence_protocol/src/packets/s2c.rs @@ -95,6 +95,7 @@ pub mod login { pub mod play { use super::*; + pub use crate::particle::ParticleS2c; pub use crate::player_list::PlayerInfoUpdate; #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] @@ -332,18 +333,6 @@ pub mod play { pub block_light_arrays: &'a [LengthPrefixedArray], } - #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] - #[packet_id = 0x22] - pub struct ParticleS2c<'a> { - pub particle_id: VarInt, - pub long_distance: bool, - pub position: [f64; 3], - pub offset: [f32; 3], - pub max_speed: f32, - pub count: i32, - pub data: RawBytes<'a>, - } - #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x24] pub struct LoginPlay<'a> { @@ -711,7 +700,7 @@ pub mod play { WorldBorderInitialize, KeepAliveS2c, ChunkDataAndUpdateLight<'a>, - ParticleS2c<'a>, + ParticleS2c, LoginPlay<'a>, UpdateEntityPosition, UpdateEntityPositionAndRotation, diff --git a/valence_protocol/src/particle.rs b/valence_protocol/src/particle.rs new file mode 100644 index 0000000..525bd7a --- /dev/null +++ b/valence_protocol/src/particle.rs @@ -0,0 +1,416 @@ +use std::io::Write; + +use anyhow::bail; + +use crate::block::BlockState; +use crate::block_pos::BlockPos; +use crate::item::ItemStack; +use crate::{Decode, DecodePacket, Encode, EncodePacket, VarInt}; + +#[derive(Clone, Debug, EncodePacket, DecodePacket)] +#[packet_id = 0x22] +pub struct ParticleS2c { + pub particle: Particle, + pub long_distance: bool, + pub position: [f64; 3], + pub offset: [f32; 3], + pub max_speed: f32, + pub count: i32, +} + +#[derive(Clone, Debug)] +pub enum Particle { + AmbientEntityEffect, + AngryVillager, + Block(BlockState), + BlockMarker(BlockState), + Bubble, + Cloud, + Crit, + DamageIndicator, + DragonBreath, + DrippingLava, + FallingLava, + LandingLava, + DrippingWater, + FallingWater, + Dust { + rgb: [f32; 3], + scale: f32, + }, + DustColorTransition { + from_rgb: [f32; 3], + scale: f32, + to_rgb: [f32; 3], + }, + Effect, + ElderGuardian, + EnchantedHit, + Enchant, + EndRod, + EntityEffect, + ExplosionEmitter, + Explosion, + SonicBoom, + FallingDust(BlockState), + Firework, + Fishing, + Flame, + SculkSoul, + SculkCharge { + roll: f32, + }, + SculkChargePop, + SoulFireFlame, + Soul, + Flash, + HappyVillager, + Composter, + Heart, + InstantEffect, + Item(Option), + /// The 'Block' variant of the 'Vibration' particle + VibrationBlock { + block_pos: BlockPos, + ticks: i32, + }, + /// The 'Entity' variant of the 'Vibration' particle + VibrationEntity { + entity_id: i32, + entity_eye_height: f32, + ticks: i32, + }, + ItemSlime, + ItemSnowball, + LargeSmoke, + Lava, + Mycelium, + Note, + Poof, + Portal, + Rain, + Smoke, + Sneeze, + Spit, + SquidInk, + SweepAttack, + TotemOfUndying, + Underwater, + Splash, + Witch, + BubblePop, + CurrentDown, + BubbleColumnUp, + Nautilus, + Dolphin, + CampfireCosySmoke, + CampfireSignalSmoke, + DrippingHoney, + FallingHoney, + LandingHoney, + FallingNectar, + FallingSporeBlossom, + Ash, + CrimsonSpore, + WarpedSpore, + SporeBlossomAir, + DrippingObsidianTear, + FallingObsidianTear, + LandingObsidianTear, + ReversePortal, + WhiteAsh, + SmallFlame, + Snowflake, + DrippingDripstoneLava, + FallingDripstoneLava, + DrippingDripstoneWater, + FallingDripstoneWater, + GlowSquidInk, + Glow, + WaxOn, + WaxOff, + ElectricSpark, + Scrape, +} + +impl Particle { + pub const fn id(&self) -> i32 { + match self { + Particle::AmbientEntityEffect => 0, + Particle::AngryVillager => 1, + Particle::Block(_) => 2, + Particle::BlockMarker(_) => 3, + Particle::Bubble => 4, + Particle::Cloud => 5, + Particle::Crit => 6, + Particle::DamageIndicator => 7, + Particle::DragonBreath => 8, + Particle::DrippingLava => 9, + Particle::FallingLava => 10, + Particle::LandingLava => 11, + Particle::DrippingWater => 12, + Particle::FallingWater => 13, + Particle::Dust { .. } => 14, + Particle::DustColorTransition { .. } => 15, + Particle::Effect => 16, + Particle::ElderGuardian => 17, + Particle::EnchantedHit => 18, + Particle::Enchant => 19, + Particle::EndRod => 20, + Particle::EntityEffect => 21, + Particle::ExplosionEmitter => 22, + Particle::Explosion => 23, + Particle::SonicBoom => 24, + Particle::FallingDust(_) => 25, + Particle::Firework => 26, + Particle::Fishing => 27, + Particle::Flame => 28, + Particle::SculkSoul => 29, + Particle::SculkCharge { .. } => 30, + Particle::SculkChargePop => 31, + Particle::SoulFireFlame => 32, + Particle::Soul => 33, + Particle::Flash => 34, + Particle::HappyVillager => 35, + Particle::Composter => 36, + Particle::Heart => 37, + Particle::InstantEffect => 38, + Particle::Item(_) => 39, + Particle::VibrationBlock { .. } => 40, + Particle::VibrationEntity { .. } => 40, + Particle::ItemSlime => 41, + Particle::ItemSnowball => 42, + Particle::LargeSmoke => 43, + Particle::Lava => 44, + Particle::Mycelium => 45, + Particle::Note => 46, + Particle::Poof => 47, + Particle::Portal => 48, + Particle::Rain => 49, + Particle::Smoke => 50, + Particle::Sneeze => 51, + Particle::Spit => 52, + Particle::SquidInk => 53, + Particle::SweepAttack => 54, + Particle::TotemOfUndying => 55, + Particle::Underwater => 56, + Particle::Splash => 57, + Particle::Witch => 58, + Particle::BubblePop => 59, + Particle::CurrentDown => 60, + Particle::BubbleColumnUp => 61, + Particle::Nautilus => 62, + Particle::Dolphin => 63, + Particle::CampfireCosySmoke => 64, + Particle::CampfireSignalSmoke => 65, + Particle::DrippingHoney => 66, + Particle::FallingHoney => 67, + Particle::LandingHoney => 68, + Particle::FallingNectar => 69, + Particle::FallingSporeBlossom => 70, + Particle::Ash => 71, + Particle::CrimsonSpore => 72, + Particle::WarpedSpore => 73, + Particle::SporeBlossomAir => 74, + Particle::DrippingObsidianTear => 75, + Particle::FallingObsidianTear => 76, + Particle::LandingObsidianTear => 77, + Particle::ReversePortal => 78, + Particle::WhiteAsh => 79, + Particle::SmallFlame => 80, + Particle::Snowflake => 81, + Particle::DrippingDripstoneLava => 82, + Particle::FallingDripstoneLava => 83, + Particle::DrippingDripstoneWater => 84, + Particle::FallingDripstoneWater => 85, + Particle::GlowSquidInk => 86, + Particle::Glow => 87, + Particle::WaxOn => 88, + Particle::WaxOff => 89, + Particle::ElectricSpark => 90, + Particle::Scrape => 91, + } + } +} + +impl Encode for ParticleS2c { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + VarInt(self.particle.id()).encode(&mut w)?; + self.long_distance.encode(&mut w)?; + self.position.encode(&mut w)?; + self.offset.encode(&mut w)?; + self.max_speed.encode(&mut w)?; + self.count.encode(&mut w)?; + + match &self.particle { + Particle::Block(block_state) => block_state.encode(w), + Particle::BlockMarker(block_state) => block_state.encode(w), + Particle::Dust { rgb, scale } => { + rgb.encode(&mut w)?; + scale.encode(w) + } + Particle::DustColorTransition { + from_rgb, + scale, + to_rgb, + } => { + from_rgb.encode(&mut w)?; + scale.encode(&mut w)?; + to_rgb.encode(w) + } + Particle::FallingDust(block_state) => block_state.encode(w), + Particle::SculkCharge { roll } => roll.encode(w), + Particle::Item(stack) => stack.encode(w), + Particle::VibrationBlock { block_pos, ticks } => { + "block".encode(&mut w)?; + block_pos.encode(&mut w)?; + VarInt(*ticks).encode(w) + } + Particle::VibrationEntity { + entity_id, + entity_eye_height, + ticks, + } => { + "entity".encode(&mut w)?; + VarInt(*entity_id).encode(&mut w)?; + entity_eye_height.encode(&mut w)?; + VarInt(*ticks).encode(w) + } + _ => Ok(()), + } + } +} + +impl<'a> Decode<'a> for ParticleS2c { + fn decode(r: &mut &'a [u8]) -> anyhow::Result { + let particle_id = VarInt::decode(r)?.0; + let long_distance = bool::decode(r)?; + let position = <[f64; 3]>::decode(r)?; + let offset = <[f32; 3]>::decode(r)?; + let max_speed = f32::decode(r)?; + let particle_count = i32::decode(r)?; + + Ok(Self { + particle: match particle_id { + 0 => Particle::AmbientEntityEffect, + 1 => Particle::AngryVillager, + 2 => Particle::Block(BlockState::decode(r)?), + 3 => Particle::BlockMarker(BlockState::decode(r)?), + 4 => Particle::Bubble, + 5 => Particle::Cloud, + 6 => Particle::Crit, + 7 => Particle::DamageIndicator, + 8 => Particle::DragonBreath, + 9 => Particle::DrippingLava, + 10 => Particle::FallingLava, + 11 => Particle::LandingLava, + 12 => Particle::DrippingWater, + 13 => Particle::FallingWater, + 14 => Particle::Dust { + rgb: <[f32; 3]>::decode(r)?, + scale: f32::decode(r)?, + }, + 15 => Particle::DustColorTransition { + from_rgb: <[f32; 3]>::decode(r)?, + scale: f32::decode(r)?, + to_rgb: <[f32; 3]>::decode(r)?, + }, + 16 => Particle::Effect, + 17 => Particle::ElderGuardian, + 18 => Particle::EnchantedHit, + 19 => Particle::Enchant, + 20 => Particle::EndRod, + 21 => Particle::EntityEffect, + 22 => Particle::ExplosionEmitter, + 23 => Particle::Explosion, + 24 => Particle::SonicBoom, + 25 => Particle::FallingDust(BlockState::decode(r)?), + 26 => Particle::Firework, + 27 => Particle::Fishing, + 28 => Particle::Flame, + 29 => Particle::SculkSoul, + 30 => Particle::SculkCharge { + roll: f32::decode(r)?, + }, + 31 => Particle::SculkChargePop, + 32 => Particle::SoulFireFlame, + 33 => Particle::Soul, + 34 => Particle::Flash, + 35 => Particle::HappyVillager, + 36 => Particle::Composter, + 37 => Particle::Heart, + 38 => Particle::InstantEffect, + 39 => Particle::Item(Decode::decode(r)?), + 40 => match <&str>::decode(r)? { + "block" => Particle::VibrationBlock { + block_pos: BlockPos::decode(r)?, + ticks: VarInt::decode(r)?.0, + }, + "entity" => Particle::VibrationEntity { + entity_id: VarInt::decode(r)?.0, + entity_eye_height: f32::decode(r)?, + ticks: VarInt::decode(r)?.0, + }, + invalid => bail!("invalid vibration position source of \"{invalid}\""), + }, + 41 => Particle::ItemSlime, + 42 => Particle::ItemSnowball, + 43 => Particle::LargeSmoke, + 44 => Particle::Lava, + 45 => Particle::Mycelium, + 46 => Particle::Note, + 47 => Particle::Poof, + 48 => Particle::Portal, + 49 => Particle::Rain, + 50 => Particle::Smoke, + 51 => Particle::Sneeze, + 52 => Particle::Spit, + 53 => Particle::SquidInk, + 54 => Particle::SweepAttack, + 55 => Particle::TotemOfUndying, + 56 => Particle::Underwater, + 57 => Particle::Splash, + 58 => Particle::Witch, + 59 => Particle::BubblePop, + 60 => Particle::CurrentDown, + 61 => Particle::BubbleColumnUp, + 62 => Particle::Nautilus, + 63 => Particle::Dolphin, + 64 => Particle::CampfireCosySmoke, + 65 => Particle::CampfireSignalSmoke, + 66 => Particle::DrippingHoney, + 67 => Particle::FallingHoney, + 68 => Particle::LandingHoney, + 69 => Particle::FallingNectar, + 70 => Particle::FallingSporeBlossom, + 71 => Particle::Ash, + 72 => Particle::CrimsonSpore, + 73 => Particle::WarpedSpore, + 74 => Particle::SporeBlossomAir, + 75 => Particle::DrippingObsidianTear, + 76 => Particle::FallingObsidianTear, + 77 => Particle::LandingObsidianTear, + 78 => Particle::ReversePortal, + 79 => Particle::WhiteAsh, + 80 => Particle::SmallFlame, + 81 => Particle::Snowflake, + 82 => Particle::DrippingDripstoneLava, + 83 => Particle::FallingDripstoneLava, + 84 => Particle::DrippingDripstoneWater, + 85 => Particle::FallingDripstoneWater, + 86 => Particle::GlowSquidInk, + 87 => Particle::Glow, + 88 => Particle::WaxOn, + 89 => Particle::WaxOff, + 90 => Particle::ElectricSpark, + 91 => Particle::Scrape, + id => bail!("invalid particle ID of {id}"), + }, + long_distance, + position, + offset, + max_speed, + count: particle_count, + }) + } +} diff --git a/valence_protocol/src/text.rs b/valence_protocol/src/text.rs index a0eb57d..80a3ba1 100644 --- a/valence_protocol/src/text.rs +++ b/valence_protocol/src/text.rs @@ -1,8 +1,8 @@ //! Formatted text. use std::borrow::Cow; -use std::fmt; use std::io::Write; +use std::{fmt, ops}; use serde::de::Visitor; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -687,7 +687,7 @@ pub trait TextFormat: Into { impl> TextFormat for T {} -impl> std::ops::Add for Text { +impl> ops::Add for Text { type Output = Self; fn add(self, rhs: T) -> Self::Output { @@ -695,7 +695,7 @@ impl> std::ops::Add for Text { } } -impl> std::ops::AddAssign for Text { +impl> ops::AddAssign for Text { fn add_assign(&mut self, rhs: T) { self.0.extra.push(rhs.into()); }