From fc1b0d1bbc06f906f8d8e20ff13a2f4c43a60c29 Mon Sep 17 00:00:00 2001 From: Viv Date: Tue, 25 Jul 2023 21:50:12 -0400 Subject: [PATCH] Start adding docstrings and fixing comments --- src/tja2fumen/__init__.py | 11 +++++- src/tja2fumen/constants.py | 13 +++++-- src/tja2fumen/parsers.py | 73 ++++++++++++++++++++++++++++++++------ 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/tja2fumen/__init__.py b/src/tja2fumen/__init__.py index 4963821..d78d2e2 100644 --- a/src/tja2fumen/__init__.py +++ b/src/tja2fumen/__init__.py @@ -3,12 +3,20 @@ import os import sys from tja2fumen.parsers import parse_tja -from tja2fumen.writers import write_fumen from tja2fumen.converters import convert_tja_to_fumen +from tja2fumen.writers import write_fumen from tja2fumen.constants import COURSE_IDS def main(argv=None): + """ + Main entry point for tja2fumen's command line interface. + + Three steps are performed: + 1. Parse TJA into multiple TJACourse objects. Then, for each course: + 2. Convert TJACourse objects into FumenCourse objects. + 3. Write each FumenCourse to its own .bin file. + """ if not argv: argv = sys.argv[1:] @@ -33,6 +41,7 @@ def main(argv=None): def convert_and_write(parsed_course, base_name, single_course=False): + """Process the parsed data for a single TJA course.""" 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 diff --git a/src/tja2fumen/constants.py b/src/tja2fumen/constants.py index d660dd8..3f6d98a 100644 --- a/src/tja2fumen/constants.py +++ b/src/tja2fumen/constants.py @@ -1,3 +1,7 @@ +# Names for branches in diverge songs +BRANCH_NAMES = ("normal", "professional", "master") + +# Types of notes that can be found in TJA files TJA_NOTE_TYPES = { '0': 'Blank', '1': 'Don', @@ -20,6 +24,7 @@ TJA_NOTE_TYPES = { 'I': 'Drumroll', # green roll } +# Types of notes that can be found in fumen files FUMEN_NOTE_TYPES = { 0x1: "Don", # ドン 0x2: "Don2", # ド @@ -49,15 +54,17 @@ FUMEN_NOTE_TYPES = { 0x22: "Unknown13", # ? (Present in some Wii1 songs) 0x62: "Drumroll2" # ? } + +# Invert the dict to go from note type to fumen byte values FUMEN_TYPE_NOTES = {v: k for k, v in FUMEN_NOTE_TYPES.items()} -BRANCH_NAMES = ("normal", "professional", "master") - +# All combinations of difficulty and single/multiplayer type TJA_COURSE_NAMES = [] for difficulty in ['Ura', 'Oni', 'Hard', 'Normal', 'Easy']: for player in ['', 'P1', 'P2']: TJA_COURSE_NAMES.append(difficulty+player) +# Normalize the various fumen course names into 1 name per difficulty NORMALIZE_COURSE = { '0': 'Easy', 'Easy': 'Easy', @@ -72,11 +79,11 @@ NORMALIZE_COURSE = { 'Edit': 'Ura' } +# Map course difficulty to filename IDs (e.g. Oni -> `song_m.bin`) COURSE_IDS = { 'Easy': 'e', 'Normal': 'n', 'Hard': 'h', 'Oni': 'm', 'Ura': 'x', - 'Edit': 'x' } diff --git a/src/tja2fumen/parsers.py b/src/tja2fumen/parsers.py index b0ba132..a94470b 100644 --- a/src/tja2fumen/parsers.py +++ b/src/tja2fumen/parsers.py @@ -14,6 +14,7 @@ from tja2fumen.constants import (NORMALIZE_COURSE, TJA_NOTE_TYPES, def parse_tja(fname_tja): + """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() except UnicodeDecodeError: @@ -28,6 +29,31 @@ def parse_tja(fname_tja): def split_tja_lines_into_courses(lines): + """ + Parse TJA metadata in order to divide TJA lines into separate courses. + + In TJA files, metadata lines are denoted by a colon (':'). These lines + provide general info about the song (BPM, TITLE, OFFSET, etc.). They also + define properties for each course in the song (difficulty, level, etc.). + + This function processes each line of metadata, and assigns the metadata + to TJACourse objects (one for each course). To separate each course, this + function uses the `COURSE:` metadata and any `#START P1/P2` commands, + resulting in the following structure: + + TJASong + ├─ TJACourse (e.g. Ura) + │ ├─ Course metadata (level, balloons, scoreinit, scorediff, etc.) + │ └─ Unparsed data (notes, commands) + ├─ TJACourse (e.g. Ura-P1) + ├─ TJACourse (e.g. Ura-P2) + ├─ TJACourse (e.g. Oni) + ├─ TJACourse (e.g. Hard) + └─ ... + + The data for each TJACourse can then be parsed individually using the + `parse_tja_course_data()` function. + """ parsed_tja = None current_course = '' current_course_cached = '' @@ -35,13 +61,13 @@ def split_tja_lines_into_courses(lines): song_offset = 0 for line in lines: - # Case 1: Header metadata - match_header = re.match(r"^([A-Z]+):(.*)", line) - if match_header: - name_upper = match_header.group(1).upper() - value = match_header.group(2).strip() + # Case 1: Metadata lines + match_metadata = re.match(r"^([A-Z]+):(.*)", line) + if match_metadata: + name_upper = match_metadata.group(1).upper() + value = match_metadata.group(2).strip() - # Global header fields + # Global metadata fields if name_upper in ['BPM', 'OFFSET']: if name_upper == 'BPM': song_bpm = value @@ -50,7 +76,7 @@ def split_tja_lines_into_courses(lines): if song_bpm and song_offset: parsed_tja = TJASong(song_bpm, song_offset) - # Course-specific header fields + # Course-specific metadata fields elif name_upper == 'COURSE': if value not in NORMALIZE_COURSE.keys(): raise ValueError(f"Invalid COURSE value: '{value}'") @@ -90,7 +116,8 @@ def split_tja_lines_into_courses(lines): 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. + # metadata from the course as a whole, so we deepcopy the + # existing course for that difficulty. if name_upper == "START": if value in ["P1", "P2"]: current_course = current_course_cached + value @@ -128,9 +155,33 @@ def split_tja_lines_into_courses(lines): def parse_tja_course_data(course): - # Check if the course has branches or not - has_branches = (True if [d for d in course.data if d.name == 'BRANCHSTART'] - else False) + """ + Parse course data (notes, commands) into a nested song structure. + + The goal of this function is to take raw note and command strings + (e.g. '1020,', '#BPMCHANGE') and parse their values into appropriate + types (e.g. lists, ints, floats, etc.). + + This function also processes measure separators (',') and branch commands + ('#BRANCHSTART`, '#N`, '#E', '#M') to split the data into branches and + measures, resulting in the following structure: + + TJACourse + ├─ TJABranch ('normal') + │ ├─ TJAMeasure + │ │ ├─ TJAData (notes, commands) + │ │ ├─ TJAData + │ │ └─ ... + │ ├─ TJAMeasure + │ ├─ TJAMeasure + │ └─ ... + ├─ TJABranch ('professional') + └─ TJABranch ('master') + + This provides a faithful, easy-to-inspect tree-style representation of the + branches and measures within each course of the .tja file. + """ + has_branches = bool([d for d in course.data if d.name == 'BRANCHSTART']) current_branch = 'all' if has_branches else 'normal' branch_condition = None flag_levelhold = False