From e1d84f63a345b34e465ce3ccf3de715e983843ab Mon Sep 17 00:00:00 2001 From: MikuroXina <10331164+MikuroXina@users.noreply.github.com> Date: Mon, 6 Jan 2025 18:36:12 +0900 Subject: [PATCH 1/3] Support SCROLL and SPEED --- src/lex/command.rs | 6 ++ src/lex/token.rs | 19 +++++++ src/parse/header.rs | 32 +++++++++++ src/parse/notes.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+) diff --git a/src/lex/command.rs b/src/lex/command.rs index 519688c..e095ffe 100644 --- a/src/lex/command.rs +++ b/src/lex/command.rs @@ -257,6 +257,10 @@ pub enum Channel { SectionLen, /// For the stop object. Stop, + /// For the scroll speed change object. + Scroll, + /// For the note spacing change object. + Speed, } impl Channel { @@ -271,6 +275,8 @@ impl Channel { "06" => BgaPoor, "07" => BgaLayer, "09" => Stop, + "SC" => Scroll, + "SP" => Speed, player1 if player1.starts_with('1') => Note { kind: NoteKind::Visible, is_player1: true, diff --git a/src/lex/token.rs b/src/lex/token.rs index 7d900e3..c770670 100644 --- a/src/lex/token.rs +++ b/src/lex/token.rs @@ -6,6 +6,7 @@ use super::{command::*, cursor::Cursor, Result}; /// A token of BMS format. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum Token<'a> { /// `#ARTIST [string]`. Defines the artist name of the music. Artist(&'a str), @@ -120,12 +121,16 @@ pub enum Token<'a> { Random(u32), /// `#RANK [0-3]`. Defines the judgement level. Rank(JudgeLevel), + /// `#SCROLL[01-ZZ] [f64]`. Defines the scroll speed change object. It changes relative falling speed of notes with keeping BPM. For example, if applying `2.0`, the scroll speed will become double. + Scroll(ObjId, &'a str), /// `#SETRANDOM [u32]`. Starts a random scope but the integer will be used as the generated random number. It should be used only for tests. SetRandom(u32), /// `#SETSWITCH [u32]`. Starts a switch scope but the integer will be used as the generated random number. It should be used only for tests. SetSwitch(u32), /// `#SKIP`. Escapes the current switch scope. It is often used in the end of every case scope. Skip, + /// `#SPEED[01-ZZ] [f64]`. Defines the spacing change object. It changes relative spacing of notes with linear interpolation. For example, if playing score between the objects `1.0` and `2.0`, the spaces of notes will increase at the certain rate until the `2.0` object. + Speed(ObjId, &'a str), /// `#STAGEFILE [filename]`. Defines the splashscreen image. It should be 640x480. StageFile(&'a Path), /// `#STOP[01-ZZ] [0-4294967295]`. Defines the stop object. The scroll will stop the beats of the integer divided by 192. A beat length depends on the current BPM. If there are other objects on same time, the stop object must be evaluated at last. @@ -275,6 +280,20 @@ impl<'a> Token<'a> { .map_err(|_| c.err_expected_token("integer"))?; Self::Stop(ObjId::from(id, c)?, stop) } + scroll if scroll.starts_with("#SCROLL") => { + let id = command.trim_start_matches("#SCROLL"); + let scroll = c + .next_token() + .ok_or_else(|| c.err_expected_token("scroll factor"))?; + Self::Scroll(ObjId::from(id, c)?, scroll) + } + speed if speed.starts_with("#SPEED") => { + let id = command.trim_start_matches("#SPEED"); + let scroll = c + .next_token() + .ok_or_else(|| c.err_expected_token("spacing factor"))?; + Self::Speed(ObjId::from(id, c)?, scroll) + } message if message.starts_with('#') && message.chars().nth(6) == Some(':') diff --git a/src/parse/header.rs b/src/parse/header.rs index d5d5b06..10f471a 100644 --- a/src/parse/header.rs +++ b/src/parse/header.rs @@ -95,6 +95,10 @@ pub struct Header { pub bmp_files: HashMap, /// The BPMs corresponding to the id of the BPM change object. pub bpm_changes: HashMap, + /// The scrolling factors corresponding to the id of the scroll speed change object. + pub scrolling_factor_changes: HashMap, + /// The spacing factors corresponding to the id of the spacing change object. + pub spacing_factor_changes: HashMap, /// The texts corresponding to the id of the text object. pub texts: HashMap, /// The option messages corresponding to the id of the change option object. @@ -211,6 +215,34 @@ impl Header { Token::PlayLevel(play_level) => self.play_level = Some(play_level), Token::PoorBga(poor_bga_mode) => self.poor_bga_mode = poor_bga_mode, Token::Rank(rank) => self.rank = Some(rank), + Token::Scroll(id, factor) => { + let parsed: f64 = factor + .parse() + .map_err(|_| ParseError::BpmParseError(factor.into()))?; + if parsed <= 0.0 || !parsed.is_finite() { + return Err(ParseError::BpmParseError(factor.into())); + } + if self.scrolling_factor_changes.insert(id, parsed).is_some() { + eprintln!( + "duplicated bpm change definition found: {:?} {:?}", + id, factor + ); + } + } + Token::Speed(id, factor) => { + let parsed: f64 = factor + .parse() + .map_err(|_| ParseError::BpmParseError(factor.into()))?; + if parsed <= 0.0 || !parsed.is_finite() { + return Err(ParseError::BpmParseError(factor.into())); + } + if self.spacing_factor_changes.insert(id, parsed).is_some() { + eprintln!( + "duplicated bpm change definition found: {:?} {:?}", + id, factor + ); + } + } Token::StageFile(file) => self.stage_file = Some(file.into()), Token::Stop(id, len) => { self.stops diff --git a/src/parse/notes.rs b/src/parse/notes.rs index 06056ef..0a55ba0 100644 --- a/src/parse/notes.rs +++ b/src/parse/notes.rs @@ -153,6 +153,66 @@ pub enum BgaLayer { Overlay, } +/// An object to change scrolling factor of the score. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ScrollingFactorObj { + /// The time to begin the change of BPM. + pub time: ObjTime, + /// The scrolling factor to be. + pub factor: f64, +} + +impl PartialEq for ScrollingFactorObj { + fn eq(&self, other: &Self) -> bool { + self.time == other.time + } +} + +impl Eq for ScrollingFactorObj {} + +impl PartialOrd for ScrollingFactorObj { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ScrollingFactorObj { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.time.cmp(&other.time) + } +} + +/// An object to change spacing factor between notes with linear interpolation. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SpacingFactorObj { + /// The time to begin the change of BPM. + pub time: ObjTime, + /// The spacing factor to be. + pub factor: f64, +} + +impl PartialEq for SpacingFactorObj { + fn eq(&self, other: &Self) -> bool { + self.time == other.time + } +} + +impl Eq for SpacingFactorObj {} + +impl PartialOrd for SpacingFactorObj { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SpacingFactorObj { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.time.cmp(&other.time) + } +} + /// The objects set for querying by lane or time. #[derive(Debug, Default)] pub struct Notes { @@ -164,6 +224,8 @@ pub struct Notes { section_len_changes: BTreeMap, stops: BTreeMap, bga_changes: BTreeMap, + scrolling_factor_changes: BTreeMap, + spacing_factor_changes: BTreeMap, } impl Notes { @@ -271,6 +333,34 @@ impl Notes { } } + /// Adds a new scrolling factor change object to the notes. + pub fn push_scrolling_factor_change(&mut self, bpm_change: ScrollingFactorObj) { + if self + .scrolling_factor_changes + .insert(bpm_change.time, bpm_change) + .is_some() + { + eprintln!( + "duplicate scrolling factor change object detected at {:?}", + bpm_change.time + ); + } + } + + /// Adds a new spacing factor change object to the notes. + pub fn push_spacing_factor_change(&mut self, bpm_change: SpacingFactorObj) { + if self + .spacing_factor_changes + .insert(bpm_change.time, bpm_change) + .is_some() + { + eprintln!( + "duplicate spacing factor change object detected at {:?}", + bpm_change.time + ); + } + } + /// Adds a new section length change object to the notes. pub fn push_section_len_change(&mut self, section_len_change: SectionLenChangeObj) { if self @@ -335,6 +425,32 @@ impl Notes { }); } } + Token::Message { + track, + channel: Channel::Scroll, + message, + } => { + for (time, obj) in ids_from_message(*track, message) { + let &factor = header + .scrolling_factor_changes + .get(&obj) + .ok_or(ParseError::UndefinedObject(obj))?; + self.push_scrolling_factor_change(ScrollingFactorObj { time, factor }); + } + } + Token::Message { + track, + channel: Channel::Speed, + message, + } => { + for (time, obj) in ids_from_message(*track, message) { + let &factor = header + .spacing_factor_changes + .get(&obj) + .ok_or(ParseError::UndefinedObject(obj))?; + self.push_spacing_factor_change(SpacingFactorObj { time, factor }); + } + } Token::Message { track, channel: Channel::SectionLen, @@ -529,6 +645,10 @@ pub struct NotesPack { pub stops: Vec, /// BGA change events. pub bga_changes: Vec, + /// Scrolling factor change events. + pub scrolling_factor_changes: Vec, + /// Spacing factor change events. + pub spacing_factor_changes: Vec, } #[cfg(feature = "serde")] @@ -543,6 +663,8 @@ impl serde::Serialize for Notes { section_len_changes: self.section_len_changes.values().cloned().collect(), stops: self.stops.values().cloned().collect(), bga_changes: self.bga_changes.values().cloned().collect(), + scrolling_factor_changes: self.scrolling_factor_changes.values().cloned().collect(), + spacing_factor_changes: self.spacing_factor_changes.values().cloned().collect(), } .serialize(serializer) } @@ -588,6 +710,14 @@ impl<'de> serde::Deserialize<'de> for Notes { for bga_change in pack.bga_changes { bga_changes.insert(bga_change.time, bga_change); } + let mut scrolling_factor_changes = BTreeMap::new(); + for scrolling_change in pack.scrolling_factor_changes { + scrolling_factor_changes.insert(scrolling_change.time, scrolling_change); + } + let mut spacing_factor_changes = BTreeMap::new(); + for spacing_change in pack.spacing_factor_changes { + spacing_factor_changes.insert(spacing_change.time, spacing_change); + } Ok(Notes { objs, bgms, @@ -596,6 +726,8 @@ impl<'de> serde::Deserialize<'de> for Notes { section_len_changes, stops, bga_changes, + scrolling_factor_changes, + spacing_factor_changes, }) } } From 51d8cd324640996f859e90bd4ae8b6da70f8988f Mon Sep 17 00:00:00 2001 From: MikuroXina <10331164+MikuroXina@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:10:22 +0900 Subject: [PATCH 2/3] Support EXT --- src/lex/command.rs | 2 ++ src/lex/token.rs | 32 +++++++++++++++++++++++++ src/parse/notes.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/lex/command.rs b/src/lex/command.rs index e095ffe..f40e47d 100644 --- a/src/lex/command.rs +++ b/src/lex/command.rs @@ -229,6 +229,8 @@ impl Default for PoorMode { /// The channel, or lane, where the note will be on. #[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[non_exhaustive] pub enum Channel { /// The BGA channel. BgaBase, diff --git a/src/lex/token.rs b/src/lex/token.rs index c770670..7462548 100644 --- a/src/lex/token.rs +++ b/src/lex/token.rs @@ -76,6 +76,15 @@ pub enum Token<'a> { EndRandom, /// `#ENDSWITCH`. Closes the random scope. See [Token::Switch]. EndSwitch, + /// `#EXT #XXXYY:...`. Defines the extended message. `XXX` is the track, `YY` is the channel. + ExtendedMessage { + /// The track, or measure, must start from 1. But some player may allow the 0 measure (i.e. Lunatic Rave 2). + track: Track, + /// The channel commonly expresses what the lane be arranged the note to. + channel: Channel, + /// The message to the channel, but not only object ids. + message: &'a str, + }, /// `#BMP[01-ZZ] [0-255],[0-255],[0-255],[0-255] [filename]`. Defines the background image/movie object with the color (alpha, red, green and blue) which will be treated as transparent. ExBmp(ObjId, Argb, &'a Path), /// `#EXRANK[01-ZZ] [0-3]`. Defines the judgement level change object. @@ -294,6 +303,29 @@ impl<'a> Token<'a> { .ok_or_else(|| c.err_expected_token("spacing factor"))?; Self::Speed(ObjId::from(id, c)?, scroll) } + ext_message if ext_message.starts_with("#EXT") => { + let message = c + .next_token() + .ok_or_else(|| c.err_expected_token("message definition"))?; + if !(message.starts_with('#') + && message.chars().nth(6) == Some(':') + && 8 <= message.len()) + { + eprintln!("unknown #EXT format: {:?}", message); + continue; + } + + let track = message[1..4] + .parse() + .map_err(|_| c.err_expected_token("[000-999]"))?; + let channel = &message[4..6]; + let message = &message[7..]; + Self::ExtendedMessage { + track: Track(track), + channel: Channel::from(channel, c)?, + message, + } + } message if message.starts_with('#') && message.chars().nth(6) == Some(':') diff --git a/src/parse/notes.rs b/src/parse/notes.rs index 0a55ba0..f3e3bf3 100644 --- a/src/parse/notes.rs +++ b/src/parse/notes.rs @@ -213,6 +213,38 @@ impl Ord for SpacingFactorObj { } } +/// An extended object on the score. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExtendedMessageObj { + /// The track which the message is on. + pub track: Track, + /// The channel which the message is on. + pub channel: Channel, + /// The extended message. + pub message: String, +} + +impl PartialEq for ExtendedMessageObj { + fn eq(&self, other: &Self) -> bool { + self.track == other.track + } +} + +impl Eq for ExtendedMessageObj {} + +impl PartialOrd for ExtendedMessageObj { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ExtendedMessageObj { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.track.cmp(&other.track) + } +} + /// The objects set for querying by lane or time. #[derive(Debug, Default)] pub struct Notes { @@ -226,6 +258,7 @@ pub struct Notes { bga_changes: BTreeMap, scrolling_factor_changes: BTreeMap, spacing_factor_changes: BTreeMap, + extended_messages: Vec, } impl Notes { @@ -392,6 +425,11 @@ impl Notes { } } + /// Adds the new extended message object to the notes. + pub fn push_extended_message(&mut self, message: ExtendedMessageObj) { + self.extended_messages.push(message); + } + pub(crate) fn parse(&mut self, token: &Token, header: &Header) -> Result<()> { match token { Token::Message { @@ -525,6 +563,18 @@ impl Notes { }); } } + Token::ExtendedMessage { + track, + channel, + message, + } => { + let track = Track(track.0); + self.push_extended_message(ExtendedMessageObj { + track, + channel: channel.clone(), + message: (*message).to_owned(), + }); + } &Token::LnObj(end_id) => { let mut end_note = self .remove_latest_note(end_id) @@ -649,6 +699,8 @@ pub struct NotesPack { pub scrolling_factor_changes: Vec, /// Spacing factor change events. pub spacing_factor_changes: Vec, + /// Extended message events. + pub extended_messages: Vec, } #[cfg(feature = "serde")] @@ -665,6 +717,7 @@ impl serde::Serialize for Notes { bga_changes: self.bga_changes.values().cloned().collect(), scrolling_factor_changes: self.scrolling_factor_changes.values().cloned().collect(), spacing_factor_changes: self.spacing_factor_changes.values().cloned().collect(), + extended_messages: self.extended_messages.clone(), } .serialize(serializer) } @@ -718,6 +771,10 @@ impl<'de> serde::Deserialize<'de> for Notes { for spacing_change in pack.spacing_factor_changes { spacing_factor_changes.insert(spacing_change.time, spacing_change); } + let mut extended_messages = vec![]; + for extended_message in pack.extended_messages { + extended_messages.push(extended_message); + } Ok(Notes { objs, bgms, @@ -728,6 +785,7 @@ impl<'de> serde::Deserialize<'de> for Notes { bga_changes, scrolling_factor_changes, spacing_factor_changes, + extended_messages, }) } } From 1077616e26def4685d93d92ae54e82ecbe8369ae Mon Sep 17 00:00:00 2001 From: MikuroXina <10331164+MikuroXina@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:14:30 +0900 Subject: [PATCH 3/3] Add test for bemuse extensions --- tests/bemuse_ext.bms | 8 ++++++++ tests/files.rs | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/bemuse_ext.bms diff --git a/tests/bemuse_ext.bms b/tests/bemuse_ext.bms new file mode 100644 index 0000000..771bc45 --- /dev/null +++ b/tests/bemuse_ext.bms @@ -0,0 +1,8 @@ +#SCROLL01 1 +#SCROLL02 0.5 +#SPEED01 1 +#SPEED02 0.5 +#001SC:01020102 +#001SP:01020102 + +#EXT #01112:hello diff --git a/tests/files.rs b/tests/files.rs index f888264..d2bda06 100644 --- a/tests/files.rs +++ b/tests/files.rs @@ -40,3 +40,11 @@ fn test_blank() { }) ); } + +#[test] +fn test_bemuse_ext() { + let source = include_str!("bemuse_ext.bms"); + let ts = parse(source).expect("must be parsed"); + let bms = Bms::from_token_stream(&ts, RngMock([1])).expect("must be parsed"); + eprintln!("{:?}", bms); +}