1use std::{collections::HashMap, io::Read};
2
3use reqwest;
4use serde_derive::Serialize;
5use serde_derive::Deserialize;
6
7
8
9#[derive(Debug)]
10#[derive(Deserialize, Serialize)]
24pub struct DSAPI {
25 game_list: GameList,
26 class_member_map: HashMap<String, OffsetInfo>,
27 class_size_map: HashMap<String, i32>,
28 function_offset_map: HashMap<String, u64>,
29 enum_name_map: HashMap<String, String>,
30 offset_map: HashMap<String, u64>,
31 game_id: String,
32 downloaded_at: u64,
33 cache_path: Option<std::path::PathBuf>,
34
35 pub engine: String,
36 pub location: String,
37
38}
39
40impl DSAPI {
41 pub fn new(game_id: &str, cache_path:Option<std::path::PathBuf>) -> Self {
48 let mut ret = DSAPI {
49 game_list: GameList::init().expect("Failed to initialize game list"),
50 class_member_map: HashMap::new(),
51 class_size_map: HashMap::new(),
52 function_offset_map: HashMap::new(),
53 enum_name_map: HashMap::new(),
54 offset_map: HashMap::new(),
55 cache_path,
56 game_id: game_id.to_string(),
57 downloaded_at: 0, engine: String::new(),
59 location: String::new(),
60 };
61 ret.engine = ret.game_list.get_game_by_hash(game_id)
62 .expect("Game not found")
63 .engine
64 .clone();
65 ret.location = ret.game_list.get_game_by_hash(game_id)
66 .expect("Game not found")
67 .location
68 .clone();
69 ret
70 }
71
72 pub fn cache_self(&self) -> Result<(), String> {
73 if let Some(cache_path) = &self.cache_path {
74 if !cache_path.exists() {
75 std::fs::create_dir_all(cache_path).map_err(|e| format!("Failed to create cache directory: {}", e))?;
76 }
77 let cache_file = cache_path.join("dsapi_cache.json");
78 let serialized = serde_json::to_string(self).map_err(|e| format!("Failed to serialize DSAPI: {}", e))?;
79 std::fs::write(cache_file, serialized).map_err(|e| format!("Failed to write cache file: {}", e))?;
80 }
81 Ok(())
82 }
83 pub fn restore_from_cache(&self) -> Result<Self, String> {
84 if self.cache_path.is_some() && self.cache_path.as_ref().unwrap().exists() {
85 let cache_file = self.cache_path.as_ref().unwrap().join("dsapi_cache.json");
86 if cache_file.exists() {
87 let serialized = std::fs::read_to_string(cache_file).map_err(|e| format!("Failed to read cache file: {}", e))?;
88 serde_json::from_str(&serialized).map_err(|e| format!("Failed to deserialize DSAPI from cache: {}", e))
89 } else {
90 Err("Cache file does not exist".to_string())
91 }
92 } else {
93 Err("Cache path does not exist".to_string())
94 }
95 }
96 pub fn download_content(&mut self) -> Result<(), String> {
100 if self.cache_path.is_some() {
101 if self.cache_path.as_ref().unwrap().exists() {
102 let restored_cache = self.restore_from_cache()
103 .map_err(|e| format!("Failed to restore from cache: {}", e))?;
104 if self.game_list.get_game_by_hash(&self.game_id).unwrap().uploaded <= restored_cache.downloaded_at {
105 *self = restored_cache;
107 return Ok(());
108 }
109 }
110 }
111
112 fn parse_class_info(classes_info: &BlobInfo, dsapi: &mut DSAPI) {
113 for class in &classes_info.data {
114
115 for (key, value) in class {
116 let class_name = key;
117 let value: Vec<HashMap<String, serde_json::Value>> = serde_json::from_str(&value.to_string()).unwrap();
118 for value in value {
119 let key = value.keys().next().unwrap().as_str();
120 assert!(value.keys().len() == 1);
121 if key == "__MDKClassSize" {
122 dsapi.class_size_map.insert(class_name.clone(), value.get("__MDKClassSize").unwrap().as_i64().unwrap() as i32);
123 continue;
124 }
125 if key == "__InheritInfo" {
126 continue;
127 }
128
129 let mut info = OffsetInfo::new();
130 let value_data = value.get(key).unwrap().as_array().unwrap();
131 info.offset = value_data[1].as_i64().unwrap();
132 info.size = value_data[2].as_i64().unwrap();
133
134 if classes_info.version == 10201 {
135 info.is_bit = value_data.len() == 4;
136 } else if classes_info.version == 10202 {
137 info.is_bit = value_data.len() == 5;
138 } else {
139 panic!("Unknown version: {}", classes_info.version);
140 }
141 info.valid = true;
142
143 if info.is_bit {
144
145 if classes_info.version == 10201 {
146 info.bit_offset = value_data[3].as_i64().unwrap() as i32;
147 dsapi.class_member_map.insert(class_name.clone() + &key[..key.len()-4], info);
148 } else if classes_info.version == 10202 {
149 info.bit_offset = value_data[4].as_i64().unwrap() as i32;
150 dsapi.class_member_map.insert(class_name.clone() + key, info);
151 } else {
153 panic!("Unknown version: {}", classes_info.version);
154 }
155 } else {
156 dsapi.class_member_map.insert(class_name.clone() + key, info);
157 }
158
159 }
160 }
161 }
162 }
163 fn download_url(url: &str) -> Result<String, String> {
164 let response = reqwest::blocking::get(url)
165 .map_err(|e| format!("Failed to fetch URL {}: {}", url, e))?;
166 if response.status().is_success() {
167 let mut d = flate2::read::GzDecoder::new(response);
168 let mut s = String::new();
169 d.read_to_string(&mut s).map_err(|e| format!("Failed to read decompressed data: {}", e))?;
170 Ok(s)
171 } else {
172 Err(format!("Request failed with status: {}", response.status()))
173 }
174 }
175 let engine = self.engine.clone();
176 let location = self.location.clone();
177 let format_url = |json_type: &str| -> String {
178 format!("https://dumpspace.spuckwaffel.com/Games/{}/{}/{}.json.gz", engine, location, json_type)
179 };
180
181
182
183
184
185 let url = format_url("ClassesInfo");
186 let resp = download_url(&url)
187 .expect("Failed to download classes info");
188 let classes_info = serde_json::from_str::<BlobInfo>(&resp)
189 .expect("Failed to parse classes info");
190 parse_class_info(&classes_info, self);
191
192
193 let url = format_url("StructsInfo");
194 let resp = download_url(&url)
195 .expect("Failed to download structs info");
196 let structs_info = serde_json::from_str::<BlobInfo>(&resp)
197 .expect("Failed to parse structs info");
198 parse_class_info(&structs_info, self);
199
200
201 let url = format_url("EnumsInfo");
202 let resp = download_url(&url)
203 .expect("Failed to download enums info");
204 let enums_info = serde_json::from_str::<BlobInfo>(&resp)
205 .expect("Failed to parse enums info");
206
207 for enum_info in &enums_info.data {
208 for (key, value) in enum_info {
209 let enum_name = key;
210 let value = &value.as_array().unwrap()[0];
211 for entry in value.as_array().unwrap() {
212 let entry: serde_json::Map<String, serde_json::Value> = entry.as_object().unwrap().clone();
213 let enum_value_name = entry.keys().next().unwrap();
214 assert!(entry.keys().len() == 1);
215 let enum_value = entry.get(enum_value_name).unwrap().as_i64().unwrap();
216 self.enum_name_map.insert(enum_name.to_owned() + &enum_value.to_string().clone(), enum_value_name.clone());
217 }
218 }
219 }
220
221
222 let url = format_url("OffsetsInfo");
239 let resp = download_url(&url)
240 .expect("Failed to download offsets info");
241 let offsets_info = serde_json::from_str::<OffsetBlob>(&resp)
242 .expect("Failed to parse offsets info");
243
244 for offset in &offsets_info.data {
245 self.offset_map.insert(offset[0].as_str().unwrap().to_string(), offset[1].as_u64().unwrap());
246 }
247
248
249
250 if self.cache_path.is_some() {
251 self.downloaded_at = self.game_list.get_game_by_hash(&self.game_id).unwrap().uploaded;
252 self.cache_self().map_err(|e| format!("Failed to cache DSAPI: {}", e))?;
253 }
254 Ok(())
255 }
256 pub fn get_member_offset(&self, class_name: &str, member_name: &str) -> Option<OffsetInfo> {
258 self.class_member_map.get(&(class_name.to_string() + member_name)).cloned()
259 }
260 pub fn get_class_size(&self, class_name: &str) -> Option<i32> {
263 self.class_size_map.get(class_name).cloned()
264 }
265 #[allow(dead_code)] fn get_function_offset(&self, function_class: &str, function_name: &str) -> Option<u64> {
270 self.function_offset_map.get(&(function_class.to_string() + function_name)).cloned()
271 }
272 pub fn get_enum_name(&self, enum_name: &str, enum_value: i64) -> Option<String> {
275 self.enum_name_map.get(&(enum_name.to_string() + &enum_value.to_string())).cloned()
276 }
277 pub fn get_offset(&self, offset_name: &str) -> Option<u64> {
280 self.offset_map.get(offset_name).cloned()
281 }
282 pub fn get_member_offset_unchecked(&self, class_name: &str, member_name: &str) -> usize {
287 self.class_member_map.get(&(class_name.to_string() + member_name)).cloned().unwrap().offset as usize
288 }
289}
290
291
292#[derive(Deserialize, Serialize, Debug)]
293pub struct GameList {
294 pub games: Vec<Game>
295}
296
297
298#[derive(Deserialize, Serialize, Debug)]
299pub struct Game {
300 pub hash: String,
301 pub name: String,
302 pub engine: String,
303 pub location: String,
304 pub uploaded: u64, pub uploader: Uploader
306}
307
308#[derive(Deserialize, Serialize, Debug)]
309pub struct Uploader {
310 pub name: String,
311 pub link: String,
312}
313#[derive(Deserialize, Serialize, Debug, Clone)]
314pub struct OffsetInfo {
315 pub offset: i64,
316 pub size: i64,
317 pub is_bit: bool,
318 pub bit_offset: i32,
319 pub valid: bool,
320}
321
322impl OffsetInfo {
323 pub fn new() -> Self {
324 OffsetInfo {
325 offset: 0,
326 size: 0,
327 is_bit: false,
328 bit_offset: 0,
329 valid: false,
330 }
331 }
332}
333
334impl Into<bool> for OffsetInfo {
336 fn into(self) -> bool {
337 self.valid
338 }
339}
340
341
342#[derive(Deserialize, Debug)]
343#[allow(dead_code)]
344struct BlobInfo {
345 data: Vec<HashMap<String, serde_json::Value>>,
346 updated_at: String, version: u64, }
349
350#[derive(Deserialize, Debug)]
351#[allow(dead_code)]
352struct OffsetBlob {
353 credit: HashMap<String, String>,
354 data: Vec<Vec<serde_json::Value>>, updated_at: String, version: u64, }
358impl GameList {
359 pub fn init() -> Result<Self, String> {
360 let url = "https://dumpspace.spuckwaffel.com/Games/GameList.json";
361
362 let response = reqwest::blocking::get(url)
363 .map_err(|e| format!("Failed to fetch game list: {}", e))?;
364
365 if response.status().is_success() {
366 let text = response.text().map_err(|e| format!("Failed to read response text: {}", e))?;
367 serde_json::from_str(&text).map_err(|e| format!("Failed to parse JSON: {}", e))
368 } else {
369 Err(format!("Request failed with status: {}", response.status()))
370 }
371 }
372 pub fn get_game_by_hash(&self, hash: &str) -> Option<&Game> {
373 self.games.iter().find(|game| game.hash == hash)
374 }
375 pub fn get_game_by_name(&self, name: &str) -> Option<&Game> {
376 self.games.iter().find(|game| game.name == name)
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 static mut LOCAL_DSAPI: std::sync::LazyLock<DSAPI> = std::sync::LazyLock::new(||{let mut res = DSAPI::new("6b77eceb", None);res.download_content().unwrap();return res;}); #[test]
386 fn test_new_dsapi() {
387 let dsapi = DSAPI::new("6b77eceb", None);
388 assert_eq!(dsapi.engine, "Unreal-Engine-5");
389 assert_eq!(dsapi.location, "Fortnite");
390 }
391
392 #[test]
393 fn test_get_member_offset_some() {
394 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
395 let info = dsapi.get_member_offset("UWorld", "OwningGameInstance");
396 assert!(info.is_some());
397 let info = info.unwrap();
398 assert_eq!(info.offset, 0x228);
399 assert_eq!(info.size, 8);
400 assert!(info.valid);
401 }
402
403 #[test]
404 fn test_get_member_offset_none() {
405 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
406 assert!(dsapi.get_member_offset("NoClass", "NoMember").is_none());
407 }
408
409 #[test]
410 fn test_get_class_size_some() {
411 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
412 assert_eq!(dsapi.get_class_size("UWorld"), Some(2536));
413 }
414
415 #[test]
416 fn test_get_class_size_none() {
417 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
418 assert_eq!(dsapi.get_class_size("NoClass"), None);
419 }
420
421 #[test]
422 #[allow(unreachable_code)] fn test_get_function_offset_some() {
424 return; let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
426 assert_eq!(dsapi.get_function_offset("TestClass", "TestFunc"), Some(0x1234));
427 }
428
429 #[test]
430 fn test_get_function_offset_none() {
431 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
432 assert_eq!(dsapi.get_function_offset("NoClass", "NoFunc"), None);
433 }
434
435 #[test]
436 fn test_get_enum_name_some() {
437 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
438 assert_eq!(dsapi.get_enum_name("EFortRarity", 1), Some("EFortRarity__Uncommon".to_string()));
439 }
440
441 #[test]
442 fn test_get_enum_name_none() {
443 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
444 assert_eq!(dsapi.get_enum_name("NoEnum", 2), None);
445 }
446
447 #[test]
448 fn test_get_offset_some() {
449 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
450 assert_eq!(dsapi.get_offset("OFFSET_GWORLD"), Some(0x14942840));
451 }
452
453 #[test]
454 fn test_get_offset_none() {
455 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
456 assert_eq!(dsapi.get_offset("NO_OFFSET"), None);
457 }
458
459 #[test]
460 fn test_get_member_offset_unchecked() {
461 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
462 let offset = dsapi.get_member_offset_unchecked("UWorld", "OwningGameInstance");
463 assert_eq!(offset, 0x228);
464 }
465
466 #[test]
467 #[should_panic]
468 fn test_get_member_offset_unchecked_panic() {
469 let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
470 dsapi.get_member_offset_unchecked("NoClass", "NoMember");
471 }
472
473 #[test]
474 fn test_cache_self() {
475 let dsapi = DSAPI::new("6b77eceb", Some(std::path::PathBuf::from("temp/test_cache")));
476 dsapi.cache_self().expect("Failed to cache DSAPI");
477 let restored_dsapi = dsapi.restore_from_cache().expect("Failed to restore from cache");
478 assert_eq!(dsapi.engine, restored_dsapi.engine);
479 assert_eq!(dsapi.location, restored_dsapi.location);
480 std::fs::remove_dir_all("temp/test_cache").expect("Failed to clean up cache directory");
481 }
482
483 #[test]
484 fn test_update_cache() {
485 let mut dsapi = DSAPI::new("6b77eceb", Some(std::path::PathBuf::from("temp/test_update_cache")));
486 dsapi.download_content().expect("Failed to download content");
487 let original_downloaded_at = dsapi.downloaded_at;
488 dsapi.cache_self().expect("Failed to update cache");
490
491 let mut new_dsapi = DSAPI::new("6b77eceb", Some(std::path::PathBuf::from("temp/test_update_cache")));
492 new_dsapi.game_list.games.iter_mut().find(|game| game.hash == new_dsapi.game_id)
493 .expect("Game not found").uploaded += 1; new_dsapi.download_content().expect("Failed to download content again");
497 assert!(new_dsapi.downloaded_at > original_downloaded_at); std::fs::remove_dir_all("temp/test_update_cache").expect("Failed to clean up cache directory");
499 }
500}