Fix parsing of BALLOON:
so that values are correctly read for all 3 branches (#80)
I was making a bad assumption: 1. Songs with branches have the same number of balloons. 2. Each balloon has the same number of hits across branches. Because I was making these assumptions, I thought I could just repeat the `BALLOON:` field for each branch. **But this is wrong!!** Branches can have different numbers of balloons, and they can have different number of hits in their balloons. So, we need to **NOT** necessarily repeat `BALLOON:`, and instead use the written value of `BALLOON:` directly. This way we can get the different values for each branch. This fixes the parsing of Emma's Ura (and probably other songs). Also, this revealed a bug in my parsing of Roppon no Bara to Sai no Uta, so I needed to make sure we account for "duplicated" balloons too, and repeat the values _only when necessary_.
This commit is contained in:
parent
4e4a90a1f7
commit
9b6f05b420
@ -42,5 +42,7 @@ disable = """
|
|||||||
too-many-branches,
|
too-many-branches,
|
||||||
too-many-arguments,
|
too-many-arguments,
|
||||||
too-many-locals,
|
too-many-locals,
|
||||||
too-many-statements
|
too-many-statements,
|
||||||
|
too-many-positional-arguments,
|
||||||
|
fixme
|
||||||
"""
|
"""
|
||||||
|
@ -195,6 +195,9 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
|||||||
len(b) for b in tja_branches_processed.values()
|
len(b) for b in tja_branches_processed.values()
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Use a single copy of the course balloons (since we use .pop())
|
||||||
|
course_balloons = tja.balloon.copy()
|
||||||
|
|
||||||
# Iterate through the different branches in the TJA
|
# Iterate through the different branches in the TJA
|
||||||
total_notes = {'normal': 0, 'professional': 0, 'master': 0}
|
total_notes = {'normal': 0, 'professional': 0, 'master': 0}
|
||||||
for current_branch, branch_tja in tja_branches_processed.items():
|
for current_branch, branch_tja in tja_branches_processed.items():
|
||||||
@ -209,7 +212,6 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
|||||||
current_levelhold = False
|
current_levelhold = False
|
||||||
branch_types: List[str] = []
|
branch_types: List[str] = []
|
||||||
branch_conditions: List[Tuple[float, float]] = []
|
branch_conditions: List[Tuple[float, float]] = []
|
||||||
course_balloons = tja.balloon.copy()
|
|
||||||
|
|
||||||
# Iterate over pairs of TJA and Fumen measures
|
# Iterate over pairs of TJA and Fumen measures
|
||||||
for idx_m, (measure_tja, measure_fumen) in \
|
for idx_m, (measure_tja, measure_fumen) in \
|
||||||
|
@ -33,7 +33,9 @@ def parse_tja(fname_tja: str) -> TJASong:
|
|||||||
tja_lines = [line for line in tja_text.splitlines() if line.strip() != '']
|
tja_lines = [line for line in tja_text.splitlines() if line.strip() != '']
|
||||||
tja = split_tja_lines_into_courses(tja_lines)
|
tja = split_tja_lines_into_courses(tja_lines)
|
||||||
for course in tja.courses.values():
|
for course in tja.courses.values():
|
||||||
course.branches = parse_tja_course_data(course.data)
|
branches, balloon_data = parse_tja_course_data(course.data)
|
||||||
|
course.branches = branches
|
||||||
|
course.balloon = fix_balloon_field(course.balloon, balloon_data)
|
||||||
|
|
||||||
return tja
|
return tja
|
||||||
|
|
||||||
@ -181,7 +183,8 @@ def split_tja_lines_into_courses(lines: List[str]) -> TJASong:
|
|||||||
return parsed_tja
|
return parsed_tja
|
||||||
|
|
||||||
|
|
||||||
def parse_tja_course_data(data: List[str]) -> Dict[str, List[TJAMeasure]]:
|
def parse_tja_course_data(data: List[str]) \
|
||||||
|
-> Tuple[Dict[str, List[TJAMeasure]], Dict[str, List[str]]]:
|
||||||
"""
|
"""
|
||||||
Parse course data (notes, commands) into a nested song structure.
|
Parse course data (notes, commands) into a nested song structure.
|
||||||
|
|
||||||
@ -208,6 +211,8 @@ def parse_tja_course_data(data: List[str]) -> Dict[str, List[TJAMeasure]]:
|
|||||||
has_branches = bool([d for d in data if d.startswith('#BRANCH')])
|
has_branches = bool([d for d in data if d.startswith('#BRANCH')])
|
||||||
current_branch = 'all' if has_branches else 'normal'
|
current_branch = 'all' if has_branches else 'normal'
|
||||||
branch_condition = ''
|
branch_condition = ''
|
||||||
|
# keep track of balloons in order to fix the 'BALLOON' field value
|
||||||
|
balloons: Dict[str, List[str]] = {k: [] for k in BRANCH_NAMES}
|
||||||
|
|
||||||
# Process course lines
|
# Process course lines
|
||||||
idx_m = 0
|
idx_m = 0
|
||||||
@ -225,6 +230,7 @@ def parse_tja_course_data(data: List[str]) -> Dict[str, List[TJAMeasure]]:
|
|||||||
|
|
||||||
# 1. Parse measure notes
|
# 1. Parse measure notes
|
||||||
if note_data:
|
if note_data:
|
||||||
|
notes_to_write: str = ""
|
||||||
# If measure has ended, then add notes to the current measure,
|
# If measure has ended, then add notes to the current measure,
|
||||||
# then start a new measure by incrementing idx_m
|
# then start a new measure by incrementing idx_m
|
||||||
if note_data.endswith(','):
|
if note_data.endswith(','):
|
||||||
@ -232,14 +238,26 @@ def parse_tja_course_data(data: List[str]) -> Dict[str, List[TJAMeasure]]:
|
|||||||
else [current_branch]):
|
else [current_branch]):
|
||||||
check_branch_length(parsed_branches, branch_name,
|
check_branch_length(parsed_branches, branch_name,
|
||||||
expected_len=idx_m+1)
|
expected_len=idx_m+1)
|
||||||
parsed_branches[branch_name][idx_m].notes += note_data[:-1]
|
notes_to_write = note_data[:-1]
|
||||||
|
parsed_branches[branch_name][idx_m].notes += notes_to_write
|
||||||
parsed_branches[branch_name].append(TJAMeasure())
|
parsed_branches[branch_name].append(TJAMeasure())
|
||||||
idx_m += 1
|
idx_m += 1
|
||||||
# Otherwise, keep adding notes to the current measure ('idx_m')
|
# Otherwise, keep adding notes to the current measure ('idx_m')
|
||||||
else:
|
else:
|
||||||
for branch_name in (BRANCH_NAMES if current_branch == 'all'
|
for branch_name in (BRANCH_NAMES if current_branch == 'all'
|
||||||
else [current_branch]):
|
else [current_branch]):
|
||||||
parsed_branches[branch_name][idx_m].notes += note_data
|
notes_to_write = note_data
|
||||||
|
parsed_branches[branch_name][idx_m].notes += notes_to_write
|
||||||
|
|
||||||
|
# Keep track of balloon notes that were added
|
||||||
|
balloon_notes = [n for n in notes_to_write if n in ['7', '9']]
|
||||||
|
# mark balloon notes as duplicates if necessary. this will be used
|
||||||
|
# to fix the BALLOON: field to account for duplicated balloons.
|
||||||
|
balloon_notes = (['DUPE'] * len(balloon_notes)
|
||||||
|
if current_branch == 'all' else balloon_notes)
|
||||||
|
for branch_name in (BRANCH_NAMES if current_branch == 'all'
|
||||||
|
else [current_branch]):
|
||||||
|
balloons[branch_name].extend(balloon_notes)
|
||||||
|
|
||||||
# 2. Parse measure commands that produce an "event"
|
# 2. Parse measure commands that produce an "event"
|
||||||
elif command in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF',
|
elif command in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF',
|
||||||
@ -381,7 +399,7 @@ def parse_tja_course_data(data: List[str]) -> Dict[str, List[TJAMeasure]]:
|
|||||||
"have in each branch.)"
|
"have in each branch.)"
|
||||||
)
|
)
|
||||||
|
|
||||||
return parsed_branches
|
return parsed_branches, balloons
|
||||||
|
|
||||||
|
|
||||||
def check_branch_length(parsed_branches: Dict[str, List[TJAMeasure]],
|
def check_branch_length(parsed_branches: Dict[str, List[TJAMeasure]],
|
||||||
@ -425,6 +443,116 @@ def check_branch_length(parsed_branches: Dict[str, List[TJAMeasure]],
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_balloon_field(balloon_field: List[int],
|
||||||
|
balloon_data: Dict[str, List[str]]) -> List[int]:
|
||||||
|
"""
|
||||||
|
Fix the 'BALLOON:' metadata field for certain branching songs.
|
||||||
|
|
||||||
|
In Taiko, branching songs may have a different amount of balloons and/or
|
||||||
|
different balloon values on their normal/professional/master branches.
|
||||||
|
However, the TJA field "BALLOON:" is limited it how it can represent
|
||||||
|
balloon hits; it uses a single comma-delimited list of integers. E.g.:
|
||||||
|
|
||||||
|
BALLOON: 13,4,52,4,52,4,52
|
||||||
|
|
||||||
|
It is unclear which of these values belong to which branches.
|
||||||
|
|
||||||
|
This is especially unclear for songs that start out on the "normal" branch,
|
||||||
|
or songs that have branching conditions that force a specific branch. These
|
||||||
|
songs are often written as TJA with only a single branch written out, yet
|
||||||
|
for official fumens, this branch information actually has to be present on
|
||||||
|
*all three branches*. So, the 'BALLOON:' field will be missing values.
|
||||||
|
|
||||||
|
In the example above, the "13" balloon actually occurs on the normal branch
|
||||||
|
before the first branch condition. Meaning that the balloons are split up
|
||||||
|
like this:
|
||||||
|
|
||||||
|
BALLOON: (13,4,52)(4,52)(4,52)
|
||||||
|
|
||||||
|
However, due to fumen requirements, we want the balloons to actually be
|
||||||
|
like this:
|
||||||
|
|
||||||
|
BALLOON: (13,4,52)(13,4,52)(13,4,52)
|
||||||
|
|
||||||
|
So, the purpose of this function is to "fix" the balloon information so
|
||||||
|
that it can be used for fumen conversion without error.
|
||||||
|
|
||||||
|
NOTE: This fix probably only applies to a VERY small minority of songs.
|
||||||
|
One example (shown above) is the Ura chart for Roppon no Bara to Sai
|
||||||
|
no Uta. You can see in the wikiwiki that the opening 'Normal'
|
||||||
|
section has a balloon note prior to the branch condition. We need
|
||||||
|
to duplicate this value across all branches.
|
||||||
|
"""
|
||||||
|
# Return early if course doesn't have branches
|
||||||
|
if not all(balloon_data.values()):
|
||||||
|
return balloon_field
|
||||||
|
|
||||||
|
# Special case: Courses where the # of balloons is the same for all
|
||||||
|
# branches, and the TJA author only listed 1 set of balloons.
|
||||||
|
# Fix: Duplicate the balloons 3 times.
|
||||||
|
if all(len(balloons) == len(balloon_field)
|
||||||
|
for balloons in balloon_data.values()):
|
||||||
|
return balloon_field * 3
|
||||||
|
|
||||||
|
# Return early if there were no duplicated balloons in the course
|
||||||
|
if not any('DUPE' in balloons for balloons in balloon_data.values()):
|
||||||
|
return balloon_field
|
||||||
|
|
||||||
|
# If balloons were duplicated, then we expect the BALLOON: field to have
|
||||||
|
# fewer hits values than the number of balloons. If this *isn't* the case,
|
||||||
|
# then perhaps the TJA author duplicated the balloon hits themselves, and
|
||||||
|
# so we don't want to make any unnecessary edits. Thus, return early.
|
||||||
|
# FIXME: This assumption fails for double-kusudama notes, where we may
|
||||||
|
# see a "fake" balloon, thus inflating the total number of balloons.
|
||||||
|
# But, this is such a rare case (double-kusudama + duplicated
|
||||||
|
# balloons + 'BALLOON:' field with implicitly duplicated hits) that
|
||||||
|
# I'm alright handling it incorrectly. If a user files a bug
|
||||||
|
# report, then I'll fix it then.
|
||||||
|
total_num_balloons = sum(len(b) for b in balloon_data.values())
|
||||||
|
if not len(balloon_field) < total_num_balloons:
|
||||||
|
return balloon_field
|
||||||
|
|
||||||
|
# OK! So, by this point in the function, we're making these assumptions:
|
||||||
|
#
|
||||||
|
# 1. The TJA chart has branches.
|
||||||
|
# 2. The TJA author wrote part of the song for only a single branch
|
||||||
|
# (e.g. the Normal branch, before the first branch condition), and thus
|
||||||
|
# we needed to duplicate some of the note data to create a valid fumen.
|
||||||
|
# 3. The 'single branch' part of the TJA contained balloon/kusudama notes,
|
||||||
|
# and thus we needed to duplicate those notes.
|
||||||
|
# 4. The TJA author wrote the 'BALLOON:' field such that there was only 1
|
||||||
|
# balloon value for the duplicated balloon note.
|
||||||
|
#
|
||||||
|
# The goal now is to identify which balloons were duplicated, and make sure
|
||||||
|
# the "hits" value is present across all branches.
|
||||||
|
duplicated_balloons = []
|
||||||
|
balloon_field_fixed = []
|
||||||
|
|
||||||
|
# Handle the normal branch first
|
||||||
|
# If balloons are duplicated, then it's probably going to be from 'normal'
|
||||||
|
# FIXME: If the balloons are duplicated from the master/professional branch
|
||||||
|
# (e.g. due to a forced branch change from a branch condition), then
|
||||||
|
# this logic will read the balloon values incorrectly.
|
||||||
|
# But, this is such a rare case that I'm alright handling it
|
||||||
|
# incorrectly. If a user files a bug report, then I'll fix it then.
|
||||||
|
for balloon_note in balloon_data['normal']:
|
||||||
|
balloon_hits = balloon_field.pop(0)
|
||||||
|
if balloon_note == 'DUPE':
|
||||||
|
duplicated_balloons.append(balloon_hits)
|
||||||
|
balloon_field_fixed.append(balloon_hits)
|
||||||
|
|
||||||
|
# Repeat any duplicated balloon notes for the professional/master branches
|
||||||
|
for branch_name in ['professional', 'master']:
|
||||||
|
dupes_to_copy = duplicated_balloons.copy()
|
||||||
|
for balloon_note in balloon_data[branch_name]:
|
||||||
|
if balloon_note == 'DUPE':
|
||||||
|
balloon_field_fixed.append(dupes_to_copy.pop(0))
|
||||||
|
else:
|
||||||
|
balloon_field_fixed.append(balloon_field.pop(0))
|
||||||
|
|
||||||
|
return balloon_field_fixed
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Fumen-parsing functions #
|
# Fumen-parsing functions #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
1526
testing/data/emma.tja
Normal file
1526
testing/data/emma.tja
Normal file
File diff suppressed because it is too large
Load Diff
BIN
testing/data/emma.zip
Normal file
BIN
testing/data/emma.zip
Normal file
Binary file not shown.
@ -519,7 +519,7 @@ SCOREDIFF:67
|
|||||||
|
|
||||||
COURSE:Oni
|
COURSE:Oni
|
||||||
LEVEL:6
|
LEVEL:6
|
||||||
BALLOON:7,9,5,3,3,15,3,3,4,23
|
BALLOON:7,13,7,4,4,21,4,4,2,35
|
||||||
SCOREINIT:520
|
SCOREINIT:520
|
||||||
SCOREDIFF:122
|
SCOREDIFF:122
|
||||||
|
|
||||||
@ -689,7 +689,7 @@ SCOREDIFF:122
|
|||||||
|
|
||||||
COURSE:Hard
|
COURSE:Hard
|
||||||
LEVEL:3
|
LEVEL:3
|
||||||
BALLOON:30,14,6,16,18,18,18,18
|
BALLOON:30,14,9,16,18,18,18,18
|
||||||
SCOREINIT:570
|
SCOREINIT:570
|
||||||
SCOREDIFF:140
|
SCOREDIFF:140
|
||||||
|
|
||||||
|
@ -463,7 +463,7 @@ SCOREDIFF:117
|
|||||||
|
|
||||||
COURSE:Normal
|
COURSE:Normal
|
||||||
LEVEL:5
|
LEVEL:5
|
||||||
BALLOON:9,9
|
BALLOON:9,10
|
||||||
SCOREINIT:570
|
SCOREINIT:570
|
||||||
SCOREDIFF:157
|
SCOREDIFF:157
|
||||||
|
|
||||||
@ -621,7 +621,7 @@ SCOREDIFF:157
|
|||||||
|
|
||||||
COURSE:Easy
|
COURSE:Easy
|
||||||
LEVEL:3
|
LEVEL:3
|
||||||
BALLOON:5,7,6
|
BALLOON:5,7,7
|
||||||
SCOREINIT:510
|
SCOREINIT:510
|
||||||
SCOREDIFF:155
|
SCOREDIFF:155
|
||||||
|
|
||||||
|
@ -777,7 +777,7 @@ SCOREDIFF:215
|
|||||||
|
|
||||||
COURSE:Easy
|
COURSE:Easy
|
||||||
LEVEL:4
|
LEVEL:4
|
||||||
BALLOON:20
|
BALLOON:18
|
||||||
SCOREINIT:710
|
SCOREINIT:710
|
||||||
SCOREDIFF:287
|
SCOREDIFF:287
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from tja2fumen.parsers import parse_fumen
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('id_song', [
|
@pytest.mark.parametrize('id_song', [
|
||||||
|
pytest.param('emma'),
|
||||||
pytest.param('butou5'),
|
pytest.param('butou5'),
|
||||||
pytest.param('shoto9',
|
pytest.param('shoto9',
|
||||||
marks=pytest.mark.skip("TJA measures do not match fumen.")),
|
marks=pytest.mark.skip("TJA measures do not match fumen.")),
|
||||||
@ -165,7 +166,10 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
|||||||
i_branch, i_note, abv=25.0)
|
i_branch, i_note, abv=25.0)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
pass
|
pass
|
||||||
if ca_note.note_type not in ["Balloon", "Kusudama"]:
|
if ca_note.note_type in ["Balloon", "Kusudama"]:
|
||||||
|
check(co_note, ca_note, 'hits', i_measure,
|
||||||
|
i_branch, i_note)
|
||||||
|
else:
|
||||||
check(co_note, ca_note, 'score_init', i_measure,
|
check(co_note, ca_note, 'score_init', i_measure,
|
||||||
i_branch, i_note)
|
i_branch, i_note)
|
||||||
check(co_note, ca_note, 'score_diff', i_measure,
|
check(co_note, ca_note, 'score_diff', i_measure,
|
||||||
|
Loading…
Reference in New Issue
Block a user