using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization.Json;
using System.Security.Cryptography;
using BD2.GameNames.Internal;
namespace BD2.GameNames;
/// Readable names for string-based reflection. Unknown names retain their literal spelling.
public static class Game
{
private sealed class Index
{
internal readonly NameTable Table;
internal readonly Dictionary Types;
internal readonly Dictionary Tokens;
internal Index()
{
using (var stream = typeof(Game).Assembly.GetManifestResourceStream("BD2.GameNames.names.json.gz"))
using (var gzip = new GZipStream(stream ?? throw new InvalidDataException("BD2.GameNames: embedded table missing"), CompressionMode.Decompress))
Table = (NameTable)new DataContractJsonSerializer(typeof(NameTable)).ReadObject(gzip);
if (Table.schema_version != 1) throw new InvalidDataException("BD2.GameNames: unsupported table schema");
Types = Table.types.ToDictionary(t => t.readable, StringComparer.Ordinal);
Tokens = Table.members.ToDictionary(m => m.token);
}
}
private static readonly Lazy Names = new Lazy(() => new Index());
private static readonly ConcurrentDictionary TypeCache = new ConcurrentDictionary();
private static readonly ConcurrentDictionary MemberCache = new ConcurrentDictionary();
private static readonly object ValidationLock = new object();
private static string ValidatedStamp;
/// Gets the game version embedded in this package.
public static string GameVersion => Names.Value.Table.game_version;
/// Translates a full readable type name. Unknown names retain their literal spelling.
public static string TypeName(string readableFullName)
{
if (readableFullName == null) throw new ArgumentNullException(nameof(readableFullName));
return Names.Value.Types.TryGetValue(readableFullName.Replace('/', '+'), out var entry) ? entry.original : readableFullName;
}
/// Finds a readable type in Assembly-CSharp or loaded assemblies; returns null when absent.
public static Type FindType(string readableFullName)
{
if (readableFullName == null) throw new ArgumentNullException(nameof(readableFullName));
if (TypeCache.TryGetValue(readableFullName, out var cached)) return cached;
string original = TypeName(readableFullName);
Type found = GameAssembly().GetType(original, false);
if (found == null)
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
found = assembly.GetType(original, false);
if (found != null) break;
}
if (found != null) TypeCache.TryAdd(readableFullName, found);
return found;
}
private static Assembly GameAssembly() => Assembly.Load("Assembly-CSharp");
private static bool Matches(MemberInfo member, string kind, string readable)
{
if (member == null) throw new ArgumentNullException(nameof(member));
if (readable == null) throw new ArgumentNullException(nameof(readable));
// Metadata tokens retain declaration identity through inheritance and closed generics.
// A hidden literal member must not be mistaken for a renamed member of a base class.
if (member.DeclaringType.Assembly.GetName().Name == Names.Value.Table.assembly_name && Names.Value.Tokens.TryGetValue(member.MetadataToken, out var entry))
return entry.kind == kind && entry.readable == readable;
return member.Name == readable;
}
/// Translate coroutine/member strings. Overloads with different runtime names require Method/MethodName(MethodInfo).
public static string MemberName(Type type, string readable, GameMemberKind kind = GameMemberKind.Method)
{
if (type == null) throw new ArgumentNullException(nameof(type));
if (readable == null) throw new ArgumentNullException(nameof(readable));
string key = kind switch
{
GameMemberKind.Method => "method",
GameMemberKind.Field => "field",
GameMemberKind.Property => "property",
GameMemberKind.Event => "event",
_ => throw new ArgumentOutOfRangeException(nameof(kind))
};
const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
var reflected = kind switch
{
GameMemberKind.Method => type.GetMethods(all).Cast(),
GameMemberKind.Field => type.GetFields(all).Cast(),
GameMemberKind.Property => type.GetProperties(all).Cast(),
GameMemberKind.Event => type.GetEvents(all).Cast(),
_ => throw new ArgumentOutOfRangeException(nameof(kind))
};
var names = reflected.Where(m => Matches(m, key, readable)).Select(m => m.Name).Distinct().ToArray();
if (names.Length == 0) return readable;
if (names.Length != 1) throw new AmbiguousMatchException(type.FullName + "." + readable + ": supply the member signature");
return names[0];
}
/// Returns the actual CLR method name for APIs that accept strings.
public static string MethodName(MethodInfo method) => method?.Name ?? throw new ArgumentNullException(nameof(method));
/// Tests a reflected method against its readable name, preserving overload identity.
public static bool IsGameMethod(this MethodInfo method, string readable) => Matches(method, "method", readable);
/// Translates a readable parameter name using the method declaration; unknown names remain unchanged.
public static string ParameterName(MethodBase method, string readable)
{
if (method == null) throw new ArgumentNullException(nameof(method));
if (readable == null) throw new ArgumentNullException(nameof(readable));
if (method.DeclaringType.Assembly.GetName().Name == Names.Value.Table.assembly_name && Names.Value.Tokens.TryGetValue(method.MetadataToken, out var entry))
{
var p = entry.parameters.SingleOrDefault(n => n.readable == readable);
if (p != null) return p.original;
}
return readable;
}
/// Extracts a MethodInfo from a call expression without executing the expression.
public static MethodInfo Method(Expression> expression) => Call(expression);
/// Extracts a MethodInfo from a call expression without executing the expression.
public static MethodInfo Method(Expression expression) => Call(expression);
/// Extracts a property getter from an expression without evaluating the property.
public static MethodInfo Getter(Expression> expression) => PropertyGetter(expression);
/// Extracts a property getter from an expression without evaluating the property.
public static MethodInfo Getter(Expression> expression) => PropertyGetter(expression);
private static MethodInfo Call(LambdaExpression expression)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
return expression.Body is MethodCallExpression call ? call.Method : throw new ArgumentException("Expected a method call", nameof(expression));
}
private static MethodInfo PropertyGetter(LambdaExpression expression)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
return expression.Body is MemberExpression member && member.Member is PropertyInfo property
? property.GetGetMethod(true) : throw new ArgumentException("Expected a property access", nameof(expression));
}
private static T Cached(Type type, string kind, string readable, BindingFlags flags, Type[] parameters, Func resolve) where T : MemberInfo
{
if (readable == null) throw new ArgumentNullException(nameof(readable));
if (type == null) return null;
string key = type.AssemblyQualifiedName + "|" + kind + "|" + readable + "|" + (int)flags + "|" + (parameters == null ? "*" : string.Join(";", parameters.Select(p => p.AssemblyQualifiedName)));
if (MemberCache.TryGetValue(key, out var cached)) return (T)cached;
T found = resolve();
if (found != null) MemberCache.TryAdd(key, found);
return found;
}
/// Finds a method by readable name and binding flags. Returns null when absent; ambiguous matches throw.
public static MethodInfo GetGameMethod(this Type type, string readable, BindingFlags flags) => Cached(type, "method", readable, flags, null,
() => Single(type.GetMethods(flags).Where(m => Matches(m, "method", readable)), type, readable));
/// Finds a method by readable name and binding flags. Returns null when absent; ambiguous matches throw.
public static MethodInfo GetGameMethod(this Type type, string readable, BindingFlags flags, Binder binder, Type[] parameters, ParameterModifier[] modifiers)
{
if (readable == null) throw new ArgumentNullException(nameof(readable));
if (parameters == null) throw new ArgumentNullException(nameof(parameters));
if (parameters.Any(p => p == null)) throw new ArgumentException("Parameter types cannot contain null", nameof(parameters));
if (type == null) return null;
MethodInfo Resolve()
{
var candidates = type.GetMethods(flags).Where(m => Matches(m, "method", readable)).ToArray();
if (candidates.Length == 0) return null;
return (MethodInfo)(binder ?? Type.DefaultBinder).SelectMethod(flags, candidates, parameters, modifiers);
}
// Custom binders/modifiers can affect resolution; do not cache those calls.
if (binder != null || modifiers != null) return Resolve();
return Cached(type, "method", readable, flags, parameters, Resolve);
}
private static T Single(IEnumerable members, Type type, string readable) where T : MemberInfo
{
var candidates = members.Take(2).ToArray();
if (candidates.Length > 1) throw new AmbiguousMatchException(type.FullName + "." + readable + ": supply the member signature");
return candidates.FirstOrDefault();
}
/// Finds a field by readable name and binding flags. Returns null when absent.
public static FieldInfo GetGameField(this Type type, string readable, BindingFlags flags) => Cached(type, "field", readable, flags, null,
() => Single(type.GetFields(flags).Where(m => Matches(m, "field", readable)), type, readable));
/// Finds a property by readable name and binding flags. Returns null when absent; ambiguous matches throw.
public static PropertyInfo GetGameProperty(this Type type, string readable, BindingFlags flags) => Cached(type, "property", readable, flags, null,
() => Single(type.GetProperties(flags).Where(m => Matches(m, "property", readable)), type, readable));
/// Finds an event by readable name and binding flags. Returns null when absent.
public static EventInfo GetGameEvent(this Type type, string readable, BindingFlags flags) => Cached(type, "event", readable, flags, null,
() => Single(type.GetEvents(flags).Where(m => Matches(m, "event", readable)), type, readable));
/// Validate plugin/table version, binary fingerprint and known metadata entries before installing patches.
public static void Validate(Assembly plugin, Action log = null) => Validate(plugin, GameVersion, log);
/// Validates only the game binary against the embedded table for plugins that do not use the compiler SDK.
public static void ValidateGame(Action log = null) => ValidateCore(null, GameVersion, log);
/// Validates a compiler SDK plugin, its expected game version, the table and the game binary. Throws on mismatch.
public static void Validate(Assembly plugin, string expectedVersion, Action log)
{
if (plugin == null) throw new ArgumentNullException(nameof(plugin));
if (expectedVersion == null) throw new ArgumentNullException(nameof(expectedVersion));
ValidateCore(plugin, expectedVersion, log);
}
private static void ValidateCore(Assembly plugin, string expectedVersion, Action log)
{
try
{
var table = Names.Value.Table;
string stamp = table.game_version + "|" + table.assembly_sha256 + "|" + table.mapping_sha256;
var metadata = plugin?.GetCustomAttributes().SingleOrDefault(a => a.Key == "BD2.GameNames");
if (expectedVersion != table.game_version || plugin != null && metadata?.Value != stamp)
throw new InvalidDataException("plugin/table mismatch; rebuild the plugin and BD2.GameNames together");
lock (ValidationLock)
{
if (ValidatedStamp != stamp)
{
var assembly = GameAssembly();
if (assembly.ManifestModule.ModuleVersionId.ToString() != table.assembly_mvid)
throw new InvalidDataException("Assembly-CSharp MVID mismatch");
using (var sha = SHA256.Create())
using (var stream = File.OpenRead(assembly.Location))
{
string actual = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
if (actual != table.assembly_sha256) throw new InvalidDataException("Assembly-CSharp SHA-256 mismatch");
}
const BindingFlags all = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
var app = FindType("AppManager") ?? throw new TypeLoadException("AppManager");
var intro = FindType("IntroUI") ?? throw new TypeLoadException("IntroUI");
var network = FindType("BDNetwork.NetworkManager") ?? throw new TypeLoadException("BDNetwork.NetworkManager");
if (app.GetGameProperty("IsPlatformLogin", all)?.PropertyType != typeof(bool) ||
intro.GetGameMethod("SendMaintenanceInfo", all, null, new[] { typeof(bool) }, null) == null ||
network.GetGameMethod("GetPachedGameDataPath", all, null, Type.EmptyTypes, null)?.ReturnType != typeof(string))
throw new MissingMemberException("known game-name probes failed");
ValidatedStamp = stamp;
}
}
log?.Invoke("BD2.GameNames self-check passed: game=" + table.game_version + ", MVID=" + table.assembly_mvid);
}
catch (Exception ex)
{
log?.Invoke("BD2.GameNames SELF-CHECK FAILED: expected game=" + expectedVersion + "; " + ex.Message + "; game patches will not be installed");
throw;
}
}
}