Skip to main content

hvz/
interactions.rs

1use std::collections::HashMap;
2
3use chrono::{NaiveDateTime, Utc};
4use diesel::{
5    ExpressionMethods, QueryDsl, RunQueryDsl, Selectable,
6    backend::Backend,
7    deserialize::{self, FromSql, FromSqlRow},
8    expression::AsExpression,
9    mysql::Mysql,
10    prelude::{Insertable, Queryable},
11    serialize::{self, IsNull, Output, ToSql},
12    sql_types::Text,
13};
14use log::warn;
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    endpoints::interactions::{InteractionReturnType, LeaderBoardPlayers, ZombieTree},
19    establish_connection,
20    player::{Player, PlayerRole, PlayerState},
21    schema::interactions,
22};
23
24/// InteractionType represents the type of interaction between a player and a zombie.
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsExpression, FromSqlRow)]
26#[diesel(sql_type = Text)]
27pub enum InteractionType {
28    Hunt,
29    Feed,
30    Cure,
31    Corpsify,
32    Starve,
33    ReviveToHuman,
34    ReviveToZombie,
35    Other,
36}
37
38impl ToSql<Text, Mysql> for InteractionType {
39    /// Converts an InteractionType to a MySQL integer value.
40    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
41        match self {
42            InteractionType::Hunt => <str as ToSql<Text, Mysql>>::to_sql("Hunt", out)?,
43            InteractionType::Feed => <str as ToSql<Text, Mysql>>::to_sql("Feed", out)?,
44            InteractionType::Cure => <str as ToSql<Text, Mysql>>::to_sql("Cure", out)?,
45            InteractionType::Corpsify => <str as ToSql<Text, Mysql>>::to_sql("Corpsify", out)?,
46            InteractionType::Starve => <str as ToSql<Text, Mysql>>::to_sql("Starve", out)?,
47            InteractionType::ReviveToHuman => {
48                <str as ToSql<Text, Mysql>>::to_sql("ReviveToHuman", out)?
49            }
50            InteractionType::ReviveToZombie => {
51                <str as ToSql<Text, Mysql>>::to_sql("ReviveToZombie", out)?
52            }
53            InteractionType::Other => <str as ToSql<Text, Mysql>>::to_sql("Other", out)?,
54        };
55        Ok(IsNull::No)
56    }
57}
58
59impl FromSql<Text, Mysql> for InteractionType {
60    /// Converts a MySQL integer value to an InteractionType.
61    fn from_sql<'a>(string: <Mysql as Backend>::RawValue<'a>) -> deserialize::Result<Self> {
62        match str::from_utf8(string.as_bytes())? {
63            "Hunt" => Ok(InteractionType::Hunt),
64            "Feed" => Ok(InteractionType::Feed),
65            "Cure" => Ok(InteractionType::Cure),
66            "Corpsify" => Ok(InteractionType::Corpsify),
67            "Starve" => Ok(InteractionType::Starve),
68            "ReviveToHuman" => Ok(InteractionType::ReviveToHuman),
69            "ReviveToZombie" => Ok(InteractionType::ReviveToZombie),
70            "Other" => Ok(InteractionType::Other),
71            _ => Err(format!("Invalid InteractionType string value").into()),
72        }
73    }
74}
75
76/// Interaction between players.
77///
78/// The type of interaction determines the nature of the interaction. It can be a Hunt, Feed, Cure, Corpsify, or any other type of interaction.
79#[derive(Debug, Clone, Serialize, Deserialize, Selectable, Queryable, Insertable)]
80#[diesel(table_name = crate::schema::interactions)]
81#[diesel(check_for_backend(diesel::mysql::Mysql))]
82#[diesel(belongs_to(Player))]
83pub struct Interaction {
84    pub id: String,
85    pub interaction_type: InteractionType,
86    pub player: String,
87    pub target: Option<String>,
88    pub date: Option<NaiveDateTime>,
89}
90
91impl Interaction {
92    /// Creates a new interaction.
93    ///
94    /// # Arguments
95    ///
96    /// * `interaction_type` - The type of interaction.
97    /// * `player_id` - The ID of the player.
98    /// * `target_id` - The ID of the target player.
99    /// * `feed1_id` - The ID of the first feed.
100    /// * `feed2_id` - The ID of the second feed.
101    /// * `message` - The message of the interaction.
102    ///
103    /// # Returns
104    ///
105    /// A new interaction.
106    pub fn new(interaction_type: InteractionType, player: String, target: Option<String>) -> Self {
107        Interaction {
108            id: uuid::Uuid::new_v4().to_string(),
109            interaction_type,
110            player,
111            target,
112            date: Some(Utc::now().naive_local()),
113        }
114    }
115
116    /// Save a hunt interaction.
117    ///
118    /// This function saves a hunt interaction between a player and a target.
119    /// It also saves feed interactions between the player and the target's feeders.
120    ///
121    /// # Arguments
122    ///
123    /// * `player` - The player who initiated the hunt.
124    /// * `target` - The player who was hunted.
125    /// * `feed1` - The first feeder of the target.
126    /// * `feed2` - The second feeder of the target.
127    ///
128    /// # Returns
129    ///
130    /// A `Result` indicating success or failure.
131    pub async fn save_hunt_interaction(
132        player: &Player,
133        target: &Player,
134        feed1: Option<&Player>,
135        feed2: Option<&Player>,
136    ) -> Result<(), String> {
137        let connection = &mut establish_connection();
138        let hunt = Interaction::new(
139            InteractionType::Hunt,
140            player.id.clone(),
141            Some(target.id.clone()),
142        );
143
144        diesel::insert_into(interactions::table)
145            .values(&hunt)
146            .execute(connection)
147            .map_err(|e| format!("Failed to save hunt interaction: {}", e))?;
148
149        if let Some(feed1) = feed1 {
150            let feed1_interaction = Interaction::new(
151                InteractionType::Feed,
152                player.id.clone(),
153                Some(feed1.id.clone()),
154            );
155
156            diesel::insert_into(interactions::table)
157                .values(&feed1_interaction)
158                .execute(connection)
159                .map_err(|e| format!("Failed to save feed interaction: {}", e))?;
160        }
161
162        if let Some(feed2) = feed2 {
163            let feed2_interaction = Interaction::new(
164                InteractionType::Feed,
165                player.id.clone(),
166                Some(feed2.id.clone()),
167            );
168
169            diesel::insert_into(interactions::table)
170                .values(&feed2_interaction)
171                .execute(connection)
172                .map_err(|e| format!("Failed to save feed interaction: {}", e))?;
173        }
174
175        Ok(())
176    }
177
178    /// This function is used to get the newest interactions:
179    ///
180    /// # Returns
181    /// A vector of interactions sorted by ID in descending order.
182    pub async fn get_newest_interactions() -> Result<Vec<Interaction>, String> {
183        let connection = &mut establish_connection();
184
185        let result = interactions::table
186            .order(interactions::id.desc())
187            .limit(1)
188            .load::<Interaction>(connection);
189
190        match result {
191            Ok(interactions) => Ok(interactions),
192            Err(err) => Err(err.to_string()),
193        }
194    }
195
196    /// This function is used to get all interactions
197    ///
198    /// Z0 is returned as anonymous
199    ///
200    /// # Returns
201    /// A vector of interactions sorted by ID in descending order.
202    pub async fn get_all_interactions() -> Result<Vec<InteractionReturnType>, String> {
203        let connection = &mut establish_connection();
204
205        let result = interactions::table
206            .load::<Interaction>(connection)
207            .map_err(|e| e.to_string())
208            .expect("Failed to load interactions");
209
210        let mut interactions = Vec::new();
211        for interaction in result {
212            match Player::get_player(interaction.player.clone()).await {
213                Ok(Some(player)) => {
214                    let player_alias = if player.state == PlayerState::Zombie0 {
215                        String::from("Z0")
216                    } else {
217                        player.alias
218                    };
219                    let target_alias = if let Some(target_id) = interaction.target {
220                        match Player::get_player(target_id.clone()).await {
221                            Ok(Some(target)) => Some(target.alias),
222                            Ok(None) => {
223                                warn!("Target not found: {}", target_id);
224                                None
225                            }
226                            Err(e) => return Err(e),
227                        }
228                    } else {
229                        None
230                    };
231                    interactions.push(InteractionReturnType {
232                        player_alias,
233                        target_alias,
234                        interaction_type: interaction.interaction_type,
235                        date: interaction.date,
236                    });
237                }
238                Ok(None) => warn!("Player not found: {}", interaction.player),
239                Err(e) => return Err(e),
240            }
241        }
242
243        Ok(interactions)
244    }
245
246    /// Get all interactions related to hunting.
247    ///
248    /// Used in leaderboards and zombie tree
249    ///
250    /// # Returns
251    /// A vector of hunt interactions
252    pub async fn get_hunt_interactions() -> Result<Vec<Interaction>, String> {
253        let connection = &mut establish_connection();
254
255        let result = interactions::table
256            .filter(interactions::interaction_type.eq(InteractionType::Hunt))
257            .load::<Interaction>(connection)
258            .map_err(|e| e.to_string());
259
260        result
261    }
262
263    /// Get leaderboard data
264    ///
265    /// This represents the zombie list and their kills, ordered by the number of kills, from most to least.
266    ///
267    /// Z0 is returned as anonymous
268    ///
269    /// # Returns
270    /// A vector of LeaderBoardPlayers, sorted by the number of kills in descending order.
271    pub async fn get_leaderboard(is_zombie: bool) -> Result<Vec<LeaderBoardPlayers>, String> {
272        let hunt_interactions = match Interaction::get_hunt_interactions().await {
273            Ok(vec) => vec,
274            Err(e) => return Err(e),
275        };
276
277        let mut kill_list: HashMap<String, Vec<String>> = HashMap::new();
278        for interaction in &hunt_interactions {
279            if let Some(user_id) = interaction.target.clone() {
280                if let Ok(Some(target)) = Player::get_player(user_id.clone()).await {
281                    kill_list
282                        .entry(interaction.player.clone())
283                        .or_insert_with(Vec::new)
284                        .push(target.alias.clone());
285                }
286            }
287        }
288
289        let mut leaderboard: Vec<LeaderBoardPlayers> = Vec::new();
290        for (user_id, kills) in kill_list.iter() {
291            match Player::get_player(user_id.clone()).await {
292                Ok(Some(player)) => {
293                    match player.state {
294                        PlayerState::Zombie0 => {
295                            if let Some(existing) = leaderboard
296                                .iter_mut()
297                                .find(|p| p.state == PlayerState::Zombie0)
298                            {
299                                existing.kills.extend(kills.clone());
300                            } else {
301                                leaderboard.push(LeaderBoardPlayers {
302                                    alias: String::from("Z0"),
303                                    kills: kills.clone(),
304                                    next_feeding: None,
305                                    state: player.state,
306                                    role: player.role,
307                                });
308                            }
309                        }
310                        _ => leaderboard.push(LeaderBoardPlayers {
311                            alias: player.alias,
312                            kills: kills.clone(),
313                            next_feeding: if is_zombie { player.next_feeding } else { None },
314                            state: player.state,
315                            role: player.role,
316                        }),
317                    };
318                }
319                Ok(None) => warn!("Player not found: {}", user_id),
320                Err(e) => return Err(e),
321            }
322        }
323
324        leaderboard.sort_by(|a, b| b.kills.len().cmp(&a.kills.len()));
325
326        Ok(leaderboard)
327    }
328
329    /// Get the zombie tree
330    ///
331    /// Z0 is returned as anonymous
332    ///
333    /// # Returns
334    /// A vector of ZombieTree objects representing the zombie tree.
335    ///
336    /// # Errors
337    /// Returns an error string if there was an error retrieving the zombie tree.
338    pub async fn get_zombie_tree()
339    -> Result<(Vec<ZombieTree>, HashMap<String, (PlayerState, PlayerRole)>), String> {
340        let hunt_interactions = match Interaction::get_hunt_interactions().await {
341            Ok(vec) => vec,
342            Err(e) => return Err(e),
343        };
344
345        let mut kill_list: HashMap<String, Vec<(NaiveDateTime, String)>> = HashMap::new();
346        for interaction in &hunt_interactions {
347            if let Some(user_id) = interaction.target.clone() {
348                kill_list
349                    .entry(interaction.player.clone())
350                    .or_insert_with(Vec::new)
351                    .push((interaction.date.unwrap_or_default(), user_id.clone()));
352            }
353        }
354
355        let mut zombie_tree = Vec::<ZombieTree>::new();
356        let mut simple_players: HashMap<String, (PlayerState, PlayerRole)> = HashMap::new();
357        for (user_id, kill_vec) in kill_list.iter() {
358            if let Ok(Some(hunter)) = Player::get_player(user_id.clone()).await {
359                let alias = match hunter.state.clone() {
360                    PlayerState::Zombie0 => String::from("Z0"),
361                    _ => hunter.alias,
362                };
363                for (date, kill) in kill_vec.iter() {
364                    if let Ok(Some(target)) = Player::get_player(kill.clone()).await {
365                        zombie_tree.push(ZombieTree {
366                            hunter_alias: alias.clone(),
367                            target_alias: target.alias.clone(),
368                            date: *date,
369                        });
370                        simple_players
371                            .entry(alias.clone())
372                            .or_insert((hunter.state.clone(), hunter.role.clone()));
373                        simple_players
374                            .entry(target.alias.clone())
375                            .or_insert((target.state.clone(), target.role.clone()));
376                    };
377                }
378            }
379        }
380
381        Ok((zombie_tree, simple_players))
382    }
383
384    /// This function is used to delete an interaction:
385    ///
386    /// # Arguments
387    /// * `id` - The ID of the interaction to delete.
388    ///
389    /// # Returns
390    /// A result indicating success or failure.
391    pub async fn delete(id: String) -> Result<(), String> {
392        let connection = &mut establish_connection();
393
394        let result =
395            diesel::delete(interactions::table.filter(interactions::id.eq(id))).execute(connection);
396
397        match result {
398            Ok(_) => Ok(()),
399            Err(err) => Err(err.to_string()),
400        }
401    }
402}