using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace ServerCore; public sealed class ConfigManager { private readonly Dictionary m_key_to_source = new(); private readonly Dictionary m_path_to_jobject = new(); private readonly Dictionary m_watchers = new(); private bool m_is_detect_duplicates = true; public ConfigManager() { } public void dnableDuplicateDetection(bool enable) { m_is_detect_duplicates = enable; } public ConfigManager addJson(uint key, string path) { loadJson(path); watchFile(path, "json"); m_key_to_source[key] = (path, "json"); return this; } public ConfigManager addYaml(uint key, string path) { loadYaml(path); watchFile(path, "yaml"); m_key_to_source[key] = (path, "yaml"); return this; } private void loadJson(string path) { if (!File.Exists(path)) throw new FileNotFoundException($"Not found Json File !!! : path:{path}"); var json_text = File.ReadAllText(path); var jObject = JObject.Parse(json_text); m_path_to_jobject[path] = jObject; } private void loadYaml(string path) { if (!File.Exists(path)) throw new FileNotFoundException($"Not found Yaml File !!! : path:{path}"); var yamlText = File.ReadAllText(path); var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); var yamlObject = deserializer.Deserialize(yamlText); var serializer = new SerializerBuilder() .JsonCompatible() .Build(); var json = serializer.Serialize(yamlObject); var jObject = JObject.Parse(json); m_path_to_jobject[path] = jObject; } private void watchFile(string path, string type) { if (m_watchers.ContainsKey(path)) return; var directory_name = Path.GetDirectoryName(path); NullReferenceCheckHelper.throwIfNull(directory_name, () => $"directory_name is null !!! : path:{path}, format:{type}"); var watcher = new FileSystemWatcher(directory_name!) { Filter = Path.GetFileName(path), NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size }; watcher.Changed += (sender, e) => reloadAll(); watcher.Created += (sender, e) => reloadAll(); watcher.Renamed += (sender, e) => reloadAll(); watcher.EnableRaisingEvents = true; m_watchers[path] = watcher; } private void reloadAll() { Log.getLogger().error("Detected changes in config file !!!, Reloading..."); m_path_to_jobject.Clear(); foreach (var key in m_key_to_source.Keys) { if (false == reloadConfig(key)) { Log.getLogger().error($"Failed to reload config for key={key} during full reload"); } } } public bool reloadConfig(uint key) { if (!m_key_to_source.TryGetValue(key, out var entry)) return false; try { if (entry.type == "json") loadJson(entry.path); else if (entry.type == "yaml") loadYaml(entry.path); Log.getLogger().info($"Reloaded config for key={key}, path={entry.path}"); return true; } catch (Exception ex) { Log.getLogger().error($"Failed to reload config: key={key}, path={entry.path}, error={ex.Message}"); return false; } } public T bind(string sectionName) { if (m_path_to_jobject.Count == 0) throw new InvalidOperationException("No config sources have been loaded !!!"); var merged = new JObject(); foreach (var source in m_path_to_jobject.Values) { if (m_is_detect_duplicates) detectDuplicateKeys(merged, source); merged.Merge(source, new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Replace, MergeNullValueHandling = MergeNullValueHandling.Merge }); } return extractSection(merged, sectionName, "merged"); } public T bindFromPath(string path, string sectionName) { if (!m_path_to_jobject.TryGetValue(path, out var jObject)) throw new ArgumentException($"Config file not loaded or not found: path={path}"); return extractSection(jObject, sectionName, $"path={path}"); } public T bindFromKey(uint key, string sectionName) { if (!m_key_to_source.TryGetValue(key, out var entry)) throw new ArgumentException($"Key not found: key={key}"); if (!m_path_to_jobject.TryGetValue(entry.path, out var jObject)) throw new Exception($"JObject not found for path associated with key={key}"); return extractSection(jObject, sectionName, $"key={key}, path={entry.path}"); } public bool tryBindFromKey(uint key, string sectionName, out T? result) { result = default; try { result = bindFromKey(key, sectionName); return true; } catch { return false; } } public bool tryBindFromPath(string path, string sectionName, out T? result) { result = default; try { result = bindFromPath(path, sectionName); return true; } catch { return false; } } public JObject? getRawJsonByKey(uint key) { if (!m_key_to_source.TryGetValue(key, out var entry)) return null; return m_path_to_jobject.TryGetValue(entry.path, out var jObject) ? jObject : null; } public JObject? getRawJsonByPath(string path) { return m_path_to_jobject.TryGetValue(path, out var jObject) ? jObject : null; } public bool removeConfig(uint key) { if (!m_key_to_source.TryGetValue(key, out var entry)) return false; m_key_to_source.Remove(key); m_path_to_jobject.Remove(entry.path); if (m_watchers.TryGetValue(entry.path, out var watcher)) { watcher.EnableRaisingEvents = false; watcher.Dispose(); m_watchers.Remove(entry.path); } return true; } public IEnumerable getAllKeys() { return m_key_to_source.Keys; } public IEnumerable getAllPaths() { return m_path_to_jobject.Keys; } private T extractSection(JObject jObject, string sectionName, string context) { if (!jObject.TryGetValue(sectionName, out var token)) { token = jObject; // fallback } if (token == null) throw new Exception($"Section not found: sectionName={sectionName}, context={context}"); return token.ToObject()!; } private void detectDuplicateKeys(JObject existing, JObject incoming) { foreach (var prop in incoming.Properties()) { if (existing.ContainsKey(prop.Name)) { Log.getLogger().error($"Duplicate key detected !!!, overwritten by later value : propertyName:{prop.Name}"); } } } }