1use std::{collections::HashMap, io::Read};
2
3use reqwest;
4use serde_derive::Deserialize;
5
6
7
8#[derive(Debug)]
9pub 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 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 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 } 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 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!("{}/{}/{}/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 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 pub fn get_class_size(&self, class_name: &str) -> Option<i32> {
209 self.class_size_map.get(class_name).cloned()
210 }
211 #[allow(dead_code)] 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 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 pub fn get_offset(&self, offset_name: &str) -> Option<u64> {
226 self.offset_map.get(offset_name).cloned()
227 }
228 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, 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
280impl 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, version: u64, }
295
296#[derive(Deserialize, Debug)]
297#[allow(dead_code)]
298struct OffsetBlob {
299 credit: HashMap<String, String>,
300 data: Vec<Vec<serde_json::Value>>, updated_at: String, version: u64, }
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;}); #[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)] fn test_get_function_offset_some() {
370 return; 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}