1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
|
//! Defines the [`enum@Event`] enum and makes those events trigger when they're
//! sent in the ECS.
use std::sync::Arc;
use azalea_chat::FormattedText;
use azalea_client::join::ConnectionFailedEvent;
use azalea_core::{entity_id::MinecraftEntityId, position::ChunkPos, tick::GameTick};
use azalea_entity::{Dead, InLoadedChunk};
use azalea_protocol::{
connect::ConnectionError, packets::game::c_player_combat_kill::ClientboundPlayerCombatKill,
};
use azalea_world::WorldName;
use bevy_app::{App, Plugin, PreUpdate, Update};
use bevy_ecs::prelude::*;
use derive_more::{Deref, DerefMut};
use tokio::sync::mpsc;
use crate::{
chunks::ReceiveChunkEvent,
client_chat::{ChatPacket, ChatReceivedEvent},
disconnect::DisconnectEvent,
packet::game::{
AddPlayerEvent, DeathEvent, KeepAliveEvent, RemovePlayerEvent, UpdatePlayerEvent,
},
player::PlayerInfo,
};
// (for contributors):
// HOW TO ADD A NEW (packet based) EVENT:
// - Add it as an ECS event first:
// - Make a struct that contains an entity field and some data fields (look
// in packet/game/events.rs for examples. These structs should always have
// their names end with "Event".
// - (the `entity` field is the local player entity that's receiving the
// event)
// - In the GamePacketHandler, you always have a `player` field that you can
// use.
// - Add the event struct in PacketPlugin::build
// - (in the `impl Plugin for PacketPlugin`)
// - To get the event writer, you have to get an MessageWriter<ThingEvent>.
// Look at other packets in packet/game/mod.rs for examples.
//
// At this point, you've created a new ECS event. That's annoying for bots to
// use though, so you might wanna add it to the Event enum too:
// - In this file, add a new variant to that Event enum with the same name
// as your event (without the "Event" suffix).
// - Create a new system function like the other ones here, and put that
// system function in the `impl Plugin for EventsPlugin`
/// Something that happened in-game, such as a tick passing or chat message
/// being sent.
///
/// Note: Events are sent before they're processed, so for example game ticks
/// happen at the beginning of a tick before anything has happened.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Event {
/// Happens right after the bot switches into the Game state, but before
/// it's actually spawned.
///
/// This can be useful for setting the client information with
/// [`Client::set_client_information`], so the packet doesn't have to be
/// sent twice.
///
/// You may want to use [`Event::Spawn`] instead to wait for the bot to be
/// in the world.
///
/// [`Client::set_client_information`]: crate::Client::set_client_information
Init,
/// Fired when we receive a login packet, which is after [`Event::Init`] but
/// before [`Event::Spawn`]. You usually want [`Event::Spawn`] instead.
///
/// Your position may be [`Vec3::ZERO`] immediately after you receive this
/// event, but it'll be ready by the time you get [`Event::Spawn`].
///
/// It's possible for this event to be sent multiple times per client if a
/// server sends multiple login packets (like when switching worlds).
///
/// [`Vec3::ZERO`]: azalea_core::position::Vec3::ZERO
Login,
/// Fired when the player fully spawns into the world (is in a loaded chunk)
/// and is ready to interact with it.
///
/// This is usually the event you should listen for when waiting for the bot
/// to be ready.
///
/// This event will be sent every time the client respawns or switches
/// worlds, as long as the server sends chunks to the client.
Spawn,
/// A chat message was sent in the game chat.
Chat(ChatPacket),
/// Happens 20 times per second, but only when the world is loaded.
Tick,
#[cfg(feature = "packet-event")]
/// We received a packet from the server.
///
/// ```
/// # use azalea::Event;
/// # use azalea_protocol::packets::game::ClientboundGamePacket;
/// # async fn example(event: Event) {
/// # match event {
/// Event::Packet(packet) => match &*packet {
/// ClientboundGamePacket::Login(_) => {
/// println!("login packet");
/// }
/// _ => {}
/// },
/// # _ => {}
/// # }
/// # }
/// ```
Packet(Arc<azalea_protocol::packets::game::ClientboundGamePacket>),
/// A player joined the game (or more specifically, was added to the tab
/// list).
AddPlayer(PlayerInfo),
/// A player left the game (or maybe is still in the game and was just
/// removed from the tab list).
RemovePlayer(PlayerInfo),
/// A player was updated in the tab list (gamemode, display
/// name, or latency changed).
UpdatePlayer(PlayerInfo),
/// The client player died in-game.
Death(Option<Arc<ClientboundPlayerCombatKill>>),
/// A `KeepAlive` packet was sent by the server.
KeepAlive(u64),
/// The client disconnected from the server.
///
/// Also see [`Event::ConnectionFailed`].
Disconnect(Option<FormattedText>),
/// The initial connection to the server failed.
///
/// Also see [`Event::Disconnect`], and the related ECS event
/// [`ConnectionFailedEvent`].
ConnectionFailed(Arc<ConnectionError>),
ReceiveChunk(ChunkPos),
}
/// A component that contains an event sender for events that are only
/// received by local players.
///
/// The receiver for this is returned by
/// [`Client::start_client`](crate::Client::start_client).
#[derive(Component, Deref, DerefMut)]
pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
pub struct EventsPlugin;
impl Plugin for EventsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
chat_listener,
login_listener,
spawn_listener,
#[cfg(feature = "packet-event")]
packet_listener,
add_player_listener,
update_player_listener,
remove_player_listener,
death_listener.after(azalea_client::packet::death_event_on_0_health),
disconnect_listener,
connection_failed_listener.after(azalea_client::join::poll_create_connection_task),
receive_chunk_listener,
),
)
.add_systems(
PreUpdate,
init_listener.before(super::connection::read_packets),
)
.add_systems(GameTick, tick_listener)
.add_observer(keepalive_listener);
}
}
// when LocalPlayerEvents is added, it means the client just started
pub fn init_listener(query: Query<&LocalPlayerEvents, Added<LocalPlayerEvents>>) {
for local_player_events in &query {
let _ = local_player_events.send(Event::Init);
}
}
// when MinecraftEntityId is added, it means the player is now in the world
pub fn login_listener(
query: Query<(Entity, &LocalPlayerEvents), Added<MinecraftEntityId>>,
mut commands: Commands,
) {
for (entity, local_player_events) in &query {
let _ = local_player_events.send(Event::Login);
commands.entity(entity).remove::<SentSpawnEvent>();
}
}
/// A unit struct component that indicates that the entity has sent
/// [`Event::Spawn`].
///
/// This is just used internally by the [`spawn_listener`] system to avoid
/// sending the event twice if we stop being in an unloaded chunk. It's removed
/// when we receive a login packet.
#[derive(Component)]
pub struct SentSpawnEvent;
#[allow(clippy::type_complexity)]
pub fn spawn_listener(
query: Query<(Entity, &LocalPlayerEvents), (Added<InLoadedChunk>, Without<SentSpawnEvent>)>,
mut commands: Commands,
) {
for (entity, local_player_events) in &query {
let _ = local_player_events.send(Event::Spawn);
commands.entity(entity).insert(SentSpawnEvent);
}
}
pub fn chat_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<ChatReceivedEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::Chat(event.packet.clone()));
}
}
}
// only tick if we're in a world
pub fn tick_listener(query: Query<&LocalPlayerEvents, With<WorldName>>) {
for local_player_events in &query {
let _ = local_player_events.send(Event::Tick);
}
}
#[cfg(feature = "packet-event")]
pub fn packet_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<super::packet::game::ReceiveGamePacketEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::Packet(event.packet.clone()));
}
}
}
pub fn add_player_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<AddPlayerEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::AddPlayer(event.info.clone()));
}
}
}
pub fn update_player_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<UpdatePlayerEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::UpdatePlayer(event.info.clone()));
}
}
}
pub fn remove_player_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<RemovePlayerEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::RemovePlayer(event.info.clone()));
}
}
}
pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: MessageReader<DeathEvent>) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::Death(event.packet.clone().map(|p| p.into())));
}
}
}
/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
///
/// [`LocalEntity`]: azalea_entity::LocalEntity
pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
for local_player_events in &query {
local_player_events.send(Event::Death(None)).unwrap();
}
}
pub fn keepalive_listener(keep_alive: On<KeepAliveEvent>, query: Query<&LocalPlayerEvents>) {
if let Ok(local_player_events) = query.get(keep_alive.entity) {
let _ = local_player_events.send(Event::KeepAlive(keep_alive.id));
}
}
pub fn disconnect_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<DisconnectEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::Disconnect(event.reason.clone()));
}
}
}
pub fn connection_failed_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<ConnectionFailedEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::ConnectionFailed(event.error.clone()));
}
}
}
pub fn receive_chunk_listener(
query: Query<&LocalPlayerEvents>,
mut events: MessageReader<ReceiveChunkEvent>,
) {
for event in events.read() {
if let Ok(local_player_events) = query.get(event.entity) {
let _ = local_player_events.send(Event::ReceiveChunk(ChunkPos::new(
event.packet.x,
event.packet.z,
)));
}
}
}
|