Merge pull request #88 from ManlyMarco/master

Overhaul config system
This commit is contained in:
Geoffrey Horsington authored and GitHub committed 2019-09-22 20:39:01 +03:00
commit 851c045d6d
32 files changed
+2064 -272

No files matched your search

+12 -15
View File
@@ -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
}
+24 -31
View File
@@ -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
View File
@@ -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
+3 -1
View File
@@ -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>
+21
View File
@@ -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>
+32 -39
View File
@@ -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
}
}
+4 -5
View File
@@ -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}";
}
}
}
+64 -14
View File
@@ -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("");
}
}
+176
View File
@@ -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)");
}
}
}
}
+263 -66
View File
@@ -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
}
}
}
+31 -19
View File
@@ -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);
};
}
}
}
}
+180
View File
@@ -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; }
}
}
+275 -44
View File
@@ -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);
}
}
}
+22
View File
@@ -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; }
}
}
+9 -11
View File
@@ -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();
+2 -11
View File
@@ -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
+16 -5
View File
@@ -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);
}
}
}
+2 -2
View File
@@ -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;
}
}
}
}
+5 -8
View File
@@ -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.");
}
}
+1
View File
@@ -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:
//
+180
View File
@@ -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
}
}
+29
View File
@@ -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);
}
}
}
+116
View File
@@ -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);
}
}
}
}
+36
View File
@@ -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")]
+5
View File
@@ -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>