using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Style; using Dalamud.Interface.Windowing.Persistence; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Storage; using Dalamud.Utility; using Newtonsoft.Json; using Serilog; using Serilog.Events; using Windows.Win32.UI.WindowsAndMessaging; namespace Dalamud.Configuration.Internal; /// /// Class containing Dalamud settings. /// [Serializable] [ServiceManager.ProvidedService] #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 internal sealed class DalamudConfiguration : IInternalDisposableService { private static readonly JsonSerializerSettings SerializerSettings = new() { TypeNameHandling = TypeNameHandling.All, TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, Formatting = Formatting.Indented, }; [JsonIgnore] private string? configPath; [JsonIgnore] private bool isSaveQueued; private Task? writeTask; /// /// Delegate for the event that occurs when the dalamud configuration is saved. /// /// The current dalamud configuration. public delegate void DalamudConfigurationSavedDelegate(DalamudConfiguration dalamudConfiguration); /// /// Event that occurs when dalamud configuration is saved. /// public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved; /// /// Gets or sets a list of muted words. /// public List? BadWords { get; set; } /// /// Gets or sets a value indicating whether the taskbar should flash once a duty is found. /// public bool DutyFinderTaskbarFlash { get; set; } = true; /// /// Gets or sets a value indicating whether a message should be sent in chat once a duty is found. /// public bool DutyFinderChatMessage { get; set; } = true; /// /// Gets or sets the language code to load Dalamud localization with. /// public string? LanguageOverride { get; set; } = null; /// /// Gets or sets the last loaded Dalamud version. /// public string? LastVersion { get; set; } = null; /// /// Gets or sets a dictionary of seen FTUE levels. /// public Dictionary SeenFtueLevels { get; set; } = []; /// /// Gets or sets the last loaded Dalamud version. /// public string? LastChangelogMajorMinor { get; set; } = null; /// /// Gets or sets the chat type used by default for plugin messages. /// public XivChatType GeneralChatType { get; set; } = XivChatType.Debug; /// /// Gets or sets a value indicating whether plugin testing builds should be shown. /// public bool DoPluginTest { get; set; } = false; /// /// Gets or sets a list of custom repos. /// public List ThirdRepoList { get; set; } = []; /// /// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed. /// public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null; /// /// Gets or sets a list of hidden plugins. /// public List HiddenPluginInternalName { get; set; } = []; /// /// Gets or sets a list of seen plugins. /// public List SeenPluginInternalName { get; set; } = []; /// /// Gets or sets a list of additional settings for devPlugins. The key is the absolute path /// to the plugin DLL. This is automatically generated for any plugins in the devPlugins folder. /// However by specifiying this value manually, you can add arbitrary files outside the normal /// file paths. /// public Dictionary DevPluginSettings { get; set; } = []; /// /// Gets or sets a list of additional locations that dev plugins should be loaded from. This can /// be either a DLL or folder, but should be the absolute path, or a path relative to the currently /// injected Dalamud instance. /// public List DevPluginLoadLocations { get; set; } = []; /// /// Gets or sets the global UI scale. /// public float GlobalUiScale { get; set; } = 1.0f; /// /// Gets or sets the default font spec. /// public IFontSpec? DefaultFontSpec { get; set; } /// Gets or sets the opacity of the IME state indicator. /// 0 will hide the state indicator. 1 will make the state indicator fully visible. Values outside the /// range will be clamped to [0, 1]. /// See to . public float ImeStateIndicatorOpacity { get; set; } = 1f; /// /// Gets or sets a value indicating whether plugin UI should be hidden. /// public bool ToggleUiHide { get; set; } = true; /// /// Gets or sets a value indicating whether plugin UI should be hidden during cutscenes. /// public bool ToggleUiHideDuringCutscenes { get; set; } = true; /// /// Gets or sets a value indicating whether plugin UI should be hidden during GPose. /// public bool ToggleUiHideDuringGpose { get; set; } = true; /// /// Gets or sets a value indicating whether a message containing Dalamud's current version and the number of loaded plugins should be sent at login. /// public bool PrintDalamudWelcomeMsg { get; set; } = true; /// /// Gets or sets a value indicating whether a message containing detailed plugin information should be sent at login. /// public bool PrintPluginsWelcomeMsg { get; set; } = true; /// /// Gets or sets a value indicating whether plugins should be auto-updated. /// [Obsolete("Use AutoUpdateBehavior instead.")] public bool AutoUpdatePlugins { get; set; } /// /// Gets or sets a value indicating whether Dalamud should add buttons to the system menu. /// public bool DoButtonsSystemMenu { get; set; } = true; /// /// Gets or sets the default Dalamud debug log level on startup. /// public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; /// /// Gets or sets a value indicating whether to write to log files synchronously. /// public bool LogSynchronously { get; set; } = false; /// /// Gets or sets a value indicating whether the debug log should scroll automatically. /// public bool LogAutoScroll { get; set; } = true; /// /// Gets or sets a value indicating whether the debug log should open at startup. /// public bool LogOpenAtStartup { get; set; } /// /// Gets or sets the number of lines to keep for the Dalamud Console window. /// public int LogLinesLimit { get; set; } = 10000; /// /// Gets or sets a list representing the command history for the Dalamud Console. /// public List LogCommandHistory { get; set; } = []; /// /// Gets or sets a value indicating whether the dev bar should open at startup. /// public bool DevBarOpenAtStartup { get; set; } /// /// Gets or sets a value indicating whether ImGui asserts should be enabled at startup. /// public bool? ImGuiAssertsEnabledAtStartup { get; set; } /// /// Gets or sets a value indicating whether docking should be globally enabled in ImGui. /// public bool IsDocking { get; set; } /// /// Gets or sets a value indicating whether plugin user interfaces should trigger sound effects. /// This setting is effected by the in-game "System Sounds" option and volume. /// [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] public bool EnablePluginUISoundEffects { get; set; } = true; /// /// Gets or sets a value indicating whether an additional button allowing pinning and clickthrough options should be shown /// on plugin title bars when using the Window System. /// public bool EnablePluginUiAdditionalOptions { get; set; } = true; /// /// Gets or sets a value indicating whether viewports should always be disabled. /// public bool IsDisableViewport { get; set; } = true; /// /// Gets or sets a value indicating whether navigation via a gamepad should be globally enabled in ImGui. /// public bool IsGamepadNavigationEnabled { get; set; } = true; /// /// Gets or sets a value indicating whether focus management is enabled. /// public bool IsFocusManagementEnabled { get; set; } = true; /// /// Gets or sets a value indicating whether to resume game main thread after plugins load. /// public bool IsResumeGameAfterPluginLoad { get; set; } = false; /// /// Gets or sets a value indicating whether any plugin should be loaded when the game is started. /// It is reset immediately when read. /// public bool PluginSafeMode { get; set; } /// /// Gets or sets a value indicating the wait time between plugin unload and plugin assembly unload. /// Uses default value that may change between versions if set to null. /// public int? PluginWaitBeforeFree { get; set; } /// /// Gets or sets a value indicating whether crashes during shutdown should be reported. /// public bool ReportShutdownCrashes { get; set; } /// /// Gets or sets a list of saved styles. /// [JsonProperty("SavedStyles")] public List? SavedStylesOld { get; set; } /// /// Gets or sets a list of saved styles. /// [JsonProperty("SavedStylesVersioned")] public List? SavedStyles { get; set; } /// /// Gets or sets the name of the currently chosen style. /// public string ChosenStyle { get; set; } = "Dalamud Standard"; /// /// Gets or sets a list of saved plugin profiles. /// public List? SavedProfiles { get; set; } /// /// Gets or sets the default plugin profile. /// public ProfileModel? DefaultProfile { get; set; } /// /// Gets or sets a value indicating whether profiles are enabled. /// public bool ProfilesEnabled { get; set; } = false; /// /// Gets or sets a value indicating whether the user has seen the profiles tutorial. /// public bool ProfilesHasSeenTutorial { get; set; } = false; /// /// Gets or sets the default UI preset. /// public PresetModel DefaultUiPreset { get; set; } = new(); /// /// Gets or sets the order of DTR elements, by title. /// public List? DtrOrder { get; set; } /// /// Gets or sets the list of ignored DTR elements, by title. /// public List? DtrIgnore { get; set; } /// /// Gets or sets the spacing used for DTR entries. /// public int DtrSpacing { get; set; } = 10; /// /// Gets or sets a value indicating whether to swap the /// direction in which elements are drawn in the DTR. /// False indicates that elements will be drawn from the end of /// the left side of the Server Info bar, and continue leftwards. /// True indicates the opposite. /// public bool DtrSwapDirection { get; set; } = false; /// /// Gets or sets a value indicating whether the title screen menu is shown. /// public bool ShowTsm { get; set; } = true; /// /// Gets or sets a value indicating whether to reduce motions (animations). /// public bool? ReduceMotions { get; set; } /// /// Gets or sets a value indicating whether market board data should be uploaded. /// public bool IsMbCollect { get; set; } = true; /// /// Gets the ISO 639-1 two-letter code for the language of the effective Dalamud display language. /// public string EffectiveLanguage { get { var languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); try { if (string.IsNullOrEmpty(this.LanguageOverride)) { var currentUiLang = CultureInfo.CurrentUICulture; if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x)) return currentUiLang.TwoLetterISOLanguageName; else return languages[0]; } else { return this.LanguageOverride; } } catch (Exception) { return languages[0]; } } } /// /// Gets or sets a value indicating whether to show info on dev bar. /// public bool ShowDevBarInfo { get; set; } = true; /// /// Gets or sets the last-used contact details for the plugin feedback form. /// public string LastFeedbackContactDetails { get; set; } = string.Empty; /// /// Gets or sets a list of plugins that testing builds should be downloaded for. /// public List PluginTestingOptIns { get; set; } = []; /// /// Gets or sets a list of plugins that have opted into or out of auto-updating. /// public List PluginAutoUpdatePreferences { get; set; } = []; /// /// Gets or sets a value indicating whether the FFXIV window should be toggled to immersive mode. /// public bool WindowIsImmersive { get; set; } = false; /// Gets or sets the mode specifying how to handle ReShade. [JsonProperty("ReShadeHandlingModeV2")] public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.Default; /// Gets or sets the swap chain hook mode. public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode; /// /// Gets or sets hitch threshold for game network up in milliseconds. /// public double GameNetworkUpHitch { get; set; } = 30; /// /// Gets or sets hitch threshold for game network down in milliseconds. /// public double GameNetworkDownHitch { get; set; } = 30; /// /// Gets or sets hitch threshold for framework update in milliseconds. /// public double FrameworkUpdateHitch { get; set; } = 50; /// /// Gets or sets hitch threshold for ui builder in milliseconds. /// public double UiBuilderHitch { get; set; } = 100; /// Gets or sets a value indicating whether to track texture allocation by plugins. public bool UseTexturePluginTracking { get; set; } /// /// Gets or sets the page of the plugin installer that is shown by default when opened. /// public PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerOpenKind.AllPlugins; /// /// Gets or sets a value indicating how auto-updating should behave. /// public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null; /// /// Gets or sets a value indicating whether users should be notified regularly about pending updates. /// public bool CheckPeriodicallyForUpdates { get; set; } = true; /// /// Gets or sets a value indicating whether users should be notified about updates in chat. /// public bool SendUpdateNotificationToChat { get; set; } = false; /// /// Gets or sets a value indicating whether disabled plugins should be auto-updated. /// public bool UpdateDisabledPlugins { get; set; } = false; /// /// Gets or sets a value indicating where notifications are anchored to on the screen. /// public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f); #pragma warning disable SA1600 #pragma warning disable SA1516 // XLCore/XoM compatibility until they move it out public string? DalamudBetaKey { get; set; } = null; public string? DalamudBetaKind { get; set; } #pragma warning restore SA1516 #pragma warning restore SA1600 /// /// Gets or sets a list of badge passwords used to unlock badges. /// public List UsedBadgePasswords { get; set; } = []; /// /// Gets or sets a value indicating whether badges should be shown on the title screen. /// public bool ShowBadgesOnTitleScreen { get; set; } = true; /// /// Load a configuration from the provided path. /// /// Path to read from. /// File storage. /// The deserialized configuration file. public static async Task Load(string path, ReliableFileStorage fs) { DalamudConfiguration deserialized = null; try { await fs.ReadAllTextAsync(path, text => { deserialized = JsonConvert.DeserializeObject(text, SerializerSettings); // If this reads as null, the file was empty, that's no good if (deserialized == null) throw new Exception("Read config was null."); }); } catch (FileNotFoundException) { // ignored } catch (Exception e) { Log.Error(e, "Could not load DalamudConfiguration at {Path}, creating new", path); } deserialized ??= new DalamudConfiguration(); deserialized.configPath = path; try { deserialized.SetDefaults(); } catch (Exception e) { Log.Error(e, "Failed to set defaults for DalamudConfiguration"); } return deserialized; } /// /// Save the configuration at the path it was loaded from, at the next frame. /// public void QueueSave() { this.isSaveQueued = true; } /// /// Immediately save the configuration. /// public void ForceSave() { this.Save(); this.isSaveQueued = false; this.writeTask?.GetAwaiter().GetResult(); } /// void IInternalDisposableService.DisposeService() { // Make sure that we save, if a save is queued while we are shutting down this.Update(); // Wait for the write task to finish this.writeTask?.Wait(); } /// /// Save the file, if needed. Only needs to be done once a frame. /// internal void Update() { if (this.isSaveQueued) { this.Save(); this.isSaveQueued = false; } } private void SetDefaults() { #pragma warning disable CS0618 // "Reduced motion" if (!this.ReduceMotions.HasValue) { // https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium var winAnimEnabled = 0; bool success; unsafe { success = Windows.Win32.PInvoke.SystemParametersInfo( SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETCLIENTAREAANIMATION, 0, &winAnimEnabled, 0); } if (!success) { Log.Warning("Failed to get Windows animation setting, assuming reduced motion is off (GetLastError: {GetLastError:X})", Marshal.GetLastPInvokeError()); this.ReduceMotions = false; } else { this.ReduceMotions = winAnimEnabled == 0; } } // Migrate old auto-update setting to new auto-update behavior this.AutoUpdateBehavior ??= this.AutoUpdatePlugins ? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll : Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify; #pragma warning restore CS0618 } private void Save() { ThreadSafety.AssertMainThread(); if (this.configPath is null) throw new InvalidOperationException("configPath is not set."); // Wait for previous write to finish this.writeTask?.Wait(); this.writeTask = Task.Run(async () => { await Service.Get().WriteAllTextAsync( this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); Log.Verbose("DalamudConfiguration saved"); }).ContinueWith(t => { if (t.IsFaulted) { Log.Error( t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath); } }); foreach (var action in Delegate.EnumerateInvocationList(this.DalamudConfigurationSaved)) { try { action(this); } catch (Exception ex) { Log.Error(ex, "Exception during raise of {handler}", action.Method); } } } }