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 }