From afbcbed295a24ab1d21943e0371d4efff90e732c Mon Sep 17 00:00:00 2001 From: Seiya KONNO Date: Mon, 24 Aug 2026 01:36:06 +0900 Subject: [PATCH 1/3] fix(examples): repair the broken test chart fixtures `benchmark-command-mix-100.bms` parsed to zero events. It opened `#SETRANDOM 2` and `#RANDOM 2` back to back, and since both push a control-flow scope while only one `#ENDRANDOM` pops one, the capture stack never drained and all 100 measures of object data were swallowed into the trailing branch. Resolving the control flow recovered 3 events out of ~600. The same mistake repeated for `#SETSWITCH 3` / `#SWITCH 3` and the second `#SETRANDOM 3` / `#RANDOM 3`, so drop the three redundant scope openers and keep the deterministic `#SET*` form. `sample.roundtrip.bms` was BMSON JSON under a `.bms` name, byte-identical to `sample.roundtrip.bmson`. Delete it. `sample.json`, `sample.edit.json` and `sjis-encoding-test.json` still carried a pre-`bms`/`bmson`-extension IR shape that no current code path can produce, and `sample.roundtrip.bmson` put `resolution` at the document root instead of inside `info` and omitted `lines`. Regenerate all four through the editor API. `lane-order-test.bms` and `sjis-encoding-test.bms` declared `#WAV01 test.wav`, which does not exist in this directory, so neither chart was playable. Point both at `sample.wav`. --- examples/test/benchmark-command-mix-100.bms | 3 - examples/test/lane-order-test.bms | 2 +- examples/test/sample.edit.json | 418 ++++++++++++++++++-- examples/test/sample.json | 414 +++++++++++++++++-- examples/test/sample.roundtrip.bms | 99 ----- examples/test/sample.roundtrip.bmson | 18 +- examples/test/sjis-encoding-test.bms | 2 +- examples/test/sjis-encoding-test.json | 95 ++++- 8 files changed, 870 insertions(+), 181 deletions(-) delete mode 100644 examples/test/sample.roundtrip.bms diff --git a/examples/test/benchmark-command-mix-100.bms b/examples/test/benchmark-command-mix-100.bms index 22ab10b6..45ae60ea 100644 --- a/examples/test/benchmark-command-mix-100.bms +++ b/examples/test/benchmark-command-mix-100.bms @@ -61,7 +61,6 @@ *---------------------- CONTROL FLOW FIELD #SETRANDOM 2 -#RANDOM 2 #IF 1 #01011:0505 #01016:05 @@ -71,7 +70,6 @@ #ENDIF #ENDRANDOM #SETSWITCH 3 -#SWITCH 3 #CASE 1 #02011:01010101 #SKIP @@ -83,7 +81,6 @@ #SKIP #ENDSW #SETRANDOM 3 -#RANDOM 3 #IF 1 #03004:01 #ELSEIF 2 diff --git a/examples/test/lane-order-test.bms b/examples/test/lane-order-test.bms index 22f68f84..cbb000db 100644 --- a/examples/test/lane-order-test.bms +++ b/examples/test/lane-order-test.bms @@ -8,7 +8,7 @@ *---------------------- RESOURCES FIELD -#WAV01 test.wav +#WAV01 sample.wav *---------------------- MEASURE LENGTH FIELD diff --git a/examples/test/sample.edit.json b/examples/test/sample.edit.json index b002999a..de1a48e3 100644 --- a/examples/test/sample.edit.json +++ b/examples/test/sample.edit.json @@ -26,128 +26,472 @@ { "measure": 0, "channel": "11", - "value": "01", "position": [ 0, - 1 - ] + 4 + ], + "value": "01" }, { "measure": 0, "channel": "11", - "value": "01", "position": [ 1, 4 - ] + ], + "value": "01" }, { "measure": 0, "channel": "11", - "value": "01", "position": [ - 1, - 2 - ] + 2, + 4 + ], + "value": "01" }, { "measure": 0, "channel": "11", - "value": "01", "position": [ 3, 4 - ] + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ 0, - 1 - ] + 4 + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ 1, 4 - ] + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ - 1, - 2 - ] + 2, + 4 + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ 3, 4 - ] + ], + "value": "01" }, { "measure": 2, "channel": "03", - "value": "64", "position": [ 0, 1 - ] + ], + "value": "64" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ 0, - 1 - ] + 4 + ], + "value": "01" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ 1, 4 - ] + ], + "value": "01" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ - 1, - 2 - ] + 2, + 4 + ], + "value": "01" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ 3, 4 - ] + ], + "value": "01" }, { "measure": 3, "channel": "11", - "value": "01", "position": [ 1, 2 + ], + "value": "01" + } + ], + "bms": { + "controlFlow": [], + "lnObjs": [], + "exRank": {}, + "argb": {}, + "stp": [], + "wavCmds": [], + "changeOption": {}, + "exWav": {}, + "exBmp": {}, + "bga": {}, + "scroll": {}, + "speed": {}, + "swBga": {} + }, + "bmson": { + "info": {}, + "bga": { + "header": [], + "events": [], + "layerEvents": [], + "poorEvents": [] + } + }, + "preservation": { + "bms": { + "sourceLines": [ + { + "kind": "header", + "command": "TITLE", + "value": "Sample" + }, + { + "kind": "header", + "command": "ARTIST", + "value": "Codex" + }, + { + "kind": "header", + "command": "BPM", + "value": "120" + }, + { + "kind": "header", + "command": "WAV01", + "value": "sample.wav" + }, + { + "kind": "object", + "measure": 0, + "channel": "11", + "events": [ + { + "measure": 0, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "kind": "object", + "measure": 1, + "channel": "11", + "events": [ + { + "measure": 1, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "kind": "object", + "measure": 2, + "channel": "03", + "events": [ + { + "measure": 2, + "channel": "03", + "position": [ + 0, + 1 + ], + "value": "64" + } + ] + }, + { + "kind": "object", + "measure": 2, + "channel": "11", + "events": [ + { + "measure": 2, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + } + ], + "objectLines": [ + { + "measure": 0, + "channel": "11", + "events": [ + { + "measure": 0, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "measure": 1, + "channel": "11", + "events": [ + { + "measure": 1, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "measure": 2, + "channel": "03", + "events": [ + { + "measure": 2, + "channel": "03", + "position": [ + 0, + 1 + ], + "value": "64" + } + ] + }, + { + "measure": 2, + "channel": "11", + "events": [ + { + "measure": 2, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + } ] + }, + "bmson": { + "lines": [], + "bpmEvents": [], + "stopEvents": [], + "soundChannels": [] } - ] + } } diff --git a/examples/test/sample.json b/examples/test/sample.json index ca52eaec..382976ff 100644 --- a/examples/test/sample.json +++ b/examples/test/sample.json @@ -21,119 +21,463 @@ { "measure": 0, "channel": "11", - "value": "01", "position": [ 0, - 1 - ] + 4 + ], + "value": "01" }, { "measure": 0, "channel": "11", - "value": "01", "position": [ 1, 4 - ] + ], + "value": "01" }, { "measure": 0, "channel": "11", - "value": "01", "position": [ - 1, - 2 - ] + 2, + 4 + ], + "value": "01" }, { "measure": 0, "channel": "11", - "value": "01", "position": [ 3, 4 - ] + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ 0, - 1 - ] + 4 + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ 1, 4 - ] + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ - 1, - 2 - ] + 2, + 4 + ], + "value": "01" }, { "measure": 1, "channel": "11", - "value": "01", "position": [ 3, 4 - ] + ], + "value": "01" }, { "measure": 2, "channel": "03", - "value": "64", "position": [ 0, 1 - ] + ], + "value": "64" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ 0, - 1 - ] + 4 + ], + "value": "01" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ 1, 4 - ] + ], + "value": "01" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ - 1, - 2 - ] + 2, + 4 + ], + "value": "01" }, { "measure": 2, "channel": "11", - "value": "01", "position": [ 3, 4 + ], + "value": "01" + } + ], + "bms": { + "controlFlow": [], + "lnObjs": [], + "exRank": {}, + "argb": {}, + "stp": [], + "wavCmds": [], + "changeOption": {}, + "exWav": {}, + "exBmp": {}, + "bga": {}, + "scroll": {}, + "speed": {}, + "swBga": {} + }, + "bmson": { + "info": {}, + "bga": { + "header": [], + "events": [], + "layerEvents": [], + "poorEvents": [] + } + }, + "preservation": { + "bms": { + "sourceLines": [ + { + "kind": "header", + "command": "TITLE", + "value": "Sample" + }, + { + "kind": "header", + "command": "ARTIST", + "value": "Codex" + }, + { + "kind": "header", + "command": "BPM", + "value": "120" + }, + { + "kind": "header", + "command": "WAV01", + "value": "sample.wav" + }, + { + "kind": "object", + "measure": 0, + "channel": "11", + "events": [ + { + "measure": 0, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "kind": "object", + "measure": 1, + "channel": "11", + "events": [ + { + "measure": 1, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "kind": "object", + "measure": 2, + "channel": "03", + "events": [ + { + "measure": 2, + "channel": "03", + "position": [ + 0, + 1 + ], + "value": "64" + } + ] + }, + { + "kind": "object", + "measure": 2, + "channel": "11", + "events": [ + { + "measure": 2, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + } + ], + "objectLines": [ + { + "measure": 0, + "channel": "11", + "events": [ + { + "measure": 0, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 0, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "measure": 1, + "channel": "11", + "events": [ + { + "measure": 1, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 1, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + }, + { + "measure": 2, + "channel": "03", + "events": [ + { + "measure": 2, + "channel": "03", + "position": [ + 0, + 1 + ], + "value": "64" + } + ] + }, + { + "measure": 2, + "channel": "11", + "events": [ + { + "measure": 2, + "channel": "11", + "position": [ + 0, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 1, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 2, + 4 + ], + "value": "01" + }, + { + "measure": 2, + "channel": "11", + "position": [ + 3, + 4 + ], + "value": "01" + } + ] + } ] + }, + "bmson": { + "lines": [], + "bpmEvents": [], + "stopEvents": [], + "soundChannels": [] } - ] + } } diff --git a/examples/test/sample.roundtrip.bms b/examples/test/sample.roundtrip.bms deleted file mode 100644 index f255169d..00000000 --- a/examples/test/sample.roundtrip.bms +++ /dev/null @@ -1,99 +0,0 @@ -{ - "version": "1.0.0", - "info": { - "title": "Sample", - "subtitle": "", - "artist": "Codex", - "genre": "", - "level": 0, - "init_bpm": 120, - "mode_hint": "beat-1k" - }, - "resolution": 240, - "bpm_events": [ - { - "y": 1920, - "bpm": 100 - } - ], - "stop_events": [], - "sound_channels": [ - { - "name": "sample.wav", - "notes": [ - { - "x": 1, - "y": 0, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 240, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 480, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 720, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 960, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 1200, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 1440, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 1680, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 1920, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 2160, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 2400, - "l": 0, - "c": false - }, - { - "x": 1, - "y": 2640, - "l": 0, - "c": false - } - ] - } - ] -} diff --git a/examples/test/sample.roundtrip.bmson b/examples/test/sample.roundtrip.bmson index f255169d..2ea04ff2 100644 --- a/examples/test/sample.roundtrip.bmson +++ b/examples/test/sample.roundtrip.bmson @@ -7,9 +7,23 @@ "genre": "", "level": 0, "init_bpm": 120, - "mode_hint": "beat-1k" + "mode_hint": "beat-1k", + "resolution": 240 }, - "resolution": 240, + "lines": [ + { + "y": 0 + }, + { + "y": 960 + }, + { + "y": 1920 + }, + { + "y": 2880 + } + ], "bpm_events": [ { "y": 1920, diff --git a/examples/test/sjis-encoding-test.bms b/examples/test/sjis-encoding-test.bms index 669b9f86..954ea67a 100644 --- a/examples/test/sjis-encoding-test.bms +++ b/examples/test/sjis-encoding-test.bms @@ -8,7 +8,7 @@ *---------------------- RESOURCES FIELD -#WAV01 test.wav +#WAV01 sample.wav *---------------------- MEASURE LENGTH FIELD diff --git a/examples/test/sjis-encoding-test.json b/examples/test/sjis-encoding-test.json index 464eed60..3fa1df57 100644 --- a/examples/test/sjis-encoding-test.json +++ b/examples/test/sjis-encoding-test.json @@ -9,7 +9,7 @@ }, "resources": { "wav": { - "01": "test.wav" + "01": "sample.wav" }, "bmp": {}, "bpm": {}, @@ -21,11 +21,100 @@ { "measure": 0, "channel": "11", - "value": "01", "position": [ 0, 1 + ], + "value": "01" + } + ], + "bms": { + "controlFlow": [], + "lnObjs": [], + "exRank": {}, + "argb": {}, + "stp": [], + "wavCmds": [], + "changeOption": {}, + "exWav": {}, + "exBmp": {}, + "bga": {}, + "scroll": {}, + "speed": {}, + "swBga": {} + }, + "bmson": { + "info": {}, + "bga": { + "header": [], + "events": [], + "layerEvents": [], + "poorEvents": [] + } + }, + "preservation": { + "bms": { + "sourceLines": [ + { + "kind": "header", + "command": "TITLE", + "value": "テスト曲" + }, + { + "kind": "header", + "command": "ARTIST", + "value": "佐藤" + }, + { + "kind": "header", + "command": "BPM", + "value": "120" + }, + { + "kind": "header", + "command": "WAV01", + "value": "sample.wav" + }, + { + "kind": "object", + "measure": 0, + "channel": "11", + "events": [ + { + "measure": 0, + "channel": "11", + "position": [ + 0, + 1 + ], + "value": "01" + } + ] + } + ], + "objectLines": [ + { + "measure": 0, + "channel": "11", + "events": [ + { + "measure": 0, + "channel": "11", + "position": [ + 0, + 1 + ], + "value": "01" + } + ] + } ] + }, + "bmson": { + "lines": [], + "bpmEvents": [], + "stopEvents": [], + "soundChannels": [] } - ] + } } From 7ab2b8370d26e78b831b4d0825bfa1ed2af6519f Mon Sep 17 00:00:00 2001 From: Seiya KONNO Date: Mon, 24 Aug 2026 01:36:33 +0900 Subject: [PATCH 2/3] test(examples): cover the untested feature clusters with real chart fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 19 of the 36 files in `examples/test` were read by no test at all, which is how a benchmark chart that parses to zero events and a stale round-trip pair went unnoticed. Add `packages/parser/src/fixtures.test.ts` as the corpus-level guard: it scans the directory, parses every chart off disk, and requires each one to yield playable events after control-flow resolution, so a new fixture is picked up automatically and a broken one fails a test. New fixtures, one per feature cluster that had none: - `extended-directives.bms` — `#EXWAV` / `#EXBMP` / `#ARGB` / `#BGA` / `#SWBGA` / `#WAVCMD` / `#EXRANK` / `#CHANGEOPTION` with payloads the `@be-music/chart` value parsers accept. `four-measure-command-combo-test.bms` proves the parser routes these headers, but its payloads are placeholders (`#EXWAV01 ex_sample.wav`, `#BGA01 01`, `#WAVCMD legacy`) that every value parser rejects, so nothing covered a realistic payload off a real file. - `scroll-speed-gimmick.bms` — one measure per timing / drawing gimmick, including the `SP` channel and the `#SCROLL 0` / `#SCROLL < 0` cases the player has an explicit compatibility policy for. - `bus-volume-channels.bms` — the `97` / `98` dynamic volume buses. - `base62-ids.bms` — `#BASE 62` case-distinct IDs in indexed headers, channel streams, `#LNOBJ`, and inside a `#RANDOM` / `#IF` block. - `invisible-notes.bms` — `3x` / `4x` invisible keysound channels, and the guarantee that 2P-side invisible channels do not turn a single-player chart into a DP chart. - `lntype2-long-note.bms` — `#LNTYPE 2` continuous runs, including two runs split by a gap and a run crossing a measure boundary. - `nonpositive-bpm.bms` — channel `08` pointed at zero, negative and undefined `#BPMxx` slots. - `5key-sp.bms`, `10key-dp.bms`, `7key-longnote.bml` — the lane-mode branches that had no fixture. Together with the existing charts this covers every `ChartPlayVariant` the resolver can return, and gives the `.bml` extension allow-lists a real file to open. - `bmson-key-channels-mines.bmson` — beatoraja `key_channels` mines, `info.ln_type` and `notes[].t`. Move the inline chart text in three tests onto these fixtures (`#SPEED` / `SP`, `#BASE 62`, bmson `key_channels`). Inline inputs stay inline where a file would hide the assertion: malformed-input probes, charset byte sequences, and table-driven micro-cases such as the 20 `resolveChartPlayVariant` channel lists. Wire up the fixtures that existed but were orphaned: the `sample.*` quintet now backs a live import → edit → export round trip in the editor, and the retrigger and ogg charts are rendered through the audio renderer instead of being described by hand-built IR. --- examples/test/10key-dp.bms | 61 +++ examples/test/5key-sp.bms | 59 +++ examples/test/7key-longnote.bml | 62 +++ examples/test/base62-ids.bms | 67 +++ examples/test/bmson-key-channels-mines.bmson | 50 +++ examples/test/bus-volume-channels.bms | 63 +++ examples/test/extended-directives.bms | 108 +++++ examples/test/invisible-notes.bms | 67 +++ examples/test/lntype2-long-note.bms | 66 +++ examples/test/nonpositive-bpm.bms | 68 +++ examples/test/scroll-speed-gimmick.bms | 103 +++++ packages/audio-renderer/src/index.test.ts | 66 +++ packages/editor/src/index.test.ts | 46 ++ packages/parser/src/fixtures.test.ts | 402 ++++++++++++++++++ packages/parser/src/index.test.ts | 73 ++-- .../src/cli/chart-selection.test.ts | 12 +- 16 files changed, 1333 insertions(+), 40 deletions(-) create mode 100644 examples/test/10key-dp.bms create mode 100644 examples/test/5key-sp.bms create mode 100644 examples/test/7key-longnote.bml create mode 100644 examples/test/base62-ids.bms create mode 100644 examples/test/bmson-key-channels-mines.bmson create mode 100644 examples/test/bus-volume-channels.bms create mode 100644 examples/test/extended-directives.bms create mode 100644 examples/test/invisible-notes.bms create mode 100644 examples/test/lntype2-long-note.bms create mode 100644 examples/test/nonpositive-bpm.bms create mode 100644 examples/test/scroll-speed-gimmick.bms create mode 100644 packages/parser/src/fixtures.test.ts diff --git a/examples/test/10key-dp.bms b/examples/test/10key-dp.bms new file mode 100644 index 00000000..04459aa8 --- /dev/null +++ b/examples/test/10key-dp.bms @@ -0,0 +1,61 @@ +*---------------------- METADATA FIELD + +#TITLE 10 KEY DP Lane Mode +#SUBTITLE IIDX 5-key double play - channels 11..16 and 21..26 +#ARTIST be-music +#GENRE TEST +#BPM 120 +#PLAYLEVEL 2 +#TOTAL 250 +#DIFFICULTY 2 + +*---------------------- EXTENDED HEADER FIELD +* The 2P-side, no-6/7 corner of the IIDX lane-mode table: five keys plus scratch +* on both sides, which must resolve to 10 KEY DP. +* +* This chart is also the counter-example for the PMS-STD content rule. That rule +* claims any chart touching `22..25` for 9 KEY, but only when the traditional +* IIDX 2P channels (`21` and `26..29`) are absent. A real 5-key DP chart authors +* `21` and `26` alongside `22..25`, so it has to stay 10 KEY DP. +* +* See 5key-sp.bms for the rest of the lane-mode fixture family. + +#PLAYER 2 +#LNTYPE 1 + +*---------------------- RESOURCES FIELD + +#WAV01 sample.wav +#WAV02 branch.wav + +*---------------------- MEASURE LENGTH FIELD + +*---------------------- OBJECT DATA FIELD + +* Measure 0 - one eighth note per lane, sweeping 1P left to right then 2P. +#00011:0100000000000000 +#00012:0001000000000000 +#00013:0000010000000000 +#00014:0000000100000000 +#00015:0000000001000000 +#00021:0000000000010000 +#00022:0000000000000100 +#00023:0000000000000001 + +* Measure 1 - scratch on both sides, keys alternating between the two players. +#00016:02000200 +#00026:00020002 +#00111:01000100 +#00115:01000100 +#00121:00010001 +#00125:00010001 + +* Measure 2 - the 2P keyboard columns 22..25 on their own. Without 21 / 26 in +* the chart this measure alone would look like PMS-STD, so it exists to keep the +* 10 KEY DP classification honest. +#00222:01010101 +#00223:01010101 +#00224:01010101 +#00225:01010101 + +*---------------------- CONTROL FLOW FIELD diff --git a/examples/test/5key-sp.bms b/examples/test/5key-sp.bms new file mode 100644 index 00000000..1bae7e35 --- /dev/null +++ b/examples/test/5key-sp.bms @@ -0,0 +1,59 @@ +*---------------------- METADATA FIELD + +#TITLE 5 KEY SP Lane Mode +#SUBTITLE IIDX 5-key single play - channels 11..16, no 18 / 19 +#ARTIST be-music +#GENRE TEST +#BPM 120 +#PLAYLEVEL 1 +#TOTAL 200 +#DIFFICULTY 1 + +*---------------------- EXTENDED HEADER FIELD +* Lane-mode detection falls through to the IIDX family when no POPN or keyboard +* marker matches, and then splits on two signals only: whether the chart uses +* the 2P side, and whether it uses the 6th / 7th columns (`18` / `19` / `28` / +* `29`). This chart is the 1P-side, no-6/7 corner of that table: five keys plus +* scratch, which must resolve to 5 KEY SP. +* +* Keeping it as `.bms` matters - the extension carries no lane information for +* this family, so the classification has to come from the channels alone. +* +* Companion fixtures for the rest of the family: +* 10key-dp.bms - 5 keys on both sides -> 10 KEY DP +* ln-mode-1-long-note.bms - 1P side with 18 / 19 -> 7 KEY SP +* lane-order-test.bms - both sides with 18 / 19 / 28 / 29 -> 14 KEY DP +* 9key-all-keys-quarter.pms - PMS-STD -> 9 KEY +* 24key-keyboard-sp.bme - extended 1A..1O columns -> 24 KEY SP +* 48key-keyboard-dp.bme - extended columns on both sides -> 48 KEY DP + +#PLAYER 1 +#LNTYPE 1 + +*---------------------- RESOURCES FIELD + +#WAV01 sample.wav +#WAV02 branch.wav + +*---------------------- MEASURE LENGTH FIELD + +*---------------------- OBJECT DATA FIELD + +* Measure 0 - one eighth note per lane, left to right, then scratch. +#00011:0100000000000000 +#00012:0001000000000000 +#00013:0000010000000000 +#00014:0000000100000000 +#00015:0000000001000000 +#00016:0000000000020000 + +* Measure 1 - all five keys as a chord on every quarter note, scratch on the +* backbeat. +#00111:01010101 +#00112:01010101 +#00113:01010101 +#00114:01010101 +#00115:01010101 +#00116:00020002 + +*---------------------- CONTROL FLOW FIELD diff --git a/examples/test/7key-longnote.bml b/examples/test/7key-longnote.bml new file mode 100644 index 00000000..a6377869 --- /dev/null +++ b/examples/test/7key-longnote.bml @@ -0,0 +1,62 @@ +*---------------------- METADATA FIELD + +#TITLE 7 KEY Long Note (BML) +#SUBTITLE The .bml extension - 7 keys plus scratch with #LNOBJ long notes +#ARTIST be-music +#GENRE TEST +#BPM 140 +#PLAYLEVEL 5 +#TOTAL 300 +#DIFFICULTY 3 + +*---------------------- EXTENDED HEADER FIELD +* `.bml` is the fourth chart extension the toolchain accepts, alongside `.bms`, +* `.bme` and `.pms`. It carries no parsing rule of its own - by convention it +* marks a 7-key chart that uses long notes - so it only shows up in the +* extension allow-lists (the player-tui chart picker, the player-web drop +* handler, the CLI usage line) and in lane-mode detection's "not .pms" path. +* +* Without a real `.bml` file in this directory those allow-list entries were +* only ever tested against synthetic paths, so this chart exists to be opened: +* it has to parse like any other BMS chart and classify as 7 KEY SP. + +#PLAYER 1 +#LNTYPE 1 +#LNMODE 1 +#LNOBJ ZZ + +*---------------------- RESOURCES FIELD + +#WAV01 sample.wav +#WAV02 branch.wav +#WAV03 right.wav +#WAVZZ sample.wav + +*---------------------- MEASURE LENGTH FIELD + +*---------------------- OBJECT DATA FIELD + +* Measure 0 - count-in on the seven keys, scratch on the backbeat. +#00011:01010101 +#00013:00020002 +#00015:02000200 +#00016:00030003 + +* Measure 1 - a 2-beat long note per key group, which is what the extension is +* conventionally used to advertise. +#00111:0100ZZ00 +#00113:0100ZZ00 +#00115:0100ZZ00 + +* Measure 2 - the 6th and 7th columns (18 / 19), which are what separate 7 KEY +* from 5 KEY in lane-mode detection. +#00218:010000ZZ +#00219:010000ZZ + +* Measure 3 - scratch plus a chord, no long notes. +#00311:01010101 +#00318:00020002 +#00319:02000200 +#00316:00030003 + +*---------------------- CONTROL FLOW FIELD diff --git a/examples/test/base62-ids.bms b/examples/test/base62-ids.bms new file mode 100644 index 00000000..6cbfd882 --- /dev/null +++ b/examples/test/base62-ids.bms @@ -0,0 +1,67 @@ +*---------------------- METADATA FIELD + +#BASE 62 + +#TITLE Base 62 Object IDs +#SUBTITLE #BASE 62 keeps upper and lower case as separate slots +#ARTIST be-music +#GENRE TEST +#BPM 120 +#PLAYLEVEL 1 +#TOTAL 200 +#DIFFICULTY 1 + +*---------------------- EXTENDED HEADER FIELD +* `#BASE 62` is the beatoraja extension that widens the two-character object ID +* space from base 36 to base 62 by making lower case letters distinct slots. +* Under the default base 36 every pair below would collapse onto one key, so +* this chart is the file-level regression guard for the case-preserving path: +* +* #WAV0a / #WAV0A - distinct sample slots +* #00111:0a0A0z - distinct channel-stream tokens +* #LNOBJ zz - LN end marker matched case-sensitively +* +* `#BASE` must appear before the first object line to take effect, so it is +* declared at the very top of the file. + +#PLAYER 1 +#LNTYPE 1 +#LNMODE 1 +#LNOBJ zz + +*---------------------- RESOURCES FIELD + +#WAV0a sample.wav +#WAV0A branch.wav +#WAV0z right.wav +#WAV0Z wrong.wav +#WAVZz sample.wav +#WAVzz sample.wav + +*---------------------- MEASURE LENGTH FIELD + +*---------------------- OBJECT DATA FIELD + +* Measure 0 - four case-distinct slots in one channel stream. +#00011:0a0A0z0Z + +* Measure 1 - the same four slots on a playable lane. +#00111:0a0A0z0Z + +* Measure 2 - a long note whose end marker is the lower case `zz`. Under base 36 +* this would fold to `ZZ` and stop matching the #LNOBJ declaration. +#00211:0a00zz00 + +* Measure 4 - lower case, upper case and mixed case IDs in one channel stream. +#00411:0a0AZz + +*---------------------- CONTROL FLOW FIELD + +* An indexed header declared inside a control-flow branch still has to register +* under its lower case key. +#RANDOM 1 +#IF 1 +#WAV0b wrong.wav +#00311:0b0b +#ENDIF +#ENDRANDOM diff --git a/examples/test/bmson-key-channels-mines.bmson b/examples/test/bmson-key-channels-mines.bmson new file mode 100644 index 00000000..144eb968 --- /dev/null +++ b/examples/test/bmson-key-channels-mines.bmson @@ -0,0 +1,50 @@ +{ + "version": "1.0.0", + "info": { + "title": "BMSON Key Channels Mines", + "subtitle": "beatoraja key_channels / ln_type / notes[].t extensions", + "artist": "be-music", + "genre": "TEST", + "level": 4, + "init_bpm": 150, + "resolution": 240, + "mode_hint": "beat-7k", + "ln_type": 2, + "judge_rank": 100, + "total": 260 + }, + "lines": [{ "y": 0 }, { "y": 960 }, { "y": 1920 }], + "bpm_events": [], + "stop_events": [], + "sound_channels": [ + { + "name": "sample.wav", + "notes": [ + { "x": 1, "y": 0, "l": 0, "c": false }, + { "x": 2, "y": 240, "l": 0, "c": false }, + { "x": 1, "y": 480, "l": 480, "c": false, "t": 3 }, + { "x": 6, "y": 960, "l": 240, "c": false, "t": 1 }, + { "x": 7, "y": 1200, "l": 0, "c": false }, + { "x": 8, "y": 1440, "l": 0, "c": false } + ] + }, + { + "name": "branch.wav", + "notes": [ + { "y": 0, "l": 0, "c": false }, + { "y": 960, "l": 0, "c": false }, + { "y": 1920, "l": 0, "c": false } + ] + } + ], + "key_channels": [ + { + "name": "wrong.wav", + "notes": [ + { "x": 1, "y": 1680, "damage": 0 }, + { "x": 6, "y": 1800, "damage": 50 }, + { "x": 8, "y": 1920, "damage": 100 } + ] + } + ] +} diff --git a/examples/test/bus-volume-channels.bms b/examples/test/bus-volume-channels.bms new file mode 100644 index 00000000..88bf2294 --- /dev/null +++ b/examples/test/bus-volume-channels.bms @@ -0,0 +1,63 @@ +*---------------------- METADATA FIELD + +#TITLE Dynamic Bus Volume Channels +#SUBTITLE Channel 97 (BGM bus) and channel 98 (key bus) +#ARTIST be-music +#GENRE TEST +#BPM 120 +#PLAYLEVEL 1 +#TOTAL 200 +#DIFFICULTY 1 + +*---------------------- EXTENDED HEADER FIELD +* Channels `97` and `98` are the two dynamic volume buses: +* +* #xxx97 - BGM-side (autoplay / channel 01) bus volume +* #xxx98 - playable-side (key sound) bus volume +* +* The token is a base-36 pair read as a 0..FF ratio of the original volume, so +* `FF` is unity, `80` is roughly half, and `01` is the quietest audible step. +* `00` is "no event" rather than silence, so it is never authored as a value. +* +* Both channels must stay out of the sample-trigger path - authoring a volume +* change must not itself make a sound - which is what makes a real chart worth +* having here rather than only the classifier unit tests. + +#PLAYER 1 +#LNTYPE 1 +#VOLWAV 100 + +*---------------------- RESOURCES FIELD + +#WAV01 sample.wav +#WAV02 branch.wav + +*---------------------- MEASURE LENGTH FIELD + +*---------------------- OBJECT DATA FIELD + +* Measure 0 - reference bar. BGM on channel 01, key sounds on channel 11, both +* at the volume the chart declares. +#00001:02020202 +#00011:01010101 + +* Measure 1 - duck the BGM bus to half and then to the quietest step while the +* key bus stays untouched. +#00197:8001 +#00101:02020202 +#00111:01010101 + +* Measure 2 - restore the BGM bus and duck the key bus instead. +#00297:FF00 +#00298:8040 +#00201:02020202 +#00211:01010101 + +* Measure 3 - both buses move on the same beat, which is where an ordering bug +* between the two gain stages shows up. +#00397:40FF +#00398:FF40 +#00301:02020202 +#00311:01010101 + +*---------------------- CONTROL FLOW FIELD diff --git a/examples/test/extended-directives.bms b/examples/test/extended-directives.bms new file mode 100644 index 00000000..8d02deac --- /dev/null +++ b/examples/test/extended-directives.bms @@ -0,0 +1,108 @@ +*---------------------- METADATA FIELD + +#TITLE Extended Directive Payloads +#SUBTITLE Structurally valid #EXWAV / #EXBMP / #ARGB / #BGA / #SWBGA / #WAVCMD / #EXRANK payloads +#ARTIST be-music +#GENRE TEST +#BPM 120 +#PLAYLEVEL 1 +#TOTAL 200 +#DIFFICULTY 1 + +*---------------------- EXTENDED HEADER FIELD +* `four-measure-command-combo-test.bms` proves the parser ROUTES every extended +* header into the right `bms` field, but its payloads are placeholders +* (`#EXWAV01 ex_sample.wav`, `#BGA01 01`, `#SWBGA01 02`, `#WAVCMD legacy`) that +* every `@be-music/chart` value parser rejects. This chart carries the same +* directive family with payloads that actually parse, so the value parsers are +* exercised against a real file instead of inline strings only. +* +* Payload layouts (hitkey BMS Memo): +* #EXWAVxx flags drawn from `pvf`, params positional +* #EXBMPxx a,r,g,b,filename ARGB tuple plus the image filename +* #ARGBxx AARRGGBB | a,r,g,b both accepted spellings +* #BGAxx YY x1 y1 x2 y2 dx dy sub-region crop of #BMPYY +* #SWBGAxx fr:tot:lp[:ARGB] N1 N2 ... switching (animated) BGA, fr in 1/100 s +* #WAVCMD pp xx vv pp = 00 pitch / 01 volume / 02 loop +* #EXRANKxx judge-window scale, cued on channel A0 +* #CHANGEOPTIONxx