Fix variable case (camelCase
-> snake_case
+ CONST_CASE
) (#47)
Wouldn't have been possible without https://gist.github.com/Mizzlr/eec29687704aa81bf61dfccda36ddb8c. Fixes #8 .
This commit is contained in:
parent
e5fcc4d497
commit
48718b2303
@ -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
|
||||
|
@ -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']:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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': <padding> (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': <padding> (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': <padding> (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': <padding>
|
||||
# - '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)
|
||||
|
@ -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]
|
||||
|
@ -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:]
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user