Initial code commit
This commit is contained in:
parent
48bc980f54
commit
f10a67e681
28
README.md
28
README.md
@ -1,2 +1,28 @@
|
||||
# SPT-AssemblyTool
|
||||
SPT-AssemblyTool
|
||||
-----
|
||||
|
||||
Can deobfuscate Assembly-CSharp.dll files, and apply deobfuscation name remapping (provided you provide a remapping file)
|
||||
|
||||
### Usage
|
||||
|
||||
#### Deobfuscation
|
||||
|
||||
```
|
||||
SPT-AssemblyTool.exe -d -m "...\EscapeFromTarkov_Data\Managed" "...\Assembly-CSharp.dll"
|
||||
```
|
||||
|
||||
Generates a `Assembly-CSharp-cleaned.dll` in the directory that Assembly-CSharp.dll resides in.
|
||||
|
||||
#### Remapping
|
||||
|
||||
```
|
||||
-r -o "...\Assembly-CSharp-old.dll" -m "...\EscapeFromTarkov_Data\Managed" --mapping-file "...\mappings.json" "...\Assembly-CSharp-cleaned.dll"
|
||||
```
|
||||
|
||||
Requires an old deobfuscated Assembly-CSharp.dll, and the mapping file created for it. An example mapping file is located under `example-mapping.json`.
|
||||
|
||||
|
||||
|
||||
### Credits
|
||||
|
||||
Uses LGPL licensed code from [IL2CPPAssemblyUnhollower](https://github.com/knah/Il2CppAssemblyUnhollower) for the remapper
|
25
SPT-AssemblyTool.sln
Normal file
25
SPT-AssemblyTool.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31424.327
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPT-AssemblyTool", "SPT-AssemblyTool\SPT-AssemblyTool.csproj", "{30471FBA-3D20-47AD-A035-46A3604BEF40}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{30471FBA-3D20-47AD-A035-46A3604BEF40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{30471FBA-3D20-47AD-A035-46A3604BEF40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{30471FBA-3D20-47AD-A035-46A3604BEF40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{30471FBA-3D20-47AD-A035-46A3604BEF40}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E939CA1A-B078-4091-8A93-78A303569BDA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
BIN
SPT-AssemblyTool/Assets/de4dot/AssemblyData.dll
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/AssemblyData.dll
Normal file
Binary file not shown.
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.blocks.dll
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.blocks.dll
Normal file
Binary file not shown.
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.code.dll
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.code.dll
Normal file
Binary file not shown.
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.cui.dll
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.cui.dll
Normal file
Binary file not shown.
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.exe
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.exe
Normal file
Binary file not shown.
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.mdecrypt.dll
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/de4dot.mdecrypt.dll
Normal file
Binary file not shown.
BIN
SPT-AssemblyTool/Assets/de4dot/dnlib.dll
Normal file
BIN
SPT-AssemblyTool/Assets/de4dot/dnlib.dll
Normal file
Binary file not shown.
79
SPT-AssemblyTool/Deobfuscator.cs
Normal file
79
SPT-AssemblyTool/Deobfuscator.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
|
||||
namespace SPT_AssemblyTool
|
||||
{
|
||||
internal static class Deobfuscator
|
||||
{
|
||||
internal static void Deobfuscate(Program.Arguments args)
|
||||
{
|
||||
var executablePath = typeof(Program).Assembly.Location;
|
||||
var de4dotLocation = Path.Combine(Path.GetDirectoryName(executablePath), "Assets", "de4dot", "de4dot.exe");
|
||||
|
||||
var assemblyPath = args.Values[0];
|
||||
|
||||
string token;
|
||||
|
||||
using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath))
|
||||
{
|
||||
var potentialStringDelegates = new List<MethodDefinition>();
|
||||
|
||||
foreach (var type in assemblyDefinition.MainModule.Types)
|
||||
{
|
||||
foreach (var method in type.Methods)
|
||||
{
|
||||
if (method.ReturnType.FullName != "System.String"
|
||||
|| method.Parameters.Count != 1
|
||||
|| method.Parameters[0].ParameterType.FullName != "System.Int32"
|
||||
|| method.Body == null
|
||||
|| !method.IsStatic)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!method.Body.Instructions.Any(x =>
|
||||
x.OpCode.Code == Code.Callvirt &&
|
||||
((MethodReference)x.Operand).FullName == "System.Object System.AppDomain::GetData(System.String)"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
potentialStringDelegates.Add(method);
|
||||
}
|
||||
}
|
||||
|
||||
if (potentialStringDelegates.Count != 1)
|
||||
{
|
||||
Program.WriteError($"Expected to find 1 potential string delegate method; found {potentialStringDelegates.Count}. Candidates: {string.Join("\r\n", potentialStringDelegates.Select(x => x.FullName))}");
|
||||
}
|
||||
|
||||
token = potentialStringDelegates[0].MetadataToken.ToString();
|
||||
}
|
||||
|
||||
var process = Process.Start(de4dotLocation,
|
||||
$"--un-name \"!^<>[a-z0-9]$&!^<>[a-z0-9]__.*$&![A-Z][A-Z]\\$<>.*$&^[a-zA-Z_<{{$][a-zA-Z_0-9<>{{}}$.`-]*$\" \"{assemblyPath}\" --strtyp delegate --strtok \"{token}\"");
|
||||
|
||||
process.WaitForExit();
|
||||
|
||||
|
||||
// Fixes "ResolutionScope is null" by rewriting the assembly
|
||||
var cleanedDllPath = Path.Combine(Path.GetDirectoryName(assemblyPath), Path.GetFileNameWithoutExtension(assemblyPath) + "-cleaned.dll");
|
||||
|
||||
var resolver = new DefaultAssemblyResolver();
|
||||
resolver.AddSearchDirectory(args.ManagedPath);
|
||||
|
||||
using (var memoryStream = new MemoryStream(File.ReadAllBytes(cleanedDllPath)))
|
||||
using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(memoryStream, new ReaderParameters()
|
||||
{
|
||||
AssemblyResolver = resolver
|
||||
}))
|
||||
{
|
||||
assemblyDefinition.Write(cleanedDllPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
405
SPT-AssemblyTool/NArgs.cs
Normal file
405
SPT-AssemblyTool/NArgs.cs
Normal file
@ -0,0 +1,405 @@
|
||||
/*
|
||||
NArgs
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright(c) 2021 Bepis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace NArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Command-line argument parser.
|
||||
/// </summary>
|
||||
public static class Arguments
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses arguments and constructs an <see cref="IArgumentCollection"/> object.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object to construct. Must inherit from <see cref="IArgumentCollection"/></typeparam>
|
||||
/// <param name="args">The command-line arguments to parse.</param>
|
||||
/// <returns></returns>
|
||||
public static T Parse<T>(string[] args) where T : IArgumentCollection, new()
|
||||
{
|
||||
Dictionary<CommandDefinitionAttribute, Action<string>> valueSwitches = new Dictionary<CommandDefinitionAttribute, Action<string>>();
|
||||
Dictionary<CommandDefinitionAttribute, Action<bool>> boolSwitches = new Dictionary<CommandDefinitionAttribute, Action<bool>>();
|
||||
|
||||
var config = new T { Values = new List<string>() };
|
||||
|
||||
var commandProps = GetCommandProperties<T>();
|
||||
|
||||
foreach (var kv in commandProps)
|
||||
{
|
||||
if (kv.Value.PropertyType == typeof(bool))
|
||||
{
|
||||
boolSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x));
|
||||
}
|
||||
else if (kv.Value.PropertyType == typeof(string))
|
||||
{
|
||||
valueSwitches.Add(kv.Key, x => kv.Value.SetValue(config, x));
|
||||
}
|
||||
else if (typeof(IList<string>).IsAssignableFrom(kv.Value.PropertyType))
|
||||
{
|
||||
if (kv.Value.GetValue(config) == null)
|
||||
{
|
||||
kv.Value.SetValue(config, new List<string>());
|
||||
}
|
||||
|
||||
valueSwitches.Add(kv.Key, x =>
|
||||
{
|
||||
var list = (IList<string>)kv.Value.GetValue(config);
|
||||
list.Add(x);
|
||||
});
|
||||
}
|
||||
else if (typeof(Enum).IsAssignableFrom(kv.Value.PropertyType))
|
||||
{
|
||||
valueSwitches.Add(kv.Key, x =>
|
||||
{
|
||||
if (!TryParseEnum(kv.Value.PropertyType, x, true, out var value))
|
||||
throw new ArgumentException("Invalid value for argument: " + x);
|
||||
|
||||
kv.Value.SetValue(config, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CommandDefinitionAttribute previousSwitchDefinition = null;
|
||||
bool valuesOnly = false;
|
||||
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (arg == "--")
|
||||
{
|
||||
// no more switches, only values
|
||||
valuesOnly = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valuesOnly)
|
||||
{
|
||||
config.Values.Add(arg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.StartsWith("-")
|
||||
|| arg.StartsWith("--"))
|
||||
{
|
||||
string previousSwitch;
|
||||
|
||||
if (arg.StartsWith("--"))
|
||||
previousSwitch = arg.Substring(2);
|
||||
else
|
||||
previousSwitch = arg.Substring(1);
|
||||
|
||||
if (boolSwitches.Keys.TryFirst(x
|
||||
=> x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase)
|
||||
|| x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true,
|
||||
out var definition))
|
||||
{
|
||||
boolSwitches[definition](true);
|
||||
previousSwitch = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueSwitches.Keys.TryFirst(x
|
||||
=> x.LongArg.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase)
|
||||
|| x.ShortArg?.Equals(previousSwitch, StringComparison.InvariantCultureIgnoreCase) == true,
|
||||
out definition))
|
||||
{
|
||||
previousSwitchDefinition = definition;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine("Unrecognized command line option: " + arg);
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
if (previousSwitchDefinition != null)
|
||||
{
|
||||
valueSwitches[previousSwitchDefinition](arg);
|
||||
previousSwitchDefinition = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
config.Values.Add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kv in commandProps)
|
||||
{
|
||||
if (!kv.Key.Required)
|
||||
continue;
|
||||
|
||||
if (kv.Value.PropertyType == typeof(string))
|
||||
if (kv.Value.GetValue(config) == null)
|
||||
throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}");
|
||||
|
||||
if (kv.Value.PropertyType == typeof(IList<string>))
|
||||
if (((IList<string>)kv.Value.GetValue(config)).Count == 0)
|
||||
throw new ArgumentException($"Required argument not provided: {kv.Key.LongArg}");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string to be printed as console help text.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the arguments object to create help instructions for. Must inherit from <see cref="IArgumentCollection"/></typeparam>
|
||||
/// <param name="copyrightText">The copyright text to add at the top, if any.</param>
|
||||
/// <param name="usageText">The usage text to add at the top, if any.</param>
|
||||
public static string PrintLongHelp<T>(string copyrightText = null, string usageText = null) where T : IArgumentCollection
|
||||
{
|
||||
var commands = GetCommandProperties<T>();
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
if (copyrightText != null)
|
||||
builder.AppendLine(copyrightText);
|
||||
|
||||
if (usageText != null)
|
||||
builder.AppendLine(usageText);
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine();
|
||||
|
||||
var orderedCommands = commands
|
||||
.OrderByDescending(x => x.Key.Order)
|
||||
.ThenBy(x => x.Key.ShortArg ?? "zzzz")
|
||||
.ThenBy(x => x.Key.LongArg);
|
||||
|
||||
foreach (var command in orderedCommands)
|
||||
{
|
||||
var valueString = string.Empty;
|
||||
|
||||
if (command.Value.PropertyType == typeof(IList<string>)
|
||||
|| command.Value.PropertyType == typeof(string))
|
||||
{
|
||||
valueString = " <value>";
|
||||
}
|
||||
else if (typeof(Enum).IsAssignableFrom(command.Value.PropertyType))
|
||||
{
|
||||
valueString = $" ({string.Join(" | ", Enum.GetNames(command.Value.PropertyType))})";
|
||||
}
|
||||
|
||||
string listing = command.Key.ShortArg != null
|
||||
? $" -{command.Key.ShortArg}, --{command.Key.LongArg}{valueString}"
|
||||
: $" --{command.Key.LongArg}{valueString}";
|
||||
|
||||
const int listingWidth = 45;
|
||||
const int descriptionWidth = 65;
|
||||
|
||||
string listingWidthString = "".PadLeft(listingWidth);
|
||||
|
||||
builder.Append(listing.PadRight(listingWidth));
|
||||
|
||||
if (listing.Length > listingWidth - 3)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(listingWidthString);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(command.Key.Description))
|
||||
{
|
||||
BuildArgumentDescription(builder, command.Key.Description, listingWidth, descriptionWidth);
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void BuildArgumentDescription(StringBuilder builder, string description, int listingWidth, int descriptionWidth)
|
||||
{
|
||||
int lineLength = 0;
|
||||
int lineStartIndex = 0;
|
||||
int lastValidLength = 0;
|
||||
|
||||
for (var index = 0; index < description.Length; index++)
|
||||
{
|
||||
char c = description[index];
|
||||
|
||||
void PrintLine()
|
||||
{
|
||||
var descriptionSubstring = description.Substring(lineStartIndex, lastValidLength);
|
||||
builder.AppendLine(descriptionSubstring);
|
||||
builder.Append(' ', listingWidth);
|
||||
|
||||
lineStartIndex = 1 + index - (lineLength - lastValidLength);
|
||||
lineLength = 1 + index - lineStartIndex;
|
||||
lastValidLength = lineLength;
|
||||
}
|
||||
|
||||
if ((c == ' ' && lineLength >= descriptionWidth) | c == '\n')
|
||||
{
|
||||
bool printAgain = false;
|
||||
|
||||
if (c == '\n' && lineLength < descriptionWidth)
|
||||
lastValidLength = lineLength;
|
||||
else if (c == '\n')
|
||||
printAgain = true;
|
||||
|
||||
PrintLine();
|
||||
|
||||
if (printAgain)
|
||||
{
|
||||
// This works and I'm not sure how.
|
||||
|
||||
lastValidLength--;
|
||||
lineLength--;
|
||||
PrintLine();
|
||||
lastValidLength++;
|
||||
lineLength++;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == ' ')
|
||||
lastValidLength = lineLength;
|
||||
|
||||
lineLength++;
|
||||
}
|
||||
|
||||
if (lineLength > 0)
|
||||
{
|
||||
var remainingSubstring = description.Substring(lineStartIndex);
|
||||
builder.AppendLine(remainingSubstring);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<CommandDefinitionAttribute, PropertyInfo> GetCommandProperties<T>()
|
||||
{
|
||||
var commands = new Dictionary<CommandDefinitionAttribute, PropertyInfo>();
|
||||
|
||||
foreach (var prop in typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
||||
{
|
||||
var commandDef = prop.GetCustomAttribute<CommandDefinitionAttribute>();
|
||||
|
||||
if (commandDef == null)
|
||||
continue;
|
||||
|
||||
commands.Add(commandDef, prop);
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static bool TryFirst<T>(this IEnumerable<T> enumerable, Func<T, bool> predicate, out T value)
|
||||
{
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (predicate(item))
|
||||
{
|
||||
value = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static MethodInfo GenericTryParseMethodInfo = null;
|
||||
private static bool TryParseEnum(Type enumType, string value, bool caseSensitive, out object val)
|
||||
{
|
||||
// Workaround for non-generic Enum.TryParse not being present below .NET 5
|
||||
|
||||
if (GenericTryParseMethodInfo == null)
|
||||
{
|
||||
GenericTryParseMethodInfo = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static)
|
||||
.First(x => x.Name == "TryParse" && x.GetGenericArguments().Length == 1 &&
|
||||
x.GetParameters().Length == 3);
|
||||
}
|
||||
|
||||
var objectArray = new object[] { value, caseSensitive, null };
|
||||
|
||||
var result = GenericTryParseMethodInfo.MakeGenericMethod(enumType)
|
||||
.Invoke(null, objectArray);
|
||||
|
||||
val = objectArray[2];
|
||||
return (bool)result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies an object is an argument collection.
|
||||
/// </summary>
|
||||
public interface IArgumentCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of values that were passed in as arguments, but not associated with an option.
|
||||
/// </summary>
|
||||
IList<string> Values { get; set; }
|
||||
}
|
||||
|
||||
public class CommandDefinitionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The short version of an option, e.g. "-a". Optional.
|
||||
/// </summary>
|
||||
public string ShortArg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The long version of an option, e.g. "--append". Required.
|
||||
/// </summary>
|
||||
public string LongArg { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to fail parsing if this argument has not been provided.
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The description of the option, to be used in the help text.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Used in ordering this command in the help list.
|
||||
/// </summary>
|
||||
public int Order { get; set; } = 0;
|
||||
|
||||
/// <param name="longArg">The long version of an option, e.g. "--append".</param>
|
||||
public CommandDefinitionAttribute(string longArg)
|
||||
{
|
||||
LongArg = longArg;
|
||||
}
|
||||
|
||||
/// <param name="shortArg">The short version of an option, e.g. "-a".</param>
|
||||
/// <param name="longArg">The long version of an option, e.g. "--append".</param>
|
||||
public CommandDefinitionAttribute(string shortArg, string longArg)
|
||||
{
|
||||
ShortArg = shortArg;
|
||||
LongArg = longArg;
|
||||
}
|
||||
}
|
||||
}
|
78
SPT-AssemblyTool/Program.cs
Normal file
78
SPT-AssemblyTool/Program.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NArgs;
|
||||
|
||||
namespace SPT_AssemblyTool
|
||||
{
|
||||
class Program
|
||||
{
|
||||
internal static void WriteError(string error)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(error);
|
||||
Console.ResetColor();
|
||||
|
||||
Environment.Exit(1);
|
||||
}
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Arguments arguments = NArgs.Arguments.Parse<Arguments>(args);
|
||||
|
||||
if (arguments.Values.Count == 0 || arguments.Help)
|
||||
{
|
||||
Console.WriteLine(NArgs.Arguments.PrintLongHelp<Arguments>(
|
||||
usageText: "Usage: SPT-AssemblyTool [options] <Assembly-CSharp.dll location>"));
|
||||
return;
|
||||
}
|
||||
|
||||
var assemblyPath = arguments.Values[0];
|
||||
|
||||
if (!File.Exists(assemblyPath))
|
||||
WriteError("Target assembly file does not exist");
|
||||
|
||||
if (!Directory.Exists(arguments.ManagedPath))
|
||||
WriteError("--managed-path option is not correct");
|
||||
|
||||
if (arguments.DeobfuscateMode)
|
||||
{
|
||||
Deobfuscator.Deobfuscate(arguments);
|
||||
}
|
||||
|
||||
if (arguments.RemapMode)
|
||||
{
|
||||
if (!File.Exists(arguments.OldAssemblyPath))
|
||||
WriteError("Old assembly path option is not correct");
|
||||
|
||||
if (!File.Exists(arguments.MappingFilePath))
|
||||
WriteError("Mapping file path option is not correct");
|
||||
|
||||
Remapper.Remap(arguments);
|
||||
}
|
||||
}
|
||||
|
||||
internal class Arguments : IArgumentCollection
|
||||
{
|
||||
public IList<string> Values { get; set; }
|
||||
|
||||
[CommandDefinition("d", "deobfuscate", Description = "Deobfuscation mode", Order = 1)]
|
||||
public bool DeobfuscateMode { get; set; }
|
||||
|
||||
[CommandDefinition("m", "managed-path", Description = "Path to EFT managed folder. Required", Required = true)]
|
||||
public string ManagedPath { get; set; }
|
||||
|
||||
[CommandDefinition("r", "remap", Description = "Remapping mode", Order = 1)]
|
||||
public bool RemapMode { get; set; }
|
||||
|
||||
[CommandDefinition("o", "old-assembly", Description = "Path to previously decompiled Assembly-CSharp.dll file. Only used in remapping mode, required")]
|
||||
public string OldAssemblyPath { get; set; }
|
||||
|
||||
[CommandDefinition("mapping-file", Description = "Path to mapping path .json file. Only used in remapping mode, required")]
|
||||
public string MappingFilePath { get; set; }
|
||||
|
||||
[CommandDefinition("h", "help", Description = "Prints help text", Order = -1)]
|
||||
public bool Help { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
8
SPT-AssemblyTool/Properties/launchSettings.json
Normal file
8
SPT-AssemblyTool/Properties/launchSettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"SPT-AssemblyTool": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "-r -o \"B:\\Assembly-CSharp-old.dll\" -m \"N:\\EFT\\EscapeFromTarkov_Data\\Managed\" -mapping-file \"D:\\Sourcecode\\GitGud-Bepsi\\SPT-AssemblyTool\\mappings.json\" \"B:\\Assembly-CSharp-mid.dll\""
|
||||
}
|
||||
}
|
||||
}
|
226
SPT-AssemblyTool/Remapper.cs
Normal file
226
SPT-AssemblyTool/Remapper.cs
Normal file
@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SPT_AssemblyTool
|
||||
{
|
||||
internal static class Remapper
|
||||
{
|
||||
public static void Remap(Program.Arguments args)
|
||||
{
|
||||
var mappingDictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(args.MappingFilePath));
|
||||
|
||||
var newMappingDictionary = new Dictionary<string, string>();
|
||||
var reportDictionary = new Dictionary<string, JObject>();
|
||||
|
||||
var resolver = new DefaultAssemblyResolver();
|
||||
resolver.AddSearchDirectory(args.ManagedPath);
|
||||
|
||||
var readerParameters = new ReaderParameters { AssemblyResolver = resolver };
|
||||
var assemblyPath = args.Values[0];
|
||||
|
||||
using var oldAssembly = AssemblyDefinition.ReadAssembly(args.OldAssemblyPath, readerParameters);
|
||||
using var newAssembly = AssemblyDefinition.ReadAssembly(assemblyPath, readerParameters);
|
||||
|
||||
foreach (var (obfuscatedName, deobfuscatedName) in mappingDictionary)
|
||||
{
|
||||
TypeDefinition oldTypeDefinition = oldAssembly.MainModule.GetType(deobfuscatedName) ?? oldAssembly.MainModule.GetType(obfuscatedName);
|
||||
|
||||
if (oldTypeDefinition == null)
|
||||
Program.WriteError($"Could not find mapping definition in old assembly: {obfuscatedName}={deobfuscatedName}");
|
||||
|
||||
var result = FindBestMatchType(oldTypeDefinition, newAssembly, null);
|
||||
|
||||
string @namespace;
|
||||
string type;
|
||||
|
||||
if (!deobfuscatedName.Contains("."))
|
||||
{
|
||||
@namespace = "";
|
||||
type = deobfuscatedName;
|
||||
}
|
||||
else
|
||||
{
|
||||
@namespace = deobfuscatedName.Substring(0, deobfuscatedName.LastIndexOf("."));
|
||||
type = deobfuscatedName.Remove(0, deobfuscatedName.LastIndexOf("."));
|
||||
}
|
||||
|
||||
var newType = result[0].Item2;
|
||||
|
||||
newMappingDictionary[newType.FullName] = deobfuscatedName;
|
||||
reportDictionary[deobfuscatedName] = result.Export();
|
||||
|
||||
newType.Namespace = @namespace;
|
||||
newType.Name = type;
|
||||
}
|
||||
|
||||
var assemblyDirectory = Path.GetDirectoryName(assemblyPath);
|
||||
|
||||
var newAssemblyPath = Path.Combine(assemblyDirectory, Path.GetFileNameWithoutExtension(assemblyPath) + "-remapped.dll");
|
||||
var newMappingPath = Path.Combine(assemblyDirectory, "new-mapping.json");
|
||||
var newMappingReportPath = Path.Combine(assemblyDirectory, "new-mapping-report.json");
|
||||
|
||||
newAssembly.Write(newAssemblyPath);
|
||||
|
||||
var serializer = new JsonSerializer
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
|
||||
void writeToJsonFile(string filename, object data)
|
||||
{
|
||||
using var textWriter = new StreamWriter(filename);
|
||||
serializer.Serialize(textWriter, data);
|
||||
}
|
||||
|
||||
writeToJsonFile(newMappingPath, newMappingDictionary);
|
||||
writeToJsonFile(newMappingReportPath, reportDictionary);
|
||||
}
|
||||
|
||||
private static WeightDictionary FindBestMatchType(TypeDefinition obfType, AssemblyDefinition newAssembly, TypeDefinition enclosingCleanType)
|
||||
{
|
||||
// This method uses code from https://github.com/knah/Il2CppAssemblyUnhollower/blob/master/AssemblyUnhollower/DeobfuscationMapGenerator.cs
|
||||
// licensed under LGPL
|
||||
|
||||
var inheritanceDepthOfOriginal = 0;
|
||||
var currentBase = obfType.BaseType;
|
||||
while (true)
|
||||
{
|
||||
var newBase = currentBase?.Resolve().BaseType;
|
||||
if (newBase == null) break;
|
||||
|
||||
inheritanceDepthOfOriginal++;
|
||||
currentBase = newBase;
|
||||
}
|
||||
|
||||
|
||||
var bestPenalty = int.MinValue;
|
||||
var weightDictionary = new WeightDictionary(5);
|
||||
|
||||
var source = enclosingCleanType?.NestedTypes ?? newAssembly.MainModule.Types;
|
||||
|
||||
foreach (var candidateCleanType in source)
|
||||
{
|
||||
if (obfType.HasMethods != candidateCleanType.HasMethods)
|
||||
continue;
|
||||
|
||||
if (obfType.HasFields != candidateCleanType.HasFields)
|
||||
continue;
|
||||
|
||||
if (obfType.IsEnum)
|
||||
if (obfType.Fields.Count != candidateCleanType.Fields.Count)
|
||||
continue;
|
||||
|
||||
int currentPenalty = 0;
|
||||
|
||||
var tryBase = candidateCleanType.BaseType;
|
||||
var actualBaseDepth = 0;
|
||||
while (tryBase != null)
|
||||
{
|
||||
if (tryBase?.Name == currentBase?.Name && tryBase?.Namespace == currentBase?.Namespace)
|
||||
break;
|
||||
|
||||
tryBase = tryBase?.Resolve().BaseType;
|
||||
actualBaseDepth++;
|
||||
}
|
||||
|
||||
if (tryBase == null && currentBase != null)
|
||||
continue;
|
||||
|
||||
var baseDepthDifference = Math.Abs(actualBaseDepth - inheritanceDepthOfOriginal);
|
||||
|
||||
if (baseDepthDifference > 1)
|
||||
continue; // heuristic optimization
|
||||
|
||||
currentPenalty -= baseDepthDifference * 50;
|
||||
|
||||
currentPenalty -= Math.Abs(candidateCleanType.Fields.Count - obfType.Fields.Count) * 5;
|
||||
currentPenalty -= Math.Abs(obfType.NestedTypes.Count - candidateCleanType.NestedTypes.Count) * 10;
|
||||
currentPenalty -= Math.Abs(obfType.Properties.Count - candidateCleanType.Properties.Count) * 5;
|
||||
currentPenalty -= Math.Abs(obfType.Interfaces.Count - candidateCleanType.Interfaces.Count) * 35;
|
||||
|
||||
foreach (var obfuscatedField in obfType.Fields)
|
||||
{
|
||||
if (candidateCleanType.Fields.Any(it => it.Name == obfuscatedField.Name))
|
||||
currentPenalty += 10;
|
||||
}
|
||||
|
||||
foreach (var obfuscatedMethod in obfType.Methods)
|
||||
{
|
||||
if (obfuscatedMethod.Name.Contains(".ctor"))
|
||||
continue;
|
||||
|
||||
if (candidateCleanType.Methods.Any(it => it.Name == obfuscatedMethod.Name))
|
||||
currentPenalty += obfuscatedMethod.Name.Length / 10 * 5 + 1;
|
||||
}
|
||||
|
||||
if (currentPenalty == bestPenalty)
|
||||
{
|
||||
weightDictionary.Add(currentPenalty, candidateCleanType);
|
||||
}
|
||||
else if (currentPenalty > bestPenalty)
|
||||
{
|
||||
bestPenalty = currentPenalty;
|
||||
weightDictionary.Add(currentPenalty, candidateCleanType);
|
||||
}
|
||||
}
|
||||
|
||||
// if (bestPenalty < -100)
|
||||
// bestMatch = null;
|
||||
|
||||
return weightDictionary;
|
||||
}
|
||||
|
||||
public class WeightDictionary : List<(int, TypeDefinition)>
|
||||
{
|
||||
private int maxValues { get; }
|
||||
|
||||
public WeightDictionary(int maxValues)
|
||||
{
|
||||
this.maxValues = maxValues;
|
||||
}
|
||||
|
||||
public void Add(int key, TypeDefinition value)
|
||||
{
|
||||
if (Count < maxValues)
|
||||
Add((key, value));
|
||||
|
||||
int lowestWeight = this.Min(x => x.Item1);
|
||||
|
||||
if (key <= lowestWeight)
|
||||
return;
|
||||
|
||||
Remove(this.Last(x => x.Item1 == lowestWeight));
|
||||
|
||||
Add((key, value));
|
||||
|
||||
Sort(new Comparer());
|
||||
}
|
||||
|
||||
public JObject Export()
|
||||
{
|
||||
var jobj = new JObject();
|
||||
|
||||
foreach (var (weight, type) in this)
|
||||
{
|
||||
jobj[type.FullName] = weight;
|
||||
}
|
||||
|
||||
return jobj;
|
||||
}
|
||||
|
||||
private class Comparer : IComparer<(int, TypeDefinition)>
|
||||
{
|
||||
public int Compare((int, TypeDefinition) x, (int, TypeDefinition) y)
|
||||
{
|
||||
// invert the direction, so the biggest is at the top
|
||||
return -x.Item1.CompareTo(y.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
183
SPT-AssemblyTool/SPT-AssemblyTool.csproj
Normal file
183
SPT-AssemblyTool/SPT-AssemblyTool.csproj
Normal file
@ -0,0 +1,183 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>SPT_AssemblyTool</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Assets\de4dot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mono.Cecil" Version="0.11.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Assets\de4dot\AssemblyData.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyData.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20-x64.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20-x64.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20-x64.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20-x64.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20-x64.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR20.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40-x64.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40-x64.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40-x64.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40-x64.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40-x64.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-CLR40.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-x64.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-x64.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-x64.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-x64.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer-x64.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\AssemblyServer.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot-x64.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot-x64.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.blocks.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.blocks.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.code.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.code.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.cui.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.cui.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.exe.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.mdecrypt.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.mdecrypt.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\de4dot.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\dnlib.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\Test.Rename.Dll.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\Test.Rename.Dll.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\Test.Rename.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\Test.Rename.pdb">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\Test.Rename.runtimeconfig.dev.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\de4dot\Test.Rename.runtimeconfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
4
example-mappings.json
Normal file
4
example-mappings.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"GClass2206`1": "Remapped.BindableState",
|
||||
"GInterface254": "Remapped.IBundleLock"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user