ddcore_rs/ddinfo/
ddcl_submit.rs

1//
2// Submissions to DD Custom Leaderboards
3//
4        
5use crate::{models::StatsBlockWithFrames, client_https};
6use anyhow::bail;
7use hyper::{Body, Client, Method, Request};
8use futures::StreamExt;
9use crate::ddinfo::get_os;
10use super::models::OperatingSystem;
11
12pub struct DdclSecrets {
13    pub iv: String,
14    pub pass: String,
15    pub salt: String,
16}
17
18#[derive(serde::Serialize, Debug)]
19#[serde(rename_all = "camelCase")]
20pub struct SubmitRunRequest {
21    pub survival_hash_md5: String,
22    pub player_id: i32,
23    pub player_name: String,
24    pub time_in_seconds: f32,
25    pub time_as_bytes: String,
26    pub gems_collected: i32,
27    pub enemies_killed: i32,
28    pub daggers_fired: i32,
29    pub daggers_hit: i32,
30    pub enemies_alive: i32,
31    pub homing_stored: i32,
32    pub homing_eaten: i32,
33    pub gems_despawned: i32,
34    pub gems_eaten: i32,
35    pub gems_total: i32,
36    pub death_type: u8,
37    pub level_up_time2_in_seconds: f32,
38    pub level_up_time3_in_seconds: f32,
39    pub level_up_time4_in_seconds: f32,
40    pub level_up_time2_as_bytes: String,
41    pub level_up_time3_as_bytes: String,
42    pub level_up_time4_as_bytes: String,
43    pub client_version: String,
44    pub operating_system: OperatingSystem,
45    pub build_mode: String,
46    pub client: String,
47    pub validation: String,
48    pub validation_version: i32,
49    pub is_replay: bool,
50    pub prohibited_mods: bool,
51    pub game_data: GameState,
52    pub status: i32,
53    pub replay_data: String,
54    pub replay_player_id: i32,
55    pub game_mode: u8,
56    pub time_attack_or_race_finished: bool,
57}
58
59#[derive(serde::Serialize, Debug, Default)]
60#[serde(rename_all = "camelCase")]
61pub struct GameState {
62    pub gems_collected: Vec<i32>,
63    pub enemies_killed: Vec<i32>,
64    pub daggers_fired: Vec<i32>,
65    pub daggers_hit: Vec<i32>,
66    pub enemies_alive: Vec<i32>,
67    pub homing_stored: Vec<i32>,
68    pub homing_eaten: Vec<i32>,
69    pub gems_despawned: Vec<i32>,
70    pub gems_eaten: Vec<i32>,
71    pub gems_total: Vec<i32>,
72    pub skull1s_alive: Vec<i32>,
73    pub skull2s_alive: Vec<i32>,
74    pub skull3s_alive: Vec<i32>,
75    pub spiderlings_alive: Vec<i32>,
76    pub skull4s_alive: Vec<i32>,
77    pub squid1s_alive: Vec<i32>,
78    pub squid2s_alive: Vec<i32>,
79    pub squid3s_alive: Vec<i32>,
80    pub centipedes_alive: Vec<i32>,
81    pub gigapedes_alive: Vec<i32>,
82    pub spider1s_alive: Vec<i32>,
83    pub spider2s_alive: Vec<i32>,
84    pub leviathans_alive: Vec<i32>,
85    pub orbs_alive: Vec<i32>,
86    pub thorns_alive: Vec<i32>,
87    pub ghostpedes_alive: Vec<i32>,
88    pub skull1s_killed: Vec<i32>,
89    pub skull2s_killed: Vec<i32>,
90    pub skull3s_killed: Vec<i32>,
91    pub spiderlings_killed: Vec<i32>,
92    pub skull4s_killed: Vec<i32>,
93    pub squid1s_killed: Vec<i32>,
94    pub squid2s_killed: Vec<i32>,
95    pub squid3s_killed: Vec<i32>,
96    pub centipedes_killed: Vec<i32>,
97    pub gigapedes_killed: Vec<i32>,
98    pub spider1s_killed: Vec<i32>,
99    pub spider2s_killed: Vec<i32>,
100    pub leviathans_killed: Vec<i32>,
101    pub orbs_killed: Vec<i32>,
102    pub thorns_killed: Vec<i32>,
103    pub ghostpedes_killed: Vec<i32>,
104    pub spider_eggs_alive: Vec<i32>,
105    pub spider_eggs_killed: Vec<i32>,
106}
107
108impl SubmitRunRequest {
109    pub fn from_compiled_run<T: ToString, K: ToString>(
110        run: std::sync::Arc<StatsBlockWithFrames>,
111        secrets: Option<DdclSecrets>,
112        client: T,
113        version: K,
114        replay_bin: std::sync::Arc<Vec<u8>>,
115    ) -> anyhow::Result<Self> {
116        if secrets.is_none() {
117            bail!("Missing DDCL Secrets");
118        }
119
120        let game_data = GameState {
121            gems_collected: run.frames.iter().map(|f| f.gems_collected).collect(),
122            enemies_killed: run.frames.iter().map(|f| f.kills).collect(),
123            daggers_fired: run.frames.iter().map(|f| f.daggers_fired).collect(),
124            daggers_hit: run.frames.iter().map(|f| f.daggers_hit).collect(),
125            enemies_alive: run.frames.iter().map(|f| f.enemies_alive).collect(),
126            homing_stored: run.frames.iter().map(|f| f.homing).collect(),
127            homing_eaten: run.frames.iter().map(|f| f.daggers_eaten).collect(),
128            gems_despawned: run.frames.iter().map(|f| f.gems_despawned).collect(),
129            gems_eaten: run.frames.iter().map(|f| f.gems_eaten).collect(),
130            gems_total: run.frames.iter().map(|f| f.gems_total).collect(),
131            skull1s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[0] as i32).collect(),
132            skull2s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[1] as i32).collect(),
133            skull3s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[2] as i32).collect(),
134            spiderlings_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[3] as i32).collect(),
135            skull4s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[4] as i32).collect(),
136            squid1s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[5] as i32).collect(),
137            squid2s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[6] as i32).collect(),
138            squid3s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[7] as i32).collect(),
139            centipedes_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[8] as i32).collect(),
140            gigapedes_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[9] as i32).collect(),
141            spider1s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[10] as i32).collect(),
142            spider2s_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[11] as i32).collect(),
143            leviathans_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[12] as i32).collect(),
144            orbs_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[13] as i32).collect(),
145            thorns_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[14] as i32).collect(),
146            ghostpedes_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[15] as i32).collect(),
147            spider_eggs_alive: run.frames.iter().map(|f| f.per_enemy_alive_count[16] as i32).collect(),
148            skull1s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[0] as i32).collect(),
149            skull2s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[1] as i32).collect(),
150            skull3s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[2] as i32).collect(),
151            spiderlings_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[3] as i32).collect(),
152            skull4s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[4] as i32).collect(),
153            squid1s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[5] as i32).collect(),
154            squid2s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[6] as i32).collect(),
155            squid3s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[7] as i32).collect(),
156            centipedes_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[8] as i32).collect(),
157            gigapedes_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[9] as i32).collect(),
158            spider1s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[10] as i32).collect(),
159            spider2s_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[11] as i32).collect(),
160            leviathans_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[12] as i32).collect(),
161            orbs_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[13] as i32).collect(),
162            thorns_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[14] as i32).collect(),
163            ghostpedes_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[15] as i32).collect(),
164            spider_eggs_killed: run.frames.iter().map(|f| f.per_enemy_kill_count[16] as i32).collect(),
165        };
166
167        let sec = secrets.unwrap();
168        let last = run.frames.last().unwrap();
169
170        let to_encrypt = vec![
171            run.block.player_id.to_string(),
172            crate::utils::md5_to_string(&run.block.time.to_le_bytes()[..]),
173            last.gems_collected.to_string(),
174            last.gems_despawned.to_string(),
175            last.gems_eaten.to_string(),
176            last.gems_total.to_string(),
177            last.enemies_alive.to_string(),
178            last.kills.to_string(),
179            run.block.death_type.to_string(),
180            last.daggers_hit.to_string(),
181            last.daggers_fired.to_string(),
182            last.homing.to_string(),
183            last.daggers_eaten.to_string(),
184            if run.block.is_replay { "True".to_owned() } else { "False".to_owned() },
185            run.block.status.to_string(),
186            crate::utils::md5_to_string(&run.block.survival_md5[..]),
187            crate::utils::md5_to_string(&run.block.time_lvl2.to_le_bytes()[..]),
188            crate::utils::md5_to_string(&run.block.time_lvl3.to_le_bytes()[..]),
189            crate::utils::md5_to_string(&run.block.time_lvl4.to_le_bytes()[..]),
190            run.block.game_mode.to_string(),
191            // TODO: !!!!!!! !!!!!!!!!!! !!!!!!!!!!!!!!!! !!!!!!!
192            // TODO: """"""""""""""""""""""""""""""""""""""""""""
193            // TODO: Remove this shit when the linux update drops
194            if run.block.is_time_attack_or_race_finished && !cfg!(target_os = "linux") { "True".to_owned() } else { "False".to_owned() },
195            if run.block.prohibited_mods { "True".to_owned() } else { "False".to_owned() },
196        ]
197        .join(";");
198
199        let validation = crypto_encoder::encrypt_and_encode(to_encrypt, sec.pass, sec.salt, sec.iv)?;
200
201        let replay_bin = base64::encode(&replay_bin[..]);
202
203        Ok(Self {
204            survival_hash_md5: base64::encode(&run.block.survival_md5),
205            player_id: run.block.player_id,
206            player_name: run.block.player_username(),
207            time_in_seconds: run.block.time,
208            time_as_bytes: base64::encode(run.block.time.to_le_bytes()),
209            gems_collected: last.gems_collected,
210            enemies_killed: last.kills,
211            daggers_fired: last.daggers_fired,
212            daggers_hit: last.daggers_hit,
213            enemies_alive: last.enemies_alive,
214            homing_stored: last.homing,
215            homing_eaten: last.daggers_eaten,
216            gems_despawned: last.gems_despawned,
217            gems_eaten: last.gems_eaten,
218            gems_total: last.gems_total,
219            death_type: run.block.death_type,
220            level_up_time2_in_seconds: run.block.time_lvl2,
221            level_up_time2_as_bytes: base64::encode(run.block.time_lvl2.to_le_bytes()),
222            level_up_time3_in_seconds: run.block.time_lvl3,
223            level_up_time3_as_bytes: base64::encode(run.block.time_lvl3.to_le_bytes()),
224            level_up_time4_in_seconds: run.block.time_lvl4,
225            level_up_time4_as_bytes: base64::encode(run.block.time_lvl4.to_le_bytes()),
226            client_version: version.to_string(),
227            operating_system: get_os(),
228            build_mode: "Release".to_owned(),
229            client: client.to_string(),
230            validation: validation.replace('=', ""),
231            validation_version: 2,
232            is_replay: run.block.is_replay,
233            prohibited_mods: run.block.prohibited_mods,
234            game_data,
235            status: run.block.status,
236            replay_data: replay_bin,
237            replay_player_id: run.block.replay_player_id,
238            game_mode: run.block.game_mode,
239            // TODO: !!!!!!! !!!!!!!!!!! !!!!!!!!!!!!!!!! !!!!!!!
240            // TODO: """"""""""""""""""""""""""""""""""""""""""""
241            // TODO: Remove this shit when the linux update drops
242            time_attack_or_race_finished: if cfg!(target_os = "linux") { false } else { run.block.is_time_attack_or_race_finished },
243        })
244    }
245}
246
247pub async fn submit<T: ToString, K: ToString>(
248    data: std::sync::Arc<StatsBlockWithFrames>,
249    secrets: Option<DdclSecrets>,
250    client: T, 
251    version: K, 
252    replay_bin: std::sync::Arc<Vec<u8>>
253) -> anyhow::Result<()> {
254    if replay_bin.is_empty() {
255        bail!("No bytes in replay!");
256    }
257
258    let req = SubmitRunRequest::from_compiled_run(data, secrets, client, version, replay_bin);
259    if req.is_ok() {
260        let client: Client<_, hyper::Body> = client_https!();
261        let path = "api/custom-entries/submit";
262        let uri = format!("https://devildaggers.info/{}", path);
263        let req = Request::builder()
264            .header("content-type", "application/json")
265            .header("accept", "application/json")
266            .method(Method::POST)
267            .uri(uri)
268            .body(Body::from(serde_json::to_string(&req.unwrap())?))
269            .unwrap();
270        let mut res = client.request(req).await?;
271        let mut body = Vec::new();
272        while let Some(chunk) = res.body_mut().next().await {
273            body.extend_from_slice(&chunk?);
274        }
275        if res.status() != 200 {
276            unsafe { bail!(String::from_utf8_unchecked(body)); }
277        }
278    }
279    Ok(())
280}
281
282//
283//  Crypto Encoding for DDCL
284//
285
286pub mod crypto_encoder {
287    use std::num::NonZeroU32;
288    use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut};
289    use ring::pbkdf2;
290    use base32::Alphabet::RFC4648;
291    use anyhow::Result;
292    use aes::cipher::KeyIvInit;
293
294    type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
295
296    pub fn encrypt_and_encode(plain: String, password: String, salt: String, iv: String) -> Result<String> {
297        let password = &password;
298        let mut pbkdf2_hash = [0u8; 16]; // 16 bytes for Aes128
299        let n_iter = NonZeroU32::new(65536).unwrap();
300        let salt = salt.as_bytes();
301        pbkdf2::derive(
302            pbkdf2::PBKDF2_HMAC_SHA1,
303            n_iter,
304            salt,
305            password.as_bytes(),
306            &mut pbkdf2_hash,
307        );
308        let plain = plain.as_bytes();
309        let mut buffer = [0_u8; 1000]; // big buffer
310        let cipher = match Aes128CbcEnc::new_from_slices(&pbkdf2_hash, iv.as_bytes()) {
311            Ok(v) => Ok(v),
312            Err(_) => Err(anyhow::anyhow!("Cipher Error")),
313        }?;
314        let pos = plain.len();
315        buffer[..pos].copy_from_slice(plain);
316        let ciphertext = match cipher.encrypt_padded_mut::<Pkcs7>(&mut buffer, pos) {
317            Ok(v) => Ok(v),
318            Err(_e) => Err(anyhow::anyhow!("Ciphertext Err")),
319        }?;
320        Ok(base32::encode(RFC4648 { padding: true }, ciphertext))
321    }
322}
323