dumpspace_api/
lib.rs

1use std::{collections::HashMap, io::Read};
2
3use reqwest;
4use serde_derive::Serialize;
5use serde_derive::Deserialize;
6
7
8
9#[derive(Debug)]
10/// The main struct for the Dumpspace API, which provides methods to interact with the Dumpspace data.
11/// It must be initialized with a specific game ID (hash) and then must call `download_content` to fetch and parse the data.
12/// # Example:
13/// ```
14/// use dumpspace_api::DSAPI;
15/// let game_id = "6b77eceb"; // Example game ID, replace with actual game hash
16/// let mut dsapi = DSAPI::new(game_id, None); // Optional cache path
17/// dsapi.download_content().unwrap(); // Download and parse the content (if this fails you're screwed anyways so might as well unwrap)
18/// println!("{:?}", dsapi.get_member_offset("UWorld", "OwningGameInstance"));
19/// println!("{:?}", dsapi.get_enum_name("EFortRarity", 4));
20/// println!("0x{:x?}", dsapi.get_class_size("AActor").unwrap());
21/// println!("0x{:x?}", dsapi.get_offset("OFFSET_GWORLD").unwrap());
22/// ```
23#[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    /// Creates a new instance of `DSAPI` for a specific game identified by its hash.
42    /// This function initializes the game list and sets the engine and location based on the provided game ID.
43    /// Game ID can be found in the url of a dumpspace game page, and will be a query argument called `hash`.
44    /// cache_path is an optional path to a directory where the API can cache downloaded content.
45    /// If caching is enabled, the API will check if the content is already cached before downloading
46    /// and parsing the content. If you want to disable caching, pass `None` as the `cache_path`.
47    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, // This will be set when the content is downloaded
58            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    /// Downloads and parses the content from the dumpspace API.
97    /// This function fetches various JSON blobs containing class, struct, enum, and function information,
98    /// and populates the internal maps with this data.
99    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                    // If the cached content is still valid, we can use it
106                    *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                                //class_member_map insertion
152                            } 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("FunctionsInfo");
223        // let resp = download_url(&url)
224        //     .expect("Failed to download functions info"); 
225        // let functions_info = serde_json::from_str::<BlobInfo>(&resp)
226        //     .expect("Failed to parse functions info");
227        // for function in &functions_info.data {
228            
229        //     for (key, value) in function {
230        //         dbg!(key, value);
231        //         let function_name = key;
232        //         let value = value.as_array().unwrap()[2].as_u64().unwrap();
233        //         self.function_offset_map.insert(function_name.clone() + &function_name, value);
234        //     }
235        // }
236
237
238        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    /// Returns the offset info for a class member as an `Option<OffsetInfo>`.
257    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    /// Returns the size of a class as an `Option<i32>`.
261    /// Returns `None` if the class is not found.
262    pub fn get_class_size(&self, class_name: &str) -> Option<i32> {
263        self.class_size_map.get(class_name).cloned()
264    }
265    /// Returns the offset of a function as an `Option<u64>`.
266    /// Returns `None` if the function is not found.
267    /// Note: Functions are not currently implemented.
268    #[allow(dead_code)] //removeme
269    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    /// Returns the name of an enum value as an `Option<String>`.
273    /// Returns `None` if the enum name or value is not found.
274    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    /// Returns the offset of a specific offset name as an `Option<u64>`.
278    /// Returns `None` if the offset name is not found.
279    pub fn get_offset(&self, offset_name: &str) -> Option<u64> {
280        self.offset_map.get(offset_name).cloned()
281    }
282    /// Returns the offset info for a class member with an .unwrap() and cast to usize.
283    /// This function will panic if the member is not found.
284    /// # Safety: This function assumes that the member exists and will panic if it does not.
285    /// This should be fine to use in practice, as the code should only panic if the member is misspelled or does not exist.
286    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, // Unix timestamp
305    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
334// converting bool() operation from c++
335impl 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, // Unix timestamp
347    version: u64, // Version number
348}
349
350#[derive(Deserialize, Debug)]
351#[allow(dead_code)]
352struct OffsetBlob {
353    credit: HashMap<String, String>,
354    data: Vec<Vec<serde_json::Value>>, //fucking hate this
355    updated_at: String, // Unix timestamp
356    version: u64, // Version number
357}
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;}); //fortnite
384
385    #[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)] //removeme
423    fn test_get_function_offset_some() {
424        return; //functions are not implemented yet.
425        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        // Update the cache
489        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; // Increment the uploaded timestamp
494        
495        // Download the content again to trigger the cache update
496        new_dsapi.download_content().expect("Failed to download content again");
497        assert!(new_dsapi.downloaded_at > original_downloaded_at); //if new downloaded_at is greater than original, cache was invalidated and fresh offsets were downloaded.
498        std::fs::remove_dir_all("temp/test_update_cache").expect("Failed to clean up cache directory");
499    }
500}