Repository: vleue/jornet (language: rust)

Issue:
Send Bevy events when things happen
It would be very useful for Bevy events to be sent when Jornet calls succeed or fail.

A couple example use-cases:

- I only want to show the leaderboard at the end of a game after the player's score has been added.
- I want to create my own player names and persist the resulting `Player` so players don't get a new name every session.

To accomplish these things, I need some more feedback from Jornet than "leaderboard was changed."

Success/Failure events for each call would be sufficient for my needs, although I could imagine more data also being useful (e.g. returning some sort of "score identifier" when sending a score succeeds)


Discussion / hints:

Fix patch:
diff --git a/bevy-jornet/src/leaderboards.rs b/bevy-jornet/src/leaderboards.rs
index e5db46d..476ff82 100644
--- a/bevy-jornet/src/leaderboards.rs
+++ b/bevy-jornet/src/leaderboards.rs
@@ -4,7 +4,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
 
 use bevy::{
     log::warn,
-    prelude::{ResMut, Resource},
+    prelude::{Event, EventWriter, ResMut, Resource},
     tasks::IoTaskPool,
 };
 use hmac::{Hmac, Mac};
@@ -14,6 +14,22 @@ use uuid::Uuid;
 
 use crate::http;
 
+/// Bevy Event that is sent when calls to Jornet finish.
+#[derive(Clone, Copy, PartialEq, Eq, Event, Debug)]
+pub enum JornetEvent {
+    /// A call to [`send_score`] succeeded.
+    SendScoreSuccess,
+    /// A Call to [`send_score`] failed.
+    SendScoreFailure,
+    /// A call to [`create_player`] succeeded.
+    CreatePlayerSuccess,
+    /// A call to [`create_player`] failed.
+    CreatePlayerFailure,
+    /// A call to [`refresh_leaderboard`] succeeded.
+    RefreshLeaderboardSuccess,
+    /// A call to [`refresh_leaderbord`] failed.
+    RefreshLeaderboardFailure,
+}
 /// Leaderboard resource, used to interact with Jornet leaderboard.
 #[derive(Resource)]
 pub struct Leaderboard {
@@ -21,6 +37,7 @@ pub struct Leaderboard {
     key: Uuid,
     leaderboard: Vec<Score>,
     updating: Arc<RwLock<Vec<Score>>>,
+    events: Arc<RwLock<Vec<JornetEvent>>>,
     host: String,
     new_player: Arc<RwLock<Option<Player>>>,
     player: Option<Player>,
@@ -34,6 +51,7 @@ impl Leaderboard {
             leaderboard: Default::default(),
             updating: Default::default(),
             host: host.unwrap_or_else(|| "https://jornet.vleue.com".to_string()),
+            events: Default::default(),
             new_player: Default::default(),
             player: Default::default(),
         }
@@ -54,18 +72,30 @@ impl Leaderboard {
     pub fn create_player(&mut self, name: Option<&str>) {
         let thread_pool = IoTaskPool::get();
         let host = self.host.clone();
+        let events = self.events.clone();
 
-        let player = PlayerInput {
+        let player_input = PlayerInput {
             name: name.map(|n| n.to_string()),
         };
         let complete_player = self.new_player.clone();
 
         thread_pool
             .spawn(async move {
-                if let Some(player) = http::post(&format!("{}/api/v1/players", host), player).await
+                if let Some(player) =
+                    http::post(&format!("{}/api/v1/players", host), player_input.clone()).await
                 {
+                    (*events)
+                        .write()
+                        .unwrap()
+                        .push(JornetEvent::CreatePlayerSuccess);
+
                     *complete_player.write().unwrap() = Some(player);
                 } else {
+                    (*events)
+                        .write()
+                        .unwrap()
+                        .push(JornetEvent::CreatePlayerFailure);
+
                     warn!("error creating a player");
                 }
             })
@@ -95,6 +125,7 @@ impl Leaderboard {
         let thread_pool = IoTaskPool::get();
         let leaderboard_id = self.id;
         let host = self.host.clone();
+        let events = self.events.clone();
 
         if let Some(player) = self.player.as_ref() {
             let score_to_send = ScoreInput::new(self.key, score, player, meta);
@@ -102,12 +133,22 @@ impl Leaderboard {
                 .spawn(async move {
                     if http::post::<_, ()>(
                         &format!("{}/api/v1/scores/{}", host, leaderboard_id),
-                        score_to_send,
+                        score_to_send.clone(),
                     )
                     .await
                     .is_none()
                     {
+                        (*events)
+                            .write()
+                            .unwrap()
+                            .push(JornetEvent::SendScoreFailure);
+
                         warn!("error sending the score");
+                    } else {
+                        (*events)
+                            .write()
+                            .unwrap()
+                            .push(JornetEvent::SendScoreSuccess);
                     }
                 })
                 .detach();
@@ -126,6 +167,7 @@ impl Leaderboard {
         let thread_pool = IoTaskPool::get();
         let leaderboard_id = self.id;
         let host = self.host.clone();
+        let events = self.events.clone();
 
         let leaderboard_to_update = self.updating.clone();
 
@@ -135,8 +177,18 @@ impl Leaderboard {
                     http::get(&format!("{}/api/v1/scores/{}", host, leaderboard_id)).await
                 {
                     *leaderboard_to_update.write().unwrap() = scores;
+
+                    (*events)
+                        .write()
+                        .unwrap()
+                        .push(JornetEvent::RefreshLeaderboardSuccess);
                 } else {
                     warn!("error getting the leaderboard");
+
+                    (*events)
+                        .write()
+                        .unwrap()
+                        .push(JornetEvent::RefreshLeaderboardFailure);
                 }
             })
             .detach();
@@ -208,7 +260,7 @@ pub struct Score {
     pub timestamp: String,
 }
 
-#[derive(Serialize)]
+#[derive(Serialize, Clone)]
 struct ScoreInput {
     pub score: f32,
     pub player: Uuid,
@@ -262,3 +314,20 @@ pub struct Player {
 struct PlayerInput {
     name: Option<String>,
 }
+
+/// System to send bevy events for results from any tasks.
+/// It is automatically added by the [`JornetPlugin`](crate::JornetPlugin) in stage
+/// [`CoreStage::Update`](bevy::prelude::CoreStage).
+pub fn send_events(leaderboard: ResMut<Leaderboard>, mut event_writer: EventWriter<JornetEvent>) {
+    if !leaderboard
+        .events
+        .try_read()
+        .map(|v| v.is_empty())
+        .unwrap_or(true)
+    {
+        let mut events = leaderboard.events.write().unwrap();
+        for event in events.drain(..) {
+            event_writer.send(event);
+        }
+    }
+}
diff --git a/bevy-jornet/src/lib.rs b/bevy-jornet/src/lib.rs
index 05fb691..dd50351 100644
--- a/bevy-jornet/src/lib.rs
+++ b/bevy-jornet/src/lib.rs
@@ -6,14 +6,15 @@
 //! - save high scores
 //! - get a leaderboard
 
-use bevy::prelude::{App, Plugin, Update};
+use bevy::prelude::{App, IntoSystemConfigs, Plugin, Update};
+use leaderboards::send_events;
 pub use leaderboards::Leaderboard;
 use uuid::Uuid;
 
 mod http;
 mod leaderboards;
 
-pub use leaderboards::{done_refreshing_leaderboard, Player, Score};
+pub use leaderboards::{done_refreshing_leaderboard, JornetEvent, Player, Score};
 
 /// Bevy Plugin handling communications with the Jornet server.
 pub struct JornetPlugin {
@@ -50,7 +51,9 @@ impl Plugin for JornetPlugin {
     fn build(&self, app: &mut App) {
         let leaderboard =
             Leaderboard::with_host_and_leaderboard(self.host.clone(), self.leaderboard, self.key);
-        app.insert_resource(leaderboard)
-            .add_systems(Update, done_refreshing_leaderboard);
+        app.add_event::<JornetEvent>()
+            .insert_resource(leaderboard)
+            .add_systems(Update, done_refreshing_leaderboard)
+            .add_systems(Update, send_events.after(done_refreshing_leaderboard));
     }
 }


Test patch: