From 1db6ae9329121d115fd20d365c9eee356a912188 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Thu, 2 Jul 2026 23:47:53 +0200 Subject: [PATCH] Improve script decompilation original filename heuristics --- .../GodotMonoDecomp/GodotStuff.cs | 187 +++++++++++++----- utility/import_exporter.cpp | 7 + 2 files changed, 142 insertions(+), 52 deletions(-) diff --git a/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs b/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs index 563834dcc..75a691c88 100644 --- a/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs +++ b/godot-mono-decomp/GodotMonoDecomp/GodotStuff.cs @@ -599,48 +599,144 @@ string GetAutoFileNameForHandle(TypeDefinitionHandle h) } } - string GetPathFromOriginalFiles(string file_path) + string StripLeadingSortablePrefix(string fileStem) + { + int index = 0; + while (index < fileStem.Length && char.IsDigit(fileStem[index])) + { + index++; + } + if (index > 0 && index < fileStem.Length && fileStem[index] == '_') + { + return fileStem[(index + 1)..]; + } + return fileStem; + } + + bool IsOriginalFileNameVariant(string originalFilePath, string generatedFilePath) { - // otherwise, try to find it in the original directory files - string scriptPath = ""; - // empty vector of strings + var originalFileName = Path.GetFileName(originalFilePath); + var generatedFileName = Path.GetFileName(generatedFilePath); + if (!Path.GetExtension(originalFileName).Equals(Path.GetExtension(generatedFileName), StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var originalStem = StripLeadingSortablePrefix(Path.GetFileNameWithoutExtension(originalFileName)); + var generatedStem = Path.GetFileNameWithoutExtension(generatedFileName); + return originalStem.Equals(generatedStem, StringComparison.OrdinalIgnoreCase) || + originalStem.EndsWith("." + generatedStem, StringComparison.OrdinalIgnoreCase); + } + + int GetCommonPathSuffixScore(string originalFilePath, string generatedFilePath) + { + var originalParts = _NormalizePath(originalFilePath).Split('/', StringSplitOptions.RemoveEmptyEntries); + var generatedParts = _NormalizePath(generatedFilePath).Split('/', StringSplitOptions.RemoveEmptyEntries); + int score = 0; + int originalIndex = originalParts.Length - 1; + int generatedIndex = generatedParts.Length - 1; + while (originalIndex >= 0 && generatedIndex >= 0 && originalParts[originalIndex].Equals(generatedParts[generatedIndex], StringComparison.OrdinalIgnoreCase)) + { + score++; + originalIndex--; + generatedIndex--; + } + return score; + } + + string PickUniqueOriginalPath(List possibles, string file_path) + { + if (possibles.Count == 0) + { + return ""; + } + if (possibles.Count == 1) + { + return possibles[0]; + } + + var exactPathMatches = possibles.Where(f => f.EndsWith(file_path, StringComparison.OrdinalIgnoreCase)).ToList(); + if (exactPathMatches.Count == 1) + { + return exactPathMatches[0]; + } + if (exactPathMatches.Count > 1) + { + possibles = exactPathMatches; + } + + var fileDir = _NormalizePath(Path.GetDirectoryName(file_path) ?? ""); + var sameDirMatches = possibles.Where(f => _NormalizePath(Path.GetDirectoryName(f) ?? "").Equals(fileDir, StringComparison.OrdinalIgnoreCase)).ToList(); + if (sameDirMatches.Count == 1) + { + return sameDirMatches[0]; + } + if (sameDirMatches.Count > 1) + { + possibles = sameDirMatches; + } + + var suffixMatches = possibles + .Select(f => new { Path = f, Score = GetCommonPathSuffixScore(f, file_path) }) + .Where(match => match.Score > 1) + .OrderByDescending(match => match.Score) + .ToList(); + if (suffixMatches.Count > 0 && suffixMatches.Count(match => match.Score == suffixMatches[0].Score) == 1) + { + return suffixMatches[0].Path; + } + + return possibles.Count > 1 ? "" : ""; + } + + List GetOriginalPathCandidates(string file_path) + { + var fileName = Path.GetFileName(file_path); var possibles = filesInOriginal.Where(f => - Path.GetFileName(f) == Path.GetFileName(file_path) + Path.GetFileName(f).Equals(fileName, StringComparison.Ordinal) && !IsInExcludedSubdir(f) ) .ToList(); - - if (possibles.Count == 0) + if (possibles.Count > 0) { - possibles = filesInOriginal.Where(f => - Path.GetFileName(f).ToLower() == Path.GetFileName(file_path).ToLower() - && !IsInExcludedSubdir(f) - ).ToList(); + return possibles; } - if (possibles.Count == 1) + possibles = filesInOriginal.Where(f => + Path.GetFileName(f).Equals(fileName, StringComparison.OrdinalIgnoreCase) + && !IsInExcludedSubdir(f) + ).ToList(); + if (possibles.Count > 0) { - scriptPath = possibles[0]; + return possibles; } - else if (possibles.Count > 1) + + return filesInOriginal.Where(f => + IsOriginalFileNameVariant(f, file_path) + && !IsInExcludedSubdir(f) + ).ToList(); + } + + string PickUniqueNamespaceDirectoryCandidate(List possibles, HashSet directories) + { + if (possibles.Count <= 1 || directories.Count == 0) { - possibles = possibles.Where(f => f.EndsWith(file_path, StringComparison.OrdinalIgnoreCase)).ToList(); - if (possibles.Count == 1) - { - scriptPath = possibles[0]; - } - } - if (string.IsNullOrEmpty(scriptPath) && possibles.Count > 1){ - return ""; + return ""; } - return scriptPath; + var normalizedDirectories = new HashSet(directories.Select(_NormalizePath), StringComparer.OrdinalIgnoreCase); + var namespaceDirMatches = possibles.Where(f => normalizedDirectories.Contains(_NormalizePath(Path.GetDirectoryName(f) ?? ""))).ToList(); + return namespaceDirMatches.Count == 1 ? namespaceDirMatches[0] : ""; + } + + string GetPathFromOriginalFiles(string file_path) + { + return PickUniqueOriginalPath(GetOriginalPathCandidates(file_path), file_path); } var potentialMap = new Dictionary>(); var processAgainAgain = new HashSet(); - var dupes = new HashSet(); foreach (var h in processAgain) { @@ -654,14 +750,7 @@ string GetPathFromOriginalFiles(string file_path) } potentialMap[real_path].Add(h); } else { - if (real_path == "" && !dupes.Contains(h)) - { - dupes.Add(h); - } - else - { - processAgainAgain.Add(h); - } + processAgainAgain.Add(h); } } @@ -699,10 +788,11 @@ HashSet GetNamespaceDirectories(string ns) var type = metadata.GetTypeDefinition(h); var ns = metadata.GetString(type.Namespace); var auto_path = GetAutoFileNameForHandle(h); + var originalPathCandidates = GetOriginalPathCandidates(auto_path); string? p = null; var namespaceParts = ns.Split('.'); var parentNamespace = ns.Contains('.') ? string.Join('.', namespaceParts.Take(namespaceParts.Length - 1)) : ""; - if (ns != "" && ns != null) + if (!string.IsNullOrEmpty(ns)) { // pop off the first part of the path, if necessary var fileStem = Common.RemoveNamespacePartOfPath(auto_path, ns); @@ -711,8 +801,13 @@ HashSet GetNamespaceDirectories(string ns) fileStem = Path.GetFileName(auto_path); } var directories = GetNamespaceDirectories(ns).Where(d => !string.IsNullOrEmpty(d) && !IsInExcludedSubdir(d)).ToHashSet(); + var namespaceCandidate = PickUniqueNamespaceDirectoryCandidate(originalPathCandidates, directories); + if (!string.IsNullOrEmpty(namespaceCandidate)) + { + p = namespaceCandidate; + } - if (directories.Count == 1) + if (string.IsNullOrEmpty(p) && directories.Count == 1) { p = _PathCombine(directories.First(), fileStem); } @@ -727,9 +822,14 @@ HashSet GetNamespaceDirectories(string ns) } if (!string.IsNullOrEmpty(parentNamespace)){ var parentDirectories = GetNamespaceDirectories(parentNamespace).Where(d => !string.IsNullOrEmpty(d) && !IsInExcludedSubdir(d)).ToHashSet(); + namespaceCandidate = PickUniqueNamespaceDirectoryCandidate(originalPathCandidates, parentDirectories); var child = string.Join('/', namespaceParts.TakeLast(i)); fileStem = _PathCombine(child, Path.GetFileName(auto_path)); - if (parentDirectories.Count == 1) + if (!string.IsNullOrEmpty(namespaceCandidate)) + { + p = namespaceCandidate; + } + else if (parentDirectories.Count == 1) { p = _PathCombine(parentDirectories.First(), fileStem); } @@ -757,23 +857,6 @@ HashSet GetNamespaceDirectories(string ns) p = _NormalizePath(p); PlaceHandleAtPath(p, h); } - - foreach (var h in dupes) - { - var auto_path = GetAutoFileNameForHandle(h); - var scriptPath = GetPathFromOriginalFiles(auto_path); - - if (scriptPath == "" || scriptPath == "") - { - scriptPath = auto_path; - } - scriptPath = _NormalizePath(scriptPath); - if (IsInExcludedSubdir(scriptPath)) - { - scriptPath = _PathCombine(default_dir, scriptPath); - } - PlaceHandleAtPath(scriptPath, h); - } var caselessDict = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var pair in fileMap) { diff --git a/utility/import_exporter.cpp b/utility/import_exporter.cpp index d3f9e9e99..2052831dc 100644 --- a/utility/import_exporter.cpp +++ b/utility/import_exporter.cpp @@ -2324,6 +2324,13 @@ String ImportExporterReport::get_report_string() { report += info->get_path() + " ( importer: " + info->get_import_info()->get_importer() + ", type: " + info->get_import_info()->get_type() + ", format: " + unsupported_format_type + ") to " + info->get_new_source_path().get_file() + String("\n"); } } + if (failed_scripts.size() > 0) { + report += "------\n"; + report += "\nThe following scripts were not decompiled:" + String("\n"); + for (const String &script : failed_scripts) { + report += script + String("\n"); + } + } if (failed.size() > 0) { String failed_report = "------\n"; failed_report += "\nFailed conversions:" + String("\n");