mirror of
https://github.com/BepInEx/BepInEx.git
synced 2026-10-20 01:46:13 +00:00
32 files changed
+2064
-272
No files matched your search
@@ -277,23 +277,20 @@ namespace BepInEx.Preloader.Patching
|
||||
|
||||
#region Config
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader",
|
||||
"DumpAssemblies",
|
||||
"If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers.",
|
||||
false);
|
||||
private static readonly ConfigEntry<bool> ConfigDumpAssemblies = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader", "DumpAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader",
|
||||
"LoadDumpedAssemblies",
|
||||
"If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies.",
|
||||
false);
|
||||
private static readonly ConfigEntry<bool> ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader", "LoadDumpedAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader",
|
||||
"BreakBeforeLoadAssemblies",
|
||||
"If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded.",
|
||||
false);
|
||||
private static readonly ConfigEntry<bool> ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader", "BreakBeforeLoadAssemblies",
|
||||
false,
|
||||
"If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded.");
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -237,42 +237,35 @@ namespace BepInEx.Preloader
|
||||
|
||||
#region Config
|
||||
|
||||
private static readonly ConfigWrapper<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader.Entrypoint",
|
||||
"Assembly",
|
||||
"The local filename of the assembly to target.",
|
||||
IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll"
|
||||
);
|
||||
private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader.Entrypoint", "Assembly",
|
||||
IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll",
|
||||
"The local filename of the assembly to target.");
|
||||
|
||||
private static readonly ConfigWrapper<string> ConfigEntrypointType = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader.Entrypoint",
|
||||
"Type",
|
||||
"The name of the type in the entrypoint assembly to search for the entrypoint method.",
|
||||
"Application");
|
||||
private static readonly ConfigEntry<string> ConfigEntrypointType = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader.Entrypoint", "Type",
|
||||
"Application",
|
||||
"The name of the type in the entrypoint assembly to search for the entrypoint method.");
|
||||
|
||||
private static readonly ConfigWrapper<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader.Entrypoint",
|
||||
"Method",
|
||||
"The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.",
|
||||
".cctor");
|
||||
private static readonly ConfigEntry<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader.Entrypoint", "Method",
|
||||
".cctor",
|
||||
"The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader",
|
||||
"ApplyRuntimePatches",
|
||||
"Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing.",
|
||||
true);
|
||||
private static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader", "ApplyRuntimePatches",
|
||||
true,
|
||||
"Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigShimHarmony = ConfigFile.CoreConfig.Wrap(
|
||||
"Preloader",
|
||||
"ShimHarmonySupport",
|
||||
"If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.",
|
||||
!Utility.CLRSupportsDynamicAssemblies);
|
||||
private static readonly ConfigEntry<bool> ConfigShimHarmony = ConfigFile.CoreConfig.AddSetting(
|
||||
"Preloader", "ShimHarmonySupport",
|
||||
!Utility.CLRSupportsDynamicAssemblies,
|
||||
"If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging",
|
||||
"PreloaderConsoleOutRedirection",
|
||||
"Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.",
|
||||
true);
|
||||
private static readonly ConfigEntry<bool> ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging", "PreloaderConsoleOutRedirection",
|
||||
true,
|
||||
"Redirects text from Console.Out during preloader patch loading to the BepInEx logging system.");
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
+25
@@ -27,27 +27,52 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.RuntimeDetour", "su
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoMod.Utils", "submodules\MonoMod\MonoMod.Utils\MonoMod.Utils.csproj", "{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInExTests", "BepInExTests\BepInExTests.csproj", "{E7CD429A-D057-48E3-8C51-E5C934E8E07B}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9} = {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|x86
|
||||
{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|x86
|
||||
{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -211,5 +211,7 @@
|
||||
<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_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -26,6 +26,18 @@
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>TRACE;DEBUG</DefineConstants>
|
||||
<DocumentationFile>
|
||||
</DocumentationFile>
|
||||
<Optimize>false</Optimize>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
<DebugType>full</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Mono.Cecil, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll</HintPath>
|
||||
@@ -37,11 +49,19 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Configuration\AcceptableValueBase.cs" />
|
||||
<Compile Include="Configuration\AcceptableValueList.cs" />
|
||||
<Compile Include="Configuration\AcceptableValueRange.cs" />
|
||||
<Compile Include="Configuration\ConfigEntryBase.cs" />
|
||||
<Compile Include="Contract\PluginInfo.cs" />
|
||||
<Compile Include="Configuration\ConfigDefinition.cs" />
|
||||
<Compile Include="Configuration\ConfigDescription.cs" />
|
||||
<Compile Include="Configuration\ConfigFile.cs" />
|
||||
<Compile Include="Configuration\ConfigWrapper.cs" />
|
||||
<Compile Include="Configuration\KeyboardShortcut.cs" />
|
||||
<Compile Include="Configuration\SettingChangedEventArgs.cs" />
|
||||
<Compile Include="Configuration\TomlTypeConverter.cs" />
|
||||
<Compile Include="Configuration\TypeConverter.cs" />
|
||||
<Compile Include="Contract\Attributes.cs" />
|
||||
<Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
|
||||
<Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.cs" />
|
||||
@@ -65,6 +85,7 @@
|
||||
<Compile Include="Paths.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Bootstrap\TypeLoader.cs" />
|
||||
<Compile Include="ThreadingHelper.cs" />
|
||||
<Compile Include="Utility.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using BepInEx.Contract;
|
||||
using Mono.Cecil;
|
||||
using UnityEngine;
|
||||
using UnityInjector.ConsoleUtil;
|
||||
@@ -46,6 +45,8 @@ namespace BepInEx.Bootstrap
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
ThreadingHelper.Initialize();
|
||||
|
||||
// Set vitals
|
||||
if (gameExePath != null)
|
||||
{
|
||||
@@ -74,10 +75,7 @@ namespace BepInEx.Bootstrap
|
||||
Logger.Listeners.Add(new UnityLogListener());
|
||||
|
||||
if (ConfigDiskLogging.Value)
|
||||
{
|
||||
var logLevel = (LogLevel)Enum.Parse(typeof(LogLevel), ConfigDiskConsoleDisplayedLevel.Value, true);
|
||||
Logger.Listeners.Add(new DiskLogListener("LogOutput.log", logLevel, ConfigDiskAppend.Value, ConfigDiskWriteUnityLog.Value));
|
||||
}
|
||||
Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskConsoleDisplayedLevel.Value, ConfigDiskAppend.Value, ConfigDiskWriteUnityLog.Value));
|
||||
|
||||
if (!TraceLogSource.IsListening)
|
||||
Logger.Sources.Add(TraceLogSource.CreateSource());
|
||||
@@ -262,7 +260,7 @@ namespace BepInEx.Bootstrap
|
||||
|
||||
if (dependsOnInvalidPlugin)
|
||||
{
|
||||
string message = $"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See above errors for details.";
|
||||
string message = $"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See previous errors for details.";
|
||||
DependencyErrors.Add(message);
|
||||
Logger.LogWarning(message);
|
||||
continue;
|
||||
@@ -270,14 +268,11 @@ namespace BepInEx.Bootstrap
|
||||
|
||||
if (missingDependencies.Count != 0)
|
||||
{
|
||||
string ToMissingString(BepInDependency s)
|
||||
{
|
||||
bool emptyVersion = s.MinimumVersion.Major == 0 && s.MinimumVersion.Minor == 0 && s.MinimumVersion.Build == 0 && s.MinimumVersion.Revision == 0;
|
||||
if (emptyVersion) return "- " + s.DependencyGUID;
|
||||
return $"- {s.DependencyGUID} (at least v{s.MinimumVersion})";
|
||||
}
|
||||
bool IsEmptyVersion(Version v) => v.Major == 0 && v.Minor == 0 && v.Build <= 0 && v.Revision <= 0;
|
||||
|
||||
string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it has missing dependencies: {string.Join(", ", missingDependencies.Select(ToMissingString).ToArray())}";
|
||||
string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it has missing dependencies: {
|
||||
string.Join(", ", missingDependencies.Select(s => IsEmptyVersion(s.MinimumVersion) ? s.DependencyGUID : $"{s.DependencyGUID} (v{s.MinimumVersion} or newer)").ToArray())
|
||||
}";
|
||||
DependencyErrors.Add(message);
|
||||
Logger.LogError(message);
|
||||
|
||||
@@ -287,7 +282,9 @@ namespace BepInEx.Bootstrap
|
||||
|
||||
if (incompatibilities.Count != 0)
|
||||
{
|
||||
string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: {string.Join(", ", incompatibilities.Select(i => i.IncompatibilityGUID).ToArray())}";
|
||||
string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: {
|
||||
string.Join(", ", incompatibilities.Select(i => i.IncompatibilityGUID).ToArray())
|
||||
}";
|
||||
DependencyErrors.Add(message);
|
||||
Logger.LogError(message);
|
||||
|
||||
@@ -335,35 +332,31 @@ namespace BepInEx.Bootstrap
|
||||
|
||||
#region Config
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging",
|
||||
"UnityLogListening",
|
||||
"Enables showing unity log messages in the BepInEx logging system.",
|
||||
true);
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigDiskLogging = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Disk",
|
||||
"Enabled",
|
||||
"Enables writing log messages to disk.",
|
||||
true);
|
||||
private static readonly ConfigEntry<bool> ConfigUnityLogging = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging", "UnityLogListening",
|
||||
true,
|
||||
"Enables showing unity log messages in the BepInEx logging system.");
|
||||
|
||||
private static readonly ConfigWrapper<string> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Disk",
|
||||
"DisplayedLogLevel",
|
||||
"Only displays the specified log level and above in the console output.",
|
||||
"Info");
|
||||
private static readonly ConfigEntry<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Disk", "WriteUnityLog",
|
||||
false,
|
||||
"Include unity log messages in log file output.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Disk",
|
||||
"WriteUnityLog",
|
||||
"Include unity log messages in log file output.",
|
||||
false);
|
||||
private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Disk", "AppendLog",
|
||||
false,
|
||||
"Appends to the log file instead of overwriting, on game startup.");
|
||||
|
||||
private static readonly ConfigWrapper<bool> ConfigDiskAppend = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Disk",
|
||||
"AppendLog",
|
||||
"Appends to the log file instead of overwriting, on game startup.",
|
||||
false);
|
||||
private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Disk", "Enabled",
|
||||
true,
|
||||
"Enables writing log messages to disk.");
|
||||
|
||||
private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Disk", "DisplayedLogLevel",
|
||||
LogLevel.Info,
|
||||
"Only displays the specified log level and above in the console output.");
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -257,11 +257,10 @@ namespace BepInEx.Bootstrap
|
||||
|
||||
#region Config
|
||||
|
||||
private static ConfigWrapper<bool> EnableAssemblyCache = ConfigFile.CoreConfig.Wrap(
|
||||
"Caching",
|
||||
"EnableAssemblyCache",
|
||||
"Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers.",
|
||||
true);
|
||||
private static readonly ConfigEntry<bool> EnableAssemblyCache = ConfigFile.CoreConfig.AddSetting(
|
||||
"Caching", "EnableAssemblyCache",
|
||||
true,
|
||||
"Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers.");
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Base type of all classes representing and enforcing acceptable values of config settings.
|
||||
/// </summary>
|
||||
public abstract class AcceptableValueBase
|
||||
{
|
||||
/// <param name="valueType">Type of values that this class can Clamp.</param>
|
||||
protected AcceptableValueBase(Type valueType)
|
||||
{
|
||||
ValueType = valueType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the value to be acceptable, if it's not already.
|
||||
/// </summary>
|
||||
public abstract object Clamp(object value);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the value is an acceptable value.
|
||||
/// </summary>
|
||||
public abstract bool IsValid(object value);
|
||||
|
||||
/// <summary>
|
||||
/// Type of the supported values.
|
||||
/// </summary>
|
||||
public Type ValueType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the string for use in config files.
|
||||
/// </summary>
|
||||
public abstract string ToDescriptionString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Specify the list of acceptable values for a setting.
|
||||
/// </summary>
|
||||
public class AcceptableValueList<T> : AcceptableValueBase where T : IEquatable<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// List of values that a setting can take.
|
||||
/// </summary>
|
||||
public virtual T[] AcceptableValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Specify the list of acceptable values for a setting.
|
||||
/// If the setting does not equal any of the values, it will be set to the first one.
|
||||
/// </summary>
|
||||
public AcceptableValueList(params T[] acceptableValues) : base(typeof(T))
|
||||
{
|
||||
if (acceptableValues == null) throw new ArgumentNullException(nameof(acceptableValues));
|
||||
if (acceptableValues.Length == 0) throw new ArgumentException("At least one acceptable value is needed", nameof(acceptableValues));
|
||||
|
||||
AcceptableValues = acceptableValues;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object Clamp(object value)
|
||||
{
|
||||
if (IsValid(value))
|
||||
return value;
|
||||
|
||||
return AcceptableValues[0];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsValid(object value)
|
||||
{
|
||||
return value is T v && AcceptableValues.Any(x => x.Equals(v));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToDescriptionString()
|
||||
{
|
||||
return "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Specify the range of acceptable values for a setting.
|
||||
/// </summary>
|
||||
public class AcceptableValueRange<T> : AcceptableValueBase where T : IComparable
|
||||
{
|
||||
/// <param name="minValue">Lowest acceptable value</param>
|
||||
/// <param name="maxValue">Highest acceptable value</param>
|
||||
public AcceptableValueRange(T minValue, T maxValue) : base(typeof(T))
|
||||
{
|
||||
if (maxValue == null)
|
||||
throw new ArgumentNullException(nameof(maxValue));
|
||||
if (minValue == null)
|
||||
throw new ArgumentNullException(nameof(minValue));
|
||||
if (minValue.CompareTo(maxValue) >= 0)
|
||||
throw new ArgumentException($"{nameof(minValue)} has to be lower than {nameof(maxValue)}");
|
||||
|
||||
MinValue = minValue;
|
||||
MaxValue = maxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lowest acceptable value
|
||||
/// </summary>
|
||||
public virtual T MinValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Highest acceptable value
|
||||
/// </summary>
|
||||
public virtual T MaxValue { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object Clamp(object value)
|
||||
{
|
||||
if (MinValue.CompareTo(value) > 0)
|
||||
return MinValue;
|
||||
|
||||
if (MaxValue.CompareTo(value) < 0)
|
||||
return MaxValue;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsValid(object value)
|
||||
{
|
||||
return MinValue.CompareTo(value) <= 0 && MaxValue.CompareTo(value) >= 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToDescriptionString()
|
||||
{
|
||||
return $"# Acceptable value range: From {MinValue} to {MaxValue}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,75 @@
|
||||
namespace BepInEx.Configuration
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
public class ConfigDefinition
|
||||
/// <summary>
|
||||
/// Section and key of a setting. Used as a unique key for identification within a <see cref="T:BepInEx.Configuration.ConfigFile" />.
|
||||
/// The same definition can be used in multiple config files, it will point to different settings then.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public class ConfigDefinition : IEquatable<ConfigDefinition>
|
||||
{
|
||||
/// <summary>
|
||||
/// Group of the setting. All settings within a config file are grouped by this.
|
||||
/// </summary>
|
||||
public string Section { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the setting.
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
|
||||
public string Description { get; internal set; }
|
||||
|
||||
public ConfigDefinition(string section, string key, string description = null)
|
||||
/// <summary>
|
||||
/// Create a new definition. Definitions with same section and key are equal.
|
||||
/// </summary>
|
||||
/// <param name="section">Group of the setting, case sensitive.</param>
|
||||
/// <param name="key">Name of the setting, case sensitive.</param>
|
||||
public ConfigDefinition(string section, string key)
|
||||
{
|
||||
CheckInvalidConfigChars(section, nameof(section));
|
||||
CheckInvalidConfigChars(key, nameof(key));
|
||||
Key = key;
|
||||
Section = section;
|
||||
|
||||
Description = description;
|
||||
}
|
||||
|
||||
private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'' };
|
||||
private static void CheckInvalidConfigChars(string val, string name)
|
||||
{
|
||||
if (val == null) throw new ArgumentNullException(name);
|
||||
if (val != val.Trim()) throw new ArgumentException("Cannot use whitespace characters at start or end of section and key names", name);
|
||||
if (val.Any(c => _invalidConfigChars.Contains(c))) throw new ArgumentException(@"Cannot use any of the following characters in section and key names: = \n \t \ "" '", name);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("description argument is no longer used, put it in a ConfigDescription instead")]
|
||||
public ConfigDefinition(string section, string key, string description) : this(section, key) { }
|
||||
|
||||
/// <summary>
|
||||
/// Check if the definitions are the same.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ConfigDefinition other)
|
||||
{
|
||||
if (other == null) return false;
|
||||
return string.Equals(Key, other.Key)
|
||||
&& string.Equals(Section, other.Section);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the definitions are the same.
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
if (ReferenceEquals(this, obj))
|
||||
return true;
|
||||
if (obj.GetType() != this.GetType())
|
||||
return false;
|
||||
|
||||
if (!(obj is ConfigDefinition other))
|
||||
return false;
|
||||
|
||||
return string.Equals(Key, other.Key)
|
||||
&& string.Equals(Section, other.Section);
|
||||
return Equals(obj as ConfigDefinition);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
@@ -42,10 +80,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the definitions are the same.
|
||||
/// </summary>
|
||||
public static bool operator ==(ConfigDefinition left, ConfigDefinition right)
|
||||
=> Equals(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the definitions are the same.
|
||||
/// </summary>
|
||||
public static bool operator !=(ConfigDefinition left, ConfigDefinition right)
|
||||
=> !Equals(left, right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Section + "." + Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata of a <see cref="ConfigEntryBase"/>.
|
||||
/// </summary>
|
||||
public class ConfigDescription
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new description.
|
||||
/// </summary>
|
||||
/// <param name="description">Text describing the function of the setting and any notes or warnings.</param>
|
||||
/// <param name="acceptableValues">Range of values that this setting can take. The setting's value will be automatically clamped.</param>
|
||||
/// <param name="tags">Objects that can be used by user-made classes to add functionality.</param>
|
||||
public ConfigDescription(string description, AcceptableValueBase acceptableValues = null, params object[] tags)
|
||||
{
|
||||
AcceptableValues = acceptableValues;
|
||||
Tags = tags;
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text describing the function of the setting and any notes or warnings.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Range of acceptable values for a setting.
|
||||
/// </summary>
|
||||
public AcceptableValueBase AcceptableValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Objects that can be used by user-made classes to add functionality.
|
||||
/// </summary>
|
||||
public object[] Tags { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An empty description.
|
||||
/// </summary>
|
||||
public static ConfigDescription Empty { get; } = new ConfigDescription("");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using BepInEx.Logging;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to a single setting inside of a <see cref="Configuration.ConfigFile"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the setting.</typeparam>
|
||||
public sealed class ConfigEntry<T> : ConfigEntryBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when the setting is changed. Does not detect changes made outside from this object.
|
||||
/// </summary>
|
||||
public event EventHandler SettingChanged;
|
||||
|
||||
private T _typedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Value of this setting.
|
||||
/// </summary>
|
||||
public T Value
|
||||
{
|
||||
get => _typedValue;
|
||||
set
|
||||
{
|
||||
value = ClampValue(value);
|
||||
if (Equals(_typedValue, value))
|
||||
return;
|
||||
|
||||
_typedValue = value;
|
||||
OnSettingChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object BoxedValue
|
||||
{
|
||||
get => Value;
|
||||
set => Value = (T)value;
|
||||
}
|
||||
|
||||
internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, T defaultValue, ConfigDescription configDescription) : base(configFile, definition, typeof(T), defaultValue, configDescription)
|
||||
{
|
||||
configFile.SettingChanged += (sender, args) =>
|
||||
{
|
||||
if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container for a single setting of a <see cref="Configuration.ConfigFile"/>.
|
||||
/// Each config entry is linked to one config file.
|
||||
/// </summary>
|
||||
public abstract class ConfigEntryBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Types of defaultValue and definition.AcceptableValues have to be the same as settingType.
|
||||
/// </summary>
|
||||
internal ConfigEntryBase(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue, ConfigDescription configDescription)
|
||||
{
|
||||
ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile));
|
||||
Definition = definition ?? throw new ArgumentNullException(nameof(definition));
|
||||
SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType));
|
||||
|
||||
Description = configDescription ?? ConfigDescription.Empty;
|
||||
if (Description.AcceptableValues != null && !SettingType.IsAssignableFrom(Description.AcceptableValues.ValueType))
|
||||
throw new ArgumentException("configDescription.AcceptableValues is for a different type than the type of this setting");
|
||||
|
||||
DefaultValue = defaultValue;
|
||||
|
||||
// Free type check and automatically calls ClampValue in case AcceptableValues were provided
|
||||
BoxedValue = defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Config file this entry is a part of.
|
||||
/// </summary>
|
||||
public ConfigFile ConfigFile { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Category and name of this setting. Used as a unique key for identification within a <see cref="Configuration.ConfigFile"/>.
|
||||
/// </summary>
|
||||
public ConfigDefinition Definition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Description / metadata of this setting.
|
||||
/// </summary>
|
||||
public ConfigDescription Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the <see cref="BoxedValue"/> that this setting holds.
|
||||
/// </summary>
|
||||
public Type SettingType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Default value of this setting (set only if the setting was not changed before).
|
||||
/// </summary>
|
||||
public object DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or set the value of the setting.
|
||||
/// </summary>
|
||||
public abstract object BoxedValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the serialized representation of the value.
|
||||
/// </summary>
|
||||
public string GetSerializedValue()
|
||||
{
|
||||
return TomlTypeConverter.ConvertToString(BoxedValue, SettingType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the value by using its serialized form.
|
||||
/// </summary>
|
||||
public void SetSerializedValue(string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newValue = TomlTypeConverter.ConvertToValue(value, SettingType);
|
||||
BoxedValue = newValue;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log(LogLevel.Warning, $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If necessary, clamp the value to acceptable value range. T has to be equal to settingType.
|
||||
/// </summary>
|
||||
protected T ClampValue<T>(T value)
|
||||
{
|
||||
if (Description.AcceptableValues != null)
|
||||
return (T)Description.AcceptableValues.Clamp(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger setting changed event.
|
||||
/// </summary>
|
||||
protected void OnSettingChanged(object sender)
|
||||
{
|
||||
ConfigFile.OnSettingChanged(sender, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a description of this setting using all available metadata.
|
||||
/// </summary>
|
||||
public void WriteDescription(StreamWriter writer)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Description.Description))
|
||||
writer.WriteLine($"## {Description.Description.Replace("\n", "\n## ")}");
|
||||
|
||||
writer.WriteLine("# Setting type: " + SettingType.Name);
|
||||
|
||||
writer.WriteLine("# Default value: " + DefaultValue);
|
||||
|
||||
if (Description.AcceptableValues != null)
|
||||
{
|
||||
writer.WriteLine(Description.AcceptableValues.ToDescriptionString());
|
||||
}
|
||||
else if (SettingType.IsEnum)
|
||||
{
|
||||
writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType)));
|
||||
|
||||
if (SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any())
|
||||
writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,37 +3,75 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
using BepInEx.Logging;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class to handle persistent data.
|
||||
/// A helper class to handle persistent data. All public methods are thread-safe.
|
||||
/// </summary>
|
||||
public class ConfigFile
|
||||
{
|
||||
private static readonly Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+");
|
||||
private readonly BepInPlugin _ownerMetadata;
|
||||
|
||||
internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true);
|
||||
|
||||
protected internal Dictionary<ConfigDefinition, string> Cache { get; } = new Dictionary<ConfigDefinition, string>();
|
||||
/// <summary>
|
||||
/// All config entries inside
|
||||
/// </summary>
|
||||
protected Dictionary<ConfigDefinition, ConfigEntryBase> Entries { get; } = new Dictionary<ConfigDefinition, ConfigEntryBase>();
|
||||
|
||||
public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions => Cache.Keys.ToList().AsReadOnly();
|
||||
private Dictionary<ConfigDefinition, string> HomelessEntries { get; } = new Dictionary<ConfigDefinition, string>();
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired every time the config is reloaded.
|
||||
/// Create a list with all config entries inside of this config file.
|
||||
/// </summary>
|
||||
public event EventHandler ConfigReloaded;
|
||||
[Obsolete("Use GetConfigEntries instead")]
|
||||
public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_ioLock) return Entries.Keys.ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an array with all config entries inside of this config file. Should be only used for metadata purposes.
|
||||
/// If you want to access and modify an existing setting then use <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>
|
||||
/// instead with no description.
|
||||
/// </summary>
|
||||
public ConfigEntryBase[] GetConfigEntries()
|
||||
{
|
||||
lock (_ioLock) return Entries.Values.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full path to the config file. The file might not exist until a setting is added and changed, or <see cref="Save"/> is called.
|
||||
/// </summary>
|
||||
public string ConfigFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, writes the config to disk every time a value is set.
|
||||
/// If enabled, writes the config to disk every time a value is set.
|
||||
/// If disabled, you have to manually use <see cref="Save"/> or the changes will be lost!
|
||||
/// </summary>
|
||||
public bool SaveOnConfigSet { get; set; } = true;
|
||||
|
||||
public ConfigFile(string configPath, bool saveOnInit)
|
||||
/// <inheritdoc cref="ConfigFile(string, bool, BepInPlugin)"/>
|
||||
public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new config file at the specified config path.
|
||||
/// </summary>
|
||||
/// <param name="configPath">Full path to a file that contains settings. The file will be created as needed.</param>
|
||||
/// <param name="saveOnInit">If the config file/directory doesn't exist, create it immediately.</param>
|
||||
/// <param name="ownerMetadata">Information about the plugin that owns this setting file.</param>
|
||||
public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata)
|
||||
{
|
||||
_ownerMetadata = ownerMetadata;
|
||||
|
||||
if (configPath == null) throw new ArgumentNullException(nameof(configPath));
|
||||
configPath = Path.GetFullPath(configPath);
|
||||
ConfigFilePath = configPath;
|
||||
|
||||
if (File.Exists(ConfigFilePath))
|
||||
@@ -46,7 +84,10 @@ namespace BepInEx.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
private object _ioLock = new object();
|
||||
#region Save/Load
|
||||
|
||||
private readonly object _ioLock = new object();
|
||||
private bool _disableSaving;
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the config from disk. Unsaved changes are lost.
|
||||
@@ -55,40 +96,51 @@ namespace BepInEx.Configuration
|
||||
{
|
||||
lock (_ioLock)
|
||||
{
|
||||
Dictionary<ConfigDefinition, string> descriptions = Cache.ToDictionary(x => x.Key, x => x.Key.Description);
|
||||
HomelessEntries.Clear();
|
||||
|
||||
string currentSection = "";
|
||||
|
||||
foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
|
||||
try
|
||||
{
|
||||
string line = rawLine.Trim();
|
||||
_disableSaving = true;
|
||||
|
||||
if (line.StartsWith("#")) //comment
|
||||
continue;
|
||||
string currentSection = string.Empty;
|
||||
|
||||
if (line.StartsWith("[") && line.EndsWith("]")) //section
|
||||
foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
|
||||
{
|
||||
currentSection = line.Substring(1, line.Length - 2);
|
||||
continue;
|
||||
string line = rawLine.Trim();
|
||||
|
||||
if (line.StartsWith("#")) //comment
|
||||
continue;
|
||||
|
||||
if (line.StartsWith("[") && line.EndsWith("]")) //section
|
||||
{
|
||||
currentSection = line.Substring(1, line.Length - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] split = line.Split('='); //actual config line
|
||||
if (split.Length != 2)
|
||||
continue; //empty/invalid line
|
||||
|
||||
string currentKey = split[0].Trim();
|
||||
string currentValue = split[1].Trim();
|
||||
|
||||
var definition = new ConfigDefinition(currentSection, currentKey);
|
||||
|
||||
Entries.TryGetValue(definition, out ConfigEntryBase entry);
|
||||
|
||||
if (entry != null)
|
||||
entry.SetSerializedValue(currentValue);
|
||||
else
|
||||
HomelessEntries[definition] = currentValue;
|
||||
}
|
||||
|
||||
string[] split = line.Split('='); //actual config line
|
||||
if (split.Length != 2)
|
||||
continue; //empty/invalid line
|
||||
|
||||
string currentKey = split[0].Trim();
|
||||
string currentValue = split[1].Trim();
|
||||
|
||||
var definition = new ConfigDefinition(currentSection, currentKey);
|
||||
|
||||
if (descriptions.ContainsKey(definition))
|
||||
definition.Description = descriptions[definition];
|
||||
|
||||
Cache[definition] = currentValue;
|
||||
}
|
||||
|
||||
ConfigReloaded?.Invoke(this, EventArgs.Empty);
|
||||
finally
|
||||
{
|
||||
_disableSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
OnConfigReloaded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,51 +150,196 @@ namespace BepInEx.Configuration
|
||||
{
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (!Directory.Exists(Paths.ConfigPath))
|
||||
Directory.CreateDirectory(Paths.ConfigPath);
|
||||
if (_disableSaving) return;
|
||||
|
||||
using (StreamWriter writer = new StreamWriter(File.Create(ConfigFilePath), System.Text.Encoding.UTF8))
|
||||
foreach (var sectionKv in Cache.GroupBy(x => x.Key.Section).OrderBy(x => x.Key))
|
||||
string directoryName = Path.GetDirectoryName(ConfigFilePath);
|
||||
if (directoryName != null) Directory.CreateDirectory(directoryName);
|
||||
|
||||
using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8))
|
||||
{
|
||||
if (_ownerMetadata != null)
|
||||
{
|
||||
writer.WriteLine($"## Settings file was created by plugin {_ownerMetadata.Name} v{_ownerMetadata.Version}");
|
||||
writer.WriteLine($"## Plugin GUID: {_ownerMetadata.GUID}");
|
||||
writer.WriteLine();
|
||||
}
|
||||
|
||||
var allConfigEntries = Entries.Select(x => new { x.Key, entry = x.Value, value = x.Value.GetSerializedValue() })
|
||||
.Concat(HomelessEntries.Select(x => new { x.Key, entry = (ConfigEntryBase)null, value = x.Value }));
|
||||
|
||||
foreach (var sectionKv in allConfigEntries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key))
|
||||
{
|
||||
// Section heading
|
||||
writer.WriteLine($"[{sectionKv.Key}]");
|
||||
|
||||
foreach (var entryKv in sectionKv)
|
||||
foreach (var configEntry in sectionKv)
|
||||
{
|
||||
writer.WriteLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(entryKv.Key.Description))
|
||||
writer.WriteLine($"# {entryKv.Key.Description.Replace("\n", "\n# ")}");
|
||||
configEntry.entry?.WriteDescription(writer);
|
||||
|
||||
writer.WriteLine($"{entryKv.Key.Key} = {entryKv.Value}");
|
||||
writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}");
|
||||
}
|
||||
|
||||
writer.WriteLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
|
||||
{
|
||||
if (!Cache.ContainsKey(configDefinition))
|
||||
{
|
||||
Cache.Add(configDefinition, TomlTypeConverter.ConvertToString(defaultValue));
|
||||
Save();
|
||||
}
|
||||
else
|
||||
{
|
||||
var original = Cache.Keys.First(x => x.Equals(configDefinition));
|
||||
|
||||
if (original.Description != configDefinition.Description)
|
||||
{
|
||||
original.Description = configDefinition.Description;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
return new ConfigWrapper<T>(this, configDefinition);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Wraps
|
||||
|
||||
/// <summary>
|
||||
/// Access one of the existing settings. If the setting has not been added yet, null is returned.
|
||||
/// If the setting exists but has a different type than T, an exception is thrown.
|
||||
/// New settings should be added with <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
|
||||
/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
|
||||
/// <param name="key">Name of the setting.</param>
|
||||
public ConfigEntry<T> GetSetting<T>(string section, string key)
|
||||
{
|
||||
lock (_ioLock)
|
||||
{
|
||||
Entries.TryGetValue(new ConfigDefinition(section, key), out var entry);
|
||||
return (ConfigEntry<T>)entry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new setting. The setting is saved to drive and loaded automatically.
|
||||
/// Each definition can be used to add only one setting, trying to add a second setting will throw an exception.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
|
||||
/// <param name="configDefinition">Section and Key of the setting.</param>
|
||||
/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
|
||||
/// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
|
||||
public ConfigEntry<T> AddSetting<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
|
||||
{
|
||||
if (!TomlTypeConverter.CanConvert(typeof(T)))
|
||||
throw new ArgumentException($"Type {typeof(T)} is not supported by the config system. Supported types: {string.Join(", ", TomlTypeConverter.GetSupportedTypes().Select(x => x.Name).ToArray())}");
|
||||
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (Entries.ContainsKey(configDefinition))
|
||||
throw new ArgumentException("The setting " + configDefinition + " has already been created. Use GetSetting to get it.");
|
||||
|
||||
try
|
||||
{
|
||||
_disableSaving = true;
|
||||
|
||||
var entry = new ConfigEntry<T>(this, configDefinition, defaultValue, configDescription);
|
||||
Entries[configDefinition] = entry;
|
||||
|
||||
if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue))
|
||||
{
|
||||
entry.SetSerializedValue(homelessValue);
|
||||
HomelessEntries.Remove(configDefinition);
|
||||
}
|
||||
|
||||
_disableSaving = false;
|
||||
if (SaveOnConfigSet)
|
||||
Save();
|
||||
|
||||
return entry;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disableSaving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new setting. The setting is saved to drive and loaded automatically.
|
||||
/// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
|
||||
/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
|
||||
/// <param name="key">Name of the setting.</param>
|
||||
/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
|
||||
/// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
|
||||
public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
|
||||
=> AddSetting(new ConfigDefinition(section, key), defaultValue, configDescription);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new setting. The setting is saved to drive and loaded automatically.
|
||||
/// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the value contained in this setting.</typeparam>
|
||||
/// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
|
||||
/// <param name="key">Name of the setting.</param>
|
||||
/// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
|
||||
/// <param name="description">Simple description of the setting shown to the user.</param>
|
||||
public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, string description)
|
||||
=> AddSetting(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
|
||||
|
||||
/// <summary>
|
||||
/// Access a setting. Use AddSetting and GetSetting instead.
|
||||
/// </summary>
|
||||
[Obsolete("Use AddSetting and GetSetting instead")]
|
||||
public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
|
||||
=> Wrap<T>(new ConfigDefinition(section, key, description), defaultValue);
|
||||
{
|
||||
lock (_ioLock)
|
||||
{
|
||||
var setting = GetSetting<T>(section, key) ?? AddSetting(section, key, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
|
||||
return new ConfigWrapper<T>(setting);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Access a setting. Use AddSetting and GetSetting instead.
|
||||
/// </summary>
|
||||
[Obsolete("Use AddSetting and GetSetting instead")]
|
||||
public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
|
||||
=> Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// An event that is fired every time the config is reloaded.
|
||||
/// </summary>
|
||||
public event EventHandler ConfigReloaded;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when one of the settings is changed.
|
||||
/// </summary>
|
||||
public event EventHandler<SettingChangedEventArgs> SettingChanged;
|
||||
|
||||
internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase)
|
||||
{
|
||||
if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase));
|
||||
|
||||
if (SaveOnConfigSet)
|
||||
Save();
|
||||
|
||||
var settingChanged = SettingChanged;
|
||||
if (settingChanged == null) return;
|
||||
|
||||
var args = new SettingChangedEventArgs(changedEntryBase);
|
||||
foreach (var callback in settingChanged.GetInvocationList().Cast<EventHandler<SettingChangedEventArgs>>())
|
||||
{
|
||||
try { callback(sender, args); }
|
||||
catch (Exception e) { Logger.Log(LogLevel.Error, e); }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigReloaded()
|
||||
{
|
||||
var configReloaded = ConfigReloaded;
|
||||
if (configReloaded == null) return;
|
||||
|
||||
foreach (var callback in configReloaded.GetInvocationList().Cast<EventHandler>())
|
||||
{
|
||||
try { callback(this, EventArgs.Empty); }
|
||||
catch (Exception e) { Logger.Log(LogLevel.Error, e); }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,38 +2,50 @@
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
public class ConfigWrapper<T>
|
||||
/// <summary>
|
||||
/// Provides access to a single setting inside of a <see cref="Configuration.ConfigFile"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the setting.</typeparam>
|
||||
[Obsolete("Use ConfigFile from new AddSetting overloads instead")]
|
||||
public sealed class ConfigWrapper<T>
|
||||
{
|
||||
public ConfigDefinition Definition { get; protected set; }
|
||||
/// <summary>
|
||||
/// Entry of this setting in the <see cref="Configuration.ConfigFile"/>.
|
||||
/// </summary>
|
||||
public ConfigEntry<T> ConfigEntry { get; }
|
||||
|
||||
public ConfigFile ConfigFile { get; protected set; }
|
||||
/// <summary>
|
||||
/// Unique definition of this setting.
|
||||
/// </summary>
|
||||
public ConfigDefinition Definition => ConfigEntry.Definition;
|
||||
|
||||
/// <summary>
|
||||
/// Config file this setting is inside of.
|
||||
/// </summary>
|
||||
public ConfigFile ConfigFile => ConfigEntry.ConfigFile;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the setting is changed. Does not detect changes made outside from this object.
|
||||
/// </summary>
|
||||
public event EventHandler SettingChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Value of this setting.
|
||||
/// </summary>
|
||||
public T Value
|
||||
{
|
||||
get => TomlTypeConverter.ConvertToValue<T>(ConfigFile.Cache[Definition]);
|
||||
set
|
||||
{
|
||||
ConfigFile.Cache[Definition] = TomlTypeConverter.ConvertToString(value);
|
||||
|
||||
if (ConfigFile.SaveOnConfigSet)
|
||||
ConfigFile.Save();
|
||||
|
||||
SettingChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
get => ConfigEntry.Value;
|
||||
set => ConfigEntry.Value = value;
|
||||
}
|
||||
|
||||
public ConfigWrapper(ConfigFile configFile, ConfigDefinition definition)
|
||||
internal ConfigWrapper(ConfigEntry<T> configEntry)
|
||||
{
|
||||
if (!TomlTypeConverter.TypeConverters.ContainsKey(typeof(T)))
|
||||
throw new ArgumentException("Unsupported config wrapper type");
|
||||
ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry));
|
||||
|
||||
ConfigFile = configFile;
|
||||
Definition = definition;
|
||||
configEntry.ConfigFile.SettingChanged += (sender, args) =>
|
||||
{
|
||||
if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BepInEx.Logging;
|
||||
using UnityEngine;
|
||||
using Logger = BepInEx.Logging.Logger;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// A keyboard shortcut that can be used in Update method to check if user presses a key combo. The shortcut is only
|
||||
/// triggered when the user presses the exact combination. For example, <c>F + LeftCtrl</c> will trigger only if user
|
||||
/// presses and holds only LeftCtrl, and then presses F. If any other keys are pressed, the shortcut will not trigger.
|
||||
///
|
||||
/// Can be used as a value of a setting in <see cref="ConfigFile.AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>
|
||||
/// to allow user to change this shortcut and have the changes saved.
|
||||
///
|
||||
/// How to use: Use <see cref="IsDown"/> in this class instead of <see cref="Input.GetKeyDown(KeyCode)"/> in the Update loop.
|
||||
/// </summary>
|
||||
public struct KeyboardShortcut
|
||||
{
|
||||
static KeyboardShortcut()
|
||||
{
|
||||
TomlTypeConverter.AddConverter(
|
||||
typeof(KeyboardShortcut),
|
||||
new TypeConverter
|
||||
{
|
||||
ConvertToString = (o, type) => ((KeyboardShortcut)o).Serialize(),
|
||||
ConvertToObject = (s, type) => Deserialize(s)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut that never triggers.
|
||||
/// </summary>
|
||||
public static readonly KeyboardShortcut Empty = new KeyboardShortcut();
|
||||
|
||||
/// <summary>
|
||||
/// All KeyCode values that can be used in a keyboard shortcut.
|
||||
/// </summary>
|
||||
public static readonly IEnumerable<KeyCode> AllKeyCodes = (KeyCode[])Enum.GetValues(typeof(KeyCode));
|
||||
|
||||
private readonly KeyCode[] _allKeys;
|
||||
private readonly HashSet<KeyCode> _allKeysLookup;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new keyboard shortcut.
|
||||
/// </summary>
|
||||
/// <param name="mainKey">Main key to press</param>
|
||||
/// <param name="modifiers">Keys that should be held down before main key is registered</param>
|
||||
public KeyboardShortcut(KeyCode mainKey, params KeyCode[] modifiers) : this(new[] { mainKey }.Concat(modifiers).ToArray())
|
||||
{
|
||||
if (mainKey == KeyCode.None && modifiers.Any())
|
||||
throw new ArgumentException($"Can't set {nameof(mainKey)} to KeyCode.None if there are any {nameof(modifiers)}");
|
||||
}
|
||||
|
||||
private KeyboardShortcut(KeyCode[] keys)
|
||||
{
|
||||
_allKeys = SanitizeKeys(keys);
|
||||
_allKeysLookup = new HashSet<KeyCode>(_allKeys);
|
||||
}
|
||||
|
||||
private static KeyCode[] SanitizeKeys(params KeyCode[] keys)
|
||||
{
|
||||
if (keys.Length == 0 || keys[0] == KeyCode.None)
|
||||
return new[] { KeyCode.None };
|
||||
|
||||
return new[] { keys[0] }.Concat(keys.Skip(1).Distinct().Where(x => x != keys[0]).OrderBy(x => (int)x)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered.
|
||||
/// If the combination is empty, <see cref="KeyCode.None"/> is returned.
|
||||
/// </summary>
|
||||
public KeyCode MainKey => _allKeys != null && _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None;
|
||||
|
||||
/// <summary>
|
||||
/// Modifiers of the key combination, if any.
|
||||
/// </summary>
|
||||
public IEnumerable<KeyCode> Modifiers => _allKeys?.Skip(1) ?? Enumerable.Empty<KeyCode>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to deserialize key combination from the string.
|
||||
/// </summary>
|
||||
public static KeyboardShortcut Deserialize(string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = str.Split(new[] { ' ', '+', ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => (KeyCode)Enum.Parse(typeof(KeyCode), x)).ToArray();
|
||||
return new KeyboardShortcut(parts);
|
||||
}
|
||||
catch (SystemException ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message);
|
||||
return Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize the key combination into a user readable string.
|
||||
/// </summary>
|
||||
public string Serialize()
|
||||
{
|
||||
if (_allKeys == null) return string.Empty;
|
||||
return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the main key was just pressed (Input.GetKeyDown), and specified modifier keys are all pressed
|
||||
/// </summary>
|
||||
public bool IsDown()
|
||||
{
|
||||
var mainKey = MainKey;
|
||||
if (mainKey == KeyCode.None) return false;
|
||||
|
||||
return Input.GetKeyDown(mainKey) && ModifierKeyTest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the main key is currently held down (Input.GetKey), and specified modifier keys are all pressed
|
||||
/// </summary>
|
||||
public bool IsPressed()
|
||||
{
|
||||
var mainKey = MainKey;
|
||||
if (mainKey == KeyCode.None) return false;
|
||||
|
||||
return Input.GetKey(mainKey) && ModifierKeyTest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the main key was just lifted (Input.GetKeyUp), and specified modifier keys are all pressed.
|
||||
/// </summary>
|
||||
public bool IsUp()
|
||||
{
|
||||
var mainKey = MainKey;
|
||||
if (mainKey == KeyCode.None) return false;
|
||||
|
||||
return Input.GetKeyUp(mainKey) && ModifierKeyTest();
|
||||
}
|
||||
|
||||
private bool ModifierKeyTest()
|
||||
{
|
||||
var lookup = _allKeysLookup;
|
||||
var mainKey = MainKey;
|
||||
return AllKeyCodes.All(c =>
|
||||
{
|
||||
if (lookup.Contains(c))
|
||||
{
|
||||
if (mainKey == c)
|
||||
return true;
|
||||
return Input.GetKey(c);
|
||||
}
|
||||
return !Input.GetKey(c);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
if (MainKey == KeyCode.None) return "Not set";
|
||||
|
||||
return string.Join(" + ", _allKeys.Select(c => c.ToString()).ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is KeyboardShortcut shortcut && MainKey == shortcut.MainKey && Modifiers.SequenceEqual(shortcut.Modifiers);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
if (MainKey == KeyCode.None) return 0;
|
||||
|
||||
return _allKeys.Aggregate(_allKeys.Length, (current, item) => unchecked(current * 31 + (int)item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Arguments for events concerning a change of a setting.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public sealed class SettingChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SettingChangedEventArgs(ConfigEntryBase changedSetting)
|
||||
{
|
||||
ChangedSetting = changedSetting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting that was changed
|
||||
/// </summary>
|
||||
public ConfigEntryBase ChangedSetting { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,113 +1,344 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using Logger = BepInEx.Logging.Logger;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
public class TypeConverter
|
||||
/// <summary>
|
||||
/// Serializer/deserializer used by the config system.
|
||||
/// </summary>
|
||||
public static class TomlTypeConverter
|
||||
{
|
||||
public Func<object, string> ConvertToString { get; set; }
|
||||
public Func<string, object> ConvertToObject { get; set; }
|
||||
}
|
||||
|
||||
internal static class TomlTypeConverter
|
||||
{
|
||||
public static Dictionary<Type, TypeConverter> TypeConverters { get; } = new Dictionary<Type, TypeConverter>
|
||||
// Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead
|
||||
private static Dictionary<Type, TypeConverter> TypeConverters { get; } = new Dictionary<Type, TypeConverter>
|
||||
{
|
||||
[typeof(string)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => (string)obj,
|
||||
ConvertToObject = (str) => str,
|
||||
ConvertToString = (obj, type) => Escape((string)obj),
|
||||
ConvertToObject = (str, type) =>
|
||||
{
|
||||
// Check if the string is a file path with unescaped \ path separators (e.g. D:\test and not D:\\test)
|
||||
if (Regex.IsMatch(str, @"^""?\w:\\(?!\\)(?!.+\\\\)"))
|
||||
return str;
|
||||
return Unescape(str);
|
||||
},
|
||||
},
|
||||
[typeof(bool)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString().ToLowerInvariant(),
|
||||
ConvertToObject = (str) => bool.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(),
|
||||
ConvertToObject = (str, type) => bool.Parse(str),
|
||||
},
|
||||
[typeof(byte)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => byte.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => byte.Parse(str),
|
||||
},
|
||||
|
||||
//integral types
|
||||
|
||||
[typeof(sbyte)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => sbyte.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => sbyte.Parse(str),
|
||||
},
|
||||
[typeof(byte)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => byte.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => byte.Parse(str),
|
||||
},
|
||||
[typeof(short)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => short.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => short.Parse(str),
|
||||
},
|
||||
[typeof(ushort)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => ushort.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => ushort.Parse(str),
|
||||
},
|
||||
[typeof(int)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => int.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => int.Parse(str),
|
||||
},
|
||||
[typeof(uint)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => uint.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => uint.Parse(str),
|
||||
},
|
||||
[typeof(long)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => long.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => long.Parse(str),
|
||||
},
|
||||
[typeof(ulong)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => obj.ToString(),
|
||||
ConvertToObject = (str) => ulong.Parse(str),
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => ulong.Parse(str),
|
||||
},
|
||||
|
||||
//floating point types
|
||||
|
||||
[typeof(float)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => ((float)obj).ToString(NumberFormatInfo.InvariantInfo),
|
||||
ConvertToObject = (str) => float.Parse(str, NumberFormatInfo.InvariantInfo),
|
||||
ConvertToString = (obj, type) => ((float)obj).ToString(NumberFormatInfo.InvariantInfo),
|
||||
ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo),
|
||||
},
|
||||
[typeof(double)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => ((double)obj).ToString(NumberFormatInfo.InvariantInfo),
|
||||
ConvertToObject = (str) => double.Parse(str, NumberFormatInfo.InvariantInfo),
|
||||
ConvertToString = (obj, type) => ((double)obj).ToString(NumberFormatInfo.InvariantInfo),
|
||||
ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo),
|
||||
},
|
||||
[typeof(decimal)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj) => ((decimal)obj).ToString(NumberFormatInfo.InvariantInfo),
|
||||
ConvertToObject = (str) => decimal.Parse(str, NumberFormatInfo.InvariantInfo),
|
||||
ConvertToString = (obj, type) => ((decimal)obj).ToString(NumberFormatInfo.InvariantInfo),
|
||||
ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo),
|
||||
},
|
||||
|
||||
//enums are special
|
||||
|
||||
[typeof(Enum)] = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj, type) => obj.ToString(),
|
||||
ConvertToObject = (str, type) => Enum.Parse(type, str, true),
|
||||
},
|
||||
};
|
||||
|
||||
public static string ConvertToString(object value)
|
||||
/// <summary>
|
||||
/// Convert object of a given type to a string using available converters.
|
||||
/// </summary>
|
||||
public static string ConvertToString(object value, Type valueType)
|
||||
{
|
||||
Type valueType = value.GetType();
|
||||
|
||||
if (!TypeConverters.ContainsKey(valueType))
|
||||
var conv = GetConverter(valueType);
|
||||
if (conv == null)
|
||||
throw new InvalidOperationException($"Cannot convert from type {valueType}");
|
||||
|
||||
return TypeConverters[valueType].ConvertToString(value);
|
||||
return conv.ConvertToString(value, valueType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert string to an object of a given type using available converters.
|
||||
/// </summary>
|
||||
public static T ConvertToValue<T>(string value)
|
||||
{
|
||||
if (!TypeConverters.ContainsKey(typeof(T)))
|
||||
throw new InvalidOperationException($"Cannot convert to type {typeof(T)}");
|
||||
return (T)ConvertToValue(value, typeof(T));
|
||||
}
|
||||
|
||||
return (T)TypeConverters[typeof(T)].ConvertToObject(value);
|
||||
/// <summary>
|
||||
/// Convert string to an object of a given type using available converters.
|
||||
/// </summary>
|
||||
public static object ConvertToValue(string value, Type valueType)
|
||||
{
|
||||
var conv = GetConverter(valueType);
|
||||
if (conv == null)
|
||||
throw new InvalidOperationException($"Cannot convert to type {valueType.Name}");
|
||||
|
||||
return conv.ConvertToObject(value, valueType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a converter for a given type if there is any.
|
||||
/// </summary>
|
||||
public static TypeConverter GetConverter(Type valueType)
|
||||
{
|
||||
if (valueType == null) throw new ArgumentNullException(nameof(valueType));
|
||||
|
||||
if (valueType.IsEnum)
|
||||
return TypeConverters[typeof(Enum)];
|
||||
|
||||
if (!TypeConverters.TryGetValue(valueType, out var result) && !_lazyLoadedConverters)
|
||||
{
|
||||
_lazyLoadedConverters = true;
|
||||
LazyLoadConverters();
|
||||
TypeConverters.TryGetValue(valueType, out result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new type converter for a given type.
|
||||
/// If a different converter is already added, this call is ignored and false is returned.
|
||||
/// </summary>
|
||||
public static bool AddConverter(Type type, TypeConverter converter)
|
||||
{
|
||||
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||
if (converter == null) throw new ArgumentNullException(nameof(converter));
|
||||
if (CanConvert(type))
|
||||
{
|
||||
Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName);
|
||||
return false;
|
||||
}
|
||||
|
||||
TypeConverters.Add(type, converter);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given type can be converted to and from string.
|
||||
/// </summary>
|
||||
public static bool CanConvert(Type type)
|
||||
{
|
||||
return GetConverter(type) != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Give a list of types with registered converters.
|
||||
/// </summary>
|
||||
public static IEnumerable<Type> GetSupportedTypes()
|
||||
{
|
||||
return TypeConverters.Keys;
|
||||
}
|
||||
|
||||
private static bool _lazyLoadedConverters;
|
||||
private static void LazyLoadConverters()
|
||||
{
|
||||
try { LazyTomlConverterLoader.AddUnityEngineConverters(); }
|
||||
catch (Exception ex) { Logger.LogWarning("Failed to load UnityEngine Toml converters - " + ex.Message); }
|
||||
}
|
||||
|
||||
private static string Escape(this string txt)
|
||||
{
|
||||
var stringBuilder = new StringBuilder(txt.Length + 2);
|
||||
foreach (char c in txt)
|
||||
switch (c)
|
||||
{
|
||||
case '\0':
|
||||
stringBuilder.Append(@"\0");
|
||||
break;
|
||||
case '\a':
|
||||
stringBuilder.Append(@"\a");
|
||||
break;
|
||||
case '\b':
|
||||
stringBuilder.Append(@"\b");
|
||||
break;
|
||||
case '\t':
|
||||
stringBuilder.Append(@"\t");
|
||||
break;
|
||||
case '\n':
|
||||
stringBuilder.Append(@"\n");
|
||||
break;
|
||||
case '\v':
|
||||
stringBuilder.Append(@"\v");
|
||||
break;
|
||||
case '\f':
|
||||
stringBuilder.Append(@"\f");
|
||||
break;
|
||||
case '\r':
|
||||
stringBuilder.Append(@"\r");
|
||||
break;
|
||||
case '\'':
|
||||
stringBuilder.Append(@"\'");
|
||||
break;
|
||||
case '\\':
|
||||
stringBuilder.Append(@"\");
|
||||
break;
|
||||
case '\"':
|
||||
stringBuilder.Append(@"\""");
|
||||
break;
|
||||
default:
|
||||
stringBuilder.Append(c);
|
||||
break;
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static string Unescape(this string txt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(txt))
|
||||
return txt;
|
||||
var stringBuilder = new StringBuilder(txt.Length);
|
||||
for (int i = 0; i < txt.Length;)
|
||||
{
|
||||
int num = txt.IndexOf('\\', i);
|
||||
if (num < 0 || num == txt.Length - 1)
|
||||
num = txt.Length;
|
||||
stringBuilder.Append(txt, i, num - i);
|
||||
if (num >= txt.Length)
|
||||
break;
|
||||
char c = txt[num + 1];
|
||||
switch (c)
|
||||
{
|
||||
case '0':
|
||||
stringBuilder.Append('\0');
|
||||
break;
|
||||
case 'a':
|
||||
stringBuilder.Append('\a');
|
||||
break;
|
||||
case 'b':
|
||||
stringBuilder.Append('\b');
|
||||
break;
|
||||
case 't':
|
||||
stringBuilder.Append('\t');
|
||||
break;
|
||||
case 'n':
|
||||
stringBuilder.Append('\n');
|
||||
break;
|
||||
case 'v':
|
||||
stringBuilder.Append('\v');
|
||||
break;
|
||||
case 'f':
|
||||
stringBuilder.Append('\f');
|
||||
break;
|
||||
case 'r':
|
||||
stringBuilder.Append('\r');
|
||||
break;
|
||||
case '\'':
|
||||
stringBuilder.Append('\'');
|
||||
break;
|
||||
case '\"':
|
||||
stringBuilder.Append('\"');
|
||||
break;
|
||||
case '\\':
|
||||
stringBuilder.Append('\\');
|
||||
break;
|
||||
default:
|
||||
stringBuilder.Append('\\').Append(c);
|
||||
break;
|
||||
}
|
||||
|
||||
i = num + 2;
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For types that are in assemblies that can't get loaded before preloader runs (or it won't work on these assemblies)
|
||||
/// </summary>
|
||||
internal static class LazyTomlConverterLoader
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void AddUnityEngineConverters()
|
||||
{
|
||||
var colorConverter = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color)obj),
|
||||
ConvertToObject = (str, type) =>
|
||||
{
|
||||
if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out var c))
|
||||
throw new FormatException("Invalid color string, expected hex #RRGGBBAA");
|
||||
return c;
|
||||
},
|
||||
};
|
||||
|
||||
TomlTypeConverter.AddConverter(typeof(Color), colorConverter);
|
||||
|
||||
var jsonConverter = new TypeConverter
|
||||
{
|
||||
ConvertToString = (obj, type) => JsonUtility.ToJson(obj),
|
||||
ConvertToObject = (str, type) => JsonUtility.FromJson(type: type, json: str),
|
||||
};
|
||||
|
||||
TomlTypeConverter.AddConverter(typeof(Vector2), jsonConverter);
|
||||
TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter);
|
||||
TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter);
|
||||
TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace BepInEx.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// A serializer/deserializer combo for some type(s). Used by the config system.
|
||||
/// </summary>
|
||||
public class TypeConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to serialize the type into a (hopefully) human-readable string.
|
||||
/// Object is the instance to serialize, Type is the object's type.
|
||||
/// </summary>
|
||||
public Func<object, Type, string> ConvertToString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to deserialize the type from a string.
|
||||
/// String is the data to deserialize, Type is the object's type, should return instance to an object of Type.
|
||||
/// </summary>
|
||||
public Func<string, Type, object> ConvertToObject { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -13,17 +13,15 @@ namespace UnityInjector.ConsoleUtil
|
||||
{
|
||||
internal class ConsoleWindow
|
||||
{
|
||||
public static readonly ConfigWrapper<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Console",
|
||||
"Enabled",
|
||||
"Enables showing a console for log output.",
|
||||
false);
|
||||
public static readonly ConfigEntry<bool> ConfigConsoleEnabled = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Console", "Enabled",
|
||||
false,
|
||||
"Enables showing a console for log output.");
|
||||
|
||||
public static readonly ConfigWrapper<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Console",
|
||||
"ShiftJisEncoding",
|
||||
"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.",
|
||||
false);
|
||||
public static readonly ConfigEntry<bool> ConfigConsoleShiftJis = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Console", "ShiftJisEncoding",
|
||||
false,
|
||||
"If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.");
|
||||
|
||||
public static bool IsAttached { get; private set; }
|
||||
private static IntPtr _cOut;
|
||||
@@ -106,7 +104,7 @@ namespace UnityInjector.ConsoleUtil
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
@@ -241,7 +241,7 @@ namespace BepInEx
|
||||
/// <summary>
|
||||
/// Retrieves the BepInPlugin metadata from a plugin type.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin type.</param>
|
||||
/// <param name="pluginType">The plugin type.</param>
|
||||
/// <returns>The BepInPlugin metadata of the plugin type.</returns>
|
||||
public static BepInPlugin GetMetadata(Type pluginType)
|
||||
{
|
||||
@@ -265,7 +265,7 @@ namespace BepInEx
|
||||
/// Gets the specified attributes of a type, if they exist.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The attribute type to retrieve.</typeparam>
|
||||
/// <param name="plugin">The plugin type.</param>
|
||||
/// <param name="pluginType">The plugin type.</param>
|
||||
/// <returns>The attributes of the type, if existing.</returns>
|
||||
public static T[] GetAttributes<T>(Type pluginType) where T : Attribute
|
||||
{
|
||||
@@ -281,7 +281,6 @@ namespace BepInEx
|
||||
public static IEnumerable<T> GetAttributes<T>(object plugin) where T : Attribute
|
||||
=> GetAttributes<T>(plugin.GetType());
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the dependencies of the specified plugin type.
|
||||
/// </summary>
|
||||
@@ -293,14 +292,6 @@ namespace BepInEx
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exception which is thrown when a plugin's dependencies cannot be found.
|
||||
/// </summary>
|
||||
public class MissingDependencyException : Exception
|
||||
{
|
||||
public MissingDependencyException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Build configuration
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using BepInEx.Bootstrap;
|
||||
using BepInEx.Configuration;
|
||||
using BepInEx.Contract;
|
||||
using BepInEx.Logging;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -11,12 +10,24 @@ namespace BepInEx
|
||||
/// </summary>
|
||||
public abstract class BaseUnityPlugin : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about this plugin as it was loaded.
|
||||
/// </summary>
|
||||
public PluginInfo Info { get; }
|
||||
/// <summary>
|
||||
/// Logger instance tied to this plugin.
|
||||
/// </summary>
|
||||
protected ManualLogSource Logger { get; }
|
||||
|
||||
protected ConfigFile Config { get; }
|
||||
|
||||
protected PluginInfo Info { get; }
|
||||
/// <summary>
|
||||
/// Default config file tied to this plugin. The config file will not be created until
|
||||
/// any settings are added and changed, or <see cref="ConfigFile.Save"/> is called.
|
||||
/// </summary>
|
||||
public ConfigFile Config { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of a plugin and all of its tied in objects.
|
||||
/// </summary>
|
||||
protected BaseUnityPlugin()
|
||||
{
|
||||
var metadata = MetadataHelper.GetMetadata(this);
|
||||
@@ -37,7 +48,7 @@ namespace BepInEx
|
||||
|
||||
Logger = Logging.Logger.CreateLogSource(metadata.Name);
|
||||
|
||||
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false);
|
||||
Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using BepInEx.Bootstrap;
|
||||
|
||||
namespace BepInEx.Contract
|
||||
namespace BepInEx
|
||||
{
|
||||
public class PluginInfo : ICacheable
|
||||
{
|
||||
@@ -80,4 +80,4 @@ namespace BepInEx.Contract
|
||||
Incompatibilities = incList;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,9 @@ namespace BepInEx.Logging
|
||||
/// </summary>
|
||||
public class ConsoleLogListener : ILogListener
|
||||
{
|
||||
protected LogLevel DisplayedLogLevel = (LogLevel)Enum.Parse(typeof(LogLevel), ConfigConsoleDisplayedLevel.Value, true);
|
||||
|
||||
public void LogEvent(object sender, LogEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Level.GetHighestLevel() > DisplayedLogLevel)
|
||||
if (eventArgs.Level.GetHighestLevel() > ConfigConsoleDisplayedLevel.Value)
|
||||
return;
|
||||
|
||||
string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n";
|
||||
@@ -25,10 +23,9 @@ namespace BepInEx.Logging
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
private static readonly ConfigWrapper<string> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Wrap(
|
||||
"Logging.Console",
|
||||
"DisplayedLogLevel",
|
||||
"Only displays the specified log level and above in the console output.",
|
||||
"Info");
|
||||
private static readonly ConfigEntry<LogLevel> ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.AddSetting(
|
||||
"Logging.Console","DisplayedLogLevel",
|
||||
LogLevel.Info,
|
||||
"Only displays the specified log level and above in the console output.");
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ using BepInEx;
|
||||
[assembly: Guid("4ffba620-f5ed-47f9-b90c-dad1316fd9b9")]
|
||||
|
||||
[assembly: InternalsVisibleTo("BepInEx.Preloader")]
|
||||
[assembly: InternalsVisibleTo("BepInExTests")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BepInEx.Logging;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BepInEx
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods for running code on other threads and synchronizing with the main thread.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(int.MinValue)]
|
||||
public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke
|
||||
{
|
||||
private readonly object _invokeLock = new object();
|
||||
private Action _invokeList;
|
||||
private Thread _mainThread;
|
||||
|
||||
/// <summary>
|
||||
/// Current instance of the helper.
|
||||
/// </summary>
|
||||
public static ThreadingHelper Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gives methods for invoking delegates on the main unity thread, both synchronously and asynchronously.
|
||||
/// Can be used in many built-in framework types, for example <see cref="System.IO.FileSystemWatcher.SynchronizingObject"/>
|
||||
/// and <see cref="System.Timers.Timer.SynchronizingObject"/> to make their events fire on the main unity thread.
|
||||
/// </summary>
|
||||
public static ISynchronizeInvoke SynchronizingObject => Instance;
|
||||
|
||||
internal static void Initialize()
|
||||
{
|
||||
var go = new GameObject("BepInEx_ThreadingHelper");
|
||||
DontDestroyOnLoad(go);
|
||||
Instance = go.AddComponent<ThreadingHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the delegate to be invoked on the main unity thread. Use to synchronize your threads.
|
||||
/// </summary>
|
||||
public void StartSyncInvoke(Action action)
|
||||
{
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
|
||||
lock (_invokeLock) _invokeList += action;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// The CurrentThread can change between Awake and later methods, it's safest to get it here.
|
||||
if (_mainThread == null)
|
||||
_mainThread = Thread.CurrentThread;
|
||||
|
||||
// Safe to do outside of lock because nothing can remove callbacks, at worst we execute with 1 frame delay
|
||||
if (_invokeList == null) return;
|
||||
|
||||
Action toRun;
|
||||
lock (_invokeLock)
|
||||
{
|
||||
toRun = _invokeList;
|
||||
_invokeList = null;
|
||||
}
|
||||
|
||||
// Need to execute outside of the lock in case the callback itself calls Invoke we could deadlock
|
||||
// The invocation would also block any threads that call Invoke
|
||||
foreach (var action in toRun.GetInvocationList().Cast<Action>())
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogInvocationException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the delegate to be invoked on a background thread. Use this to run slow tasks without affecting the game.
|
||||
/// NOTE: Most of Unity API can not be accessed while running on another thread!
|
||||
/// </summary>
|
||||
/// <param name="action">
|
||||
/// Task to be executed on another thread. Can optionally return an Action that will be executed on the main thread.
|
||||
/// You can use this action to return results of your work safely. Return null if this is not needed.
|
||||
/// </param>
|
||||
public void StartAsyncInvoke(Func<Action> action)
|
||||
{
|
||||
void DoWork(object _)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = action();
|
||||
|
||||
if (result != null)
|
||||
StartSyncInvoke(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogInvocationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ThreadPool.QueueUserWorkItem(DoWork))
|
||||
throw new NotSupportedException("Failed to queue the action on ThreadPool");
|
||||
}
|
||||
|
||||
private static void LogInvocationException(Exception ex)
|
||||
{
|
||||
Logging.Logger.Log(LogLevel.Error, ex);
|
||||
if (ex.InnerException != null) Logging.Logger.Log(LogLevel.Error, "INNER: " + ex.InnerException);
|
||||
}
|
||||
|
||||
#region ISynchronizeInvoke
|
||||
|
||||
IAsyncResult ISynchronizeInvoke.BeginInvoke(Delegate method, object[] args)
|
||||
{
|
||||
object Invoke()
|
||||
{
|
||||
try { return method.DynamicInvoke(args); }
|
||||
catch (Exception ex) { return ex; }
|
||||
}
|
||||
|
||||
var result = new InvokeResult();
|
||||
|
||||
if (!InvokeRequired)
|
||||
result.Finish(Invoke(), true);
|
||||
else
|
||||
StartSyncInvoke(() => result.Finish(Invoke(), false));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
object ISynchronizeInvoke.EndInvoke(IAsyncResult result)
|
||||
{
|
||||
result.AsyncWaitHandle.WaitOne();
|
||||
|
||||
if (result.AsyncState is Exception ex)
|
||||
throw ex;
|
||||
return result.AsyncState;
|
||||
}
|
||||
|
||||
object ISynchronizeInvoke.Invoke(Delegate method, object[] args)
|
||||
{
|
||||
var invokeResult = ((ISynchronizeInvoke)this).BeginInvoke(method, args);
|
||||
return ((ISynchronizeInvoke)this).EndInvoke(invokeResult);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// False if current code is executing on the main unity thread, otherwise True.
|
||||
/// Warning: Will return false before the first frame finishes (i.e. inside plugin Awake and Start methods).
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public bool InvokeRequired => _mainThread == null || _mainThread != Thread.CurrentThread;
|
||||
|
||||
private sealed class InvokeResult : IAsyncResult
|
||||
{
|
||||
public InvokeResult()
|
||||
{
|
||||
AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
}
|
||||
|
||||
public void Finish(object result, bool completedSynchronously)
|
||||
{
|
||||
AsyncState = result;
|
||||
CompletedSynchronously = completedSynchronously;
|
||||
IsCompleted = true;
|
||||
((EventWaitHandle)AsyncWaitHandle).Set();
|
||||
}
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
public WaitHandle AsyncWaitHandle { get; }
|
||||
public object AsyncState { get; private set; }
|
||||
public bool CompletedSynchronously { get; private set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.IO;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace BepInEx.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class AssemblyInit
|
||||
{
|
||||
private static string _testPath;
|
||||
|
||||
[AssemblyInitialize]
|
||||
public static void InitAss(TestContext context)
|
||||
{
|
||||
_testPath = Path.Combine(Path.GetTempPath(), "BepinexTestDir");
|
||||
Directory.CreateDirectory(_testPath);
|
||||
|
||||
string exePath = Path.Combine(_testPath, "Text.exe");
|
||||
File.WriteAllBytes(exePath, new byte[] { });
|
||||
|
||||
Paths.SetExecutablePath(_testPath);
|
||||
}
|
||||
|
||||
[AssemblyCleanup]
|
||||
public static void CleanupAss()
|
||||
{
|
||||
Directory.Delete(_testPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{E7CD429A-D057-48E3-8C51-E5C934E8E07B}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>BepInExTests</RootNamespace>
|
||||
<AssemblyName>BepInExTests</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
|
||||
<IsCodedUITest>False</IsCodedUITest>
|
||||
<TestProjectType>UnitTest</TestProjectType>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
<TargetFrameworkProfile />
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<UpgradeBackupLocation>
|
||||
</UpgradeBackupLocation>
|
||||
<OldToolsVersion>15.0</OldToolsVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="UnityEngine">
|
||||
<HintPath>..\lib\UnityEngine.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<Otherwise />
|
||||
</Choose>
|
||||
<ItemGroup>
|
||||
<Compile Include="AssemblyInit.cs" />
|
||||
<Compile Include="Configuration\ConfigFileTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BepInEx\BepInEx.csproj">
|
||||
<Project>{4ffba620-f5ed-47f9-b90c-dad1316fd9b9}</Project>
|
||||
<Name>BepInEx</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.props'))" />
|
||||
<Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets'))" />
|
||||
</Target>
|
||||
<Import Project="..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.1.3.2\build\net45\MSTest.TestAdapter.targets')" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BepInEx.Configuration.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class ConfigFileTests
|
||||
{
|
||||
private static ConcurrentBag<ConfigFile> _toRemove;
|
||||
|
||||
[ClassInitialize]
|
||||
public static void Init(TestContext context)
|
||||
{
|
||||
_toRemove = new ConcurrentBag<ConfigFile>();
|
||||
}
|
||||
|
||||
[ClassCleanup]
|
||||
public static void Cleanup()
|
||||
{
|
||||
foreach (var configFile in _toRemove)
|
||||
File.Delete(configFile.ConfigFilePath);
|
||||
}
|
||||
|
||||
private static ConfigFile MakeConfig()
|
||||
{
|
||||
string configPath = Path.GetTempFileName();
|
||||
if (configPath == null) throw new InvalidOperationException("Wtf");
|
||||
var config = new ConfigFile(configPath, true);
|
||||
_toRemove.Add(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SaveTest()
|
||||
{
|
||||
MakeConfig().Save();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SaveTestValueChange()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
|
||||
var lines = File.ReadAllLines(c.ConfigFilePath);
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0")));
|
||||
|
||||
c.Save();
|
||||
lines = File.ReadAllLines(c.ConfigFilePath);
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0")));
|
||||
|
||||
w.Value = 69;
|
||||
lines = File.ReadAllLines(c.ConfigFilePath);
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]")));
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("## Test")));
|
||||
Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 69")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AutoSaveTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
|
||||
|
||||
var eventFired = new AutoResetEvent(false);
|
||||
c.ConfigReloaded += (sender, args) => eventFired.Set();
|
||||
|
||||
c.Save();
|
||||
|
||||
Assert.IsFalse(eventFired.WaitOne(200));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n");
|
||||
c.Reload();
|
||||
var w = c.AddSetting("Cat", "Key", 0, "Test");
|
||||
Assert.AreEqual(w.Value, 1);
|
||||
var w2 = c.AddSetting("Cat", "Key2", 0, new ConfigDescription("Test"));
|
||||
Assert.AreEqual(w2.Value, 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadTest2()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
|
||||
Assert.AreEqual(w.Value, 0);
|
||||
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n");
|
||||
|
||||
c.Reload();
|
||||
Assert.AreEqual(w.Value, 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FileWatchTestNoSelfReload()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
|
||||
var eventFired = new AutoResetEvent(false);
|
||||
c.ConfigReloaded += (sender, args) => eventFired.Set();
|
||||
|
||||
c.Save();
|
||||
|
||||
Assert.IsFalse(eventFired.WaitOne(200));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EventTestWrapper()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
|
||||
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n");
|
||||
|
||||
var eventFired = false;
|
||||
w.SettingChanged += (sender, args) => eventFired = true;
|
||||
|
||||
c.Reload();
|
||||
|
||||
Assert.IsTrue(eventFired);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PersistHomeless()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\nHomeless=0");
|
||||
c.Reload();
|
||||
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
|
||||
|
||||
c.Save();
|
||||
|
||||
Assert.IsTrue(File.ReadAllLines(c.ConfigFilePath).Single(x => x.StartsWith("Homeless") && x.EndsWith("0")) != null);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EventTestReload()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var eventFired = false;
|
||||
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test"));
|
||||
w.SettingChanged += (sender, args) => eventFired = true;
|
||||
|
||||
Assert.IsFalse(eventFired);
|
||||
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n");
|
||||
c.Reload();
|
||||
|
||||
Assert.IsTrue(eventFired);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValueRangeTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
|
||||
|
||||
Assert.AreEqual(0, w.Value);
|
||||
w.Value = 2;
|
||||
Assert.AreEqual(2, w.Value);
|
||||
w.Value = -2;
|
||||
Assert.AreEqual(0, w.Value);
|
||||
w.Value = 4;
|
||||
Assert.AreEqual(2, w.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(ArgumentException))]
|
||||
public void ValueRangeBadTypeTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<float>(1, 2)));
|
||||
Assert.Fail();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValueRangeDefaultTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(1, 2)));
|
||||
|
||||
Assert.AreEqual(w.Value, 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValueRangeLoadTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 1\n");
|
||||
c.Reload();
|
||||
|
||||
var w = c.AddSetting("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange<int>(0, 2)));
|
||||
|
||||
Assert.AreEqual(w.Value, 1);
|
||||
|
||||
File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 5\n");
|
||||
c.Reload();
|
||||
|
||||
Assert.AreEqual(w.Value, 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValueListTest()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", "kek", new ConfigDescription("Test", new AcceptableValueList<string>("lel", "kek", "wew", "why")));
|
||||
|
||||
Assert.AreEqual("kek", w.Value);
|
||||
w.Value = "wew";
|
||||
Assert.AreEqual("wew", w.Value);
|
||||
w.Value = "no";
|
||||
Assert.AreEqual("lel", w.Value);
|
||||
w.Value = null;
|
||||
Assert.AreEqual("lel", w.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void KeyboardShortcutTest()
|
||||
{
|
||||
var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y);
|
||||
var s = shortcut.Serialize();
|
||||
var d = KeyboardShortcut.Deserialize(s);
|
||||
Assert.AreEqual(shortcut, d);
|
||||
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift));
|
||||
Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value);
|
||||
|
||||
w.Value = shortcut;
|
||||
c.Reload();
|
||||
Assert.AreEqual(shortcut, w.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void KeyboardShortcutTest2()
|
||||
{
|
||||
Assert.AreEqual(KeyboardShortcut.Empty, new KeyboardShortcut());
|
||||
|
||||
var c = MakeConfig();
|
||||
|
||||
var w = c.AddSetting("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test"));
|
||||
|
||||
Assert.AreEqual("", w.GetSerializedValue());
|
||||
|
||||
w.SetSerializedValue(w.GetSerializedValue());
|
||||
Assert.AreEqual(KeyboardShortcut.Empty, w.Value);
|
||||
|
||||
var testShortcut = new KeyboardShortcut(KeyCode.A, KeyCode.B, KeyCode.C);
|
||||
w.Value = testShortcut;
|
||||
|
||||
w.SetSerializedValue(w.GetSerializedValue());
|
||||
Assert.AreEqual(testShortcut, w.Value);
|
||||
|
||||
c.Save();
|
||||
c.Reload();
|
||||
|
||||
Assert.AreEqual(testShortcut, w.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringEscapeChars()
|
||||
{
|
||||
const string testVal = "new line\n test \t\0";
|
||||
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", testVal, new ConfigDescription("Test"));
|
||||
|
||||
Assert.AreEqual(testVal, w.Value);
|
||||
Assert.IsFalse(w.GetSerializedValue().Any(x => x == '\n'));
|
||||
|
||||
w.SetSerializedValue(w.GetSerializedValue());
|
||||
Assert.AreEqual(testVal, w.Value);
|
||||
|
||||
c.Save();
|
||||
c.Reload();
|
||||
|
||||
Assert.AreEqual(testVal, w.Value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UnescapedPathString()
|
||||
{
|
||||
var c = MakeConfig();
|
||||
var w = c.AddSetting("Cat", "Key", "", new ConfigDescription("Test"));
|
||||
|
||||
var unescaped = @"D:\test\p ath";
|
||||
foreach (string testVal in new[] { unescaped, @"D:\\test\\p ath" })
|
||||
{
|
||||
File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n");
|
||||
c.Reload();
|
||||
|
||||
Assert.AreEqual(unescaped, w.Value);
|
||||
|
||||
w.SetSerializedValue(w.GetSerializedValue());
|
||||
Assert.AreEqual(unescaped, w.Value);
|
||||
|
||||
c.Save();
|
||||
c.Reload();
|
||||
|
||||
Assert.AreEqual(unescaped, w.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("BepInExTests")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("BepInExTests")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019")]
|
||||
[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("e7cd429a-d057-48e3-8c51-e5c934e8e07b")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="MSTest.TestAdapter" version="1.3.2" targetFramework="net45" />
|
||||
<package id="MSTest.TestFramework" version="1.3.2" targetFramework="net45" />
|
||||
</packages>
|
||||
+1
-1
Submodule submodules/MonoMod updated: 9462a0f75b...a70072cdf7.
Reference in new issue
Block a user