mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-11-05 07:48:53 -05:00
404 lines
22 KiB
Python
404 lines
22 KiB
Python
import json
|
|
import os
|
|
import re
|
|
from io import StringIO
|
|
import pandas as pd
|
|
from mutagen.oggvorbis import OggVorbis
|
|
from cobblemon_drops_csv_to_json import download_spreadsheet_data
|
|
from scriptutils import printCobblemonHeader, print_list_filtered, print_cobblemon_script_description, \
|
|
print_cobblemon_script_footer, print_problems_and_paths, print_warning, sanitize_pokemon, print_separator
|
|
|
|
DEFAULT_POKEMON_MODELS_PATH = "../common/src/main/kotlin/com/cobblemon/mod/common/client/render/models/blockbench/pokemon/"
|
|
DEFAULT_POKEMON_POSER_PATH = "../common/src/main/resources/assets/cobblemon/bedrock/pokemon/posers/"
|
|
|
|
# Download the CSV file
|
|
ASSETS_CSV_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vSTisDTkJvV0GzKV1zKjAPdMAQAO7znWxjEWXrM1gZPUVmsTU91oy54aJGMpbbvOqAOg03ER1wl7eeA/pub?gid=0&single=true&output=csv"
|
|
|
|
|
|
def main(print_missing_models=True, print_missing_animations=True):
|
|
"""
|
|
cobblemon_cry_checker.py checks the consistency between the cry status of Pokémon in a CSV file and the actual
|
|
presence of their cry audio files in a specific directory. It also checks for the presence of the corresponding
|
|
model and animation files for each Pokémon. The function prints out any inconsistencies it finds.
|
|
|
|
Parameters:
|
|
print_missing_models (bool): If set to True, the function will print out the names of Pokémon for which model files are missing.
|
|
print_missing_animations (bool): If set to True, the function will print out the names of Pokémon for which animation files are missing.
|
|
|
|
Author: Waldleufer
|
|
"""
|
|
|
|
printCobblemonHeader()
|
|
|
|
scriptname = "Cobblemon Cry Checker"
|
|
scriptdescription = "This script checks the consistency between the cry status of Pokémon in a CSV file and the actual presence of their cry audio files in a specific directory. It also checks for the presence of the corresponding model and animation files for each Pokémon. The function prints out any inconsistencies it finds. WARNING: G-Max and Mega Pokémon are not checked yet."
|
|
|
|
print_cobblemon_script_description(scriptname, scriptdescription)
|
|
# Load the CSV data into a DataFrame
|
|
csv_data = download_spreadsheet_data(ASSETS_CSV_URL)
|
|
csv_file = StringIO(csv_data)
|
|
# Load the data into a DataFrame while specifying the types of the columns, skip NaN values
|
|
df = pd.read_csv(csv_file, dtype={0: str, 1: str, 2: str, 53: str}, skip_blank_lines=True)
|
|
|
|
# Extract the required columns
|
|
gen_numbers = df.iloc[3:, 0] # Column A
|
|
dex_numbers = df.iloc[3:, 1] # Column B
|
|
pokemon_names = df.iloc[3:, 2] # Column C
|
|
pokemon_in_game = df.iloc[3:, 9] # Column I
|
|
cries_on_repo = df.iloc[3:, 54] # Column BC
|
|
cries_in_game = df.iloc[3:, 55] # Column BD [Cry Audio | In-Game]
|
|
|
|
# Initialize lists for false positives and negatives
|
|
false_positives = []
|
|
false_negatives = []
|
|
implemented_and_not_marked = []
|
|
invalid_model_files = []
|
|
invalid_animation_files = []
|
|
all_warnings_combined = []
|
|
pokemon_ready_to_be_added_list = []
|
|
|
|
# Iterate over the DataFrame rows
|
|
for pokemon_name, gen_number, dex_number, cry_in_game, this_pokemon_in_game, this_cry_on_repo in zip(pokemon_names,
|
|
gen_numbers,
|
|
dex_numbers,
|
|
cries_in_game,
|
|
pokemon_in_game,
|
|
cries_on_repo):
|
|
if this_pokemon_in_game == "❌":
|
|
continue
|
|
cry_in_game = str(cry_in_game).strip() # remove whitespace
|
|
cry_on_repo = str(this_cry_on_repo).strip() # remove whitespace
|
|
# make the first letter of the Pokémon name uppercase and remove all spaces
|
|
sanitized_pokemon_name_lower = sanitize_pokemon(pokemon_name)
|
|
# if [ ] are present in the name, remove them and anything in between
|
|
sanitized_pokemon_name_lower = sanitized_pokemon_name_lower.split("[")[0]
|
|
sanitized_pokemon_name = sanitized_pokemon_name_lower.capitalize()
|
|
pokemon_form = pokemon_name.split("[")[1].split("]")[0].lower() if "[" in pokemon_name else None
|
|
|
|
# Construct the path to the audio file
|
|
if pokemon_form:
|
|
audio_file_path = f"../common/src/main/resources/assets/cobblemon/sounds/pokemon/{sanitized_pokemon_name_lower}/{sanitized_pokemon_name_lower}_{pokemon_form}_cry.ogg"
|
|
else:
|
|
audio_file_path = f"../common/src/main/resources/assets/cobblemon/sounds/pokemon/{sanitized_pokemon_name_lower}/{sanitized_pokemon_name_lower}_cry.ogg"
|
|
|
|
checks = {
|
|
'on_repo': False,
|
|
'uses_poser': False,
|
|
'poser_correct': False,
|
|
'import_correct': False,
|
|
'override_correct': False,
|
|
'cry_in_game': False,
|
|
'audio_file_exists': False,
|
|
'sound_effects_and_keyframes': False,
|
|
}
|
|
|
|
# Hardcoded odd cases that should be ignored
|
|
if pokemon_name == "Aegislash [Shield]" or pokemon_name == "Aegislash [Blade]" or pokemon_name == "Basculin [Red-Striped]" or pokemon_name == "Oinkologne":
|
|
continue
|
|
|
|
# Construct the path to the model file
|
|
## Hello, commented this out because it probably doesn't do anything, but Wald can scold me if it was important - Yours truly, Sterr
|
|
# if pokemon_name == "Rattata [Alolan]":
|
|
# print(pokemon_name)
|
|
|
|
# Try to convert gen_number to an integer and handle ValueError
|
|
try:
|
|
if pokemon_form:
|
|
# Check if the form is a regional form or F or M, if not, use the default model file
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}gen{int(gen_number.strip())}/{sanitized_pokemon_name}{pokemon_form.capitalize()}Model.kt"
|
|
if pokemon_form == "F" or pokemon_form == "M":
|
|
if pokemon_form == "F":
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}gen{int(gen_number.strip())}/{sanitized_pokemon_name}FemaleModel.kt"
|
|
elif pokemon_form == "M":
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}gen{int(gen_number.strip())}/{sanitized_pokemon_name}MaleModel.kt"
|
|
if not os.path.isfile(model_file_path):
|
|
# Fallback for models that share one model file and all forms that are no regional forms
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}gen{int(gen_number.strip())}/{sanitized_pokemon_name}Model.kt"
|
|
elif pokemon_form in ["hisuian", "alolan", "galarian", "valencian", "paldean"]:
|
|
# search for the model file with the form name in all gen folders
|
|
model_pokemon_path = DEFAULT_POKEMON_MODELS_PATH
|
|
for gen_folder in os.listdir(model_pokemon_path):
|
|
if os.path.isdir(os.path.join(model_pokemon_path, gen_folder)):
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}{gen_folder}/{sanitized_pokemon_name}{pokemon_form.capitalize()}Model.kt"
|
|
if not os.path.isfile(model_file_path):
|
|
model_file_path = ""
|
|
continue
|
|
break
|
|
if not model_file_path:
|
|
model_file_path = f"../gen*/{sanitized_pokemon_name}{pokemon_form.capitalize()}Model.kt"
|
|
else:
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}gen{int(gen_number.strip())}/{sanitized_pokemon_name}Model.kt"
|
|
else:
|
|
model_file_path = f"{DEFAULT_POKEMON_MODELS_PATH}gen{int(gen_number.strip())}/{sanitized_pokemon_name}Model.kt"
|
|
except ValueError:
|
|
all_warnings_combined.append(
|
|
f"⚠️ Warning: Invalid gen_number for pokemon {pokemon_name}. Skipping this line.")
|
|
continue # Construct the path to the animation.json file
|
|
dex_number = str(dex_number).zfill(4) # prepend with 0s to make it 4 digits long
|
|
animation_file_path = f"../common/src/main/resources/assets/cobblemon/bedrock/pokemon/animations/{dex_number}_{sanitized_pokemon_name_lower.lower()}/{sanitized_pokemon_name_lower.lower()}.animation.json"
|
|
|
|
# Check if the audio file exists
|
|
checks["audio_file_exists"] = os.path.isfile(audio_file_path)
|
|
# if the length of the audio file is smaller or equals to 0.11725 seconds, it is considered empty
|
|
if checks["audio_file_exists"]:
|
|
audio = OggVorbis(audio_file_path)
|
|
if audio.info.length <= 0.11725:
|
|
checks["audio_file_exists"] = False
|
|
|
|
# Check the cry status
|
|
if cry_in_game == "✔":
|
|
checks["cry_in_game"] = True
|
|
|
|
# If the cry isn't supposed to exist whatsoever, it'll be skipped
|
|
if cry_on_repo == "🛇":
|
|
continue
|
|
|
|
animations = None
|
|
|
|
# Check if the Model.kt file exists and contains the required import and cryAnimation override
|
|
json_poser_path = os.path.join(DEFAULT_POKEMON_POSER_PATH, dex_number + "_" + sanitized_pokemon_name_lower, sanitized_pokemon_name_lower + ".json")
|
|
if os.path.isfile(model_file_path):
|
|
content = read_file_ignore_comments(model_file_path)
|
|
if "import com.cobblemon.mod.common.client.render.models.blockbench.pokemon.CryProvider" in content:
|
|
checks["import_correct"] = True
|
|
if f'override val cryAnimation = CryProvider {{ _, _ -> bedrockStateful("{sanitized_pokemon_name_lower}", "cry") }}' in content:
|
|
checks["override_correct"] = True
|
|
if f'animations["cry"] = "q.bedrock_stateful(\'{sanitized_pokemon_name_lower}\', \'cry\')".asExpressionLike()' in content:
|
|
checks["override_correct"] = True
|
|
checks["import_correct"] = True
|
|
if not checks["override_correct"]:
|
|
# it might be an elaborate cry with multiple animations
|
|
# Search for the override and capture the content between the curly braces
|
|
pattern = r'override val cryAnimation = CryProvider \{(.*?)\}'
|
|
match = re.search(pattern, content, re.DOTALL)
|
|
|
|
if match:
|
|
# Extract the content between the curly braces
|
|
content_between_braces = match.group(1)
|
|
# Extract the animations from the content
|
|
animations, all_warnings_combined, checks["override_correct"] = (
|
|
read_content_and_extract_animations(
|
|
content_between_braces,
|
|
pokemon_name,
|
|
all_warnings_combined))
|
|
elif os.path.isfile(json_poser_path):
|
|
checks["uses_poser"] = True
|
|
try:
|
|
with open(json_poser_path, 'r', encoding="utf-8-sig") as file:
|
|
data = json.load(file)
|
|
if "animations" in data and "cry" in data["animations"] and sanitized_pokemon_name_lower in data["animations"]["cry"]:
|
|
checks["poser_correct"] = True
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
print_warning("Invalid JSON poser in " + dex_number + "_" + sanitized_pokemon_name_lower + "/" + sanitized_pokemon_name_lower + ".json")
|
|
else:
|
|
all_warnings_combined.append(
|
|
(pokemon_name, f"⚠️ Warning: Model.kt file or JSON poser not found for: {sanitized_pokemon_name_lower}"))
|
|
# Ignore the checks for this file
|
|
checks["override_correct"] = True
|
|
checks["import_correct"] = True
|
|
|
|
# Check if the animation.json file exists and contains the correct effect
|
|
if os.path.isfile(animation_file_path):
|
|
try:
|
|
with open(animation_file_path, 'r', encoding="utf-8-sig") as file:
|
|
data = json.load(file)
|
|
if not animations:
|
|
# Iterate through all animations and check if the cry effect is present
|
|
for animation_name, animation_data in data['animations'].items():
|
|
if animation_name == f"animation.{sanitized_pokemon_name_lower}.cry":
|
|
# Check if the "sound_effects" field is present
|
|
checks["sound_effects_and_keyframes"] = check_sound_effects(animation_data,
|
|
sanitized_pokemon_name_lower)
|
|
else:
|
|
for animation_name, animation_data in data['animations'].items():
|
|
for animation in animations:
|
|
if animation_name == f"animation.{sanitized_pokemon_name_lower}.{animation[1]}":
|
|
# Check if the "sound_effects" field is present
|
|
if check_sound_effects(animation_data, sanitized_pokemon_name_lower):
|
|
animations.remove(animation)
|
|
if not animations:
|
|
checks["sound_effects_and_keyframes"] = True
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
print_warning("Invalid JSON in " + animation_file_path.replace(
|
|
'../common/src/main/resources/assets/cobblemon/bedrock/pokemon/animations/', ""))
|
|
else:
|
|
all_warnings_combined.append(
|
|
(pokemon_name,
|
|
f"⚠️ Warning: animation.json file not found at: {animation_file_path.replace('../common/src/main/resources/assets/cobblemon/bedrock/pokemon/animations/', '...')}"))
|
|
# Ignore the checks for this file
|
|
checks["sound_effects_and_keyframes"] = True
|
|
|
|
# Check the condition
|
|
if this_pokemon_in_game == "✔" and this_cry_on_repo == "✔":
|
|
# check if all checks are true
|
|
if not all(checks.values()):
|
|
pokemon_ready_to_be_added_list.append(pokemon_name)
|
|
|
|
# Construct results
|
|
if not checks["cry_in_game"] and checks["audio_file_exists"]:
|
|
false_negatives.append(f"{pokemon_name}")
|
|
elif checks["cry_in_game"] and not checks["audio_file_exists"]:
|
|
false_positives.append(f"{pokemon_name}")
|
|
|
|
# Omit entries that are mega evolutions or g-max, they will be worked on at a later date
|
|
if "Mega" in pokemon_name or "G-Max" in pokemon_name:
|
|
continue
|
|
|
|
# If any of the checks failed, add the Pokemon to the corresponding lists, but not if all checks are false
|
|
if all(value is False for value in checks.values()) and checks["cry_in_game"] is False:
|
|
continue
|
|
|
|
# If both the Pokemon and audio file exist, add it to the list of implemented but unmarked Pokemon
|
|
if not checks["cry_in_game"] and checks["audio_file_exists"]:
|
|
implemented_and_not_marked.append(f"{pokemon_name}")
|
|
continue
|
|
|
|
if all(value is True for value in checks.values()):
|
|
continue
|
|
|
|
if checks["uses_poser"] and checks["cry_in_game"] and checks["audio_file_exists"]:
|
|
if not checks["poser_correct"]:
|
|
invalid_model_files.append((pokemon_name, " [Poser file has no cry specified]"))
|
|
if checks["cry_in_game"] and checks["audio_file_exists"] and not checks["uses_poser"]:
|
|
if not checks["override_correct"] and not checks["import_correct"]:
|
|
invalid_model_files.append((pokemon_name, " [Model.kt import and override not correct]"))
|
|
all_warnings_combined.append((pokemon_name, "⚠️ Warning: Model.kt import and override not correct"))
|
|
elif not checks["override_correct"]:
|
|
invalid_model_files.append((pokemon_name, " [Model.kt override not correct]"))
|
|
all_warnings_combined.append((pokemon_name, "⚠️ Warning: Model.kt override not correct"))
|
|
elif not checks["import_correct"]:
|
|
invalid_model_files.append((pokemon_name, " [Model.kt import not correct]"))
|
|
all_warnings_combined.append((pokemon_name, "⚠️ Warning: Model.kt import not correct"))
|
|
|
|
# If any of the remaining checks failed, add the Pokemon to the all_warnings_combined list
|
|
if not checks["sound_effects_and_keyframes"] and this_cry_on_repo == "✔":
|
|
invalid_animation_files.append((pokemon_name, " [Sound effect keyframe missing or incorrect in animation.json]"))
|
|
all_warnings_combined.append((pokemon_name, "⚠️ Warning: Sound effect in animation.json not correct"))
|
|
|
|
# Check if lists are empty
|
|
if not all_warnings_combined and not false_positives and not false_negatives and not invalid_model_files and not invalid_animation_files and not pokemon_ready_to_be_added_list:
|
|
print("No issues found. All cries are in order! ♪♫")
|
|
else:
|
|
# Print out pokemon cries ready to be added to the game
|
|
if pokemon_ready_to_be_added_list:
|
|
print_separator()
|
|
print("\nPokemon that have cries ready to be added to the game:")
|
|
print_list_filtered(pokemon_ready_to_be_added_list)
|
|
|
|
# Print out the lists of false positives and negatives
|
|
if false_positives:
|
|
print_separator()
|
|
print("\nCry marked as in-game, but audio file is missing or incorrect:")
|
|
print_list_filtered(false_positives)
|
|
|
|
if false_negatives:
|
|
print_separator()
|
|
print(
|
|
"\nAudio file found, but cry not marked as in-game:")
|
|
# create a list of entries that do not contain G-Max or Mega
|
|
false_negatives_filtered_no_gmax_mega = [x for x in false_negatives if "G-Max" not in x and "Mega" not in x]
|
|
print_list_filtered(false_negatives_filtered_no_gmax_mega)
|
|
|
|
## Hello, commented this out to make output cleaner as we're not doing G-Max or Mega cries just yet - Yours truly, Sterr
|
|
# print("\nFalse negatives (G-Max and Mega are Not Implemented Yet):")
|
|
## Only print entries that contain G-Max or Mega
|
|
# false_negatives_filtered = [x for x in false_negatives if "G-Max" in x or "Mega" in x]
|
|
# print_list_filtered(false_negatives_filtered)
|
|
|
|
if implemented_and_not_marked:
|
|
print_separator()
|
|
print("\nPokemon that are implemented in the game with audio, but not marked as cry ""in-game in the spreadsheet:")
|
|
print_list_filtered(implemented_and_not_marked)
|
|
|
|
# Print out the lists of invalid Model.kt and animation.json files
|
|
if invalid_model_files:
|
|
print_separator()
|
|
print(
|
|
"\nIssues with poser or model.kt files:")
|
|
if print_missing_models:
|
|
print_problems_and_paths(invalid_model_files)
|
|
else:
|
|
print_warning("WARNING: [[ main(print_missing_models=False) :: not showing missing Model.kt files]]")
|
|
print_problems_and_paths(invalid_model_files, "Missing Model.kt")
|
|
|
|
if invalid_animation_files:
|
|
print_separator()
|
|
print(
|
|
"Invalid animation.json files: (For Pokémon that have a cry on the assets repository)")
|
|
if print_missing_animations:
|
|
print_problems_and_paths(invalid_animation_files)
|
|
else:
|
|
print_warning(
|
|
"WARNING: [[ main(print_missing_animations=False) :: not showing missing animation.json files]]")
|
|
print_problems_and_paths(invalid_animation_files, "Missing animation.json")
|
|
|
|
## Hello, commented this out to make output cleaner for now - Yours truly, Sterr
|
|
# if all_warnings_combined:
|
|
# print_separator()
|
|
# print("\nAll warnings combined in one list:")
|
|
# print_problems_and_paths(all_warnings_combined)
|
|
|
|
print_cobblemon_script_footer("Thanks for using the Cobblemon cry checker, provided to you by Waldleufer!")
|
|
|
|
|
|
def check_sound_effects(animation_data, sanitized_pokemon_name_lower):
|
|
sound_effects = animation_data.get('sound_effects')
|
|
if sound_effects is not None:
|
|
# Iterate over the sound effects
|
|
for _time_point, effect_data in sound_effects.items():
|
|
# Check if the "effect" field is present and has the correct value
|
|
effect = effect_data.get('effect')
|
|
if effect == f"pokemon.{sanitized_pokemon_name_lower}.cry":
|
|
return True
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
|
|
def read_file_ignore_comments(file_path):
|
|
with open(file_path, 'r', encoding='utf-8-sig') as file:
|
|
lines = file.readlines()
|
|
content = ""
|
|
multi_line_comment = False
|
|
for line in lines:
|
|
stripped_line = line.strip()
|
|
if stripped_line.startswith("//"):
|
|
continue
|
|
elif stripped_line.startswith("/*"):
|
|
multi_line_comment = True
|
|
elif stripped_line.endswith("*/"):
|
|
multi_line_comment = False
|
|
continue
|
|
if not multi_line_comment:
|
|
content += line
|
|
return content
|
|
|
|
|
|
def read_content_and_extract_animations(content, pokemon_name, all_warnings_combined):
|
|
# Find all occurrences of bedrockStateful function call
|
|
pattern = r'bedrockStateful\("(.*?)", "(.*?)"\)'
|
|
matches = re.findall(pattern, content)
|
|
|
|
# Extract pokemonName and cryAnimationName from each occurrence
|
|
animations = [(match[0], match[1]) for match in matches]
|
|
|
|
# Count the number of occurrences
|
|
count = len(matches)
|
|
|
|
# Count the number of bedrockStateful function calls
|
|
bedrockStateful_count = content.count('bedrockStateful')
|
|
|
|
# Check if the pokemonName was misspelled
|
|
if count != bedrockStateful_count:
|
|
all_warnings_combined.append(
|
|
(pokemon_name, f"⚠️ Warning: The pokemonName might be misspelled in the Model.kt file."))
|
|
|
|
return animations, all_warnings_combined, count == bedrockStateful_count
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
input("Press Enter to exit...")
|