1use 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 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 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
282pub 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]; 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]; 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