123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.IO;
- using System.Linq;
- using System.Text;
- using BepInEx.Logging;
- namespace BepInEx.Configuration
- {
- /// <summary>
- /// A helper class to handle persistent data. All public methods are thread-safe.
- /// </summary>
- public class ConfigFile : IDictionary<ConfigDefinition, ConfigEntryBase>
- {
- private readonly BepInPlugin _ownerMetadata;
- public static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true);
- /// <summary>
- /// All config entries inside
- /// </summary>
- protected Dictionary<ConfigDefinition, ConfigEntryBase> Entries { get; } = new Dictionary<ConfigDefinition, ConfigEntryBase>();
- private Dictionary<ConfigDefinition, string> OrphanedEntries { get; } = new Dictionary<ConfigDefinition, string>();
- /// <summary>
- /// Create a list with all config entries inside of this config file.
- /// </summary>
- [Obsolete("Use Keys instead")]
- public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions
- {
- get
- {
- lock (_ioLock)
- {
- return Entries.Keys.ToList().AsReadOnly();
- }
- }
- }
-
- /// <summary>
- /// Create an array with all config entries inside of this config file. Should be only used for metadata purposes.
- /// If you want to access and modify an existing setting then use <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>
- /// instead with no description.
- /// </summary>
- [Obsolete("Use Values instead")]
- public ConfigEntryBase[] GetConfigEntries()
- {
- lock (_ioLock)
- return Entries.Values.ToArray();
- }
- /// <summary>
- /// Full path to the config file. The file might not exist until a setting is added and changed, or <see cref="Save"/> is called.
- /// </summary>
- public string ConfigFilePath { get; }
- /// <summary>
- /// If enabled, writes the config to disk every time a value is set.
- /// If disabled, you have to manually use <see cref="Save"/> or the changes will be lost!
- /// </summary>
- public bool SaveOnConfigSet { get; set; } = true;
- /// <inheritdoc cref="ConfigFile(string, bool, BepInPlugin)"/>
- public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { }
- /// <summary>
- /// Create a new config file at the specified config path.
- /// </summary>
- /// <param name="configPath">Full path to a file that contains settings. The file will be created as needed.</param>
- /// <param name="saveOnInit">If the config file/directory doesn't exist, create it immediately.</param>
- /// <param name="ownerMetadata">Information about the plugin that owns this setting file.</param>
- public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata)
- {
- _ownerMetadata = ownerMetadata;
- if (configPath == null) throw new ArgumentNullException(nameof(configPath));
- configPath = Path.GetFullPath(configPath);
- ConfigFilePath = configPath;
- if (File.Exists(ConfigFilePath))
- {
- Reload();
- }
- else if (saveOnInit)
- {
- Save();
- }
- }
- #region Save/Load
- private readonly object _ioLock = new object();
- /// <summary>
- /// Reloads the config from disk. Unsaved changes are lost.
- /// </summary>
- public void Reload()
- {
- lock (_ioLock)
- {
- OrphanedEntries.Clear();
- string currentSection = string.Empty;
- foreach (string rawLine in File.ReadAllLines(ConfigFilePath))
- {
- string line = rawLine.Trim();
- if (line.StartsWith("#")) //comment
- continue;
- if (line.StartsWith("[") && line.EndsWith("]")) //section
- {
- currentSection = line.Substring(1, line.Length - 2);
- continue;
- }
- string[] split = line.Split(new[] { '=' }, 2); //actual config line
- if (split.Length != 2)
- continue; //empty/invalid line
- string currentKey = split[0].Trim();
- string currentValue = split[1].Trim();
- var definition = new ConfigDefinition(currentSection, currentKey);
- Entries.TryGetValue(definition, out ConfigEntryBase entry);
- if (entry != null)
- entry.SetSerializedValue(currentValue);
- else
- OrphanedEntries[definition] = currentValue;
- }
- }
- OnConfigReloaded();
- }
- /// <summary>
- /// Writes the config to disk.
- /// </summary>
- public void Save()
- {
- lock (_ioLock)
- {
- string directoryName = Path.GetDirectoryName(ConfigFilePath);
- if (directoryName != null) Directory.CreateDirectory(directoryName);
- using (var writer = new StreamWriter(ConfigFilePath, false, Encoding.UTF8))
- {
- if (_ownerMetadata != null)
- {
- writer.WriteLine($"## Settings file was created by plugin {_ownerMetadata.Name} v{_ownerMetadata.Version}");
- writer.WriteLine($"## Plugin GUID: {_ownerMetadata.GUID}");
- writer.WriteLine();
- }
- var allConfigEntries = Entries.Select(x => new { x.Key, entry = x.Value, value = x.Value.GetSerializedValue() })
- .Concat(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();
- configEntry.entry?.WriteDescription(writer);
- writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}");
- }
- writer.WriteLine();
- }
- }
- }
- }
- #endregion
- #region Wraps
- /// <summary>
- /// Access one of the existing settings. If the setting has not been added yet, null is returned.
- /// If the setting exists but has a different type than T, an exception is thrown.
- /// New settings should be added with <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="configDefinition">Section and Key of the setting.</param>
- [Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
- public ConfigEntry<T> GetSetting<T>(ConfigDefinition configDefinition)
- {
- return TryGetEntry<T>(configDefinition, out var entry)
- ? entry
- : null;
- }
- /// <summary>
- /// Access one of the existing settings. If the setting has not been added yet, null is returned.
- /// If the setting exists but has a different type than T, an exception is thrown.
- /// New settings should be added with <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
- /// <param name="key">Name of the setting.</param>
- [Obsolete("Use ConfigFile[key] or TryGetEntry instead")]
- public ConfigEntry<T> GetSetting<T>(string section, string key)
- {
- return TryGetEntry<T>(section, key, out var entry)
- ? entry
- : null;
- }
- /// <summary>
- /// 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 <see cref="Bind{T}(BepInEx.Configuration.ConfigDefinition,T,BepInEx.Configuration.ConfigDescription)"/>.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="configDefinition">Section and Key of the setting.</param>
- /// <param name="entry">The ConfigEntry value to return.</param>
- public bool TryGetEntry<T>(ConfigDefinition configDefinition, out ConfigEntry<T> entry)
- {
- lock (_ioLock)
- {
- if (Entries.TryGetValue(configDefinition, out var rawEntry))
- {
- entry = (ConfigEntry<T>)rawEntry;
- return true;
- }
- entry = null;
- return false;
- }
- }
- /// <summary>
- /// Access one of the existing settings. If the setting has not been added yet, null is returned.
- /// If the setting exists but has a different type than T, an exception is thrown.
- /// New settings should be added with <see cref="Bind{T}(BepInEx.Configuration.ConfigDefinition,T,BepInEx.Configuration.ConfigDescription)"/>.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
- /// <param name="key">Name of the setting.</param>
- /// <param name="entry">The ConfigEntry value to return.</param>
- public bool TryGetEntry<T>(string section, string key, out ConfigEntry<T> entry)
- {
- return TryGetEntry<T>(new ConfigDefinition(section, key), out entry);
- }
- /// <summary>
- /// Create a new setting. The setting is saved to drive and loaded automatically.
- /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="configDefinition">Section and Key of the setting.</param>
- /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
- /// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
- public ConfigEntry<T> Bind<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
- {
- if (!TomlTypeConverter.CanConvert(typeof(T)))
- throw new ArgumentException($"Type {typeof(T)} is not supported by the config system. Supported types: {string.Join(", ", TomlTypeConverter.GetSupportedTypes().Select(x => x.Name).ToArray())}");
- lock (_ioLock)
- {
- if (Entries.TryGetValue(configDefinition, out var rawEntry))
- return (ConfigEntry<T>)rawEntry;
- var entry = new ConfigEntry<T>(this, configDefinition, defaultValue, configDescription);
- Entries[configDefinition] = entry;
- if (OrphanedEntries.TryGetValue(configDefinition, out string homelessValue))
- {
- entry.SetSerializedValue(homelessValue);
- OrphanedEntries.Remove(configDefinition);
- }
- if (SaveOnConfigSet)
- Save();
- return entry;
- }
- }
- /// <summary>
- /// Create a new setting. The setting is saved to drive and loaded automatically.
- /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
- /// <param name="key">Name of the setting.</param>
- /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
- /// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
- public ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
- => Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
- /// <summary>
- /// Create a new setting. The setting is saved to drive and loaded automatically.
- /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
- /// <param name="key">Name of the setting.</param>
- /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
- /// <param name="description">Simple description of the setting shown to the user.</param>
- public ConfigEntry<T> Bind<T>(string section, string key, T defaultValue, string description)
- => Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
- /// <summary>
- /// Create a new setting. The setting is saved to drive and loaded automatically.
- /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="configDefinition">Section and Key of the setting.</param>
- /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
- /// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
- [Obsolete("Use Bind instead")]
- public ConfigEntry<T> AddSetting<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null)
- => Bind(configDefinition, defaultValue, configDescription);
- /// <summary>
- /// Create a new setting. The setting is saved to drive and loaded automatically.
- /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
- /// <param name="key">Name of the setting.</param>
- /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
- /// <param name="configDescription">Description of the setting shown to the user and other metadata.</param>
- [Obsolete("Use Bind instead")]
- public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null)
- => Bind(new ConfigDefinition(section, key), defaultValue, configDescription);
- /// <summary>
- /// Create a new setting. The setting is saved to drive and loaded automatically.
- /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception.
- /// </summary>
- /// <typeparam name="T">Type of the value contained in this setting.</typeparam>
- /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param>
- /// <param name="key">Name of the setting.</param>
- /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param>
- /// <param name="description">Simple description of the setting shown to the user.</param>
- [Obsolete("Use Bind instead")]
- public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, string description)
- => Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description));
- /// <summary>
- /// Access a setting. Use Bind instead.
- /// </summary>
- [Obsolete("Use Bind instead")]
- public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T))
- {
- lock (_ioLock)
- {
- var definition = new ConfigDefinition(section, key, description);
- var setting = Bind(definition, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
- return new ConfigWrapper<T>(setting);
- }
- }
- /// <summary>
- /// Access a setting. Use Bind instead.
- /// </summary>
- [Obsolete("Use Bind instead")]
- public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T))
- => Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue);
- #endregion
- #region Events
- /// <summary>
- /// An event that is fired every time the config is reloaded.
- /// </summary>
- public event EventHandler ConfigReloaded;
- /// <summary>
- /// Fired when one of the settings is changed.
- /// </summary>
- public event EventHandler<SettingChangedEventArgs> SettingChanged;
- internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase)
- {
- if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase));
- if (SaveOnConfigSet)
- Save();
- var settingChanged = SettingChanged;
- if (settingChanged == null) return;
- var args = new SettingChangedEventArgs(changedEntryBase);
- foreach (var callback in settingChanged.GetInvocationList().Cast<EventHandler<SettingChangedEventArgs>>())
- {
- try { callback(sender, args); }
- catch (Exception e) { Logger.Log(LogLevel.Error, e); }
- }
- }
- private void OnConfigReloaded()
- {
- var configReloaded = ConfigReloaded;
- if (configReloaded == null) return;
- foreach (var callback in configReloaded.GetInvocationList().Cast<EventHandler>())
- {
- try { callback(this, EventArgs.Empty); }
- catch (Exception e) { Logger.Log(LogLevel.Error, e); }
- }
- }
- #endregion
- /// <inheritdoc />
- public IEnumerator<KeyValuePair<ConfigDefinition, ConfigEntryBase>> GetEnumerator()
- {
- // We can't really do a read lock for this
- return Entries.GetEnumerator();
- }
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
- void ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>.Add(KeyValuePair<ConfigDefinition, ConfigEntryBase> item)
- {
- lock (_ioLock)
- Entries.Add(item.Key, item.Value);
- }
- /// <inheritdoc />
- public bool Contains(KeyValuePair<ConfigDefinition, ConfigEntryBase> item)
- {
- lock (_ioLock)
- return ((ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>)Entries).Contains(item);
- }
- void ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>.CopyTo(KeyValuePair<ConfigDefinition, ConfigEntryBase>[] array, int arrayIndex)
- {
- lock (_ioLock)
- ((ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>)Entries).CopyTo(array, arrayIndex);
- }
- bool ICollection<KeyValuePair<ConfigDefinition, ConfigEntryBase>>.Remove(KeyValuePair<ConfigDefinition, ConfigEntryBase> item)
- {
- lock (_ioLock)
- return Entries.Remove(item.Key);
- }
- /// <inheritdoc />
- public int Count
- {
- get
- {
- lock (_ioLock)
- return Entries.Count;
- }
- }
- /// <inheritdoc />
- public bool IsReadOnly => false;
- /// <inheritdoc />
- public bool ContainsKey(ConfigDefinition key)
- {
- lock (_ioLock)
- return Entries.ContainsKey(key);
- }
- /// <inheritdoc />
- public void Add(ConfigDefinition key, ConfigEntryBase value)
- {
- throw new InvalidOperationException("Directly adding a config entry is not supported");
- }
- /// <inheritdoc />
- public bool Remove(ConfigDefinition key)
- {
- lock (_ioLock)
- return Entries.Remove(key);
- }
- /// <inheritdoc />
- public void Clear()
- {
- lock (_ioLock)
- Entries.Clear();
- }
- bool IDictionary<ConfigDefinition, ConfigEntryBase>.TryGetValue(ConfigDefinition key, out ConfigEntryBase value)
- {
- lock (_ioLock)
- return Entries.TryGetValue(key, out value);
- }
- /// <inheritdoc />
- ConfigEntryBase IDictionary<ConfigDefinition, ConfigEntryBase>.this[ConfigDefinition key]
- {
- get
- {
- lock (_ioLock)
- return Entries[key];
- }
- set => throw new InvalidOperationException("Directly setting a config entry is not supported");
- }
- /// <inheritdoc />
- public ConfigEntryBase this[ConfigDefinition key]
- {
- get
- {
- lock (_ioLock)
- return Entries[key];
- }
- }
- /// <summary>
- ///
- /// </summary>
- /// <param name="section"></param>
- /// <param name="key"></param>
- public ConfigEntryBase this[string section, string key]
- => this[new ConfigDefinition(section, key)];
- /// <summary>
- /// Returns the ConfigDefinitions that the ConfigFile contains.
- /// <para>Creates a new array when the property is accessed. Thread-safe.</para>
- /// </summary>
- public ICollection<ConfigDefinition> Keys
- {
- get
- {
- lock (_ioLock)
- return Entries.Keys.ToArray();
- }
- }
- /// <summary>
- /// Returns the ConfigEntryBase values that the ConfigFile contains.
- /// <para>Creates a new array when the property is accessed. Thread-safe.</para>
- /// </summary>
- ICollection<ConfigEntryBase> IDictionary<ConfigDefinition, ConfigEntryBase>.Values
- {
- get
- {
- lock (_ioLock)
- return Entries.Values.ToArray();
- }
- }
- }
- }
|