#!/usr/bin/env python3 """ Copies alpha eye overlay textures (*_alpha.png) from the cobblemon-assets repo into the correct locations in the cobblemon textures directory. Can use either: - A local clone of cobblemon-assets (fast, file copies) - GitLab API downloads (slower, no clone needed) Destination: common/src/main/resources/assets/cobblemon/textures/pokemon/NNNN_species/ Safe to re-run. Will prompt for overwrite behavior on startup. """ import os import sys import shutil import urllib.request import json from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = SCRIPT_DIR.parent TEXTURES_DIR = REPO_ROOT / "common/src/main/resources/assets/cobblemon/textures/pokemon" GITLAB_PROJECT = "cable-mc%2Fcobblemon-assets" GITLAB_BRANCH = "master" GITLAB_RAW_BASE = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT}/repository/files" GITLAB_TREE_BASE = f"https://gitlab.com/api/v4/projects/{GITLAB_PROJECT}/repository/tree" ASSETS_POKEMON_SUBPATH = "blockbench/pokemon" # --------------------------------------------------------------------------- # Prompts # --------------------------------------------------------------------------- def prompt_assets_root() -> Path | None: """Ask user for local assets clone path. Returns None if blank/invalid.""" print("=" * 60) print("cobblemon-assets source") print("=" * 60) response = input( "Enter path to your cobblemon-assets clone\n" "(leave blank to download via GitLab API): " ).strip() if not response: return None path = Path(response) if not path.exists(): print(f" WARN: Path does not exist: {path}") print(" Falling back to GitLab API downloads.") return None pokemon_path = path / ASSETS_POKEMON_SUBPATH if not pokemon_path.exists(): print(f" WARN: Expected '{ASSETS_POKEMON_SUBPATH}' not found under {path}") print(" Is this the correct cobblemon-assets root?") confirm = input(" Continue anyway? (y/n): ").strip().lower() if confirm != "y": print(" Falling back to GitLab API downloads.") return None return path def prompt_overwrite() -> bool: """Ask user whether to overwrite existing files.""" print() print("=" * 60) print("Overwrite existing files?") print("=" * 60) while True: response = input("Overwrite existing alpha textures? (y/n): ").strip().lower() if response in ("y", "yes"): return True if response in ("n", "no"): return False print(" Please enter y or n.") # --------------------------------------------------------------------------- # Local file discovery # --------------------------------------------------------------------------- def find_alpha_textures_local(assets_root: Path) -> list[Path]: """Find all *_alpha.png files under blockbench/pokemon/ in the local clone.""" pokemon_dir = assets_root / ASSETS_POKEMON_SUBPATH if not pokemon_dir.exists(): print(f"ERROR: {pokemon_dir} does not exist.") sys.exit(1) results = list(pokemon_dir.rglob("*_alpha.png")) print(f" Found {len(results)} alpha textures in local clone.") return results # --------------------------------------------------------------------------- # GitLab API discovery + download # --------------------------------------------------------------------------- def gitlab_list_alpha_files() -> list[str]: """ List all *_alpha.png paths under blockbench/pokemon/ via GitLab API. Handles pagination automatically. """ print(" Querying GitLab API for alpha textures (this may take a moment)...") all_files = [] page = 1 while True: url = ( f"{GITLAB_TREE_BASE}" f"?path={ASSETS_POKEMON_SUBPATH}" f"&recursive=true" f"&ref={GITLAB_BRANCH}" f"&per_page=100" f"&page={page}" ) try: with urllib.request.urlopen(url) as r: data = json.load(r) except Exception as e: print(f" ERROR querying GitLab API: {e}") sys.exit(1) if not data: break for item in data: if item.get("type") == "blob" and item["path"].endswith("_alpha.png"): all_files.append(item["path"]) page += 1 print(f" Found {len(all_files)} alpha textures via API.") return all_files def gitlab_download_file(repo_path: str, dest: Path): """Download a single file from GitLab raw API.""" encoded_path = repo_path.replace("/", "%2F") url = f"{GITLAB_RAW_BASE}/{encoded_path}/raw?ref={GITLAB_BRANCH}" try: with urllib.request.urlopen(url) as r: dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(r.read()) except Exception as e: raise RuntimeError(f"Failed to download {repo_path}: {e}") # --------------------------------------------------------------------------- # Destination matching # --------------------------------------------------------------------------- def build_folder_map() -> dict[str, Path]: """Build a lookup of normalized_species_name -> folder path from TEXTURES_DIR.""" folder_map = {} for folder in TEXTURES_DIR.iterdir(): if not folder.is_dir(): continue parts = folder.name.split("_", 1) if len(parts) == 2: # Store both the raw name and a normalized version (no hyphens/underscores) # so ho_oh -> hooh, mr_mime -> mrmime etc. can match 0250_hooh, 0122_mrmime raw_name = parts[1] folder_map[raw_name] = folder normalized = raw_name.replace("_", "").replace("-", "") if normalized != raw_name: folder_map[normalized] = folder return folder_map def normalize(name: str) -> str: """Strip hyphens and underscores for loose matching.""" return name.replace("_", "").replace("-", "") def find_dest_folder(filename_stem: str, folder_map: dict[str, Path]) -> Path | None: """ Given a stem like 'charizard_alpha', find the matching NNNN_species folder. Tries the full species name first, then progressively strips trailing segments until a match is found. Each attempt is tried both raw and normalized (hyphens/underscores stripped) to handle cases like: ho_oh_alpha -> try ho_oh, then hooh -> matches 0250_hooh mr_mime_alpha -> try mr_mime, then mrmime -> matches 0122_mrmime porygon-z_alpha -> try porygon-z, then porygonz -> matches NNNN_porygonz zorua_hisuian_alpha -> try zorua_hisuian, then zorua -> matches 0570_zorua """ if not filename_stem.endswith("_alpha"): return None full_species_name = filename_stem[:-len("_alpha")] segments = full_species_name.split("_") for i in range(len(segments), 0, -1): candidate = "_".join(segments[:i]) # Try raw match first if candidate in folder_map: return folder_map[candidate] # Try normalized match normalized = normalize(candidate) if normalized in folder_map: return folder_map[normalized] return None # --------------------------------------------------------------------------- # Main processing # --------------------------------------------------------------------------- def process_local(assets_root: Path, overwrite: bool): alpha_files = find_alpha_textures_local(assets_root) folder_map = build_folder_map() stats = {"copied": 0, "skipped_exists": 0, "skipped_no_dest": 0, "errors": 0} no_dest = [] for src_path in sorted(alpha_files): stem = src_path.stem filename = src_path.name dest_folder = find_dest_folder(stem, folder_map) if dest_folder is None: no_dest.append(stem) stats["skipped_no_dest"] += 1 continue dest_file = dest_folder / filename if dest_file.exists() and not overwrite: stats["skipped_exists"] += 1 continue try: shutil.copy2(src_path, dest_file) stats["copied"] += 1 print(f" COPIED: {filename} -> {dest_folder.name}/") except OSError as e: print(f" ERROR copying {filename}: {e}") stats["errors"] += 1 print_summary(stats, no_dest) def process_api(overwrite: bool): alpha_paths = gitlab_list_alpha_files() folder_map = build_folder_map() stats = {"copied": 0, "skipped_exists": 0, "skipped_no_dest": 0, "errors": 0} no_dest = [] for repo_path in sorted(alpha_paths): filename = Path(repo_path).name stem = Path(repo_path).stem dest_folder = find_dest_folder(stem, folder_map) if dest_folder is None: no_dest.append(stem) stats["skipped_no_dest"] += 1 continue dest_file = dest_folder / filename if dest_file.exists() and not overwrite: stats["skipped_exists"] += 1 continue try: gitlab_download_file(repo_path, dest_file) stats["copied"] += 1 print(f" DOWNLOADED: {filename} -> {dest_folder.name}/") except RuntimeError as e: print(f" ERROR: {e}") stats["errors"] += 1 print_summary(stats, no_dest) def print_summary(stats: dict, no_dest: list): print() print("=" * 60) print("SUMMARY") print("=" * 60) print(f" Copied/downloaded : {stats['copied']}") print(f" Skipped (exists) : {stats['skipped_exists']}") print(f" No matching dest : {stats['skipped_no_dest']}") print(f" Errors : {stats['errors']}") if no_dest: print() print(f"No matching texture folder for ({len(no_dest)}):") for name in sorted(no_dest): print(f" - {name}") # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- def main(): print() print("Alpha Eye Texture Sync") print() if not TEXTURES_DIR.exists(): print(f"ERROR: Cobblemon textures directory not found: {TEXTURES_DIR}") print("Is this script in the cobblemon/utilityscripts/ directory?") sys.exit(1) assets_root = prompt_assets_root() overwrite = prompt_overwrite() print() print("=" * 60) print("Processing") print("=" * 60) if assets_root is not None: print(f" Mode: local clone ({assets_root})") process_local(assets_root, overwrite) else: print(" Mode: GitLab API") process_api(overwrite) if __name__ == "__main__": main()