@ -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> |