Add support for #SENOTECHANGE
command (#81)
TJAPlayer3 implementation details (https://github.com/IepIweidieng/TJAPlayer3/blob/gh-pages/tja.md#senotechange): - `#SENOTECHANGE` can be placed at the start of a measure, or in the middle of the measure - It will only apply to 1 note immediately after the command - It will override any automatic Don2/Don3/Ka2 assignment (from note clusters) How it works in `tja2fumen`: - `#SENOTECHANGE` will be tracked in the TJA measure object - It will split the measure into sub-measures (similar to gogo/bpm/etc.) - It will only be applied to the first note, after which it will be overwritten - We set a `manually_set` flag to ensure that the manually-chosen value doesn't get overwritten. Fixes #69.
This commit is contained in:
parent
9b6f05b420
commit
85ea4d6efc
43
README.md
43
README.md
@ -143,27 +143,28 @@ If there is an unsupported feature that you would like support for, please make
|
||||
|
||||
> **Legend**: `✅` = Fully supported, `⚪️` = Ignored, `⚠️` = Incorrect behavior, `❌` = Not supported
|
||||
|
||||
| | tja 2 fumen | tja 2 bin | Comment |
|
||||
|----------------------------------------------|-------------|-----------|---------------------------------------------------------------------------------------------------------------|
|
||||
| `0`, `1`, `2`, `3`, `4` | `✅` | `⚠️` | tja2fumen will write proper SENOTES (ド, コ, ドン, カ, カッ), see [#41](https://github.com/vivaria/tja2fumen/issues/41). |
|
||||
| `5008,`, `6008,`, `7008,` | `✅` | `✅` | |
|
||||
| `9008,` | `✅` | `⚠️` | |
|
||||
| `9000,`<br>`9008,` | `⚪️` | `⚠️` | Double Kusudama note treated as 1 drumroll by tja2fumen, but 2 overlapping drumrolls by tja2bin. |
|
||||
| `A`, `B` | `✅` | `❌` | Multiplayer "hands" notes are valid in fumens, but unrecognized by tja2bin. |
|
||||
| `C`, `D`, `E`, `F`, `G`, `H`, `I` | `⚠️` | `❌` | Replaced by normal notes/rolls in tja2fumen. |
|
||||
| `#START`, `#END` | `✅` | `✅` | |
|
||||
| `#START P1`, `#START P2` | `✅` | `❌` | |
|
||||
| `#BPMCHANGE` | `✅` | `⚠️` | See [#16](https://github.com/Fluto/TakoTako/issues/16) |
|
||||
| `#MEASURE` | `✅` | `✅` | |
|
||||
| `#SCROLL` | `✅` | `✅` | |
|
||||
| `#GOGOSTART`, `#GOGOEND` | `✅` | `✅` | |
|
||||
| `#BARLINEOFF`, `#BARLINEON` | `✅` | `✅` | |
|
||||
| `#DELAY` | `✅` | `❌` | See [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
||||
| `#BRANCHSTART`, `#BRANCHEND` | `✅` | `✅` | |
|
||||
| `#N`, `#E`, `#M` | `✅` | `✅` | |
|
||||
| `#SECTION` | `⚠️` | `❌` | See [#53](https://github.com/vivaria/tja2fumen/issues/53), [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
||||
| `#LEVELHOLD` | `✅` | `❌` | |
|
||||
| `#BMSCROLL`, `#LYRIC`,<br>`#DIRECTION`, etc. | `⚪️` | `❌` | Other simulator-specific chart commands are currently ignored. |
|
||||
| | tja 2 fumen | tja 2 bin | Comment |
|
||||
|----------------------------------------------|-----------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `0`, `1`, `2`, `3`, `4` | `✅` | `⚠️` | tja2fumen will write proper SENOTES (ド, コ, ドン, カ, カッ), see [#41](https://github.com/vivaria/tja2fumen/issues/41). |
|
||||
| `5008,`, `6008,`, `7008,` | `✅` | `✅` | |
|
||||
| `9008,` | `✅` | `⚠️` | |
|
||||
| `9000,`<br>`9008,` | `⚪️` | `⚠️` | Double Kusudama note treated as 1 drumroll by tja2fumen, but 2 overlapping drumrolls by tja2bin. |
|
||||
| `A`, `B` | `✅` | `❌` | Multiplayer "hands" notes are valid in fumens, but unrecognized by tja2bin. |
|
||||
| `C`, `D`, `E`, `F`, `G`, `H`, `I` | `⚠️` | `❌` | Replaced by normal notes/rolls in tja2fumen. |
|
||||
| `#SENOTECHANGE` | `✅` | `❌` | Recently added. See [#69](https://github.com/vivaria/tja2fumen/issues/69) and [#81](https://github.com/vivaria/tja2fumen/issues/81) for details. |
|
||||
| `#START`, `#END` | `✅` | `✅` | |
|
||||
| `#START P1`, `#START P2` | `✅` | `❌` | |
|
||||
| `#BPMCHANGE` | `✅` | `⚠️` | See [#16](https://github.com/Fluto/TakoTako/issues/16) |
|
||||
| `#MEASURE` | `✅` | `✅` | |
|
||||
| `#SCROLL` | `✅` | `✅` | |
|
||||
| `#GOGOSTART`, `#GOGOEND` | `✅` | `✅` | |
|
||||
| `#BARLINEOFF`, `#BARLINEON` | `✅` | `✅` | |
|
||||
| `#DELAY` | `✅` | `❌` | See [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
||||
| `#BRANCHSTART`, `#BRANCHEND` | `✅` | `✅` | |
|
||||
| `#N`, `#E`, `#M` | `✅` | `✅` | |
|
||||
| `#SECTION` | `⚠️` | `❌` | See [#53](https://github.com/vivaria/tja2fumen/issues/53), [#27](https://github.com/Fluto/TakoTako/issues/27) |
|
||||
| `#LEVELHOLD` | `✅` | `❌` | |
|
||||
| `#BMSCROLL`, `#LYRIC`,<br>`#DIRECTION`, etc. | `⚪️` | `❌` | Other simulator-specific chart commands are currently ignored. |
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
|
@ -72,6 +72,7 @@ class TJAMeasureProcessed:
|
||||
delay: float = 0.0
|
||||
section: bool = False
|
||||
levelhold: bool = False
|
||||
senote: str = ''
|
||||
branch_type: str = ''
|
||||
branch_cond: Tuple[float, float] = (0.0, 0.0)
|
||||
notes: List[TJAData] = field(default_factory=list)
|
||||
@ -93,6 +94,7 @@ class FumenNote:
|
||||
hits: int = 0
|
||||
hits_padding: int = 0
|
||||
drumroll_bytes: bytes = b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
manually_set: bool = False
|
||||
|
||||
|
||||
@dataclass()
|
||||
|
@ -28,6 +28,15 @@ TJA_NOTE_TYPES = {
|
||||
'I': 'Drumroll', # green roll
|
||||
}
|
||||
|
||||
# Conversion for TJAPlayer3's #SENOTECHANGE command
|
||||
SENOTECHANGE_TYPES = {
|
||||
1: "Don", # ドン
|
||||
2: "Don2", # ド
|
||||
3: "Don3", # コ
|
||||
4: "Ka", # カッ
|
||||
5: "Ka2", # カ
|
||||
}
|
||||
|
||||
# Types of notes that can be found in fumen files
|
||||
FUMEN_NOTE_TYPES = {
|
||||
0x1: "Don", # ドン
|
||||
|
@ -9,7 +9,7 @@ from typing import List, Dict, Tuple, Union
|
||||
from tja2fumen.classes import (TJACourse, TJAMeasure, TJAMeasureProcessed,
|
||||
FumenCourse, FumenHeader, FumenMeasure,
|
||||
FumenNote)
|
||||
from tja2fumen.constants import BRANCH_NAMES
|
||||
from tja2fumen.constants import BRANCH_NAMES, SENOTECHANGE_TYPES
|
||||
|
||||
|
||||
def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
|
||||
@ -38,6 +38,7 @@ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
|
||||
current_scroll = 1.0
|
||||
current_gogo = False
|
||||
current_barline = True
|
||||
current_senote = ""
|
||||
current_dividend = 4
|
||||
current_divisor = 4
|
||||
for measure_tja in branch_measures_tja:
|
||||
@ -95,15 +96,18 @@ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
|
||||
# to BPM/SCROLL/GOGO, then the measure will actually be split
|
||||
# into two small submeasures. So, we need to start a new
|
||||
# measure in those cases.)
|
||||
elif data.name in ['bpm', 'scroll', 'gogo']:
|
||||
elif data.name in ['bpm', 'scroll', 'gogo', 'senote']:
|
||||
# Parse the values
|
||||
new_val: Union[bool, float]
|
||||
new_val: Union[bool, float, str]
|
||||
if data.name == 'bpm':
|
||||
new_val = current_bpm = float(data.value)
|
||||
elif data.name == 'scroll':
|
||||
new_val = current_scroll = float(data.value)
|
||||
elif data.name == 'gogo':
|
||||
new_val = current_gogo = bool(int(data.value))
|
||||
elif data.name == 'senote':
|
||||
new_val = current_senote \
|
||||
= SENOTECHANGE_TYPES[int(data.value)]
|
||||
# Check for mid-measure commands
|
||||
# - Case 1: Command happens at the start of a measure;
|
||||
# just change the value directly
|
||||
@ -123,8 +127,13 @@ def process_commands(tja_branches: Dict[str, List[TJAMeasure]], bpm: float) \
|
||||
barline=current_barline,
|
||||
time_sig=[current_dividend, current_divisor],
|
||||
subdivisions=len(measure_tja.notes),
|
||||
pos_start=data.pos
|
||||
pos_start=data.pos,
|
||||
senote=current_senote
|
||||
)
|
||||
# SENOTECHANGE commands don't carry over to next branch.
|
||||
# (But they CAN happen mid-measure, which is why we
|
||||
# process them here.)
|
||||
current_senote = ""
|
||||
|
||||
else:
|
||||
warnings.warn(f"Unexpected event type: {data.name}")
|
||||
@ -327,7 +336,15 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
# we can initialize a note and handle general note metadata.
|
||||
note = FumenNote()
|
||||
note.pos = note_pos
|
||||
note.note_type = note_tja.value
|
||||
# Account for a measure's #SENOTECHANGE command
|
||||
if measure_tja.senote:
|
||||
note.note_type = measure_tja.senote
|
||||
note.manually_set = True
|
||||
# SENOTECHANGE only applies to the note immediately after
|
||||
# So, we erase it once it's been applied.
|
||||
measure_tja.senote = ""
|
||||
else:
|
||||
note.note_type = note_tja.value
|
||||
note.score_init = tja.score_init
|
||||
note.score_diff = tja.score_diff
|
||||
|
||||
@ -508,12 +525,13 @@ def replace_alternate_don_kas(note_clusters: List[List[FumenNote]],
|
||||
positions within a cluster of notes.
|
||||
|
||||
NB: Modifies FumenNote objects in-place
|
||||
NB: FumenNote values are only updated if not manually set by #SENOTECHANGE
|
||||
"""
|
||||
big_notes = ['DON', 'DON2', 'KA', 'KA2']
|
||||
for cluster in note_clusters:
|
||||
# Replace all small notes with the basic do/ka notes ("Don2", "Ka2")
|
||||
for note in cluster:
|
||||
if note.note_type not in big_notes:
|
||||
if note.note_type not in big_notes and not note.manually_set:
|
||||
if note.note_type[-1].isdigit():
|
||||
note.note_type = note.note_type[:-1] + "2"
|
||||
else:
|
||||
@ -524,7 +542,8 @@ def replace_alternate_don_kas(note_clusters: List[List[FumenNote]],
|
||||
all_dons = all(note.note_type.startswith("Don") for note in cluster)
|
||||
for i, note in enumerate(cluster):
|
||||
if (all_dons and (len(cluster) % 2 == 1) and (i % 2 == 1)
|
||||
and note.note_type not in big_notes):
|
||||
and note.note_type not in big_notes
|
||||
and not note.manually_set):
|
||||
note.note_type = "Don3"
|
||||
|
||||
# Replace the last note in a cluster with the ending Don/Kat
|
||||
@ -538,7 +557,8 @@ def replace_alternate_don_kas(note_clusters: List[List[FumenNote]],
|
||||
pass
|
||||
else:
|
||||
# Replace last Don2/Ka2 with Don/Ka
|
||||
if cluster[-1].note_type not in big_notes:
|
||||
if (cluster[-1].note_type not in big_notes
|
||||
and not cluster[-1].manually_set):
|
||||
cluster[-1].note_type = cluster[-1].note_type[:-1]
|
||||
|
||||
|
||||
|
@ -262,7 +262,8 @@ def parse_tja_course_data(data: List[str]) \
|
||||
# 2. Parse measure commands that produce an "event"
|
||||
elif command in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF',
|
||||
'DELAY', 'SCROLL', 'BPMCHANGE', 'MEASURE',
|
||||
'LEVELHOLD', 'SECTION', 'BRANCHSTART']:
|
||||
'LEVELHOLD', 'SENOTECHANGE', 'SECTION',
|
||||
'BRANCHSTART']:
|
||||
# Get position of the event
|
||||
pos = 0
|
||||
for branch_name in (BRANCH_NAMES if current_branch == 'all'
|
||||
@ -290,6 +291,8 @@ def parse_tja_course_data(data: List[str]) \
|
||||
name = 'measure'
|
||||
elif command == 'LEVELHOLD':
|
||||
name = 'levelhold'
|
||||
elif command == "SENOTECHANGE":
|
||||
name = 'senote'
|
||||
elif command == 'SECTION':
|
||||
# If #SECTION occurs before a #BRANCHSTART, then ensure that
|
||||
# it's present on every branch. Otherwise, #SECTION will only
|
||||
|
21
testing/data/dummy_tjas/notes_senotechange.tja
Normal file
21
testing/data/dummy_tjas/notes_senotechange.tja
Normal file
@ -0,0 +1,21 @@
|
||||
// This song contains only basic notes.
|
||||
BPM:120
|
||||
OFFSET:-1.00
|
||||
|
||||
COURSE:Oni
|
||||
LEVEL:10
|
||||
BALLOON:8,8
|
||||
SCOREINIT:400
|
||||
SCOREDIFF:100
|
||||
|
||||
#START
|
||||
#SENOTECHANGE 3
|
||||
1
|
||||
#SENOTECHANGE 2
|
||||
111111,
|
||||
1020304,
|
||||
5000008,
|
||||
6000008,
|
||||
7000008,
|
||||
9000008,
|
||||
#END
|
@ -13,6 +13,7 @@ from conftest import convert
|
||||
['notes_double_kusudama', None],
|
||||
['notes_hands', None],
|
||||
['notes_sim_only', None],
|
||||
['notes_senotechange', None],
|
||||
['missing_score', None],
|
||||
['missing_balloon', "UserWarning"],
|
||||
['missing_course', "Invalid COURSE value:"],
|
||||
|
Loading…
Reference in New Issue
Block a user