/ src / retroachievements / data / achievement.rs
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  }