Created
April 15, 2026 19:08
-
-
Save I-No-oNe/077da4444b2a0325e3c4a39a83057ef8 to your computer and use it in GitHub Desktop.
MixinUpdater.py By Claude for porting from mc 1.21.11 to 26.1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import re | |
| import json | |
| # ── Helpers ─────────────────────────────────────────────────────────────────── | |
| def expected_class_name(mixin_target: str) -> str: | |
| return f"Mixin{mixin_target}" | |
| def find_mixin_json(root: str) -> list[str]: | |
| """Auto-discover *mixin*.json files under root.""" | |
| found = [] | |
| for dirpath, _, files in os.walk(root): | |
| for f in files: | |
| if "mixin" in f.lower() and f.endswith(".json"): | |
| found.append(os.path.join(dirpath, f)) | |
| return found | |
| def collect_registered_classes(json_paths: list[str]) -> set[str]: | |
| """ | |
| Return the set of bare class names (no package prefix) referenced | |
| across all mixin JSON files, across the 'mixins', 'client', and 'server' keys. | |
| """ | |
| registered: set[str] = set() | |
| for jp in json_paths: | |
| try: | |
| with open(jp, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| except (json.JSONDecodeError, OSError): | |
| continue | |
| for key in ("mixins", "client", "server"): | |
| for entry in data.get(key, []): | |
| registered.add(entry.rsplit(".", 1)[-1]) | |
| return registered | |
| # ── File-level fix ──────────────────────────────────────────────────────────── | |
| def fix_java_file(filepath: str) -> list[tuple[str, str]]: | |
| """ | |
| Parse a Java file, fix class names that don't match @Mixin(X.class) -> MixinX, | |
| rename the file if needed, and return a list of (old_name, new_name) renames. | |
| """ | |
| with open(filepath, "r", encoding="utf-8") as f: | |
| original = f.read() | |
| lines = original.splitlines() | |
| new_lines = lines[:] | |
| renames: list[tuple[str, str]] = [] | |
| i = 0 | |
| while i < len(lines): | |
| mixin_match = re.search(r'@Mixin\((\w+)\.class\)', lines[i].strip()) | |
| if mixin_match: | |
| target = mixin_match.group(1) | |
| expected = expected_class_name(target) | |
| for j in range(i + 1, min(i + 4, len(lines))): | |
| class_match = re.search(r'\bclass\s+(\w+)', lines[j]) | |
| if class_match: | |
| actual = class_match.group(1) | |
| if actual != expected: | |
| print(f"\n [MISMATCH] line {j + 1}") | |
| print(f" @Mixin target : {target}") | |
| print(f" Class found : {actual}") | |
| print(f" Expected : {expected}") | |
| new_lines = [l.replace(actual, expected) for l in new_lines] | |
| renames.append((actual, expected)) | |
| else: | |
| print(f" [OK] line {j + 1} — {actual} matches") | |
| break | |
| i += 1 | |
| new_content = "\n".join(new_lines) | |
| if new_content != original: | |
| with open(filepath, "w", encoding="utf-8") as f: | |
| f.write(new_content) | |
| # Rename the .java file itself if the class name changed | |
| for old_name, new_name in renames: | |
| basename = os.path.basename(filepath) | |
| if old_name in basename: | |
| new_basename = basename.replace(old_name, new_name) | |
| new_filepath = os.path.join(os.path.dirname(filepath), new_basename) | |
| os.rename(filepath, new_filepath) | |
| print(f" ✔ File renamed : {basename} -> {new_basename}") | |
| filepath = new_filepath | |
| break | |
| return renames | |
| # ── JSON fix ────────────────────────────────────────────────────────────────── | |
| def fix_mixin_json(json_path: str, all_renames: dict[str, str]): | |
| """Replace old class names with new names inside the mixin JSON.""" | |
| with open(json_path, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| changed = False | |
| def fix_list(lst: list) -> list: | |
| nonlocal changed | |
| result = [] | |
| for entry in lst: | |
| parts = entry.rsplit(".", 1) | |
| class_part = parts[-1] | |
| prefix = parts[0] + "." if len(parts) == 2 else "" | |
| if class_part in all_renames: | |
| new_entry = prefix + all_renames[class_part] | |
| print(f" [JSON] '{entry}' -> '{new_entry}'") | |
| result.append(new_entry) | |
| changed = True | |
| else: | |
| result.append(entry) | |
| return result | |
| for key in ("mixins", "client", "server"): | |
| if key in data and isinstance(data[key], list): | |
| data[key] = fix_list(data[key]) | |
| if changed: | |
| with open(json_path, "w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=2) | |
| print(f" ✔ JSON updated : {json_path}") | |
| else: | |
| print(f" [JSON] No changes needed in {os.path.basename(json_path)}") | |
| # ── Unused file deletion ────────────────────────────────────────────────────── | |
| def delete_unused_java_files(java_files: list[str], json_paths: list[str], dry_run: bool = False): | |
| """ | |
| Delete any .java mixin file whose class name is not referenced in any | |
| mixin JSON. Only files that contain an @Mixin annotation are considered | |
| candidates — plain helper/utility Java files are left alone. | |
| Returns the list of deleted (or would-be-deleted) file paths. | |
| """ | |
| if not json_paths: | |
| print("\n[UNUSED] No JSON files available — skipping unused-file check.") | |
| return [] | |
| registered = collect_registered_classes(json_paths) | |
| deleted: list[str] = [] | |
| print(f"\n{'=' * 50}") | |
| print("Checking for unused mixin Java files...\n") | |
| for filepath in java_files: | |
| # Only inspect files that actually declare a @Mixin — avoids deleting | |
| # base classes, interfaces, or utilities that share the directory. | |
| try: | |
| with open(filepath, "r", encoding="utf-8") as f: | |
| source = f.read() | |
| except OSError: | |
| continue | |
| if "@Mixin(" not in source: | |
| continue | |
| # Extract the declared class name from the file | |
| class_match = re.search(r'\bclass\s+(\w+)', source) | |
| if not class_match: | |
| continue | |
| class_name = class_match.group(1) | |
| if class_name not in registered: | |
| tag = "[DRY-RUN]" if dry_run else "[DELETE]" | |
| print(f" {tag} {filepath}") | |
| print(f" Class '{class_name}' is not referenced in any mixin JSON.") | |
| deleted.append(filepath) | |
| if not dry_run: | |
| os.remove(filepath) | |
| print(f" ✔ Deleted.") | |
| if not deleted: | |
| print(" No unused mixin files found.") | |
| return deleted | |
| # ── Main ────────────────────────────────────────────────────────────────────── | |
| def main(): | |
| # 1. Java source directory | |
| java_dir = input("Enter path to Java source directory: ").strip() | |
| if not os.path.isdir(java_dir): | |
| print(f"Error: '{java_dir}' is not a valid directory.") | |
| return | |
| # 2. Mixin JSON — auto-discover or manual | |
| discovered = find_mixin_json(java_dir) | |
| json_paths: list[str] = [] | |
| if discovered: | |
| print(f"\nFound {len(discovered)} mixin JSON file(s):") | |
| for idx, p in enumerate(discovered, 1): | |
| print(f" [{idx}] {p}") | |
| choice = input("Use these? (y / or enter a custom path): ").strip() | |
| if choice.lower() == "y": | |
| json_paths = discovered | |
| elif os.path.isfile(choice): | |
| json_paths = [choice] | |
| else: | |
| print("Skipping JSON update.") | |
| else: | |
| manual = input("No mixin JSON auto-detected. Enter path manually (or leave blank to skip): ").strip() | |
| if manual and os.path.isfile(manual): | |
| json_paths = [manual] | |
| elif manual: | |
| print(f"Warning: '{manual}' not found — skipping JSON update.") | |
| # 3. Scan + fix Java files | |
| java_files = [] | |
| for root, _, files in os.walk(java_dir): | |
| for f in files: | |
| if f.endswith(".java"): | |
| java_files.append(os.path.join(root, f)) | |
| if not java_files: | |
| print("No Java files found.") | |
| return | |
| print(f"\nScanning {len(java_files)} Java file(s)...\n" + "=" * 50) | |
| all_renames: dict[str, str] = {} | |
| for filepath in java_files: | |
| print(f"\n{filepath}") | |
| renames = fix_java_file(filepath) | |
| for old, new in renames: | |
| all_renames[old] = new | |
| # 4. Fix JSON files | |
| if json_paths and all_renames: | |
| print(f"\n{'=' * 50}\nUpdating mixin JSON file(s)...\n") | |
| for jp in json_paths: | |
| fix_mixin_json(jp, all_renames) | |
| # 5. Re-collect the current java file list (names may have changed after renaming) | |
| java_files_after = [] | |
| for root, _, files in os.walk(java_dir): | |
| for f in files: | |
| if f.endswith(".java"): | |
| java_files_after.append(os.path.join(root, f)) | |
| # 6. Dry-run preview of unused files, then confirm deletion | |
| if json_paths: | |
| would_delete = delete_unused_java_files(java_files_after, json_paths, dry_run=True) | |
| if would_delete: | |
| confirm = input( | |
| f"\n{len(would_delete)} unused file(s) listed above. Delete them? (yes / no): " | |
| ).strip().lower() | |
| if confirm == "yes": | |
| delete_unused_java_files(java_files_after, json_paths, dry_run=False) | |
| else: | |
| print("Skipping deletion.") | |
| # 7. Summary | |
| print(f"\n{'=' * 50}") | |
| if all_renames: | |
| print(f"Done. {len(all_renames)} rename(s) applied:") | |
| for old, new in all_renames.items(): | |
| print(f" {old} -> {new}") | |
| else: | |
| print("Done. No mismatches found — everything looks correct!") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
W