azalea_client/plugins/
chunks.rs1use std::{
6 io::Cursor,
7 time::{Duration, Instant},
8};
9
10use azalea_core::position::ChunkPos;
11use azalea_protocol::packets::game::{
12 c_level_chunk_with_light::ClientboundLevelChunkWithLight,
13 s_chunk_batch_received::ServerboundChunkBatchReceived,
14};
15use bevy_app::{App, Plugin, Update};
16use bevy_ecs::prelude::*;
17use tracing::{error, trace};
18
19use crate::{
20 inventory::InventorySystems, local_player::WorldHolder, packet::game::SendGamePacketEvent,
21 respawn::perform_respawn,
22};
23
24pub struct ChunksPlugin;
25impl Plugin for ChunksPlugin {
26 fn build(&self, app: &mut App) {
27 app.add_systems(
28 Update,
29 (
30 handle_chunk_batch_start_event,
31 handle_receive_chunk_event,
32 handle_chunk_batch_finished_event,
33 )
34 .chain()
35 .before(InventorySystems)
36 .before(perform_respawn),
37 )
38 .add_message::<ReceiveChunkEvent>()
39 .add_message::<ChunkBatchStartEvent>()
40 .add_message::<ChunkBatchFinishedEvent>();
41 }
42}
43
44#[derive(Message)]
45pub struct ReceiveChunkEvent {
46 pub entity: Entity,
47 pub packet: ClientboundLevelChunkWithLight,
48}
49
50#[derive(Clone, Component, Debug)]
51pub struct ChunkBatchInfo {
52 pub start_time: Instant,
53 pub aggregated_duration_per_chunk: Duration,
54 pub old_samples_weight: u32,
55}
56
57#[derive(Message)]
58pub struct ChunkBatchStartEvent {
59 pub entity: Entity,
60}
61#[derive(Message)]
62pub struct ChunkBatchFinishedEvent {
63 pub entity: Entity,
64 pub batch_size: u32,
65}
66
67pub fn handle_receive_chunk_event(
68 mut events: MessageReader<ReceiveChunkEvent>,
69 mut query: Query<&WorldHolder>,
70) {
71 for event in events.read() {
72 let pos = ChunkPos::new(event.packet.x, event.packet.z);
73
74 let local_player = query.get_mut(event.entity).unwrap();
75
76 let mut world = local_player.shared.write();
77 let mut partial_world = local_player.partial.write();
78
79 let shared_chunk = world.chunks.get(&pos);
84 let this_client_has_chunk = partial_world.chunks.limited_get(&pos).is_some();
85
86 if !this_client_has_chunk && let Some(shared_chunk) = shared_chunk {
87 trace!("Skipping parsing chunk {pos:?} because we already know about it");
88 partial_world.chunks.limited_set(&pos, Some(shared_chunk));
89 continue;
90 }
91
92 let heightmaps = &event.packet.chunk_data.heightmaps;
93
94 if let Err(e) = partial_world.chunks.replace_with_packet_data(
95 &pos,
96 &mut Cursor::new(&event.packet.chunk_data.data),
97 heightmaps,
98 &mut world.chunks,
99 ) {
100 error!(
101 "Couldn't set chunk data: {e}. World height: {}",
102 world.chunks.height
103 );
104 }
105 }
106}
107
108impl ChunkBatchInfo {
109 pub fn batch_finished(&mut self, batch_size: u32) {
110 if batch_size == 0 {
111 return;
112 }
113 let batch_duration = self.start_time.elapsed();
114 let duration_per_chunk = batch_duration / batch_size;
115 let clamped_duration = Duration::clamp(
116 duration_per_chunk,
117 self.aggregated_duration_per_chunk / 3,
118 self.aggregated_duration_per_chunk * 3,
119 );
120 self.aggregated_duration_per_chunk =
121 ((self.aggregated_duration_per_chunk * self.old_samples_weight) + clamped_duration)
122 / (self.old_samples_weight + 1);
123 self.old_samples_weight = u32::min(49, self.old_samples_weight + 1);
124 }
125
126 pub fn desired_chunks_per_tick(&self) -> f32 {
127 (7000000. / self.aggregated_duration_per_chunk.as_nanos() as f64) as f32
128 }
129}
130
131pub fn handle_chunk_batch_start_event(
132 mut query: Query<&mut ChunkBatchInfo>,
133 mut events: MessageReader<ChunkBatchStartEvent>,
134) {
135 for event in events.read() {
136 if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) {
137 chunk_batch_info.start_time = Instant::now();
138 }
139 }
140}
141
142pub fn handle_chunk_batch_finished_event(
143 mut query: Query<&mut ChunkBatchInfo>,
144 mut events: MessageReader<ChunkBatchFinishedEvent>,
145 mut commands: Commands,
146) {
147 for event in events.read() {
148 if let Ok(mut chunk_batch_info) = query.get_mut(event.entity) {
149 chunk_batch_info.batch_finished(event.batch_size);
150 let desired_chunks_per_tick = chunk_batch_info.desired_chunks_per_tick();
151 commands.trigger(SendGamePacketEvent::new(
152 event.entity,
153 ServerboundChunkBatchReceived {
154 desired_chunks_per_tick,
155 },
156 ));
157 }
158 }
159}
160
161#[derive(Clone, Debug)]
162pub struct ChunkReceiveSpeedAccumulator {
163 batch_sizes: Vec<u32>,
164 batch_durations: Vec<u32>,
166 index: usize,
167 filled_size: usize,
168}
169impl ChunkReceiveSpeedAccumulator {
170 pub fn new(capacity: usize) -> Self {
171 Self {
172 batch_sizes: vec![0; capacity],
173 batch_durations: vec![0; capacity],
174 index: 0,
175 filled_size: 0,
176 }
177 }
178
179 pub fn accumulate(&mut self, batch_size: u32, batch_duration: Duration) {
180 self.batch_sizes[self.index] = batch_size;
181 self.batch_durations[self.index] =
182 f32::clamp(batch_duration.as_millis() as f32, 0., 15000.) as u32;
183 self.index = (self.index + 1) % self.batch_sizes.len();
184 if self.filled_size < self.batch_sizes.len() {
185 self.filled_size += 1;
186 }
187 }
188
189 pub fn get_millis_per_chunk(&self) -> f64 {
190 let mut total_batch_size = 0;
191 let mut total_batch_duration = 0;
192 for i in 0..self.filled_size {
193 total_batch_size += self.batch_sizes[i];
194 total_batch_duration += self.batch_durations[i];
195 }
196 if total_batch_size == 0 {
197 return 0.;
198 }
199 total_batch_duration as f64 / total_batch_size as f64
200 }
201}
202
203impl Default for ChunkBatchInfo {
204 fn default() -> Self {
205 Self {
206 start_time: Instant::now(),
207 aggregated_duration_per_chunk: Duration::from_millis(2),
208 old_samples_weight: 1,
209 }
210 }
211}