using System; 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 { private readonly BepInPlugin _ownerMetadata; internal 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> HomelessEntries { get; } = new Dictionary<ConfigDefinition, string>(); /// <summary> /// Create a list with all config entries inside of this config file. /// </summary> [Obsolete("Use GetConfigEntries instead")] public ReadOnlyCollection<ConfigDefinition> ConfigDefinitions { get { lock (_ioLock) return Entries.Keys.ToList().AsReadOnly(); } } /// <summary> /// Create an array with all config entries inside of this config file. Should be only used for metadata purposes. /// If you want to access and modify an existing setting then use <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/> /// instead with no description. /// </summary> public ConfigEntryBase[] GetConfigEntries() { lock (_ioLock) return Entries.Values.ToArray(); } /// <summary> /// Full path to the config file. The file might not exist until a setting is added and changed, or <see cref="Save"/> is called. /// </summary> public string ConfigFilePath { get; } /// <summary> /// If enabled, writes the config to disk every time a value is set. /// If 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(); private bool _disableSaving; /// <summary> /// Reloads the config from disk. Unsaved changes are lost. /// </summary> public void Reload() { lock (_ioLock) { HomelessEntries.Clear(); try { _disableSaving = true; 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('='); //actual config line if (split.Length != 2) continue; //empty/invalid line string currentKey = split[0].Trim(); string currentValue = split[1].Trim(); var definition = new ConfigDefinition(currentSection, currentKey); Entries.TryGetValue(definition, out ConfigEntryBase entry); if (entry != null) entry.SetSerializedValue(currentValue); else HomelessEntries[definition] = currentValue; } } finally { _disableSaving = false; } } OnConfigReloaded(); } /// <summary> /// Writes the config to disk. /// </summary> public void Save() { lock (_ioLock) { if (_disableSaving) return; string directoryName = Path.GetDirectoryName(ConfigFilePath); if (directoryName != null) Directory.CreateDirectory(directoryName); using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8)) { if (_ownerMetadata != null) { writer.WriteLine($"## Settings file was created by plugin {_ownerMetadata.Name} v{_ownerMetadata.Version}"); writer.WriteLine($"## Plugin GUID: {_ownerMetadata.GUID}"); writer.WriteLine(); } var allConfigEntries = Entries.Select(x => new { x.Key, entry = x.Value, value = x.Value.GetSerializedValue() }) .Concat(HomelessEntries.Select(x => new { x.Key, entry = (ConfigEntryBase)null, value = x.Value })); foreach (var sectionKv in allConfigEntries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) { // Section heading writer.WriteLine($"[{sectionKv.Key}]"); foreach (var 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> public ConfigEntry<T> GetSetting<T>(ConfigDefinition configDefinition) { lock (_ioLock) { Entries.TryGetValue(configDefinition, out var entry); return (ConfigEntry<T>)entry; } } /// <summary> /// Access one of the existing settings. If the setting has not been added yet, null is returned. /// If the setting exists but has a different type than T, an exception is thrown. /// New settings should be added with <see cref="AddSetting{T}(ConfigDefinition,T,ConfigDescription)"/>. /// </summary> /// <typeparam name="T">Type of the value contained in this setting.</typeparam> /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param> /// <param name="key">Name of the setting.</param> public ConfigEntry<T> GetSetting<T>(string section, string key) { return GetSetting<T>(new ConfigDefinition(section, key)); } /// <summary> /// Create a new setting. The setting is saved to drive and loaded automatically. /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception. /// </summary> /// <typeparam name="T">Type of the value contained in this setting.</typeparam> /// <param name="configDefinition">Section and Key of the setting.</param> /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param> /// <param name="configDescription">Description of the setting shown to the user and other metadata.</param> public ConfigEntry<T> AddSetting<T>(ConfigDefinition configDefinition, T defaultValue, ConfigDescription configDescription = null) { if (!TomlTypeConverter.CanConvert(typeof(T))) throw new ArgumentException($"Type {typeof(T)} is not supported by the config system. Supported types: {string.Join(", ", TomlTypeConverter.GetSupportedTypes().Select(x => x.Name).ToArray())}"); lock (_ioLock) { if (Entries.ContainsKey(configDefinition)) throw new ArgumentException("The setting " + configDefinition + " has already been created. Use GetSetting to get it."); try { _disableSaving = true; var entry = new ConfigEntry<T>(this, configDefinition, defaultValue, configDescription); Entries[configDefinition] = entry; if (HomelessEntries.TryGetValue(configDefinition, out string homelessValue)) { entry.SetSerializedValue(homelessValue); HomelessEntries.Remove(configDefinition); } _disableSaving = false; if (SaveOnConfigSet) Save(); return entry; } finally { _disableSaving = false; } } } /// <summary> /// Create a new setting. The setting is saved to drive and loaded automatically. /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception. /// </summary> /// <typeparam name="T">Type of the value contained in this setting.</typeparam> /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param> /// <param name="key">Name of the setting.</param> /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param> /// <param name="configDescription">Description of the setting shown to the user and other metadata.</param> public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, ConfigDescription configDescription = null) => AddSetting(new ConfigDefinition(section, key), defaultValue, configDescription); /// <summary> /// Create a new setting. The setting is saved to drive and loaded automatically. /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an exception. /// </summary> /// <typeparam name="T">Type of the value contained in this setting.</typeparam> /// <param name="section">Section/category/group of the setting. Settings are grouped by this.</param> /// <param name="key">Name of the setting.</param> /// <param name="defaultValue">Value of the setting if the setting was not created yet.</param> /// <param name="description">Simple description of the setting shown to the user.</param> public ConfigEntry<T> AddSetting<T>(string section, string key, T defaultValue, string description) => AddSetting(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description)); /// <summary> /// Access a setting. Use AddSetting and GetSetting instead. /// </summary> [Obsolete("Use AddSetting and GetSetting instead")] public ConfigWrapper<T> Wrap<T>(string section, string key, string description = null, T defaultValue = default(T)) { lock (_ioLock) { var definition = new ConfigDefinition(section, key, description); var setting = GetSetting<T>(definition) ?? AddSetting(definition, defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); return new ConfigWrapper<T>(setting); } } /// <summary> /// Access a setting. Use AddSetting and GetSetting instead. /// </summary> [Obsolete("Use AddSetting and GetSetting instead")] public ConfigWrapper<T> Wrap<T>(ConfigDefinition configDefinition, T defaultValue = default(T)) => Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue); #endregion #region Events /// <summary> /// An event that is fired every time the config is reloaded. /// </summary> public event EventHandler ConfigReloaded; /// <summary> /// Fired when one of the settings is changed. /// </summary> public event EventHandler<SettingChangedEventArgs> SettingChanged; internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase) { if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase)); if (SaveOnConfigSet) Save(); var settingChanged = SettingChanged; if (settingChanged == null) return; var args = new SettingChangedEventArgs(changedEntryBase); foreach (var callback in settingChanged.GetInvocationList().Cast<EventHandler<SettingChangedEventArgs>>()) { try { callback(sender, args); } catch (Exception e) { Logger.Log(LogLevel.Error, e); } } } private void OnConfigReloaded() { var configReloaded = ConfigReloaded; if (configReloaded == null) return; foreach (var callback in configReloaded.GetInvocationList().Cast<EventHandler>()) { try { callback(this, EventArgs.Empty); } catch (Exception e) { Logger.Log(LogLevel.Error, e); } } } #endregion } }