quaver_rs/difficulty_processor/
processor.rs

1use crate::rulesets::structs::{StrainSolverData, Hand, FingerAction, StrainSolverHitObject, FingerState, LnLayerType};
2use crate::difficulty_processor::constants::{StrainConstants, StrainConstantsKeys};
3use crate::difficulty_processor::helpers::{lane_to_hand, lane_to_finger, get_rate_from_mods, mode_to_key_count};
4use crate::difficulty_processor::calculations::get_coefficient_value;
5use crate::qua::Qua;
6use crate::enums::{ModIdentifier, QssPatternFlags};
7
8/// Total amount of milliseconds in a second
9const SECONDS_TO_MILLISECONDS: f32 = 1000.0;
10
11/// Handles Difficulty Solving + Data
12#[derive(Debug, Clone)]
13pub struct DifficultyProcessor {
14    /// Current map for difficulty calculation
15    pub map: Qua,
16    
17    /// Overall Difficulty of a map
18    pub overall_difficulty: f32,
19    
20    /// Used to display prominent patterns of a map in the client
21    pub qss_pattern_flags: QssPatternFlags,
22    
23    /// Constants used for solving
24    pub strain_constants: StrainConstantsKeys,
25    
26    /// Average note density of the map
27    pub average_note_density: f32,
28    
29    /// Hit objects in the map used for solving difficulty
30    pub strain_solver_data: Vec<StrainSolverData>,
31    
32    /// Value of confidence that there's vibro manipulation in the calculated map
33    pub vibro_inaccuracy_confidence: f32,
34    
35    /// Value of confidence that there's roll manipulation in the calculated map
36    pub roll_inaccuracy_confidence: f32,
37}
38
39
40impl DifficultyProcessor {
41    /// Constructor
42    pub fn new(map: Qua, _constants: StrainConstants, _mods: ModIdentifier) -> Self {
43        let strain_constants = StrainConstantsKeys::new();
44        
45        Self {
46            map,
47            overall_difficulty: 0.0,
48            qss_pattern_flags: QssPatternFlags::UNKNOWN,
49            strain_constants,
50            average_note_density: 0.0,
51            strain_solver_data: Vec::new(),
52            vibro_inaccuracy_confidence: 0.0,
53            roll_inaccuracy_confidence: 0.0,
54        }
55    }
56    
57    /// Calculate difficulty of a map with given mods
58    pub fn calculate_difficulty(&mut self, mods: ModIdentifier) {
59        // If map does not exist, ignore calculation
60        if self.map.hit_objects.len() < 2 {
61            return;
62        }
63        
64        // Get song rate from selected mods
65        let rate = get_rate_from_mods(mods);
66        
67        // Compute for overall difficulty
68        let key_count = mode_to_key_count(self.map.mode());
69        if key_count % 2 == 0 {
70            self.overall_difficulty = self.compute_for_overall_difficulty(rate);
71        } else {
72            let left_diff = self.compute_for_overall_difficulty_with_hand(rate, Hand::Left);
73            let right_diff = self.compute_for_overall_difficulty_with_hand(rate, Hand::Right);
74            self.overall_difficulty = (left_diff + right_diff) / 2.0;
75        }
76    }
77    
78    /// Calculate overall difficulty of a map
79    fn compute_for_overall_difficulty(&mut self, rate: f32) -> f32 {
80        self.compute_for_overall_difficulty_with_hand(rate, Hand::Right)
81    }
82    
83    /// Calculate overall difficulty of a map with assumed hand
84    fn compute_for_overall_difficulty_with_hand(&mut self, rate: f32, assume_hand: Hand) -> f32 {
85        self.compute_note_density_data(rate);
86        self.compute_base_strain_states(rate, assume_hand);
87        self.compute_for_chords();
88        self.compute_for_finger_actions();
89        self.compute_for_roll_manipulation();
90        self.compute_for_jack_manipulation();
91        self.compute_for_ln_multiplier();
92        
93        // Calculate overall difficulty using the full algorithm
94        self.calculate_overall_difficulty()
95    }
96
97    /// Calculate overall difficulty using the full algorithm with continuity adjustment
98    fn calculate_overall_difficulty(&mut self) -> f32 {
99        // When the map has only scratch key notes, StrainSolverData would be empty, so we return 0
100        if self.strain_solver_data.is_empty() {
101            return 0.0;
102        }
103
104        // Solve strain value of every data point
105        for i in 0..self.strain_solver_data.len() {
106            self.strain_solver_data[i].calculate_strain_value();
107        }
108
109        let calculated_diff = self.strain_solver_data
110            .iter()
111            .filter(|s| matches!(s.hand, Hand::Left | Hand::Right))
112            .map(|s| s.total_strain_value)
113            .sum::<f32>()
114            / self.strain_solver_data
115                .iter()
116                .filter(|s| matches!(s.hand, Hand::Left | Hand::Right))
117                .count() as f32;
118
119        // Create bins for continuity calculation
120        let mut bins = Vec::new();
121
122        let map_start = self.strain_solver_data
123            .iter()
124            .map(|s| s.start_time as i32)
125            .min()
126            .unwrap_or(0) as f32;
127        let map_end = self.strain_solver_data
128            .iter()
129            .map(|s| (s.start_time.max(s.end_time) as i32))
130            .max()
131            .unwrap_or(0) as f32;
132
133        let use_fallback = self.map.get_key_count(false) % 2 == 1;
134        
135        if use_fallback {
136            // Fallback for odd key counts
137            let mut current_time = map_start as i32;
138            let map_end_int = map_end as i32;
139            while current_time < map_end_int {
140                let values_in_bin: Vec<&StrainSolverData> = self.strain_solver_data
141                    .iter()
142                    .filter(|s| s.start_time >= current_time as f32 && s.start_time < (current_time + 1000) as f32)
143                    .collect();
144                
145                let average_rating = if !values_in_bin.is_empty() {
146                    values_in_bin.iter().map(|s| s.total_strain_value).sum::<f32>() / values_in_bin.len() as f32
147                } else {
148                    0.0
149                };
150                
151                bins.push(average_rating);
152                current_time += 1000;
153            }
154        } else {
155            // Optimized binning for even key counts
156            let mut left_index = 0;
157            let mut right_index = 0;
158            
159            // Find starting index
160            while left_index < self.strain_solver_data.len() && self.strain_solver_data[left_index].start_time < map_start {
161                left_index += 1;
162            }
163            
164            let mut current_time = map_start as i32;
165            let map_end_int = map_end as i32;
166            while current_time < map_end_int {
167                // Find right index for current bin
168                while right_index < self.strain_solver_data.len() - 1 
169                    && self.strain_solver_data[right_index + 1].start_time < (current_time + 1000) as f32 {
170                    right_index += 1;
171                }
172                
173                if left_index >= self.strain_solver_data.len() {
174                    bins.push(0.0);
175                    current_time += 1000;
176                    continue;
177                }
178                
179                let values_in_bin = &self.strain_solver_data[left_index..=right_index];
180                let average_rating = if !values_in_bin.is_empty() {
181                    values_in_bin.iter().map(|s| s.total_strain_value).sum::<f32>() / values_in_bin.len() as f32
182                } else {
183                    0.0
184                };
185                
186                bins.push(average_rating);
187                left_index = right_index + 1;
188                current_time += 1000;
189            }
190        }
191
192        if bins.iter().all(|&strain| strain <= 0.0) {
193            return 0.0;
194        }
195
196        // Use the calculations module for consistency
197        use crate::difficulty_processor::calculations::{calculate_continuity_adjustment, calculate_short_map_adjustment};
198        
199        let (continuity_adjustment, continuity) = calculate_continuity_adjustment(&bins);
200        let short_map_adjustment = calculate_short_map_adjustment(&bins, continuity);
201
202        calculated_diff * continuity_adjustment * short_map_adjustment
203    }
204    
205    /// Get Note Data, and compute the base strain weights
206    fn compute_base_strain_states(&mut self, rate: f32, assume_hand: Hand) {
207        let key_count = mode_to_key_count(self.map.mode());
208        
209        for hit_object in &self.map.hit_objects {
210            if self.map.has_scratch_key() && hit_object.lane == key_count {
211                continue;
212            }
213            
214            let strain_hit_object = StrainSolverHitObject::new(hit_object.clone());
215            let mut strain_data = StrainSolverData::new(strain_hit_object, rate);
216            
217            // Assign Finger and Hand States
218            if let Ok(finger_state) = lane_to_finger(hit_object.lane, key_count) {
219                strain_data.hit_objects[0].finger_state = finger_state;
220            }
221            
222            if let Ok(hand) = lane_to_hand(hit_object.lane, key_count) {
223                strain_data.hand = match hand {
224                    Hand::Ambiguous => assume_hand,
225                    _ => hand,
226                };
227            }
228            
229            self.strain_solver_data.push(strain_data);
230        }
231    }
232    
233    /// Iterate through the HitObject list and merges the chords together into one data point
234    fn compute_for_chords(&mut self) {
235        let mut i = 0;
236        while i < self.strain_solver_data.len() - 1 {
237            let mut j = i + 1;
238            while j < self.strain_solver_data.len() {
239                let ms_diff = self.strain_solver_data[j].start_time - self.strain_solver_data[i].start_time;
240                
241                // Check if next hit object is way past the tolerance
242                if ms_diff > self.strain_constants.chord_clump_tolerance_ms {
243                    break;
244                }
245                
246                // Check if the next and current hit objects are chord-able
247                if ms_diff.abs() <= self.strain_constants.chord_clump_tolerance_ms {
248                    if self.strain_solver_data[i].hand == self.strain_solver_data[j].hand {
249                        // Merge chord objects
250                        let mut hit_objects_to_add = Vec::new();
251                        for hit_obj in &self.strain_solver_data[j].hit_objects {
252                            let same_state_found = self.strain_solver_data[i].hit_objects
253                                .iter()
254                                .any(|existing| existing.finger_state == hit_obj.finger_state);
255                            
256                            if !same_state_found {
257                                hit_objects_to_add.push(hit_obj.clone());
258                            }
259                        }
260                        
261                        self.strain_solver_data[i].hit_objects.extend(hit_objects_to_add);
262                        self.strain_solver_data.remove(j);
263                        continue;
264                    }
265                }
266                j += 1;
267            }
268            i += 1;
269        }
270        
271        // Solve finger state of every object once chords have been found and applied
272        for i in 0..self.strain_solver_data.len() {
273            self.strain_solver_data[i].solve_finger_state();
274        }
275    }
276    
277    /// Scans every finger state, and determines its action
278    fn compute_for_finger_actions(&mut self) {
279        for i in 0..self.strain_solver_data.len() - 1 {
280            // Find the next Hit Object in the current Hit Object's Hand
281            for j in i + 1..self.strain_solver_data.len() {
282                if self.strain_solver_data[i].hand == self.strain_solver_data[j].hand 
283                    && self.strain_solver_data[j].start_time > self.strain_solver_data[i].start_time {
284                    // Determine finger action
285                    let action_jack_found = (self.strain_solver_data[i].finger_state & self.strain_solver_data[j].finger_state) != FingerState::NONE;
286                    let action_chord_found = self.strain_solver_data[i].hand_chord() || self.strain_solver_data[j].hand_chord();
287                    let action_same_state = self.strain_solver_data[i].finger_state == self.strain_solver_data[j].finger_state;
288                    let action_duration = self.strain_solver_data[j].start_time - self.strain_solver_data[i].start_time;
289                    
290                    // Apply the "NextStrainSolverDataOnCurrentHand" value
291                    self.strain_solver_data[i].next_strain_solver_data_on_current_hand = Some(Box::new(self.strain_solver_data[j].clone()));
292                    self.strain_solver_data[i].finger_action_duration_ms = action_duration;
293                    
294                    // Determine action type and coefficient
295                    if !action_chord_found && !action_same_state {
296                        self.strain_solver_data[i].finger_action = FingerAction::Roll;
297                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
298                            action_duration,
299                            self.strain_constants.roll_lower_boundary_ms,
300                            self.strain_constants.roll_upper_boundary_ms,
301                            self.strain_constants.roll_max_strain_value,
302                            self.strain_constants.roll_curve_exponential,
303                            self.average_note_density,
304                        );
305                    } else if action_same_state {
306                        self.strain_solver_data[i].finger_action = FingerAction::SimpleJack;
307                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
308                            action_duration,
309                            self.strain_constants.s_jack_lower_boundary_ms,
310                            self.strain_constants.s_jack_upper_boundary_ms,
311                            self.strain_constants.s_jack_max_strain_value,
312                            self.strain_constants.s_jack_curve_exponential,
313                            self.average_note_density,
314                        );
315                    } else if action_jack_found {
316                        self.strain_solver_data[i].finger_action = FingerAction::TechnicalJack;
317                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
318                            action_duration,
319                            self.strain_constants.t_jack_lower_boundary_ms,
320                            self.strain_constants.t_jack_upper_boundary_ms,
321                            self.strain_constants.t_jack_max_strain_value,
322                            self.strain_constants.t_jack_curve_exponential,
323                            self.average_note_density,
324                        );
325                    } else {
326                        self.strain_solver_data[i].finger_action = FingerAction::Bracket;
327                        self.strain_solver_data[i].action_strain_coefficient = get_coefficient_value(
328                            action_duration,
329                            self.strain_constants.bracket_lower_boundary_ms,
330                            self.strain_constants.bracket_upper_boundary_ms,
331                            self.strain_constants.bracket_max_strain_value,
332                            self.strain_constants.bracket_curve_exponential,
333                            self.average_note_density,
334                        );
335                    }
336                    break;
337                }
338            }
339        }
340    }
341    
342    /// Scans for roll manipulation
343    fn compute_for_roll_manipulation(&mut self) {
344        let mut manipulation_index = 0;
345        
346        for data in &mut self.strain_solver_data {
347            let mut manipulation_found = false;
348            
349            if let Some(ref next) = data.next_strain_solver_data_on_current_hand {
350                if let Some(ref next_next) = next.next_strain_solver_data_on_current_hand {
351                    if data.finger_action == FingerAction::Roll && next.finger_action == FingerAction::Roll {
352                        if data.finger_state == next_next.finger_state {
353                            let duration_ratio = (data.finger_action_duration_ms / next.finger_action_duration_ms)
354                                .max(next.finger_action_duration_ms / data.finger_action_duration_ms);
355                            
356                            if duration_ratio >= self.strain_constants.roll_ratio_tolerance_ms {
357                                let duration_multiplier = 1.0 / (1.0 + (duration_ratio - 1.0) * self.strain_constants.roll_ratio_multiplier);
358                                let manipulation_found_ratio = 1.0 - manipulation_index as f32 / self.strain_constants.roll_max_length * (1.0 - self.strain_constants.roll_length_multiplier);
359                                
360                                data.roll_manipulation_strain_multiplier = duration_multiplier * manipulation_found_ratio;
361                                
362                                manipulation_found = true;
363                                self.roll_inaccuracy_confidence += 1.0;
364                                
365                                if manipulation_index < self.strain_constants.roll_max_length as usize {
366                                    manipulation_index += 1;
367                                }
368                            }
369                        }
370                    }
371                }
372            }
373            
374            if !manipulation_found && manipulation_index > 0 {
375                manipulation_index -= 1;
376            }
377        }
378    }
379    
380    /// Scans for jack manipulation
381    fn compute_for_jack_manipulation(&mut self) {
382        let mut long_jack_size = 0;
383        
384        for data in &mut self.strain_solver_data {
385            let mut manipulation_found = false;
386            
387            if let Some(ref next) = data.next_strain_solver_data_on_current_hand {
388                if data.finger_action == FingerAction::SimpleJack && next.finger_action == FingerAction::SimpleJack {
389                    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)
390                        .min(1.0)
391                        .max(0.0);
392                    
393                    let duration_multiplier = 1.0 - duration_value * (1.0 - self.strain_constants.vibro_multiplier);
394                    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);
395                    
396                    data.roll_manipulation_strain_multiplier = duration_multiplier * manipulation_found_ratio;
397                    
398                    manipulation_found = true;
399                    self.vibro_inaccuracy_confidence += 1.0;
400                    
401                    if long_jack_size < self.strain_constants.vibro_max_length as usize {
402                        long_jack_size += 1;
403                    }
404                }
405            }
406            
407            if !manipulation_found {
408                long_jack_size = 0;
409            }
410        }
411    }
412    
413    /// Scans for LN layering and applies a multiplier
414    fn compute_for_ln_multiplier(&mut self) {
415        for data in &mut self.strain_solver_data {
416            if data.end_time > data.start_time {
417                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)
418                    .min(1.0)
419                    .max(0.0);
420                
421                let base_multiplier = 1.0 + duration_value * self.strain_constants.ln_base_multiplier;
422                
423                for hit_object in &mut data.hit_objects {
424                    hit_object.ln_strain_multiplier = base_multiplier;
425                }
426                
427                if let Some(ref next) = data.next_strain_solver_data_on_current_hand {
428                    if next.start_time < data.end_time - self.strain_constants.ln_end_threshold_ms {
429                        if next.start_time >= data.start_time + self.strain_constants.ln_end_threshold_ms {
430                            if next.end_time > data.end_time + self.strain_constants.ln_end_threshold_ms {
431                                for hit_object in &mut data.hit_objects {
432                                    hit_object.ln_layer_type = LnLayerType::OutsideRelease;
433                                    hit_object.ln_strain_multiplier *= self.strain_constants.ln_release_after_multiplier;
434                                }
435                            } else if next.end_time > 0.0 {
436                                for hit_object in &mut data.hit_objects {
437                                    hit_object.ln_layer_type = LnLayerType::InsideRelease;
438                                    hit_object.ln_strain_multiplier *= self.strain_constants.ln_release_before_multiplier;
439                                }
440                            } else {
441                                for hit_object in &mut data.hit_objects {
442                                    hit_object.ln_layer_type = LnLayerType::InsideTap;
443                                    hit_object.ln_strain_multiplier *= self.strain_constants.ln_tap_multiplier;
444                                }
445                            }
446                        }
447                    }
448                }
449            }
450        }
451    }
452    
453    /// Compute and generate Note Density Data
454    fn compute_note_density_data(&mut self, rate: f32) {
455        self.average_note_density = SECONDS_TO_MILLISECONDS * self.map.hit_objects.len() as f32 
456            / (self.map.length() * (-0.5 * rate + 1.5));
457    }
458    
459}