diff --git a/.editorconfig b/.editorconfig
index 2c97ae5..918200f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -92,7 +92,7 @@ dotnet_naming_style.all_upper_style.capitalization = all_upper
dotnet_naming_style.all_upper_style.word_separator = _
dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
-dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected
+dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
dotnet_naming_symbols.constants_symbols.applicable_kinds = field
dotnet_naming_symbols.constants_symbols.required_modifiers = const
dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
@@ -105,7 +105,7 @@ dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
-dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
+dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
diff --git a/BepInEx.Core/BepInEx.Core.csproj b/BepInEx.Core/BepInEx.Core.csproj
index ebb624c..7bdbe29 100644
--- a/BepInEx.Core/BepInEx.Core.csproj
+++ b/BepInEx.Core/BepInEx.Core.csproj
@@ -5,7 +5,7 @@
trueBepInExBepInEx
- 9
+ 10..\bin\Core\$(OutputPath)\$(TargetFramework)\BepInEx.Core.xmlUnity plugin injection framework
diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs
index 459ed07..37c2fa6 100644
--- a/BepInEx.Core/Bootstrap/BaseChainloader.cs
+++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs
@@ -10,403 +10,402 @@ using BepInEx.Configuration;
using BepInEx.Logging;
using Mono.Cecil;
-namespace BepInEx.Bootstrap
+namespace BepInEx.Bootstrap;
+
+public abstract class BaseChainloader
{
- public abstract class BaseChainloader
+ protected static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
+ protected static readonly Version CurrentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
+
+ private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$");
+
+ ///
+ /// Analyzes the given type definition and attempts to convert it to a valid
+ ///
+ /// Type definition to analyze.
+ /// The filepath of the assembly, to keep as metadata.
+ /// If the type represent a valid plugin, returns a instance. Otherwise, return null.
+ public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocation)
{
- protected static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
- protected static readonly Version CurrentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
+ if (type.IsInterface || type.IsAbstract)
+ return null;
- private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$");
-
- ///
- /// Analyzes the given type definition and attempts to convert it to a valid
- ///
- /// Type definition to analyze.
- /// The filepath of the assembly, to keep as metadata.
- /// If the type represent a valid plugin, returns a instance. Otherwise, return null.
- public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocation)
+ try
{
- if (type.IsInterface || type.IsAbstract)
+ if (!type.IsSubtypeOf(typeof(TPlugin)))
return null;
-
- try
- {
- if (!type.IsSubtypeOf(typeof(TPlugin)))
- return null;
- }
- catch (AssemblyResolutionException)
- {
- // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin.
- return null;
- }
-
- var metadata = BepInPlugin.FromCecilType(type);
-
- // Perform checks that will prevent the plugin from being loaded in ALL cases
- if (metadata == null)
- {
- Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
- return null;
- }
-
- if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
- {
- Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format.");
- return null;
- }
-
- if (metadata.Version == null)
- {
- Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid.");
- return null;
- }
-
- if (metadata.Name == null)
- {
- Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null.");
- return null;
- }
-
- var filters = BepInProcess.FromCecilType(type);
- var dependencies = BepInDependency.FromCecilType(type);
- var incompatibilities = BepInIncompatibility.FromCecilType(type);
-
- var bepinVersion =
- type.Module.AssemblyReferences.FirstOrDefault(reference => reference.Name == "BepInEx.Core")?.Version ??
- new Version();
-
- return new PluginInfo
- {
- Metadata = metadata,
- Processes = filters,
- Dependencies = dependencies,
- Incompatibilities = incompatibilities,
- TypeName = type.FullName,
- TargettedBepInExVersion = bepinVersion,
- Location = assemblyLocation
- };
+ }
+ catch (AssemblyResolutionException)
+ {
+ // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin.
+ return null;
}
- protected static bool HasBepinPlugins(AssemblyDefinition ass)
- {
- if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName))
- return false;
- if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BepInPlugin).FullName))
- return false;
+ var metadata = BepInPlugin.FromCecilType(type);
- return true;
+ // Perform checks that will prevent the plugin from being loaded in ALL cases
+ if (metadata == null)
+ {
+ Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
+ return null;
}
- protected static bool PluginTargetsWrongBepin(PluginInfo pluginInfo)
+ if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
{
- var pluginTarget = pluginInfo.TargettedBepInExVersion;
- // X.X.X.x - compare normally. x.x.x.X - nightly build number, ignore
- if (pluginTarget.Major != CurrentAssemblyVersion.Major) return true;
- if (pluginTarget.Minor > CurrentAssemblyVersion.Minor) return true;
- if (pluginTarget.Minor < CurrentAssemblyVersion.Minor) return false;
- return pluginTarget.Build > CurrentAssemblyVersion.Build;
+ Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format.");
+ return null;
}
- #region Contract
-
- protected virtual string ConsoleTitle => $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}";
-
- private bool _initialized;
-
- ///
- /// List of all instances loaded via the chainloader.
- ///
- public Dictionary Plugins { get; } = new();
-
- ///
- /// Collection of error chainloader messages that occured during plugin loading.
- /// Contains information about what certain plugins were not loaded.
- ///
- public List DependencyErrors { get; } = new();
-
- public virtual void Initialize(string gameExePath = null)
+ if (metadata.Version == null)
{
- if (_initialized)
- throw new InvalidOperationException("Chainloader cannot be initialized multiple times");
-
- // Set vitals
- if (gameExePath != null)
- // Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize
- // This is used by Preloader to use environment variables, for example
- Paths.SetExecutablePath(gameExePath);
-
- InitializeLoggers();
-
- if (!Directory.Exists(Paths.PluginPath))
- Directory.CreateDirectory(Paths.PluginPath);
-
- if (!Directory.Exists(Paths.PatcherPluginPath))
- Directory.CreateDirectory(Paths.PatcherPluginPath);
-
- _initialized = true;
-
- Logger.LogMessage("Chainloader initialized");
+ Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid.");
+ return null;
}
- protected virtual void InitializeLoggers()
+ if (metadata.Name == null)
{
- if (ConsoleManager.ConsoleEnabled && !ConsoleManager.ConsoleActive)
- ConsoleManager.CreateConsole();
+ Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null.");
+ return null;
+ }
- if (ConsoleManager.ConsoleActive)
+ var filters = BepInProcess.FromCecilType(type);
+ var dependencies = BepInDependency.FromCecilType(type);
+ var incompatibilities = BepInIncompatibility.FromCecilType(type);
+
+ var bepinVersion =
+ type.Module.AssemblyReferences.FirstOrDefault(reference => reference.Name == "BepInEx.Core")?.Version ??
+ new Version();
+
+ return new PluginInfo
+ {
+ Metadata = metadata,
+ Processes = filters,
+ Dependencies = dependencies,
+ Incompatibilities = incompatibilities,
+ TypeName = type.FullName,
+ TargettedBepInExVersion = bepinVersion,
+ Location = assemblyLocation
+ };
+ }
+
+ protected static bool HasBepinPlugins(AssemblyDefinition ass)
+ {
+ if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName))
+ return false;
+ if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BepInPlugin).FullName))
+ return false;
+
+ return true;
+ }
+
+ protected static bool PluginTargetsWrongBepin(PluginInfo pluginInfo)
+ {
+ var pluginTarget = pluginInfo.TargettedBepInExVersion;
+ // X.X.X.x - compare normally. x.x.x.X - nightly build number, ignore
+ if (pluginTarget.Major != CurrentAssemblyVersion.Major) return true;
+ if (pluginTarget.Minor > CurrentAssemblyVersion.Minor) return true;
+ if (pluginTarget.Minor < CurrentAssemblyVersion.Minor) return false;
+ return pluginTarget.Build > CurrentAssemblyVersion.Build;
+ }
+
+ #region Contract
+
+ protected virtual string ConsoleTitle => $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}";
+
+ private bool _initialized;
+
+ ///
+ /// List of all instances loaded via the chainloader.
+ ///
+ public Dictionary Plugins { get; } = new();
+
+ ///
+ /// Collection of error chainloader messages that occured during plugin loading.
+ /// Contains information about what certain plugins were not loaded.
+ ///
+ public List DependencyErrors { get; } = new();
+
+ public virtual void Initialize(string gameExePath = null)
+ {
+ if (_initialized)
+ throw new InvalidOperationException("Chainloader cannot be initialized multiple times");
+
+ // Set vitals
+ if (gameExePath != null)
+ // Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize
+ // This is used by Preloader to use environment variables, for example
+ Paths.SetExecutablePath(gameExePath);
+
+ InitializeLoggers();
+
+ if (!Directory.Exists(Paths.PluginPath))
+ Directory.CreateDirectory(Paths.PluginPath);
+
+ if (!Directory.Exists(Paths.PatcherPluginPath))
+ Directory.CreateDirectory(Paths.PatcherPluginPath);
+
+ _initialized = true;
+
+ Logger.LogMessage("Chainloader initialized");
+ }
+
+ protected virtual void InitializeLoggers()
+ {
+ if (ConsoleManager.ConsoleEnabled && !ConsoleManager.ConsoleActive)
+ ConsoleManager.CreateConsole();
+
+ if (ConsoleManager.ConsoleActive)
+ {
+ if (!Logger.Listeners.Any(x => x is ConsoleLogListener))
+ Logger.Listeners.Add(new ConsoleLogListener());
+
+ ConsoleManager.SetConsoleTitle(ConsoleTitle);
+ }
+
+ if (ConfigDiskLogging.Value)
+ Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskLoggingDisplayedLevel.Value,
+ ConfigDiskAppend.Value, ConfigDiskLoggingInstantFlushing.Value,
+ ConfigDiskLoggingFileLimit.Value));
+
+ if (!TraceLogSource.IsListening)
+ Logger.Sources.Add(TraceLogSource.CreateSource());
+
+ if (!Logger.Sources.Any(x => x is HarmonyLogSource))
+ Logger.Sources.Add(new HarmonyLogSource());
+ }
+
+ protected virtual IList DiscoverPlugins()
+ {
+ var pluginsToLoad =
+ TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
+
+ return pluginsToLoad.SelectMany(p => p.Value).ToList();
+ }
+
+ protected virtual IList ModifyLoadOrder(IList plugins)
+ {
+ // We use a sorted dictionary to ensure consistent load order
+ var dependencyDict =
+ new SortedDictionary>(StringComparer.InvariantCultureIgnoreCase);
+ var pluginsByGuid = new Dictionary();
+
+ foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID))
+ {
+ PluginInfo loadedVersion = null;
+ foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
{
- if (!Logger.Listeners.Any(x => x is ConsoleLogListener))
- Logger.Listeners.Add(new ConsoleLogListener());
-
- ConsoleManager.SetConsoleTitle(ConsoleTitle);
- }
-
- if (ConfigDiskLogging.Value)
- Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskLoggingDisplayedLevel.Value,
- ConfigDiskAppend.Value, ConfigDiskLoggingInstantFlushing.Value,
- ConfigDiskLoggingFileLimit.Value));
-
- if (!TraceLogSource.IsListening)
- Logger.Sources.Add(TraceLogSource.CreateSource());
-
- if (!Logger.Sources.Any(x => x is HarmonyLogSource))
- Logger.Sources.Add(new HarmonyLogSource());
- }
-
- protected virtual IList DiscoverPlugins()
- {
- var pluginsToLoad =
- TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
-
- return pluginsToLoad.SelectMany(p => p.Value).ToList();
- }
-
- protected virtual IList ModifyLoadOrder(IList plugins)
- {
- // We use a sorted dictionary to ensure consistent load order
- var dependencyDict =
- new SortedDictionary>(StringComparer.InvariantCultureIgnoreCase);
- var pluginsByGuid = new Dictionary();
-
- foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID))
- {
- PluginInfo loadedVersion = null;
- foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
+ if (loadedVersion != null)
{
- if (loadedVersion != null)
+ Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})");
+ continue;
+ }
+
+ // Perform checks that will prevent loading plugins in this run
+ var filters = pluginInfo.Processes.ToList();
+ var invalidProcessName = filters.Count != 0 &&
+ filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""),
+ Paths.ProcessName,
+ StringComparison
+ .InvariantCultureIgnoreCase));
+
+ if (invalidProcessName)
+ {
+ Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})");
+ continue;
+ }
+
+ loadedVersion = pluginInfo;
+ dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID);
+ pluginsByGuid[pluginInfo.Metadata.GUID] = pluginInfo;
+ }
+ }
+
+ foreach (var pluginInfo in pluginsByGuid.Values.ToList())
+ if (pluginInfo.Incompatibilities.Any(incompatibility =>
+ pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID))
+ )
+ {
+ pluginsByGuid.Remove(pluginInfo.Metadata.GUID);
+ dependencyDict.Remove(pluginInfo.Metadata.GUID);
+
+ var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
+ .Where(x => pluginsByGuid.ContainsKey(x)).ToArray();
+ var message =
+ $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
+ DependencyErrors.Add(message);
+ Logger.LogError(message);
+ }
+ else if (PluginTargetsWrongBepin(pluginInfo))
+ {
+ var message =
+ $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update";
+ DependencyErrors.Add(message);
+ Logger.LogWarning(message);
+ }
+
+ var emptyDependencies = new string[0];
+
+ // Sort plugins by their dependencies.
+ // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list.
+ var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys,
+ x =>
+ dependencyDict.TryGetValue(x, out var deps)
+ ? deps
+ : emptyDependencies).ToList();
+
+ return sortedPlugins.Where(pluginsByGuid.ContainsKey).Select(x => pluginsByGuid[x]).ToList();
+ }
+
+ public virtual void Execute()
+ {
+ try
+ {
+ var plugins = DiscoverPlugins();
+
+ Logger.LogInfo($"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load");
+
+ var sortedPlugins = ModifyLoadOrder(plugins);
+
+ var invalidPlugins = new HashSet();
+ var processedPlugins = new Dictionary();
+ var loadedAssemblies = new Dictionary();
+
+ foreach (var plugin in sortedPlugins)
+ {
+ var dependsOnInvalidPlugin = false;
+ var missingDependencies = new List();
+ foreach (var dependency in plugin.Dependencies)
+ {
+ static bool IsHardDependency(BepInDependency dep) =>
+ (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
+
+ // If the dependency wasn't already processed, it's missing altogether
+ var dependencyExists =
+ processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
+ if (!dependencyExists || dependency.VersionRange != null &&
+ !dependency.VersionRange.IsSatisfied(pluginVersion))
{
- Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})");
+ // If the dependency is hard, collect it into a list to show
+ if (IsHardDependency(dependency))
+ missingDependencies.Add(dependency);
continue;
}
- // Perform checks that will prevent loading plugins in this run
- var filters = pluginInfo.Processes.ToList();
- var invalidProcessName = filters.Count != 0 &&
- filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""),
- Paths.ProcessName,
- StringComparison
- .InvariantCultureIgnoreCase));
-
- if (invalidProcessName)
+ // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
+ if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
{
- Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})");
- continue;
+ dependsOnInvalidPlugin = true;
+ break;
}
-
- loadedVersion = pluginInfo;
- dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID);
- pluginsByGuid[pluginInfo.Metadata.GUID] = pluginInfo;
}
- }
- foreach (var pluginInfo in pluginsByGuid.Values.ToList())
- if (pluginInfo.Incompatibilities.Any(incompatibility =>
- pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID))
- )
- {
- pluginsByGuid.Remove(pluginInfo.Metadata.GUID);
- dependencyDict.Remove(pluginInfo.Metadata.GUID);
+ processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version);
- var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID)
- .Where(x => pluginsByGuid.ContainsKey(x)).ToArray();
- var message =
- $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
- DependencyErrors.Add(message);
- Logger.LogError(message);
- }
- else if (PluginTargetsWrongBepin(pluginInfo))
+ if (dependsOnInvalidPlugin)
{
var message =
- $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update";
+ $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
DependencyErrors.Add(message);
Logger.LogWarning(message);
+ continue;
}
- var emptyDependencies = new string[0];
-
- // Sort plugins by their dependencies.
- // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list.
- var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys,
- x =>
- dependencyDict.TryGetValue(x, out var deps)
- ? deps
- : emptyDependencies).ToList();
-
- return sortedPlugins.Where(pluginsByGuid.ContainsKey).Select(x => pluginsByGuid[x]).ToList();
- }
-
- public virtual void Execute()
- {
- try
- {
- var plugins = DiscoverPlugins();
-
- Logger.LogInfo($"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load");
-
- var sortedPlugins = ModifyLoadOrder(plugins);
-
- var invalidPlugins = new HashSet();
- var processedPlugins = new Dictionary();
- var loadedAssemblies = new Dictionary();
-
- foreach (var plugin in sortedPlugins)
+ if (missingDependencies.Count != 0)
{
- var dependsOnInvalidPlugin = false;
- var missingDependencies = new List();
- foreach (var dependency in plugin.Dependencies)
- {
- static bool IsHardDependency(BepInDependency dep) =>
- (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
+ var message = $@"Could not load [{plugin}] because it has missing dependencies: {
+ string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
+ }";
+ DependencyErrors.Add(message);
+ Logger.LogError(message);
- // If the dependency wasn't already processed, it's missing altogether
- var dependencyExists =
- processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
- if (!dependencyExists || dependency.VersionRange != null &&
- !dependency.VersionRange.IsSatisfied(pluginVersion))
- {
- // If the dependency is hard, collect it into a list to show
- if (IsHardDependency(dependency))
- missingDependencies.Add(dependency);
- continue;
- }
-
- // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user
- if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
- {
- dependsOnInvalidPlugin = true;
- break;
- }
- }
-
- processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version);
-
- if (dependsOnInvalidPlugin)
- {
- var message =
- $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details.";
- DependencyErrors.Add(message);
- Logger.LogWarning(message);
- continue;
- }
-
- if (missingDependencies.Count != 0)
- {
- var message = $@"Could not load [{plugin}] because it has missing dependencies: {
- string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray())
- }";
- DependencyErrors.Add(message);
- Logger.LogError(message);
-
- invalidPlugins.Add(plugin.Metadata.GUID);
- continue;
- }
-
- try
- {
- Logger.LogInfo($"Loading [{plugin}]");
-
- if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass))
- loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location);
-
- Plugins[plugin.Metadata.GUID] = plugin;
- TryRunModuleCtor(plugin, ass);
- plugin.Instance = LoadPlugin(plugin, ass);
-
- //_plugins.Add((TPlugin)plugin.Instance);
- }
- catch (Exception ex)
- {
- invalidPlugins.Add(plugin.Metadata.GUID);
- Plugins.Remove(plugin.Metadata.GUID);
-
- Logger.LogError($"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex)}");
- }
+ invalidPlugins.Add(plugin.Metadata.GUID);
+ continue;
}
- }
- catch (Exception ex)
- {
+
try
{
- ConsoleManager.CreateConsole();
+ Logger.LogInfo($"Loading [{plugin}]");
+
+ if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass))
+ loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location);
+
+ Plugins[plugin.Metadata.GUID] = plugin;
+ TryRunModuleCtor(plugin, ass);
+ plugin.Instance = LoadPlugin(plugin, ass);
+
+ //_plugins.Add((TPlugin)plugin.Instance);
}
- catch { }
+ catch (Exception ex)
+ {
+ invalidPlugins.Add(plugin.Metadata.GUID);
+ Plugins.Remove(plugin.Metadata.GUID);
- Logger.LogError("Error occurred starting the game: " + ex);
+ Logger.LogError($"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex)}");
+ }
}
-
- Logger.LogMessage("Chainloader startup complete");
}
-
- private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly)
+ catch (Exception ex)
{
try
{
- RuntimeHelpers.RunModuleConstructor(assembly.GetType(plugin.TypeName).Module.ModuleHandle);
- }
- catch (Exception e)
- {
- Logger.LogWarning($"Couldn't run Module constructor for {assembly.FullName}::{plugin.TypeName}: {e}");
+ ConsoleManager.CreateConsole();
}
+ catch { }
+
+ Logger.LogError("Error occurred starting the game: " + ex);
}
- public abstract TPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly);
-
- #endregion
-
- #region Config
-
- private static readonly ConfigEntry ConfigDiskAppend = ConfigFile.CoreConfig.Bind(
- "Logging.Disk", "AppendLog",
- false,
- "Appends to the log file instead of overwriting, on game startup.");
-
- private static readonly ConfigEntry ConfigDiskLogging = ConfigFile.CoreConfig.Bind(
- "Logging.Disk", "Enabled",
- true,
- "Enables writing log messages to disk.");
-
- private static readonly ConfigEntry ConfigDiskLoggingDisplayedLevel = ConfigFile.CoreConfig.Bind(
- "Logging.Disk", "LogLevels",
- LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
- "Only displays the specified log levels in the disk log output.");
-
- private static readonly ConfigEntry ConfigDiskLoggingInstantFlushing = ConfigFile.CoreConfig.Bind(
- "Logging.Disk", "InstantFlushing",
- false,
- new StringBuilder()
- .AppendLine("If true, instantly writes any received log entries to disk.")
- .AppendLine("This incurs a major performance hit if a lot of log messages are being written, however it is really useful for debugging crashes.")
- .ToString());
-
- private static readonly ConfigEntry ConfigDiskLoggingFileLimit = ConfigFile.CoreConfig.Bind(
- "Logging.Disk", "ConcurrentFileLimit",
- 5,
- new StringBuilder()
- .AppendLine("The maximum amount of concurrent log files that will be written to disk.")
- .AppendLine("As one log file is used per open game instance, you may find it necessary to increase this limit when debugging multiple instances at the same time.")
- .ToString());
-
- #endregion
+ Logger.LogMessage("Chainloader startup complete");
}
+
+ private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly)
+ {
+ try
+ {
+ RuntimeHelpers.RunModuleConstructor(assembly.GetType(plugin.TypeName).Module.ModuleHandle);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning($"Couldn't run Module constructor for {assembly.FullName}::{plugin.TypeName}: {e}");
+ }
+ }
+
+ public abstract TPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly);
+
+ #endregion
+
+ #region Config
+
+ private static readonly ConfigEntry ConfigDiskAppend = ConfigFile.CoreConfig.Bind(
+ "Logging.Disk", "AppendLog",
+ false,
+ "Appends to the log file instead of overwriting, on game startup.");
+
+ private static readonly ConfigEntry ConfigDiskLogging = ConfigFile.CoreConfig.Bind(
+ "Logging.Disk", "Enabled",
+ true,
+ "Enables writing log messages to disk.");
+
+ private static readonly ConfigEntry ConfigDiskLoggingDisplayedLevel = ConfigFile.CoreConfig.Bind(
+ "Logging.Disk", "LogLevels",
+ LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info,
+ "Only displays the specified log levels in the disk log output.");
+
+ private static readonly ConfigEntry ConfigDiskLoggingInstantFlushing = ConfigFile.CoreConfig.Bind(
+ "Logging.Disk", "InstantFlushing",
+ false,
+ new StringBuilder()
+ .AppendLine("If true, instantly writes any received log entries to disk.")
+ .AppendLine("This incurs a major performance hit if a lot of log messages are being written, however it is really useful for debugging crashes.")
+ .ToString());
+
+ private static readonly ConfigEntry ConfigDiskLoggingFileLimit = ConfigFile.CoreConfig.Bind(
+ "Logging.Disk", "ConcurrentFileLimit",
+ 5,
+ new StringBuilder()
+ .AppendLine("The maximum amount of concurrent log files that will be written to disk.")
+ .AppendLine("As one log file is used per open game instance, you may find it necessary to increase this limit when debugging multiple instances at the same time.")
+ .ToString());
+
+ #endregion
}
diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs
index 5baa246..368a5b2 100644
--- a/BepInEx.Core/Bootstrap/TypeLoader.cs
+++ b/BepInEx.Core/Bootstrap/TypeLoader.cs
@@ -8,305 +8,304 @@ using BepInEx.Configuration;
using BepInEx.Logging;
using Mono.Cecil;
-namespace BepInEx.Bootstrap
+namespace BepInEx.Bootstrap;
+
+///
+/// A cacheable metadata item. Can be used with and
+/// to cache plugin metadata.
+///
+public interface ICacheable
{
///
- /// A cacheable metadata item. Can be used with and
- /// to cache plugin metadata.
+ /// Serialize the object into a binary format.
///
- public interface ICacheable
- {
- ///
- /// Serialize the object into a binary format.
- ///
- ///
- void Save(BinaryWriter bw);
-
- ///
- /// Loads the object from binary format.
- ///
- ///
- void Load(BinaryReader br);
- }
+ ///
+ void Save(BinaryWriter bw);
///
- /// A cached assembly.
+ /// Loads the object from binary format.
///
- ///
- public class CachedAssembly where T : ICacheable
- {
- ///
- /// List of cached items inside the assembly.
- ///
- public List CacheItems { get; set; }
+ ///
+ void Load(BinaryReader br);
+}
- ///
- /// Hash of the assembly. Used to verify that the assembly hasn't been changed.
- ///
- public string Hash { get; set; }
- }
+///
+/// A cached assembly.
+///
+///
+public class CachedAssembly where T : ICacheable
+{
+ ///
+ /// List of cached items inside the assembly.
+ ///
+ public List CacheItems { get; set; }
///
- /// Provides methods for loading specified types from an assembly.
+ /// Hash of the assembly. Used to verify that the assembly hasn't been changed.
///
- public static class TypeLoader
+ public string Hash { get; set; }
+}
+
+///
+/// Provides methods for loading specified types from an assembly.
+///
+public static class TypeLoader
+{
+ ///
+ /// Default assembly resolved used by the
+ ///
+ public static readonly DefaultAssemblyResolver CecilResolver;
+
+ ///
+ /// Default reader parameters used by
+ ///
+ public static readonly ReaderParameters ReaderParameters;
+
+ public static HashSet SearchDirectories = new();
+
+ #region Config
+
+ private static readonly ConfigEntry EnableAssemblyCache = ConfigFile.CoreConfig.Bind(
+ "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
+
+ static TypeLoader()
{
- ///
- /// Default assembly resolved used by the
- ///
- public static readonly DefaultAssemblyResolver CecilResolver;
+ CecilResolver = new DefaultAssemblyResolver();
+ ReaderParameters = new ReaderParameters { AssemblyResolver = CecilResolver };
- ///
- /// Default reader parameters used by
- ///
- public static readonly ReaderParameters ReaderParameters;
+ CecilResolver.ResolveFailure += CecilResolveOnFailure;
+ }
- public static HashSet SearchDirectories = new();
+ public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNameReference reference)
+ {
+ if (!Utility.TryParseAssemblyName(reference.FullName, out var name))
+ return null;
- #region Config
-
- private static readonly ConfigEntry EnableAssemblyCache = ConfigFile.CoreConfig.Bind(
- "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
-
- static TypeLoader()
+ var resolveDirs = new[]
{
- CecilResolver = new DefaultAssemblyResolver();
- ReaderParameters = new ReaderParameters { AssemblyResolver = CecilResolver };
+ Paths.BepInExAssemblyDirectory,
+ Paths.PluginPath,
+ Paths.PatcherPluginPath,
+ Paths.ManagedPath
+ }.Concat(SearchDirectories);
- CecilResolver.ResolveFailure += CecilResolveOnFailure;
- }
-
- public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNameReference reference)
+ foreach (var dir in resolveDirs)
{
- if (!Utility.TryParseAssemblyName(reference.FullName, out var name))
- return null;
-
- var resolveDirs = new[]
+ if (!Directory.Exists(dir))
{
- Paths.BepInExAssemblyDirectory,
- Paths.PluginPath,
- Paths.PatcherPluginPath,
- Paths.ManagedPath,
- }.Concat(SearchDirectories);
-
- foreach (var dir in resolveDirs)
- {
- if (!Directory.Exists(dir))
- {
- Logger.LogDebug($"Unable to resolve cecil search directory '{dir}'");
- continue;
- }
-
- if (Utility.TryResolveDllAssembly(name, dir, ReaderParameters, out var assembly))
- return assembly;
+ Logger.LogDebug($"Unable to resolve cecil search directory '{dir}'");
+ continue;
}
- return AssemblyResolve?.Invoke(sender, reference);
+ if (Utility.TryResolveDllAssembly(name, dir, ReaderParameters, out var assembly))
+ return assembly;
}
- ///
- /// Event fired when fails to resolve a type during type loading.
- ///
- public static event AssemblyResolveEventHandler AssemblyResolve;
+ return AssemblyResolve?.Invoke(sender, reference);
+ }
- ///
- /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata.
- ///
- /// The specific base type to search for.
- /// The directory to search for assemblies.
- /// A function to check if a type should be selected and to build the type metadata.
- /// A filter function to quickly determine if the assembly can be loaded.
- /// The name of the cache to get cached types from.
- ///
- /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the
- /// selector.
- ///
- public static Dictionary> FindPluginTypes(string directory,
- Func typeSelector,
- Func assemblyFilter =
- null,
- string cacheName = null)
- where T : ICacheable, new()
- {
- var result = new Dictionary>();
- var hashes = new Dictionary();
- Dictionary> cache = null;
+ ///
+ /// Event fired when fails to resolve a type during type loading.
+ ///
+ public static event AssemblyResolveEventHandler AssemblyResolve;
- if (cacheName != null)
- cache = LoadAssemblyCache(cacheName);
+ ///
+ /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata.
+ ///
+ /// The specific base type to search for.
+ /// The directory to search for assemblies.
+ /// A function to check if a type should be selected and to build the type metadata.
+ /// A filter function to quickly determine if the assembly can be loaded.
+ /// The name of the cache to get cached types from.
+ ///
+ /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the
+ /// selector.
+ ///
+ public static Dictionary> FindPluginTypes(string directory,
+ Func typeSelector,
+ Func assemblyFilter =
+ null,
+ string cacheName = null)
+ where T : ICacheable, new()
+ {
+ var result = new Dictionary>();
+ var hashes = new Dictionary();
+ Dictionary> cache = null;
- foreach (var dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories))
- try
- {
- using var dllMs = new MemoryStream(File.ReadAllBytes(dll));
- var hash = Utility.HashStream(dllMs);
- hashes[dll] = hash;
- dllMs.Position = 0;
- if (cache != null && cache.TryGetValue(dll, out var cacheEntry))
- if (hash == cacheEntry.Hash)
- {
- result[dll] = cacheEntry.CacheItems;
- continue;
- }
+ if (cacheName != null)
+ cache = LoadAssemblyCache(cacheName);
- using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters);
- Logger.LogDebug($"Examining '{dll}'");
-
- if (!assemblyFilter?.Invoke(ass) ?? false)
+ foreach (var dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories))
+ try
+ {
+ using var dllMs = new MemoryStream(File.ReadAllBytes(dll));
+ var hash = Utility.HashStream(dllMs);
+ hashes[dll] = hash;
+ dllMs.Position = 0;
+ if (cache != null && cache.TryGetValue(dll, out var cacheEntry))
+ if (hash == cacheEntry.Hash)
{
- result[dll] = new List();
+ result[dll] = cacheEntry.CacheItems;
continue;
}
- var matches = ass.MainModule.Types
- .Select(t => typeSelector(t, dll))
- .Where(t => t != null).ToList();
- result[dll] = matches;
- }
- catch (BadImageFormatException e)
+ using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters);
+ Logger.LogDebug($"Examining '{dll}'");
+
+ if (!assemblyFilter?.Invoke(ass) ?? false)
{
- Logger.LogDebug($"Skipping loading {dll} because it's not a valid .NET assembly. Full error: {e.Message}");
- }
- catch (Exception e)
- {
- Logger.LogError(e.ToString());
+ result[dll] = new List();
+ continue;
}
- if (cacheName != null)
- SaveAssemblyCache(cacheName, result, hashes);
+ var matches = ass.MainModule.Types
+ .Select(t => typeSelector(t, dll))
+ .Where(t => t != null).ToList();
+ result[dll] = matches;
+ }
+ catch (BadImageFormatException e)
+ {
+ Logger.LogDebug($"Skipping loading {dll} because it's not a valid .NET assembly. Full error: {e.Message}");
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e.ToString());
+ }
- return result;
- }
+ if (cacheName != null)
+ SaveAssemblyCache(cacheName, result, hashes);
- ///
- /// Loads an index of type metadatas from a cache.
- ///
- /// Name of the cache
- /// Cacheable item
- ///
- /// Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined,
- /// return null.
- ///
- public static Dictionary> LoadAssemblyCache(string cacheName)
- where T : ICacheable, new()
+ return result;
+ }
+
+ ///
+ /// Loads an index of type metadatas from a cache.
+ ///
+ /// Name of the cache
+ /// Cacheable item
+ ///
+ /// Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined,
+ /// return null.
+ ///
+ public static Dictionary> LoadAssemblyCache(string cacheName)
+ where T : ICacheable, new()
+ {
+ if (!EnableAssemblyCache.Value)
+ return null;
+
+ var result = new Dictionary>();
+ try
{
- if (!EnableAssemblyCache.Value)
+ var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
+ if (!File.Exists(path))
return null;
- var result = new Dictionary>();
- try
+ using (var br = new BinaryReader(File.OpenRead(path)))
{
- var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
- if (!File.Exists(path))
- return null;
+ var entriesCount = br.ReadInt32();
- using (var br = new BinaryReader(File.OpenRead(path)))
+ for (var i = 0; i < entriesCount; i++)
{
- var entriesCount = br.ReadInt32();
+ var entryIdentifier = br.ReadString();
+ var hash = br.ReadString();
+ var itemsCount = br.ReadInt32();
+ var items = new List();
- for (var i = 0; i < entriesCount; i++)
+ for (var j = 0; j < itemsCount; j++)
{
- var entryIdentifier = br.ReadString();
- var hash = br.ReadString();
- var itemsCount = br.ReadInt32();
- var items = new List();
-
- for (var j = 0; j < itemsCount; j++)
- {
- var entry = new T();
- entry.Load(br);
- items.Add(entry);
- }
-
- result[entryIdentifier] = new CachedAssembly { Hash = hash, CacheItems = items };
+ var entry = new T();
+ entry.Load(br);
+ items.Add(entry);
}
+
+ result[entryIdentifier] = new CachedAssembly { Hash = hash, CacheItems = items };
}
}
- catch (Exception e)
- {
- Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}.");
- }
-
- return result;
- }
-
- ///
- /// Saves indexed type metadata into a cache.
- ///
- /// Name of the cache
- /// List of plugin metadatas indexed by the path to the assembly that contains the types
- /// Hash values that can be used for checking similarity between cached and live assembly
- /// Cacheable item
- public static void SaveAssemblyCache(string cacheName,
- Dictionary> entries,
- Dictionary hashes)
- where T : ICacheable
- {
- if (!EnableAssemblyCache.Value)
- return;
-
- try
- {
- if (!Directory.Exists(Paths.CachePath))
- Directory.CreateDirectory(Paths.CachePath);
-
- var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
-
- using var bw = new BinaryWriter(File.OpenWrite(path));
- bw.Write(entries.Count);
-
- foreach (var kv in entries)
- {
- bw.Write(kv.Key);
- bw.Write(hashes.TryGetValue(kv.Key, out var hash) ? hash : "");
- bw.Write(kv.Value.Count);
-
- foreach (var item in kv.Value)
- item.Save(bw);
- }
- }
- catch (Exception e)
- {
- Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}.");
- }
}
-
- ///
- /// Converts TypeLoadException to a readable string.
- ///
- /// TypeLoadException
- /// Readable representation of the exception
- public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex)
+ catch (Exception e)
{
- var sb = new StringBuilder();
- foreach (var exSub in ex.LoaderExceptions)
+ Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}.");
+ }
+
+ return result;
+ }
+
+ ///
+ /// Saves indexed type metadata into a cache.
+ ///
+ /// Name of the cache
+ /// List of plugin metadatas indexed by the path to the assembly that contains the types
+ /// Hash values that can be used for checking similarity between cached and live assembly
+ /// Cacheable item
+ public static void SaveAssemblyCache(string cacheName,
+ Dictionary> entries,
+ Dictionary hashes)
+ where T : ICacheable
+ {
+ if (!EnableAssemblyCache.Value)
+ return;
+
+ try
+ {
+ if (!Directory.Exists(Paths.CachePath))
+ Directory.CreateDirectory(Paths.CachePath);
+
+ var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat");
+
+ using var bw = new BinaryWriter(File.OpenWrite(path));
+ bw.Write(entries.Count);
+
+ foreach (var kv in entries)
{
- sb.AppendLine(exSub.Message);
- if (exSub is FileNotFoundException exFileNotFound)
- {
- if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
- {
- sb.AppendLine("Fusion Log:");
- sb.AppendLine(exFileNotFound.FusionLog);
- }
- }
- else if (exSub is FileLoadException exLoad)
- {
- if (!string.IsNullOrEmpty(exLoad.FusionLog))
- {
- sb.AppendLine("Fusion Log:");
- sb.AppendLine(exLoad.FusionLog);
- }
- }
+ bw.Write(kv.Key);
+ bw.Write(hashes.TryGetValue(kv.Key, out var hash) ? hash : "");
+ bw.Write(kv.Value.Count);
- sb.AppendLine();
+ foreach (var item in kv.Value)
+ item.Save(bw);
}
-
- return sb.ToString();
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}.");
}
}
+
+ ///
+ /// Converts TypeLoadException to a readable string.
+ ///
+ /// TypeLoadException
+ /// Readable representation of the exception
+ public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex)
+ {
+ var sb = new StringBuilder();
+ foreach (var exSub in ex.LoaderExceptions)
+ {
+ sb.AppendLine(exSub.Message);
+ if (exSub is FileNotFoundException exFileNotFound)
+ {
+ if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
+ {
+ sb.AppendLine("Fusion Log:");
+ sb.AppendLine(exFileNotFound.FusionLog);
+ }
+ }
+ else if (exSub is FileLoadException exLoad)
+ {
+ if (!string.IsNullOrEmpty(exLoad.FusionLog))
+ {
+ sb.AppendLine("Fusion Log:");
+ sb.AppendLine(exLoad.FusionLog);
+ }
+ }
+
+ sb.AppendLine();
+ }
+
+ return sb.ToString();
+ }
}
diff --git a/BepInEx.Core/Configuration/AcceptableValueBase.cs b/BepInEx.Core/Configuration/AcceptableValueBase.cs
index 810d892..e48354a 100644
--- a/BepInEx.Core/Configuration/AcceptableValueBase.cs
+++ b/BepInEx.Core/Configuration/AcceptableValueBase.cs
@@ -1,36 +1,35 @@
using System;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Base type of all classes representing and enforcing acceptable values of config settings.
+///
+public abstract class AcceptableValueBase
{
- ///
- /// Base type of all classes representing and enforcing acceptable values of config settings.
- ///
- public abstract class AcceptableValueBase
+ /// Type of values that this class can Clamp.
+ protected AcceptableValueBase(Type valueType)
{
- /// Type of values that this class can Clamp.
- protected AcceptableValueBase(Type valueType)
- {
- ValueType = valueType;
- }
-
- ///
- /// Type of the supported values.
- ///
- public Type ValueType { get; }
-
- ///
- /// Change the value to be acceptable, if it's not already.
- ///
- public abstract object Clamp(object value);
-
- ///
- /// Check if the value is an acceptable value.
- ///
- public abstract bool IsValid(object value);
-
- ///
- /// Get the string for use in config files.
- ///
- public abstract string ToDescriptionString();
+ ValueType = valueType;
}
+
+ ///
+ /// Type of the supported values.
+ ///
+ public Type ValueType { get; }
+
+ ///
+ /// Change the value to be acceptable, if it's not already.
+ ///
+ public abstract object Clamp(object value);
+
+ ///
+ /// Check if the value is an acceptable value.
+ ///
+ public abstract bool IsValid(object value);
+
+ ///
+ /// Get the string for use in config files.
+ ///
+ public abstract string ToDescriptionString();
}
diff --git a/BepInEx.Core/Configuration/AcceptableValueList.cs b/BepInEx.Core/Configuration/AcceptableValueList.cs
index 488a2b9..5770a98 100644
--- a/BepInEx.Core/Configuration/AcceptableValueList.cs
+++ b/BepInEx.Core/Configuration/AcceptableValueList.cs
@@ -1,45 +1,44 @@
using System;
using System.Linq;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Specify the list of acceptable values for a setting.
+///
+public class AcceptableValueList : AcceptableValueBase where T : IEquatable
{
///
/// 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.
///
- public class AcceptableValueList : AcceptableValueBase where T : IEquatable
+ public AcceptableValueList(params T[] acceptableValues) : base(typeof(T))
{
- ///
- /// 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.
- ///
- 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));
+ 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;
- }
-
- ///
- /// List of values that a setting can take.
- ///
- public virtual T[] AcceptableValues { get; }
-
- ///
- public override object Clamp(object value)
- {
- if (IsValid(value))
- return value;
-
- return AcceptableValues[0];
- }
-
- ///
- public override bool IsValid(object value) => value is T v && AcceptableValues.Any(x => x.Equals(v));
-
- ///
- public override string ToDescriptionString() =>
- "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray());
+ AcceptableValues = acceptableValues;
}
+
+ ///
+ /// List of values that a setting can take.
+ ///
+ public virtual T[] AcceptableValues { get; }
+
+ ///
+ public override object Clamp(object value)
+ {
+ if (IsValid(value))
+ return value;
+
+ return AcceptableValues[0];
+ }
+
+ ///
+ public override bool IsValid(object value) => value is T v && AcceptableValues.Any(x => x.Equals(v));
+
+ ///
+ public override string ToDescriptionString() =>
+ "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray());
}
diff --git a/BepInEx.Core/Configuration/AcceptableValueRange.cs b/BepInEx.Core/Configuration/AcceptableValueRange.cs
index b25a218..2a3ddf4 100644
--- a/BepInEx.Core/Configuration/AcceptableValueRange.cs
+++ b/BepInEx.Core/Configuration/AcceptableValueRange.cs
@@ -1,53 +1,52 @@
using System;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Specify the range of acceptable values for a setting.
+///
+public class AcceptableValueRange : AcceptableValueBase where T : IComparable
{
- ///
- /// Specify the range of acceptable values for a setting.
- ///
- public class AcceptableValueRange : AcceptableValueBase where T : IComparable
+ /// Lowest acceptable value
+ /// Highest acceptable value
+ public AcceptableValueRange(T minValue, T maxValue) : base(typeof(T))
{
- /// Lowest acceptable value
- /// Highest acceptable value
- 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)}");
+ 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;
- }
-
- ///
- /// Lowest acceptable value
- ///
- public virtual T MinValue { get; }
-
- ///
- /// Highest acceptable value
- ///
- public virtual T MaxValue { get; }
-
- ///
- public override object Clamp(object value)
- {
- if (MinValue.CompareTo(value) > 0)
- return MinValue;
-
- if (MaxValue.CompareTo(value) < 0)
- return MaxValue;
-
- return value;
- }
-
- ///
- public override bool IsValid(object value) => MinValue.CompareTo(value) <= 0 && MaxValue.CompareTo(value) >= 0;
-
- ///
- public override string ToDescriptionString() => $"# Acceptable value range: From {MinValue} to {MaxValue}";
+ MinValue = minValue;
+ MaxValue = maxValue;
}
+
+ ///
+ /// Lowest acceptable value
+ ///
+ public virtual T MinValue { get; }
+
+ ///
+ /// Highest acceptable value
+ ///
+ public virtual T MaxValue { get; }
+
+ ///
+ public override object Clamp(object value)
+ {
+ if (MinValue.CompareTo(value) > 0)
+ return MinValue;
+
+ if (MaxValue.CompareTo(value) < 0)
+ return MaxValue;
+
+ return value;
+ }
+
+ ///
+ public override bool IsValid(object value) => MinValue.CompareTo(value) <= 0 && MaxValue.CompareTo(value) >= 0;
+
+ ///
+ public override string ToDescriptionString() => $"# Acceptable value range: From {MinValue} to {MaxValue}";
}
diff --git a/BepInEx.Core/Configuration/ConfigDefinition.cs b/BepInEx.Core/Configuration/ConfigDefinition.cs
index 1168c1e..d963c19 100644
--- a/BepInEx.Core/Configuration/ConfigDefinition.cs
+++ b/BepInEx.Core/Configuration/ConfigDefinition.cs
@@ -1,107 +1,106 @@
using System;
using System.Linq;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Section and key of a setting. Used as a unique key for identification within a
+/// .
+/// The same definition can be used in multiple config files, it will point to different settings then.
+///
+///
+public class ConfigDefinition : IEquatable
{
+ private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'', '[', ']' };
+
///
- /// Section and key of a setting. Used as a unique key for identification within a
- /// .
- /// The same definition can be used in multiple config files, it will point to different settings then.
+ /// Create a new definition. Definitions with same section and key are equal.
+ ///
+ /// Group of the setting, case sensitive.
+ /// Name of the setting, case sensitive.
+ public ConfigDefinition(string section, string key)
+ {
+ CheckInvalidConfigChars(section, nameof(section));
+ CheckInvalidConfigChars(key, nameof(key));
+ Key = key;
+ Section = section;
+ }
+
+ ///
+ [Obsolete("description argument is no longer used, put it in a ConfigDescription instead")]
+ public ConfigDefinition(string section, string key, string description)
+ {
+ Key = key ?? "";
+ Section = section ?? "";
+ }
+
+ ///
+ /// Group of the setting. All settings within a config file are grouped by this.
+ ///
+ public string Section { get; }
+
+ ///
+ /// Name of the setting.
+ ///
+ public string Key { get; }
+
+ ///
+ /// Check if the definitions are the same.
///
///
- public class ConfigDefinition : IEquatable
+ public bool Equals(ConfigDefinition other)
{
- private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'', '[', ']' };
-
- ///
- /// Create a new definition. Definitions with same section and key are equal.
- ///
- /// Group of the setting, case sensitive.
- /// Name of the setting, case sensitive.
- public ConfigDefinition(string section, string key)
- {
- CheckInvalidConfigChars(section, nameof(section));
- CheckInvalidConfigChars(key, nameof(key));
- Key = key;
- Section = section;
- }
-
- ///
- [Obsolete("description argument is no longer used, put it in a ConfigDescription instead")]
- public ConfigDefinition(string section, string key, string description)
- {
- Key = key ?? "";
- Section = section ?? "";
- }
-
- ///
- /// Group of the setting. All settings within a config file are grouped by this.
- ///
- public string Section { get; }
-
- ///
- /// Name of the setting.
- ///
- public string Key { get; }
-
- ///
- /// Check if the definitions are the same.
- ///
- ///
- public bool Equals(ConfigDefinition other)
- {
- if (other == null) return false;
- return string.Equals(Key, other.Key)
- && string.Equals(Section, other.Section);
- }
-
- 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);
- }
-
- ///
- /// Check if the definitions are the same.
- ///
- public override bool Equals(object obj)
- {
- if (ReferenceEquals(null, obj))
- return false;
- if (ReferenceEquals(this, obj))
- return true;
-
- return Equals(obj as ConfigDefinition);
- }
-
- ///
- public override int GetHashCode()
- {
- unchecked
- {
- var hashCode = Key != null ? Key.GetHashCode() : 0;
- hashCode = (hashCode * 397) ^ (Section != null ? Section.GetHashCode() : 0);
- return hashCode;
- }
- }
-
- ///
- /// Check if the definitions are the same.
- ///
- public static bool operator ==(ConfigDefinition left, ConfigDefinition right) => Equals(left, right);
-
- ///
- /// Check if the definitions are the same.
- ///
- public static bool operator !=(ConfigDefinition left, ConfigDefinition right) => !Equals(left, right);
-
- ///
- public override string ToString() => Section + "." + Key;
+ if (other == null) return false;
+ return string.Equals(Key, other.Key)
+ && string.Equals(Section, other.Section);
}
+
+ 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);
+ }
+
+ ///
+ /// Check if the definitions are the same.
+ ///
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+ if (ReferenceEquals(this, obj))
+ return true;
+
+ return Equals(obj as ConfigDefinition);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Key != null ? Key.GetHashCode() : 0;
+ hashCode = (hashCode * 397) ^ (Section != null ? Section.GetHashCode() : 0);
+ return hashCode;
+ }
+ }
+
+ ///
+ /// Check if the definitions are the same.
+ ///
+ public static bool operator ==(ConfigDefinition left, ConfigDefinition right) => Equals(left, right);
+
+ ///
+ /// Check if the definitions are the same.
+ ///
+ public static bool operator !=(ConfigDefinition left, ConfigDefinition right) => !Equals(left, right);
+
+ ///
+ public override string ToString() => Section + "." + Key;
}
diff --git a/BepInEx.Core/Configuration/ConfigDescription.cs b/BepInEx.Core/Configuration/ConfigDescription.cs
index 0cc4fdd..ce4d9f9 100644
--- a/BepInEx.Core/Configuration/ConfigDescription.cs
+++ b/BepInEx.Core/Configuration/ConfigDescription.cs
@@ -1,46 +1,45 @@
using System;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Metadata of a .
+///
+public class ConfigDescription
{
///
- /// Metadata of a .
+ /// Create a new description.
///
- public class ConfigDescription
+ /// Text describing the function of the setting and any notes or warnings.
+ ///
+ /// Range of values that this setting can take. The setting's value will be automatically
+ /// clamped.
+ ///
+ /// Objects that can be used by user-made classes to add functionality.
+ public ConfigDescription(string description, AcceptableValueBase acceptableValues = null, params object[] tags)
{
- ///
- /// Create a new description.
- ///
- /// Text describing the function of the setting and any notes or warnings.
- ///
- /// Range of values that this setting can take. The setting's value will be automatically
- /// clamped.
- ///
- /// Objects that can be used by user-made classes to add functionality.
- public ConfigDescription(string description, AcceptableValueBase acceptableValues = null, params object[] tags)
- {
- AcceptableValues = acceptableValues;
- Tags = tags;
- Description = description ?? throw new ArgumentNullException(nameof(description));
- }
-
- ///
- /// Text describing the function of the setting and any notes or warnings.
- ///
- public string Description { get; }
-
- ///
- /// Range of acceptable values for a setting.
- ///
- public AcceptableValueBase AcceptableValues { get; }
-
- ///
- /// Objects that can be used by user-made classes to add functionality.
- ///
- public object[] Tags { get; }
-
- ///
- /// An empty description.
- ///
- public static ConfigDescription Empty { get; } = new("");
+ AcceptableValues = acceptableValues;
+ Tags = tags;
+ Description = description ?? throw new ArgumentNullException(nameof(description));
}
+
+ ///
+ /// Text describing the function of the setting and any notes or warnings.
+ ///
+ public string Description { get; }
+
+ ///
+ /// Range of acceptable values for a setting.
+ ///
+ public AcceptableValueBase AcceptableValues { get; }
+
+ ///
+ /// Objects that can be used by user-made classes to add functionality.
+ ///
+ public object[] Tags { get; }
+
+ ///
+ /// An empty description.
+ ///
+ public static ConfigDescription Empty { get; } = new("");
}
diff --git a/BepInEx.Core/Configuration/ConfigEntryBase.cs b/BepInEx.Core/Configuration/ConfigEntryBase.cs
index a5b822f..2f1bc5f 100644
--- a/BepInEx.Core/Configuration/ConfigEntryBase.cs
+++ b/BepInEx.Core/Configuration/ConfigEntryBase.cs
@@ -3,180 +3,179 @@ using System.IO;
using System.Linq;
using BepInEx.Logging;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Provides access to a single setting inside of a .
+///
+/// Type of the setting.
+public sealed class ConfigEntry : ConfigEntryBase
{
- ///
- /// Provides access to a single setting inside of a .
- ///
- /// Type of the setting.
- public sealed class ConfigEntry : ConfigEntryBase
+ private T _typedValue;
+
+ internal ConfigEntry(ConfigFile configFile,
+ ConfigDefinition definition,
+ T defaultValue,
+ ConfigDescription configDescription) : base(configFile, definition, typeof(T),
+ defaultValue, configDescription)
{
- private T _typedValue;
-
- internal ConfigEntry(ConfigFile configFile,
- ConfigDefinition definition,
- T defaultValue,
- ConfigDescription configDescription) : base(configFile, definition, typeof(T),
- defaultValue, configDescription)
+ configFile.SettingChanged += (sender, args) =>
{
- configFile.SettingChanged += (sender, args) =>
- {
- if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args);
- };
- }
-
- ///
- /// Value of this setting.
- ///
- public T Value
- {
- get => _typedValue;
- set
- {
- value = ClampValue(value);
- if (Equals(_typedValue, value))
- return;
-
- _typedValue = value;
- OnSettingChanged(this);
- }
- }
-
- ///
- public override object BoxedValue
- {
- get => Value;
- set => Value = (T) value;
- }
-
- ///
- /// Fired when the setting is changed. Does not detect changes made outside from this object.
- ///
- public event EventHandler SettingChanged;
+ if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args);
+ };
}
///
- /// Container for a single setting of a .
- /// Each config entry is linked to one config file.
+ /// Value of this setting.
///
- public abstract class ConfigEntryBase
+ public T Value
{
- ///
- /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType.
- ///
- internal ConfigEntryBase(ConfigFile configFile,
- ConfigDefinition definition,
- Type settingType,
- object defaultValue,
- ConfigDescription configDescription)
+ get => _typedValue;
+ set
{
- ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile));
- Definition = definition ?? throw new ArgumentNullException(nameof(definition));
- SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType));
+ value = ClampValue(value);
+ if (Equals(_typedValue, value))
+ return;
- 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;
+ _typedValue = value;
+ OnSettingChanged(this);
}
+ }
- ///
- /// Config file this entry is a part of.
- ///
- public ConfigFile ConfigFile { get; }
+ ///
+ public override object BoxedValue
+ {
+ get => Value;
+ set => Value = (T) value;
+ }
- ///
- /// Category and name of this setting. Used as a unique key for identification within a
- /// .
- ///
- public ConfigDefinition Definition { get; }
+ ///
+ /// Fired when the setting is changed. Does not detect changes made outside from this object.
+ ///
+ public event EventHandler SettingChanged;
+}
- ///
- /// Description / metadata of this setting.
- ///
- public ConfigDescription Description { get; }
+///
+/// Container for a single setting of a .
+/// Each config entry is linked to one config file.
+///
+public abstract class ConfigEntryBase
+{
+ ///
+ /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType.
+ ///
+ 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));
- ///
- /// Type of the that this setting holds.
- ///
- public Type SettingType { get; }
+ 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");
- ///
- /// Default value of this setting (set only if the setting was not changed before).
- ///
- public object DefaultValue { get; }
+ DefaultValue = defaultValue;
- ///
- /// Get or set the value of the setting.
- ///
- public abstract object BoxedValue { get; set; }
+ // Free type check and automatically calls ClampValue in case AcceptableValues were provided
+ BoxedValue = defaultValue;
+ }
- ///
- /// Get the serialized representation of the value.
- ///
- public string GetSerializedValue() => TomlTypeConverter.ConvertToString(BoxedValue, SettingType);
+ ///
+ /// Config file this entry is a part of.
+ ///
+ public ConfigFile ConfigFile { get; }
- ///
- /// Set the value by using its serialized form.
- ///
- public void SetSerializedValue(string value)
+ ///
+ /// Category and name of this setting. Used as a unique key for identification within a
+ /// .
+ ///
+ public ConfigDefinition Definition { get; }
+
+ ///
+ /// Description / metadata of this setting.
+ ///
+ public ConfigDescription Description { get; }
+
+ ///
+ /// Type of the that this setting holds.
+ ///
+ public Type SettingType { get; }
+
+ ///
+ /// Default value of this setting (set only if the setting was not changed before).
+ ///
+ public object DefaultValue { get; }
+
+ ///
+ /// Get or set the value of the setting.
+ ///
+ public abstract object BoxedValue { get; set; }
+
+ ///
+ /// Get the serialized representation of the value.
+ ///
+ public string GetSerializedValue() => TomlTypeConverter.ConvertToString(BoxedValue, SettingType);
+
+ ///
+ /// Set the value by using its serialized form.
+ ///
+ public void SetSerializedValue(string value)
+ {
+ try
{
- 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}");
- }
+ var newValue = TomlTypeConverter.ConvertToValue(value, SettingType);
+ BoxedValue = newValue;
}
-
- ///
- /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType.
- ///
- protected T ClampValue(T value)
+ catch (Exception e)
{
- if (Description.AcceptableValues != null)
- return (T) Description.AcceptableValues.Clamp(value);
- return value;
+ Logger.Log(LogLevel.Warning,
+ $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}");
}
+ }
- ///
- /// Trigger setting changed event.
- ///
- protected void OnSettingChanged(object sender) => ConfigFile.OnSettingChanged(sender, this);
+ ///
+ /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType.
+ ///
+ protected T ClampValue(T value)
+ {
+ if (Description.AcceptableValues != null)
+ return (T) Description.AcceptableValues.Clamp(value);
+ return value;
+ }
- ///
- /// Write a description of this setting using all available metadata.
- ///
- public void WriteDescription(StreamWriter writer)
+ ///
+ /// Trigger setting changed event.
+ ///
+ protected void OnSettingChanged(object sender) => ConfigFile.OnSettingChanged(sender, this);
+
+ ///
+ /// Write a description of this setting using all available metadata.
+ ///
+ 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: {TomlTypeConverter.ConvertToString(DefaultValue, SettingType)}");
+
+ if (Description.AcceptableValues != null)
{
- if (!string.IsNullOrEmpty(Description.Description))
- writer.WriteLine($"## {Description.Description.Replace("\n", "\n## ")}");
+ writer.WriteLine(Description.AcceptableValues.ToDescriptionString());
+ }
+ else if (SettingType.IsEnum)
+ {
+ writer.WriteLine($"# Acceptable values: {string.Join(", ", Enum.GetNames(SettingType))}");
- writer.WriteLine("# Setting type: " + SettingType.Name);
-
- writer.WriteLine("# Default value: " + TomlTypeConverter.ConvertToString(DefaultValue, SettingType));
-
- 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)");
- }
+ 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)");
}
}
}
diff --git a/BepInEx.Core/Configuration/ConfigFile.cs b/BepInEx.Core/Configuration/ConfigFile.cs
index 5f1a711..714fdbf 100644
--- a/BepInEx.Core/Configuration/ConfigFile.cs
+++ b/BepInEx.Core/Configuration/ConfigFile.cs
@@ -7,606 +7,605 @@ using System.Linq;
using System.Text;
using BepInEx.Logging;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// A helper class to handle persistent data. All public methods are thread-safe.
+///
+public class ConfigFile : IDictionary
{
+ private readonly BepInPlugin _ownerMetadata;
+
+ ///
+ public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { }
+
///
- /// A helper class to handle persistent data. All public methods are thread-safe.
+ /// Create a new config file at the specified config path.
///
- public class ConfigFile : IDictionary
+ /// Full path to a file that contains settings. The file will be created as needed.
+ /// If the config file/directory doesn't exist, create it immediately.
+ /// Information about the plugin that owns this setting file.
+ public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata)
{
- private readonly BepInPlugin _ownerMetadata;
+ _ownerMetadata = ownerMetadata;
- ///
- public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { }
+ if (configPath == null) throw new ArgumentNullException(nameof(configPath));
+ configPath = Path.GetFullPath(configPath);
+ ConfigFilePath = configPath;
- ///
- /// Create a new config file at the specified config path.
- ///
- /// Full path to a file that contains settings. The file will be created as needed.
- /// If the config file/directory doesn't exist, create it immediately.
- /// Information about the plugin that owns this setting file.
- public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata)
- {
- _ownerMetadata = ownerMetadata;
+ if (File.Exists(ConfigFilePath))
+ Reload();
+ else if (saveOnInit) Save();
+ }
- if (configPath == null) throw new ArgumentNullException(nameof(configPath));
- configPath = Path.GetFullPath(configPath);
- ConfigFilePath = configPath;
+ public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true);
- if (File.Exists(ConfigFilePath))
- Reload();
- else if (saveOnInit) Save();
- }
+ ///
+ /// All config entries inside
+ ///
+ protected Dictionary Entries { get; } = new();
- public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true);
+ private Dictionary OrphanedEntries { get; } = new();
- ///
- /// All config entries inside
- ///
- protected Dictionary Entries { get; } = new();
-
- private Dictionary OrphanedEntries { get; } = new();
-
- ///
- /// Create a list with all config entries inside of this config file.
- ///
- [Obsolete("Use Keys instead")]
- public ReadOnlyCollection ConfigDefinitions
- {
- get
- {
- lock (_ioLock)
- {
- return Entries.Keys.ToList().AsReadOnly();
- }
- }
- }
-
- ///
- /// Full path to the config file. The file might not exist until a setting is added and changed, or
- /// is called.
- ///
- public string ConfigFilePath { get; }
-
- ///
- /// If enabled, writes the config to disk every time a value is set.
- /// If disabled, you have to manually use or the changes will be lost!
- ///
- public bool SaveOnConfigSet { get; set; } = true;
-
- ///
- public ConfigEntryBase this[ConfigDefinition key]
- {
- get
- {
- lock (_ioLock)
- {
- return Entries[key];
- }
- }
- }
-
- ///
- ///
- ///
- ///
- public ConfigEntryBase this[string section, string key] => this[new ConfigDefinition(section, key)];
-
- ///
- public IEnumerator> GetEnumerator() =>
- // We can't really do a read lock for this
- Entries.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
- void ICollection>.Add(
- KeyValuePair item)
+ ///
+ /// Create a list with all config entries inside of this config file.
+ ///
+ [Obsolete("Use Keys instead")]
+ public ReadOnlyCollection ConfigDefinitions
+ {
+ get
{
lock (_ioLock)
{
- Entries.Add(item.Key, item.Value);
+ return Entries.Keys.ToList().AsReadOnly();
}
}
+ }
- ///
- public bool Contains(KeyValuePair item)
+ ///
+ /// Full path to the config file. The file might not exist until a setting is added and changed, or
+ /// is called.
+ ///
+ public string ConfigFilePath { get; }
+
+ ///
+ /// If enabled, writes the config to disk every time a value is set.
+ /// If disabled, you have to manually use or the changes will be lost!
+ ///
+ public bool SaveOnConfigSet { get; set; } = true;
+
+ ///
+ public ConfigEntryBase this[ConfigDefinition key]
+ {
+ get
{
lock (_ioLock)
{
- return ((ICollection>) Entries).Contains(item);
+ return Entries[key];
}
}
+ }
- void ICollection>.CopyTo(
- KeyValuePair[] array,
- int arrayIndex)
+ ///
+ ///
+ ///
+ ///
+ public ConfigEntryBase this[string section, string key] => this[new ConfigDefinition(section, key)];
+
+ ///
+ public IEnumerator> GetEnumerator() =>
+ // We can't really do a read lock for this
+ Entries.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ void ICollection>.Add(
+ KeyValuePair item)
+ {
+ lock (_ioLock)
+ {
+ Entries.Add(item.Key, item.Value);
+ }
+ }
+
+ ///
+ public bool Contains(KeyValuePair item)
+ {
+ lock (_ioLock)
+ {
+ return ((ICollection>) Entries).Contains(item);
+ }
+ }
+
+ void ICollection>.CopyTo(
+ KeyValuePair[] array,
+ int arrayIndex)
+ {
+ lock (_ioLock)
+ {
+ ((ICollection>) Entries).CopyTo(array, arrayIndex);
+ }
+ }
+
+ bool ICollection>.Remove(
+ KeyValuePair item)
+ {
+ lock (_ioLock)
+ {
+ return Entries.Remove(item.Key);
+ }
+ }
+
+ ///
+ public int Count
+ {
+ get
{
lock (_ioLock)
{
- ((ICollection>) Entries).CopyTo(array, arrayIndex);
+ return Entries.Count;
}
}
+ }
- bool ICollection>.Remove(
- KeyValuePair item)
+ ///
+ public bool IsReadOnly => false;
+
+ ///
+ public bool ContainsKey(ConfigDefinition key)
+ {
+ lock (_ioLock)
+ {
+ return Entries.ContainsKey(key);
+ }
+ }
+
+ ///
+ public void Add(ConfigDefinition key, ConfigEntryBase value) =>
+ throw new InvalidOperationException("Directly adding a config entry is not supported");
+
+ ///
+ public bool Remove(ConfigDefinition key)
+ {
+ lock (_ioLock)
+ {
+ return Entries.Remove(key);
+ }
+ }
+
+ ///
+ public void Clear()
+ {
+ lock (_ioLock)
+ {
+ Entries.Clear();
+ }
+ }
+
+ bool IDictionary.TryGetValue(ConfigDefinition key, out ConfigEntryBase value)
+ {
+ lock (_ioLock)
+ {
+ return Entries.TryGetValue(key, out value);
+ }
+ }
+
+ ///
+ ConfigEntryBase IDictionary.this[ConfigDefinition key]
+ {
+ get
{
lock (_ioLock)
{
- return Entries.Remove(item.Key);
+ return Entries[key];
}
}
+ set => throw new InvalidOperationException("Directly setting a config entry is not supported");
+ }
- ///
- public int Count
- {
- get
- {
- lock (_ioLock)
- {
- return Entries.Count;
- }
- }
- }
-
- ///
- public bool IsReadOnly => false;
-
- ///
- public bool ContainsKey(ConfigDefinition key)
+ ///
+ /// Returns the ConfigDefinitions that the ConfigFile contains.
+ /// Creates a new array when the property is accessed. Thread-safe.
+ ///
+ public ICollection Keys
+ {
+ get
{
lock (_ioLock)
{
- return Entries.ContainsKey(key);
+ return Entries.Keys.ToArray();
}
}
+ }
- ///
- public void Add(ConfigDefinition key, ConfigEntryBase value) =>
- throw new InvalidOperationException("Directly adding a config entry is not supported");
-
- ///
- public bool Remove(ConfigDefinition key)
- {
- lock (_ioLock)
- {
- return Entries.Remove(key);
- }
- }
-
- ///
- public void Clear()
- {
- lock (_ioLock)
- {
- Entries.Clear();
- }
- }
-
- bool IDictionary.TryGetValue(ConfigDefinition key, out ConfigEntryBase value)
- {
- lock (_ioLock)
- {
- return Entries.TryGetValue(key, out value);
- }
- }
-
- ///
- ConfigEntryBase IDictionary.this[ConfigDefinition key]
- {
- get
- {
- lock (_ioLock)
- {
- return Entries[key];
- }
- }
- set => throw new InvalidOperationException("Directly setting a config entry is not supported");
- }
-
- ///
- /// Returns the ConfigDefinitions that the ConfigFile contains.
- /// Creates a new array when the property is accessed. Thread-safe.
- ///
- public ICollection Keys
- {
- get
- {
- lock (_ioLock)
- {
- return Entries.Keys.ToArray();
- }
- }
- }
-
- ///
- /// Returns the ConfigEntryBase values that the ConfigFile contains.
- /// Creates a new array when the property is accessed. Thread-safe.
- ///
- ICollection IDictionary.Values
- {
- get
- {
- lock (_ioLock)
- {
- return Entries.Values.ToArray();
- }
- }
- }
-
- ///
- /// 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
- ///
- /// instead with no description.
- ///
- [Obsolete("Use Values instead")]
- public ConfigEntryBase[] GetConfigEntries()
+ ///
+ /// Returns the ConfigEntryBase values that the ConfigFile contains.
+ /// Creates a new array when the property is accessed. Thread-safe.
+ ///
+ ICollection IDictionary.Values
+ {
+ get
{
lock (_ioLock)
{
return Entries.Values.ToArray();
}
}
+ }
- #region Save/Load
-
- private readonly object _ioLock = new();
-
- ///
- /// Generate user-readable comments for each of the settings in the saved .cfg file.
- ///
- public bool GenerateSettingDescriptions { get; set; } = true;
-
- ///
- /// Reloads the config from disk. Unsaved changes are lost.
- ///
- public void Reload()
+ ///
+ /// 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
+ ///
+ /// instead with no description.
+ ///
+ [Obsolete("Use Values instead")]
+ public ConfigEntryBase[] GetConfigEntries()
+ {
+ lock (_ioLock)
{
- lock (_ioLock)
+ return Entries.Values.ToArray();
+ }
+ }
+
+ #region Save/Load
+
+ private readonly object _ioLock = new();
+
+ ///
+ /// Generate user-readable comments for each of the settings in the saved .cfg file.
+ ///
+ public bool GenerateSettingDescriptions { get; set; } = true;
+
+ ///
+ /// Reloads the config from disk. Unsaved changes are lost.
+ ///
+ public void Reload()
+ {
+ lock (_ioLock)
+ {
+ OrphanedEntries.Clear();
+
+ var currentSection = string.Empty;
+
+ foreach (var rawLine in File.ReadAllLines(ConfigFilePath))
{
- OrphanedEntries.Clear();
+ var line = rawLine.Trim();
- var currentSection = string.Empty;
+ if (line.StartsWith("#")) //comment
+ continue;
- foreach (var rawLine in File.ReadAllLines(ConfigFilePath))
+ if (line.StartsWith("[") && line.EndsWith("]")) //section
{
- var line = rawLine.Trim();
-
- if (line.StartsWith("#")) //comment
- continue;
-
- if (line.StartsWith("[") && line.EndsWith("]")) //section
- {
- currentSection = line.Substring(1, line.Length - 2);
- continue;
- }
-
- var split = line.Split(new[] { '=' }, 2); //actual config line
- if (split.Length != 2)
- continue; //empty/invalid line
-
- var currentKey = split[0].Trim();
- var currentValue = split[1].Trim();
-
- var definition = new ConfigDefinition(currentSection, currentKey);
-
- Entries.TryGetValue(definition, out var entry);
-
- if (entry != null)
- entry.SetSerializedValue(currentValue);
- else
- OrphanedEntries[definition] = currentValue;
+ currentSection = line.Substring(1, line.Length - 2);
+ continue;
}
- }
- OnConfigReloaded();
+ var split = line.Split(new[] { '=' }, 2); //actual config line
+ if (split.Length != 2)
+ continue; //empty/invalid line
+
+ var currentKey = split[0].Trim();
+ var currentValue = split[1].Trim();
+
+ var definition = new ConfigDefinition(currentSection, currentKey);
+
+ Entries.TryGetValue(definition, out var entry);
+
+ if (entry != null)
+ entry.SetSerializedValue(currentValue);
+ else
+ OrphanedEntries[definition] = currentValue;
+ }
}
- ///
- /// Writes the config to disk.
- ///
- public void Save()
+ OnConfigReloaded();
+ }
+
+ ///
+ /// Writes the config to disk.
+ ///
+ public void Save()
+ {
+ lock (_ioLock)
{
- lock (_ioLock)
+ var directoryName = Path.GetDirectoryName(ConfigFilePath);
+ if (directoryName != null) Directory.CreateDirectory(directoryName);
+
+ using (var writer = new StreamWriter(ConfigFilePath, false, Utility.UTF8NoBom))
{
- var directoryName = Path.GetDirectoryName(ConfigFilePath);
- if (directoryName != null) Directory.CreateDirectory(directoryName);
-
- using (var writer = new StreamWriter(ConfigFilePath, false, Utility.UTF8NoBom))
+ if (_ownerMetadata != null)
{
- 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(OrphanedEntries.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 configEntry in sectionKv)
{
- 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(OrphanedEntries.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 configEntry in sectionKv)
+ if (GenerateSettingDescriptions)
{
- if (GenerateSettingDescriptions)
- {
- writer.WriteLine();
- configEntry.entry?.WriteDescription(writer);
- }
-
- writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}");
+ writer.WriteLine();
+ configEntry.entry?.WriteDescription(writer);
}
- writer.WriteLine();
+ writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}");
}
+
+ writer.WriteLine();
}
}
}
+ }
- #endregion
+ #endregion
- #region Wraps
+ #region Wraps
- ///
- /// 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 .
- ///
- /// Type of the value contained in this setting.
- /// Section and Key of the setting.
- [Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
- public ConfigEntry GetSetting(ConfigDefinition configDefinition) =>
- TryGetEntry(configDefinition, out var entry)
- ? entry
- : null;
+ ///
+ /// 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 .
+ ///
+ /// Type of the value contained in this setting.
+ /// Section and Key of the setting.
+ [Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
+ public ConfigEntry GetSetting(ConfigDefinition configDefinition) =>
+ TryGetEntry(configDefinition, out var entry)
+ ? entry
+ : null;
- ///
- /// 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 .
- ///
- /// Type of the value contained in this setting.
- /// Section/category/group of the setting. Settings are grouped by this.
- /// Name of the setting.
- [Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
- public ConfigEntry GetSetting(string section, string key) =>
- TryGetEntry(section, key, out var entry)
- ? entry
- : null;
+ ///
+ /// 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 .
+ ///
+ /// Type of the value contained in this setting.
+ /// Section/category/group of the setting. Settings are grouped by this.
+ /// Name of the setting.
+ [Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
+ public ConfigEntry GetSetting(string section, string key) =>
+ TryGetEntry(section, key, out var entry)
+ ? entry
+ : null;
- ///
- /// Access one of the existing settings. If the setting has not been added yet, false is returned. Otherwise, true.
- /// If the setting exists but has a different type than T, an exception is thrown.
- /// New settings should be added with
- /// .
- ///
- /// Type of the value contained in this setting.
- /// Section and Key of the setting.
- /// The ConfigEntry value to return.
- public bool TryGetEntry(ConfigDefinition configDefinition, out ConfigEntry entry)
+ ///
+ /// Access one of the existing settings. If the setting has not been added yet, false is returned. Otherwise, true.
+ /// If the setting exists but has a different type than T, an exception is thrown.
+ /// New settings should be added with
+ /// .
+ ///
+ /// Type of the value contained in this setting.
+ /// Section and Key of the setting.
+ /// The ConfigEntry value to return.
+ public bool TryGetEntry(ConfigDefinition configDefinition, out ConfigEntry entry)
+ {
+ lock (_ioLock)
{
- lock (_ioLock)
+ if (Entries.TryGetValue(configDefinition, out var rawEntry))
{
- if (Entries.TryGetValue(configDefinition, out var rawEntry))
- {
- entry = (ConfigEntry) rawEntry;
- return true;
- }
-
- entry = null;
- return false;
+ entry = (ConfigEntry) rawEntry;
+ return true;
}
+
+ entry = null;
+ return false;
}
+ }
- ///
- /// 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
- /// .
- ///
- /// Type of the value contained in this setting.
- /// Section/category/group of the setting. Settings are grouped by this.
- /// Name of the setting.
- /// The ConfigEntry value to return.
- public bool TryGetEntry(string section, string key, out ConfigEntry entry) =>
- TryGetEntry(new ConfigDefinition(section, key), out entry);
+ ///
+ /// 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
+ /// .
+ ///
+ /// Type of the value contained in this setting.
+ /// Section/category/group of the setting. Settings are grouped by this.
+ /// Name of the setting.
+ /// The ConfigEntry value to return.
+ public bool TryGetEntry(string section, string key, out ConfigEntry entry) =>
+ TryGetEntry(new ConfigDefinition(section, key), out entry);
- ///
- /// 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.
- ///
- /// Type of the value contained in this setting.
- /// Section and Key of the setting.
- /// Value of the setting if the setting was not created yet.
- /// Description of the setting shown to the user and other metadata.
- public ConfigEntry Bind(ConfigDefinition configDefinition,
- T defaultValue,
- ConfigDescription configDescription = null)
+ ///
+ /// 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.
+ ///
+ /// Type of the value contained in this setting.
+ /// Section and Key of the setting.
+ /// Value of the setting if the setting was not created yet.
+ /// Description of the setting shown to the user and other metadata.
+ public ConfigEntry Bind(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 (!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())}");
+ if (Entries.TryGetValue(configDefinition, out var rawEntry))
+ return (ConfigEntry) rawEntry;
- lock (_ioLock)
+ var entry = new ConfigEntry(this, configDefinition, defaultValue, configDescription);
+
+ Entries[configDefinition] = entry;
+
+ if (OrphanedEntries.TryGetValue(configDefinition, out var homelessValue))
{
- if (Entries.TryGetValue(configDefinition, out var rawEntry))
- return (ConfigEntry) rawEntry;
-
- var entry = new ConfigEntry(this, configDefinition, defaultValue, configDescription);
-
- Entries[configDefinition] = entry;
-
- if (OrphanedEntries.TryGetValue(configDefinition, out var homelessValue))
- {
- entry.SetSerializedValue(homelessValue);
- OrphanedEntries.Remove(configDefinition);
- }
-
- if (SaveOnConfigSet)
- Save();
-
- return entry;
+ entry.SetSerializedValue(homelessValue);
+ OrphanedEntries.Remove(configDefinition);
}
- }
-
- ///
- /// 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.
- ///
- /// Type of the value contained in this setting.
- /// Section/category/group of the setting. Settings are grouped by this.
- /// Name of the setting.
- /// Value of the setting if the setting was not created yet.
- /// Description of the setting shown to the user and other metadata.
- public ConfigEntry Bind(string section,
- string key,
- T defaultValue,
- ConfigDescription configDescription = null) =>
- Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
-
- ///
- /// 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.
- ///
- /// Type of the value contained in this setting.
- /// Section/category/group of the setting. Settings are grouped by this.
- /// Name of the setting.
- /// Value of the setting if the setting was not created yet.
- /// Simple description of the setting shown to the user.
- public ConfigEntry Bind(string section, string key, T defaultValue, string description) =>
- Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
-
- ///
- /// 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.
- ///
- /// Type of the value contained in this setting.
- /// Section and Key of the setting.
- /// Value of the setting if the setting was not created yet.
- /// Description of the setting shown to the user and other metadata.
- [Obsolete("Use Bind instead")]
- public ConfigEntry AddSetting(ConfigDefinition configDefinition,
- T defaultValue,
- ConfigDescription configDescription = null) =>
- Bind(configDefinition, defaultValue, configDescription);
-
- ///
- /// 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.
- ///
- /// Type of the value contained in this setting.
- /// Section/category/group of the setting. Settings are grouped by this.
- /// Name of the setting.
- /// Value of the setting if the setting was not created yet.
- /// Description of the setting shown to the user and other metadata.
- [Obsolete("Use Bind instead")]
- public ConfigEntry AddSetting(string section,
- string key,
- T defaultValue,
- ConfigDescription configDescription = null) =>
- Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
-
- ///
- /// 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.
- ///
- /// Type of the value contained in this setting.
- /// Section/category/group of the setting. Settings are grouped by this.
- /// Name of the setting.
- /// Value of the setting if the setting was not created yet.
- /// Simple description of the setting shown to the user.
- [Obsolete("Use Bind instead")]
- public ConfigEntry AddSetting(string section, string key, T defaultValue, string description) =>
- Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
-
- ///
- /// Access a setting. Use Bind instead.
- ///
- [Obsolete("Use Bind instead")]
- public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default)
- {
- lock (_ioLock)
- {
- var definition = new ConfigDefinition(section, key, description);
- var setting = Bind(definition, defaultValue,
- string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
- return new ConfigWrapper(setting);
- }
- }
-
- ///
- /// Access a setting. Use Bind instead.
- ///
- [Obsolete("Use Bind instead")]
- public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default) =>
- Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue);
-
- #endregion
-
- #region Events
-
- ///
- /// An event that is fired every time the config is reloaded.
- ///
- public event EventHandler ConfigReloaded;
-
- ///
- /// Fired when one of the settings is changed.
- ///
- public event EventHandler 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 (EventHandler callback in settingChanged.GetInvocationList())
- try
- {
- callback(sender, args);
- }
- catch (Exception e)
- {
- Logger.Log(LogLevel.Error, e);
- }
+ return entry;
}
-
- private void OnConfigReloaded()
- {
- var configReloaded = ConfigReloaded;
- if (configReloaded == null) return;
-
- foreach (EventHandler callback in configReloaded.GetInvocationList())
- try
- {
- callback(this, EventArgs.Empty);
- }
- catch (Exception e)
- {
- Logger.Log(LogLevel.Error, e);
- }
- }
-
- #endregion
}
+
+ ///
+ /// 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.
+ ///
+ /// Type of the value contained in this setting.
+ /// Section/category/group of the setting. Settings are grouped by this.
+ /// Name of the setting.
+ /// Value of the setting if the setting was not created yet.
+ /// Description of the setting shown to the user and other metadata.
+ public ConfigEntry Bind(string section,
+ string key,
+ T defaultValue,
+ ConfigDescription configDescription = null) =>
+ Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
+
+ ///
+ /// 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.
+ ///
+ /// Type of the value contained in this setting.
+ /// Section/category/group of the setting. Settings are grouped by this.
+ /// Name of the setting.
+ /// Value of the setting if the setting was not created yet.
+ /// Simple description of the setting shown to the user.
+ public ConfigEntry Bind(string section, string key, T defaultValue, string description) =>
+ Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
+
+ ///
+ /// 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.
+ ///
+ /// Type of the value contained in this setting.
+ /// Section and Key of the setting.
+ /// Value of the setting if the setting was not created yet.
+ /// Description of the setting shown to the user and other metadata.
+ [Obsolete("Use Bind instead")]
+ public ConfigEntry AddSetting(ConfigDefinition configDefinition,
+ T defaultValue,
+ ConfigDescription configDescription = null) =>
+ Bind(configDefinition, defaultValue, configDescription);
+
+ ///
+ /// 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.
+ ///
+ /// Type of the value contained in this setting.
+ /// Section/category/group of the setting. Settings are grouped by this.
+ /// Name of the setting.
+ /// Value of the setting if the setting was not created yet.
+ /// Description of the setting shown to the user and other metadata.
+ [Obsolete("Use Bind instead")]
+ public ConfigEntry AddSetting(string section,
+ string key,
+ T defaultValue,
+ ConfigDescription configDescription = null) =>
+ Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
+
+ ///
+ /// 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.
+ ///
+ /// Type of the value contained in this setting.
+ /// Section/category/group of the setting. Settings are grouped by this.
+ /// Name of the setting.
+ /// Value of the setting if the setting was not created yet.
+ /// Simple description of the setting shown to the user.
+ [Obsolete("Use Bind instead")]
+ public ConfigEntry AddSetting(string section, string key, T defaultValue, string description) =>
+ Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
+
+ ///
+ /// Access a setting. Use Bind instead.
+ ///
+ [Obsolete("Use Bind instead")]
+ public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default)
+ {
+ lock (_ioLock)
+ {
+ var definition = new ConfigDefinition(section, key, description);
+ var setting = Bind(definition, defaultValue,
+ string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
+ return new ConfigWrapper(setting);
+ }
+ }
+
+ ///
+ /// Access a setting. Use Bind instead.
+ ///
+ [Obsolete("Use Bind instead")]
+ public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default) =>
+ Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue);
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// An event that is fired every time the config is reloaded.
+ ///
+ public event EventHandler ConfigReloaded;
+
+ ///
+ /// Fired when one of the settings is changed.
+ ///
+ public event EventHandler 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 (EventHandler callback in settingChanged.GetInvocationList())
+ try
+ {
+ callback(sender, args);
+ }
+ catch (Exception e)
+ {
+ Logger.Log(LogLevel.Error, e);
+ }
+ }
+
+ private void OnConfigReloaded()
+ {
+ var configReloaded = ConfigReloaded;
+ if (configReloaded == null) return;
+
+ foreach (EventHandler callback in configReloaded.GetInvocationList())
+ try
+ {
+ callback(this, EventArgs.Empty);
+ }
+ catch (Exception e)
+ {
+ Logger.Log(LogLevel.Error, e);
+ }
+ }
+
+ #endregion
}
diff --git a/BepInEx.Core/Configuration/ConfigWrapper.cs b/BepInEx.Core/Configuration/ConfigWrapper.cs
index 792c765..6ba0166 100644
--- a/BepInEx.Core/Configuration/ConfigWrapper.cs
+++ b/BepInEx.Core/Configuration/ConfigWrapper.cs
@@ -1,51 +1,50 @@
using System;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Provides access to a single setting inside of a .
+///
+/// Type of the setting.
+[Obsolete("Use ConfigFile from new Bind overloads instead")]
+public sealed class ConfigWrapper
{
- ///
- /// Provides access to a single setting inside of a .
- ///
- /// Type of the setting.
- [Obsolete("Use ConfigFile from new Bind overloads instead")]
- public sealed class ConfigWrapper
+ internal ConfigWrapper(ConfigEntry configEntry)
{
- internal ConfigWrapper(ConfigEntry configEntry)
+ ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry));
+
+ configEntry.ConfigFile.SettingChanged += (sender, args) =>
{
- ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry));
-
- configEntry.ConfigFile.SettingChanged += (sender, args) =>
- {
- if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args);
- };
- }
-
- ///
- /// Entry of this setting in the .
- ///
- public ConfigEntry ConfigEntry { get; }
-
- ///
- /// Unique definition of this setting.
- ///
- public ConfigDefinition Definition => ConfigEntry.Definition;
-
- ///
- /// Config file this setting is inside of.
- ///
- public ConfigFile ConfigFile => ConfigEntry.ConfigFile;
-
- ///
- /// Value of this setting.
- ///
- public T Value
- {
- get => ConfigEntry.Value;
- set => ConfigEntry.Value = value;
- }
-
- ///
- /// Fired when the setting is changed. Does not detect changes made outside from this object.
- ///
- public event EventHandler SettingChanged;
+ if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args);
+ };
}
+
+ ///
+ /// Entry of this setting in the .
+ ///
+ public ConfigEntry ConfigEntry { get; }
+
+ ///
+ /// Unique definition of this setting.
+ ///
+ public ConfigDefinition Definition => ConfigEntry.Definition;
+
+ ///
+ /// Config file this setting is inside of.
+ ///
+ public ConfigFile ConfigFile => ConfigEntry.ConfigFile;
+
+ ///
+ /// Value of this setting.
+ ///
+ public T Value
+ {
+ get => ConfigEntry.Value;
+ set => ConfigEntry.Value = value;
+ }
+
+ ///
+ /// Fired when the setting is changed. Does not detect changes made outside from this object.
+ ///
+ public event EventHandler SettingChanged;
}
diff --git a/BepInEx.Core/Configuration/SettingChangedEventArgs.cs b/BepInEx.Core/Configuration/SettingChangedEventArgs.cs
index 9beffa0..7e167ec 100644
--- a/BepInEx.Core/Configuration/SettingChangedEventArgs.cs
+++ b/BepInEx.Core/Configuration/SettingChangedEventArgs.cs
@@ -1,22 +1,21 @@
using System;
-namespace BepInEx.Configuration
-{
- ///
- /// Arguments for events concerning a change of a setting.
- ///
- ///
- public sealed class SettingChangedEventArgs : EventArgs
- {
- ///
- public SettingChangedEventArgs(ConfigEntryBase changedSetting)
- {
- ChangedSetting = changedSetting;
- }
+namespace BepInEx.Configuration;
- ///
- /// Setting that was changed
- ///
- public ConfigEntryBase ChangedSetting { get; }
+///
+/// Arguments for events concerning a change of a setting.
+///
+///
+public sealed class SettingChangedEventArgs : EventArgs
+{
+ ///
+ public SettingChangedEventArgs(ConfigEntryBase changedSetting)
+ {
+ ChangedSetting = changedSetting;
}
+
+ ///
+ /// Setting that was changed
+ ///
+ public ConfigEntryBase ChangedSetting { get; }
}
diff --git a/BepInEx.Core/Configuration/TomlTypeConverter.cs b/BepInEx.Core/Configuration/TomlTypeConverter.cs
index eaf8315..779cb67 100644
--- a/BepInEx.Core/Configuration/TomlTypeConverter.cs
+++ b/BepInEx.Core/Configuration/TomlTypeConverter.cs
@@ -5,288 +5,287 @@ using System.Text;
using System.Text.RegularExpressions;
using BepInEx.Logging;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// Serializer/deserializer used by the config system.
+///
+public static class TomlTypeConverter
{
- ///
- /// Serializer/deserializer used by the config system.
- ///
- public static class TomlTypeConverter
+ // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead
+ private static Dictionary TypeConverters { get; } = new()
{
- // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead
- private static Dictionary TypeConverters { get; } = new()
+ [typeof(string)] = new TypeConverter
{
- [typeof(string)] = new TypeConverter
+ ConvertToString = (obj, type) => Escape((string) obj),
+ ConvertToObject = (str, type) =>
{
- 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, type) => obj.ToString().ToLowerInvariant(),
- ConvertToObject = (str, type) => bool.Parse(str)
- },
- [typeof(byte)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => byte.Parse(str)
- },
-
- //integral types
-
- [typeof(sbyte)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => sbyte.Parse(str)
- },
- [typeof(byte)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => byte.Parse(str)
- },
- [typeof(short)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => short.Parse(str)
- },
- [typeof(ushort)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => ushort.Parse(str)
- },
- [typeof(int)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => int.Parse(str)
- },
- [typeof(uint)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => uint.Parse(str)
- },
- [typeof(long)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => long.Parse(str)
- },
- [typeof(ulong)] = new TypeConverter
- {
- ConvertToString = (obj, type) => obj.ToString(),
- ConvertToObject = (str, type) => ulong.Parse(str)
- },
-
- //floating point types
-
- [typeof(float)] = new TypeConverter
- {
- ConvertToString = (obj, type) => ((float) obj).ToString(NumberFormatInfo.InvariantInfo),
- ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo)
- },
- [typeof(double)] = new TypeConverter
- {
- ConvertToString = (obj, type) => ((double) obj).ToString(NumberFormatInfo.InvariantInfo),
- ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo)
- },
- [typeof(decimal)] = new TypeConverter
- {
- 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)
+ // 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);
}
- };
-
- ///
- /// Convert object of a given type to a string using available converters.
- ///
- public static string ConvertToString(object value, Type valueType)
+ },
+ [typeof(bool)] = new TypeConverter
{
- var conv = GetConverter(valueType);
- if (conv == null)
- throw new InvalidOperationException($"Cannot convert from type {valueType}");
+ ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(),
+ ConvertToObject = (str, type) => bool.Parse(str)
+ },
+ [typeof(byte)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => byte.Parse(str)
+ },
- return conv.ConvertToString(value, valueType);
+ //integral types
+
+ [typeof(sbyte)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => sbyte.Parse(str)
+ },
+ [typeof(byte)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => byte.Parse(str)
+ },
+ [typeof(short)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => short.Parse(str)
+ },
+ [typeof(ushort)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => ushort.Parse(str)
+ },
+ [typeof(int)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => int.Parse(str)
+ },
+ [typeof(uint)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => uint.Parse(str)
+ },
+ [typeof(long)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => long.Parse(str)
+ },
+ [typeof(ulong)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => obj.ToString(),
+ ConvertToObject = (str, type) => ulong.Parse(str)
+ },
+
+ //floating point types
+
+ [typeof(float)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => ((float) obj).ToString(NumberFormatInfo.InvariantInfo),
+ ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo)
+ },
+ [typeof(double)] = new TypeConverter
+ {
+ ConvertToString = (obj, type) => ((double) obj).ToString(NumberFormatInfo.InvariantInfo),
+ ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo)
+ },
+ [typeof(decimal)] = new TypeConverter
+ {
+ 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)
+ }
+ };
+
+ ///
+ /// Convert object of a given type to a string using available converters.
+ ///
+ public static string ConvertToString(object value, Type valueType)
+ {
+ var conv = GetConverter(valueType);
+ if (conv == null)
+ throw new InvalidOperationException($"Cannot convert from type {valueType}");
+
+ return conv.ConvertToString(value, valueType);
+ }
+
+ ///
+ /// Convert string to an object of a given type using available converters.
+ ///
+ public static T ConvertToValue(string value) => (T) ConvertToValue(value, typeof(T));
+
+ ///
+ /// Convert string to an object of a given type using available converters.
+ ///
+ 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);
+ }
+
+ ///
+ /// Get a converter for a given type if there is any.
+ ///
+ public static TypeConverter GetConverter(Type valueType)
+ {
+ if (valueType == null)
+ throw new ArgumentNullException(nameof(valueType));
+
+ if (valueType.IsEnum)
+ return TypeConverters[typeof(Enum)];
+
+ TypeConverters.TryGetValue(valueType, out var result);
+
+ return result;
+ }
+
+ ///
+ /// Add a new type converter for a given type.
+ /// If a different converter is already added, this call is ignored and false is returned.
+ ///
+ 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;
}
- ///
- /// Convert string to an object of a given type using available converters.
- ///
- public static T ConvertToValue(string value) => (T) ConvertToValue(value, typeof(T));
+ TypeConverters.Add(type, converter);
+ return true;
+ }
- ///
- /// Convert string to an object of a given type using available converters.
- ///
- public static object ConvertToValue(string value, Type valueType)
- {
- var conv = GetConverter(valueType);
- if (conv == null)
- throw new InvalidOperationException($"Cannot convert to type {valueType.Name}");
+ ///
+ /// Check if a given type can be converted to and from string.
+ ///
+ public static bool CanConvert(Type type) => GetConverter(type) != null;
- return conv.ConvertToObject(value, valueType);
- }
+ ///
+ /// Give a list of types with registered converters.
+ ///
+ public static IEnumerable GetSupportedTypes() => TypeConverters.Keys;
- ///
- /// Get a converter for a given type if there is any.
- ///
- public static TypeConverter GetConverter(Type valueType)
- {
- if (valueType == null)
- throw new ArgumentNullException(nameof(valueType));
+ private static string Escape(this string txt)
+ {
+ if (string.IsNullOrEmpty(txt)) return string.Empty;
- if (valueType.IsEnum)
- return TypeConverters[typeof(Enum)];
-
- TypeConverters.TryGetValue(valueType, out var result);
-
- return result;
- }
-
- ///
- /// Add a new type converter for a given type.
- /// If a different converter is already added, this call is ignored and false is returned.
- ///
- 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))
+ var stringBuilder = new StringBuilder(txt.Length + 2);
+ foreach (var c in txt)
+ switch (c)
{
- Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName);
- return false;
- }
-
- TypeConverters.Add(type, converter);
- return true;
- }
-
- ///
- /// Check if a given type can be converted to and from string.
- ///
- public static bool CanConvert(Type type) => GetConverter(type) != null;
-
- ///
- /// Give a list of types with registered converters.
- ///
- public static IEnumerable GetSupportedTypes() => TypeConverters.Keys;
-
- private static string Escape(this string txt)
- {
- if (string.IsNullOrEmpty(txt)) return string.Empty;
-
- var stringBuilder = new StringBuilder(txt.Length + 2);
- foreach (var 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 (var i = 0; i < txt.Length;)
- {
- var num = txt.IndexOf('\\', i);
- if (num < 0 || num == txt.Length - 1)
- num = txt.Length;
- stringBuilder.Append(txt, i, num - i);
- if (num >= txt.Length)
+ 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;
- var 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();
+ return stringBuilder.ToString();
+ }
+
+ private static string Unescape(this string txt)
+ {
+ if (string.IsNullOrEmpty(txt))
+ return txt;
+ var stringBuilder = new StringBuilder(txt.Length);
+ for (var i = 0; i < txt.Length;)
+ {
+ var 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;
+ var 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();
}
}
diff --git a/BepInEx.Core/Configuration/TypeConverter.cs b/BepInEx.Core/Configuration/TypeConverter.cs
index 833f401..71a92c8 100644
--- a/BepInEx.Core/Configuration/TypeConverter.cs
+++ b/BepInEx.Core/Configuration/TypeConverter.cs
@@ -1,22 +1,21 @@
using System;
-namespace BepInEx.Configuration
+namespace BepInEx.Configuration;
+
+///
+/// A serializer/deserializer combo for some type(s). Used by the config system.
+///
+public class TypeConverter
{
///
- /// A serializer/deserializer combo for some type(s). Used by the config system.
+ /// Used to serialize the type into a (hopefully) human-readable string.
+ /// Object is the instance to serialize, Type is the object's type.
///
- public class TypeConverter
- {
- ///
- /// Used to serialize the type into a (hopefully) human-readable string.
- /// Object is the instance to serialize, Type is the object's type.
- ///
- public Func