Add Particles (#150)

Implements the clientbound particle packet.

Co-authored-by: Ryan Johnson <ryanj00a@gmail.com>
This commit is contained in:
jivvy 2022-12-29 02:26:19 -06:00 committed by GitHub
parent 5d3364d452
commit 295678e92d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 761 additions and 18 deletions

View file

@ -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

312
examples/particles.rs Normal file
View file

@ -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<PlayerListId>,
particle_list: Vec<Particle>,
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<Self>,
_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<Self>) {
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<Self>) {
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<Particle> {
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,
]
}

View file

@ -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) {

View file

@ -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<C: Config> Client<C> {
});
}
pub fn play_particle(
&mut self,
particle: &Particle,
long_distance: bool,
position: impl Into<Vec3<f64>>,
offset: impl Into<Vec3<f32>>,
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

View file

@ -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::{

View file

@ -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;

View file

@ -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<u8, 2048>],
}
#[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,

View file

@ -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<ItemStack>),
/// 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<Self> {
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,
})
}
}

View file

@ -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<Text> {
impl<T: Into<Text>> TextFormat for T {}
impl<T: Into<Text>> std::ops::Add<T> for Text {
impl<T: Into<Text>> ops::Add<T> for Text {
type Output = Self;
fn add(self, rhs: T) -> Self::Output {
@ -695,7 +695,7 @@ impl<T: Into<Text>> std::ops::Add<T> for Text {
}
}
impl<T: Into<Text>> std::ops::AddAssign<T> for Text {
impl<T: Into<Text>> ops::AddAssign<T> for Text {
fn add_assign(&mut self, rhs: T) {
self.0.extra.push(rhs.into());
}