Make project pylint
-compliant (and add pylint
check to test suite) (#59)
This commit is contained in:
parent
5229db4aab
commit
387cbbcdfa
5
.github/workflows/check_code_quality.yml
vendored
5
.github/workflows/check_code_quality.yml
vendored
@ -25,8 +25,11 @@ jobs:
|
||||
run: |
|
||||
pip install -e .[dev]
|
||||
|
||||
- name: Lint project using flake8
|
||||
- name: Simple linting using flake8
|
||||
run: pflake8
|
||||
|
||||
- name: Strict linting using pylint
|
||||
run: pylint src
|
||||
|
||||
- name: Type analysis using mypy
|
||||
run: mypy src --strict
|
||||
|
@ -19,7 +19,7 @@ tja2fumen = "tja2fumen:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest", "build", "pyinstaller", "twine", "toml-cli",
|
||||
"flake8", "pyproject-flake8", "mypy"]
|
||||
"flake8", "pyproject-flake8", "mypy", "pylint"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
@ -34,3 +34,12 @@ per-file-ignores = """
|
||||
./src/tja2fumen/types.py: E221
|
||||
./testing/test_conversion.py: E221, E272
|
||||
"""
|
||||
|
||||
[tool.pylint.'MESSAGES CONTROL']
|
||||
disable = """
|
||||
too-many-instance-attributes,
|
||||
too-many-branches,
|
||||
too-many-arguments,
|
||||
too-many-locals,
|
||||
too-many-statements
|
||||
"""
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Entry points for tja2fumen.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
@ -39,7 +43,7 @@ def main(argv: Sequence[str] = ()) -> None:
|
||||
# Convert parsed TJA courses and write each course to `.bin` files
|
||||
for course_name, course in parsed_tja.courses.items():
|
||||
convert_and_write(course, course_name, base_name,
|
||||
single_course=(len(parsed_tja.courses) == 1))
|
||||
single_course=len(parsed_tja.courses) == 1)
|
||||
|
||||
|
||||
def convert_and_write(tja_data: TJACourse,
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Constant song properties of TJA and fumen files.
|
||||
"""
|
||||
|
||||
# Names for branches in diverge songs
|
||||
BRANCH_NAMES = ("normal", "professional", "master")
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Functions for converting TJA song data to Fumen song data.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from tja2fumen.types import (TJACourse, TJAMeasureProcessed,
|
||||
@ -27,7 +31,7 @@ def process_tja_commands(tja: TJACourse) \
|
||||
branch_name: [] for branch_name in tja.branches.keys()
|
||||
}
|
||||
for branch_name, branch_measures_tja in tja.branches.items():
|
||||
current_bpm = tja.BPM
|
||||
current_bpm = tja.bpm
|
||||
current_scroll = 1.0
|
||||
current_gogo = False
|
||||
current_barline = True
|
||||
@ -92,7 +96,7 @@ def process_tja_commands(tja: TJACourse) \
|
||||
# - Case 1: Command happens at the start of a measure;
|
||||
# just change the value directly
|
||||
if data.pos == 0:
|
||||
measure_tja_processed.__setattr__(data.name, new_val)
|
||||
setattr(measure_tja_processed, data.name, new_val)
|
||||
# - Case 2: Command happens in the middle of a measure;
|
||||
# start a new sub-measure
|
||||
else:
|
||||
@ -117,14 +121,7 @@ def process_tja_commands(tja: TJACourse) \
|
||||
|
||||
has_branches = all(len(b) for b in tja_branches_processed.values())
|
||||
if has_branches:
|
||||
if len(set([len(b) for b in tja.branches.values()])) != 1:
|
||||
raise ValueError(
|
||||
"Branches do not have the same number of measures. (This "
|
||||
"check was performed prior to splitting up the measures due "
|
||||
"to mid-measure commands. Please check the number of ',' you"
|
||||
"have in each branch.)"
|
||||
)
|
||||
if len(set([len(b) for b in tja_branches_processed.values()])) != 1:
|
||||
if len({len(b) for b in tja_branches_processed.values()}) != 1:
|
||||
raise ValueError(
|
||||
"Branches do not have the same number of measures. (This "
|
||||
"check was performed after splitting up the measures due "
|
||||
@ -182,13 +179,13 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
# Set song metadata using information from the processed measures
|
||||
fumen.header.b512_b515_number_of_measures = n_measures
|
||||
fumen.header.b432_b435_has_branches = int(all(
|
||||
[len(b) for b in tja_branches_processed.values()]
|
||||
len(b) for b in tja_branches_processed.values()
|
||||
))
|
||||
|
||||
# Iterate through the different branches in the TJA
|
||||
total_notes = {'normal': 0, 'professional': 0, 'master': 0}
|
||||
for current_branch, branch_tja in tja_branches_processed.items():
|
||||
if not len(branch_tja):
|
||||
if not branch_tja:
|
||||
continue
|
||||
branch_points_total = 0
|
||||
branch_points_measure = 0
|
||||
@ -242,8 +239,6 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
measure_fumen.set_branch_info(
|
||||
branch_type, branch_cond,
|
||||
branch_points_total, current_branch,
|
||||
first_branch_condition=(not branch_conditions),
|
||||
has_section=bool(measure_tja.section),
|
||||
has_levelhold=current_levelhold
|
||||
)
|
||||
# Reset the points to prepare for the next `#BRANCHSTART p`
|
||||
@ -274,11 +269,11 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
|
||||
# Create notes based on TJA measure data
|
||||
branch_points_measure = 0
|
||||
for idx_d, data in enumerate(measure_tja.data):
|
||||
for data in measure_tja.data:
|
||||
# Compute the ms position of the note
|
||||
pos_ratio = ((data.pos - measure_tja.pos_start) /
|
||||
(measure_tja.pos_end - measure_tja.pos_start))
|
||||
note_pos = (measure_fumen.duration * pos_ratio)
|
||||
note_pos = measure_fumen.duration * pos_ratio
|
||||
|
||||
# Handle '8' notes (end of a drumroll/balloon)
|
||||
if data.value == "EndDRB":
|
||||
@ -320,9 +315,9 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
if note.note_type in ["Balloon", "Kusudama"]:
|
||||
try:
|
||||
note.hits = course_balloons.pop(0)
|
||||
except IndexError:
|
||||
except IndexError as exc:
|
||||
raise ValueError(f"Not enough values for 'BALLOON: "
|
||||
f"{course_balloons}'")
|
||||
f"{course_balloons}'") from exc
|
||||
current_drumroll = note
|
||||
elif note.note_type in ["Drumroll", "DRUMROLL"]:
|
||||
current_drumroll = note
|
||||
@ -334,13 +329,13 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
|
||||
# Track branch points (to later compute `#BRANCHSTART p` vals)
|
||||
if note.note_type in ['Don', 'Ka']:
|
||||
pts_to_add = fumen.header.b468_b471_branch_points_good
|
||||
pts_to_add = fumen.header.b468_b471_branch_pts_good
|
||||
elif note.note_type in ['DON', 'KA']:
|
||||
pts_to_add = fumen.header.b484_b487_branch_points_good_big
|
||||
pts_to_add = fumen.header.b484_b487_branch_pts_good_big
|
||||
elif note.note_type == 'Balloon':
|
||||
pts_to_add = fumen.header.b496_b499_branch_points_balloon
|
||||
pts_to_add = fumen.header.b496_b499_branch_pts_balloon
|
||||
elif note.note_type == 'Kusudama':
|
||||
pts_to_add = fumen.header.b500_b503_branch_points_kusudama
|
||||
pts_to_add = fumen.header.b500_b503_branch_pts_kusudama
|
||||
else:
|
||||
pts_to_add = 0 # Drumrolls not relevant for `p` conditions
|
||||
branch_points_measure += pts_to_add
|
||||
@ -366,29 +361,29 @@ def convert_tja_to_fumen(tja: TJACourse) -> FumenCourse:
|
||||
# If song has only drumroll branching conditions (also allowing percentage
|
||||
# conditions that force a level up/level down), then set the header bytes
|
||||
# so that only drumrolls contribute to branching.
|
||||
drumroll_only = branch_types != [] and branch_conditions != [] and all([
|
||||
drumroll_only = branch_types and branch_conditions and all(
|
||||
(branch_type == 'r') or
|
||||
(branch_type == 'p' and cond[0] == 0.0 and cond[1] == 0.0) or
|
||||
(branch_type == 'p' and cond[0] > 1.00 and cond[1] > 1.00)
|
||||
for branch_type, cond in zip(branch_types, branch_conditions)
|
||||
])
|
||||
)
|
||||
if drumroll_only:
|
||||
fumen.header.b468_b471_branch_points_good = 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.b496_b499_branch_points_balloon = 0
|
||||
fumen.header.b500_b503_branch_points_kusudama = 0
|
||||
fumen.header.b468_b471_branch_pts_good = 0
|
||||
fumen.header.b484_b487_branch_pts_good_big = 0
|
||||
fumen.header.b472_b475_branch_pts_ok = 0
|
||||
fumen.header.b488_b491_branch_pts_ok_big = 0
|
||||
fumen.header.b496_b499_branch_pts_balloon = 0
|
||||
fumen.header.b500_b503_branch_pts_kusudama = 0
|
||||
|
||||
# Alternatively, if the song has only percentage-based conditions, then set
|
||||
# the header bytes so that only notes and balloons contribute to branching.
|
||||
percentage_only = branch_types != [] and all([
|
||||
percentage_only = branch_types and all(
|
||||
(branch_type != 'r')
|
||||
for branch_type in branch_types
|
||||
])
|
||||
)
|
||||
if percentage_only:
|
||||
fumen.header.b480_b483_branch_points_drumroll = 0
|
||||
fumen.header.b492_b495_branch_points_drumroll_big = 0
|
||||
fumen.header.b480_b483_branch_pts_drumroll = 0
|
||||
fumen.header.b492_b495_branch_pts_drumroll_big = 0
|
||||
|
||||
# Compute the ratio between normal and professional/master branches
|
||||
if total_notes['professional']:
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Functions for parsing TJA files (.tja) and Fumen files (.bin)
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
@ -19,9 +23,11 @@ from tja2fumen.constants import (NORMALIZE_COURSE, COURSE_NAMES, BRANCH_NAMES,
|
||||
def parse_tja(fname_tja: str) -> TJASong:
|
||||
"""Read in lines of a .tja file and load them into a TJASong object."""
|
||||
try:
|
||||
tja_text = open(fname_tja, "r", encoding="utf-8-sig").read()
|
||||
with open(fname_tja, "r", encoding="utf-8-sig") as tja_file:
|
||||
tja_text = tja_file.read()
|
||||
except UnicodeDecodeError:
|
||||
tja_text = open(fname_tja, "r", encoding="shift-jis").read()
|
||||
with open(fname_tja, "r", encoding="shift-jis") as tja_file:
|
||||
tja_text = tja_file.read()
|
||||
|
||||
tja_lines = [line for line in tja_text.splitlines() if line.strip() != '']
|
||||
tja = split_tja_lines_into_courses(tja_lines)
|
||||
@ -67,9 +73,9 @@ def split_tja_lines_into_courses(lines: list[str]) -> TJASong:
|
||||
offset = float([line.split(":")[1] for line in lines
|
||||
if line.startswith("OFFSET")][0])
|
||||
parsed_tja = TJASong(
|
||||
BPM=bpm,
|
||||
bpm=bpm,
|
||||
offset=offset,
|
||||
courses={course: TJACourse(BPM=bpm, offset=offset, course=course)
|
||||
courses={course: TJACourse(bpm=bpm, offset=offset, course=course)
|
||||
for course in TJA_COURSE_NAMES}
|
||||
)
|
||||
|
||||
@ -87,7 +93,7 @@ def split_tja_lines_into_courses(lines: list[str]) -> TJASong:
|
||||
|
||||
# Course-specific metadata fields
|
||||
if name_upper == 'COURSE':
|
||||
if value not in NORMALIZE_COURSE.keys():
|
||||
if value not in NORMALIZE_COURSE:
|
||||
raise ValueError(f"Invalid COURSE value: '{value}'")
|
||||
current_course = NORMALIZE_COURSE[value]
|
||||
current_course_basename = current_course
|
||||
@ -125,7 +131,7 @@ def split_tja_lines_into_courses(lines: list[str]) -> TJASong:
|
||||
current_course = current_course_basename + value
|
||||
parsed_tja.courses[current_course] = \
|
||||
deepcopy(parsed_tja.courses[current_course_basename])
|
||||
parsed_tja.courses[current_course].data = list()
|
||||
parsed_tja.courses[current_course].data = []
|
||||
elif value:
|
||||
raise ValueError(f"Invalid value '{value}' for #START.")
|
||||
|
||||
@ -275,7 +281,7 @@ def parse_tja_course_data(course: TJACourse) -> None:
|
||||
# 3. Parse commands that don't create an event
|
||||
# (e.g. simply changing the current branch)
|
||||
else:
|
||||
if command == 'START' or command == 'END':
|
||||
if command in ('START', 'END'):
|
||||
current_branch = 'all' if has_branches else 'normal'
|
||||
elif command == 'N':
|
||||
current_branch = 'normal'
|
||||
@ -318,7 +324,7 @@ def parse_tja_course_data(course: TJACourse) -> None:
|
||||
|
||||
# Ensure all branches have the same number of measures
|
||||
if has_branches:
|
||||
if len(set([len(b) for b in course.branches.values()])) != 1:
|
||||
if len({len(b) for b in course.branches.values()}) != 1:
|
||||
raise ValueError(
|
||||
"Branches do not have the same number of measures. (This "
|
||||
"check was performed prior to splitting up the measures due "
|
||||
@ -357,106 +363,103 @@ def parse_fumen(fumen_file: str,
|
||||
├─ FumenMeasure
|
||||
└─ ...
|
||||
"""
|
||||
file = open(fumen_file, "rb")
|
||||
size = os.fstat(file.fileno()).st_size
|
||||
with open(fumen_file, "rb") as file:
|
||||
size = os.fstat(file.fileno()).st_size
|
||||
|
||||
header = FumenHeader()
|
||||
header.parse_header_values(file.read(520))
|
||||
song = FumenCourse(header=header)
|
||||
header = FumenHeader()
|
||||
header.parse_header_values(file.read(520))
|
||||
song = FumenCourse(header=header)
|
||||
|
||||
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 (one float (4 bytes))
|
||||
# - 'f': fumenOffset (one float (4 bytes))
|
||||
# - 'B': gogo (one unsigned char (1 byte))
|
||||
# - 'B': barline (one unsigned char (1 byte))
|
||||
# - 'H': <padding> (one unsigned short (2 bytes))
|
||||
# - 'iiiiii': branch_info (six integers (24 bytes))
|
||||
# - 'i': <padding> (one integer (4 bytes)
|
||||
measure_struct = read_struct(file, song.header.order,
|
||||
format_string="ffBBHiiiiiii")
|
||||
|
||||
# Create the measure dictionary using the newly-parsed measure data
|
||||
measure = FumenMeasure(
|
||||
bpm=measure_struct[0],
|
||||
offset_start=measure_struct[1],
|
||||
gogo=bool(measure_struct[2]),
|
||||
barline=bool(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 branch_name in BRANCH_NAMES:
|
||||
for _ in range(song.header.b512_b515_number_of_measures):
|
||||
# Parse the measure data using the following `format_string`:
|
||||
# "HHf" (3 format characters, 8 bytes per branch)
|
||||
# - 'H': total_notes ( one unsigned short (2 bytes))
|
||||
# - 'H': <padding> ( one unsigned short (2 bytes))
|
||||
# - 'f': speed ( one float (4 bytes)
|
||||
branch_struct = read_struct(file, song.header.order,
|
||||
format_string="HHf")
|
||||
# "ffBBHiiiiiii" (12 format characters, 40 bytes per measure)
|
||||
# - 'f': BPM (one float (4 bytes))
|
||||
# - 'f': fumenOffset (one float (4 bytes))
|
||||
# - 'B': gogo (one unsigned char (1 byte))
|
||||
# - 'B': barline (one unsigned char (1 byte))
|
||||
# - 'H': <padding> (one unsigned short (2 bytes))
|
||||
# - 'iiiiii': branch_info (six integers (24 bytes))
|
||||
# - 'i': <padding> (one integer (4 bytes)
|
||||
measure_struct = read_struct(file, song.header.order,
|
||||
format_string="ffBBHiiiiiii")
|
||||
|
||||
# Create the branch dictionary using the newly-parsed branch data
|
||||
total_notes = branch_struct[0]
|
||||
branch = FumenBranch(
|
||||
length=total_notes,
|
||||
padding=branch_struct[1],
|
||||
speed=branch_struct[2],
|
||||
# Create the measure dictionary using the newly-parsed measure data
|
||||
measure = FumenMeasure(
|
||||
bpm=measure_struct[0],
|
||||
offset_start=measure_struct[1],
|
||||
gogo=bool(measure_struct[2]),
|
||||
barline=bool(measure_struct[3]),
|
||||
padding1=measure_struct[4],
|
||||
branch_info=list(measure_struct[5:11]),
|
||||
padding2=measure_struct[11]
|
||||
)
|
||||
|
||||
# Iterate through each note in the measure (per branch)
|
||||
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': score_init
|
||||
# - 'H': score_diff
|
||||
# - 'f': duration
|
||||
# NB: 'item' doesn't seem to be used at all in this function.
|
||||
note_struct = read_struct(file, song.header.order,
|
||||
format_string="ififHHf")
|
||||
# Iterate through the three branch types
|
||||
for branch_name in BRANCH_NAMES:
|
||||
# Parse the measure data using the following `format_string`:
|
||||
# "HHf" (3 format characters, 8 bytes per branch)
|
||||
# - 'H': total_notes ( one unsigned short (2 bytes))
|
||||
# - 'H': <padding> ( one unsigned short (2 bytes))
|
||||
# - 'f': speed ( one float (4 bytes)
|
||||
branch_struct = read_struct(file, song.header.order,
|
||||
format_string="HHf")
|
||||
|
||||
# Create the note dictionary using the newly-parsed note data
|
||||
note_type = note_struct[0]
|
||||
note = FumenNote(
|
||||
note_type=FUMEN_NOTE_TYPES[note_type],
|
||||
pos=note_struct[1],
|
||||
item=note_struct[2],
|
||||
padding=note_struct[3],
|
||||
# Create the branch dictionary using newly-parsed branch data
|
||||
total_notes = branch_struct[0]
|
||||
branch = FumenBranch(
|
||||
length=total_notes,
|
||||
padding=branch_struct[1],
|
||||
speed=branch_struct[2],
|
||||
)
|
||||
|
||||
if note_type == 0xa or note_type == 0xc:
|
||||
# Balloon hits
|
||||
note.hits = note_struct[4]
|
||||
note.hits_padding = note_struct[5]
|
||||
else:
|
||||
song.score_init = note.score_init = note_struct[4]
|
||||
song.score_diff = note.score_diff = note_struct[5] // 4
|
||||
# Iterate through each note in the measure (per branch)
|
||||
for _ in range(total_notes):
|
||||
# Parse the note data using the following `format_string`:
|
||||
# "ififHHf" (7 format characters, 24b per note cluster)
|
||||
# - 'i': note type
|
||||
# - 'f': note position
|
||||
# - 'i': item
|
||||
# - 'f': <padding>
|
||||
# - 'H': score_init
|
||||
# - 'H': score_diff
|
||||
# - 'f': duration
|
||||
note_struct = read_struct(file, song.header.order,
|
||||
format_string="ififHHf")
|
||||
|
||||
# Drumroll/balloon duration
|
||||
note.duration = note_struct[6]
|
||||
# Create the note dictionary using newly-parsed note data
|
||||
note_type = note_struct[0]
|
||||
note = FumenNote(
|
||||
note_type=FUMEN_NOTE_TYPES[note_type],
|
||||
pos=note_struct[1],
|
||||
item=note_struct[2],
|
||||
padding=note_struct[3],
|
||||
)
|
||||
|
||||
# Account for padding at the end of drumrolls
|
||||
if note_type == 0x6 or note_type == 0x9 or note_type == 0x62:
|
||||
note.drumroll_bytes = file.read(8)
|
||||
if note_type in (0xa, 0xc):
|
||||
# Balloon hits
|
||||
note.hits = note_struct[4]
|
||||
note.hits_padding = note_struct[5]
|
||||
else:
|
||||
song.score_init = note.score_init = note_struct[4]
|
||||
song.score_diff = note.score_diff = note_struct[5] // 4
|
||||
|
||||
# Assign the note to the branch
|
||||
branch.notes.append(note)
|
||||
# Drumroll/balloon duration
|
||||
note.duration = note_struct[6]
|
||||
|
||||
# Assign the branch to the measure
|
||||
measure.branches[branch_name] = branch
|
||||
# Account for padding at the end of drumrolls
|
||||
if note_type in (0x6, 0x9, 0x62):
|
||||
note.drumroll_bytes = file.read(8)
|
||||
|
||||
# Assign the measure to the song
|
||||
song.measures.append(measure)
|
||||
if file.tell() >= size:
|
||||
break
|
||||
# Assign the note to the branch
|
||||
branch.notes.append(note)
|
||||
|
||||
file.close()
|
||||
# Assign the branch to the measure
|
||||
measure.branches[branch_name] = branch
|
||||
|
||||
# Assign the measure to the song
|
||||
song.measures.append(measure)
|
||||
if file.tell() >= size:
|
||||
break
|
||||
|
||||
# NB: Official fumens often include empty measures as a way of inserting
|
||||
# barlines for visual effect. But, TJA authors tend not to add these empty
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Dataclasses used to represent song courses, branches, measures, and notes.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import struct
|
||||
@ -28,7 +32,7 @@ class TJAMeasure:
|
||||
@dataclass(slots=True)
|
||||
class TJACourse:
|
||||
"""Contains all the data in a single TJA `COURSE:` section."""
|
||||
BPM: float
|
||||
bpm: float
|
||||
offset: float
|
||||
course: str
|
||||
level: int = 0
|
||||
@ -44,7 +48,7 @@ class TJACourse:
|
||||
@dataclass(slots=True)
|
||||
class TJASong:
|
||||
"""Contains all the data in a single TJA (`.tja`) chart file."""
|
||||
BPM: float
|
||||
bpm: float
|
||||
offset: float
|
||||
courses: dict[str, TJACourse]
|
||||
|
||||
@ -123,7 +127,7 @@ class FumenMeasure:
|
||||
subdivisions: int) -> None:
|
||||
"""Compute the millisecond duration of the measure."""
|
||||
# First, we compute the duration for a full 4/4 measure.
|
||||
full_duration = (4 * 60_000 / self.bpm)
|
||||
full_duration = 4 * 60_000 / self.bpm
|
||||
# Next, we adjust this duration based on both:
|
||||
# 1. The *actual* measure size (e.g. #MEASURE 1/8, 5/4, etc.)
|
||||
# 2. Whether this is a "submeasure" (i.e. whether it contains
|
||||
@ -136,11 +140,12 @@ class FumenMeasure:
|
||||
1.0 if subdivisions == 0.0 # Avoid DivisionByZeroErrors
|
||||
else (measure_length / subdivisions)
|
||||
)
|
||||
self.duration = (full_duration * measure_size * measure_ratio)
|
||||
self.duration = full_duration * measure_size * measure_ratio
|
||||
|
||||
def set_first_ms_offsets(self, song_offset: float) -> None:
|
||||
"""Compute the ms offsets for the start/end of the first measure."""
|
||||
# First, start with song's OFFSET: metadata
|
||||
self.offset_start = (song_offset * -1 * 1000) # s -> ms
|
||||
self.offset_start = song_offset * -1 * 1000 # s -> ms
|
||||
# Then, subtract a full 4/4 measure for the current BPM
|
||||
self.offset_start -= (4 * 60_000 / self.bpm)
|
||||
# Compute the end offset by adding the duration to the start offset
|
||||
@ -149,7 +154,7 @@ class FumenMeasure:
|
||||
def set_ms_offsets(self,
|
||||
delay: float,
|
||||
prev_measure: 'FumenMeasure') -> None:
|
||||
"""Compute the millisecond offsets for the start/end of the measure."""
|
||||
"""Compute the ms offsets for the start/end of a given measure."""
|
||||
# First, start with the end timing of the previous measure
|
||||
self.offset_start = prev_measure.offset_end
|
||||
# Add any #DELAY commands
|
||||
@ -166,8 +171,6 @@ class FumenMeasure:
|
||||
branch_cond: tuple[float, float],
|
||||
branch_points_total: int,
|
||||
current_branch: str,
|
||||
first_branch_condition: bool,
|
||||
has_section: bool,
|
||||
has_levelhold: bool) -> None:
|
||||
"""Compute the values that represent branching/diverge conditions."""
|
||||
# If levelhold is set, force the branch to stay the same,
|
||||
@ -208,7 +211,6 @@ class FumenMeasure:
|
||||
# has a #SECTION command to reset the accuracy.
|
||||
# 3. It's not the first branching condition, and it
|
||||
# doesn't have a #SECTION command.
|
||||
# TODO: Determine the behavior for these 3 conditions
|
||||
elif branch_type == 'r':
|
||||
vals = [int(v) for v in branch_cond]
|
||||
if current_branch == 'normal':
|
||||
@ -234,78 +236,78 @@ class FumenHeader:
|
||||
b456_b459_normal_normal_ratio: int = 65536
|
||||
b460_b463_normal_professional_ratio: int = 65536
|
||||
b464_b467_normal_master_ratio: int = 65536
|
||||
b468_b471_branch_points_good: int = 20
|
||||
b472_b475_branch_points_ok: int = 10
|
||||
b476_b479_branch_points_bad: int = 0
|
||||
b480_b483_branch_points_drumroll: int = 1
|
||||
b484_b487_branch_points_good_big: int = 20
|
||||
b488_b491_branch_points_ok_big: int = 10
|
||||
b492_b495_branch_points_drumroll_big: int = 1
|
||||
b496_b499_branch_points_balloon: int = 30
|
||||
b500_b503_branch_points_kusudama: int = 30
|
||||
b504_b507_branch_points_unknown: int = 20
|
||||
b468_b471_branch_pts_good: int = 20
|
||||
b472_b475_branch_pts_ok: int = 10
|
||||
b476_b479_branch_pts_bad: int = 0
|
||||
b480_b483_branch_pts_drumroll: int = 1
|
||||
b484_b487_branch_pts_good_big: int = 20
|
||||
b488_b491_branch_pts_ok_big: int = 10
|
||||
b492_b495_branch_pts_drumroll_big: int = 1
|
||||
b496_b499_branch_pts_balloon: int = 30
|
||||
b500_b503_branch_pts_kusudama: int = 30
|
||||
b504_b507_branch_pts_unknown: int = 20
|
||||
b508_b511_dummy_data: int = 12345678
|
||||
b512_b515_number_of_measures: int = 0
|
||||
b516_b519_unknown_data: int = 0
|
||||
|
||||
def parse_header_values(self, raw_bytes: bytes) -> None:
|
||||
"""Parse a raw string of 520 bytes to get the header values."""
|
||||
self.order = self._parse_order(raw_bytes)
|
||||
rb = raw_bytes # We use a shortened form just for visual clarity:
|
||||
self.b000_b431_timing_windows = self.up(rb, "f"*108, 0, 431)
|
||||
self.b432_b435_has_branches = self.up(rb, "i", 432, 435)
|
||||
self.b436_b439_hp_max = self.up(rb, "i", 436, 439)
|
||||
self.b440_b443_hp_clear = self.up(rb, "i", 440, 443)
|
||||
self.b444_b447_hp_gain_good = self.up(rb, "i", 444, 447)
|
||||
self.b448_b451_hp_gain_ok = self.up(rb, "i", 448, 451)
|
||||
self.b452_b455_hp_loss_bad = self.up(rb, "i", 452, 455)
|
||||
self.b456_b459_normal_normal_ratio = self.up(rb, "i", 456, 459)
|
||||
self.b460_b463_normal_professional_ratio = self.up(rb, "i", 460, 463)
|
||||
self.b464_b467_normal_master_ratio = self.up(rb, "i", 464, 467)
|
||||
self.b468_b471_branch_points_good = self.up(rb, "i", 468, 471)
|
||||
self.b472_b475_branch_points_ok = self.up(rb, "i", 472, 475)
|
||||
self.b476_b479_branch_points_bad = self.up(rb, "i", 476, 479)
|
||||
self.b480_b483_branch_points_drumroll = self.up(rb, "i", 480, 483)
|
||||
self.b484_b487_branch_points_good_big = self.up(rb, "i", 484, 487)
|
||||
self.b488_b491_branch_points_ok_big = self.up(rb, "i", 488, 491)
|
||||
self.b492_b495_branch_points_drumroll_big = self.up(rb, "i", 492, 495)
|
||||
self.b496_b499_branch_points_balloon = self.up(rb, "i", 496, 499)
|
||||
self.b500_b503_branch_points_kusudama = self.up(rb, "i", 500, 503)
|
||||
self.b504_b507_branch_points_unknown = self.up(rb, "i", 504, 507)
|
||||
self.b508_b511_dummy_data = self.up(rb, "i", 508, 511)
|
||||
self.b512_b515_number_of_measures = self.up(rb, "i", 512, 515)
|
||||
self.b516_b519_unknown_data = self.up(rb, "i", 516, 519)
|
||||
self._parse_order(raw_bytes)
|
||||
raw = raw_bytes # We use a shortened form just for visual clarity:
|
||||
self.b000_b431_timing_windows = self.unp(raw, "f"*108, 0, 431)
|
||||
self.b432_b435_has_branches = self.unp(raw, "i", 432, 435)
|
||||
self.b436_b439_hp_max = self.unp(raw, "i", 436, 439)
|
||||
self.b440_b443_hp_clear = self.unp(raw, "i", 440, 443)
|
||||
self.b444_b447_hp_gain_good = self.unp(raw, "i", 444, 447)
|
||||
self.b448_b451_hp_gain_ok = self.unp(raw, "i", 448, 451)
|
||||
self.b452_b455_hp_loss_bad = self.unp(raw, "i", 452, 455)
|
||||
self.b456_b459_normal_normal_ratio = self.unp(raw, "i", 456, 459)
|
||||
self.b460_b463_normal_professional_ratio = self.unp(raw, "i", 460, 463)
|
||||
self.b464_b467_normal_master_ratio = self.unp(raw, "i", 464, 467)
|
||||
self.b468_b471_branch_pts_good = self.unp(raw, "i", 468, 471)
|
||||
self.b472_b475_branch_pts_ok = self.unp(raw, "i", 472, 475)
|
||||
self.b476_b479_branch_pts_bad = self.unp(raw, "i", 476, 479)
|
||||
self.b480_b483_branch_pts_drumroll = self.unp(raw, "i", 480, 483)
|
||||
self.b484_b487_branch_pts_good_big = self.unp(raw, "i", 484, 487)
|
||||
self.b488_b491_branch_pts_ok_big = self.unp(raw, "i", 488, 491)
|
||||
self.b492_b495_branch_pts_drumroll_big = self.unp(raw, "i", 492, 495)
|
||||
self.b496_b499_branch_pts_balloon = self.unp(raw, "i", 496, 499)
|
||||
self.b500_b503_branch_pts_kusudama = self.unp(raw, "i", 500, 503)
|
||||
self.b504_b507_branch_pts_unknown = self.unp(raw, "i", 504, 507)
|
||||
self.b508_b511_dummy_data = self.unp(raw, "i", 508, 511)
|
||||
self.b512_b515_number_of_measures = self.unp(raw, "i", 512, 515)
|
||||
self.b516_b519_unknown_data = self.unp(raw, "i", 516, 519)
|
||||
|
||||
def up(self, raw_bytes: bytes, type_string: str,
|
||||
s: Optional[int] = None, e: Optional[int] = None) -> Any:
|
||||
def unp(self, raw_bytes: bytes, type_string: str,
|
||||
start: Optional[int] = None, end: Optional[int] = None) -> Any:
|
||||
"""Unpack a raw byte string according to specific types."""
|
||||
if s is not None and e is not None:
|
||||
raw_bytes = raw_bytes[s:e+1]
|
||||
if start is not None and end is not None:
|
||||
raw_bytes = raw_bytes[start:end+1]
|
||||
vals = struct.unpack(self.order + type_string, raw_bytes)
|
||||
return vals[0] if len(vals) == 1 else vals
|
||||
|
||||
def _parse_order(self, raw_bytes: bytes) -> str:
|
||||
def _parse_order(self, raw_bytes: bytes) -> None:
|
||||
"""Parse the order of the song (little or big endian)."""
|
||||
self.order = ''
|
||||
# Bytes 512-515 are the number of measures. We check the values using
|
||||
# both little and big endian, then compare to see which is correct.
|
||||
if (self.up(raw_bytes, ">I", 512, 515) <
|
||||
self.up(raw_bytes, "<I", 512, 515)):
|
||||
return ">"
|
||||
if (self.unp(raw_bytes, ">I", 512, 515) <
|
||||
self.unp(raw_bytes, "<I", 512, 515)):
|
||||
self.order = ">"
|
||||
else:
|
||||
return "<"
|
||||
self.order = "<"
|
||||
|
||||
def set_hp_bytes(self, n_notes: int, difficulty: str,
|
||||
stars: int) -> None:
|
||||
"""Compute header bytes related to the soul gauge (HP) behavior."""
|
||||
# Note: Ura Oni is equivalent to Oni for soul gauge behavior
|
||||
difficulty = 'Oni' if difficulty in ['Ura', 'Edit'] else difficulty
|
||||
self._get_hp_from_LUTs(n_notes, difficulty, stars)
|
||||
self._get_hp_from_lookup_tables(n_notes, difficulty, stars)
|
||||
self.b440_b443_hp_clear = {'Easy': 6000, 'Normal': 7000,
|
||||
'Hard': 7000, 'Oni': 8000}[difficulty]
|
||||
|
||||
def _get_hp_from_LUTs(self, n_notes: int, difficulty: str,
|
||||
stars: int) -> None:
|
||||
def _get_hp_from_lookup_tables(self, n_notes: int, difficulty: str,
|
||||
stars: int) -> None:
|
||||
"""Fetch pre-computed soul gauge values from lookup tables (LUTs)."""
|
||||
if not 0 < n_notes <= 2500:
|
||||
return
|
||||
@ -321,8 +323,9 @@ class FumenHeader:
|
||||
}
|
||||
key = f"{difficulty}-{star_to_key[difficulty][stars]}"
|
||||
pkg_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
with open(os.path.join(pkg_dir, "hp_values.csv"), newline='') as fp:
|
||||
for num, line in enumerate(csv.DictReader(fp)):
|
||||
with open(os.path.join(pkg_dir, "hp_values.csv"),
|
||||
newline='', encoding="utf-8") as csv_file:
|
||||
for num, line in enumerate(csv.DictReader(csv_file)):
|
||||
if num+1 == n_notes:
|
||||
self.b444_b447_hp_gain_good = int(line[f"good_{key}"])
|
||||
self.b448_b451_hp_gain_ok = int(line[f"ok_{key}"])
|
||||
@ -334,14 +337,14 @@ class FumenHeader:
|
||||
"""Represent the header values as a string of raw bytes."""
|
||||
value_list = []
|
||||
format_string = self.order
|
||||
for f in fields(self):
|
||||
if f.name in ["order", "_raw_bytes"]:
|
||||
for byte_field in fields(self):
|
||||
if byte_field.name in ["order", "_raw_bytes"]:
|
||||
pass
|
||||
elif f.name == "b000_b431_timing_windows":
|
||||
value_list.extend(list(getattr(self, f.name)))
|
||||
format_string += "f" * len(getattr(self, f.name))
|
||||
elif byte_field.name == "b000_b431_timing_windows":
|
||||
value_list.extend(list(getattr(self, byte_field.name)))
|
||||
format_string += "f" * len(getattr(self, byte_field.name))
|
||||
else:
|
||||
value_list.append(getattr(self, f.name))
|
||||
value_list.append(getattr(self, byte_field.name))
|
||||
format_string += "i"
|
||||
raw_bytes = struct.pack(format_string, *value_list)
|
||||
assert len(raw_bytes) == 520
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Functions for writing song data to fumen files (.bin)
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import BinaryIO, Any
|
||||
|
||||
@ -15,8 +19,7 @@ def write_fumen(path_out: str, song: FumenCourse) -> None:
|
||||
with open(path_out, "wb") as file:
|
||||
file.write(song.header.raw_bytes)
|
||||
|
||||
for measure_number in range(len(song.measures)):
|
||||
measure = song.measures[measure_number]
|
||||
for measure in song.measures:
|
||||
measure_struct = ([measure.bpm, measure.offset_start,
|
||||
int(measure.gogo), int(measure.barline),
|
||||
measure.padding1] + measure.branch_info +
|
||||
@ -25,15 +28,14 @@ def write_fumen(path_out: str, song: FumenCourse) -> None:
|
||||
format_string="ffBBHiiiiiii",
|
||||
value_list=measure_struct)
|
||||
|
||||
for branch_number in range(len(BRANCH_NAMES)):
|
||||
branch = measure.branches[BRANCH_NAMES[branch_number]]
|
||||
for branch_name in BRANCH_NAMES:
|
||||
branch = measure.branches[branch_name]
|
||||
branch_struct = [branch.length, branch.padding, branch.speed]
|
||||
write_struct(file, song.header.order,
|
||||
format_string="HHf",
|
||||
value_list=branch_struct)
|
||||
|
||||
for note_number in range(branch.length):
|
||||
note = branch.notes[note_number]
|
||||
for note in branch.notes:
|
||||
note_struct = [FUMEN_TYPE_NOTES[note.note_type], note.pos,
|
||||
note.item, note.padding]
|
||||
if note.hits:
|
||||
|
@ -79,15 +79,15 @@ def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
||||
# cannot be expressed in a TJA file. So, we skip checking the
|
||||
# `branch_point` header values for KAGEKIYO.
|
||||
if id_song != 'genpe':
|
||||
for header_property in ['b468_b471_branch_points_good',
|
||||
'b472_b475_branch_points_ok',
|
||||
'b476_b479_branch_points_bad',
|
||||
'b480_b483_branch_points_drumroll',
|
||||
'b484_b487_branch_points_good_big',
|
||||
'b488_b491_branch_points_ok_big',
|
||||
'b492_b495_branch_points_drumroll_big',
|
||||
'b496_b499_branch_points_balloon',
|
||||
'b500_b503_branch_points_kusudama']:
|
||||
for header_property in ['b468_b471_branch_pts_good',
|
||||
'b472_b475_branch_pts_ok',
|
||||
'b476_b479_branch_pts_bad',
|
||||
'b480_b483_branch_pts_drumroll',
|
||||
'b484_b487_branch_pts_good_big',
|
||||
'b488_b491_branch_pts_ok_big',
|
||||
'b492_b495_branch_pts_drumroll_big',
|
||||
'b496_b499_branch_pts_balloon',
|
||||
'b500_b503_branch_pts_kusudama']:
|
||||
check(co_song.header, ca_song.header, header_property)
|
||||
# 2. Check song metadata
|
||||
check(co_song, ca_song, 'score_init')
|
||||
@ -199,22 +199,22 @@ def normalize_type(note_type):
|
||||
|
||||
|
||||
def checkValidHeader(header):
|
||||
assert len(header.raw_bytes) == 520
|
||||
assert header.b432_b435_has_branches in [0, 1]
|
||||
assert header.b436_b439_hp_max == 10000
|
||||
assert header.b440_b443_hp_clear in [6000, 7000, 8000]
|
||||
assert 10 <= header.b444_b447_hp_gain_good <= 1020
|
||||
assert 5 <= header.b448_b451_hp_gain_ok <= 1020
|
||||
assert -765 <= header.b452_b455_hp_loss_bad <= -20
|
||||
assert header.b456_b459_normal_normal_ratio <= 65536
|
||||
assert header.b460_b463_normal_professional_ratio <= 65536
|
||||
assert header.b464_b467_normal_master_ratio <= 65536
|
||||
assert header.b468_b471_branch_points_good in [20, 0, 1, 2]
|
||||
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.b496_b499_branch_points_balloon in [30, 0, 1]
|
||||
assert header.b500_b503_branch_points_kusudama in [30, 0]
|
||||
assert len(header.raw_bytes) == 520
|
||||
assert header.b432_b435_has_branches in [0, 1]
|
||||
assert header.b436_b439_hp_max == 10000
|
||||
assert header.b440_b443_hp_clear in [6000, 7000, 8000]
|
||||
assert 10 <= header.b444_b447_hp_gain_good <= 1020
|
||||
assert 5 <= header.b448_b451_hp_gain_ok <= 1020
|
||||
assert -765 <= header.b452_b455_hp_loss_bad <= -20
|
||||
assert header.b456_b459_normal_normal_ratio <= 65536
|
||||
assert header.b460_b463_normal_professional_ratio <= 65536
|
||||
assert header.b464_b467_normal_master_ratio <= 65536
|
||||
assert header.b468_b471_branch_pts_good in [20, 0, 1, 2]
|
||||
assert header.b472_b475_branch_pts_ok in [10, 0, 1]
|
||||
assert header.b476_b479_branch_pts_bad == 0
|
||||
assert header.b480_b483_branch_pts_drumroll in [1, 0]
|
||||
assert header.b484_b487_branch_pts_good_big in [20, 0, 1, 2]
|
||||
assert header.b488_b491_branch_pts_ok_big in [10, 0, 1]
|
||||
assert header.b492_b495_branch_pts_drumroll_big in [1, 0]
|
||||
assert header.b496_b499_branch_pts_balloon in [30, 0, 1]
|
||||
assert header.b500_b503_branch_pts_kusudama in [30, 0]
|
||||
|
Loading…
x
Reference in New Issue
Block a user