achievement.rs
1 use std::cmp::Ordering; 2 use std::io::ErrorKind; 3 use anyhow::{anyhow, Result}; 4 use chrono::NaiveDateTime; 5 use serde::{Deserialize, Serialize}; 6 use serde_json::{Map, Value}; 7 use crate::constants::{Format_ChronoDateTime, TheString}; 8 use crate::retroachievements::platform::AchievementMetadata; 9 use super::makeRelative; 10 use super::mode::RetroAchievementsMode; 11 12 #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Ord, Serialize)] 13 pub struct Achievement 14 { 15 /// Number of users who have unlocked the achievement in Casual mode. 16 #[serde(default)] 17 pub awardedCasual: u64, 18 19 /// Number of users who have unlocked the achievement in Hardcore mode. 20 #[serde(default)] 21 pub awardedHardcore: u64, 22 23 /// Description of the achievement. 24 #[serde(default)] 25 pub description: String, 26 27 /// Value denoting RetroAchievements' ordering of the achievement. 28 #[serde(default)] 29 pub displayOrder: u64, 30 31 /// The timestamp when the user unlocked the achievement in Hardcore mode. 32 #[serde(default)] 33 pub earnedTimestampHardcore: Option<String>, 34 35 /// The timestamp when the user unlocked the achievement in Casual mode. 36 #[serde(default)] 37 pub earnedTimestampCasual: Option<String>, 38 39 /// Unique ID of the achievement. 40 pub id: u64, 41 42 /// Path to the icon image file. 43 #[serde(default)] 44 pub icon: String, 45 46 /// Title of the achievement. 47 #[serde(default)] 48 pub name: String, 49 50 /// The amount of points gained when unlocking the achievement. 51 #[serde(default)] 52 pub points: u64, 53 } 54 55 impl From<AchievementMetadata> for Achievement 56 { 57 fn from(value: AchievementMetadata) -> Self 58 { 59 let mut instance = Self::default(); 60 instance.update(&value); 61 return instance; 62 } 63 } 64 65 impl PartialOrd for Achievement 66 { 67 fn partial_cmp(&self, other: &Self) -> Option<Ordering> 68 { 69 let unlocked = self.unlocked(RetroAchievementsMode::Casual) || self.unlocked(RetroAchievementsMode::Hardcore); 70 let otherUnlocked = other.unlocked(RetroAchievementsMode::Casual) || other.unlocked(RetroAchievementsMode::Hardcore); 71 72 return match unlocked.partial_cmp(&otherUnlocked) 73 { 74 Some(c) => match c 75 { 76 Ordering::Greater => Some(Ordering::Less), 77 Ordering::Less => Some(Ordering::Greater), 78 79 Ordering::Equal => match self.sortName().partial_cmp(&other.sortName()) 80 { 81 None => self.id.partial_cmp(&other.id), 82 Some(c) => match c 83 { 84 Ordering::Equal => self.id.partial_cmp(&other.id), 85 _ => Some(c), 86 }, 87 }, 88 }, 89 90 None => match self.sortName().partial_cmp(&other.sortName()) 91 { 92 None => self.id.partial_cmp(&other.id), 93 Some(c) => match c 94 { 95 Ordering::Equal => self.id.partial_cmp(&other.id), 96 _ => Some(c), 97 }, 98 }, 99 }; 100 } 101 } 102 103 impl Achievement 104 { 105 pub fn formatEarnedTimestamp(&self, mode: RetroAchievementsMode) -> Result<String> 106 { 107 if let Some(timestamp) = match mode { 108 RetroAchievementsMode::Casual => &self.earnedTimestampCasual, 109 RetroAchievementsMode::Hardcore => &self.earnedTimestampHardcore, 110 } 111 { 112 let dt = self.parseTimestamp(timestamp)?; 113 return Ok(dt.format(Format_ChronoDateTime).to_string()); 114 } 115 116 return Err(anyhow!(ErrorKind::NotFound)); 117 } 118 119 pub fn parseJsonMap(map: &Map<String, Value>) -> Option<Self> 120 { 121 let mut achievement = Self::default(); 122 123 if let Some((_, value)) = map.iter() 124 .find(|(key, _)| key.as_str() == "awardedCasual") 125 { 126 if let Value::Number(inner) = value 127 { 128 if let Some(number) = inner.as_u64() 129 { 130 achievement.awardedCasual = number; 131 } 132 } 133 } 134 135 if let Some((_, value)) = map.iter() 136 .find(|(key, _)| key.as_str() == "awardedHardcore") 137 { 138 if let Value::Number(inner) = value 139 { 140 if let Some(number) = inner.as_u64() 141 { 142 achievement.awardedHardcore = number; 143 } 144 } 145 } 146 147 if let Some((_, value)) = map.iter() 148 .find(|(key, _)| key.as_str() == "description") 149 { 150 if let Value::String(inner) = value 151 { 152 achievement.description = inner.clone(); 153 } 154 } 155 156 if let Some((_, value)) = map.iter() 157 .find(|(key, _)| key.as_str() == "displayOrder") 158 { 159 if let Value::Number(inner) = value 160 { 161 if let Some(number) = inner.as_u64() 162 { 163 achievement.displayOrder = number; 164 } 165 } 166 } 167 168 if let Some((_, value)) = map.iter() 169 .find(|(key, _)| key.as_str() == "earnedTimestampCasual") 170 { 171 if let Value::String(inner) = value 172 { 173 if !inner.is_empty() 174 { 175 achievement.earnedTimestampCasual = Some(inner.clone()); 176 } 177 } 178 } 179 180 if let Some((_, value)) = map.iter() 181 .find(|(key, _)| key.as_str() == "earnedTimestampHardcore") 182 { 183 if let Value::String(inner) = value 184 { 185 if !inner.is_empty() 186 { 187 achievement.earnedTimestampHardcore = Some(inner.clone()); 188 } 189 } 190 } 191 192 if let Some((_, value)) = map.iter() 193 .find(|(key, _)| key.as_str() == "id") 194 { 195 if let Value::Number(inner) = value 196 { 197 if let Some(number) = inner.as_u64() 198 { 199 achievement.id = number; 200 } 201 } 202 } 203 204 if let Some((_, value)) = map.iter() 205 .find(|(key, _)| key.as_str() == "icon") 206 { 207 if let Value::String(inner) = value 208 { 209 achievement.icon = inner.clone(); 210 } 211 } 212 213 if let Some((_, value)) = map.iter() 214 .find(|(key, _)| key.as_str() == "name") 215 { 216 if let Value::String(inner) = value 217 { 218 achievement.name = inner.clone(); 219 } 220 } 221 222 if let Some((_, value)) = map.iter() 223 .find(|(key, _)| key.as_str() == "points") 224 { 225 if let Value::Number(inner) = value 226 { 227 if let Some(number) = inner.as_u64() 228 { 229 achievement.points = number; 230 } 231 } 232 } 233 234 return match achievement.id 235 { 236 0 => None, 237 _ => Some(achievement), 238 }; 239 } 240 241 fn parseTimestamp(&self, value: &String) -> Result<NaiveDateTime> 242 { 243 return Ok(NaiveDateTime::parse_from_str( 244 value.as_str(), 245 "%Y-%m-%d %H:%M:%S" 246 )?); 247 } 248 249 pub fn sortName(&self) -> String 250 { 251 return match self.name.starts_with(TheString) 252 { 253 true => { 254 let mut the = self.name.clone(); 255 let name = the.split_off(TheString.len()); 256 format!("{}, {}", name, the.trim()) 257 }, 258 259 false => self.name.clone(), 260 }; 261 } 262 263 pub fn unlocked(&self, mode: RetroAchievementsMode) -> bool 264 { 265 return match mode 266 { 267 RetroAchievementsMode::Casual => self.earnedTimestampCasual.is_some(), 268 RetroAchievementsMode::Hardcore => self.earnedTimestampHardcore.is_some(), 269 }; 270 } 271 272 pub fn unlockedPercent(&self, mode: RetroAchievementsMode, distinctPlayers: u64) -> f64 273 { 274 return match distinctPlayers > 0 275 { 276 false => 0 as f64, 277 true => (match mode 278 { 279 RetroAchievementsMode::Casual => self.awardedCasual, 280 RetroAchievementsMode::Hardcore => self.awardedHardcore, 281 } as f64 282 / distinctPlayers as f64) 283 * 100.0, 284 }; 285 } 286 287 pub fn update(&mut self, achievement: &AchievementMetadata) 288 { 289 self.awardedCasual = achievement.NumAwarded; 290 self.awardedHardcore = achievement.NumAwardedHardcore; 291 self.description = achievement.Description.clone(); 292 self.displayOrder = achievement.DisplayOrder; 293 self.earnedTimestampHardcore = achievement.DateEarnedHardcore.clone(); 294 self.earnedTimestampCasual = achievement.DateEarned.clone(); 295 self.icon = makeRelative(&achievement.BadgeName); 296 self.id = achievement.ID; 297 self.name = achievement.Title.clone(); 298 self.points = achievement.Points; 299 } 300 } 301 302 #[cfg(test)] 303 mod tests 304 { 305 use super::*; 306 307 fn buildMap(successful: bool) -> Map<String, Value> 308 { 309 let mut map = Map::new(); 310 311 map.insert("awardedCasual".into(), 25.into()); 312 map.insert("awardedHardcore".into(), 5.into()); 313 map.insert("description".into(), "The description".into()); 314 map.insert("displayOrder".into(), 1.into()); 315 map.insert("earnedTimestampCasual".into(), "The timestamp".into()); 316 map.insert("earnedTimestampHardcore".into(), Value::Null); 317 318 if successful 319 { 320 map.insert("id".into(), 2.into()); 321 } 322 323 map.insert("icon".into(), "The icon".into()); 324 map.insert("name".into(), "The name".into()); 325 map.insert("points".into(), 15.into()); 326 327 return map; 328 } 329 330 #[test] 331 fn parseJsonMap() 332 { 333 let mut map = buildMap(false); 334 let fail = Achievement::parseJsonMap(&map); 335 assert_eq!(fail, None); 336 337 map = buildMap(true); 338 let success = Achievement::parseJsonMap(&map); 339 assert_ne!(success, None); 340 341 let achievement = success.unwrap(); 342 assert_eq!(achievement.awardedCasual, 25); 343 assert_eq!(achievement.awardedHardcore, 5); 344 assert_eq!(achievement.description, "The description".to_string()); 345 assert_eq!(achievement.displayOrder, 1); 346 assert_eq!(achievement.earnedTimestampCasual, Some("The timestamp".to_string())); 347 assert_eq!(achievement.earnedTimestampHardcore, None); 348 assert_eq!(achievement.icon, "The icon".to_string()); 349 assert_eq!(achievement.id, 2); 350 assert_eq!(achievement.name, "The name".to_string()); 351 assert_eq!(achievement.points, 15); 352 } 353 }