diff --git a/Cargo.lock b/Cargo.lock index c69e8ee..0e0bed0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,7 +1396,7 @@ dependencies = [ [[package]] name = "ruborute" -version = "0.1.3" +version = "0.2.0" dependencies = [ "clap", "derive-getters", diff --git a/Cargo.toml b/Cargo.toml index fd339fb..f90dbdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ authors = ["RinChanNOW "] description = "ruborute is a command-line tool to get asphyxia@sdvx gaming data." edition = "2021" name = "ruborute" -version = "0.1.3" +version = "0.2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/example/config.toml b/example/config.toml index 0095995..68604dd 100644 --- a/example/config.toml +++ b/example/config.toml @@ -8,4 +8,5 @@ db_name = "bemani" db_password = "bemani" db_port = 3306 db_user = "bemani" +game_version = 6 username = "rinchannow" diff --git a/src/command/command.rs b/src/command/command.rs index 19af97c..69670d6 100644 --- a/src/command/command.rs +++ b/src/command/command.rs @@ -197,17 +197,7 @@ impl Cmd for CmdCount { } else { return Err(Error::DoCmdError(String::from("args unmatched."))); }; - let mut tab = table!([ - "level", - "S", - "AAA+", - "AAA", - "PUC", - "UC", - "HC", - "NC", - "played/total" - ]); + let mut tab = table!(["level", "S", "AAA+", "AAA", "PUC", "UC", "HC", "NC", "played"]); for s in stats.iter() { tab.add_row(row![ s.level(), @@ -218,7 +208,7 @@ impl Cmd for CmdCount { s.uc_num(), s.hc_num(), s.nc_num(), - format!("{}/{}", s.played(), self.store.get_level_count(*s.level())), + format!("{}", s.played()), ]); } tab.printstd(); diff --git a/src/config/bemaniutils_config.rs b/src/config/bemaniutils_config.rs index 96df248..cb0474b 100644 --- a/src/config/bemaniutils_config.rs +++ b/src/config/bemaniutils_config.rs @@ -28,6 +28,9 @@ pub struct BemaniutilsConfig { #[clap(long, default_value = "", help = "the user to query")] pub username: String, + + #[clap(long, default_value = "6", help = "the game version")] + pub game_version: u8, } impl Default for BemaniutilsConfig { @@ -39,6 +42,7 @@ impl Default for BemaniutilsConfig { db_user: "root".to_string(), db_password: "".to_string(), username: "".to_string(), + game_version: 6, } } } diff --git a/src/data_source/asphyxia.rs b/src/data_source/asphyxia.rs index 4b9343c..01b8e4b 100644 --- a/src/data_source/asphyxia.rs +++ b/src/data_source/asphyxia.rs @@ -1,5 +1,6 @@ use crate::config::AsphyxiaConfig; use crate::data_source::DataSource; +use crate::model; use crate::model::*; use crate::Result; use quick_xml; @@ -48,9 +49,6 @@ impl DataSource for AsphyxiaDataSource { fn get_level_stat(&self, level: Option) -> Vec { self.record_store.get_level_stat(level) } - fn get_level_count(&self, level: u8) -> usize { - self.music_store.get_level_count(level) - } } /// MusicRecordStore is used to get sdvx music record from asphyxia db file. @@ -76,7 +74,7 @@ impl RecordStore { { // let music = music_store. let music = music_store.get_music_ref(music_record.get_music_id()); - let full_record = FullRecord::from_record_with_music(&music_record, music); + let full_record = music_record.to_full_record(music); if let Some(rec) = records.get_mut(&full_record.get_music_id()) { let level = full_record.get_level(); if !rec.contains_key(&level) { @@ -98,8 +96,7 @@ impl RecordStore { _ => {} } } - println!("your play data has been loaded."); - println!("you have {} records.", records.len()); + println!("{} records loaded.", records.len()); Ok(RecordStore { records }) } @@ -202,6 +199,74 @@ impl RecordStore { } } +#[derive(Debug, Deserialize)] +pub struct Record { + #[serde(default)] + collection: String, + #[serde(rename = "mid", default)] + music_id: u16, + #[serde(rename = "type", default)] + music_type: u8, + #[serde(default)] + score: u32, + #[serde(rename = "clear", default)] + clear_type: u8, + #[serde(default)] + grade: u8, + #[serde(rename = "__refid", default)] + user_id: String, +} + +impl Record { + pub fn get_collectoin_str(&self) -> &str { + self.collection.as_str() + } + pub fn get_music_id(&self) -> u16 { + self.music_id + } + pub fn get_score(&self) -> u32 { + self.score + } + pub fn get_user_id_str(&self) -> &str { + self.user_id.as_str() + } + pub fn get_music_type(&self) -> u8 { + self.music_type + } + pub fn get_grade(&self) -> u8 { + self.grade + } + pub fn get_clear_type(&self) -> u8 { + self.clear_type + } + + pub fn to_full_record(&self, mus: Option<&Music>) -> FullRecord { + let mut ful_rec = FullRecord { + music_id: self.get_music_id(), + music_name: String::from("(NOT FOUND)"), + difficulty: model::Difficulty::Unknown, + level: 0, + score: self.get_score(), + grade: Grade::from(self.get_grade()), + clear_type: ClearType::from(self.get_clear_type()), + volfoce: Volfoce::default(), + }; + if let Some(m) = mus { + ful_rec.music_name = m.get_name(); + ful_rec.difficulty = + model::Difficulty::from(self.get_music_type()).inf_ver(m.get_inf_ver()); + ful_rec.level = m.get_level(self.get_music_type()); + } + ful_rec.volfoce = compute_volforce( + ful_rec.level, + ful_rec.score, + ful_rec.grade, + ful_rec.clear_type, + ); + ful_rec + } +} + #[derive(Debug, Deserialize, PartialEq)] struct Mdb { music: Vec, @@ -227,7 +292,6 @@ impl MusicStore { impl MusicStore { pub fn open(path: impl Into) -> Result { let mdb: Mdb = quick_xml::de::from_reader(BufReader::new(File::open(path.into())?))?; - println!("{} music loaded.", mdb.music.len()); Ok(MusicStore::from_mdb(mdb)) } @@ -256,11 +320,4 @@ impl MusicStore { .map(|(_, &id)| id) .collect::>() } - - pub fn get_level_count(&self, level: u8) -> usize { - self.music - .iter() - .filter(|(_, m)| m.has_level(level)) - .count() - } } diff --git a/src/data_source/bemaniutils.rs b/src/data_source/bemaniutils.rs index 6a820fd..bae716d 100644 --- a/src/data_source/bemaniutils.rs +++ b/src/data_source/bemaniutils.rs @@ -1,38 +1,141 @@ +use std::collections::HashMap; + use super::DataSource; use crate::config::BemaniutilsConfig; -use crate::model::{FullRecord, LevelStat}; -use crate::Result; +use crate::model::{FullRecord, LevelStat, self}; +use crate::{Result, errors}; +use mysql::prelude::*; +use mysql::*; +use serde::Deserialize; +use rust_fuzzy_search::fuzzy_compare; -pub struct BemaniutilsDataSource {} +pub struct BemaniutilsDataSource { + records: Vec, +} impl DataSource for BemaniutilsDataSource { /// Get records of music_ids fn get_record_by_id(&self, music_id: Vec) -> Vec { - vec![] + self.records.iter().filter(|r| {music_id.contains(&r.music_id)}).cloned().collect() } /// Get records by name. The implementation is probably fuzzy search. fn get_record_by_name(&self, name: String) -> Vec { - vec![] + self.records + .iter() + .filter(|r| {fuzzy_compare(&name.to_lowercase(), &r.music_name) > 0.5}) + .cloned() + .collect() } /// Get best 50 records of current user. fn get_best50_records(&self) -> Vec { - vec![] + self.records.iter().take(50).cloned().collect() } /// Show how many CLEARs and GRADEs dose the user have at each type at the level. /// If `level` is `None`, return all level stats. fn get_level_stat(&self, level: Option) -> Vec { - vec![] - } - /// Show how many musics dose the user have played at the level. - fn get_level_count(&self, level: u8) -> usize { - 0 + let mut level_stat: HashMap = HashMap::new(); + for r in self + .records + .iter() + .filter(|r| match level { + Some(l) => r.get_level() == l, + None => true, + }) + { + let mut stat = LevelStat::new(r.get_level(), 0, 0, 0, 0, 0, 0, 0, 1); + match r.get_clear_type() { + model::ClearType::Complete => stat.incr_nc_num(1), + model::ClearType::HardComplete => stat.incr_hc_num(1), + model::ClearType::UltimateChain => stat.incr_uc_num(1), + model::ClearType::PerfectUltimateChain => stat.incr_puc_num(1), + _ => {} + } + match r.get_grade() { + model::Grade::AAA => stat.incr_ta_num(1), + model::Grade::AAAPlus => stat.incr_tap_num(1), + model::Grade::S => stat.incr_s_num(1), + _ => {} + } + if let Some(old_stat) = level_stat.get_mut(&r.get_level()) { + *old_stat = *old_stat + stat; + } else { + level_stat.insert(r.get_level(), stat); + } + } + let mut r = level_stat + .iter() + .map(|(_, &s)| s) + .collect::>(); + r.sort_by_key(|&s| s.get_level()); + r } } impl BemaniutilsDataSource { pub fn open(conf: BemaniutilsConfig) -> Result { - println!("{:?}", conf); + // read all need data when open + let url = format!( + "mysql://{}:{}@{}:{}/{}", + conf.db_user, conf.db_password, conf.db_address, conf.db_port, conf.db_name + ); + let pool = Pool::new(mysql::Opts::from_url(url.as_str()).unwrap())?; + let mut conn = pool.get_conn()?; + // get user id by username first + let user_id: u16 = + if let Some(id) = conn.exec_first("SELECT id FROM user WHERE username = ?", (conf.username,))? { + id + } else { + return Err(errors::Error::OtherError("bemanitutils: username not found".to_string())); + }; + // get records by user id + #[derive(Debug, Deserialize)] + struct Records { + songid: u16, + name: String, + chart: u8, + points: u32, + sdata: String, + mdata: String, + } + + let sql = + "SELECT music.songid AS songid, music.name AS name, music.chart AS chart, score.points AS points, score.data AS sdata, music.data AS mdata \ + FROM score, music \ + WHERE score.userid = ? AND score.musicid = music.id AND music.game = 'sdvx' AND music.version = ?"; + let result: Vec = conn.exec_map( + sql, + (user_id, conf.game_version,), + |(songid, name, chart, points, sdata, mdata)| { + Records { songid, name, chart, points, sdata, mdata } + } + )?; + + let mut full_records = result.into_iter().map(|r| { + #[derive(Debug, Deserialize)] + struct Mdata { difficulty: u8 } + #[derive(Debug, Deserialize)] + struct SData { grade: u16, clear_type: u16 } + let mdata: Mdata = serde_json::from_str(r.mdata.as_str()).unwrap(); + let sdata: SData = serde_json::from_str(r.sdata.as_str()).unwrap(); + let grade = model::Grade::from(sdata.grade); + let clear_type = model::ClearType::from(sdata.clear_type); + + FullRecord { + music_id: r.songid, + music_name: r.name, + difficulty: model::Difficulty::from(r.chart), + level: mdata.difficulty, + score: r.points, + grade: grade, + clear_type:clear_type, + volfoce: model::compute_volforce(mdata.difficulty, r.points, grade, clear_type), + } + }).collect::>(); + + full_records.sort_by_key(|rec| rec.get_volforce()); + + println!("{} records loaded.", full_records.len()); println!("data loaded from Bemaniutils server database succeeded!"); - Ok(Self {}) + Ok(Self {records: full_records.into_iter().rev().collect()}) } } diff --git a/src/data_source/mod.rs b/src/data_source/mod.rs index f3f56c8..f73d580 100644 --- a/src/data_source/mod.rs +++ b/src/data_source/mod.rs @@ -22,6 +22,4 @@ pub trait DataSource { /// Show how many CLEARs and GRADEs dose the user have at each type at the level. /// If `level` is `None`, return all level stats. fn get_level_stat(&self, level: Option) -> Vec; - /// Show how many musics dose the user have played at the level. - fn get_level_count(&self, level: u8) -> usize; } diff --git a/src/errors.rs b/src/errors.rs index 3610b67..fd7a36a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -13,6 +13,8 @@ pub enum Error { TomlDeError(toml::de::Error), #[fail(display = "do command failed: {}", _0)] DoCmdError(String), + #[fail(display = "connect to mysql failed: {}", _0)] + MySQLError(mysql::Error), #[fail(display = "{}", _0)] OtherError(String), } @@ -41,4 +43,10 @@ impl From for Error { } } +impl From for Error { + fn from(e: mysql::Error) -> Self { + Error::MySQLError(e) + } +} + pub type Result = result::Result; diff --git a/src/model/music.rs b/src/model/music.rs index 97bbff7..45250fb 100644 --- a/src/model/music.rs +++ b/src/model/music.rs @@ -128,18 +128,6 @@ impl Music { _ => self.difficulty.infinite.level, } } - pub fn has_level(&self, level: u8) -> bool { - if self.difficulty.novice.level == level - || self.difficulty.advanced.level == level - || self.difficulty.exhaust.level == level - || self.difficulty.infinite.level == level - || self.difficulty.maximum.level == level - { - true - } else { - false - } - } } impl Clone for Music { diff --git a/src/model/record.rs b/src/model/record.rs index e443c4f..844983b 100644 --- a/src/model/record.rs +++ b/src/model/record.rs @@ -1,6 +1,5 @@ -use super::music::{self, Music}; +use super::music::{self}; use derive_getters::Getters; -use serde::Deserialize; use std::fmt::Display; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -36,6 +35,7 @@ impl Display for Grade { } } +// for asyphyxia format impl From for Grade { fn from(g: u8) -> Self { match g { @@ -54,6 +54,25 @@ impl From for Grade { } } +// for bemaniutils format +impl From for Grade { + fn from(g: u16) -> Self { + match g { + 200 => Grade::D, + 300 => Grade::C, + 400 => Grade::B, + 500 => Grade::A, + 550 => Grade::APlus, + 600 => Grade::AA, + 650 => Grade::AAPlus, + 700 => Grade::AAA, + 800 => Grade::AAAPlus, + 900 => Grade::S, + _ => Grade::None, + } + } +} + impl Grade { pub fn get_vf_coef(&self) -> u64 { match *self { @@ -95,6 +114,7 @@ impl Display for ClearType { } } +// for asyphyxia format impl From for ClearType { fn from(t: u8) -> Self { match t { @@ -108,6 +128,20 @@ impl From for ClearType { } } +// for bemaniutils format +impl From for ClearType { + fn from(t: u16) -> Self { + match t { + 100 => ClearType::Played, + 200 => ClearType::Complete, + 300 => ClearType::HardComplete, + 400 => ClearType::UltimateChain, + 500 => ClearType::PerfectUltimateChain, + _ => ClearType::None, + } + } +} + impl ClearType { pub fn get_vf_coef(&self) -> u64 { match *self { @@ -120,48 +154,6 @@ impl ClearType { } } -#[derive(Debug, Deserialize)] -pub struct Record { - #[serde(default)] - collection: String, - #[serde(rename = "mid", default)] - music_id: u16, - #[serde(rename = "type", default)] - music_type: u8, - #[serde(default)] - score: u32, - #[serde(rename = "clear", default)] - clear_type: u8, - #[serde(default)] - grade: u8, - #[serde(rename = "__refid", default)] - user_id: String, -} - -impl Record { - pub fn get_collectoin_str(&self) -> &str { - self.collection.as_str() - } - pub fn get_music_id(&self) -> u16 { - self.music_id - } - pub fn get_score(&self) -> u32 { - self.score - } - pub fn get_user_id_str(&self) -> &str { - self.user_id.as_str() - } - pub fn get_music_type(&self) -> u8 { - self.music_type - } - pub fn get_grade(&self) -> u8 { - self.grade - } - pub fn get_clear_type(&self) -> u8 { - self.clear_type - } -} - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Volfoce(u32); @@ -174,6 +166,12 @@ impl Volfoce { } } +impl From for Volfoce { + fn from(vf: u32) -> Self { + Self(vf) + } +} + impl Display for Volfoce { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let i = self.0 / 10_u32.pow(3); @@ -200,14 +198,14 @@ pub fn compute_volforce(level: u8, score: u32, grade: Grade, clear: ClearType) - #[derive(Debug)] pub struct FullRecord { - music_id: u16, - music_name: String, - difficulty: music::Difficulty, - level: u8, - score: u32, - grade: Grade, - clear_type: ClearType, - volfoce: Volfoce, + pub music_id: u16, + pub music_name: String, + pub difficulty: music::Difficulty, + pub level: u8, + pub score: u32, + pub grade: Grade, + pub clear_type: ClearType, + pub volfoce: Volfoce, } impl Clone for FullRecord { @@ -226,32 +224,6 @@ impl Clone for FullRecord { } impl FullRecord { - pub fn from_record_with_music(rec: &Record, mus: Option<&Music>) -> Self { - let mut ful_rec = FullRecord { - music_id: rec.get_music_id(), - music_name: String::from("(NOT FOUND)"), - difficulty: music::Difficulty::Unknown, - level: 0, - score: rec.get_score(), - grade: Grade::from(rec.get_grade()), - clear_type: ClearType::from(rec.get_clear_type()), - volfoce: Volfoce::default(), - }; - if let Some(m) = mus { - ful_rec.music_name = m.get_name(); - ful_rec.difficulty = - music::Difficulty::from(rec.get_music_type()).inf_ver(m.get_inf_ver()); - ful_rec.level = m.get_level(rec.get_music_type()); - } - ful_rec.volfoce = compute_volforce( - ful_rec.level, - ful_rec.score, - ful_rec.grade, - ful_rec.clear_type, - ); - ful_rec - } - pub fn get_music_id(&self) -> u16 { self.music_id }