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