1#![doc = include_str!("../README.md")]
2
3mod components;
4use std::collections::BTreeSet;
5
6use bevy_app::prelude::*;
7use bevy_ecs::prelude::*;
8use chunkedge_server::client::{Client, OldVisibleEntityLayers, VisibleEntityLayers};
9use chunkedge_server::entity::EntityLayerId;
10use chunkedge_server::layer::UpdateLayersPreClientSet;
11use chunkedge_server::protocol::packets::play::set_display_objective_s2c::ScoreboardPosition;
12use chunkedge_server::protocol::packets::play::set_objective_s2c::{
13 ObjectiveMode, ObjectiveRenderType,
14};
15use chunkedge_server::protocol::packets::play::{
16 ResetScoreS2c, SetDisplayObjectiveS2c, SetObjectiveS2c, SetScoreS2c,
17};
18use chunkedge_server::protocol::{IntoTextComponent, VarInt, WritePacket};
19use chunkedge_server::{Despawned, EntityLayer};
20pub use components::*;
21use tracing::{debug, warn};
22
23pub struct ScoreboardPlugin;
25
26impl Plugin for ScoreboardPlugin {
27 fn build(&self, app: &mut App) {
28 app.configure_sets(PostUpdate, ScoreboardSet.before(UpdateLayersPreClientSet));
29
30 app.add_systems(
31 PostUpdate,
32 (
33 create_or_update_objectives,
34 display_objectives.after(create_or_update_objectives),
35 )
36 .in_set(ScoreboardSet),
37 )
38 .add_systems(
39 PostUpdate,
40 remove_despawned_objectives.in_set(ScoreboardSet),
41 )
42 .add_systems(PostUpdate, handle_new_clients.in_set(ScoreboardSet))
43 .add_systems(
44 PostUpdate,
45 update_scores
46 .after(create_or_update_objectives)
47 .after(handle_new_clients)
48 .in_set(ScoreboardSet),
49 );
50 }
51}
52
53#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)]
54pub struct ScoreboardSet;
55
56fn create_or_update_objectives(
57 objectives: Query<
58 (
59 Ref<Objective>,
60 &ObjectiveDisplay,
61 &ObjectiveRenderType,
62 &ObjectiveNumberFormat,
63 &EntityLayerId,
64 ),
65 Or<(Changed<ObjectiveDisplay>, Changed<ObjectiveRenderType>)>,
66 >,
67 mut layers: Query<&mut EntityLayer>,
68) {
69 for (objective, display, render_type, number_format, entity_layer) in objectives.iter() {
70 if objective.name().is_empty() {
71 warn!("Objective name is empty");
72 }
73 let mode = if objective.is_added() {
74 ObjectiveMode::Create {
75 objective_display_name: (&display.0).into_cow_text_component(),
76 render_type: *render_type,
77 number_format: number_format.0.clone(),
78 }
79 } else {
80 ObjectiveMode::Update {
81 objective_display_name: (&display.0).into_cow_text_component(),
82 render_type: *render_type,
83 number_format: number_format.0.clone(),
84 }
85 };
86
87 let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
88 warn!(
89 "No layer found for entity layer ID {:?}, can't update scoreboard objective",
90 entity_layer
91 );
92 continue;
93 };
94
95 layer.write_packet(&SetObjectiveS2c {
96 objective_name: &objective.0,
97 mode,
98 });
99 }
100}
101
102fn display_objectives(
104 objectives: Query<
105 (&Objective, Ref<ScoreboardPosition>, &EntityLayerId),
106 Changed<ScoreboardPosition>,
107 >,
108 mut layers: Query<&mut EntityLayer>,
109) {
110 for (objective, position, entity_layer) in objectives.iter() {
111 let packet = SetDisplayObjectiveS2c {
112 score_name: &objective.0,
113 position: *position,
114 };
115
116 let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
117 warn!(
118 "No layer found for entity layer ID {:?}, can't update scoreboard display",
119 entity_layer
120 );
121 continue;
122 };
123
124 layer.write_packet(&packet);
125 }
126}
127
128fn remove_despawned_objectives(
129 mut commands: Commands,
130 objectives: Query<(Entity, &Objective, &EntityLayerId), With<Despawned>>,
131 mut layers: Query<&mut EntityLayer>,
132) {
133 for (entity, objective, entity_layer) in objectives.iter() {
134 commands.entity(entity).despawn();
135 let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
136 warn!(
137 "No layer found for entity layer ID {:?}, can't remove scoreboard objective",
138 entity_layer
139 );
140 continue;
141 };
142
143 layer.write_packet(&SetObjectiveS2c {
144 objective_name: &objective.0,
145 mode: ObjectiveMode::Remove,
146 });
147 }
148}
149
150fn handle_new_clients(
151 mut clients: Query<
152 (&mut Client, &VisibleEntityLayers, &OldVisibleEntityLayers),
153 Or<(Added<Client>, Changed<VisibleEntityLayers>)>,
154 >,
155 objectives: Query<
156 (
157 &Objective,
158 &ObjectiveDisplay,
159 &ObjectiveRenderType,
160 &ObjectiveNumberFormat,
161 &ScoreboardPosition,
162 &ObjectiveScores,
163 &EntityLayerId,
164 ),
165 Without<Despawned>,
166 >,
167) {
168 for (mut client, visible_layers, old_visible_layers) in &mut clients {
171 let removed_layers: BTreeSet<_> = old_visible_layers
172 .get()
173 .difference(&visible_layers.0)
174 .collect();
175
176 for (objective, _, _, _, _, _, layer) in objectives.iter() {
177 if !removed_layers.contains(&layer.0) {
178 continue;
179 }
180 client.write_packet(&SetObjectiveS2c {
181 objective_name: &objective.0,
182 mode: ObjectiveMode::Remove,
183 });
184 }
185 }
186
187 for (mut client, visible_layers, old_visible_layers) in &mut clients {
190 let added_layers = if client.is_added() {
192 debug!("client is new, sending all objectives");
193 visible_layers.0.clone()
194 } else {
195 visible_layers
196 .0
197 .difference(old_visible_layers.get())
198 .copied()
199 .collect::<BTreeSet<_>>()
200 };
201
202 for (objective, display, render_type, number_format, position, scores, layer) in
203 objectives.iter()
204 {
205 if !added_layers.contains(&layer.0) {
206 continue;
207 }
208
209 client.write_packet(&SetObjectiveS2c {
210 objective_name: &objective.0,
211 mode: ObjectiveMode::Create {
212 objective_display_name: (&display.0).into_cow_text_component(),
213 render_type: *render_type,
214 number_format: number_format.0.clone(),
215 },
216 });
217 client.write_packet(&SetDisplayObjectiveS2c {
218 score_name: &objective.0,
219 position: *position,
220 });
221
222 for (key, score) in &scores.0 {
223 let packet = SetScoreS2c {
224 entity_name: key,
225 objective_name: &objective.0,
226 value: VarInt(*score),
227 display_name: None,
228 number_format: None,
229 };
230
231 client.write_packet(&packet);
232 }
233 }
234 }
235}
236
237fn update_scores(
238 mut objectives: Query<
239 (
240 &Objective,
241 &ObjectiveScores,
242 &mut OldObjectiveScores,
243 &EntityLayerId,
244 ),
245 (Changed<ObjectiveScores>, Without<Despawned>),
246 >,
247 mut layers: Query<&mut EntityLayer>,
248) {
249 for (objective, scores, mut old_scores, entity_layer) in &mut objectives {
250 let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
251 warn!(
252 "No layer found for entity layer ID {:?}, can't update scores",
253 entity_layer
254 );
255 continue;
256 };
257
258 for changed_key in old_scores.diff(scores) {
259 match scores.0.get(changed_key) {
260 Some(score) => {
261 let packet = SetScoreS2c {
262 entity_name: changed_key,
263 objective_name: &objective.0,
264 value: VarInt(*score),
265 display_name: None,
266 number_format: None,
267 };
268
269 layer.write_packet(&packet);
270 }
271 None => {
272 let packet = ResetScoreS2c {
273 entity_name: changed_key,
274 objective_name: Some(&objective.0),
275 };
276
277 layer.write_packet(&packet);
278 }
279 };
280 }
281
282 old_scores.0.clone_from(&scores.0);
283 }
284}