diff --git a/.gitignore b/.gitignore index 86c353cf5a..0a27b16f2c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ eclipse run runClient runServer +utilityscripts/implementation/*.txt # Files from Forge MDK forge*changelog.txt diff --git a/utilityscripts/implementation/README.md b/utilityscripts/implementation/README.md new file mode 100644 index 0000000000..36bd30ad13 --- /dev/null +++ b/utilityscripts/implementation/README.md @@ -0,0 +1,17 @@ +Hello! +This is a script for checking asset implementation. + +There are a few things needed in this folder to get this running: +- Create a text document called `assets_directory.txt` and paste in the path to where you have cloned the asset repository, for example `D:\Dev\Cobblemon\Repository\cobblemon-assets` +- Create a text document called `sheet_id.txt` and paste in our spreadsheet's identifier, it's found in the link between `../d/` and `/edit..` + +Both text files and are ignored by git and will not get pushed. + +If you don't have Python already, you will need to install it from here: https://www.python.org/downloads/ + +Then you will need external libraries, open the terminal (search for `cmd` on Windows) and type in the following: `pip install pandas soundfile` +If the terminal says "Requirement already satisfied," but the script doesn't work, try this instead: +- Type in `py -0p`, this will show installed versions and highlight your default with `*` +- Type in `py -3.14 -m pip install pandas soundfile`, replace `-3.14` with your default version + +If there are any issues, or you need help setting your folder up, poke Sterr on Discord. \ No newline at end of file diff --git a/utilityscripts/implementation/check_implementation.py b/utilityscripts/implementation/check_implementation.py new file mode 100644 index 0000000000..2cf31081a1 --- /dev/null +++ b/utilityscripts/implementation/check_implementation.py @@ -0,0 +1,757 @@ +import os +import json +import re +try: import msvcrt +except: print("Could not import msvcrt") +import pandas as pd +import soundfile as sf + +# Quick class to make getch work on all systems +class Getch: + def __init__(self): + try: + self.impl = self._getch_windows + except ImportError: + self.impl = self._getch_unix + + def _getch_windows(self): + return msvcrt.getch() + + def _getch_unix(self): + import sys, tty, termios + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + def __call__(self): return self.impl() +getch = Getch() + +# Colors weeeee +def print_color(color): + if color == "reset": print("\033[0m", end="") + if color == "red": print("\033[31m", end="") + if color == "green": print("\033[32m", end="") + if color == "yellow": print("\033[33m", end="") + if color == "blue": print("\033[34m", end="") + if color == "magenta": print("\033[35m", end="") + if color == "cyan": print("\033[36m", end="") + if color == "white": print("\033[37m", end="") + +# Debug function +def debug(string): + if (log_debug): print(f"\033[33m" + f"{string}" + "\033[0m") + +# Directories +script_dir = os.path.dirname(os.path.abspath(__file__)) +assets_dir_config = os.path.join(script_dir, "assets_directory.txt") +sheet_id_config = os.path.join(script_dir, "sheet_id.txt") + +if not os.path.exists(assets_dir_config): + print(f"Please create assets_directory.txt and paste your directory path inside\nCheck README.md for more information") + getch() + quit() + +with open(assets_dir_config, "r") as f: + assets_dir = os.path.normpath(f.read().strip().replace('"', '')) + +if not os.path.exists(sheet_id_config): + print(f"Please create sheet_id.txt and paste our spreadsheet's identifier inside\nCheck README.md for more information") + getch() + quit() + +with open(sheet_id_config, "r") as f: + sheet_id = os.path.normpath(f.read().strip().replace('"', '')) + +## Script start +print(f"Press either of these keys to check Cobblemon assets implementation") +print(f"► [1] - Basic") +print(f"► [2] - Full") +print(f"► [4] - Debug") +print(f"► [Esc] Exit") +print(f"\n[1] Basic only shows critical issues that will be apparent to players in-game.") +print(f"Issues with Pokémon cries show only if a cry is made and should be implemented.") +print(f"Some sheet disparities will be shown if a Pokémon's implementation status is incorrect.") +print(f"\n[2] Full shows all issues, inconsistencies and disparities.") +print(f"Files are compared one to one with the assets repository.") +print(f"\nThis script is still in development, but can already be used.") +print(f"Report any false positives or issues and feel free to request any features.\n") +log_debug = False +# Set the operation +while True: + input = getch() + if input == b"1": + mode = "basic" + break + elif input == b"2": + mode = "full" + break + elif input == b"4": + mode = "debug" + log_debug = True + break + elif input == b'\x1b': + quit() +debug("Debug enabled, missile inbound") +print(f"Found the Cobblemon assets repository at \"{assets_dir}\"") + + + +## Spreadsheet +# Individual sheet GIDs +gid_implementation = 0 +gid_cries = 1849522968 +gid_models = 1841954062 +# DataFrames +print(f"Fetching spreadsheets, this may take a few seconds...") +try: + df_models = pd.read_csv(f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv&gid={gid_models}") + df_implementation = pd.read_csv(f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv&gid={gid_implementation}") + df_cries = pd.read_csv(f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=csv&gid={gid_cries}") +except: + print("Failed to fetch spreadsheets, make sure the sheet identifier is correct.") + getch() + quit() +print("\n") + +# Clear the name from special characters +def sanitize_name(name): + # Lower case everything + name = name.lower() + # Edge cases + name = name.replace("mr. mime", "mr_mime") # Mr. Mime + name = name.replace("mr. rime", "mr_rime") # Mr. Rime + name = name.replace("mr.rime", "mr_rime") # Also Mr. Rime + name = name.replace("mime jr.", "mime_jr") # Also Mr. Rime + name = re.sub(r"furfrou.*", "furfrou", name) # Furfrou + # fefe + name = name.replace(" [", "_") + name = name.replace("]", "") + # Turn signs into letters + name = name.replace("♀", "f") + name = name.replace("♂", "m") + # Turn m and f into full words + name = re.sub(r"_m$", "_male", name) + name = re.sub(r"_f$", "_female", name) + # éééééééééé + name = name.replace("é", "e") + name = name.replace("\'", "") + name = name.replace("-", "_") + name = name.replace(" ", "_") + # Correct regional names + name = name.replace("alola", "alolan") + name = name.replace("galar", "galarian") + name = name.replace("hisui", "hisuian") + name = name.replace("paldea", "paldean") + # We go + return name + +# Removes regions and special forms from the name +def default_name(name): + # Remove special forms + name = name.replace("_midday", "") + name = name.replace("_midnight", "") + name = name.replace("_dusk", "") + # Remove regions from names + name = name.replace("_alolan", "") + name = name.replace("_galarian", "") + name = name.replace("_hisuian", "") + name = name.replace("_paldean", "") + # Tauros stuff + name = name.replace("paldeancombat", "") + name = name.replace("paldeanblaze", "") + name = name.replace("paldeanaqua", "") + # Turn remove variants + name = name.replace("_male", "") + name = name.replace("_female", "") + # Edge cases + name = re.sub(r"unown.*", "unown", name) # Unown + # We go + return name + +# This is used for unconventional names +def incorrect_region_name(name): + name = name.replace("_alolan", "_alola") + name = name.replace("_galarian", "_galar") + name = name.replace("_hisuian", "_hisui") + name = name.replace("_paldean", "_paldea") + # We go + return name + +# Handle all edge cases with different uses for Pokémon names +def normalize_name(name, mode): + pokemon = name + + ## DIRECTORY + if mode == "directory": + # Remove special forms + name = name.replace("_midday", "") + name = name.replace("_midnight", "") + name = name.replace("_dusk", "") + # Remove regions from names + name = name.replace("_alolan", "") + name = name.replace("_galarian", "") + name = name.replace("_hisuian", "") + name = name.replace("_paldean", "") + # Remove variants + if "unown" not in name: name = name.replace("_male", "") + if "unown" not in name: name = name.replace("_female", "") + ## Edge cases + # Unown + name = re.sub(r"unown.*", "unown", name) + # Toxtricity stuff + name = name.replace("_amped", "") + name = name.replace("_low_key", "") + name = name.replace("_lowkey", "") + # Tauros stuff + if "tauros" in name: return "tauros" + # Gimmighoul + if "gimmighoul" in name: return "gimmighoul" + # Shellos + if "shellos" in pokemon: return "shellos" + # Cherrim + if "cherrim" in pokemon: return "cherrim" + # Remove underscores + name = name.replace('_', '') + # We go + return name + + ## POSER + if mode == "poser": + # Unown + if "unown" in pokemon: + name = name.replace('?', 'question') + name = name.replace('!', 'exclamation') + return name + # Mr. Mime and Mr. Rime + if "mr_mime" in pokemon: return "mrmime" + if "mr_rime" in pokemon: return "mrrime" + # Toxtricity + if "toxtricity_low_key" in pokemon: return "toxtricity_lowkey" + # Gimmighoul Roaming without an underscore apparently + if "gimmighoul_roaming" in pokemon: return "gimmighoulroaming" + # Ho-oh + if "ho_oh" in pokemon: return "hooh" + # Milotic + if "milotic_male" in pokemon: return "milotic" + # Porygon-Z + if "porygon_z" in pokemon: return "porygon-z" + + ## ANIMATION + if mode == "animation": + # Unown + if "unown" in pokemon: return re.sub(r"unown.*", "unown", name) + # Toxtricity + if "toxtricity_low_key" in pokemon: return "toxtricity_lowkey" + # Shellos + if "shellos" in pokemon: return "shellos" + # Jangmo-o line and my depleting sanity + if "jangmo" in pokemon: return "jangmo-o" + if "hakamo" in pokemon: return "hakamo-o" + if "kommo" in pokemon: return "kommo-o" + # Unfezant, only male, because it's not named that on the sheet + if pokemon == "unfezant": return "unfezant_male" + # Milotic + if "milotic" in pokemon: return "milotic" + # Porygon-Z + if "porygon_z" in pokemon: return "porygon-z" + + ## CRY + if mode == "cry": + # Kadabra and Alakazam + if "kadabra_" in pokemon: return "kadabra" + if "alakazam_" in pokemon: return "alakazam" + # Meowstic + if "meowstic" in pokemon: return "meowstic" + # Unown + name = re.sub(r"unown.*", "unown", name) + # Toxtricity + if "toxtricity_low_key" in pokemon: return "toxtricity_lowkey" + # Gimmighoul Roaming without an underscore apparently + if "gimmighoul_roaming" in pokemon: return "gimmighoulroaming" + # Shellos + if "shellos" in pokemon: return "shellos" + # Jangmo-o line + if "jangmo" in pokemon: return "jangmoo" + if "hakamo" in pokemon: return "hakamoo" + if "kommo" in pokemon: return "kommoo" + # Ho-oh + if "ho_oh" in pokemon: return "hooh" + # Unfezant, only male, because it's not named that on the sheet + if "unfezant" in pokemon: return "unfezant" + # Milotic + if "milotic" in pokemon: return "milotic" + # Pyroar + if "pyroar" in pokemon: return "pyroar" + # Porygon-Z + if "porygon_z" in pokemon: return "porygonz" + + return name + + +# These Pokémon use their base form for implementation, but have their own row on the sheet +use_default_name = { + "weezing_galarian", + "farfetchd_galarian" +} + +def check_implementation(status, name, dex, gen, model_status): + debug(f" Checking Implementation for {name} ({sanitize_name(name)})") + name = sanitize_name(name) + dex = f"{dex:>04}" + + # Check if a poser exists, if not, fallback to Kotlin and check that + poser_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/bedrock/pokemon/posers/{dex}_{normalize_name(name,'directory')}/{normalize_name(name,'poser')}.json") + if poser_path: debug(f" Looking for \"{poser_path}\"") + if os.path.isfile(poser_path): + status["imp_implemented"] = True + debug(" Found JSON poser") + + # If not found, try a different, incorrectly named file + else: + poser_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/bedrock/pokemon/posers/{dex}_{normalize_name(name,'directory')}/{incorrect_region_name(name)}.json") + if poser_path: debug(f" Looking for a poser with an incorrect region name instead, \"{poser_path}\"") + if os.path.isfile(poser_path): + status["imp_implemented"] = True + debug(" Found JSON poser") + status["imp_unconventional_name"] = os.path.basename(poser_path) + debug(" JSON poser is using an incorrect region name (eg. \"_alola\" instead of \"_alolan\")") + else: + debug(" Could not find any JSON poser") + + # If still not found the poser, try searching for Kotlin + if not status.get("imp_implemented"): + kotlin_poser_name = name.title().replace("_", "") + kotlin_poser_directory = os.path.join(script_dir, "../..", f"common/src/main/kotlin/com/cobblemon/mod/common/client/render/models/blockbench/pokemon/") + debug(f" Searching for Kotlin poser \"{kotlin_poser_name}Model.kt\" in \"{kotlin_poser_directory}\"") + + # Check if a Kotlin poser exists in all gen folders + if os.path.exists(kotlin_poser_directory): + for root, dirs, files in os.walk(kotlin_poser_directory): + if f"{kotlin_poser_name}Model.kt" in files: + # Set the new poser path + poser_path = os.path.join(root, f"{kotlin_poser_name}Model.kt") + # Debug + debug(f" Found Kotlin poser in \"{poser_path}\"") + break + + # If poser exists, flag the proper status + if os.path.isfile(poser_path): + status["imp_implemented"] = True + status["imp_using_kotlin_poser"] = True + else: + if model_status == "Implemented": + status["sht_erroneous_implemented_flag"] = True + debug(" Could not find any poser, not flagging the Pokémon as implemented") + debug(" Pokémon is not implemented, but flagged as such on the assets sheet") + return + else: + debug(" Could not find any poser, not flagging the Pokémon as implemented") + + # Check implementation against the Models spreadsheet + debug(" Checking if the implementation status on the Models spreadsheet matches...") + if ("imp_implemented" in status) and (model_status != "Implemented"): + if model_status == "Work in Progress": status["sht_erroneous_work_in_progress_flag"] = True + elif model_status == "Not Started": status["sht_erroneous_not_started_flag"] = True + elif model_status == "Needs Approval": status["sht_erroneous_needs_approval_flag"] = True + elif model_status == "Needs Implementation": status["sht_erroneous_needs_implementation_flag"] = True + elif model_status == "Needs Rework": status["sht_erroneous_needs_rework_flag"] = True + elif model_status == "Needs Help": status["sht_erroneous_needs_help_flag"] = True + debug(" Found paperwork mismatch") + else: debug(" Implementation status and mod implementation match") + + +def scan_animation(status, name, gen, dex): + debug(" Scanning the animations") + name = sanitize_name(name) + dex = f"{dex:>04}" + + if name in use_default_name: name = default_name(name) + + ani_mod_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/bedrock/pokemon/animations/{dex}_{normalize_name(name,'directory')}/{normalize_name(name,'animation')}.animation.json") + ani_repo_path = os.path.join(assets_dir, f"blockbench/pokemon/gen{gen}/{dex}_{normalize_name(name,'directory')}/{normalize_name(name,'animation')}.animation.json") + + # Check if files exist + if not os.path.isfile(ani_mod_path): + # Check if an incorrectly named animation exists first + ani_mod_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/bedrock/pokemon/animations/{dex}_{normalize_name(name,'directory')}/{incorrect_region_name(name)}.animation.json") + if not os.path.isfile(ani_mod_path): + status["ani_file_missing"] = True + debug(f" Animation file does not exist, could not find \"{ani_mod_path}\"") + return + else: + status["ani_unconventional_name"] = os.path.basename(ani_mod_path) + debug(f" Could not find the correct animation, but found one using an unconventional name") + + debug(f" Found \"{ani_mod_path}\"") + + with open(ani_mod_path, "r") as file: + ani_data = json.load(file) + + # Check for issues with the cry animation + if status.get("ani_unconventional_name", False): cry_animation = ani_data.get("animations", {}).get(f"animation.{incorrect_region_name(normalize_name(name,'animation'))}.cry") + else: cry_animation = ani_data.get("animations", {}).get(f"animation.{normalize_name(name,'animation')}.cry") + + if (not cry_animation) or cry_animation == None: + status["ani_cry_animation_missing"] = True + debug(f" Could not find the cry animation") + else: + debug(f" Scanning \"animation.{normalize_name(name,'animation')}.cry\" for potential issues with the cry") + cry_animation_sound_effects = cry_animation.get("sound_effects") + if (not cry_animation_sound_effects) or cry_animation_sound_effects == None: + status["ani_cry_keyframe_missing"] = True + debug(f" Could not find the cry keyframe") + else: + for key, value in cry_animation_sound_effects.items(): + if not isinstance(value, dict): + debug(f" Omitting {key}") + continue + effect = value.get("effect") + if (not effect) or effect == None: + status["ani_cry_keyframe_missing"] = True + debug(f" Could not find the cry keyframe in {cry_animation}") + elif effect != f"pokemon.{normalize_name(name,'cry')}.cry": + status["ani_cry_keyframe_invalid"] = effect + debug(f" Found an incorrectly named cry keyframe ({effect})") + else: debug(f" Found no issues with the cry implementation") + + # Check for disparity with repository assets + debug(f" Checking parity with the assets repository") + if not os.path.isfile(ani_repo_path): + debug(f" Could not find \"{ani_repo_path}") + else: + with open(ani_repo_path, "r") as file: + repo_data = json.load(file) + if ani_data != repo_data: + status["ani_file_repo_disparity"] = True + debug(f" Animations files do not match") + else: debug(f" Animation files are identical") + +def scan_poser(status, name, gen, dex): + debug(" Scanning the poser") + name = sanitize_name(name) + dex = f"{dex:>04}" + + # Return if no JSON poser exists + if status.get("imp_using_kotlin_poser", False): + debug(f" Cannot check a Kotlin poser") + return + + # Get the poser + imp_mod_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/bedrock/pokemon/posers/{dex}_{normalize_name(name,'directory')}/{normalize_name(name,'poser')}.json") + if not os.path.isfile(imp_mod_path): + debug(f" Poser file does not exist, could not find \"{imp_mod_path}\"") + return + else: debug(f" Found \"{imp_mod_path}\"") + + # Get the animation + ani_mod_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/bedrock/pokemon/animations/{dex}_{normalize_name(name,'directory')}/{normalize_name(name,'animation')}.animation.json") + if not os.path.isfile(ani_mod_path): + debug(f" Animation file does not exist, could not find \"{ani_mod_path}\"") + return + else: + debug(f" Found \"{ani_mod_path}\"") + + # Check for special animation implementation + possible_targets = ["faint", "cry", "recoil", "physical", "special", "air_physical", "air_special", "status"] + with open(ani_mod_path, "r", encoding="utf-8") as file: + anim_data = json.load(file) + existing_animations = anim_data.get("animations", {}).keys() + targets = [t for t in possible_targets if f"animation.{name}.{t}" in existing_animations] + if targets: + debug(f" Found animations {targets} and checking if they are implemented in the poser...") + with open(imp_mod_path, "r", encoding="utf-8") as file: + imp_data = json.load(file) + animations = imp_data.get("animations", {}).keys() + missing_animations = [a for a in targets if a not in animations] + if missing_animations: + status["imp_missing_animations"] = missing_animations + debug(f" Could not find {missing_animations} in the poser") + else: + debug(f" All aforementioned animations are implemented in the poser") + +def scan_sound(status, name, gen, dex): + debug(" Scanning audio stuff") + name = sanitize_name(name) + dex = f"{dex:>04}" + aud_mod_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/sounds/pokemon/{normalize_name(name,'directory')}/{normalize_name(name,'cry')}_cry.ogg") + aud_repo_path = os.path.join(assets_dir, f"sounds/pokemon/gen{gen}/{dex}_{normalize_name(name,'directory')}/{normalize_name(name,'cry')}_cry.ogg") + + # Check if files exist + debug(f" Searching for cry audio files...") + if not os.path.isfile(aud_mod_path): + debug(f" Could not find mod's audio file in \"{aud_mod_path}\"") + status["aud_cry_file_missing"] = True + no_mod_cry_very_sad = True + else: + debug(f" Found cry in the mod in \"{aud_mod_path}\"") + no_mod_cry_very_sad = False + if not os.path.isfile(aud_repo_path): + debug(f" Could not find repository's audio file \"{aud_repo_path}\"") + no_repo_cry_very_sad = True + else: + debug(f" Found cry on the repository in \"{aud_repo_path}\"") + no_repo_cry_very_sad = False + + # If cries exist, check if the files are the same + if not (no_mod_cry_very_sad or no_repo_cry_very_sad): + # Read the audio data and sample rate + mod_audio_data, mod_audio_samplerate = sf.read(aud_mod_path) + repo_audio_data, repo_audio_samplerate = sf.read(aud_repo_path) + + # Check if sample rates match + if mod_audio_samplerate != repo_audio_samplerate: + status["aud_cry_file_disparity"] = True + debug(f" Sample rate of audio files does not match") + else: + debug(f" Sample rate of audio files matches") + + # Check if the Pokémon is in the sounds.json + sounds_json_path = os.path.join(script_dir, "../..", f"common/src/main/resources/assets/cobblemon/sounds.json") + if not os.path.isfile(sounds_json_path): + debug(f" Somehow did not find the sounds.json idk how Sterr can mess up a script this bad") + else: + with open(sounds_json_path) as file: + sounds_json_data = json.load(file) + sound_event = f"pokemon.{normalize_name(name,'cry')}.cry" + if sound_event in sounds_json_data: + debug(f" Found \"{sound_event}\" in sounds.json") + else: + # Check if an incorrectly named file exists instead + unconventional_sound_event = f"pokemon.{incorrect_region_name(name)}.cry" + if unconventional_sound_event in sounds_json_data: + debug(f" Found \"{unconventional_sound_event}\" in sounds.json") + status["aud_cry_sound_event_unconventional_name"] = os.path.basename(unconventional_sound_event) + else: + status["aud_cry_sound_event_missing"] = True + debug(f" Could not find \"{sound_event}\" in sounds.json") + + + + +exclude_from_checks = { + "Substitute": "(Always skipped)", + "Unown": "(Ignore this! Only Unown with letters are checked and implemented)", + "Steelix": "(Specific check is a work in progress)", + "Frillish": "(Specific check is a work in progress)", + "Jellicent": "(Specific check is a work in progress)", + "Rotom": "(Specific check is a work in progress)", + "Mime Jr.": "(Specific check is a work in progress)", + "Gastrodon": "(Specific check is a work in progress)", + "Basculin": "(Specific check is a work in progress)", + "Darmanitan [Zen]": "(Specific check is a work in progress)", + "Wishiwashi": "(Specific check is a work in progress)", + "Aegislash": "(Specific check is a work in progress)", + "Clauncher": "(Specific check is a work in progress)", + "Clawitzer": "(Specific check is a work in progress)", + "Eiscue": "(Specific check is a work in progress)", + "Palafin": "(Specific check is a work in progress)", + "Dudunsparce": "(Specific check is a work in progress)", + "Gimmighoul": "(Specific check is a work in progress)" +} + +# These are Pokémon whose gender difference implementation separated their poser +# Does not include Pokémon already split into [Male] and [Female] on the assets sheet +gender_variant_pokemon ={ + "Kadabra", + "Alakazam", + "Milotic", # (Male is missing from the poser name, there is only "milotic" and "milotic_female") + "Pyroar", + "Indeedee", + "Oinkologne" +} + +# Exclude specific keywords, only Mega and G-Max for now +exclude_keywords = { + "Mega", + "G-Max" +} + +## Main loop +for i, row in df_implementation.iterrows(): + # Skip first three rows + if i < 3: continue + + debug(f"Checking {row['Name']}...") + + # Skip keywords + for k in exclude_keywords: + if re.search(rf"\b{k}\b", (df_implementation.loc[i, "Name"])): + debug(f" Keyword \"{k}\" is excluded, skipping!\n") + skip = True + break + else: skip = False + if skip: continue + + # Skip if Pokémon is excluded + if df_implementation.loc[i, "Name"] in exclude_from_checks: + debug(f" {row['Name']} is excluded, skipping!\n") + continue + for c in exclude_from_checks: + if re.search(rf"\b{c}\b", (df_implementation.loc[i, "Name"])): + debug(f" Pokémon \"{c}\" is excluded, skipping!\n") + skip = True + break + else: skip = False + if skip: continue + + ## Set up variables + # Gen + gen = df_implementation.loc[i,"Gen"] + # Dex + dex = df_implementation.loc[i,"Dex"] + # Name + name = df_implementation.loc[i,"Name"] + + # Split the checks if Pokémon has variants with separate implementation + if name in gender_variant_pokemon: + debug(f" {name} will be split into \"{name} [Male]\" and \"{name} [Female]\"") + variants = [f"{name} [Male]", f"{name} [Female]"] + else: + variants = [name] + + for variant in variants: + name = variant + + # Clear error flags + try: status.clear() + except: status = {} + status = {} + + # Check implementation status, skip if unimplemented in the mod + check_implementation(status, name, dex, gen, df_models.loc[i,"Model Status"]) + + if not status.get("imp_implemented", True): + debug(" Pokémon is not implemented in the mod, skipping!") + continue + + # Cry sound status + if (df_cries.loc[i, "Cry"] == "Approved"): cry_available = True + else: cry_available = False + + # Scan for issues + if "imp_implemented" in status: + scan_poser(status, name, gen, dex) + scan_animation(status, name, gen, dex) + scan_sound(status, name, gen, dex) + + # Delete non-issues when done + del status["imp_implemented"] + + # Exclude checks from basic mode + if mode == "basic": + if not cry_available: + if "ani_cry_animation_missing" in status: del status["ani_cry_animation_missing"] + if "ani_cry_keyframe_missing" in status: del status["ani_cry_keyframe_missing"] + if "ani_cry_keyframe_invalid" in status: del status["ani_cry_keyframe_invalid"] + if "aud_cry_file_disparity" in status: del status["aud_cry_file_disparity"] + if "aud_cry_file_missing" in status: del status["aud_cry_file_missing"] + if "aud_cry_sound_event_missing" in status: del status["aud_cry_sound_event_missing"] + + if "sht_erroneous_work_in_progress_flag" in status: del status["sht_erroneous_work_in_progress_flag"] + if "sht_erroneous_not_started_flag" in status: del status["sht_erroneous_not_started_flag"] + if "sht_erroneous_needs_approval_flag" in status: del status["sht_erroneous_needs_approval_flag"] + if "sht_erroneous_needs_implementation_flag" in status: del status["sht_erroneous_needs_implementation_flag"] + if "sht_erroneous_needs_rework_flag" in status: del status["sht_erroneous_needs_rework_flag"] + if "sht_erroneous_needs_help_flag" in status: del status["sht_erroneous_needs_help_flag"] + + if "imp_unconventional_name" in status: del status["imp_unconventional_name"] + if "imp_using_kotlin_poser" in status: del status["imp_using_kotlin_poser"] + + if "ani_file_repo_disparity" in status: del status["ani_file_repo_disparity"] + + if "ani_unconventional_name" in status: del status["ani_unconventional_name"] + if "aud_cry_sound_event_unconventional_name" in status: del status["aud_cry_sound_event_unconventional_name"] + + + # Print out issues + if any(status.values()): + print_color("white") + print(f"✘ {name} has the following issues:") + if len(variants)>1: print(f"(The male/female split is present only in implementation, not on the spreadsheet)") + + name = sanitize_name(name) + + # Sheet Issues + print_color("red") + if status.get("sht_erroneous_implemented_flag", False): print(f" ► Pokémon is not implemented, but flagged as such on the Models sheet") + if status.get("sht_erroneous_work_in_progress_flag", False): print(f" ► Flagged as Work in Progress on the Models sheet, while implemented in the mod") + if status.get("sht_erroneous_not_started_flag", False): print(f" ► Flagged as Not Started on the Models sheet, while implemented in the mod") + if status.get("sht_erroneous_needs_approval_flag", False): print(f" ► Flagged as Needs Approval on the Models sheet, while implemented in the mod") + if status.get("sht_erroneous_needs_implementation_flag", False): print(f" ► Flagged as Needs Implementation on the Models sheet, while implemented in the mod") + if status.get("sht_erroneous_needs_rework_flag", False): print(f" ► Flagged as Needs Rework on the Models sheet, while implemented in the mod") + if status.get("sht_erroneous_needs_help_flag", False): print(f" ► Flagged as Needs Help on the Models sheet, while implemented in the mod") + if status.get("sht_erroneous_implemented_flag", False) or status.get("sht_erroneous_work_in_progress_flag", False) or status.get("sht_erroneous_not_started_flag", False) or status.get("sht_erroneous_needs_approval_flag", False) or status.get("sht_erroneous_needs_implementation_flag", False) or status.get("sht_erroneous_needs_rework_flag", False) or status.get("sht_erroneous_needs_help_flag", False): + print(f" ▲ Check notes and ask approvers, before changing anything on the Models sheet") + + # Implementation Issues + print_color("green") + if status.get("imp_unconventional_name", False): + print(f" ► Poser is using an unconventional name \"{status['imp_unconventional_name']}\"") + region_name = status["imp_unconventional_name"] + if "_galar" in region_name: print(f" ▲ The convention is to use \"_galarian\" instead of \"_galar\"") + if "_alola" in region_name: print(f" ▲ The convention is to use \"_alolan\" instead of \"_alola\"") + if "_hisui" in region_name: print(f" ▲ The convention is to use \"_hisuian\" instead of \"_hisui\"") + if "_paldea" in region_name: print(f" ▲ The convention is to use \"_paldean\" instead of \"_paldea\"") + + if status.get("imp_using_kotlin_poser", False): print(f" ► Still using a Kotlin poser, the script cannot check for poser issues") + if status.get("imp_missing_animations", False): print(f" ► Found animations in \"{normalize_name(name,'animation')}.animation.json\" that are not implemented in the poser: {status['imp_missing_animations']}") + + # Animation Issues + print_color("cyan") + if status.get("ani_unconventional_name", False): + print(f" ► Animation is using an unconventional name \"{status['ani_unconventional_name']}\"") + region_name = status["ani_unconventional_name"] + if "_galar" in region_name: print(f" ▲ The convention is to use \"_galarian\" instead of \"_galar\"") + elif "_alola" in region_name: print(f" ▲ The convention is to use \"_alolan\" instead of \"_alola\"") + elif "_hisui" in region_name: print(f" ▲ The convention is to use \"_hisuian\" instead of \"_hisui\"") + elif "_paldea" in region_name: print(f" ▲ The convention is to use \"_paldean\" instead of \"_paldea\"") + else: print(f" ▲ Whatever triggered this is likely incorrect, report this pls") + + + if status.get("ani_file_missing", False): print(f" ► Animation file does not exist, could not find \"pokemon/animations/{dex:>04}_{normalize_name(name,'directory')}/{normalize_name(name,'animation')}.animation.json\"") + if status.get("ani_file_repo_disparity", False): print(f" ► Animation file \"{sanitize_name(normalize_name(name, 'animation'))}.animation.json\" does not match the Assets Repository") + + if status.get("ani_cry_animation_missing", False): print(f" ► Cry animation is missing, could not find \"animation.{normalize_name(name,'animation')}.cry\"") + if status.get("ani_cry_keyframe_missing", False): print(f" ► Cry keyframe is missing in \"animation.{normalize_name(name,'animation')}.cry\"") + if status.get("ani_cry_keyframe_invalid", False): print(f" ► Cry keyframe in \"animation.{normalize_name(name,'animation')}.cry\" has an incorrect name, expected \"pokemon.{normalize_name(name,'animation')}.cry\" and found \"{status['ani_cry_keyframe_invalid']}\"") + if (status.get("ani_cry_animation_missing", False) or status.get("ani_cry_keyframe_missing", False)) and not cry_available: + print(f" ▲ Cry sound doesn't exist, but a cry animation should always be implemented at least as a placeholder") + + # Audio issues + print_color("magenta") + if status.get("aud_cry_sound_event_unconventional_name", False): + print(f" ► Sound event is using an unconventional name \"{status['aud_cry_sound_event_unconventional_name']}\"") + region_name = status["aud_cry_sound_event_unconventional_name"] + if "_galar" in region_name: print(f" ▲ The convention is to use \"_galarian\" instead of \"_galar\"") + if "_alola" in region_name: print(f" ▲ The convention is to use \"_alolan\" instead of \"_alola\"") + if "_hisui" in region_name: print(f" ▲ The convention is to use \"_hisuian\" instead of \"_hisui\"") + if "_paldea" in region_name: print(f" ▲ The convention is to use \"_paldean\" instead of \"_paldea\"") + + if status.get("aud_cry_sound_event_missing", False): print(f" ► Could not find a sound event \"pokemon.{normalize_name(name,'cry')}.cry\" in sounds.json") + if status.get("aud_cry_file_disparity", False): print(f" ► Replace \"{normalize_name(name,'cry')}_cry.ogg\" with the updated file from the asset repository") + if status.get("aud_cry_file_missing", False): print(f" ► Cry sound is missing, at least an empty \"{normalize_name(name,'cry')}_cry.ogg\" is needed") + + print_color("reset") + if not log_debug: print("\n") + if log_debug: print("\n") + + + +# We done +print("\nWe done!\n") +print("The following Pokémon are excluded from this script and must be checked manually:") +for c, v in exclude_from_checks.items(): + print(f"► {c} {v}") +print("\nPress [Esc] to exit") +while True: + input = getch() + if input == b'\x1b': + quit() + + +