Skip to content

Commit 6350ee7

Browse files
committed
feat(data_source): implement DataSource for bemaniutils
1 parent 65f1825 commit 6350ee7

11 files changed

Lines changed: 254 additions & 133 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ authors = ["RinChanNOW <https://github.com/RinChanNOWWW>"]
33
description = "ruborute is a command-line tool to get asphyxia@sdvx gaming data."
44
edition = "2021"
55
name = "ruborute"
6-
version = "0.1.3"
6+
version = "0.2.0"
77

88
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
99

example/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ db_name = "bemani"
88
db_password = "bemani"
99
db_port = 3306
1010
db_user = "bemani"
11+
game_version = 6
1112
username = "rinchannow"

src/command/command.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,7 @@ impl<T: DataSource> Cmd for CmdCount<T> {
197197
} else {
198198
return Err(Error::DoCmdError(String::from("args unmatched.")));
199199
};
200-
let mut tab = table!([
201-
"level",
202-
"S",
203-
"AAA+",
204-
"AAA",
205-
"PUC",
206-
"UC",
207-
"HC",
208-
"NC",
209-
"played/total"
210-
]);
200+
let mut tab = table!(["level", "S", "AAA+", "AAA", "PUC", "UC", "HC", "NC", "played"]);
211201
for s in stats.iter() {
212202
tab.add_row(row![
213203
s.level(),
@@ -218,7 +208,7 @@ impl<T: DataSource> Cmd for CmdCount<T> {
218208
s.uc_num(),
219209
s.hc_num(),
220210
s.nc_num(),
221-
format!("{}/{}", s.played(), self.store.get_level_count(*s.level())),
211+
format!("{}", s.played()),
222212
]);
223213
}
224214
tab.printstd();

src/config/bemaniutils_config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pub struct BemaniutilsConfig {
2828

2929
#[clap(long, default_value = "", help = "the user to query")]
3030
pub username: String,
31+
32+
#[clap(long, default_value = "6", help = "the game version")]
33+
pub game_version: u8,
3134
}
3235

3336
impl Default for BemaniutilsConfig {
@@ -39,6 +42,7 @@ impl Default for BemaniutilsConfig {
3942
db_user: "root".to_string(),
4043
db_password: "".to_string(),
4144
username: "".to_string(),
45+
game_version: 6,
4246
}
4347
}
4448
}

src/data_source/asphyxia.rs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::config::AsphyxiaConfig;
22
use crate::data_source::DataSource;
3+
use crate::model;
34
use crate::model::*;
45
use crate::Result;
56
use quick_xml;
@@ -48,9 +49,6 @@ impl DataSource for AsphyxiaDataSource {
4849
fn get_level_stat(&self, level: Option<u8>) -> Vec<LevelStat> {
4950
self.record_store.get_level_stat(level)
5051
}
51-
fn get_level_count(&self, level: u8) -> usize {
52-
self.music_store.get_level_count(level)
53-
}
5452
}
5553

5654
/// MusicRecordStore is used to get sdvx music record from asphyxia db file.
@@ -76,7 +74,7 @@ impl RecordStore {
7674
{
7775
// let music = music_store.
7876
let music = music_store.get_music_ref(music_record.get_music_id());
79-
let full_record = FullRecord::from_record_with_music(&music_record, music);
77+
let full_record = music_record.to_full_record(music);
8078
if let Some(rec) = records.get_mut(&full_record.get_music_id()) {
8179
let level = full_record.get_level();
8280
if !rec.contains_key(&level) {
@@ -98,8 +96,7 @@ impl RecordStore {
9896
_ => {}
9997
}
10098
}
101-
println!("your play data has been loaded.");
102-
println!("you have {} records.", records.len());
99+
println!("{} records loaded.", records.len());
103100
Ok(RecordStore { records })
104101
}
105102

@@ -202,6 +199,74 @@ impl RecordStore {
202199
}
203200
}
204201

202+
#[derive(Debug, Deserialize)]
203+
pub struct Record {
204+
#[serde(default)]
205+
collection: String,
206+
#[serde(rename = "mid", default)]
207+
music_id: u16,
208+
#[serde(rename = "type", default)]
209+
music_type: u8,
210+
#[serde(default)]
211+
score: u32,
212+
#[serde(rename = "clear", default)]
213+
clear_type: u8,
214+
#[serde(default)]
215+
grade: u8,
216+
#[serde(rename = "__refid", default)]
217+
user_id: String,
218+
}
219+
220+
impl Record {
221+
pub fn get_collectoin_str(&self) -> &str {
222+
self.collection.as_str()
223+
}
224+
pub fn get_music_id(&self) -> u16 {
225+
self.music_id
226+
}
227+
pub fn get_score(&self) -> u32 {
228+
self.score
229+
}
230+
pub fn get_user_id_str(&self) -> &str {
231+
self.user_id.as_str()
232+
}
233+
pub fn get_music_type(&self) -> u8 {
234+
self.music_type
235+
}
236+
pub fn get_grade(&self) -> u8 {
237+
self.grade
238+
}
239+
pub fn get_clear_type(&self) -> u8 {
240+
self.clear_type
241+
}
242+
243+
pub fn to_full_record(&self, mus: Option<&Music>) -> FullRecord {
244+
let mut ful_rec = FullRecord {
245+
music_id: self.get_music_id(),
246+
music_name: String::from("(NOT FOUND)"),
247+
difficulty: model::Difficulty::Unknown,
248+
level: 0,
249+
score: self.get_score(),
250+
grade: Grade::from(self.get_grade()),
251+
clear_type: ClearType::from(self.get_clear_type()),
252+
volfoce: Volfoce::default(),
253+
};
254+
if let Some(m) = mus {
255+
ful_rec.music_name = m.get_name();
256+
ful_rec.difficulty =
257+
model::Difficulty::from(self.get_music_type()).inf_ver(m.get_inf_ver());
258+
ful_rec.level = m.get_level(self.get_music_type());
259+
}
260+
ful_rec.volfoce = compute_volforce(
261+
ful_rec.level,
262+
ful_rec.score,
263+
ful_rec.grade,
264+
ful_rec.clear_type,
265+
);
266+
ful_rec
267+
}
268+
}
269+
205270
#[derive(Debug, Deserialize, PartialEq)]
206271
struct Mdb {
207272
music: Vec<Music>,
@@ -227,7 +292,6 @@ impl MusicStore {
227292
impl MusicStore {
228293
pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
229294
let mdb: Mdb = quick_xml::de::from_reader(BufReader::new(File::open(path.into())?))?;
230-
println!("{} music loaded.", mdb.music.len());
231295
Ok(MusicStore::from_mdb(mdb))
232296
}
233297

@@ -256,11 +320,4 @@ impl MusicStore {
256320
.map(|(_, &id)| id)
257321
.collect::<Vec<u16>>()
258322
}
259-
260-
pub fn get_level_count(&self, level: u8) -> usize {
261-
self.music
262-
.iter()
263-
.filter(|(_, m)| m.has_level(level))
264-
.count()
265-
}
266323
}

src/data_source/bemaniutils.rs

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,141 @@
1+
use std::collections::HashMap;
2+
13
use super::DataSource;
24
use crate::config::BemaniutilsConfig;
3-
use crate::model::{FullRecord, LevelStat};
4-
use crate::Result;
5+
use crate::model::{FullRecord, LevelStat, self};
6+
use crate::{Result, errors};
7+
use mysql::prelude::*;
8+
use mysql::*;
9+
use serde::Deserialize;
10+
use rust_fuzzy_search::fuzzy_compare;
511

6-
pub struct BemaniutilsDataSource {}
12+
pub struct BemaniutilsDataSource {
13+
records: Vec<FullRecord>,
14+
}
715

816
impl DataSource for BemaniutilsDataSource {
917
/// Get records of music_ids
1018
fn get_record_by_id(&self, music_id: Vec<u16>) -> Vec<FullRecord> {
11-
vec![]
19+
self.records.iter().filter(|r| {music_id.contains(&r.music_id)}).cloned().collect()
1220
}
1321
/// Get records by name. The implementation is probably fuzzy search.
1422
fn get_record_by_name(&self, name: String) -> Vec<FullRecord> {
15-
vec![]
23+
self.records
24+
.iter()
25+
.filter(|r| {fuzzy_compare(&name.to_lowercase(), &r.music_name) > 0.5})
26+
.cloned()
27+
.collect()
1628
}
1729
/// Get best 50 records of current user.
1830
fn get_best50_records(&self) -> Vec<FullRecord> {
19-
vec![]
31+
self.records.iter().take(50).cloned().collect()
2032
}
2133
/// Show how many CLEARs and GRADEs dose the user have at each type at the level.
2234
/// If `level` is `None`, return all level stats.
2335
fn get_level_stat(&self, level: Option<u8>) -> Vec<LevelStat> {
24-
vec![]
25-
}
26-
/// Show how many musics dose the user have played at the level.
27-
fn get_level_count(&self, level: u8) -> usize {
28-
0
36+
let mut level_stat: HashMap<u8, LevelStat> = HashMap::new();
37+
for r in self
38+
.records
39+
.iter()
40+
.filter(|r| match level {
41+
Some(l) => r.get_level() == l,
42+
None => true,
43+
})
44+
{
45+
let mut stat = LevelStat::new(r.get_level(), 0, 0, 0, 0, 0, 0, 0, 1);
46+
match r.get_clear_type() {
47+
model::ClearType::Complete => stat.incr_nc_num(1),
48+
model::ClearType::HardComplete => stat.incr_hc_num(1),
49+
model::ClearType::UltimateChain => stat.incr_uc_num(1),
50+
model::ClearType::PerfectUltimateChain => stat.incr_puc_num(1),
51+
_ => {}
52+
}
53+
match r.get_grade() {
54+
model::Grade::AAA => stat.incr_ta_num(1),
55+
model::Grade::AAAPlus => stat.incr_tap_num(1),
56+
model::Grade::S => stat.incr_s_num(1),
57+
_ => {}
58+
}
59+
if let Some(old_stat) = level_stat.get_mut(&r.get_level()) {
60+
*old_stat = *old_stat + stat;
61+
} else {
62+
level_stat.insert(r.get_level(), stat);
63+
}
64+
}
65+
let mut r = level_stat
66+
.iter()
67+
.map(|(_, &s)| s)
68+
.collect::<Vec<LevelStat>>();
69+
r.sort_by_key(|&s| s.get_level());
70+
r
2971
}
3072
}
3173

3274
impl BemaniutilsDataSource {
3375
pub fn open(conf: BemaniutilsConfig) -> Result<Self> {
34-
println!("{:?}", conf);
76+
// read all need data when open
77+
let url = format!(
78+
"mysql://{}:{}@{}:{}/{}",
79+
conf.db_user, conf.db_password, conf.db_address, conf.db_port, conf.db_name
80+
);
81+
let pool = Pool::new(mysql::Opts::from_url(url.as_str()).unwrap())?;
82+
let mut conn = pool.get_conn()?;
83+
// get user id by username first
84+
let user_id: u16 =
85+
if let Some(id) = conn.exec_first("SELECT id FROM user WHERE username = ?", (conf.username,))? {
86+
id
87+
} else {
88+
return Err(errors::Error::OtherError("bemanitutils: username not found".to_string()));
89+
};
90+
// get records by user id
91+
#[derive(Debug, Deserialize)]
92+
struct Records {
93+
songid: u16,
94+
name: String,
95+
chart: u8,
96+
points: u32,
97+
sdata: String,
98+
mdata: String,
99+
}
100+
101+
let sql =
102+
"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 \
103+
FROM score, music \
104+
WHERE score.userid = ? AND score.musicid = music.id AND music.game = 'sdvx' AND music.version = ?";
105+
let result: Vec<Records> = conn.exec_map(
106+
sql,
107+
(user_id, conf.game_version,),
108+
|(songid, name, chart, points, sdata, mdata)| {
109+
Records { songid, name, chart, points, sdata, mdata }
110+
}
111+
)?;
112+
113+
let mut full_records = result.into_iter().map(|r| {
114+
#[derive(Debug, Deserialize)]
115+
struct Mdata { difficulty: u8 }
116+
#[derive(Debug, Deserialize)]
117+
struct SData { grade: u16, clear_type: u16 }
118+
let mdata: Mdata = serde_json::from_str(r.mdata.as_str()).unwrap();
119+
let sdata: SData = serde_json::from_str(r.sdata.as_str()).unwrap();
120+
let grade = model::Grade::from(sdata.grade);
121+
let clear_type = model::ClearType::from(sdata.clear_type);
122+
123+
FullRecord {
124+
music_id: r.songid,
125+
music_name: r.name,
126+
difficulty: model::Difficulty::from(r.chart),
127+
level: mdata.difficulty,
128+
score: r.points,
129+
grade: grade,
130+
clear_type:clear_type,
131+
volfoce: model::compute_volforce(mdata.difficulty, r.points, grade, clear_type),
132+
}
133+
}).collect::<Vec<FullRecord>>();
134+
135+
full_records.sort_by_key(|rec| rec.get_volforce());
136+
137+
println!("{} records loaded.", full_records.len());
35138
println!("data loaded from Bemaniutils server database succeeded!");
36-
Ok(Self {})
139+
Ok(Self {records: full_records.into_iter().rev().collect()})
37140
}
38141
}

src/data_source/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,4 @@ pub trait DataSource {
2222
/// Show how many CLEARs and GRADEs dose the user have at each type at the level.
2323
/// If `level` is `None`, return all level stats.
2424
fn get_level_stat(&self, level: Option<u8>) -> Vec<LevelStat>;
25-
/// Show how many musics dose the user have played at the level.
26-
fn get_level_count(&self, level: u8) -> usize;
2725
}

src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub enum Error {
1313
TomlDeError(toml::de::Error),
1414
#[fail(display = "do command failed: {}", _0)]
1515
DoCmdError(String),
16+
#[fail(display = "connect to mysql failed: {}", _0)]
17+
MySQLError(mysql::Error),
1618
#[fail(display = "{}", _0)]
1719
OtherError(String),
1820
}
@@ -41,4 +43,10 @@ impl From<toml::de::Error> for Error {
4143
}
4244
}
4345

46+
impl From<mysql::Error> for Error {
47+
fn from(e: mysql::Error) -> Self {
48+
Error::MySQLError(e)
49+
}
50+
}
51+
4452
pub type Result<T> = result::Result<T, Error>;

0 commit comments

Comments
 (0)