ddr::ssq::Difficulty: Refactor and rename to Level

* New name reflects contents better
 * Uses explicit functions instead of Into for only converting the
   difficulty
 * Uses errors instead of defaults for invalid values
 * Difficulty mapping is separate function
 * Simplified tests
This commit is contained in:
Simon Bruder 2020-07-23 15:16:14 +02:00
parent 161f472a19
commit f1d460cffe
No known key found for this signature in database
GPG key ID: 6F03E0000CC5B62F
2 changed files with 112 additions and 77 deletions

View file

@ -316,7 +316,7 @@ impl ssq::Step {
} }
struct ConvertedChart { struct ConvertedChart {
difficulty: ssq::Difficulty, level: ssq::Level,
hit_objects: beatmap::HitObjects, hit_objects: beatmap::HitObjects,
timing_points: beatmap::TimingPoints, timing_points: beatmap::TimingPoints,
} }
@ -350,18 +350,18 @@ impl ConvertedChart {
creator: format!("{}", config), creator: format!("{}", config),
version: match &config.metadata.levels { version: match &config.metadata.levels {
Some(levels) => { Some(levels) => {
let level = self.difficulty.to_level(levels); let level = self.level.to_value(levels);
format!("{} (Lv. {})", self.difficulty, level) format!("{} (Lv. {})", self.level, level)
} }
None => format!("{}", self.difficulty), None => format!("{}", self.level),
}, },
source: config.metadata.source.clone(), source: config.metadata.source.clone(),
tags: vec![], tags: vec![],
}, },
difficulty: beatmap::Difficulty { difficulty: beatmap::Difficulty {
hp_drain_rate: config.hp_drain.map_from(self.difficulty.clone().into()), hp_drain_rate: config.hp_drain.map_from(self.level.relative_difficulty()),
circle_size: f32::from(self.difficulty.players) * 4.0, circle_size: f32::from(self.level.players) * 4.0,
overall_difficulty: config.accuracy.map_from(self.difficulty.clone().into()), overall_difficulty: config.accuracy.map_from(self.level.relative_difficulty()),
approach_rate: 8.0, approach_rate: 8.0,
slider_multiplier: 0.64, slider_multiplier: 0.64,
slider_tick_rate: 1.0, slider_tick_rate: 1.0,
@ -411,7 +411,7 @@ impl ssq::SSQ {
} }
let converted_chart = ConvertedChart { let converted_chart = ConvertedChart {
difficulty: chart.difficulty.clone(), level: chart.difficulty.clone(),
hit_objects, hit_objects,
timing_points: timing_points.clone(), timing_points: timing_points.clone(),
}; };

View file

@ -1,5 +1,5 @@
use std::convert::From; use std::convert::From;
use std::convert::TryInto; use std::convert::{TryFrom, TryInto};
use std::fmt; use std::fmt;
use std::io; use std::io;
use std::io::prelude::*; use std::io::prelude::*;
@ -22,6 +22,8 @@ pub enum Error {
NotEnoughFreezeData, NotEnoughFreezeData,
#[error("Invalid player count {0} (valid options: 1, 2)")] #[error("Invalid player count {0} (valid options: 1, 2)")]
InvalidPlayerCount(u8), InvalidPlayerCount(u8),
#[error("Invalid difficulty {0} (valid options: 4, 1, 2, 3, 6)")]
InvalidDifficulty(u8),
#[error(transparent)] #[error(transparent)]
IOError(#[from] io::Error), IOError(#[from] io::Error),
#[error(transparent)] #[error(transparent)]
@ -216,13 +218,13 @@ pub enum Step {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Chart { pub struct Chart {
pub difficulty: Difficulty, pub difficulty: Level,
pub steps: Vec<Step>, pub steps: Vec<Step>,
} }
impl Chart { impl Chart {
fn parse(data: &[u8], parameter: u16) -> Result<Self, Error> { fn parse(data: &[u8], parameter: u16) -> Result<Self, Error> {
let difficulty: Difficulty = parameter.into(); let difficulty: Level = parameter.try_into()?;
let mut cursor = Cursor::new(data); let mut cursor = Cursor::new(data);
@ -330,55 +332,86 @@ impl Chart {
} }
} }
/// Metadata about a level.
///
/// It does not store the level visible to the user. However it can when povided with
/// [`ddr::musicdb::Entry.diff_lv`] return the user visible level with the [`to_value`] method.
///
/// [`ddr::musicdb::Entry.diff_lv`]: ../musicdb/struct.Entry.html#structfield.diff_lv
/// [`to_value`]: #method.to_value
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Difficulty { pub struct Level {
pub players: u8, pub players: u8,
difficulty: u8, pub difficulty: u8,
} }
impl From<u16> for Difficulty { impl Level {
fn from(parameter: u16) -> Self { /// Creates a new instance and maps the SSQ difficulty to ordered difficulty.
Self { ///
difficulty: ((parameter & 0xFF00) >> 8) as u8, /// # Errors
players: (parameter & 0xF) as u8 / 4, ///
/// This checks if the players and difficulty and valid, otherwise it returns
/// [`InvalidPlayerCount`] or [`InvalidDifficulty`].
///
/// [`InvalidPlayerCount`]: enum.Error.html#variant.InvalidPlayerCount
/// [`InvalidDifficulty`]: enum.Error.html#variant.InvalidDifficulty
pub fn new(players: u8, ssq_difficulty: u8) -> Result<Self, Error> {
if ![1, 2].contains(&players) {
return Err(Error::InvalidPlayerCount(players));
} }
let difficulty = Self::ssq_to_ordered(&ssq_difficulty)?;
Ok(Level {
players,
difficulty,
})
} }
}
impl Into<u8> for Difficulty { /// The SSQ file stores the difficulty in a format, where the lower numeric value does not mean
fn into(self) -> u8 { /// the easier level. This function maps the values from the SSQ file to ordered values (lower
match self.difficulty { /// values → easier, higher values → harder)
///
/// # Errors
///
/// This checks if the difficulty is valid, otherwise it returns [`InvalidDifficulty`].
///
/// [`InvalidDifficulty`]: enum.Error.html#variant.InvalidDifficulty
fn ssq_to_ordered(ssq_difficulty: &u8) -> Result<u8, Error> {
Ok(match ssq_difficulty {
4 => 0,
1 => 1, 1 => 1,
2 => 2, 2 => 2,
3 => 3, 3 => 3,
4 => 0,
6 => 4, 6 => 4,
_ => 4, _ => return Err(Error::InvalidDifficulty(*ssq_difficulty)),
} })
} }
}
impl Into<f32> for Difficulty { /// Returns the difficulty as `f32` where 0.0 is the easiest and 1.0 is the hardest.
fn into(self) -> f32 { pub fn relative_difficulty(&self) -> f32 {
let difficulty: u8 = self.into(); f32::from(self.difficulty) / 4.0
f32::from(difficulty) / 4.0
} }
}
impl Difficulty { /// Returns the user visible level value for difficulty from [`ddr::musicdb::Entry.diff_lv`].
/// Gets level for difficulty from [`ddr::musicdb::Entry.diff_lv`].
/// ///
/// [`ddr::musicdb::Entry.diff_lv`]: ../musicdb/struct.Entry.html#structfield.diff_lv /// [`ddr::musicdb::Entry.diff_lv`]: ../musicdb/struct.Entry.html#structfield.diff_lv
pub fn to_level(&self, levels: &[u8]) -> u8 { pub fn to_value(&self, levels: &[u8]) -> u8 {
let base: u8 = self.clone().into(); let index: usize = (self.difficulty + (self.players - 1) * 5).into();
let index: usize = (base + (self.players - 1) * 5).into();
levels[index] levels[index]
} }
} }
impl fmt::Display for Difficulty { impl TryFrom<u16> for Level {
type Error = Error;
fn try_from(parameter: u16) -> Result<Self, Error> {
let players = (parameter & 0xF) as u8 / 4;
let difficulty = ((parameter & 0xFF00) >> 8) as u8;
Self::new(players, difficulty)
}
}
impl fmt::Display for Level {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let players = match self.players { let players = match self.players {
1 => "Single", 1 => "Single",
@ -432,7 +465,7 @@ impl SSQ {
ssq.tempo_changes = TempoChanges::parse(parameter.into(), &data)?; ssq.tempo_changes = TempoChanges::parse(parameter.into(), &data)?;
} }
3 => { 3 => {
debug!("Parsing step chunk ({})", Difficulty::from(parameter)); debug!("Parsing step chunk ({})", Level::try_from(parameter)?);
ssq.charts.push(Chart::parse(&data, parameter)?) ssq.charts.push(Chart::parse(&data, parameter)?)
} }
_ => { _ => {
@ -597,43 +630,48 @@ mod tests {
} }
#[test] #[test]
fn test_difficuly_from_u16() { fn test_difficulty_ssq_to_ordered() {
let values = [(0b0000010000001000, 4, 2), (0b0000011000000100, 6, 1)]; let ssq_difficulties = vec![4, 1, 2, 3, 6];
for (data, difficulty, players) in values.iter() { let difficulties = ssq_difficulties
let diff = Difficulty::from(*data); .iter()
assert_eq!(diff.players, *players); .map(Level::ssq_to_ordered)
assert_eq!(diff.difficulty, *difficulty); .map(|d| d.unwrap())
.collect::<Vec<u8>>();
// check if ordered ascending
for window in difficulties.windows(2) {
assert!(window[0] < window[1]);
} }
} }
#[test] #[test]
fn test_difficulty_into() { fn test_difficuly_new() {
let sorted_difficulties: Vec<Difficulty> = vec![4, 1, 2, 3, 6] assert_eq!(Level::new(2, 4).unwrap().difficulty, 0);
.iter() assert!(Level::new(0, 0).is_err());
.map(|difficulty| Difficulty { assert!(Level::new(4, 0).is_err());
players: 1, assert!(Level::new(1, 5).is_err());
difficulty: *difficulty, assert!(Level::new(1, 8).is_err());
}) }
.collect();
let difficulties_u8: Vec<u8> = sorted_difficulties #[test]
.iter() fn test_difficulty_relative() {
.map(|difficulty| difficulty.clone().into()) let mut difficulty = Level {
.collect(); players: 1,
for window in difficulties_u8.windows(2) { difficulty: 0,
assert_eq!(window.len(), 2); };
assert!(dbg!(window[0]) < dbg!(window[1])); assert_eq!(difficulty.relative_difficulty(), 0.0);
} difficulty.difficulty = 2;
for difficulty in &difficulties_u8 { assert_eq!(difficulty.relative_difficulty(), 0.5);
assert!(*difficulty <= 4); difficulty.difficulty = 4;
} assert_eq!(difficulty.relative_difficulty(), 1.0);
}
let difficulties_f32: Vec<f32> = sorted_difficulties #[test]
.iter() fn test_difficuly_from_u16() {
.map(|difficulty| difficulty.clone().into()) let values = [(0b0000010000001000, 0, 2), (0b0000011000000100, 4, 1)];
.collect(); for (data, difficulty, players) in values.iter() {
for (i, difficulty) in difficulties_f32.iter().enumerate() { let diff = Level::try_from(*data).unwrap();
assert_eq!((difficulty * 4.0) as u8, difficulties_u8[i]); assert_eq!(diff.players, *players);
assert_eq!(diff.difficulty, *difficulty);
} }
} }
@ -652,7 +690,7 @@ mod tests {
assert_eq!( assert_eq!(
format!( format!(
"{}", "{}",
Difficulty { Level {
players: *players, players: *players,
difficulty: *difficulty, difficulty: *difficulty,
} }
@ -663,16 +701,13 @@ mod tests {
} }
#[test] #[test]
fn test_difficulty_to_level() { fn test_difficulty_to_value() {
let levels: Vec<u8> = (1..=10).collect(); let levels: Vec<u8> = (1..=10).collect();
let mut last_level = 0; let mut last_level = 0;
for players in [1, 2].iter() { for players in [1, 2].iter() {
for difficulty in [4, 1, 2, 3, 6].iter() { for difficulty in [4, 1, 2, 3, 6].iter() {
let difficulty = Difficulty { let difficulty = Level::new(*players, *difficulty).unwrap();
players: *players, let level = difficulty.to_value(&levels);
difficulty: *difficulty,
};
let level = difficulty.to_level(&levels);
assert!(last_level < level); assert!(last_level < level);
last_level = level; last_level = level;
} }