chunkedge_scoreboard/
lib.rs

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
23/// Provides all necessary systems to manage scoreboards.
24pub 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            &EntityLayerId,
63        ),
64        Or<(Changed<ObjectiveDisplay>, Changed<ObjectiveRenderType>)>,
65    >,
66    mut layers: Query<&mut EntityLayer>,
67) {
68    for (objective, display, render_type, entity_layer) in objectives.iter() {
69        if objective.name().is_empty() {
70            warn!("Objective name is empty");
71        }
72        let mode = if objective.is_added() {
73            ObjectiveMode::Create {
74                objective_display_name: (&display.0).into_cow_text_component(),
75                render_type: *render_type,
76                number_format: None,
77            }
78        } else {
79            ObjectiveMode::Update {
80                objective_display_name: (&display.0).into_cow_text_component(),
81                render_type: *render_type,
82                number_format: None,
83            }
84        };
85
86        let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
87            warn!(
88                "No layer found for entity layer ID {:?}, can't update scoreboard objective",
89                entity_layer
90            );
91            continue;
92        };
93
94        layer.write_packet(&SetObjectiveS2c {
95            objective_name: &objective.0,
96            mode,
97        });
98    }
99}
100
101/// Must occur after `create_or_update_objectives`.
102fn display_objectives(
103    objectives: Query<
104        (&Objective, Ref<ScoreboardPosition>, &EntityLayerId),
105        Changed<ScoreboardPosition>,
106    >,
107    mut layers: Query<&mut EntityLayer>,
108) {
109    for (objective, position, entity_layer) in objectives.iter() {
110        let packet = SetDisplayObjectiveS2c {
111            score_name: &objective.0,
112            position: *position,
113        };
114
115        let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
116            warn!(
117                "No layer found for entity layer ID {:?}, can't update scoreboard display",
118                entity_layer
119            );
120            continue;
121        };
122
123        layer.write_packet(&packet);
124    }
125}
126
127fn remove_despawned_objectives(
128    mut commands: Commands,
129    objectives: Query<(Entity, &Objective, &EntityLayerId), With<Despawned>>,
130    mut layers: Query<&mut EntityLayer>,
131) {
132    for (entity, objective, entity_layer) in objectives.iter() {
133        commands.entity(entity).despawn();
134        let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
135            warn!(
136                "No layer found for entity layer ID {:?}, can't remove scoreboard objective",
137                entity_layer
138            );
139            continue;
140        };
141
142        layer.write_packet(&SetObjectiveS2c {
143            objective_name: &objective.0,
144            mode: ObjectiveMode::Remove,
145        });
146    }
147}
148
149fn handle_new_clients(
150    mut clients: Query<
151        (&mut Client, &VisibleEntityLayers, &OldVisibleEntityLayers),
152        Or<(Added<Client>, Changed<VisibleEntityLayers>)>,
153    >,
154    objectives: Query<
155        (
156            &Objective,
157            &ObjectiveDisplay,
158            &ObjectiveRenderType,
159            &ScoreboardPosition,
160            &ObjectiveScores,
161            &EntityLayerId,
162        ),
163        Without<Despawned>,
164    >,
165) {
166    // Remove objectives from the old visible layers that are not in the new visible
167    // layers.
168    for (mut client, visible_layers, old_visible_layers) in &mut clients {
169        let removed_layers: BTreeSet<_> = old_visible_layers
170            .get()
171            .difference(&visible_layers.0)
172            .collect();
173
174        for (objective, _, _, _, _, layer) in objectives.iter() {
175            if !removed_layers.contains(&layer.0) {
176                continue;
177            }
178            client.write_packet(&SetObjectiveS2c {
179                objective_name: &objective.0,
180                mode: ObjectiveMode::Remove,
181            });
182        }
183    }
184
185    // Add objectives from the new visible layers that are not in the old visible
186    // layers, or send all objectives if the client is new.
187    for (mut client, visible_layers, old_visible_layers) in &mut clients {
188        // not sure how to avoid the clone here
189        let added_layers = if client.is_added() {
190            debug!("client is new, sending all objectives");
191            visible_layers.0.clone()
192        } else {
193            visible_layers
194                .0
195                .difference(old_visible_layers.get())
196                .copied()
197                .collect::<BTreeSet<_>>()
198        };
199
200        for (objective, display, render_type, position, scores, layer) in objectives.iter() {
201            if !added_layers.contains(&layer.0) {
202                continue;
203            }
204
205            client.write_packet(&SetObjectiveS2c {
206                objective_name: &objective.0,
207                mode: ObjectiveMode::Create {
208                    objective_display_name: (&display.0).into_cow_text_component(),
209                    render_type: *render_type,
210                    number_format: None,
211                },
212            });
213            client.write_packet(&SetDisplayObjectiveS2c {
214                score_name: &objective.0,
215                position: *position,
216            });
217
218            for (key, score) in &scores.0 {
219                let packet = SetScoreS2c {
220                    entity_name: key,
221                    objective_name: &objective.0,
222                    value: VarInt(*score),
223                    display_name: None,
224                    number_format: None,
225                };
226
227                client.write_packet(&packet);
228            }
229        }
230    }
231}
232
233fn update_scores(
234    mut objectives: Query<
235        (
236            &Objective,
237            &ObjectiveScores,
238            &mut OldObjectiveScores,
239            &EntityLayerId,
240        ),
241        (Changed<ObjectiveScores>, Without<Despawned>),
242    >,
243    mut layers: Query<&mut EntityLayer>,
244) {
245    for (objective, scores, mut old_scores, entity_layer) in &mut objectives {
246        let Ok(mut layer) = layers.get_mut(entity_layer.0) else {
247            warn!(
248                "No layer found for entity layer ID {:?}, can't update scores",
249                entity_layer
250            );
251            continue;
252        };
253
254        for changed_key in old_scores.diff(scores) {
255            match scores.0.get(changed_key) {
256                Some(score) => {
257                    let packet = SetScoreS2c {
258                        entity_name: changed_key,
259                        objective_name: &objective.0,
260                        value: VarInt(*score),
261                        display_name: None,
262                        number_format: None,
263                    };
264
265                    layer.write_packet(&packet);
266                }
267                None => {
268                    let packet = ResetScoreS2c {
269                        entity_name: changed_key,
270                        objective_name: Some(&objective.0),
271                    };
272
273                    layer.write_packet(&packet);
274                }
275            };
276        }
277
278        old_scores.0.clone_from(&scores.0);
279    }
280}