Переглянути джерело

Large config file upgrade - backend rewrite; config file change monitoring; setting changed events propagate from ConfigFile; added unit tests and Test configuration

ManlyMarco 4 роки тому
батько
коміт
65177c419e

+ 123 - 0
BepInEx.sln

@@ -27,44 +27,167 @@ 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
+		Debug|Test = Debug|Test
 		Legacy|Any CPU = Legacy|Any CPU
+		Legacy|Test = Legacy|Test
+		Release|Any CPU = Release|Any CPU
+		Release|Test = Release|Test
 		v2018|Any CPU = v2018|Any CPU
+		v2018|Test = v2018|Test
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Test.ActiveCfg = v2018|x86
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Test.Build.0 = v2018|x86
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.Build.0 = Legacy|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Test.ActiveCfg = Legacy|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Test.Build.0 = Legacy|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Test.ActiveCfg = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Test.Build.0 = v2018|Any CPU
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
 		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.Build.0 = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Test.ActiveCfg = v2018|Any CPU
+		{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Test.Build.0 = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Test.ActiveCfg = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Test.Build.0 = v2018|Any CPU
 		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU
 		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.Build.0 = Legacy|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Test.ActiveCfg = Legacy|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Test.Build.0 = Legacy|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Test.ActiveCfg = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Test.Build.0 = v2018|Any CPU
 		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
 		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.Build.0 = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Test.ActiveCfg = v2018|Any CPU
+		{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Test.Build.0 = v2018|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}.Debug|Test.ActiveCfg = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Test.Build.0 = Release|Any CPU
 		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
 		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.Build.0 = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Test.ActiveCfg = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Test.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
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Test.ActiveCfg = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Test.Build.0 = Release|Any CPU
 		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.ActiveCfg = Release|Any CPU
 		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Test.ActiveCfg = Release|Any CPU
+		{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Test.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}.Debug|Test.ActiveCfg = Debug|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Test.Build.0 = Debug|Any CPU
 		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
 		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.Build.0 = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Test.ActiveCfg = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Test.Build.0 = Release|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
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Test.ActiveCfg = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Test.Build.0 = Release|Any CPU
 		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.ActiveCfg = Release|Any CPU
 		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Test.ActiveCfg = Release|Any CPU
+		{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Test.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}.Debug|Test.ActiveCfg = Debug|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Test.Build.0 = Debug|Any CPU
 		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
 		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.Build.0 = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Test.ActiveCfg = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Test.Build.0 = Release|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
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Test.ActiveCfg = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Test.Build.0 = Release|Any CPU
 		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.ActiveCfg = Release|Any CPU
 		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Test.ActiveCfg = Release|Any CPU
+		{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Test.Build.0 = Release|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Test.ActiveCfg = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Test.Build.0 = v2018|Any CPU
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.Build.0 = Legacy|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Test.ActiveCfg = Legacy|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Test.Build.0 = Legacy|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Test.ActiveCfg = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Test.Build.0 = v2018|Any CPU
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
 		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.Build.0 = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Test.ActiveCfg = v2018|Any CPU
+		{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Test.Build.0 = v2018|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}.Debug|Test.ActiveCfg = Debug|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Test.Build.0 = Debug|Any CPU
 		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
 		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.Build.0 = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Test.ActiveCfg = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Test.Build.0 = Release|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
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Test.ActiveCfg = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Test.Build.0 = Release|Any CPU
 		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.ActiveCfg = Release|Any CPU
 		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Test.ActiveCfg = Release|Any CPU
+		{D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Test.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}.Debug|Test.ActiveCfg = Debug|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Test.Build.0 = Debug|Any CPU
 		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
 		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.Build.0 = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Test.ActiveCfg = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Test.Build.0 = Release|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
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Test.ActiveCfg = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Test.Build.0 = Release|Any CPU
 		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.ActiveCfg = Release|Any CPU
 		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Test.ActiveCfg = Release|Any CPU
+		{1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Test.Build.0 = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Test.ActiveCfg = Debug|x86
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Test.Build.0 = Debug|x86
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.ActiveCfg = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.Build.0 = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Test.ActiveCfg = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Test.Build.0 = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Test.ActiveCfg = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Test.Build.0 = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.ActiveCfg = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.Build.0 = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Test.ActiveCfg = Release|Any CPU
+		{E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Test.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 33 - 0
BepInEx/BepInEx.csproj

@@ -33,11 +33,41 @@
     <PlatformTarget>AnyCPU</PlatformTarget>
     <ErrorReport>prompt</ErrorReport>
     <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+    <DocumentationFile>..\bin\BepInEx.xml</DocumentationFile>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Legacy|x86'">
+    <OutputPath>bin\x86\Legacy\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <Optimize>false</Optimize>
+    <PlatformTarget>x86</PlatformTarget>
+    <ErrorReport>prompt</ErrorReport>
+    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
+    <DebugType>full</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'v2018|x86'">
+    <OutputPath>bin\x86\v2018\</OutputPath>
+    <DefineConstants>DEBUG;TRACE;UNITY_2018</DefineConstants>
+    <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>
     </Reference>
+    <Reference Include="Mono.Cecil.Mdb, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Pdb, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll</HintPath>
+    </Reference>
+    <Reference Include="Mono.Cecil.Rocks, Version=0.10.3.0, Culture=neutral, PublicKeyToken=50cebf1cceb9d05e, processorArchitecture=MSIL">
+      <HintPath>..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll</HintPath>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="UnityEngine">
       <HintPath>..\lib\UnityEngine.dll</HintPath>
@@ -46,8 +76,11 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Configuration\ConfigDefinition.cs" />
+    <Compile Include="Configuration\ConfigDescription.cs" />
+    <Compile Include="Configuration\ConfigEntry.cs" />
     <Compile Include="Configuration\ConfigFile.cs" />
     <Compile Include="Configuration\ConfigWrapper.cs" />
+    <Compile Include="Configuration\SettingChangedEventArgs.cs" />
     <Compile Include="Configuration\TomlTypeConverter.cs" />
     <Compile Include="Contract\Attributes.cs" />
     <Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />

+ 6 - 22
BepInEx/Configuration/ConfigDefinition.cs

@@ -2,27 +2,6 @@
 
 namespace BepInEx.Configuration
 {
-	public class ConfigDescription
-	{
-		public ConfigDescription(string description, Type settingType, object defaultValue)
-		{
-			Description = description ?? throw new ArgumentNullException(nameof(description));
-			SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType));
-			DefaultValue = defaultValue;
-
-			if(defaultValue == null && settingType.IsByRef)
-				throw new ArgumentException("defaultValue is null while settingType is a value type");
-
-			if(defaultValue != null && !settingType.IsInstanceOfType(defaultValue))
-				throw new ArgumentException("defaultValue can not be assigned to type " + settingType.Name);
-		}
-
-		public string Description { get; }
-		public Type SettingType { get; }
-		public object DefaultValue { get; }
-		//todo value range
-	}
-
 	public class ConfigDefinition : IEquatable<ConfigDefinition>
 	{
 		public string Section { get; }
@@ -39,7 +18,7 @@ namespace BepInEx.Configuration
 		{
 			if (other == null) return false;
 			return string.Equals(Key, other.Key)
-			       && string.Equals(Section, other.Section);
+				   && string.Equals(Section, other.Section);
 		}
 
 		public override bool Equals(object obj)
@@ -67,5 +46,10 @@ namespace BepInEx.Configuration
 
 		public static bool operator !=(ConfigDefinition left, ConfigDefinition right)
 			=> !Equals(left, right);
+
+		public override string ToString()
+		{
+			return Section + " / " + Key;
+		}
 	}
 }

+ 21 - 0
BepInEx/Configuration/ConfigDescription.cs

@@ -0,0 +1,21 @@
+using System;
+
+namespace BepInEx.Configuration
+{
+	public class ConfigDescription
+	{
+		public ConfigDescription(string description)
+		{
+			Description = description ?? throw new ArgumentNullException(nameof(description));
+		}
+
+		public string Description { get; }
+
+		//todo value range
+
+		public string ToSerializedString()
+		{
+			return $"# {Description.Replace("\n", "\n# ")}";
+		}
+	}
+}

+ 182 - 0
BepInEx/Configuration/ConfigEntry.cs

@@ -0,0 +1,182 @@
+using System;
+using System.IO;
+using System.Linq;
+using BepInEx.Logging;
+
+namespace BepInEx.Configuration
+{
+	public sealed class ConfigEntry
+	{
+		internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition, Type settingType, object defaultValue) : this(configFile, definition)
+		{
+			SetTypeAndDefaultValue(settingType, defaultValue, true);
+		}
+
+		internal ConfigEntry(ConfigFile configFile, ConfigDefinition definition)
+		{
+			ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile));
+			Definition = definition ?? throw new ArgumentNullException(nameof(definition));
+		}
+
+		internal void SetTypeAndDefaultValue(Type settingType, object defaultValue, bool uniqueDefaultValue)
+		{
+			if (settingType == null) throw new ArgumentNullException(nameof(settingType));
+
+			if (settingType == SettingType)
+			{
+				if (uniqueDefaultValue)
+					DefaultValue = defaultValue;
+				return;
+			}
+
+			if (SettingType != null)
+			{
+				throw new ArgumentException($"Tried to define setting \"{Definition}\" as type {settingType.Name} " +
+				                            $"while it was already defined as type {SettingType.Name}. Use the same " +
+				                            $"Type for all Wrappers of a single setting.");
+			}
+
+			if (defaultValue == null && settingType.IsValueType)
+				throw new ArgumentException("defaultValue is null while settingType is a value type");
+
+			if (defaultValue != null && !settingType.IsInstanceOfType(defaultValue))
+				throw new ArgumentException("defaultValue can not be assigned to type " + settingType.Name);
+
+			SettingType = settingType;
+			DefaultValue = defaultValue;
+		}
+
+		private object _convertedValue;
+		private string _serializedValue;
+
+		public ConfigFile ConfigFile { get; }
+		public ConfigDefinition Definition { get; }
+
+		public ConfigDescription Description { get; internal set; }
+
+		public Type SettingType { get; private set; }
+		public object DefaultValue { get; private set; }
+
+		/// <summary>
+		/// Is the type of this setting defined, and by extension can <see cref="Value"/> of this setting be accessed.
+		/// Setting is defined when any <see cref="ConfigWrapper{T}"/> objects reference it.
+		/// </summary>
+		public bool IsDefined => SettingType != null;
+
+		/// <summary>
+		/// Can't be used when <see cref="IsDefined"/> is false.
+		/// </summary>
+		public object Value
+		{
+			get
+			{
+				ProcessSerializedValue();
+
+				return _convertedValue;
+			}
+			set => SetValue(value, true, this);
+		}
+
+		internal void SetValue(object newValue, bool fireEvent, object sender)
+		{
+			bool wasChanged = ProcessSerializedValue();
+			wasChanged = wasChanged || !Equals(newValue, _convertedValue);
+
+			if (wasChanged)
+			{
+				_convertedValue = newValue;
+
+				if (fireEvent)
+					OnSettingChanged(sender);
+			}
+		}
+
+		public string GetSerializedValue()
+		{
+			if (_serializedValue != null)
+				return _serializedValue;
+
+			if (!IsDefined)
+				return null;
+
+			return TomlTypeConverter.ConvertToString(Value, SettingType);
+		}
+
+		public void SetSerializedValue(string newValue, bool fireEvent, object sender)
+		{
+			string current = GetSerializedValue();
+			if (string.Equals(current, newValue)) return;
+
+			_serializedValue = newValue;
+
+			if (!IsDefined) return;
+			
+			if (ProcessSerializedValue())
+			{
+				if (fireEvent)
+					OnSettingChanged(sender);
+			}
+		}
+
+		private bool ProcessSerializedValue()
+		{
+			if (!IsDefined)
+				throw new InvalidOperationException("Can't get the value before the SettingType is specified");
+
+			if (_serializedValue != null)
+			{
+				string value = _serializedValue;
+				_serializedValue = null;
+
+				if (value != "")
+				{
+					try
+					{
+						var newValue = TomlTypeConverter.ConvertToValue(value, SettingType);
+						if (!Equals(newValue, _convertedValue))
+						{
+							_convertedValue = newValue;
+							return true;
+						}
+						return false;
+					}
+					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}");
+					}
+				}
+			}
+
+			if (_convertedValue == null && DefaultValue != null)
+			{
+				_convertedValue = DefaultValue;
+				return true;
+			}
+
+			return false;
+		}
+
+		private void OnSettingChanged(object sender)
+		{
+			ConfigFile.OnSettingChanged(sender, this);
+		}
+
+		public void WriteDescription(StreamWriter writer)
+		{
+			if (Description != null)
+				writer.WriteLine(Description.ToSerializedString());
+
+			if (SettingType != null)
+			{
+				writer.WriteLine("# Setting type: " + SettingType.Name);
+				writer.WriteLine("# Default value: " + DefaultValue);
+
+				// todo acceptable values
+
+				if (SettingType.IsEnum && 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)");
+			}
+		}
+	}
+}

+ 175 - 40
BepInEx/Configuration/ConfigFile.cs

@@ -3,7 +3,8 @@ 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
 {
@@ -12,28 +13,30 @@ namespace BepInEx.Configuration
 	/// </summary>
 	public class ConfigFile
 	{
-		private static readonly Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+");
+		// Need to be lazy evaluated to not cause problems for unit tests
+		private static ConfigFile _coreConfig;
+		internal static ConfigFile CoreConfig => _coreConfig ?? (_coreConfig = new ConfigFile(Paths.BepInExConfigPath, true));
 
-		internal static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true);
+		protected Dictionary<ConfigDefinition, ConfigEntry> Entries { get; } = new Dictionary<ConfigDefinition, ConfigEntry>();
 
-		protected internal Dictionary<ConfigDefinition, string> Cache { get; } = new Dictionary<ConfigDefinition, string>();
+		[Obsolete("Use ConfigEntries instead")]
+		public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions => Entries.Keys.ToList().AsReadOnly();
 
-		public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions => Cache.Keys.ToList().AsReadOnly();
-
-		/// <summary>
-		/// An event that is fired every time the config is reloaded.
-		/// </summary>
-		public event EventHandler ConfigReloaded;
+		public ReadOnlyCollection<ConfigEntry> ConfigEntries => Entries.Values.ToList().AsReadOnly();
 
 		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 save or the changes will be lost!
 		/// </summary>
 		public bool SaveOnConfigSet { get; set; } = true;
 
 		public ConfigFile(string configPath, bool saveOnInit)
 		{
+			if (configPath == null) throw new ArgumentNullException(nameof(configPath));
+
+			configPath = Path.GetFullPath(configPath);
+
 			ConfigFilePath = configPath;
 
 			if (File.Exists(ConfigFilePath))
@@ -44,9 +47,13 @@ namespace BepInEx.Configuration
 			{
 				Save();
 			}
+
+			StartWatching();
 		}
 
-		private object _ioLock = new object();
+		#region Save/Load
+
+		private readonly object _ioLock = new object();
 
 		/// <summary>
 		/// Reloads the config from disk. Unsaved changes are lost.
@@ -55,9 +62,7 @@ namespace BepInEx.Configuration
 		{
 			lock (_ioLock)
 			{
-				Dictionary<ConfigDefinition, string> descriptions = Cache.ToDictionary(x => x.Key, x => x.Key.Description);
-
-				string currentSection = "";
+				string currentSection = string.Empty;
 
 				foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
 				{
@@ -81,14 +86,18 @@ namespace BepInEx.Configuration
 
 					var definition = new ConfigDefinition(currentSection, currentKey);
 
-					if (descriptions.ContainsKey(definition))
-						definition.Description = descriptions[definition];
+					Entries.TryGetValue(definition, out ConfigEntry entry);
+					if (entry == null)
+					{
+						entry = new ConfigEntry(this, definition);
+						Entries[definition] = entry;
+					}
 
-					Cache[definition] = currentValue;
+					entry.SetSerializedValue(currentValue, true, this);
 				}
-
-				ConfigReloaded?.Invoke(this, EventArgs.Empty);
 			}
+
+			OnConfigReloaded();
 		}
 
 		/// <summary>
@@ -98,51 +107,177 @@ namespace BepInEx.Configuration
 		{
 			lock (_ioLock)
 			{
-				if (!Directory.Exists(Paths.ConfigPath))
-					Directory.CreateDirectory(Paths.ConfigPath);
+				StopWatching();
+
+				string directoryName = Path.GetDirectoryName(ConfigFilePath);
+				if (directoryName != null) Directory.CreateDirectory(directoryName);
 
-				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))
+				using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8))
+				{
+					foreach (var sectionKv in Entries.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.Select(x => x.Value))
 						{
 							writer.WriteLine();
 
-							if (!string.IsNullOrEmpty(entryKv.Key.Description))
-								writer.WriteLine($"# {entryKv.Key.Description.Replace("\n", "\n# ")}");
+							configEntry.WriteDescription(writer);
 
-							writer.WriteLine($"{entryKv.Key.Key} = {entryKv.Value}");
+							writer.WriteLine($"{configEntry.Definition.Key} = {configEntry.GetSerializedValue()}");
 						}
 
 						writer.WriteLine();
 					}
+				}
+
+				StartWatching();
 			}
 		}
 
-		public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
+		#endregion
+
+		#region Wraps
+
+		public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
 		{
-			if (!Cache.ContainsKey(configDefinition))
+			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())}");
+
+			Entries.TryGetValue(configDefinition, out var entry);
+
+			if (entry == null)
 			{
-				Cache.Add(configDefinition, TomlTypeConverter.ConvertToString(defaultValue));
-				Save();
+				entry = new ConfigEntry(this, configDefinition, typeof(T), defaultValue);
+				Entries[configDefinition] = entry;
 			}
 			else
 			{
-				var original = Cache.Keys.First(x => x.Equals(configDefinition));
+				entry.SetTypeAndDefaultValue(typeof(T), defaultValue, !Equals(defaultValue, default(T)));
+			}
+
+			if (configDescription != null)
+			{
+				if (entry.Description != null)
+					Logger.Log(LogLevel.Warning, $"Tried to add configDescription to setting {configDefinition} when it already had one defined. Only add configDescription once or a random one will be used.");
+
+				entry.Description = configDescription;
+			}
+
+			return new ConfigWrapper<T>(entry);
+		}
+
+		[Obsolete("Use other Wrap overloads instead")]
+		public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
+			=> Wrap(new ConfigDefinition(section, key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
+
+		public ConfigWrapper<T> Wrap<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
+			=> Wrap(new ConfigDefinition(section, key), defaultValue, configDescription);
+
+		#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;
+
+		protected internal void OnSettingChanged(object sender, ConfigEntry changedEntry)
+		{
+			if (changedEntry == null) throw new ArgumentNullException(nameof(changedEntry));
 
-				if (original.Description != configDefinition.Description)
+			if (SettingChanged != null)
+			{
+				var args = new SettingChangedEventArgs(changedEntry);
+
+				foreach (var callback in SettingChanged.GetInvocationList().Cast<EventHandler<SettingChangedEventArgs>>())
 				{
-					original.Description = configDefinition.Description;
-					Save();
+					try
+					{
+						callback(sender, args);
+					}
+					catch (Exception e)
+					{
+						Logger.Log(LogLevel.Error, e);
+					}
 				}
 			}
 
-			return new ConfigWrapper<T>(this, configDefinition);
+			// todo better way to prevent write loop? maybe do some caching?
+			if (sender != this && SaveOnConfigSet)
+				Save();
 		}
 
-		public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
-			=> Wrap<T>(new ConfigDefinition(section, key, description), defaultValue);
+		protected void OnConfigReloaded()
+		{
+			if (ConfigReloaded != null)
+			{
+				foreach (var callback in ConfigReloaded.GetInvocationList().Cast<EventHandler>())
+				{
+					try
+					{
+						callback(this, EventArgs.Empty);
+					}
+					catch (Exception e)
+					{
+						Logger.Log(LogLevel.Error, e);
+					}
+				}
+			}
+		}
+
+		#endregion
+
+		#region File watcher
+
+		private FileSystemWatcher _watcher;
+
+		/// <summary>
+		/// Start watching the config file on disk for changes.
+		/// </summary>
+		public void StartWatching()
+		{
+			lock (_ioLock)
+			{
+				if (_watcher != null) return;
+
+				_watcher = new FileSystemWatcher
+				{
+					Path = Path.GetDirectoryName(ConfigFilePath) ?? throw new ArgumentException("Invalid config path"),
+					Filter = Path.GetFileName(ConfigFilePath),
+					IncludeSubdirectories = false,
+					NotifyFilter = NotifyFilters.LastWrite,
+					EnableRaisingEvents = true
+				};
+
+				_watcher.Changed += (sender, args) => Reload();
+			}
+		}
+
+		/// <summary>
+		/// Stop watching the config file on disk for changes.
+		/// </summary>
+		public void StopWatching()
+		{
+			lock (_ioLock)
+			{
+				_watcher?.Dispose();
+				_watcher = null;
+			}
+		}
+
+		~ConfigFile()
+		{
+			StopWatching();
+		}
+
+		#endregion
 	}
-}
+}

+ 12 - 18
BepInEx/Configuration/ConfigWrapper.cs

@@ -2,11 +2,12 @@
 
 namespace BepInEx.Configuration
 {
-	public class ConfigWrapper<T>
+	public sealed class ConfigWrapper<T>
 	{
-		public ConfigDefinition Definition { get; protected set; }
+		public ConfigEntry ConfigEntry { get; }
 
-		public ConfigFile ConfigFile { get; protected set; }
+		public ConfigDefinition Definition => ConfigEntry.Definition;
+		public ConfigFile ConfigFile => ConfigEntry.ConfigFile;
 
 		/// <summary>
 		/// Fired when the setting is changed. Does not detect changes made outside from this object.
@@ -15,25 +16,18 @@ namespace BepInEx.Configuration
 
 		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 => (T)ConfigEntry.Value;
+			set => ConfigEntry.SetValue(value, true, this);
 		}
 
-		public ConfigWrapper(ConfigFile configFile, ConfigDefinition definition)
+		internal ConfigWrapper(ConfigEntry 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);
+			};
 		}
 	}
 }

+ 14 - 0
BepInEx/Configuration/SettingChangedEventArgs.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace BepInEx.Configuration
+{
+	public sealed class SettingChangedEventArgs : EventArgs
+	{
+		public SettingChangedEventArgs(ConfigEntry changedSetting)
+		{
+			ChangedSetting = changedSetting;
+		}
+
+		public ConfigEntry ChangedSetting { get; }
+	}
+}

+ 6 - 5
BepInEx/Configuration/TomlTypeConverter.cs

@@ -98,10 +98,8 @@ namespace BepInEx.Configuration
 			},
 		};
 
-		public static string ConvertToString<T>(T value)
+		public static string ConvertToString(object value, Type valueType)
 		{
-			var valueType = typeof(T);
-
 			var conv = GetConverter(valueType);
 			if (conv == null)
 				throw new InvalidOperationException($"Cannot convert from type {valueType}");
@@ -111,13 +109,16 @@ namespace BepInEx.Configuration
 
 		public static T ConvertToValue<T>(string value)
 		{
-			var valueType = typeof(T);
+			return (T)ConvertToValue(value, typeof(T));
+		}
 
+		public static object ConvertToValue(string value, Type valueType)
+		{
 			var conv = GetConverter(valueType);
 			if (conv == null)
 				throw new InvalidOperationException($"Cannot convert to type {valueType}");
 
-			return (T)conv.ConvertToObject(value, valueType);
+			return conv.ConvertToObject(value, valueType);
 		}
 
 		private static TypeConverter GetConverter(Type valueType)

+ 131 - 0
BepInExTests/BepInExTests.csproj

@@ -0,0 +1,131 @@
+<?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|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <Prefer32Bit>false</Prefer32Bit>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <Prefer32Bit>false</Prefer32Bit>
+  </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" />
+  </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="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>

+ 172 - 0
BepInExTests/Configuration/ConfigFileTests.cs

@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Concurrent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.IO;
+using System.Linq;
+using System.Threading;
+
+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)
+			{
+				configFile.StopWatching();
+				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.Wrap("Cat", "Key", 0, new ConfigDescription("Test"));
+			var lines = File.ReadAllLines(c.ConfigFilePath);
+			Assert.AreEqual(0, lines.Count(x => x.Equals("[Cat]")));
+			Assert.AreEqual(0, lines.Count(x => x.Equals("# Test")));
+			Assert.AreEqual(0, 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.Wrap("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.Wrap("Cat", "Key", 0, new ConfigDescription("Test"));
+			Assert.AreEqual(w.Value, 1);
+			var w2 = c.Wrap("Cat", "Key2", 0, new ConfigDescription("Test"));
+			Assert.AreEqual(w2.Value, 0);
+		}
+
+		[TestMethod]
+		public void ReadTest2()
+		{
+			var c = MakeConfig();
+			var w = c.Wrap("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 FileWatchTest()
+		{
+			var c = MakeConfig();
+			var w = c.Wrap("Cat", "Key", 0, new ConfigDescription("Test"));
+			Assert.AreEqual(w.Value, 0);
+
+			var eventFired = new AutoResetEvent(false);
+			w.SettingChanged += (sender, args) => eventFired.Set();
+
+			File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n");
+
+			Assert.IsTrue(eventFired.WaitOne(500));
+
+			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.Wrap("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 EventTestReload()
+		{
+			var c = MakeConfig();
+			var eventFired = false;
+
+			var w = c.Wrap("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);
+		}
+	}
+}

+ 36 - 0
BepInExTests/Properties/AssemblyInfo.cs

@@ -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 - 0
BepInExTests/packages.config

@@ -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>