mirror of
https://gitlab.com/cable-mc/cobblemon.git
synced 2026-10-29 13:28:54 -04:00
381 lines
12 KiB
Python
381 lines
12 KiB
Python
#!/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 unicodedata
|
|
import urllib.request
|
|
import json
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
REPO_ROOT = SCRIPT_DIR.parent.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"
|
|
|
|
# Manual aliases for filenames in cobblemon-assets that differ from folder names in the mod.
|
|
# Key: species name as it appears in the alpha filename (without _alpha).
|
|
# Value: species name as it appears in the NNNN_species folder.
|
|
ALIASES: dict[str, str] = {
|
|
"basculeigon": "basculegion",
|
|
"basculeigon_female": "basculegion",
|
|
"basculeigon_male": "basculegion",
|
|
"klevin": "kleavor",
|
|
# Add further entries here as needed.
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 (ensure its up to date)\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 normalize(name: str) -> str:
|
|
"""
|
|
Normalize a species name for loose matching:
|
|
1. NFD decomposition strips diacritics (é -> e, etc.)
|
|
2. Drop any remaining non-ASCII bytes
|
|
3. Strip underscores and hyphens
|
|
"""
|
|
nfd = unicodedata.normalize("NFD", name)
|
|
ascii_only = nfd.encode("ascii", "ignore").decode("ascii")
|
|
return ascii_only.replace("_", "").replace("-", "")
|
|
|
|
|
|
def build_folder_map() -> dict[str, Path]:
|
|
"""
|
|
Build a lookup of species_name -> folder path from TEXTURES_DIR.
|
|
Stores both the raw name and the normalized form (no hyphens/underscores/diacritics).
|
|
"""
|
|
folder_map = {}
|
|
for folder in TEXTURES_DIR.iterdir():
|
|
if not folder.is_dir():
|
|
continue
|
|
parts = folder.name.split("_", 1)
|
|
if len(parts) == 2:
|
|
raw_name = parts[1]
|
|
folder_map[raw_name] = folder
|
|
norm = normalize(raw_name)
|
|
if norm != raw_name:
|
|
folder_map[norm] = folder
|
|
return folder_map
|
|
|
|
|
|
def _try_candidate(candidate: str, folder_map: dict[str, Path]) -> Path | None:
|
|
"""Try a candidate string against the folder map, raw then normalized."""
|
|
if candidate in folder_map:
|
|
return folder_map[candidate]
|
|
norm = normalize(candidate)
|
|
if norm in folder_map:
|
|
return folder_map[norm]
|
|
return None
|
|
|
|
|
|
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.
|
|
|
|
Resolution order:
|
|
1. Apply ALIASES to the full species name if present.
|
|
2. Strip trailing segments one at a time (handles form suffixes like _hisuian).
|
|
3. Strip leading segments one at a time (handles prefix forms like ash_greninja).
|
|
4. Each candidate is tried raw, then with Unicode/hyphen/underscore normalization.
|
|
|
|
Examples:
|
|
charizard_alpha -> charizard -> 0006_charizard
|
|
zorua_hisuian_alpha -> zorua_hisuian, zorua -> 0570_zorua
|
|
ash_greninja_alpha -> ash_greninja, ash (fail), greninja -> 0658_greninja
|
|
ho_oh_alpha -> ho_oh / hooh -> 0250_hooh
|
|
porygon-z_alpha -> porygon-z / porygonz -> NNNN_porygonz
|
|
flabébé_alpha -> flabebe (NFD strip) -> NNNN_flabebe
|
|
basculeigon_alpha -> alias -> basculegion -> NNNN_basculegion
|
|
"""
|
|
if not filename_stem.endswith("_alpha"):
|
|
return None
|
|
|
|
full_species_name = filename_stem[:-len("_alpha")]
|
|
|
|
# 1. Alias resolution
|
|
if full_species_name in ALIASES:
|
|
aliased = ALIASES[full_species_name]
|
|
result = _try_candidate(aliased, folder_map)
|
|
if result:
|
|
return result
|
|
|
|
segments = full_species_name.split("_")
|
|
|
|
# 2. Strip trailing segments (longest match first)
|
|
for i in range(len(segments), 0, -1):
|
|
candidate = "_".join(segments[:i])
|
|
result = _try_candidate(candidate, folder_map)
|
|
if result:
|
|
return result
|
|
|
|
# 3. Strip leading segments (handles prefix-form names like ash_greninja)
|
|
for i in range(1, len(segments)):
|
|
candidate = "_".join(segments[i:])
|
|
result = _try_candidate(candidate, folder_map)
|
|
if result:
|
|
return result
|
|
|
|
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/alphas 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() |