Last active
May 26, 2025 04:48
-
-
Save CodeZombie/427148ff6f1b2f9810ff1b5d813eec1b to your computer and use it in GitHub Desktop.
Godot UID conflict fixer
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 argparse | |
import random | |
import string | |
def generate_new_uid(all_known_uids_set): | |
""" | |
Generates a new unique Godot-like UID (uid:// followed by 12 lowercase alphanumeric chars). | |
Args: | |
all_known_uids_set (set): A set of all UIDs currently known (original + newly generated) | |
to avoid collisions. | |
Returns: | |
tuple: (new_full_uid, new_id_part) | |
new_full_uid is like "uid://abc123xyz789" | |
new_id_part is like "abc123xyz789" | |
""" | |
characters = string.ascii_lowercase + string.digits | |
while True: | |
new_id_part = ''.join(random.choices(characters, k=12)) | |
new_full_uid = f"uid://{new_id_part}" | |
if new_full_uid not in all_known_uids_set: | |
all_known_uids_set.add(new_full_uid) # Add to the set to prevent reuse in the same run | |
return new_full_uid, new_id_part | |
def find_and_optionally_fix_uid_conflicts(folder_path, fix_conflicts): | |
""" | |
Recursively reads a folder to identify UID conflicts in .tres and .tscn files, | |
and optionally fixes them by assigning new UIDs. | |
Args: | |
folder_path (str): The path to the folder to scan. | |
fix_conflicts (bool): Whether to attempt to fix found conflicts. | |
""" | |
uid_map = {} | |
initial_conflicts_found = False | |
files_modified_count = 0 | |
# Regex to capture UIDs from both .tres and .tscn files | |
uid_regex_capture = re.compile(r'uid="uid://([^"]+)"') | |
print(f"Scanning folder: {folder_path}\n") | |
for root, _, files in os.walk(folder_path): | |
for filename in files: | |
if filename.endswith(".tres") or filename.endswith(".tscn"): | |
filepath = os.path.join(root, filename) | |
try: | |
with open(filepath, 'r', encoding='utf-8') as f: | |
first_line = f.readline() | |
match = uid_regex_capture.search(first_line) | |
if match: | |
uid_part = match.group(1) | |
full_uid = f"uid://{uid_part}" | |
if full_uid in uid_map: | |
uid_map[full_uid].append(filepath) | |
else: | |
uid_map[full_uid] = [filepath] | |
# else: | |
# print(f"DEBUG: No UID found in header of: {filepath}") | |
except Exception as e: | |
print(f"Error reading file {filepath}: {e}") | |
print("--- UID Conflict Report ---") | |
conflicting_uids_details = {} | |
for uid, paths in uid_map.items(): | |
if len(paths) > 1: | |
initial_conflicts_found = True | |
conflicting_uids_details[uid] = paths | |
print(f"\n🚨 Conflict for UID: {uid}") | |
for path in paths: | |
print(f" -> {path}") | |
if not initial_conflicts_found: | |
print("\n✅ No UID conflicts found.") | |
else: | |
print(f"\nFound {len(conflicting_uids_details)} UID(s) with conflicts.") | |
if initial_conflicts_found and fix_conflicts: | |
print("\n--- Attempting to Fix Conflicts ---") | |
# Create a set of all UIDs currently in the project for new UID generation | |
all_project_uids = set(uid_map.keys()) | |
for conflicting_uid_full, paths in conflicting_uids_details.items(): | |
if len(paths) <= 1: # Should not happen if it's in conflicting_uids_details | |
continue | |
file_to_keep_uid = paths[0] | |
files_to_change_uid = paths[1:] | |
original_uid_part = conflicting_uid_full.split("://")[1] | |
print(f"\nResolving conflict for UID: {conflicting_uid_full}") | |
print(f" Keeping UID for: {file_to_keep_uid}") | |
for filepath_to_change in files_to_change_uid: | |
try: | |
new_full_uid, new_id_part = generate_new_uid(all_project_uids) | |
with open(filepath_to_change, 'r', encoding='utf-8') as f_read: | |
lines = f_read.readlines() | |
if not lines: | |
print(f" ❌ Error: File {filepath_to_change} is empty. Skipping.") | |
continue | |
first_line_content = lines[0] | |
# Pattern to match the specific UID to be replaced | |
# Ensure we only replace the exact conflicting UID string | |
regex_to_replace = f'uid="uid://{original_uid_part}"' | |
replacement_string = f'uid="uid://{new_id_part}"' | |
modified_first_line, num_replacements = re.subn( | |
regex_to_replace, | |
replacement_string, | |
first_line_content, | |
count=1 # Only replace the first occurrence in the line | |
) | |
if num_replacements > 0: | |
with open(filepath_to_change, 'w', encoding='utf-8') as f_write: | |
f_write.write(modified_first_line) | |
f_write.writelines(lines[1:]) | |
print(f" ✅ Changed UID in: {filepath_to_change}") | |
print(f" Old UID: {conflicting_uid_full}") | |
print(f" New UID: {new_full_uid}") | |
files_modified_count += 1 | |
else: | |
print(f" ⚠️ Warning: UID {conflicting_uid_full} not found as expected in header of {filepath_to_change}. File not modified.") | |
print(f" Expected pattern in line: {regex_to_replace}") | |
print(f" Actual line content: {first_line_content.strip()}") | |
except Exception as e: | |
print(f" ❌ Error processing file {filepath_to_change} for fix: {e}") | |
if files_modified_count > 0: | |
print(f"\nSuccessfully modified {files_modified_count} file(s).") | |
else: | |
print("\nNo files were modified during the fix attempt.") | |
if files_modified_count > 0: | |
cache_file_path = os.path.join(folder_path, ".godot", "uid_cache.bin") | |
print(f"\nAttempting to delete UID cache file: {cache_file_path}") | |
if os.path.exists(cache_file_path): | |
try: | |
os.remove(cache_file_path) | |
print(" ✅ Successfully deleted .godot/uid_cache.bin") | |
except Exception as e: | |
print(f" ❌ Error deleting .godot/uid_cache.bin: {e}") | |
else: | |
print(" ℹ️ .godot/uid_cache.bin not found. Please delete it manually, if needed.") | |
print("\n--- End of Report ---") | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser( | |
description="Scans a Godot project folder for UID conflicts in .tres and .tscn files and optionally fixes them.", | |
formatter_class=argparse.RawTextHelpFormatter, | |
epilog=""" | |
Examples: | |
Scan only: | |
python uid_checker.py /path/to/your/godot_project | |
Scan and fix conflicts: | |
python uid_checker.py /path/to/your/godot_project --fix | |
Notes: | |
- The script assumes UIDs are in the first line of .tres and .tscn files. | |
- When fixing, the first file found with a conflicting UID keeps its original UID. | |
Subsequent files with the same UID will be assigned new, unique UIDs. | |
- The .godot/uid_cache.bin file (relative to the scanned folder) will be deleted if any UIDs are changed. | |
It's recommended to run this script from the root of your Godot project. | |
""" | |
) | |
parser.add_argument( | |
"folder", | |
help="The root folder of the Godot project to scan." | |
) | |
parser.add_argument( | |
"--fix", | |
action="store_true", | |
help="Attempt to fix UID conflicts by assigning new UIDs to subsequent conflicting files." | |
) | |
args = parser.parse_args() | |
if os.path.isdir(args.folder): | |
find_and_optionally_fix_uid_conflicts(args.folder, args.fix) | |
else: | |
print(f"Error: Folder not found or is not a directory: {args.folder}") | |
parser.print_help() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This python script searches through your Godot project directory and creates a UID conflict report. This will tell you if two or more resource or scene files share the same UID, which if the case, will cause major headaches in your project.
If you run the script with the
--fix
flag, the script will attempt to give new, unique UIDs to any files with conflicts present.