diff --git a/.gitignore b/.gitignore index 1cde24bd..5faf71bf 100644 --- a/.gitignore +++ b/.gitignore @@ -250,6 +250,5 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml -/MigrationBackup/d2a2abe6/IPA.Injector /bsinstalldir.txt /.wiki diff --git a/.gitmodules b/.gitmodules index 8d6898d0..ad87e188 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/BSIPA.sln b/BSIPA.sln index 11836794..db7546b8 100644 --- a/BSIPA.sln +++ b/BSIPA.sln @@ -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} diff --git a/BSIPA.sln.DotSettings b/BSIPA.sln.DotSettings index 3bbe3c40..88825ef8 100644 --- a/BSIPA.sln.DotSettings +++ b/BSIPA.sln.DotSettings @@ -1,17 +1,29 @@  WARNING + False + False + False + False BS IPA + VR <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /></Policy> + True + True + True + True True True True True True + True True True True diff --git a/BuildTools b/BuildTools new file mode 160000 index 00000000..de919c49 --- /dev/null +++ b/BuildTools @@ -0,0 +1 @@ +Subproject commit de919c49496a7e7e11382bd876473018ddceaa1a diff --git a/CollectDependencies/App.config b/CollectDependencies/App.config deleted file mode 100644 index 56efbc7b..00000000 --- a/CollectDependencies/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/CollectDependencies/CollectDependencies.csproj b/CollectDependencies/CollectDependencies.csproj deleted file mode 100644 index 266be02a..00000000 --- a/CollectDependencies/CollectDependencies.csproj +++ /dev/null @@ -1,74 +0,0 @@ - - - - - Debug - AnyCPU - {5F33B310-DC8D-4C0D-877E-BAC3908DE10F} - Exe - CollectDependencies - CollectDependencies - v4.7.2 - 512 - true - true - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.dll - True - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Mdb.dll - True - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Pdb.dll - True - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Rocks.dll - True - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/CollectDependencies/Program.cs b/CollectDependencies/Program.cs deleted file mode 100644 index 669bcd43..00000000 --- a/CollectDependencies/Program.cs +++ /dev/null @@ -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>(); - { // Create files from stuff in depsfile - var stack = new Stack(); - - 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(Replace(path), lineNo)); - else if (level < stack.Count - 1) - { - files.Add(new Tuple(Pop(), lineNo)); - while (level < stack.Count) - Pop(); - Push(path); - } - - lineNo++; - } - - files.Add(new Tuple(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}"); - } - } - - } - } -} diff --git a/CollectDependencies/Properties/AssemblyInfo.cs b/CollectDependencies/Properties/AssemblyInfo.cs deleted file mode 100644 index 83c77d77..00000000 --- a/CollectDependencies/Properties/AssemblyInfo.cs +++ /dev/null @@ -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")] diff --git a/CollectDependencies/Virtualizer.cs b/CollectDependencies/Virtualizer.cs deleted file mode 100644 index ac9db5c2..00000000 --- a/CollectDependencies/Virtualizer.cs +++ /dev/null @@ -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); - } - - /// - /// - /// - /// - 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; - } - } - } -} diff --git a/CollectDependencies/packages.config b/CollectDependencies/packages.config deleted file mode 100644 index 353fb5d7..00000000 --- a/CollectDependencies/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Doorstop b/Doorstop index 0a350fe8..310ab026 160000 --- a/Doorstop +++ b/Doorstop @@ -1 +1 @@ -Subproject commit 0a350fe8a9792cd6708f5d5e805d82ec115c5e7d +Subproject commit 310ab026a8905588dab29f250a2385ed2cb7c41f diff --git a/IPA.Injector/Backups/BackupUnit.cs b/IPA.Injector/Backups/BackupUnit.cs index 99b2158e..d168ffe1 100644 --- a/IPA.Injector/Backups/BackupUnit.cs +++ b/IPA.Injector/Backups/BackupUnit.cs @@ -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 /// 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 diff --git a/IPA.Injector/IPA.Injector.csproj b/IPA.Injector/IPA.Injector.csproj index 117be32f..10a01011 100644 --- a/IPA.Injector/IPA.Injector.csproj +++ b/IPA.Injector/IPA.Injector.csproj @@ -13,10 +13,10 @@ 512 true $(SolutionDir)=C:\ + portable true - full false bin\Debug\ DEBUG;TRACE @@ -24,8 +24,8 @@ 4 - pdbonly true + pdbonly bin\Release\ TRACE prompt @@ -57,7 +57,6 @@ - @@ -83,7 +82,6 @@ Libraries\Mono\Microsoft.CSharp.dll - Always Libraries\Mono\System.Runtime.Serialization.dll @@ -91,9 +89,6 @@ - - 4.2.3.4 - 1.2.0 diff --git a/IPA.Injector/Injector.cs b/IPA.Injector/Injector.cs index f882f8a1..9829801c 100644 --- a/IPA.Injector/Injector.cs +++ b/IPA.Injector/Injector.cs @@ -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.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.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(); } } -} +} \ No newline at end of file diff --git a/IPA.Injector/PostBuild.msbuild b/IPA.Injector/PostBuild.msbuild index 930720a6..c2c0aa8d 100644 --- a/IPA.Injector/PostBuild.msbuild +++ b/IPA.Injector/PostBuild.msbuild @@ -5,8 +5,7 @@ - - + @@ -30,9 +29,7 @@ - - \ No newline at end of file diff --git a/IPA.Injector/Properties/AssemblyInfo.cs b/IPA.Injector/Properties/AssemblyInfo.cs index 26a9a855..0b0212a3 100644 --- a/IPA.Injector/Properties/AssemblyInfo.cs +++ b/IPA.Injector/Properties/AssemblyInfo.cs @@ -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")] \ No newline at end of file diff --git a/IPA.Injector/Updates.cs b/IPA.Injector/Updates.cs index 5c09f427..94522478 100644 --- a/IPA.Injector/Updates.cs +++ b/IPA.Injector/Updates.cs @@ -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) { diff --git a/IPA.Loader/Config/Config.cs b/IPA.Loader/Config/Config.cs new file mode 100644 index 00000000..68169fca --- /dev/null +++ b/IPA.Loader/Config/Config.cs @@ -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 +{ + /// + /// A class to handle updating ConfigProviders automatically + /// + public static class Config + { + static Config() + { + JsonConfigProvider.RegisterConfig(); + } + + /// + /// + /// Defines the type of the + /// + [AttributeUsage(AttributeTargets.Class)] + public class TypeAttribute : Attribute + { + /// + /// The extension associated with this type, without the '.' + /// + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public string Extension { get; private set; } + + /// + /// + /// Constructs the attribute with a specified extension. + /// + /// the extension associated with this type, without the '.' + public TypeAttribute(string ext) + { + Extension = ext; + } + } + + /// + /// + /// Specifies that a particular parameter is preferred to be a specific type of . If it is not available, also specifies backups. If none are available, the default is used. + /// + [AttributeUsage(AttributeTargets.Parameter)] + public class PreferAttribute : Attribute + { + /// + /// The order of preference for the config type. + /// + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public string[] PreferenceOrder { get; private set; } + + /// + /// + /// Constructs the attribute with a specific preference list. Each entry is the extension without a '.' + /// + /// The preferences in order of preference. + public PreferAttribute(params string[] preference) + { + PreferenceOrder = preference; + } + } + + /// + /// + /// Specifies a preferred config name, instead of using the plugin's name. + /// + public class NameAttribute : Attribute + { + /// + /// The name to use for the config. + /// + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public string Name { get; private set; } + + /// + /// + /// Constructs the attribute with a specific name. + /// + /// the name to use for the config. + public NameAttribute(string name) + { + Name = name; + } + } + + private static readonly Dictionary registeredProviders = new Dictionary(); + + /// + /// Registers a to use for configs. + /// + /// the type to register + public static void Register() where T : IConfigProvider => Register(typeof(T)); + + /// + /// Registers a to use for configs. + /// + /// the type to register + 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, IConfigProvider> configProviders = new SortedList, IConfigProvider>(); + + /// + /// Gets an using the specified list pf preferred config types. + /// + /// the name of the mod for this config + /// the preferred config types to try to get + /// an of the requested type, or of type JSON. + 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() is PreferAttribute prefer) + prefs = prefer.PreferenceOrder; + if (info.GetCustomAttribute() is NameAttribute name) + modName = name.Name; + + return GetProviderFor(modName, prefs); + } + + private static Dictionary linkedProviders = + new Dictionary(); + + /// + /// Creates a linked for the config provider. This will be automatically updated whenever the file on-disk changes. + /// + /// the type of the parsed value + /// the to create a link to + /// an action to perform on value change + /// a to an ever-changing value, mirroring whatever the file contains. + public static Ref MakeLink(this IConfigProvider config, Action> onChange = null) + { + Ref @ref = config.Parse(); + void ChangeDelegate() + { + @ref.Value = config.Parse(); + 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; + } + + /// + /// Removes all linked such that they are no longer updated. + /// + /// the to unlink + 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); + } + } + + } +} diff --git a/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs b/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs index 4c2eba10..be81baea 100644 --- a/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs +++ b/IPA.Loader/Config/ConfigProviders/JsonConfigProvider.cs @@ -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(); + } + 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() { + if (jsonObj == null) + return default(T); return jsonObj.ToObject(); } 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; } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Config/IConfigProvider.cs b/IPA.Loader/Config/IConfigProvider.cs index d26816d3..88f23efe 100644 --- a/IPA.Loader/Config/IConfigProvider.cs +++ b/IPA.Loader/Config/IConfigProvider.cs @@ -32,7 +32,11 @@ namespace IPA.Config /// bool HasChanged { get; } /// - /// 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 if the data in memory has been changed - notably including loads. + /// + bool InMemoryChanged { get; set; } + /// + /// 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. /// string Filename { set; } /// diff --git a/IPA.Loader/Config/ModPrefs.cs b/IPA.Loader/Config/ModPrefs.cs index 16dd5c7b..e63002f7 100644 --- a/IPA.Loader/Config/ModPrefs.cs +++ b/IPA.Loader/Config/ModPrefs.cs @@ -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 ModPrefss { get; set; } = new Dictionary(); - private readonly IniFile _instance; /// /// Constructs a ModPrefs object for the provide plugin. /// /// the plugin to get the preferences file for - 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); } - - /// - /// An extension class for IBeatSaberPlugins. - /// - public static class ModPrefsExtensions { - /// - /// Gets the ModPrefs object for the provided plugin. - /// - /// the plugin wanting the prefrences - /// the ModPrefs object - public static IModPrefs GetModPrefs(this IBeatSaberPlugin plugin) { - return ModPrefs.ModPrefss.First(o => o.Key == plugin).Value; - } - } } diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs new file mode 100644 index 00000000..f5c2d7f1 --- /dev/null +++ b/IPA.Loader/Config/SelfConfig.cs @@ -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((c, v) => + { + if (v.Value.Regenerate) + c.Store(v.Value = new SelfConfig { Regenerate = false }); + + StandardLogger.Configure(v.Value); + }); + _loaderConfig = value; + } + } + + public static Ref 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(); + } +} \ No newline at end of file diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index 4de4de52..0c83a962 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -13,10 +13,11 @@ 512 true $(SolutionDir)=C:\ + portable + true - full false bin\Debug\ DEBUG;TRACE @@ -56,9 +57,20 @@ + + + + + + + + + + + @@ -70,9 +82,9 @@ - - - + + + @@ -82,7 +94,7 @@ - + @@ -90,14 +102,14 @@ - + 1.9.1.8 - 0.10.1 + 0.10.3 12.0.1 @@ -106,6 +118,8 @@ 1.2.0 - + + + \ No newline at end of file diff --git a/IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs b/IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs new file mode 100644 index 00000000..18cfa2e0 --- /dev/null +++ b/IPA.Loader/JsonConverters/ModSaberDependencyConverter.cs @@ -0,0 +1,25 @@ +using System; +using IPA.Updating.ModSaber; +using Newtonsoft.Json; +using SemVer; + +namespace IPA.JsonConverters +{ + internal class ModSaberDependencyConverter : JsonConverter + { + 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}"); + } + } +} diff --git a/IPA.Loader/Updating/Converters/SemverRangeConverter.cs b/IPA.Loader/JsonConverters/SemverRangeConverter.cs similarity index 87% rename from IPA.Loader/Updating/Converters/SemverRangeConverter.cs rename to IPA.Loader/JsonConverters/SemverRangeConverter.cs index 08e837df..f570b4af 100644 --- a/IPA.Loader/Updating/Converters/SemverRangeConverter.cs +++ b/IPA.Loader/JsonConverters/SemverRangeConverter.cs @@ -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 diff --git a/IPA.Loader/Updating/Converters/SemverVersionConverter.cs b/IPA.Loader/JsonConverters/SemverVersionConverter.cs similarity index 73% rename from IPA.Loader/Updating/Converters/SemverVersionConverter.cs rename to IPA.Loader/JsonConverters/SemverVersionConverter.cs index 751a89ae..50e13791 100644 --- a/IPA.Loader/Updating/Converters/SemverVersionConverter.cs +++ b/IPA.Loader/JsonConverters/SemverVersionConverter.cs @@ -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 { - 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()); } diff --git a/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs b/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs index 5080628f..2efa8158 100644 --- a/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs +++ b/IPA.Loader/Loader/Composite/CompositeBSPlugin.cs @@ -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 => { diff --git a/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs b/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs index b254aa52..9825f6bc 100644 --- a/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs +++ b/IPA.Loader/Loader/Composite/CompositeIPAPlugin.cs @@ -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 => { diff --git a/IPA.Loader/Loader/Features/AddInFeature.cs b/IPA.Loader/Loader/Features/AddInFeature.cs new file mode 100644 index 00000000..08da7fdc --- /dev/null +++ b/IPA.Loader/Loader/Features/AddInFeature.cs @@ -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 { } + } + } +} diff --git a/IPA.Loader/Loader/Features/DefineFeature.cs b/IPA.Loader/Loader/Features/DefineFeature.cs new file mode 100644 index 00000000..15fffad0 --- /dev/null +++ b/IPA.Loader/Loader/Features/DefineFeature.cs @@ -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; + } + } + } +} diff --git a/IPA.Loader/Loader/Features/Feature.cs b/IPA.Loader/Loader/Features/Feature.cs new file mode 100644 index 00000000..ec84ec20 --- /dev/null +++ b/IPA.Loader/Loader/Features/Feature.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace IPA.Loader.Features +{ + /// + /// The root interface for a mod Feature. + /// + /// + /// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. + /// + public abstract class Feature + { + /// + /// Initializes the feature with the parameters provided in the definition. + /// + /// Note: When no parenthesis are provided, is an empty array. + /// + /// + /// Returning does *not* prevent the plugin from being loaded. It simply prevents the feature from being used. + /// + /// the metadata of the plugin that is being prepared + /// the parameters passed to the feature definition, or null + /// if the feature is valid for the plugin, otherwise + public abstract bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters); + + /// + /// 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 from + /// + /// the truthiness of the Feature. + public virtual bool Evaluate() => true; + + /// + /// The message to be logged when the feature is not valid for a plugin. + /// This should also be set whenever either or returns false. + /// + public virtual string InvalidMessage { get; protected set; } + + /// + /// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error. + /// + /// + /// The assembly will still be loaded, but the plugin will not be constructed if this returns . + /// Any features it defines, for example, will still be loaded. + /// + /// the plugin about to be loaded + /// whether or not the plugin should be loaded + public virtual bool BeforeLoad(PluginLoader.PluginMetadata plugin) => true; + + /// + /// 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. + /// + /// the plugin to be initialized + /// whether or not to call the Init method + public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; + + /// + /// Called after a plugin has been fully initialized, whether or not there is an Init method. This should never throw an exception. + /// + /// the plugin that was just initialized + public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } + + /// + /// Ensures a plugin's assembly is loaded. Do not use unless you need to. + /// + /// the plugin to ensure is loaded. + protected void RequireLoaded(PluginLoader.PluginMetadata plugin) => PluginLoader.Load(plugin); + + internal virtual bool StoreOnPlugin => true; + + private static readonly Dictionary featureTypes = new Dictionary + { + { "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(); + + 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; + } + } + } +} \ No newline at end of file diff --git a/IPA.Loader/Loader/Features/NoUpdateFeature.cs b/IPA.Loader/Loader/Features/NoUpdateFeature.cs new file mode 100644 index 00000000..ed5ebd97 --- /dev/null +++ b/IPA.Loader/Loader/Features/NoUpdateFeature.cs @@ -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"; + } +} diff --git a/IPA.Loader/Loader/Features/PrintFeature.cs b/IPA.Loader/Loader/Features/PrintFeature.cs new file mode 100644 index 00000000..401a8e6f --- /dev/null +++ b/IPA.Loader/Loader/Features/PrintFeature.cs @@ -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; + } + } +} diff --git a/IPA.Injector/LibLoader.cs b/IPA.Loader/Loader/LibLoader.cs similarity index 74% rename from IPA.Injector/LibLoader.cs rename to IPA.Loader/Loader/LibLoader.cs index 0c57f403..effc6bf8 100644 --- a/IPA.Injector/LibLoader.cs +++ b/IPA.Loader/Loader/LibLoader.cs @@ -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 filenameLocations; + internal static string LibraryPath => Path.Combine(Environment.CurrentDirectory, "Libs"); + internal static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); + internal static Dictionary 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(); 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 } } } + + } } diff --git a/IPA.Loader/Loader/PluginComponent.cs b/IPA.Loader/Loader/PluginComponent.cs index d0e1b36a..60f45c71 100644 --- a/IPA.Loader/Loader/PluginComponent.cs +++ b/IPA.Loader/Loader/PluginComponent.cs @@ -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(); @@ -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; } diff --git a/IPA.Loader/Loader/PluginInitInjector.cs b/IPA.Loader/Loader/PluginInitInjector.cs new file mode 100644 index 00000000..aaf82437 --- /dev/null +++ b/IPA.Loader/Loader/PluginInitInjector.cs @@ -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 +{ + /// + /// The type that handles value injecting into a plugin's Init. + /// + public static class PluginInitInjector + { + + /// + /// 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. + /// + /// the previous return value of the function, or if never called for plugin. + /// the of the parameter being injected. + /// the for the plugin being loaded. + /// the value to inject into that parameter. + public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta); + + /// + /// Adds an injector to be used when calling future plugins' Init methods. + /// + /// the type of the parameter. + /// the function to call for injection. + public static void AddInjector(Type type, InjectParameter injector) + { + injectors.Add(new Tuple(type, injector)); + } + + private static readonly List> injectors = new List> + { + new Tuple(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), + new Tuple(typeof(IModPrefs), (prev, param, meta) => prev ?? new ModPrefs(meta)), + new Tuple(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(); + var initParams = init.GetParameters(); + + Dictionary, object> previousValues = + new Dictionary, 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()); + } + } +} diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs new file mode 100644 index 00000000..cb281187 --- /dev/null +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -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 +{ + /// + /// A type to manage the loading of plugins. + /// + public class PluginLoader + { + internal static Task LoadTask() => Task.Run(() => + { + LoadMetadata(); + Resolve(); + ComputeLoadOrder(); + InitFeatures(); + }); + + /// + /// A class which describes + /// + public class PluginMetadata + { + /// + /// The assembly the plugin was loaded from. + /// + public Assembly Assembly { get; internal set; } + + /// + /// The TypeDefinition for the main type of the plugin. + /// + public TypeDefinition PluginType { get; internal set; } + + /// + /// The human readable name of the plugin. + /// + public string Name { get; internal set; } + + /// + /// The ModSaber ID of the plugin, or null if it doesn't have one. + /// + public string Id { get; internal set; } + + /// + /// The version of the plugin. + /// + public Version Version { get; internal set; } + + /// + /// The file the plugin was loaded from. + /// + public FileInfo File { get; internal set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + /// + /// The features this plugin requests. + /// + public IReadOnlyList Features => InternalFeatures; + + internal readonly List InternalFeatures = new List(); + + internal bool IsSelf; + + private PluginManifest manifest; + + internal PluginManifest Manifest + { + get => manifest; + set + { + manifest = value; + Name = value.Name; + Version = value.Version; + Id = value.Id; + } + } + + /// + public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; + } + + /// + /// A container object for all the data relating to a plugin. + /// + public class PluginInfo + { + internal IBeatSaberPlugin Plugin { get; set; } + + /// + /// Metadata for the plugin. + /// + public PluginMetadata Metadata { get; internal set; } = new PluginMetadata(); + } + + internal static List PluginsMetadata = new List(); + + 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(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(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(); + var ignore = new HashSet(); + var resolved = new List(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(); + var pluginsToLoad = new Dictionary(); + 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(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 LoadPlugins() => PluginsMetadata.Select(InitPlugin).Where(p => p != null).ToList(); + } +} \ No newline at end of file diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 860ad191..4d76a875 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -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) - /// - /// A container object for all the data relating to a plugin. - /// - public class PluginInfo - { - internal IBeatSaberPlugin Plugin { get; set; } - internal string Filename { get; set; } - /// - /// The ModSaber updating info for the mod, or null. - /// - public ModsaberModInfo ModSaberInfo { get; internal set; } - } + /// /// An of new Beat Saber plugins @@ -72,7 +59,7 @@ namespace IPA.Loader /// the plugin info for the requested plugin or null public static PluginInfo GetPlugin(string name) { - return BSMetas.FirstOrDefault(p => p.Plugin.Name == name); + return BSMetas.FirstOrDefault(p => p.Metadata.Name == name); } /// @@ -82,7 +69,7 @@ namespace IPA.Loader /// the plugin info for the requested plugin or null public static PluginInfo GetPluginFromModSaberName(string name) { - return BSMetas.FirstOrDefault(p => p.ModSaberInfo.InternalName == name); + return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); } /// @@ -104,8 +91,6 @@ namespace IPA.Loader internal static IConfigProvider SelfConfigProvider { get; set; } - internal static readonly List>> configProviders = new List>>(); - 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>( - SelfConfigProvider = new JsonConfigProvider {Filename = Path.Combine("UserData", SelfPlugin.IPA_Name)}, - new Ref(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> LoadPluginsFromFile(string file, string exeName) + private static Tuple, IEnumerable> LoadPluginsFromFile(string file) { - List bsPlugins = new List(); List ipaPlugins = new List(); if (!File.Exists(file) || !file.EndsWith(".dll", true, null)) - return new Tuple, IEnumerable>(bsPlugins, ipaPlugins); + return new Tuple, IEnumerable>(null, ipaPlugins); T OptionalGetPlugin(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(t); - if (bsPlugin != null) - { - try - { - var init = t.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public); - if (init != null) - { - var initArgs = new List(); - 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>(cfgProvider, new Ref(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(t); + if (ipaPlugin != null) { - IPlugin ipaPlugin = OptionalGetPlugin(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>(bsPlugins, ipaPlugins); + return new Tuple, IEnumerable>(null, ipaPlugins); } internal class AppInfo diff --git a/IPA.Loader/Loader/PluginManifest.cs b/IPA.Loader/Loader/PluginManifest.cs new file mode 100644 index 00000000..f7b25207 --- /dev/null +++ b/IPA.Loader/Loader/PluginManifest.cs @@ -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 Dependencies = new Dictionary(); + + [JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] + public Dictionary Conflicts = new Dictionary(); + + [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]; + } +} \ No newline at end of file diff --git a/IPA.Loader/Loader/manifest.json b/IPA.Loader/Loader/manifest.json new file mode 100644 index 00000000..26c2f1da --- /dev/null +++ b/IPA.Loader/Loader/manifest.json @@ -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)" + ] +} \ No newline at end of file diff --git a/IPA.Loader/Logging/LogPrinter.cs b/IPA.Loader/Logging/LogPrinter.cs index 2c82e9f6..a03aee44 100644 --- a/IPA.Loader/Logging/LogPrinter.cs +++ b/IPA.Loader/Logging/LogPrinter.cs @@ -11,6 +11,7 @@ namespace IPA.Logging /// Provides a filter for which log levels to allow through. /// public abstract Logger.LogLevel Filter { get; set; } + /// /// Prints a provided message from a given log at the specified time. /// @@ -19,15 +20,19 @@ namespace IPA.Logging /// the name of the log that created this message /// the message public abstract void Print(Logger.Level level, DateTime time, string logName, string message); + /// /// Called before the first print in a group. May be called multiple times. /// Use this to create file handles and the like. /// public virtual void StartPrint() { } + /// /// Called after the last print in a group. May be called multiple times. /// Use this to dispose file handles and the like. /// public virtual void EndPrint() { } + + internal DateTime LastUse { get; set; } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/Logger.cs b/IPA.Loader/Logging/Logger.cs index 5bd74266..33b5b5fd 100644 --- a/IPA.Loader/Logging/Logger.cs +++ b/IPA.Loader/Logging/Logger.cs @@ -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; /// /// The standard format for log messages. @@ -38,22 +42,27 @@ namespace IPA.Logging /// No associated level. These never get shown. /// None = 0, + /// /// A debug message. /// Debug = 1, + /// /// An informational message. /// Info = 2, + /// /// A warning message. /// Warning = 4, + /// /// An error message. /// Error = 8, + /// /// A critical error message. /// @@ -70,22 +79,27 @@ namespace IPA.Logging /// Allow no messages through. /// None = Level.None, + /// /// Only shows Debug messages. /// DebugOnly = Level.Debug, + /// /// Only shows info messages. /// InfoOnly = Level.Info, + /// /// Only shows Warning messages. /// WarningOnly = Level.Warning, + /// /// Only shows Error messages. /// ErrorOnly = Level.Error, + /// /// Only shows Critical messages. /// @@ -95,18 +109,26 @@ namespace IPA.Logging /// Shows all messages error and up. /// ErrorUp = ErrorOnly | CriticalOnly, + /// /// Shows all messages warning and up. /// WarningUp = WarningOnly | ErrorUp, + /// /// Shows all messages info and up. /// InfoUp = InfoOnly | WarningUp, + /// /// Shows all messages. /// All = DebugOnly | InfoUp, + + /// + /// Used for when the level is undefined. + /// + Undefined = Byte.MaxValue } /// @@ -115,19 +137,22 @@ namespace IPA.Logging /// the level of the message /// the message to log public abstract void Log(Level level, string message); + /// /// A basic log function taking an exception to log. /// /// the level of the message /// the exception to log public virtual void Log(Level level, Exception e) => Log(level, e.ToString()); + /// - /// Sends a debug message. + /// Sends a debug message. /// Equivalent to Log(Level.Debug, message); /// /// /// the message to log public virtual void Debug(string message) => Log(Level.Debug, message); + /// /// Sends an exception as a debug message. /// Equivalent to Log(Level.Debug, e); @@ -135,13 +160,15 @@ namespace IPA.Logging /// /// the exception to log public virtual void Debug(Exception e) => Log(Level.Debug, e); + /// - /// Sends an info message. + /// Sends an info message. /// Equivalent to Log(Level.Info, message). /// /// /// the message to log public virtual void Info(string message) => Log(Level.Info, message); + /// /// Sends an exception as an info message. /// Equivalent to Log(Level.Info, e); @@ -149,13 +176,15 @@ namespace IPA.Logging /// /// the exception to log public virtual void Info(Exception e) => Log(Level.Info, e); + /// - /// Sends a warning message. + /// Sends a warning message. /// Equivalent to Log(Level.Warning, message). /// /// /// the message to log public virtual void Warn(string message) => Log(Level.Warning, message); + /// /// Sends an exception as a warning message. /// Equivalent to Log(Level.Warning, e); @@ -163,13 +192,15 @@ namespace IPA.Logging /// /// the exception to log public virtual void Warn(Exception e) => Log(Level.Warning, e); + /// - /// Sends an error message. + /// Sends an error message. /// Equivalent to Log(Level.Error, message). /// /// /// the message to log public virtual void Error(string message) => Log(Level.Error, message); + /// /// Sends an exception as an error message. /// Equivalent to Log(Level.Error, e); @@ -177,13 +208,15 @@ namespace IPA.Logging /// /// the exception to log public virtual void Error(Exception e) => Log(Level.Error, e); + /// - /// Sends a critical message. + /// Sends a critical message. /// Equivalent to Log(Level.Critical, message). /// /// /// the message to log public virtual void Critical(string message) => Log(Level.Critical, message); + /// /// Sends an exception as a critical message. /// Equivalent to Log(Level.Critical, e); @@ -192,4 +225,4 @@ namespace IPA.Logging /// the exception to log public virtual void Critical(Exception e) => Log(Level.Critical, e); } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/Printers/GZFilePrinter.cs b/IPA.Loader/Logging/Printers/GZFilePrinter.cs index eb25ff56..5f39d9cc 100644 --- a/IPA.Loader/Logging/Printers/GZFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/GZFilePrinter.cs @@ -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; + /// /// The that writes to the GZip file. /// protected StreamWriter FileWriter; + private GZipStream zstream; private FileStream fstream; @@ -99,9 +101,7 @@ namespace IPA.Logging.Printers fstream.Dispose(); } - /// - /// Disposes the file printer. - /// + /// public void Dispose() { Dispose(true); @@ -128,4 +128,4 @@ namespace IPA.Logging.Printers } } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs b/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs index fffcb27d..59329a54 100644 --- a/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/GlobalLogFilePrinter.cs @@ -38,4 +38,4 @@ namespace IPA.Logging.Printers return finfo; } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs b/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs index bae0d762..eda96b21 100644 --- a/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs +++ b/IPA.Loader/Logging/Printers/PluginLogFilePrinter.cs @@ -21,7 +21,7 @@ namespace IPA.Logging.Printers /// 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()); } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs b/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs index d501c862..54bdead2 100644 --- a/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs +++ b/IPA.Loader/Logging/Printers/PluginSubLogPrinter.cs @@ -52,4 +52,4 @@ namespace IPA.Logging.Printers FileWriter.WriteLine("[{2} @ {1:HH:mm:ss}] {0}", line, time, level.ToString().ToUpper()); } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/StandardLogger.cs b/IPA.Loader/Logging/StandardLogger.cs index aa64f4d2..b141d7fe 100644 --- a/IPA.Loader/Logging/StandardLogger.cs +++ b/IPA.Loader/Logging/StandardLogger.cs @@ -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 { /// - /// The default implementation. + /// The default (and standard) implementation. /// + /// + /// 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 to safely get a child. + /// The modification of printers on a parent are reflected down the chain. + /// public class StandardLogger : Logger { - private static readonly IReadOnlyList defaultPrinters = new List() + private static readonly List defaultPrinters = new List() { new ColoredConsolePrinter() { @@ -43,29 +51,45 @@ namespace IPA.Logging new GlobalLogFilePrinter() }; + /// + /// Adds to the default printer pool that all printers inherit from. Printers added this way will be passed every message from every logger. + /// + /// + internal static void AddDefaultPrinter(LogPrinter printer) + { + defaultPrinters.Add(printer); + } + private readonly string logName; - private static readonly bool showSourceClass; + private static bool showSourceClass; + /// /// All levels defined by this filter will be sent to loggers. All others will be ignored. /// - public static LogLevel PrintFilter { get; set; } - private readonly List printers = new List(defaultPrinters); + public static LogLevel PrintFilter { get; set; } = LogLevel.All; + + private readonly List printers = new List(); + private readonly StandardLogger parent; private readonly Dictionary children = new Dictionary(); - - static StandardLogger() + + /// + /// Configures internal debug settings based on the config passed in. + /// + /// + 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(inherited) + logName = $"{parent.logName}/{subName}"; + this.parent = parent; + printers = new List() { - 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 } } + /// + /// Gets a child printer with the given name, either constructing a new one or using one that was already made. + /// + /// + /// a child with the given sub-name 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 }); } - + /// /// /// An override to 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 logQueue = new BlockingCollection(); private static Thread logThread; + private static StandardLogger loggerLogger; + + private const int LogCloseTimeout = 500; + + /// + /// The log printer thread for . + /// 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(); - 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 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(); + // 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(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 } } + /// + /// Stops and joins the log printer thread. + /// internal static void StopLogThread() { logQueue.CompleteAdding(); @@ -211,7 +331,7 @@ namespace IPA.Logging public static class LoggerExtensions { /// - /// Gets a child logger, if supported. + /// Gets a child logger, if supported. Currently the only defined and supported logger is , and most plugins will only ever receive this anyway. /// /// the parent /// the name of the child @@ -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(); } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Logging/UnityLogInterceptor.cs b/IPA.Loader/Logging/UnityLogProvider.cs similarity index 89% rename from IPA.Loader/Logging/UnityLogInterceptor.cs rename to IPA.Loader/Logging/UnityLogProvider.cs index 3f36918f..4bb8af41 100644 --- a/IPA.Loader/Logging/UnityLogInterceptor.cs +++ b/IPA.Loader/Logging/UnityLogProvider.cs @@ -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) diff --git a/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs b/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs index 027c3e1f..6a853c96 100644 --- a/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs +++ b/IPA.Loader/PluginInterfaces/IGenericEnhancedPlugin.cs @@ -1,4 +1,7 @@ // ReSharper disable CheckNamespace + +using System; + namespace IPA { /// @@ -10,6 +13,7 @@ namespace IPA /// Gets a list of executables this plugin should be executed on (without the file ending) /// /// { "PlayClub", "PlayClubStudio" } + [Obsolete("Ignored.")] string[] Filter { get; } /// diff --git a/IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs b/IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs deleted file mode 100644 index f5415d3c..00000000 --- a/IPA.Loader/Updating/Converters/ModsaberDependencyConverter.cs +++ /dev/null @@ -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 - { - 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}"); - } - } -} diff --git a/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs b/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs index cb3c9535..72d51825 100644 --- a/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs +++ b/IPA.Loader/Updating/ModSaber/ApiEndpoint.cs @@ -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] diff --git a/IPA.Loader/Updating/ModSaber/Updater.cs b/IPA.Loader/Updating/ModSaber/Updater.cs index 26307c36..64536ff4 100644 --- a/IPA.Loader/Updating/ModSaber/Updater.cs +++ b/IPA.Loader/Updating/ModSaber/Updater.cs @@ -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 Consumers { get; set; } = new HashSet(); 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(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"); diff --git a/IPA.Loader/Updating/SelfPlugin.cs b/IPA.Loader/Updating/SelfPlugin.cs index 40a1f0f4..52074ee5 100644 --- a/IPA.Loader/Updating/SelfPlugin.cs +++ b/IPA.Loader/Updating/SelfPlugin.cs @@ -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 { } } -} +} \ No newline at end of file diff --git a/IPA.Loader/Utilities/BeatSaber.cs b/IPA.Loader/Utilities/BeatSaber.cs index 3585b5be..18f39990 100644 --- a/IPA.Loader/Utilities/BeatSaber.cs +++ b/IPA.Loader/Utilities/BeatSaber.cs @@ -48,6 +48,14 @@ namespace IPA.Utilities /// The path to the `Libs\Native` folder. Use only if necessary. /// public static string NativeLibraryPath => Path.Combine(LibraryPath, "Native"); + /// + /// The directory to load plugins from. + /// + public static string PluginsPath => Path.Combine(InstallPath, "Plugins"); + /// + /// The path to the `UserData` folder. + /// + public static string UserDataPath => Path.Combine(InstallPath, "UserData"); private static bool FindSteamVRAsset() { diff --git a/IPA.Loader/Utilities/Extensions.cs b/IPA.Loader/Utilities/Extensions.cs index 4cd6b762..cfcfcb29 100644 --- a/IPA.Loader/Utilities/Extensions.cs +++ b/IPA.Loader/Utilities/Extensions.cs @@ -16,5 +16,12 @@ namespace IPA.Utilities { return type.IsValueType ? Activator.CreateInstance(type) : null; } + + /// + /// Unwraps a where T is such that if the value is null, it gives . + /// + /// the bool? to unwrap + /// the unwrapped value, or if it was + public static bool Unwrap(this bool? self) => self != null && self.Value; } } diff --git a/IPA.Loader/Utilities/Ref.cs b/IPA.Loader/Utilities/Ref.cs index f710ad6a..bcb1ff62 100644 --- a/IPA.Loader/Utilities/Ref.cs +++ b/IPA.Loader/Utilities/Ref.cs @@ -4,11 +4,28 @@ using System.Reflection; namespace IPA.Utilities { + /// + /// Utilities to create using type inference. + /// + public static class Ref + { + /// + /// Creates a . + /// + /// the type to reference. + /// the default value. + /// the new . + public static Ref Create(T val) + { + return new Ref(val); + } + } + /// /// A class to store a reference for passing to methods which cannot take ref parameters. /// /// the type of the value - public class Ref + public class Ref : IComparable, IComparable> { private T _value; /// @@ -76,6 +93,20 @@ namespace IPA.Utilities { if (Error != null) throw Error; } + + /// + public int CompareTo(T other) + { + if (Value is IComparable compare) + return compare.CompareTo(other); + return Equals(Value, other) ? 0 : -1; + } + + /// + public int CompareTo(Ref other) + { + return CompareTo(other.Value); + } } internal static class ExceptionUtilities diff --git a/IPA.Loader/Utilities/LoneFunctions.cs b/IPA.Loader/Utilities/Utils.cs similarity index 99% rename from IPA.Loader/Utilities/LoneFunctions.cs rename to IPA.Loader/Utilities/Utils.cs index 10df9a53..a702f5b3 100644 --- a/IPA.Loader/Utilities/LoneFunctions.cs +++ b/IPA.Loader/Utilities/Utils.cs @@ -7,7 +7,7 @@ namespace IPA.Utilities /// /// A class providing static utility functions that in any other language would just *exist*. /// - public static class LoneFunctions + public static class Utils { /// /// Converts a hex string to a byte array. diff --git a/IPA/IPA.csproj b/IPA/IPA.csproj index bb828bf3..5cf84b11 100644 --- a/IPA/IPA.csproj +++ b/IPA/IPA.csproj @@ -100,8 +100,6 @@ - - @@ -115,7 +113,7 @@ - 0.10.1 + 0.10.3 diff --git a/IPA/Patcher/Patcher.cs b/IPA/Patcher/Patcher.cs deleted file mode 100644 index a6119a61..00000000 --- a/IPA/Patcher/Patcher.cs +++ /dev/null @@ -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 FindEntryTypes() - { - return _module.GetTypes().Where(m => EntryTypes.Contains(m.Name)); - } - } -} diff --git a/IPA/Patcher/Virtualizer.cs b/IPA/Patcher/Virtualizer.cs deleted file mode 100644 index bfc3ce5b..00000000 --- a/IPA/Patcher/Virtualizer.cs +++ /dev/null @@ -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); - } - - /// - /// - /// - 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; - } - } - } -} diff --git a/IPA/Program.cs b/IPA/Program.cs index 68d3b376..120ad5f8 100644 --- a/IPA/Program.cs +++ b/IPA/Program.cs @@ -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) { diff --git a/IPA/Properties/AssemblyInfo.cs b/IPA/Properties/AssemblyInfo.cs index 03855bb7..d76c55bc 100644 --- a/IPA/Properties/AssemblyInfo.cs +++ b/IPA/Properties/AssemblyInfo.cs @@ -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")] \ No newline at end of file diff --git a/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache b/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache index 608c7d4d..d16fba0d 100644 --- a/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache +++ b/IPA/obj/Debug/IPA.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -27a27b93f9cca058d6b4f09d77cb8a300416a979 +5d76d76cc5c14257f2b9071c928c27b3edd80cc0 diff --git a/Libs/Mono.Debugger.Soft.dll b/Libs/Mono.Debugger.Soft.dll new file mode 100644 index 00000000..0467709c Binary files /dev/null and b/Libs/Mono.Debugger.Soft.dll differ diff --git a/MSBuildTasks/AssemblyRenameTask.cs b/MSBuildTasks/AssemblyRenameTask.cs deleted file mode 100644 index ad002073..00000000 --- a/MSBuildTasks/AssemblyRenameTask.cs +++ /dev/null @@ -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; - } - } -} diff --git a/MSBuildTasks/MSBuildTasks.csproj b/MSBuildTasks/MSBuildTasks.csproj deleted file mode 100644 index 2009ef84..00000000 --- a/MSBuildTasks/MSBuildTasks.csproj +++ /dev/null @@ -1,84 +0,0 @@ - - - - - Debug - AnyCPU - {F08C3C7A-3221-432E-BAB8-32BCE58408C8} - Library - Properties - MSBuildTasks - MSBuildTasks - v4.6 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - true - - - - ..\packages\Microsoft.Build.Framework.15.9.20\lib\net46\Microsoft.Build.Framework.dll - - - ..\packages\Microsoft.Build.Utilities.Core.15.9.20\lib\net46\Microsoft.Build.Utilities.Core.dll - - - ..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.16.30\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll - True - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.dll - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Mdb.dll - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Pdb.dll - - - ..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Rocks.dll - - - - ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard1.3\System.Collections.Immutable.dll - - - - - ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - True - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/MSBuildTasks/Pdb2Mdb.cs b/MSBuildTasks/Pdb2Mdb.cs deleted file mode 100644 index ca99642e..00000000 --- a/MSBuildTasks/Pdb2Mdb.cs +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/MSBuildTasks/Properties/AssemblyInfo.cs b/MSBuildTasks/Properties/AssemblyInfo.cs deleted file mode 100644 index 74f94c1c..00000000 --- a/MSBuildTasks/Properties/AssemblyInfo.cs +++ /dev/null @@ -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")] diff --git a/MSBuildTasks/packages.config b/MSBuildTasks/packages.config deleted file mode 100644 index 5f2085c5..00000000 --- a/MSBuildTasks/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 2a014edb..55150a1f 100644 --- a/README.md +++ b/README.md @@ -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 Alt - 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/` \ No newline at end of file diff --git a/Refs/UnityEngine.CoreModule.dll b/Refs/UnityEngine.CoreModule.dll index a802a5c6..860de28d 100644 Binary files a/Refs/UnityEngine.CoreModule.dll and b/Refs/UnityEngine.CoreModule.dll differ diff --git a/Refs/UnityEngine.UnityWebRequestModule.dll b/Refs/UnityEngine.UnityWebRequestModule.dll index b89e09d4..5f9e279e 100644 Binary files a/Refs/UnityEngine.UnityWebRequestModule.dll and b/Refs/UnityEngine.UnityWebRequestModule.dll differ diff --git a/Refs/refs.txt b/Refs/refs.txt index f79922cf..f34bab9f 100644 --- a/Refs/refs.txt +++ b/Refs/refs.txt @@ -1,4 +1,4 @@ -::from ../bsinstalldir.txt +::from ./bsinstalldir.txt "Beat Saber_Data/ ""Managed/ """UnityEngine. diff --git a/appveyor.yml b/appveyor.yml index 11ec7f3b..cd57bffa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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: