mirror of
https://github.com/BepInEx/BepInEx.git
synced 2026-10-20 01:46:13 +00:00
Use C# 10, reformat project
This commit is contained in:
1 parent
62e27a96d3
commit
325e0df14b
110 files changed
+9502
-9609
No files matched your search
@@ -6,7 +6,7 @@
|
||||
<Product>BepInEx.Preloader.Core</Product>
|
||||
<OutputPath>..\bin\Core</OutputPath>
|
||||
<DocumentationFile>$(OutputPath)\$(TargetFramework)\BepInEx.Preloader.Core.xml</DocumentationFile>
|
||||
<LangVersion>9</LangVersion>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace BepInEx.Preloader.Core
|
||||
namespace BepInEx.Preloader.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Doorstop environment variables, passed into the BepInEx preloader.
|
||||
/// <para>https://github.com/NeighTools/UnityDoorstop/wiki#environment-variables</para>
|
||||
/// </summary>
|
||||
public static class EnvVars
|
||||
{
|
||||
/// <summary>
|
||||
/// Doorstop environment variables, passed into the BepInEx preloader.
|
||||
/// <para>https://github.com/NeighTools/UnityDoorstop/wiki#environment-variables</para>
|
||||
/// Path to the assembly that was invoked via Doorstop. Contains the same value as in "targetAssembly" configuration
|
||||
/// option in the config file.
|
||||
/// </summary>
|
||||
public static class EnvVars
|
||||
public static string DOORSTOP_INVOKE_DLL_PATH { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the game's "Managed" folder that contains all the game's managed assemblies
|
||||
/// </summary>
|
||||
public static string DOORSTOP_MANAGED_FOLDER_DIR { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the game executable currently running.
|
||||
/// </summary>
|
||||
public static string DOORSTOP_PROCESS_PATH { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Array of paths where Mono searches DLLs from before assembly resolvers are invoked.
|
||||
/// </summary>
|
||||
public static string[] DOORSTOP_DLL_SEARCH_DIRS { get; private set; }
|
||||
|
||||
internal static void LoadVars()
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to the assembly that was invoked via Doorstop. Contains the same value as in "targetAssembly" configuration
|
||||
/// option in the config file.
|
||||
/// </summary>
|
||||
public static string DOORSTOP_INVOKE_DLL_PATH { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the game's "Managed" folder that contains all the game's managed assemblies
|
||||
/// </summary>
|
||||
public static string DOORSTOP_MANAGED_FOLDER_DIR { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the game executable currently running.
|
||||
/// </summary>
|
||||
public static string DOORSTOP_PROCESS_PATH { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Array of paths where Mono searches DLLs from before assembly resolvers are invoked.
|
||||
/// </summary>
|
||||
public static string[] DOORSTOP_DLL_SEARCH_DIRS { get; private set; }
|
||||
|
||||
internal static void LoadVars()
|
||||
{
|
||||
DOORSTOP_INVOKE_DLL_PATH = Environment.GetEnvironmentVariable("DOORSTOP_INVOKE_DLL_PATH");
|
||||
DOORSTOP_MANAGED_FOLDER_DIR = Environment.GetEnvironmentVariable("DOORSTOP_MANAGED_FOLDER_DIR");
|
||||
DOORSTOP_PROCESS_PATH = Environment.GetEnvironmentVariable("DOORSTOP_PROCESS_PATH");
|
||||
DOORSTOP_DLL_SEARCH_DIRS =
|
||||
Environment.GetEnvironmentVariable("DOORSTOP_DLL_SEARCH_DIRS")?.Split(Path.PathSeparator) ??
|
||||
new string[0];
|
||||
}
|
||||
DOORSTOP_INVOKE_DLL_PATH = Environment.GetEnvironmentVariable("DOORSTOP_INVOKE_DLL_PATH");
|
||||
DOORSTOP_MANAGED_FOLDER_DIR = Environment.GetEnvironmentVariable("DOORSTOP_MANAGED_FOLDER_DIR");
|
||||
DOORSTOP_PROCESS_PATH = Environment.GetEnvironmentVariable("DOORSTOP_PROCESS_PATH");
|
||||
DOORSTOP_DLL_SEARCH_DIRS =
|
||||
Environment.GetEnvironmentVariable("DOORSTOP_DLL_SEARCH_DIRS")?.Split(Path.PathSeparator) ??
|
||||
new string[0];
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using BepInEx.Logging;
|
||||
|
||||
namespace BepInEx.Preloader.Core
|
||||
namespace BepInEx.Preloader.Core;
|
||||
|
||||
public static class PreloaderLogger
|
||||
{
|
||||
public static class PreloaderLogger
|
||||
{
|
||||
public static ManualLogSource Log { get; } = Logger.CreateLogSource("Preloader");
|
||||
}
|
||||
public static ManualLogSource Log { get; } = Logger.CreateLogSource("Preloader");
|
||||
}
|
||||
@@ -3,46 +3,45 @@ using BepInEx.Logging;
|
||||
using BepInEx.Shared;
|
||||
using MonoMod.Utils;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Logging
|
||||
namespace BepInEx.Preloader.Core.Logging;
|
||||
|
||||
public static class ChainloaderLogHelper
|
||||
{
|
||||
public static class ChainloaderLogHelper
|
||||
public static void PrintLogInfo(ManualLogSource log)
|
||||
{
|
||||
public static void PrintLogInfo(ManualLogSource log)
|
||||
var consoleTitle = $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}";
|
||||
log.LogMessage(consoleTitle);
|
||||
|
||||
if (ConsoleManager.ConsoleActive)
|
||||
ConsoleManager.SetConsoleTitle(consoleTitle);
|
||||
|
||||
//See BuildInfoAttribute for more information about this section.
|
||||
var attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);
|
||||
|
||||
if (attributes.Length > 0)
|
||||
{
|
||||
var consoleTitle = $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}";
|
||||
log.LogMessage(consoleTitle);
|
||||
|
||||
if (ConsoleManager.ConsoleActive)
|
||||
ConsoleManager.SetConsoleTitle(consoleTitle);
|
||||
|
||||
//See BuildInfoAttribute for more information about this section.
|
||||
var attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false);
|
||||
|
||||
if (attributes.Length > 0)
|
||||
{
|
||||
var attribute = (BuildInfoAttribute) attributes[0];
|
||||
log.LogMessage(attribute.Info);
|
||||
}
|
||||
|
||||
Logger.LogInfo($"System platform: {PlatformHelper.Current}");
|
||||
var attribute = (BuildInfoAttribute) attributes[0];
|
||||
log.LogMessage(attribute.Info);
|
||||
}
|
||||
|
||||
public static void RewritePreloaderLogs()
|
||||
{
|
||||
if (PreloaderConsoleListener.LogEvents == null || PreloaderConsoleListener.LogEvents.Count == 0)
|
||||
return;
|
||||
Logger.LogInfo($"System platform: {PlatformHelper.Current}");
|
||||
}
|
||||
|
||||
// Temporarily disable the console log listener (if there is one from preloader) as we replay the preloader logs
|
||||
var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
|
||||
public static void RewritePreloaderLogs()
|
||||
{
|
||||
if (PreloaderConsoleListener.LogEvents == null || PreloaderConsoleListener.LogEvents.Count == 0)
|
||||
return;
|
||||
|
||||
if (logListener != null)
|
||||
Logger.Listeners.Remove(logListener);
|
||||
// Temporarily disable the console log listener (if there is one from preloader) as we replay the preloader logs
|
||||
var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
|
||||
|
||||
foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents)
|
||||
Logger.InternalLogEvent(PreloaderLogger.Log, preloaderLogEvent);
|
||||
if (logListener != null)
|
||||
Logger.Listeners.Remove(logListener);
|
||||
|
||||
if (logListener != null)
|
||||
Logger.Listeners.Add(logListener);
|
||||
}
|
||||
foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents)
|
||||
Logger.InternalLogEvent(PreloaderLogger.Log, preloaderLogEvent);
|
||||
|
||||
if (logListener != null)
|
||||
Logger.Listeners.Add(logListener);
|
||||
}
|
||||
}
|
||||
@@ -2,33 +2,32 @@
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Logging;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Logging
|
||||
namespace BepInEx.Preloader.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Log listener that listens to logs during preloading time and buffers messages for output in Unity logs later.
|
||||
/// </summary>
|
||||
public class PreloaderConsoleListener : ILogListener
|
||||
{
|
||||
private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
|
||||
"Logging.Console", "LogLevels",
|
||||
LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
|
||||
"Which log levels to show in the console output.");
|
||||
|
||||
/// <summary>
|
||||
/// Log listener that listens to logs during preloading time and buffers messages for output in Unity logs later.
|
||||
/// A list of all <see cref="LogEventArgs" /> objects that this listener has received.
|
||||
/// </summary>
|
||||
public class PreloaderConsoleListener : ILogListener
|
||||
public static List<LogEventArgs> LogEvents { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LogEvent(object sender, LogEventArgs eventArgs)
|
||||
{
|
||||
private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
|
||||
"Logging.Console", "LogLevels",
|
||||
LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
|
||||
"Which log levels to show in the console output.");
|
||||
if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0)
|
||||
return;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all <see cref="LogEventArgs" /> objects that this listener has received.
|
||||
/// </summary>
|
||||
public static List<LogEventArgs> LogEvents { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LogEvent(object sender, LogEventArgs eventArgs)
|
||||
{
|
||||
if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0)
|
||||
return;
|
||||
|
||||
LogEvents.Add(eventArgs);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
LogEvents.Add(eventArgs);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() { }
|
||||
}
|
||||
@@ -12,517 +12,518 @@ using BepInEx.Logging;
|
||||
using HarmonyLib;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Patching
|
||||
namespace BepInEx.Preloader.Core.Patching;
|
||||
|
||||
/// <summary>
|
||||
/// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and
|
||||
/// loading assemblies one at a time.
|
||||
/// </summary>
|
||||
public class AssemblyPatcher : IDisposable
|
||||
{
|
||||
private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
|
||||
|
||||
/// <summary>
|
||||
/// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and
|
||||
/// loading assemblies one at a time.
|
||||
/// The context of this assembly patcher instance that is passed to all patcher plugins.
|
||||
/// </summary>
|
||||
public class AssemblyPatcher : IDisposable
|
||||
public PatcherContext PatcherContext { get; } = new()
|
||||
{
|
||||
private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
|
||||
DumpedAssembliesPath = Utility.CombinePaths(Paths.BepInExRootPath, "DumpedAssemblies", Paths.ProcessName)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The context of this assembly patcher instance that is passed to all patcher plugins.
|
||||
/// </summary>
|
||||
public PatcherContext PatcherContext { get; } = new()
|
||||
/// <summary>
|
||||
/// A cloned version of <see cref="PatcherPlugins" /> to ensure that any foreach loops do not break when the collection
|
||||
/// gets modified.
|
||||
/// </summary>
|
||||
private IEnumerable<BasePatcher> PatcherPluginsSafe => PatcherContext.PatcherPlugins.ToList();
|
||||
|
||||
private ManualLogSource Logger { get; } = BepInEx.Logging.Logger.CreateLogSource("AssemblyPatcher");
|
||||
|
||||
private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$");
|
||||
|
||||
/// <summary>
|
||||
/// Performs work to dispose collection objects.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var assembly in PatcherContext.AvailableAssemblies)
|
||||
assembly.Value.Dispose();
|
||||
|
||||
PatcherContext.AvailableAssemblies.Clear();
|
||||
|
||||
// Clear to allow GC collection.
|
||||
PatcherContext.PatcherPlugins.Clear();
|
||||
}
|
||||
|
||||
private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath)
|
||||
{
|
||||
if (type.IsInterface || type.IsAbstract && !type.IsSealed)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
DumpedAssembliesPath = Utility.CombinePaths(Paths.BepInExRootPath, "DumpedAssemblies", Paths.ProcessName)
|
||||
if (!type.IsSubtypeOf(typeof(BasePatcher)))
|
||||
return null;
|
||||
}
|
||||
catch (AssemblyResolutionException)
|
||||
{
|
||||
// Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin.
|
||||
return null;
|
||||
}
|
||||
|
||||
var metadata = PatcherPluginInfoAttribute.FromCecilType(type);
|
||||
|
||||
// Perform checks that will prevent the plugin from being loaded in ALL cases
|
||||
if (metadata == null)
|
||||
{
|
||||
Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
|
||||
{
|
||||
Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metadata.Version == null)
|
||||
{
|
||||
Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metadata.Name == null)
|
||||
{
|
||||
Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PatcherPluginMetadata
|
||||
{
|
||||
TypeName = type.FullName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A cloned version of <see cref="PatcherPlugins" /> to ensure that any foreach loops do not break when the collection
|
||||
/// gets modified.
|
||||
/// </summary>
|
||||
private IEnumerable<BasePatcher> PatcherPluginsSafe => PatcherContext.PatcherPlugins.ToList();
|
||||
private bool HasPatcherPlugins(AssemblyDefinition ass)
|
||||
{
|
||||
if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName) &&
|
||||
ass.Name.Name != CurrentAssemblyName)
|
||||
return false;
|
||||
if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName))
|
||||
return false;
|
||||
|
||||
private ManualLogSource Logger { get; } = BepInEx.Logging.Logger.CreateLogSource("AssemblyPatcher");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$");
|
||||
/// <summary>
|
||||
/// Adds all patchers from all managed assemblies specified in a directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory to search patcher DLLs from.</param>
|
||||
public void AddPatchersFromDirectory(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
/// <summary>
|
||||
/// Performs work to dispose collection objects.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
var sortedPatchers = new List<PatchDefinition>();
|
||||
|
||||
var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins);
|
||||
|
||||
// TODO: Add dependency ordering and process attribute filtering
|
||||
|
||||
foreach (var keyValuePair in patchers)
|
||||
{
|
||||
foreach (var assembly in PatcherContext.AvailableAssemblies)
|
||||
assembly.Value.Dispose();
|
||||
var assemblyPath = keyValuePair.Key;
|
||||
var patcherCollection = keyValuePair.Value;
|
||||
|
||||
PatcherContext.AvailableAssemblies.Clear();
|
||||
if (patcherCollection.Count == 0)
|
||||
continue;
|
||||
|
||||
// Clear to allow GC collection.
|
||||
PatcherContext.PatcherPlugins.Clear();
|
||||
}
|
||||
var ass = Assembly.LoadFrom(assemblyPath);
|
||||
|
||||
private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath)
|
||||
{
|
||||
if (type.IsInterface || type.IsAbstract && !type.IsSealed)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
if (!type.IsSubtypeOf(typeof(BasePatcher)))
|
||||
return null;
|
||||
}
|
||||
catch (AssemblyResolutionException)
|
||||
{
|
||||
// Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin.
|
||||
return null;
|
||||
}
|
||||
|
||||
var metadata = PatcherPluginInfoAttribute.FromCecilType(type);
|
||||
|
||||
// Perform checks that will prevent the plugin from being loaded in ALL cases
|
||||
if (metadata == null)
|
||||
{
|
||||
Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
|
||||
{
|
||||
Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metadata.Version == null)
|
||||
{
|
||||
Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (metadata.Name == null)
|
||||
{
|
||||
Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PatcherPluginMetadata
|
||||
{
|
||||
TypeName = type.FullName
|
||||
};
|
||||
}
|
||||
|
||||
private bool HasPatcherPlugins(AssemblyDefinition ass)
|
||||
{
|
||||
if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName) &&
|
||||
ass.Name.Name != CurrentAssemblyName)
|
||||
return false;
|
||||
if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all patchers from all managed assemblies specified in a directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory to search patcher DLLs from.</param>
|
||||
public void AddPatchersFromDirectory(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
var sortedPatchers = new List<PatchDefinition>();
|
||||
|
||||
var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins);
|
||||
|
||||
// TODO: Add dependency ordering and process attribute filtering
|
||||
|
||||
foreach (var keyValuePair in patchers)
|
||||
{
|
||||
var assemblyPath = keyValuePair.Key;
|
||||
var patcherCollection = keyValuePair.Value;
|
||||
|
||||
if (patcherCollection.Count == 0)
|
||||
continue;
|
||||
|
||||
var ass = Assembly.LoadFrom(assemblyPath);
|
||||
|
||||
foreach (var patcherPlugin in patcherCollection)
|
||||
try
|
||||
{
|
||||
var type = ass.GetType(patcherPlugin.TypeName);
|
||||
|
||||
var instance = (BasePatcher)Activator.CreateInstance(type);
|
||||
instance.Context = PatcherContext;
|
||||
|
||||
PatcherContext.PatcherPlugins.Add(instance);
|
||||
|
||||
var methods =
|
||||
type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var targetAssemblies = MetadataHelper.GetAttributes<TargetAssemblyAttribute>(method);
|
||||
var targetTypes = MetadataHelper.GetAttributes<TargetTypeAttribute>(method);
|
||||
|
||||
if (targetAssemblies.Length == 0 && targetTypes.Length == 0)
|
||||
continue;
|
||||
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
if (parameters.Length < 1 || parameters.Length > 2
|
||||
// Next few lines ensure that the first parameter is AssemblyDefinition and does not have any
|
||||
// target type attributes, and vice versa
|
||||
|| !(
|
||||
(parameters[0].ParameterType == typeof(AssemblyDefinition)
|
||||
|| parameters[0].ParameterType ==
|
||||
typeof(AssemblyDefinition).MakeByRefType()
|
||||
&& targetTypes.Length == 0)
|
||||
|| (parameters[0].ParameterType == typeof(TypeDefinition)
|
||||
&& targetAssemblies.Length == 0)
|
||||
)
|
||||
|| (parameters.Length == 2 &&
|
||||
parameters[1].ParameterType != typeof(string))
|
||||
|| (method.ReturnType != typeof(void) &&
|
||||
method.ReturnType != typeof(bool))
|
||||
)
|
||||
{
|
||||
Logger
|
||||
.LogWarning($"Skipping method [{method.FullDescription()}] as it is not a valid patcher method");
|
||||
continue;
|
||||
}
|
||||
|
||||
void AddDefinition(PatchDefinition definition)
|
||||
{
|
||||
Logger.LogDebug($"Discovered patch [{definition.FullName}]");
|
||||
sortedPatchers.Add(definition);
|
||||
}
|
||||
|
||||
foreach (var targetAssembly in targetAssemblies)
|
||||
AddDefinition(new PatchDefinition(targetAssembly, instance, method));
|
||||
foreach (var targetType in targetTypes)
|
||||
AddDefinition(new PatchDefinition(targetType, instance, method));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e)}");
|
||||
}
|
||||
|
||||
var assName = ass.GetName();
|
||||
Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
|
||||
$"Loaded {patcherCollection.Count} patcher type{(patcherCollection.Count == 1 ? "" : "s")} from [{assName.Name} {assName.Version}]");
|
||||
}
|
||||
|
||||
PatcherContext.PatchDefinitions.AddRange(sortedPatchers);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all .dll assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies
|
||||
/// are skipped.
|
||||
/// </summary>
|
||||
/// <param name="directories">The directories to search.</param>
|
||||
public void LoadAssemblyDirectories(params string[] directories) =>
|
||||
LoadAssemblyDirectories(directories, new[] { "dll" });
|
||||
|
||||
/// <summary>
|
||||
/// Adds all assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies are
|
||||
/// skipped.
|
||||
/// </summary>
|
||||
/// <param name="directories">The directory to search.</param>
|
||||
/// <param name="assemblyExtensions">The file extensions to attempt to load.</param>
|
||||
public void LoadAssemblyDirectories(IEnumerable<string> directories, IEnumerable<string> assemblyExtensions)
|
||||
{
|
||||
var filesToSearch = assemblyExtensions
|
||||
.SelectMany(ext => Utility.GetUniqueFilesInDirectories(directories, $"*.{ext}"));
|
||||
|
||||
foreach (var assemblyPath in filesToSearch)
|
||||
{
|
||||
if (!TryLoadAssembly(assemblyPath, out var assembly))
|
||||
continue;
|
||||
|
||||
// NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky
|
||||
// System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency
|
||||
// It's also generally dangerous to change system.dll since so many things rely on it,
|
||||
// and it's already loaded into the appdomain since this loader references it, so we might as well skip it
|
||||
if (assembly.Name.Name == "System"
|
||||
|| assembly.Name.Name ==
|
||||
"mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched
|
||||
{
|
||||
assembly.Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
PatcherContext.AvailableAssemblies.Add(Path.GetFileName(assemblyPath), assembly);
|
||||
|
||||
Logger.LogDebug($"Assembly loaded: {Path.GetFileName(assemblyPath)}");
|
||||
|
||||
//if (UnityPatches.AssemblyLocations.ContainsKey(assembly.FullName))
|
||||
//{
|
||||
// Logger.LogWarning($"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping...");
|
||||
// continue;
|
||||
//}
|
||||
|
||||
//assemblies.Add(Path.GetFileName(assemblyPath), assembly);
|
||||
//UnityPatches.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load a managed assembly as an <see cref="AssemblyDefinition" />. Returns true if successful.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the assembly.</param>
|
||||
/// <param name="assembly">The loaded assembly. Null if not successful in loading.</param>
|
||||
public static bool TryLoadAssembly(string path, out AssemblyDefinition assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
assembly = AssemblyDefinition.ReadAssembly(path, TypeLoader.ReaderParameters);
|
||||
return true;
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
// Not a managed assembly
|
||||
assembly = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies patchers to all assemblies loaded into this assembly patcher and then loads patched assemblies into memory.
|
||||
/// </summary>
|
||||
public void PatchAndLoad()
|
||||
{
|
||||
// First, create a copy of the assembly dictionary as the initializer can change them
|
||||
var assemblies =
|
||||
new Dictionary<string, AssemblyDefinition>(PatcherContext.AvailableAssemblies,
|
||||
StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
// Next, initialize all the patchers
|
||||
foreach (var assemblyPatcher in PatcherPluginsSafe)
|
||||
foreach (var patcherPlugin in patcherCollection)
|
||||
try
|
||||
{
|
||||
assemblyPatcher.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to run initializer of {assemblyPatcher.Info.GUID}: {ex}");
|
||||
}
|
||||
var type = ass.GetType(patcherPlugin.TypeName);
|
||||
|
||||
// Then, perform the actual patching
|
||||
var instance = (BasePatcher) Activator.CreateInstance(type);
|
||||
instance.Context = PatcherContext;
|
||||
|
||||
var patchedAssemblies = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var resolvedAssemblies = new Dictionary<string, string>();
|
||||
PatcherContext.PatcherPlugins.Add(instance);
|
||||
|
||||
// TODO: Maybe instead reload the assembly and repatch with other valid patchers?
|
||||
var invalidAssemblies = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var methods =
|
||||
type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
Logger.LogMessage($"Executing {PatcherContext.PatchDefinitions.Count} patch(es)");
|
||||
|
||||
foreach (var patchDefinition in PatcherContext.PatchDefinitions.ToList())
|
||||
{
|
||||
string targetDll = patchDefinition.TargetAssembly?.TargetAssembly ??
|
||||
patchDefinition.TargetType.TargetAssembly;
|
||||
|
||||
bool isAssemblyPatch = patchDefinition.TargetAssembly != null;
|
||||
|
||||
if (targetDll == TargetAssemblyAttribute.AllAssemblies)
|
||||
{
|
||||
foreach (var kv in PatcherContext.AvailableAssemblies.ToList())
|
||||
foreach (var method in methods)
|
||||
{
|
||||
if (invalidAssemblies.Contains(kv.Key))
|
||||
var targetAssemblies = MetadataHelper.GetAttributes<TargetAssemblyAttribute>(method);
|
||||
var targetTypes = MetadataHelper.GetAttributes<TargetTypeAttribute>(method);
|
||||
|
||||
if (targetAssemblies.Length == 0 && targetTypes.Length == 0)
|
||||
continue;
|
||||
|
||||
RunPatcher(kv.Value, kv.Key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!PatcherContext.AvailableAssemblies.TryGetValue(targetDll, out var assembly)
|
||||
|| invalidAssemblies.Contains(targetDll))
|
||||
continue;
|
||||
var parameters = method.GetParameters();
|
||||
|
||||
RunPatcher(assembly, targetDll);
|
||||
}
|
||||
|
||||
|
||||
bool RunPatcher(AssemblyDefinition assembly, string targetDll)
|
||||
{
|
||||
try
|
||||
{
|
||||
var arguments = new object[patchDefinition.MethodInfo.GetParameters().Length];
|
||||
|
||||
if (!isAssemblyPatch)
|
||||
if (parameters.Length < 1 || parameters.Length > 2
|
||||
// Next few lines ensure that the first parameter is AssemblyDefinition and does not have any
|
||||
// target type attributes, and vice versa
|
||||
|| !(
|
||||
parameters[0].ParameterType == typeof(AssemblyDefinition)
|
||||
|| parameters[0].ParameterType ==
|
||||
typeof(AssemblyDefinition).MakeByRefType()
|
||||
&& targetTypes.Length == 0
|
||||
|| parameters[0].ParameterType == typeof(TypeDefinition)
|
||||
&& targetAssemblies.Length == 0
|
||||
)
|
||||
|| parameters.Length == 2 &&
|
||||
parameters[1].ParameterType != typeof(string)
|
||||
|| method.ReturnType != typeof(void) &&
|
||||
method.ReturnType != typeof(bool)
|
||||
)
|
||||
{
|
||||
var targetType =
|
||||
assembly.MainModule.Types.FirstOrDefault(x => x.FullName ==
|
||||
patchDefinition.TargetType.TargetType);
|
||||
|
||||
if (targetType == null)
|
||||
{
|
||||
Logger
|
||||
.LogWarning($"Unable to find type [{patchDefinition.TargetType.TargetType}] defined in {patchDefinition.MethodInfo.Name}. Skipping patcher"); //TODO: Proper name
|
||||
return false;
|
||||
}
|
||||
|
||||
arguments[0] = targetType;
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments[0] = assembly;
|
||||
}
|
||||
|
||||
if (arguments.Length > 1)
|
||||
arguments[1] = targetDll;
|
||||
|
||||
var result = patchDefinition.MethodInfo.Invoke(patchDefinition.Instance, arguments);
|
||||
|
||||
if (patchDefinition.MethodInfo.ReturnType == typeof(void)
|
||||
|| patchDefinition.MethodInfo.ReturnType == typeof(bool) && (bool)result)
|
||||
{
|
||||
if (isAssemblyPatch)
|
||||
{
|
||||
assembly = (AssemblyDefinition)arguments[0];
|
||||
PatcherContext.AvailableAssemblies[targetDll] = assembly;
|
||||
}
|
||||
|
||||
patchedAssemblies.Add(targetDll);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Failed to run [{patchDefinition.FullName}] when patching [{assembly.Name.Name}]. This assembly will not be patched. Error: {e}");
|
||||
patchedAssemblies.Remove(targetDll);
|
||||
invalidAssemblies.Add(targetDll);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach (var resolvedAss in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var name = Utility.TryParseAssemblyName(resolvedAss.FullName, out var assName)
|
||||
? assName.Name
|
||||
: resolvedAss.FullName;
|
||||
|
||||
// Report only the first type that caused the assembly to load, because any subsequent ones can be false positives
|
||||
if (!resolvedAssemblies.ContainsKey(name))
|
||||
resolvedAssemblies[name] = patchDefinition.MethodInfo.DeclaringType.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any patched assemblies have been already resolved by the CLR
|
||||
// If there are any, they cannot be loaded by the preloader
|
||||
var patchedAssemblyNames =
|
||||
new
|
||||
HashSet<string>(assemblies.Where(kv => patchedAssemblies.Contains(kv.Key)).Select(kv => kv.Value.Name.Name),
|
||||
StringComparer.InvariantCultureIgnoreCase);
|
||||
var earlyLoadAssemblies = resolvedAssemblies.Where(kv => patchedAssemblyNames.Contains(kv.Key)).ToList();
|
||||
|
||||
if (earlyLoadAssemblies.Count != 0)
|
||||
Logger.LogWarning(new StringBuilder()
|
||||
.AppendLine("The following assemblies have been loaded too early and will not be patched by preloader:")
|
||||
.AppendLine(string.Join(Environment.NewLine,
|
||||
earlyLoadAssemblies
|
||||
.Select(kv =>
|
||||
$"* [{kv.Key}] (first loaded by [{kv.Value}])")
|
||||
.ToArray()))
|
||||
.AppendLine("Expect unexpected behavior and issues with plugins and patchers not being loaded.")
|
||||
.ToString());
|
||||
|
||||
var dumpedAssemblyPaths = new Dictionary<string, string>();
|
||||
// Finally, load patched assemblies into memory
|
||||
if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value)
|
||||
{
|
||||
if (!Directory.Exists(PatcherContext.DumpedAssembliesPath))
|
||||
Directory.CreateDirectory(PatcherContext.DumpedAssembliesPath);
|
||||
|
||||
foreach (var kv in assemblies)
|
||||
{
|
||||
var filename = kv.Key;
|
||||
var name = Path.GetFileNameWithoutExtension(filename);
|
||||
var ext = Path.GetExtension(filename);
|
||||
var assembly = kv.Value;
|
||||
|
||||
if (!patchedAssemblies.Contains(filename))
|
||||
continue;
|
||||
for (var i = 0;; i++)
|
||||
{
|
||||
var postfix = i > 0 ? $"_{i}" : "";
|
||||
var path = Path.Combine(PatcherContext.DumpedAssembliesPath, $"{name}{postfix}{ext}");
|
||||
if (!Utility.TryOpenFileStream(path, FileMode.Create, out var fs))
|
||||
Logger
|
||||
.LogWarning($"Skipping method [{method.FullDescription()}] as it is not a valid patcher method");
|
||||
continue;
|
||||
assembly.Write(fs);
|
||||
fs.Dispose();
|
||||
dumpedAssemblyPaths[filename] = path;
|
||||
break;
|
||||
}
|
||||
|
||||
void AddDefinition(PatchDefinition definition)
|
||||
{
|
||||
Logger.LogDebug($"Discovered patch [{definition.FullName}]");
|
||||
sortedPatchers.Add(definition);
|
||||
}
|
||||
|
||||
foreach (var targetAssembly in targetAssemblies)
|
||||
AddDefinition(new PatchDefinition(targetAssembly, instance, method));
|
||||
foreach (var targetType in targetTypes)
|
||||
AddDefinition(new PatchDefinition(targetType, instance, method));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e)}");
|
||||
}
|
||||
|
||||
var assName = ass.GetName();
|
||||
Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug,
|
||||
$"Loaded {patcherCollection.Count} patcher type{(patcherCollection.Count == 1 ? "" : "s")} from [{assName.Name} {assName.Version}]");
|
||||
}
|
||||
|
||||
PatcherContext.PatchDefinitions.AddRange(sortedPatchers);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds all .dll assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies
|
||||
/// are skipped.
|
||||
/// </summary>
|
||||
/// <param name="directories">The directories to search.</param>
|
||||
public void LoadAssemblyDirectories(params string[] directories) =>
|
||||
LoadAssemblyDirectories(directories, new[] { "dll" });
|
||||
|
||||
/// <summary>
|
||||
/// Adds all assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies are
|
||||
/// skipped.
|
||||
/// </summary>
|
||||
/// <param name="directories">The directory to search.</param>
|
||||
/// <param name="assemblyExtensions">The file extensions to attempt to load.</param>
|
||||
public void LoadAssemblyDirectories(IEnumerable<string> directories, IEnumerable<string> assemblyExtensions)
|
||||
{
|
||||
var filesToSearch = assemblyExtensions
|
||||
.SelectMany(ext => Utility.GetUniqueFilesInDirectories(directories, $"*.{ext}"));
|
||||
|
||||
foreach (var assemblyPath in filesToSearch)
|
||||
{
|
||||
if (!TryLoadAssembly(assemblyPath, out var assembly))
|
||||
continue;
|
||||
|
||||
// NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky
|
||||
// System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency
|
||||
// It's also generally dangerous to change system.dll since so many things rely on it,
|
||||
// and it's already loaded into the appdomain since this loader references it, so we might as well skip it
|
||||
if (assembly.Name.Name == "System"
|
||||
|| assembly.Name.Name ==
|
||||
"mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched
|
||||
{
|
||||
assembly.Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
PatcherContext.AvailableAssemblies.Add(Path.GetFileName(assemblyPath), assembly);
|
||||
|
||||
Logger.LogDebug($"Assembly loaded: {Path.GetFileName(assemblyPath)}");
|
||||
|
||||
//if (UnityPatches.AssemblyLocations.ContainsKey(assembly.FullName))
|
||||
//{
|
||||
// Logger.LogWarning($"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping...");
|
||||
// continue;
|
||||
//}
|
||||
|
||||
//assemblies.Add(Path.GetFileName(assemblyPath), assembly);
|
||||
//UnityPatches.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to load a managed assembly as an <see cref="AssemblyDefinition" />. Returns true if successful.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the assembly.</param>
|
||||
/// <param name="assembly">The loaded assembly. Null if not successful in loading.</param>
|
||||
public static bool TryLoadAssembly(string path, out AssemblyDefinition assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
assembly = AssemblyDefinition.ReadAssembly(path, TypeLoader.ReaderParameters);
|
||||
return true;
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
// Not a managed assembly
|
||||
assembly = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies patchers to all assemblies loaded into this assembly patcher and then loads patched assemblies into memory.
|
||||
/// </summary>
|
||||
public void PatchAndLoad()
|
||||
{
|
||||
// First, create a copy of the assembly dictionary as the initializer can change them
|
||||
var assemblies =
|
||||
new Dictionary<string, AssemblyDefinition>(PatcherContext.AvailableAssemblies,
|
||||
StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
// Next, initialize all the patchers
|
||||
foreach (var assemblyPatcher in PatcherPluginsSafe)
|
||||
try
|
||||
{
|
||||
assemblyPatcher.Initialize();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to run initializer of {assemblyPatcher.Info.GUID}: {ex}");
|
||||
}
|
||||
|
||||
// Then, perform the actual patching
|
||||
|
||||
var patchedAssemblies = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var resolvedAssemblies = new Dictionary<string, string>();
|
||||
|
||||
// TODO: Maybe instead reload the assembly and repatch with other valid patchers?
|
||||
var invalidAssemblies = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
Logger.LogMessage($"Executing {PatcherContext.PatchDefinitions.Count} patch(es)");
|
||||
|
||||
foreach (var patchDefinition in PatcherContext.PatchDefinitions.ToList())
|
||||
{
|
||||
var targetDll = patchDefinition.TargetAssembly?.TargetAssembly ??
|
||||
patchDefinition.TargetType.TargetAssembly;
|
||||
|
||||
var isAssemblyPatch = patchDefinition.TargetAssembly != null;
|
||||
|
||||
if (targetDll == TargetAssemblyAttribute.AllAssemblies)
|
||||
{
|
||||
foreach (var kv in PatcherContext.AvailableAssemblies.ToList())
|
||||
{
|
||||
if (invalidAssemblies.Contains(kv.Key))
|
||||
continue;
|
||||
|
||||
RunPatcher(kv.Value, kv.Key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!PatcherContext.AvailableAssemblies.TryGetValue(targetDll, out var assembly)
|
||||
|| invalidAssemblies.Contains(targetDll))
|
||||
continue;
|
||||
|
||||
RunPatcher(assembly, targetDll);
|
||||
}
|
||||
|
||||
|
||||
bool RunPatcher(AssemblyDefinition assembly, string targetDll)
|
||||
{
|
||||
try
|
||||
{
|
||||
var arguments = new object[patchDefinition.MethodInfo.GetParameters().Length];
|
||||
|
||||
if (!isAssemblyPatch)
|
||||
{
|
||||
var targetType =
|
||||
assembly.MainModule.Types.FirstOrDefault(x => x.FullName ==
|
||||
patchDefinition.TargetType.TargetType);
|
||||
|
||||
if (targetType == null)
|
||||
{
|
||||
Logger
|
||||
.LogWarning($"Unable to find type [{patchDefinition.TargetType.TargetType}] defined in {patchDefinition.MethodInfo.Name}. Skipping patcher"); //TODO: Proper name
|
||||
return false;
|
||||
}
|
||||
|
||||
arguments[0] = targetType;
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments[0] = assembly;
|
||||
}
|
||||
|
||||
if (arguments.Length > 1)
|
||||
arguments[1] = targetDll;
|
||||
|
||||
var result = patchDefinition.MethodInfo.Invoke(patchDefinition.Instance, arguments);
|
||||
|
||||
if (patchDefinition.MethodInfo.ReturnType == typeof(void)
|
||||
|| patchDefinition.MethodInfo.ReturnType == typeof(bool) && (bool) result)
|
||||
{
|
||||
if (isAssemblyPatch)
|
||||
{
|
||||
assembly = (AssemblyDefinition) arguments[0];
|
||||
PatcherContext.AvailableAssemblies[targetDll] = assembly;
|
||||
}
|
||||
|
||||
patchedAssemblies.Add(targetDll);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Failed to run [{patchDefinition.FullName}] when patching [{assembly.Name.Name}]. This assembly will not be patched. Error: {e}");
|
||||
patchedAssemblies.Remove(targetDll);
|
||||
invalidAssemblies.Add(targetDll);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ConfigBreakBeforeLoadAssemblies.Value)
|
||||
|
||||
foreach (var resolvedAss in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
Logger.LogInfo($"BepInEx is about load the following assemblies:\n{string.Join("\n", patchedAssemblies.ToArray())}");
|
||||
Logger.LogInfo($"The assemblies were dumped into {PatcherContext.DumpedAssembliesPath}");
|
||||
Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution.");
|
||||
Debugger.Break();
|
||||
var name = Utility.TryParseAssemblyName(resolvedAss.FullName, out var assName)
|
||||
? assName.Name
|
||||
: resolvedAss.FullName;
|
||||
|
||||
// Report only the first type that caused the assembly to load, because any subsequent ones can be false positives
|
||||
if (!resolvedAssemblies.ContainsKey(name))
|
||||
resolvedAssemblies[name] = patchDefinition.MethodInfo.DeclaringType.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any patched assemblies have been already resolved by the CLR
|
||||
// If there are any, they cannot be loaded by the preloader
|
||||
var patchedAssemblyNames =
|
||||
new
|
||||
HashSet<string>(assemblies.Where(kv => patchedAssemblies.Contains(kv.Key)).Select(kv => kv.Value.Name.Name),
|
||||
StringComparer.InvariantCultureIgnoreCase);
|
||||
var earlyLoadAssemblies = resolvedAssemblies.Where(kv => patchedAssemblyNames.Contains(kv.Key)).ToList();
|
||||
|
||||
if (earlyLoadAssemblies.Count != 0)
|
||||
Logger.LogWarning(new StringBuilder()
|
||||
.AppendLine("The following assemblies have been loaded too early and will not be patched by preloader:")
|
||||
.AppendLine(string.Join(Environment.NewLine,
|
||||
earlyLoadAssemblies
|
||||
.Select(kv =>
|
||||
$"* [{kv.Key}] (first loaded by [{kv.Value}])")
|
||||
.ToArray()))
|
||||
.AppendLine("Expect unexpected behavior and issues with plugins and patchers not being loaded.")
|
||||
.ToString());
|
||||
|
||||
var dumpedAssemblyPaths = new Dictionary<string, string>();
|
||||
// Finally, load patched assemblies into memory
|
||||
if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value)
|
||||
{
|
||||
if (!Directory.Exists(PatcherContext.DumpedAssembliesPath))
|
||||
Directory.CreateDirectory(PatcherContext.DumpedAssembliesPath);
|
||||
|
||||
foreach (var kv in assemblies)
|
||||
{
|
||||
var filename = kv.Key;
|
||||
var name = Path.GetFileNameWithoutExtension(filename);
|
||||
var ext = Path.GetExtension(filename);
|
||||
var assembly = kv.Value;
|
||||
|
||||
// Note that since we only *load* assemblies, they shouldn't trigger dependency loading
|
||||
// Not loading all assemblies is very important not only because of memory reasons,
|
||||
// but because some games *rely* on that because of messed up internal dependencies.
|
||||
if (patchedAssemblies.Contains(filename))
|
||||
if (!patchedAssemblies.Contains(filename))
|
||||
continue;
|
||||
for (var i = 0;; i++)
|
||||
{
|
||||
Assembly loadedAssembly;
|
||||
|
||||
if (ConfigLoadDumpedAssemblies.Value &&
|
||||
dumpedAssemblyPaths.TryGetValue(filename, out var dumpedAssemblyPath))
|
||||
loadedAssembly = Assembly.LoadFrom(dumpedAssemblyPath);
|
||||
else
|
||||
{
|
||||
using var assemblyStream = new MemoryStream();
|
||||
assembly.Write(assemblyStream);
|
||||
loadedAssembly = Assembly.Load(assemblyStream.ToArray());
|
||||
}
|
||||
|
||||
PatcherContext.LoadedAssemblies.Add(filename, loadedAssembly);
|
||||
|
||||
Logger.LogDebug($"Loaded '{assembly.FullName}' into memory");
|
||||
var postfix = i > 0 ? $"_{i}" : "";
|
||||
var path = Path.Combine(PatcherContext.DumpedAssembliesPath, $"{name}{postfix}{ext}");
|
||||
if (!Utility.TryOpenFileStream(path, FileMode.Create, out var fs))
|
||||
continue;
|
||||
assembly.Write(fs);
|
||||
fs.Dispose();
|
||||
dumpedAssemblyPaths[filename] = path;
|
||||
break;
|
||||
}
|
||||
|
||||
// Though we have to dispose of all assemblies regardless of them being patched or not
|
||||
assembly.Dispose();
|
||||
}
|
||||
|
||||
// Finally, run all finalizers
|
||||
foreach (var assemblyPatcher in PatcherPluginsSafe)
|
||||
try
|
||||
{
|
||||
assemblyPatcher.Finalizer();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to run finalizer of {assemblyPatcher.Info.GUID}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
#region Config
|
||||
if (ConfigBreakBeforeLoadAssemblies.Value)
|
||||
{
|
||||
Logger.LogInfo($"BepInEx is about load the following assemblies:\n{string.Join("\n", patchedAssemblies.ToArray())}");
|
||||
Logger.LogInfo($"The assemblies were dumped into {PatcherContext.DumpedAssembliesPath}");
|
||||
Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution.");
|
||||
Debugger.Break();
|
||||
}
|
||||
|
||||
private static readonly ConfigEntry<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.Bind(
|
||||
"Preloader", "DumpAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers.");
|
||||
foreach (var kv in assemblies)
|
||||
{
|
||||
var filename = kv.Key;
|
||||
var assembly = kv.Value;
|
||||
|
||||
private static readonly ConfigEntry<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Bind(
|
||||
"Preloader", "LoadDumpedAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies.");
|
||||
// Note that since we only *load* assemblies, they shouldn't trigger dependency loading
|
||||
// Not loading all assemblies is very important not only because of memory reasons,
|
||||
// but because some games *rely* on that because of messed up internal dependencies.
|
||||
if (patchedAssemblies.Contains(filename))
|
||||
{
|
||||
Assembly loadedAssembly;
|
||||
|
||||
private static readonly ConfigEntry<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Bind(
|
||||
"Preloader", "BreakBeforeLoadAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded.");
|
||||
if (ConfigLoadDumpedAssemblies.Value &&
|
||||
dumpedAssemblyPaths.TryGetValue(filename, out var dumpedAssemblyPath))
|
||||
{
|
||||
loadedAssembly = Assembly.LoadFrom(dumpedAssemblyPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var assemblyStream = new MemoryStream();
|
||||
assembly.Write(assemblyStream);
|
||||
loadedAssembly = Assembly.Load(assemblyStream.ToArray());
|
||||
}
|
||||
|
||||
#endregion
|
||||
PatcherContext.LoadedAssemblies.Add(filename, loadedAssembly);
|
||||
|
||||
Logger.LogDebug($"Loaded '{assembly.FullName}' into memory");
|
||||
}
|
||||
|
||||
// Though we have to dispose of all assemblies regardless of them being patched or not
|
||||
assembly.Dispose();
|
||||
}
|
||||
|
||||
// Finally, run all finalizers
|
||||
foreach (var assemblyPatcher in PatcherPluginsSafe)
|
||||
try
|
||||
{
|
||||
assemblyPatcher.Finalizer();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to run finalizer of {assemblyPatcher.Info.GUID}: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
#region Config
|
||||
|
||||
private static readonly ConfigEntry<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.Bind(
|
||||
"Preloader", "DumpAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers.");
|
||||
|
||||
private static readonly ConfigEntry<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Bind(
|
||||
"Preloader", "LoadDumpedAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies.");
|
||||
|
||||
private static readonly ConfigEntry<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Bind(
|
||||
"Preloader", "BreakBeforeLoadAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded.");
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -3,127 +3,126 @@ using System.Linq;
|
||||
using Mono.Cecil;
|
||||
using Version = SemanticVersioning.Version;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Patching
|
||||
namespace BepInEx.Preloader.Core.Patching;
|
||||
|
||||
/// <summary>
|
||||
/// This attribute denotes that a class is a patcher plugin, and specifies the required metadata.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PatcherPluginInfoAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// This attribute denotes that a class is a patcher plugin, and specifies the required metadata.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PatcherPluginInfoAttribute : Attribute
|
||||
/// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
|
||||
/// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
|
||||
/// <param name="Version">The specific version of the plugin.</param>
|
||||
public PatcherPluginInfoAttribute(string GUID, string Name, string Version)
|
||||
{
|
||||
/// <param name="GUID">The unique identifier of the plugin. Should not change between plugin versions.</param>
|
||||
/// <param name="Name">The user friendly name of the plugin. Is able to be changed between versions.</param>
|
||||
/// <param name="Version">The specific version of the plugin.</param>
|
||||
public PatcherPluginInfoAttribute(string GUID, string Name, string Version)
|
||||
this.GUID = GUID;
|
||||
this.Name = Name;
|
||||
this.Version = TryParseLongVersion(Version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier of the plugin. Should not change between plugin versions.
|
||||
/// </summary>
|
||||
public string GUID { get; protected set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The user friendly name of the plugin. Is able to be changed between versions.
|
||||
/// </summary>
|
||||
public string Name { get; protected set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The specific version of the plugin.
|
||||
/// </summary>
|
||||
public Version Version { get; protected set; }
|
||||
|
||||
private static Version TryParseLongVersion(string version)
|
||||
{
|
||||
if (Version.TryParse(version, out var v))
|
||||
return v;
|
||||
|
||||
// no System.Version.TryParse() on .NET 3.5
|
||||
try
|
||||
{
|
||||
this.GUID = GUID;
|
||||
this.Name = Name;
|
||||
this.Version = TryParseLongVersion(Version);
|
||||
var longVersion = new System.Version(version);
|
||||
|
||||
return new Version(longVersion.Major, longVersion.Minor,
|
||||
longVersion.Build != -1 ? longVersion.Build : 0);
|
||||
}
|
||||
catch { }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier of the plugin. Should not change between plugin versions.
|
||||
/// </summary>
|
||||
public string GUID { get; protected set; }
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static PatcherPluginInfoAttribute FromCecilType(TypeDefinition td)
|
||||
{
|
||||
var attr = MetadataHelper.GetCustomAttributes<PatcherPluginInfoAttribute>(td, false).FirstOrDefault();
|
||||
|
||||
/// <summary>
|
||||
/// The user friendly name of the plugin. Is able to be changed between versions.
|
||||
/// </summary>
|
||||
public string Name { get; protected set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The specific version of the plugin.
|
||||
/// </summary>
|
||||
public Version Version { get; protected set; }
|
||||
|
||||
private static Version TryParseLongVersion(string version)
|
||||
{
|
||||
if (Version.TryParse(version, out var v))
|
||||
return v;
|
||||
|
||||
// no System.Version.TryParse() on .NET 3.5
|
||||
try
|
||||
{
|
||||
var longVersion = new System.Version(version);
|
||||
|
||||
return new Version(longVersion.Major, longVersion.Minor,
|
||||
longVersion.Build != -1 ? longVersion.Build : 0);
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (attr == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static PatcherPluginInfoAttribute FromCecilType(TypeDefinition td)
|
||||
{
|
||||
var attr = MetadataHelper.GetCustomAttributes<PatcherPluginInfoAttribute>(td, false).FirstOrDefault();
|
||||
|
||||
if (attr == null)
|
||||
return null;
|
||||
|
||||
return new PatcherPluginInfoAttribute((string)attr.ConstructorArguments[0].Value,
|
||||
(string)attr.ConstructorArguments[1].Value,
|
||||
(string)attr.ConstructorArguments[2].Value);
|
||||
}
|
||||
|
||||
internal static PatcherPluginInfoAttribute FromType(Type type)
|
||||
{
|
||||
var attributes = type.GetCustomAttributes(typeof(PatcherPluginInfoAttribute), false);
|
||||
|
||||
if (attributes.Length == 0)
|
||||
return null;
|
||||
|
||||
return (PatcherPluginInfoAttribute)attributes[0];
|
||||
}
|
||||
return new PatcherPluginInfoAttribute((string) attr.ConstructorArguments[0].Value,
|
||||
(string) attr.ConstructorArguments[1].Value,
|
||||
(string) attr.ConstructorArguments[2].Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines an assembly that a patch method will target.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class TargetAssemblyAttribute : Attribute
|
||||
internal static PatcherPluginInfoAttribute FromType(Type type)
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker used to indicate all possible assemblies to be targeted by a patch method.
|
||||
/// </summary>
|
||||
public const string AllAssemblies = "_all";
|
||||
var attributes = type.GetCustomAttributes(typeof(PatcherPluginInfoAttribute), false);
|
||||
|
||||
/// <param name="targetAssembly">The short filename of the assembly. Use <see cref="AllAssemblies"/> to mark all possible assemblies as targets.</param>
|
||||
public TargetAssemblyAttribute(string targetAssembly)
|
||||
{
|
||||
TargetAssembly = targetAssembly;
|
||||
}
|
||||
if (attributes.Length == 0)
|
||||
return null;
|
||||
|
||||
/// <summary>
|
||||
/// The short filename of the assembly to target.
|
||||
/// </summary>
|
||||
public string TargetAssembly { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a type that a patch method will target.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class TargetTypeAttribute : Attribute
|
||||
{
|
||||
/// <param name="targetAssembly">The short filename of the assembly of which <paramref name="targetType"/> belongs to.</param>
|
||||
/// <param name="targetType">The full name of the type to target for patching.</param>
|
||||
public TargetTypeAttribute(string targetAssembly, string targetType)
|
||||
{
|
||||
TargetAssembly = targetAssembly;
|
||||
TargetType = targetType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The short filename of the assembly to target.
|
||||
/// </summary>
|
||||
public string TargetAssembly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full name of the type to target for patching.
|
||||
/// </summary>
|
||||
public string TargetType { get; }
|
||||
return (PatcherPluginInfoAttribute) attributes[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines an assembly that a patch method will target.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class TargetAssemblyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker used to indicate all possible assemblies to be targeted by a patch method.
|
||||
/// </summary>
|
||||
public const string AllAssemblies = "_all";
|
||||
|
||||
/// <param name="targetAssembly">The short filename of the assembly. Use <see cref="AllAssemblies"/> to mark all possible assemblies as targets.</param>
|
||||
public TargetAssemblyAttribute(string targetAssembly)
|
||||
{
|
||||
TargetAssembly = targetAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The short filename of the assembly to target.
|
||||
/// </summary>
|
||||
public string TargetAssembly { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a type that a patch method will target.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class TargetTypeAttribute : Attribute
|
||||
{
|
||||
/// <param name="targetAssembly">The short filename of the assembly of which <paramref name="targetType"/> belongs to.</param>
|
||||
/// <param name="targetType">The full name of the type to target for patching.</param>
|
||||
public TargetTypeAttribute(string targetAssembly, string targetType)
|
||||
{
|
||||
TargetAssembly = targetAssembly;
|
||||
TargetType = targetType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The short filename of the assembly to target.
|
||||
/// </summary>
|
||||
public string TargetAssembly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full name of the type to target for patching.
|
||||
/// </summary>
|
||||
public string TargetType { get; }
|
||||
}
|
||||
@@ -1,51 +1,50 @@
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Logging;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Patching
|
||||
namespace BepInEx.Preloader.Core.Patching;
|
||||
|
||||
/// <summary>
|
||||
/// A patcher that can contain multiple methods for patching assemblies.
|
||||
/// </summary>
|
||||
public abstract class BasePatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// A patcher that can contain multiple methods for patching assemblies.
|
||||
/// A <see cref="ILogSource"/> instance created for use by this patcher plugin.
|
||||
/// </summary>
|
||||
public abstract class BasePatcher
|
||||
public ManualLogSource Log { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A configuration file binding created with the <see cref="PatcherPluginInfoAttribute.GUID"/> of this plugin as the filename.
|
||||
/// </summary>
|
||||
public ConfigFile Config { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata associated with this patcher plugin.
|
||||
/// </summary>
|
||||
public PatcherPluginInfoAttribute Info { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The context of the <see cref="AssemblyPatcher"/> this BasePatcher is associated with.
|
||||
/// </summary>
|
||||
public PatcherContext Context { get; set; }
|
||||
|
||||
protected BasePatcher()
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="ILogSource"/> instance created for use by this patcher plugin.
|
||||
/// </summary>
|
||||
public ManualLogSource Log { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A configuration file binding created with the <see cref="PatcherPluginInfoAttribute.GUID"/> of this plugin as the filename.
|
||||
/// </summary>
|
||||
public ConfigFile Config { get; }
|
||||
Info = PatcherPluginInfoAttribute.FromType(GetType());
|
||||
|
||||
/// <summary>
|
||||
/// Metadata associated with this patcher plugin.
|
||||
/// </summary>
|
||||
public PatcherPluginInfoAttribute Info { get; }
|
||||
Log = Logger.CreateLogSource(Info.Name);
|
||||
|
||||
/// <summary>
|
||||
/// The context of the <see cref="AssemblyPatcher"/> this BasePatcher is associated with.
|
||||
/// </summary>
|
||||
public PatcherContext Context { get; set; }
|
||||
|
||||
protected BasePatcher()
|
||||
{
|
||||
Info = PatcherPluginInfoAttribute.FromType(GetType());
|
||||
|
||||
Log = Logger.CreateLogSource(Info.Name);
|
||||
|
||||
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Info.GUID + ".cfg"), false,
|
||||
new BepInPlugin(Info.GUID, Info.Name, Info.Version.ToString()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executed before any patches from any plugin are applied.
|
||||
/// </summary>
|
||||
public virtual void Initialize() { }
|
||||
|
||||
/// <summary>
|
||||
/// Executed after all patches from all plugins have been applied.
|
||||
/// </summary>
|
||||
public virtual void Finalizer() { }
|
||||
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Info.GUID + ".cfg"), false,
|
||||
new BepInPlugin(Info.GUID, Info.Name, Info.Version.ToString()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executed before any patches from any plugin are applied.
|
||||
/// </summary>
|
||||
public virtual void Initialize() { }
|
||||
|
||||
/// <summary>
|
||||
/// Executed after all patches from all plugins have been applied.
|
||||
/// </summary>
|
||||
public virtual void Finalizer() { }
|
||||
}
|
||||
@@ -2,97 +2,96 @@ using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Patching
|
||||
namespace BepInEx.Preloader.Core.Patching;
|
||||
|
||||
/// <summary>
|
||||
/// A definition of an individual patch for use by <see cref="AssemblyPatcher"/>.
|
||||
/// </summary>
|
||||
public class PatchDefinition
|
||||
{
|
||||
public PatchDefinition(TargetAssemblyAttribute targetAssembly, BasePatcher instance, MethodInfo methodInfo)
|
||||
{
|
||||
TargetAssembly = targetAssembly;
|
||||
Instance = instance;
|
||||
MethodInfo = methodInfo;
|
||||
|
||||
FullName = $"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetAssembly.TargetAssembly}";
|
||||
}
|
||||
|
||||
public PatchDefinition(TargetTypeAttribute targetType, BasePatcher instance, MethodInfo methodInfo)
|
||||
{
|
||||
TargetType = targetType;
|
||||
Instance = instance;
|
||||
MethodInfo = methodInfo;
|
||||
|
||||
FullName =
|
||||
$"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetType.TargetAssembly}/{TargetType.TargetType}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The assembly / assemblies this patch will target, if there any.
|
||||
/// </summary>
|
||||
public TargetAssemblyAttribute TargetAssembly { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The type / types this patch will target, if there are any.
|
||||
/// </summary>
|
||||
public TargetTypeAttribute TargetType { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The instance of the <see cref="BasePatcher"/> this <see cref="PatchDefinition"/> originates from.
|
||||
/// </summary>
|
||||
public BasePatcher Instance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The method that will perform the patching logic defined by this <see cref="PatchDefinition"/> instance.
|
||||
/// </summary>
|
||||
public MethodInfo MethodInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A friendly name for this patch definition, for use in logging and error tracking.
|
||||
/// </summary>
|
||||
public string FullName { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context provided to patcher plugins from the associated patcher engine.
|
||||
/// </summary>
|
||||
public class PatcherContext
|
||||
{
|
||||
/// <summary>
|
||||
/// A definition of an individual patch for use by <see cref="AssemblyPatcher"/>.
|
||||
/// <para>Contains a list of assemblies that will be patched and loaded into the runtime.</para>
|
||||
/// <para>
|
||||
/// The dictionary has the name of the file, without any directories. These are used by the dumping
|
||||
/// functionality, and as such, these are also required to be unique. They do not have to be exactly the same as
|
||||
/// the real filename, however they have to be mapped deterministically.
|
||||
/// </para>
|
||||
/// <para>Order is not respected, as it will be sorted by dependencies.</para>
|
||||
/// </summary>
|
||||
public class PatchDefinition
|
||||
{
|
||||
public PatchDefinition(TargetAssemblyAttribute targetAssembly, BasePatcher instance, MethodInfo methodInfo)
|
||||
{
|
||||
TargetAssembly = targetAssembly;
|
||||
Instance = instance;
|
||||
MethodInfo = methodInfo;
|
||||
|
||||
FullName = $"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetAssembly.TargetAssembly}";
|
||||
}
|
||||
|
||||
public PatchDefinition(TargetTypeAttribute targetType, BasePatcher instance, MethodInfo methodInfo)
|
||||
{
|
||||
TargetType = targetType;
|
||||
Instance = instance;
|
||||
MethodInfo = methodInfo;
|
||||
|
||||
FullName =
|
||||
$"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetType.TargetAssembly}/{TargetType.TargetType}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The assembly / assemblies this patch will target, if there any.
|
||||
/// </summary>
|
||||
public TargetAssemblyAttribute TargetAssembly { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The type / types this patch will target, if there are any.
|
||||
/// </summary>
|
||||
public TargetTypeAttribute TargetType { get; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The instance of the <see cref="BasePatcher"/> this <see cref="PatchDefinition"/> originates from.
|
||||
/// </summary>
|
||||
public BasePatcher Instance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The method that will perform the patching logic defined by this <see cref="PatchDefinition"/> instance.
|
||||
/// </summary>
|
||||
public MethodInfo MethodInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A friendly name for this patch definition, for use in logging and error tracking.
|
||||
/// </summary>
|
||||
public string FullName { get; }
|
||||
}
|
||||
public Dictionary<string, AssemblyDefinition> AvailableAssemblies { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Context provided to patcher plugins from the associated patcher engine.
|
||||
/// <para>Contains a dictionary of assemblies that have been loaded as part of executing this assembly patcher.</para>
|
||||
/// <para>
|
||||
/// The key is the same key as used in <see cref="LoadedAssemblies" />, while the value is the actual assembly
|
||||
/// itself.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class PatcherContext
|
||||
{
|
||||
/// <summary>
|
||||
/// <para>Contains a list of assemblies that will be patched and loaded into the runtime.</para>
|
||||
/// <para>
|
||||
/// The dictionary has the name of the file, without any directories. These are used by the dumping
|
||||
/// functionality, and as such, these are also required to be unique. They do not have to be exactly the same as
|
||||
/// the real filename, however they have to be mapped deterministically.
|
||||
/// </para>
|
||||
/// <para>Order is not respected, as it will be sorted by dependencies.</para>
|
||||
/// </summary>
|
||||
public Dictionary<string, AssemblyDefinition> AvailableAssemblies { get; } = new();
|
||||
public Dictionary<string, Assembly> LoadedAssemblies { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// <para>Contains a dictionary of assemblies that have been loaded as part of executing this assembly patcher.</para>
|
||||
/// <para>
|
||||
/// The key is the same key as used in <see cref="LoadedAssemblies" />, while the value is the actual assembly
|
||||
/// itself.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Dictionary<string, Assembly> LoadedAssemblies { get; } = new();
|
||||
/// <summary>
|
||||
/// A list of plugins that will be initialized and executed, in the order of the list.
|
||||
/// </summary>
|
||||
public List<BasePatcher> PatcherPlugins { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list of plugins that will be initialized and executed, in the order of the list.
|
||||
/// </summary>
|
||||
public List<BasePatcher> PatcherPlugins { get; } = new();
|
||||
/// <summary>
|
||||
/// A list of individual patches that <see cref="AssemblyPatcher"/> will execute, generated by parsing <see cref="PatcherPlugins"/>.
|
||||
/// </summary>
|
||||
public List<PatchDefinition> PatchDefinitions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list of individual patches that <see cref="AssemblyPatcher"/> will execute, generated by parsing <see cref="PatcherPlugins"/>.
|
||||
/// </summary>
|
||||
public List<PatchDefinition> PatchDefinitions { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The directory location as to where patched assemblies will be saved to and loaded from disk, for debugging
|
||||
/// purposes. Defaults to BepInEx/DumpedAssemblies/<ProcessName>
|
||||
/// </summary>
|
||||
public string DumpedAssembliesPath { get; internal set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// The directory location as to where patched assemblies will be saved to and loaded from disk, for debugging
|
||||
/// purposes. Defaults to BepInEx/DumpedAssemblies/<ProcessName>
|
||||
/// </summary>
|
||||
public string DumpedAssembliesPath { get; internal set; }
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
using System.IO;
|
||||
using BepInEx.Bootstrap;
|
||||
|
||||
namespace BepInEx.Preloader.Core.Patching
|
||||
namespace BepInEx.Preloader.Core.Patching;
|
||||
|
||||
/// <summary>
|
||||
/// A single cached assembly patcher.
|
||||
/// </summary>
|
||||
internal class PatcherPluginMetadata : ICacheable
|
||||
{
|
||||
/// <summary>
|
||||
/// A single cached assembly patcher.
|
||||
/// Type name of the patcher.
|
||||
/// </summary>
|
||||
internal class PatcherPluginMetadata : ICacheable
|
||||
{
|
||||
/// <summary>
|
||||
/// Type name of the patcher.
|
||||
/// </summary>
|
||||
public string TypeName { get; set; } = string.Empty;
|
||||
public string TypeName { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save(BinaryWriter bw) => bw.Write(TypeName);
|
||||
/// <inheritdoc />
|
||||
public void Save(BinaryWriter bw) => bw.Write(TypeName);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Load(BinaryReader br) => TypeName = br.ReadString();
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public void Load(BinaryReader br) => TypeName = br.ReadString();
|
||||
}
|
||||
@@ -4,133 +4,132 @@ using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using MonoMod.Utils;
|
||||
|
||||
namespace BepInEx.Preloader.Core
|
||||
namespace BepInEx.Preloader.Core;
|
||||
|
||||
internal static class PlatformUtils
|
||||
{
|
||||
internal static class PlatformUtils
|
||||
[DllImport("libc.so.6", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr uname_linux(ref utsname_linux utsname);
|
||||
|
||||
[DllImport("/usr/lib/libSystem.dylib", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr uname_osx(ref utsname_osx utsname);
|
||||
|
||||
/// <summary>
|
||||
/// Recreation of MonoMod's PlatformHelper.DeterminePlatform method, but with libc calls instead of creating processes.
|
||||
/// </summary>
|
||||
public static void SetPlatform()
|
||||
{
|
||||
[DllImport("libc.so.6", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr uname_linux(ref utsname_linux utsname);
|
||||
var current = Platform.Unknown;
|
||||
|
||||
[DllImport("/usr/lib/libSystem.dylib", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl,
|
||||
CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr uname_osx(ref utsname_osx utsname);
|
||||
// For old Mono, get from a private property to accurately get the platform.
|
||||
// static extern PlatformID Platform
|
||||
var p_Platform = typeof(Environment).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
string platID;
|
||||
if (p_Platform != null)
|
||||
platID = p_Platform.GetValue(null, new object[0]).ToString();
|
||||
else
|
||||
// For .NET and newer Mono, use the usual value.
|
||||
platID = Environment.OSVersion.Platform.ToString();
|
||||
platID = platID.ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Recreation of MonoMod's PlatformHelper.DeterminePlatform method, but with libc calls instead of creating processes.
|
||||
/// </summary>
|
||||
public static void SetPlatform()
|
||||
if (platID.Contains("win"))
|
||||
current = Platform.Windows;
|
||||
else if (platID.Contains("mac") || platID.Contains("osx"))
|
||||
current = Platform.MacOS;
|
||||
else if (platID.Contains("lin") || platID.Contains("unix")) current = Platform.Linux;
|
||||
|
||||
if (Is(current, Platform.Linux) && Directory.Exists("/data") && File.Exists("/system/build.prop"))
|
||||
current = Platform.Android;
|
||||
else if (Is(current, Platform.Unix) && Directory.Exists("/System/Library/AccessibilityBundles"))
|
||||
current = Platform.iOS;
|
||||
|
||||
// Is64BitOperatingSystem has been added in .NET Framework 4.0
|
||||
var m_get_Is64BitOperatingSystem =
|
||||
typeof(Environment).GetProperty("Is64BitOperatingSystem")?.GetGetMethod();
|
||||
if (m_get_Is64BitOperatingSystem != null)
|
||||
current |= (bool) m_get_Is64BitOperatingSystem.Invoke(null, new object[0]) ? Platform.Bits64 : 0;
|
||||
else
|
||||
current |= IntPtr.Size >= 8 ? Platform.Bits64 : 0;
|
||||
|
||||
if ((Is(current, Platform.MacOS) || Is(current, Platform.Linux)) && Type.GetType("Mono.Runtime") != null)
|
||||
{
|
||||
var current = Platform.Unknown;
|
||||
string arch;
|
||||
IntPtr result;
|
||||
|
||||
// For old Mono, get from a private property to accurately get the platform.
|
||||
// static extern PlatformID Platform
|
||||
var p_Platform = typeof(Environment).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
string platID;
|
||||
if (p_Platform != null)
|
||||
platID = p_Platform.GetValue(null, new object[0]).ToString();
|
||||
else
|
||||
// For .NET and newer Mono, use the usual value.
|
||||
platID = Environment.OSVersion.Platform.ToString();
|
||||
platID = platID.ToLowerInvariant();
|
||||
|
||||
if (platID.Contains("win"))
|
||||
current = Platform.Windows;
|
||||
else if (platID.Contains("mac") || platID.Contains("osx"))
|
||||
current = Platform.MacOS;
|
||||
else if (platID.Contains("lin") || platID.Contains("unix")) current = Platform.Linux;
|
||||
|
||||
if (Is(current, Platform.Linux) && Directory.Exists("/data") && File.Exists("/system/build.prop"))
|
||||
current = Platform.Android;
|
||||
else if (Is(current, Platform.Unix) && Directory.Exists("/System/Library/AccessibilityBundles"))
|
||||
current = Platform.iOS;
|
||||
|
||||
// Is64BitOperatingSystem has been added in .NET Framework 4.0
|
||||
var m_get_Is64BitOperatingSystem =
|
||||
typeof(Environment).GetProperty("Is64BitOperatingSystem")?.GetGetMethod();
|
||||
if (m_get_Is64BitOperatingSystem != null)
|
||||
current |= (bool) m_get_Is64BitOperatingSystem.Invoke(null, new object[0]) ? Platform.Bits64 : 0;
|
||||
else
|
||||
current |= IntPtr.Size >= 8 ? Platform.Bits64 : 0;
|
||||
|
||||
if ((Is(current, Platform.MacOS) || Is(current, Platform.Linux)) && Type.GetType("Mono.Runtime") != null)
|
||||
if (Is(current, Platform.MacOS))
|
||||
{
|
||||
string arch;
|
||||
IntPtr result;
|
||||
|
||||
if (Is(current, Platform.MacOS))
|
||||
{
|
||||
var utsname_osx = new utsname_osx();
|
||||
result = uname_osx(ref utsname_osx);
|
||||
arch = utsname_osx.machine;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Linux
|
||||
var utsname_linux = new utsname_linux();
|
||||
result = uname_linux(ref utsname_linux);
|
||||
arch = utsname_linux.machine;
|
||||
}
|
||||
|
||||
if (result == IntPtr.Zero && (arch.StartsWith("aarch") || arch.StartsWith("arm")))
|
||||
current |= Platform.ARM;
|
||||
var utsname_osx = new utsname_osx();
|
||||
result = uname_osx(ref utsname_osx);
|
||||
arch = utsname_osx.machine;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Detect ARM based on PE info or uname.
|
||||
typeof(object).Module.GetPEKind(out var peKind, out var machine);
|
||||
if (machine == (ImageFileMachine) 0x01C4 /* ARM, .NET Framework 4.5 */)
|
||||
current |= Platform.ARM;
|
||||
// Linux
|
||||
var utsname_linux = new utsname_linux();
|
||||
result = uname_linux(ref utsname_linux);
|
||||
arch = utsname_linux.machine;
|
||||
}
|
||||
|
||||
PlatformHelper.Current = current;
|
||||
if (result == IntPtr.Zero && (arch.StartsWith("aarch") || arch.StartsWith("arm")))
|
||||
current |= Platform.ARM;
|
||||
}
|
||||
|
||||
private static bool Is(Platform current, Platform expected) => (current & expected) == expected;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct utsname_osx
|
||||
else
|
||||
{
|
||||
private const int osx_utslen = 256;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string sysname;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string nodename;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string release;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string machine;
|
||||
// Detect ARM based on PE info or uname.
|
||||
typeof(object).Module.GetPEKind(out var peKind, out var machine);
|
||||
if (machine == (ImageFileMachine) 0x01C4 /* ARM, .NET Framework 4.5 */)
|
||||
current |= Platform.ARM;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct utsname_linux
|
||||
{
|
||||
private const int linux_utslen = 65;
|
||||
PlatformHelper.Current = current;
|
||||
}
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string sysname;
|
||||
private static bool Is(Platform current, Platform expected) => (current & expected) == expected;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string nodename;
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct utsname_osx
|
||||
{
|
||||
private const int osx_utslen = 256;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string release;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string sysname;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string version;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string nodename;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string machine;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string release;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string domainname;
|
||||
}
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)]
|
||||
public string machine;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public struct utsname_linux
|
||||
{
|
||||
private const int linux_utslen = 65;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string sysname;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string nodename;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string release;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string version;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string machine;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)]
|
||||
public string domainname;
|
||||
}
|
||||
}
|
||||
@@ -4,47 +4,46 @@ using System.Text;
|
||||
using BepInEx.Logging;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace BepInEx.Preloader.RuntimeFixes
|
||||
namespace BepInEx.Preloader.RuntimeFixes;
|
||||
|
||||
public static class ConsoleSetOutFix
|
||||
{
|
||||
public static class ConsoleSetOutFix
|
||||
private static LoggedTextWriter loggedTextWriter;
|
||||
internal static ManualLogSource ConsoleLogSource = Logger.CreateLogSource("Console");
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
private static LoggedTextWriter loggedTextWriter;
|
||||
internal static ManualLogSource ConsoleLogSource = Logger.CreateLogSource("Console");
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
loggedTextWriter = new LoggedTextWriter { Parent = Console.Out };
|
||||
Console.SetOut(loggedTextWriter);
|
||||
Harmony.CreateAndPatchAll(typeof(ConsoleSetOutFix));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Console), nameof(Console.SetOut))]
|
||||
[HarmonyPrefix]
|
||||
private static bool OnSetOut(TextWriter newOut)
|
||||
{
|
||||
loggedTextWriter.Parent = newOut;
|
||||
return false;
|
||||
}
|
||||
loggedTextWriter = new LoggedTextWriter { Parent = Console.Out };
|
||||
Console.SetOut(loggedTextWriter);
|
||||
Harmony.CreateAndPatchAll(typeof(ConsoleSetOutFix));
|
||||
}
|
||||
|
||||
internal class LoggedTextWriter : TextWriter
|
||||
[HarmonyPatch(typeof(Console), nameof(Console.SetOut))]
|
||||
[HarmonyPrefix]
|
||||
private static bool OnSetOut(TextWriter newOut)
|
||||
{
|
||||
public override Encoding Encoding { get; } = Encoding.UTF8;
|
||||
loggedTextWriter.Parent = newOut;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public TextWriter Parent { get; set; }
|
||||
internal class LoggedTextWriter : TextWriter
|
||||
{
|
||||
public override Encoding Encoding { get; } = Encoding.UTF8;
|
||||
|
||||
public override void Flush() => Parent.Flush();
|
||||
public TextWriter Parent { get; set; }
|
||||
|
||||
public override void Write(string value)
|
||||
{
|
||||
ConsoleSetOutFix.ConsoleLogSource.LogInfo(value);
|
||||
Parent.Write(value);
|
||||
}
|
||||
public override void Flush() => Parent.Flush();
|
||||
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
ConsoleSetOutFix.ConsoleLogSource.LogInfo(value);
|
||||
Parent.WriteLine(value);
|
||||
}
|
||||
public override void Write(string value)
|
||||
{
|
||||
ConsoleSetOutFix.ConsoleLogSource.LogInfo(value);
|
||||
Parent.Write(value);
|
||||
}
|
||||
|
||||
public override void WriteLine(string value)
|
||||
{
|
||||
ConsoleSetOutFix.ConsoleLogSource.LogInfo(value);
|
||||
Parent.WriteLine(value);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user