using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using BepInEx.Logging; 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) { } /// /// 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 (configPath == null) throw new ArgumentNullException(nameof(configPath)); configPath = Path.GetFullPath(configPath); ConfigFilePath = configPath; if (File.Exists(ConfigFilePath)) Reload(); else if (saveOnInit) Save(); } public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true); /// /// 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) { 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) { return Entries.Count; } } } /// 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[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. /// public ICollection 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() { 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)) { 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; } } OnConfigReloaded(); } /// /// Writes the config to disk. /// public void Save() { lock (_ioLock) { var directoryName = Path.GetDirectoryName(ConfigFilePath); if (directoryName != null) Directory.CreateDirectory(directoryName); using (var writer = new StreamWriter(ConfigFilePath, false, Utility.UTF8NoBom)) { 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) { if (GenerateSettingDescriptions) { writer.WriteLine(); configEntry.entry?.WriteDescription(writer); } writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}"); } writer.WriteLine(); } } } } #endregion #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/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) { lock (_ioLock) { if (Entries.TryGetValue(configDefinition, out var rawEntry)) { 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); /// /// 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 (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; } } /// /// 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 }