diff --git a/scripts/Fl4sh9174/README.md b/scripts/Fl4sh9174/README.md deleted file mode 100644 index bd14653..0000000 --- a/scripts/Fl4sh9174/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This script is used to format the pchtxt heirarchy appropriately for the following repository: - -https://github.com/Fl4sh9174/Switch-Ultrawide-Mods diff --git a/scripts/Fl4sh9174/format_repo.py b/scripts/Fl4sh9174/format_repo.py deleted file mode 100644 index 60bcac4..0000000 --- a/scripts/Fl4sh9174/format_repo.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python3 -import os -import zipfile -import shutil -import re -import sys -import unicodedata - -# ----- Normalization and Capitalization Helpers ----- - -def sanitize_name(name): - """ - Remove accents and unwanted characters, and replace ' - ' with a single space. - """ - normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii') - cleaned = normalized.replace("'", "").replace("’", "").replace("`", "").replace('"', "") - cleaned = cleaned.replace(" - ", " ") # Merge any " - " into a single space - cleaned = ' '.join(cleaned.split()) # Collapse multiple spaces - return cleaned.strip() - -def capitalize_hyphenated(word): - """ - Capitalize both parts of a hyphenated word. - E.g. "yooka-laylee" → "Yooka-Laylee" - """ - parts = word.split('-') - capitalized_parts = [] - for part in parts: - if part: - capitalized_parts.append(part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper()) - else: - capitalized_parts.append('') - return '-'.join(capitalized_parts) - -# Regex for Roman numerals (supports up to 3999: I, II, III, IV, …, XIII, …, MMMCMXCIX, etc.) -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -# Known acronyms to force‐uppercase exactly -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4", "DOF" -} - -def is_roman_numeral(word): - """ - Return True if the word is a valid Roman numeral (case-insensitive). - """ - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - """ - Title-case with these rules: - • Fully uppercase acronyms remain unchanged (e.g. HD, 2D, 3D, FFVII, etc.). - • Roman numerals are fully uppercase (e.g. 'iii' → 'III', 'xI' → 'XI'). - • Hyphenated words are capitalized on both sides (→ 'Yooka-Laylee'). - • Conjoined Roman numerals with &, +, or | become fully uppercase (e.g. 'I&ii' → 'I&II'). - • Small filler words (a, an, and, the, of, in, etc.) become lowercase only if: - – they appear in the middle (not first or last), - – and they are not immediately after a subtitle marker. - • After a subtitle marker (":", "~", "–", "—", or "-"), force capitalization on all subsequent words - until the next subtitle marker or end-of-title. - """ - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - - words = name.split() - capitalized_words = [] - force_capitalize = False - - for idx, raw_word in enumerate(words): - # Check if this word contains a subtitle marker - contains_marker = any(marker in raw_word for marker in subtitle_markers) - - # Break on subtitle markers, but keep them in the split list - split_parts = re.split(r'([:~\-–—])', raw_word) - rebuilt = [] - - for part in split_parts: - if part in subtitle_markers: - # Keep the marker as-is, then force the next real segment to capitalize - rebuilt.append(part) - force_capitalize = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - def capitalize_special(subword): - # 1) If subword (uppercased) is in ACRONYMS, return it unchanged - if subword.upper() in ACRONYMS: - return subword.upper() - # 2) If subword is a standalone Roman numeral, uppercase it - if is_roman_numeral(subword): - return subword.upper() - # 3) Check for compound Roman numerals (e.g. "I&ii") - for sep in ['&', '+', '|']: - if sep in subword: - pieces = subword.split(sep) - if all(is_roman_numeral(p) for p in pieces): - return sep.join(p.upper() for p in pieces) - # 4) Otherwise, capitalize hyphens normally - return capitalize_hyphenated(subword) - - # Determine whether to capitalize or lowercase this segment - if force_capitalize or is_first or is_last or (lower_part not in lowercase_exceptions): - sub_hyphens = part.split('-') - rebuilt.append('-'.join(capitalize_special(p) for p in sub_hyphens)) - else: - # It's a filler word in the middle → keep lowercase - rebuilt.append(lower_part) - - capitalized_words.append(''.join(rebuilt)) - - # If this raw_word did NOT contain a marker, stop forcing capitalization on the next word - if not contains_marker: - force_capitalize = False - - # Finally, ensure the first and last words are definitely capitalized with special rules: - if capitalized_words: - # First word: - first_split = capitalized_words[0].split('-') - capitalized_words[0] = '-'.join( - p.upper() if (p.upper() in ACRONYMS or is_roman_numeral(p)) else capitalize_hyphenated(p) - for p in first_split - ) - # Last word: - last_split = capitalized_words[-1].split('-') - capitalized_words[-1] = '-'.join( - p.upper() if (p.upper() in ACRONYMS or is_roman_numeral(p)) else capitalize_hyphenated(p) - for p in last_split - ) - - return ' '.join(capitalized_words) - -def clean_title(name): - """ - Convenience function to run both sanitize_name → title_case_preserve_numbers - in one call. - """ - return title_case_preserve_numbers(sanitize_name(name)) - -# ----- Unzipping and Formatting for this Repo ----- - -def unzip_files(folder_path): - print("Unzipping files...\n") - for item in os.listdir(folder_path): - if item.lower().endswith('.zip'): - file_path = os.path.join(folder_path, item) - # Remove any bracketed tags (e.g. "[something]") then strip “.zip” - raw_game_name = re.sub(r'\[.*?\]', '', item).replace('.zip', '').strip() - cleaned_game_name = clean_title(raw_game_name) - extract_to = os.path.join(folder_path, cleaned_game_name) - with zipfile.ZipFile(file_path, 'r') as zip_ref: - zip_ref.extractall(extract_to) - print(f"✅ Unzipped: {file_path} → {extract_to}") - -def create_formatted_structure(folder_path): - formatted_path = os.path.join(folder_path, 'formatted') - os.makedirs(formatted_path, exist_ok=True) - print(f"\nOrganizing into: {formatted_path}\n") - - for game_dir in os.listdir(folder_path): - game_dir_path = os.path.join(folder_path, game_dir) - # Skip the "formatted" folder itself - if not os.path.isdir(game_dir_path) or game_dir == 'formatted': - continue - - # Compute the cleaned, title-cased game name once: - cleaned_game_name = clean_title(game_dir) - - for root, dirs, files in os.walk(game_dir_path): - for file in files: - if file.lower().endswith('.pchtxt'): - # Look for a [mod_name vX.Y] segment in the path - relative_path = os.path.relpath(root, folder_path) - mod_match = re.search(r'\[(.*?)\]', relative_path) - if not mod_match: - continue - - raw_mod_name = mod_match.group(1) - # Strip off any trailing " v" from the bracketed part - mod_name_no_version = re.sub(r' v[0-9.]+$', '', raw_mod_name).strip() - - cleaned_mod_name = clean_title(mod_name_no_version) - version = file[:-len('.pchtxt')].strip() - - target_dir = os.path.join( - formatted_path, - f"{cleaned_game_name} - {cleaned_mod_name}" - ) - os.makedirs(target_dir, exist_ok=True) - - source_file = os.path.join(root, file) - dest_file = os.path.join(target_dir, f"{version}.pchtxt") - shutil.move(source_file, dest_file) - print(f"📦 Moved {file} → {os.path.join(target_dir, f'{version}.pchtxt')}") - - # Once done walking this game’s directory, remove it entirely - shutil.rmtree(game_dir_path) - print(f"🗑️ Removed temporary folder: {game_dir_path}") - - print("\n✅ All files organized successfully.") - -def main(folder_path): - unzip_files(folder_path) - create_formatted_structure(folder_path) - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python format_repo.py /path/to/folder/of/zips/") - sys.exit(1) - - folder_path = sys.argv[1] - main(folder_path) diff --git a/scripts/KeatonTheBot/README.md b/scripts/KeatonTheBot/README.md deleted file mode 100644 index 7306c47..0000000 --- a/scripts/KeatonTheBot/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This script is used to format the pchtxt heirarchy appropriately for the following repository: - -https://github.com/KeatonTheBot/switch-pchtxt-mods diff --git a/scripts/KeatonTheBot/format_repo3.py b/scripts/KeatonTheBot/format_repo3.py deleted file mode 100644 index ae5179d..0000000 --- a/scripts/KeatonTheBot/format_repo3.py +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env python3 -import os -import shutil -import re -import sys -import unicodedata - -# ----- Normalization & Title‐Casing Helpers (identical to other repos) ----- - -def sanitize_name(name): - """ - Remove accents and unwanted characters, and replace ' - ' with a single space. - """ - normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii') - cleaned = normalized.replace("'", "").replace("’", "").replace("`", "").replace('"', "") - # Merge any " - " into a single space, collapse extra spaces - cleaned = cleaned.replace(" - ", " ") - cleaned = ' '.join(cleaned.split()) - return cleaned.strip() - -def capitalize_hyphenated(word): - """ - Capitalize both parts of a hyphenated word. E.g. "yooka-laylee" → "Yooka-Laylee". - """ - parts = word.split('-') - capitalized_parts = [] - for part in parts: - if part: - capitalized_parts.append(part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper()) - else: - capitalized_parts.append('') - return '-'.join(capitalized_parts) - -# Regex for Roman numerals up to 3999 -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -# Known acronyms to force‐uppercase exactly -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4" -} - -def is_roman_numeral(word): - """ - Return True if `word` is a valid Roman numeral (case‐insensitive). - """ - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - """ - Title‐case with these rules: - • Fully uppercase acronyms remain unchanged (e.g. HD, 2D, 3D, FFVII, etc.). - • Roman numerals become fully uppercase (e.g. 'iii' → 'III', 'xI' → 'XI'). - • Hyphenated words are capitalized on both sides (→ 'Yooka-Laylee'). - • Small filler words (a, an, and, the, of, in, etc.) become lowercase - only if they appear in the middle and are not immediately after a subtitle marker, - except the first and last words (which always get capitalized). - • After a subtitle marker (":", "~", "–", "—", or "-"), force capitalization - on all subsequent words (until the next subtitle marker or end). - • Compound Roman numerals joined by &, +, or | become fully uppercase (e.g. "I&ii" → "I&II"). - """ - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - - words = name.split() - result = [] - force_capitalize_mode = False - - for idx, raw_word in enumerate(words): - # Check if this word contains any subtitle marker - contains_marker = any(marker in raw_word for marker in subtitle_markers) - - # Split on subtitle markers (keeping them in the list) - split_parts = re.split(r'([:~\-–—])', raw_word) - rebuilt_parts = [] - - for part in split_parts: - if part in subtitle_markers: - # Keep marker, then force next segments to capitalize - rebuilt_parts.append(part) - force_capitalize_mode = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - def capitalize_special(subword): - # 1) If subword uppercase is in ACRONYMS → uppercase it - if subword.upper() in ACRONYMS: - return subword.upper() - # 2) If subword is a Roman numeral → uppercase it - if is_roman_numeral(subword): - return subword.upper() - # 3) Check for compound Roman numerals joined by &, +, | - for sep in ('&', '+', '|'): - if sep in subword: - pieces = subword.split(sep) - if all(is_roman_numeral(p) for p in pieces): - return sep.join(p.upper() for p in pieces) - # 4) Otherwise → just hyphen‐capitalize normally - return capitalize_hyphenated(subword) - - if force_capitalize_mode or is_first or is_last or (lower_part not in lowercase_exceptions): - sub_parts = part.split('-') - rebuilt_parts.append('-'.join(capitalize_special(p) for p in sub_parts)) - else: - # In-middle filler word → keep lowercase - rebuilt_parts.append(lower_part) - - result.append(''.join(rebuilt_parts)) - - # If this raw_word did NOT contain a marker, stop forcing capitalization on the next - if not contains_marker: - force_capitalize_mode = False - - # Finally, ensure the first and last words are capitalized with special logic: - if result: - # First word: - first_split = result[0].split('-') - new_first = [] - for p in first_split: - if p.upper() in ACRONYMS or is_roman_numeral(p): - new_first.append(p.upper()) - else: - new_first.append(capitalize_hyphenated(p)) - result[0] = '-'.join(new_first) - - # Last word: - last_split = result[-1].split('-') - new_last = [] - for p in last_split: - if p.upper() in ACRONYMS or is_roman_numeral(p): - new_last.append(p.upper()) - else: - new_last.append(capitalize_hyphenated(p)) - result[-1] = '-'.join(new_last) - - return ' '.join(result) - -def clean_title(name): - """ - Convenience: run sanitize_name → title_case_preserve_numbers in one shot. - """ - return title_case_preserve_numbers(sanitize_name(name)) - -# ----- Original Repo Logic, Now Injecting Our Title‐Casing ----- - -def transform_game_name(game_name): - """ - 1) Move ", The" to front, if present. - 2) Remove any " - " substring. - """ - if ', The' in game_name: - parts = game_name.split(', The') - # e.g., "Zelda, The" → "The Zelda" - game_name = f"The {parts[0]}{parts[1]}" - # Remove " - " exactly - game_name = game_name.replace(' - ', ' ') - return game_name - -def get_game_name_and_mod_name(path, root_dir): - """ - Given `path` (where a .pchtxt file lives) and the `root_dir`, - derive: - • game_name (with country if present, then sanitized+title‐cased) - • mod_name (sanitized+title‐cased), handling Aspect Ratio and version suffix. - """ - relative_path = os.path.relpath(path, root_dir) - parts = relative_path.split(os.sep) - - # The first part is the raw game folder name - raw_game = parts[0] - # Strip out any bracketed tags, then transform - raw_game = re.sub(r'\[.*?\]', '', raw_game).strip() - raw_game = transform_game_name(raw_game) - - # Check for country‐specific folders (look for something like "[USA]" or "[JP]" etc.) - country = None - for part in parts[1:]: - if re.search(r'\[.*?\]', part): - country = re.sub(r'\[.*?\]', '', part).strip() - break - - if country: - raw_game = f"{raw_game} ({country})" - - # Now sanitize + title‐case the game name exactly as in other repos: - game_name = clean_title(raw_game) - - # Determine mod_name - # If path contains "Aspect Ratio", then: - if 'Aspect Ratio' in relative_path: - # e.g. "<...>/Aspect Ratio/16:9/[files]" - aspect_ratio = os.path.basename(os.path.dirname(path)).replace("'", ".") - raw_mod = f"Aspect Ratio {aspect_ratio}" - else: - # If the last folder has a version suffix " v\d+", attach it to the previous part - last_part = parts[-1] - if re.search(r' v\d+', last_part): - # e.g. .../SomeMod/Disable Fog v1/file.pchtxt → mod = "Disable Fog v1" - raw_mod = parts[-2] + " " + last_part - else: - # Otherwise just take the immediate parent folder name - raw_mod = parts[-2] - - # Sanitize + title‐case mod_name as well - mod_name = clean_title(raw_mod) - - return game_name, mod_name - -def create_formatted_structure(folder_path): - """ - Walk `folder_path` for all .pchtxt files. For each one: - 1) Derive (game_name, mod_name) via get_game_name_and_mod_name. - 2) Sanitize and title‐case those exactly the same as other repos. - 3) Copy <…>.pchtxt into formatted/ - /.pchtxt. - """ - formatted_path = os.path.join(folder_path, 'formatted') - os.makedirs(formatted_path, exist_ok=True) - print(f"Creating formatted structure at: {formatted_path}\n") - - for root, dirs, files in os.walk(folder_path): - # Skip the “formatted” directory itself - if 'formatted' in root.split(os.sep): - continue - - for file in files: - if not file.lower().endswith('.pchtxt'): - continue - - # Derive game_name and mod_name - game_name, mod_name = get_game_name_and_mod_name(root, folder_path) - - version = file[:-len('.pchtxt')].strip() # strip the ".pchtxt" - new_dir = os.path.join(formatted_path, f"{game_name} - {mod_name}") - os.makedirs(new_dir, exist_ok=True) - - src = os.path.join(root, file) - dst = os.path.join(new_dir, f"{version}.pchtxt") - shutil.copy(src, dst) - print(f"Copied {src} → {dst}") - - print("\nDone!\n") - -def main(folder_path): - create_formatted_structure(folder_path) - print("All files have been organized successfully.") - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python format_repo_3.py /path/to/folder/") - sys.exit(1) - - folder_path = sys.argv[1] - main(folder_path) diff --git a/scripts/StevensND/README.md b/scripts/StevensND/README.md deleted file mode 100644 index dcc3812..0000000 --- a/scripts/StevensND/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This script is used to format the pchtxt heirarchy appropriately for the following repository: - -https://github.com/StevensND/switch-port-mods diff --git a/scripts/StevensND/format_repo2.py b/scripts/StevensND/format_repo2.py deleted file mode 100644 index 069d522..0000000 --- a/scripts/StevensND/format_repo2.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -import os -import shutil -import re -import sys -import unicodedata - -# ----- Normalization & Title‐Casing Helpers ----- - -def sanitize_name(name): - normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii') - cleaned = normalized.replace("'", "").replace("’", "").replace("`", "").replace('"', "") - cleaned = cleaned.replace(" - ", " ") - cleaned = ' '.join(cleaned.split()) - return cleaned.strip() - -def capitalize_hyphenated(word): - parts = word.split('-') - capitalized_parts = [] - for part in parts: - if part: - capitalized_parts.append(part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper()) - else: - capitalized_parts.append('') - return '-'.join(capitalized_parts) - -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -# Known acronyms to force‐uppercase exactly -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4", "DOF" -} - -def is_roman_numeral(word): - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - words = name.split() - result = [] - force_capitalize_mode = False - - for idx, raw_word in enumerate(words): - contains_marker = any(marker in raw_word for marker in subtitle_markers) - split_parts = re.split(r'([:~\-–—])', raw_word) - rebuilt_parts = [] - - for part in split_parts: - if part in subtitle_markers: - rebuilt_parts.append(part) - force_capitalize_mode = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - def capitalize_special(subword): - if subword.upper() in ACRONYMS: - return subword.upper() - if is_roman_numeral(subword): - return subword.upper() - for sep in ('&', '+', '|'): - if sep in subword: - pieces = subword.split(sep) - if all(is_roman_numeral(p) for p in pieces): - return sep.join(p.upper() for p in pieces) - return capitalize_hyphenated(subword) - - if force_capitalize_mode or is_first or is_last or (lower_part not in lowercase_exceptions): - sub_parts = part.split('-') - rebuilt_parts.append('-'.join(capitalize_special(s) for s in sub_parts)) - else: - rebuilt_parts.append(lower_part) - - result.append(''.join(rebuilt_parts)) - if not contains_marker: - force_capitalize_mode = False - - if result: - first_split = result[0].split('-') - result[0] = "-".join(capitalize_hyphenated(p) if not is_roman_numeral(p) else p.upper() for p in first_split) - - last_split = result[-1].split('-') - result[-1] = "-".join(capitalize_hyphenated(p) if not is_roman_numeral(p) else p.upper() for p in last_split) - - return " ".join(result) - -def clean_title(name): - return title_case_preserve_numbers(sanitize_name(name)) - - -# ----- Game & Mod Name Logic ----- - -def strip_versions(text): - """ - Remove any substrings that look like version numbers, e.g.: - - 1.0, 1.2.3 - - v1.0, v2.3.4 - """ - return re.sub(r'\b(v?\d+(?:\.\d+){1,2})\b', '', text, flags=re.IGNORECASE).strip() - - -def get_game_name_and_mod_name(path, root_dir): - relative_path = os.path.relpath(path, root_dir) - parts = relative_path.split(os.sep) - - raw_game = parts[0] - raw_game = re.sub(r'\[.*?\]', '', raw_game).strip() - if ", The" in raw_game: - p = raw_game.split(", The") - raw_game = f"The {p[0]}{p[1]}" - raw_game = raw_game.replace(" - ", " ") - - country = None - for p in parts[1:]: - if re.search(r'\[.*?\]', p): - country = re.sub(r'\[.*?\]', '', p).strip() - break - if country: - raw_game = f"{raw_game} ({country})" - game_name = clean_title(raw_game) - - sub_folders = [ re.sub(r'\[.*?\]', '', p).strip() for p in parts[1:] ] - sub_folders = [sf for sf in sub_folders if sf.lower() != "pchtxt"] - - if "Aspect Ratio" in relative_path: - aspect_folder = os.path.basename(path) - raw_mod = f"Aspect Ratio {aspect_folder}" - else: - if sub_folders: - m = re.match(r'^([0-9]+(?:\.[0-9]+)*)\s*(.*)$', sub_folders[0]) - if m: - trailing = m.group(2).strip() - if trailing: - sub_folders[0] = trailing - else: - sub_folders = sub_folders[1:] - - if country and sub_folders: - prefix = country.lower() - candidate = sub_folders[0].lower() - if candidate.startswith(prefix): - sub_folders[0] = sub_folders[0][len(country):].lstrip() - - if sub_folders: - raw_mod = " ".join(sub_folders).strip() - else: - raw_mod = "" - - raw_mod = strip_versions(raw_mod) - m2 = re.match(r'^(.*)\s+v[0-9.]+$', raw_mod, re.IGNORECASE) - if m2: - raw_mod = m2.group(1).strip() - - mod_name = clean_title(raw_mod) if raw_mod else "" - return game_name, mod_name - - -# ----- File Structure Logic ----- - -def create_formatted_structure(folder_path): - formatted_path = os.path.join(folder_path, "formatted") - os.makedirs(formatted_path, exist_ok=True) - print(f"Creating formatted structure at: {formatted_path}\n") - - for root, dirs, files in os.walk(folder_path): - if "formatted" in root.split(os.sep): - continue - - for filename in files: - if not filename.lower().endswith(".pchtxt"): - continue - - game_name, mod_name = get_game_name_and_mod_name(root, folder_path) - version = filename[:-len(".pchtxt")].strip() - combined_dir = f"{game_name} - {mod_name}".rstrip() - new_dir = os.path.join(formatted_path, combined_dir) - os.makedirs(new_dir, exist_ok=True) - - src = os.path.join(root, filename) - dst = os.path.join(new_dir, f"{version}.pchtxt") - shutil.copy(src, dst) - print(f"Copied {src} → {dst}") - - print("\nDone!\n") - - -def main(folder_path): - create_formatted_structure(folder_path) - print("All files have been organized successfully.") - - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python format_repo_2.py /path/to/folder/") - sys.exit(1) - - folder_path = sys.argv[1] - main(folder_path) diff --git a/scripts/cucholix/EE.en.json.zip b/scripts/cucholix/EE.en.json.zip deleted file mode 100644 index db46f12..0000000 Binary files a/scripts/cucholix/EE.en.json.zip and /dev/null differ diff --git a/scripts/cucholix/README.md b/scripts/cucholix/README.md deleted file mode 100644 index 08c7c3d..0000000 --- a/scripts/cucholix/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This script is used to format the pchtxt heirarchy appropriately for the following repository: - -https://github.com/cucholix/NX-IPS-romfs-compilation diff --git a/scripts/cucholix/cucholix_aio.py b/scripts/cucholix/cucholix_aio.py deleted file mode 100644 index 3398ed0..0000000 --- a/scripts/cucholix/cucholix_aio.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -""" -Headless extractor using unar/7z (no Keka). Designed for the NX-IPS-romfs-compilation repo. -Installs required: `brew install unar p7zip` -Set KEEP_TMP=1 to preserve temp extraction folders for debugging. -""" -import os, re, sys, shutil, tempfile, subprocess, unicodedata, urllib.request, zipfile - -# ---------------- Config ---------------- -TMP_ROOT = os.path.expanduser("~/keka_extract_tmp") # safe, in HOME -KEEP_TMP = os.environ.get("KEEP_TMP", "") != "" -# ---------------- Helpers (naming/title) ---------------- -def sanitize_name(name): - n = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii") - for ch in ("'", "’", "`", '"'): - n = n.replace(ch, "") - n = n.replace(" - ", " ") - return " ".join(n.split()).strip() - -def capitalize_hyphenated(word): - parts = word.split("-") - out = [] - for p in parts: - if not p: - out.append("") - elif len(p) == 1: - out.append(p.upper()) - else: - out.append(p[0].upper() + p[1:].lower()) - return "-".join(out) - -ROMAN_NUMERAL_PATTERN = re.compile(r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", re.IGNORECASE) -ACRONYMS = {"HD","2D","3D","4K","VR","AI","API","USB","CPU","GPU","DVD","CD", - "RPG","FPS","MMO","MMORPG","LAN","GUI","NPC", - "FFVII","FFVIII","FFIX","FFX","FFXII","FX","2K","5K","8K","V1","V2","V3","V4","DOF"} - -def is_roman_numeral(w): return bool(ROMAN_NUMERAL_PATTERN.match(w)) - -def title_case_preserve_numbers(name): - lowercase_exceptions = {"a","an","and","as","at","but","by","for","from","in","nor","of","on","or","so","the","to","with","yet"} - subtitle_markers = {":","~","-","–","—"} - words = name.split() - result = [] - force_cap = False - for idx, word in enumerate(words): - contains_marker = any(m in word for m in subtitle_markers) - parts = re.split(r'([:~\-–—])', word) - out_parts = [] - for part in parts: - if part in subtitle_markers: - out_parts.append(part) - force_cap = True - continue - lp = part.lower() - is_first = idx == 0 - is_last = idx == len(words) - 1 - def cap_special(w): - if w.upper() in ACRONYMS: return w.upper() - if is_roman_numeral(w): return w.upper() - for sep in ('&','+','|'): - if sep in w: - sub = w.split(sep) - if all(is_roman_numeral(x) for x in sub): - return sep.join(x.upper() for x in sub) - return capitalize_hyphenated(w) - if force_cap or is_first or is_last or (lp not in lowercase_exceptions): - out_parts.append('-'.join(cap_special(sp) for sp in part.split('-'))) - else: - out_parts.append(lp) - result.append(''.join(out_parts)) - if not contains_marker: - force_cap = False - if result: - result[0] = '-'.join(sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) for sp in result[0].split('-')) - result[-1] = '-'.join(sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) for sp in result[-1].split('-')) - return ' '.join(result) - -def find_title_path(path): - hex_re = re.compile(r'^[0-9A-Fa-f]{16}$') - for root, dirs, _ in os.walk(path): - for d in dirs: - if hex_re.match(d): - return os.path.join(root, d) - return None - -# ---------------- Extraction functions ---------------- -def which_exec(names): - for n in names: - p = shutil.which(n) - if p: - return p - return None - -UNAR = which_exec(["unar"]) -SEVENZ = which_exec(["7z", "7zz"]) # prefer 7z if present - -def extract_with_unar(archive_path, outdir): - if UNAR is None: - return False, "unar not installed" - os.makedirs(outdir, exist_ok=True) - cmd = [UNAR, "-o", outdir, "-f", archive_path] # -f overwrite if needed - proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out = proc.stdout.decode(errors="replace") - err = proc.stderr.decode(errors="replace") - ok = (proc.returncode == 0) - return ok, f"rc={proc.returncode} stdout={out[:1000]!r} stderr={err[:1000]!r}" - -def extract_with_7z(archive_path, outdir): - if SEVENZ is None: - return False, "7z not installed" - os.makedirs(outdir, exist_ok=True) - # 7z expects -o (no space) — but passing as single arg works too for many wrappers; use concatenated to be safe - cmd = [SEVENZ, "x", archive_path, f"-o{outdir}", "-y"] - proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out = proc.stdout.decode(errors="replace") - err = proc.stderr.decode(errors="replace") - ok = (proc.returncode == 0) - return ok, f"rc={proc.returncode} stdout={out[:1000]!r} stderr={err[:1000]!r}" - -def extract_archive_headless(archive_path, outdir): - """ - Try best extractor for extension: - .rar -> unar then 7z - .7z -> 7z then unar - Returns (True, debugstr) or (False, debugstr) - """ - ext = archive_path.lower().rsplit(".",1)[-1] - if ext == "rar": - # prefer unar for rar - if UNAR: - ok, dbg = extract_with_unar(archive_path, outdir) - if ok: return True, "unar: " + dbg - # else try 7z - if SEVENZ: - ok, dbg = extract_with_7z(archive_path, outdir) - if ok: return True, "7z: " + dbg - return False, "no suitable extractor succeeded; tried unar and 7z. " + (dbg if 'dbg' in locals() else "") - elif ext == "7z": - if SEVENZ: - ok, dbg = extract_with_7z(archive_path, outdir) - if ok: return True, "7z: " + dbg - if UNAR: - ok, dbg = extract_with_unar(archive_path, outdir) - if ok: return True, "unar: " + dbg - return False, "no suitable extractor succeeded for .7z" - else: - # generic try unar then 7z - if UNAR: - ok, dbg = extract_with_unar(archive_path, outdir) - if ok: return True, "unar: " + dbg - if SEVENZ: - ok, dbg = extract_with_7z(archive_path, outdir) - if ok: return True, "7z: " + dbg - return False, "no extractor available for extension: " + ext - -# ---------------- Processing per archive ---------------- -def process_rar(root_folder, rar_relpath, output_root): - subdir, filename = os.path.split(rar_relpath) - version_match = re.match(r"release_(.+?)(?:\.part\d+)?\.(rar|7z)$", filename, re.IGNORECASE) - if not version_match: - print(f"❌ Invalid release name: {filename}") - return - version = version_match.group(1) - raw_game_name = os.path.basename(subdir) - cleaned_name = sanitize_name(raw_game_name) - game_name = title_case_preserve_numbers(cleaned_name) - pack_label = f"{game_name} - Graphics Pack" - - rar_path = os.path.join(root_folder, rar_relpath) - - os.makedirs(TMP_ROOT, exist_ok=True) - tmpdir = tempfile.mkdtemp(prefix="extract_", dir=TMP_ROOT) - try: - ok, dbg = extract_archive_headless(rar_path, tmpdir) - if not ok: - print(f"❌ Extraction error (headless) on {rar_relpath}: {dbg}") - if KEEP_TMP: - print("Temp dir kept at:", tmpdir) - return - - title_path = find_title_path(tmpdir) - if not title_path: - # maybe files extracted but no TitleID folder, show debugging - any_files = False - for _r,_d,files in os.walk(tmpdir): - if files: - any_files = True - break - if any_files: - print(f"❌ Extracted but no TitleID found for {rar_relpath}. Inspect: {tmpdir} — dbg: {dbg}") - else: - print(f"❌ No files extracted for {rar_relpath}. dbg: {dbg}") - if KEEP_TMP: - print("Temp dir kept at:", tmpdir) - return - - title_id = os.path.basename(title_path) - version_dir = os.path.join(output_root, pack_label, version) - os.makedirs(version_dir, exist_ok=True) - dst = os.path.join(version_dir, title_id) - try: - shutil.copytree(title_path, dst, dirs_exist_ok=True) - print(f"✅ {rar_relpath} → {os.path.join(pack_label, version, title_id)}") - except Exception as e: - print(f"❌ Copy failed for {rar_relpath}: {e}") - - finally: - if KEEP_TMP: - print("Kept tmpdir for debugging:", tmpdir) - else: - try: - shutil.rmtree(tmpdir) - except Exception: - pass - -# ---------------- pchtxt processing (unchanged) ---------------- -def process_pchtxt(root_folder, output_path): - os.makedirs(output_path, exist_ok=True) - print(f"Creating formatted pchtxt structure at: {output_path}\n") - for current_root, dirs, files in os.walk(root_folder): - if 'contents_formatted' in current_root or 'pchtxt_formatted' in current_root: - continue - for file in files: - if not file.lower().endswith('.pchtxt'): - continue - version = file[:-len('.pchtxt')].strip() - parent_dir = os.path.basename(current_root) - game_name = sanitize_name(parent_dir) - if game_name.endswith("Graphics"): - game_name = game_name[:-len("Graphics")].strip() - game_name = title_case_preserve_numbers(game_name) - mod_name = "Graphics Mods" - target_dir = os.path.join(output_path, f"{game_name} - {mod_name}") - os.makedirs(target_dir, exist_ok=True) - source_path = os.path.join(current_root, file) - dest_path = os.path.join(target_dir, f"{version}.pchtxt") - shutil.copy2(source_path, dest_path) - print(f"Copied {source_path} → {dest_path}") - print("\nDone processing pchtxt!") - -# ---------------- main ---------------- -def main(): - # refuse to run as root — running as root was causing /var/root/ issues earlier - if hasattr(os, "geteuid") and os.geteuid() == 0: - print("Refusing to run as root. Run this script as your normal user (no sudo).") - sys.exit(1) - - zip_url = "https://github.com/cucholix/NX-IPS-romfs-compilation/archive/refs/heads/main.zip" - zip_path = "NX-IPS-romfs-compilation-main.zip" - unzip_dir = "NX-IPS-romfs-compilation-main" - - if not os.path.exists(unzip_dir): - if not os.path.exists(zip_path): - print("Downloading repo zip...") - urllib.request.urlretrieve(zip_url, zip_path) - print("Download complete.") - print("Unzipping repo...") - with zipfile.ZipFile(zip_path, 'r') as zip_ref: - zip_ref.extractall(".") - print("Unzip complete.") - - root = unzip_dir - output_contents = os.path.join(".", "contents_formatted") - os.makedirs(output_contents, exist_ok=True) - - tasks = [] - for dirpath, _, files in os.walk(root): - for fn in files: - if re.match(r"release_(.+?)(?:\.part\d+)?\.(rar|7z)$", fn, re.IGNORECASE): - if fn.lower().endswith(".part01.rar") or (fn.lower().endswith(".rar") and ".part" not in fn.lower()) or fn.lower().endswith(".7z"): - rel = os.path.relpath(os.path.join(dirpath, fn), root) - tasks.append(rel) - - if not UNAR and not SEVENZ: - print("ERROR: neither 'unar' nor '7z' found. Install with: brew install unar p7zip") - sys.exit(1) - - for relpath in tasks: - process_rar(root, relpath, output_contents) - - print("✅ Done processing content mods.") - output_pchtxt = os.path.join(".", "pchtxt_formatted") - process_pchtxt(root, output_pchtxt) - -if __name__ == '__main__': - main() diff --git a/scripts/cucholix/format_contents_repo.py b/scripts/cucholix/format_contents_repo.py deleted file mode 100644 index 3674279..0000000 --- a/scripts/cucholix/format_contents_repo.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python3 -import os -import re -import sys -import shutil -import tempfile -import subprocess -import unicodedata - -def sanitize_name(name): - """Normalize game names: remove symbols, accents, and replace ' - ' with space.""" - n = unicodedata.normalize('NFKD', name).encode('ascii','ignore').decode('ascii') - n = n.replace("'", "").replace("’", "").replace("`", "").replace('"', "") - n = n.replace(" - ", " ") # Replace problematic ' - ' only in the game name - return ' '.join(n.split()).strip() - -def capitalize_hyphenated(word): - """ - Capitalize both parts of a hyphenated word. E.g. "yooka-laylee" → "Yooka-Laylee". - """ - parts = word.split('-') - capitalized = [] - for part in parts: - if part: - capitalized.append( - part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper() - ) - else: - capitalized.append('') - return '-'.join(capitalized) - -# Regex for Roman numerals (supports up to 3999): I, II, III, IV, V, etc. -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -# A set of known acronyms that should remain uppercase exactly as is. -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4", "DOF" -} - -def is_roman_numeral(word): - """Return True if the word is a valid Roman numeral (e.g. I, ii, Xx, etc.).""" - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - """ - Title-case with these rules: - • Fully uppercase acronyms remain unchanged. - • Roman numerals are fully uppercase. - • Hyphenated words are capitalized on both sides. - • Conjoined Roman numerals with '&', '+', or '|' become fully uppercase. - • Small filler words (a, an, and, the, of, in, etc.) become lowercase - only if they are in the middle of the title (not first, not last, not - immediately after a subtitle marker). - • After a subtitle marker ( :, ~, –, —, - ), force capitalization on all - subsequent words until the next subtitle marker or end-of-title. - """ - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - - words = name.split() - result = [] - force_capitalize_mode = False # Once True, it stays True until next subtitle marker - - for idx, word in enumerate(words): - # Detect if this word contains any subtitle marker character - contains_marker = any(marker in word for marker in subtitle_markers) - - # Split on subtitle markers but keep them in the list - split_parts = re.split(r'([:~\-–—])', word) - capitalized_parts = [] - - for part in split_parts: - if part in subtitle_markers: - # Keep the subtitle marker, then force subsequent parts to capitalize - capitalized_parts.append(part) - force_capitalize_mode = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - # Helper: capitalize a sub-word with special rules - def capitalize_special(w): - if w.upper() in ACRONYMS: - return w.upper() - if is_roman_numeral(w): - return w.upper() - for sep in ['&', '+', '|']: - if sep in w: - sub = w.split(sep) - if all(is_roman_numeral(x) for x in sub): - return sep.join(x.upper() for x in sub) - # Otherwise, capitalize hyphenated words normally - return capitalize_hyphenated(w) - - # Decide how to capitalize this segment: - if ( - force_capitalize_mode or - is_first or - is_last or - (lower_part not in lowercase_exceptions) - ): - sub_parts = part.split('-') - capitalized_sub_parts = [capitalize_special(sp) for sp in sub_parts] - capitalized_parts.append('-'.join(capitalized_sub_parts)) - else: - capitalized_parts.append(lower_part) - - result.append(''.join(capitalized_parts)) - - # If this word did not contain a subtitle marker, stop forcing next capitalization - if not contains_marker: - force_capitalize_mode = False - - # Always re-capitalize FIRST and LAST words fully using the same special rules - if result: - first_word_parts = result[0].split('-') - result[0] = '-'.join( - sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) - for sp in first_word_parts - ) - last_word_parts = result[-1].split('-') - result[-1] = '-'.join( - sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) - for sp in last_word_parts - ) - - return ' '.join(result) - -def find_title_id(path): - """Look for a 16-character Title ID under any 'contents/' directory in path.""" - for root, dirs, _ in os.walk(path): - if os.path.basename(root).lower() == 'contents': - for d in dirs: - if re.fullmatch(r'[0-9A-Fa-f]{16}', d): - return d - return None - -def extract_with_7z(rar_path, tmpdir): - """ - Extract only atmosphere/contents/* using 7z. - Return True if extraction succeeded (7z return code 0 OR - we already found a Title ID folder inside tmpdir). - """ - cmd = [ - "7z", "x", "-y", - rar_path, - f"-o{tmpdir}", - "atmosphere/contents/*" - ] - proc = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - return (proc.returncode == 0) or (find_title_id(tmpdir) is not None) - -def process_rar(root_folder, rar_relpath, output_root): - """ - 1) Parse “release_.rar” or “release_.part01.rar” → version string. - 2) Sanitize + title-case the game folder name. - 3) Extract with 7z into a temp dir. - 4) Find Title ID under atmosphere/contents/<16hex>/. - 5) Copy that content into output//version//. - """ - subdir, filename = os.path.split(rar_relpath) - - # New regex: capture base version, ignoring any “.partXX” - version_match = re.match(r"release_(.+?)(?:\.part\d+)?\.rar$", filename, re.IGNORECASE) - if not version_match: - print(f"❌ Invalid release name: {filename}") - return - version = version_match.group(1) # e.g. “1.2.4” even if filename was “release_1.2.4.part01.rar” - raw_game_name = os.path.basename(subdir) - - # 2) Clean & normalize game name - cleaned_name = sanitize_name(raw_game_name) - game_name = title_case_preserve_numbers(cleaned_name) - pack_label = f"{game_name} - Graphics Pack" - - rar_path = os.path.join(root_folder, rar_relpath) - with tempfile.TemporaryDirectory() as tmp: - ok = extract_with_7z(rar_path, tmp) - if not ok: - print(f"❌ Extraction error (7z) on {rar_relpath}") - return - - title_id = find_title_id(tmp) - if not title_id: - print(f"❌ No Title ID found in {rar_relpath}") - return - - version_dir = os.path.join(output_root, pack_label, version) - os.makedirs(version_dir, exist_ok=True) - - src = os.path.join(tmp, "atmosphere", "contents", title_id) - dst = os.path.join(version_dir, title_id) - try: - shutil.copytree(src, dst, dirs_exist_ok=True) - print(f"✅ {rar_relpath} → {os.path.join(pack_label, version, title_id)}") - except Exception as e: - print(f"❌ Copy failed for {rar_relpath}: {e}") - -def main(): - if len(sys.argv) != 2: - print("Usage: python collect_content_mods.py /path/to/root") - sys.exit(1) - - root = sys.argv[1] - output = os.path.join(root, "format2") - os.makedirs(output, exist_ok=True) - - tasks = [] - for dirpath, _, files in os.walk(root): - for fn in files: - # 1) Only consider RARs that are either: - # • single-part: “release_.rar” - # • first part of multi-part: “release_.part01.rar” - # - # Regex explanation: - # release_ → literal prefix - # (.+?) → capture “version” (non-greedy) - # (?:\.part\d+)? → optionally “.partNN” (where NN = digits) - # \.rar$ → end with “.rar” - m = re.match(r"release_(.+?)(?:\.part\d+)?\.rar$", fn, re.IGNORECASE) - if not m: - continue - - # If it really is a “.partNN.rar” (some NN > 1), skip it. - # We only want “.part01” or no “.part” at all. - if fn.lower().endswith(".part01.rar") or fn.lower().endswith(".rar") and ".part" not in fn.lower(): - rel = os.path.relpath(os.path.join(dirpath, fn), root) - tasks.append(rel) - - for relpath in tasks: - process_rar(root, relpath, output) - - print("✅ Done.") - -if __name__ == "__main__": - main() diff --git a/scripts/cucholix/format_repo5.py b/scripts/cucholix/format_repo5.py deleted file mode 100644 index f8e7932..0000000 --- a/scripts/cucholix/format_repo5.py +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -import os -import shutil -import unicodedata -import sys -import re - -def sanitize_name(name): - """ - Remove accents and unwanted characters, and replace ' - ' with a single space. - """ - normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii') - cleaned = normalized.replace("'", "").replace("’", "").replace("`", "").replace('"', '') - cleaned = cleaned.replace(" - ", " ") # Remove " - " to avoid duplication - cleaned = ' '.join(cleaned.split()) # Collapse multiple spaces - return cleaned.strip() - -def capitalize_hyphenated(word): - """ - Capitalize both parts of a hyphenated word. E.g. "yooka-laylee" → "Yooka-Laylee". - """ - parts = word.split('-') - capitalized_parts = [] - for part in parts: - if part: - capitalized_parts.append(part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper()) - else: - capitalized_parts.append('') - return '-'.join(capitalized_parts) - -# Regex for Roman numerals (supports up to 3999): I, II, III, IV, V, VI, VII, VIII, IX, X, XI, etc. -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -# Known acronyms to force‐uppercase exactly -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4", "DOF" -} - -def is_roman_numeral(word): - """ - Return True if the word is a valid Roman numeral (case-insensitive). - """ - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - """ - Title-case with these rules: - • Fully uppercase acronyms remain unchanged (e.g. HD, 2D, 3D, FFVII, etc.). - • Roman numerals become fully uppercase (e.g. iIi → III, xI → XI). - • Hyphenated words are capitalized on both sides (→ Yooka-Laylee). - • Conjoined Roman numerals with '&', '+', or '|' become fully uppercase (e.g. I&ii → I&II). - • Small filler words (a, an, and, the, of, in, etc.) become lowercase - only if they appear in the middle and are not immediately after a subtitle marker, - except the first and last words (which always capitalized). - • After a subtitle marker (":", "~", "–", "—", or "-"), force capitalization - on all subsequent words (until the next subtitle marker or end). - """ - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - - words = name.split() - result = [] - force_capitalize_mode = False # Once True, stays True until next subtitle marker - - for idx, word in enumerate(words): - # Detect if this word contains any subtitle marker character - contains_marker = any(marker in word for marker in subtitle_markers) - - # Split on subtitle markers but keep them in the list - split_parts = re.split(r'([:~\-–—])', word) - capitalized_parts = [] - - for part in split_parts: - if part in subtitle_markers: - # Append the marker itself, then force-capitalize subsequent parts - capitalized_parts.append(part) - force_capitalize_mode = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - # Helper: capitalize a sub-word with special rules for acronyms, roman numerals, and conjoined numerals - def capitalize_special(w): - # If w (case-insensitive) is in our ACRONYMS set, uppercase it fully. - if w.upper() in ACRONYMS: - return w.upper() - # If w alone is a Roman numeral, uppercase it fully. - if is_roman_numeral(w): - return w.upper() - # Handle compound Roman numerals separated by &, +, or | - for sep in ['&', '+', '|']: - if sep in w: - parts = w.split(sep) - if all(is_roman_numeral(p) for p in parts): - return sep.join(p.upper() for p in parts) - # Otherwise, capitalize hyphenated words normally. - return capitalize_hyphenated(w) - - # Decide how to capitalize this segment: - if force_capitalize_mode or is_first or is_last or (lower_part not in lowercase_exceptions): - # Split any hyphens, apply capitalize_special to each half - sub_parts = part.split('-') - capitalized_sub = [capitalize_special(sp) for sp in sub_parts] - capitalized_parts.append('-'.join(capitalized_sub)) - else: - # In-the-middle filler word: keep lowercase - capitalized_parts.append(lower_part) - - result.append(''.join(capitalized_parts)) - - # If this word did not contain a subtitle marker, stop forcing next capitalization - if not contains_marker: - force_capitalize_mode = False - - # Always capitalize the FIRST and LAST words (using the same special rules): - if result: - first_parts = result[0].split('-') - result[0] = '-'.join( - sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) - for sp in first_parts - ) - - last_parts = result[-1].split('-') - result[-1] = '-'.join( - sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) - for sp in last_parts - ) - - return ' '.join(result) - -def create_formatted_structure(root_folder): - """ - Walk root_folder for all .pchtxt files. For each one: - 1. Extract folder name → raw game name. - 2. sanitize_name() → remove weird characters. - 3. Remove trailing "Graphics" if present. - 4. title_case_preserve_numbers() to get final Game Name. - 5. Create folder: formatted/ - Graphics Mods/ - 6. Copy each .pchtxt into that folder as .pchtxt. - """ - formatted_path = os.path.join(root_folder, 'formatted') - os.makedirs(formatted_path, exist_ok=True) - print(f"Creating formatted structure at: {formatted_path}\n") - - for current_root, dirs, files in os.walk(root_folder): - for file in files: - if not file.lower().endswith('.pchtxt'): - continue - - version = file[:-len('.pchtxt')].strip() - parent_dir = os.path.basename(current_root) - game_name = sanitize_name(parent_dir) - - # Remove trailing "Graphics" if it exists (exact match at end) - if game_name.endswith("Graphics"): - game_name = game_name[:-len("Graphics")].strip() - - # Title-case with acronyms, roman-numeral, and compound-numeral logic - game_name = title_case_preserve_numbers(game_name) - - mod_name = "Graphics Mods" - target_dir = os.path.join(formatted_path, f"{game_name} - {mod_name}") - os.makedirs(target_dir, exist_ok=True) - - source_path = os.path.join(current_root, file) - dest_path = os.path.join(target_dir, f"{version}.pchtxt") - - shutil.copy2(source_path, dest_path) - print(f"Copied {source_path} → {dest_path}") - - print("\nDone!") - -def main(): - if len(sys.argv) != 2: - print("Usage: python collect_graphics_mods.py /path/to/root/folder") - sys.exit(1) - - folder_path = sys.argv[1] - create_formatted_structure(folder_path) - -if __name__ == "__main__": - main() diff --git a/scripts/cucholix/titleid_patcher.py b/scripts/cucholix/titleid_patcher.py deleted file mode 100644 index 1f9e5e6..0000000 --- a/scripts/cucholix/titleid_patcher.py +++ /dev/null @@ -1,99 +0,0 @@ -import os -import json -import re -import unicodedata -from pathlib import Path -from difflib import get_close_matches - -# Load title DB -with open("EE.en.json", "r", encoding="utf-8") as f: - title_db = json.load(f) - -# Normalize title names for matching -def normalize_title(text): - text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii') # remove accents - text = re.sub(r'[^a-zA-Z0-9 ]', '', text).lower() # remove punctuation and lowercase - return ' '.join(text.split()) # normalize whitespace - -# Create a normalized mapping of game name -> title ID -title_map = {} -normalized_title_map = {} -for entry in title_db.values(): - name = entry.get("name") - tid = entry.get("id") - if name and tid: - title_map[name.strip()] = tid.strip() - normalized_title_map[normalize_title(name)] = tid.strip() - -# Fuzzy match game name to title ID using normalized names -def get_title_id(game_name): - if not game_name: - return None - - norm_query = normalize_title(game_name) - - # Exact match - if norm_query in normalized_title_map: - return normalized_title_map[norm_query] - - # Substring match (e.g., "Atelier Sophie 2" in "Atelier Sophie 2: The Alchemist of the Mysterious Dream") - for title_key in normalized_title_map.keys(): - if norm_query in title_key: - return normalized_title_map[title_key] - - # Fuzzy fallback - matches = get_close_matches(norm_query, normalized_title_map.keys(), n=1, cutoff=0.5) - if matches: - return normalized_title_map[matches[0]] - - return None - -# Update all .pchtxt files in formatted/ -def update_pchtxt_headers(formatted_dir): - formatted_path = Path(formatted_dir) - if not formatted_path.exists(): - print(f"❌ Directory not found: {formatted_dir}") - return - - for game_folder in formatted_path.iterdir(): - if not game_folder.is_dir() or " - Graphics Mods" not in game_folder.name: - continue - - game_name = game_folder.name.replace(" - Graphics Mods", "").strip() - if not game_name: - print(f"⚠️ Could not extract game name from folder: {game_folder.name}") - continue - - title_id = get_title_id(game_name) - if not title_id: - print(f"❌ Title ID not found for: {game_name}") - continue - - for pchtxt in game_folder.glob("*.pchtxt"): - try: - with open(pchtxt, "r", encoding="utf-8") as f: - lines = f.readlines() - - # Replace or insert title ID in header line - updated = False - for i, line in enumerate(lines): - if line.startswith("#"): - if "[" in line and "]" in line: - lines[i] = f"# {game_name} [{title_id}]\n" - else: - lines[i] = line.strip() + f" [{title_id}]\n" - updated = True - break - - if not updated: - lines.insert(1, f"# {game_name} [{title_id}]\n") - - with open(pchtxt, "w", encoding="utf-8") as f: - f.writelines(lines) - - print(f"✅ Updated: {pchtxt.name} with [{title_id}]") - except Exception as e: - print(f"❌ Error processing {pchtxt.name}: {e}") - -# Run it -update_pchtxt_headers("formatted") diff --git a/scripts/cucholix/titleid_patcher2.py b/scripts/cucholix/titleid_patcher2.py deleted file mode 100644 index 804a2e6..0000000 --- a/scripts/cucholix/titleid_patcher2.py +++ /dev/null @@ -1,409 +0,0 @@ -#!/usr/bin/env python3 -import os -import json -import re -import unicodedata -import sys -from pathlib import Path -from difflib import get_close_matches - -def load_title_database(db_path="US.en.json"): - """Load the title database from JSON file.""" - try: - with open(db_path, "r", encoding="utf-8") as f: - return json.load(f) - except FileNotFoundError: - print(f"❌ Title database not found: {db_path}") - sys.exit(1) - except json.JSONDecodeError: - print(f"❌ Invalid JSON in title database: {db_path}") - sys.exit(1) - -def load_cnmts_database(cnmts_path="cnmts.json"): - """Load the cnmts database from JSON file.""" - try: - with open(cnmts_path, "r", encoding="utf-8") as f: - return json.load(f) - except FileNotFoundError: - print(f"❌ CNMTS database not found: {cnmts_path}") - sys.exit(1) - except json.JSONDecodeError: - print(f"❌ Invalid JSON in CNMTS database: {cnmts_path}") - sys.exit(1) - -def sanitize_name(name): - """Remove accents and unwanted characters, and replace ' - ' with a single space.""" - normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii') - cleaned = normalized.replace("'", "").replace("'", "").replace("`", "").replace('"', '') - cleaned = cleaned.replace(" - ", " ") - cleaned = ' '.join(cleaned.split()) - return cleaned.strip() - -def capitalize_hyphenated(word): - """Capitalize both parts of a hyphenated word.""" - parts = word.split('-') - capitalized_parts = [] - for part in parts: - if part: - capitalized_parts.append(part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper()) - else: - capitalized_parts.append('') - return '-'.join(capitalized_parts) - -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4", "DOF" -} - -def is_roman_numeral(word): - """Return True if the word is a valid Roman numeral.""" - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - """Title-case with special rules for acronyms, Roman numerals, etc.""" - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - - words = name.split() - result = [] - force_capitalize_mode = False - - for idx, word in enumerate(words): - contains_marker = any(marker in word for marker in subtitle_markers) - split_parts = re.split(r'([:~\-–—])', word) - capitalized_parts = [] - - for part in split_parts: - if part in subtitle_markers: - capitalized_parts.append(part) - force_capitalize_mode = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - def capitalize_special(w): - if w.upper() in ACRONYMS: - return w.upper() - if is_roman_numeral(w): - return w.upper() - for sep in ['&', '+', '|']: - if sep in w: - parts = w.split(sep) - if all(is_roman_numeral(p) for p in parts): - return sep.join(p.upper() for p in parts) - return capitalize_hyphenated(w) - - if force_capitalize_mode or is_first or is_last or (lower_part not in lowercase_exceptions): - sub_parts = part.split('-') - capitalized_sub = [capitalize_special(sp) for sp in sub_parts] - capitalized_parts.append('-'.join(capitalized_sub)) - else: - capitalized_parts.append(lower_part) - - result.append(''.join(capitalized_parts)) - - if not contains_marker: - force_capitalize_mode = False - - if result: - first_parts = result[0].split('-') - result[0] = '-'.join( - sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) - for sp in first_parts - ) - - last_parts = result[-1].split('-') - result[-1] = '-'.join( - sp.upper() if (sp.upper() in ACRONYMS or is_roman_numeral(sp)) else capitalize_hyphenated(sp) - for sp in last_parts - ) - - return ' '.join(result) - -def normalize_title(text): - """Normalize title names for matching.""" - text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii') - text = re.sub(r'[^a-zA-Z0-9 ]', '', text).lower() - return ' '.join(text.split()) - -def create_title_mapping(title_db): - """Create normalized mapping of game name -> title ID.""" - title_map = {} - normalized_title_map = {} - - for entry in title_db.values(): - name = entry.get("name") - tid = entry.get("id") - if name and tid: - title_map[name.strip()] = tid.strip() - normalized_title_map[normalize_title(name)] = tid.strip() - - return title_map, normalized_title_map - -def find_title_id_by_build_id(build_id, cnmts_db, debug=False): - """Find title ID by matching build ID prefix against full build IDs in cnmts.json.""" - if not build_id: - return None, None - - build_id = build_id.upper() - - for title_id, versions in cnmts_db.items(): - for version_num, version_data in versions.items(): - content_entries = version_data.get("contentEntries", []) - for entry in content_entries: - full_build_id = entry.get("buildId") - if full_build_id and entry.get("type") == 1: - # Check if the build ID from .pchtxt matches the start of the full build ID - if full_build_id.upper().startswith(build_id): - if debug: - return title_id.upper(), full_build_id - return title_id.upper(), None - - return None, None - -def get_title_id_from_name(game_name, normalized_title_map): - """Fuzzy match game name to title ID using normalized names.""" - if not game_name: - return None - - norm_query = normalize_title(game_name) - - # Exact match - if norm_query in normalized_title_map: - return normalized_title_map[norm_query] - - # Substring match - for title_key in normalized_title_map.keys(): - if norm_query in title_key: - return normalized_title_map[title_key] - - # Fuzzy fallback - matches = get_close_matches(norm_query, normalized_title_map.keys(), n=1, cutoff=0.5) - if matches: - return normalized_title_map[matches[0]] - - return None - -def extract_game_name_from_folder(folder_name): - """Extract and format game name from folder name.""" - game_name = sanitize_name(folder_name) - - # Remove trailing "Graphics" if it exists - if game_name.endswith("Graphics"): - game_name = game_name[:-len("Graphics")].strip() - - # Apply title case formatting - game_name = title_case_preserve_numbers(game_name) - - return game_name - -def extract_nsobid_from_pchtxt(file_path): - """Extract the nsobid (build ID) from a .pchtxt file.""" - try: - with open(file_path, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if line.startswith("@nsobid-"): - # Extract the build ID after @nsobid- - build_id = line[8:] # Remove "@nsobid-" - return build_id.upper() - return None - except Exception as e: - print(f"❌ Error reading {file_path}: {e}") - return None - -def extract_region_from_filename(filename): - """Extract region code from filename like '1.0.3_US.pchtxt' -> 'US'.""" - # Remove .pchtxt extension - name_without_ext = filename.replace('.pchtxt', '') - - # Look for pattern like _XX where XX is 2-3 letter region code - region_match = re.search(r'_([A-Z]{2,3})$', name_without_ext) - if region_match: - return region_match.group(1) - - return None - -def patch_pchtxt_file(file_path, game_name, title_id): - """Patch a single .pchtxt file with the title ID.""" - try: - with open(file_path, "r", encoding="utf-8") as f: - lines = f.readlines() - - # Find and update the header line (starts with #) - updated = False - for i, line in enumerate(lines): - if line.startswith("#"): - # Check if it already has a title ID in brackets - if "[" in line and "]" in line: - # Replace existing title ID - lines[i] = f"# {game_name} [{title_id}]\n" - else: - # Add title ID to existing header - lines[i] = line.strip() + f" [{title_id}]\n" - updated = True - break - - if not updated: - # Find the line after @nsobid to insert header - nsobid_index = -1 - for i, line in enumerate(lines): - if line.startswith("@nsobid-"): - nsobid_index = i - break - - if nsobid_index >= 0: - # Insert after @nsobid line, but before any empty line - insert_index = nsobid_index + 1 - if insert_index < len(lines) and lines[insert_index].strip() == "": - insert_index += 1 - lines.insert(insert_index, f"# {game_name} [{title_id}]\n") - else: - # Insert at the beginning if no @nsobid found - lines.insert(0, f"# {game_name} [{title_id}]\n") - - with open(file_path, "w", encoding="utf-8") as f: - f.writelines(lines) - - return True - except Exception as e: - print(f"❌ Error processing {file_path}: {e}") - return False - -def patch_original_files_with_buildid(root_folder, title_db_path="US.en.json", cnmts_db_path="cnmts.json"): - """ - Walk through the original folder structure and patch all .pchtxt files - with their corresponding title IDs using build ID mapping. - """ - # Load databases - title_db = load_title_database(title_db_path) - cnmts_db = load_cnmts_database(cnmts_db_path) - - title_map, normalized_title_map = create_title_mapping(title_db) - - print(f"Loaded {len(normalized_title_map)} titles from title database") - print(f"Loaded CNMTS database with {len(cnmts_db)} title entries") - print(f"Scanning folder: {root_folder}\n") - - stats = { - 'processed': 0, - 'patched_by_buildid': 0, - 'patched_by_name': 0, - 'no_buildid': 0, - 'no_title_id': 0, - 'errors': 0, - 'regional_variants': 0 - } - - for current_root, dirs, files in os.walk(root_folder): - # Skip the formatted directory if it exists - if 'formatted' in current_root: - continue - - pchtxt_files = [f for f in files if f.lower().endswith('.pchtxt')] - if not pchtxt_files: - continue - - # Extract game name from folder - folder_name = os.path.basename(current_root) - game_name = extract_game_name_from_folder(folder_name) - - print(f"📁 {folder_name}") - print(f" Game: {game_name}") - - # Group files by region to show regional variants - regional_files = {} - for file in pchtxt_files: - region = extract_region_from_filename(file) - if region: - regional_files[region] = file - stats['regional_variants'] += 1 - - if regional_files: - print(f" 🌍 Regional variants detected: {', '.join(regional_files.keys())}") - - # Process each .pchtxt file in this folder - for file in pchtxt_files: - file_path = os.path.join(current_root, file) - stats['processed'] += 1 - - region = extract_region_from_filename(file) - region_suffix = f" ({region})" if region else "" - - # First, try to get title ID from build ID - build_id = extract_nsobid_from_pchtxt(file_path) - title_id = None - method = None - - if build_id: - title_id, full_build_id = find_title_id_by_build_id(build_id, cnmts_db, debug=True) - if title_id: - method = "build_id" - stats['patched_by_buildid'] += 1 - print(f" 🔍 Build ID match: {build_id} -> {full_build_id}") - else: - print(f" ⚠️ Build ID {build_id} not found in CNMTS database") - else: - stats['no_buildid'] += 1 - print(f" ⚠️ No build ID found in {file}") - - # If build ID lookup failed, try name-based lookup - if not title_id: - title_id = get_title_id_from_name(game_name, normalized_title_map) - if title_id: - method = "name" - stats['patched_by_name'] += 1 - - if title_id: - if patch_pchtxt_file(file_path, game_name, title_id): - print(f" ✅ Patched: {file}{region_suffix} with [{title_id}] (via {method})") - else: - stats['errors'] += 1 - else: - print(f" ❌ No title ID found for: {file}{region_suffix}") - stats['no_title_id'] += 1 - - print() # Empty line for readability - - # Print summary - print("=" * 60) - print("SUMMARY:") - print(f"Files processed: {stats['processed']}") - print(f"Files patched via build ID: {stats['patched_by_buildid']}") - print(f"Files patched via name matching: {stats['patched_by_name']}") - print(f"Regional variant files detected: {stats['regional_variants']}") - print(f"Files with no build ID: {stats['no_buildid']}") - print(f"Files with no title ID found: {stats['no_title_id']}") - print(f"Errors: {stats['errors']}") - print("=" * 60) - -def main(): - if len(sys.argv) not in [2, 3, 4]: - print("Usage: python patch_buildid_titleids.py /path/to/root/folder [path/to/US.en.json] [path/to/cnmts.json]") - print("If database paths are not provided, it will look for 'US.en.json' and 'cnmts.json' in the current directory.") - sys.exit(1) - - root_folder = sys.argv[1] - title_db_path = sys.argv[2] if len(sys.argv) >= 3 else "US.en.json" - cnmts_db_path = sys.argv[3] if len(sys.argv) == 4 else "cnmts.json" - - if not os.path.exists(root_folder): - print(f"❌ Root folder not found: {root_folder}") - sys.exit(1) - - patch_original_files_with_buildid(root_folder, title_db_path, cnmts_db_path) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/theboy181/README.md b/scripts/theboy181/README.md deleted file mode 100644 index 4049994..0000000 --- a/scripts/theboy181/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This script is used to format the pchtxt heirarchy appropriately for the following repository: - -https://github.com/theboy181/switch-ptchtxt-mods diff --git a/scripts/theboy181/format_repo4.py b/scripts/theboy181/format_repo4.py deleted file mode 100644 index e70edc2..0000000 --- a/scripts/theboy181/format_repo4.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env python3 -import os -import shutil -import re -import sys -import rarfile -import unicodedata - -# ----- Normalization & Title‐Casing Helpers ----- - -def sanitize_name(name): - """ - Remove accents and unwanted characters, replace ' - ' with a single space, - remove extra quotes/apostrophes, collapse multiple spaces. - """ - normalized = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore').decode('ascii') - cleaned = normalized.replace("'", "").replace("’", "").replace("`", "").replace('"', "") - cleaned = cleaned.replace(" - ", " ") - cleaned = ' '.join(cleaned.split()) - return cleaned.strip() - -def capitalize_hyphenated(word): - """ - Capitalize both parts of a hyphenated word. E.g. "yooka-laylee" → "Yooka-Laylee". - """ - parts = word.split('-') - capitalized = [] - for part in parts: - if part: - capitalized.append(part[0].upper() + part[1:].lower() if len(part) > 1 else part.upper()) - else: - capitalized.append('') - return '-'.join(capitalized) - -# Regex for Roman numerals (supports up to 3999) -ROMAN_NUMERAL_PATTERN = re.compile( - r"^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$", - re.IGNORECASE -) - -# Known acronyms to force‐uppercase exactly -ACRONYMS = { - "HD", "2D", "3D", "4K", "VR", "AI", "API", "USB", "CPU", "GPU", "DVD", "CD", - "RPG", "FPS", "MMO", "MMORPG", "LAN", "GUI", "NPC", - "FFVII", "FFVIII", "FFIX", "FFX", "FFXII", - "FX", "2K", "5K", "8K", "V1", "V2", "V3", "V4", "DOF" -} - -def is_roman_numeral(word): - """ - Return True if `word` is a valid Roman numeral (case‐insensitive). - """ - return bool(ROMAN_NUMERAL_PATTERN.match(word)) - -def title_case_preserve_numbers(name): - """ - Title‐case `name` with these rules: - • Fully uppercase acronyms remain unchanged (e.g. HD, 2D, 3D, FFVII, etc.). - • Roman numerals become fully uppercase (e.g. 'iii' → 'III', 'xI' → 'XI'). - • Hyphenated words are capitalized on both sides (→ 'Yooka-Laylee'). - • Small filler words (a, an, and, the, of, in, etc.) become lowercase - only if they appear in the middle and are not immediately after a subtitle marker, - except the first and last words (always capitalized). - • After a subtitle marker (":", "~", "–", "—", or "-"), force capitalization - on all subsequent words until the next subtitle marker or the end. - • Compound Roman numerals joined by "&", "+", or "|" become fully uppercase - (e.g. "I&ii" → "I&II"). - """ - lowercase_exceptions = { - "a", "an", "and", "as", "at", "but", "by", "for", "from", - "in", "nor", "of", "on", "or", "so", "the", "to", "with", "yet" - } - subtitle_markers = {":", "~", "-", "–", "—"} - - words = name.split() - result = [] - force_capitalize_mode = False - - for idx, raw_word in enumerate(words): - # Check if this raw_word contains any subtitle marker (to force‐caps afterward) - contains_marker = any(marker in raw_word for marker in subtitle_markers) - - # Split on any subtitle marker but keep the markers themselves - split_parts = re.split(r'([:~\-–—])', raw_word) - compounded = [] - - for part in split_parts: - if part in subtitle_markers: - # Keep the marker, then force‐capitalize subsequent parts - compounded.append(part) - force_capitalize_mode = True - continue - - lower_part = part.lower() - is_first = (idx == 0) - is_last = (idx == len(words) - 1) - - def cap_one(subword): - # If it's an acronym, uppercase it - if subword.upper() in ACRONYMS: - return subword.upper() - # If it's a Roman numeral, uppercase it - if is_roman_numeral(subword): - return subword.upper() - # If it’s a compound Roman numeral joined by &, +, or | - for sep in ("&", "+", "|"): - if sep in subword: - pieces = subword.split(sep) - if all(is_roman_numeral(p) for p in pieces): - return sep.join(p.upper() for p in pieces) - # Otherwise just capitalize hyphenated segments - return capitalize_hyphenated(subword) - - if force_capitalize_mode or is_first or is_last or (lower_part not in lowercase_exceptions): - # Split hyphens, apply cap_one to each - subs = part.split('-') - compounded.append("-".join(cap_one(s) for s in subs)) - else: - # Middle filler word → keep lowercase - compounded.append(lower_part) - - result.append("".join(compounded)) - # If this raw_word did not contain a subtitle marker, exit force‐capitalize - if not contains_marker: - force_capitalize_mode = False - - # Finally, ALWAYS capitalize the very first and very last words (same rules) - if result: - first_split = result[0].split("-") - new_first = [] - for p in first_split: - if p.upper() in ACRONYMS or is_roman_numeral(p): - new_first.append(p.upper()) - else: - new_first.append(capitalize_hyphenated(p)) - result[0] = "-".join(new_first) - - last_split = result[-1].split("-") - new_last = [] - for p in last_split: - if p.upper() in ACRONYMS or is_roman_numeral(p): - new_last.append(p.upper()) - else: - new_last.append(capitalize_hyphenated(p)) - result[-1] = "-".join(new_last) - - return " ".join(result) - -def clean_title(name): - """ - Combine sanitize_name() + title_case_preserve_numbers() into one call. - """ - return title_case_preserve_numbers(sanitize_name(name)) - -def transform_game_name_raw(raw_game): - """ - Move ", The" to the front and remove any stray colons: - e.g. "Skyrim, The" → "The Skyrim" - """ - if ", The" in raw_game: - parts = raw_game.split(", The") - raw_game = f"The {parts[0]}{parts[1]}" - raw_game = raw_game.replace(":", "") # strip out colons - raw_game = raw_game.replace(" - ", " ") # remove literal “ - ” - return raw_game - -def extract_rar_files(folder_path): - """ - Only extract top‐level archives matching "release_*.rar" in the root folder_path. - Do NOT dive into subfolders (so we skip those tiny per‐mod RARs). - """ - # We look at *only* the immediate children of `folder_path`. - for item in os.listdir(folder_path): - full = os.path.join(folder_path, item) - if not os.path.isfile(full): - continue - - # Only process those .rar that match "release_*.rar" at the top level - if item.lower().startswith("release_") and item.lower().endswith(".rar"): - try: - with rarfile.RarFile(full) as rf: - rf.extractall(folder_path) - print(f"Extracted top‐level archive: {full}") - except rarfile.Error as e: - print(f"❌ Failed to extract {full}: {e}") - -def get_game_name_and_mod_name(path, root_dir): - """ - Given a folder `path` containing a .pchtxt, return (game_name, mod_name). - 1) game_name ← first‐level folder under root_dir, strip bracketed tags, move ", The", - remove colons, possibly append "(Country)", then run clean_title(...). - 2) mod_name ← if 'Aspect Ratio' in path → "Aspect Ratio "; - else if last folder ends in " v" → " "; - else immediate parent folder. - Afterwards, replace ' / ` → ".", "21-9" → "21.9", remove colons, - handle "Trailblazers" → "4K", then run clean_title(...). - """ - relative = os.path.relpath(path, root_dir) - parts = relative.split(os.sep) - - # --- raw_game_name logic --- - raw_game = parts[0] - raw_game = re.sub(r'\[.*?\]', '', raw_game).strip() - raw_game = transform_game_name_raw(raw_game) - - # check for country code deeper in path - country = None - for p in parts[1:]: - if re.search(r'\[.*?\]', p): - country = re.sub(r'\[.*?\]', '', p).strip() - break - if country: - raw_game = f"{raw_game} ({country})" - - game_name = clean_title(raw_game) - - # --- raw_mod_name logic --- - if "Aspect Ratio" in relative: - aspect_folder = os.path.basename(path) - raw_mod = f"Aspect Ratio {aspect_folder}" - else: - last_folder = parts[-1] - if re.search(r' v\d+', last_folder): - parent_folder = parts[-2] - raw_mod = f"{parent_folder} {last_folder}" - else: - raw_mod = parts[-2] if len(parts) > 1 else "" - - raw_mod = raw_mod.strip() - raw_mod = raw_mod.replace("'", ".").replace("`", ".") - raw_mod = raw_mod.replace("21-9", "21.9") - raw_mod = raw_mod.replace(":", "") - if raw_mod == "Trailblazers": - raw_mod = "4K" - - mod_name = clean_title(raw_mod) if raw_mod else "" - return game_name, mod_name - -def create_formatted_structure(folder_path): - """ - 1) extract_rar_files(folder_path) # only top‐level releases - 2) walk every subfolder for .pchtxt - 3) for each .pchtxt, compute (game_name, mod_name) with get_game_name_and_mod_name - 4) copy into formatted/" - "/".pchtxt" - """ - extract_rar_files(folder_path) - - formatted_path = os.path.join(folder_path, "formatted") - os.makedirs(formatted_path, exist_ok=True) - - for root, dirs, files in os.walk(folder_path): - # Skip anything already under “formatted” - if "formatted" in root.split(os.sep): - continue - - for f in files: - if not f.lower().endswith(".pchtxt"): - continue - - game_name, mod_name = get_game_name_and_mod_name(root, folder_path) - version = f[:-len(".pchtxt")].strip() - - combined = f"{game_name} - {mod_name}".strip() - target_dir = os.path.join(formatted_path, combined) - os.makedirs(target_dir, exist_ok=True) - - src = os.path.join(root, f) - dst = os.path.join(target_dir, f"{version}.pchtxt") - shutil.copy(src, dst) - print(f"Copied {src} → {dst}") - - print("\nAll files have been organized successfully.") - -def main(folder_path): - create_formatted_structure(folder_path) - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python format_repo_4.py /path/to/folder/") - sys.exit(1) - - folder_path = sys.argv[1] - main(folder_path)