1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 06:16:04 +00:00

Better chat events (#67)

* Better chat events

* add a comment explaining why SendChatKindEvent is only one event
This commit is contained in:
mat 2023-02-12 17:49:09 -06:00 committed by GitHub
parent 962cb576b3
commit 5d53d063c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 206 additions and 70 deletions

View file

@ -1,6 +1,12 @@
//! Implementations of chat-related features. //! Implementations of chat-related features.
use azalea_chat::FormattedText; use azalea_chat::FormattedText;
use azalea_ecs::{
app::{App, Plugin},
entity::Entity,
event::{EventReader, EventWriter},
schedule::IntoSystemDescriptor,
};
use azalea_protocol::packets::game::{ use azalea_protocol::packets::game::{
clientbound_player_chat_packet::ClientboundPlayerChatPacket, clientbound_player_chat_packet::ClientboundPlayerChatPacket,
clientbound_system_chat_packet::ClientboundSystemChatPacket, clientbound_system_chat_packet::ClientboundSystemChatPacket,
@ -13,7 +19,7 @@ use std::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::client::Client; use crate::{client::Client, local_player::SendPacketEvent};
/// A chat packet, either a system message or a chat message. /// A chat packet, either a system message or a chat message.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -107,42 +113,23 @@ impl Client {
/// whether the message is a command and using the proper packet for you, /// whether the message is a command and using the proper packet for you,
/// so you should use that instead. /// so you should use that instead.
pub fn send_chat_packet(&self, message: &str) { pub fn send_chat_packet(&self, message: &str) {
// TODO: chat signing self.ecs.lock().send_event(SendChatKindEvent {
// let signature = sign_message(); entity: self.entity,
let packet = ServerboundChatPacket { content: message.to_string(),
message: message.to_string(), kind: ChatPacketKind::Message,
timestamp: SystemTime::now() });
.duration_since(UNIX_EPOCH) self.run_schedule_sender.send(()).unwrap();
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get();
self.write_packet(packet);
} }
/// Send a command packet to the server. The `command` argument should not /// Send a command packet to the server. The `command` argument should not
/// include the slash at the front. /// include the slash at the front.
pub fn send_command_packet(&self, command: &str) { pub fn send_command_packet(&self, command: &str) {
// TODO: chat signing self.ecs.lock().send_event(SendChatKindEvent {
let packet = ServerboundChatCommandPacket { entity: self.entity,
command: command.to_string(), content: command.to_string(),
timestamp: SystemTime::now() kind: ChatPacketKind::Command,
.duration_since(UNIX_EPOCH) });
.expect("Time shouldn't be before epoch") self.run_schedule_sender.send(()).unwrap();
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
argument_signatures: vec![],
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get();
self.write_packet(packet);
} }
/// Send a message in chat. /// Send a message in chat.
@ -154,13 +141,130 @@ impl Client {
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub fn chat(&self, message: &str) { pub fn chat(&self, content: &str) {
if let Some(command) = message.strip_prefix('/') { self.ecs.lock().send_event(SendChatEvent {
self.send_command_packet(command); entity: self.entity,
} else { content: content.to_string(),
self.send_chat_packet(message); });
} }
} }
pub struct ChatPlugin;
impl Plugin for ChatPlugin {
fn build(&self, app: &mut App) {
app.add_event::<SendChatEvent>()
.add_event::<SendChatKindEvent>()
.add_event::<ChatReceivedEvent>()
.add_system(
handle_send_chat_event
.label("handle_send_chat_event")
.after("packet"),
)
.add_system(
handle_send_chat_kind_event
.label("handle_send_chat_kind_event")
.after("handle_send_chat_event"),
);
}
}
/// A client received a chat message packet.
#[derive(Debug, Clone)]
pub struct ChatReceivedEvent {
pub entity: Entity,
pub packet: ChatPacket,
}
/// Send a chat message (or command, if it starts with a slash) to the server.
pub struct SendChatEvent {
pub entity: Entity,
pub content: String,
}
fn handle_send_chat_event(
mut events: EventReader<SendChatEvent>,
mut send_chat_kind_events: EventWriter<SendChatKindEvent>,
) {
for event in events.iter() {
if event.content.starts_with('/') {
send_chat_kind_events.send(SendChatKindEvent {
entity: event.entity,
content: event.content[1..].to_string(),
kind: ChatPacketKind::Command,
});
} else {
send_chat_kind_events.send(SendChatKindEvent {
entity: event.entity,
content: event.content.clone(),
kind: ChatPacketKind::Message,
});
}
}
}
/// Send a chat packet to the server of a specific kind (chat message or
/// command). Usually you just want [`SendChatEvent`] instead.
///
/// Usually setting the kind to `Message` will make it send a chat message even
/// if it starts with a slash, but some server implementations will always do a
/// command if it starts with a slash.
///
/// If you're wondering why this isn't two separate events, it's so ordering is
/// preserved if multiple chat messages and commands are sent at the same time.
pub struct SendChatKindEvent {
pub entity: Entity,
pub content: String,
pub kind: ChatPacketKind,
}
/// A kind of chat packet, either a chat message or a command.
pub enum ChatPacketKind {
Message,
Command,
}
fn handle_send_chat_kind_event(
mut events: EventReader<SendChatKindEvent>,
mut send_packet_events: EventWriter<SendPacketEvent>,
) {
for event in events.iter() {
let packet = match event.kind {
ChatPacketKind::Message => ServerboundChatPacket {
message: event.content.clone(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
signature: None,
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get(),
ChatPacketKind::Command => {
// TODO: chat signing
ServerboundChatCommandPacket {
command: event.content.clone(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time shouldn't be before epoch")
.as_millis()
.try_into()
.expect("Instant should fit into a u64"),
salt: azalea_crypto::make_salt(),
argument_signatures: vec![],
last_seen_messages: LastSeenMessagesUpdate::default(),
}
.get()
}
};
send_packet_events.send(SendPacketEvent {
entity: event.entity,
packet,
});
}
} }
// TODO // TODO

View file

@ -1,8 +1,9 @@
pub use crate::chat::ChatPacket;
use crate::{ use crate::{
chat::ChatPlugin,
events::{Event, EventPlugin, LocalPlayerEvents}, events::{Event, EventPlugin, LocalPlayerEvents},
local_player::{ local_player::{
death_event, update_in_loaded_chunk, GameProfileComponent, LocalPlayer, PhysicsState, death_event, handle_send_packet_event, update_in_loaded_chunk, GameProfileComponent,
LocalPlayer, PhysicsState, SendPacketEvent,
}, },
movement::{local_player_ai_step, send_position, sprint_listener, walk_listener}, movement::{local_player_ai_step, send_position, sprint_listener, walk_listener},
packet_handling::{self, PacketHandlerPlugin}, packet_handling::{self, PacketHandlerPlugin},
@ -80,6 +81,9 @@ pub struct Client {
/// directly. Note that if you're using a shared world (i.e. a swarm), this /// directly. Note that if you're using a shared world (i.e. a swarm), this
/// will contain all entities in all worlds. /// will contain all entities in all worlds.
pub ecs: Arc<Mutex<Ecs>>, pub ecs: Arc<Mutex<Ecs>>,
/// Use this to force the client to run the schedule outside of a tick.
pub run_schedule_sender: mpsc::UnboundedSender<()>,
} }
/// An error that happened while joining the server. /// An error that happened while joining the server.
@ -107,7 +111,12 @@ impl Client {
/// Create a new client from the given GameProfile, Connection, and World. /// Create a new client from the given GameProfile, Connection, and World.
/// You should only use this if you want to change these fields from the /// You should only use this if you want to change these fields from the
/// defaults, otherwise use [`Client::join`]. /// defaults, otherwise use [`Client::join`].
pub fn new(profile: GameProfile, entity: Entity, ecs: Arc<Mutex<Ecs>>) -> Self { pub fn new(
profile: GameProfile,
entity: Entity,
ecs: Arc<Mutex<Ecs>>,
run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Self {
Self { Self {
profile, profile,
// default our id to 0, it'll be set later // default our id to 0, it'll be set later
@ -115,6 +124,8 @@ impl Client {
world: Arc::new(RwLock::new(PartialWorld::default())), world: Arc::new(RwLock::new(PartialWorld::default())),
ecs, ecs,
run_schedule_sender,
} }
} }
@ -146,7 +157,7 @@ impl Client {
let resolved_address = resolver::resolve_address(&address).await?; let resolved_address = resolver::resolve_address(&address).await?;
// An event that causes the schedule to run. This is only used internally. // An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1); let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let app = init_ecs_app(); let app = init_ecs_app();
let ecs_lock = start_ecs(app, run_schedule_receiver, run_schedule_sender.clone()); let ecs_lock = start_ecs(app, run_schedule_receiver, run_schedule_sender.clone());
@ -167,7 +178,7 @@ impl Client {
account: &Account, account: &Account,
address: &ServerAddress, address: &ServerAddress,
resolved_address: &SocketAddr, resolved_address: &SocketAddr,
run_schedule_sender: mpsc::Sender<()>, run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> { ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
let conn = Connection::new(resolved_address).await?; let conn = Connection::new(resolved_address).await?;
let (conn, game_profile) = Self::handshake(conn, account, address).await?; let (conn, game_profile) = Self::handshake(conn, account, address).await?;
@ -182,7 +193,12 @@ impl Client {
let entity = entity_mut.id(); let entity = entity_mut.id();
// we got the GameConnection, so the server is now connected :) // we got the GameConnection, so the server is now connected :)
let client = Client::new(game_profile.clone(), entity, ecs_lock.clone()); let client = Client::new(
game_profile.clone(),
entity,
ecs_lock.clone(),
run_schedule_sender.clone(),
);
let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel(); let (packet_writer_sender, packet_writer_receiver) = mpsc::unbounded_channel();
@ -458,8 +474,6 @@ impl Plugin for AzaleaPlugin {
app.add_event::<StartWalkEvent>() app.add_event::<StartWalkEvent>()
.add_event::<StartSprintEvent>(); .add_event::<StartSprintEvent>();
app.add_plugins(DefaultPlugins);
app.add_tick_system_set( app.add_tick_system_set(
SystemSet::new() SystemSet::new()
.with_system(send_position) .with_system(send_position)
@ -490,6 +504,9 @@ impl Plugin for AzaleaPlugin {
.after("packet"), .after("packet"),
); );
app.add_event::<SendPacketEvent>()
.add_system(handle_send_packet_event.after("tick").after("packet"));
app.init_resource::<WorldContainer>(); app.init_resource::<WorldContainer>();
} }
} }
@ -509,7 +526,7 @@ pub fn init_ecs_app() -> App {
// you might be able to just drop the lock or put it in its own scope to fix // you might be able to just drop the lock or put it in its own scope to fix
let mut app = App::new(); let mut app = App::new();
app.add_plugin(AzaleaPlugin); app.add_plugins(DefaultPlugins);
app app
} }
@ -518,8 +535,8 @@ pub fn init_ecs_app() -> App {
#[doc(hidden)] #[doc(hidden)]
pub fn start_ecs( pub fn start_ecs(
app: App, app: App,
run_schedule_receiver: mpsc::Receiver<()>, run_schedule_receiver: mpsc::UnboundedReceiver<()>,
run_schedule_sender: mpsc::Sender<()>, run_schedule_sender: mpsc::UnboundedSender<()>,
) -> Arc<Mutex<Ecs>> { ) -> Arc<Mutex<Ecs>> {
// all resources should have been added by now so we can take the ecs from the // all resources should have been added by now so we can take the ecs from the
// app // app
@ -538,7 +555,7 @@ pub fn start_ecs(
async fn run_schedule_loop( async fn run_schedule_loop(
ecs: Arc<Mutex<Ecs>>, ecs: Arc<Mutex<Ecs>>,
mut schedule: Schedule, mut schedule: Schedule,
mut run_schedule_receiver: mpsc::Receiver<()>, mut run_schedule_receiver: mpsc::UnboundedReceiver<()>,
) { ) {
loop { loop {
// whenever we get an event from run_schedule_receiver, run the schedule // whenever we get an event from run_schedule_receiver, run the schedule
@ -549,14 +566,14 @@ async fn run_schedule_loop(
/// Send an event to run the schedule every 50 milliseconds. It will stop when /// Send an event to run the schedule every 50 milliseconds. It will stop when
/// the receiver is dropped. /// the receiver is dropped.
pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) { pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::UnboundedSender<()>) {
let mut game_tick_interval = time::interval(time::Duration::from_millis(50)); let mut game_tick_interval = time::interval(time::Duration::from_millis(50));
// TODO: Minecraft bursts up to 10 ticks and then skips, we should too // TODO: Minecraft bursts up to 10 ticks and then skips, we should too
game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst); game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
loop { loop {
game_tick_interval.tick().await; game_tick_interval.tick().await;
if let Err(e) = run_schedule_sender.send(()).await { if let Err(e) = run_schedule_sender.send(()) {
println!("tick_run_schedule_loop error: {e}"); println!("tick_run_schedule_loop error: {e}");
// the sender is closed so end the task // the sender is closed so end the task
return; return;
@ -572,10 +589,12 @@ impl PluginGroup for DefaultPlugins {
fn build(self) -> PluginGroupBuilder { fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>() PluginGroupBuilder::start::<Self>()
.add(TickPlugin::default()) .add(TickPlugin::default())
.add(AzaleaPlugin)
.add(PacketHandlerPlugin) .add(PacketHandlerPlugin)
.add(EntityPlugin) .add(EntityPlugin)
.add(PhysicsPlugin) .add(PhysicsPlugin)
.add(EventPlugin) .add(EventPlugin)
.add(TaskPoolPlugin::default()) .add(TaskPoolPlugin::default())
.add(ChatPlugin)
} }
} }

View file

@ -19,11 +19,12 @@ use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::{ use crate::{
chat::{ChatPacket, ChatReceivedEvent},
packet_handling::{ packet_handling::{
AddPlayerEvent, ChatReceivedEvent, DeathEvent, KeepAliveEvent, PacketReceiver, AddPlayerEvent, DeathEvent, KeepAliveEvent, PacketReceiver, RemovePlayerEvent,
RemovePlayerEvent, UpdatePlayerEvent, UpdatePlayerEvent,
}, },
ChatPacket, PlayerInfo, PlayerInfo,
}; };
// (for contributors): // (for contributors):

View file

@ -12,7 +12,7 @@
#![feature(type_alias_impl_trait)] #![feature(type_alias_impl_trait)]
mod account; mod account;
mod chat; pub mod chat;
mod client; mod client;
mod entity_query; mod entity_query;
mod events; mod events;
@ -26,7 +26,7 @@ pub mod task_pool;
pub use account::Account; pub use account::Account;
pub use azalea_ecs as ecs; pub use azalea_ecs as ecs;
pub use client::{init_ecs_app, start_ecs, ChatPacket, Client, ClientInformation, JoinError}; pub use client::{init_ecs_app, start_ecs, Client, ClientInformation, JoinError};
pub use events::Event; pub use events::Event;
pub use local_player::{GameProfileComponent, LocalPlayer}; pub use local_player::{GameProfileComponent, LocalPlayer};
pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection}; pub use movement::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection};

View file

@ -4,6 +4,7 @@ use azalea_auth::game_profile::GameProfile;
use azalea_core::ChunkPos; use azalea_core::ChunkPos;
use azalea_ecs::component::Component; use azalea_ecs::component::Component;
use azalea_ecs::entity::Entity; use azalea_ecs::entity::Entity;
use azalea_ecs::event::EventReader;
use azalea_ecs::{query::Added, system::Query}; use azalea_ecs::{query::Added, system::Query};
use azalea_protocol::packets::game::ServerboundGamePacket; use azalea_protocol::packets::game::ServerboundGamePacket;
use azalea_world::{ use azalea_world::{
@ -168,3 +169,20 @@ impl<T> From<std::sync::PoisonError<T>> for HandlePacketError {
HandlePacketError::Poison(e.to_string()) HandlePacketError::Poison(e.to_string())
} }
} }
/// Event for sending a packet to the server.
pub struct SendPacketEvent {
pub entity: Entity,
pub packet: ServerboundGamePacket,
}
pub fn handle_send_packet_event(
mut send_packet_events: EventReader<SendPacketEvent>,
mut query: Query<&mut LocalPlayer>,
) {
for event in send_packet_events.iter() {
if let Ok(mut local_player) = query.get_mut(event.entity) {
local_player.write_packet(event.packet.clone());
}
}
}

View file

@ -37,8 +37,9 @@ use parking_lot::Mutex;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::{ use crate::{
chat::{ChatPacket, ChatReceivedEvent},
local_player::{GameProfileComponent, LocalPlayer}, local_player::{GameProfileComponent, LocalPlayer},
ChatPacket, ClientInformation, PlayerInfo, ClientInformation, PlayerInfo,
}; };
pub struct PacketHandlerPlugin; pub struct PacketHandlerPlugin;
@ -82,13 +83,6 @@ pub struct UpdatePlayerEvent {
pub info: PlayerInfo, pub info: PlayerInfo,
} }
/// A client received a chat message packet.
#[derive(Debug, Clone)]
pub struct ChatReceivedEvent {
pub entity: Entity,
pub packet: ChatPacket,
}
/// Event for when an entity dies. dies. If it's a local player and there's a /// Event for when an entity dies. dies. If it's a local player and there's a
/// reason in the death screen, the [`ClientboundPlayerCombatKillPacket`] will /// reason in the death screen, the [`ClientboundPlayerCombatKillPacket`] will
/// be included. /// be included.
@ -112,7 +106,7 @@ pub struct KeepAliveEvent {
#[derive(Component, Clone)] #[derive(Component, Clone)]
pub struct PacketReceiver { pub struct PacketReceiver {
pub packets: Arc<Mutex<Vec<ClientboundGamePacket>>>, pub packets: Arc<Mutex<Vec<ClientboundGamePacket>>>,
pub run_schedule_sender: mpsc::Sender<()>, pub run_schedule_sender: mpsc::UnboundedSender<()>,
} }
fn handle_packets(ecs: &mut Ecs) { fn handle_packets(ecs: &mut Ecs) {
@ -950,7 +944,7 @@ impl PacketReceiver {
Ok(packet) => { Ok(packet) => {
self.packets.lock().push(packet); self.packets.lock().push(packet);
// tell the client to run all the systems // tell the client to run all the systems
self.run_schedule_sender.send(()).await.unwrap(); self.run_schedule_sender.send(()).unwrap();
} }
Err(error) => { Err(error) => {
if !matches!(*error, ReadPacketError::ConnectionClosed) { if !matches!(*error, ReadPacketError::ConnectionClosed) {

View file

@ -135,7 +135,7 @@ where
let resolved_address = resolver::resolve_address(&address).await?; let resolved_address = resolver::resolve_address(&address).await?;
// An event that causes the schedule to run. This is only used internally. // An event that causes the schedule to run. This is only used internally.
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1); let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone()); let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone());
let (bot, mut rx) = Client::start_client( let (bot, mut rx) = Client::start_client(

View file

@ -13,7 +13,7 @@
// in Swarm that's set to the smallest index of all the bots, and we remove all // in Swarm that's set to the smallest index of all the bots, and we remove all
// messages from the queue that are before that index. // messages from the queue that are before that index.
use azalea_client::{packet_handling::ChatReceivedEvent, ChatPacket}; use azalea_client::chat::{ChatPacket, ChatReceivedEvent};
use azalea_ecs::{ use azalea_ecs::{
app::{App, Plugin}, app::{App, Plugin},
component::Component, component::Component,

View file

@ -5,7 +5,7 @@ mod events;
pub mod prelude; pub mod prelude;
use crate::{bot::DefaultBotPlugins, HandleFn}; use crate::{bot::DefaultBotPlugins, HandleFn};
use azalea_client::{init_ecs_app, start_ecs, Account, ChatPacket, Client, Event, JoinError}; use azalea_client::{chat::ChatPacket, init_ecs_app, start_ecs, Account, Client, Event, JoinError};
use azalea_ecs::{ use azalea_ecs::{
app::{App, Plugin, PluginGroup, PluginGroupBuilder}, app::{App, Plugin, PluginGroup, PluginGroupBuilder},
component::Component, component::Component,
@ -47,7 +47,7 @@ pub struct Swarm {
bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>, bots_tx: mpsc::UnboundedSender<(Option<Event>, Client)>,
swarm_tx: mpsc::UnboundedSender<SwarmEvent>, swarm_tx: mpsc::UnboundedSender<SwarmEvent>,
run_schedule_sender: mpsc::Sender<()>, run_schedule_sender: mpsc::UnboundedSender<()>,
} }
/// Create a new [`Swarm`]. /// Create a new [`Swarm`].
@ -253,7 +253,7 @@ where
let (bots_tx, mut bots_rx) = mpsc::unbounded_channel(); let (bots_tx, mut bots_rx) = mpsc::unbounded_channel();
let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel(); let (swarm_tx, mut swarm_rx) = mpsc::unbounded_channel();
let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1); let (run_schedule_sender, run_schedule_receiver) = mpsc::unbounded_channel();
let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone()); let ecs_lock = start_ecs(self.app, run_schedule_receiver, run_schedule_sender.clone());
let swarm = Swarm { let swarm = Swarm {