bms-rs 1.0.0

The BMS format parser.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
//! The [bmson format](https://bmson-spec.readthedocs.io/en/master/doc/index.html) definition.
//!
//! # Order of Processing
//!
//! When there are coincident events in the same pulse, they are processed in the order below:
//!
//! - [`Note`] and [`BgaEvent`] (are independent each other),
//! - [`BpmEvent`],
//! - [`StopEvent`].
//!
//! If a [`BpmEvent`] and a [`StopEvent`] appear on the same pulse, the current BPM will be changed at first, then scrolling the chart will be stopped for a while depending the changed BPM.
//!
//! If a [`Note`] and a [`StopEvent`] appear on the same pulse, the sound will be played (or should be hit by a player), then scrolling the chart will be stopped.
//!
//! # Layered Notes
//!
//! When notes (not BGM) from different sound channels exist on the same (key and pulse) position:
//!
//! - If their lengths are not equal, treat this as an error and warn the player.
//! - Otherwise, the player may fuse the notes. When a player hits the key, the sound slice from each sound channel is played.
//!   This is similar to multiplex WAV definitions in BMS.
//!
//! # Differences from BMS
//!
//! - BMS can play different sound on the start and end of long note. But bmson does not allow this.
//! - Transparent color on BGA is not supported. But you can use PNG files having RGBA channels.
#![cfg(feature = "bmson")]
#![cfg_attr(docsrs, doc(cfg(feature = "bmson")))]

pub mod bms_to_bmson;
pub mod bmson_to_bms;
pub mod parse;
pub mod prelude;
pub mod pulse;

use std::{
    borrow::Cow,
    num::{NonZeroU8, NonZeroU64},
};

#[cfg(feature = "diagnostics")]
use ariadne::{Color, Report, ReportKind};
use chumsky::{prelude::*, span::SimpleSpan};
use serde::{Deserialize, Deserializer, Serialize};

use crate::bms::command::LnMode;

#[cfg(feature = "diagnostics")]
use crate::diagnostics::{ToAriadne, build_report};

use self::{
    parse::{
        Error as JsonError, Recovered as JsonRecovered, Warning as JsonWarning, parser,
        split_chumsky_errors,
    },
    pulse::PulseNumber,
};

use strict_num_extended::{FinF64, PositiveF64};

/// Top-level object for bmson format.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bmson<'a> {
    /// Version of bmson format, which should be compared using [Semantic Version 2.0.0](http://semver.org/spec/v2.0.0.html). Older bmson file may not have this field, but lacking this must be an error.
    pub version: Cow<'a, str>,
    /// Score metadata.
    pub info: BmsonInfo<'a>,
    /// Location of bar lines in pulses. If `None`, then a 4/4 beat is assumed and bar lines will be generates every 4 quarter notes. If `Some(vec![])`, this chart will not have any bar line.
    ///
    /// This format represents an irregular meter by bar lines.
    pub lines: Option<Vec<BarLine>>,
    /// Events of bpm change. If there are coincident events, the successor is only applied.
    #[serde(default)]
    pub bpm_events: Vec<BpmEvent>,
    /// Events of scroll stop. If there are coincident events, they are happened in succession.
    #[serde(default)]
    pub stop_events: Vec<StopEvent>,
    /// Note data.
    pub sound_channels: Vec<SoundChannel<'a>>,
    /// BGA data.
    #[serde(default)]
    pub bga: Bga<'a>,
    /// Beatoraja implementation of scroll events.
    #[serde(default)]
    pub scroll_events: Vec<ScrollEvent>,
    /// Beatoraja implementation of mine channel.
    #[serde(default)]
    pub mine_channels: Vec<MineChannel<'a>>,
    /// Beatoraja implementation of invisible key channel.
    #[serde(default)]
    pub key_channels: Vec<KeyChannel<'a>>,
}

/// Header metadata of chart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BmsonInfo<'a> {
    /// Self explanatory title.
    pub title: Cow<'a, str>,
    /// Self explanatory subtitle. Usually this is shown as a smaller text than `title`.
    #[serde(default)]
    pub subtitle: Cow<'a, str>,
    /// Author of the chart. It may multiple names such as `Alice vs Bob`, `Alice feat. Bob` and so on. But you should respect the value because it usually have special meaning.
    pub artist: Cow<'a, str>,
    /// Other authors of the chart. This is useful for indexing and searching.
    ///
    /// Value of the array has form of `key:value`. The `key` can be `music`, `vocal`, `chart`, `image`, `movie` or `other`. If it has no `key`, you should treat as that `key` equals to `other`. The value may contains the spaces before and after `key` and `value`, so you should trim them.
    ///
    /// # Example
    ///
    /// ```json
    /// "subartists": ["music:5argon", "music:encX", "chart:flicknote", "movie:5argon", "image:5argon"]
    /// ```
    #[serde(default)]
    pub subartists: Vec<Cow<'a, str>>,
    /// Self explanatory genre.
    pub genre: Cow<'a, str>,
    /// Hint for layout lanes, e.g. "beat-7k", "popn-5k", "generic-nkeys". Defaults to `"beat-7k"`.
    ///
    /// If you want to support many lane modes of BMS, you should check this to determine the layout for lanes. Also you can check all lane information in `sound_channels` for strict implementation.
    #[serde(default = "default_mode_hint_cow")]
    pub mode_hint: Cow<'a, str>,
    /// Special chart name, e.g. "BEGINNER", "NORMAL", "HYPER", "FOUR DIMENSIONS".
    #[serde(default)]
    pub chart_name: Cow<'a, str>,
    /// Self explanatory level number. It is usually set with subjective rating by the author.
    pub level: u32,
    /// Initial BPM.
    pub init_bpm: PositiveF64,
    /// Relative judge width in percentage. The variation amount may different by BMS player. Larger is easier.
    #[serde(default = "default_percentage")]
    pub judge_rank: FinF64,
    /// Relative life bar gain in percentage. The variation amount may different by BMS player. Larger is easier.
    #[serde(default = "default_percentage")]
    pub total: FinF64,
    /// Background image file name. This should be displayed during the game play.
    #[serde(default)]
    pub back_image: Option<Cow<'a, str>>,
    /// Eyecatch image file name. This should be displayed during the chart is loading.
    #[serde(default)]
    pub eyecatch_image: Option<Cow<'a, str>>,
    /// Title image file name. This should be displayed before the game starts instead of title of the music.
    #[serde(default)]
    pub title_image: Option<Cow<'a, str>>,
    /// Banner image file name. This should be displayed in music select or result scene. The aspect ratio of image is usually 15:4.
    #[serde(default)]
    pub banner_image: Option<Cow<'a, str>>,
    /// Preview music file name. This should be played when this chart is selected in a music select scene.
    #[serde(default)]
    pub preview_music: Option<Cow<'a, str>>,
    /// Numbers of pulse per quarter note in 4/4 measure. You must check this because it affects the actual seconds of `PulseNumber`.
    #[serde(
        default = "default_resolution_nonzero",
        deserialize_with = "deserialize_resolution"
    )]
    pub resolution: NonZeroU64,
    /// Beatoraja implementation of long note type.
    #[serde(default)]
    pub ln_type: LnMode,
}

/// Default mode hint, beatmania 7 keys.
#[must_use]
pub const fn default_mode_hint() -> &'static str {
    "beat-7k"
}

const fn default_mode_hint_cow() -> Cow<'static, str> {
    Cow::Borrowed(default_mode_hint())
}

/// Default relative percentage constant, 100%.
const DEFAULT_PERCENTAGE: FinF64 = FinF64::new_const(100.0);

/// Default relative percentage, 100%.
#[must_use]
pub const fn default_percentage() -> FinF64 {
    DEFAULT_PERCENTAGE
}

/// Default resolution pulses per quarter note in 4/4 measure, 240 pulses.
#[must_use]
pub const fn default_resolution() -> u64 {
    240
}

const fn default_resolution_nonzero() -> NonZeroU64 {
    let Some(v) = NonZeroU64::new(default_resolution()) else {
        return NonZeroU64::MIN;
    };
    v
}

fn deserialize_resolution<'de, D>(deserializer: D) -> Result<NonZeroU64, D::Error>
where
    D: Deserializer<'de>,
{
    use serde::de::{Error, Visitor};
    use std::fmt;

    struct ResolutionVisitor;

    impl<'de> Visitor<'de> for ResolutionVisitor {
        type Value = NonZeroU64;

        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
            formatter.write_str("a number or null")
        }

        fn visit_none<E>(self) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(default_resolution_nonzero())
        }

        fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
        where
            D: Deserializer<'de>,
        {
            deserializer.deserialize_any(self)
        }

        fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(NonZeroU64::new(v.unsigned_abs()).unwrap_or_else(default_resolution_nonzero))
        }

        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            Ok(NonZeroU64::new(v).unwrap_or_else(default_resolution_nonzero))
        }

        fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
        where
            E: Error,
        {
            // Bmson (WebIDL unsigned long): must be an unsigned integer; negative allowed via abs
            let av = v.abs();
            if !av.is_finite() {
                return Err(E::custom("Resolution must be a finite number"));
            }
            // Reject any non-integer (has fractional part)
            if av.fract() != 0.0 {
                return Err(E::custom("Resolution must be an integer (unsigned long)"));
            }
            if av == 0.0 {
                return Ok(default_resolution_nonzero());
            }
            // Now av is a positive integer value in f64
            if av > (u64::MAX as f64) {
                return Err(E::custom(format!("Resolution value too large: {v}")));
            }
            Ok(NonZeroU64::new(av as u64).unwrap_or_else(default_resolution_nonzero))
        }
    }

    deserializer.deserialize_option(ResolutionVisitor)
}

/// Event of bar line of the chart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BarLine {
    /// Pulse number to place the line.
    pub y: PulseNumber,
}

/// Note sound file and positions to be placed in the chart.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SoundChannel<'a> {
    /// Sound file path. If the extension is not specified or not supported, you can try search files about other extensions for fallback.
    ///
    /// BMS players are expected to support the audio containers below:
    ///
    /// - WAV (`.wav`),
    /// - OGG (`.ogg`),
    /// - Audio-only MPEG-4 (`.m4a`).
    ///
    /// BMS players are expected to support the audio codec below:
    ///
    /// - LPCM (Linear Pulse-Code Modulation),
    /// - Ogg Vorbis,
    /// - AAC (Advanced Audio Coding).
    pub name: Cow<'a, str>,
    /// Data of note to be placed.
    pub notes: Vec<Note>,
}

/// Sound note to ring a sound file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Note {
    /// Position to be placed.
    pub y: PulseNumber,
    /// Lane information. The `Some` number represents the key to play, otherwise it is not playable (BGM) note.
    #[serde(deserialize_with = "deserialize_x_none_if_zero")]
    pub x: Option<NonZeroU8>,
    /// Length of pulses of the note. It will be a normal note if zero, otherwise a long note.
    pub l: u64,
    /// Continuation flag. Controls whether the audio restarts at this note position.
    ///
    /// - `false`: The audio will restart from the beginning at this note position.
    /// - `true`: The audio will continue from where it left off (no restart).
    ///
    /// This affects the slicing algorithm for sound playback:
    /// 1. All pulse numbers in the sound channel are collected (duplicates discarded).
    /// 2. Pulse numbers are converted to metric time (seconds).
    /// 3. When a note with `c: false` is encountered, the audio restarts.
    /// 4. Audio is sliced using the calculated time points.
    ///
    /// **Polyphony**: Each slice has a polyphony of 1. If multiple notes share the same slice
    /// (triggered simultaneously), the slice should not sound louder than normal.
    /// However, different slices from the same sound channel can play simultaneously
    /// (similar to BMS multiplex WAV definitions).
    pub c: bool,
    /// Beatoraja implementation of long note type.
    #[serde(default)]
    pub t: Option<LnMode>,
    /// Beatoraja implementation of long note up flag.
    /// If it is true and configured at the end position of a long note, then this position will become the ending note of the long note.
    #[serde(default)]
    pub up: Option<bool>,
}

fn deserialize_x_none_if_zero<'de, D>(deserializer: D) -> Result<Option<NonZeroU8>, D::Error>
where
    D: Deserializer<'de>,
{
    let opt = Option::<u8>::deserialize(deserializer)?;
    Ok(opt.and_then(NonZeroU8::new))
}

/// BPM change note.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BpmEvent {
    /// Position to change BPM of the chart.
    pub y: PulseNumber,
    /// New BPM to be.
    pub bpm: PositiveF64,
}

/// Scroll stop note.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StopEvent {
    /// Start position to scroll stop.
    pub y: PulseNumber,
    /// Stopping duration in pulses.
    pub duration: u64,
}

/// BGA data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Bga<'a> {
    /// Pictures data for playing BGA.
    #[serde(default)]
    pub bga_header: Vec<BgaHeader<'a>>,
    /// Base picture sequence.
    #[serde(default)]
    pub bga_events: Vec<BgaEvent>,
    /// Layered picture sequence.
    #[serde(default)]
    pub layer_events: Vec<BgaEvent>,
    /// Picture sequence displayed when missed.
    #[serde(default)]
    pub poor_events: Vec<BgaEvent>,
}

/// Picture file information.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BgaHeader<'a> {
    /// Self explanatory ID of picture.
    pub id: BgaId,
    /// Picture file name.
    pub name: Cow<'a, str>,
}

/// BGA note to display the picture.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BgaEvent {
    /// Position to display the picture in pulses.
    pub y: PulseNumber,
    /// ID of picture to display.
    pub id: BgaId,
}

/// Picture id for [`Bga`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct BgaId(pub u32);

impl BgaId {
    /// Returns the contained BGA id value.
    #[must_use]
    pub const fn value(self) -> u32 {
        self.0
    }
}

/// Beatoraja implementation of scroll event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScrollEvent {
    /// Position to scroll.
    pub y: PulseNumber,
    /// Scroll rate.
    pub rate: FinF64,
}

/// Beatoraja implementation of mine channel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MineEvent {
    /// Lane information. The `Some` number represents the key to play, otherwise it is not playable (BGM) note.
    #[serde(deserialize_with = "deserialize_x_none_if_zero")]
    pub x: Option<NonZeroU8>,
    /// Position to be placed.
    pub y: PulseNumber,
    /// Damage of the mine.
    pub damage: FinF64,
}

/// Beatoraja implementation of mine channel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MineChannel<'a> {
    /// Name of the mine sound file.
    pub name: Cow<'a, str>,
    /// Mine notes.
    pub notes: Vec<MineEvent>,
}

/// Beatoraja implementation of invisible key event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyEvent {
    /// Lane information. The `Some` number represents the key to play, otherwise it is not playable (BGM) note.
    #[serde(deserialize_with = "deserialize_x_none_if_zero")]
    pub x: Option<NonZeroU8>,
    /// Position to be placed.
    pub y: PulseNumber,
}

/// Beatoraja implementation of invisible key channel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyChannel<'a> {
    /// Name of the key sound file.
    pub name: Cow<'a, str>,
    /// Invisible key notes.
    pub notes: Vec<KeyEvent>,
}

/// Errors that can occur during BMSON parsing.
#[derive(Debug)]
pub enum BmsonParseError<'a> {
    /// JSON parsing warning intentionally emitted by the parser.
    JsonWarning {
        /// The parser-emitted warning diagnostic.
        warning: JsonWarning<'a>,
    },
    /// JSON grammar error that was recovered by the parser.
    JsonRecovered {
        /// The recovered chumsky error.
        error: JsonRecovered<'a>,
    },
    /// Unrecoverable JSON parsing error (no value produced).
    JsonError {
        /// The unrecoverable JSON parsing error.
        error: JsonError<'a>,
    },
    /// Deserialization error from serde.
    Deserialize {
        /// The serde deserialization error.
        error: serde_path_to_error::Error<serde_json::Error>,
    },
}

#[cfg(feature = "diagnostics")]
impl ToAriadne for serde_path_to_error::Error<serde_json::Error> {
    fn to_report<'b>(
        &self,
        src: &crate::diagnostics::SimpleSource<'b>,
    ) -> Report<'b, (String, std::ops::Range<usize>)> {
        let message = format!("{self}");
        build_report(
            src,
            ReportKind::Error,
            0..0,
            "BMSON deserialization error",
            &message,
            Color::Red,
        )
    }
}

#[cfg(feature = "diagnostics")]
impl ToAriadne for BmsonParseError<'_> {
    fn to_report<'b>(
        &self,
        src: &crate::diagnostics::SimpleSource<'b>,
    ) -> Report<'b, (String, std::ops::Range<usize>)> {
        match self {
            BmsonParseError::JsonWarning { warning } => warning.to_report(src),
            BmsonParseError::JsonRecovered { error } => error.to_report(src),
            BmsonParseError::JsonError { error } => error.to_report(src),
            BmsonParseError::Deserialize { error } => error.to_report(src),
        }
    }
}

/// Output of parsing a BMSON file.
///
/// This struct contains the parsed BMSON data (if successful), along with any
/// parsing errors that occurred during the process.
pub struct BmsonParseOutput<'a> {
    /// The parsed BMSON data, or None if parsing failed.
    pub bmson: Option<Bmson<'a>>,
    /// Errors that occurred during parsing.
    pub errors: Vec<BmsonParseError<'a>>,
}

/// Parse a BMSON file from JSON string.
///
/// This function provides a convenient way to parse a BMSON file in one step.
/// It uses chumsky parser internally to parse JSON, then deserializes the result
/// using `serde_path_to_error` for detailed error information.
///
/// # Returns
///
/// Returns a `BmsonParseOutput` containing the parsed BMSON data (if successful),
/// or various types of parsing errors that occurred during the process.
#[must_use]
pub fn parse_bmson<'a>(json: &'a str) -> BmsonParseOutput<'a> {
    // First parse JSON using chumsky parser
    let (value, parse_errors) = parser().parse(json.trim()).into_output_errors();

    let had_output = value.is_some();
    // Split chumsky errors into warnings, recovered errors, and fatal errors
    let (warnings, recovered, fatal) = split_chumsky_errors(parse_errors, had_output);

    // Collect JSON parsing diagnostics
    let mut errors: Vec<BmsonParseError<'a>> =
        Vec::with_capacity(warnings.len() + recovered.len() + fatal.len());
    errors.extend(
        warnings
            .into_iter()
            .map(|warning| BmsonParseError::JsonWarning { warning }),
    );
    errors.extend(
        recovered
            .into_iter()
            .map(|error| BmsonParseError::JsonRecovered { error }),
    );
    errors.extend(
        fatal
            .into_iter()
            .map(|error| BmsonParseError::JsonError { error }),
    );

    // Try to get a JSON value from either chumsky or serde_json
    let serde_fallback = serde_json::from_str(json);
    let json_value = value.or_else(|| serde_fallback.as_ref().ok().cloned());

    // If neither parser produced a value and no fatal chumsky error existed,
    // synthesize a fatal JSON error from the serde error for better diagnostics.
    if json_value.is_none()
        && let Err(e) = serde_fallback
        && !errors
            .iter()
            .any(|err| matches!(err, BmsonParseError::JsonError { .. }))
    {
        let span = SimpleSpan::new((), 0..json.len());
        errors.push(BmsonParseError::JsonError {
            error: JsonError(Rich::custom(span, format!("Invalid JSON: {e}"))),
        });
    }

    // Try to deserialize the JSON value into Bmson
    let bmson = json_value
        .map(|json_value| serde_path_to_error::deserialize(&json_value))
        .and_then(|deserialize_result| {
            deserialize_result
                .map_err(|error| errors.push(BmsonParseError::Deserialize { error }))
                .ok()
        });

    BmsonParseOutput { bmson, errors }
}