Improve multi platform support, fix signatures again

This commit is contained in:
js6pak committed 2021-02-16 15:29:09 +01:00
1 parent 81696bc1ce
commit f2631d375b
16 files changed
+324 -153

No files matched your search

+8
View File
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Mono.Cecil;
@@ -36,5 +38,11 @@ namespace Reactor.Greenhouse
_stopwatch.Stop();
return _stopwatch.Elapsed;
}
public static void ForceExtractToFile(this ZipArchiveEntry source, string destinationFileName)
{
Directory.GetParent(destinationFileName)!.Create();
source.ExtractToFile(destinationFileName, true);
}
}
}
+2 -10
View File
@@ -102,11 +102,7 @@ namespace Reactor.Greenhouse.Generation
{
var matched = matching.Single();
mappedType.Methods.Add(new MappedMethod(new OriginalDescriptor
{
Name = matched.Name,
Signature = matched.GetSignature()
}, cleanMethod.Name));
mappedType.Methods.Add(new MappedMethod(matched, cleanMethod.Name));
}
}
@@ -153,11 +149,7 @@ namespace Reactor.Greenhouse.Generation
if (methodMatch.Success)
{
nested.Methods.Add(new MappedMethod(new OriginalDescriptor
{
Name = nestedMethod.Name,
Signature = nestedMethod.GetSignature()
}, methodMatch.Groups[1].Value));
nested.Methods.Add(new MappedMethod(nestedMethod, methodMatch.Groups[1].Value));
}
}
+15 -35
View File
@@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -12,31 +10,25 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Reactor.Greenhouse.Generation;
using Reactor.Greenhouse.Setup;
using Reactor.Greenhouse.Setup.Provider;
using Reactor.OxygenFilter;
namespace Reactor.Greenhouse
{
internal static class Program
{
private static Task<int> Main(string[] args)
public static async Task Main(string[] args)
{
var rootCommand = new RootCommand
if (!args.Any())
{
new Option<bool>("steam"),
new Option<bool>("itch"),
};
Console.WriteLine("No game versions used!");
return;
}
rootCommand.Handler = CommandHandler.Create<bool, bool>(GenerateAsync);
var gameVersions = args.Select(x => new GameVersion(x)).ToArray();
return rootCommand.InvokeAsync(args);
}
var gameManager = new GameManager(gameVersions);
public static async Task GenerateAsync(bool steam, bool itch)
{
var gameManager = new GameManager();
await gameManager.SetupAsync(steam, itch);
await gameManager.SetupAsync();
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
@@ -50,42 +42,30 @@ namespace Reactor.Greenhouse
Console.WriteLine($"Generating mappings from {oldFile}");
using var cleanModule = ModuleDefinition.ReadModule(File.OpenRead(oldFile));
if (!steam && !itch)
foreach (var game in gameManager.Games)
{
Console.WriteLine("No game providers used! (use --help for more info)");
return;
}
if (steam)
{
await GenerateAsync(gameManager.Steam, cleanModule);
}
if (itch)
{
await GenerateAsync(gameManager.Itch, cleanModule);
await GenerateAsync(game, cleanModule);
}
}
private static async Task GenerateAsync(Game game, ModuleDefinition cleanModule)
{
Console.WriteLine($"Compiling mappings for {game.Name} ({game.Version})");
var version = game.Provider.Version;
Console.WriteLine($"Compiling mappings for {game.Name} ({version})");
using var moduleDef = ModuleDefinition.ReadModule(File.OpenRead(game.Dll));
var version = game.Version;
var postfix = game.Postfix;
var generated = Generator.Generate(new GenerationContext(cleanModule, moduleDef));
await File.WriteAllTextAsync(Path.Combine("work", version + postfix + ".generated.json"), JsonConvert.SerializeObject(generated, Formatting.Indented));
await File.WriteAllTextAsync(Path.Combine("work", version + ".generated.json"), JsonConvert.SerializeObject(generated, Formatting.Indented));
Apply(generated, Path.Combine("universal.json"));
Apply(generated, Path.Combine(version + postfix + ".json"));
Apply(generated, Path.Combine(version + ".json"));
generated.Compile(moduleDef);
Directory.CreateDirectory(Path.Combine("bin"));
await File.WriteAllTextAsync(Path.Combine("bin", version + postfix + ".json"), JsonConvert.SerializeObject(generated));
await File.WriteAllTextAsync(Path.Combine("bin", version + ".json"), JsonConvert.SerializeObject(generated));
}
private static void Apply(Mappings generated, string file)
@@ -12,7 +12,6 @@
<PackageReference Include="Iced" Version="1.10.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
<ProjectReference Include="..\Reactor.OxygenFilter\Reactor.OxygenFilter.csproj" />
<PackageReference Include="js6pak.Il2CppDumper" Version="6.4.20" />
</ItemGroup>
+21 -11
View File
@@ -13,17 +13,14 @@ namespace Reactor.Greenhouse.Setup
{
public BaseProvider Provider { get; }
public string Name { get; }
public char Postfix { get; }
public string Path { get; }
public string Dll { get; }
public string Version { get; private set; }
public Game(BaseProvider provider, string name, string path)
{
Provider = provider;
provider.Game = this;
Name = name;
Postfix = char.ToLowerInvariant(name[0]);
Path = path;
Dll = IOPath.Combine(Path, "DummyDll", "Assembly-CSharp.dll");
}
@@ -32,23 +29,36 @@ namespace Reactor.Greenhouse.Setup
{
Provider.Setup();
await Provider.DownloadAsync();
UpdateVersion();
CheckVersion();
}
public void UpdateVersion()
public void CheckVersion()
{
if (Version != null)
return;
if (Provider.Version.Platform == GamePlatform.Android)
{
return; // TODO
}
Version = GameVersionParser.Parse(System.IO.Path.Combine(Path, "Among Us_Data", "globalgamemanagers"));
var version = GameVersionParser.Parse(System.IO.Path.Combine(Path, "Among Us_Data", "globalgamemanagers"));
if (!Provider.Version.Equals(new GameVersion(version), true))
{
throw new Exception("Downloaded game has invalid version");
}
}
public void Dump()
{
Console.WriteLine($"Dumping {Name}");
var hash = ComputeHash(IOPath.Combine(Path, "GameAssembly.dll"));
var hashFile = IOPath.Combine(Path, "Assembly-CSharp.dll.md5");
var gameAssembly = Provider.Version.Platform switch
{
GamePlatform.Android => "libil2cpp.so",
_ => "GameAssembly.dll"
};
var hash = ComputeHash(IOPath.Combine(Path, gameAssembly));
var hashFile = IOPath.Combine(Path, gameAssembly + ".md5");
if (File.Exists(hashFile) && File.ReadAllText(hashFile) == hash)
{
@@ -56,7 +66,7 @@ namespace Reactor.Greenhouse.Setup
}
if (!Il2CppDumper.Il2CppDumper.PerformDump(
IOPath.Combine(Path, "GameAssembly.dll"),
IOPath.Combine(Path, gameAssembly),
IOPath.Combine(Path, "Among Us_Data", "il2cpp_data", "Metadata", "global-metadata.dat"),
Path,
new Config
+21 -38
View File
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using DepotDownloader;
using Reactor.Greenhouse.Setup.Provider;
@@ -10,58 +11,40 @@ namespace Reactor.Greenhouse.Setup
{
public string WorkPath { get; }
public Game Steam { get; }
public Game Itch { get; }
public Game[] Games { get; }
public GameManager()
public GameManager(GameVersion[] gameVersions)
{
WorkPath = Path.GetFullPath("work");
Steam = new Game(new SteamProvider(false), "steam", Path.Combine(WorkPath, "steam"));
Itch = new Game(new ItchProvider(), "itch", Path.Combine(WorkPath, "itch"));
Games = gameVersions.Select(gameVersion => new Game(
gameVersion.Platform switch
{
GamePlatform.Steam => new SteamProvider(gameVersion),
GamePlatform.Itch => new ItchProvider(gameVersion),
GamePlatform.Android => new AndroidProvider(gameVersion),
_ => throw new ArgumentOutOfRangeException(nameof(gameVersions))
}, gameVersion.ToString(), Path.Combine(WorkPath, gameVersion.ToString())
)).ToArray();
}
public async Task SetupAsync(bool setupSteam, bool setupItch)
public async Task SetupAsync()
{
var steam = setupSteam && Steam.Provider.IsUpdateNeeded();
var itch = setupItch && Itch.Provider.IsUpdateNeeded();
if (steam || itch)
foreach (var game in Games)
{
ContentDownloader.ShutdownSteam3();
if (steam)
if (game.Provider.IsUpdateNeeded())
{
await Steam.DownloadAsync();
Console.WriteLine($"Downloaded {nameof(Steam)} ({Steam.Version})");
}
if (itch)
{
await Itch.DownloadAsync();
Console.WriteLine($"Downloaded {nameof(Itch)} ({Itch.Version})");
await game.DownloadAsync();
Console.WriteLine($"Downloaded {game.Provider.Version}");
}
}
ContentDownloader.ShutdownSteam3();
if (setupSteam)
foreach (var game in Games)
{
Steam.UpdateVersion();
}
if (setupItch)
{
Itch.UpdateVersion();
}
if (setupSteam)
{
Steam.Dump();
}
if (setupItch)
{
Itch.Dump();
game.CheckVersion();
game.Dump();
}
}
}
+75
View File
@@ -0,0 +1,75 @@
using System;
using System.Text.RegularExpressions;
namespace Reactor.Greenhouse.Setup
{
public class GameVersion
{
private static readonly Regex _regex = new Regex(@"^(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)(?<platform>[sia])?", RegexOptions.Compiled);
public static GamePlatform GamePlatformFromShorthand(string shorthand)
{
return shorthand switch
{
"s" => GamePlatform.Steam,
"i" => GamePlatform.Itch,
"a" => GamePlatform.Android,
_ => throw new ArgumentOutOfRangeException(nameof(shorthand))
};
}
public int Major { get; }
public int Minor { get; }
public int Patch { get; }
public GamePlatform? Platform { get; }
public GameVersion(string version)
{
var match = _regex.Match(version);
Major = int.Parse(match.Groups["major"].Value);
Minor = int.Parse(match.Groups["minor"].Value);
Patch = int.Parse(match.Groups["patch"].Value);
var platform = match.Groups["platform"];
Platform = platform.Success && !string.IsNullOrEmpty(platform.Value) ? GamePlatformFromShorthand(platform.Value) : null;
}
public override string ToString()
{
return $"{Major}.{Minor}.{Patch}" + Platform switch
{
GamePlatform.Steam => "s",
GamePlatform.Itch => "i",
GamePlatform.Android => "a",
null => string.Empty,
_ => throw new ArgumentOutOfRangeException()
};
}
public bool Equals(GameVersion other, bool ignorePlatform = false)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Major == other.Major && Minor == other.Minor && Patch == other.Patch && (ignorePlatform || Platform == other.Platform);
}
public override bool Equals(object obj)
{
return Equals((GameVersion) obj);
}
public override int GetHashCode()
{
return HashCode.Combine(Major, Minor, Patch, Platform);
}
}
public enum GamePlatform
{
Steam,
Itch,
Android
}
}
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Threading.Tasks;
namespace Reactor.Greenhouse.Setup.Provider
{
public class AndroidProvider : BaseProvider
{
private Dictionary<GameVersion, string> VersionMap { get; } = new Dictionary<GameVersion, string>
{
[new GameVersion("2020.11.17a")] = "https://www.apkmirror.com/wp-content/themes/APKMirror/download.php?id=1753109"
};
public string DownloadUrl { get; }
public AndroidProvider(GameVersion version) : base(version)
{
DownloadUrl = VersionMap[version];
}
private HttpClient HttpClient { get; } = new HttpClient();
public override void Setup()
{
}
public override async Task DownloadAsync()
{
using var zipArchive = new ZipArchive(await HttpClient.GetStreamAsync(DownloadUrl), ZipArchiveMode.Read);
zipArchive.GetEntry("lib/arm64-v8a/libil2cpp.so")!
.ForceExtractToFile(Path.Combine(Game.Path, "libil2cpp.so"));
zipArchive.GetEntry("assets/bin/Data/Managed/Metadata/global-metadata.dat")!
.ForceExtractToFile(Path.Combine(Game.Path, "Among Us_Data", "il2cpp_data", "Metadata", "global-metadata.dat"));
}
public override bool IsUpdateNeeded()
{
return !Directory.Exists(Game.Path);
}
}
}
@@ -6,6 +6,12 @@ namespace Reactor.Greenhouse.Setup.Provider
public abstract class BaseProvider
{
public Game Game { get; internal set; }
public GameVersion Version { get; }
protected BaseProvider(GameVersion version)
{
Version = version;
}
public abstract void Setup();
public abstract Task DownloadAsync();
@@ -16,6 +16,18 @@ namespace Reactor.Greenhouse.Setup.Provider
{
public class ItchProvider : BaseProvider
{
private Dictionary<GameVersion, int> VersionMap { get; } = new Dictionary<GameVersion, int>
{
[new GameVersion("2020.11.17i")] = 1047908,
};
public int UploadId { get; }
public ItchProvider(GameVersion version) : base(version)
{
UploadId = VersionMap[version];
}
private HttpClient HttpClient { get; } = new HttpClient(new HttpClientHandler
{
CookieContainer = new CookieContainer()
@@ -71,7 +83,7 @@ namespace Reactor.Greenhouse.Setup.Provider
var downloadPageUrl = ((IHtmlAnchorElement) pageDocument.QuerySelector("a[class=button]")).Href;
var downloadDocument = htmlParser.ParseDocument(await HttpClient.GetStringAsync(downloadPageUrl));
var uploadId = ((IHtmlAnchorElement) downloadDocument.QuerySelector("a[class='button download_btn']")).Dataset["upload_id"];
// var uploadId = ((IHtmlAnchorElement) downloadDocument.QuerySelector("a[class='button download_btn']")).Dataset["upload_id"];
var keyRegex = new Regex("key\":\"(.+)\",");
var key = downloadDocument.QuerySelectorAll("script[type='text/javascript']")
@@ -80,7 +92,7 @@ namespace Reactor.Greenhouse.Setup.Provider
.Single(x => x.Success)
.Groups[1].Value;
var json = await HttpClient.PostAsync($"https://innersloth.itch.io/among-us/file/{uploadId}?key={key}", null);
var json = await HttpClient.PostAsync($"https://innersloth.itch.io/among-us/file/{UploadId}?key={key}", null);
var response = JsonConvert.DeserializeObject<DownloadResponse>(await json.Content.ReadAsStringAsync());
Console.WriteLine($"Downloading {response.Url}");
@@ -91,10 +103,8 @@ namespace Reactor.Greenhouse.Setup.Provider
{
if (files.Contains(entry.Name))
{
var path = Path.Combine("work", "itch", entry.FullName.Substring(entry.FullName.IndexOf("/", StringComparison.Ordinal) + 1));
Directory.GetParent(path)!.Create();
entry.ExtractToFile(path, true);
var path = Path.Combine(Game.Path, entry.FullName[(entry.FullName.IndexOf("/", StringComparison.Ordinal) + 1)..]);
entry.ForceExtractToFile(path);
}
}
}
@@ -4,7 +4,6 @@ using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using DepotDownloader;
using SteamKit2;
namespace Reactor.Greenhouse.Setup.Provider
{
@@ -12,43 +11,37 @@ namespace Reactor.Greenhouse.Setup.Provider
{
private const uint AppId = 945360;
private const uint DepotId = 945361;
private const ulong PreObfuscationManifest = 3596575937380717449;
public bool IsPreObfuscation { get; }
public SteamProvider(bool isPreObfuscation)
private Dictionary<GameVersion, ulong> VersionMap { get; } = new Dictionary<GameVersion, ulong>
{
IsPreObfuscation = isPreObfuscation;
[new GameVersion("2019.10.10s")] = 3162069540887216240,
[new GameVersion("2020.12.9s")] = 3306639722673334636
};
public ulong Manifest { get; }
public SteamProvider(GameVersion version) : base(version)
{
Manifest = VersionMap[version];
}
public override bool IsUpdateNeeded()
{
DepotConfigStore.LoadFromFile(Path.Combine(Game.Path, ".DepotDownloader", "depot.config"));
if (DepotConfigStore.Instance.InstalledManifestIDs.TryGetValue(DepotId, out var installedManifest))
try
{
if (IsPreObfuscation)
DepotConfigStore.LoadFromFile(Path.Combine(Game.Path, ".DepotDownloader", "depot.config"));
if (DepotConfigStore.Instance.InstalledManifestIDs.TryGetValue(DepotId, out var installedManifest))
{
if (installedManifest == PreObfuscationManifest)
{
return false;
}
}
else
{
if (ContentDownloader.steam3 == null)
{
ContentDownloader.InitializeSteam3();
}
ContentDownloader.steam3!.RequestAppInfo(AppId);
var depots = ContentDownloader.GetSteam3AppSection(AppId, EAppInfoSection.Depots);
if (installedManifest == depots[DepotId.ToString()]["manifests"][ContentDownloader.DEFAULT_BRANCH].AsUnsignedLong())
if (installedManifest == Manifest)
{
return false;
}
}
}
finally
{
ContentDownloader.ShutdownSteam3();
}
return true;
}
@@ -114,7 +107,7 @@ namespace Reactor.Greenhouse.Setup.Provider
public override Task DownloadAsync()
{
ContentDownloader.Config.InstallDirectory = Game.Path;
return ContentDownloader.DownloadAppAsync(AppId, DepotId, IsPreObfuscation ? PreObfuscationManifest : ContentDownloader.INVALID_MANIFEST_ID);
return ContentDownloader.DownloadAppAsync(AppId, DepotId, Manifest);
}
}
}
@@ -4,7 +4,7 @@
<LangVersion>latest</LangVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Version>0.2.6</Version>
<Version>0.2.7</Version>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/NuclearPowered/Reactor.OxygenFilter</RepositoryUrl>
<PackageLicenseExpression>LGPL-3.0-or-later</PackageLicenseExpression>
+22 -2
View File
@@ -229,8 +229,28 @@ namespace Reactor.OxygenFilter.MSBuild
}
// get same method from obfuscated assembly
var signature = deobfuscatedCall.GetSignature(t => GetObfuscated(t) ?? t.FullName);
MethodReference definition = typeDefinition.GetMethods().Single(x => x.Name == obfuscated && x.GetSignature() == signature);
var nameMatched = typeDefinition.GetMethods().Where(x => x.Name == obfuscated).ToArray();
var signature = deobfuscatedCall.GetSignature((member, original) =>
{
if (member is TypeReference typeReference && typeReference.Scope.Name == "Assembly-CSharp.dll-Deobfuscated")
{
var resolved = typeReference.Resolve();
if (resolved != null)
{
var s = GetObfuscated(resolved);
if (s != null)
{
return s;
}
}
}
return original;
});
MethodReference definition = nameMatched.Length <= 1 ? nameMatched.Single() : nameMatched.Single(x => x.GetSignature() == signature);
// obfuscate generics
if (deobfuscatedCallReference is GenericInstanceMethod generic)
+61 -21
View File
@@ -1,5 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using Mono.Cecil;
@@ -7,34 +9,72 @@ namespace Reactor.OxygenFilter
{
public static class Extensions
{
private static readonly PropertyInfo _suffix = typeof(ArrayType).GetProperty("Suffix", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly Func<TypeSpecification, string> _callBaseFullName;
static Extensions()
{
var dynamicMethod = new DynamicMethod("CallBaseFullname", typeof(string), new[] { typeof(TypeSpecification) }, typeof(Extensions));
var il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, typeof(TypeSpecification).GetProperty(nameof(TypeSpecification.FullName))!.GetMethod);
il.Emit(OpCodes.Ret);
_callBaseFullName = (Func<TypeSpecification, string>) dynamicMethod.CreateDelegate(typeof(Func<TypeSpecification, string>));
}
public delegate string MapDelegate(MemberReference member, string original);
public static void AppendMappedFullName(this StringBuilder sb, MemberReference member, MapDelegate map = null)
{
map ??= (_, original) => original;
if (member is TypeSpecification typeSpecification)
{
sb.Append(map(typeSpecification, _callBaseFullName(typeSpecification)));
if (typeSpecification is GenericInstanceType genericInstanceType)
{
sb.Append("<");
var arguments = genericInstanceType.GenericArguments;
for (var i = 0; i < arguments.Count; i++)
{
if (i > 0)
sb.Append(",");
sb.AppendMappedFullName(arguments[i], map);
}
sb.Append(">");
}
if (typeSpecification is ArrayType arrayType)
{
sb.Append((string) _suffix.GetValue(arrayType));
}
if (typeSpecification is ByReferenceType)
{
sb.Append("&");
}
}
else
{
sb.Append(map(member, member.FullName));
}
}
public static bool IsObfuscated(this string text)
{
return text.Length == 11 && text.All(char.IsUpper);
}
public static string GetSignature(this MethodDefinition methodDefinition, Func<TypeDefinition, string> map = null)
public static string GetSignature(this MethodDefinition methodDefinition, MapDelegate map = null)
{
map ??= provider => provider.FullName;
string MapIfResolved(TypeReference typeReference)
{
TypeDefinition resolved = null;
try
{
resolved = typeReference.Resolve();
}
catch
{
// ignored
}
return resolved != null ? map(resolved) : typeReference.FullName;
}
var sb = new StringBuilder();
sb.Append(MapIfResolved(methodDefinition.ReturnType));
sb.AppendMappedFullName(methodDefinition.ReturnType, map);
sb.Append(" ");
sb.Append("(");
@@ -50,7 +90,7 @@ namespace Reactor.OxygenFilter
if (parameterType is SentinelType)
sb.Append("...,");
sb.Append(MapIfResolved(parameterType));
sb.AppendMappedFullName(parameterType, map);
}
}
+9
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Newtonsoft.Json;
namespace Reactor.OxygenFilter
@@ -188,6 +189,14 @@ namespace Reactor.OxygenFilter
public MappedMethod(OriginalDescriptor original, string mapped) : base(original, mapped)
{
}
public MappedMethod(MethodDefinition methodDefinition, string mapped) : base(new OriginalDescriptor
{
Name = methodDefinition.Name,
Signature = methodDefinition.GetSignature()
}, mapped)
{
}
}
public class MappedType : MappedMember
@@ -3,15 +3,16 @@
<TargetFrameworks>net472;netstandard2.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/NuclearPowered/Reactor.OxygenFilter</RepositoryUrl>
<PackageLicenseExpression>LGPL-3.0-or-later</PackageLicenseExpression>
<Description>Library for deobfuscating Among Us</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
</ItemGroup>
</Project>