using Dalamud.Interface.Internal; using Dalamud.Interface.Windowing; using Dalamud.Logging; using HSUI.Config.Profiles; using HSUI.Config.Tree; using HSUI.Config.Windows; using HSUI.Helpers; using HSUI.Interface; using HSUI.Interface.EnemyList; using HSUI.Interface.GeneralElements; using HSUI.Interface.Jobs; using HSUI.Interface.Party; using HSUI.Interface.PartyCooldowns; using HSUI.Interface.StatusEffects; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; namespace HSUI.Config { public delegate void ConfigurationManagerEventHandler(ConfigurationManager configurationManager); public delegate void StrataLevelsEventHandler(ConfigurationManager configurationManager, PluginConfigObject config); public delegate void GlobalVisibilityEventHandler(ConfigurationManager configurationManager, VisibilityConfig config); public class ConfigurationManager : IDisposable { public static ConfigurationManager Instance { get; private set; } = null!; private BaseNode _configBaseNode; private Dictionary _configBaseNodeByProfile; public BaseNode ConfigBaseNode { get => _configBaseNode; set { _configBaseNode = value; _mainConfigWindow.node = value; } } private WindowSystem _windowSystem; private MainConfigWindow _mainConfigWindow; private ChangelogWindow _changelogWindow; private GridWindow _gridWindow; public bool IsConfigWindowOpened => _mainConfigWindow.IsOpen; public bool IsChangelogWindowOpened => _changelogWindow.IsOpen; public bool ShowingModalWindow = false; public GradientDirection GradientDirection { get { var config = Instance.GetConfigObject(); return config != null ? config.GradientDirection : GradientDirection.None; } } public bool OverrideDalamudStyle { get { HUDOptionsConfig config = Instance.GetConfigObject(); return config != null ? config.OverrideDalamudStyle : true; } } public CultureInfo ActiveCultreInfo { get { HUDOptionsConfig config = Instance.GetConfigObject(); return config == null || config.UseRegionalNumberFormats ? CultureInfo.CurrentCulture : CultureInfo.InvariantCulture; } } public string ConfigDirectory; public string CurrentVersion => Plugin.Version; public string? PreviousVersion { get; private set; } = null; private bool _needsProfileUpdate = false; private bool _lockHUD = true; public bool LockHUD { get => _lockHUD; set { if (_lockHUD == value) { return; } _lockHUD = value; _mainConfigWindow.IsOpen = value; _gridWindow.IsOpen = !value; LockEvent?.Invoke(this); if (_lockHUD) { ConfigBaseNode.NeedsSave = true; } } } public bool ShowHUD = true; public event ConfigurationManagerEventHandler? ResetEvent; public event ConfigurationManagerEventHandler? LockEvent; public event ConfigurationManagerEventHandler? ConfigClosedEvent; public event StrataLevelsEventHandler? StrataLevelsChangedEvent; public event GlobalVisibilityEventHandler? GlobalVisibilityEvent; public ConfigurationManager() { ConfigDirectory = Plugin.PluginInterface.GetPluginConfigDirectory(); _configBaseNodeByProfile = new Dictionary(); _configBaseNode = new BaseNode(); InitializeBaseNode(_configBaseNode); _configBaseNode.ConfigObjectResetEvent += OnConfigObjectReset; _mainConfigWindow = new MainConfigWindow("HSUI Settings"); _mainConfigWindow.node = _configBaseNode; _mainConfigWindow.CloseAction = () => { ConfigClosedEvent?.Invoke(this); if (ConfigBaseNode.NeedsSave) { SaveConfigurations(); } if (_needsProfileUpdate) { UpdateCurrentProfile(); _needsProfileUpdate = false; } }; string changelog = LoadChangelog(); _changelogWindow = new ChangelogWindow("HSUI Changelog v" + Plugin.Version, changelog); _gridWindow = new GridWindow("Grid ##HSUI"); _windowSystem = new WindowSystem("HSUI_Windows"); _windowSystem.AddWindow(_mainConfigWindow); _windowSystem.AddWindow(_changelogWindow); _windowSystem.AddWindow(_gridWindow); CheckVersion(); Plugin.ClientState.Logout += OnLogout; Plugin.ClientState.TerritoryChanged += OnTerritoryChanged; Plugin.JobChangedEvent += OnJobChanged; _configBaseNode.CreateNodesIfNeeded(); } ~ConfigurationManager() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected void Dispose(bool disposing) { if (!disposing) { return; } ConfigBaseNode.ConfigObjectResetEvent -= OnConfigObjectReset; Plugin.ClientState.Logout -= OnLogout; Plugin.ClientState.TerritoryChanged -= OnTerritoryChanged; Plugin.JobChangedEvent -= OnJobChanged; Instance = null!; } public static void Initialize() { Instance = new ConfigurationManager(); } private void OnConfigObjectReset(BaseNode sender) { ResetEvent?.Invoke(this); } private void OnLogout(int type, int code) { SaveConfigurations(); ProfilesManager.Instance?.SaveCurrentProfile(); } private void OnTerritoryChanged(ushort territoryId) { // Persist config on teleport/zone change so changes aren't lost during loading screens if (ConfigBaseNode.NeedsSave) SaveConfigurations(); } private void OnJobChanged(uint jobId) { UpdateCurrentProfile(); } private string LoadChangelog() { string path = Path.Combine(Plugin.AssemblyLocation, "changelog.md"); try { string fullChangelog = File.ReadAllText(path); string versionChangelog = fullChangelog.Split("#", StringSplitOptions.RemoveEmptyEntries)[0]; return versionChangelog.Replace(Plugin.Version, ""); } catch (Exception e) { Plugin.Logger.Error("Error loading changelog: " + e.Message); } return ""; } private void CheckVersion() { string path = Path.Combine(ConfigDirectory, "version"); bool needsBackup = false; try { bool needsWrite = false; if (!File.Exists(path)) { needsWrite = true; } else { PreviousVersion = File.ReadAllText(path); if (PreviousVersion != Plugin.Version) { needsWrite = true; needsBackup = true; } } _changelogWindow.IsOpen = needsWrite; _changelogWindow.AutoClose = true; if (needsWrite) { File.WriteAllText(path, Plugin.Version); } } catch (Exception e) { Plugin.Logger.Error("Error checking version: " + e.Message); } try { if (needsBackup && PreviousVersion != null) { BackupFiles(PreviousVersion); } } catch (Exception e) { Plugin.Logger.Error("Error making backup: " + e.Message); } } private void BackupFiles(string version) { string backupsRoot = Path.Combine(ConfigDirectory, "Backups"); if (!Directory.Exists(backupsRoot)) { Directory.CreateDirectory(backupsRoot); } string backupPath = Path.Combine(backupsRoot, version); foreach (string folderPath in Directory.GetDirectories(ConfigDirectory, "*", SearchOption.AllDirectories)) { if (folderPath.Contains("Backups")) { continue; } Directory.CreateDirectory(folderPath.Replace(ConfigDirectory, backupPath)); } foreach (string filePath in Directory.GetFiles(ConfigDirectory, "*.*", SearchOption.AllDirectories)) { File.Copy(filePath, filePath.Replace(ConfigDirectory, backupPath), true); } } #region strata public void OnStrataLevelChanged(PluginConfigObject config) { StrataLevelsChangedEvent?.Invoke(this, config); } #endregion #region visibility public void OnGlobalVisibilityChanged(VisibilityConfig config) { GlobalVisibilityEvent?.Invoke(this, config); } #endregion #region windows public void ToggleConfigWindow() { _mainConfigWindow.Toggle(); } public void OpenConfigWindow() { _mainConfigWindow.IsOpen = true; } public void CloseConfigWindow() { _mainConfigWindow.IsOpen = false; } public void OpenChangelogWindow() { _changelogWindow.IsOpen = true; } public void Draw() { _windowSystem.Draw(); } public void AddExtraSectionNode(SectionNode node) { ConfigBaseNode.AddExtraSectionNode(node); } #endregion #region config getters and setters public PluginConfigObject GetConfigObjectForType(Type type) { MethodInfo? genericMethod = GetType().GetMethod("GetConfigObject"); MethodInfo? method = genericMethod?.MakeGenericMethod(type); return (PluginConfigObject)method?.Invoke(this, null)!; } public T GetConfigObject() where T : PluginConfigObject => ConfigBaseNode.GetConfigObject()!; public static PluginConfigObject GetDefaultConfigObjectForType(Type type) { MethodInfo? method = type.GetMethod("DefaultConfig", BindingFlags.Public | BindingFlags.Static); return (PluginConfigObject)method?.Invoke(null, null)!; } public ConfigPageNode GetConfigPageNode() where T : PluginConfigObject => ConfigBaseNode.GetConfigPageNode()!; public void SetConfigObject(PluginConfigObject configObject) => ConfigBaseNode.SetConfigObject(configObject); public List GetObjects() => ConfigBaseNode.GetObjects(); #endregion #region load / save / profiles public bool IsFreshInstall() { return Directory.GetDirectories(ConfigDirectory).Length == 0; } public void LoadOrInitializeFiles() { try { if (!IsFreshInstall()) { LoadConfigurations(); // gotta save after initial load store possible version update changes right away SaveConfigurations(true); } } catch (Exception e) { Plugin.Logger.Error("Error initializing configurations: " + e.Message); if (e.StackTrace != null) { Plugin.Logger.Error(e.StackTrace); } } } public void ForceNeedsSave() { ConfigBaseNode.NeedsSave = true; } public void LoadConfigurations() { PerformV2Migration(); ConfigBaseNode.Load(ConfigDirectory); } public void SaveConfigurations(bool forced = false) { if (!forced && !ConfigBaseNode.NeedsSave) { return; } ConfigBaseNode.Save(ConfigDirectory); ProfilesManager.Instance?.SaveCurrentProfile(); ConfigBaseNode.NeedsSave = false; } public void PerformV2Migration() { // create necessary folders string[] newFolders = new string[] { "Other Elements", "Customization" }; foreach (string folder in newFolders) { string path = Path.Combine(ConfigDirectory, folder); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } } // move files Dictionary files = new Dictionary() { ["Misc\\Experience Bar.json"] = "Other Elements\\Experience Bar.json", ["Misc\\GCD Indicator.json"] = "Other Elements\\GCD Indicator.json", ["Misc\\Limit Break.json"] = "Other Elements\\Limit Break.json", ["Misc\\MP Ticker.json"] = "Other Elements\\MP Ticker.json", ["Misc\\Pull Timer.json"] = "Other Elements\\Pull Timer.json", ["Misc\\Fonts.json"] = "Customization\\Fonts.json" }; foreach (string key in files.Keys) { string v1Path = Path.Combine(ConfigDirectory, key); string v2Path = Path.Combine(ConfigDirectory, files[key]); try { if (File.Exists(v1Path) && !File.Exists(v2Path)) { File.Move(v1Path, v2Path); } } catch (Exception e) { Plugin.Logger.Error("Error migrating file \"" + v1Path + "\" to v2 config structure: " + e.Message); } } } public void UpdateCurrentProfile() { // dont update the profile on job change when the config window is opened if (_mainConfigWindow.IsOpen) { _needsProfileUpdate = true; return; } ProfilesManager.Instance?.UpdateCurrentProfile(); } public string? ExportCurrentConfigs() { return ConfigBaseNode.GetBase64String(); } public void OnProfileDeleted(string profileName) { try { _configBaseNodeByProfile.Remove(profileName); } catch { } } public bool ImportProfile(string oldProfileName, string profileName, string rawString, bool forceLoad = false) { // cache old profile _configBaseNodeByProfile[oldProfileName] = ConfigBaseNode; // load profile from cache or from rawString BaseNode? loadedNode = null; if (forceLoad || !_configBaseNodeByProfile.TryGetValue(profileName, out loadedNode)) { ImportProfileNonCached(rawString, out loadedNode); } if (loadedNode == null) { return false; } if (IsConfigWindowOpened || string.IsNullOrEmpty(loadedNode.SelectedOptionName)) { loadedNode.SelectedOptionName = ConfigBaseNode.SelectedOptionName; loadedNode.RefreshSelectedNode(); } ConfigBaseNode.ConfigObjectResetEvent -= OnConfigObjectReset; ConfigBaseNode = loadedNode; ConfigBaseNode.ConfigObjectResetEvent += OnConfigObjectReset; PerformV2Migration(); ResetEvent?.Invoke(this); return true; } private bool ImportProfileNonCached(string rawString, out BaseNode? node) { List importStrings = new List(rawString.Trim().Split(new string[] { "|" }, StringSplitOptions.RemoveEmptyEntries)); ImportData[] imports = importStrings.Select(str => new ImportData(str)).ToArray(); node = new BaseNode(); InitializeBaseNode(node); Dictionary oldConfigObjects = new Dictionary(); foreach (ImportData importData in imports) { PluginConfigObject? config = importData.GetObject(); if (config == null) { return false; } if (!node.SetConfigObject(config)) { oldConfigObjects.Add(config.GetType(), config); } } if (ProfilesManager.Instance != null) { node.AddExtraSectionNode(ProfilesManager.Instance.ProfilesNode); } return true; } #endregion #region initialization private static void InitializeBaseNode(BaseNode node) { // creates node tree in the right order... foreach (Type type in ConfigObjectTypes) { var genericMethod = node.GetType().GetMethod("GetConfigPageNode"); var method = genericMethod?.MakeGenericMethod(type); method?.Invoke(node, null); } } private static Type[] ConfigObjectTypes = new Type[] { // Unit Frames typeof(PlayerUnitFrameConfig), typeof(TargetUnitFrameConfig), typeof(TargetOfTargetUnitFrameConfig), typeof(FocusTargetUnitFrameConfig), // Mana Bars typeof(PlayerPrimaryResourceConfig), typeof(TargetPrimaryResourceConfig), typeof(TargetOfTargetPrimaryResourceConfig), typeof(FocusTargetPrimaryResourceConfig), // Castbars typeof(PlayerCastbarConfig), typeof(TargetCastbarConfig), typeof(TargetOfTargetCastbarConfig), typeof(FocusTargetCastbarConfig), // Buffs and Debuffs typeof(PlayerBuffsListConfig), typeof(PlayerDebuffsListConfig), typeof(TargetBuffsListConfig), typeof(TargetDebuffsListConfig), typeof(FocusTargetBuffsListConfig), typeof(FocusTargetDebuffsListConfig), typeof(CustomEffectsListConfig), // Nameplates typeof(NameplatesGeneralConfig), typeof(PlayerNameplateConfig), typeof(EnemyNameplateConfig), typeof(PartyMembersNameplateConfig), typeof(AllianceMembersNameplateConfig), typeof(FriendPlayerNameplateConfig), typeof(OtherPlayerNameplateConfig), typeof(PetNameplateConfig), typeof(NPCNameplateConfig), typeof(MinionNPCNameplateConfig), typeof(ObjectsNameplateConfig), // Party Frames typeof(PartyFramesConfig), typeof(AllianceFramesHealthBarsConfig), typeof(AllianceFramesManaBarConfig), typeof(AllianceFramesCastbarConfig), typeof(AllianceFramesIconsConfig), typeof(AllianceFramesBuffsConfig), typeof(AllianceFramesDebuffsConfig), typeof(AllianceFramesTrackersConfig), typeof(AllianceFramesCooldownListConfig), typeof(AllianceFrames1Config), typeof(AllianceFrames2Config), typeof(PartyFramesHealthBarsConfig), typeof(PartyFramesManaBarConfig), typeof(PartyFramesCastbarConfig), typeof(PartyFramesIconsConfig), typeof(PartyFramesBuffsConfig), typeof(PartyFramesDebuffsConfig), typeof(PartyFramesTrackersConfig), typeof(PartyFramesCooldownListConfig), // Party Cooldowns typeof(PartyCooldownsConfig), typeof(PartyCooldownsBarConfig), typeof(PartyCooldownsDataConfig), // Enemy List typeof(EnemyListConfig), typeof(EnemyListHealthBarConfig), typeof(EnemyListEnmityIconConfig), typeof(EnemyListSignIconConfig), typeof(EnemyListCastbarConfig), typeof(EnemyListBuffsConfig), typeof(EnemyListDebuffsConfig), // Job Specific Bars typeof(PaladinConfig), typeof(WarriorConfig), typeof(DarkKnightConfig), typeof(GunbreakerConfig), typeof(WhiteMageConfig), typeof(ScholarConfig), typeof(AstrologianConfig), typeof(SageConfig), typeof(MonkConfig), typeof(DragoonConfig), typeof(NinjaConfig), typeof(SamuraiConfig), typeof(ReaperConfig), typeof(ViperConfig), typeof(BardConfig), typeof(MachinistConfig), typeof(DancerConfig), typeof(BlackMageConfig), typeof(SummonerConfig), typeof(RedMageConfig), typeof(BlueMageConfig), typeof(PictomancerConfig), // Other Elements typeof(ExperienceBarConfig), typeof(GCDIndicatorConfig), typeof(HotbarsConfig), typeof(Hotbar1BarConfig), typeof(Hotbar2BarConfig), typeof(Hotbar3BarConfig), typeof(Hotbar4BarConfig), typeof(Hotbar5BarConfig), typeof(Hotbar6BarConfig), typeof(Hotbar7BarConfig), typeof(Hotbar8BarConfig), typeof(Hotbar9BarConfig), typeof(Hotbar10BarConfig), typeof(PullTimerConfig), typeof(LimitBreakConfig), typeof(MPTickerConfig), // Colors typeof(TanksColorConfig), typeof(HealersColorConfig), typeof(MeleeColorConfig), typeof(RangedColorConfig), typeof(CastersColorConfig), typeof(RolesColorConfig), typeof(MiscColorConfig), // Customization typeof(FontsConfig), typeof(BarTexturesConfig), // Visibility typeof(GlobalVisibilityConfig), typeof(HotbarsVisibilityConfig), // Misc typeof(HUDOptionsConfig), typeof(WindowClippingConfig), typeof(TooltipsConfig), typeof(GridConfig), // Import typeof(ImportConfig) }; #endregion } }