diff --git a/README.md b/README.md
index 0008ca0..8b017f3 100644
--- a/README.md
+++ b/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,`
`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`,
`#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,`
`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`,
`#DIRECTION`, etc. | `⚪️` | `❌` | Other simulator-specific chart commands are currently ignored. |
## Reporting bugs
diff --git a/src/tja2fumen/classes.py b/src/tja2fumen/classes.py
index acd1247..3086b9c 100644
--- a/src/tja2fumen/classes.py
+++ b/src/tja2fumen/classes.py
@@ -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()
diff --git a/src/tja2fumen/constants.py b/src/tja2fumen/constants.py
index 0876266..776528f 100644
--- a/src/tja2fumen/constants.py
+++ b/src/tja2fumen/constants.py
@@ -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", # ドン
diff --git a/src/tja2fumen/converters.py b/src/tja2fumen/converters.py
index 5d04300..6d21d3e 100644
--- a/src/tja2fumen/converters.py
+++ b/src/tja2fumen/converters.py
@@ -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]
diff --git a/src/tja2fumen/parsers.py b/src/tja2fumen/parsers.py
index a2b9e5f..1eef511 100644
--- a/src/tja2fumen/parsers.py
+++ b/src/tja2fumen/parsers.py
@@ -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
diff --git a/testing/data/dummy_tjas/notes_senotechange.tja b/testing/data/dummy_tjas/notes_senotechange.tja
new file mode 100644
index 0000000..3f60ae2
--- /dev/null
+++ b/testing/data/dummy_tjas/notes_senotechange.tja
@@ -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
diff --git a/testing/test_command_support.py b/testing/test_command_support.py
index d02598a..854cf72 100644
--- a/testing/test_command_support.py
+++ b/testing/test_command_support.py
@@ -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:"],