dumpspace_api/
lib.rs

1use std::{collections::HashMap, io::Read};
2
3use reqwest;
4use serde_derive::Deserialize;
5
6
7
8#[derive(Debug)]
9/// The main struct for the Dumpspace API, which provides methods to interact with the Dumpspace data.
10/// It must be initialized with a specific game ID (hash) and then must call `download_content` to fetch and parse the data.
11/// # Example:
12/// ```
13/// use dumpspace_api::DSAPI;
14/// let game_id = "6b77eceb"; // Example game ID, replace with actual game hash
15/// let mut dsapi = DSAPI::new(game_id);
16/// dsapi.download_content().unwrap(); // Download and parse the content (if this fails you're screwed anyways so might as well unwrap)
17/// println!("{:?}", dsapi.get_member_offset("UWorld", "OwningGameInstance"));
18/// println!("{:?}", dsapi.get_enum_name("EFortRarity", 4));
19/// println!("0x{:x?}", dsapi.get_class_size("AActor").unwrap());
20/// println!("0x{:x?}", dsapi.get_offset("OFFSET_GWORLD").unwrap());
21/// ```
22pub struct DSAPI {
23    game_list: GameList,
24    class_member_map: HashMap<String, OffsetInfo>,
25    class_size_map: HashMap<String, i32>,
26    function_offset_map: HashMap<String, u64>,
27    enum_name_map: HashMap<String, String>,
28    offset_map: HashMap<String, u64>,
29
30    pub engine: String,
31    pub location: String,
32
33}
34
35impl DSAPI {
36    /// Creates a new instance of `DSAPI` for a specific game identified by its hash.
37    /// This function initializes the game list and sets the engine and location based on the provided game ID.
38    /// Game ID can be found in the url of a dumpspace game page, and will be a query argument called `hash`.
39    pub fn new(game_id: &str) -> Self {
40        let mut ret = DSAPI {
41            game_list: GameList::init().expect("Failed to initialize game list"),
42            class_member_map: HashMap::new(),
43            class_size_map: HashMap::new(),
44            function_offset_map: HashMap::new(),
45            enum_name_map: HashMap::new(),
46            offset_map: HashMap::new(),
47            engine: String::new(),
48            location: String::new(),
49        };
50        ret.engine = ret.game_list.get_game_by_hash(game_id)
51            .expect("Game not found")
52            .engine
53            .clone();
54        ret.location = ret.game_list.get_game_by_hash(game_id)
55            .expect("Game not found")
56            .location
57            .clone();
58        ret
59    }
60    /// Downloads and parses the content from the dumpspace API.
61    /// This function fetches various JSON blobs containing class, struct, enum, and function information,
62    /// and populates the internal maps with this data.
63    pub fn download_content(&mut self) -> Result<(), String> {
64        fn parse_class_info(classes_info: &BlobInfo, dsapi: &mut DSAPI) {
65            for class in &classes_info.data {
66
67                for (key, value) in class {
68                    let class_name = key;
69                    let value: Vec<HashMap<String, serde_json::Value>> = serde_json::from_str(&value.to_string()).unwrap();
70                    for value in value {
71                        let key = value.keys().next().unwrap().as_str();
72                        assert!(value.keys().len() == 1);
73                        if key == "__MDKClassSize" {
74                            dsapi.class_size_map.insert(class_name.clone(), value.get("__MDKClassSize").unwrap().as_i64().unwrap() as i32);
75                            continue;
76                        }
77                        if key == "__InheritInfo" {
78                            continue;
79                        }
80
81                        let mut info = OffsetInfo::new();
82                        let value_data = value.get(key).unwrap().as_array().unwrap();
83                        info.offset = value_data[1].as_i64().unwrap();
84                        info.size = value_data[2].as_i64().unwrap();
85
86                        if classes_info.version == 10201 {
87                            info.is_bit = value_data.len() == 4;
88                        } else if classes_info.version == 10202 {
89                            info.is_bit = value_data.len() == 5;
90                        } else {
91                            panic!("Unknown version: {}", classes_info.version);
92                        }
93                        info.valid = true;
94
95                        if info.is_bit {
96                            
97                            if classes_info.version == 10201 {
98                                info.bit_offset = value_data[3].as_i64().unwrap() as i32;
99                                dsapi.class_member_map.insert(class_name.clone() + &key[..key.len()-4], info);
100                            } else if classes_info.version == 10202 {
101                                info.bit_offset = value_data[4].as_i64().unwrap() as i32;
102                                dsapi.class_member_map.insert(class_name.clone() + key, info);
103                                //class_member_map insertion
104                            } else {
105                                panic!("Unknown version: {}", classes_info.version);
106                            }
107                        } else {
108                            dsapi.class_member_map.insert(class_name.clone() + key, info);
109                        }
110                        
111                    }
112                }
113            }
114        }
115        fn download_url(url: &str) -> Result<String, String> {
116            let response = reqwest::blocking::get(url)
117                .map_err(|e| format!("Failed to fetch URL {}: {}", url, e))?;
118            if response.status().is_success() {
119                let mut d = flate2::read::GzDecoder::new(response);
120                let mut s = String::new();
121                d.read_to_string(&mut s).map_err(|e| format!("Failed to read decompressed data: {}", e))?;
122                Ok(s)
123            } else {
124                Err(format!("Request failed with status: {}", response.status()))
125            }
126        }
127        //do stuff
128        let base_url = "https://dumpspace.spuckwaffel.com/Games";
129
130
131
132
133
134        let url = format!("{}/{}/{}/ClassesInfo.json.gz", base_url, self.engine, self.location);
135        let resp = download_url(&url)
136            .expect("Failed to download classes info");
137        let classes_info = serde_json::from_str::<BlobInfo>(&resp)
138            .expect("Failed to parse classes info");
139        parse_class_info(&classes_info, self);
140
141
142        let url = format!("{}/{}/{}/StructsInfo.json.gz", base_url, self.engine, self.location);
143        let resp = download_url(&url)
144            .expect("Failed to download structs info"); 
145        let structs_info = serde_json::from_str::<BlobInfo>(&resp)
146            .expect("Failed to parse structs info");
147        parse_class_info(&structs_info, self);
148
149
150        let url = format!("{}/{}/{}/EnumsInfo.json.gz", base_url, self.engine, self.location);
151        let resp = download_url(&url)
152            .expect("Failed to download enums info");
153        let enums_info = serde_json::from_str::<BlobInfo>(&resp)
154            .expect("Failed to parse enums info");
155
156        for enum_info in &enums_info.data {
157            for (key, value) in enum_info {
158                let enum_name = key;
159                let value = &value.as_array().unwrap()[0];
160                for entry in value.as_array().unwrap() {
161                    let entry: serde_json::Map<String, serde_json::Value> = entry.as_object().unwrap().clone();
162                    let enum_value_name = entry.keys().next().unwrap();
163                    assert!(entry.keys().len() == 1);
164                    let enum_value = entry.get(enum_value_name).unwrap().as_i64().unwrap();
165                    self.enum_name_map.insert(enum_name.to_owned() + &enum_value.to_string().clone(), enum_value_name.clone());
166                }
167            }
168        }
169
170
171        // let url = format!("{}/{}/{}/FunctionsInfo.json.gz", base_url, self.engine, self.location);
172        // let resp = download_url(&url)
173        //     .expect("Failed to download functions info"); 
174        // let functions_info = serde_json::from_str::<BlobInfo>(&resp)
175        //     .expect("Failed to parse functions info");
176        // for function in &functions_info.data {
177            
178        //     for (key, value) in function {
179        //         dbg!(key, value);
180        //         let function_name = key;
181        //         let value = value.as_array().unwrap()[2].as_u64().unwrap();
182        //         self.function_offset_map.insert(function_name.clone() + &function_name, value);
183        //     }
184        // }
185
186
187        let url = format!("{}/{}/{}/OffsetsInfo.json.gz", base_url, self.engine, self.location);
188        let resp = download_url(&url)
189            .expect("Failed to download offsets info"); 
190        let offsets_info = serde_json::from_str::<OffsetBlob>(&resp)
191            .expect("Failed to parse offsets info");
192        
193        for offset in &offsets_info.data {
194            self.offset_map.insert(offset[0].as_str().unwrap().to_string(), offset[1].as_u64().unwrap());
195        }
196
197
198
199
200        Ok(())
201    }
202    /// Returns the offset info for a class member as an `Option<OffsetInfo>`.
203    pub fn get_member_offset(&self, class_name: &str, member_name: &str) -> Option<OffsetInfo> {
204        self.class_member_map.get(&(class_name.to_string() + member_name)).cloned()
205    }
206    /// Returns the size of a class as an `Option<i32>`.
207    /// Returns `None` if the class is not found.
208    pub fn get_class_size(&self, class_name: &str) -> Option<i32> {
209        self.class_size_map.get(class_name).cloned()
210    }
211    /// Returns the offset of a function as an `Option<u64>`.
212    /// Returns `None` if the function is not found.
213    /// Note: Functions are not currently implemented.
214    #[allow(dead_code)] //removeme
215    fn get_function_offset(&self, function_class: &str, function_name: &str) -> Option<u64> {
216        self.function_offset_map.get(&(function_class.to_string() + function_name)).cloned()
217    }
218    /// Returns the name of an enum value as an `Option<String>`.
219    /// Returns `None` if the enum name or value is not found.
220    pub fn get_enum_name(&self, enum_name: &str, enum_value: i64) -> Option<String> {
221        self.enum_name_map.get(&(enum_name.to_string() + &enum_value.to_string())).cloned()
222    }
223    /// Returns the offset of a specific offset name as an `Option<u64>`.
224    /// Returns `None` if the offset name is not found.
225    pub fn get_offset(&self, offset_name: &str) -> Option<u64> {
226        self.offset_map.get(offset_name).cloned()
227    }
228    /// Returns the offset info for a class member with an .unwrap() and cast to usize.
229    /// This function will panic if the member is not found.
230    /// # Safety: This function assumes that the member exists and will panic if it does not.
231    /// This should be fine to use in practice, as the code should only panic if the member is misspelled or does not exist.
232    pub fn get_member_offset_unchecked(&self, class_name: &str, member_name: &str) -> usize {
233        self.class_member_map.get(&(class_name.to_string() + member_name)).cloned().unwrap().offset as usize
234    }
235}
236
237
238#[derive(Deserialize, Debug)]
239pub struct GameList {
240    pub games: Vec<Game>
241}
242
243
244#[derive(Deserialize, Debug)]
245pub struct Game {
246    pub hash: String,
247    pub name: String,
248    pub engine: String,
249    pub location: String,
250    pub uploaded: u64, // Unix timestamp
251    pub uploader: Uploader
252}
253
254#[derive(Deserialize, Debug)]
255pub struct Uploader {
256    pub name: String,
257    pub link: String,
258}
259#[derive(Deserialize, Debug, Clone)]
260pub struct OffsetInfo {
261    pub offset: i64,
262    pub size: i64,
263    pub is_bit: bool,
264    pub bit_offset: i32,
265    pub valid: bool,
266}
267
268impl OffsetInfo {
269    pub fn new() -> Self {
270        OffsetInfo {
271            offset: 0,
272            size: 0,
273            is_bit: false,
274            bit_offset: 0,
275            valid: false,
276        }
277    }
278}
279
280// converting bool() operation from c++
281impl Into<bool> for OffsetInfo {
282    fn into(self) -> bool {
283        self.valid
284    }
285}
286
287
288#[derive(Deserialize, Debug)]
289#[allow(dead_code)]
290struct BlobInfo {
291    data: Vec<HashMap<String, serde_json::Value>>,
292    updated_at: String, // Unix timestamp
293    version: u64, // Version number
294}
295
296#[derive(Deserialize, Debug)]
297#[allow(dead_code)]
298struct OffsetBlob {
299    credit: HashMap<String, String>,
300    data: Vec<Vec<serde_json::Value>>, //fucking hate this
301    updated_at: String, // Unix timestamp
302    version: u64, // Version number
303}
304impl GameList {
305    pub fn init() -> Result<Self, String> {
306        let url = "https://dumpspace.spuckwaffel.com/Games/GameList.json";
307
308        let response = reqwest::blocking::get(url)
309            .map_err(|e| format!("Failed to fetch game list: {}", e))?;
310
311        if response.status().is_success() {
312            let text = response.text().map_err(|e| format!("Failed to read response text: {}", e))?;
313            serde_json::from_str(&text).map_err(|e| format!("Failed to parse JSON: {}", e))
314        } else {
315            Err(format!("Request failed with status: {}", response.status()))
316        }
317    }
318    pub fn get_game_by_hash(&self, hash: &str) -> Option<&Game> {
319        self.games.iter().find(|game| game.hash == hash)
320    }
321    pub fn get_game_by_name(&self, name: &str) -> Option<&Game> {
322        self.games.iter().find(|game| game.name == name)
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    static mut LOCAL_DSAPI: std::sync::LazyLock<DSAPI> = std::sync::LazyLock::new(||{let mut res = DSAPI::new("6b77eceb");res.download_content().unwrap();return res;}); //fortnite
330
331    #[test]
332    fn test_new_dsapi() {
333        let dsapi = DSAPI::new("6b77eceb");
334        assert_eq!(dsapi.engine, "Unreal-Engine-5");
335        assert_eq!(dsapi.location, "Fortnite");
336    }
337
338    #[test]
339    fn test_get_member_offset_some() {
340        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
341        let info = dsapi.get_member_offset("UWorld", "OwningGameInstance");
342        assert!(info.is_some());
343        let info = info.unwrap();
344        assert_eq!(info.offset, 0x228);
345        assert_eq!(info.size, 8);
346        assert!(info.valid);
347    }
348
349    #[test]
350    fn test_get_member_offset_none() {
351        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
352        assert!(dsapi.get_member_offset("NoClass", "NoMember").is_none());
353    }
354
355    #[test]
356    fn test_get_class_size_some() {
357        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
358        assert_eq!(dsapi.get_class_size("UWorld"), Some(2536));
359    }
360
361    #[test]
362    fn test_get_class_size_none() {
363        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
364        assert_eq!(dsapi.get_class_size("NoClass"), None);
365    }
366
367    #[test]
368    #[allow(unreachable_code)] //removeme
369    fn test_get_function_offset_some() {
370        return; //functions are not implemented yet.
371        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
372        assert_eq!(dsapi.get_function_offset("TestClass", "TestFunc"), Some(0x1234));
373    }
374
375    #[test]
376    fn test_get_function_offset_none() {
377        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
378        assert_eq!(dsapi.get_function_offset("NoClass", "NoFunc"), None);
379    }
380
381    #[test]
382    fn test_get_enum_name_some() {
383        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
384        assert_eq!(dsapi.get_enum_name("EFortRarity", 1), Some("EFortRarity__Uncommon".to_string()));
385    }
386
387    #[test]
388    fn test_get_enum_name_none() {
389        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
390        assert_eq!(dsapi.get_enum_name("NoEnum", 2), None);
391    }
392
393    #[test]
394    fn test_get_offset_some() {
395        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
396        assert_eq!(dsapi.get_offset("OFFSET_GWORLD"), Some(0x14942840));
397    }
398
399    #[test]
400    fn test_get_offset_none() {
401        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
402        assert_eq!(dsapi.get_offset("NO_OFFSET"), None);
403    }
404
405    #[test]
406    fn test_get_member_offset_unchecked() {
407        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
408        let offset = dsapi.get_member_offset_unchecked("UWorld", "OwningGameInstance");
409        assert_eq!(offset, 0x228);
410    }
411
412    #[test]
413    #[should_panic]
414    fn test_get_member_offset_unchecked_panic() {
415        let dsapi = unsafe{ (&raw const LOCAL_DSAPI).as_ref().unwrap() };
416        dsapi.get_member_offset_unchecked("NoClass", "NoMember");
417    }
418}