2021-05-05 00:33:43 +00:00
|
|
|
import yaml
|
|
|
|
|
|
|
|
# Assumes this is ran from the root of the repository
|
2021-11-06 03:33:27 +00:00
|
|
|
file_path = "./bin/resources/GameIndex.yaml"
|
2021-05-05 00:33:43 +00:00
|
|
|
|
|
|
|
# These settings have to be manually kept in sync with the emulator code unfortunately.
|
|
|
|
# up to date validation should ALWAYS be provided via the application!
|
|
|
|
allowed_game_options = [
|
|
|
|
"name",
|
|
|
|
"region",
|
|
|
|
"compat",
|
|
|
|
"roundModes",
|
|
|
|
"clampModes",
|
|
|
|
"gameFixes",
|
2022-03-03 14:32:26 +00:00
|
|
|
"gsHWFixes",
|
2021-05-05 00:33:43 +00:00
|
|
|
"speedHacks",
|
|
|
|
"memcardFilters",
|
|
|
|
"patches",
|
|
|
|
]
|
|
|
|
allowed_round_modes = ["eeRoundMode", "vuRoundMode"]
|
|
|
|
allowed_clamp_modes = ["eeClampMode", "vuClampMode"]
|
|
|
|
allowed_game_fixes = [
|
|
|
|
"FpuMulHack",
|
|
|
|
"FpuNegDivHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"GoemonTlbHack",
|
|
|
|
"SoftwareRendererFMVHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
"SkipMPEGHack",
|
|
|
|
"OPHFlagHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"EETimingHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
"DMABusyHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"GIFFIFOHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
"VIFFIFOHack",
|
|
|
|
"VIF1StallHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"VuAddSubHack",
|
2021-06-08 18:07:19 +00:00
|
|
|
"IbitHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"VUSyncHack",
|
2021-09-29 09:50:49 +00:00
|
|
|
"VUOverflowHack",
|
2022-03-14 22:47:23 +00:00
|
|
|
"XGKickHack",
|
2021-05-05 00:33:43 +00:00
|
|
|
]
|
2022-03-03 14:32:26 +00:00
|
|
|
allowed_gs_hw_fixes = [
|
|
|
|
"autoFlush",
|
|
|
|
"conservativeFramebuffer",
|
|
|
|
"cpuFramebufferConversion",
|
|
|
|
"disableDepthSupport",
|
|
|
|
"wrapGSMem",
|
|
|
|
"preloadFrameData",
|
2022-03-18 13:06:26 +00:00
|
|
|
"disablePartialInvalidation",
|
2022-03-03 14:32:26 +00:00
|
|
|
"textureInsideRT",
|
|
|
|
"alignSprite",
|
|
|
|
"mergeSprite",
|
|
|
|
"wildArmsHack",
|
|
|
|
"pointListPalette",
|
|
|
|
"mipmap",
|
|
|
|
"trilinearFiltering",
|
|
|
|
"skipDrawStart",
|
|
|
|
"skipDrawEnd",
|
|
|
|
"halfBottomOverride",
|
|
|
|
"halfPixelOffset",
|
|
|
|
"roundSprite",
|
|
|
|
"texturePreloading",
|
|
|
|
]
|
|
|
|
gs_hw_fix_ranges = {
|
|
|
|
"mipmap": (0, 2),
|
|
|
|
"trilinearFiltering": (0, 2),
|
|
|
|
"skipDrawStart": (0, 100000),
|
|
|
|
"skipDrawEnd": (0, 100000),
|
|
|
|
"halfPixelOffset": (0, 3),
|
|
|
|
"roundSprite": (0, 2),
|
|
|
|
}
|
2021-10-23 19:52:41 +00:00
|
|
|
allowed_speed_hacks = ["mvuFlagSpeedHack", "InstantVU1SpeedHack", "MTVUSpeedHack"]
|
2021-05-05 00:33:43 +00:00
|
|
|
# Patches are allowed to have a 'default' key or a crc-32 key, followed by
|
|
|
|
allowed_patch_options = ["author", "content"]
|
|
|
|
|
|
|
|
issue_list = []
|
|
|
|
|
|
|
|
|
|
|
|
def is_hex_number(string):
|
|
|
|
try:
|
|
|
|
int(string, 16)
|
|
|
|
return True
|
2021-05-05 01:16:05 +00:00
|
|
|
except Exception:
|
2021-05-05 00:33:43 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def validate_string_option(serial, key, value):
|
2021-05-05 01:16:05 +00:00
|
|
|
if not isinstance(value, str):
|
2021-05-05 00:33:43 +00:00
|
|
|
issue_list.append("[{}]: '{}' must be a string".format(serial, key))
|
|
|
|
|
|
|
|
|
|
|
|
def validate_int_option(serial, key, value, low, high):
|
2021-05-05 01:16:05 +00:00
|
|
|
if not isinstance(value, int) or (value < low or value > high):
|
2021-05-05 00:33:43 +00:00
|
|
|
issue_list.append(
|
|
|
|
"[{}]: '{}' must be an int and between {}-{} (inclusive)".format(
|
|
|
|
serial, key, low, high
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
def validate_list_of_strings(serial, key, value):
|
|
|
|
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
|
|
issue_list.append("[{}]: '{}' must be a list of strings".format(serial, key))
|
|
|
|
|
|
|
|
|
2021-05-05 00:33:43 +00:00
|
|
|
def validate_valid_options(serial, key, value, allowed_values):
|
|
|
|
if value not in allowed_values:
|
|
|
|
issue_list.append("[{}]: Invalid '{}' option [{}]".format(serial, key, value))
|
|
|
|
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
def validate_clamp_round_modes(serial, key, value, allowed_values):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append("[{}]: '{}' must be a valid object".format(serial, key))
|
|
|
|
return
|
|
|
|
for mode_key, mode_value in value.items():
|
|
|
|
validate_valid_options(serial, key, mode_key, allowed_values)
|
|
|
|
validate_int_option(serial, key, mode_value, 0, 3)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_game_fixes(serial, key, value):
|
|
|
|
if not isinstance(value, list):
|
|
|
|
issue_list.append(
|
|
|
|
"[{}]: 'gameFixes' must be a list of valid gameFixes".format(serial)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
for gamefix in value:
|
|
|
|
validate_valid_options(serial, key, gamefix, allowed_game_fixes)
|
|
|
|
|
|
|
|
|
2022-03-03 14:32:26 +00:00
|
|
|
def validate_gs_hw_fix_value(serial, key, value):
|
|
|
|
low, high = 0, 1
|
|
|
|
if key in gs_hw_fix_ranges:
|
|
|
|
low, high = gs_hw_fix_ranges[key]
|
|
|
|
validate_int_option(serial, key, value, low, high)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_gs_hw_fixes(serial, key, value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append("[{}]: 'gsHWFixes' must be a valid object".format(serial))
|
|
|
|
return
|
|
|
|
for fix, fix_value in value.items():
|
|
|
|
validate_valid_options(serial, key, fix, allowed_gs_hw_fixes)
|
|
|
|
validate_gs_hw_fix_value(serial, fix, fix_value)
|
|
|
|
|
|
|
|
# skipdraw range must have end >= start
|
|
|
|
skip_draw_start = value["skipDrawStart"] if "skipDrawStart" in value else 0
|
|
|
|
skip_draw_end = value["skipDrawEnd"] if "skipDrawEnd" in value else 0
|
|
|
|
if isinstance(skip_draw_start, int) and isinstance(skip_draw_end, int) and skip_draw_end < skip_draw_start:
|
|
|
|
issue_list.append("[{}]: skipDrawStart({}) must be greater or equal to skipDrawEnd({})".format(
|
|
|
|
serial, skip_draw_start, skip_draw_end))
|
|
|
|
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
def validate_speed_hacks(serial, key, value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append("[{}]: 'speedHacks' must be a valid object".format(serial))
|
|
|
|
return
|
|
|
|
for speedhack, speedhack_value in value.items():
|
|
|
|
validate_valid_options(serial, key, speedhack, allowed_speed_hacks)
|
|
|
|
validate_int_option(serial, speedhack, speedhack_value, 0, 1)
|
|
|
|
|
|
|
|
|
|
|
|
def validate_patches(serial, key, value):
|
|
|
|
if not isinstance(value, dict):
|
|
|
|
issue_list.append(
|
|
|
|
"[{}]: 'patches' must be valid mapping of CRC32 -> Patch Objects".format(
|
|
|
|
serial
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
for crc, patch in value.items():
|
|
|
|
if crc != "default" and not is_hex_number(str(crc)):
|
|
|
|
issue_list.append(
|
|
|
|
"[{}]: Patches must either be key'd with 'default' or a valid CRC-32 Hex String".format(
|
|
|
|
serial
|
|
|
|
)
|
|
|
|
)
|
|
|
|
continue
|
|
|
|
for patch_option, option_value in patch.items():
|
|
|
|
validate_valid_options(serial, key, patch_option, allowed_patch_options)
|
|
|
|
if patch_option == "author":
|
|
|
|
validate_string_option(serial, patch_option, option_value)
|
|
|
|
if patch_option == "content":
|
|
|
|
validate_string_option(serial, patch_option, option_value)
|
|
|
|
|
|
|
|
|
|
|
|
# pylint:disable=unnecessary-lambda
|
|
|
|
option_validation_handlers = {
|
|
|
|
"name": (lambda serial, key, value: validate_string_option(serial, key, value)),
|
|
|
|
"region": (lambda serial, key, value: validate_string_option(serial, key, value)),
|
|
|
|
"compat": (
|
|
|
|
lambda serial, key, value: validate_int_option(serial, key, value, 0, 6)
|
|
|
|
),
|
|
|
|
"roundModes": (
|
|
|
|
lambda serial, key, value: validate_clamp_round_modes(
|
|
|
|
serial, key, value, allowed_round_modes
|
|
|
|
)
|
|
|
|
),
|
|
|
|
"clampModes": (
|
|
|
|
lambda serial, key, value: validate_clamp_round_modes(
|
|
|
|
serial, key, value, allowed_clamp_modes
|
|
|
|
)
|
|
|
|
),
|
|
|
|
"gameFixes": (lambda serial, key, value: validate_game_fixes(serial, key, value)),
|
2022-03-03 14:32:26 +00:00
|
|
|
"gsHWFixes": (lambda serial, key, value: validate_gs_hw_fixes(serial, key, value)),
|
2021-05-05 01:16:05 +00:00
|
|
|
"speedHacks": (lambda serial, key, value: validate_speed_hacks(serial, key, value)),
|
|
|
|
"memcardFilters": (
|
|
|
|
lambda serial, key, value: validate_list_of_strings(serial, key, value)
|
|
|
|
),
|
|
|
|
"patches": (lambda serial, key, value: validate_patches(serial, key, value)),
|
|
|
|
}
|
|
|
|
|
2021-05-05 00:33:43 +00:00
|
|
|
print("Opening {}...".format(file_path))
|
|
|
|
with open(file_path) as f:
|
|
|
|
try:
|
|
|
|
print("Attempting to parse GameDB file...")
|
|
|
|
gamedb = yaml.load(f, Loader=yaml.FullLoader)
|
|
|
|
except Exception as err:
|
2021-05-05 01:16:05 +00:00
|
|
|
print(err)
|
2021-05-05 00:33:43 +00:00
|
|
|
print(
|
|
|
|
"Unable to parse GameDB. Exiting, verify that the file indeed is valid YAML."
|
|
|
|
)
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
print("File loaded successfully, validating schema...")
|
|
|
|
progress_counter = 0
|
|
|
|
for serial, game_options in gamedb.items():
|
|
|
|
progress_counter = progress_counter + 1
|
|
|
|
if progress_counter % 500 == 0 or progress_counter >= len(gamedb.items()):
|
|
|
|
print(
|
|
|
|
"[{}/{}] Processing GameDB Entries...".format(
|
|
|
|
progress_counter, len(gamedb.items())
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Check for required values
|
|
|
|
if not "name" in game_options.keys():
|
|
|
|
issue_list.append("[{}]: 'name' is a required value".format(serial))
|
|
|
|
if not "region" in game_options.keys():
|
|
|
|
issue_list.append("[{}]: 'region' is a required value".format(serial))
|
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
# Check the options
|
2021-05-05 00:33:43 +00:00
|
|
|
for key, value in game_options.items():
|
|
|
|
if key not in allowed_game_options:
|
|
|
|
issue_list.append("[{}]: Invalid option [{}]".format(serial, key))
|
2021-05-05 01:16:05 +00:00
|
|
|
continue
|
2021-05-05 00:33:43 +00:00
|
|
|
|
2021-05-05 01:16:05 +00:00
|
|
|
if key in option_validation_handlers:
|
|
|
|
option_validation_handlers[key](serial, key, value)
|
2021-05-05 00:33:43 +00:00
|
|
|
|
|
|
|
if len(issue_list) > 0:
|
|
|
|
print("Issues found during validation:")
|
|
|
|
print(*issue_list, sep="\n")
|
|
|
|
exit(1)
|
|
|
|
else:
|
|
|
|
print("GameDB Validated Successfully!")
|
|
|
|
exit(0)
|