@ -1,3 +1,6 @@ | |||||
[submodule "Doorstop"] | [submodule "Doorstop"] | ||||
path = Doorstop | path = Doorstop | ||||
url = https://github.com/nike4613/UnityDoorstop-BSIPA | url = https://github.com/nike4613/UnityDoorstop-BSIPA | ||||
[submodule "BuildTools"] | |||||
path = BuildTools | |||||
url = https://github.com/nike4613/BS-Plugin-BuildTools.git |
@ -1,17 +1,29 @@ | |||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> | ||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue">WARNING</s:String> | <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue">WARNING</s:String> | ||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_BINARY_EXPRESSIONS/@EntryValue">False</s:Boolean> | |||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_FIELDS/@EntryValue">False</s:Boolean> | |||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_METHODS/@EntryValue">False</s:Boolean> | |||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INT_ALIGN_PROPERTIES/@EntryValue">False</s:Boolean> | |||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BS/@EntryIndexedValue">BS</s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BS/@EntryIndexedValue">BS</s:String> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPA/@EntryIndexedValue">IPA</s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPA/@EntryIndexedValue">IPA</s:String> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=VR/@EntryIndexedValue">VR</s:String> | |||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></s:String> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></s:String> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></s:String> | |||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></s:String> | |||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /></Policy></s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PublicFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /></Policy></s:String> | ||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /></Policy></s:String> | <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb_aaBb" /></Policy></s:String> | ||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean> | |||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean> | |||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean> | |||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> | |||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatsaber/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=beatsaber/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Coroutine/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Coroutine/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=deps/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=deps/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Modsaber/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Modsaber/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pdbs/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Pdbs/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=plugin_0027s/@EntryIndexedValue">True</s:Boolean> | |||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Prefs/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Prefs/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=unpatch/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=unpatch/@EntryIndexedValue">True</s:Boolean> | ||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Virtualize/@EntryIndexedValue">True</s:Boolean> | <s:Boolean x:Key="/Default/UserDictionary/Words/=Virtualize/@EntryIndexedValue">True</s:Boolean> |
@ -1,6 +0,0 @@ | |||||
<?xml version="1.0" encoding="utf-8" ?> | |||||
<configuration> | |||||
<startup> | |||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> | |||||
</startup> | |||||
</configuration> |
@ -1,74 +0,0 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
<PropertyGroup> | |||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
<ProjectGuid>{5F33B310-DC8D-4C0D-877E-BAC3908DE10F}</ProjectGuid> | |||||
<OutputType>Exe</OutputType> | |||||
<RootNamespace>CollectDependencies</RootNamespace> | |||||
<AssemblyName>CollectDependencies</AssemblyName> | |||||
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion> | |||||
<FileAlignment>512</FileAlignment> | |||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> | |||||
<Deterministic>true</Deterministic> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
<PlatformTarget>AnyCPU</PlatformTarget> | |||||
<DebugSymbols>true</DebugSymbols> | |||||
<DebugType>full</DebugType> | |||||
<Optimize>false</Optimize> | |||||
<OutputPath>bin\Debug\</OutputPath> | |||||
<DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
<PlatformTarget>AnyCPU</PlatformTarget> | |||||
<DebugType>pdbonly</DebugType> | |||||
<Optimize>true</Optimize> | |||||
<OutputPath>bin\Release\</OutputPath> | |||||
<DefineConstants>TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<Reference Include="Mono.Cecil, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.dll</HintPath> | |||||
<Private>True</Private> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil.Mdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Mdb.dll</HintPath> | |||||
<Private>True</Private> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil.Pdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Pdb.dll</HintPath> | |||||
<Private>True</Private> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil.Rocks, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Rocks.dll</HintPath> | |||||
<Private>True</Private> | |||||
</Reference> | |||||
<Reference Include="System" /> | |||||
<Reference Include="System.Core" /> | |||||
<Reference Include="System.Xml.Linq" /> | |||||
<Reference Include="System.Data.DataSetExtensions" /> | |||||
<Reference Include="Microsoft.CSharp" /> | |||||
<Reference Include="System.Data" /> | |||||
<Reference Include="System.Net.Http" /> | |||||
<Reference Include="System.Xml" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Compile Include="Program.cs" /> | |||||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||||
<Compile Include="Virtualizer.cs" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<None Include="App.config" /> | |||||
<None Include="packages.config" /> | |||||
</ItemGroup> | |||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
<Target Name="AfterBuild"> | |||||
<Exec Command=""$(SolutionDir)$(AssemblyName)\$(OutputPath)$(AssemblyName).exe" Refs/refs.txt" WorkingDirectory="$(SolutionDir)"></Exec> | |||||
</Target> | |||||
</Project> |
@ -1,135 +0,0 @@ | |||||
using Mono.Cecil; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
namespace CollectDependencies | |||||
{ | |||||
static class Program | |||||
{ | |||||
static void Main(string[] args) | |||||
{ | |||||
var depsFile = File.ReadAllText(args[0]); | |||||
var directoryName = Path.GetDirectoryName(args[0]); | |||||
var files = new List<Tuple<string, int>>(); | |||||
{ // Create files from stuff in depsfile | |||||
var stack = new Stack<string>(); | |||||
void Push(string val) | |||||
{ | |||||
string pre = ""; | |||||
if (stack.Count > 0) | |||||
pre = stack.First(); | |||||
stack.Push(pre + val); | |||||
} | |||||
string Pop() => stack.Pop(); | |||||
string Replace(string val) | |||||
{ | |||||
var v2 = Pop(); | |||||
Push(val); | |||||
return v2; | |||||
} | |||||
var lineNo = 0; | |||||
foreach (var line in depsFile.Split(new[] { Environment.NewLine }, StringSplitOptions.None)) | |||||
{ | |||||
var parts = line.Split('"'); | |||||
var path = parts.Last(); | |||||
var level = parts.Length - 1; | |||||
if (path.StartsWith("::")) | |||||
{ // pseudo-command | |||||
parts = path.Split(' '); | |||||
var command = parts[0].Substring(2); | |||||
parts = parts.Skip(1).ToArray(); | |||||
var arglist = string.Join(" ", parts); | |||||
if (command == "from") | |||||
{ // an "import" type command | |||||
path = File.ReadAllText(Path.Combine(directoryName ?? throw new InvalidOperationException(), arglist)); | |||||
} | |||||
else if (command == "prompt") | |||||
{ | |||||
Console.Write(arglist); | |||||
path = Console.ReadLine(); | |||||
} | |||||
else | |||||
{ | |||||
path = ""; | |||||
Console.Error.WriteLine($"Invalid command {command}"); | |||||
} | |||||
} | |||||
if (level > stack.Count - 1) | |||||
Push(path); | |||||
else if (level == stack.Count - 1) | |||||
files.Add(new Tuple<string, int>(Replace(path), lineNo)); | |||||
else if (level < stack.Count - 1) | |||||
{ | |||||
files.Add(new Tuple<string, int>(Pop(), lineNo)); | |||||
while (level < stack.Count) | |||||
Pop(); | |||||
Push(path); | |||||
} | |||||
lineNo++; | |||||
} | |||||
files.Add(new Tuple<string, int>(Pop(), lineNo)); | |||||
} | |||||
foreach (var file in files) | |||||
{ | |||||
try | |||||
{ | |||||
var fparts = file.Item1.Split('?'); | |||||
var fname = fparts[0]; | |||||
if (fname == "") continue; | |||||
var outp = Path.Combine(directoryName ?? throw new InvalidOperationException(), | |||||
Path.GetFileName(fname) ?? throw new InvalidOperationException()); | |||||
Console.WriteLine($"Copying \"{fname}\" to \"{outp}\""); | |||||
if (File.Exists(outp)) File.Delete(outp); | |||||
if (Path.GetExtension(fname)?.ToLower() == ".dll") | |||||
{ | |||||
// ReSharper disable once StringLiteralTypo | |||||
if (fparts.Length > 1 && fparts[1] == "virt") | |||||
{ | |||||
var module = VirtualizedModule.Load(fname); | |||||
module.Virtualize(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName(), | |||||
Path.GetFileName(fname) ?? throw new InvalidOperationException())); | |||||
} | |||||
var modl = ModuleDefinition.ReadModule(fparts[0]); | |||||
foreach (var t in modl.Types) | |||||
{ | |||||
foreach (var m in t.Methods) | |||||
{ | |||||
if (m.Body != null) | |||||
{ | |||||
m.Body.Instructions.Clear(); | |||||
m.Body.InitLocals = false; | |||||
m.Body.Variables.Clear(); | |||||
} | |||||
} | |||||
} | |||||
modl.Write(outp); | |||||
} | |||||
else | |||||
{ | |||||
File.Copy(fname, outp); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Console.WriteLine($"{Path.Combine(Environment.CurrentDirectory, args[0])}({file.Item2}): error: {e}"); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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")] |
@ -1,111 +0,0 @@ | |||||
using Mono.Cecil; | |||||
using System.IO; | |||||
using System.Linq; | |||||
namespace CollectDependencies | |||||
{ | |||||
class VirtualizedModule | |||||
{ | |||||
private readonly FileInfo _file; | |||||
private ModuleDefinition _module; | |||||
public static VirtualizedModule Load(string engineFile) | |||||
{ | |||||
return new VirtualizedModule(engineFile); | |||||
} | |||||
private VirtualizedModule(string assemblyFile) | |||||
{ | |||||
_file = new FileInfo(assemblyFile); | |||||
LoadModules(); | |||||
} | |||||
private void LoadModules() | |||||
{ | |||||
var resolver = new DefaultAssemblyResolver(); | |||||
resolver.AddSearchDirectory(_file.DirectoryName); | |||||
var parameters = new ReaderParameters | |||||
{ | |||||
AssemblyResolver = resolver, | |||||
}; | |||||
_module = ModuleDefinition.ReadModule(_file.FullName, parameters); | |||||
} | |||||
/// <summary> | |||||
/// | |||||
/// </summary> | |||||
/// <param name="targetFile"></param> | |||||
public void Virtualize(string targetFile) | |||||
{ | |||||
foreach (var type in _module.Types) | |||||
{ | |||||
VirtualizeType(type); | |||||
} | |||||
_module.Write(targetFile); | |||||
} | |||||
private void VirtualizeType(TypeDefinition type) | |||||
{ | |||||
if(type.IsSealed) | |||||
{ | |||||
// Unseal | |||||
type.IsSealed = false; | |||||
} | |||||
if (type.IsInterface) return; | |||||
if (type.IsAbstract) return; | |||||
// These two don't seem to work. | |||||
if (type.Name == "SceneControl" || type.Name == "ConfigUI") return; | |||||
// Take care of sub types | |||||
foreach (var subType in type.NestedTypes) | |||||
{ | |||||
VirtualizeType(subType); | |||||
} | |||||
foreach (var method in type.Methods) | |||||
{ | |||||
if (method.IsManaged | |||||
&& method.IsIL | |||||
&& !method.IsStatic | |||||
&& !method.IsVirtual | |||||
&& !method.IsAbstract | |||||
&& !method.IsAddOn | |||||
&& !method.IsConstructor | |||||
&& !method.IsSpecialName | |||||
&& !method.IsGenericInstance | |||||
&& !method.HasOverrides) | |||||
{ | |||||
method.IsVirtual = true; | |||||
method.IsPublic = true; | |||||
method.IsPrivate = false; | |||||
method.IsNewSlot = true; | |||||
method.IsHideBySig = true; | |||||
} | |||||
} | |||||
foreach (var field in type.Fields) | |||||
{ | |||||
if (field.IsPrivate) field.IsFamily = true; | |||||
} | |||||
} | |||||
public bool IsVirtualized | |||||
{ | |||||
get | |||||
{ | |||||
var awakeMethods = _module.GetTypes().SelectMany(t => t.Methods.Where(m => m.Name == "Awake")); | |||||
var methodDefinitions = awakeMethods as MethodDefinition[] ?? awakeMethods.ToArray(); | |||||
if (!methodDefinitions.Any()) return false; | |||||
return ((float)methodDefinitions.Count(m => m.IsVirtual) / methodDefinitions.Count()) > 0.5f; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -1,4 +0,0 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<packages> | |||||
<package id="Mono.Cecil" version="0.10.1" targetFramework="net472" /> | |||||
</packages> |
@ -1 +1 @@ | |||||
Subproject commit 0a350fe8a9792cd6708f5d5e805d82ec115c5e7d | |||||
Subproject commit 310ab026a8905588dab29f250a2385ed2cb7c41f |
@ -0,0 +1,255 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using IPA.Config.ConfigProviders; | |||||
using IPA.Utilities; | |||||
namespace IPA.Config | |||||
{ | |||||
/// <summary> | |||||
/// A class to handle updating ConfigProviders automatically | |||||
/// </summary> | |||||
public static class Config | |||||
{ | |||||
static Config() | |||||
{ | |||||
JsonConfigProvider.RegisterConfig(); | |||||
} | |||||
/// <inheritdoc /> | |||||
/// <summary> | |||||
/// Defines the type of the <see cref="T:IPA.Config.IConfigProvider" /> | |||||
/// </summary> | |||||
[AttributeUsage(AttributeTargets.Class)] | |||||
public class TypeAttribute : Attribute | |||||
{ | |||||
/// <summary> | |||||
/// The extension associated with this type, without the '.' | |||||
/// </summary> | |||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global | |||||
public string Extension { get; private set; } | |||||
/// <inheritdoc /> | |||||
/// <summary> | |||||
/// Constructs the attribute with a specified extension. | |||||
/// </summary> | |||||
/// <param name="ext">the extension associated with this type, without the '.'</param> | |||||
public TypeAttribute(string ext) | |||||
{ | |||||
Extension = ext; | |||||
} | |||||
} | |||||
/// <inheritdoc /> | |||||
/// <summary> | |||||
/// Specifies that a particular parameter is preferred to be a specific type of <see cref="T:IPA.Config.IConfigProvider" />. If it is not available, also specifies backups. If none are available, the default is used. | |||||
/// </summary> | |||||
[AttributeUsage(AttributeTargets.Parameter)] | |||||
public class PreferAttribute : Attribute | |||||
{ | |||||
/// <summary> | |||||
/// The order of preference for the config type. | |||||
/// </summary> | |||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global | |||||
public string[] PreferenceOrder { get; private set; } | |||||
/// <inheritdoc /> | |||||
/// <summary> | |||||
/// Constructs the attribute with a specific preference list. Each entry is the extension without a '.' | |||||
/// </summary> | |||||
/// <param name="preference">The preferences in order of preference.</param> | |||||
public PreferAttribute(params string[] preference) | |||||
{ | |||||
PreferenceOrder = preference; | |||||
} | |||||
} | |||||
/// <inheritdoc /> | |||||
/// <summary> | |||||
/// Specifies a preferred config name, instead of using the plugin's name. | |||||
/// </summary> | |||||
public class NameAttribute : Attribute | |||||
{ | |||||
/// <summary> | |||||
/// The name to use for the config. | |||||
/// </summary> | |||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global | |||||
public string Name { get; private set; } | |||||
/// <inheritdoc /> | |||||
/// <summary> | |||||
/// Constructs the attribute with a specific name. | |||||
/// </summary> | |||||
/// <param name="name">the name to use for the config.</param> | |||||
public NameAttribute(string name) | |||||
{ | |||||
Name = name; | |||||
} | |||||
} | |||||
private static readonly Dictionary<string, Type> registeredProviders = new Dictionary<string, Type>(); | |||||
/// <summary> | |||||
/// Registers a <see cref="IConfigProvider"/> to use for configs. | |||||
/// </summary> | |||||
/// <typeparam name="T">the type to register</typeparam> | |||||
public static void Register<T>() where T : IConfigProvider => Register(typeof(T)); | |||||
/// <summary> | |||||
/// Registers a <see cref="IConfigProvider"/> to use for configs. | |||||
/// </summary> | |||||
/// <param name="type">the type to register</param> | |||||
public static void Register(Type type) | |||||
{ | |||||
if (!(type.GetCustomAttribute(typeof(TypeAttribute)) is TypeAttribute ext)) | |||||
throw new InvalidOperationException("Type does not have TypeAttribute"); | |||||
if (!typeof(IConfigProvider).IsAssignableFrom(type)) | |||||
throw new InvalidOperationException("Type not IConfigProvider"); | |||||
if (registeredProviders.ContainsKey(ext.Extension)) | |||||
throw new InvalidOperationException($"Extension provider for {ext.Extension} already exists"); | |||||
registeredProviders.Add(ext.Extension, type); | |||||
} | |||||
private static SortedList<Ref<DateTime>, IConfigProvider> configProviders = new SortedList<Ref<DateTime>, IConfigProvider>(); | |||||
/// <summary> | |||||
/// Gets an <see cref="IConfigProvider"/> using the specified list pf preferred config types. | |||||
/// </summary> | |||||
/// <param name="configName">the name of the mod for this config</param> | |||||
/// <param name="extensions">the preferred config types to try to get</param> | |||||
/// <returns>an <see cref="IConfigProvider"/> of the requested type, or of type JSON.</returns> | |||||
public static IConfigProvider GetProviderFor(string configName, params string[] extensions) | |||||
{ | |||||
var chosenExt = extensions.FirstOrDefault(s => registeredProviders.ContainsKey(s)) ?? "json"; | |||||
var type = registeredProviders[chosenExt]; | |||||
var provider = Activator.CreateInstance(type) as IConfigProvider; | |||||
if (provider != null) | |||||
{ | |||||
provider.Filename = Path.Combine(BeatSaber.UserDataPath, configName); | |||||
configProviders.Add(provider.LastModified, provider); | |||||
} | |||||
return provider; | |||||
} | |||||
internal static IConfigProvider GetProviderFor(string modName, ParameterInfo info) | |||||
{ | |||||
var prefs = new string[0]; | |||||
if (info.GetCustomAttribute<PreferAttribute>() is PreferAttribute prefer) | |||||
prefs = prefer.PreferenceOrder; | |||||
if (info.GetCustomAttribute<NameAttribute>() is NameAttribute name) | |||||
modName = name.Name; | |||||
return GetProviderFor(modName, prefs); | |||||
} | |||||
private static Dictionary<IConfigProvider, Action> linkedProviders = | |||||
new Dictionary<IConfigProvider, Action>(); | |||||
/// <summary> | |||||
/// Creates a linked <see cref="Ref{T}"/> for the config provider. This <see cref="Ref{T}"/> will be automatically updated whenever the file on-disk changes. | |||||
/// </summary> | |||||
/// <typeparam name="T">the type of the parsed value</typeparam> | |||||
/// <param name="config">the <see cref="IConfigProvider"/> to create a link to</param> | |||||
/// <param name="onChange">an action to perform on value change</param> | |||||
/// <returns>a <see cref="Ref{T}"/> to an ever-changing value, mirroring whatever the file contains.</returns> | |||||
public static Ref<T> MakeLink<T>(this IConfigProvider config, Action<IConfigProvider, Ref<T>> onChange = null) | |||||
{ | |||||
Ref<T> @ref = config.Parse<T>(); | |||||
void ChangeDelegate() | |||||
{ | |||||
@ref.Value = config.Parse<T>(); | |||||
onChange?.Invoke(config, @ref); | |||||
} | |||||
if (linkedProviders.ContainsKey(config)) | |||||
linkedProviders[config] = (Action) Delegate.Combine(linkedProviders[config], (Action) ChangeDelegate); | |||||
else | |||||
linkedProviders.Add(config, ChangeDelegate); | |||||
ChangeDelegate(); | |||||
return @ref; | |||||
} | |||||
/// <summary> | |||||
/// Removes all linked <see cref="Ref{T}"/> such that they are no longer updated. | |||||
/// </summary> | |||||
/// <param name="config">the <see cref="IConfigProvider"/> to unlink</param> | |||||
public static void RemoveLinks(this IConfigProvider config) | |||||
{ | |||||
if (linkedProviders.ContainsKey(config)) | |||||
linkedProviders.Remove(config); | |||||
} | |||||
internal static void Update() | |||||
{ | |||||
foreach (var provider in configProviders) | |||||
{ | |||||
if (provider.Value.LastModified > provider.Key.Value) | |||||
{ | |||||
try | |||||
{ | |||||
provider.Value.Load(); // auto reload if it changes | |||||
provider.Key.Value = provider.Value.LastModified; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logging.Logger.config.Error("Error when trying to load config"); | |||||
Logging.Logger.config.Error(e); | |||||
} | |||||
} | |||||
if (provider.Value.HasChanged) | |||||
{ | |||||
try | |||||
{ | |||||
provider.Value.Save(); | |||||
provider.Key.Value = DateTime.Now; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logging.Logger.config.Error("Error when trying to save config"); | |||||
Logging.Logger.config.Error(e); | |||||
} | |||||
} | |||||
if (provider.Value.InMemoryChanged) | |||||
{ | |||||
provider.Value.InMemoryChanged = false; | |||||
try | |||||
{ | |||||
if (linkedProviders.ContainsKey(provider.Value)) | |||||
linkedProviders[provider.Value](); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logging.Logger.config.Error("Error running link change events"); | |||||
Logging.Logger.config.Error(e); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
internal static void Save() | |||||
{ | |||||
foreach (var provider in configProviders) | |||||
if (provider.Value.HasChanged) | |||||
try | |||||
{ | |||||
provider.Value.Save(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logging.Logger.config.Error("Error when trying to save config"); | |||||
Logging.Logger.config.Error(e); | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,48 @@ | |||||
using IPA.Logging; | |||||
using IPA.Utilities; | |||||
namespace IPA.Config | |||||
{ | |||||
internal class SelfConfig | |||||
{ | |||||
private static IConfigProvider _loaderConfig; | |||||
public static IConfigProvider LoaderConfig | |||||
{ | |||||
get => _loaderConfig; | |||||
set | |||||
{ | |||||
_loaderConfig?.RemoveLinks(); | |||||
value.Load(); | |||||
SelfConfigRef = value.MakeLink<SelfConfig>((c, v) => | |||||
{ | |||||
if (v.Value.Regenerate) | |||||
c.Store(v.Value = new SelfConfig { Regenerate = false }); | |||||
StandardLogger.Configure(v.Value); | |||||
}); | |||||
_loaderConfig = value; | |||||
} | |||||
} | |||||
public static Ref<SelfConfig> SelfConfigRef; | |||||
public static void Set() | |||||
{ | |||||
LoaderConfig = Config.GetProviderFor(IPA_Name, "json"); | |||||
} | |||||
internal const string IPA_Name = "Beat Saber IPA"; | |||||
internal const string IPA_Version = "3.12.0"; | |||||
public bool Regenerate = true; | |||||
public class DebugObject | |||||
{ | |||||
public bool ShowCallSource = false; | |||||
public bool ShowDebug = false; | |||||
} | |||||
public DebugObject Debug = new DebugObject(); | |||||
} | |||||
} |
@ -0,0 +1,25 @@ | |||||
using System; | |||||
using IPA.Updating.ModSaber; | |||||
using Newtonsoft.Json; | |||||
using SemVer; | |||||
namespace IPA.JsonConverters | |||||
{ | |||||
internal class ModSaberDependencyConverter : JsonConverter<ApiEndpoint.Mod.Dependency> | |||||
{ | |||||
public override ApiEndpoint.Mod.Dependency ReadJson(JsonReader reader, Type objectType, ApiEndpoint.Mod.Dependency existingValue, bool hasExistingValue, JsonSerializer serializer) | |||||
{ | |||||
var parts = (reader.Value as string)?.Split('@'); | |||||
return new ApiEndpoint.Mod.Dependency | |||||
{ | |||||
Name = parts?[0], | |||||
VersionRange = new Range(parts?[1]) | |||||
}; | |||||
} | |||||
public override void WriteJson(JsonWriter writer, ApiEndpoint.Mod.Dependency value, JsonSerializer serializer) | |||||
{ | |||||
writer.WriteValue($"{value.Name}@{value.VersionRange}"); | |||||
} | |||||
} | |||||
} |
@ -1,9 +1,9 @@ | |||||
using Newtonsoft.Json; | |||||
using SemVer; | |||||
using System; | |||||
using System; | |||||
using System.Diagnostics.CodeAnalysis; | using System.Diagnostics.CodeAnalysis; | ||||
using Newtonsoft.Json; | |||||
using SemVer; | |||||
namespace IPA.Updating.Converters | |||||
namespace IPA.JsonConverters | |||||
{ | { | ||||
[SuppressMessage("ReSharper", "UnusedMember.Global")] | [SuppressMessage("ReSharper", "UnusedMember.Global")] | ||||
internal class SemverRangeConverter : JsonConverter<Range> | internal class SemverRangeConverter : JsonConverter<Range> |
@ -1,12 +1,12 @@ | |||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System; | |||||
using Newtonsoft.Json; | |||||
using Version = SemVer.Version; | using Version = SemVer.Version; | ||||
namespace IPA.Updating.Converters | |||||
namespace IPA.JsonConverters | |||||
{ | { | ||||
internal class SemverVersionConverter : JsonConverter<Version> | internal class SemverVersionConverter : JsonConverter<Version> | ||||
{ | { | ||||
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => new Version(reader.Value as string); | |||||
public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer) => new Version(reader.Value as string, true); | |||||
public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer) => writer.WriteValue(value.ToString()); | public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer) => writer.WriteValue(value.ToString()); | ||||
} | } |
@ -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 { } | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,197 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace IPA.Loader.Features | |||||
{ | |||||
/// <summary> | |||||
/// The root interface for a mod Feature. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. | |||||
/// </remarks> | |||||
public abstract class Feature | |||||
{ | |||||
/// <summary> | |||||
/// Initializes the feature with the parameters provided in the definition. | |||||
/// | |||||
/// Note: When no parenthesis are provided, <paramref name="parameters"/> is an empty array. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Returning <see langword="false" /> does *not* prevent the plugin from being loaded. It simply prevents the feature from being used. | |||||
/// </remarks> | |||||
/// <param name="meta">the metadata of the plugin that is being prepared</param> | |||||
/// <param name="parameters">the parameters passed to the feature definition, or null</param> | |||||
/// <returns><see langword="true"/> if the feature is valid for the plugin, <see langword="false"/> otherwise</returns> | |||||
public abstract bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters); | |||||
/// <summary> | |||||
/// Evaluates the Feature for use in conditional meta-Features. This should be re-calculated on every call, unless it can be proven to not change. | |||||
/// | |||||
/// This will be called on every feature that returns <see langword="true" /> from <see cref="Initialize"/> | |||||
/// </summary> | |||||
/// <returns>the truthiness of the Feature.</returns> | |||||
public virtual bool Evaluate() => true; | |||||
/// <summary> | |||||
/// The message to be logged when the feature is not valid for a plugin. | |||||
/// This should also be set whenever either <see cref="BeforeLoad"/> or <see cref="BeforeInit"/> returns false. | |||||
/// </summary> | |||||
public virtual string InvalidMessage { get; protected set; } | |||||
/// <summary> | |||||
/// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// The assembly will still be loaded, but the plugin will not be constructed if this returns <see langword="false" />. | |||||
/// Any features it defines, for example, will still be loaded. | |||||
/// </remarks> | |||||
/// <param name="plugin">the plugin about to be loaded</param> | |||||
/// <returns>whether or not the plugin should be loaded</returns> | |||||
public virtual bool BeforeLoad(PluginLoader.PluginMetadata plugin) => true; | |||||
/// <summary> | |||||
/// Called before a plugin's Init method is called. This will not be called if there is no Init method. This should never throw an exception. An exception will abort the loading of the plugin with an error. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to be initialized</param> | |||||
/// <returns>whether or not to call the Init method</returns> | |||||
public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; | |||||
/// <summary> | |||||
/// Called after a plugin has been fully initialized, whether or not there is an Init method. This should never throw an exception. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was just initialized</param> | |||||
public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } | |||||
/// <summary> | |||||
/// Ensures a plugin's assembly is loaded. Do not use unless you need to. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to ensure is loaded.</param> | |||||
protected void RequireLoaded(PluginLoader.PluginMetadata plugin) => PluginLoader.Load(plugin); | |||||
internal virtual bool StoreOnPlugin => true; | |||||
private static readonly Dictionary<string, Type> featureTypes = new Dictionary<string, Type> | |||||
{ | |||||
{ "define-feature", typeof(DefineFeature) } | |||||
}; | |||||
internal static bool HasFeature(string name) => featureTypes.ContainsKey(name); | |||||
internal static bool RegisterFeature(string name, Type type) | |||||
{ | |||||
if (!typeof(Feature).IsAssignableFrom(type)) | |||||
throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type)); | |||||
if (featureTypes.ContainsKey(name)) return false; | |||||
featureTypes.Add(name, type); | |||||
return true; | |||||
} | |||||
internal struct FeatureParse | |||||
{ | |||||
public readonly string Name; | |||||
public readonly string[] Parameters; | |||||
public FeatureParse(string name, string[] parameters) | |||||
{ | |||||
Name = name; | |||||
Parameters = parameters; | |||||
} | |||||
} | |||||
// returns false with both outs null for no such feature | |||||
internal static bool TryParseFeature(string featureString, PluginLoader.PluginMetadata plugin, | |||||
out Feature feature, out Exception failException, out bool featureValid, out FeatureParse parsed, | |||||
FeatureParse? preParsed = null) | |||||
{ | |||||
failException = null; | |||||
feature = null; | |||||
featureValid = false; | |||||
if (preParsed == null) | |||||
{ | |||||
var builder = new StringBuilder(); | |||||
string name = null; | |||||
var parameters = new List<string>(); | |||||
bool escape = false; | |||||
int parens = 0; | |||||
bool removeWhitespace = true; | |||||
foreach (var chr in featureString) | |||||
{ | |||||
if (escape) | |||||
{ | |||||
builder.Append(chr); | |||||
escape = false; | |||||
} | |||||
else | |||||
{ | |||||
switch (chr) | |||||
{ | |||||
case '\\': | |||||
escape = true; | |||||
break; | |||||
case '(': | |||||
parens++; | |||||
if (parens != 1) goto default; | |||||
removeWhitespace = true; | |||||
name = builder.ToString(); | |||||
builder.Clear(); | |||||
break; | |||||
case ')': | |||||
parens--; | |||||
if (parens != 0) goto default; | |||||
goto case ','; | |||||
case ',': | |||||
if (parens > 1) goto default; | |||||
parameters.Add(builder.ToString()); | |||||
builder.Clear(); | |||||
removeWhitespace = true; | |||||
break; | |||||
default: | |||||
if (removeWhitespace && !char.IsWhiteSpace(chr)) | |||||
removeWhitespace = false; | |||||
if (!removeWhitespace) | |||||
builder.Append(chr); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (name == null) | |||||
name = builder.ToString(); | |||||
parsed = new FeatureParse(name, parameters.ToArray()); | |||||
if (parens != 0) | |||||
{ | |||||
failException = new Exception("Malformed feature definition"); | |||||
return false; | |||||
} | |||||
} | |||||
else | |||||
parsed = preParsed.Value; | |||||
if (!featureTypes.TryGetValue(parsed.Name, out var featureType)) | |||||
return false; | |||||
try | |||||
{ | |||||
if (!(Activator.CreateInstance(featureType) is Feature aFeature)) | |||||
{ | |||||
failException = new InvalidCastException("Feature type not a subtype of Feature"); | |||||
return false; | |||||
} | |||||
featureValid = aFeature.Initialize(plugin, parsed.Parameters); | |||||
feature = aFeature; | |||||
return true; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
failException = e; | |||||
return false; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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"; | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,89 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using IPA.Config; | |||||
using IPA.Logging; | |||||
using IPA.Utilities; | |||||
namespace IPA.Loader | |||||
{ | |||||
/// <summary> | |||||
/// The type that handles value injecting into a plugin's Init. | |||||
/// </summary> | |||||
public static class PluginInitInjector | |||||
{ | |||||
/// <summary> | |||||
/// A typed injector for a plugin's Init method. When registered, called for all associated types. If it returns null, the default for the type will be used. | |||||
/// </summary> | |||||
/// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param> | |||||
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param> | |||||
/// <param name="meta">the <see cref="PluginLoader.PluginMetadata"/> for the plugin being loaded.</param> | |||||
/// <returns>the value to inject into that parameter.</returns> | |||||
public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta); | |||||
/// <summary> | |||||
/// Adds an injector to be used when calling future plugins' Init methods. | |||||
/// </summary> | |||||
/// <param name="type">the type of the parameter.</param> | |||||
/// <param name="injector">the function to call for injection.</param> | |||||
public static void AddInjector(Type type, InjectParameter injector) | |||||
{ | |||||
injectors.Add(new Tuple<Type, InjectParameter>(type, injector)); | |||||
} | |||||
private static readonly List<Tuple<Type, InjectParameter>> injectors = new List<Tuple<Type, InjectParameter>> | |||||
{ | |||||
new Tuple<Type, InjectParameter>(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), | |||||
new Tuple<Type, InjectParameter>(typeof(IModPrefs), (prev, param, meta) => prev ?? new ModPrefs(meta)), | |||||
new Tuple<Type, InjectParameter>(typeof(IConfigProvider), (prev, param, meta) => | |||||
{ | |||||
if (prev != null) return prev; | |||||
var cfgProvider = Config.Config.GetProviderFor(meta.Name, param); | |||||
cfgProvider.Load(); | |||||
return cfgProvider; | |||||
}) | |||||
}; | |||||
internal static void Inject(MethodInfo init, PluginLoader.PluginInfo info) | |||||
{ | |||||
var instance = info.Plugin; | |||||
var meta = info.Metadata; | |||||
var initArgs = new List<object>(); | |||||
var initParams = init.GetParameters(); | |||||
Dictionary<Tuple<Type, InjectParameter>, object> previousValues = | |||||
new Dictionary<Tuple<Type, InjectParameter>, object>(injectors.Count); | |||||
foreach (var param in initParams) | |||||
{ | |||||
var paramType = param.ParameterType; | |||||
var value = paramType.GetDefault(); | |||||
foreach (var pair in injectors.Where(t => paramType.IsAssignableFrom(t.Item1))) | |||||
{ | |||||
object prev = null; | |||||
if (previousValues.ContainsKey(pair)) | |||||
prev = previousValues[pair]; | |||||
var val = pair.Item2?.Invoke(prev, param, meta); | |||||
if (previousValues.ContainsKey(pair)) | |||||
previousValues[pair] = val; | |||||
else | |||||
previousValues.Add(pair, val); | |||||
if (val == null) continue; | |||||
value = val; | |||||
break; | |||||
} | |||||
initArgs.Add(value); | |||||
} | |||||
init.Invoke(instance, initArgs.ToArray()); | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,434 @@ | |||||
using IPA.Loader.Features; | |||||
using IPA.Logging; | |||||
using IPA.Utilities; | |||||
using Mono.Cecil; | |||||
using Newtonsoft.Json; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Reflection; | |||||
using System.Threading.Tasks; | |||||
using Version = SemVer.Version; | |||||
namespace IPA.Loader | |||||
{ | |||||
/// <summary> | |||||
/// A type to manage the loading of plugins. | |||||
/// </summary> | |||||
public class PluginLoader | |||||
{ | |||||
internal static Task LoadTask() => Task.Run(() => | |||||
{ | |||||
LoadMetadata(); | |||||
Resolve(); | |||||
ComputeLoadOrder(); | |||||
InitFeatures(); | |||||
}); | |||||
/// <summary> | |||||
/// A class which describes | |||||
/// </summary> | |||||
public class PluginMetadata | |||||
{ | |||||
/// <summary> | |||||
/// The assembly the plugin was loaded from. | |||||
/// </summary> | |||||
public Assembly Assembly { get; internal set; } | |||||
/// <summary> | |||||
/// The TypeDefinition for the main type of the plugin. | |||||
/// </summary> | |||||
public TypeDefinition PluginType { get; internal set; } | |||||
/// <summary> | |||||
/// The human readable name of the plugin. | |||||
/// </summary> | |||||
public string Name { get; internal set; } | |||||
/// <summary> | |||||
/// The ModSaber ID of the plugin, or null if it doesn't have one. | |||||
/// </summary> | |||||
public string Id { get; internal set; } | |||||
/// <summary> | |||||
/// The version of the plugin. | |||||
/// </summary> | |||||
public Version Version { get; internal set; } | |||||
/// <summary> | |||||
/// The file the plugin was loaded from. | |||||
/// </summary> | |||||
public FileInfo File { get; internal set; } | |||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global | |||||
/// <summary> | |||||
/// The features this plugin requests. | |||||
/// </summary> | |||||
public IReadOnlyList<Feature> Features => InternalFeatures; | |||||
internal readonly List<Feature> InternalFeatures = new List<Feature>(); | |||||
internal bool IsSelf; | |||||
private PluginManifest manifest; | |||||
internal PluginManifest Manifest | |||||
{ | |||||
get => manifest; | |||||
set | |||||
{ | |||||
manifest = value; | |||||
Name = value.Name; | |||||
Version = value.Version; | |||||
Id = value.Id; | |||||
} | |||||
} | |||||
/// <inheritdoc /> | |||||
public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; | |||||
} | |||||
/// <summary> | |||||
/// A container object for all the data relating to a plugin. | |||||
/// </summary> | |||||
public class PluginInfo | |||||
{ | |||||
internal IBeatSaberPlugin Plugin { get; set; } | |||||
/// <summary> | |||||
/// Metadata for the plugin. | |||||
/// </summary> | |||||
public PluginMetadata Metadata { get; internal set; } = new PluginMetadata(); | |||||
} | |||||
internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>(); | |||||
internal static void LoadMetadata() | |||||
{ | |||||
string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll"); | |||||
try | |||||
{ | |||||
var selfMeta = new PluginMetadata | |||||
{ | |||||
Assembly = Assembly.GetExecutingAssembly(), | |||||
File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")), | |||||
PluginType = null, | |||||
IsSelf = true | |||||
}; | |||||
string manifest; | |||||
using (var manifestReader = | |||||
new StreamReader( | |||||
selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ?? | |||||
throw new InvalidOperationException())) | |||||
manifest = manifestReader.ReadToEnd(); | |||||
selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest); | |||||
PluginsMetadata.Add(selfMeta); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.loader.Critical("Error loading own manifest"); | |||||
Logger.loader.Critical(e); | |||||
} | |||||
foreach (var plugin in plugins) | |||||
{ | |||||
try | |||||
{ | |||||
var metadata = new PluginMetadata | |||||
{ | |||||
File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)), | |||||
IsSelf = false | |||||
}; | |||||
var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters | |||||
{ | |||||
ReadingMode = ReadingMode.Immediate, | |||||
ReadWrite = false, | |||||
AssemblyResolver = new CecilLibLoader() | |||||
}).MainModule; | |||||
var iBeatSaberPlugin = pluginModule.ImportReference(typeof(IBeatSaberPlugin)); | |||||
foreach (var type in pluginModule.Types) | |||||
{ | |||||
foreach (var inter in type.Interfaces) | |||||
{ | |||||
var ifType = inter.InterfaceType; | |||||
if (iBeatSaberPlugin.FullName == ifType.FullName) | |||||
{ | |||||
metadata.PluginType = type; | |||||
break; | |||||
} | |||||
} | |||||
if (metadata.PluginType != null) break; | |||||
} | |||||
if (metadata.PluginType == null) | |||||
{ | |||||
Logger.loader.Warn($"Could not find plugin type for {Path.GetFileName(plugin)}"); | |||||
continue; | |||||
} | |||||
foreach (var resource in pluginModule.Resources) | |||||
{ | |||||
if (!(resource is EmbeddedResource embedded) || | |||||
embedded.Name != $"{metadata.PluginType.Namespace}.manifest.json") continue; | |||||
string manifest; | |||||
using (var manifestReader = new StreamReader(embedded.GetResourceStream())) | |||||
manifest = manifestReader.ReadToEnd(); | |||||
metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest); | |||||
break; | |||||
} | |||||
Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}"); | |||||
PluginsMetadata.Add(metadata); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}"); | |||||
Logger.loader.Error(e); | |||||
} | |||||
} | |||||
} | |||||
internal static void Resolve() | |||||
{ // resolves duplicates and conflicts, etc | |||||
PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version)); | |||||
var ids = new HashSet<string>(); | |||||
var ignore = new HashSet<PluginMetadata>(); | |||||
var resolved = new List<PluginMetadata>(PluginsMetadata.Count); | |||||
foreach (var meta in PluginsMetadata) | |||||
{ | |||||
if (meta.Id != null) | |||||
{ | |||||
if (ids.Contains(meta.Id)) | |||||
{ | |||||
Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest"); | |||||
ignore.Add(meta); | |||||
continue; // because of sorted order, hightest order will always be the first one | |||||
} | |||||
bool processedLater = false; | |||||
foreach (var meta2 in PluginsMetadata) | |||||
{ | |||||
if (ignore.Contains(meta2)) continue; | |||||
if (meta == meta2) | |||||
{ | |||||
processedLater = true; | |||||
continue; | |||||
} | |||||
if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue; | |||||
var range = meta2.Manifest.Conflicts[meta.Id]; | |||||
if (!range.IsSatisfied(meta.Version)) continue; | |||||
Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}"); | |||||
if (processedLater) | |||||
{ | |||||
Logger.loader.Warn($"Ignoring {meta2.Name}"); | |||||
ignore.Add(meta2); | |||||
} | |||||
else | |||||
{ | |||||
Logger.loader.Warn($"Ignoring {meta.Name}"); | |||||
ignore.Add(meta); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (ignore.Contains(meta)) continue; | |||||
if (meta.Id != null) ids.Add(meta.Id); | |||||
resolved.Add(meta); | |||||
} | |||||
PluginsMetadata = resolved; | |||||
} | |||||
internal static void ComputeLoadOrder() | |||||
{ | |||||
PluginsMetadata.Sort((a, b) => | |||||
{ | |||||
if (a.Id == b.Id) return 0; | |||||
if (a.Id != null) | |||||
{ | |||||
if (b.Manifest.Dependencies.ContainsKey(a.Id) || b.Manifest.LoadAfter.Contains(a.Id)) return -1; | |||||
if (b.Manifest.LoadBefore.Contains(a.Id)) return 1; | |||||
} | |||||
if (b.Id != null) | |||||
{ | |||||
if (a.Manifest.Dependencies.ContainsKey(b.Id) || a.Manifest.LoadAfter.Contains(b.Id)) return 1; | |||||
if (a.Manifest.LoadBefore.Contains(b.Id)) return -1; | |||||
} | |||||
return 0; | |||||
}); | |||||
var metadata = new List<PluginMetadata>(); | |||||
var pluginsToLoad = new Dictionary<string, Version>(); | |||||
foreach (var meta in PluginsMetadata) | |||||
{ | |||||
bool load = true; | |||||
foreach (var dep in meta.Manifest.Dependencies) | |||||
{ | |||||
if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) continue; | |||||
load = false; | |||||
Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}"); | |||||
} | |||||
if (load) | |||||
{ | |||||
metadata.Add(meta); | |||||
if (meta.Id != null) | |||||
pluginsToLoad.Add(meta.Id, meta.Version); | |||||
} | |||||
} | |||||
PluginsMetadata = metadata; | |||||
} | |||||
internal static void InitFeatures() | |||||
{ | |||||
var parsedFeatures = PluginsMetadata.Select(m => | |||||
Tuple.Create(m, | |||||
m.Manifest.Features.Select(f => | |||||
Tuple.Create(f, Ref.Create<Feature.FeatureParse?>(null)) | |||||
).ToList() | |||||
) | |||||
).ToList(); | |||||
while (DefineFeature.NewFeature) | |||||
{ | |||||
DefineFeature.NewFeature = false; | |||||
foreach (var plugin in parsedFeatures) | |||||
for (var i = 0; i < plugin.Item2.Count; i++) | |||||
{ | |||||
var feature = plugin.Item2[i]; | |||||
var success = Feature.TryParseFeature(feature.Item1, plugin.Item1, out var featureObj, | |||||
out var exception, out var valid, out var parsed, feature.Item2.Value); | |||||
if (!success && !valid && featureObj == null && exception == null) // no feature of type found | |||||
feature.Item2.Value = parsed; | |||||
else if (success) | |||||
{ | |||||
if (valid && featureObj.StoreOnPlugin) | |||||
plugin.Item1.InternalFeatures.Add(featureObj); | |||||
else if (!valid) | |||||
Logger.features.Warn( | |||||
$"Feature not valid on {plugin.Item1.Name}: {featureObj.InvalidMessage}"); | |||||
plugin.Item2.RemoveAt(i--); | |||||
} | |||||
else | |||||
{ | |||||
Logger.features.Error($"Error parsing feature definition on {plugin.Item1.Name}"); | |||||
Logger.features.Error(exception); | |||||
plugin.Item2.RemoveAt(i--); | |||||
} | |||||
} | |||||
foreach (var plugin in PluginsMetadata) | |||||
foreach (var feature in plugin.Features) | |||||
feature.Evaluate(); | |||||
} | |||||
foreach (var plugin in parsedFeatures) | |||||
{ | |||||
if (plugin.Item2.Count <= 0) continue; | |||||
Logger.features.Warn($"On plugin {plugin.Item1.Name}:"); | |||||
foreach (var feature in plugin.Item2) | |||||
Logger.features.Warn($" Feature not found with name {feature.Item1}"); | |||||
} | |||||
} | |||||
internal static void Load(PluginMetadata meta) | |||||
{ | |||||
if (meta.Assembly == null) | |||||
meta.Assembly = Assembly.LoadFrom(meta.File.FullName); | |||||
} | |||||
internal static PluginInfo InitPlugin(PluginMetadata meta) | |||||
{ | |||||
if (meta.PluginType == null) | |||||
return new PluginInfo() | |||||
{ | |||||
Metadata = meta, | |||||
Plugin = null | |||||
}; | |||||
var info = new PluginInfo(); | |||||
try | |||||
{ | |||||
Load(meta); | |||||
Feature denyingFeature = null; | |||||
if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta))) | |||||
{ | |||||
Logger.loader.Warn( | |||||
$"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}"); | |||||
return null; | |||||
} | |||||
var type = meta.Assembly.GetType(meta.PluginType.FullName); | |||||
var instance = (IBeatSaberPlugin)Activator.CreateInstance(type); | |||||
info.Metadata = meta; | |||||
info.Plugin = instance; | |||||
var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public); | |||||
if (init != null) | |||||
{ | |||||
denyingFeature = null; | |||||
if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info))) | |||||
{ | |||||
Logger.loader.Warn( | |||||
$"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}"); | |||||
return null; | |||||
} | |||||
PluginInitInjector.Inject(init, info); | |||||
} | |||||
foreach (var feature in meta.Features) | |||||
try | |||||
{ | |||||
feature.AfterInit(info); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}"); | |||||
} | |||||
} | |||||
catch (AmbiguousMatchException) | |||||
{ | |||||
Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})"); | |||||
return null; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
Logger.loader.Error($"Could not init plugin {meta.Name}: {e}"); | |||||
return null; | |||||
} | |||||
return info; | |||||
} | |||||
internal static List<PluginInfo> LoadPlugins() => PluginsMetadata.Select(InitPlugin).Where(p => p != null).ToList(); | |||||
} | |||||
} |
@ -0,0 +1,43 @@ | |||||
using IPA.JsonConverters; | |||||
using Newtonsoft.Json; | |||||
using SemVer; | |||||
using System.Collections.Generic; | |||||
namespace IPA.Loader | |||||
{ | |||||
internal class PluginManifest | |||||
{ | |||||
[JsonProperty("name", Required = Required.Always)] | |||||
public string Name; | |||||
[JsonProperty("id", Required = Required.AllowNull)] | |||||
public string Id; | |||||
[JsonProperty("description", Required = Required.Always)] | |||||
public string Description; | |||||
[JsonProperty("version", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] | |||||
public Version Version; | |||||
[JsonProperty("gameVersion", Required = Required.Always), JsonConverter(typeof(SemverVersionConverter))] | |||||
public Version GameVersion; | |||||
[JsonProperty("author", Required = Required.Always)] | |||||
public string Author; | |||||
[JsonProperty("dependsOn", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] | |||||
public Dictionary<string, Range> Dependencies = new Dictionary<string, Range>(); | |||||
[JsonProperty("conflictsWith", Required = Required.DisallowNull, ItemConverterType = typeof(SemverRangeConverter))] | |||||
public Dictionary<string, Range> Conflicts = new Dictionary<string, Range>(); | |||||
[JsonProperty("features", Required = Required.Always)] | |||||
public string[] Features; | |||||
[JsonProperty("loadBefore", Required = Required.DisallowNull)] | |||||
public string[] LoadBefore = new string[0]; | |||||
[JsonProperty("loadAfter", Required = Required.DisallowNull)] | |||||
public string[] LoadAfter = new string[0]; | |||||
} | |||||
} |
@ -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)" | |||||
] | |||||
} |
@ -1,25 +0,0 @@ | |||||
using System; | |||||
using Newtonsoft.Json; | |||||
using SemVer; | |||||
using static IPA.Updating.ModSaber.ApiEndpoint.Mod; | |||||
namespace IPA.Updating.Converters | |||||
{ | |||||
internal class ModSaberDependencyConverter : JsonConverter<Dependency> | |||||
{ | |||||
public override Dependency ReadJson(JsonReader reader, Type objectType, Dependency existingValue, bool hasExistingValue, JsonSerializer serializer) | |||||
{ | |||||
var parts = (reader.Value as string)?.Split('@'); | |||||
return new Dependency | |||||
{ | |||||
Name = parts?[0], | |||||
VersionRange = new Range(parts?[1]) | |||||
}; | |||||
} | |||||
public override void WriteJson(JsonWriter writer, Dependency value, JsonSerializer serializer) | |||||
{ | |||||
writer.WriteValue($"{value.Name}@{value.VersionRange}"); | |||||
} | |||||
} | |||||
} |
@ -1,134 +0,0 @@ | |||||
using Mono.Cecil; | |||||
using Mono.Cecil.Cil; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
namespace IPA.Patcher | |||||
{ | |||||
internal class PatchedModule | |||||
{ | |||||
private static readonly string[] EntryTypes = { "Input", "Display" }; | |||||
private readonly FileInfo _file; | |||||
private ModuleDefinition _module; | |||||
internal struct PatchData { | |||||
public bool IsPatched; | |||||
public Version Version; | |||||
} | |||||
public static PatchedModule Load(string engineFile) | |||||
{ | |||||
return new PatchedModule(engineFile); | |||||
} | |||||
private PatchedModule(string engineFile) | |||||
{ | |||||
_file = new FileInfo(engineFile); | |||||
LoadModules(); | |||||
} | |||||
private void LoadModules() | |||||
{ | |||||
var resolver = new DefaultAssemblyResolver(); | |||||
resolver.AddSearchDirectory(_file.DirectoryName); | |||||
var parameters = new ReaderParameters | |||||
{ | |||||
AssemblyResolver = resolver, | |||||
}; | |||||
_module = ModuleDefinition.ReadModule(_file.FullName, parameters); | |||||
} | |||||
public PatchData Data | |||||
{ | |||||
get | |||||
{ | |||||
var data = new PatchData { IsPatched = false, Version = null }; | |||||
foreach (var @ref in _module.AssemblyReferences) { | |||||
switch (@ref.Name) | |||||
{ | |||||
case "IllusionInjector": | |||||
case "IllusionPlugin": | |||||
data = new PatchData { IsPatched = true, Version = new Version(0, 0, 0, 0) }; | |||||
break; | |||||
case "IPA.Injector": | |||||
return new PatchData { IsPatched = true, Version = @ref.Version }; | |||||
} | |||||
} | |||||
return data; | |||||
} | |||||
} | |||||
public void Patch(Version v) | |||||
{ | |||||
// First, let's add the reference | |||||
var nameReference = new AssemblyNameReference("IPA.Injector", v); | |||||
var injectorPath = Path.Combine(_file.DirectoryName ?? throw new InvalidOperationException(), "IPA.Injector.dll"); | |||||
var injector = ModuleDefinition.ReadModule(injectorPath); | |||||
bool hasIPAInjector = false; | |||||
for (int i = 0; i < _module.AssemblyReferences.Count; i++) | |||||
{ | |||||
if (_module.AssemblyReferences[i].Name == "IllusionInjector") | |||||
_module.AssemblyReferences.RemoveAt(i--); | |||||
if (_module.AssemblyReferences[i].Name == "IllusionPlugin") | |||||
_module.AssemblyReferences.RemoveAt(i--); | |||||
if (_module.AssemblyReferences[i].Name == "IPA.Injector") | |||||
{ | |||||
hasIPAInjector = true; | |||||
_module.AssemblyReferences[i].Version = v; | |||||
} | |||||
} | |||||
if (!hasIPAInjector) | |||||
{ | |||||
_module.AssemblyReferences.Add(nameReference); | |||||
int patched = 0; | |||||
foreach (var type in FindEntryTypes()) | |||||
{ | |||||
if (PatchType(type, injector)) | |||||
{ | |||||
patched++; | |||||
} | |||||
} | |||||
if (patched > 0) | |||||
{ | |||||
_module.Write(_file.FullName); | |||||
} | |||||
else | |||||
{ | |||||
throw new Exception("Could not find any entry type!"); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
_module.Write(_file.FullName); | |||||
} | |||||
} | |||||
private bool PatchType(TypeDefinition targetType, ModuleDefinition injector) | |||||
{ | |||||
var targetMethod = targetType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); | |||||
if (targetMethod != null) | |||||
{ | |||||
var methodReference = _module.ImportReference(injector.GetType("IPA.Injector.Injector").Methods.First(m => m.Name == "Inject")); | |||||
targetMethod.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Call, methodReference)); | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
private IEnumerable<TypeDefinition> FindEntryTypes() | |||||
{ | |||||
return _module.GetTypes().Where(m => EntryTypes.Contains(m.Name)); | |||||
} | |||||
} | |||||
} |
@ -1,116 +0,0 @@ | |||||
using Mono.Cecil; | |||||
using System; | |||||
using System.IO; | |||||
using System.Linq; | |||||
namespace IPA.Patcher | |||||
{ | |||||
class VirtualizedModule | |||||
{ | |||||
private readonly FileInfo _file; | |||||
private ModuleDefinition _module; | |||||
public static VirtualizedModule Load(string engineFile) | |||||
{ | |||||
return new VirtualizedModule(engineFile); | |||||
} | |||||
private VirtualizedModule(string assemblyFile) | |||||
{ | |||||
_file = new FileInfo(assemblyFile); | |||||
LoadModules(); | |||||
} | |||||
private void LoadModules() | |||||
{ | |||||
var resolver = new DefaultAssemblyResolver(); | |||||
resolver.AddSearchDirectory(_file.DirectoryName); | |||||
var parameters = new ReaderParameters | |||||
{ | |||||
AssemblyResolver = resolver, | |||||
}; | |||||
_module = ModuleDefinition.ReadModule(_file.FullName, parameters); | |||||
} | |||||
/// <summary> | |||||
/// | |||||
/// </summary> | |||||
public void Virtualize() | |||||
{ | |||||
foreach (var type in _module.Types) | |||||
{ | |||||
VirtualizeType(type); | |||||
} | |||||
Console.WriteLine(); | |||||
_module.Write(_file.FullName); | |||||
} | |||||
private void VirtualizeType(TypeDefinition type) | |||||
{ | |||||
if(type.IsSealed) | |||||
{ | |||||
// Unseal | |||||
type.IsSealed = false; | |||||
} | |||||
if (type.IsInterface) return; | |||||
if (type.IsAbstract) return; | |||||
// These two don't seem to work. | |||||
if (type.Name == "SceneControl" || type.Name == "ConfigUI") return; | |||||
//Console.CursorTop--; | |||||
Console.CursorLeft = 0; | |||||
Program.ClearLine(); | |||||
Console.Write("Virtualizing {0}", type.Name); | |||||
// Take care of sub types | |||||
foreach (var subType in type.NestedTypes) | |||||
{ | |||||
VirtualizeType(subType); | |||||
} | |||||
foreach (var method in type.Methods) | |||||
{ | |||||
if (method.IsManaged | |||||
&& method.IsIL | |||||
&& !method.IsStatic | |||||
&& !method.IsVirtual | |||||
&& !method.IsAbstract | |||||
&& !method.IsAddOn | |||||
&& !method.IsConstructor | |||||
&& !method.IsSpecialName | |||||
&& !method.IsGenericInstance | |||||
&& !method.HasOverrides) | |||||
{ | |||||
method.IsVirtual = true; | |||||
method.IsPublic = true; | |||||
method.IsPrivate = false; | |||||
method.IsNewSlot = true; | |||||
method.IsHideBySig = true; | |||||
} | |||||
} | |||||
foreach (var field in type.Fields) | |||||
{ | |||||
if (field.IsPrivate) field.IsFamily = true; | |||||
} | |||||
} | |||||
public bool IsVirtualized | |||||
{ | |||||
get | |||||
{ | |||||
var awakeMethods = _module.GetTypes().SelectMany(t => t.Methods.Where(m => m.Name == "Awake")); | |||||
var methodDefinitions = awakeMethods as MethodDefinition[] ?? awakeMethods.ToArray(); | |||||
if (!methodDefinitions.Any()) return false; | |||||
return ((float)methodDefinitions.Count(m => m.IsVirtual) / methodDefinitions.Count()) > 0.5f; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -1 +1 @@ | |||||
27a27b93f9cca058d6b4f09d77cb8a300416a979 | |||||
5d76d76cc5c14257f2b9071c928c27b3edd80cc0 |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -1,84 +0,0 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> | |||||
<PropertyGroup> | |||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> | |||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> | |||||
<ProjectGuid>{F08C3C7A-3221-432E-BAB8-32BCE58408C8}</ProjectGuid> | |||||
<OutputType>Library</OutputType> | |||||
<AppDesignerFolder>Properties</AppDesignerFolder> | |||||
<RootNamespace>MSBuildTasks</RootNamespace> | |||||
<AssemblyName>MSBuildTasks</AssemblyName> | |||||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion> | |||||
<FileAlignment>512</FileAlignment> | |||||
<TargetFrameworkProfile /> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> | |||||
<DebugSymbols>true</DebugSymbols> | |||||
<DebugType>full</DebugType> | |||||
<Optimize>false</Optimize> | |||||
<OutputPath>bin\Debug\</OutputPath> | |||||
<DefineConstants>DEBUG;TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
</PropertyGroup> | |||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> | |||||
<DebugType>pdbonly</DebugType> | |||||
<Optimize>true</Optimize> | |||||
<OutputPath>bin\Release\</OutputPath> | |||||
<DefineConstants>TRACE</DefineConstants> | |||||
<ErrorReport>prompt</ErrorReport> | |||||
<WarningLevel>4</WarningLevel> | |||||
<UseVSHostingProcess>true</UseVSHostingProcess> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<Reference Include="Microsoft.Build.Framework, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Microsoft.Build.Framework.15.9.20\lib\net46\Microsoft.Build.Framework.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Microsoft.Build.Utilities.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Microsoft.Build.Utilities.Core.15.9.20\lib\net46\Microsoft.Build.Utilities.Core.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Microsoft.VisualStudio.Setup.Configuration.Interop, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Microsoft.VisualStudio.Setup.Configuration.Interop.1.16.30\lib\net35\Microsoft.VisualStudio.Setup.Configuration.Interop.dll</HintPath> | |||||
<EmbedInteropTypes>True</EmbedInteropTypes> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil.Mdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Mdb.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil.Pdb, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Pdb.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="Mono.Cecil.Rocks, Version=0.10.1.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\Mono.Cecil.0.10.1\lib\net40\Mono.Cecil.Rocks.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="System" /> | |||||
<Reference Include="System.Collections.Immutable, Version=1.2.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\System.Collections.Immutable.1.5.0\lib\netstandard1.3\System.Collections.Immutable.dll</HintPath> | |||||
</Reference> | |||||
<Reference Include="System.Configuration" /> | |||||
<Reference Include="System.Core" /> | |||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> | |||||
<HintPath>..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath> | |||||
<Private>True</Private> | |||||
</Reference> | |||||
<Reference Include="System.Xaml" /> | |||||
<Reference Include="System.Xml.Linq" /> | |||||
<Reference Include="System.Data.DataSetExtensions" /> | |||||
<Reference Include="Microsoft.CSharp" /> | |||||
<Reference Include="System.Data" /> | |||||
<Reference Include="System.Net.Http" /> | |||||
<Reference Include="System.Xml" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Compile Include="AssemblyRenameTask.cs" /> | |||||
<Compile Include="Pdb2Mdb.cs" /> | |||||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<None Include="packages.config" /> | |||||
</ItemGroup> | |||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> | |||||
</Project> |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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")] |
@ -1,9 +0,0 @@ | |||||
<?xml version="1.0" encoding="utf-8"?> | |||||
<packages> | |||||
<package id="Microsoft.Build.Framework" version="15.9.20" targetFramework="net46" /> | |||||
<package id="Microsoft.Build.Utilities.Core" version="15.9.20" targetFramework="net46" /> | |||||
<package id="Microsoft.VisualStudio.Setup.Configuration.Interop" version="1.16.30" targetFramework="net46" developmentDependency="true" /> | |||||
<package id="Mono.Cecil" version="0.10.1" targetFramework="net46" /> | |||||
<package id="System.Collections.Immutable" version="1.5.0" targetFramework="net46" /> | |||||
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net46" /> | |||||
</packages> |