bms_table/
lib.rs

1//! 示例程序
2//!
3
4#![warn(missing_docs)]
5#![warn(clippy::must_use_candidate)]
6#![deny(rustdoc::broken_intra_doc_links)]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9pub mod fetch;
10
11use anyhow::Result;
12use serde::Serialize;
13use serde_json::Value;
14
15/// BMS难度表数据,看这一个就够了
16#[derive(Debug, Clone, PartialEq)]
17pub struct BmsTable {
18    /// 表头信息与额外字段
19    pub header: BmsTableHeader,
20    /// 表数据,包含谱面列表
21    pub data: BmsTableData,
22}
23
24/// BMS表头信息
25#[derive(Debug, Clone, PartialEq, Serialize)]
26pub struct BmsTableHeader {
27    /// 表格名称,如 "Satellite"
28    pub name: String,
29    /// 表格符号,如 "sl"
30    pub symbol: String,
31    /// 谱面数据文件的URL(原样保存来自header JSON的字符串)
32    pub data_url: String,
33    /// 课程信息数组,每个元素是一个课程组的数组
34    pub course: Vec<Vec<CourseInfo>>,
35    /// 难度等级顺序,包含数字和字符串
36    pub level_order: Vec<String>,
37    /// 额外数据(来自header JSON中未识别的字段)
38    pub extra: serde_json::Value,
39}
40
41impl<'de> serde::Deserialize<'de> for BmsTableHeader {
42    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43    where
44        D: serde::Deserializer<'de>,
45    {
46        // 先把整个JSON作为Value读取,便于提取已知字段并收集额外字段
47        let mut value: Value = Value::deserialize(deserializer)?;
48
49        // name
50        let name = value
51            .get("name")
52            .and_then(|v| v.as_str())
53            .ok_or_else(|| serde::de::Error::missing_field("name"))?
54            .to_string();
55
56        // symbol
57        let symbol = value
58            .get("symbol")
59            .and_then(|v| v.as_str())
60            .ok_or_else(|| serde::de::Error::missing_field("symbol"))?
61            .to_string();
62
63        // data_url
64        let data_url = value
65            .get("data_url")
66            .and_then(|v| v.as_str())
67            .ok_or_else(|| serde::de::Error::missing_field("data_url"))?
68            .to_string();
69
70        // course:支持 Vec<CourseInfo> 和 Vec<Vec<CourseInfo>> 两种格式
71        let course = match value.get("course") {
72            Some(Value::Array(arr)) if !arr.is_empty() => {
73                if matches!(arr.first(), Some(Value::Array(_))) {
74                    // Vec<Vec<CourseInfo>>
75                    serde_json::from_value::<Vec<Vec<CourseInfo>>>(value["course"].clone())
76                        .map_err(serde::de::Error::custom)?
77                } else {
78                    // Vec<CourseInfo> -> 包装为 Vec<Vec<CourseInfo>>
79                    let inner: Vec<CourseInfo> = serde_json::from_value(value["course"].clone())
80                        .map_err(serde::de::Error::custom)?;
81                    vec![inner]
82                }
83            }
84            _ => Vec::new(),
85        };
86
87        // level_order:数字和字符串统一转为字符串
88        let level_order = match value.get("level_order") {
89            Some(Value::Array(arr)) => arr
90                .iter()
91                .map(|v| match v {
92                    Value::Number(n) => n.to_string(),
93                    Value::String(s) => s.clone(),
94                    _ => v.to_string(),
95                })
96                .collect::<Vec<String>>(),
97            _ => Vec::new(),
98        };
99
100        // 收集额外字段:移除已知字段后剩余的内容
101        if let Some(obj) = value.as_object_mut() {
102            obj.remove("name");
103            obj.remove("symbol");
104            obj.remove("data_url");
105            obj.remove("course");
106            obj.remove("level_order");
107        }
108
109        Ok(Self {
110            name,
111            symbol,
112            data_url,
113            course,
114            level_order,
115            extra: value,
116        })
117    }
118}
119
120/// BMS表数据
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct BmsTableData {
123    /// 谱面数据
124    pub charts: Vec<ChartItem>,
125}
126
127impl<'de> serde::Deserialize<'de> for BmsTableData {
128    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
129    where
130        D: serde::Deserializer<'de>,
131    {
132        // 支持两种输入:
133        // 1) 直接是数组: [ {...}, {...} ]
134        // 2) 对象包裹: { "charts": [ {...}, {...} ] }
135        let value: Value = Value::deserialize(deserializer)?;
136        match value {
137            Value::Array(arr) => {
138                let charts: Vec<ChartItem> =
139                    serde_json::from_value(Value::Array(arr)).map_err(serde::de::Error::custom)?;
140                Ok(Self { charts })
141            }
142            Value::Object(mut obj) => {
143                let charts_value = obj.remove("charts").unwrap_or(Value::Array(vec![]));
144                let charts: Vec<ChartItem> =
145                    serde_json::from_value(charts_value).map_err(serde::de::Error::custom)?;
146                Ok(Self { charts })
147            }
148            _ => Err(serde::de::Error::custom(
149                "BmsTableData expects array or object with charts",
150            )),
151        }
152    }
153}
154
155/// 课程信息
156///
157/// 定义了一个BMS课程的所有相关信息,包括约束条件、奖杯要求和谱面数据。
158#[derive(Debug, Clone, Serialize, PartialEq)]
159pub struct CourseInfo {
160    /// 课程名称,如 "Satellite Skill Analyzer 2nd sl0"
161    pub name: String,
162    /// 约束条件列表,如 ["grade_mirror", "gauge_lr2", "ln"]
163    #[serde(default)]
164    pub constraint: Vec<String>,
165    /// 奖杯信息列表,定义不同等级的奖杯要求
166    #[serde(default)]
167    pub trophy: Vec<Trophy>,
168    /// 谱面数据列表,包含该课程的所有谱面信息
169    #[serde(default)]
170    pub charts: Vec<ChartItem>,
171}
172
173impl<'de> serde::Deserialize<'de> for CourseInfo {
174    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175    where
176        D: serde::Deserializer<'de>,
177    {
178        #[derive(serde::Deserialize)]
179        struct CourseInfoHelper {
180            name: String,
181            #[serde(default)]
182            constraint: Vec<String>,
183            #[serde(default)]
184            trophy: Vec<Trophy>,
185            #[serde(default, rename = "md5")]
186            md5list: Vec<String>,
187            #[serde(default, rename = "sha256")]
188            sha256list: Vec<String>,
189            #[serde(default)]
190            charts: Vec<Value>,
191        }
192
193        let helper = CourseInfoHelper::deserialize(deserializer)?;
194
195        // 处理charts字段,为ChartItem的level字段设置默认值
196        let mut charts = helper
197            .charts
198            .into_iter()
199            .map(|mut chart_value| {
200                if chart_value.get("level").is_none() {
201                    // 如果level字段不存在,添加默认值0
202                    let Some(chart_obj) = chart_value.as_object() else {
203                        return Err(serde::de::Error::custom("chart_value is not an object"));
204                    };
205                    let mut chart_obj = chart_obj.clone();
206                    chart_obj.insert(
207                        "level".to_string(),
208                        serde_json::Value::String("0".to_string()),
209                    );
210                    chart_value = serde_json::Value::Object(chart_obj);
211                }
212                serde_json::from_value(chart_value)
213            })
214            .collect::<Result<Vec<ChartItem>, serde_json::Error>>()
215            .map_err(serde::de::Error::custom)?;
216
217        // 将md5list转换为charts
218        for md5 in &helper.md5list {
219            charts.push(ChartItem {
220                level: "0".to_string(),
221                md5: Some(md5.clone()),
222                sha256: None,
223                title: None,
224                subtitle: None,
225                artist: None,
226                subartist: None,
227                url: None,
228                url_diff: None,
229                extra: serde_json::Value::Object(serde_json::Map::new()),
230            });
231        }
232
233        // 将sha256list转换为charts
234        for sha256 in &helper.sha256list {
235            charts.push(ChartItem {
236                level: "0".to_string(),
237                md5: None,
238                sha256: Some(sha256.clone()),
239                title: None,
240                subtitle: None,
241                artist: None,
242                subartist: None,
243                url: None,
244                url_diff: None,
245                extra: serde_json::Value::Object(serde_json::Map::new()),
246            });
247        }
248
249        Ok(Self {
250            name: helper.name,
251            constraint: helper.constraint,
252            trophy: helper.trophy,
253            charts,
254        })
255    }
256}
257
258/// 谱面数据项
259///
260/// 表示一个BMS文件的谱面数据,包含文件信息和下载链接。
261/// 所有字段都是可选的,因为不同的BMS表格可能有不同的字段。
262#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
263pub struct ChartItem {
264    /// 难度等级,如 "0"
265    pub level: String,
266    /// 文件的MD5哈希值
267    pub md5: Option<String>,
268    /// 文件的SHA256哈希值
269    pub sha256: Option<String>,
270    /// 歌曲标题
271    pub title: Option<String>,
272    /// 歌曲副标题
273    pub subtitle: Option<String>,
274    /// 艺术家名称
275    pub artist: Option<String>,
276    /// 歌曲副艺术家
277    pub subartist: Option<String>,
278    /// 文件下载链接
279    pub url: Option<String>,
280    /// 差分文件下载链接(可选)
281    pub url_diff: Option<String>,
282    /// 额外数据
283    pub extra: Value,
284}
285
286impl<'de> serde::Deserialize<'de> for ChartItem {
287    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288    where
289        D: serde::Deserializer<'de>,
290    {
291        // 首先将整个值解析为Value
292        let value: Value = Value::deserialize(deserializer)?;
293
294        // 提取已知字段
295        let level = value
296            .get("level")
297            .and_then(|v| v.as_str())
298            .ok_or_else(|| serde::de::Error::missing_field("level"))?
299            .to_string();
300
301        let md5 = value
302            .get("md5")
303            .and_then(|v| v.as_str())
304            .filter(|s| !s.is_empty())
305            .map(|s| s.to_string());
306        let sha256 = value
307            .get("sha256")
308            .and_then(|v| v.as_str())
309            .filter(|s| !s.is_empty())
310            .map(|s| s.to_string());
311        let title = value
312            .get("title")
313            .and_then(|v| v.as_str())
314            .filter(|s| !s.is_empty())
315            .map(|s| s.to_string());
316        let subtitle = value
317            .get("subtitle")
318            .and_then(|v| v.as_str())
319            .filter(|s| !s.is_empty())
320            .map(|s| s.to_string());
321        let artist = value
322            .get("artist")
323            .and_then(|v| v.as_str())
324            .filter(|s| !s.is_empty())
325            .map(|s| s.to_string());
326        let subartist = value
327            .get("subartist")
328            .and_then(|v| v.as_str())
329            .filter(|s| !s.is_empty())
330            .map(|s| s.to_string());
331        let url = value
332            .get("url")
333            .and_then(|v| v.as_str())
334            .filter(|s| !s.is_empty())
335            .map(|s| s.to_string());
336        let url_diff = value
337            .get("url_diff")
338            .and_then(|v| v.as_str())
339            .filter(|s| !s.is_empty())
340            .map(|s| s.to_string());
341
342        // 提取额外数据(除了已知字段之外的所有数据)
343        let mut extra_data = value;
344        if let Some(obj) = extra_data.as_object_mut() {
345            // 移除已知字段,保留额外字段
346            obj.remove("level");
347            obj.remove("md5");
348            obj.remove("sha256");
349            obj.remove("title");
350            obj.remove("subtitle");
351            obj.remove("artist");
352            obj.remove("subartist");
353            obj.remove("url");
354            obj.remove("url_diff");
355        }
356
357        Ok(Self {
358            level,
359            md5,
360            sha256,
361            title,
362            subtitle,
363            artist,
364            subartist,
365            url,
366            url_diff,
367            extra: extra_data,
368        })
369    }
370}
371/// 奖杯信息
372///
373/// 定义了获得特定奖杯需要达到的谱面要求。
374#[derive(Debug, Clone, serde::Deserialize, Serialize, PartialEq)]
375pub struct Trophy {
376    /// 奖杯名称,如 "silvermedal" 或 "goldmedal"
377    pub name: String,
378    /// 最大miss率(百分比),如 5.0 表示最大5%的miss率
379    pub missrate: f64,
380    /// 最小得分率(百分比),如 70.0 表示至少70%的得分率
381    pub scorerate: f64,
382}