From 9c8ad31af87690a6fcb32c28ea9736ea556b25bf Mon Sep 17 00:00:00 2001 From: Bepis <36346617+bbepis@users.noreply.github.com> Date: Mon, 27 Apr 2020 23:44:50 +1000 Subject: [PATCH] Implement prototype .NET loader --- BepInEx.Core/ConsoleManager.cs | 5 + BepInEx.NetLauncher/App.config | 6 + BepInEx.NetLauncher/BasePlugin.cs | 21 +++ .../BepInEx.NetLauncher.csproj | 92 ++++++++++++ BepInEx.NetLauncher/NetChainloader.cs | 20 +++ BepInEx.NetLauncher/NetPreloader.cs | 118 ++++++++++++++++ BepInEx.NetLauncher/Program.cs | 131 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 36 +++++ BepInEx.NetLauncher/Utility.cs | 62 +++++++++ BepInEx.NetLauncher/packages.config | 6 + .../Patching/AssemblyPatcher.cs | 8 +- BepInEx.sln | 42 ++++++ 12 files changed, 545 insertions(+), 2 deletions(-) create mode 100644 BepInEx.NetLauncher/App.config create mode 100644 BepInEx.NetLauncher/BasePlugin.cs create mode 100644 BepInEx.NetLauncher/BepInEx.NetLauncher.csproj create mode 100644 BepInEx.NetLauncher/NetChainloader.cs create mode 100644 BepInEx.NetLauncher/NetPreloader.cs create mode 100644 BepInEx.NetLauncher/Program.cs create mode 100644 BepInEx.NetLauncher/Properties/AssemblyInfo.cs create mode 100644 BepInEx.NetLauncher/Utility.cs create mode 100644 BepInEx.NetLauncher/packages.config diff --git a/BepInEx.Core/ConsoleManager.cs b/BepInEx.Core/ConsoleManager.cs index 2a02a09..ab62d2d 100644 --- a/BepInEx.Core/ConsoleManager.cs +++ b/BepInEx.Core/ConsoleManager.cs @@ -85,6 +85,11 @@ namespace BepInEx } } + public static void ForceSetActive(bool value) + { + ConsoleActive = value; + } + public static readonly ConfigEntry ConfigConsoleEnabled = ConfigFile.CoreConfig.Bind( "Logging.Console", "Enabled", false, diff --git a/BepInEx.NetLauncher/App.config b/BepInEx.NetLauncher/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/BepInEx.NetLauncher/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BepInEx.NetLauncher/BasePlugin.cs b/BepInEx.NetLauncher/BasePlugin.cs new file mode 100644 index 0000000..926bc45 --- /dev/null +++ b/BepInEx.NetLauncher/BasePlugin.cs @@ -0,0 +1,21 @@ +namespace BepInEx.NetLauncher +{ + public abstract class BasePlugin + { + protected HarmonyLib.Harmony HarmonyInstance { get; } + + protected BasePlugin() + { + //var info = PluginInfoHelper.GetPluginInfo(this); + + //HarmonyInstance = new HarmonyLib.Harmony("BepInEx.Plugin." + info.GUID); + } + + public abstract void Load(); + + public virtual bool Unload() + { + return false; + } + } +} diff --git a/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj b/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj new file mode 100644 index 0000000..d7af4bc --- /dev/null +++ b/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {490B052B-7F23-4052-AD04-613856618901} + Exe + BepInEx.NetLauncher + BepInEx.NetLauncher + v4.5.2 + 512 + true + true + + + + + AnyCPU + true + full + false + ..\bin\net\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\bin\net\ + TRACE + prompt + 4 + + + + + + ..\packages\Mono.Cecil.0.11.2\lib\net40\Mono.Cecil.dll + + + ..\packages\Mono.Cecil.0.11.2\lib\net40\Mono.Cecil.Mdb.dll + + + ..\packages\Mono.Cecil.0.11.2\lib\net40\Mono.Cecil.Pdb.dll + + + ..\packages\Mono.Cecil.0.11.2\lib\net40\Mono.Cecil.Rocks.dll + + + ..\packages\MonoMod.RuntimeDetour.20.4.3.1\lib\net40\MonoMod.RuntimeDetour.dll + + + ..\packages\MonoMod.Utils.20.4.3.1\lib\net40\MonoMod.Utils.dll + + + + + + + + + + + + + + + + + + + {4ffba620-f5ed-47f9-b90c-dad1316fd9b9} + BepInEx.Core + + + {15f8bc38-a761-4f93-8903-1b531ac5d9f9} + BepInEx.Preloader.Core + + + {54161cfe-ff42-4dde-b161-3a49545db5cd} + BepInEx.Harmony + + + {a15d6ee6-f954-415b-8605-8a8470cc87dc} + Harmony + + + + \ No newline at end of file diff --git a/BepInEx.NetLauncher/NetChainloader.cs b/BepInEx.NetLauncher/NetChainloader.cs new file mode 100644 index 0000000..33d3f1b --- /dev/null +++ b/BepInEx.NetLauncher/NetChainloader.cs @@ -0,0 +1,20 @@ +using System; +using System.Reflection; +using BepInEx.Bootstrap; + +namespace BepInEx.NetLauncher +{ + public class NetChainloader : BaseChainloader + { + public override BasePlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) + { + var type = pluginAssembly.GetType(pluginInfo.TypeName); + + var pluginInstance = (BasePlugin)Activator.CreateInstance(type); + + pluginInstance.Load(); + + return pluginInstance; + } + } +} \ No newline at end of file diff --git a/BepInEx.NetLauncher/NetPreloader.cs b/BepInEx.NetLauncher/NetPreloader.cs new file mode 100644 index 0000000..a63365e --- /dev/null +++ b/BepInEx.NetLauncher/NetPreloader.cs @@ -0,0 +1,118 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using BepInEx.Bootstrap; +using BepInEx.Configuration; +using BepInEx.Logging; +using BepInEx.Preloader.Core; +using BepInEx.Preloader.Core.RuntimeFixes; +using MonoMod.RuntimeDetour; + +namespace BepInEx.NetLauncher +{ + public static class NetPreloader + { + private static readonly ManualLogSource Log = PreloaderLogger.Log; + + + public static void Start(string[] args) + { + if (ConfigEntrypointExecutable.Value == null) + { + Log.LogFatal($"Entry executable was not set. Please set this in your config before launching the application"); + Program.ReadExit(); + return; + } + + string executablePath = Path.GetFullPath(ConfigEntrypointExecutable.Value); + + if (!File.Exists(executablePath)) + { + Log.LogFatal($"Unable to locate executable: {ConfigEntrypointExecutable.Value}"); + Program.ReadExit(); + return; + } + + Paths.SetExecutablePath(executablePath); + Program.ResolveDirectories.Add(Paths.GameRootPath); + TypeLoader.SearchDirectories.Add(Paths.GameRootPath); + + bool bridgeInitialized = Utility.TryDo(() => + { + if (ConfigShimHarmony.Value) + HarmonyDetourBridge.Init(); + }, out var harmonyBridgeException); + + Logger.Sources.Add(TraceLogSource.CreateSource()); + + HarmonyFixes.Apply(); + + string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}"; + Log.LogMessage(consoleTile); + + if (ConsoleManager.ConsoleActive) + ConsoleManager.SetConsoleTitle(consoleTile); + + //See BuildInfoAttribute for more information about this section. + object[] attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false); + + if (attributes.Length > 0) + { + var attribute = (BuildInfoAttribute)attributes[0]; + Log.LogMessage(attribute.Info); + } + + Log.LogInfo($"CLR runtime version: {Environment.Version}"); + + if (harmonyBridgeException != null) + Log.LogWarning($"Failed to enable fix for Harmony for .NET Standard API. Error message: {harmonyBridgeException.Message}"); + + Log.LogMessage("Preloader started"); + + + using (var assemblyPatcher = new AssemblyPatcher()) + { + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + + Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded"); + + assemblyPatcher.LoadAssemblyDirectory(Paths.GameRootPath, "dll", "exe"); + + Log.LogInfo($"{assemblyPatcher.AssembliesToPatch.Count} assemblies discovered"); + + assemblyPatcher.PatchAndLoad(); + } + + Log.LogMessage("Preloader finished"); + + var chainloader = new NetChainloader(); + chainloader.Initialize(); + chainloader.Execute(); + + var assemblyName = AssemblyName.GetAssemblyName(executablePath); + var entrypointAssembly = Assembly.Load(assemblyName); + + entrypointAssembly.EntryPoint.Invoke(null, new [] { args }); + } + + #region Config + + private static readonly ConfigEntry ConfigEntrypointExecutable = ConfigFile.CoreConfig.Bind( + "Preloader.Entrypoint", "Assembly", + null, + "The local filename of the .NET executable to target."); + + private static readonly ConfigEntry ConfigShimHarmony = ConfigFile.CoreConfig.Bind( + "Preloader", "ShimHarmonySupport", + !Utility.CLRSupportsDynamicAssemblies, + "If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game."); + + private static readonly ConfigEntry ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Bind( + "Logging", "PreloaderConsoleOutRedirection", + true, + "Redirects text from Console.Out during preloader patch loading to the BepInEx logging system."); + + #endregion + } +} diff --git a/BepInEx.NetLauncher/Program.cs b/BepInEx.NetLauncher/Program.cs new file mode 100644 index 0000000..34cc58d --- /dev/null +++ b/BepInEx.NetLauncher/Program.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using BepInEx.Logging; +using BepInEx.Preloader.Core; + +namespace BepInEx.NetLauncher +{ + internal static class NetPreloaderRunner + { + internal static void PreloaderMain(string[] args) + { + Logger.Listeners.Add(new ConsoleLogListener()); + + ConsoleManager.ForceSetActive(true); + + NetPreloader.Start(args); + } + + internal static void OuterMain(string[] args, string filename) + { + try + { + Paths.SetExecutablePath(filename); + + AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; + + PreloaderMain(args); + } + catch (Exception ex) + { + PreloaderLogger.Log.LogFatal("Unhandled exception"); + PreloaderLogger.Log.LogFatal(ex); + Program.ReadExit(); + } + } + + private static Assembly LocalResolve(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + var foundAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(x => x.GetName().Name == assemblyName.Name); + + if (foundAssembly != null) + return foundAssembly; + + if (LocalUtility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) + || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) + || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) + return foundAssembly; + + return null; + } + } + + class Program + { + internal static void ReadExit() + { + Console.WriteLine("Press enter to exit..."); + Console.ReadLine(); + Environment.Exit(-1); + } + + static void Main(string[] args) + { + try + { + Console.WriteLine("test"); + + string filename; + +#if DEBUG + filename = Path.Combine(Directory.GetCurrentDirectory(), Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName)); + +#else + filename = Process.GetCurrentProcess().MainModule.FileName; +#endif + + ResolveDirectories.Add(Path.Combine(Path.GetDirectoryName(filename), "BepInEx", "Core")); + + AppDomain.CurrentDomain.AssemblyResolve += RemoteResolve; + + NetPreloaderRunner.OuterMain(args, filename); + } + catch (Exception ex) + { + Console.WriteLine("Unhandled exception"); + Console.WriteLine(ex); + ReadExit(); + } + } + + public static List ResolveDirectories { get; set; } = new List + { + "C:\\Windows\\Microsoft.NET\\assembly\\GAC_32\\Microsoft.Xna.Framework.Game\\v4.0_4.0.0.0__842cf8be1de50553\\" + }; + + private static Assembly RemoteResolve(object sender, ResolveEventArgs reference) + { + var assemblyName = new AssemblyName(reference.Name); + + foreach (var directory in ResolveDirectories) + { + var potentialDirectories = new List { directory }; + + potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); + + var potentialFiles = potentialDirectories.Select(x => Path.Combine(x, $"{assemblyName.Name}.dll")) + .Concat(potentialDirectories.Select(x => Path.Combine(x, $"{assemblyName.Name}.exe"))); + + foreach (string path in potentialFiles) + { + if (!File.Exists(path)) + continue; + + var assembly = Assembly.LoadFrom(path); + + if (assembly.GetName().Name == assemblyName.Name) + return assembly; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/BepInEx.NetLauncher/Properties/AssemblyInfo.cs b/BepInEx.NetLauncher/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..775e854 --- /dev/null +++ b/BepInEx.NetLauncher/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("BepInEx.NetLauncher")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("BepInEx.NetLauncher")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("490b052b-7f23-4052-ad04-613856618901")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/BepInEx.NetLauncher/Utility.cs b/BepInEx.NetLauncher/Utility.cs new file mode 100644 index 0000000..f0fc177 --- /dev/null +++ b/BepInEx.NetLauncher/Utility.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace BepInEx.NetLauncher +{ + /// + /// Generic helper properties and methods. + /// + public static class LocalUtility + { + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + private static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, Func loader, out T assembly) where T : class + { + assembly = null; + + var potentialDirectories = new List { directory }; + + potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); + + foreach (string subDirectory in potentialDirectories) + { + string path = Path.Combine(subDirectory, $"{assemblyName.Name}.dll"); + + if (!File.Exists(path)) + continue; + + try + { + assembly = loader(path); + } + catch (Exception) + { + continue; + } + + return true; + } + + return false; + } + + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) + { + return TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly); + } + } +} \ No newline at end of file diff --git a/BepInEx.NetLauncher/packages.config b/BepInEx.NetLauncher/packages.config new file mode 100644 index 0000000..921b72e --- /dev/null +++ b/BepInEx.NetLauncher/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 09a8d68..7cf30d9 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -43,7 +43,7 @@ namespace BepInEx.Preloader.Core /// public string DumpedAssembliesPath { get; set; } = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies"); - public ManualLogSource Logger { get; } = new ManualLogSource("AssemblyPatcher"); + public ManualLogSource Logger { get; } = BepInEx.Logging.Logger.CreateLogSource("AssemblyPatcher"); private static T CreateDelegate(MethodInfo method) where T : class => method != null ? Delegate.CreateDelegate(typeof(T), method) as T : null; @@ -173,7 +173,7 @@ namespace BepInEx.Preloader.Core public void LoadAssemblyDirectory(string directory, params string[] assemblyExtensions) { var filesToSearch = assemblyExtensions - .SelectMany(ext => Directory.GetFiles(directory, "*." + ext)); + .SelectMany(ext => Directory.GetFiles(directory, "*." + ext, SearchOption.TopDirectoryOnly)); foreach (string assemblyPath in filesToSearch) { @@ -192,6 +192,8 @@ namespace BepInEx.Preloader.Core AssembliesToPatch.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..."); @@ -343,6 +345,8 @@ namespace BepInEx.Preloader.Core Assembly.Load(assemblyStream.ToArray()); } } + + Logger.LogDebug($"Loaded '{assembly.FullName}' into memory"); } // Though we have to dispose of all assemblies regardless of them being patched or not diff --git a/BepInEx.sln b/BepInEx.sln index 474b7bd..387adda 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -34,47 +34,88 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.Core", "BepInEx.Cor EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unity", "Unity", "{6E2DD21E-0854-4F4A-B925-D90E0016D03D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.NetLauncher", "BepInEx.NetLauncher\BepInEx.NetLauncher.csproj", "{490B052B-7F23-4052-AD04-613856618901}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NetFramework", "NetFramework", "{6EC98884-A3DC-4828-85EF-5F10F7519429}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Release_NetFramework|Any CPU = Release_NetFramework|Any CPU + Release_Unity|Any CPU = Release_Unity|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = Release|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = Release|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.Build.0 = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release_NetFramework|Any CPU.Build.0 = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release_NetFramework|Any CPU.Build.0 = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|x86 {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release_NetFramework|Any CPU.ActiveCfg = Release|x86 + {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release_Unity|Any CPU.ActiveCfg = Release|x86 {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|x86 {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Release_NetFramework|Any CPU.Build.0 = Release|Any CPU + {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {15F8BC38-A761-4F93-8903-1B531AC5D9F9}.Release|Any CPU.Build.0 = Release|Any CPU {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Release|Any CPU.ActiveCfg = Release|Any CPU {EAE9FAE6-8011-45A3-8B6E-0C7F14210533}.Release|Any CPU.Build.0 = Release|Any CPU {D404C973-441D-48ED-B266-C21320BA0D87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D404C973-441D-48ED-B266-C21320BA0D87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D404C973-441D-48ED-B266-C21320BA0D87}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {D404C973-441D-48ED-B266-C21320BA0D87}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {D404C973-441D-48ED-B266-C21320BA0D87}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {D404C973-441D-48ED-B266-C21320BA0D87}.Release|Any CPU.ActiveCfg = Release|Any CPU {D404C973-441D-48ED-B266-C21320BA0D87}.Release|Any CPU.Build.0 = Release|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release_NetFramework|Any CPU.Build.0 = Release|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = Release|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Release_NetFramework|Any CPU.ActiveCfg = Release|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Release_NetFramework|Any CPU.Build.0 = Release|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Release_Unity|Any CPU.ActiveCfg = Release|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {490B052B-7F23-4052-AD04-613856618901}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -86,6 +127,7 @@ Global {A15D6EE6-F954-415B-8605-8A8470CC87DC} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB} {EAE9FAE6-8011-45A3-8B6E-0C7F14210533} = {6E2DD21E-0854-4F4A-B925-D90E0016D03D} {D404C973-441D-48ED-B266-C21320BA0D87} = {6E2DD21E-0854-4F4A-B925-D90E0016D03D} + {490B052B-7F23-4052-AD04-613856618901} = {6EC98884-A3DC-4828-85EF-5F10F7519429} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55AC11EF-F568-4C79-A356-7ED9510145B1}