From 8c2f7a9960b09ac6e49a7f4dd26c80db7d9f5609 Mon Sep 17 00:00:00 2001 From: LukeFZ <17146677+LukeFZ@users.noreply.github.com> Date: Wed, 14 Jan 2026 17:40:51 +0100 Subject: [PATCH] implement (redux cli only) support for specifying a name translation map --- Il2CppInspector.Common/Il2CppInspector.csproj | 4 + .../NameTranslationApplierContext.cs | 97 ++++++++++ .../NameTranslation/NameTranslationInfo.cs | 8 + .../NameTranslationParserContext.cs | 182 ++++++++++++++++++ .../Reflection/ParameterInfo.cs | 2 +- .../Reflection/TypeModel.cs | 14 ++ .../Commands/ProcessCommand.cs | 12 +- .../InspectorSettings.cs | 2 +- .../UiContext.cs | 12 +- 9 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 Il2CppInspector.Common/Next/NameTranslation/NameTranslationApplierContext.cs create mode 100644 Il2CppInspector.Common/Next/NameTranslation/NameTranslationInfo.cs create mode 100644 Il2CppInspector.Common/Next/NameTranslation/NameTranslationParserContext.cs diff --git a/Il2CppInspector.Common/Il2CppInspector.csproj b/Il2CppInspector.Common/Il2CppInspector.csproj index 0e3ddf2..c2052eb 100644 --- a/Il2CppInspector.Common/Il2CppInspector.csproj +++ b/Il2CppInspector.Common/Il2CppInspector.csproj @@ -60,6 +60,10 @@ + + + + diff --git a/Il2CppInspector.Common/Next/NameTranslation/NameTranslationApplierContext.cs b/Il2CppInspector.Common/Next/NameTranslation/NameTranslationApplierContext.cs new file mode 100644 index 0000000..4f7a378 --- /dev/null +++ b/Il2CppInspector.Common/Next/NameTranslation/NameTranslationApplierContext.cs @@ -0,0 +1,97 @@ +using dnlib.DotNet; +using Il2CppInspector.Reflection; +using System.Diagnostics.CodeAnalysis; + +namespace Il2CppInspector.Next.NameTranslation; + +public readonly struct NameTranslationApplierContext +{ + private readonly NameTranslationInfo _nameTranslationInfo; + + private NameTranslationApplierContext(NameTranslationInfo nameTranslationInfo) + { + _nameTranslationInfo = nameTranslationInfo; + } + + public static void Process(Assembly assemblyDefinition, NameTranslationInfo nameTranslationInfo) + { + var ctx = new NameTranslationApplierContext(nameTranslationInfo); + ctx.Process(assemblyDefinition); + } + + private string TranslateName(string name) => + _nameTranslationInfo.NameTranslation.GetValueOrDefault(name, name); + + private bool TryTranslateName(string name, out string translatedName) + { + if (_nameTranslationInfo.NameTranslation.TryGetValue(name, out var translated)) + { + translatedName = translated; + return true; + } + + translatedName = null; + return false; + } + + private string TranslateNamespace(string className, string currentNamespace) => + _nameTranslationInfo.ClassNamespaces.GetValueOrDefault(className, currentNamespace); + + private void Process(Assembly assemblyDefinition) + { + foreach (var module in assemblyDefinition.DefinedTypes) + Process(module); + } + + private void Process(TypeInfo typeDefinition) + { + if (TryTranslateName(typeDefinition.Name, out var translatedName)) + { + typeDefinition.Namespace = TranslateNamespace(typeDefinition.Name, typeDefinition.Namespace); + typeDefinition.Name = translatedName; + } + + foreach (var method in typeDefinition.DeclaredMethods) + Process(method); + + foreach (var field in typeDefinition.DeclaredFields) + Process(field); + + foreach (var property in typeDefinition.DeclaredProperties) + Process(property); + + foreach (var evt in typeDefinition.DeclaredEvents) + Process(evt); + + foreach (var nestedType in typeDefinition.DeclaredNestedTypes) + Process(nestedType); + } + + private void Process(MethodInfo methodDefinition) + { + methodDefinition.Name = TranslateName(methodDefinition.Name); + + foreach (var parameter in methodDefinition.DeclaredParameters) + Process(parameter); + } + + private void Process(FieldInfo fieldDefinition) + { + fieldDefinition.Name = TranslateName(fieldDefinition.Name); + } + + private void Process(PropertyInfo propertyDefinition) + { + propertyDefinition.Name = TranslateName(propertyDefinition.Name); + } + + private void Process(EventInfo eventDefinition) + { + eventDefinition.Name = TranslateName(eventDefinition.Name); + } + + private void Process(ParameterInfo parameterDefinition) + { + parameterDefinition.Name = TranslateName(parameterDefinition.Name); + } +} \ No newline at end of file diff --git a/Il2CppInspector.Common/Next/NameTranslation/NameTranslationInfo.cs b/Il2CppInspector.Common/Next/NameTranslation/NameTranslationInfo.cs new file mode 100644 index 0000000..65b9897 --- /dev/null +++ b/Il2CppInspector.Common/Next/NameTranslation/NameTranslationInfo.cs @@ -0,0 +1,8 @@ +using System.Collections.Frozen; + +namespace Il2CppInspector.Next.NameTranslation; + +public sealed record NameTranslationInfo( + FrozenDictionary NameTranslation, + FrozenDictionary ClassNamespaces +); \ No newline at end of file diff --git a/Il2CppInspector.Common/Next/NameTranslation/NameTranslationParserContext.cs b/Il2CppInspector.Common/Next/NameTranslation/NameTranslationParserContext.cs new file mode 100644 index 0000000..8befdb2 --- /dev/null +++ b/Il2CppInspector.Common/Next/NameTranslation/NameTranslationParserContext.cs @@ -0,0 +1,182 @@ +using dnlib.DotNet; +using System.Collections.Frozen; +using System.Diagnostics; + +namespace Il2CppInspector.Next.NameTranslation; + +public ref struct NameTranslationParserContext +{ + private enum CurrentSection + { + None, + Classes, + Methods, + Fields, + Properties, + Events, + Parameters + } + + private CurrentSection _currentSection; + private bool _reverseOrder; + + private readonly ReadOnlySpan _nameTranslationLines; + private readonly char _seperator; + + private NameTranslationParserContext(ReadOnlySpan nameTranslationLines, char seperator) + { + _currentSection = CurrentSection.None; + _nameTranslationLines = nameTranslationLines; + _seperator = seperator; + } + + public static NameTranslationInfo Parse(ReadOnlySpan nameTranslationLines, char seperator = '⇨') + { + var ctx = new NameTranslationParserContext(nameTranslationLines, seperator); + return ctx.Parse(); + } + + private void HandleMetadataLine(string line) + { + switch (line) + { + case "#ReverseOrder": + _reverseOrder = true; + break; + case "#Classes": + _currentSection = CurrentSection.Classes; + break; + case "#Methods": + _currentSection = CurrentSection.Methods; + break; + case "#Fields": + _currentSection = CurrentSection.Fields; + break; + case "#Properties": + _currentSection = CurrentSection.Properties; + break; + case "#Events": + _currentSection = CurrentSection.Events; + break; + case "#Parameters": + _currentSection = CurrentSection.Parameters; + break; + default: + Debug.Assert(false); + break; + } + } + + private NameTranslationInfo Parse() + { + var nameTranslationMap = new Dictionary(_nameTranslationLines.Length); + var namespaceTranslationMap = new Dictionary(); + + foreach (var line in _nameTranslationLines) + { + if (line.StartsWith('#')) + { + HandleMetadataLine(line); + continue; + } + + var split = line.Split(_seperator, 2); + + var obfuscatedName = split[0]; + var deobfuscatedFullName = split[1].TrimEnd(); + if (!_reverseOrder) + { + (obfuscatedName, deobfuscatedFullName) = (deobfuscatedFullName, obfuscatedName); + } + + string deobfuscatedName; + string? deobfuscatedNamespace; + if (_currentSection != CurrentSection.Classes) + { + deobfuscatedNamespace = null; + deobfuscatedName = ParseFullName(deobfuscatedFullName); + } + else + { + (deobfuscatedName, deobfuscatedNamespace) = ParseFullClassName(deobfuscatedFullName); + } + + // This sometimes happens, just ignore it if so + if (obfuscatedName == deobfuscatedName) + continue; + + if (nameTranslationMap.TryGetValue(obfuscatedName, out var alreadySeenName)) + { + if (alreadySeenName != deobfuscatedName) + throw new InvalidDataException( + $"Name translation collision detected for name {obfuscatedName}: {alreadySeenName} vs. {deobfuscatedName}"); + } + else + { + nameTranslationMap[obfuscatedName] = deobfuscatedName; + + if (deobfuscatedNamespace != null) + namespaceTranslationMap[obfuscatedName] = deobfuscatedNamespace; + } + } + + return new NameTranslationInfo( + nameTranslationMap + .ToFrozenDictionary(), + namespaceTranslationMap + .ToFrozenDictionary()); + } + + private string ParseFullName(string fullName) + => _currentSection switch + { + CurrentSection.Classes => ParseFullClassName(fullName).Item1, + CurrentSection.Methods or CurrentSection.Properties => ParseFullMemberWithArgumentsName(fullName), + CurrentSection.Fields or CurrentSection.Events => ParseFullMemberName(fullName), + CurrentSection.Parameters => ParseFullParameterName(fullName), + _ => throw new UnreachableException() + }; + + private static (string, string?) ParseFullClassName(string fullName) + { + // For the namespace, return everything up until the last part of the fully qualified class name + var lastNamespaceIdx = fullName.LastIndexOf('.'); + + var ns = lastNamespaceIdx == -1 + ? null + : fullName[..lastNamespaceIdx]; + + // If we have a nested class, return that name + var nestedClassIdx = fullName.LastIndexOf('/') + 1; + if (nestedClassIdx != 0) + return (fullName[nestedClassIdx..], ns); + + // Otherwise, if we have a namespace, return the last part of the fully qualified name + if (lastNamespaceIdx != -1) + return (fullName[(lastNamespaceIdx + 1)..], ns); + + // Otherwise the class has no namespace, return the full name + return (fullName, ns); + } + + // Used for both fields and events + private static string ParseFullMemberName(string fullName) + { + var memberNameStartIdx = fullName.LastIndexOf("::", StringComparison.Ordinal) + 2; + return fullName[memberNameStartIdx..]; + } + + private static string ParseFullMemberWithArgumentsName(string fullName) + { + var methodNameWithParameters = ParseFullMemberName(fullName); + var parametersStartIdx = methodNameWithParameters.IndexOf('('); + + return methodNameWithParameters[..parametersStartIdx]; + } + + private static string ParseFullParameterName(string fullName) + { + var parameterNameStartIdx = fullName.LastIndexOf(' ') + 1; + return fullName[parameterNameStartIdx..]; + } +} \ No newline at end of file diff --git a/Il2CppInspector.Common/Reflection/ParameterInfo.cs b/Il2CppInspector.Common/Reflection/ParameterInfo.cs index f31fe5d..371cbd3 100644 --- a/Il2CppInspector.Common/Reflection/ParameterInfo.cs +++ b/Il2CppInspector.Common/Reflection/ParameterInfo.cs @@ -48,7 +48,7 @@ namespace Il2CppInspector.Reflection public int MetadataToken { get; } // Name of parameter - public string Name { get; } + public string Name { get; set; } public string CSharpName => Constants.Keywords.Contains(Name) ? "@" + Name : Name.ToCIdentifier(); // Type of this parameter diff --git a/Il2CppInspector.Common/Reflection/TypeModel.cs b/Il2CppInspector.Common/Reflection/TypeModel.cs index 1dbb8b0..840c615 100644 --- a/Il2CppInspector.Common/Reflection/TypeModel.cs +++ b/Il2CppInspector.Common/Reflection/TypeModel.cs @@ -11,6 +11,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using Il2CppInspector.Next.BinaryMetadata; +using Il2CppInspector.Next.NameTranslation; namespace Il2CppInspector.Reflection { @@ -187,6 +188,19 @@ namespace Il2CppInspector.Reflection PluginHooks.PostProcessTypeModel(this); } + public void ApplyNameTranslationFromFile(string nameTranslationMapPath) + { + var lines = File.ReadAllLines(nameTranslationMapPath); + ApplyNameTranslation(lines); + } + + public void ApplyNameTranslation(ReadOnlySpan nameTranslationLines) + { + var info = NameTranslationParserContext.Parse(nameTranslationLines); + foreach (var assembly in Assemblies) + NameTranslationApplierContext.Process(assembly, info); + } + // Get generic arguments from either a type or method instanceIndex from a MethodSpec public TypeInfo[] ResolveGenericArguments(Il2CppGenericInst inst) { diff --git a/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs b/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs index 43018e4..c7a512b 100644 --- a/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs +++ b/Il2CppInspector.Redux.CLI/Commands/ProcessCommand.cs @@ -72,6 +72,9 @@ internal sealed class ProcessCommand(PortProvider portProvider) : ManualCommand< [CommandOption("--image-base")] public string? ImageBase { get; init; } + + [CommandOption("--name-translation-map")] + public string? NameTranslationMap { get; init; } } protected override async Task ExecuteAsync(CliClient client, Settings settings) @@ -79,17 +82,19 @@ internal sealed class ProcessCommand(PortProvider portProvider) : ManualCommand< var inspectorVersion = await client.GetInspectorVersion(); AnsiConsole.MarkupLineInterpolated($"Using inspector [gray]{inspectorVersion}[/]"); + var imageBase = 0uL; if (settings.ImageBase != null) { - var imageBase = ulong.Parse(settings.ImageBase, + imageBase = ulong.Parse(settings.ImageBase, settings.ImageBase.StartsWith("0x") ? NumberStyles.HexNumber : NumberStyles.Integer); AnsiConsole.MarkupLineInterpolated($"Setting image base to [white]0x{imageBase:x}[/]"); - await client.SetSettings(new InspectorSettings(imageBase)); } + await client.SetSettings(new InspectorSettings(imageBase, settings.NameTranslationMap)); + await client.SubmitInputFiles(settings.InputPaths.ToList()); await client.WaitForLoadingToFinishAsync(); if (!client.ImportCompleted) @@ -176,6 +181,9 @@ internal sealed class ProcessCommand(PortProvider portProvider) : ManualCommand< return ValidationResult.Error( $"Provided extracted IL2CPP files path {settings.ExtractIl2CppFilesPath} already exists as a file."); + if (settings.NameTranslationMap != null && !File.Exists(settings.NameTranslationMap)) + return ValidationResult.Error($"Provided name translation map {settings.NameTranslationMap} does not exist."); + if (settings is { CppScaffolding: false, CSharpStubs: false, DisassemblerMetadata: false, DummyDlls: false, diff --git a/Il2CppInspector.Redux.FrontendCore/InspectorSettings.cs b/Il2CppInspector.Redux.FrontendCore/InspectorSettings.cs index 6104c36..cdae876 100644 --- a/Il2CppInspector.Redux.FrontendCore/InspectorSettings.cs +++ b/Il2CppInspector.Redux.FrontendCore/InspectorSettings.cs @@ -1,3 +1,3 @@ namespace Il2CppInspector.Redux.FrontendCore; -public sealed record InspectorSettings(ulong ImageBase); \ No newline at end of file +public sealed record InspectorSettings(ulong ImageBase = 0, string? NameTranslationMapPath = null); \ No newline at end of file diff --git a/Il2CppInspector.Redux.FrontendCore/UiContext.cs b/Il2CppInspector.Redux.FrontendCore/UiContext.cs index 7f84454..3e1764a 100644 --- a/Il2CppInspector.Redux.FrontendCore/UiContext.cs +++ b/Il2CppInspector.Redux.FrontendCore/UiContext.cs @@ -23,6 +23,7 @@ public class UiContext private readonly List _potentialUnityVersions = []; private readonly LoadOptions _loadOptions = new(); + private InspectorSettings _settings = new(); private readonly List<(string FormatId, string OutputDirectory, Dictionary Settings)> _queuedExports = []; @@ -96,6 +97,12 @@ public class UiContext { var typeModel = new TypeModel(inspector); + if (_settings.NameTranslationMapPath != null) + { + await client.ShowLogMessage("Applying name translation map"); + typeModel.ApplyNameTranslationFromFile(_settings.NameTranslationMapPath); + } + // Just create the app model, do not initialize it - this is done lazily depending on the exports _appModels.Add(new AppModel(typeModel, makeDefaultBuild: false)); } @@ -127,6 +134,8 @@ public class UiContext { await using (await LoadingSession.Start(client)) { + _loadOptions.ImageBase = _settings.ImageBase; + var streams = Inspector.GetStreamsFromPackage(inputFiles); if (streams != null) { @@ -160,6 +169,7 @@ public class UiContext else if (_binary == null && PathHeuristics.IsBinaryPath(inputFile)) { stream.Position = 0; + _loadOptions.BinaryFilePath = inputFile; if (await TryLoadBinaryFromStreamAsync(client, stream)) @@ -251,7 +261,7 @@ public class UiContext public Task SetSettingsAsync(UiClient client, InspectorSettings settings) { - _loadOptions.ImageBase = settings.ImageBase; + _settings = settings; return Task.CompletedTask; } } \ No newline at end of file