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
|
use std::{
fmt::Debug,
mem,
sync::Arc,
thread,
time::{Duration, Instant},
};
use azalea_core::tick::GameTick;
use azalea_entity::{
EntityUpdateSystems, PlayerAbilities, indexing::EntityIdIndex, inventory::Inventory,
};
use azalea_physics::local_player::PhysicsState;
use azalea_world::Worlds;
use bevy_app::{App, AppExit, Plugin, PluginsState, SubApp, Update};
use bevy_ecs::{
message::MessageCursor,
prelude::*,
schedule::{InternedScheduleLabel, LogLevel, ScheduleBuildSettings},
};
use parking_lot::RwLock;
use tokio::{sync::oneshot, time};
use tracing::{info, warn};
use crate::{
attack,
block_update::QueuedServerBlockUpdates,
chunks::ChunkBatchInfo,
connection::RawConnection,
cookies::ServerCookies,
interact::BlockStatePredictionHandler,
local_player::{Experience, Hunger, PermissionLevel, TabList, WorldHolder},
mining,
movement::LastSentLookDirection,
player::retroactively_add_game_profile_component,
};
/// A bundle of components that's inserted right when we switch to the `login`
/// state and stay present on our clients until we disconnect.
///
/// For the components that are only present in the `game` state, see
/// [`JoinedClientBundle`].
#[derive(Bundle)]
pub struct LocalPlayerBundle {
pub raw_connection: RawConnection,
pub world_holder: WorldHolder,
pub metadata: azalea_entity::metadata::PlayerMetadataBundle,
}
/// A bundle for the components that are present on a local player that is
/// currently in the `game` protocol state.
///
/// All of these components are also removed when the client disconnects.
///
/// If you want to filter for this, use [`InGameState`].
#[derive(Bundle, Default)]
pub struct JoinedClientBundle {
// note that WorldHolder isn't here because it's set slightly before we fully join the world
pub physics_state: PhysicsState,
pub inventory: Inventory,
pub tab_list: TabList,
pub block_state_prediction_handler: BlockStatePredictionHandler,
pub queued_server_block_updates: QueuedServerBlockUpdates,
pub last_sent_direction: LastSentLookDirection,
pub abilities: PlayerAbilities,
pub permission_level: PermissionLevel,
pub chunk_batch_info: ChunkBatchInfo,
pub hunger: Hunger,
pub experience: Experience,
pub cookies: ServerCookies,
pub entity_id_index: EntityIdIndex,
pub mining: mining::MineBundle,
pub attack: attack::AttackBundle,
pub in_game_state: InGameState,
}
/// A marker component for local players that are currently in the
/// `game` state.
#[derive(Clone, Component, Debug, Default)]
pub struct InGameState;
/// A marker component for local players that are currently in the
/// `configuration` state.
#[derive(Clone, Component, Debug, Default)]
pub struct InConfigState;
pub struct AzaleaPlugin;
impl Plugin for AzaleaPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
// add GameProfileComponent when we get an AddPlayerEvent
retroactively_add_game_profile_component
.after(EntityUpdateSystems::Index)
.after(crate::join::handle_start_join_server_event),
),
)
.init_resource::<Worlds>()
.init_resource::<TabList>();
}
}
/// Create the ECS world, and return a function that begins running systems.
/// This exists to allow you to make last-millisecond updates to the world
/// before any systems start running.
///
/// You can create your app with `App::new()`, but don't forget to add
/// [`DefaultPlugins`].
///
/// # Panics
///
/// This function panics if it's called outside of a Tokio `LocalSet` (or
/// `LocalRuntime`). This exists so Azalea doesn't unexpectedly run game ticks
/// in the middle of blocking user code.
#[doc(hidden)]
pub fn start_ecs_runner(
app: &mut SubApp,
) -> (
Arc<RwLock<World>>,
impl FnOnce(),
oneshot::Receiver<AppExit>,
) {
// this block is based on Bevy's default runner:
// https://github.com/bevyengine/bevy/blob/390877cdae7a17095a75c8f9f1b4241fe5047e83/crates/bevy_app/src/schedule_runner.rs#L77-L85
if app.plugins_state() != PluginsState::Cleaned {
// Wait for plugins to load
if app.plugins_state() == PluginsState::Adding {
info!("Waiting for plugins to load ...");
while app.plugins_state() == PluginsState::Adding {
thread::yield_now();
}
}
// Finish adding plugins and cleanup
app.finish();
app.cleanup();
}
// all resources should have been added by now so we can take the ecs from the
// app
let ecs = Arc::new(RwLock::new(mem::take(app.world_mut())));
let ecs_clone = ecs.clone();
let outer_schedule_label = *app.update_schedule.as_ref().unwrap();
let (appexit_tx, appexit_rx) = oneshot::channel();
let start_running_systems = move || {
tokio::task::spawn_local(async move {
let appexit = run_schedule_loop(ecs_clone, outer_schedule_label).await;
appexit_tx.send(appexit)
});
};
(ecs, start_running_systems, appexit_rx)
}
/// Runs the `Update` schedule 60 times per second and the `GameTick` schedule
/// 20 times per second.
///
/// Exits when we receive an `AppExit` event.
async fn run_schedule_loop(
ecs: Arc<RwLock<World>>,
outer_schedule_label: InternedScheduleLabel,
) -> AppExit {
let mut last_update: Option<Instant> = None;
let mut last_tick: Option<Instant> = None;
// azalea runs the Update schedule at most 60 times per second to simulate
// framerate. unlike vanilla though, we also only handle packets during Updates
// due to everything running in ecs systems.
const UPDATE_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 60);
// minecraft runs at 20 tps
const GAME_TICK_DURATION_TARGET: Duration = Duration::from_micros(1_000_000 / 20);
loop {
// sleep until the next update if necessary
let now = Instant::now();
if let Some(last_update) = last_update {
let elapsed = now.duration_since(last_update);
if elapsed < UPDATE_DURATION_TARGET {
time::sleep(UPDATE_DURATION_TARGET - elapsed).await;
}
}
last_update = Some(now);
let mut ecs = ecs.write();
// if last tick is None or more than 50ms ago, run the GameTick schedule
ecs.run_schedule(outer_schedule_label);
if last_tick
.map(|last_tick| last_tick.elapsed() > GAME_TICK_DURATION_TARGET)
.unwrap_or(true)
{
if let Some(last_tick) = &mut last_tick {
*last_tick += GAME_TICK_DURATION_TARGET;
// if we're more than 10 ticks behind, set last_tick to now.
// vanilla doesn't do it in exactly the same way but it shouldn't really matter
if (now - *last_tick) > GAME_TICK_DURATION_TARGET * 10 {
warn!(
"GameTick is more than 10 ticks behind, skipping ticks so we don't have to burst too much"
);
*last_tick = now;
}
} else {
last_tick = Some(now);
}
ecs.run_schedule(GameTick);
}
ecs.clear_trackers();
if let Some(exit) = should_exit(&mut ecs) {
// it's possible for references to the World to stay around, so we clear the ecs
ecs.clear_all();
// ^ note that this also forcefully disconnects all of our bots without sending
// a disconnect packet (which is fine because we want to disconnect immediately)
return exit;
}
}
}
/// Checks whether the [`AppExit`] event was sent, and if so returns it.
///
/// This is based on Bevy's `should_exit` function: https://github.com/bevyengine/bevy/blob/b9fd7680e78c4073dfc90fcfdc0867534d92abe0/crates/bevy_app/src/app.rs#L1292
fn should_exit(ecs: &mut World) -> Option<AppExit> {
let mut reader = MessageCursor::default();
let events = ecs.get_resource::<Messages<AppExit>>()?;
let mut events = reader.read(events);
if events.len() != 0 {
return Some(
events
.find(|exit| exit.is_error())
.cloned()
.unwrap_or(AppExit::Success),
);
}
None
}
pub struct AmbiguityLoggerPlugin;
impl Plugin for AmbiguityLoggerPlugin {
fn build(&self, app: &mut App) {
app.edit_schedule(Update, |schedule| {
schedule.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Warn,
..Default::default()
});
});
app.edit_schedule(GameTick, |schedule| {
schedule.set_build_settings(ScheduleBuildSettings {
ambiguity_detection: LogLevel::Warn,
..Default::default()
});
});
}
}
|