Browse Source

Began refactor

pull/46/head
Anairkoen Schno 6 years ago
parent
commit
2b32d6f91e
43 changed files with 3077 additions and 13 deletions
  1. +12
    -0
      BSIPA.sln
  2. +31
    -0
      IPA.Injector/Bootstrapper.cs
  3. +72
    -0
      IPA.Injector/ConsoleWindow.cs
  4. +91
    -0
      IPA.Injector/IPA.Injector.csproj
  5. +63
    -0
      IPA.Injector/Injector.cs
  6. +35
    -0
      IPA.Injector/PostBuild.msbuild
  7. +37
    -0
      IPA.Injector/Properties/AssemblyInfo.cs
  8. +5
    -0
      IPA.Injector/packages.config
  9. +97
    -0
      IPA.Loader/BeatSaber/CompositeBSPlugin.cs
  10. +94
    -0
      IPA.Loader/IPA.Loader.csproj
  11. +75
    -0
      IPA.Loader/IPA/CompositeIPAPlugin.cs
  12. +71
    -0
      IPA.Loader/IllusionPlugin/BeatSaber/IBeatSaberPlugin.cs
  13. +13
    -0
      IPA.Loader/IllusionPlugin/BeatSaber/IEnhancedBeatSaberPlugin.cs
  14. +24
    -0
      IPA.Loader/IllusionPlugin/BeatSaber/ModsaberModInfo.cs
  15. +25
    -0
      IPA.Loader/IllusionPlugin/IGenericEnhancedPlugin.cs
  16. +14
    -0
      IPA.Loader/IllusionPlugin/IPA/IEnhancedPlugin.cs
  17. +58
    -0
      IPA.Loader/IllusionPlugin/IPA/IPlugin.cs
  18. +100
    -0
      IPA.Loader/IllusionPlugin/IniFile.cs
  19. +37
    -0
      IPA.Loader/IllusionPlugin/Logging/LogPrinter.cs
  20. +182
    -0
      IPA.Loader/IllusionPlugin/Logging/Logger.cs
  21. +285
    -0
      IPA.Loader/IllusionPlugin/ModPrefs.cs
  22. +203
    -0
      IPA.Loader/IllusionPlugin/Utils/ReflectionUtil.cs
  23. +29
    -0
      IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs
  24. +92
    -0
      IPA.Loader/Logging/Printers/GZFilePrinter.cs
  25. +30
    -0
      IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs
  26. +37
    -0
      IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs
  27. +168
    -0
      IPA.Loader/Logging/StandardLogger.cs
  28. +34
    -0
      IPA.Loader/Logging/UnityLogInterceptor.cs
  29. +109
    -0
      IPA.Loader/PluginComponent.cs
  30. +264
    -0
      IPA.Loader/PluginManager.cs
  31. +37
    -0
      IPA.Loader/Properties/AssemblyInfo.cs
  32. +123
    -0
      IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs
  33. +346
    -0
      IPA.Loader/Updating/ModsaberML/Updater.cs
  34. +55
    -0
      IPA.Loader/Updating/SelfPlugin.cs
  35. +20
    -0
      IPA.Loader/Utilities/Extensions.cs
  36. +63
    -0
      IPA.Loader/Utilities/LoneFunctions.cs
  37. +27
    -0
      IPA.Loader/Utilities/SteamCheck.cs
  38. +5
    -0
      IPA.Loader/packages.config
  39. +1
    -1
      IPA/IPA.csproj
  40. +8
    -7
      IPA/Patcher/Patcher.cs
  41. +2
    -2
      IPA/Program.cs
  42. +2
    -2
      IPA/Properties/AssemblyInfo.cs
  43. +1
    -1
      IllusionInjector/Updating/SelfPlugin.cs

+ 12
- 0
BSIPA.sln View File

@ -17,6 +17,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Tests", "IPA.Tests\IPA.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSBuildTasks", "MSBuildTasks\MSBuildTasks.csproj", "{F08C3C7A-3221-432E-BAB8-32BCE58408C8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Loader", "IPA.Loader\IPA.Loader.csproj", "{5AD344F0-01A0-4CA8-92E5-9D095737744D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Injector", "IPA.Injector\IPA.Injector.csproj", "{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -43,6 +47,14 @@ Global
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|Any CPU.Build.0 = Release|Any CPU
{5AD344F0-01A0-4CA8-92E5-9D095737744D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AD344F0-01A0-4CA8-92E5-9D095737744D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AD344F0-01A0-4CA8-92E5-9D095737744D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AD344F0-01A0-4CA8-92E5-9D095737744D}.Release|Any CPU.Build.0 = Release|Any CPU
{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A1AF16B-27F1-46E0-9A95-181516BC1CB7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE


+ 31
- 0
IPA.Injector/Bootstrapper.cs View File

@ -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();
}
}
}

+ 72
- 0
IPA.Injector/ConsoleWindow.cs View File

@ -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);
}
}

+ 91
- 0
IPA.Injector/IPA.Injector.csproj View File

@ -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="&quot;$(MSBuildBinPath)\MSBuild.exe&quot; &quot;$(MSBuildProjectDirectory)\PostBuild.msbuild&quot; /property:OPath=$(OutputPath) /property:Configuration=$(Configuration) /property:SolDir=$(SolutionDir)" />
</Target>
</Project>

+ 63
- 0
IPA.Injector/Injector.cs View File

@ -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();
}
}
}

+ 35
- 0
IPA.Injector/PostBuild.msbuild View File

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

+ 37
- 0
IPA.Injector/Properties/AssemblyInfo.cs View File

@ -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")]

+ 5
- 0
IPA.Injector/packages.config View File

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

+ 97
- 0
IPA.Loader/BeatSaber/CompositeBSPlugin.cs View File

@ -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();
});
}
}
}

+ 94
- 0
IPA.Loader/IPA.Loader.csproj View File

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

+ 75
- 0
IPA.Loader/IPA/CompositeIPAPlugin.cs View File

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

+ 71
- 0
IPA.Loader/IllusionPlugin/BeatSaber/IBeatSaberPlugin.cs View File

@ -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);
}
}

+ 13
- 0
IPA.Loader/IllusionPlugin/BeatSaber/IEnhancedBeatSaberPlugin.cs View File

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

+ 24
- 0
IPA.Loader/IllusionPlugin/BeatSaber/ModsaberModInfo.cs View File

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

+ 25
- 0
IPA.Loader/IllusionPlugin/IGenericEnhancedPlugin.cs View File

@ -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();
}
}

+ 14
- 0
IPA.Loader/IllusionPlugin/IPA/IEnhancedPlugin.cs View File

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

+ 58
- 0
IPA.Loader/IllusionPlugin/IPA/IPlugin.cs View File

@ -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();
}
}

+ 100
- 0
IPA.Loader/IllusionPlugin/IniFile.cs View File

@ -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();
}
}
}

+ 37
- 0
IPA.Loader/IllusionPlugin/Logging/LogPrinter.cs View File

@ -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() { }
}
}

+ 182
- 0
IPA.Loader/IllusionPlugin/Logging/Logger.cs View File

@ -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);
}
}

+ 285
- 0
IPA.Loader/IllusionPlugin/ModPrefs.cs View File

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

+ 203
- 0
IPA.Loader/IllusionPlugin/Utils/ReflectionUtil.cs View File

@ -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);
}
}
}

+ 29
- 0
IPA.Loader/Logging/Printers/ColoredConsolePrinter.cs View File

@ -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();
}
}
}

+ 92
- 0
IPA.Loader/Logging/Printers/GZFilePrinter.cs View File

@ -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();
}
}
}

+ 30
- 0
IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs View File

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

+ 37
- 0
IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs View File

@ -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()));
}
}
}

+ 168
- 0
IPA.Loader/Logging/StandardLogger.cs View File

@ -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();
}
}
}

+ 34
- 0
IPA.Loader/Logging/UnityLogInterceptor.cs View File

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

+ 109
- 0
IPA.Loader/PluginComponent.cs View File

@ -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);
}
}
}

+ 264
- 0
IPA.Loader/PluginManager.cs View File

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

+ 37
- 0
IPA.Loader/Properties/AssemblyInfo.cs View File

@ -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")]

+ 123
- 0
IPA.Loader/Updating/ModsaberML/ApiEndpoint.cs View File

@ -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}\"}}";
}
}
}
}

+ 346
- 0
IPA.Loader/Updating/ModsaberML/Updater.cs View File

@ -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");
}
}
}

+ 55
- 0
IPA.Loader/Updating/SelfPlugin.cs View File

@ -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()
{
}
}
}

+ 20
- 0
IPA.Loader/Utilities/Extensions.cs View File

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

+ 63
- 0
IPA.Loader/Utilities/LoneFunctions.cs View File

@ -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));
}
}
}

+ 27
- 0
IPA.Loader/Utilities/SteamCheck.cs View File

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

+ 5
- 0
IPA.Loader/packages.config View File

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

+ 1
- 1
IPA/IPA.csproj View File

@ -96,7 +96,7 @@
<Target Name="AfterBuild">
<Message Text="Packing..." Importance="normal" />
<ItemGroup>
<Dlls Include="$(SolutionDir)IllusionInjector\$(OutDir)**\*" />
<Dlls Include="$(SolutionDir)IPA.Injector\$(OutDir)**\*" />
</ItemGroup>
<Copy SourceFiles="@(Dlls)" DestinationFolder="$(OutputPath)IPA\%(RecursiveDir)" />
</Target>


+ 8
- 7
IPA/Patcher/Patcher.cs View File

@ -51,8 +51,9 @@ namespace IPA.Patcher
{
var IIdata = new PatchData { IsPatched = false, Version = null };
foreach (var @ref in _Module.AssemblyReferences) {
if (@ref.Name == "IllusionInjector") IIdata = new PatchData { IsPatched = true, Version = new Version(0,0,0,0) };
if (@ref.Name == "IllusionPlugin") return new PatchData { IsPatched = true, Version = @ref.Version };
if (@ref.Name == "IllusionInjector") IIdata = new PatchData { IsPatched = true, Version = new Version(0, 0, 0, 0) };
if (@ref.Name == "IllusionPlugin") IIdata = new PatchData { IsPatched = true, Version = new Version(0, 0, 0, 0) };
if (@ref.Name == "IPA.Injector") return new PatchData { IsPatched = true, Version = @ref.Version };
}
return IIdata;
}
@ -61,9 +62,8 @@ namespace IPA.Patcher
public void Patch(Version v)
{
// First, let's add the reference
var nameReference = new AssemblyNameReference("IllusionInjector", new Version(1,0,0,0));
var versionNameReference = new AssemblyNameReference("IllusionPlugin", v);
var injectorPath = Path.Combine(_File.DirectoryName, "IllusionInjector.dll");
var nameReference = new AssemblyNameReference("IPA.Injector", Program.Version);
var injectorPath = Path.Combine(_File.DirectoryName, "IPA.Injector.dll");
var injector = ModuleDefinition.ReadModule(injectorPath);
for (int i = 0; i < _Module.AssemblyReferences.Count; i++)
@ -72,10 +72,11 @@ namespace IPA.Patcher
_Module.AssemblyReferences.RemoveAt(i--);
if (_Module.AssemblyReferences[i].Name == "IllusionPlugin")
_Module.AssemblyReferences.RemoveAt(i--);
if (_Module.AssemblyReferences[i].Name == "IPA.Injector")
_Module.AssemblyReferences.RemoveAt(i--);
}
_Module.AssemblyReferences.Add(nameReference);
_Module.AssemblyReferences.Add(versionNameReference);
int patched = 0;
foreach(var type in FindEntryTypes())
@ -100,7 +101,7 @@ namespace IPA.Patcher
var targetMethod = targetType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
if (targetMethod != null)
{
var methodReference = _Module.Import(injector.GetType("IllusionInjector.Injector").Methods.First(m => m.Name == "Inject"));
var methodReference = _Module.Import(injector.GetType("IPA.Injector.Injector").Methods.First(m => m.Name == "Inject"));
targetMethod.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, methodReference));
return true;
}


+ 2
- 2
IPA/Program.cs View File

@ -19,7 +19,7 @@ namespace IPA {
Unknown
}
private static Version Version => Assembly.GetEntryAssembly().GetName().Version;
public static Version Version => Assembly.GetEntryAssembly().GetName().Version;
public static ArgumentFlag ArgHelp = new ArgumentFlag("--help", "-h") { DocString = "prints this message" };
public static ArgumentFlag ArgWaitFor = new ArgumentFlag("--waitfor", "-w") { DocString = "waits for the specified PID to exit", ValueString = "PID" };
@ -110,7 +110,7 @@ namespace IPA {
#region Patch Version Check
var patchedModule = PatchedModule.Load(context.EngineFile);
var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) > 0;
var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) >= 0;
Console.WriteLine($"Current: {Version} Patched: {patchedModule.Data.Version}");
if (isCurrentNewer) {
Console.ForegroundColor = ConsoleColor.White;


+ 2
- 2
IPA/Properties/AssemblyInfo.cs View File

@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// 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.8.9.*")]
[assembly: AssemblyFileVersion("3.8.9")]
[assembly: AssemblyVersion("3.9.0")]
[assembly: AssemblyFileVersion("3.9.0")]

+ 1
- 1
IllusionInjector/Updating/SelfPlugin.cs View File

@ -12,7 +12,7 @@ namespace IllusionInjector.Updating
internal class SelfPlugin : IBeatSaberPlugin
{
internal const string IPA_Name = "Beat Saber IPA";
internal const string IPA_Version = "3.8.9";
internal const string IPA_Version = "3.9.0";
public string Name => IPA_Name;


Loading…
Cancel
Save