quaver-rs 0.1.0

A Rust library for parsing and analyzing Quaver rhythm game maps
Documentation
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
use crate::rulesets::structs::{StrainSolverData, Hand, FingerAction, StrainSolverHitObject, FingerState, LnLayerType};
use crate::difficulty_processor::constants::{StrainConstants, StrainConstantsKeys};
use crate::difficulty_processor::helpers::{lane_to_hand, lane_to_finger, get_rate_from_mods, mode_to_key_count};
use crate::difficulty_processor::calculations::get_coefficient_value;
use crate::qua::Qua;
use crate::enums::{ModIdentifier, QssPatternFlags};

/// Total amount of milliseconds in a second
const SECONDS_TO_MILLISECONDS: f32 = 1000.0;

/// Handles Difficulty Solving + Data
#[derive(Debug, Clone)]
pub struct DifficultyProcessor {
    /// Current map for difficulty calculation
    pub map: Qua,
    
    /// Overall Difficulty of a map
    pub overall_difficulty: f32,
    
    /// Used to display prominent patterns of a map in the client
    pub qss_pattern_flags: QssPatternFlags,
    
    /// Constants used for solving
    pub strain_constants: StrainConstantsKeys,
    
    /// Average note density of the map
    pub average_note_density: f32,
    
    /// Hit objects in the map used for solving difficulty
    pub strain_solver_data: Vec<StrainSolverData>,
    
    /// Value of confidence that there's vibro manipulation in the calculated map
    pub vibro_inaccuracy_confidence: f32,
    
    /// Value of confidence that there's roll manipulation in the calculated map
    pub roll_inaccuracy_confidence: f32,
}


impl DifficultyProcessor {
    /// Constructor
    pub fn new(map: Qua, _constants: StrainConstants, _mods: ModIdentifier) -> Self {
        let strain_constants = StrainConstantsKeys::new();
        
        Self {
            map,
            overall_difficulty: 0.0,
            qss_pattern_flags: QssPatternFlags::UNKNOWN,
            strain_constants,
            average_note_density: 0.0,
            strain_solver_data: Vec::new(),
            vibro_inaccuracy_confidence: 0.0,
            roll_inaccuracy_confidence: 0.0,
        }
    }
    
    /// Calculate difficulty of a map with given mods
    pub fn calculate_difficulty(&mut self, mods: ModIdentifier) {
        // If map does not exist, ignore calculation
        if self.map.hit_objects.len() < 2 {
            return;
        }
        
        // Get song rate from selected mods
        let rate = get_rate_from_mods(mods);
        
        // Compute for overall difficulty
        let key_count = mode_to_key_count(self.map.mode());
        if key_count % 2 == 0 {
            self.overall_difficulty = self.compute_for_overall_difficulty(rate);
        } else {
            let left_diff = self.compute_for_overall_difficulty_with_hand(rate, Hand::Left);
            let right_diff = self.compute_for_overall_difficulty_with_hand(rate, Hand::Right);
            self.overall_difficulty = (left_diff + right_diff) / 2.0;
        }
    }
    
    /// Calculate overall difficulty of a map
    fn compute_for_overall_difficulty(&mut self, rate: f32) -> f32 {
        self.compute_for_overall_difficulty_with_hand(rate, Hand::Right)
    }
    
    /// Calculate overall difficulty of a map with assumed hand
    fn compute_for_overall_difficulty_with_hand(&mut self, rate: f32, assume_hand: Hand) -> f32 {
        self.compute_note_density_data(rate);
        self.compute_base_strain_states(rate, assume_hand);
        self.compute_for_chords();
        self.compute_for_finger_actions();
        self.compute_for_roll_manipulation();
        self.compute_for_jack_manipulation();
        self.compute_for_ln_multiplier();
        
        // Calculate overall difficulty using the full algorithm
        self.calculate_overall_difficulty()
    }

    /// Calculate overall difficulty using the full algorithm with continuity adjustment
    fn calculate_overall_difficulty(&mut self) -> f32 {
        // When the map has only scratch key notes, StrainSolverData would be empty, so we return 0
        if self.strain_solver_data.is_empty() {
            return 0.0;
        }

        // Solve strain value of every data point
        for i in 0..self.strain_solver_data.len() {
            self.strain_solver_data[i].calculate_strain_value();
        }

        let calculated_diff = self.strain_solver_data
            .iter()
            .filter(|s| matches!(s.hand, Hand::Left | Hand::Right))
            .map(|s| s.total_strain_value)
            .sum::<f32>()
            / self.strain_solver_data
                .iter()
                .filter(|s| matches!(s.hand, Hand::Left | Hand::Right))
                .count() as f32;

        // Create bins for continuity calculation
        let mut bins = Vec::new();

        let map_start = self.strain_solver_data
            .iter()
            .map(|s| s.start_time as i32)
            .min()
            .unwrap_or(0) as f32;
        let map_end = self.strain_solver_data
            .iter()
            .map(|s| (s.start_time.max(s.end_time) as i32))
            .max()
            .unwrap_or(0) as f32;

        let use_fallback = self.map.get_key_count(false) % 2 == 1;
        
        if use_fallback {
            // Fallback for odd key counts
            let mut current_time = map_start as i32;
            let map_end_int = map_end as i32;
            while current_time < map_end_int {
                let values_in_bin: Vec<&StrainSolverData> = self.strain_solver_data
                    .iter()
                    .filter(|s| s.start_time >= current_time as f32 && s.start_time < (current_time + 1000) as f32)
                    .collect();
                
                let average_rating = if !values_in_bin.is_empty() {
                    values_in_bin.iter().map(|s| s.total_strain_value).sum::<f32>() / values_in_bin.len() as f32
                } else {
                    0.0
                };
                
                bins.push(average_rating);
                current_time += 1000;
            }
        } else {
            // Optimized binning for even key counts
            let mut left_index = 0;
            let mut right_index = 0;
            
            // Find starting index
            while left_index < self.strain_solver_data.len() && self.strain_solver_data[left_index].start_time < map_start {
                left_index += 1;
            }
            
            let mut current_time = map_start as i32;
            let map_end_int = map_end as i32;
            while current_time < map_end_int {
                // Find right index for current bin
                while right_index < self.strain_solver_data.len() - 1 
                    && self.strain_solver_data[right_index + 1].start_time < (current_time + 1000) as f32 {
                    right_index += 1;
                }
                
                if left_index >= self.strain_solver_data.len() {
                    bins.push(0.0);
                    current_time += 1000;
                    continue;
                }
                
                let values_in_bin = &self.strain_solver_data[left_index..=right_index];
                let average_rating = if !values_in_bin.is_empty() {
                    values_in_bin.iter().map(|s| s.total_strain_value).sum::<f32>() / values_in_bin.len() as f32
                } else {
                    0.0
                };
                
                bins.push(average_rating);
                left_index = right_index + 1;
                current_time += 1000;
            }
        }

        if bins.iter().all(|&strain| strain <= 0.0) {
            return 0.0;
        }

        // Use the calculations module for consistency
        use crate::difficulty_processor::calculations::{calculate_continuity_adjustment, calculate_short_map_adjustment};
        
        let (continuity_adjustment, continuity) = calculate_continuity_adjustment(&bins);
        let short_map_adjustment = calculate_short_map_adjustment(&bins, continuity);

        calculated_diff * continuity_adjustment * short_map_adjustment
    }
    
    /// Get Note Data, and compute the base strain weights
    fn compute_base_strain_states(&mut self, rate: f32, assume_hand: Hand) {
        let key_count = mode_to_key_count(self.map.mode());
        
        for hit_object in &self.map.hit_objects {
            if self.map.has_scratch_key() && hit_object.lane == key_count {
                continue;
            }
            
            let strain_hit_object = StrainSolverHitObject::new(hit_object.clone());
            let mut strain_data = StrainSolverData::new(strain_hit_object, rate);
            
            // Assign Finger and Hand States
            if let Ok(finger_state) = lane_to_finger(hit_object.lane, key_count) {
                strain_data.hit_objects[0].finger_state = finger_state;
            }
            
            if let Ok(hand) = lane_to_hand(hit_object.lane, key_count) {
                strain_data.hand = match hand {
                    Hand::Ambiguous => assume_hand,
                    _ => hand,
                };
            }
            
            self.strain_solver_data.push(strain_data);
        }
    }
    
    /// Iterate through the HitObject list and merges the chords together into one data point
    fn compute_for_chords(&mut self) {
        let mut i = 0;
        while i < self.strain_solver_data.len() - 1 {
            let mut j = i + 1;
            while j < self.strain_solver_data.len() {
                let ms_diff = self.strain_solver_data[j].start_time - self.strain_solver_data[i].start_time;
                
                // Check if next hit object is way past the tolerance
                if ms_diff > self.strain_constants.chord_clump_tolerance_ms {
                    break;
                }
                
                // Check if the next and current hit objects are chord-able
                if ms_diff.abs() <= self.strain_constants.chord_clump_tolerance_ms {
                    if self.strain_solver_data[i].hand == self.strain_solver_data[j].hand {
                        // Merge chord objects
                        let mut hit_objects_to_add = Vec::new();
                        for hit_obj in &self.strain_solver_data[j].hit_objects {
                            let same_state_found = self.strain_solver_data[i].hit_objects
                                .iter()
                                .any(|existing| existing.finger_state == hit_obj.finger_state);
                            
                            if !same_state_found {
                                hit_objects_to_add.push(hit_obj.clone());
                            }
                        }
                        
                        self.strain_solver_data[i].hit_objects.extend(hit_objects_to_add);
                        self.strain_solver_data.remove(j);
                        continue;
                    }
                }
                j += 1;
            }
            i += 1;
        }
        
        // Solve finger state of every object once chords have been found and applied
        for i in 0..self.strain_solver_data.len() {
            self.strain_solver_data[i].solve_finger_state();
        }
    }
    
    /// Scans every finger state, and determines its action
    fn compute_for_finger_actions(&mut self) {
        for i in 0..self.strain_solver_data.len() - 1 {
            // Find the next Hit Object in the current Hit Object's Hand
            for j in i + 1..self.strain_solver_data.len() {
                if self.strain_solver_data[i].hand == self.strain_solver_data[j].hand 
                    && self.strain_solver_data[j].start_time > self.strain_solver_data[i].start_time {
                    // Determine finger action
                    let action_jack_found = (self.strain_solver_data[i].finger_state & self.strain_solver_data[j].finger_state) != FingerState::NONE;
                    let action_chord_found = self.strain_solver_data[i].hand_chord() || self.strain_solver_data[j].hand_chord();
                    let action_same_state = self.strain_solver_data[i].finger_state == self.strain_solver_data[j].finger_state;
                    let action_duration = self.strain_solver_data[j].start_time - self.strain_solver_data[i].start_time;
                    
                    // Apply the "NextStrainSolverDataOnCurrentHand" value
                    self.strain_solver_data[i].next_strain_solver_data_on_current_hand = Some(Box::new(self.strain_solver_data[j].clone()));
                    self.strain_solver_data[i].finger_action_duration_ms = action_duration;
                    
                    // Determine action type and coefficient
                    if !action_chord_found && !action_same_state {
                        self.strain_solver_data[i].finger_action = FingerAction::Roll;
                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
                            action_duration,
                            self.strain_constants.roll_lower_boundary_ms,
                            self.strain_constants.roll_upper_boundary_ms,
                            self.strain_constants.roll_max_strain_value,
                            self.strain_constants.roll_curve_exponential,
                            self.average_note_density,
                        );
                    } else if action_same_state {
                        self.strain_solver_data[i].finger_action = FingerAction::SimpleJack;
                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
                            action_duration,
                            self.strain_constants.s_jack_lower_boundary_ms,
                            self.strain_constants.s_jack_upper_boundary_ms,
                            self.strain_constants.s_jack_max_strain_value,
                            self.strain_constants.s_jack_curve_exponential,
                            self.average_note_density,
                        );
                    } else if action_jack_found {
                        self.strain_solver_data[i].finger_action = FingerAction::TechnicalJack;
                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
                            action_duration,
                            self.strain_constants.t_jack_lower_boundary_ms,
                            self.strain_constants.t_jack_upper_boundary_ms,
                            self.strain_constants.t_jack_max_strain_value,
                            self.strain_constants.t_jack_curve_exponential,
                            self.average_note_density,
                        );
                    } else {
                        self.strain_solver_data[i].finger_action = FingerAction::Bracket;
                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
                            action_duration,
                            self.strain_constants.bracket_lower_boundary_ms,
                            self.strain_constants.bracket_upper_boundary_ms,
                            self.strain_constants.bracket_max_strain_value,
                            self.strain_constants.bracket_curve_exponential,
                            self.average_note_density,
                        );
                    }
                    break;
                }
            }
        }
    }
    
    /// Scans for roll manipulation
    fn compute_for_roll_manipulation(&mut self) {
        let mut manipulation_index = 0;
        
        for data in &mut self.strain_solver_data {
            let mut manipulation_found = false;
            
            if let Some(ref next) = data.next_strain_solver_data_on_current_hand {
                if let Some(ref next_next) = next.next_strain_solver_data_on_current_hand {
                    if data.finger_action == FingerAction::Roll && next.finger_action == FingerAction::Roll {
                        if data.finger_state == next_next.finger_state {
                            let duration_ratio = (data.finger_action_duration_ms / next.finger_action_duration_ms)
                                .max(next.finger_action_duration_ms / data.finger_action_duration_ms);
                            
                            if duration_ratio >= self.strain_constants.roll_ratio_tolerance_ms {
                                let duration_multiplier = 1.0 / (1.0 + (duration_ratio - 1.0) * self.strain_constants.roll_ratio_multiplier);
                                let manipulation_found_ratio = 1.0 - manipulation_index as f32 / self.strain_constants.roll_max_length * (1.0 - self.strain_constants.roll_length_multiplier);
                                
                                data.roll_manipulation_strain_multiplier = duration_multiplier * manipulation_found_ratio;
                                
                                manipulation_found = true;
                                self.roll_inaccuracy_confidence += 1.0;
                                
                                if manipulation_index < self.strain_constants.roll_max_length as usize {
                                    manipulation_index += 1;
                                }
                            }
                        }
                    }
                }
            }
            
            if !manipulation_found && manipulation_index > 0 {
                manipulation_index -= 1;
            }
        }
    }
    
    /// Scans for jack manipulation
    fn compute_for_jack_manipulation(&mut self) {
        let mut long_jack_size = 0;
        
        for data in &mut self.strain_solver_data {
            let mut manipulation_found = false;
            
            if let Some(ref next) = data.next_strain_solver_data_on_current_hand {
                if data.finger_action == FingerAction::SimpleJack && next.finger_action == FingerAction::SimpleJack {
                    let duration_value = ((self.strain_constants.vibro_action_duration_ms + self.strain_constants.vibro_action_tolerance_ms - data.finger_action_duration_ms) / self.strain_constants.vibro_action_tolerance_ms)
                        .min(1.0)
                        .max(0.0);
                    
                    let duration_multiplier = 1.0 - duration_value * (1.0 - self.strain_constants.vibro_multiplier);
                    let manipulation_found_ratio = 1.0 - long_jack_size as f32 / self.strain_constants.vibro_max_length * (1.0 - self.strain_constants.vibro_length_multiplier);
                    
                    data.roll_manipulation_strain_multiplier = duration_multiplier * manipulation_found_ratio;
                    
                    manipulation_found = true;
                    self.vibro_inaccuracy_confidence += 1.0;
                    
                    if long_jack_size < self.strain_constants.vibro_max_length as usize {
                        long_jack_size += 1;
                    }
                }
            }
            
            if !manipulation_found {
                long_jack_size = 0;
            }
        }
    }
    
    /// Scans for LN layering and applies a multiplier
    fn compute_for_ln_multiplier(&mut self) {
        for data in &mut self.strain_solver_data {
            if data.end_time > data.start_time {
                let duration_value = 1.0 - ((self.strain_constants.ln_layer_threshold_ms + self.strain_constants.ln_layer_tolerance_ms - (data.end_time - data.start_time)) / self.strain_constants.ln_layer_tolerance_ms)
                    .min(1.0)
                    .max(0.0);
                
                let base_multiplier = 1.0 + duration_value * self.strain_constants.ln_base_multiplier;
                
                for hit_object in &mut data.hit_objects {
                    hit_object.ln_strain_multiplier = base_multiplier;
                }
                
                if let Some(ref next) = data.next_strain_solver_data_on_current_hand {
                    if next.start_time < data.end_time - self.strain_constants.ln_end_threshold_ms {
                        if next.start_time >= data.start_time + self.strain_constants.ln_end_threshold_ms {
                            if next.end_time > data.end_time + self.strain_constants.ln_end_threshold_ms {
                                for hit_object in &mut data.hit_objects {
                                    hit_object.ln_layer_type = LnLayerType::OutsideRelease;
                                    hit_object.ln_strain_multiplier *= self.strain_constants.ln_release_after_multiplier;
                                }
                            } else if next.end_time > 0.0 {
                                for hit_object in &mut data.hit_objects {
                                    hit_object.ln_layer_type = LnLayerType::InsideRelease;
                                    hit_object.ln_strain_multiplier *= self.strain_constants.ln_release_before_multiplier;
                                }
                            } else {
                                for hit_object in &mut data.hit_objects {
                                    hit_object.ln_layer_type = LnLayerType::InsideTap;
                                    hit_object.ln_strain_multiplier *= self.strain_constants.ln_tap_multiplier;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    /// Compute and generate Note Density Data
    fn compute_note_density_data(&mut self, rate: f32) {
        self.average_note_density = SECONDS_TO_MILLISECONDS * self.map.hit_objects.len() as f32 
            / (self.map.length() * (-0.5 * rate + 1.5));
    }
    
}