Browse Source

Merged in 3.12.0

pull/11/head 3.12.0
Anairkoen Schno 5 years ago
committed by GitHub
parent
commit
befdaf8479
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1994 additions and 1576 deletions
  1. +0
    -1
      .gitignore
  2. +3
    -0
      .gitmodules
  3. +32
    -30
      BSIPA.sln
  4. +12
    -0
      BSIPA.sln.DotSettings
  5. +1
    -0
      BuildTools
  6. +0
    -6
      CollectDependencies/App.config
  7. +0
    -74
      CollectDependencies/CollectDependencies.csproj
  8. +0
    -135
      CollectDependencies/Program.cs
  9. +0
    -35
      CollectDependencies/Properties/AssemblyInfo.cs
  10. +0
    -111
      CollectDependencies/Virtualizer.cs
  11. +0
    -4
      CollectDependencies/packages.config
  12. +1
    -1
      Doorstop
  13. +2
    -3
      IPA.Injector/Backups/BackupUnit.cs
  14. +2
    -7
      IPA.Injector/IPA.Injector.csproj
  15. +163
    -114
      IPA.Injector/Injector.cs
  16. +1
    -4
      IPA.Injector/PostBuild.msbuild
  17. +2
    -2
      IPA.Injector/Properties/AssemblyInfo.cs
  18. +2
    -2
      IPA.Injector/Updates.cs
  19. +255
    -0
      IPA.Loader/Config/Config.cs
  20. +24
    -10
      IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs
  21. +5
    -1
      IPA.Loader/Config/IConfigProvider.cs
  22. +2
    -21
      IPA.Loader/Config/ModPrefs.cs
  23. +48
    -0
      IPA.Loader/Config/SelfConfig.cs
  24. +22
    -8
      IPA.Loader/IPA.Loader.csproj
  25. +25
    -0
      IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs
  26. +4
    -4
      IPA.Loader/JsonConverters/SemverRangeConverter.cs
  27. +4
    -4
      IPA.Loader/JsonConverters/SemverVersionConverter.cs
  28. +3
    -3
      IPA.Loader/Loader/Composite/CompositeBSPlugin.cs
  29. +2
    -2
      IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs
  30. +27
    -0
      IPA.Loader/Loader/Features/AddInFeature.cs
  31. +68
    -0
      IPA.Loader/Loader/Features/DefineFeature.cs
  32. +197
    -0
      IPA.Loader/Loader/Features/Feature.cs
  33. +12
    -0
      IPA.Loader/Loader/Features/NoUpdateFeature.cs
  34. +32
    -0
      IPA.Loader/Loader/Features/PrintFeature.cs
  35. +45
    -18
      IPA.Loader/Loader/LibLoader.cs
  36. +9
    -52
      IPA.Loader/Loader/PluginComponent.cs
  37. +89
    -0
      IPA.Loader/Loader/PluginInitInjector.cs
  38. +434
    -0
      IPA.Loader/Loader/PluginLoader.cs
  39. +32
    -102
      IPA.Loader/Loader/PluginManager.cs
  40. +43
    -0
      IPA.Loader/Loader/PluginManifest.cs
  41. +17
    -0
      IPA.Loader/Loader/manifest.json
  42. +6
    -1
      IPA.Loader/Logging/LogPrinter.cs
  43. +40
    -7
      IPA.Loader/Logging/Logger.cs
  44. +5
    -5
      IPA.Loader/Logging/Printers/GZFilePrinter.cs
  45. +1
    -1
      IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs
  46. +2
    -2
      IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs
  47. +1
    -1
      IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs
  48. +154
    -38
      IPA.Loader/Logging/StandardLogger.cs
  49. +4
    -1
      IPA.Loader/Logging/UnityLogProvider.cs
  50. +4
    -0
      IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs
  51. +0
    -25
      IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs
  52. +5
    -6
      IPA.Loader/Updating/ModSaber/ApiEndpoint.cs
  53. +28
    -30
      IPA.Loader/Updating/ModSaber/Updater.cs
  54. +8
    -8
      IPA.Loader/Updating/SelfPlugin.cs
  55. +8
    -0
      IPA.Loader/Utilities/BeatSaber.cs
  56. +7
    -0
      IPA.Loader/Utilities/Extensions.cs
  57. +32
    -1
      IPA.Loader/Utilities/Ref.cs
  58. +1
    -1
      IPA.Loader/Utilities/Utils.cs
  59. +1
    -3
      IPA/IPA.csproj
  60. +0
    -134
      IPA/Patcher/Patcher.cs
  61. +0
    -116
      IPA/Patcher/Virtualizer.cs
  62. +28
    -143
      IPA/Program.cs
  63. +8
    -8
      IPA/Properties/AssemblyInfo.cs
  64. +1
    -1
      IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache
  65. BIN
      Libs/Mono.Debugger.Soft.dll
  66. +0
    -73
      MSBuildTasks/AssemblyRenameTask.cs
  67. +0
    -84
      MSBuildTasks/MSBuildTasks.csproj
  68. +0
    -79
      MSBuildTasks/Pdb2Mdb.cs
  69. +0
    -35
      MSBuildTasks/Properties/AssemblyInfo.cs
  70. +0
    -9
      MSBuildTasks/packages.config
  71. +28
    -8
      README.md
  72. BIN
      Refs/UnityEngine.CoreModule.dll
  73. BIN
      Refs/UnityEngine.UnityWebRequestModule.dll
  74. +1
    -1
      Refs/refs.txt
  75. +1
    -1
      appveyor.yml

+ 0
- 1
.gitignore View File

@ -250,6 +250,5 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
/MigrationBackup/d2a2abe6/IPA.Injector
/bsinstalldir.txt
/.wiki

+ 3
- 0
.gitmodules View File

@ -1,3 +1,6 @@
[submodule "Doorstop"]
path = Doorstop
url = https://github.com/nike4613/UnityDoorstop-BSIPA
[submodule "BuildTools"]
path = BuildTools
url = https://github.com/nike4613/BS-Plugin-BuildTools.git

+ 32
- 30
BSIPA.sln View File

@ -5,11 +5,11 @@ VisualStudioVersion = 15.0.27428.2043
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA", "IPA\IPA.csproj", "{14092533-98BB-40A4-9AFC-27BB75672A70}"
ProjectSection(ProjectDependencies) = postProject
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F} = {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A} = {E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}
{2A1AF16B-27F1-46E0-9A95-181516BC1CB7} = {2A1AF16B-27F1-46E0-9A95-181516BC1CB7}
EndProjectSection
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}"
ProjectSection(ProjectDependencies) = postProject
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F} = {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}
@ -19,7 +19,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IPA.Injector", "IPA.Injecto
ProjectSection(ProjectDependencies) = postProject
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F} = {5F33B310-DC8D-4C0D-877E-BAC3908DE10F}
{88609E16-731F-46C9-8139-6B1A7A83240D} = {88609E16-731F-46C9-8139-6B1A7A83240D}
{F08C3C7A-3221-432E-BAB8-32BCE58408C8} = {F08C3C7A-3221-432E-BAB8-32BCE58408C8}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "proxy", "Doorstop\Proxy\Proxy.vcxproj", "{88609E16-731F-46C9-8139-6B1A7A83240D}"
@ -29,14 +28,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{C79C2C3A
Refs\refs.txt = Refs\refs.txt
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollectDependencies", "CollectDependencies\CollectDependencies.csproj", "{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4D6639A2-BD39-4F9B-AF7F-8E5F3B88243D}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CollectDependencies", "BuildTools\CollectDependencies\CollectDependencies.csproj", "{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyRenameStep", "BuildTools\AssemblyRenameStep\AssemblyRenameStep.csproj", "{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -77,30 +78,6 @@ Global
{14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x64.Build.0 = Verbose|Any CPU
{14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x86.ActiveCfg = Release|Any CPU
{14092533-98BB-40A4-9AFC-27BB75672A70}.Verbose|x86.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|x64.ActiveCfg = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|x64.Build.0 = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|x86.ActiveCfg = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Debug|x86.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
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|x64.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|x64.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|x86.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Release|x86.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose_Release|x64.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose_Release|x64.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose_Release|x86.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose_Release|x86.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose|Any CPU.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose|Any CPU.Build.0 = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose|x64.ActiveCfg = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose|x64.Build.0 = Debug|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose|x86.ActiveCfg = Release|Any CPU
{F08C3C7A-3221-432E-BAB8-32BCE58408C8}.Verbose|x86.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}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -183,6 +160,7 @@ Global
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|x64.ActiveCfg = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|x64.Build.0 = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|x86.ActiveCfg = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose_Release|x86.Build.0 = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|Any CPU.ActiveCfg = Release|Any CPU
@ -191,13 +169,37 @@ Global
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x64.Build.0 = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x86.ActiveCfg = Release|Any CPU
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}.Verbose|x86.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Debug|x64.ActiveCfg = Debug|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Debug|x64.Build.0 = Debug|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Debug|x86.ActiveCfg = Debug|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Debug|x86.Build.0 = Debug|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Release|Any CPU.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Release|x64.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Release|x64.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Release|x86.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Release|x86.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose_Release|Any CPU.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose_Release|Any CPU.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose_Release|x64.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose_Release|x64.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose_Release|x86.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose_Release|x86.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose|Any CPU.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose|Any CPU.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose|x64.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose|x64.Build.0 = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose|x86.ActiveCfg = Release|Any CPU
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A}.Verbose|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F08C3C7A-3221-432E-BAB8-32BCE58408C8} = {C79C2C3A-A7FC-40D6-A5CC-9752A661AFA9}
{5F33B310-DC8D-4C0D-877E-BAC3908DE10F} = {C79C2C3A-A7FC-40D6-A5CC-9752A661AFA9}
{E2CCDD2F-1D4F-4B06-9CD4-E0D2B9AE543A} = {C79C2C3A-A7FC-40D6-A5CC-9752A661AFA9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C7380FAB-02D6-4A2A-B428-B4BFCFE3A054}


+ 12
- 0
BSIPA.sln.DotSettings View File

@ -1,17 +1,29 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue">WARNING</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_BINARY_EXPRESSIONS/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_FIELDS/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_METHODS/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_PROPERTIES/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BS/@EntryIndexedValue">BS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPA/@EntryIndexedValue">IPA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=VR/@EntryIndexedValue">VR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="_" Suffix="" Style="aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"&gt;&lt;ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatsaber/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Coroutine/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=deps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Modsaber/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pdbs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=plugin_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Prefs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpatch/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Virtualize/@EntryIndexedValue">True</s:Boolean>

+ 1
- 0
BuildTools

@ -0,0 +1 @@
Subproject commit de919c49496a7e7e11382bd876473018ddceaa1a

+ 0
- 6
CollectDependencies/App.config View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

+ 0
- 74
CollectDependencies/CollectDependencies.csproj View File

@ -1,74 +0,0 @@
<?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>{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>CollectDependencies</RootNamespace>
<AssemblyName>CollectDependencies</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<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' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mono.Cecil, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Mdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Mdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Pdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Pdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Rocks, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Rocks.dll</HintPath>
<Private>True</Private>
</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" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Virtualizer.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="AfterBuild">
<Exec Command="&quot;$(SolutionDir)$(AssemblyName)\$(OutputPath)$(AssemblyName).exe&quot; Refs/refs.txt" WorkingDirectory="$(SolutionDir)"></Exec>
</Target>
</Project>

+ 0
- 135
CollectDependencies/Program.cs View File

@ -1,135 +0,0 @@
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace CollectDependencies
{
static class Program
{
static void Main(string[] args)
{
var depsFile = File.ReadAllText(args[0]);
var directoryName = Path.GetDirectoryName(args[0]);
var files = new List<Tuple<string, int>>();
{ // Create files from stuff in depsfile
var stack = new Stack<string>();
void Push(string val)
{
string pre = "";
if (stack.Count > 0)
pre = stack.First();
stack.Push(pre + val);
}
string Pop() => stack.Pop();
string Replace(string val)
{
var v2 = Pop();
Push(val);
return v2;
}
var lineNo = 0;
foreach (var line in depsFile.Split(new[] { Environment.NewLine }, StringSplitOptions.None))
{
var parts = line.Split('"');
var path = parts.Last();
var level = parts.Length - 1;
if (path.StartsWith("::"))
{ // pseudo-command
parts = path.Split(' ');
var command = parts[0].Substring(2);
parts = parts.Skip(1).ToArray();
var arglist = string.Join(" ", parts);
if (command == "from")
{ // an "import" type command
path = File.ReadAllText(Path.Combine(directoryName ?? throw new InvalidOperationException(), arglist));
}
else if (command == "prompt")
{
Console.Write(arglist);
path = Console.ReadLine();
}
else
{
path = "";
Console.Error.WriteLine($"Invalid command {command}");
}
}
if (level > stack.Count - 1)
Push(path);
else if (level == stack.Count - 1)
files.Add(new Tuple<string, int>(Replace(path), lineNo));
else if (level < stack.Count - 1)
{
files.Add(new Tuple<string, int>(Pop(), lineNo));
while (level < stack.Count)
Pop();
Push(path);
}
lineNo++;
}
files.Add(new Tuple<string, int>(Pop(), lineNo));
}
foreach (var file in files)
{
try
{
var fparts = file.Item1.Split('?');
var fname = fparts[0];
if (fname == "") continue;
var outp = Path.Combine(directoryName ?? throw new InvalidOperationException(),
Path.GetFileName(fname) ?? throw new InvalidOperationException());
Console.WriteLine($"Copying \"{fname}\" to \"{outp}\"");
if (File.Exists(outp)) File.Delete(outp);
if (Path.GetExtension(fname)?.ToLower() == ".dll")
{
// ReSharper disable once StringLiteralTypo
if (fparts.Length > 1 && fparts[1] == "virt")
{
var module = VirtualizedModule.Load(fname);
module.Virtualize(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName(),
Path.GetFileName(fname) ?? throw new InvalidOperationException()));
}
var modl = ModuleDefinition.ReadModule(fparts[0]);
foreach (var t in modl.Types)
{
foreach (var m in t.Methods)
{
if (m.Body != null)
{
m.Body.Instructions.Clear();
m.Body.InitLocals = false;
m.Body.Variables.Clear();
}
}
}
modl.Write(outp);
}
else
{
File.Copy(fname, outp);
}
}
catch (Exception e)
{
Console.WriteLine($"{Path.Combine(Environment.CurrentDirectory, args[0])}({file.Item2}): error: {e}");
}
}
}
}
}

+ 0
- 35
CollectDependencies/Properties/AssemblyInfo.cs View File

@ -1,35 +0,0 @@
using System.Reflection;
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("CollectDependencies")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("CollectDependencies")]
[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("5f33b310-dc8d-4c0d-877e-bac3908de10f")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

+ 0
- 111
CollectDependencies/Virtualizer.cs View File

@ -1,111 +0,0 @@
using Mono.Cecil;
using System.IO;
using System.Linq;
namespace CollectDependencies
{
class VirtualizedModule
{
private readonly FileInfo _file;
private ModuleDefinition _module;
public static VirtualizedModule Load(string engineFile)
{
return new VirtualizedModule(engineFile);
}
private VirtualizedModule(string assemblyFile)
{
_file = new FileInfo(assemblyFile);
LoadModules();
}
private void LoadModules()
{
var resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(_file.DirectoryName);
var parameters = new ReaderParameters
{
AssemblyResolver = resolver,
};
_module = ModuleDefinition.ReadModule(_file.FullName, parameters);
}
/// <summary>
///
/// </summary>
/// <param name="targetFile"></param>
public void Virtualize(string targetFile)
{
foreach (var type in _module.Types)
{
VirtualizeType(type);
}
_module.Write(targetFile);
}
private void VirtualizeType(TypeDefinition type)
{
if(type.IsSealed)
{
// Unseal
type.IsSealed = false;
}
if (type.IsInterface) return;
if (type.IsAbstract) return;
// These two don't seem to work.
if (type.Name == "SceneControl" || type.Name == "ConfigUI") return;
// Take care of sub types
foreach (var subType in type.NestedTypes)
{
VirtualizeType(subType);
}
foreach (var method in type.Methods)
{
if (method.IsManaged
&& method.IsIL
&& !method.IsStatic
&& !method.IsVirtual
&& !method.IsAbstract
&& !method.IsAddOn
&& !method.IsConstructor
&& !method.IsSpecialName
&& !method.IsGenericInstance
&& !method.HasOverrides)
{
method.IsVirtual = true;
method.IsPublic = true;
method.IsPrivate = false;
method.IsNewSlot = true;
method.IsHideBySig = true;
}
}
foreach (var field in type.Fields)
{
if (field.IsPrivate) field.IsFamily = true;
}
}
public bool IsVirtualized
{
get
{
var awakeMethods = _module.GetTypes().SelectMany(t => t.Methods.Where(m => m.Name == "Awake"));
var methodDefinitions = awakeMethods as MethodDefinition[] ?? awakeMethods.ToArray();
if (!methodDefinitions.Any()) return false;
return ((float)methodDefinitions.Count(m => m.IsVirtual) / methodDefinitions.Count()) > 0.5f;
}
}
}
}

+ 0
- 4
CollectDependencies/packages.config View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Mono.Cecil" version="0.10.1" targetFramework="net472" />
</packages>

+ 1
- 1
Doorstop

@ -1 +1 @@
Subproject commit 0a350fe8a9792cd6708f5d5e805d82ec115c5e7d
Subproject commit 310ab026a8905588dab29f250a2385ed2cb7c41f

+ 2
- 3
IPA.Injector/Backups/BackupUnit.cs View File

@ -1,5 +1,4 @@
using IPA.Utilities;
using System;
using System;
using System.Collections.Generic;
using System.IO;
@ -68,7 +67,7 @@ namespace IPA.Injector.Backups
/// <param name="file"></param>
public void Add(FileInfo file)
{
var relativePath = LoneFunctions.GetRelativePath(file.FullName, Environment.CurrentDirectory);
var relativePath = Utilities.Utils.GetRelativePath(file.FullName, Environment.CurrentDirectory);
var backupPath = new FileInfo(Path.Combine(_backupPath.FullName, relativePath));
// Copy over


+ 2
- 7
IPA.Injector/IPA.Injector.csproj View File

@ -13,10 +13,10 @@
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<PathMap>$(SolutionDir)=C:\</PathMap>
<DebugType>portable</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
@ -24,8 +24,8 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
@ -57,7 +57,6 @@
<Compile Include="Bootstrapper.cs" />
<Compile Include="ConsoleWindow.cs" />
<Compile Include="Injector.cs" />
<Compile Include="LibLoader.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Updates.cs" />
<Compile Include="Virtualizer.cs" />
@ -83,7 +82,6 @@
</Content>
<Content Include="..\Libs\Microsoft.CSharp.dll">
<Link>Libraries\Mono\Microsoft.CSharp.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\Libs\System.Runtime.Serialization.dll">
<Link>Libraries\Mono\System.Runtime.Serialization.dll</Link>
@ -91,9 +89,6 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mono.Unofficial.pdb2mdb">
<Version>4.2.3.4</Version>
</PackageReference>
<PackageReference Include="SemanticVersioning">
<Version>1.2.0</Version>
</PackageReference>


+ 163
- 114
IPA.Injector/Injector.cs View File

@ -1,28 +1,32 @@
using IPA.Injector.Backups;
using IPA.Config;
using IPA.Injector.Backups;
using IPA.Loader;
using IPA.Logging;
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using UnityEngine;
using static IPA.Logging.Logger;
using MethodAttributes = Mono.Cecil.MethodAttributes;
namespace IPA.Injector
{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
// ReSharper disable once UnusedMember.Global
public static class Injector
{
private static Task pluginAsyncLoadTask;
// ReSharper disable once UnusedParameter.Global
public static void Main(string[] args)
{ // entry point for doorstop
// At this point, literally nothing but mscorlib is loaded,
// and since this class doesn't have any static fields that
// aren't defined in mscorlib, we can control exactly what
// and since this class doesn't have any static fields that
// aren't defined in mscorlib, we can control exactly what
// gets loaded.
try
@ -32,11 +36,58 @@ namespace IPA.Injector
SetupLibraryLoading();
EnsureUserData();
// this is weird, but it prevents Mono from having issues loading the type.
// IMPORTANT: NO CALLS TO ANY LOGGER CAN HAPPEN BEFORE THIS
var unused = StandardLogger.PrintFilter;
#region // Above hack explaination
/*
* Due to an unknown bug in the version of Mono that Unity 2018.1.8 uses, if the first access to StandardLogger
* is a call to a constructor, then Mono fails to load the type correctly. However, if the first access is to
* the above static property (or maybe any, but I don't really know) it behaves as expected and works fine.
*/
#endregion
log.Debug("Initializing logger");
SelfConfig.Set();
loader.Debug("Prepping bootstrapper");
InstallBootstrapPatch();
// The whole mess that follows is an attempt to work around Mono failing to
// call the library load routine for Mono.Cecil when the debugger is attached.
bool runProperly = false;
while (!runProperly) // retry until it finishes, or errors
try // TODO: fix this mess
{ // currently it gets stuck in an infinite loop because Mono refuses
// to use Mono.Cecil even if it is loaded when the debugger is attached.
InstallBootstrapPatch();
runProperly = true;
}
catch (FileNotFoundException e)
{
var asmName = e.FileName;
AssemblyName name;
try
{ // try to parse as an AssemblyName, if it isn't, rethrow the outer exception
name = new AssemblyName(asmName);
}
catch (Exception)
{
ExceptionDispatchInfo.Capture(e).Throw();
throw;
}
// name is failed lookup, try to manually load it
LibLoader.AssemblyLibLoader(null,
new ResolveEventArgs(name.FullName, Assembly.GetExecutingAssembly()));
}
Updates.InstallPendingUpdates();
pluginAsyncLoadTask = PluginLoader.LoadTask();
}
catch (Exception e)
{
@ -44,168 +95,166 @@ namespace IPA.Injector
}
}
private static void EnsureUserData()
{
string path;
if (!Directory.Exists(path = Path.Combine(Environment.CurrentDirectory, "UserData")))
Directory.CreateDirectory(path);
}
private static void SetupLibraryLoading()
{
if (loadingDone) return;
loadingDone = true;
AppDomain.CurrentDomain.AssemblyResolve += LibLoader.AssemblyLibLoader;
}
private static void InstallBootstrapPatch()
{
var cAsmName = Assembly.GetExecutingAssembly().GetName();
loader.Debug("Finding backup");
var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA","Backups","Beat Saber");
var backupPath = Path.Combine(Environment.CurrentDirectory, "IPA", "Backups", "Beat Saber");
var bkp = BackupManager.FindLatestBackup(backupPath);
if (bkp == null)
loader.Warn("No backup found! Was BSIPA installed using the installer?");
loader.Debug("Ensuring patch on UnityEngine.CoreModule exists");
#region Insert patch into UnityEngine.CoreModule.dll
var unityPath = Path.Combine(Environment.CurrentDirectory, "Beat Saber_Data", "Managed", "UnityEngine.CoreModule.dll");
var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
var unityModDef = unityAsmDef.MainModule;
bool modified = false;
foreach (var asmref in unityModDef.AssemblyReferences)
{
if (asmref.Name == cAsmName.Name)
var unityPath = Path.Combine(Environment.CurrentDirectory, "Beat Saber_Data", "Managed",
"UnityEngine.CoreModule.dll");
var unityAsmDef = AssemblyDefinition.ReadAssembly(unityPath, new ReaderParameters
{
ReadWrite = false,
InMemory = true,
ReadingMode = ReadingMode.Immediate
});
var unityModDef = unityAsmDef.MainModule;
bool modified = false;
foreach (var asmref in unityModDef.AssemblyReferences)
{
if (asmref.Version != cAsmName.Version)
if (asmref.Name == cAsmName.Name)
{
asmref.Version = cAsmName.Version;
modified = true;
if (asmref.Version != cAsmName.Version)
{
asmref.Version = cAsmName.Version;
modified = true;
}
}
}
}
var application = unityModDef.GetType("UnityEngine", "Application");
var application = unityModDef.GetType("UnityEngine", "Application");
MethodDefinition cctor = null;
foreach (var m in application.Methods)
if (m.IsRuntimeSpecialName && m.Name == ".cctor")
cctor = m;
MethodDefinition cctor = null;
foreach (var m in application.Methods)
if (m.IsRuntimeSpecialName && m.Name == ".cctor")
cctor = m;
var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method);
var cbs = unityModDef.ImportReference(((Action)CreateBootstrapper).Method);
if (cctor == null)
{
cctor = new MethodDefinition(".cctor", MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName, unityModDef.TypeSystem.Void);
application.Methods.Add(cctor);
modified = true;
if (cctor == null)
{
cctor = new MethodDefinition(".cctor",
MethodAttributes.RTSpecialName | MethodAttributes.Static | MethodAttributes.SpecialName,
unityModDef.TypeSystem.Void);
application.Methods.Add(cctor);
modified = true;
var ilp = cctor.Body.GetILProcessor();
ilp.Emit(OpCodes.Call, cbs);
ilp.Emit(OpCodes.Ret);
}
else
{
var ilp = cctor.Body.GetILProcessor();
for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++)
var ilp = cctor.Body.GetILProcessor();
ilp.Emit(OpCodes.Call, cbs);
ilp.Emit(OpCodes.Ret);
}
else
{
var ins = cctor.Body.Instructions[i];
switch (i)
var ilp = cctor.Body.GetILProcessor();
for (var i = 0; i < Math.Min(2, cctor.Body.Instructions.Count); i++)
{
case 0 when ins.OpCode != OpCodes.Call:
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
break;
case 0:
var ins = cctor.Body.Instructions[i];
switch (i)
{
var methodRef = ins.Operand as MethodReference;
if (methodRef?.FullName != cbs.FullName)
{
case 0 when ins.OpCode != OpCodes.Call:
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
}
break;
case 0:
{
var methodRef = ins.Operand as MethodReference;
if (methodRef?.FullName != cbs.FullName)
{
ilp.Replace(ins, ilp.Create(OpCodes.Call, cbs));
modified = true;
}
break;
break;
}
case 1 when ins.OpCode != OpCodes.Ret:
ilp.Replace(ins, ilp.Create(OpCodes.Ret));
modified = true;
break;
}
case 1 when ins.OpCode != OpCodes.Ret:
ilp.Replace(ins, ilp.Create(OpCodes.Ret));
modified = true;
break;
}
}
}
if (modified)
{
bkp?.Add(unityPath);
unityAsmDef.Write(unityPath);
if (modified)
{
bkp?.Add(unityPath);
unityAsmDef.Write(unityPath);
}
}
#endregion
#endregion Insert patch into UnityEngine.CoreModule.dll
loader.Debug("Ensuring Assembly-CSharp is virtualized");
#region Virtualize Assembly-CSharp.dll
var ascPath = Path.Combine(Environment.CurrentDirectory, "Beat Saber_Data", "Managed", "Assembly-CSharp.dll");
var ascModule = VirtualizedModule.Load(ascPath);
ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath));
#endregion
{
var ascPath = Path.Combine(Environment.CurrentDirectory, "Beat Saber_Data", "Managed",
"Assembly-CSharp.dll");
var ascModule = VirtualizedModule.Load(ascPath);
ascModule.Virtualize(cAsmName, () => bkp?.Add(ascPath));
}
#endregion Virtualize Assembly-CSharp.dll
}
private static bool _bootstrapped;
private static bool bootstrapped;
private static void CreateBootstrapper()
{
if (_bootstrapped) return;
_bootstrapped = true;
if (bootstrapped) return;
bootstrapped = true;
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()}");
var level = UnityLogRedirector.LogTypeToLevel(type);
UnityLogProvider.UnityLogger.Log(level, $"{condition.Trim()}");
UnityLogProvider.UnityLogger.Log(level, $"{stackTrace.Trim()}");
};
// need to reinit streams singe Unity seems to redirect stdout
WinConsole.InitializeStreams();
var bootstrapper = new GameObject("NonDestructiveBootstrapper").AddComponent<Bootstrapper>();
bootstrapper.Destroyed += Bootstrapper_Destroyed;
}
private static bool _injected;
public static void Inject()
{
if (!_injected)
{
_injected = true;
WinConsole.Initialize();
SetupLibraryLoading();
var bootstrapper = new GameObject("Bootstrapper").AddComponent<Bootstrapper>();
bootstrapper.Destroyed += Bootstrapper_Destroyed;
}
}
private static bool _loadingDone;
private static void SetupLibraryLoading()
{
if (_loadingDone) return;
_loadingDone = true;
#region Add Library load locations
AppDomain.CurrentDomain.AssemblyResolve += LibLoader.AssemblyLibLoader;
/*try
{
if (!SetDllDirectory(LibLoader.NativeDir))
{
libLoader.Warn("Unable to add native library path to load path");
}
}
catch (Exception) { }*/
#endregion
}
/*
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetDllDirectory(string lpPathName);
*/
private static bool loadingDone;
private static void Bootstrapper_Destroyed()
{
// wait for plugins to finish loading
pluginAsyncLoadTask.Wait();
log.Debug("Plugins loaded");
log.Debug(string.Join(", ", PluginLoader.PluginsMetadata));
PluginComponent.Create();
}
}
}
}

+ 1
- 4
IPA.Injector/PostBuild.msbuild View File

@ -5,8 +5,7 @@
<SolDir></SolDir>
</PropertyGroup>
<UsingTask TaskName="AssemblyRename" AssemblyFile="$(SolDir)MSBuildTasks\bin\$(Configuration)\MSBuildTasks.dll" />
<UsingTask TaskName="PdbToMdb" AssemblyFile="$(SolDir)MSBuildTasks\bin\$(Configuration)\MSBuildTasks.dll" />
<UsingTask TaskName="AssemblyRename" AssemblyFile="$(SolDir)BuildTools\AssemblyRenameStep\bin\$(Configuration)\AssemblyRenameStep.dll" />
<Target Name="PostBuild">
<Message Text="Relocating" Importance="normal" />
@ -30,9 +29,7 @@
<ItemGroup>
<ToRename Include="$(OPath)Libs\**\*.dll" />
<ToMdb Include="$(OPath)**\*.pdb" />
</ItemGroup>
<AssemblyRename Assemblies="@(ToRename)" />
<PdbToMdb Binaries="@(ToMdb)" />
</Target>
</Project>

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

@ -33,5 +33,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.11.6")]
[assembly: AssemblyFileVersion("3.11.6")]
[assembly: AssemblyVersion("3.12.0")]
[assembly: AssemblyFileVersion("3.12.0")]

+ 2
- 2
IPA.Injector/Updates.cs View File

@ -73,7 +73,7 @@ namespace IPA.Injector
{
try
{
if (!LoneFunctions.GetRelativePath(file, path).Split(Path.PathSeparator).Contains("Pending"))
if (!Utils.GetRelativePath(file, path).Split(Path.PathSeparator).Contains("Pending"))
File.Delete(file);
}
catch (FileNotFoundException e)
@ -97,7 +97,7 @@ namespace IPA.Injector
try
{
LoneFunctions.CopyAll(new DirectoryInfo(pendingDir), new DirectoryInfo(BeatSaber.InstallPath));
Utils.CopyAll(new DirectoryInfo(pendingDir), new DirectoryInfo(BeatSaber.InstallPath));
}
catch (Exception e)
{


+ 255
- 0
IPA.Loader/Config/Config.cs View File

@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using IPA.Config.ConfigProviders;
using IPA.Utilities;
namespace IPA.Config
{
/// <summary>
/// A class to handle updating ConfigProviders automatically
/// </summary>
public static class Config
{
static Config()
{
JsonConfigProvider.RegisterConfig();
}
/// <inheritdoc />
/// <summary>
/// Defines the type of the <see cref="T:IPA.Config.IConfigProvider" />
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class TypeAttribute : Attribute
{
/// <summary>
/// The extension associated with this type, without the '.'
/// </summary>
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string Extension { get; private set; }
/// <inheritdoc />
/// <summary>
/// Constructs the attribute with a specified extension.
/// </summary>
/// <param name="ext">the extension associated with this type, without the '.'</param>
public TypeAttribute(string ext)
{
Extension = ext;
}
}
/// <inheritdoc />
/// <summary>
/// Specifies that a particular parameter is preferred to be a specific type of <see cref="T:IPA.Config.IConfigProvider" />. If it is not available, also specifies backups. If none are available, the default is used.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class PreferAttribute : Attribute
{
/// <summary>
/// The order of preference for the config type.
/// </summary>
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string[] PreferenceOrder { get; private set; }
/// <inheritdoc />
/// <summary>
/// Constructs the attribute with a specific preference list. Each entry is the extension without a '.'
/// </summary>
/// <param name="preference">The preferences in order of preference.</param>
public PreferAttribute(params string[] preference)
{
PreferenceOrder = preference;
}
}
/// <inheritdoc />
/// <summary>
/// Specifies a preferred config name, instead of using the plugin's name.
/// </summary>
public class NameAttribute : Attribute
{
/// <summary>
/// The name to use for the config.
/// </summary>
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public string Name { get; private set; }
/// <inheritdoc />
/// <summary>
/// Constructs the attribute with a specific name.
/// </summary>
/// <param name="name">the name to use for the config.</param>
public NameAttribute(string name)
{
Name = name;
}
}
private static readonly Dictionary<string, Type> registeredProviders = new Dictionary<string, Type>();
/// <summary>
/// Registers a <see cref="IConfigProvider"/> to use for configs.
/// </summary>
/// <typeparam name="T">the type to register</typeparam>
public static void Register<T>() where T : IConfigProvider => Register(typeof(T));
/// <summary>
/// Registers a <see cref="IConfigProvider"/> to use for configs.
/// </summary>
/// <param name="type">the type to register</param>
public static void Register(Type type)
{
if (!(type.GetCustomAttribute(typeof(TypeAttribute)) is TypeAttribute ext))
throw new InvalidOperationException("Type does not have TypeAttribute");
if (!typeof(IConfigProvider).IsAssignableFrom(type))
throw new InvalidOperationException("Type not IConfigProvider");
if (registeredProviders.ContainsKey(ext.Extension))
throw new InvalidOperationException($"Extension provider for {ext.Extension} already exists");
registeredProviders.Add(ext.Extension, type);
}
private static SortedList<Ref<DateTime>, IConfigProvider> configProviders = new SortedList<Ref<DateTime>, IConfigProvider>();
/// <summary>
/// Gets an <see cref="IConfigProvider"/> using the specified list pf preferred config types.
/// </summary>
/// <param name="configName">the name of the mod for this config</param>
/// <param name="extensions">the preferred config types to try to get</param>
/// <returns>an <see cref="IConfigProvider"/> of the requested type, or of type JSON.</returns>
public static IConfigProvider GetProviderFor(string configName, params string[] extensions)
{
var chosenExt = extensions.FirstOrDefault(s => registeredProviders.ContainsKey(s)) ?? "json";
var type = registeredProviders[chosenExt];
var provider = Activator.CreateInstance(type) as IConfigProvider;
if (provider != null)
{
provider.Filename = Path.Combine(BeatSaber.UserDataPath, configName);
configProviders.Add(provider.LastModified, provider);
}
return provider;
}
internal static IConfigProvider GetProviderFor(string modName, ParameterInfo info)
{
var prefs = new string[0];
if (info.GetCustomAttribute<PreferAttribute>() is PreferAttribute prefer)
prefs = prefer.PreferenceOrder;
if (info.GetCustomAttribute<NameAttribute>() is NameAttribute name)
modName = name.Name;
return GetProviderFor(modName, prefs);
}
private static Dictionary<IConfigProvider, Action> linkedProviders =
new Dictionary<IConfigProvider, Action>();
/// <summary>
/// Creates a linked <see cref="Ref{T}"/> for the config provider. This <see cref="Ref{T}"/> will be automatically updated whenever the file on-disk changes.
/// </summary>
/// <typeparam name="T">the type of the parsed value</typeparam>
/// <param name="config">the <see cref="IConfigProvider"/> to create a link to</param>
/// <param name="onChange">an action to perform on value change</param>
/// <returns>a <see cref="Ref{T}"/> to an ever-changing value, mirroring whatever the file contains.</returns>
public static Ref<T> MakeLink<T>(this IConfigProvider config, Action<IConfigProvider, Ref<T>> onChange = null)
{
Ref<T> @ref = config.Parse<T>();
void ChangeDelegate()
{
@ref.Value = config.Parse<T>();
onChange?.Invoke(config, @ref);
}
if (linkedProviders.ContainsKey(config))
linkedProviders[config] = (Action) Delegate.Combine(linkedProviders[config], (Action) ChangeDelegate);
else
linkedProviders.Add(config, ChangeDelegate);
ChangeDelegate();
return @ref;
}
/// <summary>
/// Removes all linked <see cref="Ref{T}"/> such that they are no longer updated.
/// </summary>
/// <param name="config">the <see cref="IConfigProvider"/> to unlink</param>
public static void RemoveLinks(this IConfigProvider config)
{
if (linkedProviders.ContainsKey(config))
linkedProviders.Remove(config);
}
internal static void Update()
{
foreach (var provider in configProviders)
{
if (provider.Value.LastModified > provider.Key.Value)
{
try
{
provider.Value.Load(); // auto reload if it changes
provider.Key.Value = provider.Value.LastModified;
}
catch (Exception e)
{
Logging.Logger.config.Error("Error when trying to load config");
Logging.Logger.config.Error(e);
}
}
if (provider.Value.HasChanged)
{
try
{
provider.Value.Save();
provider.Key.Value = DateTime.Now;
}
catch (Exception e)
{
Logging.Logger.config.Error("Error when trying to save config");
Logging.Logger.config.Error(e);
}
}
if (provider.Value.InMemoryChanged)
{
provider.Value.InMemoryChanged = false;
try
{
if (linkedProviders.ContainsKey(provider.Value))
linkedProviders[provider.Value]();
}
catch (Exception e)
{
Logging.Logger.config.Error("Error running link change events");
Logging.Logger.config.Error(e);
}
}
}
}
internal static void Save()
{
foreach (var provider in configProviders)
if (provider.Value.HasChanged)
try
{
provider.Value.Save();
}
catch (Exception e)
{
Logging.Logger.config.Error("Error when trying to save config");
Logging.Logger.config.Error(e);
}
}
}
}

+ 24
- 10
IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs View File

@ -1,25 +1,33 @@
using System;
using IPA.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using IPA.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace IPA.Config.ConfigProviders
{
[Config.Type("json")]
internal class JsonConfigProvider : IConfigProvider
{
public static void RegisterConfig()
{
Config.Register<JsonConfigProvider>();
}
private JObject jsonObj;
// TODO: create a wrapper that allows empty object creation
public dynamic Dynamic => jsonObj;
public bool HasChanged { get; private set; }
public bool InMemoryChanged { get; set; }
public DateTime LastModified => File.GetLastWriteTime(Filename + ".json");
private string _filename;
public string Filename
{
get => _filename;
@ -38,7 +46,7 @@ namespace IPA.Config.ConfigProviders
var fileInfo = new FileInfo(Filename + ".json");
if (fileInfo.Exists)
{
var json = fileInfo.OpenText().ReadToEnd();
string json = File.ReadAllText(fileInfo.FullName);
try
{
jsonObj = JObject.Parse(json);
@ -57,6 +65,7 @@ namespace IPA.Config.ConfigProviders
}
SetupListeners();
InMemoryChanged = true;
}
private void SetupListeners()
@ -69,30 +78,34 @@ namespace IPA.Config.ConfigProviders
private void JsonObj_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
HasChanged = true;
InMemoryChanged = true;
}
private void JsonObj_ListChanged(object sender, ListChangedEventArgs e)
{
HasChanged = true;
InMemoryChanged = true;
}
private void JsonObj_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
HasChanged = true;
InMemoryChanged = true;
}
public T Parse<T>()
{
if (jsonObj == null)
return default(T);
return jsonObj.ToObject<T>();
}
public void Save()
{
Logger.config.Debug($"Saving file {Filename}.json");
var fileInfo = new FileInfo(Filename + ".json");
File.WriteAllText(fileInfo.FullName, JsonConvert.SerializeObject(jsonObj, Formatting.Indented));
if (!Directory.Exists(Path.GetDirectoryName(Filename)))
Directory.CreateDirectory(Path.GetDirectoryName(Filename) ?? throw new InvalidOperationException());
File.WriteAllText(Filename + ".json", JsonConvert.SerializeObject(jsonObj, Formatting.Indented));
HasChanged = false;
}
@ -102,6 +115,7 @@ namespace IPA.Config.ConfigProviders
jsonObj = JObject.FromObject(obj);
SetupListeners();
HasChanged = true;
InMemoryChanged = true;
}
}
}
}

+ 5
- 1
IPA.Loader/Config/IConfigProvider.cs View File

@ -32,7 +32,11 @@ namespace IPA.Config
/// </summary>
bool HasChanged { get; }
/// <summary>
/// Will be set with the filename (no extension) to save to. When saving, the implimentation should add the appropriate extension. Should error if set multiple times.
/// Returns <see langword="true"/> if the data in memory has been changed - notably including loads.
/// </summary>
bool InMemoryChanged { get; set; }
/// <summary>
/// Will be set with the filename (no extension) to save to. When saving, the implementation should add the appropriate extension. Should error if set multiple times.
/// </summary>
string Filename { set; }
/// <summary>


+ 2
- 21
IPA.Loader/Config/ModPrefs.cs View File

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using IPA.Loader;
namespace IPA.Config
{
@ -93,19 +92,15 @@ namespace IPA.Config
private static ModPrefs _staticInstance;
private static IModPrefs StaticInstance => _staticInstance ?? (_staticInstance = new ModPrefs());
// ReSharper disable once IdentifierTypo
internal static Dictionary<IBeatSaberPlugin, ModPrefs> ModPrefss { get; set; } = new Dictionary<IBeatSaberPlugin, ModPrefs>();
private readonly 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) {
public ModPrefs(PluginLoader.PluginMetadata plugin) {
_instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "ModPrefs",
$"{plugin.Name}.ini"));
ModPrefss.Add(plugin, this);
}
private ModPrefs()
@ -263,18 +258,4 @@ namespace IPA.Config
public static void SetBool(string section, string name, bool value)
=> StaticInstance.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.ModPrefss.First(o => o.Key == plugin).Value;
}
}
}

+ 48
- 0
IPA.Loader/Config/SelfConfig.cs View File

@ -0,0 +1,48 @@
using IPA.Logging;
using IPA.Utilities;
namespace IPA.Config
{
internal class SelfConfig
{
private static IConfigProvider _loaderConfig;
public static IConfigProvider LoaderConfig
{
get => _loaderConfig;
set
{
_loaderConfig?.RemoveLinks();
value.Load();
SelfConfigRef = value.MakeLink<SelfConfig>((c, v) =>
{
if (v.Value.Regenerate)
c.Store(v.Value = new SelfConfig { Regenerate = false });
StandardLogger.Configure(v.Value);
});
_loaderConfig = value;
}
}
public static Ref<SelfConfig> SelfConfigRef;
public static void Set()
{
LoaderConfig = Config.GetProviderFor(IPA_Name, "json");
}
internal const string IPA_Name = "Beat Saber IPA";
internal const string IPA_Version = "3.12.0";
public bool Regenerate = true;
public class DebugObject
{
public bool ShowCallSource = false;
public bool ShowDebug = false;
}
public DebugObject Debug = new DebugObject();
}
}

+ 22
- 8
IPA.Loader/IPA.Loader.csproj View File

@ -13,10 +13,11 @@
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<PathMap>$(SolutionDir)=C:\</PathMap>
<DebugType>portable</DebugType>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
@ -56,9 +57,20 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Config\Config.cs" />
<Compile Include="Config\ConfigProviders\JsonConfigProvider.cs" />
<Compile Include="Config\IConfigProvider.cs" />
<Compile Include="Config\SelfConfig.cs" />
<Compile Include="Loader\Composite\CompositeBSPlugin.cs" />
<Compile Include="Loader\Features\AddInFeature.cs" />
<Compile Include="Loader\Features\DefineFeature.cs" />
<Compile Include="Loader\Features\NoUpdateFeature.cs" />
<Compile Include="Loader\Features\PrintFeature.cs" />
<Compile Include="Loader\PluginInitInjector.cs" />
<Compile Include="Loader\LibLoader.cs" />
<Compile Include="Loader\Features\Feature.cs" />
<Compile Include="Loader\PluginLoader.cs" />
<Compile Include="Loader\PluginManifest.cs" />
<Compile Include="Logging\Printers\PluginSubLogPrinter.cs" />
<Compile Include="PluginInterfaces\BeatSaber\IBeatSaberPlugin.cs" />
<Compile Include="PluginInterfaces\BeatSaber\IEnhancedBeatSaberPlugin.cs" />
@ -70,9 +82,9 @@
<Compile Include="Logging\Logger.cs" />
<Compile Include="Logging\LogPrinter.cs" />
<Compile Include="Config\ModPrefs.cs" />
<Compile Include="Updating\Converters\ModSaberDependencyConverter.cs" />
<Compile Include="Updating\Converters\SemverRangeConverter.cs" />
<Compile Include="Updating\Converters\SemverVersionConverter.cs" />
<Compile Include="JsonConverters\ModSaberDependencyConverter.cs" />
<Compile Include="JsonConverters\SemverRangeConverter.cs" />
<Compile Include="JsonConverters\SemverVersionConverter.cs" />
<Compile Include="Utilities\BeatSaber.cs" />
<Compile Include="Utilities\Ref.cs" />
<Compile Include="Utilities\ReflectionUtil.cs" />
@ -82,7 +94,7 @@
<Compile Include="Logging\Printers\GZFilePrinter.cs" />
<Compile Include="Logging\Printers\PluginLogFilePrinter.cs" />
<Compile Include="Logging\StandardLogger.cs" />
<Compile Include="Logging\UnityLogInterceptor.cs" />
<Compile Include="Logging\UnityLogProvider.cs" />
<Compile Include="Loader\PluginComponent.cs" />
<Compile Include="Loader\PluginManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
@ -90,14 +102,14 @@
<Compile Include="Updating\ModSaber\Updater.cs" />
<Compile Include="Updating\SelfPlugin.cs" />
<Compile Include="Utilities\Extensions.cs" />
<Compile Include="Utilities\LoneFunctions.cs" />
<Compile Include="Utilities\Utils.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ionic.Zip">
<Version>1.9.1.8</Version>
</PackageReference>
<PackageReference Include="Mono.Cecil">
<Version>0.10.1</Version>
<Version>0.10.3</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>12.0.1</Version>
@ -106,6 +118,8 @@
<Version>1.2.0</Version>
</PackageReference>
</ItemGroup>
<ItemGroup />
<ItemGroup>
<EmbeddedResource Include="Loader\manifest.json" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

+ 25
- 0
IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs View File

@ -0,0 +1,25 @@
using System;
using IPA.Updating.ModSaber;
using Newtonsoft.Json;
using SemVer;
namespace IPA.JsonConverters
{
internal class ModSaberDependencyConverter : JsonConverter<ApiEndpoint.Mod.Dependency>
{
public override ApiEndpoint.Mod.Dependency ReadJson(JsonReader reader, Type objectType, ApiEndpoint.Mod.Dependency existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var parts = (reader.Value as string)?.Split('@');
return new ApiEndpoint.Mod.Dependency
{
Name = parts?[0],
VersionRange = new Range(parts?[1])
};
}
public override void WriteJson(JsonWriter writer, ApiEndpoint.Mod.Dependency value, JsonSerializer serializer)
{
writer.WriteValue($"{value.Name}@{value.VersionRange}");
}
}
}

IPA.Loader/Updating/Converters/SemverRangeConverter.cs → IPA.Loader/JsonConverters/SemverRangeConverter.cs View File

@ -1,9 +1,9 @@
using Newtonsoft.Json;
using SemVer;
using System;
using System;
using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using SemVer;
namespace IPA.Updating.Converters
namespace IPA.JsonConverters
{
[SuppressMessage("ReSharper", "UnusedMember.Global")]
internal class SemverRangeConverter : JsonConverter<Range>

IPA.Loader/Updating/Converters/SemverVersionConverter.cs → IPA.Loader/JsonConverters/SemverVersionConverter.cs View File

@ -1,12 +1,12 @@
using Newtonsoft.Json;
using System;
using System;
using Newtonsoft.Json;
using Version = SemVer.Version;
namespace IPA.Updating.Converters
namespace IPA.JsonConverters
{
internal class SemverVersionConverter : JsonConverter<Version>
{
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => new Version(reader.Value as string);
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => new Version(reader.Value as string, true);
public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer) => writer.WriteValue(value.ToString());
}

+ 3
- 3
IPA.Loader/Loader/Composite/CompositeBSPlugin.cs View File

@ -54,11 +54,11 @@ namespace IPA.Loader.Composite
Invoke(plugin => plugin.OnFixedUpdate());
}
public string Name => throw new NotImplementedException();
public string Name => throw new InvalidOperationException();
public string Version => throw new NotImplementedException();
public string Version => throw new InvalidOperationException();
public ModsaberModInfo ModInfo => throw new NotImplementedException();
public ModsaberModInfo ModInfo => throw new InvalidOperationException();
public void OnLateUpdate() {
Invoke(plugin => {


+ 2
- 2
IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs View File

@ -43,9 +43,9 @@ namespace IPA.Loader.Composite
Invoke(plugin => plugin.OnFixedUpdate());
}
public string Name => throw new NotImplementedException();
public string Name => throw new InvalidOperationException();
public string Version => throw new NotImplementedException();
public string Version => throw new InvalidOperationException();
public void OnLateUpdate() {
Invoke(plugin => {


+ 27
- 0
IPA.Loader/Loader/Features/AddInFeature.cs View File

@ -0,0 +1,27 @@
namespace IPA.Loader.Features
{
internal class AddInFeature : Feature
{
private PluginLoader.PluginMetadata selfMeta;
public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters)
{
selfMeta = meta;
RequireLoaded(meta);
return true;
}
public override bool BeforeLoad(PluginLoader.PluginMetadata plugin)
{
return plugin != selfMeta;
}
public override string InvalidMessage
{
get => "Plugin is an add-in for some other mod, therefore should not be loaded.";
protected set { }
}
}
}

+ 68
- 0
IPA.Loader/Loader/Features/DefineFeature.cs View File

@ -0,0 +1,68 @@
using System;
using System.IO;
namespace IPA.Loader.Features
{
internal class DefineFeature : Feature
{
public static bool NewFeature = true;
internal override bool StoreOnPlugin => false;
public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters)
{ // parameters should be (name, fully qualified type)
if (parameters.Length != 2)
{
InvalidMessage = "Incorrect number of parameters";
return false;
}
RequireLoaded(meta);
Type type;
try
{
type = meta.Assembly.GetType(parameters[1]);
}
catch (ArgumentException)
{
InvalidMessage = $"Invalid type name {parameters[1]}";
return false;
}
catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException)
{
var filename = "";
switch (e)
{
case FileNotFoundException fn:
filename = fn.FileName;
break;
case FileLoadException fl:
filename = fl.FileName;
break;
case BadImageFormatException bi:
filename = bi.FileName;
break;
}
InvalidMessage = $"Could not find {filename} while loading type";
return false;
}
try
{
if (RegisterFeature(parameters[0], type)) return NewFeature = true;
InvalidMessage = $"Feature with name {parameters[0]} already exists";
return false;
}
catch (ArgumentException)
{
InvalidMessage = $"{type.FullName} not a subclass of {nameof(Feature)}";
return false;
}
}
}
}

+ 197
- 0
IPA.Loader/Loader/Features/Feature.cs View File

@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace IPA.Loader.Features
{
/// <summary>
/// The root interface for a mod Feature.
/// </summary>
/// <remarks>
/// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature.
/// </remarks>
public abstract class Feature
{
/// <summary>
/// Initializes the feature with the parameters provided in the definition.
///
/// Note: When no parenthesis are provided, <paramref name="parameters"/> is an empty array.
/// </summary>
/// <remarks>
/// Returning <see langword="false" /> does *not* prevent the plugin from being loaded. It simply prevents the feature from being used.
/// </remarks>
/// <param name="meta">the metadata of the plugin that is being prepared</param>
/// <param name="parameters">the parameters passed to the feature definition, or null</param>
/// <returns><see langword="true"/> if the feature is valid for the plugin, <see langword="false"/> otherwise</returns>
public abstract bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters);
/// <summary>
/// Evaluates the Feature for use in conditional meta-Features. This should be re-calculated on every call, unless it can be proven to not change.
///
/// This will be called on every feature that returns <see langword="true" /> from <see cref="Initialize"/>
/// </summary>
/// <returns>the truthiness of the Feature.</returns>
public virtual bool Evaluate() => true;
/// <summary>
/// The message to be logged when the feature is not valid for a plugin.
/// This should also be set whenever either <see cref="BeforeLoad"/> or <see cref="BeforeInit"/> returns false.
/// </summary>
public virtual string InvalidMessage { get; protected set; }
/// <summary>
/// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error.
/// </summary>
/// <remarks>
/// The assembly will still be loaded, but the plugin will not be constructed if this returns <see langword="false" />.
/// Any features it defines, for example, will still be loaded.
/// </remarks>
/// <param name="plugin">the plugin about to be loaded</param>
/// <returns>whether or not the plugin should be loaded</returns>
public virtual bool BeforeLoad(PluginLoader.PluginMetadata plugin) => true;
/// <summary>
/// Called before a plugin's Init method is called. This will not be called if there is no Init method. This should never throw an exception. An exception will abort the loading of the plugin with an error.
/// </summary>
/// <param name="plugin">the plugin to be initialized</param>
/// <returns>whether or not to call the Init method</returns>
public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true;
/// <summary>
/// Called after a plugin has been fully initialized, whether or not there is an Init method. This should never throw an exception.
/// </summary>
/// <param name="plugin">the plugin that was just initialized</param>
public virtual void AfterInit(PluginLoader.PluginInfo plugin) { }
/// <summary>
/// Ensures a plugin's assembly is loaded. Do not use unless you need to.
/// </summary>
/// <param name="plugin">the plugin to ensure is loaded.</param>
protected void RequireLoaded(PluginLoader.PluginMetadata plugin) => PluginLoader.Load(plugin);
internal virtual bool StoreOnPlugin => true;
private static readonly Dictionary<string, Type> featureTypes = new Dictionary<string, Type>
{
{ "define-feature", typeof(DefineFeature) }
};
internal static bool HasFeature(string name) => featureTypes.ContainsKey(name);
internal static bool RegisterFeature(string name, Type type)
{
if (!typeof(Feature).IsAssignableFrom(type))
throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type));
if (featureTypes.ContainsKey(name)) return false;
featureTypes.Add(name, type);
return true;
}
internal struct FeatureParse
{
public readonly string Name;
public readonly string[] Parameters;
public FeatureParse(string name, string[] parameters)
{
Name = name;
Parameters = parameters;
}
}
// returns false with both outs null for no such feature
internal static bool TryParseFeature(string featureString, PluginLoader.PluginMetadata plugin,
out Feature feature, out Exception failException, out bool featureValid, out FeatureParse parsed,
FeatureParse? preParsed = null)
{
failException = null;
feature = null;
featureValid = false;
if (preParsed == null)
{
var builder = new StringBuilder();
string name = null;
var parameters = new List<string>();
bool escape = false;
int parens = 0;
bool removeWhitespace = true;
foreach (var chr in featureString)
{
if (escape)
{
builder.Append(chr);
escape = false;
}
else
{
switch (chr)
{
case '\\':
escape = true;
break;
case '(':
parens++;
if (parens != 1) goto default;
removeWhitespace = true;
name = builder.ToString();
builder.Clear();
break;
case ')':
parens--;
if (parens != 0) goto default;
goto case ',';
case ',':
if (parens > 1) goto default;
parameters.Add(builder.ToString());
builder.Clear();
removeWhitespace = true;
break;
default:
if (removeWhitespace && !char.IsWhiteSpace(chr))
removeWhitespace = false;
if (!removeWhitespace)
builder.Append(chr);
break;
}
}
}
if (name == null)
name = builder.ToString();
parsed = new FeatureParse(name, parameters.ToArray());
if (parens != 0)
{
failException = new Exception("Malformed feature definition");
return false;
}
}
else
parsed = preParsed.Value;
if (!featureTypes.TryGetValue(parsed.Name, out var featureType))
return false;
try
{
if (!(Activator.CreateInstance(featureType) is Feature aFeature))
{
failException = new InvalidCastException("Feature type not a subtype of Feature");
return false;
}
featureValid = aFeature.Initialize(plugin, parsed.Parameters);
feature = aFeature;
return true;
}
catch (Exception e)
{
failException = e;
return false;
}
}
}
}

+ 12
- 0
IPA.Loader/Loader/Features/NoUpdateFeature.cs View File

@ -0,0 +1,12 @@
namespace IPA.Loader.Features
{
internal class NoUpdateFeature : Feature
{
public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters)
{
return meta.Id != null;
}
public override string InvalidMessage { get; protected set; } = "No ID specified; cannot update anyway";
}
}

+ 32
- 0
IPA.Loader/Loader/Features/PrintFeature.cs View File

@ -0,0 +1,32 @@

using IPA.Logging;
namespace IPA.Loader.Features
{
internal class PrintFeature : Feature
{
public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters)
{
Logger.features.Info($"{meta.Name}: {string.Join(" ", parameters)}");
return true;
}
}
internal class DebugFeature : Feature
{
public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters)
{
Logger.features.Debug($"{meta.Name}: {string.Join(" ", parameters)}");
return true;
}
}
internal class WarnFeature : Feature
{
public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters)
{
Logger.features.Debug($"{meta.Name}: {string.Join(" ", parameters)}");
return true;
}
}
}

IPA.Injector/LibLoader.cs → IPA.Loader/Loader/LibLoader.cs View File

@ -3,62 +3,87 @@ using System.Collections.Generic;
using System.IO;
using System.Reflection;
using IPA.Logging;
using static IPA.Logging.Logger;
using Mono.Cecil;
namespace IPA.Injector
namespace IPA.Loader
{
internal class CecilLibLoader : BaseAssemblyResolver
{
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
{
LibLoader.SetupAssemblyFilenames();
var testFile = $"{name.Name}.{name.Version}.dll";
if (LibLoader.filenameLocations.TryGetValue(testFile, out string path))
{
if (File.Exists(path))
{
return AssemblyDefinition.ReadAssembly(path, parameters);
}
}
return base.Resolve(name, parameters);
}
}
internal static class LibLoader
{
private static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs");
private static string NativeLibraryPath => Path.Combine(LibraryPath, "Native");
private static Dictionary<string, string> filenameLocations;
internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs");
internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native");
internal static Dictionary<string, string> filenameLocations;
public static Assembly AssemblyLibLoader(object source, ResolveEventArgs e)
internal static void SetupAssemblyFilenames()
{
var asmName = new AssemblyName(e.Name);
Log(Level.Debug, $"Resolving library {asmName}");
if (filenameLocations == null)
{
filenameLocations = new Dictionary<string, string>();
foreach (var fn in TraverseTree(LibraryPath, s => s != NativeLibraryPath))
if (filenameLocations.ContainsKey(fn.Name))
Log(Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}");
Log(Logger.Level.Critical, $"Multiple instances of {fn.Name} exist in Libs! Ignoring {fn.FullName}");
else filenameLocations.Add(fn.Name, fn.FullName);
}
}
public static Assembly AssemblyLibLoader(object source, ResolveEventArgs e)
{
var asmName = new AssemblyName(e.Name);
Log(Logger.Level.Debug, $"Resolving library {asmName}");
SetupAssemblyFilenames();
var testFile = $"{asmName.Name}.{asmName.Version}.dll";
Log(Level.Debug, $"Looking for file {testFile}");
Log(Logger.Level.Debug, $"Looking for file {testFile}");
if (filenameLocations.TryGetValue(testFile, out string path))
{
Log(Level.Debug, $"Found file {testFile} as {path}");
Log(Logger.Level.Debug, $"Found file {testFile} as {path}");
if (File.Exists(path))
{
return Assembly.LoadFrom(path);
}
Log(Level.Critical, $"but {path} no longer exists!");
Log(Logger.Level.Critical, $"but {path} no longer exists!");
}
Log(Level.Critical, $"No library {asmName} found");
Log(Logger.Level.Critical, $"No library {asmName} found");
return null;
}
private static void Log(Level lvl, string message)
internal static void Log(Logger.Level lvl, string message)
{ // multiple proxy methods to delay loading of assemblies until it's done
if (LogCreated)
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)
private static void AssemblyLibLoaderCallLogger(Logger.Level lvl, string message)
{
libLoader.Log(lvl, message);
Logger.libLoader.Log(lvl, message);
}
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-iterate-through-a-directory-tree
@ -152,5 +177,7 @@ namespace IPA.Injector
}
}
}
}
}

+ 9
- 52
IPA.Loader/Loader/PluginComponent.cs View File

@ -1,6 +1,6 @@
using IPA.Loader.Composite;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
// ReSharper disable UnusedMember.Local
@ -23,10 +23,10 @@ namespace IPA.Loader
{
DontDestroyOnLoad(gameObject);
bsPlugins = new CompositeBSPlugin(PluginManager.BSPlugins);
#pragma warning disable CS0618 // Type or member is obsolete
bsPlugins = new CompositeBSPlugin(PluginManager.BSPlugins.Where(p => p != null));
#pragma warning disable 618
ipaPlugins = new CompositeIPAPlugin(PluginManager.Plugins);
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning restore 618
gameObject.AddComponent<Updating.ModSaber.Updater>();
@ -37,23 +37,15 @@ namespace IPA.Loader
SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded;
foreach (var provider in PluginManager.configProviders)
if (provider.Key.HasChanged)
try
{
provider.Key.Save();
}
catch (Exception e)
{
Logging.Logger.log.Error("Error when trying to save config");
Logging.Logger.log.Error(e);
}
Config.Config.Save();
}
void Update()
{
bsPlugins.OnUpdate();
ipaPlugins.OnUpdate();
Config.Config.Update();
}
void LateUpdate()
@ -61,32 +53,7 @@ namespace IPA.Loader
bsPlugins.OnLateUpdate();
ipaPlugins.OnLateUpdate();
foreach (var provider in PluginManager.configProviders)
{
if (provider.Key.HasChanged)
try
{
provider.Key.Save();
}
catch (Exception e)
{
Logging.Logger.log.Error("Error when trying to save config");
Logging.Logger.log.Error(e);
}
else if (provider.Key.LastModified > provider.Value.Value)
{
try
{
provider.Key.Load(); // auto reload if it changes
provider.Value.Value = provider.Key.LastModified;
}
catch (Exception e)
{
Logging.Logger.log.Error("Error when trying to load config");
Logging.Logger.log.Error(e);
}
}
}
//Config.Config.Update();
}
void FixedUpdate()
@ -112,17 +79,7 @@ namespace IPA.Loader
bsPlugins.OnApplicationQuit();
ipaPlugins.OnApplicationQuit();
foreach (var provider in PluginManager.configProviders)
if (provider.Key.HasChanged)
try
{
provider.Key.Save();
}
catch (Exception e)
{
Logging.Logger.log.Error("Error when trying to save config");
Logging.Logger.log.Error(e);
}
Config.Config.Save();
quitting = true;
}


+ 89
- 0
IPA.Loader/Loader/PluginInitInjector.cs View File

@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using IPA.Config;
using IPA.Logging;
using IPA.Utilities;
namespace IPA.Loader
{
/// <summary>
/// The type that handles value injecting into a plugin's Init.
/// </summary>
public static class PluginInitInjector
{
/// <summary>
/// A typed injector for a plugin's Init method. When registered, called for all associated types. If it returns null, the default for the type will be used.
/// </summary>
/// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param>
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param>
/// <param name="meta">the <see cref="PluginLoader.PluginMetadata"/> for the plugin being loaded.</param>
/// <returns>the value to inject into that parameter.</returns>
public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta);
/// <summary>
/// Adds an injector to be used when calling future plugins' Init methods.
/// </summary>
/// <param name="type">the type of the parameter.</param>
/// <param name="injector">the function to call for injection.</param>
public static void AddInjector(Type type, InjectParameter injector)
{
injectors.Add(new Tuple<Type, InjectParameter>(type, injector));
}
private static readonly List<Tuple<Type, InjectParameter>> injectors = new List<Tuple<Type, InjectParameter>>
{
new Tuple<Type, InjectParameter>(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)),
new Tuple<Type, InjectParameter>(typeof(IModPrefs), (prev, param, meta) => prev ?? new ModPrefs(meta)),
new Tuple<Type, InjectParameter>(typeof(IConfigProvider), (prev, param, meta) =>
{
if (prev != null) return prev;
var cfgProvider = Config.Config.GetProviderFor(meta.Name, param);
cfgProvider.Load();
return cfgProvider;
})
};
internal static void Inject(MethodInfo init, PluginLoader.PluginInfo info)
{
var instance = info.Plugin;
var meta = info.Metadata;
var initArgs = new List<object>();
var initParams = init.GetParameters();
Dictionary<Tuple<Type, InjectParameter>, object> previousValues =
new Dictionary<Tuple<Type, InjectParameter>, object>(injectors.Count);
foreach (var param in initParams)
{
var paramType = param.ParameterType;
var value = paramType.GetDefault();
foreach (var pair in injectors.Where(t => paramType.IsAssignableFrom(t.Item1)))
{
object prev = null;
if (previousValues.ContainsKey(pair))
prev = previousValues[pair];
var val = pair.Item2?.Invoke(prev, param, meta);
if (previousValues.ContainsKey(pair))
previousValues[pair] = val;
else
previousValues.Add(pair, val);
if (val == null) continue;
value = val;
break;
}
initArgs.Add(value);
}
init.Invoke(instance, initArgs.ToArray());
}
}
}

+ 434
- 0
IPA.Loader/Loader/PluginLoader.cs View File

@ -0,0 +1,434 @@
using IPA.Loader.Features;
using IPA.Logging;
using IPA.Utilities;
using Mono.Cecil;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Version = SemVer.Version;
namespace IPA.Loader
{
/// <summary>
/// A type to manage the loading of plugins.
/// </summary>
public class PluginLoader
{
internal static Task LoadTask() => Task.Run(() =>
{
LoadMetadata();
Resolve();
ComputeLoadOrder();
InitFeatures();
});
/// <summary>
/// A class which describes
/// </summary>
public class PluginMetadata
{
/// <summary>
/// The assembly the plugin was loaded from.
/// </summary>
public Assembly Assembly { get; internal set; }
/// <summary>
/// The TypeDefinition for the main type of the plugin.
/// </summary>
public TypeDefinition PluginType { get; internal set; }
/// <summary>
/// The human readable name of the plugin.
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// The ModSaber ID of the plugin, or null if it doesn't have one.
/// </summary>
public string Id { get; internal set; }
/// <summary>
/// The version of the plugin.
/// </summary>
public Version Version { get; internal set; }
/// <summary>
/// The file the plugin was loaded from.
/// </summary>
public FileInfo File { get; internal set; }
// ReSharper disable once UnusedAutoPropertyAccessor.Global
/// <summary>
/// The features this plugin requests.
/// </summary>
public IReadOnlyList<Feature> Features => InternalFeatures;
internal readonly List<Feature> InternalFeatures = new List<Feature>();
internal bool IsSelf;
private PluginManifest manifest;
internal PluginManifest Manifest
{
get => manifest;
set
{
manifest = value;
Name = value.Name;
Version = value.Version;
Id = value.Id;
}
}
/// <inheritdoc />
public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
}
/// <summary>
/// A container object for all the data relating to a plugin.
/// </summary>
public class PluginInfo
{
internal IBeatSaberPlugin Plugin { get; set; }
/// <summary>
/// Metadata for the plugin.
/// </summary>
public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
}
internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
internal static void LoadMetadata()
{
string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
try
{
var selfMeta = new PluginMetadata
{
Assembly = Assembly.GetExecutingAssembly(),
File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
PluginType = null,
IsSelf = true
};
string manifest;
using (var manifestReader =
new StreamReader(
selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
throw new InvalidOperationException()))
manifest = manifestReader.ReadToEnd();
selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
PluginsMetadata.Add(selfMeta);
}
catch (Exception e)
{
Logger.loader.Critical("Error loading own manifest");
Logger.loader.Critical(e);
}
foreach (var plugin in plugins)
{
try
{
var metadata = new PluginMetadata
{
File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)),
IsSelf = false
};
var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
{
ReadingMode = ReadingMode.Immediate,
ReadWrite = false,
AssemblyResolver = new CecilLibLoader()
}).MainModule;
var iBeatSaberPlugin = pluginModule.ImportReference(typeof(IBeatSaberPlugin));
foreach (var type in pluginModule.Types)
{
foreach (var inter in type.Interfaces)
{
var ifType = inter.InterfaceType;
if (iBeatSaberPlugin.FullName == ifType.FullName)
{
metadata.PluginType = type;
break;
}
}
if (metadata.PluginType != null) break;
}
if (metadata.PluginType == null)
{
Logger.loader.Warn($"Could not find plugin type for {Path.GetFileName(plugin)}");
continue;
}
foreach (var resource in pluginModule.Resources)
{
if (!(resource is EmbeddedResource embedded) ||
embedded.Name != $"{metadata.PluginType.Namespace}.manifest.json") continue;
string manifest;
using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
manifest = manifestReader.ReadToEnd();
metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
break;
}
Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
PluginsMetadata.Add(metadata);
}
catch (Exception e)
{
Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
Logger.loader.Error(e);
}
}
}
internal static void Resolve()
{ // resolves duplicates and conflicts, etc
PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version));
var ids = new HashSet<string>();
var ignore = new HashSet<PluginMetadata>();
var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
foreach (var meta in PluginsMetadata)
{
if (meta.Id != null)
{
if (ids.Contains(meta.Id))
{
Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
ignore.Add(meta);
continue; // because of sorted order, hightest order will always be the first one
}
bool processedLater = false;
foreach (var meta2 in PluginsMetadata)
{
if (ignore.Contains(meta2)) continue;
if (meta == meta2)
{
processedLater = true;
continue;
}
if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
var range = meta2.Manifest.Conflicts[meta.Id];
if (!range.IsSatisfied(meta.Version)) continue;
Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
if (processedLater)
{
Logger.loader.Warn($"Ignoring {meta2.Name}");
ignore.Add(meta2);
}
else
{
Logger.loader.Warn($"Ignoring {meta.Name}");
ignore.Add(meta);
break;
}
}
}
if (ignore.Contains(meta)) continue;
if (meta.Id != null) ids.Add(meta.Id);
resolved.Add(meta);
}
PluginsMetadata = resolved;
}
internal static void ComputeLoadOrder()
{
PluginsMetadata.Sort((a, b) =>
{
if (a.Id == b.Id) return 0;
if (a.Id != null)
{
if (b.Manifest.Dependencies.ContainsKey(a.Id) || b.Manifest.LoadAfter.Contains(a.Id)) return -1;
if (b.Manifest.LoadBefore.Contains(a.Id)) return 1;
}
if (b.Id != null)
{
if (a.Manifest.Dependencies.ContainsKey(b.Id) || a.Manifest.LoadAfter.Contains(b.Id)) return 1;
if (a.Manifest.LoadBefore.Contains(b.Id)) return -1;
}
return 0;
});
var metadata = new List<PluginMetadata>();
var pluginsToLoad = new Dictionary<string, Version>();
foreach (var meta in PluginsMetadata)
{
bool load = true;
foreach (var dep in meta.Manifest.Dependencies)
{
if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) continue;
load = false;
Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
}
if (load)
{
metadata.Add(meta);
if (meta.Id != null)
pluginsToLoad.Add(meta.Id, meta.Version);
}
}
PluginsMetadata = metadata;
}
internal static void InitFeatures()
{
var parsedFeatures = PluginsMetadata.Select(m =>
Tuple.Create(m,
m.Manifest.Features.Select(f =>
Tuple.Create(f, Ref.Create<Feature.FeatureParse?>(null))
).ToList()
)
).ToList();
while (DefineFeature.NewFeature)
{
DefineFeature.NewFeature = false;
foreach (var plugin in parsedFeatures)
for (var i = 0; i < plugin.Item2.Count; i++)
{
var feature = plugin.Item2[i];
var success = Feature.TryParseFeature(feature.Item1, plugin.Item1, out var featureObj,
out var exception, out var valid, out var parsed, feature.Item2.Value);
if (!success && !valid && featureObj == null && exception == null) // no feature of type found
feature.Item2.Value = parsed;
else if (success)
{
if (valid && featureObj.StoreOnPlugin)
plugin.Item1.InternalFeatures.Add(featureObj);
else if (!valid)
Logger.features.Warn(
$"Feature not valid on {plugin.Item1.Name}: {featureObj.InvalidMessage}");
plugin.Item2.RemoveAt(i--);
}
else
{
Logger.features.Error($"Error parsing feature definition on {plugin.Item1.Name}");
Logger.features.Error(exception);
plugin.Item2.RemoveAt(i--);
}
}
foreach (var plugin in PluginsMetadata)
foreach (var feature in plugin.Features)
feature.Evaluate();
}
foreach (var plugin in parsedFeatures)
{
if (plugin.Item2.Count <= 0) continue;
Logger.features.Warn($"On plugin {plugin.Item1.Name}:");
foreach (var feature in plugin.Item2)
Logger.features.Warn($" Feature not found with name {feature.Item1}");
}
}
internal static void Load(PluginMetadata meta)
{
if (meta.Assembly == null)
meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
}
internal static PluginInfo InitPlugin(PluginMetadata meta)
{
if (meta.PluginType == null)
return new PluginInfo()
{
Metadata = meta,
Plugin = null
};
var info = new PluginInfo();
try
{
Load(meta);
Feature denyingFeature = null;
if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
{
Logger.loader.Warn(
$"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
return null;
}
var type = meta.Assembly.GetType(meta.PluginType.FullName);
var instance = (IBeatSaberPlugin)Activator.CreateInstance(type);
info.Metadata = meta;
info.Plugin = instance;
var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
if (init != null)
{
denyingFeature = null;
if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
{
Logger.loader.Warn(
$"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
return null;
}
PluginInitInjector.Inject(init, info);
}
foreach (var feature in meta.Features)
try
{
feature.AfterInit(info);
}
catch (Exception e)
{
Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
}
}
catch (AmbiguousMatchException)
{
Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
return null;
}
catch (Exception e)
{
Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
return null;
}
return info;
}
internal static List<PluginInfo> LoadPlugins() => PluginsMetadata.Select(InitPlugin).Where(p => p != null).ToList();
}
}

+ 32
- 102
IPA.Loader/Loader/PluginManager.cs View File

@ -7,14 +7,12 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using IPA.Config;
using IPA.Config.ConfigProviders;
using IPA.Logging;
using IPA.Old;
using IPA.Updating;
using IPA.Utilities;
using Mono.Cecil;
using UnityEngine;
using Logger = IPA.Logging.Logger;
using static IPA.Loader.PluginLoader;
namespace IPA.Loader
{
@ -25,18 +23,7 @@ namespace IPA.Loader
{
#pragma warning disable CS0618 // Type or member is obsolete (IPlugin)
/// <summary>
/// A container object for all the data relating to a plugin.
/// </summary>
public class PluginInfo
{
internal IBeatSaberPlugin Plugin { get; set; }
internal string Filename { get; set; }
/// <summary>
/// The ModSaber updating info for the mod, or null.
/// </summary>
public ModsaberModInfo ModSaberInfo { get; internal set; }
}
/// <summary>
/// An <see cref="IEnumerable"/> of new Beat Saber plugins
@ -72,7 +59,7 @@ namespace IPA.Loader
/// <returns>the plugin info for the requested plugin or null</returns>
public static PluginInfo GetPlugin(string name)
{
return BSMetas.FirstOrDefault(p => p.Plugin.Name == name);
return BSMetas.FirstOrDefault(p => p.Metadata.Name == name);
}
/// <summary>
@ -82,7 +69,7 @@ namespace IPA.Loader
/// <returns>the plugin info for the requested plugin or null</returns>
public static PluginInfo GetPluginFromModSaberName(string name)
{
return BSMetas.FirstOrDefault(p => p.ModSaberInfo.InternalName == name);
return BSMetas.FirstOrDefault(p => p.Metadata.Id == name);
}
/// <summary>
@ -104,8 +91,6 @@ namespace IPA.Loader
internal static IConfigProvider SelfConfigProvider { get; set; }
internal static readonly List<KeyValuePair<IConfigProvider,Ref<DateTime>>> configProviders = new List<KeyValuePair<IConfigProvider, Ref<DateTime>>>();
private static void LoadPlugins()
{
string pluginDirectory = Path.Combine(Environment.CurrentDirectory, "Plugins");
@ -136,6 +121,7 @@ namespace IPA.Loader
string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll");
foreach (string s in originalPlugins)
{
if (PluginsMetadata.Select(m => m.File.Name).Contains(s)) continue;
string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s));
#region Fix assemblies for refactor
@ -179,38 +165,42 @@ namespace IPA.Loader
#endregion
}
var selfPlugin = new PluginInfo
/*var selfPlugin = new PluginInfo
{
Filename = Path.Combine(BeatSaber.InstallPath, "IPA.exe"),
Plugin = SelfPlugin.Instance
};
selfPlugin.ModSaberInfo = selfPlugin.Plugin.ModInfo;
_bsPlugins.Add(selfPlugin);
selfPlugin.Metadata.Manifest = new PluginManifest
{
Author = "DaNike",
Features = new string[0],
Description = "",
Version = new SemVer.Version(SelfConfig.IPA_Version),
GameVersion = BeatSaber.GameVersion,
Id = "beatsaber-ipa-reloaded"
};
selfPlugin.Metadata.File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe"));
configProviders.Add(new KeyValuePair<IConfigProvider, Ref<DateTime>>(
SelfConfigProvider = new JsonConfigProvider {Filename = Path.Combine("UserData", SelfPlugin.IPA_Name)},
new Ref<DateTime>(SelfConfigProvider.LastModified)));
SelfConfigProvider.Load();
_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);
var result = LoadPluginsFromFile(s);
_ipaPlugins.AddRange(result.Item2);
}
_bsPlugins.AddRange(PluginLoader.LoadPlugins());
Logger.log.Info(exeName);
Logger.log.Info($"Running on Unity {Application.unityVersion}");
Logger.log.Info($"Game version {BeatSaber.GameVersion}");
Logger.log.Info("-----------------------------");
Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}");
Logger.log.Info($"Loading plugins from {Utils.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($"{plugin.Metadata.Name}: {plugin.Metadata.Version}");
}
Logger.log.Info("-----------------------------");
foreach (var plugin in _ipaPlugins)
@ -220,13 +210,12 @@ namespace IPA.Loader
Logger.log.Info("-----------------------------");
}
private static Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>> LoadPluginsFromFile(string file, string exeName)
private static Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>> LoadPluginsFromFile(string file)
{
List<PluginInfo> bsPlugins = new List<PluginInfo>();
List<IPlugin> ipaPlugins = new List<IPlugin>();
if (!File.Exists(file) || !file.EndsWith(".dll", true, null))
return new Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>>(bsPlugins, ipaPlugins);
return new Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>>(null, ipaPlugins);
T OptionalGetPlugin<T>(Type t) where T : class
{
@ -236,15 +225,15 @@ namespace IPA.Loader
try
{
T pluginInstance = Activator.CreateInstance(t) as T;
string[] filter = null;
/*string[] filter = null;
if (typeof(T) == typeof(IPlugin) && pluginInstance is IEnhancedPlugin enhancedPlugin)
filter = enhancedPlugin.Filter;
else if (pluginInstance is IGenericEnhancedPlugin plugin)
filter = plugin.Filter;
filter = plugin.Filter;*/
if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase))
return pluginInstance;
//if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase))
return pluginInstance;
}
catch (Exception e)
{
@ -257,74 +246,15 @@ namespace IPA.Loader
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();
Logger modLogger = null;
IModPrefs modPrefs = null;
IConfigProvider cfgProvider = null;
foreach (var param in initParams)
{
var ptype = param.ParameterType;
if (ptype.IsAssignableFrom(typeof(Logger))) {
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 if (ptype.IsAssignableFrom(typeof(IConfigProvider)))
{
if (cfgProvider == null)
{
cfgProvider = new JsonConfigProvider { Filename = Path.Combine("UserData", $"{bsPlugin.Name}") };
configProviders.Add(new KeyValuePair<IConfigProvider, Ref<DateTime>>(cfgProvider, new Ref<DateTime>(cfgProvider.LastModified)));
cfgProvider.Load();
}
initArgs.Add(cfgProvider);
}
else
initArgs.Add(ptype.GetDefault());
}
init.Invoke(bsPlugin, initArgs.ToArray());
}
bsPlugins.Add(new PluginInfo
{
Plugin = bsPlugin,
Filename = file.Replace("\\.cache", ""), // quick and dirty fix
ModSaberInfo = bsPlugin.ModInfo
});
}
catch (AmbiguousMatchException)
{
Logger.loader.Error("Only one Init allowed per plugin");
}
}
else
IPlugin ipaPlugin = OptionalGetPlugin<IPlugin>(t);
if (ipaPlugin != null)
{
IPlugin ipaPlugin = OptionalGetPlugin<IPlugin>(t);
if (ipaPlugin != null)
{
ipaPlugins.Add(ipaPlugin);
}
ipaPlugins.Add(ipaPlugin);
}
}
@ -334,7 +264,7 @@ namespace IPA.Loader
Logger.loader.Error($"Could not load {Path.GetFileName(file)}! {e}");
}
return new Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>>(bsPlugins, ipaPlugins);
return new Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>>(null, ipaPlugins);
}
internal class AppInfo


+ 43
- 0
IPA.Loader/Loader/PluginManifest.cs View File

@ -0,0 +1,43 @@
using IPA.JsonConverters;
using Newtonsoft.Json;
using SemVer;
using System.Collections.Generic;
namespace IPA.Loader
{
internal class PluginManifest
{
[JsonProperty("name", Required = Required.Always)]
public string Name;
[JsonProperty("id", Required = Required.AllowNull)]
public string Id;
[JsonProperty("description", Required = Required.Always)]
public string Description;
[JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))]
public Version Version;
[JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))]
public Version GameVersion;
[JsonProperty("author", Required = Required.Always)]
public string Author;
[JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))]
public Dictionary<string, Range> Dependencies = new Dictionary<string, Range>();
[JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))]
public Dictionary<string, Range> Conflicts = new Dictionary<string, Range>();
[JsonProperty("features", Required = Required.Always)]
public string[] Features;
[JsonProperty("loadBefore", Required = Required.DisallowNull)]
public string[] LoadBefore = new string[0];
[JsonProperty("loadAfter", Required = Required.DisallowNull)]
public string[] LoadAfter = new string[0];
}
}

+ 17
- 0
IPA.Loader/Loader/manifest.json View File

@ -0,0 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/nike4613/ModSaber-MetadataFileSchema/master/Schema.json",
"author": "DaNike",
"description": "A mod loader specifically for Beat Saber",
"gameVersion": "0.12.2",
"id": "beatsaber-ipa-reloaded",
"name": "BSIPA",
"version": "3.12.0",
"features": [
"define-feature(print, IPA.Loader.Features.PrintFeature)",
"define-feature(debug, IPA.Loader.Features.DebugFeature)",
"define-feature(warn, IPA.Loader.Features.WarnFeature)",
"define-feature(no-update, IPA.Loader.Features.NoUpdateFeature)",
"define-feature(add-in, IPA.Loader.Features.AddInFeature)",
"print(YO! Howz it goin\\, its ya boi desinc here)"
]
}

+ 6
- 1
IPA.Loader/Logging/LogPrinter.cs View File

@ -11,6 +11,7 @@ namespace IPA.Logging
/// 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>
@ -19,15 +20,19 @@ namespace IPA.Logging
/// <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() { }
internal DateTime LastUse { get; set; }
}
}
}

+ 40
- 7
IPA.Loader/Logging/Logger.cs View File

@ -1,4 +1,5 @@
using System;
// ReSharper disable InconsistentNaming
namespace IPA.Logging
@ -9,6 +10,7 @@ namespace IPA.Logging
public abstract class Logger
{
private static Logger _log;
internal static Logger log
{
get
@ -18,11 +20,13 @@ namespace IPA.Logging
return _log;
}
}
internal static Logger updater => log.GetChildLogger("Updater");
internal static Logger libLoader => log.GetChildLogger("LibraryLoader");
internal static Logger loader => log.GetChildLogger("Loader");
internal static Logger features => loader.GetChildLogger("Features");
internal static Logger config => log.GetChildLogger("Config");
internal static bool LogCreated => _log != null || UnityLogInterceptor.Logger != null;
internal static bool LogCreated => _log != null || UnityLogProvider.Logger != null;
/// <summary>
/// The standard format for log messages.
@ -38,22 +42,27 @@ namespace IPA.Logging
/// 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>
@ -70,22 +79,27 @@ namespace IPA.Logging
/// 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>
@ -95,18 +109,26 @@ namespace IPA.Logging
/// 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>
/// Used for when the level is undefined.
/// </summary>
Undefined = Byte.MaxValue
}
/// <summary>
@ -115,19 +137,22 @@ namespace IPA.Logging
/// <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="e">the exception to log</param>
public virtual void Log(Level level, Exception e) => Log(level, e.ToString());
/// <summary>
/// Sends a debug message.
/// 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);
@ -135,13 +160,15 @@ namespace IPA.Logging
/// </summary>
/// <param name="e">the exception to log</param>
public virtual void Debug(Exception e) => Log(Level.Debug, e);
/// <summary>
/// Sends an info message.
/// 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);
@ -149,13 +176,15 @@ namespace IPA.Logging
/// </summary>
/// <param name="e">the exception to log</param>
public virtual void Info(Exception e) => Log(Level.Info, e);
/// <summary>
/// Sends a warning message.
/// 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);
@ -163,13 +192,15 @@ namespace IPA.Logging
/// </summary>
/// <param name="e">the exception to log</param>
public virtual void Warn(Exception e) => Log(Level.Warning, e);
/// <summary>
/// Sends an error message.
/// 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);
@ -177,13 +208,15 @@ namespace IPA.Logging
/// </summary>
/// <param name="e">the exception to log</param>
public virtual void Error(Exception e) => Log(Level.Error, e);
/// <summary>
/// Sends a critical message.
/// 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);
@ -192,4 +225,4 @@ namespace IPA.Logging
/// <param name="e">the exception to log</param>
public virtual void Critical(Exception e) => Log(Level.Critical, e);
}
}
}

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

@ -12,17 +12,19 @@ namespace IPA.Logging.Printers
public abstract class GZFilePrinter : LogPrinter, IDisposable
{
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool CreateHardLink(
private static extern bool CreateHardLink(
string lpFileName,
string lpExistingFileName,
IntPtr lpSecurityAttributes
);
private FileInfo fileInfo;
/// <summary>
/// The <see cref="StreamWriter"/> that writes to the GZip file.
/// </summary>
protected StreamWriter FileWriter;
private GZipStream zstream;
private FileStream fstream;
@ -99,9 +101,7 @@ namespace IPA.Logging.Printers
fstream.Dispose();
}
/// <summary>
/// Disposes the file printer.
/// </summary>
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
@ -128,4 +128,4 @@ namespace IPA.Logging.Printers
}
}
}
}
}

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

@ -38,4 +38,4 @@ namespace IPA.Logging.Printers
return finfo;
}
}
}
}

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

@ -21,7 +21,7 @@ namespace IPA.Logging.Printers
/// <returns></returns>
protected override FileInfo GetFileInfo()
{
var logsDir = new DirectoryInfo(Path.Combine("Logs",name));
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;
@ -49,4 +49,4 @@ namespace IPA.Logging.Printers
FileWriter.WriteLine(Logger.LogFormat, line, logName, time, level.ToString().ToUpper());
}
}
}
}

+ 1
- 1
IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs View File

@ -52,4 +52,4 @@ namespace IPA.Logging.Printers
FileWriter.WriteLine("[{2} @ {1:HH:mm:ss}] {0}", line, time, level.ToString().ToUpper());
}
}
}
}

+ 154
- 38
IPA.Loader/Logging/StandardLogger.cs View File

@ -4,16 +4,24 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
namespace IPA.Logging
{
/// <summary>
/// The default <see cref="Logger"/> implementation.
/// The default (and standard) <see cref="Logger"/> implementation.
/// </summary>
/// <remarks>
/// <see cref="StandardLogger"/> uses a multi-threaded approach to logging. All actual I/O is done on another thread,
/// where all messaged are guaranteed to be logged in the order they appeared. It is up to the printers to format them.
///
/// This logger supports child loggers. Use <see cref="LoggerExtensions.GetChildLogger"/> to safely get a child.
/// The modification of printers on a parent are reflected down the chain.
/// </remarks>
public class StandardLogger : Logger
{
private static readonly IReadOnlyList<LogPrinter> defaultPrinters = new List<LogPrinter>()
private static readonly List<LogPrinter> defaultPrinters = new List<LogPrinter>()
{
new ColoredConsolePrinter()
{
@ -43,29 +51,45 @@ namespace IPA.Logging
new GlobalLogFilePrinter()
};
/// <summary>
/// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger.
/// </summary>
/// <param name="printer"></param>
internal static void AddDefaultPrinter(LogPrinter printer)
{
defaultPrinters.Add(printer);
}
private readonly string logName;
private static readonly bool showSourceClass;
private static bool showSourceClass;
/// <summary>
/// All levels defined by this filter will be sent to loggers. All others will be ignored.
/// </summary>
public static LogLevel PrintFilter { get; set; }
private readonly List<LogPrinter> printers = new List<LogPrinter>(defaultPrinters);
public static LogLevel PrintFilter { get; set; } = LogLevel.All;
private readonly List<LogPrinter> printers = new List<LogPrinter>();
private readonly StandardLogger parent;
private readonly Dictionary<string, StandardLogger> children = new Dictionary<string, StandardLogger>();
static StandardLogger()
/// <summary>
/// Configures internal debug settings based on the config passed in.
/// </summary>
/// <param name="cfg"></param>
internal static void Configure(SelfConfig cfg)
{
showSourceClass = ModPrefs.GetBool("IPA", "DebugShowCallSource", false, true);
PrintFilter = ModPrefs.GetBool("IPA", "PrintDebug", false, true) ? LogLevel.All : LogLevel.InfoUp;
showSourceClass = cfg.Debug.ShowCallSource;
PrintFilter = cfg.Debug.ShowDebug ? LogLevel.All : LogLevel.InfoUp;
}
private StandardLogger(string mainName, string subName, params LogPrinter[] inherited)
private StandardLogger(StandardLogger parent, string subName)
{
logName = $"{mainName}/{subName}";
printers = new List<LogPrinter>(inherited)
logName = $"{parent.logName}/{subName}";
this.parent = parent;
printers = new List<LogPrinter>()
{
new PluginSubLogPrinter(mainName, subName)
new PluginSubLogPrinter(parent.logName, subName)
};
if (logThread == null || !logThread.IsAlive)
@ -78,7 +102,6 @@ namespace IPA.Logging
internal StandardLogger(string name)
{
logName = name;
printers.Add(new PluginLogFilePrinter(name));
if (logThread == null || !logThread.IsAlive)
@ -88,11 +111,16 @@ namespace IPA.Logging
}
}
/// <summary>
/// Gets a child printer with the given name, either constructing a new one or using one that was already made.
/// </summary>
/// <param name="name"></param>
/// <returns>a child <see cref="StandardLogger"/> with the given sub-name</returns>
internal StandardLogger GetChild(string name)
{
if (!children.TryGetValue(name, out var child))
{
child = new StandardLogger(logName, name, printers.ToArray());
child = new StandardLogger(this, name);
children.Add(name, child);
}
@ -118,6 +146,8 @@ namespace IPA.Logging
if (message == null)
throw new ArgumentNullException(nameof(message));
// make sure that the queue isn't being cleared
logWaitEvent.Wait();
logQueue.Add(new LogMessage
{
Level = level,
@ -126,7 +156,7 @@ namespace IPA.Logging
Time = DateTime.Now
});
}
/// <inheritdoc />
/// <summary>
/// An override to <see cref="M:IPA.Logging.Logger.Debug(System.String)" /> which shows the method that called it.
@ -135,12 +165,12 @@ namespace IPA.Logging
public override void Debug(string message)
{
// add source to message
var stackFrame = new StackTrace().GetFrame(1);
var stackFrame = new StackTrace(true).GetFrame(1);
var method = stackFrame.GetMethod();
var lineNo = stackFrame.GetFileLineNumber();
var lineOffs = stackFrame.GetFileColumnNumber();
var paramString = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName));
base.Debug(showSourceClass
? $"{{{method.DeclaringType?.FullName}::{method.Name}({lineNo}:{lineOffs})}} {message}"
? $"{{{method.DeclaringType?.FullName}::{method.Name}({paramString}):{lineNo}}} {message}"
: message);
}
@ -152,33 +182,120 @@ namespace IPA.Logging
public DateTime Time;
}
private static ManualResetEventSlim logWaitEvent = new ManualResetEventSlim(true);
private static readonly BlockingCollection<LogMessage> logQueue = new BlockingCollection<LogMessage>();
private static Thread logThread;
private static StandardLogger loggerLogger;
private const int LogCloseTimeout = 500;
/// <summary>
/// The log printer thread for <see cref="StandardLogger"/>.
/// </summary>
private static void LogThread()
{
AppDomain.CurrentDomain.ProcessExit += (sender, args) =>
{
StopLogThread();
};
loggerLogger = new StandardLogger("Log Subsystem");
loggerLogger.printers.Clear();
var timeout = TimeSpan.FromMilliseconds(LogCloseTimeout);
var started = new HashSet<LogPrinter>();
while (logQueue.TryTake(out var msg, Timeout.Infinite)) {
foreach (var printer in msg.Logger.printers)
while (logQueue.TryTake(out var msg, Timeout.Infinite))
{
do
{
try
var logger = msg.Logger;
IEnumerable<LogPrinter> printers = logger.printers;
do
{
if (((byte)msg.Level & (byte)printer.Filter) != 0)
logger = logger.parent;
if (logger != null)
printers = printers.Concat(logger.printers);
} while (logger != null);
foreach (var printer in printers.Concat(defaultPrinters))
{
try
{
if (!started.Contains(printer))
if (((byte) msg.Level & (byte) printer.Filter) != 0)
{
printer.StartPrint();
started.Add(printer);
if (!started.Contains(printer))
{
printer.StartPrint();
started.Add(printer);
}
printer.LastUse = DateTime.Now;
printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message);
}
}
catch (Exception e)
{
Console.WriteLine($"printer errored: {e}");
}
}
printer.Print(msg.Level, msg.Time, msg.Logger.logName, msg.Message);
if (logQueue.Count > 512)
{
logWaitEvent.Reset();
loggerLogger.printers.Clear();
var prints = new HashSet<LogPrinter>();
// clear the queue
while (logQueue.TryTake(out var message))
{
var messageLogger = message.Logger;
foreach (var print in messageLogger.printers)
prints.Add(print);
do
{
messageLogger = messageLogger.parent;
if (messageLogger != null)
foreach (var print in messageLogger.printers)
prints.Add(print);
} while (messageLogger != null);
}
loggerLogger.printers.AddRange(prints);
logQueue.Add(new LogMessage
{
Level = Level.Warning,
Logger = loggerLogger,
Message = $"{loggerLogger.logName.ToUpper()}: Messages omitted to improve performance",
Time = DateTime.Now
});
logWaitEvent.Set();
}
catch (Exception e)
var now = DateTime.Now;
var copy = new List<LogPrinter>(started);
foreach (var printer in copy)
{
Console.WriteLine($"printer errored: {e}");
// close printer after 500ms from its last use
if (now - printer.LastUse > timeout)
{
try
{
printer.EndPrint();
}
catch (Exception e)
{
Console.WriteLine($"printer errored: {e}");
}
started.Remove(printer);
}
}
}
// wait for messages for 500ms before ending the prints
while (logQueue.TryTake(out msg, timeout));
if (logQueue.Count == 0)
{
@ -198,6 +315,9 @@ namespace IPA.Logging
}
}
/// <summary>
/// Stops and joins the log printer thread.
/// </summary>
internal static void StopLogThread()
{
logQueue.CompleteAdding();
@ -211,7 +331,7 @@ namespace IPA.Logging
public static class LoggerExtensions
{
/// <summary>
/// Gets a child logger, if supported.
/// Gets a child logger, if supported. Currently the only defined and supported logger is <see cref="StandardLogger"/>, and most plugins will only ever receive this anyway.
/// </summary>
/// <param name="logger">the parent <see cref="Logger"/></param>
/// <param name="name">the name of the child</param>
@ -219,13 +339,9 @@ namespace IPA.Logging
public static Logger GetChildLogger(this Logger logger, string name)
{
if (logger is StandardLogger standardLogger)
{
return standardLogger.GetChild(name);
}
else
{
throw new InvalidOperationException();
}
throw new InvalidOperationException();
}
}
}
}

IPA.Loader/Logging/UnityLogInterceptor.cs → IPA.Loader/Logging/UnityLogProvider.cs View File

@ -2,11 +2,14 @@
namespace IPA.Logging
{
internal static class UnityLogInterceptor
internal static class UnityLogProvider
{
internal static Logger Logger;
public static Logger UnityLogger => Logger ?? (Logger = new StandardLogger("UnityEngine"));
}
internal static class UnityLogRedirector
{
public static Logger.Level LogTypeToLevel(LogType type)
{
switch (type)

+ 4
- 0
IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs View File

@ -1,4 +1,7 @@
// ReSharper disable CheckNamespace
using System;
namespace IPA
{
/// <summary>
@ -10,6 +13,7 @@ namespace IPA
/// Gets a list of executables this plugin should be executed on (without the file ending)
/// </summary>
/// <example>{ "PlayClub", "PlayClubStudio" }</example>
[Obsolete("Ignored.")]
string[] Filter { get; }
/// <summary>


+ 0
- 25
IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs View File

@ -1,25 +0,0 @@
using System;
using Newtonsoft.Json;
using SemVer;
using static IPA.Updating.ModSaber.ApiEndpoint.Mod;
namespace IPA.Updating.Converters
{
internal class ModSaberDependencyConverter : JsonConverter<Dependency>
{
public override Dependency ReadJson(JsonReader reader, Type objectType, Dependency existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var parts = (reader.Value as string)?.Split('@');
return new Dependency
{
Name = parts?[0],
VersionRange = new Range(parts?[1])
};
}
public override void WriteJson(JsonWriter writer, Dependency value, JsonSerializer serializer)
{
writer.WriteValue($"{value.Name}@{value.VersionRange}");
}
}
}

+ 5
- 6
IPA.Loader/Updating/ModSaber/ApiEndpoint.cs View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using IPA.Updating.Converters;
using IPA.JsonConverters;
using IPA.Utilities;
using Newtonsoft.Json;
using SemVer;
@ -32,7 +32,7 @@ namespace IPA.Updating.ModSaber
{
try
{
return LoneFunctions.StringToByteArray((string)reader.Value);
return Utils.StringToByteArray((string)reader.Value);
}
catch (Exception ex)
{
@ -55,7 +55,7 @@ namespace IPA.Updating.ModSaber
{
throw new JsonSerializationException("Expected byte[] object value");
}
writer.WriteValue(LoneFunctions.ByteArrayToString((byte[]) value));
writer.WriteValue(Utils.ByteArrayToString((byte[]) value));
}
}
}
@ -120,8 +120,7 @@ namespace IPA.Updating.ModSaber
public string Manifest;
}
[JsonProperty("gameVersion"),
JsonConverter(typeof(SemverVersionConverter))]
[JsonProperty("gameVersion")]
public GameVersionType GameVersion;
#pragma warning restore CS0649
@ -139,7 +138,7 @@ namespace IPA.Updating.ModSaber
public string DownloadPath;
public override string ToString() =>
$"{LoneFunctions.ByteArrayToString(Hash)}@{DownloadPath}({string.Join(",", FileHashes.Select(o => $"\"{o.Key}\":\"{LoneFunctions.ByteArrayToString(o.Value)}\""))})";
$"{Utils.ByteArrayToString(Hash)}@{DownloadPath}({string.Join(",", FileHashes.Select(o => $"\"{o.Key}\":\"{Utils.ByteArrayToString(o.Value)}\""))})";
}
[Serializable]


+ 28
- 30
IPA.Loader/Updating/ModSaber/Updater.cs View File

@ -10,6 +10,7 @@ using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Ionic.Zip;
using IPA.Loader;
using IPA.Utilities;
using Newtonsoft.Json;
using SemVer;
@ -55,14 +56,14 @@ namespace IPA.Updating.ModSaber
public Version Version { get; set; }
public Version ResolvedVersion { get; set; }
public Range Requirement { get; set; }
public Range Conflicts { get; set; }
public Range Conflicts { get; set; } // a range of versions that are not allowed to be downloaded
public bool Resolved { get; set; }
public bool Has { get; set; }
public HashSet<string> Consumers { get; set; } = new HashSet<string>();
public bool MetaRequestFailed { get; set; }
public PluginInfo LocalPluginMeta { get; set; }
public PluginLoader.PluginInfo LocalPluginMeta { get; set; }
public override string ToString()
{
@ -169,13 +170,13 @@ namespace IPA.Updating.ModSaber
foreach (var plugin in BSMetas)
{ // initialize with data to resolve (1.1)
if (plugin.ModSaberInfo != null)
if (plugin.Metadata.Id != null)
{ // updatable
var msinfo = plugin.ModSaberInfo;
var msinfo = plugin.Metadata;
depList.Value.Add(new DependencyObject {
Name = msinfo.InternalName,
Version = msinfo.SemverVersion,
Requirement = new Range($">={msinfo.CurrentVersion}"),
Name = msinfo.Id,
Version = msinfo.Version,
Requirement = new Range($">={msinfo.Version}"),
LocalPluginMeta = plugin
});
}
@ -204,10 +205,8 @@ namespace IPA.Updating.ModSaber
var dep = list.Value[i];
var mod = new Ref<ApiEndpoint.Mod>(null);
#region TEMPORARY get latest // SHOULD BE GREATEST OF VERSION // not going to happen because of disagreements with ModSaber
yield return GetModInfo(dep.Name, "", mod);
#endregion
try { mod.Verify(); }
catch (Exception e)
@ -277,10 +276,12 @@ namespace IPA.Updating.ModSaber
continue;
}
var ver = modsMatching.Value.Where(nullCheck => nullCheck != null)
.Where(versionCheck => versionCheck.GameVersion.Version == BeatSaber.GameVersion && versionCheck.Approval.Status)
.Where(conflictsCheck => dep.Conflicts == null || !dep.Conflicts.IsSatisfied(conflictsCheck.Version))
.Select(mod => mod.Version).Max(); // (2.1)
var ver = modsMatching.Value
.Where(nullCheck => nullCheck != null) // entry is not null
.Where(versionCheck => versionCheck.GameVersion.Version == BeatSaber.GameVersion) // game version matches
.Where(approvalCheck => approvalCheck.Approval.Status) // version approved
.Where(conflictsCheck => dep.Conflicts == null || !dep.Conflicts.IsSatisfied(conflictsCheck.Version)) // not a conflicting version
.Select(mod => mod.Version).Max(); // (2.1) get the max version
// ReSharper disable once AssignmentInConditionalExpression
if (dep.Resolved = ver != null) dep.ResolvedVersion = ver; // (2.2)
dep.Has = dep.Version == dep.ResolvedVersion && dep.Resolved; // dep.Version is only not null if its already installed
@ -310,11 +311,6 @@ namespace IPA.Updating.ModSaber
Logger.updater.Debug($"To Download {string.Join(", ", toDl.Select(d => $"{d.Name}@{d.ResolvedVersion}"))}");
string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
Logger.updater.Debug($"Temp directory: {tempDirectory}");
foreach (var item in toDl)
StartCoroutine(UpdateModCoroutine(item));
}
@ -462,7 +458,7 @@ namespace IPA.Updating.ModSaber
var data = stream.GetBuffer();
SHA1 sha = new SHA1CryptoServiceProvider();
var hash = sha.ComputeHash(data);
if (!LoneFunctions.UnsafeCompare(hash, fileInfo.Hash))
if (!Utils.UnsafeCompare(hash, fileInfo.Hash))
throw new Exception("The hash for the file doesn't match what is defined");
var targetDir = Path.Combine(BeatSaber.InstallPath, "IPA", Path.GetRandomFileName() + "_Pending");
@ -474,7 +470,7 @@ namespace IPA.Updating.ModSaber
try
{
bool shouldDeleteOldFile = !(item.LocalPluginMeta?.Plugin is SelfPlugin);
bool shouldDeleteOldFile = !(item.LocalPluginMeta?.Metadata.IsSelf).Unwrap();
using (var zipFile = ZipFile.Read(stream))
{
@ -498,7 +494,7 @@ namespace IPA.Updating.ModSaber
try
{
if (!LoneFunctions.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName]))
if (!Utils.UnsafeCompare(fileHash, fileInfo.FileHashes[entry.FileName]))
throw new Exception("The hash for the file doesn't match what is defined");
}
catch (KeyNotFoundException)
@ -510,7 +506,7 @@ namespace IPA.Updating.ModSaber
FileInfo targetFile = new FileInfo(Path.Combine(targetDir, entry.FileName));
Directory.CreateDirectory(targetFile.DirectoryName ?? throw new InvalidOperationException());
if (LoneFunctions.GetRelativePath(targetFile.FullName, targetDir) == LoneFunctions.GetRelativePath(item.LocalPluginMeta?.Filename, BeatSaber.InstallPath))
if (Utils.GetRelativePath(targetFile.FullName, targetDir) == Utils.GetRelativePath(item.LocalPluginMeta?.Metadata.File.FullName, BeatSaber.InstallPath))
shouldDeleteOldFile = false; // overwriting old file, no need to delete
/*if (targetFile.Exists)
@ -529,7 +525,7 @@ namespace IPA.Updating.ModSaber
}
if (shouldDeleteOldFile && item.LocalPluginMeta != null)
File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { LoneFunctions.GetRelativePath(item.LocalPluginMeta.Filename, BeatSaber.InstallPath) });
File.AppendAllLines(Path.Combine(targetDir, SpecialDeletionsFile), new[] { Utils.GetRelativePath(item.LocalPluginMeta?.Metadata.File.FullName, BeatSaber.InstallPath) });
}
catch (Exception)
{ // something failed; restore
@ -542,19 +538,21 @@ namespace IPA.Updating.ModSaber
throw;
}
if (item.LocalPluginMeta?.Plugin is SelfPlugin)
if ((item.LocalPluginMeta?.Metadata.IsSelf).Unwrap())
{ // currently updating self, so copy to working dir and update
LoneFunctions.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(BeatSaber.InstallPath));
if (File.Exists(Path.Combine(BeatSaber.InstallPath, SpecialDeletionsFile))) File.Delete(Path.Combine(BeatSaber.InstallPath, SpecialDeletionsFile));
Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(BeatSaber.InstallPath));
var deleteFile = Path.Combine(BeatSaber.InstallPath, SpecialDeletionsFile);
if (File.Exists(deleteFile)) File.Delete(deleteFile);
Process.Start(new ProcessStartInfo
{
FileName = item.LocalPluginMeta.Filename,
// will never actually be null
FileName = item.LocalPluginMeta?.Metadata.File.FullName ?? throw new InvalidOperationException(),
Arguments = $"-nw={Process.GetCurrentProcess().Id}",
UseShellExecute = false
});
}
else
LoneFunctions.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(eventualOutput), SpecialDeletionsFile);
Utils.CopyAll(new DirectoryInfo(targetDir), new DirectoryInfo(eventualOutput), SpecialDeletionsFile);
Directory.Delete(targetDir, true); // delete extraction site
Logger.updater.Debug("Extractor exited");


+ 8
- 8
IPA.Loader/Updating/SelfPlugin.cs View File

@ -1,21 +1,21 @@
using UnityEngine.SceneManagement;
using IPA.Config;
using System;
using UnityEngine.SceneManagement;
namespace IPA.Updating
{
[Obsolete("Only used for old updating system, replaced with a PluginMeta for the embedded manifest")]
internal class SelfPlugin : IBeatSaberPlugin
{
internal const string IPA_Name = "Beat Saber IPA";
internal const string IPA_Version = "3.11.6";
public static SelfPlugin Instance { get; set; } = new SelfPlugin();
public string Name => IPA_Name;
public string Name => SelfConfig.IPA_Name;
public string Version => IPA_Version;
public string Version => SelfConfig.IPA_Version;
public ModsaberModInfo ModInfo => new ModsaberModInfo
{
CurrentVersion = IPA_Version,
CurrentVersion = SelfConfig.IPA_Version,
InternalName = "beatsaber-ipa-reloaded"
};
@ -47,4 +47,4 @@ namespace IPA.Updating
{
}
}
}
}

+ 8
- 0
IPA.Loader/Utilities/BeatSaber.cs View File

@ -48,6 +48,14 @@ namespace IPA.Utilities
/// The path to the `Libs\Native` folder. Use only if necessary.
/// </summary>
public static string NativeLibraryPath => Path.Combine(LibraryPath, "Native");
/// <summary>
/// The directory to load plugins from.
/// </summary>
public static string PluginsPath => Path.Combine(InstallPath, "Plugins");
/// <summary>
/// The path to the `UserData` folder.
/// </summary>
public static string UserDataPath => Path.Combine(InstallPath, "UserData");
private static bool FindSteamVRAsset()
{


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

@ -16,5 +16,12 @@ namespace IPA.Utilities
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
}
/// <summary>
/// Unwraps a <see cref="Nullable{T}"/> where T is <see cref="bool"/> such that if the value is null, it gives <see langword="false"/>.
/// </summary>
/// <param name="self">the bool? to unwrap</param>
/// <returns>the unwrapped value, or <see langword="false"/> if it was <see langword="null"/></returns>
public static bool Unwrap(this bool? self) => self != null && self.Value;
}
}

+ 32
- 1
IPA.Loader/Utilities/Ref.cs View File

@ -4,11 +4,28 @@ using System.Reflection;
namespace IPA.Utilities
{
/// <summary>
/// Utilities to create <see cref="Ref{T}"/> using type inference.
/// </summary>
public static class Ref
{
/// <summary>
/// Creates a <see cref="Ref{T}"/>.
/// </summary>
/// <typeparam name="T">the type to reference.</typeparam>
/// <param name="val">the default value.</param>
/// <returns>the new <see cref="Ref{T}"/>.</returns>
public static Ref<T> Create<T>(T val)
{
return new Ref<T>(val);
}
}
/// <summary>
/// A class to store a reference for passing to methods which cannot take ref parameters.
/// </summary>
/// <typeparam name="T">the type of the value</typeparam>
public class Ref<T>
public class Ref<T> : IComparable<T>, IComparable<Ref<T>>
{
private T _value;
/// <summary>
@ -76,6 +93,20 @@ namespace IPA.Utilities
{
if (Error != null) throw Error;
}
/// <inheritdoc />
public int CompareTo(T other)
{
if (Value is IComparable<T> compare)
return compare.CompareTo(other);
return Equals(Value, other) ? 0 : -1;
}
/// <inheritdoc />
public int CompareTo(Ref<T> other)
{
return CompareTo(other.Value);
}
}
internal static class ExceptionUtilities


IPA.Loader/Utilities/LoneFunctions.cs → IPA.Loader/Utilities/Utils.cs View File

@ -7,7 +7,7 @@ namespace IPA.Utilities
/// <summary>
/// A class providing static utility functions that in any other language would just *exist*.
/// </summary>
public static class LoneFunctions
public static class Utils
{
/// <summary>
/// Converts a hex string to a byte array.

+ 1
- 3
IPA/IPA.csproj View File

@ -100,8 +100,6 @@
<Compile Include="PatchContext.cs" />
<Compile Include="Patcher\BackupManager.cs" />
<Compile Include="Patcher\BackupUnit.cs" />
<Compile Include="Patcher\Patcher.cs" />
<Compile Include="Patcher\Virtualizer.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Shortcut.cs" />
@ -115,7 +113,7 @@
<ItemGroup />
<ItemGroup>
<PackageReference Include="Mono.Cecil">
<Version>0.10.1</Version>
<Version>0.10.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>


+ 0
- 134
IPA/Patcher/Patcher.cs View File

@ -1,134 +0,0 @@
using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace IPA.Patcher
{
internal class PatchedModule
{
private static readonly string[] EntryTypes = { "Input", "Display" };
private readonly FileInfo _file;
private ModuleDefinition _module;
internal struct PatchData {
public bool IsPatched;
public Version Version;
}
public static PatchedModule Load(string engineFile)
{
return new PatchedModule(engineFile);
}
private PatchedModule(string engineFile)
{
_file = new FileInfo(engineFile);
LoadModules();
}
private void LoadModules()
{
var resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(_file.DirectoryName);
var parameters = new ReaderParameters
{
AssemblyResolver = resolver,
};
_module = ModuleDefinition.ReadModule(_file.FullName, parameters);
}
public PatchData Data
{
get
{
var data = new PatchData { IsPatched = false, Version = null };
foreach (var @ref in _module.AssemblyReferences) {
switch (@ref.Name)
{
case "IllusionInjector":
case "IllusionPlugin":
data = new PatchData { IsPatched = true, Version = new Version(0, 0, 0, 0) };
break;
case "IPA.Injector":
return new PatchData { IsPatched = true, Version = @ref.Version };
}
}
return data;
}
}
public void Patch(Version v)
{
// First, let's add the reference
var nameReference = new AssemblyNameReference("IPA.Injector", v);
var injectorPath = Path.Combine(_file.DirectoryName ?? throw new InvalidOperationException(), "IPA.Injector.dll");
var injector = ModuleDefinition.ReadModule(injectorPath);
bool hasIPAInjector = false;
for (int i = 0; i < _module.AssemblyReferences.Count; i++)
{
if (_module.AssemblyReferences[i].Name == "IllusionInjector")
_module.AssemblyReferences.RemoveAt(i--);
if (_module.AssemblyReferences[i].Name == "IllusionPlugin")
_module.AssemblyReferences.RemoveAt(i--);
if (_module.AssemblyReferences[i].Name == "IPA.Injector")
{
hasIPAInjector = true;
_module.AssemblyReferences[i].Version = v;
}
}
if (!hasIPAInjector)
{
_module.AssemblyReferences.Add(nameReference);
int patched = 0;
foreach (var type in FindEntryTypes())
{
if (PatchType(type, injector))
{
patched++;
}
}
if (patched > 0)
{
_module.Write(_file.FullName);
}
else
{
throw new Exception("Could not find any entry type!");
}
}
else
{
_module.Write(_file.FullName);
}
}
private bool PatchType(TypeDefinition targetType, ModuleDefinition injector)
{
var targetMethod = targetType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
if (targetMethod != null)
{
var methodReference = _module.ImportReference(injector.GetType("IPA.Injector.Injector").Methods.First(m => m.Name == "Inject"));
targetMethod.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, methodReference));
return true;
}
return false;
}
private IEnumerable<TypeDefinition> FindEntryTypes()
{
return _module.GetTypes().Where(m => EntryTypes.Contains(m.Name));
}
}
}

+ 0
- 116
IPA/Patcher/Virtualizer.cs View File

@ -1,116 +0,0 @@
using Mono.Cecil;
using System;
using System.IO;
using System.Linq;
namespace IPA.Patcher
{
class VirtualizedModule
{
private readonly FileInfo _file;
private ModuleDefinition _module;
public static VirtualizedModule Load(string engineFile)
{
return new VirtualizedModule(engineFile);
}
private VirtualizedModule(string assemblyFile)
{
_file = new FileInfo(assemblyFile);
LoadModules();
}
private void LoadModules()
{
var resolver = new DefaultAssemblyResolver();
resolver.AddSearchDirectory(_file.DirectoryName);
var parameters = new ReaderParameters
{
AssemblyResolver = resolver,
};
_module = ModuleDefinition.ReadModule(_file.FullName, parameters);
}
/// <summary>
///
/// </summary>
public void Virtualize()
{
foreach (var type in _module.Types)
{
VirtualizeType(type);
}
Console.WriteLine();
_module.Write(_file.FullName);
}
private void VirtualizeType(TypeDefinition type)
{
if(type.IsSealed)
{
// Unseal
type.IsSealed = false;
}
if (type.IsInterface) return;
if (type.IsAbstract) return;
// These two don't seem to work.
if (type.Name == "SceneControl" || type.Name == "ConfigUI") return;
//Console.CursorTop--;
Console.CursorLeft = 0;
Program.ClearLine();
Console.Write("Virtualizing {0}", type.Name);
// Take care of sub types
foreach (var subType in type.NestedTypes)
{
VirtualizeType(subType);
}
foreach (var method in type.Methods)
{
if (method.IsManaged
&& method.IsIL
&& !method.IsStatic
&& !method.IsVirtual
&& !method.IsAbstract
&& !method.IsAddOn
&& !method.IsConstructor
&& !method.IsSpecialName
&& !method.IsGenericInstance
&& !method.HasOverrides)
{
method.IsVirtual = true;
method.IsPublic = true;
method.IsPrivate = false;
method.IsNewSlot = true;
method.IsHideBySig = true;
}
}
foreach (var field in type.Fields)
{
if (field.IsPrivate) field.IsFamily = true;
}
}
public bool IsVirtualized
{
get
{
var awakeMethods = _module.GetTypes().SelectMany(t => t.Methods.Where(m => m.Name == "Awake"));
var methodDefinitions = awakeMethods as MethodDefinition[] ?? awakeMethods.ToArray();
if (!methodDefinitions.Any()) return false;
return ((float)methodDefinitions.Count(m => m.IsVirtual) / methodDefinitions.Count()) > 0.5f;
}
}
}
}

+ 28
- 143
IPA/Program.cs View File

@ -32,12 +32,12 @@ namespace IPA
public static readonly ArgumentFlag ArgNoWait = new ArgumentFlag("--nowait", "-n") { DocString = "doesn't wait for user input after the operation" };
public static readonly ArgumentFlag ArgStart = new ArgumentFlag("--start", "-s") { DocString = "uses value_ as arguments to start the game after the patch/unpatch", ValueString = "ARGUMENTS" };
public static readonly ArgumentFlag ArgLaunch = new ArgumentFlag("--launch", "-l") { DocString = "uses positional parameters as arguments to start the game after patch/unpatch" };
public static readonly ArgumentFlag ArgDestructive = new ArgumentFlag("--destructive", "-d") { DocString = "patches the game using the now outdated destructive methods" };
//public static readonly ArgumentFlag ArgDestructive = new ArgumentFlag("--destructive", "-d") { DocString = "patches the game using the now outdated destructive methods" };
[STAThread]
public static void Main(string[] args)
{
Arguments.CmdLine.Flags(ArgHelp, ArgWaitFor, ArgForce, ArgRevert, ArgNoWait, ArgStart, ArgLaunch, ArgDestructive).Process();
Arguments.CmdLine.Flags(ArgHelp, ArgWaitFor, ArgForce, ArgRevert, ArgNoWait, ArgStart, ArgLaunch/*, ArgDestructive*/).Process();
if (ArgHelp)
{
@ -139,132 +139,35 @@ namespace IPA
try
{
var backup = new BackupUnit(context);
if (ArgDestructive)
{
#region Patch Version Check
var patchedModule = PatchedModule.Load(context.EngineFile);
#if DEBUG
var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) >= 0;
#else
var isCurrentNewer = Version.CompareTo(patchedModule.Data.Version) > 0;
#endif
Console.WriteLine($"Current: {Version} Patched: {patchedModule.Data.Version}");
if (isCurrentNewer)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(
$"Preparing for update, {(patchedModule.Data.Version == null ? "UnPatched" : patchedModule.Data.Version.ToString())} => {Version}");
Console.WriteLine("--- Starting ---");
Revert(context);
Console.ResetColor();
#region File Copying
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine("Updating files... ");
var nativePluginFolder = Path.Combine(context.DataPathDst, "Plugins");
bool isFlat = Directory.Exists(nativePluginFolder) &&
Directory.GetFiles(nativePluginFolder).Any(f => f.EndsWith(".dll"));
bool force = !BackupManager.HasBackup(context) || ArgForce;
var architecture = DetectArchitecture(context.Executable);
Console.WriteLine("Architecture: {0}", architecture);
CopyAll(new DirectoryInfo(context.DataPathSrc), new DirectoryInfo(context.DataPathDst), force,
backup,
(from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
architecture));
CopyAll(new DirectoryInfo(context.LibsPathSrc), new DirectoryInfo(context.LibsPathDst), force,
backup,
(from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
architecture));
Console.WriteLine("Successfully updated files!");
#endregion
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Files up to date @ Version {Version}!");
Console.ResetColor();
}
#endregion
#region Patching
if (!patchedModule.Data.IsPatched || isCurrentNewer)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Patching UnityEngine.dll with Version {Application.ProductVersion}... ");
backup.Add(context.EngineFile);
patchedModule.Patch(Version);
Console.WriteLine("Done!");
Console.ResetColor();
}
#endregion
#region Creating shortcut
if (!File.Exists(context.ShortcutPath))
{
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine("Creating shortcut to IPA ({0})... ", context.IPA);
try
{
Shortcut.Create(
fileName: context.ShortcutPath,
targetPath: context.IPA,
arguments: Args(context.Executable, "-ln"),
workingDirectory: context.ProjectRoot,
description: "Launches the game and makes sure it's in a patched state",
hotkey: "",
iconPath: context.Executable
);
}
catch (Exception)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine("Failed to create shortcut, but game was patched!");
}
Console.ResetColor();
}
#endregion
}
else
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Restoring old version... ");
if (BackupManager.HasBackup(context))
BackupManager.Restore(context);
var nativePluginFolder = Path.Combine(context.DataPathDst, "Plugins");
bool isFlat = Directory.Exists(nativePluginFolder) &&
Directory.GetFiles(nativePluginFolder).Any(f => f.EndsWith(".dll"));
bool force = !BackupManager.HasBackup(context) || ArgForce;
var architecture = DetectArchitecture(context.Executable);
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine("Installing files... ");
CopyAll(new DirectoryInfo(context.DataPathSrc), new DirectoryInfo(context.DataPathDst), force,
backup,
(from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
architecture));
CopyAll(new DirectoryInfo(context.LibsPathSrc), new DirectoryInfo(context.LibsPathDst), force,
backup,
(from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
architecture));
CopyAll(new DirectoryInfo(context.IPARoot), new DirectoryInfo(context.ProjectRoot), force,
backup,
null, false);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine("Restoring old version... ");
if (BackupManager.HasBackup(context))
BackupManager.Restore(context);
var nativePluginFolder = Path.Combine(context.DataPathDst, "Plugins");
bool isFlat = Directory.Exists(nativePluginFolder) &&
Directory.GetFiles(nativePluginFolder).Any(f => f.EndsWith(".dll"));
bool force = !BackupManager.HasBackup(context) || ArgForce;
var architecture = DetectArchitecture(context.Executable);
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine("Installing files... ");
CopyAll(new DirectoryInfo(context.DataPathSrc), new DirectoryInfo(context.DataPathDst), force,
backup,
(from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
architecture));
CopyAll(new DirectoryInfo(context.LibsPathSrc), new DirectoryInfo(context.LibsPathDst), force,
backup,
(from, to) => NativePluginInterceptor(from, to, new DirectoryInfo(nativePluginFolder), isFlat,
architecture));
CopyAll(new DirectoryInfo(context.IPARoot), new DirectoryInfo(context.ProjectRoot), force,
backup,
null, false);
//backup.Add(context.AssemblyFile);
//backup.Add(context.EngineFile);
}
#region Create Plugin Folder
@ -278,24 +181,6 @@ namespace IPA
#endregion
#region Virtualizing
if (ArgDestructive && File.Exists(context.AssemblyFile))
{
var virtualizedModule = VirtualizedModule.Load(context.AssemblyFile);
if (!virtualizedModule.IsVirtualized)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Virtualizing Assembly-Csharp.dll... ");
backup.Add(context.AssemblyFile);
virtualizedModule.Virtualize();
Console.WriteLine("Done!");
Console.ResetColor();
}
}
#endregion
}
catch (Exception e)
{


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

@ -1,10 +1,10 @@
using System.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// 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("Illusion Plugin Architecture")]
[assembly: AssemblyTitle("IPA.Installer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
@ -13,8 +13,8 @@ using System.Runtime.InteropServices;
[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
// 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)]
@ -24,12 +24,12 @@ using System.Runtime.InteropServices;
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// 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.11.6")]
[assembly: AssemblyFileVersion("3.11.6")]
[assembly: AssemblyVersion("3.12.0")]
[assembly: AssemblyFileVersion("3.12.0")]

+ 1
- 1
IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache View File

@ -1 +1 @@
27a27b93f9cca058d6b4f09d77cb8a300416a979
5d76d76cc5c14257f2b9071c928c27b3edd80cc0

BIN
Libs/Mono.Debugger.Soft.dll View File


+ 0
- 73
MSBuildTasks/AssemblyRenameTask.cs View File

@ -1,73 +0,0 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Mono.Cecil;
using System;
using System.IO;
namespace MSBuildTasks
{
public class AssemblyRename : Task
{
[Required]
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public ITaskItem[] Assemblies { get; set; }
public override bool Execute()
{
foreach (ITaskItem assembly in Assemblies)
{
// ItemSpec holds the filename or path of an Item
if (assembly.ItemSpec.Length > 0)
{
if (!File.Exists(assembly.ItemSpec))
{
Log.LogMessage(MessageImportance.Normal, "No file at " + assembly.ItemSpec);
continue;
}
if (Path.GetExtension(assembly.ItemSpec) != ".dll")
{
Log.LogMessage(MessageImportance.Normal, assembly.ItemSpec + " not a DLL");
continue;
}
try
{
Log.LogMessage(MessageImportance.Normal, "Reading " + assembly.ItemSpec);
var module = ModuleDefinition.ReadModule(assembly.ItemSpec);
var asmName = module.Assembly.Name;
var name = asmName.Name;
var version = asmName.Version;
var newFilen = $"{name}.{version}.dll";
var newFilePath = Path.Combine(Path.GetDirectoryName(assembly.ItemSpec) ?? throw new InvalidOperationException(), newFilen);
module.Dispose();
Log.LogMessage(MessageImportance.Normal, $"Old file: {assembly.ItemSpec}, new file: {newFilePath}");
if (File.Exists(newFilePath))
File.Delete(newFilePath);
Log.LogMessage(MessageImportance.Normal, "Moving");
try
{
File.Move(assembly.ItemSpec, newFilePath);
}
catch (Exception)
{
File.Copy(assembly.ItemSpec, newFilePath);
File.Delete(assembly.ItemSpec);
}
}
catch (Exception e)
{
Log.LogErrorFromException(e);
}
}
}
return !Log.HasLoggedErrors;
}
}
}

+ 0
- 84
MSBuildTasks/MSBuildTasks.csproj View File

@ -1,84 +0,0 @@
<?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>{F08C3C7A-3221-432E-BAB8-32BCE58408C8}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MSBuildTasks</RootNamespace>
<AssemblyName>MSBuildTasks</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<TargetFrameworkProfile />
</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>
<UseVSHostingProcess>true</UseVSHostingProcess>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Build.Framework.15.9.20\lib\net46\Microsoft.Build.Framework.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Build.Utilities.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Build.Utilities.Core.15.9.20\lib\net46\Microsoft.Build.Utilities.Core.dll</HintPath>
</Reference>
<Reference Include="Microsoft.VisualStudio.Setup.Configuration.Interop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.16.30\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll</HintPath>
<EmbedInteropTypes>True</EmbedInteropTypes>
</Reference>
<Reference Include="Mono.Cecil, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.dll</HintPath>
</Reference>
<Reference Include="Mono.Cecil.Mdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Mdb.dll</HintPath>
</Reference>
<Reference Include="Mono.Cecil.Pdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Pdb.dll</HintPath>
</Reference>
<Reference Include="Mono.Cecil.Rocks, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Rocks.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Collections.Immutable, Version=1.2.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Collections.Immutable.1.5.0\lib\netstandard1.3\System.Collections.Immutable.dll</HintPath>
</Reference>
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Xaml" />
<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" />
</ItemGroup>
<ItemGroup>
<Compile Include="AssemblyRenameTask.cs" />
<Compile Include="Pdb2Mdb.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

+ 0
- 79
MSBuildTasks/Pdb2Mdb.cs View File

@ -1,79 +0,0 @@
using System;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace MSBuildTasks
{
public class PdbToMdb : Task
{
[Required]
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public ITaskItem[] Binaries { get; set; }
public override bool Execute()
{
//var readerProvider = new PdbReaderProvider();
//var writerProvider = new MdbWriterProvider();
foreach (ITaskItem dll in Binaries)
{
// ItemSpec holds the filename or path of an Item
if (dll.ItemSpec.Length > 0)
{
if (!File.Exists(dll.ItemSpec))
{
Log.LogMessage(MessageImportance.Normal, "No file at " + dll.ItemSpec);
continue;
}
if (Path.GetExtension(dll.ItemSpec) != ".dll" && Path.GetExtension(dll.ItemSpec) != ".pdb")
{
Log.LogMessage(MessageImportance.Normal, dll.ItemSpec + " not a DLL or PDB");
continue;
}
try
{
/*Log.LogMessage(MessageImportance.Normal, "Processing PDB for " + dll.ItemSpec);
var path = Path.ChangeExtension(dll.ItemSpec, ".dll");
var module = ModuleDefinition.ReadModule(path);
var reader = readerProvider.GetSymbolReader(module, path);
var writer = writerProvider.GetSymbolWriter(module, path);
foreach (var type in module.Types)
foreach (var method in type.Methods)
{
var read = reader.Read(method);
if (read == null) Log.LogWarning($"Method {module.FileName} -> {method.FullName} read from PDB as null");
else writer.Write(read);
}
writer.Dispose();
reader.Dispose();
module.Dispose();*/
var path = Path.ChangeExtension(dll.ItemSpec, ".dll");
Log.LogMessage(MessageImportance.Normal, "Processing PDB for " + path);
/*Process.Start(new ProcessStartInfo
{
WorkingDirectory = Path.GetDirectoryName(path) ?? throw new InvalidOperationException(),
FileName = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().CodeBase) ?? throw new InvalidOperationException(), "pdb2mdb.exe"),
Arguments = Path.GetFileName(path)
});*/
//Pdb2Mdb.Converter.Convert(path);
}
catch (Exception e)
{
Log.LogErrorFromException(e);
Log.LogError(e.ToString());
}
}
}
return !Log.HasLoggedErrors;
}
}
}

+ 0
- 35
MSBuildTasks/Properties/AssemblyInfo.cs View File

@ -1,35 +0,0 @@
using System.Reflection;
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("MSBuildTasks")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MSBuildTasks")]
[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("f08c3c7a-3221-432e-bab8-32bce58408c8")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

+ 0
- 9
MSBuildTasks/packages.config View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Build.Framework" version="15.9.20" targetFramework="net46" />
<package id="Microsoft.Build.Utilities.Core" version="15.9.20" targetFramework="net46" />
<package id="Microsoft.VisualStudio.Setup.Configuration.Interop" version="1.16.30" targetFramework="net46" developmentDependency="true" />
<package id="Mono.Cecil" version="0.10.1" targetFramework="net46" />
<package id="System.Collections.Immutable" version="1.5.0" targetFramework="net46" />
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net46" />
</packages>

+ 28
- 8
README.md View File

@ -18,7 +18,17 @@ To disable this console window, pass `--no-console` to the game.
1. Drag & drop the game exe onto **IPA.exe** while holding <kbd>Alt</kbd>
- Or run `ipa -rn` in a command window
2. Done
## Arguments
`IPA.exe file-to-patch [arguments]`
- `--launch`: Launch the game after patching
- `--revert`: Revert changes made by IPA (= unpatch the game)
- `--nowait`: Never keep the console open
- See `-h` or `--help` for more options.
Unconsumed arguments will be passed on to the game in case of `--launch`.
## How To Develop
@ -33,13 +43,23 @@ See [Developing](https://github.com/nike4613/BeatSaber-IPA-Reloaded/wiki/Develop
BSIPA will automatically repatch the game when it updates, as long as `winhttp.dll` is present in the install directory.
## Arguments
## Building
`IPA.exe file-to-patch [arguments]`
### Prerequisites
- `--launch`: Launch the game after patching
- `--revert`: Revert changes made by IPA (= unpatch the game)
- `--nowait`: Never keep the console open
- See `-h` or `--help` for more options.
- Microsoft Visual Studio 2017 or later
- Tools for C/C++ (MSVC)
- .NET 4.6 SDK and .NET 4.7.1 SDK
Unconsumed arguments will be passed on to the game in case of `--launch`.
### Building
1. Clone with `git clone https://github.com/nike4613/BeatSaber-IPA-Reloaded.git --recursive`
2. Create a file, `bsinstalldir.txt` in the solution root. Do NOT create this in Visual Studio; VS adds a BOM at the begginning of the file that the tools used cannot read. It should contain the path to your Beat Saber installation, using forward slashes with a trailing slash. e.g.
```
C:/Program Files (x86)/Steam/steamapps/common/Beat Saber/
```
3. Open `BSIPA.sln` in Visual Studio.
4. Choose the configuration `x64`
5. Rebuild all. Any time you make a change, ALWAYS Rebuild All.
When building a Debug build, all referenced assemblies from Beat Saber will be copied from the install directory provided in `bsinstalldir.txt` into `Refs/`. Any new references should reference the copy in there. When building for Release, it just uses the files already in `Refs/`

BIN
Refs/UnityEngine.CoreModule.dll View File


BIN
Refs/UnityEngine.UnityWebRequestModule.dll View File


+ 1
- 1
Refs/refs.txt View File

@ -1,4 +1,4 @@
::from ../bsinstalldir.txt
::from ./bsinstalldir.txt
"Beat Saber_Data/
""Managed/
"""UnityEngine.


+ 1
- 1
appveyor.yml View File

@ -1,6 +1,6 @@
version: 'BSIPA-{branch}-{build}'
environment:
bsipa_version: '3.11.6'
bsipa_version: '3.12.0'
pull_requests:
do_not_increment_build_number: true
install:


Loading…
Cancel
Save