using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using BepInEx.Common; using BepInEx.Logging; namespace BepInEx { /// <summary> /// A helper class to handle persistent data. /// </summary> public static class Config { private static readonly Dictionary<string, Dictionary<string, string>> cache = new Dictionary<string, Dictionary<string, string>>(); private static string configPath => Path.Combine(Paths.PluginPath, "config.ini"); private static readonly Regex sanitizeKeyRegex = new Regex(@"[^a-zA-Z0-9\-\.]+"); private static void RaiseConfigReloaded() { ConfigReloaded?.Invoke(); } /// <summary> /// An event that is fired every time the config is reloaded. /// </summary> public static event Action ConfigReloaded; /// <summary> /// If enabled, writes the config to disk every time a value is set. /// </summary> public static bool SaveOnConfigSet { get; set; } = true; static Config() { if (File.Exists(configPath)) { ReloadConfig(); } else { SaveConfig(); } } /// <summary> /// Returns the value of the key if found, otherwise returns the default value. /// </summary> /// <param name="key">The key to search for.</param> /// <param name="defaultValue">The default value to return if the key is not found.</param> /// <param name="section">The section of the config to search the key for.</param> /// <returns>The value of the key.</returns> public static string GetEntry(string key, string defaultValue = "", string section = "") { try { key = Sanitize(key); section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section); if (!cache.TryGetValue(section, out Dictionary<string, string> subdict)) { SetEntry(key, defaultValue, section); return defaultValue; } if (subdict.TryGetValue(key, out string value)) return value; SetEntry(key, defaultValue, section); return defaultValue; } catch (Exception ex) { Logger.Log(LogLevel.Error | LogLevel.Message, "Unable to read config entry!"); Logger.Log(LogLevel.Error, ex); return defaultValue; } } /// <summary> /// Reloads the config from disk. Unwritten changes are lost. /// </summary> public static void ReloadConfig() { cache.Clear(); string currentSection = ""; foreach (string rawLine in File.ReadAllLines(configPath)) { string line = rawLine.Trim(); bool commentIndex = line.StartsWith(";") || line.StartsWith("#"); if (commentIndex) //trim 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 if (!cache.ContainsKey(currentSection)) cache[currentSection] = new Dictionary<string, string>(); cache[currentSection][split[0]] = split[1]; } RaiseConfigReloaded(); } /// <summary> /// Writes the config to disk. /// </summary> public static void SaveConfig() { using (StreamWriter writer = new StreamWriter(File.Create(configPath), System.Text.Encoding.UTF8)) foreach (var sectionKv in cache) { writer.WriteLine($"[{sectionKv.Key}]"); foreach (var entryKv in sectionKv.Value) writer.WriteLine($"{entryKv.Key}={entryKv.Value}"); writer.WriteLine(); } } /// <summary> /// Sets the value of the key in the config. /// </summary> /// <param name="key">The key to set the value to.</param> /// <param name="value">The value to set.</param> public static void SetEntry(string key, string value, string section = "") { try { key = Sanitize(key); section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section); if (!cache.TryGetValue(section, out Dictionary<string, string> subdict)) { subdict = new Dictionary<string, string>(); cache[section] = subdict; } subdict[key] = value; if (SaveOnConfigSet) SaveConfig(); } catch (Exception ex) { Logger.Log(LogLevel.Error | LogLevel.Message, "Unable to save config entry!"); Logger.Log(LogLevel.Error, ex); } } /// <summary> /// Returns wether a value is currently set. /// </summary> /// <param name="key">The key to check against</param> /// <param name="section">The section to check in</param> /// <returns>True if the key is present</returns> public static bool HasEntry(string key, string section = "") { key = Sanitize(key); section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section); return cache.ContainsKey(section) && cache[section].ContainsKey(key); } /// <summary> /// Removes a value from the config. /// </summary> /// <param name="key">The key to remove</param> /// <param name="section">The section to remove from</param> /// <returns>True if the key was removed</returns> public static bool UnsetEntry(string key, string section = "") { key = Sanitize(key); section = section.IsNullOrWhiteSpace() ? "Global" : Sanitize(section); if (!HasEntry(key, section)) return false; cache[section].Remove(key); return true; } /// <summary> /// Replaces any potentially breaking input with underscores. /// </summary> /// <param name="text">The text to sanitize.</param> /// <returns>Sanitized text.</returns> public static string Sanitize(string text) { return sanitizeKeyRegex.Replace(text, "_"); } #region Extensions public static string GetEntry(this BaseUnityPlugin plugin, string key, string defaultValue = "") { return GetEntry(key, defaultValue, MetadataHelper.GetMetadata(plugin).GUID); } public static void SetEntry(this BaseUnityPlugin plugin, string key, string value) { SetEntry(key, value, MetadataHelper.GetMetadata(plugin).GUID); } public static bool HasEntry(this BaseUnityPlugin plugin, string key) { return HasEntry(key, MetadataHelper.GetMetadata(plugin).GUID); } #endregion Extensions } }