1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//
// Models
//

pub mod replay;
pub mod spawnset;

use crate::utils;
use std::fmt::Write;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;

#[derive(Debug, FromPrimitive, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum GameMode {
    Survival = 0,
    TimeAttack,
    Race,
}

impl std::default::Default for GameMode {
    fn default() -> Self {
        GameMode::Survival
    }
}

impl std::convert::From<u8> for GameMode {
    fn from(v: u8) -> Self {
        match v {
            0 => GameMode::Survival,
            1 => GameMode::TimeAttack,
            2 => GameMode::Race,
            _ => GameMode::default(),
        }
    }
}

#[repr(C)]
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct StatsDataBlock {
    marker: [u8; 11],
    pub ddstats_version: i32,
    pub player_id: i32,
    pub username: [u8; 32],
    pub time: f32,
    pub gems_collected: i32,
    pub kills: i32,
    pub daggers_fired: i32,
    pub daggers_hit: i32,
    pub enemies_alive: i32,
    pub level_gems: i32,
    pub homing: i32,
    pub gems_despawned: i32,
    pub gems_eaten: i32,
    pub gems_total: i32,
    pub daggers_eaten: i32,
    pub per_enemy_alive_count: [i16; 17],
    pub per_enemy_kill_count: [i16; 17],
    pub is_player_alive: bool,
    pub is_replay: bool,
    pub death_type: u8,
    pub is_in_game: bool,
    pub replay_player_id: i32,
    pub replay_player_name: [u8; 32],
    pub survival_md5: [u8; 16],
    pub time_lvl2: f32,
    pub time_lvl3: f32,
    pub time_lvl4: f32,
    pub levi_down_time: f32,
    pub orb_down_time: f32,
    pub status: i32, // 0 = Intro Screen | 1 = Main Menu | 2 = InGame | 3 = DEAD
    pub max_homing: i32,
    pub time_max_homing: f32, // gets updated every gem you get even if you dont have any homing
    pub enemies_alive_max: i32, // doesn't get reset sometimes when restarting a run
    pub time_enemies_alive_max: f32,
    pub time_max: f32,       // Max time of replay / current time in-game
    padding1: [u8; 4],       // fun
    pub stats_base: [u8; 8], // Pointer to frames
    pub stats_frames_loaded: i32,
    pub stats_finished_loading: bool,
    padding2: [u8; 3],
    pub starting_hand: i32,
    pub starting_homing: i32,
    pub starting_time: f32,
    pub prohibited_mods: bool,
    padding3: [u8; 3],
    pub replay_base: [u8; 8],
    pub replay_buffer_length: i32,
    pub replay_flag: bool,
    pub game_mode: u8,
    pub is_time_attack_or_race_finished: bool
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default, serde::Serialize)]
pub struct StatsFrame {
    pub gems_collected: i32,
    pub kills: i32,
    pub daggers_fired: i32,
    pub daggers_hit: i32,
    pub enemies_alive: i32,
    pub level_gems: i32,
    pub homing: i32,
    pub gems_despawned: i32,
    pub gems_eaten: i32,
    pub gems_total: i32,
    pub daggers_eaten: i32,
    pub per_enemy_alive_count: [i16; 17],
    pub per_enemy_kill_count: [i16; 17],
}

#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct StatsBlockWithFrames {
    pub block: StatsDataBlock,
    pub frames: Vec<StatsFrame>,
}

impl StatsDataBlock {
    pub fn player_username(&self) -> String {
        utils::byte_array_to_string(&self.username[..]).unwrap_or_else(|_| "unknown".into())
    }

    pub fn replay_player_username(&self) -> String {
        utils::byte_array_to_string(&self.replay_player_name[..]).unwrap_or_else(|_| "unknown".into())
    }

    pub fn level_hash(&self) -> String {
        let mut s = String::with_capacity(2 * self.survival_md5.len());
        for byte in self.survival_md5 {
            write!(s, "{:02X}", byte).expect("Couldn't decode hash byte");
        }
        s
    }

    pub fn get_stats_pointer(&self) -> usize {
        i64::from_le_bytes(self.stats_base) as usize
    }

    pub fn get_replay_pointer(&self) -> usize {
        i64::from_le_bytes(self.replay_base) as usize
    }

    pub fn status(&self) -> GameStatus {
        FromPrimitive::from_i32(self.status).unwrap()
    }
}

impl StatsBlockWithFrames {
    #[rustfmt::skip]
    pub fn get_frame_for_time(&self, time: f32) -> Option<&StatsFrame> {
        let real_time = time - self.block.starting_time;
        if real_time <= 0. { return None; }
        if real_time + 1. > self.frames.len() as f32 { return None; }
        Some(&self.frames[real_time as usize])
    }

    pub fn get_frames_until_time(&self, mut time: f32) -> Vec<&StatsFrame> {
        let mut res = vec![];
        if time as usize + 1 > self.frames.len() {
            time = self.frames.len() as f32 - 1.;
        }
        res.extend(self.frames[..time as usize].iter());
        res
    }

    #[rustfmt::skip]
    pub fn homing_usage_from_frames(&self, time: Option<f32>) -> u32 {
        let mut neg_diff_lvl3 = 0;
        let mut neg_diff_lvl4 = 0;
        let mut last_frame_homing_lvl3 = 0;
        let mut last_frame_homing_lvl4 = 0;
        let cutoff = if let Some(time) = time { time } else { f32::MAX };
        for frame in &self.get_frames_until_time(cutoff) {
            if frame.level_gems == 70 {
                if frame.homing < last_frame_homing_lvl3 {
                    neg_diff_lvl3 += -(frame.homing - last_frame_homing_lvl3);
                }
                last_frame_homing_lvl3 = frame.homing;
            } else if frame.level_gems == 71 {
                if frame.homing < last_frame_homing_lvl4 {
                    neg_diff_lvl4 += -(frame.homing - last_frame_homing_lvl4);
                }
                last_frame_homing_lvl4 = frame.homing;
            }
        }
        (neg_diff_lvl3 + neg_diff_lvl4) as u32
    }
}

#[derive(FromPrimitive, Debug, PartialEq, Clone, Copy)]
pub enum GameStatus {
    Title = 0,
    Menu,
    Lobby,
    Playing,
    Dead,
    OwnReplayFromLastRun,
    OwnReplayFromLeaderboard,
    OtherReplay,
    LocalReplay,
}