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
{
///
/// A helper class to handle persistent data. All public methods are thread-safe.
///
public class ConfigFile : IDictionary
{
private readonly BepInPlugin _ownerMetadata;
public static ConfigFile CoreConfig { get; } = new ConfigFile(Paths.BepInExConfigPath, true);
///
/// All config entries inside
///
protected Dictionary Entries { get; } = new Dictionary();
private Dictionary OrphanedEntries { get; } = new Dictionary();
///
/// 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();
}
}
}
///
/// 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();
}
///
/// 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 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();
}
}
#region Save/Load
private readonly object _ioLock = new object();
///
/// Reloads the config from disk. Unsaved changes are lost.
///
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('='); //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();
}
///
/// Writes the config to disk.
///
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
///
/// 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)
{
return 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)
{
return 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)
{
return 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 string 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(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(setting);
}
}
///
/// Access a setting. Use Bind instead.
///
[Obsolete("Use Bind instead")]
public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default(T))
=> 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 (var callback in settingChanged.GetInvocationList().Cast>())
{
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())
{
try { callback(this, EventArgs.Empty); }
catch (Exception e) { Logger.Log(LogLevel.Error, e); }
}
}
#endregion
///
public IEnumerator> GetEnumerator()
{
// We can't really do a read lock for this
return Entries.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return 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");
}
///
public ConfigEntryBase this[ConfigDefinition key]
{
get
{
lock (_ioLock)
return Entries[key];
}
}
///
///
///
///
///
public ConfigEntryBase this[string section, string key]
=> this[new ConfigDefinition(section, 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.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();
}
}
}
}