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, position::Vec3, resource_location::ResourceLocation,
14 tick::GameTick,
15};
16use azalea_entity::{
17 EntityUpdateSystems, PlayerAbilities, Position,
18 dimensions::EntityDimensions,
19 indexing::{EntityIdIndex, EntityUuidIndex},
20 metadata::Health,
21};
22use azalea_physics::local_player::PhysicsState;
23use azalea_protocol::{
24 ServerAddress,
25 connect::Proxy,
26 packets::{Packet, game::ServerboundGamePacket},
27 resolver,
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 simdnbt::owned::NbtCompound;
38use thiserror::Error;
39use tokio::{
40 sync::{
41 mpsc::{self},
42 oneshot,
43 },
44 time,
45};
46use tracing::{info, warn};
47use uuid::Uuid;
48
49use crate::{
50 Account, DefaultPlugins,
51 attack::{self},
52 block_update::QueuedServerBlockUpdates,
53 chunks::ChunkBatchInfo,
54 connection::RawConnection,
55 disconnect::DisconnectEvent,
56 events::Event,
57 interact::BlockStatePredictionHandler,
58 inventory::Inventory,
59 join::{ConnectOpts, StartJoinServerEvent},
60 local_player::{Hunger, InstanceHolder, PermissionLevel, TabList},
61 mining::{self},
62 movement::LastSentLookDirection,
63 packet::game::SendGamePacketEvent,
64 player::{GameProfileComponent, PlayerInfo, retroactively_add_game_profile_component},
65};
66
67#[derive(Clone)]
77pub struct Client {
78 pub entity: Entity,
80
81 pub ecs: Arc<Mutex<World>>,
87}
88
89#[derive(Error, Debug)]
91pub enum JoinError {
92 #[error("{0}")]
93 Resolver(#[from] resolver::ResolverError),
94 #[error("The given address could not be parsed into a ServerAddress")]
95 InvalidAddress,
96}
97
98pub struct StartClientOpts {
99 pub ecs_lock: Arc<Mutex<World>>,
100 pub account: Account,
101 pub connect_opts: ConnectOpts,
102 pub event_sender: Option<mpsc::UnboundedSender<Event>>,
103}
104
105impl StartClientOpts {
106 pub fn new(
107 account: Account,
108 address: ServerAddress,
109 resolved_address: SocketAddr,
110 event_sender: Option<mpsc::UnboundedSender<Event>>,
111 ) -> StartClientOpts {
112 let mut app = App::new();
113 app.add_plugins(DefaultPlugins);
114
115 let (ecs_lock, start_running_systems, _appexit_rx) = start_ecs_runner(app.main_mut());
118 start_running_systems();
119
120 Self {
121 ecs_lock,
122 account,
123 connect_opts: ConnectOpts {
124 address,
125 resolved_address,
126 proxy: None,
127 },
128 event_sender,
129 }
130 }
131
132 pub fn proxy(mut self, proxy: Proxy) -> Self {
133 self.connect_opts.proxy = Some(proxy);
134 self
135 }
136}
137
138impl Client {
139 pub fn new(entity: Entity, ecs: Arc<Mutex<World>>) -> Self {
144 Self {
145 entity,
147
148 ecs,
149 }
150 }
151
152 pub async fn join(
173 account: Account,
174 address: impl TryInto<ServerAddress>,
175 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
176 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
177 let resolved_address = resolver::resolve_address(&address).await?;
178 let (tx, rx) = mpsc::unbounded_channel();
179
180 let client = Self::start_client(StartClientOpts::new(
181 account,
182 address,
183 resolved_address,
184 Some(tx),
185 ))
186 .await;
187 Ok((client, rx))
188 }
189
190 pub async fn join_with_proxy(
191 account: Account,
192 address: impl TryInto<ServerAddress>,
193 proxy: Proxy,
194 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
195 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
196 let resolved_address = resolver::resolve_address(&address).await?;
197 let (tx, rx) = mpsc::unbounded_channel();
198
199 let client = Self::start_client(
200 StartClientOpts::new(account, address, resolved_address, Some(tx)).proxy(proxy),
201 )
202 .await;
203 Ok((client, rx))
204 }
205
206 pub async fn start_client(
209 StartClientOpts {
210 ecs_lock,
211 account,
212 connect_opts,
213 event_sender,
214 }: StartClientOpts,
215 ) -> Self {
216 let (start_join_callback_tx, mut start_join_callback_rx) =
219 mpsc::unbounded_channel::<Entity>();
220
221 ecs_lock.lock().write_message(StartJoinServerEvent {
222 account,
223 connect_opts,
224 event_sender,
225 start_join_callback_tx: Some(start_join_callback_tx),
226 });
227
228 let entity = start_join_callback_rx.recv().await.expect(
229 "start_join_callback should not be dropped before sending a message, this is a bug in Azalea",
230 );
231
232 Client::new(entity, ecs_lock)
233 }
234
235 pub fn write_packet(&self, packet: impl Packet<ServerboundGamePacket>) {
237 let packet = packet.into_variant();
238 self.ecs
239 .lock()
240 .commands()
241 .trigger(SendGamePacketEvent::new(self.entity, packet));
242 }
243
244 pub fn disconnect(&self) {
249 self.ecs.lock().write_message(DisconnectEvent {
250 entity: self.entity,
251 reason: None,
252 });
253 }
254
255 pub fn with_raw_connection<R>(&self, f: impl FnOnce(&RawConnection) -> R) -> R {
256 self.query_self::<&RawConnection, _>(f)
257 }
258 pub fn with_raw_connection_mut<R>(&self, f: impl FnOnce(Mut<'_, RawConnection>) -> R) -> R {
259 self.query_self::<&mut RawConnection, _>(f)
260 }
261
262 pub fn component<T: Component + Clone>(&self) -> T {
286 self.query_self::<&T, _>(|t| t.clone())
287 }
288
289 pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
296 self.query_self::<Option<&T>, _>(|t| t.cloned())
297 }
298
299 pub fn resource<T: Resource + Clone>(&self) -> T {
301 self.ecs.lock().resource::<T>().clone()
302 }
303
304 pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R {
306 let ecs = self.ecs.lock();
307 let value = ecs.resource::<T>();
308 f(value)
309 }
310
311 pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
313 let ecs = self.ecs.lock();
314 let value = ecs.get_resource::<T>();
315 f(value)
316 }
317
318 pub fn world(&self) -> Arc<RwLock<Instance>> {
325 let instance_holder = self.component::<InstanceHolder>();
326 instance_holder.instance.clone()
327 }
328
329 pub fn partial_world(&self) -> Arc<RwLock<PartialInstance>> {
339 let instance_holder = self.component::<InstanceHolder>();
340 instance_holder.partial_instance.clone()
341 }
342
343 pub fn logged_in(&self) -> bool {
345 self.query_self::<Option<&InstanceName>, _>(|ins| ins.is_some())
347 }
348}
349
350impl Client {
351 pub fn position(&self) -> Vec3 {
359 Vec3::from(
360 &self
361 .get_component::<Position>()
362 .expect("the client's position hasn't been initialized yet"),
363 )
364 }
365
366 pub fn dimensions(&self) -> EntityDimensions {
372 self.component::<EntityDimensions>()
373 }
374
375 pub fn eye_position(&self) -> Vec3 {
380 self.query_self::<(&Position, &EntityDimensions), _>(|(pos, dim)| {
381 pos.up(dim.eye_height as f64)
382 })
383 }
384
385 pub fn health(&self) -> f32 {
389 *self.component::<Health>()
390 }
391
392 pub fn hunger(&self) -> Hunger {
397 self.component::<Hunger>().to_owned()
398 }
399
400 pub fn username(&self) -> String {
405 self.profile().name.to_owned()
406 }
407
408 pub fn uuid(&self) -> Uuid {
412 self.profile().uuid
413 }
414
415 pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> {
419 (*self.component::<TabList>()).clone()
420 }
421
422 pub fn profile(&self) -> GameProfile {
432 (*self.component::<GameProfileComponent>()).clone()
433 }
434
435 pub fn player_uuid_by_username(&self, username: &str) -> Option<Uuid> {
441 self.tab_list()
442 .values()
443 .find(|player| player.profile.name == username)
444 .map(|player| player.profile.uuid)
445 }
446
447 pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> {
450 self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid))
451 }
452
453 pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> {
455 self.query_self::<&EntityIdIndex, _>(|entity_id_index| {
456 entity_id_index.get_by_ecs_entity(entity)
457 })
458 }
459 pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> {
461 self.query_self::<&EntityIdIndex, _>(|entity_id_index| {
462 entity_id_index.get_by_minecraft_entity(entity)
463 })
464 }
465
466 pub fn with_registry_holder<R>(
474 &self,
475 f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R,
476 ) -> R {
477 let instance = self.world();
478 let registries = &instance.read().registries;
479 f(registries)
480 }
481
482 pub fn resolve_registry_name(
488 &self,
489 registry: &impl ResolvableDataRegistry,
490 ) -> Option<ResourceLocation> {
491 self.with_registry_holder(|registries| registry.resolve_name(registries))
492 }
493 pub fn with_resolved_registry<R>(
503 &self,
504 registry: impl ResolvableDataRegistry,
505 f: impl FnOnce(&ResourceLocation, &NbtCompound) -> R,
506 ) -> Option<R> {
507 self.with_registry_holder(|registries| {
508 registry
509 .resolve(registries)
510 .map(|(name, data)| f(name, data))
511 })
512 }
513}
514
515#[derive(Bundle)]
521pub struct LocalPlayerBundle {
522 pub raw_connection: RawConnection,
523 pub instance_holder: InstanceHolder,
524
525 pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
526}
527
528#[derive(Bundle, Default)]
533pub struct JoinedClientBundle {
534 pub physics_state: PhysicsState,
536 pub inventory: Inventory,
537 pub tab_list: TabList,
538 pub block_state_prediction_handler: BlockStatePredictionHandler,
539 pub queued_server_block_updates: QueuedServerBlockUpdates,
540 pub last_sent_direction: LastSentLookDirection,
541 pub abilities: PlayerAbilities,
542 pub permission_level: PermissionLevel,
543 pub chunk_batch_info: ChunkBatchInfo,
544 pub hunger: Hunger,
545
546 pub entity_id_index: EntityIdIndex,
547
548 pub mining: mining::MineBundle,
549 pub attack: attack::AttackBundle,
550
551 pub in_game_state: InGameState,
552}
553
554#[derive(Component, Clone, Debug, Default)]
557pub struct InGameState;
558#[derive(Component, Clone, Debug, Default)]
561pub struct InConfigState;
562
563pub struct AzaleaPlugin;
564impl Plugin for AzaleaPlugin {
565 fn build(&self, app: &mut App) {
566 app.add_systems(
567 Update,
568 (
569 retroactively_add_game_profile_component
571 .after(EntityUpdateSystems::Index)
572 .after(crate::join::handle_start_join_server_event),
573 ),
574 )
575 .init_resource::<InstanceContainer>()
576 .init_resource::<TabList>();
577 }
578}
579
580#[doc(hidden)]
587pub fn start_ecs_runner(
588 app: &mut SubApp,
589) -> (Arc<Mutex<World>>, impl FnOnce(), oneshot::Receiver<AppExit>) {
590 if app.plugins_state() != PluginsState::Cleaned {
593 if app.plugins_state() == PluginsState::Adding {
595 info!("Waiting for plugins to load ...");
596 while app.plugins_state() == PluginsState::Adding {
597 thread::yield_now();
598 }
599 }
600 app.finish();
602 app.cleanup();
603 }
604
605 let ecs = Arc::new(Mutex::new(mem::take(app.world_mut())));
608
609 let ecs_clone = ecs.clone();
610 let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
611
612 let (appexit_tx, appexit_rx) = oneshot::channel();
613 let start_running_systems = move || {
614 tokio::spawn(async move {
615 let appexit = run_schedule_loop(ecs_clone, outer_schedule_label).await;
616 appexit_tx.send(appexit)
617 });
618 };
619
620 (ecs, start_running_systems, appexit_rx)
621}
622
623async fn run_schedule_loop(
628 ecs: Arc<Mutex<World>>,
629 outer_schedule_label: InternedScheduleLabel,
630) -> AppExit {
631 let mut last_update: Option<Instant> = None;
632 let mut last_tick: Option<Instant> = None;
633
634 const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
638 const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
640
641 loop {
642 let now = Instant::now();
644 if let Some(last_update) = last_update {
645 let elapsed = now.duration_since(last_update);
646 if elapsed < UPDATE_DURATION_TARGET {
647 time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
648 }
649 }
650 last_update = Some(now);
651
652 let mut ecs = ecs.lock();
653
654 ecs.run_schedule(outer_schedule_label);
656 if last_tick
657 .map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
658 .unwrap_or(true)
659 {
660 if let Some(last_tick) = &mut last_tick {
661 *last_tick += GAME_TICK_DURATION_TARGET;
662
663 if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
666 warn!(
667 "GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
668 );
669 *last_tick = now;
670 }
671 } else {
672 last_tick = Some(now);
673 }
674 ecs.run_schedule(GameTick);
675 }
676
677 ecs.clear_trackers();
678 if let Some(exit) = should_exit(&mut ecs) {
679 ecs.clear_all();
681 return exit;
685 }
686 }
687}
688
689fn should_exit(ecs: &mut World) -> Option<AppExit> {
693 let mut reader = MessageCursor::default();
694
695 let events = ecs.get_resource::<Messages<AppExit>>()?;
696 let mut events = reader.read(events);
697
698 if events.len() != 0 {
699 return Some(
700 events
701 .find(|exit| exit.is_error())
702 .cloned()
703 .unwrap_or(AppExit::Success),
704 );
705 }
706
707 None
708}
709
710pub struct AmbiguityLoggerPlugin;
711impl Plugin for AmbiguityLoggerPlugin {
712 fn build(&self, app: &mut App) {
713 app.edit_schedule(Update, |schedule| {
714 schedule.set_build_settings(ScheduleBuildSettings {
715 ambiguity_detection: LogLevel::Warn,
716 ..Default::default()
717 });
718 });
719 app.edit_schedule(GameTick, |schedule| {
720 schedule.set_build_settings(ScheduleBuildSettings {
721 ambiguity_detection: LogLevel::Warn,
722 ..Default::default()
723 });
724 });
725 }
726}