1use std::{
2 collections::HashMap,
3 fmt::Debug,
4 io,
5 net::SocketAddr,
6 sync::Arc,
7 thread,
8 time::{Duration, Instant},
9};
10
11use azalea_auth::{game_profile::GameProfile, sessionserver::ClientSessionServerError};
12use azalea_chat::FormattedText;
13use azalea_core::{
14 data_registry::ResolvableDataRegistry, position::Vec3, resource_location::ResourceLocation,
15 tick::GameTick,
16};
17use azalea_entity::{
18 EntityPlugin, EntityUpdateSet, EyeHeight, LocalEntity, Position,
19 indexing::{EntityIdIndex, EntityUuidIndex},
20 metadata::Health,
21};
22use azalea_physics::PhysicsPlugin;
23use azalea_protocol::{
24 ServerAddress,
25 common::client_information::ClientInformation,
26 connect::{Connection, ConnectionError, Proxy},
27 packets::{
28 self, ClientIntention, ConnectionProtocol, PROTOCOL_VERSION, Packet,
29 config::{ClientboundConfigPacket, ServerboundConfigPacket},
30 game::ServerboundGamePacket,
31 handshake::{
32 ClientboundHandshakePacket, ServerboundHandshakePacket,
33 s_intention::ServerboundIntention,
34 },
35 login::{
36 ClientboundLoginPacket, s_hello::ServerboundHello, s_key::ServerboundKey,
37 s_login_acknowledged::ServerboundLoginAcknowledged,
38 },
39 },
40 resolver,
41};
42use azalea_world::{Instance, InstanceContainer, InstanceName, MinecraftEntityId, PartialInstance};
43use bevy_app::{App, Plugin, PluginGroup, PluginGroupBuilder, PluginsState, Update};
44use bevy_ecs::{
45 bundle::Bundle,
46 component::Component,
47 entity::Entity,
48 schedule::{InternedScheduleLabel, IntoSystemConfigs, LogLevel, ScheduleBuildSettings},
49 system::Resource,
50 world::World,
51};
52use bevy_time::TimePlugin;
53use parking_lot::{Mutex, RwLock};
54use simdnbt::owned::NbtCompound;
55use thiserror::Error;
56use tokio::{
57 sync::mpsc::{self, error::TrySendError},
58 time,
59};
60use tracing::{debug, error, info};
61use uuid::Uuid;
62
63use crate::{
64 Account, PlayerInfo,
65 attack::{self, AttackPlugin},
66 brand::BrandPlugin,
67 chat::ChatPlugin,
68 chunks::{ChunkBatchInfo, ChunksPlugin},
69 disconnect::{DisconnectEvent, DisconnectPlugin},
70 events::{Event, EventsPlugin, LocalPlayerEvents},
71 interact::{CurrentSequenceNumber, InteractPlugin},
72 inventory::{Inventory, InventoryPlugin},
73 local_player::{
74 GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
75 },
76 mining::{self, MiningPlugin},
77 movement::{LastSentLookDirection, MovementPlugin, PhysicsState},
78 packet::{
79 PacketPlugin,
80 login::{self, InLoginState, LoginSendPacketQueue},
81 },
82 player::retroactively_add_game_profile_component,
83 pong::PongPlugin,
84 raw_connection::RawConnection,
85 respawn::RespawnPlugin,
86 task_pool::TaskPoolPlugin,
87 tick_broadcast::TickBroadcastPlugin,
88 tick_end::TickEndPlugin,
89};
90
91#[derive(Clone)]
101pub struct Client {
102 pub profile: GameProfile,
111 pub entity: Entity,
113
114 pub ecs: Arc<Mutex<World>>,
118
119 pub run_schedule_sender: mpsc::Sender<()>,
121}
122
123#[derive(Error, Debug)]
125pub enum JoinError {
126 #[error("{0}")]
127 Resolver(#[from] resolver::ResolverError),
128 #[error("{0}")]
129 Connection(#[from] ConnectionError),
130 #[error("{0}")]
131 ReadPacket(#[from] Box<azalea_protocol::read::ReadPacketError>),
132 #[error("{0}")]
133 Io(#[from] io::Error),
134 #[error("{0}")]
135 SessionServer(#[from] azalea_auth::sessionserver::ClientSessionServerError),
136 #[error("The given address could not be parsed into a ServerAddress")]
137 InvalidAddress,
138 #[error("Couldn't refresh access token: {0}")]
139 Auth(#[from] azalea_auth::AuthError),
140 #[error("Disconnected: {reason}")]
141 Disconnect { reason: FormattedText },
142}
143
144pub struct StartClientOpts<'a> {
145 pub ecs_lock: Arc<Mutex<World>>,
146 pub account: &'a Account,
147 pub address: &'a ServerAddress,
148 pub resolved_address: &'a SocketAddr,
149 pub proxy: Option<Proxy>,
150 pub run_schedule_sender: mpsc::Sender<()>,
151 pub event_sender: Option<mpsc::UnboundedSender<Event>>,
152}
153
154impl<'a> StartClientOpts<'a> {
155 pub fn new(
156 account: &'a Account,
157 address: &'a ServerAddress,
158 resolved_address: &'a SocketAddr,
159 event_sender: Option<mpsc::UnboundedSender<Event>>,
160 ) -> StartClientOpts<'a> {
161 let (run_schedule_sender, run_schedule_receiver) = mpsc::channel(1);
163
164 let mut app = App::new();
165 app.add_plugins(DefaultPlugins);
166
167 let ecs_lock = start_ecs_runner(app, run_schedule_receiver, run_schedule_sender.clone());
168
169 Self {
170 ecs_lock,
171 account,
172 address,
173 resolved_address,
174 proxy: None,
175 run_schedule_sender,
176 event_sender,
177 }
178 }
179
180 pub fn proxy(mut self, proxy: Proxy) -> Self {
181 self.proxy = Some(proxy);
182 self
183 }
184}
185
186impl Client {
187 pub fn new(
192 profile: GameProfile,
193 entity: Entity,
194 ecs: Arc<Mutex<World>>,
195 run_schedule_sender: mpsc::Sender<()>,
196 ) -> Self {
197 Self {
198 profile,
199 entity,
201
202 ecs,
203
204 run_schedule_sender,
205 }
206 }
207
208 pub async fn join(
229 account: &Account,
230 address: impl TryInto<ServerAddress>,
231 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
232 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
233 let resolved_address = resolver::resolve_address(&address).await?;
234 let (tx, rx) = mpsc::unbounded_channel();
235
236 let client = Self::start_client(StartClientOpts::new(
237 account,
238 &address,
239 &resolved_address,
240 Some(tx),
241 ))
242 .await?;
243 Ok((client, rx))
244 }
245
246 pub async fn join_with_proxy(
247 account: &Account,
248 address: impl TryInto<ServerAddress>,
249 proxy: Proxy,
250 ) -> Result<(Self, mpsc::UnboundedReceiver<Event>), JoinError> {
251 let address: ServerAddress = address.try_into().map_err(|_| JoinError::InvalidAddress)?;
252 let resolved_address = resolver::resolve_address(&address).await?;
253 let (tx, rx) = mpsc::unbounded_channel();
254
255 let client = Self::start_client(
256 StartClientOpts::new(account, &address, &resolved_address, Some(tx)).proxy(proxy),
257 )
258 .await?;
259 Ok((client, rx))
260 }
261
262 pub async fn start_client(
265 StartClientOpts {
266 ecs_lock,
267 account,
268 address,
269 resolved_address,
270 proxy,
271 run_schedule_sender,
272 event_sender,
273 }: StartClientOpts<'_>,
274 ) -> Result<Self, JoinError> {
275 let entity = {
278 let mut ecs = ecs_lock.lock();
279
280 let entity_uuid_index = ecs.resource::<EntityUuidIndex>();
281 let uuid = account.uuid_or_offline();
282 let entity = if let Some(entity) = entity_uuid_index.get(&account.uuid_or_offline()) {
283 debug!("Reusing entity {entity:?} for client");
284 entity
285 } else {
286 let entity = ecs.spawn_empty().id();
287 debug!("Created new entity {entity:?} for client");
288 let mut entity_uuid_index = ecs.resource_mut::<EntityUuidIndex>();
290 entity_uuid_index.insert(uuid, entity);
291 entity
292 };
293
294 ecs.entity_mut(entity).insert(account.to_owned());
296
297 entity
298 };
299
300 let conn = if let Some(proxy) = proxy {
301 Connection::new_with_proxy(resolved_address, proxy).await?
302 } else {
303 Connection::new(resolved_address).await?
304 };
305 let (conn, game_profile) =
306 Self::handshake(ecs_lock.clone(), entity, conn, account, address).await?;
307
308 let (read_conn, write_conn) = conn.into_split();
312 let (read_conn, write_conn) = (read_conn.raw, write_conn.raw);
313
314 let mut ecs = ecs_lock.lock();
317
318 let client = Client::new(
320 game_profile.clone(),
321 entity,
322 ecs_lock.clone(),
323 run_schedule_sender.clone(),
324 );
325
326 let instance = Instance::default();
327 let instance_holder = crate::local_player::InstanceHolder::new(
328 entity,
329 Arc::new(RwLock::new(instance)),
332 );
333
334 let mut entity = ecs.entity_mut(entity);
335 entity.insert((
336 LocalPlayerBundle {
338 raw_connection: RawConnection::new(
339 run_schedule_sender,
340 ConnectionProtocol::Configuration,
341 read_conn,
342 write_conn,
343 ),
344 game_profile: GameProfileComponent(game_profile),
345 client_information: crate::ClientInformation::default(),
346 instance_holder,
347 metadata: azalea_entity::metadata::PlayerMetadataBundle::default(),
348 },
349 InConfigState,
350 LocalEntity,
352 ));
353 if let Some(event_sender) = event_sender {
354 entity.insert(LocalPlayerEvents(event_sender));
356 }
357
358 Ok(client)
359 }
360
361 pub async fn handshake(
367 ecs_lock: Arc<Mutex<World>>,
368 entity: Entity,
369 mut conn: Connection<ClientboundHandshakePacket, ServerboundHandshakePacket>,
370 account: &Account,
371 address: &ServerAddress,
372 ) -> Result<
373 (
374 Connection<ClientboundConfigPacket, ServerboundConfigPacket>,
375 GameProfile,
376 ),
377 JoinError,
378 > {
379 conn.write(ServerboundIntention {
381 protocol_version: PROTOCOL_VERSION,
382 hostname: address.host.clone(),
383 port: address.port,
384 intention: ClientIntention::Login,
385 })
386 .await?;
387 let mut conn = conn.login();
388
389 let (ecs_packets_tx, mut ecs_packets_rx) = mpsc::unbounded_channel();
392 ecs_lock.lock().entity_mut(entity).insert((
393 LoginSendPacketQueue { tx: ecs_packets_tx },
394 crate::packet::login::IgnoreQueryIds::default(),
395 InLoginState,
396 ));
397
398 conn.write(ServerboundHello {
400 name: account.username.clone(),
401 profile_id: account.uuid.unwrap_or_default(),
404 })
405 .await?;
406
407 let (conn, profile) = loop {
408 let packet = tokio::select! {
409 packet = conn.read() => packet?,
410 Some(packet) = ecs_packets_rx.recv() => {
411 conn.write(packet).await?;
413 continue;
414 }
415 };
416
417 ecs_lock.lock().send_event(login::LoginPacketEvent {
418 entity,
419 packet: Arc::new(packet.clone()),
420 });
421
422 match packet {
423 ClientboundLoginPacket::Hello(p) => {
424 debug!("Got encryption request");
425 let Ok(e) = azalea_crypto::encrypt(&p.public_key, &p.challenge) else {
426 error!("Failed to encrypt the challenge from the server for {p:?}");
427 continue;
428 };
429
430 if let Some(access_token) = &account.access_token {
431 let mut attempts: usize = 1;
434
435 while let Err(e) = {
436 let access_token = access_token.lock().clone();
437 conn.authenticate(
438 &access_token,
439 &account
440 .uuid
441 .expect("Uuid must be present if access token is present."),
442 e.secret_key,
443 &p,
444 )
445 .await
446 } {
447 if attempts >= 2 {
448 return Err(e.into());
451 }
452 if matches!(
453 e,
454 ClientSessionServerError::InvalidSession
455 | ClientSessionServerError::ForbiddenOperation
456 ) {
457 account.refresh().await?;
460 } else {
461 return Err(e.into());
462 }
463 attempts += 1;
464 }
465 }
466
467 conn.write(ServerboundKey {
468 key_bytes: e.encrypted_public_key,
469 encrypted_challenge: e.encrypted_challenge,
470 })
471 .await?;
472
473 conn.set_encryption_key(e.secret_key);
474 }
475 ClientboundLoginPacket::LoginCompression(p) => {
476 debug!("Got compression request {:?}", p.compression_threshold);
477 conn.set_compression_threshold(p.compression_threshold);
478 }
479 ClientboundLoginPacket::LoginFinished(p) => {
480 debug!(
481 "Got profile {:?}. handshake is finished and we're now switching to the configuration state",
482 p.game_profile
483 );
484 conn.write(ServerboundLoginAcknowledged {}).await?;
485
486 break (conn.config(), p.game_profile);
487 }
488 ClientboundLoginPacket::LoginDisconnect(p) => {
489 debug!("Got disconnect {:?}", p);
490 return Err(JoinError::Disconnect { reason: p.reason });
491 }
492 ClientboundLoginPacket::CustomQuery(p) => {
493 debug!("Got custom query {:?}", p);
494 }
497 ClientboundLoginPacket::CookieRequest(p) => {
498 debug!("Got cookie request {:?}", p);
499
500 conn.write(packets::login::ServerboundCookieResponse {
501 key: p.key,
502 payload: None,
504 })
505 .await?;
506 }
507 }
508 };
509
510 ecs_lock
511 .lock()
512 .entity_mut(entity)
513 .remove::<login::IgnoreQueryIds>()
514 .remove::<LoginSendPacketQueue>()
515 .remove::<InLoginState>();
516
517 Ok((conn, profile))
518 }
519
520 pub fn write_packet(
522 &self,
523 packet: impl Packet<ServerboundGamePacket>,
524 ) -> Result<(), crate::raw_connection::WritePacketError> {
525 let packet = packet.into_variant();
526 self.raw_connection_mut(&mut self.ecs.lock())
527 .write_packet(packet)
528 }
529
530 pub fn disconnect(&self) {
535 self.ecs.lock().send_event(DisconnectEvent {
536 entity: self.entity,
537 reason: None,
538 });
539 }
540
541 pub fn raw_connection<'a>(&'a self, ecs: &'a mut World) -> &'a RawConnection {
542 self.query::<&RawConnection>(ecs)
543 }
544 pub fn raw_connection_mut<'a>(
545 &'a self,
546 ecs: &'a mut World,
547 ) -> bevy_ecs::world::Mut<'a, RawConnection> {
548 self.query::<&mut RawConnection>(ecs)
549 }
550
551 pub fn component<T: Component + Clone>(&self) -> T {
574 self.query::<&T>(&mut self.ecs.lock()).clone()
575 }
576
577 pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
582 self.query::<Option<&T>>(&mut self.ecs.lock()).cloned()
583 }
584
585 pub fn resource<T: Resource + Clone>(&self) -> T {
587 self.ecs.lock().resource::<T>().clone()
588 }
589
590 pub fn map_resource<T: Resource, R>(&self, f: impl FnOnce(&T) -> R) -> R {
592 let ecs = self.ecs.lock();
593 let value = ecs.resource::<T>();
594 f(value)
595 }
596
597 pub fn map_get_resource<T: Resource, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
599 let ecs = self.ecs.lock();
600 let value = ecs.get_resource::<T>();
601 f(value)
602 }
603
604 pub fn map_component<T: Component, R>(&self, f: impl FnOnce(&T) -> R) -> R {
624 let mut ecs = self.ecs.lock();
625 let value = self.query::<&T>(&mut ecs);
626 f(value)
627 }
628
629 pub fn map_get_component<T: Component, R>(&self, f: impl FnOnce(Option<&T>) -> R) -> R {
642 let mut ecs = self.ecs.lock();
643 let value = self.query::<Option<&T>>(&mut ecs);
644 f(value)
645 }
646
647 pub fn world(&self) -> Arc<RwLock<Instance>> {
654 let instance_holder = self.component::<InstanceHolder>();
655 instance_holder.instance.clone()
656 }
657
658 pub fn partial_world(&self) -> Arc<RwLock<PartialInstance>> {
668 let instance_holder = self.component::<InstanceHolder>();
669 instance_holder.partial_instance.clone()
670 }
671
672 pub fn logged_in(&self) -> bool {
674 self.query::<Option<&InstanceName>>(&mut self.ecs.lock())
676 .is_some()
677 }
678
679 pub async fn set_client_information(
695 &self,
696 client_information: ClientInformation,
697 ) -> Result<(), crate::raw_connection::WritePacketError> {
698 {
699 let mut ecs = self.ecs.lock();
700 let mut client_information_mut = self.query::<&mut ClientInformation>(&mut ecs);
701 *client_information_mut = client_information.clone();
702 }
703
704 if self.logged_in() {
705 debug!(
706 "Sending client information (already logged in): {:?}",
707 client_information
708 );
709 self.write_packet(azalea_protocol::packets::game::s_client_information::ServerboundClientInformation { information: client_information.clone() })?;
710 }
711
712 Ok(())
713 }
714}
715
716impl Client {
717 pub fn position(&self) -> Vec3 {
725 Vec3::from(
726 &self
727 .get_component::<Position>()
728 .expect("the client's position hasn't been initialized yet"),
729 )
730 }
731
732 pub fn eye_position(&self) -> Vec3 {
737 self.position().up((*self.component::<EyeHeight>()) as f64)
738 }
739
740 pub fn health(&self) -> f32 {
744 *self.component::<Health>()
745 }
746
747 pub fn hunger(&self) -> Hunger {
752 self.component::<Hunger>().to_owned()
753 }
754
755 pub fn username(&self) -> String {
760 self.component::<GameProfileComponent>().name.to_owned()
761 }
762
763 pub fn uuid(&self) -> Uuid {
767 self.component::<GameProfileComponent>().uuid
768 }
769
770 pub fn tab_list(&self) -> HashMap<Uuid, PlayerInfo> {
774 (*self.component::<TabList>()).clone()
775 }
776
777 pub fn player_uuid_by_username(&self, username: &str) -> Option<Uuid> {
783 self.tab_list()
784 .values()
785 .find(|player| player.profile.name == username)
786 .map(|player| player.profile.uuid)
787 }
788
789 pub fn entity_by_uuid(&self, uuid: Uuid) -> Option<Entity> {
792 self.map_resource::<EntityUuidIndex, _>(|entity_uuid_index| entity_uuid_index.get(&uuid))
793 }
794
795 pub fn minecraft_entity_by_ecs_entity(&self, entity: Entity) -> Option<MinecraftEntityId> {
797 self.map_component::<EntityIdIndex, _>(|entity_id_index| {
798 entity_id_index.get_by_ecs_entity(entity)
799 })
800 }
801 pub fn ecs_entity_by_minecraft_entity(&self, entity: MinecraftEntityId) -> Option<Entity> {
803 self.map_component::<EntityIdIndex, _>(|entity_id_index| {
804 entity_id_index.get_by_minecraft_entity(entity)
805 })
806 }
807
808 pub fn with_registry_holder<R>(
816 &self,
817 f: impl FnOnce(&azalea_core::registry_holder::RegistryHolder) -> R,
818 ) -> R {
819 let instance = self.world();
820 let registries = &instance.read().registries;
821 f(registries)
822 }
823
824 pub fn resolve_registry_name(
830 &self,
831 registry: &impl ResolvableDataRegistry,
832 ) -> Option<ResourceLocation> {
833 self.with_registry_holder(|registries| registry.resolve_name(registries))
834 }
835 pub fn with_resolved_registry<R>(
845 &self,
846 registry: impl ResolvableDataRegistry,
847 f: impl FnOnce(&ResourceLocation, &NbtCompound) -> R,
848 ) -> Option<R> {
849 self.with_registry_holder(|registries| {
850 registry
851 .resolve(registries)
852 .map(|(name, data)| f(name, data))
853 })
854 }
855}
856
857#[derive(Bundle)]
863pub struct LocalPlayerBundle {
864 pub raw_connection: RawConnection,
865 pub game_profile: GameProfileComponent,
866 pub client_information: ClientInformation,
867 pub instance_holder: InstanceHolder,
868
869 pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
870}
871
872#[derive(Bundle, Default)]
876pub struct JoinedClientBundle {
877 pub physics_state: PhysicsState,
879 pub inventory: Inventory,
880 pub tab_list: TabList,
881 pub current_sequence_number: CurrentSequenceNumber,
882 pub last_sent_direction: LastSentLookDirection,
883 pub abilities: PlayerAbilities,
884 pub permission_level: PermissionLevel,
885 pub chunk_batch_info: ChunkBatchInfo,
886 pub hunger: Hunger,
887
888 pub entity_id_index: EntityIdIndex,
889
890 pub mining: mining::MineBundle,
891 pub attack: attack::AttackBundle,
892
893 pub in_game_state: InGameState,
894}
895
896#[derive(Component, Clone, Debug, Default)]
899pub struct InGameState;
900#[derive(Component, Clone, Debug, Default)]
903pub struct InConfigState;
904
905pub struct AzaleaPlugin;
906impl Plugin for AzaleaPlugin {
907 fn build(&self, app: &mut App) {
908 app.add_systems(
909 Update,
910 (
911 retroactively_add_game_profile_component.after(EntityUpdateSet::Index),
913 ),
914 )
915 .init_resource::<InstanceContainer>()
916 .init_resource::<TabList>();
917 }
918}
919
920#[doc(hidden)]
925pub fn start_ecs_runner(
926 mut app: App,
927 run_schedule_receiver: mpsc::Receiver<()>,
928 run_schedule_sender: mpsc::Sender<()>,
929) -> Arc<Mutex<World>> {
930 if app.plugins_state() != PluginsState::Cleaned {
933 if app.plugins_state() == PluginsState::Adding {
935 info!("Waiting for plugins to load ...");
936 while app.plugins_state() == PluginsState::Adding {
937 thread::yield_now();
938 }
939 }
940 app.finish();
942 app.cleanup();
943 }
944
945 let ecs = Arc::new(Mutex::new(std::mem::take(app.world_mut())));
948
949 tokio::spawn(run_schedule_loop(
950 ecs.clone(),
951 *app.main().update_schedule.as_ref().unwrap(),
952 run_schedule_receiver,
953 ));
954 tokio::spawn(tick_run_schedule_loop(run_schedule_sender));
955
956 ecs
957}
958
959async fn run_schedule_loop(
960 ecs: Arc<Mutex<World>>,
961 outer_schedule_label: InternedScheduleLabel,
962 mut run_schedule_receiver: mpsc::Receiver<()>,
963) {
964 let mut last_tick: Option<Instant> = None;
965 loop {
966 run_schedule_receiver.recv().await;
968
969 let mut ecs = ecs.lock();
970
971 ecs.run_schedule(outer_schedule_label);
973 if last_tick
974 .map(|last_tick| last_tick.elapsed() > Duration::from_millis(50))
975 .unwrap_or(true)
976 {
977 if let Some(last_tick) = &mut last_tick {
978 *last_tick += Duration::from_millis(50);
979 } else {
980 last_tick = Some(Instant::now());
981 }
982 ecs.run_schedule(GameTick);
983 }
984
985 ecs.clear_trackers();
986 }
987}
988
989pub async fn tick_run_schedule_loop(run_schedule_sender: mpsc::Sender<()>) {
992 let mut game_tick_interval = time::interval(Duration::from_millis(50));
993 game_tick_interval.set_missed_tick_behavior(time::MissedTickBehavior::Burst);
995
996 loop {
997 game_tick_interval.tick().await;
998 if let Err(TrySendError::Closed(())) = run_schedule_sender.try_send(()) {
999 error!("tick_run_schedule_loop failed because run_schedule_sender was closed");
1000 return;
1002 }
1003 }
1004}
1005
1006pub struct AmbiguityLoggerPlugin;
1007impl Plugin for AmbiguityLoggerPlugin {
1008 fn build(&self, app: &mut App) {
1009 app.edit_schedule(Update, |schedule| {
1010 schedule.set_build_settings(ScheduleBuildSettings {
1011 ambiguity_detection: LogLevel::Warn,
1012 ..Default::default()
1013 });
1014 });
1015 app.edit_schedule(GameTick, |schedule| {
1016 schedule.set_build_settings(ScheduleBuildSettings {
1017 ambiguity_detection: LogLevel::Warn,
1018 ..Default::default()
1019 });
1020 });
1021 }
1022}
1023
1024pub struct DefaultPlugins;
1027
1028impl PluginGroup for DefaultPlugins {
1029 fn build(self) -> PluginGroupBuilder {
1030 #[allow(unused_mut)]
1031 let mut group = PluginGroupBuilder::start::<Self>()
1032 .add(AmbiguityLoggerPlugin)
1033 .add(TimePlugin)
1034 .add(PacketPlugin)
1035 .add(AzaleaPlugin)
1036 .add(EntityPlugin)
1037 .add(PhysicsPlugin)
1038 .add(EventsPlugin)
1039 .add(TaskPoolPlugin::default())
1040 .add(InventoryPlugin)
1041 .add(ChatPlugin)
1042 .add(DisconnectPlugin)
1043 .add(MovementPlugin)
1044 .add(InteractPlugin)
1045 .add(RespawnPlugin)
1046 .add(MiningPlugin)
1047 .add(AttackPlugin)
1048 .add(ChunksPlugin)
1049 .add(TickEndPlugin)
1050 .add(BrandPlugin)
1051 .add(TickBroadcastPlugin)
1052 .add(PongPlugin);
1053 #[cfg(feature = "log")]
1054 {
1055 group = group.add(bevy_log::LogPlugin::default());
1056 }
1057 group
1058 }
1059}