2023-06-25 07:02:31 +02:00
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import zipfile
|
|
|
|
import re
|
2023-06-29 07:20:46 +02:00
|
|
|
import glob
|
2023-06-25 07:02:31 +02:00
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
2023-06-30 02:34:08 +02:00
|
|
|
from tja2fumen import main as convert
|
|
|
|
from tja2fumen.parsers import readFumen
|
|
|
|
from tja2fumen.constants import COURSE_IDS, NORMALIZE_COURSE, simpleHeaders, byte_strings
|
2023-06-25 07:02:31 +02:00
|
|
|
|
|
|
|
|
2023-06-29 07:39:55 +02:00
|
|
|
@pytest.mark.parametrize('id_song', [
|
2023-07-10 04:54:45 +02:00
|
|
|
pytest.param('gimcho'),
|
Add support for `#BRANCHSTART p,999,999` and `#SECTION` commands via `imcanz.tja` (#30)
This PR adds a new chart to the test suite (`imcanz.tja`) that uses
`p,0,0`, `p,999,999` _and_ `#SECTION` commands:
- `p,0,0`: Forces the chart into the Master branch (since it's
impossible to fail a 0% accuracy requirement)
- `p,999,999`: Forces the chart into the Normal branch (since it's
impossible to pass a 999% accuracy requirement)
- `#SECTION`: Resets accuracy values for notes and drumrolls on the next
measure. (In practice, this just means that the branch condition is
repeated on the next measure, at least according to the official fumen I
have.)
Note: Only the Oni and Hard difficulties have actually been added to the
test suite. The Normal and Easy charts were too broken to easily match
the official fumens. They will need a lot of work to fix charting
errors, so I'm leaving them commented out for now.
Fixes #27.
2023-07-10 01:56:57 +02:00
|
|
|
pytest.param('imcanz'),
|
2023-07-09 00:04:24 +02:00
|
|
|
pytest.param('clsca'),
|
2023-07-06 05:53:48 +02:00
|
|
|
pytest.param('linda'),
|
2023-07-05 22:03:41 +02:00
|
|
|
pytest.param('senpac'),
|
2023-07-02 06:00:36 +02:00
|
|
|
pytest.param('butou5'),
|
2023-07-01 23:41:23 +02:00
|
|
|
pytest.param('hol6po'),
|
2023-06-29 07:39:55 +02:00
|
|
|
pytest.param('mikdp'),
|
2023-06-30 01:46:57 +02:00
|
|
|
pytest.param('ia6cho'),
|
2023-06-29 07:39:55 +02:00
|
|
|
])
|
2023-06-29 07:20:46 +02:00
|
|
|
def test_converted_tja_vs_cached_fumen(id_song, tmp_path, entry_point):
|
2023-06-25 07:02:31 +02:00
|
|
|
# Define the testing directory
|
|
|
|
path_test = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
|
|
|
|
# Define the working directory
|
|
|
|
path_temp = os.path.join(tmp_path, id_song)
|
|
|
|
os.mkdir(path_temp)
|
|
|
|
|
|
|
|
# Copy input TJA to working directory
|
|
|
|
path_tja = os.path.join(path_test, "data", f"{id_song}.tja")
|
|
|
|
path_tja_tmp = os.path.join(path_temp, f"{id_song}.tja")
|
|
|
|
shutil.copy(path_tja, path_tja_tmp)
|
|
|
|
|
|
|
|
# Convert TJA file to fumen files
|
2023-06-29 07:20:46 +02:00
|
|
|
if entry_point == "python-api":
|
|
|
|
convert(argv=[path_tja_tmp])
|
|
|
|
elif entry_point == "python-cli":
|
|
|
|
os.system(f"tja2fumen {path_tja_tmp}")
|
|
|
|
elif entry_point == "exe":
|
|
|
|
exe_path = glob.glob(os.path.join(os.path.split(path_test)[0], "dist", "*.exe"))[0]
|
|
|
|
os.system(f"{exe_path} {path_tja_tmp}")
|
|
|
|
|
|
|
|
# Fetch output fumen paths
|
|
|
|
paths_out = glob.glob(os.path.join(path_temp, "*.bin"))
|
|
|
|
assert paths_out, f"No bin files generated in {path_temp}"
|
2023-06-29 21:40:05 +02:00
|
|
|
order = "xmhne" # Ura Oni -> Oni -> Hard -> Normal -> Easy
|
|
|
|
paths_out = sorted(paths_out, key=lambda s: [order.index(c) if c in order else len(order) for c in s])
|
2023-06-25 07:02:31 +02:00
|
|
|
|
|
|
|
# Extract cached fumen files to working directory
|
|
|
|
path_binzip = os.path.join(path_test, "data", f"{id_song}.zip")
|
|
|
|
path_bin = os.path.join(path_temp, "ca_bins")
|
|
|
|
with zipfile.ZipFile(path_binzip, 'r') as zip_ref:
|
|
|
|
zip_ref.extractall(path_bin)
|
|
|
|
|
|
|
|
# Compare cached fumen with generated fumen
|
|
|
|
for path_out in paths_out:
|
|
|
|
# Difficulty introspection to help with debugging
|
|
|
|
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)
|
2023-06-29 21:39:39 +02:00
|
|
|
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)
|
2023-06-25 07:02:31 +02:00
|
|
|
# 1. Check song headers
|
2023-07-12 03:30:55 +02:00
|
|
|
checkValidHeader(co_song.headerPadding+co_song.headerMetadata, strict=True)
|
|
|
|
checkValidHeader(ca_song.headerPadding+ca_song.headerMetadata)
|
2023-06-25 07:02:31 +02:00
|
|
|
# 2. Check song metadata
|
|
|
|
assert_song_property(co_song, ca_song, 'order')
|
2023-07-12 03:30:55 +02:00
|
|
|
assert_song_property(co_song, ca_song, 'hasBranches')
|
2023-06-25 07:02:31 +02:00
|
|
|
assert_song_property(co_song, ca_song, 'scoreInit')
|
|
|
|
assert_song_property(co_song, ca_song, 'scoreDiff')
|
|
|
|
# 3. Check measure data
|
2023-07-12 03:30:55 +02:00
|
|
|
for i_measure in range(max([len(co_song.measures), len(ca_song.measures)])):
|
2023-06-29 21:40:38 +02:00
|
|
|
# NB: We could assert that len(measures) is the same for both songs, then iterate through zipped measures.
|
|
|
|
# But, if there is a mismatched number of measures, we want to know _where_ it occurs. So, we let the
|
|
|
|
# comparison go on using the max length of both songs until something else fails.
|
2023-07-12 03:30:55 +02:00
|
|
|
co_measure = co_song.measures[i_measure]
|
|
|
|
ca_measure = ca_song.measures[i_measure]
|
2023-06-25 07:02:31 +02:00
|
|
|
# 3a. Check measure metadata
|
|
|
|
assert_song_property(co_measure, ca_measure, 'bpm', i_measure, abs=0.01)
|
Simplify how `#BPMCHANGE`s adjust the `fumenOffset`
Before:
- If there was a #BPMCHANGE, tana's adjustment would be applied to _the current measure's duration_. This would result in very strange looking, impossible durations (e.g. negative)
- Additionally, this method required us to look ahead to the _next_ measure to determine the adjustment.
- Finally, it required us to keep track of two different durations: measureDurationBase (unadjusted) and measureDuration (adjusted)
After:
- Instead, we now leave the measureDuration as purely as "unadjusted".
- Then, we apply the "BPMCHANGE adjustment" _only_ when computing the fumenOffset start of the next measure.
- This requires us to keep track of both the start *and* end fumenOffset (where end = start + duration)
- However, this greatly simplifies and clarifies the code, since we:
- No longer need to "look ahead" to the next measure to compute the offset adjustment.
- No longer need to keep track of two different measureDurations (adjusted/unadjusted)
- Only need to work with a single pair of measures at a time (measureFumen, measureFumenPrev)
- The measure duration (and fumenOffsetEnd) times are now more comprehensible, since any negative offsets are only applied to the fumenOffsetStart value of the next measure.
After:
2023-07-09 20:08:55 +02:00
|
|
|
assert_song_property(co_measure, ca_measure, 'fumenOffsetStart', i_measure, abs=0.15)
|
2023-06-25 07:02:31 +02:00
|
|
|
assert_song_property(co_measure, ca_measure, 'gogo', i_measure)
|
|
|
|
assert_song_property(co_measure, ca_measure, 'barline', i_measure)
|
2023-07-01 23:41:23 +02:00
|
|
|
assert_song_property(co_measure, ca_measure, 'branchInfo', i_measure)
|
2023-06-25 07:02:31 +02:00
|
|
|
# 3b. Check measure notes
|
|
|
|
for i_branch in ['normal', 'advanced', 'master']:
|
2023-07-12 03:30:55 +02:00
|
|
|
co_branch = co_measure.branches[i_branch]
|
|
|
|
ca_branch = ca_measure.branches[i_branch]
|
2023-06-25 07:02:31 +02:00
|
|
|
# NB: We check for branching before checking speed as fumens store speed changes even for empty branches
|
2023-07-12 03:30:55 +02:00
|
|
|
if co_branch.length == 0:
|
2023-06-25 07:02:31 +02:00
|
|
|
continue
|
|
|
|
assert_song_property(co_branch, ca_branch, 'speed', i_measure, i_branch)
|
2023-06-30 01:43:48 +02:00
|
|
|
# NB: We could assert that len(notes) is the same for both songs, then iterate through zipped notes.
|
|
|
|
# But, if there is a mismatched number of notes, we want to know _where_ it occurs. So, we let the
|
|
|
|
# comparison go on using the max length of both branches until something else fails.
|
2023-07-12 03:30:55 +02:00
|
|
|
for i_note in range(max([co_branch.length, ca_branch.length])):
|
|
|
|
co_note = co_branch.notes[i_note]
|
|
|
|
ca_note = ca_branch.notes[i_note]
|
|
|
|
assert_song_property(co_note, ca_note, 'note_type', i_measure, i_branch, i_note, func=normalize_type)
|
2023-07-09 00:34:03 +02:00
|
|
|
assert_song_property(co_note, ca_note, 'pos', i_measure, i_branch, i_note, abs=0.1)
|
2023-07-01 23:41:23 +02:00
|
|
|
# NB: Drumroll duration doesn't always end exactly on a beat. Plus, TJA charters often eyeball
|
|
|
|
# drumrolls, leading them to be often off by a 1/4th/8th/16th/32th/etc. These charting errors
|
|
|
|
# are fixable, but tedious to do when writing tests. So, I've added a try/except so that they
|
|
|
|
# can be checked locally with a breakpoint when adding new songs, but so that fixing every
|
|
|
|
# duration-related chart error isn't 100% mandatory.
|
|
|
|
try:
|
|
|
|
assert_song_property(co_note, ca_note, 'duration', i_measure, i_branch, i_note, abs=25.0)
|
|
|
|
except AssertionError:
|
|
|
|
pass
|
2023-07-12 03:30:55 +02:00
|
|
|
if ca_note.note_type not in ["Balloon", "Kusudama"]:
|
2023-06-25 07:02:31 +02:00
|
|
|
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)
|
2023-06-25 18:14:56 +02:00
|
|
|
# NB: 'item' still needs to be implemented: https://github.com/vivaria/tja2fumen/issues/17
|
2023-06-25 07:02:31 +02:00
|
|
|
# assert_song_property(co_note, ca_note, 'item', i_measure, i_branch, i_note)
|
|
|
|
|
2023-06-25 19:19:50 +02:00
|
|
|
|
2023-06-29 21:45:01 +02:00
|
|
|
def assert_song_property(converted_obj, cached_obj, prop, measure=None, branch=None, note=None, func=None, abs=None):
|
2023-06-29 20:47:12 +02:00
|
|
|
# NB: TJA parser/converter uses 0-based indexing, but TJA files use 1-based indexing.
|
|
|
|
# So, we increment 1 in the error message to more easily identify problematic lines in TJA files.
|
|
|
|
msg_failure = f"'{prop}' mismatch"
|
|
|
|
msg_failure += f": measure '{measure+1}'" if measure is not None else ""
|
|
|
|
msg_failure += f", branch '{branch}'" if branch is not None else ""
|
|
|
|
msg_failure += f", note '{note+1}'" if note is not None else ""
|
2023-07-12 03:30:55 +02:00
|
|
|
converted_val = converted_obj.__getattribute__(prop)
|
|
|
|
cached_val = cached_obj.__getattribute__(prop)
|
2023-06-29 20:47:12 +02:00
|
|
|
if func:
|
2023-07-12 03:30:55 +02:00
|
|
|
assert func(converted_val) == func(cached_val), msg_failure
|
2023-06-29 20:47:12 +02:00
|
|
|
elif abs:
|
2023-07-12 03:30:55 +02:00
|
|
|
assert converted_val == pytest.approx(cached_val, abs=abs), msg_failure
|
2023-06-29 20:47:12 +02:00
|
|
|
else:
|
2023-07-12 03:30:55 +02:00
|
|
|
assert converted_val == cached_val, msg_failure
|
2023-06-29 20:47:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
def normalize_type(note_type):
|
|
|
|
return re.sub(r'[0-9]', '', note_type)
|
|
|
|
|
|
|
|
|
2023-06-25 19:33:28 +02:00
|
|
|
def checkValidHeader(headerBytes, strict=False):
|
2023-06-25 19:19:50 +02:00
|
|
|
# Fumen headers should contain 512 bytes.
|
|
|
|
assert len(headerBytes) == 512
|
2023-06-25 19:33:28 +02:00
|
|
|
# The header for fumens can be split into 2 groups: The first 432 bytes (padding), and the last 80 bytes (metadata).
|
2023-06-25 19:19:50 +02:00
|
|
|
headerPadding = headerBytes[:432]
|
|
|
|
headerMetadata = headerBytes[-80:]
|
|
|
|
|
|
|
|
# 1. Check the header's padding bytes for several possible combinations
|
2023-06-25 19:33:28 +02:00
|
|
|
# 1a. These simple headers (12-byte substrings repeated 36 times) are used for many Gen2 systems (AC, Wii, etc.)
|
2023-06-25 19:19:50 +02:00
|
|
|
cond1 = headerPadding in simpleHeaders
|
|
|
|
# 1b. Starting with Gen3, they began using unique headers for every song. (3DS and PSPDX are the big offenders.)
|
|
|
|
# - They seem to be some random combination of b_x00 + one of the non-null byte substrings.
|
|
|
|
# - To avoid enumerating every combination of 432 bytes, we do a lazy check instead.
|
|
|
|
cond2 = (byte_strings['x00'] in headerPadding and
|
|
|
|
any(b in headerPadding for b in
|
|
|
|
[byte_strings[key] for key in ['431', '432', '433', '434', 'V1', 'V2', 'V3']]))
|
|
|
|
# 1c. The PS4 song 'wii5op' is a special case: It throws in this odd 'g1' string in combo with 2 other substrings.
|
|
|
|
cond3 = (byte_strings['g1'] in headerPadding and
|
|
|
|
any(b in headerPadding for b in [byte_strings[key] for key in ['431', 'V2']]))
|
|
|
|
# Otherwise, this is some unknown header we haven't seen before.
|
|
|
|
assert cond1 or cond2 or cond3, "Header padding bytes do not match expected fumen byte substrings."
|
|
|
|
|
|
|
|
# 2. Check the header's metadata bytes
|
|
|
|
for idx, val in enumerate(headerMetadata):
|
2023-07-01 23:41:23 +02:00
|
|
|
# Whether the song has branches
|
|
|
|
if idx == 0:
|
|
|
|
assert val in [0, 1], f"Expected 0/1 at position '{idx}', got '{val}' instead."
|
|
|
|
|
2023-06-25 19:19:50 +02:00
|
|
|
# 0. Unknown
|
|
|
|
# Notes:
|
|
|
|
# * Breakdown of distribution of different byte combinations:
|
2023-07-01 23:41:23 +02:00
|
|
|
# - 5832/7482 charts: [0, 0, 0] (Most platforms)
|
|
|
|
# - 386/7482 charts: [151, 68, 0]
|
|
|
|
# - 269/7482 charts: [1, 57, 0]
|
|
|
|
# - 93/7482 charts: [64, 153, 0]
|
2023-06-25 19:19:50 +02:00
|
|
|
# - And more...
|
|
|
|
# - After this, we see a long tail of hundreds of different unique byte combinations.
|
|
|
|
# * Games with the greatest number of unique byte combinations:
|
|
|
|
# - VitaMS: 258 unique byte combinations
|
|
|
|
# - iOSU: 164 unique byte combinations
|
|
|
|
# - Vita: 153 unique byte combinations
|
2023-07-01 23:41:23 +02:00
|
|
|
# Given that most platforms use the values (0, 0, 0), and unique values are very platform-specific,
|
|
|
|
# I'm going to stick with (0, 0, 0) bytes when it comes to converting TJA files to fumens.
|
|
|
|
elif idx in [1, 2, 3]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 0, f"Expected 0 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
pass
|
2023-06-25 19:19:50 +02:00
|
|
|
|
|
|
|
# 1. <padding>
|
|
|
|
# Notes: These values are ALWAYS (16, 39), for every valid fumen.
|
|
|
|
elif idx == 4:
|
|
|
|
assert val == 16, f"Expected 16 at position '{idx}', got '{val}' instead."
|
|
|
|
elif idx == 5:
|
|
|
|
assert val == 39, f"Expected 39 at position '{idx}', got '{val}' instead."
|
|
|
|
|
|
|
|
# 2. Difficulty
|
|
|
|
# Notes:
|
|
|
|
# * Breakdown of distribution of different byte combinations:
|
|
|
|
# - 1805/7482 charts: [112, 23] (Easy)
|
|
|
|
# - 3611/7482 charts: [88, 27] (Normal, Hard)
|
|
|
|
# - 2016/7482 charts: [64, 31] (Oni, Ura)
|
|
|
|
# * In other words, all 5 difficulties map to only three different byte-pairs across all valid fumens.
|
|
|
|
elif idx == 8:
|
|
|
|
assert val in [88, 64, 112], f"Expected 88/64/112 at position '{idx}', got '{val}' instead."
|
|
|
|
elif idx == 9:
|
|
|
|
assert val in [27, 31, 23], f"Expected 27/31/23 at position '{idx}', got '{val}' instead."
|
|
|
|
|
2023-06-26 03:54:12 +02:00
|
|
|
# 6. Soul gauge bytes
|
2023-06-25 19:19:50 +02:00
|
|
|
# Notes:
|
2023-06-26 03:54:12 +02:00
|
|
|
# * These bytes determine how quickly the soul gauge should increase
|
|
|
|
# * The precise mapping between n_notes and byte values is complex, and depends on difficulty/stars.
|
|
|
|
# - See also: https://github.com/vivaria/tja2fumen/issues/14
|
2023-07-11 14:07:30 +02:00
|
|
|
# * Generally speaking, though, the higher the number of notes, then:
|
|
|
|
# - The lower that bytes 12/16 will go.
|
|
|
|
# - The higher that byte 21 will go.
|
|
|
|
# * Also, most of the time [13, 17] will be 0 and [21, 22, 23] will be 255.
|
|
|
|
# * However, a very small number of songs (~30) have values different from 0/255.
|
2023-06-26 03:54:12 +02:00
|
|
|
# - This applies to Easy/Normal songs with VERY few notes (<30).
|
2023-07-11 14:07:30 +02:00
|
|
|
# * Bytes 12/16 will go above 255 and wrap around back to >=0, incrementing bytes 13/17 by one.
|
|
|
|
# * Byte 20 will go below and wrap around back to <=255, decrementing byte 21 by one.
|
|
|
|
elif idx == 12:
|
|
|
|
assert 1 <= val <= 255
|
|
|
|
elif idx == 13:
|
|
|
|
assert val in [0, 1, 2, 3]
|
|
|
|
elif idx == 16:
|
|
|
|
assert 1 <= val <= 255
|
|
|
|
elif idx == 17:
|
|
|
|
assert val in [0, 1, 2, 3]
|
2023-06-26 03:54:12 +02:00
|
|
|
elif idx == 20:
|
|
|
|
assert 1 <= val <= 255
|
|
|
|
elif idx == 21:
|
|
|
|
assert val in [253, 254, 255]
|
|
|
|
elif idx in [22, 23]:
|
|
|
|
assert val == 255
|
2023-06-25 19:19:50 +02:00
|
|
|
|
|
|
|
# 7. <padding>
|
|
|
|
# Notes:
|
|
|
|
# * For the vast majority (99%) of charts, bytes 21, 22, and 23 have the values (1, 1, 1)
|
|
|
|
# * For a small minority of charts (~100), one or both of bytes 30/34 will be 0 instead of 1
|
|
|
|
# Given that most platforms use the values (1, 1, 1), and unique values are very platform-specific,
|
2023-06-25 19:33:28 +02:00
|
|
|
# I'm going to stick with (1, 1, 1) when it comes to converting TJA files to fumens.
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx == 26:
|
|
|
|
assert val == 1, f"Expected 1 at position '{idx}', got '{val}' instead."
|
|
|
|
elif idx in [30, 34]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 1, f"Expected 1 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
assert val in [1, 0], f"Expected 1/0 at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
|
|
|
|
# 8. Unknown
|
|
|
|
# Notes:
|
|
|
|
# * For the vast majority (99%) of charts, bytes (28, 29) and (32, 33) have the values (0, 0)
|
|
|
|
# * But, for some games (Gen3Arcade, 3DS), unique values will be stored in these bytes.
|
|
|
|
# Given that most platforms use the values (0, 0), and unique values are very platform-specific,
|
2023-06-25 19:33:28 +02:00
|
|
|
# I'm going to stick with (0, 0) when it comes to converting TJA files to fumens.
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx in [28, 29]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 0, f"Expected 0 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
pass
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx in [32, 33]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 0, f"Expected 0 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
pass
|
2023-06-25 19:19:50 +02:00
|
|
|
|
2023-06-25 19:33:28 +02:00
|
|
|
# 9. <padding>
|
2023-06-25 19:19:50 +02:00
|
|
|
# Notes:
|
|
|
|
# * For the vast majority (99%) of charts, bytes (36, 40, 48) and (52, 56, 50) have the values (20, 10, 1)
|
|
|
|
# * For a small minority of charts (~45), these values can be 0,1,2 instead.
|
|
|
|
# Given that most platforms use the values (20, 10, 1), and unique values are very platform-specific,
|
2023-06-25 19:33:28 +02:00
|
|
|
# I'm going to stick with (20, 10, 0) when it comes to converting TJA files to fumens.
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx in [36, 52]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 20, f"Expected 20 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
assert val in [20, 0, 1, 2], f"Expected 20 (or 0,1,2) at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx in [40, 56]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 10, f"Expected 10 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
assert val in [10, 0, 1], f"Expected 10 (or 0,1) at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx in [48, 60]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 1, f"Expected 1 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
# NB: See below for an explanation for why '255' is included for byte 60
|
|
|
|
assert val in [1, 0, 255], f"Expected 1 (or 0) at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
|
2023-06-25 19:33:28 +02:00
|
|
|
# 10. <padding>
|
2023-06-25 19:19:50 +02:00
|
|
|
# Notes:
|
|
|
|
# * For the vast majority (99%) of charts, bytes (61, 62, 63) have the values (0, 0, 0)
|
|
|
|
# * However, for iOS and iOSU charts (144 total), bytes (60, 61, 62, 63) are (255, 255, 255, 255) instead.
|
|
|
|
# Given that most platforms use the values (0, 0, 0), and unique values are very platform-specific,
|
2023-06-25 19:33:28 +02:00
|
|
|
# I'm going to stick with (0, 0, 0) when it comes to converting TJA files to fumens.
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx in [61, 62, 63]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 0, f"Expected 0/255 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
assert val in [0, 255], f"Expected 0/255 at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
|
2023-06-25 19:33:28 +02:00
|
|
|
# 11. <padding>
|
2023-06-25 19:19:50 +02:00
|
|
|
# Notes:
|
|
|
|
# * Breakdown of distribution of different byte combinations:
|
|
|
|
# - 5809/7482 charts: (30, 30, 20)
|
|
|
|
# - 1577/7482 charts: (30, 30, 0)
|
|
|
|
# - 41/7482 charts: (0, 0, 0)
|
|
|
|
# - 3/7482 charts: (1, 0, 0)
|
|
|
|
# - 2/7482 charts: (0, 0, 20)
|
|
|
|
# Given that most platforms use the values (30, 30, 20), and unique values are very platform-specific,
|
|
|
|
# I'm going to ignore the unique bytes when it comes to converting TJA files to fumens.
|
|
|
|
elif idx in [64, 68]:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 30, f"Expected 30 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
assert val in [30, 0, 1], f"Expected 30 (or 0,1) at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
elif idx == 72:
|
2023-06-25 19:33:28 +02:00
|
|
|
if strict:
|
|
|
|
assert val == 20, f"Expected 20 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
assert val in [20, 0], f"Expected 20 (or 0) at position '{idx}', got '{val}' instead."
|
2023-06-25 19:19:50 +02:00
|
|
|
|
2023-06-25 19:33:28 +02:00
|
|
|
# 12. Difficulty (Gen2) and ???? (Gen3)
|
2023-06-25 19:19:50 +02:00
|
|
|
# Notes:
|
|
|
|
# * In Gen2 charts (AC, Wii), these values would be one of 4 different byte combinations.
|
|
|
|
# * These values correspond to the difficulty of the song (no Uras in Gen2, hence 4 values):
|
|
|
|
# - [192, 42, 12] (Easy)
|
|
|
|
# - [92, 205, 23] (Normal)
|
|
|
|
# - [8, 206, 31] (Hard)
|
|
|
|
# - [288, 193, 44] (Oni)
|
|
|
|
# * However, starting in Gen3 (AC, console), these bytes were given unique per-song, per-chart values.
|
|
|
|
# - In total, Gen3 contains 6449 unique combinations of bytes (with some minor overlaps between games).
|
2023-06-25 19:33:28 +02:00
|
|
|
# For TJA conversion, I plan to just stick with one set of values (78, 97, 188) -- also used by tja2bin.exe.
|
|
|
|
elif idx == 76:
|
|
|
|
if strict:
|
|
|
|
assert val == 78, f"Expected 78 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
pass
|
|
|
|
elif idx == 77:
|
|
|
|
if strict:
|
|
|
|
assert val == 97, f"Expected 20 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
pass
|
|
|
|
elif idx == 78:
|
|
|
|
if strict:
|
|
|
|
assert val == 188, f"Expected 20 at position '{idx}', got '{val}' instead."
|
|
|
|
else:
|
|
|
|
pass
|
2023-06-25 19:19:50 +02:00
|
|
|
|
2023-06-25 19:33:28 +02:00
|
|
|
# 13. Empty bytes
|
2023-06-25 19:19:50 +02:00
|
|
|
else:
|
|
|
|
assert val == 0, f"Expected 0 at position '{idx}', got '{val}' instead."
|
|
|
|
|