@ -0,0 +1,31 @@ | |||||
using IllusionInjector.Logging; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using UnityEngine; | |||||
namespace IPA.Injector | |||||
{ | |||||
class Bootstrapper : MonoBehaviour | |||||
{ | |||||
public event Action Destroyed = delegate {}; | |||||
void Awake() | |||||
{ | |||||
//if (Environment.CommandLine.Contains("--verbose")) | |||||
//{ | |||||
Windows.GuiConsole.CreateConsole(); | |||||
//} | |||||
} | |||||
void Start() | |||||
{ | |||||
Destroy(gameObject); | |||||
} | |||||
void OnDestroy() | |||||
{ | |||||
Destroyed(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,72 @@ | |||||
using System; | |||||
using System.Collections; | |||||
using System.Runtime.InteropServices; | |||||
using System.IO; | |||||
using System.Text; | |||||
using Microsoft.Win32.SafeHandles; | |||||
namespace Windows | |||||
{ | |||||
class GuiConsole | |||||
{ | |||||
public static void CreateConsole() | |||||
{ | |||||
if (hasConsole) | |||||
return; | |||||
if (oldOut == IntPtr.Zero) | |||||
oldOut = GetStdHandle( -11 ); | |||||
if (! AllocConsole()) | |||||
throw new Exception("AllocConsole() failed"); | |||||
conOut = CreateFile( "CONOUT$", 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero ); | |||||
if (! SetStdHandle(-11, conOut)) | |||||
throw new Exception("SetStdHandle() failed"); | |||||
StreamToConsole(); | |||||
hasConsole = true; | |||||
} | |||||
public static void ReleaseConsole() | |||||
{ | |||||
if (! hasConsole) | |||||
return; | |||||
if (! CloseHandle(conOut)) | |||||
throw new Exception("CloseHandle() failed"); | |||||
conOut = IntPtr.Zero; | |||||
if (! FreeConsole()) | |||||
throw new Exception("FreeConsole() failed"); | |||||
if (! SetStdHandle(-11, oldOut)) | |||||
throw new Exception("SetStdHandle() failed"); | |||||
StreamToConsole(); | |||||
hasConsole = false; | |||||
} | |||||
private static void StreamToConsole() | |||||
{ | |||||
Stream cstm = Console.OpenStandardOutput(); | |||||
StreamWriter cstw = new StreamWriter( cstm, Encoding.Default ); | |||||
cstw.AutoFlush = true; | |||||
Console.SetOut( cstw ); | |||||
Console.SetError( cstw ); | |||||
} | |||||
private static bool hasConsole = false; | |||||
private static IntPtr conOut; | |||||
private static IntPtr oldOut; | |||||
[DllImport("kernel32.dll", SetLastError=true)] | |||||
private static extern bool AllocConsole(); | |||||
[DllImport("kernel32.dll", SetLastError=false)] | |||||
private static extern bool FreeConsole(); | |||||
[DllImport("kernel32.dll", SetLastError=true)] | |||||
private static extern IntPtr GetStdHandle( int nStdHandle ); | |||||
[DllImport("kernel32.dll", SetLastError=true)] | |||||
private static extern bool SetStdHandle(int nStdHandle, IntPtr hConsoleOutput); | |||||
[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)] | |||||
private static extern IntPtr CreateFile( | |||||
string fileName, | |||||
int desiredAccess, | |||||
int shareMode, | |||||
IntPtr securityAttributes, | |||||
int creationDisposition, | |||||
int flagsAndAttributes, | |||||
IntPtr templateFile ); | |||||
[DllImport("kernel32.dll", ExactSpelling=true, SetLastError=true)] | |||||
private static extern bool CloseHandle(IntPtr handle); | |||||
} | |||||
} |
@ -0,0 +1,91 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
<PropertyGroup> | |||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
<ProjectGuid>{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}</ProjectGuid> | |||||
<OutputType>Library</OutputType> | |||||
<AppDesignerFolder>Properties</AppDesignerFolder> | |||||
<RootNamespace>IPA.Injector</RootNamespace> | |||||
<AssemblyName>IPA.Injector</AssemblyName> | |||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion> | |||||
<FileAlignment>512</FileAlignment> | |||||
<Deterministic>true</Deterministic> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
<DebugSymbols>true</DebugSymbols> | |||||
<DebugType>full</DebugType> | |||||
<Optimize>false</Optimize> | |||||
<OutputPath>bin\Debug\</OutputPath> | |||||
<DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
<DebugType>pdbonly</DebugType> | |||||
<Optimize>true</Optimize> | |||||
<OutputPath>bin\Release\</OutputPath> | |||||
<DefineConstants>TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<Reference Include="Ionic.Zip, Version=1.9.1.8, Culture=neutral, PublicKeyToken=edbe51ad942a3f5c, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Ionic.Zip.1.9.1.8\lib\Ionic.Zip.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="System" /> | |||||
<Reference Include="System.Core" /> | |||||
<Reference Include="System.Xml.Linq" /> | |||||
<Reference Include="System.Data.DataSetExtensions" /> | |||||
<Reference Include="Microsoft.CSharp" /> | |||||
<Reference Include="System.Data" /> | |||||
<Reference Include="System.Net.Http" /> | |||||
<Reference Include="System.Xml" /> | |||||
<Reference Include="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL"> | |||||
<SpecificVersion>False</SpecificVersion> | |||||
<HintPath>..\..\..\..\..\..\Game Library\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll</HintPath> | |||||
<Private>False</Private> | |||||
</Reference> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Compile Include="Bootstrapper.cs" /> | |||||
<Compile Include="ConsoleWindow.cs" /> | |||||
<Compile Include="Injector.cs" /> | |||||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\IPA.Loader\IPA.Loader.csproj"> | |||||
<Project>{5ad344f0-01a0-4ca8-92e5-9d095737744d}</Project> | |||||
<Name>IPA.Loader</Name> | |||||
</ProjectReference> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<None Include="packages.config" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Content Include="..\Libs\0Harmony.dll"> | |||||
<Link>Libraries\Included\0Harmony.dll</Link> | |||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> | |||||
</Content> | |||||
<Content Include="..\Libs\I18N.dll"> | |||||
<Link>Libraries\Mono\I18N.dll</Link> | |||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> | |||||
</Content> | |||||
<Content Include="..\Libs\I18N.West.dll"> | |||||
<Link>Libraries\Mono\I18N.West.dll</Link> | |||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> | |||||
</Content> | |||||
<Content Include="..\Libs\System.Runtime.Serialization.dll"> | |||||
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link> | |||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory> | |||||
</Content> | |||||
</ItemGroup> | |||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
<Target Name="AfterBuild"> | |||||
<Exec Command=""$(MSBuildBinPath)\MSBuild.exe" "$(MSBuildProjectDirectory)\PostBuild.msbuild" /property:OPath=$(OutputPath) /property:Configuration=$(Configuration) /property:SolDir=$(SolutionDir)" /> | |||||
</Target> | |||||
</Project> |
@ -0,0 +1,63 @@ | |||||
using IllusionInjector; | |||||
using IllusionInjector.Logging; | |||||
using System; | |||||
using System.IO; | |||||
using System.Reflection; | |||||
using UnityEngine; | |||||
using static IllusionPlugin.Logging.Logger; | |||||
using Logger = IllusionInjector.Logging.Logger; | |||||
namespace IPA.Injector | |||||
{ | |||||
public static class Injector | |||||
{ | |||||
private static bool injected = false; | |||||
public static void Inject() | |||||
{ | |||||
if (!injected) | |||||
{ | |||||
injected = true; | |||||
AppDomain.CurrentDomain.AssemblyResolve += AssemblyLibLoader; | |||||
var bootstrapper = new GameObject("Bootstrapper").AddComponent<Bootstrapper>(); | |||||
bootstrapper.Destroyed += Bootstrapper_Destroyed; | |||||
} | |||||
} | |||||
private static string libsDir; | |||||
private static Assembly AssemblyLibLoader(object source, ResolveEventArgs e) | |||||
{ | |||||
if (libsDir == null) | |||||
libsDir = Path.Combine(Environment.CurrentDirectory, "Libs"); | |||||
var asmName = new AssemblyName(e.Name); | |||||
Log(Level.Debug, $"Resolving library {asmName}"); | |||||
var testFilen = Path.Combine(libsDir, $"{asmName.Name}.{asmName.Version}.dll"); | |||||
Log(Level.Debug, $"Looking for file {testFilen}"); | |||||
if (File.Exists(testFilen)) | |||||
return Assembly.LoadFile(testFilen); | |||||
Log(Level.Critical, $"Could not load library {asmName}"); | |||||
return null; | |||||
} | |||||
private static void Log(Level lvl, string message) | |||||
{ // multiple proxy methods to delay loading of assemblies until it's done | |||||
if (Logger.LogCreated) | |||||
AssemblyLibLoaderCallLogger(lvl, message); | |||||
else | |||||
if (((byte)lvl & (byte)StandardLogger.PrintFilter) != 0) | |||||
Console.WriteLine($"[{lvl}] {message}"); | |||||
} | |||||
private static void AssemblyLibLoaderCallLogger(Level lvl, string message) | |||||
{ | |||||
Logger.log.Log(lvl, message); | |||||
} | |||||
private static void Bootstrapper_Destroyed() | |||||
{ | |||||
PluginComponent.Create(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,35 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<Project DefaultTargets="PostBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0"> | |||||
<PropertyGroup> | |||||
<OPath></OPath> | |||||
<SolDir></SolDir> | |||||
</PropertyGroup> | |||||
<UsingTask TaskName="AssemblyRename" AssemblyFile="$(SolDir)MSBuildTasks\bin\$(Configuration)\MSBuildTasks.dll" /> | |||||
<Target Name="PostBuild"> | |||||
<Message Text="Relocating" Importance="normal" /> | |||||
<ItemGroup> | |||||
<SystemFiles Include="$(OPath)IPA.Injector.*" /> | |||||
<SystemFiles Include="$(OPath)IPA.Loader.*" /> | |||||
<SystemFiles Include="$(OPath)Libraries\Mono\**\*" /> | |||||
<OldLibFiles Include="$(OPath)Libs\**\*" /> | |||||
</ItemGroup> | |||||
<Move SourceFiles="@(SystemFiles)" DestinationFolder="$(OPath)Data\Managed" /> | |||||
<RemoveDir Directories="$(OPath)Libraries\Mono" /> | |||||
<Delete Files="@(OldLibFiles)" /> | |||||
<RemoveDir Directories="$(OPath)Libs" /> | |||||
<ItemGroup> | |||||
<LibFiles Include="$(OPath)**\*" Exclude="$(OPath)Data\**\*;$(OPath)Libs\**\*" /> | |||||
</ItemGroup> | |||||
<Move SourceFiles="@(LibFiles)" DestinationFolder="$(OPath)Libs\" /> | |||||
<RemoveDir Directories="$(OPath)Libraries\Included" /> | |||||
<RemoveDir Directories="$(OPath)Libraries" /> | |||||
<ItemGroup> | |||||
<ToRename Include="$(OPath)Libs\**\*.dll" /> | |||||
</ItemGroup> | |||||
<AssemblyRename Assemblies="@(ToRename)" /> | |||||
</Target> | |||||
</Project> |
@ -0,0 +1,37 @@ | |||||
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("IPA.Injector")] | |||||
[assembly: AssemblyDescription("")] | |||||
[assembly: AssemblyConfiguration("")] | |||||
[assembly: AssemblyCompany("")] | |||||
[assembly: AssemblyProduct("IPA.Injector")] | |||||
[assembly: AssemblyCopyright("Copyright © 2018")] | |||||
[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("2a1af16b-27f1-46e0-9a95-181516bc1cb7")] | |||||
[assembly: InternalsVisibleTo("IPA.Loader")] | |||||
// 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("3.9.0")] | |||||
[assembly: AssemblyFileVersion("3.9.0")] |
@ -0,0 +1,5 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<packages> | |||||
<package id="Ionic.Zip" version="1.9.1.8" targetFramework="net46" /> | |||||
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net46" /> | |||||
</packages> |
@ -0,0 +1,97 @@ | |||||
using IllusionPlugin; | |||||
using IllusionPlugin.BeatSaber; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using UnityEngine; | |||||
using UnityEngine.SceneManagement; | |||||
using Logger = IllusionInjector.Logging.Logger; | |||||
namespace IllusionInjector { | |||||
public class CompositeBSPlugin : IBeatSaberPlugin | |||||
{ | |||||
IEnumerable<IBeatSaberPlugin> plugins; | |||||
private delegate void CompositeCall(IBeatSaberPlugin plugin); | |||||
public CompositeBSPlugin(IEnumerable<IBeatSaberPlugin> plugins) { | |||||
this.plugins = plugins; | |||||
} | |||||
public void OnApplicationStart() { | |||||
Invoke(plugin => plugin.OnApplicationStart()); | |||||
} | |||||
public void OnApplicationQuit() { | |||||
Invoke(plugin => plugin.OnApplicationQuit()); | |||||
} | |||||
public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) { | |||||
foreach (var plugin in plugins) { | |||||
try { | |||||
plugin.OnSceneLoaded(scene, sceneMode); | |||||
} | |||||
catch (Exception ex) { | |||||
Logger.log.Error($"{plugin.Name}: {ex}"); | |||||
} | |||||
} | |||||
} | |||||
public void OnSceneUnloaded(Scene scene) { | |||||
foreach (var plugin in plugins) { | |||||
try { | |||||
plugin.OnSceneUnloaded(scene); | |||||
} | |||||
catch (Exception ex) { | |||||
Logger.log.Error($"{plugin.Name}: {ex}"); | |||||
} | |||||
} | |||||
} | |||||
public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) { | |||||
foreach (var plugin in plugins) { | |||||
try { | |||||
plugin.OnActiveSceneChanged(prevScene, nextScene); | |||||
} | |||||
catch (Exception ex) { | |||||
Logger.log.Error($"{plugin.Name}: {ex}"); | |||||
} | |||||
} | |||||
} | |||||
private void Invoke(CompositeCall callback) { | |||||
foreach (var plugin in plugins) { | |||||
try { | |||||
callback(plugin); | |||||
} | |||||
catch (Exception ex) { | |||||
Logger.log.Error($"{plugin.Name}: {ex}"); | |||||
} | |||||
} | |||||
} | |||||
public void OnUpdate() { | |||||
Invoke(plugin => plugin.OnUpdate()); | |||||
} | |||||
public void OnFixedUpdate() { | |||||
Invoke(plugin => plugin.OnFixedUpdate()); | |||||
} | |||||
public string Name => throw new NotImplementedException(); | |||||
public string Version => throw new NotImplementedException(); | |||||
public Uri UpdateUri => throw new NotImplementedException(); | |||||
public ModsaberModInfo ModInfo => throw new NotImplementedException(); | |||||
public void OnLateUpdate() { | |||||
Invoke(plugin => { | |||||
if (plugin is IEnhancedBeatSaberPlugin) | |||||
((IEnhancedBeatSaberPlugin) plugin).OnLateUpdate(); | |||||
}); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,94 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
<PropertyGroup> | |||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
<ProjectGuid>{5AD344F0-01A0-4CA8-92E5-9D095737744D}</ProjectGuid> | |||||
<OutputType>Library</OutputType> | |||||
<AppDesignerFolder>Properties</AppDesignerFolder> | |||||
<RootNamespace>IPA.Loader</RootNamespace> | |||||
<AssemblyName>IPA.Loader</AssemblyName> | |||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion> | |||||
<FileAlignment>512</FileAlignment> | |||||
<Deterministic>true</Deterministic> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
<DebugSymbols>true</DebugSymbols> | |||||
<DebugType>full</DebugType> | |||||
<Optimize>false</Optimize> | |||||
<OutputPath>bin\Debug\</OutputPath> | |||||
<DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
<DebugType>pdbonly</DebugType> | |||||
<Optimize>true</Optimize> | |||||
<OutputPath>bin\Release\</OutputPath> | |||||
<DefineConstants>TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<Reference Include="Ionic.Zip, Version=1.9.1.8, Culture=neutral, PublicKeyToken=edbe51ad942a3f5c, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Ionic.Zip.1.9.1.8\lib\Ionic.Zip.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="System" /> | |||||
<Reference Include="System.Core" /> | |||||
<Reference Include="System.Xml.Linq" /> | |||||
<Reference Include="System.Data.DataSetExtensions" /> | |||||
<Reference Include="Microsoft.CSharp" /> | |||||
<Reference Include="System.Data" /> | |||||
<Reference Include="System.Net.Http" /> | |||||
<Reference Include="System.Xml" /> | |||||
<Reference Include="UnityEngine.CoreModule"> | |||||
<HintPath>..\..\..\..\..\..\Game Library\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll</HintPath> | |||||
<Private>False</Private> | |||||
</Reference> | |||||
<Reference Include="UnityEngine.UnityWebRequestModule"> | |||||
<HintPath>..\..\..\..\..\..\Game Library\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestModule.dll</HintPath> | |||||
<Private>False</Private> | |||||
</Reference> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Compile Include="BeatSaber\CompositeBSPlugin.cs" /> | |||||
<Compile Include="IllusionPlugin\BeatSaber\IBeatSaberPlugin.cs" /> | |||||
<Compile Include="IllusionPlugin\BeatSaber\IEnhancedBeatSaberPlugin.cs" /> | |||||
<Compile Include="IllusionPlugin\BeatSaber\ModsaberModInfo.cs" /> | |||||
<Compile Include="IllusionPlugin\IGenericEnhancedPlugin.cs" /> | |||||
<Compile Include="IllusionPlugin\IniFile.cs" /> | |||||
<Compile Include="IllusionPlugin\IPA\IEnhancedPlugin.cs" /> | |||||
<Compile Include="IllusionPlugin\IPA\IPlugin.cs" /> | |||||
<Compile Include="IllusionPlugin\Logging\Logger.cs" /> | |||||
<Compile Include="IllusionPlugin\Logging\LogPrinter.cs" /> | |||||
<Compile Include="IllusionPlugin\ModPrefs.cs" /> | |||||
<Compile Include="IllusionPlugin\Utils\ReflectionUtil.cs" /> | |||||
<Compile Include="IPA\CompositeIPAPlugin.cs" /> | |||||
<Compile Include="Logging\Printers\ColoredConsolePrinter.cs" /> | |||||
<Compile Include="Logging\Printers\GlobalLogFilePrinter.cs" /> | |||||
<Compile Include="Logging\Printers\GZFilePrinter.cs" /> | |||||
<Compile Include="Logging\Printers\PluginLogFilePrinter.cs" /> | |||||
<Compile Include="Logging\StandardLogger.cs" /> | |||||
<Compile Include="Logging\UnityLogInterceptor.cs" /> | |||||
<Compile Include="PluginComponent.cs" /> | |||||
<Compile Include="PluginManager.cs" /> | |||||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||||
<Compile Include="Updating\Backup\BackupUnit.cs" /> | |||||
<Compile Include="Updating\ModsaberML\ApiEndpoint.cs" /> | |||||
<Compile Include="Updating\ModsaberML\Updater.cs" /> | |||||
<Compile Include="Updating\SelfPlugin.cs" /> | |||||
<Compile Include="Utilities\Extensions.cs" /> | |||||
<Compile Include="Utilities\LoneFunctions.cs" /> | |||||
<Compile Include="Utilities\SteamCheck.cs" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<None Include="packages.config" /> | |||||
</ItemGroup> | |||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
</Project> |
@ -0,0 +1,75 @@ | |||||
using IllusionPlugin; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using UnityEngine; | |||||
using UnityEngine.SceneManagement; | |||||
using Logger = IllusionInjector.Logging.Logger; | |||||
namespace IllusionInjector { | |||||
#pragma warning disable CS0618 // Type or member is obsolete | |||||
public class CompositeIPAPlugin : IPlugin | |||||
{ | |||||
IEnumerable<IPlugin> plugins; | |||||
private delegate void CompositeCall(IPlugin plugin); | |||||
public CompositeIPAPlugin(IEnumerable<IPlugin> plugins) { | |||||
this.plugins = plugins; | |||||
} | |||||
public void OnApplicationStart() { | |||||
Invoke(plugin => plugin.OnApplicationStart()); | |||||
} | |||||
public void OnApplicationQuit() { | |||||
Invoke(plugin => plugin.OnApplicationQuit()); | |||||
} | |||||
private void Invoke(CompositeCall callback) { | |||||
foreach (var plugin in plugins) { | |||||
try { | |||||
callback(plugin); | |||||
} | |||||
catch (Exception ex) { | |||||
Logger.log.Error($"{plugin.Name}: {ex}"); | |||||
} | |||||
} | |||||
} | |||||
public void OnUpdate() { | |||||
Invoke(plugin => plugin.OnUpdate()); | |||||
} | |||||
public void OnFixedUpdate() { | |||||
Invoke(plugin => plugin.OnFixedUpdate()); | |||||
} | |||||
public string Name { | |||||
get { throw new NotImplementedException(); } | |||||
} | |||||
public string Version { | |||||
get { throw new NotImplementedException(); } | |||||
} | |||||
public void OnLateUpdate() { | |||||
Invoke(plugin => { | |||||
if (plugin is IEnhancedBeatSaberPlugin) | |||||
((IEnhancedBeatSaberPlugin) plugin).OnLateUpdate(); | |||||
}); | |||||
} | |||||
public void OnLevelWasLoaded(int level) | |||||
{ | |||||
Invoke(plugin => plugin.OnLevelWasLoaded(level)); | |||||
} | |||||
public void OnLevelWasInitialized(int level) | |||||
{ | |||||
Invoke(plugin => plugin.OnLevelWasInitialized(level)); | |||||
} | |||||
} | |||||
#pragma warning restore CS0618 // Type or member is obsolete | |||||
} |
@ -0,0 +1,71 @@ | |||||
using IllusionPlugin.BeatSaber; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using UnityEngine.SceneManagement; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Interface for Beat Saber plugins. Every class that implements this will be loaded if the DLL is placed at | |||||
/// data/Managed/Plugins. | |||||
/// </summary> | |||||
public interface IBeatSaberPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Gets the name of the plugin. | |||||
/// </summary> | |||||
string Name { get; } | |||||
/// <summary> | |||||
/// Gets the version of the plugin. | |||||
/// </summary> | |||||
string Version { get; } | |||||
/// <summary> | |||||
/// Gets the info for the Modsaber release of this plugin. Return null if there is no Modsaber release. | |||||
/// </summary> | |||||
ModsaberModInfo ModInfo { get; } | |||||
/// <summary> | |||||
/// Gets invoked when the application is started. | |||||
/// </summary> | |||||
void OnApplicationStart(); | |||||
/// <summary> | |||||
/// Gets invoked when the application is closed. | |||||
/// </summary> | |||||
void OnApplicationQuit(); | |||||
/// <summary> | |||||
/// Gets invoked on every graphic update. | |||||
/// </summary> | |||||
void OnUpdate(); | |||||
/// <summary> | |||||
/// Gets invoked on ever physics update. | |||||
/// </summary> | |||||
void OnFixedUpdate(); | |||||
/// <summary> | |||||
/// Gets invoked whenever a scene is loaded. | |||||
/// </summary> | |||||
/// <param name="scene">The scene currently loaded</param> | |||||
/// <param name="sceneMode">The type of loading</param> | |||||
void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode); | |||||
/// <summary> | |||||
/// Gets invoked whenever a scene is unloaded | |||||
/// </summary> | |||||
/// <param name="scene">The unloaded scene</param> | |||||
void OnSceneUnloaded(Scene scene); | |||||
/// <summary> | |||||
/// Gets invoked whenever a scene is changed | |||||
/// </summary> | |||||
/// <param name="prevScene">The Scene that was previously loaded</param> | |||||
/// <param name="nextScene">The Scene being loaded</param> | |||||
void OnActiveSceneChanged(Scene prevScene, Scene nextScene); | |||||
} | |||||
} |
@ -0,0 +1,13 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// An enhanced version of a standard BeatSaber plugin. | |||||
/// </summary> | |||||
public interface IEnhancedBeatSaberPlugin : IBeatSaberPlugin, IGenericEnhancedPlugin | |||||
{ | |||||
} | |||||
} |
@ -0,0 +1,24 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionPlugin.BeatSaber | |||||
{ | |||||
/// <summary> | |||||
/// A class to provide information about a mod on ModSaber.ML | |||||
/// </summary> | |||||
public class ModsaberModInfo | |||||
{ | |||||
/// <summary> | |||||
/// The name the mod uses on ModSaber as an identifier. | |||||
/// </summary> | |||||
public string InternalName { get; set; } | |||||
/// <summary> | |||||
/// The version of the currently installed mod. Used to compare to the version on ModSaber. | |||||
/// </summary> | |||||
public Version CurrentVersion { get; set; } | |||||
} | |||||
} |
@ -0,0 +1,25 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// A generic interface for the modification for enhanced plugins. | |||||
/// </summary> | |||||
public interface IGenericEnhancedPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Gets a list of executables this plugin should be excuted on (without the file ending) | |||||
/// </summary> | |||||
/// <example>{ "PlayClub", "PlayClubStudio" }</example> | |||||
string[] Filter { get; } | |||||
/// <summary> | |||||
/// Called after Update. | |||||
/// </summary> | |||||
void OnLateUpdate(); | |||||
} | |||||
} |
@ -0,0 +1,14 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// An enhanced version of the standard IPA plugin. | |||||
/// </summary> | |||||
[Obsolete("When building plugins for Beat Saber, use IEnhancedBeatSaberPlugin")] | |||||
public interface IEnhancedPlugin : IPlugin, IGenericEnhancedPlugin | |||||
{ | |||||
} | |||||
} |
@ -0,0 +1,58 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Interface for generic Illusion unity plugins. Every class that implements this will be loaded if the DLL is placed at | |||||
/// data/Managed/Plugins. | |||||
/// </summary> | |||||
[Obsolete("When building plugins for Beat Saber, use IBeatSaberPlugin")] | |||||
public interface IPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Gets the name of the plugin. | |||||
/// </summary> | |||||
string Name { get; } | |||||
/// <summary> | |||||
/// Gets the version of the plugin. | |||||
/// </summary> | |||||
string Version { get; } | |||||
/// <summary> | |||||
/// Gets invoked when the application is started. | |||||
/// </summary> | |||||
void OnApplicationStart(); | |||||
/// <summary> | |||||
/// Gets invoked when the application is closed. | |||||
/// </summary> | |||||
void OnApplicationQuit(); | |||||
/// <summary> | |||||
/// Gets invoked whenever a level is loaded. | |||||
/// </summary> | |||||
/// <param name="level"></param> | |||||
void OnLevelWasLoaded(int level); | |||||
/// <summary> | |||||
/// Gets invoked after the first update cycle after a level was loaded. | |||||
/// </summary> | |||||
/// <param name="level"></param> | |||||
void OnLevelWasInitialized(int level); | |||||
/// <summary> | |||||
/// Gets invoked on every graphic update. | |||||
/// </summary> | |||||
void OnUpdate(); | |||||
/// <summary> | |||||
/// Gets invoked on ever physics update. | |||||
/// </summary> | |||||
void OnFixedUpdate(); | |||||
} | |||||
} |
@ -0,0 +1,100 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Runtime.InteropServices; | |||||
using System.Text; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Create a New INI file to store or load data | |||||
/// </summary> | |||||
internal class IniFile | |||||
{ | |||||
[DllImport("KERNEL32.DLL", EntryPoint = "GetPrivateProfileStringW", | |||||
SetLastError = true, | |||||
CharSet = CharSet.Unicode, ExactSpelling = true, | |||||
CallingConvention = CallingConvention.StdCall)] | |||||
private static extern int GetPrivateProfileString( | |||||
string lpSection, | |||||
string lpKey, | |||||
string lpDefault, | |||||
StringBuilder lpReturnString, | |||||
int nSize, | |||||
string lpFileName); | |||||
[DllImport("KERNEL32.DLL", EntryPoint = "WritePrivateProfileStringW", | |||||
SetLastError = true, | |||||
CharSet = CharSet.Unicode, ExactSpelling = true, | |||||
CallingConvention = CallingConvention.StdCall)] | |||||
private static extern int WritePrivateProfileString( | |||||
string lpSection, | |||||
string lpKey, | |||||
string lpValue, | |||||
string lpFileName); | |||||
/*private string _path = ""; | |||||
public string Path | |||||
{ | |||||
get | |||||
{ | |||||
return _path; | |||||
} | |||||
set | |||||
{ | |||||
if (!File.Exists(value)) | |||||
File.WriteAllText(value, "", Encoding.Unicode); | |||||
_path = value; | |||||
} | |||||
}*/ | |||||
private FileInfo _iniFileInfo; | |||||
public FileInfo IniFileInfo { | |||||
get => _iniFileInfo; | |||||
set { | |||||
_iniFileInfo = value; | |||||
if (_iniFileInfo.Exists) return; | |||||
_iniFileInfo.Directory?.Create(); | |||||
_iniFileInfo.Create(); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// INIFile Constructor. | |||||
/// </summary> | |||||
/// <PARAM name="iniPath"></PARAM> | |||||
public IniFile(string iniPath) | |||||
{ | |||||
IniFileInfo = new FileInfo(iniPath); | |||||
//this.Path = INIPath; | |||||
} | |||||
/// <summary> | |||||
/// Write Data to the INI File | |||||
/// </summary> | |||||
/// <PARAM name="Section"></PARAM> | |||||
/// Section name | |||||
/// <PARAM name="Key"></PARAM> | |||||
/// Key Name | |||||
/// <PARAM name="Value"></PARAM> | |||||
/// Value Name | |||||
public void IniWriteValue(string Section, string Key, string Value) | |||||
{ | |||||
WritePrivateProfileString(Section, Key, Value, IniFileInfo.FullName); | |||||
} | |||||
/// <summary> | |||||
/// Read Data Value From the Ini File | |||||
/// </summary> | |||||
/// <PARAM name="Section"></PARAM> | |||||
/// <PARAM name="Key"></PARAM> | |||||
/// <returns></returns> | |||||
public string IniReadValue(string Section, string Key) | |||||
{ | |||||
const int MAX_CHARS = 1023; | |||||
StringBuilder result = new StringBuilder(MAX_CHARS); | |||||
GetPrivateProfileString(Section, Key, "", result, MAX_CHARS, IniFileInfo.FullName); | |||||
return result.ToString(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,37 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionPlugin.Logging | |||||
{ | |||||
/// <summary> | |||||
/// The log printer's base class. | |||||
/// </summary> | |||||
public abstract class LogPrinter | |||||
{ | |||||
/// <summary> | |||||
/// Provides a filter for which log levels to allow through. | |||||
/// </summary> | |||||
public abstract Logger.LogLevel Filter { get; set; } | |||||
/// <summary> | |||||
/// Prints a provided message from a given log at the specified time. | |||||
/// </summary> | |||||
/// <param name="level">the log level</param> | |||||
/// <param name="time">the time the message was composed</param> | |||||
/// <param name="logName">the name of the log that created this message</param> | |||||
/// <param name="message">the message</param> | |||||
public abstract void Print(Logger.Level level, DateTime time, string logName, string message); | |||||
/// <summary> | |||||
/// Called before the first print in a group. May be called multiple times. | |||||
/// Use this to create file handles and the like. | |||||
/// </summary> | |||||
public virtual void StartPrint() { } | |||||
/// <summary> | |||||
/// Called after the last print in a group. May be called multiple times. | |||||
/// Use this to dispose file handles and the like. | |||||
/// </summary> | |||||
public virtual void EndPrint() { } | |||||
} | |||||
} |
@ -0,0 +1,182 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionPlugin.Logging | |||||
{ | |||||
/// <summary> | |||||
/// The logger base class. Provides the format for console logs. | |||||
/// </summary> | |||||
public abstract class Logger | |||||
{ | |||||
/// <summary> | |||||
/// The standard format for log messages. | |||||
/// </summary> | |||||
public static string LogFormat { get; protected internal set; } = "[{3} @ {2:HH:mm:ss} | {1}] {0}"; | |||||
/// <summary> | |||||
/// An enum specifying the level of the message. Resembles Syslog. | |||||
/// </summary> | |||||
public enum Level : byte | |||||
{ | |||||
/// <summary> | |||||
/// No associated level. These never get shown. | |||||
/// </summary> | |||||
None = 0, | |||||
/// <summary> | |||||
/// A debug message. | |||||
/// </summary> | |||||
Debug = 1, | |||||
/// <summary> | |||||
/// An informational message. | |||||
/// </summary> | |||||
Info = 2, | |||||
/// <summary> | |||||
/// A warning message. | |||||
/// </summary> | |||||
Warning = 4, | |||||
/// <summary> | |||||
/// An error message. | |||||
/// </summary> | |||||
Error = 8, | |||||
/// <summary> | |||||
/// A critical error message. | |||||
/// </summary> | |||||
Critical = 16 | |||||
} | |||||
/// <summary> | |||||
/// An enum providing log level filters. | |||||
/// </summary> | |||||
[Flags] | |||||
public enum LogLevel : byte | |||||
{ | |||||
/// <summary> | |||||
/// Allow no messages through. | |||||
/// </summary> | |||||
None = Level.None, | |||||
/// <summary> | |||||
/// Only shows Debug messages. | |||||
/// </summary> | |||||
DebugOnly = Level.Debug, | |||||
/// <summary> | |||||
/// Only shows info messages. | |||||
/// </summary> | |||||
InfoOnly = Level.Info, | |||||
/// <summary> | |||||
/// Only shows Warning messages. | |||||
/// </summary> | |||||
WarningOnly = Level.Warning, | |||||
/// <summary> | |||||
/// Only shows Error messages. | |||||
/// </summary> | |||||
ErrorOnly = Level.Error, | |||||
/// <summary> | |||||
/// Only shows Critical messages. | |||||
/// </summary> | |||||
CriticalOnly = Level.Critical, | |||||
/// <summary> | |||||
/// Shows all messages error and up. | |||||
/// </summary> | |||||
ErrorUp = ErrorOnly | CriticalOnly, | |||||
/// <summary> | |||||
/// Shows all messages warning and up. | |||||
/// </summary> | |||||
WarningUp = WarningOnly | ErrorUp, | |||||
/// <summary> | |||||
/// Shows all messages info and up. | |||||
/// </summary> | |||||
InfoUp = InfoOnly | WarningUp, | |||||
/// <summary> | |||||
/// Shows all messages. | |||||
/// </summary> | |||||
All = DebugOnly | InfoUp, | |||||
} | |||||
/// <summary> | |||||
/// A basic log function. | |||||
/// </summary> | |||||
/// <param name="level">the level of the message</param> | |||||
/// <param name="message">the message to log</param> | |||||
public abstract void Log(Level level, string message); | |||||
/// <summary> | |||||
/// A basic log function taking an exception to log. | |||||
/// </summary> | |||||
/// <param name="level">the level of the message</param> | |||||
/// <param name="exeption">the exception to log</param> | |||||
public virtual void Log(Level level, Exception exeption) => Log(level, exeption.ToString()); | |||||
/// <summary> | |||||
/// Sends a debug message. | |||||
/// Equivalent to Log(Level.Debug, message); | |||||
/// <see cref="Log(Level, string)"/> | |||||
/// </summary> | |||||
/// <param name="message">the message to log</param> | |||||
public virtual void Debug(string message) => Log(Level.Debug, message); | |||||
/// <summary> | |||||
/// Sends an exception as a debug message. | |||||
/// Equivalent to Log(Level.Debug, e); | |||||
/// <see cref="Log(Level, Exception)"/> | |||||
/// </summary> | |||||
/// <param name="e">the exception to log</param> | |||||
public virtual void Debug(Exception e) => Log(Level.Debug, e); | |||||
/// <summary> | |||||
/// Sends an info message. | |||||
/// Equivalent to Log(Level.Info, message). | |||||
/// <see cref="Log(Level, string)"/> | |||||
/// </summary> | |||||
/// <param name="message">the message to log</param> | |||||
public virtual void Info(string message) => Log(Level.Info, message); | |||||
/// <summary> | |||||
/// Sends an exception as an info message. | |||||
/// Equivalent to Log(Level.Info, e); | |||||
/// <see cref="Log(Level, Exception)"/> | |||||
/// </summary> | |||||
/// <param name="e">the exception to log</param> | |||||
public virtual void Info(Exception e) => Log(Level.Info, e); | |||||
/// <summary> | |||||
/// Sends a warning message. | |||||
/// Equivalent to Log(Level.Warning, message). | |||||
/// <see cref="Log(Level, string)"/> | |||||
/// </summary> | |||||
/// <param name="message">the message to log</param> | |||||
public virtual void Warn(string message) => Log(Level.Warning, message); | |||||
/// <summary> | |||||
/// Sends an exception as a warning message. | |||||
/// Equivalent to Log(Level.Warning, e); | |||||
/// <see cref="Log(Level, Exception)"/> | |||||
/// </summary> | |||||
/// <param name="e">the exception to log</param> | |||||
public virtual void Warn(Exception e) => Log(Level.Warning, e); | |||||
/// <summary> | |||||
/// Sends an error message. | |||||
/// Equivalent to Log(Level.Error, message). | |||||
/// <see cref="Log(Level, string)"/> | |||||
/// </summary> | |||||
/// <param name="message">the message to log</param> | |||||
public virtual void Error(string message) => Log(Level.Error, message); | |||||
/// <summary> | |||||
/// Sends an exception as an error message. | |||||
/// Equivalent to Log(Level.Error, e); | |||||
/// <see cref="Log(Level, Exception)"/> | |||||
/// </summary> | |||||
/// <param name="e">the exception to log</param> | |||||
public virtual void Error(Exception e) => Log(Level.Error, e); | |||||
/// <summary> | |||||
/// Sends a critical message. | |||||
/// Equivalent to Log(Level.Critical, message). | |||||
/// <see cref="Log(Level, string)"/> | |||||
/// </summary> | |||||
/// <param name="message">the message to log</param> | |||||
public virtual void Critical(string message) => Log(Level.Critical, message); | |||||
/// <summary> | |||||
/// Sends an exception as a critical message. | |||||
/// Equivalent to Log(Level.Critical, e); | |||||
/// <see cref="Log(Level, Exception)"/> | |||||
/// </summary> | |||||
/// <param name="e">the exception to log</param> | |||||
public virtual void Critical(Exception e) => Log(Level.Critical, e); | |||||
} | |||||
} |
@ -0,0 +1,285 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using System.Text; | |||||
namespace IllusionPlugin | |||||
{ | |||||
/// <summary> | |||||
/// Allows to get and set preferences for your mod. | |||||
/// </summary> | |||||
public interface IModPrefs | |||||
{ | |||||
/// <summary> | |||||
/// Gets a string from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
string GetString(string section, string name, string defaultValue = "", bool autoSave = false); | |||||
/// <summary> | |||||
/// Gets an int from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
int GetInt(string section, string name, int defaultValue = 0, bool autoSave = false); | |||||
/// <summary> | |||||
/// Gets a float from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
float GetFloat(string section, string name, float defaultValue = 0f, bool autoSave = false); | |||||
/// <summary> | |||||
/// Gets a bool from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
bool GetBool(string section, string name, bool defaultValue = false, bool autoSave = false); | |||||
/// <summary> | |||||
/// Checks whether or not a key exists in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <returns></returns> | |||||
bool HasKey(string section, string name); | |||||
/// <summary> | |||||
/// Sets a float in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
void SetFloat(string section, string name, float value); | |||||
/// <summary> | |||||
/// Sets an int in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
void SetInt(string section, string name, int value); | |||||
/// <summary> | |||||
/// Sets a string in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
void SetString(string section, string name, string value); | |||||
/// <summary> | |||||
/// Sets a bool in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
void SetBool(string section, string name, bool value); | |||||
} | |||||
/// <summary> | |||||
/// Allows to get and set preferences for your mod. | |||||
/// </summary> | |||||
public class ModPrefs : IModPrefs | |||||
{ | |||||
private static ModPrefs _staticInstance = null; | |||||
private static IModPrefs StaticInstace | |||||
{ | |||||
get | |||||
{ | |||||
if (_staticInstance == null) | |||||
_staticInstance = new ModPrefs(); | |||||
return _staticInstance; | |||||
} | |||||
} | |||||
internal static Dictionary<IBeatSaberPlugin, ModPrefs> ModPrefses { get; set; } = new Dictionary<IBeatSaberPlugin, ModPrefs>(); | |||||
private IniFile Instance; | |||||
/// <summary> | |||||
/// Constructs a ModPrefs object for the provide plugin. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to get the preferences file for</param> | |||||
public ModPrefs(IBeatSaberPlugin plugin) { | |||||
Instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "ModPrefs", $"{plugin.Name}.ini")); | |||||
ModPrefses.Add(plugin, this); | |||||
} | |||||
private ModPrefs() | |||||
{ | |||||
Instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "modprefs.ini")); | |||||
} | |||||
string IModPrefs.GetString(string section, string name, string defaultValue, bool autoSave) | |||||
{ | |||||
var value = Instance.IniReadValue(section, name); | |||||
if (value != "") | |||||
return value; | |||||
else if (autoSave) | |||||
(this as IModPrefs).SetString(section, name, defaultValue); | |||||
return defaultValue; | |||||
} | |||||
/// <summary> | |||||
/// Gets a string from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
public static string GetString(string section, string name, string defaultValue = "", bool autoSave = false) | |||||
=> StaticInstace.GetString(section, name, defaultValue, autoSave); | |||||
int IModPrefs.GetInt(string section, string name, int defaultValue, bool autoSave) | |||||
{ | |||||
if (int.TryParse(Instance.IniReadValue(section, name), out var value)) | |||||
return value; | |||||
else if (autoSave) | |||||
(this as IModPrefs).SetInt(section, name, defaultValue); | |||||
return defaultValue; | |||||
} | |||||
/// <summary> | |||||
/// Gets an int from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
public static int GetInt(string section, string name, int defaultValue = 0, bool autoSave = false) | |||||
=> StaticInstace.GetInt(section, name, defaultValue, autoSave); | |||||
float IModPrefs.GetFloat(string section, string name, float defaultValue, bool autoSave) | |||||
{ | |||||
if (float.TryParse(Instance.IniReadValue(section, name), out var value)) | |||||
return value; | |||||
else if (autoSave) | |||||
(this as IModPrefs).SetFloat(section, name, defaultValue); | |||||
return defaultValue; | |||||
} | |||||
/// <summary> | |||||
/// Gets a float from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
public static float GetFloat(string section, string name, float defaultValue = 0f, bool autoSave = false) | |||||
=> StaticInstace.GetFloat(section, name, defaultValue, autoSave); | |||||
bool IModPrefs.GetBool(string section, string name, bool defaultValue, bool autoSave) | |||||
{ | |||||
string sVal = GetString(section, name, null); | |||||
if (sVal == "1" || sVal == "0") | |||||
{ | |||||
return sVal == "1"; | |||||
} else if (autoSave) | |||||
{ | |||||
(this as IModPrefs).SetBool(section, name, defaultValue); | |||||
} | |||||
return defaultValue; | |||||
} | |||||
/// <summary> | |||||
/// Gets a bool from the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="defaultValue">Value that should be used when no value is found.</param> | |||||
/// <param name="autoSave">Whether or not the default value should be written if no value is found.</param> | |||||
/// <returns></returns> | |||||
public static bool GetBool(string section, string name, bool defaultValue = false, bool autoSave = false) | |||||
=> StaticInstace.GetBool(section, name, defaultValue, autoSave); | |||||
bool IModPrefs.HasKey(string section, string name) | |||||
{ | |||||
return Instance.IniReadValue(section, name) != null; | |||||
} | |||||
/// <summary> | |||||
/// Checks whether or not a key exists in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <returns></returns> | |||||
public static bool HasKey(string section, string name) => StaticInstace.HasKey(section, name); | |||||
void IModPrefs.SetFloat(string section, string name, float value) | |||||
{ | |||||
Instance.IniWriteValue(section, name, value.ToString()); | |||||
} | |||||
/// <summary> | |||||
/// Sets a float in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
public static void SetFloat(string section, string name, float value) | |||||
=> StaticInstace.SetFloat(section, name, value); | |||||
void IModPrefs.SetInt(string section, string name, int value) | |||||
{ | |||||
Instance.IniWriteValue(section, name, value.ToString()); | |||||
} | |||||
/// <summary> | |||||
/// Sets an int in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
public static void SetInt(string section, string name, int value) | |||||
=> StaticInstace.SetInt(section, name, value); | |||||
void IModPrefs.SetString(string section, string name, string value) | |||||
{ | |||||
Instance.IniWriteValue(section, name, value); | |||||
} | |||||
/// <summary> | |||||
/// Sets a string in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
public static void SetString(string section, string name, string value) | |||||
=> StaticInstace.SetString(section, name, value); | |||||
void IModPrefs.SetBool(string section, string name, bool value) | |||||
{ | |||||
Instance.IniWriteValue(section, name, value ? "1" : "0"); | |||||
} | |||||
/// <summary> | |||||
/// Sets a bool in the ini. | |||||
/// </summary> | |||||
/// <param name="section">Section of the key.</param> | |||||
/// <param name="name">Name of the key.</param> | |||||
/// <param name="value">Value that should be written.</param> | |||||
public static void SetBool(string section, string name, bool value) | |||||
=> StaticInstace.SetBool(section, name, value); | |||||
} | |||||
/// <summary> | |||||
/// An extension class for IBeatSaberPlugins. | |||||
/// </summary> | |||||
public static class ModPrefsExtensions { | |||||
/// <summary> | |||||
/// Gets the ModPrefs object for the provided plugin. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin wanting the prefrences</param> | |||||
/// <returns>the ModPrefs object</returns> | |||||
public static IModPrefs GetModPrefs(this IBeatSaberPlugin plugin) { | |||||
return ModPrefs.ModPrefses.First(o => o.Key == plugin).Value; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,203 @@ | |||||
using System; | |||||
using System.Reflection; | |||||
using UnityEngine; | |||||
namespace IllusionPlugin.Utils | |||||
{ | |||||
/// <summary> | |||||
/// A utility class providing reflection helper methods. | |||||
/// </summary> | |||||
public static class ReflectionUtil | |||||
{ | |||||
/// <summary> | |||||
/// Sets a (potentially) private field on the target object. | |||||
/// </summary> | |||||
/// <param name="obj">the object instance</param> | |||||
/// <param name="fieldName">the field to set</param> | |||||
/// <param name="value">the value to set it to</param> | |||||
public static void SetPrivateField(this object obj, string fieldName, object value) | |||||
{ | |||||
var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); | |||||
prop.SetValue(obj, value); | |||||
} | |||||
/// <summary> | |||||
/// Gets the value of a (potentially) private field. | |||||
/// </summary> | |||||
/// <typeparam name="T">the type of te field (result casted)</typeparam> | |||||
/// <param name="obj">the object instance to pull from</param> | |||||
/// <param name="fieldName">the name of the field to read</param> | |||||
/// <returns>the value of the field</returns> | |||||
public static T GetPrivateField<T>(this object obj, string fieldName) | |||||
{ | |||||
var prop = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); | |||||
var value = prop.GetValue(obj); | |||||
return (T) value; | |||||
} | |||||
/// <summary> | |||||
/// Sets a (potentially) private propert on the target object. | |||||
/// </summary> | |||||
/// <param name="obj">the target object instance</param> | |||||
/// <param name="propertyName">the name of the property</param> | |||||
/// <param name="value">the value to set it to</param> | |||||
public static void SetPrivateProperty(this object obj, string propertyName, object value) | |||||
{ | |||||
var prop = obj.GetType() | |||||
.GetProperty(propertyName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); | |||||
prop.SetValue(obj, value, null); | |||||
} | |||||
/// <summary> | |||||
/// Invokes a (potentially) private method. | |||||
/// </summary> | |||||
/// <param name="obj">the object to call from</param> | |||||
/// <param name="methodName">the method name</param> | |||||
/// <param name="methodParams">the method parameters</param> | |||||
/// <returns>the return value</returns> | |||||
public static object InvokePrivateMethod(this object obj, string methodName, params object[] methodParams) | |||||
{ | |||||
MethodInfo dynMethod = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); | |||||
return dynMethod.Invoke(obj, methodParams); | |||||
} | |||||
/// <summary> | |||||
/// Invokes a (potentially) private method. | |||||
/// </summary> | |||||
/// <typeparam name="T">the return type</typeparam> | |||||
/// <param name="obj">the object to call from</param> | |||||
/// <param name="methodName">the method name to call</param> | |||||
/// <param name="methodParams">the method's parameters</param> | |||||
/// <returns>the return value</returns> | |||||
public static T InvokePrivateMethod<T>(this object obj, string methodName, params object[] methodParams) | |||||
{ | |||||
return (T)InvokePrivateMethod(obj, methodName, methodParams); | |||||
} | |||||
/// <summary> | |||||
/// Copies a component of type originalType to a component of overridingType on the destination GameObject. | |||||
/// </summary> | |||||
/// <param name="original">the original component</param> | |||||
/// <param name="overridingType">the new component's type</param> | |||||
/// <param name="destination">the destination GameObject</param> | |||||
/// <param name="originalTypeOverride">overrides the source component type (for example, to a superclass)</param> | |||||
/// <returns>the copied component</returns> | |||||
public static Component CopyComponent(this Component original, Type overridingType, GameObject destination, Type originalTypeOverride = null) | |||||
{ | |||||
var copy = destination.AddComponent(overridingType); | |||||
var originalType = originalTypeOverride ?? original.GetType(); | |||||
Type type = originalType; | |||||
while (type != typeof(MonoBehaviour)) | |||||
{ | |||||
CopyForType(type, original, copy); | |||||
type = type.BaseType; | |||||
} | |||||
return copy; | |||||
} | |||||
/// <summary> | |||||
/// A generic version of CopyComponent. | |||||
/// <see cref="CopyComponent(Component, Type, GameObject, Type)"/> | |||||
/// </summary> | |||||
/// <typeparam name="T">the overriding type</typeparam> | |||||
/// <param name="original">the original component</param> | |||||
/// <param name="destination">the destination game object</param> | |||||
/// <param name="originalTypeOverride">overrides the source component type (for example, to a superclass)</param> | |||||
/// <returns>the copied component</returns> | |||||
public static T CopyComponent<T>(this Component original, GameObject destination, Type originalTypeOverride = null) | |||||
where T : Component | |||||
{ | |||||
var copy = destination.AddComponent<T>(); | |||||
var originalType = originalTypeOverride ?? original.GetType(); | |||||
Type type = originalType; | |||||
while (type != typeof(MonoBehaviour)) | |||||
{ | |||||
CopyForType(type, original, copy); | |||||
type = type.BaseType; | |||||
} | |||||
return copy; | |||||
} | |||||
private static void CopyForType(Type type, Component source, Component destination) | |||||
{ | |||||
FieldInfo[] myObjectFields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetField); | |||||
foreach (FieldInfo fi in myObjectFields) | |||||
{ | |||||
fi.SetValue(destination, fi.GetValue(source)); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Calls an instance method on a type specified by functionClass and dependency. | |||||
/// <seealso cref="CallNonStaticMethod(Type, string, Type[], object[])"/> | |||||
/// </summary> | |||||
/// <param name="functionClass">the type name</param> | |||||
/// <param name="dependency">the assembly the type is in</param> | |||||
/// <param name="function">the name of the method to call</param> | |||||
/// <param name="methodSig">the type signature of the method</param> | |||||
/// <param name="parameters">the method parameters</param> | |||||
/// <returns>the result of the call</returns> | |||||
public static object CallNonStaticMethod(string functionClass, string dependency, string function, Type[] methodSig, params object[] parameters) | |||||
{ | |||||
return CallNonStaticMethod(Type.GetType(string.Format("{0},{1}", functionClass, dependency)), function, methodSig, parameters); | |||||
} | |||||
/// <summary> | |||||
/// Calls an instance method on a new object. | |||||
/// </summary> | |||||
/// <param name="type">the object type</param> | |||||
/// <param name="function">the name of the method to call</param> | |||||
/// <param name="methodSig">the type signature</param> | |||||
/// <param name="parameters">the parameters</param> | |||||
/// <returns>the result of the call</returns> | |||||
public static object CallNonStaticMethod(this Type type, /*string functionClass, string dependency,*/ string function, Type[] methodSig, params object[] parameters) | |||||
{ | |||||
//Type FunctionClass = Type.GetType(string.Format("{0},{1}", functionClass, dependency)); | |||||
if (type != null) | |||||
{ | |||||
object instance = Activator.CreateInstance(type); | |||||
if (instance != null) | |||||
{ | |||||
Type instType = instance.GetType(); | |||||
MethodInfo methodInfo = instType.GetMethod(function, methodSig); | |||||
if (methodInfo != null) | |||||
{ | |||||
return methodInfo.Invoke(instance, parameters); | |||||
} | |||||
else | |||||
{ | |||||
throw new Exception("Method not found"); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
throw new Exception("Unable to instantiate object of type"); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
throw new ArgumentNullException("type"); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Calls an instance method on a new object. | |||||
/// <seealso cref="CallNonStaticMethod(Type, string, Type[], object[])"/> | |||||
/// </summary> | |||||
/// <typeparam name="T">the return type</typeparam> | |||||
/// <param name="type">the object type</param> | |||||
/// <param name="function">the name of the method to call</param> | |||||
/// <param name="methodSig">the type signature</param> | |||||
/// <param name="parameters">the parameters</param> | |||||
/// <returns>the result of the call</returns> | |||||
public static T CallNonStaticMethod<T>(this Type type, string function, Type[] methodSig, params object[] parameters) | |||||
{ | |||||
return (T)CallNonStaticMethod(type, function, methodSig, parameters); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,29 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using IllusionPlugin.Logging; | |||||
using LoggerBase = IllusionPlugin.Logging.Logger; | |||||
namespace IllusionInjector.Logging.Printers | |||||
{ | |||||
public class ColoredConsolePrinter : LogPrinter | |||||
{ | |||||
LoggerBase.LogLevel filter = LoggerBase.LogLevel.All; | |||||
public override LoggerBase.LogLevel Filter { get => filter; set => filter = value; } | |||||
ConsoleColor color = Console.ForegroundColor; | |||||
public ConsoleColor Color { get => color; set => color = value; } | |||||
public override void Print(LoggerBase.Level level, DateTime time, string logName, string message) | |||||
{ | |||||
if (((byte)level & (byte)StandardLogger.PrintFilter) == 0) return; | |||||
Console.ForegroundColor = color; | |||||
foreach (var line in message.Split(new string[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) | |||||
Console.WriteLine(string.Format(LoggerBase.LogFormat, line, logName, time, level.ToString().ToUpper())); | |||||
Console.ResetColor(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,92 @@ | |||||
using IllusionPlugin.Logging; | |||||
using Ionic.Zlib; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Runtime.InteropServices; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionInjector.Logging.Printers | |||||
{ | |||||
public abstract class GZFilePrinter : LogPrinter | |||||
{ | |||||
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | |||||
static extern bool CreateHardLink( | |||||
string lpFileName, | |||||
string lpExistingFileName, | |||||
IntPtr lpSecurityAttributes | |||||
); | |||||
[DllImport("Kernel32.dll")] | |||||
static extern Int32 GetLastError(); | |||||
private FileInfo fileInfo; | |||||
protected StreamWriter fileWriter; | |||||
private GZipStream zstream; | |||||
private FileStream fstream; | |||||
protected abstract FileInfo GetFileInfo(); | |||||
private void InitLog() | |||||
{ | |||||
try | |||||
{ | |||||
if (fileInfo == null) | |||||
{ // first init | |||||
fileInfo = GetFileInfo(); | |||||
var ext = fileInfo.Extension; | |||||
fileInfo = new FileInfo(fileInfo.FullName + ".gz"); | |||||
fileInfo.Create().Close(); | |||||
var symlink = new FileInfo(Path.Combine(fileInfo.DirectoryName, $"latest{ext}.gz")); | |||||
if (symlink.Exists) symlink.Delete(); | |||||
try | |||||
{ | |||||
if (!CreateHardLink(symlink.FullName, fileInfo.FullName, IntPtr.Zero)) | |||||
{ | |||||
Logger.log.Error($"Hardlink creation failed {GetLastError()}"); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.log.Error("Error creating latest hardlink!"); | |||||
Logger.log.Error(e); | |||||
} | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.log.Error("Error initializing log!"); | |||||
Logger.log.Error(e); | |||||
} | |||||
} | |||||
public override sealed void StartPrint() | |||||
{ | |||||
InitLog(); | |||||
fstream = fileInfo.Open(FileMode.Append, FileAccess.Write); | |||||
zstream = new GZipStream(fstream, CompressionMode.Compress) | |||||
{ | |||||
FlushMode = FlushType.Full | |||||
}; | |||||
fileWriter = new StreamWriter(zstream, new UTF8Encoding(false)); | |||||
} | |||||
public override sealed void EndPrint() | |||||
{ | |||||
fileWriter.Flush(); | |||||
zstream.Flush(); | |||||
fstream.Flush(); | |||||
fileWriter.Close(); | |||||
zstream.Close(); | |||||
fstream.Close(); | |||||
fileWriter.Dispose(); | |||||
zstream.Dispose(); | |||||
fstream.Dispose(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,30 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using IllusionPlugin.Logging; | |||||
using LoggerBase = IllusionPlugin.Logging.Logger; | |||||
namespace IllusionInjector.Logging.Printers | |||||
{ | |||||
class GlobalLogFilePrinter : GZFilePrinter | |||||
{ | |||||
public override LoggerBase.LogLevel Filter { get; set; } = LoggerBase.LogLevel.All; | |||||
public override void Print(IllusionPlugin.Logging.Logger.Level level, DateTime time, string logName, string message) | |||||
{ | |||||
foreach (var line in message.Split(new string[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) | |||||
fileWriter.WriteLine(string.Format(LoggerBase.LogFormat, line, logName, time, level.ToString().ToUpper())); | |||||
} | |||||
protected override FileInfo GetFileInfo() | |||||
{ | |||||
var logsDir = new DirectoryInfo("Logs"); | |||||
logsDir.Create(); | |||||
var finfo = new FileInfo(Path.Combine(logsDir.FullName, $"{DateTime.Now:yyyy.MM.dd.HH.mm}.log")); | |||||
return finfo; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,37 @@ | |||||
using IllusionPlugin.Logging; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using LoggerBase = IllusionPlugin.Logging.Logger; | |||||
namespace IllusionInjector.Logging.Printers | |||||
{ | |||||
class PluginLogFilePrinter : GZFilePrinter | |||||
{ | |||||
public override LoggerBase.LogLevel Filter { get; set; } = LoggerBase.LogLevel.All; | |||||
private string name; | |||||
protected override FileInfo GetFileInfo() | |||||
{ | |||||
var logsDir = new DirectoryInfo(Path.Combine("Logs",name)); | |||||
logsDir.Create(); | |||||
var finfo = new FileInfo(Path.Combine(logsDir.FullName, $"{DateTime.Now:yyyy.MM.dd.HH.mm}.log")); | |||||
return finfo; | |||||
} | |||||
public PluginLogFilePrinter(string name) | |||||
{ | |||||
this.name = name; | |||||
} | |||||
public override void Print(IllusionPlugin.Logging.Logger.Level level, DateTime time, string logName, string message) | |||||
{ | |||||
foreach (var line in message.Split(new string[] { "\n", Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) | |||||
fileWriter.WriteLine(string.Format("[{3} @ {2:HH:mm:ss}] {0}", line, logName, time, level.ToString().ToUpper())); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,168 @@ | |||||
using IllusionInjector.Logging.Printers; | |||||
using IllusionPlugin; | |||||
using IllusionPlugin.Logging; | |||||
using System; | |||||
using System.Collections.Concurrent; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using LoggerBase = IllusionPlugin.Logging.Logger; | |||||
namespace IllusionInjector.Logging | |||||
{ | |||||
internal static class Logger | |||||
{ | |||||
private static LoggerBase _log; | |||||
internal static LoggerBase log | |||||
{ | |||||
get | |||||
{ | |||||
if (_log == null) | |||||
_log = new StandardLogger("IPA"); | |||||
return _log; | |||||
} | |||||
} | |||||
internal static bool LogCreated => _log != null; | |||||
} | |||||
public class StandardLogger : LoggerBase | |||||
{ | |||||
private static readonly IReadOnlyList<LogPrinter> defaultPrinters = new List<LogPrinter>() | |||||
{ | |||||
new ColoredConsolePrinter() | |||||
{ | |||||
Filter = LogLevel.DebugOnly, | |||||
Color = ConsoleColor.Green, | |||||
}, | |||||
new ColoredConsolePrinter() | |||||
{ | |||||
Filter = LogLevel.InfoOnly, | |||||
Color = ConsoleColor.White, | |||||
}, | |||||
new ColoredConsolePrinter() | |||||
{ | |||||
Filter = LogLevel.WarningOnly, | |||||
Color = ConsoleColor.Yellow, | |||||
}, | |||||
new ColoredConsolePrinter() | |||||
{ | |||||
Filter = LogLevel.ErrorOnly, | |||||
Color = ConsoleColor.Red, | |||||
}, | |||||
new ColoredConsolePrinter() | |||||
{ | |||||
Filter = LogLevel.CriticalOnly, | |||||
Color = ConsoleColor.Magenta, | |||||
}, | |||||
new GlobalLogFilePrinter() | |||||
}; | |||||
private string logName; | |||||
private static bool showSourceClass = true; | |||||
public static LogLevel PrintFilter { get; set; } = LogLevel.InfoUp; | |||||
private List<LogPrinter> printers = new List<LogPrinter>(defaultPrinters); | |||||
static StandardLogger() | |||||
{ | |||||
if (ModPrefs.GetBool("IPA", "PrintDebug", false, true)) | |||||
PrintFilter = LogLevel.All; | |||||
showSourceClass = ModPrefs.GetBool("IPA", "DebugShowCallSource", false, true); | |||||
} | |||||
internal StandardLogger(string name) | |||||
{ | |||||
logName = name; | |||||
printers.Add(new PluginLogFilePrinter(name)); | |||||
if (_logThread == null || !_logThread.IsAlive) | |||||
{ | |||||
_logThread = new Thread(LogThread); | |||||
_logThread.Start(); | |||||
} | |||||
} | |||||
public override void Log(Level level, string message) | |||||
{ | |||||
_logQueue.Add(new LogMessage | |||||
{ | |||||
level = level, | |||||
message = message, | |||||
logger = this, | |||||
time = DateTime.Now | |||||
}); | |||||
} | |||||
public override void Debug(string message) | |||||
{ // add source to message | |||||
var stfm = new StackTrace().GetFrame(1).GetMethod(); | |||||
if (showSourceClass) | |||||
base.Debug($"{{{stfm.DeclaringType.FullName}::{stfm.Name}}} {message}"); | |||||
else | |||||
base.Debug(message); | |||||
} | |||||
internal struct LogMessage | |||||
{ | |||||
public Level level; | |||||
public StandardLogger logger; | |||||
public string message; | |||||
public DateTime time; | |||||
} | |||||
private static BlockingCollection<LogMessage> _logQueue = new BlockingCollection<LogMessage>(); | |||||
private static Thread _logThread; | |||||
private static void LogThread() | |||||
{ | |||||
HashSet<LogPrinter> started = new HashSet<LogPrinter>(); | |||||
while (_logQueue.TryTake(out LogMessage msg, Timeout.Infinite)) { | |||||
foreach (var printer in msg.logger.printers) | |||||
{ | |||||
try | |||||
{ | |||||
if (((byte)msg.level & (byte)printer.Filter) != 0) | |||||
{ | |||||
if (!started.Contains(printer)) | |||||
{ | |||||
printer.StartPrint(); | |||||
started.Add(printer); | |||||
} | |||||
printer.Print(msg.level, msg.time, msg.logger.logName, msg.message); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Console.WriteLine($"printer errored {e}"); | |||||
} | |||||
} | |||||
if (_logQueue.Count == 0) | |||||
{ | |||||
foreach (var printer in started) | |||||
{ | |||||
try | |||||
{ | |||||
printer.EndPrint(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Console.WriteLine($"printer errored {e}"); | |||||
} | |||||
} | |||||
started.Clear(); | |||||
} | |||||
} | |||||
} | |||||
public static void StopLogThread() | |||||
{ | |||||
_logQueue.CompleteAdding(); | |||||
_logThread.Join(); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,34 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using UnityEngine; | |||||
using LoggerBase = IllusionPlugin.Logging.Logger; | |||||
namespace IllusionInjector.Logging | |||||
{ | |||||
public class UnityLogInterceptor | |||||
{ | |||||
public static LoggerBase Unitylogger = new StandardLogger("UnityEngine"); | |||||
public static LoggerBase.Level LogTypeToLevel(LogType type) | |||||
{ | |||||
switch (type) | |||||
{ | |||||
case LogType.Assert: | |||||
return LoggerBase.Level.Debug; | |||||
case LogType.Error: | |||||
return LoggerBase.Level.Error; | |||||
case LogType.Exception: | |||||
return LoggerBase.Level.Critical; | |||||
case LogType.Log: | |||||
return LoggerBase.Level.Info; | |||||
case LogType.Warning: | |||||
return LoggerBase.Level.Warning; | |||||
default: | |||||
return LoggerBase.Level.Info; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,109 @@ | |||||
using IllusionInjector.Logging; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using UnityEngine; | |||||
using UnityEngine.SceneManagement; | |||||
namespace IllusionInjector | |||||
{ | |||||
public class PluginComponent : MonoBehaviour | |||||
{ | |||||
private CompositeBSPlugin bsPlugins; | |||||
private CompositeIPAPlugin ipaPlugins; | |||||
private bool quitting = false; | |||||
public static PluginComponent Create() | |||||
{ | |||||
Application.logMessageReceived += delegate (string condition, string stackTrace, LogType type) | |||||
{ | |||||
var level = UnityLogInterceptor.LogTypeToLevel(type); | |||||
UnityLogInterceptor.Unitylogger.Log(level, $"{condition.Trim()}"); | |||||
UnityLogInterceptor.Unitylogger.Log(level, $"{stackTrace.Trim()}"); | |||||
}; | |||||
return new GameObject("IPA_PluginManager").AddComponent<PluginComponent>(); | |||||
} | |||||
void Awake() | |||||
{ | |||||
DontDestroyOnLoad(gameObject); | |||||
bsPlugins = new CompositeBSPlugin(PluginManager.BSPlugins); | |||||
ipaPlugins = new CompositeIPAPlugin(PluginManager.Plugins); | |||||
// this has no relevance since there is a new mod updater system | |||||
//gameObject.AddComponent<ModUpdater>(); // AFTER plugins are loaded, but before most things | |||||
gameObject.AddComponent<Updating.ModsaberML.Updater>(); | |||||
bsPlugins.OnApplicationStart(); | |||||
ipaPlugins.OnApplicationStart(); | |||||
SceneManager.activeSceneChanged += OnActiveSceneChanged; | |||||
SceneManager.sceneLoaded += OnSceneLoaded; | |||||
SceneManager.sceneUnloaded += OnSceneUnloaded; | |||||
} | |||||
void Update() | |||||
{ | |||||
bsPlugins.OnUpdate(); | |||||
ipaPlugins.OnUpdate(); | |||||
} | |||||
void LateUpdate() | |||||
{ | |||||
bsPlugins.OnLateUpdate(); | |||||
ipaPlugins.OnLateUpdate(); | |||||
} | |||||
void FixedUpdate() | |||||
{ | |||||
bsPlugins.OnFixedUpdate(); | |||||
ipaPlugins.OnFixedUpdate(); | |||||
} | |||||
void OnDestroy() | |||||
{ | |||||
if (!quitting) | |||||
{ | |||||
Create(); | |||||
} | |||||
} | |||||
void OnApplicationQuit() | |||||
{ | |||||
SceneManager.activeSceneChanged -= OnActiveSceneChanged; | |||||
SceneManager.sceneLoaded -= OnSceneLoaded; | |||||
SceneManager.sceneUnloaded -= OnSceneUnloaded; | |||||
bsPlugins.OnApplicationQuit(); | |||||
ipaPlugins.OnApplicationQuit(); | |||||
quitting = true; | |||||
} | |||||
void OnLevelWasLoaded(int level) | |||||
{ | |||||
ipaPlugins.OnLevelWasLoaded(level); | |||||
} | |||||
public void OnLevelWasInitialized(int level) | |||||
{ | |||||
ipaPlugins.OnLevelWasInitialized(level); | |||||
} | |||||
void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) | |||||
{ | |||||
bsPlugins.OnSceneLoaded(scene, sceneMode); | |||||
} | |||||
private void OnSceneUnloaded(Scene scene) { | |||||
bsPlugins.OnSceneUnloaded(scene); | |||||
} | |||||
private void OnActiveSceneChanged(Scene prevScene, Scene nextScene) { | |||||
bsPlugins.OnActiveSceneChanged(prevScene, nextScene); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,264 @@ | |||||
using IllusionInjector.Logging; | |||||
using IllusionInjector.Updating; | |||||
using IllusionInjector.Utilities; | |||||
using IllusionPlugin; | |||||
using IllusionPlugin.BeatSaber; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using System.Runtime.CompilerServices; | |||||
using System.Runtime.InteropServices; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using LoggerBase = IllusionPlugin.Logging.Logger; | |||||
namespace IllusionInjector | |||||
{ | |||||
public static class PluginManager | |||||
{ | |||||
#pragma warning disable CS0618 // Type or member is obsolete (IPlugin) | |||||
public class BSPluginMeta | |||||
{ | |||||
public IBeatSaberPlugin Plugin { get; internal set; } | |||||
public string Filename { get; internal set; } | |||||
public ModsaberModInfo ModsaberInfo { get; internal set; } | |||||
} | |||||
public static IEnumerable<IBeatSaberPlugin> BSPlugins | |||||
{ | |||||
get | |||||
{ | |||||
if(_bsPlugins == null) | |||||
{ | |||||
LoadPlugins(); | |||||
} | |||||
return _bsPlugins.Select(p => p.Plugin); | |||||
} | |||||
} | |||||
private static List<BSPluginMeta> _bsPlugins = null; | |||||
internal static IEnumerable<BSPluginMeta> BSMetas | |||||
{ | |||||
get | |||||
{ | |||||
if (_bsPlugins == null) | |||||
{ | |||||
LoadPlugins(); | |||||
} | |||||
return _bsPlugins; | |||||
} | |||||
} | |||||
public static IEnumerable<IPlugin> Plugins | |||||
{ | |||||
get | |||||
{ | |||||
if (_ipaPlugins == null) | |||||
{ | |||||
LoadPlugins(); | |||||
} | |||||
return _ipaPlugins; | |||||
} | |||||
} | |||||
private static List<IPlugin> _ipaPlugins = null; | |||||
private static void LoadPlugins() | |||||
{ | |||||
string pluginDirectory = Path.Combine(Environment.CurrentDirectory, "Plugins"); | |||||
// Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL, | |||||
// so we need to resort to P/Invoke | |||||
string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath); | |||||
Logger.log.Info(exeName); | |||||
_bsPlugins = new List<BSPluginMeta>(); | |||||
_ipaPlugins = new List<IPlugin>(); | |||||
if (!Directory.Exists(pluginDirectory)) return; | |||||
string cacheDir = Path.Combine(pluginDirectory, ".cache"); | |||||
if (!Directory.Exists(cacheDir)) | |||||
{ | |||||
Directory.CreateDirectory(cacheDir); | |||||
} | |||||
else | |||||
{ | |||||
foreach (string plugin in Directory.GetFiles(cacheDir, "*")) | |||||
{ | |||||
File.Delete(plugin); | |||||
} | |||||
} | |||||
//Copy plugins to .cache | |||||
string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll"); | |||||
foreach (string s in originalPlugins) | |||||
{ | |||||
string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s)); | |||||
File.Copy(Path.Combine(pluginDirectory, s), pluginCopy); | |||||
} | |||||
var selfPlugin = new BSPluginMeta | |||||
{ | |||||
Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"), | |||||
Plugin = new SelfPlugin() | |||||
}; | |||||
selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo; | |||||
_bsPlugins.Add(selfPlugin); | |||||
//Load copied plugins | |||||
string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll"); | |||||
foreach (string s in copiedPlugins) | |||||
{ | |||||
var result = LoadPluginsFromFile(s, exeName); | |||||
_bsPlugins.AddRange(result.Item1); | |||||
_ipaPlugins.AddRange(result.Item2); | |||||
} | |||||
// DEBUG | |||||
Logger.log.Info($"Running on Unity {UnityEngine.Application.unityVersion}"); | |||||
Logger.log.Info($"Game version {UnityEngine.Application.version}"); | |||||
Logger.log.Info("-----------------------------"); | |||||
Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); | |||||
Logger.log.Info("-----------------------------"); | |||||
foreach (var plugin in _bsPlugins) | |||||
{ | |||||
Logger.log.Info($"{plugin.Plugin.Name}: {plugin.Plugin.Version}"); | |||||
} | |||||
Logger.log.Info("-----------------------------"); | |||||
foreach (var plugin in _ipaPlugins) | |||||
{ | |||||
Logger.log.Info($"{plugin.Name}: {plugin.Version}"); | |||||
} | |||||
Logger.log.Info("-----------------------------"); | |||||
} | |||||
private static Tuple<IEnumerable<BSPluginMeta>, IEnumerable<IPlugin>> LoadPluginsFromFile(string file, string exeName) | |||||
{ | |||||
List<BSPluginMeta> bsPlugins = new List<BSPluginMeta>(); | |||||
List<IPlugin> ipaPlugins = new List<IPlugin>(); | |||||
if (!File.Exists(file) || !file.EndsWith(".dll", true, null)) | |||||
return new Tuple<IEnumerable<BSPluginMeta>, IEnumerable<IPlugin>>(bsPlugins, ipaPlugins); | |||||
T OptionalGetPlugin<T>(Type t) where T : class | |||||
{ | |||||
// use typeof() to allow for easier renaming (in an ideal world this compiles to a string, but ¯\_(ツ)_/¯) | |||||
if (t.GetInterface(typeof(T).Name) != null) | |||||
{ | |||||
try | |||||
{ | |||||
T pluginInstance = Activator.CreateInstance(t) as T; | |||||
string[] filter = null; | |||||
if (pluginInstance is IGenericEnhancedPlugin) | |||||
{ | |||||
filter = ((IGenericEnhancedPlugin)pluginInstance).Filter; | |||||
} | |||||
if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase)) | |||||
return pluginInstance; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.log.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}"); | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
try | |||||
{ | |||||
Assembly assembly = Assembly.LoadFrom(file); | |||||
foreach (Type t in assembly.GetTypes()) | |||||
{ | |||||
IBeatSaberPlugin bsPlugin = OptionalGetPlugin<IBeatSaberPlugin>(t); | |||||
if (bsPlugin != null) | |||||
{ | |||||
try | |||||
{ | |||||
var init = t.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public); | |||||
if (init != null) | |||||
{ | |||||
var initArgs = new List<object>(); | |||||
var initParams = init.GetParameters(); | |||||
LoggerBase modLogger = null; | |||||
IModPrefs modPrefs = null; | |||||
foreach (var param in initParams) | |||||
{ | |||||
var ptype = param.ParameterType; | |||||
if (ptype.IsAssignableFrom(typeof(LoggerBase))) { | |||||
if (modLogger == null) modLogger = new StandardLogger(bsPlugin.Name); | |||||
initArgs.Add(modLogger); | |||||
} | |||||
else if (ptype.IsAssignableFrom(typeof(IModPrefs))) | |||||
{ | |||||
if (modPrefs == null) modPrefs = new ModPrefs(bsPlugin); | |||||
initArgs.Add(modPrefs); | |||||
} | |||||
else | |||||
initArgs.Add(ptype.GetDefault()); | |||||
} | |||||
init.Invoke(bsPlugin, initArgs.ToArray()); | |||||
} | |||||
bsPlugins.Add(new BSPluginMeta | |||||
{ | |||||
Plugin = bsPlugin, | |||||
Filename = file.Replace("\\.cache", ""), // quick and dirty fix | |||||
ModsaberInfo = bsPlugin.ModInfo | |||||
}); | |||||
} | |||||
catch (AmbiguousMatchException) | |||||
{ | |||||
Logger.log.Error($"Only one Init allowed per plugin"); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
IPlugin ipaPlugin = OptionalGetPlugin<IPlugin>(t); | |||||
if (ipaPlugin != null) | |||||
{ | |||||
ipaPlugins.Add(ipaPlugin); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.log.Error($"Could not load {Path.GetFileName(file)}! {e}"); | |||||
} | |||||
return new Tuple<IEnumerable<BSPluginMeta>, IEnumerable<IPlugin>>(bsPlugins, ipaPlugins); | |||||
} | |||||
public class AppInfo | |||||
{ | |||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = false)] | |||||
private static extern int GetModuleFileName(HandleRef hModule, StringBuilder buffer, int length); | |||||
private static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); | |||||
public static string StartupPath | |||||
{ | |||||
get | |||||
{ | |||||
StringBuilder stringBuilder = new StringBuilder(260); | |||||
GetModuleFileName(NullHandleRef, stringBuilder, stringBuilder.Capacity); | |||||
return stringBuilder.ToString(); | |||||
} | |||||
} | |||||
} | |||||
#pragma warning restore CS0618 // Type or member is obsolete (IPlugin) | |||||
} | |||||
} |
@ -0,0 +1,37 @@ | |||||
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("IPA.Loader")] | |||||
[assembly: AssemblyDescription("")] | |||||
[assembly: AssemblyConfiguration("")] | |||||
[assembly: AssemblyCompany("")] | |||||
[assembly: AssemblyProduct("IPA.Loader")] | |||||
[assembly: AssemblyCopyright("Copyright © 2018")] | |||||
[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("5ad344f0-01a0-4ca8-92e5-9d095737744d")] | |||||
[assembly: InternalsVisibleTo("IPA.Injector")] | |||||
// 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")] |
@ -0,0 +1,123 @@ | |||||
using IllusionInjector.Logging; | |||||
using IllusionInjector.Utilities; | |||||
using Newtonsoft.Json; | |||||
using Newtonsoft.Json.Converters; | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionInjector.Updating.ModsaberML | |||||
{ | |||||
class ApiEndpoint | |||||
{ | |||||
#if DEBUG && UPDATETEST | |||||
public const string ApiBase = "file://Z:/Users/aaron/Source/Repos/IPA-Reloaded-BeatSaber/IPA.Tests/"; | |||||
public const string GetApprovedEndpoint = "updater_test.json"; | |||||
#else | |||||
public const string ApiBase = "https://www.modsaber.ml/"; | |||||
public const string GetApprovedEndpoint = "registry/{0}"; | |||||
#endif | |||||
class HexArrayConverter : JsonConverter | |||||
{ | |||||
public override bool CanConvert(Type objectType) | |||||
{ | |||||
return objectType == typeof(byte[]); | |||||
} | |||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | |||||
{ | |||||
if (reader.TokenType == JsonToken.Null) | |||||
{ | |||||
return null; | |||||
} | |||||
if (reader.TokenType == JsonToken.String) | |||||
{ | |||||
try | |||||
{ | |||||
return LoneFunctions.StringToByteArray((string)reader.Value); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
throw new Exception(string.Format("Error parsing version string: {0}", reader.Value), ex); | |||||
} | |||||
} | |||||
throw new Exception(string.Format("Unexpected token or value when parsing hex string. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); | |||||
} | |||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | |||||
{ | |||||
if (value == null) | |||||
{ | |||||
writer.WriteNull(); | |||||
} | |||||
else | |||||
{ | |||||
if (!(value is byte[])) | |||||
{ | |||||
throw new JsonSerializationException("Expected byte[] object value"); | |||||
} | |||||
writer.WriteValue(LoneFunctions.ByteArrayToString(value as byte[])); | |||||
} | |||||
} | |||||
} | |||||
[Serializable] | |||||
public class Mod | |||||
{ | |||||
#pragma warning disable CS0649 | |||||
[JsonProperty("name")] | |||||
public string Name; | |||||
[JsonProperty("version"), | |||||
JsonConverter(typeof(VersionConverter))] | |||||
public Version Version; | |||||
[JsonProperty("approved")] | |||||
public bool Approved; | |||||
[JsonProperty("title")] | |||||
public string Title; | |||||
[JsonProperty("gameVersion"), | |||||
JsonConverter(typeof(VersionConverter))] | |||||
public Version GameVersion; | |||||
[JsonProperty("author")] | |||||
public string Author; | |||||
#pragma warning restore CS0649 | |||||
[Serializable] | |||||
public class PlatformFile | |||||
{ | |||||
[JsonProperty("hash"), | |||||
JsonConverter(typeof(HexArrayConverter))] | |||||
public byte[] Hash = new byte[20]; | |||||
[JsonProperty("files", ItemConverterType = typeof(HexArrayConverter))] | |||||
public Dictionary<string, byte[]> FileHashes = new Dictionary<string, byte[]>(); | |||||
[JsonProperty("url")] | |||||
public string DownloadPath = null; | |||||
public override string ToString() | |||||
{ | |||||
return $"{LoneFunctions.ByteArrayToString(Hash)}@{DownloadPath}({string.Join(",",FileHashes.Select(o=>$"\"{o.Key}\":\"{LoneFunctions.ByteArrayToString(o.Value)}\""))})"; | |||||
} | |||||
} | |||||
[Serializable] | |||||
public class FilesObject | |||||
{ | |||||
[JsonProperty("steam")] | |||||
public PlatformFile Steam = null; | |||||
[JsonProperty("oculus")] | |||||
public PlatformFile Oculus = null; | |||||
} | |||||
[JsonProperty("files")] | |||||
public FilesObject Files = null; | |||||
public override string ToString() | |||||
{ | |||||
return $"{{\"{Title} ({Name})\"v{Version} for {GameVersion} by {Author} with \"{Files.Steam}\" and \"{Files.Oculus}\"}}"; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,346 @@ | |||||
using IllusionInjector.Updating.Backup; | |||||
using IllusionInjector.Utilities; | |||||
using Ionic.Zip; | |||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System.Collections; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Security.Cryptography; | |||||
using System.Text; | |||||
using System.Text.RegularExpressions; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using UnityEngine; | |||||
using UnityEngine.Networking; | |||||
using Logger = IllusionInjector.Logging.Logger; | |||||
namespace IllusionInjector.Updating.ModsaberML | |||||
{ | |||||
class Updater : MonoBehaviour | |||||
{ | |||||
public static Updater instance; | |||||
public void Awake() | |||||
{ | |||||
try | |||||
{ | |||||
if (instance != null) | |||||
Destroy(this); | |||||
else | |||||
{ | |||||
instance = this; | |||||
CheckForUpdates(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.log.Error(e); | |||||
} | |||||
} | |||||
public void CheckForUpdates() | |||||
{ | |||||
StartCoroutine(CheckForUpdatesCoroutine()); | |||||
} | |||||
private struct UpdateStruct | |||||
{ | |||||
public PluginManager.BSPluginMeta plugin; | |||||
public ApiEndpoint.Mod externInfo; | |||||
} | |||||
IEnumerator CheckForUpdatesCoroutine() | |||||
{ | |||||
Logger.log.Info("Checking for mod updates..."); | |||||
var toUpdate = new List<UpdateStruct>(); | |||||
var GameVersion = new Version(Application.version); | |||||
foreach (var plugin in PluginManager.BSMetas) | |||||
{ | |||||
var info = plugin.ModsaberInfo; | |||||
if (info == null) continue; | |||||
using (var request = UnityWebRequest.Get(ApiEndpoint.ApiBase + string.Format(ApiEndpoint.GetApprovedEndpoint, info.InternalName))) | |||||
{ | |||||
yield return request.SendWebRequest(); | |||||
if (request.isNetworkError) | |||||
{ | |||||
Logger.log.Error("Network error while trying to update mods"); | |||||
Logger.log.Error(request.error); | |||||
continue; | |||||
} | |||||
if (request.isHttpError) | |||||
{ | |||||
if (request.responseCode == 404) | |||||
{ | |||||
Logger.log.Error($"Mod {plugin.Plugin.Name} not found under name {info.InternalName}"); | |||||
continue; | |||||
} | |||||
Logger.log.Error($"Server returned an error code while trying to update mod {plugin.Plugin.Name}"); | |||||
Logger.log.Error(request.error); | |||||
continue; | |||||
} | |||||
var json = request.downloadHandler.text; | |||||
ApiEndpoint.Mod modRegistry; | |||||
try | |||||
{ | |||||
modRegistry = JsonConvert.DeserializeObject<ApiEndpoint.Mod>(json); | |||||
Logger.log.Debug(modRegistry.ToString()); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.log.Error($"Parse error while trying to update mods"); | |||||
Logger.log.Error(e); | |||||
continue; | |||||
} | |||||
Logger.log.Debug($"Found Modsaber.ML registration for {plugin.Plugin.Name} ({info.InternalName})"); | |||||
Logger.log.Debug($"Installed version: {info.CurrentVersion}; Latest version: {modRegistry.Version}"); | |||||
if (modRegistry.Version > info.CurrentVersion) | |||||
{ | |||||
Logger.log.Debug($"{plugin.Plugin.Name} needs an update!"); | |||||
if (modRegistry.GameVersion == GameVersion) | |||||
{ | |||||
Logger.log.Debug($"Queueing update..."); | |||||
toUpdate.Add(new UpdateStruct | |||||
{ | |||||
plugin = plugin, | |||||
externInfo = modRegistry | |||||
}); | |||||
} | |||||
else | |||||
{ | |||||
Logger.log.Warn($"Update avaliable for {plugin.Plugin.Name}, but for a different Beat Saber version!"); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
Logger.log.Info($"{toUpdate.Count} mods need updating"); | |||||
if (toUpdate.Count == 0) yield break; | |||||
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName()); | |||||
Directory.CreateDirectory(tempDirectory); | |||||
foreach (var item in toUpdate) | |||||
{ | |||||
StartCoroutine(UpdateModCoroutine(item, tempDirectory)); | |||||
} | |||||
} | |||||
class StreamDownloadHandler : DownloadHandlerScript | |||||
{ | |||||
public MemoryStream Stream { get; set; } | |||||
public StreamDownloadHandler(MemoryStream stream) : base() | |||||
{ | |||||
Stream = stream; | |||||
} | |||||
protected override void ReceiveContentLength(int contentLength) | |||||
{ | |||||
Stream.Capacity = contentLength; | |||||
Logger.log.Debug($"Got content length: {contentLength}"); | |||||
} | |||||
protected override void CompleteContent() | |||||
{ | |||||
Logger.log.Debug("Download complete"); | |||||
} | |||||
protected override bool ReceiveData(byte[] data, int dataLength) | |||||
{ | |||||
if (data == null || data.Length < 1) | |||||
{ | |||||
Logger.log.Debug("CustomWebRequest :: ReceiveData - received a null/empty buffer"); | |||||
return false; | |||||
} | |||||
Stream.Write(data, 0, dataLength); | |||||
return true; | |||||
} | |||||
protected override byte[] GetData() { return null; } | |||||
protected override float GetProgress() | |||||
{ | |||||
return 0f; | |||||
} | |||||
public override string ToString() | |||||
{ | |||||
return $"{base.ToString()} ({Stream?.ToString()})"; | |||||
} | |||||
} | |||||
private void ExtractPluginAsync(MemoryStream stream, UpdateStruct item, ApiEndpoint.Mod.PlatformFile fileInfo, string tempDirectory) | |||||
{ | |||||
Logger.log.Debug($"Extracting ZIP file for {item.plugin.Plugin.Name}"); | |||||
var data = stream.GetBuffer(); | |||||
SHA1 sha = new SHA1CryptoServiceProvider(); | |||||
var hash = sha.ComputeHash(data); | |||||
if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash)) | |||||
throw new Exception("The hash for the file doesn't match what is defined"); | |||||
var newFiles = new List<FileInfo>(); | |||||
var backup = new BackupUnit(tempDirectory, $"backup-{item.plugin.ModsaberInfo.InternalName}"); | |||||
try | |||||
{ | |||||
bool shouldDeleteOldFile = true; | |||||
using (var zipFile = ZipFile.Read(stream)) | |||||
{ | |||||
Logger.log.Debug("Streams opened"); | |||||
foreach (var entry in zipFile) | |||||
{ | |||||
if (entry.IsDirectory) | |||||
{ | |||||
Logger.log.Debug($"Creating directory {entry.FileName}"); | |||||
Directory.CreateDirectory(Path.Combine(Environment.CurrentDirectory, entry.FileName)); | |||||
} | |||||
else | |||||
{ | |||||
using (var ostream = new MemoryStream((int)entry.UncompressedSize)) | |||||
{ | |||||
entry.Extract(ostream); | |||||
ostream.Seek(0, SeekOrigin.Begin); | |||||
sha = new SHA1CryptoServiceProvider(); | |||||
var fileHash = sha.ComputeHash(ostream); | |||||
if (!LoneFunctions.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName])) | |||||
throw new Exception("The hash for the file doesn't match what is defined"); | |||||
ostream.Seek(0, SeekOrigin.Begin); | |||||
FileInfo targetFile = new FileInfo(Path.Combine(Environment.CurrentDirectory, entry.FileName)); | |||||
Directory.CreateDirectory(targetFile.DirectoryName); | |||||
if (targetFile.FullName == item.plugin.Filename) | |||||
shouldDeleteOldFile = false; // overwriting old file, no need to delete | |||||
if (targetFile.Exists) | |||||
backup.Add(targetFile); | |||||
else | |||||
newFiles.Add(targetFile); | |||||
Logger.log.Debug($"Extracting file {targetFile.FullName}"); | |||||
var fstream = targetFile.Create(); | |||||
ostream.CopyTo(fstream); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
if (item.plugin.Plugin is SelfPlugin) | |||||
{ // currently updating self | |||||
Process.Start(new ProcessStartInfo | |||||
{ | |||||
FileName = item.plugin.Filename, | |||||
Arguments = $"--waitfor={Process.GetCurrentProcess().Id} --nowait", | |||||
UseShellExecute = false | |||||
}); | |||||
} | |||||
else if (shouldDeleteOldFile) | |||||
File.Delete(item.plugin.Filename); | |||||
} | |||||
catch (Exception) | |||||
{ // something failed; restore | |||||
foreach (var file in newFiles) | |||||
file.Delete(); | |||||
backup.Restore(); | |||||
backup.Delete(); | |||||
throw; | |||||
} | |||||
backup.Delete(); | |||||
Logger.log.Debug("Downloader exited"); | |||||
} | |||||
IEnumerator UpdateModCoroutine(UpdateStruct item, string tempDirectory) | |||||
{ | |||||
Logger.log.Debug($"Steam avaliable: {SteamCheck.IsAvailable}"); | |||||
ApiEndpoint.Mod.PlatformFile platformFile; | |||||
if (SteamCheck.IsAvailable || item.externInfo.Files.Oculus == null) | |||||
platformFile = item.externInfo.Files.Steam; | |||||
else | |||||
platformFile = item.externInfo.Files.Oculus; | |||||
string url = platformFile.DownloadPath; | |||||
Logger.log.Debug($"URL = {url}"); | |||||
const int MaxTries = 3; | |||||
int maxTries = MaxTries; | |||||
while (maxTries > 0) | |||||
{ | |||||
if (maxTries-- != MaxTries) | |||||
Logger.log.Info($"Re-trying download..."); | |||||
using (var stream = new MemoryStream()) | |||||
using (var request = UnityWebRequest.Get(url)) | |||||
using (var taskTokenSource = new CancellationTokenSource()) | |||||
{ | |||||
var dlh = new StreamDownloadHandler(stream); | |||||
request.downloadHandler = dlh; | |||||
Logger.log.Debug("Sending request"); | |||||
//Logger.log.Debug(request?.downloadHandler?.ToString() ?? "DLH==NULL"); | |||||
yield return request.SendWebRequest(); | |||||
Logger.log.Debug("Download finished"); | |||||
if (request.isNetworkError) | |||||
{ | |||||
Logger.log.Error("Network error while trying to update mod"); | |||||
Logger.log.Error(request.error); | |||||
taskTokenSource.Cancel(); | |||||
continue; | |||||
} | |||||
if (request.isHttpError) | |||||
{ | |||||
Logger.log.Error($"Server returned an error code while trying to update mod"); | |||||
Logger.log.Error(request.error); | |||||
taskTokenSource.Cancel(); | |||||
continue; | |||||
} | |||||
stream.Seek(0, SeekOrigin.Begin); // reset to beginning | |||||
var downloadTask = Task.Run(() => | |||||
{ // use slightly more multithreaded approach than coroutines | |||||
ExtractPluginAsync(stream, item, platformFile, tempDirectory); | |||||
}, taskTokenSource.Token); | |||||
while (!(downloadTask.IsCompleted || downloadTask.IsCanceled || downloadTask.IsFaulted)) | |||||
yield return null; // pause coroutine until task is done | |||||
if (downloadTask.IsFaulted) | |||||
{ | |||||
Logger.log.Error($"Error downloading mod {item.plugin.Plugin.Name}"); | |||||
Logger.log.Error(downloadTask.Exception); | |||||
continue; | |||||
} | |||||
break; | |||||
} | |||||
} | |||||
if (maxTries == 0) | |||||
Logger.log.Warn($"Plugin download failed {MaxTries} times, not re-trying"); | |||||
else | |||||
Logger.log.Debug("Download complete"); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,55 @@ | |||||
using IllusionPlugin; | |||||
using IllusionPlugin.BeatSaber; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using UnityEngine.SceneManagement; | |||||
namespace IllusionInjector.Updating | |||||
{ | |||||
internal class SelfPlugin : IBeatSaberPlugin | |||||
{ | |||||
internal const string IPA_Name = "Beat Saber IPA"; | |||||
internal const string IPA_Version = "3.9.0"; | |||||
public string Name => IPA_Name; | |||||
public string Version => IPA_Version; | |||||
public ModsaberModInfo ModInfo => new ModsaberModInfo | |||||
{ | |||||
CurrentVersion = new Version(IPA_Version), | |||||
InternalName = "beatsaber-ipa-reloaded" | |||||
}; | |||||
public void OnActiveSceneChanged(Scene prevScene, Scene nextScene) | |||||
{ | |||||
} | |||||
public void OnApplicationQuit() | |||||
{ | |||||
} | |||||
public void OnApplicationStart() | |||||
{ | |||||
} | |||||
public void OnFixedUpdate() | |||||
{ | |||||
} | |||||
public void OnSceneLoaded(Scene scene, LoadSceneMode sceneMode) | |||||
{ | |||||
} | |||||
public void OnSceneUnloaded(Scene scene) | |||||
{ | |||||
} | |||||
public void OnUpdate() | |||||
{ | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,20 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionInjector.Utilities | |||||
{ | |||||
public static class Extensions | |||||
{ | |||||
public static object GetDefault(this Type type) | |||||
{ | |||||
if (type.IsValueType) | |||||
{ | |||||
return Activator.CreateInstance(type); | |||||
} | |||||
return null; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,63 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionInjector.Utilities | |||||
{ | |||||
public static class LoneFunctions | |||||
{ | |||||
public static byte[] StringToByteArray(string hex) | |||||
{ | |||||
int NumberChars = hex.Length; | |||||
byte[] bytes = new byte[NumberChars / 2]; | |||||
for (int i = 0; i < NumberChars; i += 2) | |||||
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); | |||||
return bytes; | |||||
} | |||||
public static string ByteArrayToString(byte[] ba) | |||||
{ | |||||
StringBuilder hex = new StringBuilder(ba.Length * 2); | |||||
foreach (byte b in ba) | |||||
hex.AppendFormat("{0:x2}", b); | |||||
return hex.ToString(); | |||||
} | |||||
// Copyright (c) 2008-2013 Hafthor Stefansson | |||||
// Distributed under the MIT/X11 software license | |||||
// Ref: http://www.opensource.org/licenses/mit-license.php. | |||||
// From: https://stackoverflow.com/a/8808245/3117125 | |||||
public static unsafe bool UnsafeCompare(byte[] a1, byte[] a2) | |||||
{ | |||||
if (a1 == a2) return true; | |||||
if (a1 == null || a2 == null || a1.Length != a2.Length) | |||||
return false; | |||||
fixed (byte* p1 = a1, p2 = a2) | |||||
{ | |||||
byte* x1 = p1, x2 = p2; | |||||
int l = a1.Length; | |||||
for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8) | |||||
if (*((long*)x1) != *((long*)x2)) return false; | |||||
if ((l & 4) != 0) { if (*((int*)x1) != *((int*)x2)) return false; x1 += 4; x2 += 4; } | |||||
if ((l & 2) != 0) { if (*((short*)x1) != *((short*)x2)) return false; x1 += 2; x2 += 2; } | |||||
if ((l & 1) != 0) if (*((byte*)x1) != *((byte*)x2)) return false; | |||||
return true; | |||||
} | |||||
} | |||||
public static string GetRelativePath(string filespec, string folder) | |||||
{ | |||||
Uri pathUri = new Uri(filespec); | |||||
// Folders must end in a slash | |||||
if (!folder.EndsWith(Path.DirectorySeparatorChar.ToString())) | |||||
{ | |||||
folder += Path.DirectorySeparatorChar; | |||||
} | |||||
Uri folderUri = new Uri(folder); | |||||
return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,27 @@ | |||||
using IllusionInjector.Logging; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace IllusionInjector.Utilities | |||||
{ | |||||
public static class SteamCheck | |||||
{ | |||||
public static Type SteamVRCamera; | |||||
public static Type SteamVRExternalCamera; | |||||
public static Type SteamVRFade; | |||||
public static bool IsAvailable => FindSteamVRAsset(); | |||||
private static bool FindSteamVRAsset() | |||||
{ | |||||
// these require assembly qualified names.... | |||||
SteamVRCamera = Type.GetType("SteamVR_Camera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); | |||||
SteamVRExternalCamera = Type.GetType("SteamVR_ExternalCamera, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); | |||||
SteamVRFade = Type.GetType("SteamVR_Fade, Assembly-CSharp-firstpass, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", false); | |||||
return SteamVRCamera != null && SteamVRExternalCamera != null && SteamVRFade != null; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,5 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<packages> | |||||
<package id="Ionic.Zip" version="1.9.1.8" targetFramework="net46" /> | |||||
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net46" /> | |||||
</packages> |