Skip to content

Commit

Permalink
feat(data_source): implement DataSource for bemaniutils
Browse files Browse the repository at this point in the history
  • Loading branch information
RinChanNOWWW committed Mar 25, 2022
1 parent 65f1825 commit 6350ee7
Show file tree
Hide file tree
Showing 11 changed files with 254 additions and 133 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ authors = ["RinChanNOW <https://github.com/RinChanNOWWW>"]
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

Expand Down
1 change: 1 addition & 0 deletions example/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ db_name = "bemani"
db_password = "bemani"
db_port = 3306
db_user = "bemani"
game_version = 6
username = "rinchannow"
14 changes: 2 additions & 12 deletions src/command/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,7 @@ impl<T: DataSource> Cmd for CmdCount<T> {
} 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(),
Expand All @@ -218,7 +208,7 @@ impl<T: DataSource> Cmd for CmdCount<T> {
s.uc_num(),
s.hc_num(),
s.nc_num(),
format!("{}/{}", s.played(), self.store.get_level_count(*s.level())),
format!("{}", s.played()),
]);
}
tab.printstd();
Expand Down
4 changes: 4 additions & 0 deletions src/config/bemaniutils_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -39,6 +42,7 @@ impl Default for BemaniutilsConfig {
db_user: "root".to_string(),
db_password: "".to_string(),
username: "".to_string(),
game_version: 6,
}
}
}
85 changes: 71 additions & 14 deletions src/data_source/asphyxia.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -48,9 +49,6 @@ impl DataSource for AsphyxiaDataSource {
fn get_level_stat(&self, level: Option<u8>) -> Vec<LevelStat> {
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.
Expand All @@ -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) {
Expand All @@ -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 })
}

Expand Down Expand Up @@ -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<Music>,
Expand All @@ -227,7 +292,6 @@ impl MusicStore {
impl MusicStore {
pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
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))
}

Expand Down Expand Up @@ -256,11 +320,4 @@ impl MusicStore {
.map(|(_, &id)| id)
.collect::<Vec<u16>>()
}

pub fn get_level_count(&self, level: u8) -> usize {
self.music
.iter()
.filter(|(_, m)| m.has_level(level))
.count()
}
}
129 changes: 116 additions & 13 deletions src/data_source/bemaniutils.rs
Original file line number Diff line number Diff line change
@@ -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<FullRecord>,
}

impl DataSource for BemaniutilsDataSource {
/// Get records of music_ids
fn get_record_by_id(&self, music_id: Vec<u16>) -> Vec<FullRecord> {
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<FullRecord> {
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<FullRecord> {
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<u8>) -> Vec<LevelStat> {
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<u8, LevelStat> = 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::<Vec<LevelStat>>();
r.sort_by_key(|&s| s.get_level());
r
}
}

impl BemaniutilsDataSource {
pub fn open(conf: BemaniutilsConfig) -> Result<Self> {
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<Records> = 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::<Vec<FullRecord>>();

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()})
}
}
2 changes: 0 additions & 2 deletions src/data_source/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>) -> Vec<LevelStat>;
/// Show how many musics dose the user have played at the level.
fn get_level_count(&self, level: u8) -> usize;
}
8 changes: 8 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -41,4 +43,10 @@ impl From<toml::de::Error> for Error {
}
}

impl From<mysql::Error> for Error {
fn from(e: mysql::Error) -> Self {
Error::MySQLError(e)
}
}

pub type Result<T> = result::Result<T, Error>;
Loading

0 comments on commit 6350ee7

Please sign in to comment.