/ src / rpcs3 / platform / api.rs
api.rs
  1  use std::fs::{File, copy, read, read_dir, read_to_string};
  2  use std::io::{self, Cursor};
  3  use std::path::Path;
  4  use anyhow::Result;
  5  use saphyr::{LoadableYamlNode, Scalar, Yaml};
  6  use tracing::warn;
  7  use crate::join;
  8  use crate::io::{Path_Games, generateImageCacheDir, getImagePath};
  9  use crate::net::limiter::request::FileLocation;
 10  use crate::rpcs3::data::game::Game;
 11  use crate::rpcs3::data::settings::Rpcs3Settings;
 12  use crate::rpcs3::platform::data::conf::{TrophyConf, TrophyMetadata};
 13  use crate::rpcs3::platform::data::user::{DatFile, EntryType6};
 14  
 15  const DefaultAccountId: u64 = 1;
 16  
 17  pub struct Rpcs3Api
 18  {
 19  	pub accountId: u64,
 20  	pub rootDir: String,
 21  }
 22  
 23  impl Default for Rpcs3Api
 24  {
 25  	fn default() -> Self
 26  	{
 27  		return Self
 28  		{
 29  			accountId: DefaultAccountId,
 30  			rootDir: Default::default(),
 31  		};
 32  	}
 33  }
 34  
 35  impl From<Rpcs3Settings> for Rpcs3Api
 36  {
 37  	fn from(value: Rpcs3Settings) -> Self
 38  	{
 39  		return Self
 40  		{
 41  			accountId: value.accountId,
 42  			rootDir: value.appDataDirectory.clone(),
 43  		};
 44  	}
 45  }
 46  
 47  impl Rpcs3Api
 48  {
 49  	/**
 50  	The ticks value representing 1970-01-01 00:00:00.000000
 51  	
 52  	Used to convert timestamps stored in TROPUSR.DAT into a Unix-compatible
 53  	timestamp, in microseconds.
 54  	*/
 55  	const TicksMagicOffset: u64 = 62135596800000000;
 56  	
 57  	pub const Platform: &str = "RPCS3";
 58  	
 59  	pub const GameIconFileName: &str = "ICON0.PNG";
 60  	pub const TrophyIconPrefix: &str = "TROP";
 61  	
 62  	const ConfFileName: &str = "TROPCONF.SFM";
 63  	const DatFileName: &str = "TROPUSR.DAT";
 64  	const RelativeConfigDir: &str = "config/rpcs3/";
 65  	const RelativeHomeDir: &str = "dev_hdd0/home";
 66  	const RelativeUserTrophyDir: &str = "trophy";
 67  	const RpcnFileName: &str = "rpcn.yml";
 68  	const RpcnIdYamlKey: &str = "NPID";
 69  	
 70  	pub fn cacheGameIcons(&self, npCommId: &String) -> Result<()>
 71  	{
 72  		let group = join!(Path_Games, npCommId);
 73  		let platform = Self::Platform.into();
 74  		
 75  		generateImageCacheDir(&platform, &group)?;
 76  		
 77  		let gamePath = Path::new(&self.rootDir)
 78  			.join(Self::RelativeConfigDir)
 79  			.join(Self::RelativeHomeDir)
 80  			.join(self.formatAccountId())
 81  			.join(Self::RelativeUserTrophyDir)
 82  			.join(npCommId);
 83  		
 84  		let paths = read_dir(gamePath)?;
 85  		
 86  		for path in paths
 87  		{
 88  			let entry = path?;
 89  			
 90  			if let Some(fullPath) = entry.path().to_str()
 91  			{
 92  				if fullPath.ends_with(".PNG")
 93  				{
 94  					if let Ok(fileName) = entry.file_name().into_string()
 95  					{
 96  						if let Some(imagePath) = getImagePath(&FileLocation
 97  						{
 98  							fileName,
 99  							group: group.clone(),
100  							platform: platform.clone(),
101  						})
102  						{
103  							if !Path::new(&imagePath).exists()
104  							{
105  								copy(fullPath, imagePath)?;
106  							}
107  						}
108  					}
109  				}
110  			}
111  		}
112  		
113  		return Ok(());
114  	}
115  	
116  	pub fn generateGameList(&self) -> Result<Vec<Game>>
117  	{
118  		let mut games: Vec<Game> = vec![];
119  		
120  		match self.getNpCommIdList()
121  		{
122  			Err(e) => warn!("[RPCS3] Error reading the NpCommId list (RPCS3): {:?}", e),
123  			Ok(npCommIds) => {
124  				for npCommId in npCommIds
125  				{
126  					match self.parseTrophyConf(npCommId.clone())
127  					{
128  						Err(e) => warn!("[RPCS3] Error parsing the TROPHYCONF.SFM for {}: {:?}", npCommId, e),
129  						Ok(trophyConf) => games.push(trophyConf.into()),
130  					}
131  					
132  					match self.parseTrophies(npCommId.clone())
133  					{
134  						Err(e) => warn!("[RPCS3] Error parsing the trophies for {}: {:?}", npCommId, e),
135  						Ok(trophies) => {
136  							for (metadata, type6) in trophies
137  							{
138  								if let Some(game) = games.iter_mut()
139  									.find(|g| g.npCommId == npCommId)
140  								{
141  									if let Some(trophy) = game.trophies.iter_mut()
142  										.find(|t| t.id == metadata.id as u64)
143  									{
144  										trophy.unlocked = type6.trophyState > 0;
145  										if trophy.unlocked
146  										{
147  											trophy.unlockedTimestamp = Some(type6.timestamp2 - Self::TicksMagicOffset);
148  										}
149  									}
150  								}
151  							}
152  						}
153  					}
154  				}
155  			}
156  		}
157  		
158  		return Ok(games);
159  	}
160  	
161  	pub fn getNpCommIdList(&self) -> Result<Vec<String>>
162  	{
163  		let trophiesPath = Path::new(&self.rootDir)
164  			.join(Self::RelativeConfigDir)
165  			.join(Self::RelativeHomeDir)
166  			.join(self.formatAccountId())
167  			.join(Self::RelativeUserTrophyDir);
168  		
169  		let paths = read_dir(trophiesPath)?;
170  		
171  		let mut npCommIds = vec![];
172  		
173  		for path in paths
174  		{
175  			match path?.file_name().into_string()
176  			{
177  				Err(e) => println!("Failed to into_string() this path: {:?}", e),
178  				Ok(p) => npCommIds.push(p),
179  			}
180  		}
181  		
182  		return Ok(npCommIds);
183  	}
184  	
185  	pub fn parseDatFile(&self, npCommId: String) -> Result<DatFile>
186  	{
187  		let datPath = Path::new(&self.rootDir)
188  			.join(Self::RelativeConfigDir)
189  			.join(Self::RelativeHomeDir)
190  			.join(self.formatAccountId())
191  			.join(Self::RelativeUserTrophyDir)
192  			.join(npCommId)
193  			.join(Self::DatFileName);
194  		
195  		let buffer = read(&datPath)?;
196  		let mut cursor = Cursor::new(buffer);
197  		let datFile = DatFile::readFromCursor(&mut cursor)?;
198  		
199  		return Ok(datFile);
200  	}
201  	
202  	pub fn getRpcnId(&self) -> Result<String>
203  	{
204  		let rpcnPath = Path::new(&self.rootDir)
205  			.join(Self::RelativeConfigDir)
206  			.join(Self::RpcnFileName);
207  		
208  		let file = File::open(rpcnPath)?;
209  		let data = io::read_to_string(file)?;
210  		let yaml = Yaml::load_from_str(&data.as_str())?;
211  		
212  		let rpcnId = match yaml.iter().find(|y| y.is_mapping())
213  		{
214  			Some(Yaml::Mapping(map)) => match map.get(&Yaml::Value(Scalar::String(Self::RpcnIdYamlKey.into())))
215  			{
216  				Some(Yaml::Value(Scalar::String(id))) => id.to_string(),
217  				_ => String::default(),
218  			},
219  			_ => String::default(),
220  		};
221  		
222  		return Ok(rpcnId);
223  	}
224  	
225  	pub fn parseTrophies(&self, npCommId: String) -> Result<Vec<(TrophyMetadata, EntryType6)>>
226  	{
227  		let datFile = self.parseDatFile(npCommId.clone())?;
228  		let trophyConf = self.parseTrophyConf(npCommId.clone())?;
229  		
230  		let mut trophies = vec![];
231  		
232  		for metadata in trophyConf.trophies
233  		{
234  			if let Some(entry) = datFile.type6.iter()
235  				.find(|entry| entry.trophyId == metadata.id)
236  			{
237  				trophies.push((metadata.clone(), entry.clone()));
238  			}
239  		}
240  		
241  		return Ok(trophies);
242  	}
243  	
244  	pub fn parseTrophyConf(&self, npCommId: String) -> Result<TrophyConf>
245  	{
246  		let confPath = Path::new(&self.rootDir)
247  			.join(Self::RelativeConfigDir)
248  			.join(Self::RelativeHomeDir)
249  			.join(self.formatAccountId())
250  			.join(Self::RelativeUserTrophyDir)
251  			.join(npCommId)
252  			.join(Self::ConfFileName);
253  		
254  		let xml = read_to_string(confPath)?;
255  		let trophyConf = serde_xml_rs::from_str::<TrophyConf>(&xml)?;
256  		return Ok(trophyConf);
257  	}
258  	
259  	fn formatAccountId(&self) -> String
260  	{
261  		return format!("{:08}", self.accountId);
262  	}
263  }
264  
265  #[cfg(test)]
266  mod tests
267  {
268  	use std::env;
269  	use crate::rpcs3::data::trophy::TrophyGrade;
270  	use super::*;
271  	
272  	#[test]
273  	fn accountId()
274  	{
275  		let api = Rpcs3Api { accountId: 1, ..Default::default() };
276  		let accountId = api.formatAccountId();
277  		
278  		assert_eq!(accountId, "00000001");
279  	}
280  	
281  	/**
282  	Requires the following environment variable to be set in order to run successfully.
283  	
284  	- `RPCS3_TEST_ROOT`: The absolute path to the RPCS3 app data directory.
285  	- `RPCS3_TEST_RPCN_ID`: The expected RPCN ID. Used to verify the parsed value.
286  	*/
287  	#[ignore]
288  	#[test]
289  	fn rpcnId()
290  	{
291  		let rootDir = env::var("RPCS3_TEST_ROOT").unwrap();
292  		let expected = env::var("RPCS3_TEST_RPCN_ID").unwrap();
293  		
294  		let api = Rpcs3Api { rootDir, ..Default::default() };
295  		let rpcnId = api.getRpcnId();
296  		assert!(rpcnId.is_ok());
297  		assert_eq!(rpcnId.unwrap(), expected);
298  	}
299  	
300  	/**
301  	Requires several environment variables to be set in order to run successfully.
302  	
303  	- `RPCS3_TEST_ROOT`: The absolute path to the RPCS3 app data directory.
304  	- `RPCS3_TEST_ACCOUNTID`: An integer value matching the relevant RPCS3 account id.
305  	- `RPCS3_TEST_NPCOMMID`: An NpCommId representing the game whose trophy data should be used in this test.
306  	*/
307  	#[ignore]
308  	#[test]
309  	fn trophyList()
310  	{
311  		let rootDir = env::var("RPCS3_TEST_ROOT").unwrap();
312  		let accountIdString = env::var("RPCS3_TEST_ACCOUNTID").unwrap();
313  		let accountId = accountIdString.parse::<u64>().unwrap();
314  		let npCommId = env::var("RPCS3_TEST_NPCOMMID").unwrap();
315  		
316  		let api = Rpcs3Api { rootDir, accountId, };
317  		let trophies = api.parseTrophies(npCommId.clone()).unwrap();
318  		
319  		assert_ne!(trophies.len(), 0);
320  		if let Some((metadata, type6)) = trophies.first()
321  		{
322  			let grade: TrophyGrade = metadata.ttype.clone().into();
323  			assert_eq!(metadata.id, 0);
324  			assert_ne!(grade, TrophyGrade::Unknown);
325  			assert!(!metadata.detail.is_empty());
326  			assert_ne!(metadata.hidden, TrophyMetadata::HiddenTrue);
327  			assert!(!metadata.name.is_empty());
328  			assert_eq!(type6.timestamp1, 0);
329  			assert_eq!(type6.timestamp2, 0);
330  			
331  			match grade == TrophyGrade::Platinum
332  			{
333  				false => assert_ne!(metadata.pid, -1),
334  				true => assert_eq!(metadata.pid, -1),
335  			}
336  		}
337  	}
338  }