From 48718b2303c11ed6b39cbb9412f06f3d947b7c73 Mon Sep 17 00:00:00 2001 From: Viv Date: Wed, 19 Jul 2023 21:50:09 -0400 Subject: [PATCH] Fix variable case (`camelCase` -> `snake_case` + `CONST_CASE`) (#47) Wouldn't have been possible without https://gist.github.com/Mizzlr/eec29687704aa81bf61dfccda36ddb8c. Fixes #8 . --- src/tja2fumen/__init__.py | 36 ++--- src/tja2fumen/constants.py | 8 +- src/tja2fumen/converters.py | 272 ++++++++++++++++++------------------ src/tja2fumen/parsers.py | 244 ++++++++++++++++---------------- src/tja2fumen/types.py | 52 +++---- src/tja2fumen/utils.py | 6 +- src/tja2fumen/writers.py | 38 ++--- testing/test_conversion.py | 24 ++-- 8 files changed, 339 insertions(+), 341 deletions(-) diff --git a/src/tja2fumen/__init__.py b/src/tja2fumen/__init__.py index 69dc488..8927676 100644 --- a/src/tja2fumen/__init__.py +++ b/src/tja2fumen/__init__.py @@ -2,9 +2,9 @@ import argparse import os import sys -from tja2fumen.parsers import parseTJA -from tja2fumen.writers import writeFumen -from tja2fumen.converters import convertTJAToFumen +from tja2fumen.parsers import parse_tja +from tja2fumen.writers import write_fumen +from tja2fumen.converters import convert_tja_to_fumen from tja2fumen.constants import COURSE_IDS @@ -20,30 +20,30 @@ def main(argv=None): help="Path to a Taiko no Tatsujin TJA file.", ) args = parser.parse_args(argv) - fnameTJA = getattr(args, "file.tja") - baseName = os.path.splitext(fnameTJA)[0] + fname_tja = getattr(args, "file.tja") + base_name = os.path.splitext(fname_tja)[0] # Parse lines in TJA file - parsedTJA = parseTJA(fnameTJA) + parsed_tja = parse_tja(fname_tja) # Convert parsed TJA courses to Fumen data, and write each course to `.bin` files - for course in parsedTJA.courses.items(): - convert_and_write(course, baseName, singleCourse=(len(parsedTJA.courses) == 1)) + for course in parsed_tja.courses.items(): + convert_and_write(course, base_name, single_course=(len(parsed_tja.courses) == 1)) -def convert_and_write(parsedCourse, baseName, singleCourse=False): - courseName, tjaData = parsedCourse - fumenData = convertTJAToFumen(tjaData) +def convert_and_write(parsed_course, base_name, single_course=False): + course_name, tja_data = parsed_course + fumen_data = convert_tja_to_fumen(tja_data) # Add course ID (e.g. '_x', '_x_1', '_x_2') to the output file's base name - outputName = baseName - if singleCourse: + output_name = base_name + if single_course: pass # Replicate tja2bin.exe behavior by excluding course ID if there's only one course else: - splitName = courseName.split("P") # e.g. 'OniP2' -> ['Oni', '2'], 'Oni' -> ['Oni'] - outputName += f"_{COURSE_IDS[splitName[0]]}" - if len(splitName) == 2: - outputName += f"_{splitName[1]}" # Add "_1" or "_2" if P1/P2 chart - writeFumen(f"{outputName}.bin", fumenData) + split_name = course_name.split("P") # e.g. 'OniP2' -> ['Oni', '2'], 'Oni' -> ['Oni'] + output_name += f"_{COURSE_IDS[split_name[0]]}" + if len(split_name) == 2: + output_name += f"_{split_name[1]}" # Add "_1" or "_2" if P1/P2 chart + write_fumen(f"{output_name}.bin", fumen_data) # NB: This entry point is necessary for the Pyinstaller executable diff --git a/src/tja2fumen/constants.py b/src/tja2fumen/constants.py index 708303a..221efa6 100644 --- a/src/tja2fumen/constants.py +++ b/src/tja2fumen/constants.py @@ -1,4 +1,3 @@ -# Note types for TJA files TJA_NOTE_TYPES = { '1': 'Don', '2': 'Ka', @@ -13,8 +12,7 @@ TJA_NOTE_TYPES = { 'B': 'KA', # hands } -# Note types for fumen files -noteTypes = { +FUMEN_NOTE_TYPES = { 0x1: "Don", # ドン 0x2: "Don2", # ド 0x3: "Don3", # コ @@ -43,9 +41,9 @@ noteTypes = { 0x22: "Unknown13", # ? (Present in some Wii1 songs) 0x62: "Drumroll2" # ? } -typeNotes = {v: k for k, v in noteTypes.items()} +FUMEN_TYPE_NOTES = {v: k for k, v in FUMEN_NOTE_TYPES.items()} -branchNames = ("normal", "advanced", "master") +BRANCH_NAMES = ("normal", "advanced", "master") TJA_COURSE_NAMES = [] for difficulty in ['Ura', 'Oni', 'Hard', 'Normal', 'Easy']: diff --git a/src/tja2fumen/converters.py b/src/tja2fumen/converters.py index e6bf29e..d9320c2 100644 --- a/src/tja2fumen/converters.py +++ b/src/tja2fumen/converters.py @@ -3,7 +3,7 @@ import re from tja2fumen.types import TJAMeasureProcessed, FumenCourse, FumenNote -def processTJACommands(tja): +def process_tja_commands(tja): """ Merge TJA 'data' and 'event' fields into a single measure property, and split measures into sub-measures whenever a mid-measure BPM/SCROLL/GOGO change occurs. @@ -21,46 +21,46 @@ def processTJACommands(tja): In the future, this logic should probably be moved into the TJA parser itself. """ - tjaBranchesProcessed = {branchName: [] for branchName in tja.branches.keys()} - for branchName, branchMeasuresTJA in tja.branches.items(): - currentBPM = tja.BPM - currentScroll = 1.0 - currentGogo = False - currentBarline = True - currentDividend = 4 - currentDivisor = 4 - for measureTJA in branchMeasuresTJA: + tja_branches_processed = {branch_name: [] for branch_name in tja.branches.keys()} + for branch_name, branch_measures_tja in tja.branches.items(): + current_bpm = tja.BPM + current_scroll = 1.0 + current_gogo = False + current_barline = True + current_dividend = 4 + current_divisor = 4 + for measure_tja in branch_measures_tja: # Split measure into submeasure - measureTJAProcessed = TJAMeasureProcessed( - bpm=currentBPM, - scroll=currentScroll, - gogo=currentGogo, - barline=currentBarline, - time_sig=[currentDividend, currentDivisor], - subdivisions=len(measureTJA.notes), + measure_tja_processed = TJAMeasureProcessed( + bpm=current_bpm, + scroll=current_scroll, + gogo=current_gogo, + barline=current_barline, + time_sig=[current_dividend, current_divisor], + subdivisions=len(measure_tja.notes), ) - for data in measureTJA.combined: + for data in measure_tja.combined: # Handle note data if data.name == 'note': - measureTJAProcessed.data.append(data) + measure_tja_processed.data.append(data) # Handle commands that can only be placed between measures (i.e. no mid-measure variations) elif data.name == 'delay': - measureTJAProcessed.delay = data.value * 1000 # ms -> s - elif data.name == 'branchStart': - measureTJAProcessed.branchStart = data.value + measure_tja_processed.delay = data.value * 1000 # ms -> s + elif data.name == 'branch_start': + measure_tja_processed.branch_start = data.value elif data.name == 'section': - measureTJAProcessed.section = data.value + measure_tja_processed.section = data.value elif data.name == 'barline': - currentBarline = bool(int(data.value)) - measureTJAProcessed.barline = currentBarline + current_barline = bool(int(data.value)) + measure_tja_processed.barline = current_barline elif data.name == 'measure': - matchMeasure = re.match(r"(\d+)/(\d+)", data.value) - if not matchMeasure: + match_measure = re.match(r"(\d+)/(\d+)", data.value) + if not match_measure: continue - currentDividend = int(matchMeasure.group(1)) - currentDivisor = int(matchMeasure.group(2)) - measureTJAProcessed.time_sig = [currentDividend, currentDivisor] + current_dividend = int(match_measure.group(1)) + current_divisor = int(match_measure.group(2)) + measure_tja_processed.time_sig = [current_dividend, current_divisor] # Handle commands that can be placed in the middle of a measure. # NB: For fumen files, if there is a mid-measure change to BPM/SCROLL/GOGO, then the measure will @@ -68,118 +68,118 @@ def processTJACommands(tja): elif data.name in ['bpm', 'scroll', 'gogo']: # Parse the values if data.name == 'bpm': - new_val = currentBPM = float(data.value) + new_val = current_bpm = float(data.value) elif data.name == 'scroll': - new_val = currentScroll = data.value + new_val = current_scroll = data.value elif data.name == 'gogo': - new_val = currentGogo = bool(int(data.value)) + new_val = current_gogo = bool(int(data.value)) # Check for mid-measure commands # - Case 1: Command happens at the start of a measure; just change the value directly if data.pos == 0: - measureTJAProcessed.__setattr__(data.name, new_val) + measure_tja_processed.__setattr__(data.name, new_val) # - Case 2: Command occurs mid-measure, so start a new sub-measure else: - measureTJAProcessed.pos_end = data.pos - tjaBranchesProcessed[branchName].append(measureTJAProcessed) - measureTJAProcessed = TJAMeasureProcessed( - bpm=currentBPM, - scroll=currentScroll, - gogo=currentGogo, - barline=currentBarline, - time_sig=[currentDividend, currentDivisor], - subdivisions=len(measureTJA.notes), + measure_tja_processed.pos_end = data.pos + tja_branches_processed[branch_name].append(measure_tja_processed) + measure_tja_processed = TJAMeasureProcessed( + bpm=current_bpm, + scroll=current_scroll, + gogo=current_gogo, + barline=current_barline, + time_sig=[current_dividend, current_divisor], + subdivisions=len(measure_tja.notes), pos_start=data.pos ) else: print(f"Unexpected event type: {data.name}") - measureTJAProcessed.pos_end = len(measureTJA.notes) - tjaBranchesProcessed[branchName].append(measureTJAProcessed) + measure_tja_processed.pos_end = len(measure_tja.notes) + tja_branches_processed[branch_name].append(measure_tja_processed) - hasBranches = all(len(b) for b in tjaBranchesProcessed.values()) - if hasBranches: + has_branches = all(len(b) for b in tja_branches_processed.values()) + if has_branches: branch_lens = [len(b) for b in tja.branches.values()] if not branch_lens.count(branch_lens[0]) == len(branch_lens): raise ValueError("Branches do not have the same number of measures.") else: - branchCorrected_lens = [len(b) for b in tjaBranchesProcessed.values()] - if not branchCorrected_lens.count(branchCorrected_lens[0]) == len(branchCorrected_lens): + branch_corrected_lens = [len(b) for b in tja_branches_processed.values()] + if not branch_corrected_lens.count(branch_corrected_lens[0]) == len(branch_corrected_lens): raise ValueError("Branches do not have matching GOGO/SCROLL/BPM commands.") - return tjaBranchesProcessed + return tja_branches_processed -def convertTJAToFumen(tja): +def convert_tja_to_fumen(tja): # Preprocess commands - processedTJABranches = processTJACommands(tja) + processed_tja_branches = process_tja_commands(tja) # Pre-allocate the measures for the converted TJA fumen = FumenCourse( - measures=len(processedTJABranches['normal']), - scoreInit=tja.scoreInit, - scoreDiff=tja.scoreDiff, + measures=len(processed_tja_branches['normal']), + score_init=tja.score_init, + score_diff=tja.score_diff, ) # Iterate through the different branches in the TJA total_notes = {'normal': 0, 'advanced': 0, 'master': 0} - for currentBranch, branchMeasuresTJAProcessed in processedTJABranches.items(): - if not len(branchMeasuresTJAProcessed): + for current_branch, branch_measures_tja_processed in processed_tja_branches.items(): + if not len(branch_measures_tja_processed): continue total_notes_branch = 0 note_counter_branch = 0 - currentDrumroll = None - branchConditions = [] - courseBalloons = tja.balloon.copy() + current_drumroll = None + branch_conditions = [] + course_balloons = tja.balloon.copy() # Iterate through the measures within the branch - for idx_m, measureTJAProcessed in enumerate(branchMeasuresTJAProcessed): + for idx_m, measure_tja_processed in enumerate(branch_measures_tja_processed): # Fetch a pair of measures - measureFumenPrev = fumen.measures[idx_m-1] if idx_m != 0 else None - measureFumen = fumen.measures[idx_m] + measure_fumen_prev = fumen.measures[idx_m-1] if idx_m != 0 else None + measure_fumen = fumen.measures[idx_m] # Copy over basic measure properties from the TJA (that don't depend on notes or commands) - measureFumen.branches[currentBranch].speed = measureTJAProcessed.scroll - measureFumen.gogo = measureTJAProcessed.gogo - measureFumen.bpm = measureTJAProcessed.bpm + measure_fumen.branches[current_branch].speed = measure_tja_processed.scroll + measure_fumen.gogo = measure_tja_processed.gogo + measure_fumen.bpm = measure_tja_processed.bpm # Compute the duration of the measure # First, we compute the duration for a full 4/4 measure - measureDurationFullMeasure = 4 * 60_000 / measureTJAProcessed.bpm + measure_duration_full_measure = 4 * 60_000 / measure_tja_processed.bpm # Next, we adjust this duration based on both: # 1. The *actual* measure size (e.g. #MEASURE 1/8, #MEASURE 5/4, etc.) - measureSize = measureTJAProcessed.time_sig[0] / measureTJAProcessed.time_sig[1] + measure_size = measure_tja_processed.time_sig[0] / measure_tja_processed.time_sig[1] # 2. Whether this is a "submeasure" (i.e. it contains mid-measure commands, which split up the measure) - # - If this is a submeasure, then `measureLength` will be less than the total number of subdivisions. - measureLength = measureTJAProcessed.pos_end - measureTJAProcessed.pos_start - # - In other words, `measureRatio` will be less than 1.0: - measureRatio = (1.0 if measureTJAProcessed.subdivisions == 0.0 # Avoid division by 0 for empty measures - else (measureLength / measureTJAProcessed.subdivisions)) + # - If this is a submeasure, then `measure_length` will be less than the total number of subdivisions. + measure_length = measure_tja_processed.pos_end - measure_tja_processed.pos_start + # - In other words, `measure_ratio` will be less than 1.0: + measure_ratio = (1.0 if measure_tja_processed.subdivisions == 0.0 # Avoid division by 0 for empty measures + else (measure_length / measure_tja_processed.subdivisions)) # Apply the 2 adjustments to the measure duration - measureFumen.duration = measureDuration = measureDurationFullMeasure * measureSize * measureRatio + measure_fumen.duration = measure_duration = measure_duration_full_measure * measure_size * measure_ratio # Compute the millisecond offsets for the start and end of each measure # - Start: When the notes first appear on screen (to the right) # - End: When the notes arrive at the judgment line, and the note gets hit. if idx_m == 0: - measureFumen.fumenOffsetStart = (tja.offset * 1000 * -1) - measureDurationFullMeasure + measure_fumen.fumen_offset_start = (tja.offset * 1000 * -1) - measure_duration_full_measure else: # First, start the measure using the end timing of the previous measure (plus any #DELAY commands) - measureFumen.fumenOffsetStart = measureFumenPrev.fumenOffsetEnd + measureTJAProcessed.delay + measure_fumen.fumen_offset_start = measure_fumen_prev.fumen_offset_end + measure_tja_processed.delay # Next, adjust the start timing to account for #BPMCHANGE commands (!!! Discovered by tana :3 !!!) # To understand what's going on here, imagine the following simple example: # * You have a very slow-moving note (i.e. low BPM), like the big DON in Donkama 2000. # * All the other notes move fast (i.e. high BPM), moving past the big slow note. # * To get this overlapping to work, you need the big slow note to START EARLY, but also END LATE: # - An early start means you need to subtract a LOT of time from the starting fumenOffset. - # - Thankfully, the low BPM of the slow note will create a HUGE `measureOffsetAdjustment`, + # - Thankfully, the low BPM of the slow note will create a HUGE `measure_offset_adjustment`, # since we are dividing by the BPMs, and dividing by a small number will result in a big number. - measureOffsetAdjustment = (4 * 60_000 / measureFumen.bpm) - (4 * 60_000 / measureFumenPrev.bpm) - # - When we subtract this adjustment from the fumenOffsetStart, we get the "START EARLY" part: - measureFumen.fumenOffsetStart -= measureOffsetAdjustment + measure_offset_adjustment = (4 * 60_000 / measure_fumen.bpm) - (4 * 60_000 / measure_fumen_prev.bpm) + # - When we subtract this adjustment from the fumen_offset_start, we get the "START EARLY" part: + measure_fumen.fumen_offset_start -= measure_offset_adjustment # - The low BPM of the slow note will also create a HUGE measure duration. # - When we add this long duration to the EARLY START, we end up with the "END LATE" part: - measureFumen.fumenOffsetEnd = measureFumen.fumenOffsetStart + measureFumen.duration + measure_fumen.fumen_offset_end = measure_fumen.fumen_offset_start + measure_fumen.duration # Best guess at what 'barline' status means for each measure: # - 'True' means the measure lands on a barline (i.e. most measures), and thus barline should be shown @@ -187,22 +187,22 @@ def convertTJAToFumen(tja): # For example: # 1. Measures where #BARLINEOFF has been set # 2. Sub-measures that don't fall on the barline - if measureTJAProcessed.barline is False or (measureRatio != 1.0 and measureTJAProcessed.pos_start != 0): - measureFumen.barline = False + if measure_tja_processed.barline is False or (measure_ratio != 1.0 and measure_tja_processed.pos_start != 0): + measure_fumen.barline = False - # If a #SECTION command occurs in isolation, and it has a valid condition, then treat it like a branchStart - if (measureTJAProcessed.section is not None and measureTJAProcessed.section != 'not_available' - and not measureTJAProcessed.branchStart): - branchCondition = measureTJAProcessed.section + # If a #SECTION command occurs in isolation, and it has a valid condition, then treat it like a branch_start + if (measure_tja_processed.section is not None and measure_tja_processed.section != 'not_available' + and not measure_tja_processed.branch_start): + branch_condition = measure_tja_processed.section else: - branchCondition = measureTJAProcessed.branchStart + branch_condition = measure_tja_processed.branch_start # Check to see if the measure contains a branching condition - if branchCondition: + if branch_condition: # Determine which values to assign based on the type of branching condition - if branchCondition[0] == 'p': + if branch_condition[0] == 'p': vals = [] - for percent in branchCondition[1:]: + for percent in branch_condition[1:]: # Ensure percentage is between 0% and 100% if 0 <= percent <= 1: val = total_notes_branch * percent * 20 @@ -216,12 +216,12 @@ def convertTJAToFumen(tja): # If it is below 0%, it is a guaranteed "level up". Fumens use 0 for this. else: vals.append(0) - if currentBranch == 'normal': - measureFumen.branchInfo[0:2] = vals - elif currentBranch == 'advanced': - measureFumen.branchInfo[2:4] = vals - elif currentBranch == 'master': - measureFumen.branchInfo[4:6] = vals + if current_branch == 'normal': + measure_fumen.branch_info[0:2] = vals + elif current_branch == 'advanced': + measure_fumen.branch_info[2:4] = vals + elif current_branch == 'master': + measure_fumen.branch_info[4:6] = vals # If it's a drumroll, then the values to use depends on whether there is a #SECTION in the same measure # - If there is a #SECTION, then accuracy is reset, so repeat the same condition for all 3 branches @@ -234,23 +234,23 @@ def convertTJAToFumen(tja): # * If the player made it to Master, then only use the "master condition" value (2), otherwise # they fall back to Normal. # - The "no-#SECTION" behavior can be seen in songs like "Shoutoku Taiko no 「Hi Izuru Made Asuka」" - elif branchCondition[0] == 'r': - if currentBranch == 'normal': - measureFumen.branchInfo[0:2] = (branchCondition[1:] if measureTJAProcessed.section or - not measureTJAProcessed.section and not branchConditions + elif branch_condition[0] == 'r': + if current_branch == 'normal': + measure_fumen.branch_info[0:2] = (branch_condition[1:] if measure_tja_processed.section or + not measure_tja_processed.section and not branch_conditions else [999, 999]) - elif currentBranch == 'advanced': - measureFumen.branchInfo[2:4] = branchCondition[1:] - elif currentBranch == 'master': - measureFumen.branchInfo[4:6] = (branchCondition[1:] if measureTJAProcessed.section or - not measureTJAProcessed.section and not branchConditions - else [branchCondition[2]] * 2) + elif current_branch == 'advanced': + measure_fumen.branch_info[2:4] = branch_condition[1:] + elif current_branch == 'master': + measure_fumen.branch_info[4:6] = (branch_condition[1:] if measure_tja_processed.section or + not measure_tja_processed.section and not branch_conditions + else [branch_condition[2]] * 2) # Reset the note counter corresponding to this branch (i.e. reset the accuracy) total_notes_branch = 0 # Cache branch conditions, but skip conditions where the only purpose is to level down to 'normal' - if measureFumen.branchInfo != [999, 999, 999, 999, 999, 999]: - branchConditions.append(branchCondition) + if measure_fumen.branch_info != [999, 999, 999, 999, 999, 999]: + branch_conditions.append(branch_condition) # NB: We update the branch condition note counter *after* we check the current measure's branch condition. # This is because the TJA spec says: @@ -262,72 +262,72 @@ def convertTJAToFumen(tja): # Create notes based on TJA measure data note_counter_branch = 0 note_counter = 0 - for idx_d, data in enumerate(measureTJAProcessed.data): + for idx_d, data in enumerate(measure_tja_processed.data): if data.name == 'note': note = FumenNote() # Note positions must be calculated using the base measure duration (that uses a single BPM value) # (In other words, note positions do not take into account any mid-measure BPM change adjustments.) - note.pos = measureDuration * (data.pos - measureTJAProcessed.pos_start) / measureLength + note.pos = measure_duration * (data.pos - measure_tja_processed.pos_start) / measure_length # Handle the note that represents the end of a drumroll/balloon if data.value == "EndDRB": # If a drumroll spans a single measure, then add the difference between start/end position - if not currentDrumroll.multimeasure: - currentDrumroll.duration += (note.pos - currentDrumroll.pos) + if not current_drumroll.multimeasure: + current_drumroll.duration += (note.pos - current_drumroll.pos) # Otherwise, if a drumroll spans multiple measures, then we want to add the duration between # the start of the measure (i.e. pos=0.0) and the drumroll's end position. else: - currentDrumroll.duration += (note.pos - 0.0) + current_drumroll.duration += (note.pos - 0.0) # 1182, 1385, 1588, 2469, 1568, 752, 1568 - currentDrumroll.duration = float(int(currentDrumroll.duration)) - currentDrumroll = None + current_drumroll.duration = float(int(current_drumroll.duration)) + current_drumroll = None continue # The TJA spec technically allows you to place double-Kusudama notes: # "Use another 9 to specify when to lower the points for clearing." # But this is unsupported in fumens, so just skip the second Kusudama note. - if data.value == "Kusudama" and currentDrumroll: + if data.value == "Kusudama" and current_drumroll: continue # Handle the remaining non-EndDRB, non-double Kusudama notes note.type = data.value - note.scoreInit = tja.scoreInit - note.scoreDiff = tja.scoreDiff + note.score_init = tja.score_init + note.score_diff = tja.score_diff # Handle drumroll/balloon-specific metadata if note.type in ["Balloon", "Kusudama"]: - note.hits = courseBalloons.pop(0) - currentDrumroll = note - total_notes[currentBranch] -= 1 + note.hits = course_balloons.pop(0) + current_drumroll = note + total_notes[current_branch] -= 1 if note.type in ["Drumroll", "DRUMROLL"]: - currentDrumroll = note - total_notes[currentBranch] -= 1 + current_drumroll = note + total_notes[current_branch] -= 1 # Count dons, kas, and balloons for the purpose of tracking branching accuracy if note.type.lower() in ['don', 'ka']: note_counter_branch += 1 elif note.type.lower() in ['balloon', 'kusudama']: note_counter_branch += 1.5 - measureFumen.branches[currentBranch].notes.append(note) + measure_fumen.branches[current_branch].notes.append(note) note_counter += 1 # If drumroll hasn't ended by the end of this measure, increase duration by measure timing - if currentDrumroll: - if currentDrumroll.duration == 0.0: - currentDrumroll.duration += (measureDuration - currentDrumroll.pos) - currentDrumroll.multimeasure = True + if current_drumroll: + if current_drumroll.duration == 0.0: + current_drumroll.duration += (measure_duration - current_drumroll.pos) + current_drumroll.multimeasure = True else: - currentDrumroll.duration += measureDuration + current_drumroll.duration += measure_duration - measureFumen.branches[currentBranch].length = note_counter - total_notes[currentBranch] += note_counter + measure_fumen.branches[current_branch].length = note_counter + total_notes[current_branch] += note_counter # Set song-specific metadata fumen.header.b512_b515_number_of_measures = len(fumen.measures) - fumen.header.b432_b435_has_branches = int(all([len(b) for b in processedTJABranches.values()])) + fumen.header.b432_b435_has_branches = int(all([len(b) for b in processed_tja_branches.values()])) fumen.header.set_hp_bytes(total_notes, tja.course, tja.level) # If song has only drumroll branching conditions, then only drumrolls should contribute to branching - if all([condition[0] == 'r' for condition in branchConditions]): + if all([condition[0] == 'r' for condition in branch_conditions]): fumen.header.b468_b471_branch_points_good = 0 - fumen.header.b484_b487_branch_points_good_BIG = 0 + fumen.header.b484_b487_branch_points_good_big = 0 fumen.header.b472_b475_branch_points_ok = 0 - fumen.header.b488_b491_branch_points_ok_BIG = 0 + fumen.header.b488_b491_branch_points_ok_big = 0 fumen.header.b496_b499_branch_points_balloon = 0 fumen.header.b500_b503_branch_points_kusudama = 0 diff --git a/src/tja2fumen/parsers.py b/src/tja2fumen/parsers.py index 756ccd8..3f41e2a 100644 --- a/src/tja2fumen/parsers.py +++ b/src/tja2fumen/parsers.py @@ -2,8 +2,8 @@ import os import re from copy import deepcopy -from tja2fumen.utils import readStruct, shortHex -from tja2fumen.constants import NORMALIZE_COURSE, TJA_NOTE_TYPES, branchNames, noteTypes +from tja2fumen.utils import read_struct, short_hex +from tja2fumen.constants import NORMALIZE_COURSE, TJA_NOTE_TYPES, BRANCH_NAMES, FUMEN_NOTE_TYPES from tja2fumen.types import (TJASong, TJAMeasure, TJAData, FumenCourse, FumenMeasure, FumenBranch, FumenNote, FumenHeader) @@ -12,64 +12,64 @@ from tja2fumen.types import (TJASong, TJAMeasure, TJAData, ######################################################################################################################## -def parseTJA(fnameTJA): +def parse_tja(fname_tja): try: - tja_text = open(fnameTJA, "r", encoding="utf-8-sig").read() + tja_text = open(fname_tja, "r", encoding="utf-8-sig").read() except UnicodeDecodeError: - tja_text = open(fnameTJA, "r", encoding="shift-jis").read() + tja_text = open(fname_tja, "r", encoding="shift-jis").read() lines = [line for line in tja_text.splitlines() if line.strip() != ''] - parsedTJA = getCourseData(lines) - for course in parsedTJA.courses.values(): - parseCourseMeasures(course) + parsed_tja = get_course_data(lines) + for course in parsed_tja.courses.values(): + parse_course_measures(course) - return parsedTJA + return parsed_tja -def getCourseData(lines): - parsedTJA = None - currentCourse = '' - currentCourseCached = '' - songBPM = 0 - songOffset = 0 +def get_course_data(lines): + parsed_tja = None + current_course = '' + current_course_cached = '' + song_bpm = 0 + song_offset = 0 for line in lines: # Case 1: Header metadata match_header = re.match(r"^([A-Z]+):(.*)", line) if match_header: - nameUpper = match_header.group(1).upper() + name_upper = match_header.group(1).upper() value = match_header.group(2).strip() # Global header fields - if nameUpper in ['BPM', 'OFFSET']: - if nameUpper == 'BPM': - songBPM = value - elif nameUpper == 'OFFSET': - songOffset = value - if songBPM and songOffset: - parsedTJA = TJASong(songBPM, songOffset) + if name_upper in ['BPM', 'OFFSET']: + if name_upper == 'BPM': + song_bpm = value + elif name_upper == 'OFFSET': + song_offset = value + if song_bpm and song_offset: + parsed_tja = TJASong(song_bpm, song_offset) # Course-specific header fields - elif nameUpper == 'COURSE': - currentCourse = NORMALIZE_COURSE[value] - currentCourseCached = currentCourse - if currentCourse not in parsedTJA.courses.keys(): + elif name_upper == 'COURSE': + current_course = NORMALIZE_COURSE[value] + current_course_cached = current_course + if current_course not in parsed_tja.courses.keys(): raise ValueError() - elif nameUpper == 'LEVEL': - parsedTJA.courses[currentCourse].level = int(value) if value else 0 + elif name_upper == 'LEVEL': + parsed_tja.courses[current_course].level = int(value) if value else 0 # NB: If there are multiple SCOREINIT/SCOREDIFF values, use the last one (shinuti) - elif nameUpper == 'SCOREINIT': - parsedTJA.courses[currentCourse].scoreInit = int(value.split(",")[-1]) if value else 0 - elif nameUpper == 'SCOREDIFF': - parsedTJA.courses[currentCourse].scoreDiff = int(value.split(",")[-1]) if value else 0 - elif nameUpper == 'BALLOON': + elif name_upper == 'SCOREINIT': + parsed_tja.courses[current_course].score_init = int(value.split(",")[-1]) if value else 0 + elif name_upper == 'SCOREDIFF': + parsed_tja.courses[current_course].score_diff = int(value.split(",")[-1]) if value else 0 + elif name_upper == 'BALLOON': if value: balloons = [int(v) for v in value.split(",") if v] - parsedTJA.courses[currentCourse].balloon = balloons - elif nameUpper == 'STYLE': + parsed_tja.courses[current_course].balloon = balloons + elif name_upper == 'STYLE': # Reset the course name to remove "P1/P2" that may have been added by a previous STYLE:DOUBLE chart if value == 'Single': - currentCourse = currentCourseCached + current_course = current_course_cached else: pass # Ignore other header fields such as 'TITLE', 'SUBTITLE', 'WAVE', etc. @@ -78,43 +78,43 @@ def getCourseData(lines): match_command = re.match(r"^#([A-Z]+)(?:\s+(.+))?", line) match_notes = re.match(r"^(([0-9]|A|B|C|F|G)*,?).*$", line) if match_command: - nameUpper = match_command.group(1).upper() + name_upper = match_command.group(1).upper() value = match_command.group(2).strip() if match_command.group(2) else '' # For STYLE:Double, #START P1/P2 indicates the start of a new chart # But, we want multiplayer charts to inherit the metadata from the course as a whole, so we deepcopy - if nameUpper == "START": + if name_upper == "START": if value in ["P1", "P2"]: - currentCourse = currentCourseCached + value - parsedTJA.courses[currentCourse] = deepcopy(parsedTJA.courses[currentCourseCached]) - parsedTJA.courses[currentCourse].data = list() # Keep the metadata, but reset the note data + current_course = current_course_cached + value + parsed_tja.courses[current_course] = deepcopy(parsed_tja.courses[current_course_cached]) + parsed_tja.courses[current_course].data = list() # Keep the metadata, but reset the note data value = '' # Once we've made the new course, we can reset this to a normal #START command elif value: raise ValueError(f"Invalid value '{value}' for #START command.") elif match_notes: - nameUpper = 'NOTES' + name_upper = 'NOTES' value = match_notes.group(1) - parsedTJA.courses[currentCourse].data.append(TJAData(nameUpper, value)) + parsed_tja.courses[current_course].data.append(TJAData(name_upper, value)) # If a course has no song data, then this is likely because the course has "STYLE: Double" but no "STYLE: Single". # To fix this, we copy over the P1 chart from "STYLE: Double" to fill the "STYLE: Single" role. - for courseName, course in parsedTJA.courses.items(): + for course_name, course in parsed_tja.courses.items(): if not course.data: - if courseName+"P1" in parsedTJA.courses.keys(): - parsedTJA.courses[courseName] = deepcopy(parsedTJA.courses[courseName+"P1"]) + if course_name+"P1" in parsed_tja.courses.keys(): + parsed_tja.courses[course_name] = deepcopy(parsed_tja.courses[course_name+"P1"]) # Remove any charts (e.g. P1/P2) not present in the TJA file - for course_name in [k for k, v in parsedTJA.courses.items() if not v.data]: - del parsedTJA.courses[course_name] + for course_name in [k for k, v in parsed_tja.courses.items() if not v.data]: + del parsed_tja.courses[course_name] - return parsedTJA + return parsed_tja -def parseCourseMeasures(course): +def parse_course_measures(course): # Check if the course has branches or not - hasBranches = True if [l for l in course.data if l.name == 'BRANCHSTART'] else False - currentBranch = 'all' if hasBranches else 'normal' + has_branches = True if [l for l in course.data if l.name == 'BRANCHSTART'] else False + current_branch = 'all' if has_branches else 'normal' branch_condition = None - flagLevelhold = False + flag_levelhold = False # Process course lines idx_m = 0 @@ -125,53 +125,53 @@ def parseCourseMeasures(course): notes = line.value # If measure has ended, then add notes to the current measure, then start a new one by incrementing idx_m if notes.endswith(','): - for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]: + for branch in course.branches.keys() if current_branch == 'all' else [current_branch]: course.branches[branch][idx_m].notes += notes[0:-1] course.branches[branch].append(TJAMeasure()) idx_m += 1 # Otherwise, keep adding notes to the current measure ('idx_m') else: - for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]: + for branch in course.branches.keys() if current_branch == 'all' else [current_branch]: course.branches[branch][idx_m].notes += notes # 2. Parse measure commands that produce an "event" elif line.name in ['GOGOSTART', 'GOGOEND', 'BARLINEON', 'BARLINEOFF', 'DELAY', 'SCROLL', 'BPMCHANGE', 'MEASURE', 'SECTION', 'BRANCHSTART']: # Get position of the event - for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]: + for branch in course.branches.keys() if current_branch == 'all' else [current_branch]: pos = len(course.branches[branch][idx_m].notes) # Parse event type if line.name == 'GOGOSTART': - currentEvent = TJAData('gogo', '1', pos) + current_event = TJAData('gogo', '1', pos) elif line.name == 'GOGOEND': - currentEvent = TJAData('gogo', '0', pos) + current_event = TJAData('gogo', '0', pos) elif line.name == 'BARLINEON': - currentEvent = TJAData('barline', '1', pos) + current_event = TJAData('barline', '1', pos) elif line.name == 'BARLINEOFF': - currentEvent = TJAData('barline', '0', pos) + current_event = TJAData('barline', '0', pos) elif line.name == 'DELAY': - currentEvent = TJAData('delay', float(line.value), pos) + current_event = TJAData('delay', float(line.value), pos) elif line.name == 'SCROLL': - currentEvent = TJAData('scroll', float(line.value), pos) + current_event = TJAData('scroll', float(line.value), pos) elif line.name == 'BPMCHANGE': - currentEvent = TJAData('bpm', float(line.value), pos) + current_event = TJAData('bpm', float(line.value), pos) elif line.name == 'MEASURE': - currentEvent = TJAData('measure', line.value, pos) + current_event = TJAData('measure', line.value, pos) elif line.name == 'SECTION': if branch_condition is None: - currentEvent = TJAData('section', 'not_available', pos) + current_event = TJAData('section', 'not_available', pos) else: - currentEvent = TJAData('section', branch_condition, pos) + current_event = TJAData('section', branch_condition, pos) # If the command immediately after #SECTION is #BRANCHSTART, then we need to make sure that #SECTION # is put on every branch. (We can't do this unconditionally because #SECTION commands can also exist # in isolation in the middle of separate branches.) if course.data[idx_l+1].name == 'BRANCHSTART': - currentBranch = 'all' + current_branch = 'all' elif line.name == 'BRANCHSTART': - if flagLevelhold: + if flag_levelhold: continue - currentBranch = 'all' # Ensure that the #BRANCHSTART command is present for all branches + current_branch = 'all' # Ensure that the #BRANCHSTART command is present for all branches branch_condition = line.value.split(',') if branch_condition[0] == 'r': # r = drumRoll branch_condition[1] = int(branch_condition[1]) # # of drumrolls @@ -179,31 +179,31 @@ def parseCourseMeasures(course): elif branch_condition[0] == 'p': # p = Percentage branch_condition[1] = float(branch_condition[1]) / 100 # % branch_condition[2] = float(branch_condition[2]) / 100 # % - currentEvent = TJAData('branchStart', branch_condition, pos) + current_event = TJAData('branch_start', branch_condition, pos) idx_m_branchstart = idx_m # Preserve the index of the BRANCHSTART command to re-use for each branch # Append event to the current measure's events - for branch in course.branches.keys() if currentBranch == 'all' else [currentBranch]: - course.branches[branch][idx_m].events.append(currentEvent) + for branch in course.branches.keys() if current_branch == 'all' else [current_branch]: + course.branches[branch][idx_m].events.append(current_event) # 3. Parse commands that don't create an event (e.g. simply changing the current branch) else: if line.name == 'START' or line.name == 'END': - currentBranch = 'all' if hasBranches else 'normal' - flagLevelhold = False + current_branch = 'all' if has_branches else 'normal' + flag_levelhold = False elif line.name == 'LEVELHOLD': - flagLevelhold = True + flag_levelhold = True elif line.name == 'N': - currentBranch = 'normal' + current_branch = 'normal' idx_m = idx_m_branchstart elif line.name == 'E': - currentBranch = 'advanced' + current_branch = 'advanced' idx_m = idx_m_branchstart elif line.name == 'M': - currentBranch = 'master' + current_branch = 'master' idx_m = idx_m_branchstart elif line.name == 'BRANCHEND': - currentBranch = 'all' + current_branch = 'all' else: print(f"Ignoring unsupported command '{line.name}'") @@ -214,7 +214,7 @@ def parseCourseMeasures(course): del branch[-1] # Merge measure data and measure events in chronological order - for branchName, branch in course.branches.items(): + for branch_name, branch in course.branches.items(): for measure in branch: notes = [TJAData('note', TJA_NOTE_TYPES[note], i) for i, note in enumerate(measure.notes) if note != '0'] @@ -231,7 +231,7 @@ def parseCourseMeasures(course): measure.combined.append(notes.pop(0)) # Ensure all branches have the same number of measures - if hasBranches: + if has_branches: branch_lens = [len(b) for b in course.branches.values()] if not branch_lens.count(branch_lens[0]) == len(branch_lens): raise ValueError("Branches do not have the same number of measures.") @@ -247,21 +247,21 @@ def parseCourseMeasures(course): # TODO: Figure out what the unknown Wii1, Wii4, and PS4 notes represent (just in case they're important somehow) -def readFumen(fumenFile, exclude_empty_measures=False): +def read_fumen(fumen_file, exclude_empty_measures=False): """ Parse bytes of a fumen .bin file into nested measure, branch, and note dictionaries. - For more information on any of the terms used in this function (e.g. scoreInit, scoreDiff), + For more information on any of the terms used in this function (e.g. score_init, score_diff), please refer to KatieFrog's excellent guide: https://gist.github.com/KatieFrogs/e000f406bbc70a12f3c34a07303eec8b """ - file = open(fumenFile, "rb") + file = open(fumen_file, "rb") size = os.fstat(file.fileno()).st_size song = FumenCourse( header=FumenHeader(raw_bytes=file.read(520)) ) - for measureNumber in range(song.header.b512_b515_number_of_measures): + for measure_number in range(song.header.b512_b515_number_of_measures): # Parse the measure data using the following `format_string`: # "ffBBHiiiiiii" (12 format characters, 40 bytes per measure) # - 'f': BPM (represented by one float (4 bytes)) @@ -269,88 +269,88 @@ def readFumen(fumenFile, exclude_empty_measures=False): # - 'B': gogo (represented by one unsigned char (1 byte)) # - 'B': barline (represented by one unsigned char (1 byte)) # - 'H': (represented by one unsigned short (2 bytes)) - # - 'iiiiii': branchInfo (represented by six integers (24 bytes)) + # - 'iiiiii': branch_info (represented by six integers (24 bytes)) # - 'i': (represented by one integer (4 bytes) - measureStruct = readStruct(file, song.header.order, format_string="ffBBHiiiiiii") + measure_struct = read_struct(file, song.header.order, format_string="ffBBHiiiiiii") # Create the measure dictionary using the newly-parsed measure data measure = FumenMeasure( - bpm=measureStruct[0], - fumenOffsetStart=measureStruct[1], - gogo=measureStruct[2], - barline=measureStruct[3], - padding1=measureStruct[4], - branchInfo=list(measureStruct[5:11]), - padding2=measureStruct[11] + bpm=measure_struct[0], + fumen_offset_start=measure_struct[1], + gogo=measure_struct[2], + barline=measure_struct[3], + padding1=measure_struct[4], + branch_info=list(measure_struct[5:11]), + padding2=measure_struct[11] ) # Iterate through the three branch types - for branchName in branchNames: + for branch_name in BRANCH_NAMES: # Parse the measure data using the following `format_string`: # "HHf" (3 format characters, 8 bytes per branch) - # - 'H': totalNotes (represented by one unsigned short (2 bytes)) + # - 'H': total_notes (represented by one unsigned short (2 bytes)) # - 'H': (represented by one unsigned short (2 bytes)) # - 'f': speed (represented by one float (4 bytes) - branchStruct = readStruct(file, song.header.order, format_string="HHf") + branch_struct = read_struct(file, song.header.order, format_string="HHf") # Create the branch dictionary using the newly-parsed branch data - totalNotes = branchStruct[0] + total_notes = branch_struct[0] branch = FumenBranch( - length=totalNotes, - padding=branchStruct[1], - speed=branchStruct[2], + length=total_notes, + padding=branch_struct[1], + speed=branch_struct[2], ) # Iterate through each note in the measure (per branch) - for noteNumber in range(totalNotes): + for note_number in range(total_notes): # Parse the note data using the following `format_string`: # "ififHHf" (7 format characters, 24 bytes per note cluster) # - 'i': note type # - 'f': note position # - 'i': item # - 'f': - # - 'H': scoreInit - # - 'H': scoreDiff + # - 'H': score_init + # - 'H': score_diff # - 'f': duration # NB: 'item' doesn't seem to be used at all in this function. - noteStruct = readStruct(file, song.header.order, format_string="ififHHf") + note_struct = read_struct(file, song.header.order, format_string="ififHHf") # Validate the note type - noteType = noteStruct[0] - if noteType not in noteTypes: + note_type = note_struct[0] + if note_type not in FUMEN_NOTE_TYPES: raise ValueError("Error: Unknown note type '{0}' at offset {1}".format( - shortHex(noteType).upper(), + short_hex(note_type).upper(), hex(file.tell() - 0x18)) ) # Create the note dictionary using the newly-parsed note data note = FumenNote( - note_type=noteTypes[noteType], - pos=noteStruct[1], - item=noteStruct[2], - padding=noteStruct[3], + note_type=FUMEN_NOTE_TYPES[note_type], + pos=note_struct[1], + item=note_struct[2], + padding=note_struct[3], ) - if noteType == 0xa or noteType == 0xc: + if note_type == 0xa or note_type == 0xc: # Balloon hits - note.hits = noteStruct[4] - note.hitsPadding = noteStruct[5] + note.hits = note_struct[4] + note.hits_padding = note_struct[5] else: - song.scoreInit = note.scoreInit = noteStruct[4] - song.scoreDiff = note.scoreDiff = noteStruct[5] // 4 + song.score_init = note.score_init = note_struct[4] + song.score_diff = note.score_diff = note_struct[5] // 4 # Drumroll/balloon duration - note.duration = noteStruct[6] + note.duration = note_struct[6] # Seek forward 8 bytes to account for padding bytes at the end of drumrolls - if noteType == 0x6 or noteType == 0x9 or noteType == 0x62: - note.drumrollBytes = file.read(8) + if note_type == 0x6 or note_type == 0x9 or note_type == 0x62: + note.drumroll_bytes = file.read(8) # Assign the note to the branch branch.notes.append(note) # Assign the branch to the measure - measure.branches[branchName] = branch + measure.branches[branch_name] = branch # Assign the measure to the song song.measures.append(measure) diff --git a/src/tja2fumen/types.py b/src/tja2fumen/types.py index cc2c045..8b25d35 100644 --- a/src/tja2fumen/types.py +++ b/src/tja2fumen/types.py @@ -16,11 +16,11 @@ class TJASong: class TJACourse: - def __init__(self, BPM, offset, course, level=0, balloon=None, scoreInit=0, scoreDiff=0): + def __init__(self, BPM, offset, course, level=0, balloon=None, score_init=0, score_diff=0): self.level = level self.balloon = [] if balloon is None else balloon - self.scoreInit = scoreInit - self.scoreDiff = scoreDiff + self.score_init = score_init + self.score_diff = score_diff self.BPM = BPM self.offset = offset self.course = course @@ -47,7 +47,7 @@ class TJAMeasure: class TJAMeasureProcessed: def __init__(self, bpm, scroll, gogo, barline, time_sig, subdivisions, - pos_start=0, pos_end=0, delay=0, section=None, branchStart=None, data=None): + pos_start=0, pos_end=0, delay=0, section=None, branch_start=None, data=None): self.bpm = bpm self.scroll = scroll self.gogo = gogo @@ -58,7 +58,7 @@ class TJAMeasureProcessed: self.pos_end = pos_end self.delay = delay self.section = section - self.branchStart = branchStart + self.branch_start = branch_start self.data = [] if data is None else data def __repr__(self): @@ -76,30 +76,30 @@ class TJAData: class FumenCourse: - def __init__(self, measures=None, header=None, scoreInit=0, scoreDiff=0): + def __init__(self, measures=None, header=None, score_init=0, score_diff=0): if isinstance(measures, int): self.measures = [FumenMeasure() for _ in range(measures)] else: self.measures = [] if measures is None else measures self.header = FumenHeader() if header is None else header - self.scoreInit = scoreInit - self.scoreDiff = scoreDiff + self.score_init = score_init + self.score_diff = score_diff def __repr__(self): return str(self.__dict__) class FumenMeasure: - def __init__(self, bpm=0.0, fumenOffsetStart=0.0, fumenOffsetEnd=0.0, duration=0.0, - gogo=False, barline=True, branchStart=None, branchInfo=None, padding1=0, padding2=0): + def __init__(self, bpm=0.0, fumen_offset_start=0.0, fumen_offset_end=0.0, duration=0.0, + gogo=False, barline=True, branch_start=None, branch_info=None, padding1=0, padding2=0): self.bpm = bpm - self.fumenOffsetStart = fumenOffsetStart - self.fumenOffsetEnd = fumenOffsetEnd + self.fumen_offset_start = fumen_offset_start + self.fumen_offset_end = fumen_offset_end self.duration = duration self.gogo = gogo self.barline = barline - self.branchStart = branchStart - self.branchInfo = [-1, -1, -1, -1, -1, -1] if branchInfo is None else branchInfo + self.branch_start = branch_start + self.branch_info = [-1, -1, -1, -1, -1, -1] if branch_info is None else branch_info self.branches = {'normal': FumenBranch(), 'advanced': FumenBranch(), 'master': FumenBranch()} self.padding1 = padding1 self.padding2 = padding2 @@ -120,12 +120,12 @@ class FumenBranch: class FumenNote: - def __init__(self, note_type='', pos=0.0, scoreInit=0, scoreDiff=0, padding=0, item=0, duration=0.0, - multimeasure=False, hits=0, hitsPadding=0, drumrollBytes=b'\x00\x00\x00\x00\x00\x00\x00\x00'): + def __init__(self, note_type='', pos=0.0, score_init=0, score_diff=0, padding=0, item=0, duration=0.0, + multimeasure=False, hits=0, hits_padding=0, drumroll_bytes=b'\x00\x00\x00\x00\x00\x00\x00\x00'): self.note_type = note_type self.pos = pos - self.scoreInit = scoreInit - self.scoreDiff = scoreDiff + self.score_init = score_init + self.score_diff = score_diff self.padding = padding # TODO: Determine how to properly set the item byte (https://github.com/vivaria/tja2fumen/issues/17) self.item = item @@ -133,8 +133,8 @@ class FumenNote: self.duration = duration self.multimeasure = multimeasure self.hits = hits - self.hitsPadding = hitsPadding - self.drumrollBytes = drumrollBytes + self.hits_padding = hits_padding + self.drumroll_bytes = drumroll_bytes def __repr__(self): return str(self.__dict__) @@ -165,9 +165,9 @@ class FumenHeader: self.b472_b475_branch_points_ok = 10 self.b476_b479_branch_points_bad = 0 self.b480_b483_branch_points_drumroll = 1 - self.b484_b487_branch_points_good_BIG = 20 - self.b488_b491_branch_points_ok_BIG = 10 - self.b492_b495_branch_points_drumroll_BIG = 1 + self.b484_b487_branch_points_good_big = 20 + self.b488_b491_branch_points_ok_big = 10 + self.b492_b495_branch_points_drumroll_big = 1 self.b496_b499_branch_points_balloon = 30 self.b500_b503_branch_points_kusudama = 30 self.b504_b507_branch_points_unknown = 20 @@ -190,9 +190,9 @@ class FumenHeader: self.b472_b475_branch_points_ok = struct.unpack(self.order + "i", raw_bytes[472:476])[0] self.b476_b479_branch_points_bad = struct.unpack(self.order + "i", raw_bytes[476:480])[0] self.b480_b483_branch_points_drumroll = struct.unpack(self.order + "i", raw_bytes[480:484])[0] - self.b484_b487_branch_points_good_BIG = struct.unpack(self.order + "i", raw_bytes[484:488])[0] - self.b488_b491_branch_points_ok_BIG = struct.unpack(self.order + "i", raw_bytes[488:492])[0] - self.b492_b495_branch_points_drumroll_BIG = struct.unpack(self.order + "i", raw_bytes[492:496])[0] + self.b484_b487_branch_points_good_big = struct.unpack(self.order + "i", raw_bytes[484:488])[0] + self.b488_b491_branch_points_ok_big = struct.unpack(self.order + "i", raw_bytes[488:492])[0] + self.b492_b495_branch_points_drumroll_big = struct.unpack(self.order + "i", raw_bytes[492:496])[0] self.b496_b499_branch_points_balloon = struct.unpack(self.order + "i", raw_bytes[496:500])[0] self.b500_b503_branch_points_kusudama = struct.unpack(self.order + "i", raw_bytes[500:504])[0] self.b504_b507_branch_points_unknown = struct.unpack(self.order + "i", raw_bytes[504:508])[0] diff --git a/src/tja2fumen/utils.py b/src/tja2fumen/utils.py index 1ec2b00..481eb21 100644 --- a/src/tja2fumen/utils.py +++ b/src/tja2fumen/utils.py @@ -1,7 +1,7 @@ import struct -def readStruct(file, order, format_string, seek=None): +def read_struct(file, order, format_string, seek=None): """ Interpret bytes as packed binary data. @@ -29,12 +29,12 @@ def readStruct(file, order, format_string, seek=None): return interpreted_string -def writeStruct(file, order, format_string, value_list, seek=None): +def write_struct(file, order, format_string, value_list, seek=None): if seek: file.seek(seek) packed_bytes = struct.pack(order + format_string, *value_list) file.write(packed_bytes) -def shortHex(number): +def short_hex(number): return hex(number)[2:] diff --git a/src/tja2fumen/writers.py b/src/tja2fumen/writers.py index 1947896..47445c4 100644 --- a/src/tja2fumen/writers.py +++ b/src/tja2fumen/writers.py @@ -1,30 +1,30 @@ -from tja2fumen.utils import writeStruct -from tja2fumen.constants import branchNames, typeNotes +from tja2fumen.utils import write_struct +from tja2fumen.constants import BRANCH_NAMES, FUMEN_TYPE_NOTES -def writeFumen(path_out, song): +def write_fumen(path_out, song): with open(path_out, "wb") as file: file.write(song.header.raw_bytes) - for measureNumber in range(len(song.measures)): - measure = song.measures[measureNumber] - measureStruct = [measure.bpm, measure.fumenOffsetStart, int(measure.gogo), int(measure.barline)] - measureStruct.extend([measure.padding1] + measure.branchInfo + [measure.padding2]) - writeStruct(file, song.header.order, format_string="ffBBHiiiiiii", value_list=measureStruct) + for measure_number in range(len(song.measures)): + measure = song.measures[measure_number] + measure_struct = [measure.bpm, measure.fumen_offset_start, int(measure.gogo), int(measure.barline)] + measure_struct.extend([measure.padding1] + measure.branch_info + [measure.padding2]) + write_struct(file, song.header.order, format_string="ffBBHiiiiiii", value_list=measure_struct) - for branchNumber in range(len(branchNames)): - branch = measure.branches[branchNames[branchNumber]] - branchStruct = [branch.length, branch.padding, branch.speed] - writeStruct(file, song.header.order, format_string="HHf", value_list=branchStruct) + for branch_number in range(len(BRANCH_NAMES)): + branch = measure.branches[BRANCH_NAMES[branch_number]] + branch_struct = [branch.length, branch.padding, branch.speed] + write_struct(file, song.header.order, format_string="HHf", value_list=branch_struct) - for noteNumber in range(branch.length): - note = branch.notes[noteNumber] - noteStruct = [typeNotes[note.type], note.pos, note.item, note.padding] + for note_number in range(branch.length): + note = branch.notes[note_number] + note_struct = [FUMEN_TYPE_NOTES[note.type], note.pos, note.item, note.padding] if note.hits: - noteStruct.extend([note.hits, note.hitsPadding, note.duration]) + note_struct.extend([note.hits, note.hits_padding, note.duration]) else: - noteStruct.extend([note.scoreInit, note.scoreDiff * 4, note.duration]) - writeStruct(file, song.header.order, format_string="ififHHf", value_list=noteStruct) + note_struct.extend([note.score_init, note.score_diff * 4, note.duration]) + write_struct(file, song.header.order, format_string="ififHHf", value_list=note_struct) if note.type.lower() == "drumroll": - file.write(note.drumrollBytes) + file.write(note.drumroll_bytes) diff --git a/testing/test_conversion.py b/testing/test_conversion.py index ee1bb6c..c3a18e8 100644 --- a/testing/test_conversion.py +++ b/testing/test_conversion.py @@ -7,7 +7,7 @@ import glob import pytest from tja2fumen import main as convert -from tja2fumen.parsers import readFumen +from tja2fumen.parsers import read_fumen from tja2fumen.constants import COURSE_IDS, NORMALIZE_COURSE @@ -64,16 +64,16 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point): i_difficult_id = os.path.basename(path_out).split(".")[0].split("_")[1] i_difficulty = NORMALIZE_COURSE[{v: k for k, v in COURSE_IDS.items()}[i_difficult_id]] # noqa # 0. Read fumen data (converted vs. cached) - co_song = readFumen(path_out, exclude_empty_measures=True) - ca_song = readFumen(os.path.join(path_bin, os.path.basename(path_out)), exclude_empty_measures=True) + co_song = read_fumen(path_out, exclude_empty_measures=True) + ca_song = read_fumen(os.path.join(path_bin, os.path.basename(path_out)), exclude_empty_measures=True) # 1. Check song headers checkValidHeader(co_song.header) checkValidHeader(ca_song.header) # 2. Check song metadata assert_song_property(co_song.header, ca_song.header, 'order') assert_song_property(co_song.header, ca_song.header, 'b432_b435_has_branches') - assert_song_property(co_song, ca_song, 'scoreInit') - assert_song_property(co_song, ca_song, 'scoreDiff') + assert_song_property(co_song, ca_song, 'score_init') + assert_song_property(co_song, ca_song, 'score_diff') # 3. Check measure data for i_measure in range(max([len(co_song.measures), len(ca_song.measures)])): # NB: We could assert that len(measures) is the same for both songs, then iterate through zipped measures. @@ -83,7 +83,7 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point): ca_measure = ca_song.measures[i_measure] # 3a. Check measure metadata assert_song_property(co_measure, ca_measure, 'bpm', i_measure, abs=0.01) - assert_song_property(co_measure, ca_measure, 'fumenOffsetStart', i_measure, abs=0.15) + assert_song_property(co_measure, ca_measure, 'fumen_offset_start', i_measure, abs=0.15) assert_song_property(co_measure, ca_measure, 'gogo', i_measure) assert_song_property(co_measure, ca_measure, 'barline', i_measure) @@ -99,7 +99,7 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point): # B) The branching condition for KAGEKIYO is very strange (accuracy for the 7 big notes in the song) # So, we only test the branchInfo bytes for non-KAGEKIYO songs: else: - assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure) + assert_song_property(co_measure, ca_measure, 'branch_info', i_measure) # 3b. Check measure notes for i_branch in ['normal', 'advanced', 'master']: @@ -126,8 +126,8 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point): except AssertionError: pass if ca_note.note_type not in ["Balloon", "Kusudama"]: - assert_song_property(co_note, ca_note, 'scoreInit', i_measure, i_branch, i_note) - assert_song_property(co_note, ca_note, 'scoreDiff', i_measure, i_branch, i_note) + assert_song_property(co_note, ca_note, 'score_init', i_measure, i_branch, i_note) + assert_song_property(co_note, ca_note, 'score_diff', i_measure, i_branch, i_note) # NB: 'item' still needs to be implemented: https://github.com/vivaria/tja2fumen/issues/17 # assert_song_property(co_note, ca_note, 'item', i_measure, i_branch, i_note) @@ -168,9 +168,9 @@ def checkValidHeader(header): assert header.b472_b475_branch_points_ok in [10, 0, 1] assert header.b476_b479_branch_points_bad == 0 assert header.b480_b483_branch_points_drumroll in [1, 0] - assert header.b484_b487_branch_points_good_BIG in [20, 0, 1, 2] - assert header.b488_b491_branch_points_ok_BIG in [10, 0, 1] - assert header.b492_b495_branch_points_drumroll_BIG in [1, 0] + assert header.b484_b487_branch_points_good_big in [20, 0, 1, 2] + assert header.b488_b491_branch_points_ok_big in [10, 0, 1] + assert header.b492_b495_branch_points_drumroll_big in [1, 0] assert header.b496_b499_branch_points_balloon in [30, 0, 1] assert header.b500_b503_branch_points_kusudama in [30, 0]