1use std::{
2 collections::HashMap,
3 fmt::Debug,
4 mem,
5 net::SocketAddr,
6 sync::Arc,
7 thread,
8 time::{Duration, Instant},
9};
10
11use azalea_auth::game_profile::GameProfile;
12use azalea_core::{
13 data_registry::ResolvableDataRegistry, identifier::Identifier, position::Vec3, tick::GameTick,
14};
15use azalea_entity::{
16 Attributes, EntityUpdateSystems, PlayerAbilities, Position,
17 dimensions::EntityDimensions,
18 indexing::{EntityIdIndex, EntityUuidIndex},
19 inventory::Inventory,
20 metadata::Health,
21};
22use azalea_physics::local_player::PhysicsState;
23use azalea_protocol::{
24 ServerAddress,
25 connect::Proxy,
26 packets::{Packet, game::ServerboundGamePacket},
27 resolve,
28};
29use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
30use bevy_app::{App, AppExit, Plugin, PluginsState, SubApp, Update};
31use bevy_ecs::{
32 message::MessageCursor,
33 prelude::*,
34 schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
35};
36use parking_lot::{Mutex, RwLock};
37use thiserror::Error;
38use tokio::{
39 sync::{
40 mpsc::{self},
41 oneshot,
42 },
43 time,
44};
45use tracing::{info, warn};
46use uuid::Uuid;
47
48use crate::{
49 Account, DefaultPlugins,
50 attack::{self},
51 block_update::QueuedServerBlockUpdates,
52 chunks::ChunkBatchInfo,
53 connection::RawConnection,
54 disconnect::DisconnectEvent,
55 events::Event,
56 interact::BlockStatePredictionHandler,
57 join::{ConnectOpts, StartJoinServerEvent},
58 local_player::{Hunger, InstanceHolder, PermissionLevel, TabList},
59 mining::{self},
60 movement::LastSentLookDirection,
61 packet::game::SendGamePacketEvent,
62 player::{GameProfileComponent, PlayerInfo, retroactively_add_game_profile_component},
63};
64
65#[derive(Clone)]
75pub struct Client {
76 pub entity: Entity,
78
79 pub ecs: Arc<Mutex<World>>,
85}
86
87#[derive(Error, Debug)]
89pub enum JoinError {
90 #[error(transparent)]
91 Resolver(#[from] resolve::ResolveError),
92 #[error("The given address could not be parsed into a ServerAddress")]
93 InvalidAddress,
94}
95
96pub struct StartClientOpts {
97 pub ecs_lock: Arc<Mutex<World>>,
98 pub account: Account,
99 pub connect_opts: ConnectOpts,
100 pub event_sender: Option<mpsc::UnboundedSender<Event>>,
101}
102
103impl StartClientOpts {
104 pub fn new(
105 account: Account,
106 address: ServerAddress,
107 resolved_address: SocketAddr,
108 event_sender: Option<mpsc::UnboundedSender<Event>>,
109 ) -> StartClientOpts {
110 let mut app = App::new();
111 app.add_plugins(DefaultPlugins);
112
113 let (ecs_lock, start_running_systems, _appexit_rx) = start_ecs_runner(app.main_mut());
116 start_running_systems();
117
118 Self {
119 ecs_lock,
120 account,
121 connect_opts: ConnectOpts {
122 address,
123 resolved_address,
124 proxy: None,
125 },
126 event_sender,
127 }
128 }
129
130 pub fn proxy(mut self, proxy: Proxy) -> Self {
131 self.connect_opts.proxy = Some(proxy);
132 self
133 }
134}
135
136impl Client {
137 pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self {
142 Self {
143 entity,
145
146 ecs,
147 }
148 }
149
150 pub async fn join(
171 account: Account,
172 address: impl TryInto<ServerAddress>,
173 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
174 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
175 let resolved_address = resolve::resolve_address(&address).await?;
176 let (tx, rx) = mpsc::unbounded_channel();
177
178 let client = Self::start_client(StartClientOpts::new(
179 account,
180 address,
181 resolved_address,
182 Some(tx),
183 ))
184 .await;
185 Ok((client, rx))
186 }
187
188 pub async fn join_with_proxy(
189 account: Account,
190 address: impl TryInto<ServerAddress>,
191 proxy: Proxy,
192 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
193 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
194 let resolved_address = resolve::resolve_address(&address).await?;
195 let (tx, rx) = mpsc::unbounded_channel();
196
197 let client = Self::start_client(
198 StartClientOpts::new(account, address, resolved_address, Some(tx)).proxy(proxy),
199 )
200 .await;
201 Ok((client, rx))
202 }
203
204 pub async fn start_client(
207 StartClientOpts {
208 ecs_lock,
209 account,
210 connect_opts,
211 event_sender,
212 }: StartClientOpts,
213 ) -> Self {
214 let (start_join_callback_tx, mut start_join_callback_rx) =
217 mpsc::unbounded_channel::<Entity>();
218
219 ecs_lock.lock().write_message(StartJoinServerEvent {
220 account,
221 connect_opts,
222 event_sender,
223 start_join_callback_tx: Some(start_join_callback_tx),
224 });
225
226 let entity = start_join_callback_rx.recv().await.expect(
227 "start_join_callback should not be dropped before sending a message, this is a bug in Azalea",
228 );
229
230 Client::new(entity, ecs_lock)
231 }
232
233 pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) {
235 let packet = packet.into_variant();
236 self.ecs
237 .lock()
238 .commands()
239 .trigger(SendGamePacketEvent::new(self.entity, packet));
240 }
241
242 pub fn disconnect(&self) {
247 self.ecs.lock().write_message(DisconnectEvent {
248 entity: self.entity,
249 reason: None,
250 });
251 }
252
253 pub fn with_raw_connection<R>(&self, f: impl FnOnce(&RawConnection) -> R) -> R {
254 self.query_self::<&RawConnection, _>(f)
255 }
256 pub fn with_raw_connection_mut<R>(&self, f: impl FnOnce(Mut<'_, RawConnection>) -> R) -> R {
257 self.query_self::<&mut RawConnection, _>(f)
258 }
259
260 pub fn component<T: Component + Clone>(&self) -> T {
284 self.query_self::<&T, _>(|t| t.clone())
285 }
286
287 pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
294 self.query_self::<Option<&T>, _>(|t| t.cloned())
295 }
296
297 pub fn resource<T: Resource + Clone>(&self) -> T {
299 self.ecs.lock().resource::<T>().clone()
300 }
301
302 pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R {
304 let ecs = self.ecs.lock();
305 let value = ecs.resource::<T>();
306 f(value)
307 }
308
309 pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
311 let ecs = self.ecs.lock();
312 let value = ecs.get_resource::<T>();
313 f(value)
314 }
315
316 pub fn world(&self) -> Arc<RwLock<Instance>> {
323 let instance_holder = self.component::<InstanceHolder>();
324 instance_holder.instance.clone()
325 }
326
327 pub fn partial_world(&self) -> Arc<RwLock<PartialInstance>> {
337 let instance_holder = self.component::<InstanceHolder>();
338 instance_holder.partial_instance.clone()
339 }
340
341 pub fn logged_in(&self) -> bool {
343 self.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some())
345 }
346}
347
348impl Client {
349 pub fn position(&self) -> Vec3 {
357 Vec3::from(
358 &self
359 .get_component::<Position>()
360 .expect("the client's position hasn't been initialized yet"),
361 )
362 }
363
364 pub fn dimensions(&self) -> EntityDimensions {
370 self.component::<EntityDimensions>()
371 }
372
373 pub fn eye_position(&self) -> Vec3 {
378 self.query_self::<(&Position, &EntityDimensions), _>(|(pos, dim)| {
379 pos.up(dim.eye_height as f64)
380 })
381 }
382
383 pub fn health(&self) -> f32 {
387 *self.component::<Health>()
388 }
389
390 pub fn hunger(&self) -> Hunger {
395 self.component::<Hunger>().to_owned()
396 }
397
398 pub fn username(&self) -> String {
403 self.profile().name.to_owned()
404 }
405
406 pub fn uuid(&self) -> Uuid {
410 self.profile().uuid
411 }
412
413 pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> {
417 (*self.component::<TabList>()).clone()
418 }
419
420 pub fn profile(&self) -> GameProfile {
430 (*self.component::<GameProfileComponent>()).clone()
431 }
432
433 pub fn attributes(&self) -> Attributes {
436 self.component::<Attributes>()
437 }
438
439 pub fn player_uuid_by_username(&self, username: &str) -> Option<Uuid> {
445 self.tab_list()
446 .values()
447 .find(|player| player.profile.name == username)
448 .map(|player| player.profile.uuid)
449 }
450
451 pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> {
454 self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid))
455 }
456
457 pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> {
459 self.query_self::<&EntityIdIndex, _>(|entity_id_index| {
460 entity_id_index.get_by_ecs_entity(entity)
461 })
462 }
463 pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> {
465 self.query_self::<&EntityIdIndex, _>(|entity_id_index| {
466 entity_id_index.get_by_minecraft_entity(entity)
467 })
468 }
469
470 pub fn with_registry_holder<R>(
478 &self,
479 f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R,
480 ) -> R {
481 let instance = self.world();
482 let registries = &instance.read().registries;
483 f(registries)
484 }
485
486 pub fn resolve_registry_name(
492 &self,
493 registry: &impl ResolvableDataRegistry,
494 ) -> Option<Identifier> {
495 self.with_registry_holder(|registries| registry.resolve_name(registries).cloned())
496 }
497 pub fn with_resolved_registry<R: ResolvableDataRegistry, Ret>(
507 &self,
508 registry: R,
509 f: impl FnOnce(&Identifier, &R::DeserializesTo) -> Ret,
510 ) -> Option<Ret> {
511 self.with_registry_holder(|registries| {
512 registry
513 .resolve(registries)
514 .map(|(name, data)| f(name, data))
515 })
516 }
517}
518
519#[derive(Bundle)]
525pub struct LocalPlayerBundle {
526 pub raw_connection: RawConnection,
527 pub instance_holder: InstanceHolder,
528
529 pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
530}
531
532#[derive(Bundle, Default)]
537pub struct JoinedClientBundle {
538 pub physics_state: PhysicsState,
540 pub inventory: Inventory,
541 pub tab_list: TabList,
542 pub block_state_prediction_handler: BlockStatePredictionHandler,
543 pub queued_server_block_updates: QueuedServerBlockUpdates,
544 pub last_sent_direction: LastSentLookDirection,
545 pub abilities: PlayerAbilities,
546 pub permission_level: PermissionLevel,
547 pub chunk_batch_info: ChunkBatchInfo,
548 pub hunger: Hunger,
549
550 pub entity_id_index: EntityIdIndex,
551
552 pub mining: mining::MineBundle,
553 pub attack: attack::AttackBundle,
554
555 pub in_game_state: InGameState,
556}
557
558#[derive(Component, Clone, Debug, Default)]
561pub struct InGameState;
562#[derive(Component, Clone, Debug, Default)]
565pub struct InConfigState;
566
567pub struct AzaleaPlugin;
568impl Plugin for AzaleaPlugin {
569 fn build(&self, app: &mut App) {
570 app.add_systems(
571 Update,
572 (
573 retroactively_add_game_profile_component
575 .after(EntityUpdateSystems::Index)
576 .after(crate::join::handle_start_join_server_event),
577 ),
578 )
579 .init_resource::<InstanceContainer>()
580 .init_resource::<TabList>();
581 }
582}
583
584#[doc(hidden)]
591pub fn start_ecs_runner(
592 app: &mut SubApp,
593) -> (Arc<Mutex<World>>, impl FnOnce(), oneshot::Receiver<AppExit>) {
594 if app.plugins_state() != PluginsState::Cleaned {
597 if app.plugins_state() == PluginsState::Adding {
599 info!("Waiting for plugins to load ...");
600 while app.plugins_state() == PluginsState::Adding {
601 thread::yield_now();
602 }
603 }
604 app.finish();
606 app.cleanup();
607 }
608
609 let ecs = Arc::new(Mutex::new(mem::take(app.world_mut())));
612
613 let ecs_clone = ecs.clone();
614 let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
615
616 let (appexit_tx, appexit_rx) = oneshot::channel();
617 let start_running_systems = move || {
618 tokio::spawn(async move {
619 let appexit = run_schedule_loop(ecs_clone, outer_schedule_label).await;
620 appexit_tx.send(appexit)
621 });
622 };
623
624 (ecs, start_running_systems, appexit_rx)
625}
626
627async fn run_schedule_loop(
632 ecs: Arc<Mutex<World>>,
633 outer_schedule_label: InternedScheduleLabel,
634) -> AppExit {
635 let mut last_update: Option<Instant> = None;
636 let mut last_tick: Option<Instant> = None;
637
638 const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
642 const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
644
645 loop {
646 let now = Instant::now();
648 if let Some(last_update) = last_update {
649 let elapsed = now.duration_since(last_update);
650 if elapsed < UPDATE_DURATION_TARGET {
651 time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
652 }
653 }
654 last_update = Some(now);
655
656 let mut ecs = ecs.lock();
657
658 ecs.run_schedule(outer_schedule_label);
660 if last_tick
661 .map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
662 .unwrap_or(true)
663 {
664 if let Some(last_tick) = &mut last_tick {
665 *last_tick += GAME_TICK_DURATION_TARGET;
666
667 if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
670 warn!(
671 "GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
672 );
673 *last_tick = now;
674 }
675 } else {
676 last_tick = Some(now);
677 }
678 ecs.run_schedule(GameTick);
679 }
680
681 ecs.clear_trackers();
682 if let Some(exit) = should_exit(&mut ecs) {
683 ecs.clear_all();
685 return exit;
689 }
690 }
691}
692
693fn should_exit(ecs: &mut World) -> Option<AppExit> {
697 let mut reader = MessageCursor::default();
698
699 let events = ecs.get_resource::<Messages<AppExit>>()?;
700 let mut events = reader.read(events);
701
702 if events.len() != 0 {
703 return Some(
704 events
705 .find(|exit| exit.is_error())
706 .cloned()
707 .unwrap_or(AppExit::Success),
708 );
709 }
710
711 None
712}
713
714pub struct AmbiguityLoggerPlugin;
715impl Plugin for AmbiguityLoggerPlugin {
716 fn build(&self, app: &mut App) {
717 app.edit_schedule(Update, |schedule| {
718 schedule.set_build_settings(ScheduleBuildSettings {
719 ambiguity_detection: LogLevel::Warn,
720 ..Default::default()
721 });
722 });
723 app.edit_schedule(GameTick, |schedule| {
724 schedule.set_build_settings(ScheduleBuildSettings {
725 ambiguity_detection: LogLevel::Warn,
726 ..Default::default()
727 });
728 });
729 }
730}