1use std::{
2 collections::HashMap,
3 fmt::Debug,
4 io, mem,
5 net::SocketAddr,
6 sync::Arc,
7 thread,
8 time::{Duration, Instant},
9};
10
11use azalea_auth::game_profile::GameProfile;
12use azalea_chat::FormattedText;
13use azalea_core::{
14 data_registry::ResolvableDataRegistry, position::Vec3, resource_location::ResourceLocation,
15 tick::GameTick,
16};
17use azalea_entity::{
18 EntityUpdateSet, EyeHeight, Position,
19 indexing::{EntityIdIndex, EntityUuidIndex},
20 metadata::Health,
21};
22use azalea_protocol::{
23 ServerAddress,
24 common::client_information::ClientInformation,
25 connect::{ConnectionError, Proxy},
26 packets::{
27 self, Packet,
28 game::{self, ServerboundGamePacket},
29 },
30 resolver,
31};
32use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
33use bevy_app::{App, Plugin, PluginsState, SubApp, Update};
34use bevy_ecs::{
35 prelude::*,
36 schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
37};
38use parking_lot::{Mutex, RwLock};
39use simdnbt::owned::NbtCompound;
40use thiserror::Error;
41use tokio::{
42 sync::mpsc::{self},
43 time,
44};
45use tracing::{debug, error, info, warn};
46use uuid::Uuid;
47
48use crate::{
49 Account, DefaultPlugins, PlayerInfo,
50 attack::{self},
51 chunks::ChunkBatchInfo,
52 connection::RawConnection,
53 disconnect::DisconnectEvent,
54 events::Event,
55 interact::CurrentSequenceNumber,
56 inventory::Inventory,
57 join::{ConnectOpts, StartJoinCallback, StartJoinServerEvent},
58 local_player::{
59 GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
60 },
61 mining::{self},
62 movement::{LastSentLookDirection, PhysicsState},
63 packet::game::SendPacketEvent,
64 player::retroactively_add_game_profile_component,
65};
66
67#[derive(Clone)]
77pub struct Client {
78 pub entity: Entity,
80
81 pub ecs: Arc<Mutex<World>>,
85}
86
87#[derive(Error, Debug)]
89pub enum JoinError {
90 #[error("{0}")]
91 Resolver(#[from] resolver::ResolverError),
92 #[error("{0}")]
93 Connection(#[from] ConnectionError),
94 #[error("{0}")]
95 ReadPacket(#[from] Box<azalea_protocol::read::ReadPacketError>),
96 #[error("{0}")]
97 Io(#[from] io::Error),
98 #[error("Failed to encrypt the challenge from the server for {0:?}")]
99 EncryptionError(packets::login::ClientboundHello),
100 #[error("{0}")]
101 SessionServer(#[from] azalea_auth::sessionserver::ClientSessionServerError),
102 #[error("The given address could not be parsed into a ServerAddress")]
103 InvalidAddress,
104 #[error("Couldn't refresh access token: {0}")]
105 Auth(#[from] azalea_auth::AuthError),
106 #[error("Disconnected: {reason}")]
107 Disconnect { reason: FormattedText },
108}
109
110pub struct StartClientOpts {
111 pub ecs_lock: Arc<Mutex<World>>,
112 pub account: Account,
113 pub connect_opts: ConnectOpts,
114 pub event_sender: Option<mpsc::UnboundedSender<Event>>,
115}
116
117impl StartClientOpts {
118 pub fn new(
119 account: Account,
120 address: ServerAddress,
121 resolved_address: SocketAddr,
122 event_sender: Option<mpsc::UnboundedSender<Event>>,
123 ) -> StartClientOpts {
124 let mut app = App::new();
125 app.add_plugins(DefaultPlugins);
126
127 let (ecs_lock, start_running_systems) = start_ecs_runner(app.main_mut());
128 start_running_systems();
129
130 Self {
131 ecs_lock,
132 account,
133 connect_opts: ConnectOpts {
134 address,
135 resolved_address,
136 proxy: None,
137 },
138 event_sender,
139 }
140 }
141
142 pub fn proxy(mut self, proxy: Proxy) -> Self {
143 self.connect_opts.proxy = Some(proxy);
144 self
145 }
146}
147
148impl Client {
149 pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self {
154 Self {
155 entity,
157
158 ecs,
159 }
160 }
161
162 pub async fn join(
183 account: Account,
184 address: impl TryInto<ServerAddress>,
185 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
186 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
187 let resolved_address = resolver::resolve_address(&address).await?;
188 let (tx, rx) = mpsc::unbounded_channel();
189
190 let client = Self::start_client(StartClientOpts::new(
191 account,
192 address,
193 resolved_address,
194 Some(tx),
195 ))
196 .await?;
197 Ok((client, rx))
198 }
199
200 pub async fn join_with_proxy(
201 account: Account,
202 address: impl TryInto<ServerAddress>,
203 proxy: Proxy,
204 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
205 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
206 let resolved_address = resolver::resolve_address(&address).await?;
207 let (tx, rx) = mpsc::unbounded_channel();
208
209 let client = Self::start_client(
210 StartClientOpts::new(account, address, resolved_address, Some(tx)).proxy(proxy),
211 )
212 .await?;
213 Ok((client, rx))
214 }
215
216 pub async fn start_client(
219 StartClientOpts {
220 ecs_lock,
221 account,
222 connect_opts,
223 event_sender,
224 }: StartClientOpts,
225 ) -> Result<Self, JoinError> {
226 let (start_join_callback_tx, mut start_join_callback_rx) =
229 mpsc::unbounded_channel::<Result<Entity, JoinError>>();
230
231 ecs_lock.lock().send_event(StartJoinServerEvent {
232 account,
233 connect_opts,
234 event_sender,
235 start_join_callback_tx: Some(StartJoinCallback(start_join_callback_tx)),
236 });
237
238 let entity = start_join_callback_rx.recv().await.expect(
239 "StartJoinCallback should not be dropped before sending a message, this is a bug in Azalea",
240 )?;
241
242 let client = Client::new(entity, ecs_lock.clone());
243 Ok(client)
244 }
245
246 pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) {
248 let packet = packet.into_variant();
249 self.ecs
250 .lock()
251 .commands()
252 .trigger(SendPacketEvent::new(self.entity, packet));
253 }
254
255 pub fn disconnect(&self) {
260 self.ecs.lock().send_event(DisconnectEvent {
261 entity: self.entity,
262 reason: None,
263 });
264 }
265
266 pub fn raw_connection<'a>(&'a self, ecs: &'a mut World) -> &'a RawConnection {
267 self.query::<&RawConnection>(ecs)
268 }
269 pub fn raw_connection_mut<'a>(
270 &'a self,
271 ecs: &'a mut World,
272 ) -> bevy_ecs::world::Mut<'a, RawConnection> {
273 self.query::<&mut RawConnection>(ecs)
274 }
275
276 pub fn component<T: Component + Clone>(&self) -> T {
299 self.query::<&T>(&mut self.ecs.lock()).clone()
300 }
301
302 pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
307 self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
308 }
309
310 pub fn resource<T: Resource + Clone>(&self) -> T {
312 self.ecs.lock().resource::<T>().clone()
313 }
314
315 pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R {
317 let ecs = self.ecs.lock();
318 let value = ecs.resource::<T>();
319 f(value)
320 }
321
322 pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
324 let ecs = self.ecs.lock();
325 let value = ecs.get_resource::<T>();
326 f(value)
327 }
328
329 pub fn map_component<T: Component, R>(&self, f: impl FnOnce(&T) -> R) -> R {
349 let mut ecs = self.ecs.lock();
350 let value = self.query::<&T>(&mut ecs);
351 f(value)
352 }
353
354 pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
367 let mut ecs = self.ecs.lock();
368 let value = self.query::<Option<&T>>(&mut ecs);
369 f(value)
370 }
371
372 pub fn world(&self) -> Arc<RwLock<Instance>> {
379 let instance_holder = self.component::<InstanceHolder>();
380 instance_holder.instance.clone()
381 }
382
383 pub fn partial_world(&self) -> Arc<RwLock<PartialInstance>> {
393 let instance_holder = self.component::<InstanceHolder>();
394 instance_holder.partial_instance.clone()
395 }
396
397 pub fn logged_in(&self) -> bool {
399 self.query::<Option<&InstanceName>>(&mut self.ecs.lock())
401 .is_some()
402 }
403
404 pub async fn set_client_information(&self, client_information: ClientInformation) {
420 {
421 let mut ecs = self.ecs.lock();
422 let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs);
423 *client_information_mut = client_information.clone();
424 }
425
426 if self.logged_in() {
427 debug!(
428 "Sending client information (already logged in): {:?}",
429 client_information
430 );
431 self.write_packet(game::s_client_information::ServerboundClientInformation {
432 client_information,
433 });
434 }
435 }
436}
437
438impl Client {
439 pub fn position(&self) -> Vec3 {
447 Vec3::from(
448 &self
449 .get_component::<Position>()
450 .expect("the client's position hasn't been initialized yet"),
451 )
452 }
453
454 pub fn eye_position(&self) -> Vec3 {
459 self.position().up((*self.component::<EyeHeight>()) as f64)
460 }
461
462 pub fn health(&self) -> f32 {
466 *self.component::<Health>()
467 }
468
469 pub fn hunger(&self) -> Hunger {
474 self.component::<Hunger>().to_owned()
475 }
476
477 pub fn username(&self) -> String {
482 self.profile().name.to_owned()
483 }
484
485 pub fn uuid(&self) -> Uuid {
489 self.profile().uuid
490 }
491
492 pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> {
496 (*self.component::<TabList>()).clone()
497 }
498
499 pub fn profile(&self) -> GameProfile {
509 (*self.component::<GameProfileComponent>()).clone()
510 }
511
512 pub fn player_uuid_by_username(&self, username: &str) -> Option<Uuid> {
518 self.tab_list()
519 .values()
520 .find(|player| player.profile.name == username)
521 .map(|player| player.profile.uuid)
522 }
523
524 pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> {
527 self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid))
528 }
529
530 pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> {
532 self.map_component::<EntityIdIndex, _>(|entity_id_index| {
533 entity_id_index.get_by_ecs_entity(entity)
534 })
535 }
536 pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> {
538 self.map_component::<EntityIdIndex, _>(|entity_id_index| {
539 entity_id_index.get_by_minecraft_entity(entity)
540 })
541 }
542
543 pub fn with_registry_holder<R>(
551 &self,
552 f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R,
553 ) -> R {
554 let instance = self.world();
555 let registries = &instance.read().registries;
556 f(registries)
557 }
558
559 pub fn resolve_registry_name(
565 &self,
566 registry: &impl ResolvableDataRegistry,
567 ) -> Option<ResourceLocation> {
568 self.with_registry_holder(|registries| registry.resolve_name(registries))
569 }
570 pub fn with_resolved_registry<R>(
580 &self,
581 registry: impl ResolvableDataRegistry,
582 f: impl FnOnce(&ResourceLocation, &NbtCompound) -> R,
583 ) -> Option<R> {
584 self.with_registry_holder(|registries| {
585 registry
586 .resolve(registries)
587 .map(|(name, data)| f(name, data))
588 })
589 }
590}
591
592#[derive(Bundle)]
598pub struct LocalPlayerBundle {
599 pub raw_connection: RawConnection,
600 pub client_information: ClientInformation,
601 pub instance_holder: InstanceHolder,
602
603 pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
604}
605
606#[derive(Bundle, Default)]
610pub struct JoinedClientBundle {
611 pub physics_state: PhysicsState,
613 pub inventory: Inventory,
614 pub tab_list: TabList,
615 pub current_sequence_number: CurrentSequenceNumber,
616 pub last_sent_direction: LastSentLookDirection,
617 pub abilities: PlayerAbilities,
618 pub permission_level: PermissionLevel,
619 pub chunk_batch_info: ChunkBatchInfo,
620 pub hunger: Hunger,
621
622 pub entity_id_index: EntityIdIndex,
623
624 pub mining: mining::MineBundle,
625 pub attack: attack::AttackBundle,
626
627 pub in_game_state: InGameState,
628}
629
630#[derive(Component, Clone, Debug, Default)]
633pub struct InGameState;
634#[derive(Component, Clone, Debug, Default)]
637pub struct InConfigState;
638
639pub struct AzaleaPlugin;
640impl Plugin for AzaleaPlugin {
641 fn build(&self, app: &mut App) {
642 app.add_systems(
643 Update,
644 (
645 retroactively_add_game_profile_component
647 .after(EntityUpdateSet::Index)
648 .after(crate::join::handle_start_join_server_event),
649 ),
650 )
651 .init_resource::<InstanceContainer>()
652 .init_resource::<TabList>();
653 }
654}
655
656#[doc(hidden)]
663pub fn start_ecs_runner(app: &mut SubApp) -> (Arc<Mutex<World>>, impl FnOnce()) {
664 if app.plugins_state() != PluginsState::Cleaned {
667 if app.plugins_state() == PluginsState::Adding {
669 info!("Waiting for plugins to load ...");
670 while app.plugins_state() == PluginsState::Adding {
671 thread::yield_now();
672 }
673 }
674 app.finish();
676 app.cleanup();
677 }
678
679 let ecs = Arc::new(Mutex::new(mem::take(app.world_mut())));
682
683 let ecs_clone = ecs.clone();
684 let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
685 let start_running_systems = move || {
686 tokio::spawn(run_schedule_loop(ecs_clone, outer_schedule_label));
687 };
688
689 (ecs, start_running_systems)
690}
691
692async fn run_schedule_loop(ecs: Arc<Mutex<World>>, outer_schedule_label: InternedScheduleLabel) {
693 let mut last_update: Option<Instant> = None;
694 let mut last_tick: Option<Instant> = None;
695
696 const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
700 const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
702
703 loop {
704 let now = Instant::now();
706 if let Some(last_update) = last_update {
707 let elapsed = now.duration_since(last_update);
708 if elapsed < UPDATE_DURATION_TARGET {
709 time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
710 }
711 }
712 last_update = Some(now);
713
714 let mut ecs = ecs.lock();
715
716 ecs.run_schedule(outer_schedule_label);
718 if last_tick
719 .map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
720 .unwrap_or(true)
721 {
722 if let Some(last_tick) = &mut last_tick {
723 *last_tick += GAME_TICK_DURATION_TARGET;
724
725 if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
728 warn!(
729 "GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
730 );
731 *last_tick = now;
732 }
733 } else {
734 last_tick = Some(now);
735 }
736 ecs.run_schedule(GameTick);
737 }
738
739 ecs.clear_trackers();
740 }
741}
742
743pub struct AmbiguityLoggerPlugin;
744impl Plugin for AmbiguityLoggerPlugin {
745 fn build(&self, app: &mut App) {
746 app.edit_schedule(Update, |schedule| {
747 schedule.set_build_settings(ScheduleBuildSettings {
748 ambiguity_detection: LogLevel::Warn,
749 ..Default::default()
750 });
751 });
752 app.edit_schedule(GameTick, |schedule| {
753 schedule.set_build_settings(ScheduleBuildSettings {
754 ambiguity_detection: LogLevel::Warn,
755 ..Default::default()
756 });
757 });
758 }
759}