diff --git a/Cargo.lock b/Cargo.lock index 396583d..fefbdbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2518,6 +2518,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "katexit" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfb0b7ce7938f84a5ecbdca5d0a991e46bc9d6d078934ad5e92c5270fe547db" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -3235,6 +3246,7 @@ dependencies = [ name = "parser" version = "0.1.0" dependencies = [ + "katexit", "strum 0.27.1", "strum_macros 0.27.1", "winnow", diff --git a/Cargo.toml b/Cargo.toml index 17b6c15..30409ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] -resolver = "3" +resolver = "2" members = [ "parser","player"] diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 1ec21bb..8cce0fd 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +katexit = "0.1.5" strum = "0.27.1" strum_macros = "0.27.1" winnow = "0.7.11" diff --git a/parser/src/header.rs b/parser/src/header.rs index cbf663b..620c000 100644 --- a/parser/src/header.rs +++ b/parser/src/header.rs @@ -1,11 +1,13 @@ -struct Header { +use strum_macros::FromRepr; + +pub struct Header { player: Player, } -/// Defines the play side. +/// `#PLAYER [1-4]`. Defines the play side. #[derive(FromRepr, Debug, PartialEq, Clone)] #[repr(u8)] -enum Player { +pub enum Player { One, // SP Two, // Couple play Three, // DP @@ -17,3 +19,377 @@ impl Default for Player { Self::One } } + +/// `#RANK [0-3]`. Defines the judge difficulty. +/// +/// We follow LR2 convention here, so Rank is 0,1,2,3 +#[derive(FromRepr, Debug, PartialEq, Clone)] +#[repr(u8)] +pub enum Rank { + VeryHard, // RANK 0, +-8ms + Hard, // RANK 1, +- 15ms + Normal, // RANK 2, +- 18ms + Easy, // RANK 3, +- 21ms +} + +// LR2 Convention is to apply Normal when unspecified. +impl Default for Rank { + fn default() -> Self { + Self::Normal + } +} + +pub enum JudgeRankType { + /// `#RANK [0-3]` Normal rank system. + /// + /// This is what you see 99% of the time. + Rank, + + /// `#DEFEXRANK n`. Percentage judge. + /// Defexrank is very strange. It specifies judge difficulty as a percentage of RANK 2. + /// This means that 100 is equal to RANK 2. If a DefExRank of 199.97 is sepcified then + /// the rank is 199.97% of RANK 2. + /// + /// TODO: Implement Defexrank + Defexrank(f32), + /// `#EXRANK[01-ZZ] n`. In-chart adjustable rank. + /// Exrank is also weird. It allows the timing window of the chart to be changed + /// during play. + /// + /// The string represents the identifier (In the example "cc" or "aa") + /// and the f32 represents the rank as a percentage of rank 2. + /// + /// # Example + /// To steal from hitkey as an example. + /// ``` + /// #RANK 2 + /// #EXRANKaa 48 + /// #EXRANKcc 100 + /// #114a0:aa0000cc + /// ``` + /// Would result in the timing window for measure 114 changing to DEFEXRANK 48, + /// and then going back to DEFEXRANK 100 at the end of the measure. + Exrank(String, f32), +} + +#[cfg_attr(doc, katexit::katexit)] +/// `#TOTAL n`. Rate of Gague Recovery ™ +/// +/// Total is a bit tricky to wrap your head around. +/// As far as the player is concerned, TOTAL is what defines the rate of gauge recovery. +/// +/// Let n be the value of #TOTAL n, and y the total amount of objects in the chart +/// the rate of gauge recovery on a PGREAT is +/// $\frac{n}{y}$ +/// +/// If there are 400 objects to hit, and TOTAL is 200, we get $\frac{200}{400} = 0.5$ +/// +/// If this field is omitted, the spec does not specify what the default value should be. +/// - LR2 uses 160 +/// - jbmsparser for beatoraja uses 100. +/// - We will use 160 +pub struct Total(f64); + +impl Default for Total { + fn default() -> Total { + Self(160.0) + } +} + +/// `#VOLWAV n`. Flat volume multiplier. +/// +/// Defaults to 100. +/// +/// In general, this gets ignored. +/// +/// #VOLWAV 250 would be playing at 250% volume. +/// +/// #VOLWAV 25 would be playing at 25% volume. +pub struct Volwav(i32); + +/// `#STAGEFILE imagefilename`. Splash screen. +/// +/// This command is omissible. When omitted it is expected that the default splashscreen +/// will be used. +pub struct Stagefile(String); + +/// `#BANNER imagefilename`. Song select banner image. +pub struct Banner(String); + +/// `#BACKBMP imagefilename`. Static "movie" background. +/// +/// If we chose to follow the OverActive style, then this is a pre-movie splash +/// like the song title, genre and such in IIDX. +/// https://right-stick.sub.jp/backbmp/index.html +pub struct BackBmp(String); + +/// `#PLAYLEVEL n`. Song difficulty. +/// +/// Reported difficulty level. This will usually be [1-12] IIDX style. +/// +/// #PLAYLEVEL 0 is a strange case. This is usually for gimmick charts which +/// use commands like `#RANDOM` or `#SWITCH` +pub struct PlayLevel(u16); + +/// For whatever reason, BM98 used #PLAYLEVEL 3 as it's default if this was +/// omitted. Apparently many followed this, even through it's not spec. +impl Default for PlayLevel { + fn default() -> Self { + PlayLevel(3) + } +} + +/// `#DIFFICULTY [1-5]`. Difficulty. Normal/Hyper etc +/// +/// We follow an adjusted IIDX naming convention in this enum. +/// Beginner, Normal, Hyper, Another are from IIDX, but we take INSANE from bms. +/// There's a lot of naiming conventions, these are disambiguated in code comments +/// +/// This command is omissable, and anything which doesn't have it is expected to be +/// unsortable and unfilterable by this metric. +#[derive(FromRepr, Debug, PartialEq, Clone)] +#[repr(u8)] +enum Difficulty { + Beginner, // Easy/Beginner/Light + Normal, // Normal/Standard + Hyper, // Hard + Another, // EX, Maximum + Insane, // Kusofumen, 糞譜面, INSANE, 発狂, hakkyou, SUPER-CRAZY +} + +/// `#TITLE string` Title of the track. +/// +/// Unsurprisingly, defines the title of the track. +/// +/// When omitted, it's not actually defined what will happen. +/// +/// There's no limit on the length of the title. +/// +/// LR2 trims whitespace on the left and right of the title. +/// +/// Encoding and decoding can be hellish, because of how old BMS as a format is. +/// Don't expect UTF-8 everywhere, expect a good chunk of UTF-16, SHIFT-JIS, +/// IEC 2022 and even EUC. +/// +/// Implicit Subtitles exist as a form of in-band signalling. +/// The subtitle is delimited by any of the following +/// - `#TITLE main-sub-` +/// - `#TITLE main~sub~` +/// - `#TITLE main(sub)` +/// - `#TITLE main[sub]` +/// - `#TITLE main` +/// - `#TITLE main␣␣sub` (Two+ halfwidth spaces) +/// - `#TITLE main"sub"` (Straight double quotes) +/// +/// Because of how varied these are, we will make a choice as to which we will support. +/// This is fine since there's now a `#SUBTITLE` command. +/// +/// We will support full width tilde and quote marks only. +pub struct Title(String); + +/// `#SUBTITLE string` Subtitle of the track +/// +/// Due to the limitations of the implicit subtitle, an actual SUBTITLE field was +/// defined. +/// +/// Omissible. LR2 will only check for a implicit subtitle if `#SUBTITLE` doesn't exist. +pub struct Subtitle(String); + +/// `#ARTIST string` +/// +/// Definition of the track artist. Interestingly Artist isn't actually defined +/// in the spec. +pub struct Artist(String); + +/// `#SUBARTIST string` +/// +/// Added by LR2. This is used usually to define things like BGA artists, +/// noters and other such co-artists. +pub struct Subartist(String); + +/// `#MAKER string` +/// +/// Sometimes supported. +/// +/// Used to denote when a composer differs from the chart maker. In this case +/// it is used to store the chart makers name. +pub struct Maker(String); + +/// `#GENRE string` +/// +/// Denotes the genre of music for the chart. +/// By default it will be empty if not set. +/// +/// Supported by basically every client. +pub struct Genre(String); + +// TODO: Landmine +// It's in WAV00 + +/// `#BPM n` +/// +/// Defines the BPM of the music. Defines the scroll speed etc. +/// The BMS spec defines 130 as the default value. +/// +/// it is expected that fractional BPMs are supported, as such we will repr +/// this as a float. +pub struct ConstantBPM(f32); + +// Standard defined default. +// TODO. Implement BPM changes +impl Default for ConstantBPM { + fn default() -> Self { + Self(130.0) + } +} + +/// `#BPMxx n` OR `#EXBPM[01-ZZ] n` +/// +/// Hitkey refers to this as exBPM or "Extended BPM Change Command". +/// This operates on Channel #xxx08. +/// +/// This command allows the chart designer to describe BPM changes. +/// +/// Negative BPMs are allowed, and in general are expected to make the chart scroll _backwards_. +/// +/// For more info, see https://hitkey.nekokan.dyndns.info/exbpm-object.htm +/// +/// In parsing, we expect to parse the identifier to the string, and the bpm to the float. +/// +/// # Example +/// ``` +/// #BPMAA 256 +/// #BPMBB 155.5 +/// #00108:AABBAABB +/// ``` +/// +/// This defines the BPM AA to be 256, the BPM BB to be 155.5 and says where to use it +/// in the chart itself. +pub struct ExBPM(String, f32); + +/// Represent the multiple types of BPM as enum variants. +pub enum BPM { + Constant(ConstantBPM), + Extended(ExBPM), +} + +/// `#STOP[01-ZZ] n`, DDR Style stops! +/// +/// Operates on Channel #xxx09 +/// +/// A STOP sequence is an object which stops scrolling of the chart for a defined +/// period of time. +/// Once the time is elapsed, the chart will resume normal scrolling. +/// +/// We should treat a `#STOPxx 1` as defining a STOP for 1/192nd of a note in 4/4. +/// It's also important to note that we use the BPM _at time of stop_. +/// +/// Implementing this in game is a bit tricky, due to the #xxx02 command which adjusts +/// how long a measure is. +/// +/// Decimal stops have their fractional part dropped. +/// +/// A negative STOP duration is weirdly defined. For LR2 it applies "-1" and causes it +/// to skip some amount of notes. +/// Generally, it's ignored. +/// +/// # Example +/// +/// ``` +/// #STOP11 96 +/// #STOP22 96 +/// #00109:0011 +/// #00202:0.5 +/// #00209:0022 +/// ``` +/// +/// Reading BMS is awkward, so think about each line here being split like so +/// [AAA][BB]:[CC] +/// Where AAA is the measure, BB is the command we're executing on that measure +/// and CC is the "operand" to that command. +/// +/// Defines STOP 11 as having a length of 192nd note * 96, and so does STOP 22. +/// #00109:0011 says to do STOP 11 on measure 1 +/// #00209:0022 says to do STOP 22 on measure 2 +/// +/// An example of a 1 second stop +/// ``` +/// #BPM 60 // Set BPM to 60 +/// #STOP33 48 // 48 * 1/192 stop length +/// #00109:33 +/// ``` +/// +/// If a STOP and a note are on the same time, the note plays first then timing changes. +/// If a STOP and a BPM change are on the same time, then BPM changes then STOP is applied. +/// +/// For some more examples, please refer to +/// https://hitkey.bms.ms/cmds.htm#STOP +pub struct Stop(String, u32); + +/// `#LNTYPE[0-3]`. Long Note type +/// +/// LNType is a field kept for backwards compatibility, as it's no longer needed +/// with the advent of LNOBJ. +/// +/// LnType 1 is what we hope we see. We should probably just parse error and +/// tell the user we're ignoring their chart if it's LNType 2 or 3. +/// +/// This is omissble. +pub struct LNType(u8); + +/// `#LNOBJ xx` +/// +/// This is RDM type LNs. They have sounds on keyup and they're annoying. +/// +/// TODO: Explain how this works better rather than handwaving it. +pub struct LNObj(u32); + +/// `#WAV[00-ZZ] filename` +/// +/// One of the most common commands! This defines the sound files that we actually +/// play! +/// +/// A single file is assignable to two or more indexes. This is used for polyphony. +/// +/// "Alternate search" is expected now. If we cant find example.wav we should search +/// for example.ogg, example.mp3 etc +/// +/// Certain channels are assigned, to certain things. +/// +/// - `#xxx01` is assigned to the BGM. +/// - `#xxx11-19` is assigned to P1 notes +/// - `#xxx21-29` is assigned to P2 notes +/// - `#xxx31-39` is assigned to P1 _invisible_ notes +/// - `#xxx41-49` is assigned to P2 _invisible_ notes +/// - `#xxx51-59` are assigned to P1 LNs. +/// - `#xxx61-69` are assigned to P2 LNs. +/// - `#xxxD1-D9` are assigned to P1 landmines. +/// - `#xxxE1-E9` are assigned to P2 landmines. +/// +/// For more info see https://hitkey.bms.ms/cmds.htm#WAVXX +/// as this is one of the most complex commands we encounter +pub struct Wav(u32, String); + +/// `#BMP[00-ZZ] filename` +/// +/// Image resources. And Also video! +/// +/// Interesringly, certain channels are assigned to certain images. +/// +/// - `#xxx04` is the base bga +/// - `#xxx06` is the POOR image +/// - `#xxx07` is the "BGA Layer" +/// - `#xxx0A` is the "BGA Layer 2" +/// +/// Videos can be used in any of these indexs. In general, we should expect video playback +/// to only be possible on #xxx04. +/// +/// When a video is being played, we should apply layers/miss animation etc overtop. +/// +/// If nothing is found in channel `#xxx04/06/07` we should display just a black image. +/// +/// We should support as many image formats as is reasonable. +/// PNG, JPG, GIF, TGA, DDS are all common. +/// +/// Like with #WAV we should support alternate search. So try PNG then JPEG then GIF etc. +pub struct Bmp(u32, String); diff --git a/parser/src/lib.rs b/parser/src/lib.rs index b93cf3f..f505d68 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod header; diff --git a/player/Cargo.toml b/player/Cargo.toml index 4921fce..72c7d56 100644 --- a/player/Cargo.toml +++ b/player/Cargo.toml @@ -5,3 +5,12 @@ edition = "2024" [dependencies] bevy = { version = "0.16.1", features = ["dynamic_linking"] } + + +# Enable a small amount of optimization in the dev profile. +[profile.dev] +opt-level = 1 + +# Enable a large amount of optimization in the dev profile for dependencies. +[profile.dev.package."*"] +opt-level = 3