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
{
///
/// A helper class to handle persistent data.
///
public class ConfigFile
{
// Need to be lazy evaluated to not cause problems for unit tests
private static ConfigFile _coreConfig;
internal static ConfigFile CoreConfig => _coreConfig ?? (_coreConfig = new ConfigFile(Paths.BepInExConfigPath, true));
protected Dictionary Entries { get; } = new Dictionary();
[Obsolete("Use ConfigEntries instead")]
public ReadOnlyCollection ConfigDefinitions => Entries.Keys.ToList().AsReadOnly();
public ReadOnlyCollection ConfigEntries => Entries.Values.ToList().AsReadOnly();
public string ConfigFilePath { get; }
///
/// If enabled, writes the config to disk every time a value is set. If disabled, you have to manually save or the changes will be lost!
///
public bool SaveOnConfigSet { get; set; } = true;
public ConfigFile(string configPath, bool saveOnInit)
{
if (configPath == null) throw new ArgumentNullException(nameof(configPath));
configPath = Path.GetFullPath(configPath);
ConfigFilePath = configPath;
if (File.Exists(ConfigFilePath))
{
Reload();
}
else if (saveOnInit)
{
Save();
}
StartWatching();
}
#region Save/Load
private readonly object _ioLock = new object();
///
/// Reloads the config from disk. Unsaved changes are lost.
///
public void Reload()
{
lock (_ioLock)
{
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 ConfigEntry entry);
if (entry == null)
{
entry = new ConfigEntry(this, definition);
Entries[definition] = entry;
}
entry.SetSerializedValue(currentValue, true, this);
}
}
OnConfigReloaded();
}
///
/// Writes the config to disk.
///
public void Save()
{
lock (_ioLock)
{
StopWatching();
string directoryName = Path.GetDirectoryName(ConfigFilePath);
if (directoryName != null) Directory.CreateDirectory(directoryName);
using (var writer = new StreamWriter(File.Create(ConfigFilePath), Encoding.UTF8))
{
foreach (var sectionKv in Entries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key))
{
// Section heading
writer.WriteLine($"[{sectionKv.Key}]");
foreach (var configEntry in sectionKv.Select(x => x.Value))
{
writer.WriteLine();
configEntry.WriteDescription(writer);
writer.WriteLine($"{configEntry.Definition.Key} = {configEntry.GetSerializedValue()}");
}
writer.WriteLine();
}
}
StartWatching();
}
}
#endregion
#region Wraps
public ConfigWrapper Wrap(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())}");
Entries.TryGetValue(configDefinition, out var entry);
if (entry == null)
{
entry = new ConfigEntry(this, configDefinition, typeof(T), defaultValue);
Entries[configDefinition] = entry;
}
else
{
entry.SetTypeAndDefaultValue(typeof(T), defaultValue, !Equals(defaultValue, default(T)));
}
if (configDescription != null)
{
if (entry.Description != null)
Logger.Log(LogLevel.Warning, $"Tried to add configDescription to setting {configDefinition} when it already had one defined. Only add configDescription once or a random one will be used.");
entry.Description = configDescription;
}
return new ConfigWrapper(entry);
}
[Obsolete("Use other Wrap overloads instead")]
public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default(T))
=> Wrap(new ConfigDefinition(section, key), defaultValue, string.IsNullOrEmpty(description) ? null : new ConfigDescription(description));
public ConfigWrapper Wrap(string section, string key, T defaultValue, ConfigDescription configDescription = null)
=> Wrap(new ConfigDefinition(section, key), defaultValue, configDescription);
#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;
protected internal void OnSettingChanged(object sender, ConfigEntry changedEntry)
{
if (changedEntry == null) throw new ArgumentNullException(nameof(changedEntry));
if (SettingChanged != null)
{
var args = new SettingChangedEventArgs(changedEntry);
foreach (var callback in SettingChanged.GetInvocationList().Cast>())
{
try
{
callback(sender, args);
}
catch (Exception e)
{
Logger.Log(LogLevel.Error, e);
}
}
}
// todo better way to prevent write loop? maybe do some caching?
if (sender != this && SaveOnConfigSet)
Save();
}
protected void OnConfigReloaded()
{
if (ConfigReloaded != null)
{
foreach (var callback in ConfigReloaded.GetInvocationList().Cast())
{
try
{
callback(this, EventArgs.Empty);
}
catch (Exception e)
{
Logger.Log(LogLevel.Error, e);
}
}
}
}
#endregion
#region File watcher
private FileSystemWatcher _watcher;
///
/// Start watching the config file on disk for changes.
///
public void StartWatching()
{
lock (_ioLock)
{
if (_watcher != null) return;
_watcher = new FileSystemWatcher
{
Path = Path.GetDirectoryName(ConfigFilePath) ?? throw new ArgumentException("Invalid config path"),
Filter = Path.GetFileName(ConfigFilePath),
IncludeSubdirectories = false,
NotifyFilter = NotifyFilters.LastWrite,
EnableRaisingEvents = true
};
_watcher.Changed += (sender, args) => Reload();
}
}
///
/// Stop watching the config file on disk for changes.
///
public void StopWatching()
{
lock (_ioLock)
{
_watcher?.Dispose();
_watcher = null;
}
}
~ConfigFile()
{
StopWatching();
}
#endregion
}
}