chore(all): resolve lint findings, update example plugin, refine docs and add LGPL licenses
This commit is contained in:
+107
-108
@@ -19,18 +19,9 @@ internal static class Program
|
||||
switch (args.FirstOrDefault())
|
||||
{
|
||||
case "prepare" when args.Length == 5: Prepare(args[1], args[2], args[3], args[4]); break;
|
||||
case "prepare-embedded" when args.Length is >= 3 and <= 6:
|
||||
var prepareOptions = args.Skip(3).ToList();
|
||||
string expectedGameVersion = null;
|
||||
int versionOption = prepareOptions.IndexOf("--game-version");
|
||||
if (versionOption >= 0)
|
||||
{
|
||||
if (versionOption + 1 >= prepareOptions.Count) throw new ArgumentException("--game-version requires a version");
|
||||
expectedGameVersion = prepareOptions[versionOption + 1];
|
||||
prepareOptions.RemoveRange(versionOption, 2);
|
||||
}
|
||||
if (prepareOptions.Count > 1) throw new ArgumentException("Invalid prepare-embedded options");
|
||||
PrepareEmbedded(args[1], args[2], prepareOptions.FirstOrDefault(), expectedGameVersion); break;
|
||||
case "prepare-embedded" when args.Length is 3 or 5:
|
||||
if (args.Length == 5 && args[3] != "--game-version") throw new ArgumentException("Invalid prepare-embedded options");
|
||||
PrepareEmbedded(args[1], args[2], args.Length == 5 ? args[4] : null); break;
|
||||
case "export-names" when args.Length == 2: ExportNames(args[1]); break;
|
||||
case "names" when args.Length == 5: GenerateNames(args[1], args[2], args[3], args[4]); break;
|
||||
case "shell" when args.Length == 4: GenerateShell(args[1], args[2], args[3]); break;
|
||||
@@ -40,7 +31,7 @@ internal static class Program
|
||||
case "verify" when args.Length is 3 or 4: Verify(args[1], args[2], args.Length == 4 ? args[3] : null); break;
|
||||
case "verify-runtime" when args.Length == 3: SelfTest.VerifyRuntime(args[1], args[2]); break;
|
||||
case "self-test": SelfTest.Run(); break;
|
||||
default: throw new ArgumentException("Usage: prepare-embedded <Assembly-CSharp.dll> <output-dir> [versions.json] | export-names <names.json> | names <Assembly-CSharp.dll> <mapping.obfuscate> <versions.json> <names.json> | shell <names.json> <Assembly-CSharp.dll> <output.dll> | source-navigation <readable-implementation.dll> <dependency-directory> | verify-navigation <sdk-directory> | reobf <names.json> <input.dll> <output.dll> [Assembly-CSharp.dll] [dependency-directory] | verify <names.json> <plugin.dll> [Assembly-CSharp.dll] | verify-runtime <names.json> <BD2.GameNames.dll> | self-test");
|
||||
default: throw new ArgumentException("Usage: prepare-embedded <Assembly-CSharp.dll> <output-dir> [--game-version version] | export-names <names.json> | names <Assembly-CSharp.dll> <mapping.obfuscate> <versions.json> <names.json> | shell <names.json> <Assembly-CSharp.dll> <output.dll> | source-navigation <readable-implementation.dll> <dependency-directory> | verify-navigation <sdk-directory> | reobf <names.json> <input.dll> <output.dll> [Assembly-CSharp.dll] [dependency-directory] | verify <names.json> <plugin.dll> [Assembly-CSharp.dll] | verify-runtime <names.json> <BD2.GameNames.dll> | self-test");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -58,7 +49,7 @@ internal static class Program
|
||||
|
||||
internal static byte[] EmbeddedNames()
|
||||
{
|
||||
using var stream = typeof(Program).Assembly.GetManifestResourceStream("BD2.GameNames.names.json.gz")
|
||||
using Stream stream = typeof(Program).Assembly.GetManifestResourceStream("BD2.GameNames.names.json.gz")
|
||||
?? throw new InvalidDataException("SDK embedded names table is missing; rebuild the SDK");
|
||||
using var output = new MemoryStream();
|
||||
stream.CopyTo(output);
|
||||
@@ -71,7 +62,7 @@ internal static class Program
|
||||
byte[] compressed = EmbeddedNames();
|
||||
File.WriteAllBytes(tablePath + ".gz", compressed);
|
||||
using var gzip = new GZipStream(new MemoryStream(compressed), CompressionMode.Decompress);
|
||||
using var destination = File.Create(tablePath);
|
||||
using FileStream destination = File.Create(tablePath);
|
||||
gzip.CopyTo(destination);
|
||||
}
|
||||
|
||||
@@ -81,17 +72,17 @@ internal static class Program
|
||||
return File.Exists(pointer) ? File.ReadAllText(pointer).Trim() : Path.GetFullPath(directory);
|
||||
}
|
||||
|
||||
internal static void PrepareEmbedded(string assembly, string output, string versions = null, string expectedGameVersion = null) =>
|
||||
PrepareShared(EmbeddedNames(), assembly, output, versions, expectedGameVersion);
|
||||
internal static void PrepareEmbedded(string assembly, string output, string expectedGameVersion = null) =>
|
||||
PrepareShared(EmbeddedNames(), assembly, output, expectedGameVersion);
|
||||
|
||||
internal static void PreparePackage(string compressedTable, string assembly, string output) =>
|
||||
PrepareShared(File.ReadAllBytes(compressedTable), assembly, output, null, null);
|
||||
PrepareShared(File.ReadAllBytes(compressedTable), assembly, output, null);
|
||||
|
||||
private static void PrepareShared(byte[] compressed, string assembly, string output, string versions, string expectedGameVersion)
|
||||
private static void PrepareShared(byte[] compressed, string assembly, string output, string expectedGameVersion)
|
||||
{
|
||||
output = Path.GetFullPath(output);
|
||||
Directory.CreateDirectory(output);
|
||||
using var outputLock = AcquireCacheLock(Path.Combine(output, "prepare.lock"), "Waiting for this project's game SDK preparation to finish...");
|
||||
using FileStream outputLock = AcquireCacheLock(Path.Combine(output, "prepare.lock"), "Waiting for this project's game SDK preparation to finish...");
|
||||
NameTable table;
|
||||
using (var gzip = new GZipStream(new MemoryStream(compressed), CompressionMode.Decompress))
|
||||
table = JsonSerializer.Deserialize<NameTable>(gzip, Json);
|
||||
@@ -102,12 +93,6 @@ internal static class Program
|
||||
throw new InvalidDataException($"Plugin requires game {expectedGameVersion}, but SDK names target game {gameVersion}. Install the matching SDK package.");
|
||||
if (Hash(assembly) != table.assembly_sha256)
|
||||
throw new InvalidDataException("Game DLL does not match SDK names for game " + gameVersion);
|
||||
if (versions != null)
|
||||
{
|
||||
using var config = JsonDocument.Parse(File.ReadAllText(versions));
|
||||
string expected = config.RootElement.GetProperty("game_version").GetString();
|
||||
if (expected != gameVersion) throw new InvalidDataException($"Repository game_version {expected} does not match SDK embedded names {gameVersion}");
|
||||
}
|
||||
string cacheRoot = Environment.GetEnvironmentVariable("BD2_GAME_SDK_CACHE");
|
||||
if (string.IsNullOrEmpty(cacheRoot)) cacheRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "BD2", "GameSdk", "navigation");
|
||||
string tableHash = Convert.ToHexString(SHA256.HashData(compressed)).ToLowerInvariant();
|
||||
@@ -120,8 +105,8 @@ internal static class Program
|
||||
using (AcquireCacheLock(Path.Combine(shared, "generation.lock")))
|
||||
{
|
||||
string tablePath = Path.Combine(shared, "names.json"), ready = Path.Combine(shared, "ready.txt");
|
||||
string[] relativeFiles = { "names.json", "names.json.gz", ShellName + ".dll", ShellName + ".xml", "navigation.json",
|
||||
"ref/" + ShellName + ".dll", "ref/" + ShellName + ".xml", "lib/" + ShellName + ".dll", "lib/" + ShellName + ".pdb", "lib/" + ShellName + ".xml", "lib/navigation.json" };
|
||||
string[] relativeFiles = [ "names.json", "names.json.gz", ShellName + ".dll", ShellName + ".xml", "navigation.json",
|
||||
"ref/" + ShellName + ".dll", "ref/" + ShellName + ".xml", "lib/" + ShellName + ".dll", "lib/" + ShellName + ".pdb", "lib/" + ShellName + ".xml", "lib/navigation.json" ];
|
||||
string[] files = relativeFiles.Select(p => Path.Combine(shared, p)).ToArray();
|
||||
string StampFiles() => inputs + "|" + string.Join("|", files.Select(Hash));
|
||||
if (!File.Exists(ready) || !files.All(File.Exists) || File.ReadAllText(ready) != StampFiles())
|
||||
@@ -129,7 +114,7 @@ internal static class Program
|
||||
File.Delete(ready);
|
||||
File.WriteAllBytes(tablePath + ".gz", compressed);
|
||||
using (var gzip = new GZipStream(new MemoryStream(compressed), CompressionMode.Decompress))
|
||||
using (var destination = File.Create(tablePath)) gzip.CopyTo(destination);
|
||||
using (FileStream destination = File.Create(tablePath)) gzip.CopyTo(destination);
|
||||
GenerateShell(tablePath, assembly, Path.Combine(shared, ShellName + ".dll"));
|
||||
File.WriteAllText(ready, StampFiles());
|
||||
}
|
||||
@@ -176,11 +161,11 @@ internal static class Program
|
||||
|
||||
private static void WriteNavigationItems(string output)
|
||||
{
|
||||
var manifest = JsonSerializer.Deserialize<SourceNavigation.Manifest>(File.ReadAllText(Path.Combine(output, "navigation.json")));
|
||||
SourceNavigation.Manifest manifest = JsonSerializer.Deserialize<SourceNavigation.Manifest>(File.ReadAllText(Path.Combine(output, "navigation.json")));
|
||||
if (!Directory.Exists(manifest.SourceRoot) || Directory.EnumerateFiles(manifest.SourceRoot, "*.cs", SearchOption.AllDirectories).Count() != manifest.Documents)
|
||||
SourceNavigation.RestoreSources(output);
|
||||
var items = new System.Xml.Linq.XElement("ItemGroup");
|
||||
foreach (var path in Directory.EnumerateFiles(manifest.SourceRoot, "*.cs", SearchOption.AllDirectories).OrderBy(p => p, StringComparer.Ordinal))
|
||||
foreach (string path in Directory.EnumerateFiles(manifest.SourceRoot, "*.cs", SearchOption.AllDirectories).OrderBy(p => p, StringComparer.Ordinal))
|
||||
items.Add(new System.Xml.Linq.XElement("None", new System.Xml.Linq.XAttribute("Include", path),
|
||||
new System.Xml.Linq.XElement("Link", "Game Sources/" + Path.GetRelativePath(manifest.SourceRoot, path)),
|
||||
new System.Xml.Linq.XElement("CopyToOutputDirectory", "Never")));
|
||||
@@ -235,7 +220,7 @@ internal static class Program
|
||||
|
||||
internal static void Prepare(string assembly, string mapping, string versions, string output)
|
||||
{
|
||||
var version = JsonDocument.Parse(File.ReadAllText(versions)).RootElement.GetProperty("game_version").GetString();
|
||||
string version = JsonDocument.Parse(File.ReadAllText(versions)).RootElement.GetProperty("game_version").GetString();
|
||||
if (!Path.GetFileName(mapping).Contains(version, StringComparison.Ordinal))
|
||||
throw new InvalidDataException("Mapping filename must identify game_version " + version);
|
||||
Directory.CreateDirectory(output);
|
||||
@@ -263,24 +248,24 @@ internal static class Program
|
||||
if (string.IsNullOrWhiteSpace(version) || !Path.GetFileName(mapping).Contains(version, StringComparison.Ordinal))
|
||||
throw new InvalidDataException("Mapping filename must identify game_version " + version);
|
||||
var map = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var raw in File.ReadLines(mapping))
|
||||
foreach (string raw in File.ReadLines(mapping))
|
||||
{
|
||||
string line = raw.Trim().TrimStart('\ufeff');
|
||||
if (line.Length == 0 || line.StartsWith('#') || line.StartsWith("//")) continue;
|
||||
var parts = line.Split('⇨');
|
||||
if (line.Length == 0 || line.StartsWith('#') || line.StartsWith("//", StringComparison.Ordinal)) continue;
|
||||
string[] parts = line.Split('⇨');
|
||||
if (parts.Length != 2 || string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1]))
|
||||
throw new InvalidDataException("Invalid mapping row: " + line);
|
||||
string key = parts[0].Trim(), value = parts[1].Trim();
|
||||
if (map.TryGetValue(key, out var old) && old != value) throw new InvalidDataException("Ambiguous mapping: " + key);
|
||||
if (map.TryGetValue(key, out string old) && old != value) throw new InvalidDataException("Ambiguous mapping: " + key);
|
||||
map[key] = value;
|
||||
}
|
||||
string Translate(string name)
|
||||
{
|
||||
// Dots in explicit interface members are part of the CLR name, not a scope prefix.
|
||||
if (map.TryGetValue(name, out var value)) return value.Split('/').Last();
|
||||
if (map.TryGetValue(name, out string value)) return value.Split('/').Last();
|
||||
// Accessors can retain their CLR prefix while the property/event is renamed.
|
||||
foreach (string prefix in new[] { "get_", "set_", "add_", "remove_" })
|
||||
if (name.StartsWith(prefix) && map.TryGetValue(name[prefix.Length..], out value))
|
||||
if (name.StartsWith(prefix, StringComparison.Ordinal) && map.TryGetValue(name[prefix.Length..], out value))
|
||||
{
|
||||
string property = value.Split('/').Last();
|
||||
int dot = property.LastIndexOf('.');
|
||||
@@ -288,11 +273,18 @@ internal static class Program
|
||||
}
|
||||
return name;
|
||||
}
|
||||
using var module = ReadModule(assembly);
|
||||
using ModuleDefinition module = ReadModule(assembly);
|
||||
if (module.Assembly.Name.Name != "Assembly-CSharp") throw new InvalidDataException("Expected Assembly-CSharp");
|
||||
var table = new NameTable { game_version = version, assembly_name = module.Assembly.Name.Name,
|
||||
assembly_mvid = module.Mvid.ToString(), assembly_sha256 = Hash(assembly), mapping_sha256 = Hash(mapping), generator_sha256 = Hash(typeof(Program).Assembly.Location) };
|
||||
var definitions = Types(module).ToArray();
|
||||
var table = new NameTable
|
||||
{
|
||||
game_version = version,
|
||||
assembly_name = module.Assembly.Name.Name,
|
||||
assembly_mvid = module.Mvid.ToString(),
|
||||
assembly_sha256 = Hash(assembly),
|
||||
mapping_sha256 = Hash(mapping),
|
||||
generator_sha256 = Hash(typeof(Program).Assembly.Location)
|
||||
};
|
||||
TypeDefinition[] definitions = Types(module).ToArray();
|
||||
var originalTypes = definitions.ToDictionary(t => t, TypeKey);
|
||||
string ReadableType(TypeDefinition t)
|
||||
{
|
||||
@@ -300,27 +292,34 @@ internal static class Program
|
||||
if (map.TryGetValue(t.Name, out string full)) return full.Replace('/', '+');
|
||||
return string.IsNullOrEmpty(t.Namespace) ? Translate(t.Name) : t.Namespace + "." + Translate(t.Name);
|
||||
}
|
||||
foreach (var type in definitions)
|
||||
foreach (TypeDefinition type in definitions)
|
||||
table.types.Add(new TypeName { token = type.MetadataToken.ToInt32(), original = originalTypes[type], readable = ReadableType(type) });
|
||||
if (table.types.GroupBy(t => t.readable).Any(g => g.Count() > 1)) throw new InvalidDataException("Readable type collision");
|
||||
var memberKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var type in definitions)
|
||||
foreach (TypeDefinition type in definitions)
|
||||
{
|
||||
void Add(IMemberDefinition member, string kind, string signature = null, MethodDefinition method = null)
|
||||
{
|
||||
var entry = new MemberName { token = member.MetadataToken.ToInt32(), kind = kind,
|
||||
declaring_type = originalTypes[type], original = member.Name, readable = Translate(member.Name), signature = signature };
|
||||
var entry = new MemberName
|
||||
{
|
||||
token = member.MetadataToken.ToInt32(),
|
||||
kind = kind,
|
||||
declaring_type = originalTypes[type],
|
||||
original = member.Name,
|
||||
readable = Translate(member.Name),
|
||||
signature = signature
|
||||
};
|
||||
if (!memberKeys.Add(originalTypes[type] + "|" + kind + "|" + entry.readable + "|" + signature))
|
||||
throw new InvalidDataException("Readable member collision: " + originalTypes[type] + "." + entry.readable + " " + signature);
|
||||
if (method != null)
|
||||
foreach (var p in method.Parameters)
|
||||
foreach (ParameterDefinition p in method.Parameters)
|
||||
if (Translate(p.Name) != p.Name) entry.parameters.Add(new ParameterName { position = p.Index, original = p.Name, readable = Translate(p.Name) });
|
||||
table.members.Add(entry);
|
||||
}
|
||||
foreach (var m in type.Methods) Add(m, "method", Signature(m), m);
|
||||
foreach (var f in type.Fields) Add(f, "field", Signature(f));
|
||||
foreach (var p in type.Properties) Add(p, "property", TypeKey(p.PropertyType) + "(" + string.Join(",", p.Parameters.Select(a => TypeKey(a.ParameterType))) + ")");
|
||||
foreach (var e in type.Events) Add(e, "event", TypeKey(e.EventType));
|
||||
foreach (MethodDefinition m in type.Methods) Add(m, "method", Signature(m), m);
|
||||
foreach (FieldDefinition f in type.Fields) Add(f, "field", Signature(f));
|
||||
foreach (PropertyDefinition p in type.Properties) Add(p, "property", TypeKey(p.PropertyType) + "(" + string.Join(",", p.Parameters.Select(a => TypeKey(a.ParameterType))) + ")");
|
||||
foreach (EventDefinition e in type.Events) Add(e, "event", TypeKey(e.EventType));
|
||||
}
|
||||
// Keep unrenamed overloads that share a readable name with a renamed member.
|
||||
// Otherwise runtime lookup would accidentally omit the literal overload.
|
||||
@@ -336,23 +335,23 @@ internal static class Program
|
||||
|
||||
internal static void GenerateShell(string tablePath, string assembly, string shellPath)
|
||||
{
|
||||
var table = ReadTable(tablePath);
|
||||
NameTable table = ReadTable(tablePath);
|
||||
if (Hash(assembly) != table.assembly_sha256) throw new InvalidDataException($"Assembly-CSharp does not match SDK names for game {table.game_version}; install the matching client or update the SDK names table");
|
||||
using var module = ReadModule(assembly);
|
||||
var definitions = Types(module).ToArray();
|
||||
using ModuleDefinition module = ReadModule(assembly);
|
||||
TypeDefinition[] definitions = Types(module).ToArray();
|
||||
// Rename references while their declaring types still have original names.
|
||||
Rewrite(module, table, toReadable: true);
|
||||
var membersByToken = table.members.ToDictionary(m => m.token);
|
||||
var typesByToken = table.types.ToDictionary(t => t.token);
|
||||
foreach (var type in definitions)
|
||||
foreach (TypeDefinition type in definitions)
|
||||
{
|
||||
foreach (var member in type.Methods.Cast<IMemberDefinition>().Concat(type.Fields).Concat(type.Properties).Concat(type.Events))
|
||||
foreach (IMemberDefinition member in type.Methods.Cast<IMemberDefinition>().Concat(type.Fields).Concat(type.Properties).Concat(type.Events))
|
||||
{
|
||||
membersByToken.TryGetValue(member.MetadataToken.ToInt32(), out var entry);
|
||||
membersByToken.TryGetValue(member.MetadataToken.ToInt32(), out MemberName entry);
|
||||
if (entry != null) member.Name = entry.readable;
|
||||
if (member is MethodDefinition m)
|
||||
{
|
||||
if (entry != null) foreach (var p in entry.parameters) m.Parameters[p.position].Name = p.readable;
|
||||
if (entry != null) foreach (ParameterName p in entry.parameters) m.Parameters[p.position].Name = p.readable;
|
||||
// Retain IL only for offline source navigation/decompilation.
|
||||
// ReferenceAssemblyAttribute prevents this assembly from executing.
|
||||
}
|
||||
@@ -387,7 +386,7 @@ internal static class Program
|
||||
{
|
||||
var resolver = new DefaultAssemblyResolver();
|
||||
resolver.AddSearchDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
|
||||
foreach (var directory in searchDirectories) resolver.AddSearchDirectory(directory);
|
||||
foreach (string directory in searchDirectories) resolver.AddSearchDirectory(directory);
|
||||
return ModuleDefinition.ReadModule(path, new ReaderParameters { InMemory = true, AssemblyResolver = resolver });
|
||||
}
|
||||
|
||||
@@ -406,7 +405,7 @@ internal static class Program
|
||||
string TranslateSignature(string sig)
|
||||
{
|
||||
// Type references in signatures are tokenized, so substring collisions are avoided.
|
||||
return System.Text.RegularExpressions.Regex.Replace(sig, @"[^,:()\[\]&*<>\s]+", m => readableByOriginal.TryGetValue(m.Value, out var v) ? v : m.Value);
|
||||
return System.Text.RegularExpressions.Regex.Replace(sig, @"[^,:()\[\]&*<>\s]+", m => readableByOriginal.TryGetValue(m.Value, out string v) ? v : m.Value);
|
||||
}
|
||||
var members = table.members.Where(m => m.kind is "method" or "field").ToDictionary(
|
||||
m => (toReadable ? m.declaring_type : readableByOriginal[m.declaring_type]) + "|" + m.kind + "|" + (toReadable ? m.original : m.readable) + "|" + (toReadable ? m.signature : TranslateSignature(m.signature)));
|
||||
@@ -425,38 +424,38 @@ internal static class Program
|
||||
if (provider.HasCustomAttributes) attributes.AddRange(provider.CustomAttributes);
|
||||
}
|
||||
Attributes(module); Attributes(module.Assembly);
|
||||
foreach (var t in Types(module))
|
||||
foreach (TypeDefinition t in Types(module))
|
||||
{
|
||||
Attributes(t);
|
||||
foreach (var gp in t.GenericParameters) Attributes(gp);
|
||||
foreach (var i in t.Interfaces) Attributes(i);
|
||||
foreach (var f in t.Fields) Attributes(f);
|
||||
foreach (var p in t.Properties) Attributes(p);
|
||||
foreach (var e in t.Events) Attributes(e);
|
||||
foreach (var m in t.Methods)
|
||||
foreach (GenericParameter gp in t.GenericParameters) Attributes(gp);
|
||||
foreach (InterfaceImplementation i in t.Interfaces) Attributes(i);
|
||||
foreach (FieldDefinition f in t.Fields) Attributes(f);
|
||||
foreach (PropertyDefinition p in t.Properties) Attributes(p);
|
||||
foreach (EventDefinition e in t.Events) Attributes(e);
|
||||
foreach (MethodDefinition m in t.Methods)
|
||||
{
|
||||
Attributes(m); Attributes(m.MethodReturnType);
|
||||
foreach (var p in m.Parameters) Attributes(p);
|
||||
foreach (var gp in m.GenericParameters) Attributes(gp);
|
||||
foreach (ParameterDefinition p in m.Parameters) Attributes(p);
|
||||
foreach (GenericParameter gp in m.GenericParameters) Attributes(gp);
|
||||
}
|
||||
}
|
||||
foreach (var attribute in attributes) memberRefs.Add(attribute.Constructor);
|
||||
foreach (var type in Types(module))
|
||||
foreach (var method in type.Methods)
|
||||
foreach (CustomAttribute attribute in attributes) memberRefs.Add(attribute.Constructor);
|
||||
foreach (TypeDefinition type in Types(module))
|
||||
foreach (MethodDefinition method in type.Methods)
|
||||
{
|
||||
foreach (var ov in method.Overrides) memberRefs.Add(ov);
|
||||
if (method.HasBody) foreach (var instruction in method.Body.Instructions)
|
||||
if (instruction.Operand is MemberReference mr) memberRefs.Add(mr);
|
||||
foreach (MethodReference ov in method.Overrides) memberRefs.Add(ov);
|
||||
if (method.HasBody) foreach (Instruction instruction in method.Body.Instructions)
|
||||
if (instruction.Operand is MemberReference mr) memberRefs.Add(mr);
|
||||
}
|
||||
foreach (var reference in memberRefs.Distinct())
|
||||
foreach (MemberReference reference in memberRefs.Distinct())
|
||||
{
|
||||
MemberReference member = reference is GenericInstanceMethod gm ? gm.ElementMethod : reference;
|
||||
if (member is not MethodReference && member is not FieldReference || member is IMemberDefinition || !IsGame(member.DeclaringType)) continue;
|
||||
var declaring = member.DeclaringType is GenericInstanceType gi ? gi.ElementType : member.DeclaringType;
|
||||
TypeReference declaring = member.DeclaringType is GenericInstanceType gi ? gi.ElementType : member.DeclaringType;
|
||||
// CLR array Get/Set/Address pseudo-methods have no metadata definitions.
|
||||
if (declaring is ArrayType) continue;
|
||||
string key = TypeKey(declaring) + "|" + (member is MethodReference ? "method" : "field") + "|" + member.Name + "|" + Signature(member);
|
||||
if (members.TryGetValue(key, out var entry)) member.Name = toReadable ? entry.readable : entry.original;
|
||||
if (members.TryGetValue(key, out MemberName entry)) member.Name = toReadable ? entry.readable : entry.original;
|
||||
else if (namedMembers.Contains(key[..key.LastIndexOf('|')])) throw new InvalidDataException("Game member signature does not match the names table: " + key);
|
||||
}
|
||||
// Snapshot names before renaming a parent of a nested type.
|
||||
@@ -464,17 +463,17 @@ internal static class Program
|
||||
void Visit(TypeReference type)
|
||||
{
|
||||
if (type == null || type is GenericParameter) return;
|
||||
if (type is FunctionPointerType fp) { Visit(fp.ReturnType); foreach (var p in fp.Parameters) Visit(p.ParameterType); return; }
|
||||
if (type is FunctionPointerType fp) { Visit(fp.ReturnType); foreach (ParameterDefinition p in fp.Parameters) Visit(p.ParameterType); return; }
|
||||
if (type is TypeSpecification spec)
|
||||
{
|
||||
Visit(spec.ElementType);
|
||||
if (type is GenericInstanceType gi) foreach (var a in gi.GenericArguments) Visit(a);
|
||||
if (type is GenericInstanceType gi) foreach (TypeReference a in gi.GenericArguments) Visit(a);
|
||||
if (type is IModifierType modifier) Visit(modifier.ModifierType);
|
||||
return;
|
||||
}
|
||||
if (!renames.ContainsKey(type) && IsGame(type))
|
||||
{
|
||||
if (!types.TryGetValue(TypeKey(type), out var entry)) throw new InvalidDataException("Unmapped game type: " + TypeKey(type));
|
||||
if (!types.TryGetValue(TypeKey(type), out TypeName entry)) throw new InvalidDataException("Unmapped game type: " + TypeKey(type));
|
||||
renames[type] = toReadable ? entry.readable : entry.original;
|
||||
}
|
||||
Visit(type.DeclaringType);
|
||||
@@ -484,21 +483,21 @@ internal static class Program
|
||||
Visit(argument.Type);
|
||||
if (argument.Value is TypeReference type) Visit(type);
|
||||
if (argument.Value is CustomAttributeArgument boxed) VisitArgument(boxed);
|
||||
if (argument.Value is CustomAttributeArgument[] array) foreach (var item in array) VisitArgument(item);
|
||||
if (argument.Value is CustomAttributeArgument[] array) foreach (CustomAttributeArgument item in array) VisitArgument(item);
|
||||
}
|
||||
foreach (var attribute in attributes)
|
||||
foreach (CustomAttribute attribute in attributes)
|
||||
{
|
||||
// Decode blobs before changing assembly scopes. System.Type arguments are typed
|
||||
// metadata even though the ECMA-335 blob stores assembly-qualified text.
|
||||
foreach (var argument in attribute.ConstructorArguments) VisitArgument(argument);
|
||||
foreach (CustomAttributeArgument argument in attribute.ConstructorArguments) VisitArgument(argument);
|
||||
void NamedArguments(Mono.Collections.Generic.Collection<CustomAttributeNamedArgument> arguments, string kind)
|
||||
{
|
||||
for (int i = 0; i < arguments.Count; i++)
|
||||
{
|
||||
var argument = arguments[i]; VisitArgument(argument.Argument);
|
||||
CustomAttributeNamedArgument argument = arguments[i]; VisitArgument(argument.Argument);
|
||||
if (!IsGame(attribute.AttributeType)) continue;
|
||||
string declaring = TypeKey(attribute.AttributeType);
|
||||
var entry = table.members.SingleOrDefault(m => m.kind == kind &&
|
||||
MemberName entry = table.members.SingleOrDefault(m => m.kind == kind &&
|
||||
(toReadable ? m.declaring_type : readableByOriginal[m.declaring_type]) == declaring &&
|
||||
(toReadable ? m.original : m.readable) == argument.Name);
|
||||
if (entry != null) arguments[i] = new CustomAttributeNamedArgument(toReadable ? entry.readable : entry.original, argument.Argument);
|
||||
@@ -506,43 +505,43 @@ internal static class Program
|
||||
}
|
||||
NamedArguments(attribute.Fields, "field"); NamedArguments(attribute.Properties, "property");
|
||||
}
|
||||
foreach (var t in module.GetTypeReferences()) Visit(t);
|
||||
foreach (var member in memberRefs)
|
||||
foreach (TypeReference t in module.GetTypeReferences()) Visit(t);
|
||||
foreach (MemberReference member in memberRefs)
|
||||
{
|
||||
Visit(member.DeclaringType);
|
||||
if (member is MethodReference m) { Visit(m.ReturnType); foreach (var p in m.Parameters) Visit(p.ParameterType); if (m is GenericInstanceMethod gm) foreach (var a in gm.GenericArguments) Visit(a); }
|
||||
if (member is MethodReference m) { Visit(m.ReturnType); foreach (ParameterDefinition p in m.Parameters) Visit(p.ParameterType); if (m is GenericInstanceMethod gm) foreach (TypeReference a in gm.GenericArguments) Visit(a); }
|
||||
if (member is FieldReference f) Visit(f.FieldType);
|
||||
}
|
||||
foreach (var type in Types(module))
|
||||
foreach (TypeDefinition type in Types(module))
|
||||
{
|
||||
Visit(type.BaseType);
|
||||
foreach (var i in type.Interfaces) Visit(i.InterfaceType);
|
||||
foreach (var f in type.Fields) Visit(f.FieldType);
|
||||
foreach (var p in type.Properties) { Visit(p.PropertyType); foreach (var a in p.Parameters) Visit(a.ParameterType); }
|
||||
foreach (var e in type.Events) Visit(e.EventType);
|
||||
foreach (var gp in type.GenericParameters) foreach (var c in gp.Constraints) Visit(c.ConstraintType);
|
||||
foreach (var m in type.Methods)
|
||||
foreach (InterfaceImplementation i in type.Interfaces) Visit(i.InterfaceType);
|
||||
foreach (FieldDefinition f in type.Fields) Visit(f.FieldType);
|
||||
foreach (PropertyDefinition p in type.Properties) { Visit(p.PropertyType); foreach (ParameterDefinition a in p.Parameters) Visit(a.ParameterType); }
|
||||
foreach (EventDefinition e in type.Events) Visit(e.EventType);
|
||||
foreach (GenericParameter gp in type.GenericParameters) foreach (GenericParameterConstraint c in gp.Constraints) Visit(c.ConstraintType);
|
||||
foreach (MethodDefinition m in type.Methods)
|
||||
{
|
||||
Visit(m.ReturnType);
|
||||
foreach (var p in m.Parameters) Visit(p.ParameterType);
|
||||
foreach (var gp in m.GenericParameters) foreach (var c in gp.Constraints) Visit(c.ConstraintType);
|
||||
if (m.HasBody) { foreach (var v in m.Body.Variables) Visit(v.VariableType); foreach (var h in m.Body.ExceptionHandlers) Visit(h.CatchType); foreach (var i in m.Body.Instructions) if (i.Operand is TypeReference t) Visit(t); }
|
||||
foreach (ParameterDefinition p in m.Parameters) Visit(p.ParameterType);
|
||||
foreach (GenericParameter gp in m.GenericParameters) foreach (GenericParameterConstraint c in gp.Constraints) Visit(c.ConstraintType);
|
||||
if (m.HasBody) { foreach (VariableDefinition v in m.Body.Variables) Visit(v.VariableType); foreach (ExceptionHandler h in m.Body.ExceptionHandlers) Visit(h.CatchType); foreach (Instruction i in m.Body.Instructions) if (i.Operand is TypeReference t) Visit(t); }
|
||||
}
|
||||
}
|
||||
foreach (var pair in renames) SetTypeName(pair.Key, pair.Value);
|
||||
foreach (KeyValuePair<TypeReference, string> pair in renames) SetTypeName(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
internal static void Reobfuscate(string tablePath, string input, string output, string originalAssembly = null, string dependencyDirectory = null)
|
||||
{
|
||||
var table = ReadTable(tablePath);
|
||||
NameTable table = ReadTable(tablePath);
|
||||
var dependencies = new List<string> { Path.GetDirectoryName(Path.GetFullPath(tablePath)), Path.GetDirectoryName(Path.GetFullPath(output)) };
|
||||
if (originalAssembly != null) dependencies.Add(Path.GetDirectoryName(Path.GetFullPath(originalAssembly)));
|
||||
if (dependencyDirectory != null) dependencies.Add(Path.GetFullPath(dependencyDirectory));
|
||||
using var module = ReadModule(input, dependencies.ToArray());
|
||||
using ModuleDefinition module = ReadModule(input, [.. dependencies]);
|
||||
if (module.Assembly.Name.HasPublicKey) throw new InvalidDataException("Signed plugins require an explicit signing workflow");
|
||||
VerifyStamp(module, table);
|
||||
Rewrite(module, table, false);
|
||||
foreach (var a in module.AssemblyReferences.Where(a => a.Name == ShellName)) a.Name = table.assembly_name;
|
||||
foreach (AssemblyNameReference a in module.AssemblyReferences.Where(a => a.Name == ShellName)) a.Name = table.assembly_name;
|
||||
// Preserve compiler PDBs in obj; rewritten runtime DLL deliberately has no stale symbols.
|
||||
string absoluteOutput = Path.GetFullPath(output);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(absoluteOutput));
|
||||
@@ -559,17 +558,17 @@ internal static class Program
|
||||
|
||||
internal static void Verify(string tablePath, string plugin, string originalAssembly = null)
|
||||
{
|
||||
var table = ReadTable(tablePath);
|
||||
using var module = originalAssembly == null ? ReadModule(plugin) : ReadModule(plugin, Path.GetDirectoryName(Path.GetFullPath(originalAssembly)));
|
||||
NameTable table = ReadTable(tablePath);
|
||||
using ModuleDefinition module = originalAssembly == null ? ReadModule(plugin) : ReadModule(plugin, Path.GetDirectoryName(Path.GetFullPath(originalAssembly)));
|
||||
if (module.AssemblyReferences.Any(a => a.Name == ShellName)) throw new InvalidDataException("Runtime DLL still references the readable shell");
|
||||
VerifyStamp(module, table);
|
||||
if (originalAssembly != null)
|
||||
{
|
||||
if (Hash(originalAssembly) != table.assembly_sha256) throw new InvalidDataException("Verification game binary does not match the names table");
|
||||
bool IsGame(TypeReference t) => t.GetElementType().Scope is AssemblyNameReference a && a.Name == table.assembly_name;
|
||||
foreach (var type in module.GetTypeReferences().Where(IsGame))
|
||||
foreach (TypeReference type in module.GetTypeReferences().Where(IsGame))
|
||||
if (type.Resolve() == null) throw new InvalidDataException("Unresolvable game type reference: " + type.FullName);
|
||||
foreach (var member in module.GetMemberReferences().Where(m => IsGame(m.DeclaringType)))
|
||||
foreach (MemberReference member in module.GetMemberReferences().Where(m => IsGame(m.DeclaringType)))
|
||||
{
|
||||
if (member.DeclaringType is ArrayType) continue;
|
||||
if (member is MethodReference method && method.Resolve() == null || member is FieldReference field && field.Resolve() == null)
|
||||
@@ -580,7 +579,7 @@ internal static class Program
|
||||
|
||||
private static void VerifyStamp(ModuleDefinition module, NameTable table)
|
||||
{
|
||||
var markers = module.Assembly.CustomAttributes.Where(a => a.AttributeType.FullName == "System.Reflection.AssemblyMetadataAttribute" && a.ConstructorArguments.Count == 2 && a.ConstructorArguments[0].Value as string == "BD2.GameNames").ToArray();
|
||||
CustomAttribute[] markers = module.Assembly.CustomAttributes.Where(a => a.AttributeType.FullName == "System.Reflection.AssemblyMetadataAttribute" && a.ConstructorArguments.Count == 2 && a.ConstructorArguments[0].Value as string == "BD2.GameNames").ToArray();
|
||||
if (markers.Length != 1 || markers[0].ConstructorArguments[1].Value as string != Stamp(table))
|
||||
throw new InvalidDataException("Missing/mismatched SDK stamp: compile with GameSdkIdentity.g.cs from this names table");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user