This commit is contained in:
goaaats 2025-06-22 21:39:38 +02:00
commit 95ec633cc5
163 changed files with 7036 additions and 1585 deletions

View file

@ -648,6 +648,48 @@ void xivfixes::symbol_load_patches(bool bApply) {
} }
} }
void xivfixes::disable_game_debugging_protection(bool bApply) {
static const char* LogTag = "[xivfixes:disable_game_debugging_protection]";
static const std::vector<uint8_t> patchBytes = {
0x31, 0xC0, // XOR EAX, EAX
0x90, // NOP
0x90, // NOP
0x90, // NOP
0x90 // NOP
};
if (!bApply)
return;
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
// Find IsDebuggerPresent in Framework.Tick()
const char* matchPtr = utils::signature_finder()
.look_in(utils::loaded_module(g_hGameInstance), ".text")
.look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41")
.find_one()
.Match.data();
if (!matchPtr) {
logging::E("{} Failed to find signature.", LogTag);
return;
}
void* address = const_cast<void*>(static_cast<const void*>(matchPtr));
DWORD oldProtect;
if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) {
memcpy(address, patchBytes.data(), patchBytes.size());
VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect);
logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast<uintptr_t>(address));
} else {
logging::E("{} Failed to change memory protection.", LogTag);
}
}
void xivfixes::apply_all(bool bApply) { void xivfixes::apply_all(bool bApply) {
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>> for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
{ {
@ -658,6 +700,7 @@ void xivfixes::apply_all(bool bApply) {
{ "backup_userdata_save", &backup_userdata_save }, { "backup_userdata_save", &backup_userdata_save },
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }, { "prevent_icmphandle_crashes", &prevent_icmphandle_crashes },
{ "symbol_load_patches", &symbol_load_patches }, { "symbol_load_patches", &symbol_load_patches },
{ "disable_game_debugging_protection", &disable_game_debugging_protection },
} }
) { ) {
try { try {

View file

@ -8,6 +8,7 @@ namespace xivfixes {
void backup_userdata_save(bool bApply); void backup_userdata_save(bool bApply);
void prevent_icmphandle_crashes(bool bApply); void prevent_icmphandle_crashes(bool bApply);
void symbol_load_patches(bool bApply); void symbol_load_patches(bool bApply);
void disable_game_debugging_protection(bool bApply);
void apply_all(bool bApply); void apply_all(bool bApply);
} }

View file

@ -46,6 +46,8 @@ namespace Dalamud.CorePlugin
#else #else
private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin"); private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin");
private readonly PluginWindow window;
private Localization localization; private Localization localization;
private IPluginLog pluginLog; private IPluginLog pluginLog;
@ -63,7 +65,8 @@ namespace Dalamud.CorePlugin
this.Interface = pluginInterface; this.Interface = pluginInterface;
this.pluginLog = log; this.pluginLog = log;
this.windowSystem.AddWindow(new PluginWindow()); this.window = new PluginWindow();
this.windowSystem.AddWindow(this.window);
this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.Draw += this.OnDraw;
this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
@ -136,12 +139,12 @@ namespace Dalamud.CorePlugin
{ {
this.pluginLog.Information("Command called!"); this.pluginLog.Information("Command called!");
// this.window.IsOpen = true; this.window.IsOpen ^= true;
} }
private void OnOpenConfigUi() private void OnOpenConfigUi()
{ {
// this.window.IsOpen = true; this.window.IsOpen = true;
} }
private void OnOpenMainUi() private void OnOpenMainUi()

View file

@ -478,6 +478,7 @@ namespace Dalamud.Injector
"backup_userdata_save", "backup_userdata_save",
"prevent_icmphandle_crashes", "prevent_icmphandle_crashes",
"symbol_load_patches", "symbol_load_patches",
"disable_game_debugging_protection",
}; };
startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootDotnetOpenProcessHookMode = 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;

View file

@ -3,12 +3,14 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
@ -67,12 +69,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public List<string>? BadWords { get; set; } public List<string>? BadWords { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. /// Gets or sets a value indicating whether the taskbar should flash once a duty is found.
/// </summary> /// </summary>
public bool DutyFinderTaskbarFlash { get; set; } = true; public bool DutyFinderTaskbarFlash { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not a message should be sent in chat once a duty is found. /// Gets or sets a value indicating whether a message should be sent in chat once a duty is found.
/// </summary> /// </summary>
public bool DutyFinderChatMessage { get; set; } = true; public bool DutyFinderChatMessage { get; set; } = true;
@ -102,7 +104,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public XivChatType GeneralChatType { get; set; } = XivChatType.Debug; public XivChatType GeneralChatType { get; set; } = XivChatType.Debug;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin testing builds should be shown. /// Gets or sets a value indicating whether plugin testing builds should be shown.
/// </summary> /// </summary>
public bool DoPluginTest { get; set; } = false; public bool DoPluginTest { get; set; } = false;
@ -117,7 +119,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new(); public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed. /// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed.
/// </summary> /// </summary>
public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null; public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null;
@ -175,38 +177,38 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public float ImeStateIndicatorOpacity { get; set; } = 1f; public float ImeStateIndicatorOpacity { get; set; } = 1f;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden. /// Gets or sets a value indicating whether plugin UI should be hidden.
/// </summary> /// </summary>
public bool ToggleUiHide { get; set; } = true; public bool ToggleUiHide { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden during cutscenes. /// Gets or sets a value indicating whether plugin UI should be hidden during cutscenes.
/// </summary> /// </summary>
public bool ToggleUiHideDuringCutscenes { get; set; } = true; public bool ToggleUiHideDuringCutscenes { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden during GPose. /// Gets or sets a value indicating whether plugin UI should be hidden during GPose.
/// </summary> /// </summary>
public bool ToggleUiHideDuringGpose { get; set; } = true; public bool ToggleUiHideDuringGpose { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not a message containing Dalamud's current version and the number of loaded plugins should be sent at login. /// 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.
/// </summary> /// </summary>
public bool PrintDalamudWelcomeMsg { get; set; } = true; public bool PrintDalamudWelcomeMsg { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login. /// Gets or sets a value indicating whether a message containing detailed plugin information should be sent at login.
/// </summary> /// </summary>
public bool PrintPluginsWelcomeMsg { get; set; } = true; public bool PrintPluginsWelcomeMsg { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugins should be auto-updated. /// Gets or sets a value indicating whether plugins should be auto-updated.
/// </summary> /// </summary>
[Obsolete("Use AutoUpdateBehavior instead.")] [Obsolete("Use AutoUpdateBehavior instead.")]
public bool AutoUpdatePlugins { get; set; } public bool AutoUpdatePlugins { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not Dalamud should add buttons to the system menu. /// Gets or sets a value indicating whether Dalamud should add buttons to the system menu.
/// </summary> /// </summary>
public bool DoButtonsSystemMenu { get; set; } = true; public bool DoButtonsSystemMenu { get; set; } = true;
@ -221,12 +223,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public bool LogSynchronously { get; set; } = false; public bool LogSynchronously { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the debug log should scroll automatically. /// Gets or sets a value indicating whether the debug log should scroll automatically.
/// </summary> /// </summary>
public bool LogAutoScroll { get; set; } = true; public bool LogAutoScroll { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the debug log should open at startup. /// Gets or sets a value indicating whether the debug log should open at startup.
/// </summary> /// </summary>
public bool LogOpenAtStartup { get; set; } public bool LogOpenAtStartup { get; set; }
@ -241,29 +243,29 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public List<string> LogCommandHistory { get; set; } = new(); public List<string> LogCommandHistory { get; set; } = new();
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the dev bar should open at startup. /// Gets or sets a value indicating whether the dev bar should open at startup.
/// </summary> /// </summary>
public bool DevBarOpenAtStartup { get; set; } public bool DevBarOpenAtStartup { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup. /// Gets or sets a value indicating whether ImGui asserts should be enabled at startup.
/// </summary> /// </summary>
public bool? ImGuiAssertsEnabledAtStartup { get; set; } public bool? ImGuiAssertsEnabledAtStartup { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. /// Gets or sets a value indicating whether docking should be globally enabled in ImGui.
/// </summary> /// </summary>
public bool IsDocking { get; set; } public bool IsDocking { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// 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. /// This setting is effected by the in-game "System Sounds" option and volume.
/// </summary> /// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; } public bool EnablePluginUISoundEffects { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown /// 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. /// on plugin title bars when using the Window System.
/// </summary> /// </summary>
public bool EnablePluginUiAdditionalOptions { get; set; } = true; public bool EnablePluginUiAdditionalOptions { get; set; } = true;
@ -274,20 +276,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public bool IsDisableViewport { get; set; } = true; public bool IsDisableViewport { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui. /// Gets or sets a value indicating whether navigation via a gamepad should be globally enabled in ImGui.
/// </summary> /// </summary>
public bool IsGamepadNavigationEnabled { get; set; } = true; public bool IsGamepadNavigationEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not focus management is enabled. /// Gets or sets a value indicating whether focus management is enabled.
/// </summary> /// </summary>
public bool IsFocusManagementEnabled { get; set; } = true; public bool IsFocusManagementEnabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup.
/// </summary>
public bool IsAntiAntiDebugEnabled { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to resume game main thread after plugins load. /// Gets or sets a value indicating whether to resume game main thread after plugins load.
/// </summary> /// </summary>
@ -299,7 +296,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public string? DalamudBetaKind { get; set; } public string? DalamudBetaKind { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. /// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
/// It is reset immediately when read. /// It is reset immediately when read.
/// </summary> /// </summary>
public bool PluginSafeMode { get; set; } public bool PluginSafeMode { get; set; }
@ -311,7 +308,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public int? PluginWaitBeforeFree { get; set; } public int? PluginWaitBeforeFree { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not crashes during shutdown should be reported. /// Gets or sets a value indicating whether crashes during shutdown should be reported.
/// </summary> /// </summary>
public bool ReportShutdownCrashes { get; set; } public bool ReportShutdownCrashes { get; set; }
@ -343,12 +340,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public ProfileModel? DefaultProfile { get; set; } public ProfileModel? DefaultProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not profiles are enabled. /// Gets or sets a value indicating whether profiles are enabled.
/// </summary> /// </summary>
public bool ProfilesEnabled { get; set; } = false; public bool ProfilesEnabled { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the user has seen the profiles tutorial. /// Gets or sets a value indicating whether the user has seen the profiles tutorial.
/// </summary> /// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false; public bool ProfilesHasSeenTutorial { get; set; } = false;
@ -392,7 +389,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public bool? ReduceMotions { get; set; } public bool? ReduceMotions { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not market board data should be uploaded. /// Gets or sets a value indicating whether market board data should be uploaded.
/// </summary> /// </summary>
public bool IsMbCollect { get; set; } = true; public bool IsMbCollect { get; set; } = true;
@ -428,7 +425,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not to show info on dev bar. /// Gets or sets a value indicating whether to show info on dev bar.
/// </summary> /// </summary>
public bool ShowDevBarInfo { get; set; } = true; public bool ShowDevBarInfo { get; set; } = true;
@ -502,6 +499,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public bool SendUpdateNotificationToChat { get; set; } = false; public bool SendUpdateNotificationToChat { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether disabled plugins should be auto-updated.
/// </summary>
public bool UpdateDisabledPlugins { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating where notifications are anchored to on the screen.
/// </summary>
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
/// <summary> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>
@ -562,6 +569,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public void ForceSave() public void ForceSave()
{ {
this.Save(); this.Save();
this.isSaveQueued = false;
this.writeTask?.GetAwaiter().GetResult();
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -12,6 +12,12 @@ internal sealed class DevPluginSettings
/// </summary> /// </summary>
public bool StartOnBoot { get; set; } = true; public bool StartOnBoot { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether we should show notifications for errors this plugin
/// is creating.
/// </summary>
public bool NotifyForErrors { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this plugin should automatically reload on file change. /// Gets or sets a value indicating whether this plugin should automatically reload on file change.
/// </summary> /// </summary>

View file

@ -21,7 +21,7 @@ internal class EnvironmentConfiguration
public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK"); public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK");
/// <summary> /// <summary>
/// Gets a value indicating whether or not Dalamud context menus should be disabled. /// Gets a value indicating whether Dalamud context menus should be disabled.
/// </summary> /// </summary>
public static bool DalamudDoContextMenu { get; } = GetEnvironmentVariable("DALAMUD_ENABLE_CONTEXTMENU"); public static bool DalamudDoContextMenu { get; } = GetEnvironmentVariable("DALAMUD_ENABLE_CONTEXTMENU");

View file

@ -27,7 +27,7 @@ public interface IConsoleCommand : IConsoleEntry
/// Execute this command. /// Execute this command.
/// </summary> /// </summary>
/// <param name="arguments">Arguments to invoke the entry with.</param> /// <param name="arguments">Arguments to invoke the entry with.</param>
/// <returns>Whether or not execution succeeded.</returns> /// <returns>Whether execution succeeded.</returns>
public bool Invoke(IEnumerable<object> arguments); public bool Invoke(IEnumerable<object> arguments);
} }

View file

@ -181,7 +181,7 @@ internal partial class ConsoleManager : IServiceType
/// Process a console command. /// Process a console command.
/// </summary> /// </summary>
/// <param name="command">The command to process.</param> /// <param name="command">The command to process.</param>
/// <returns>Whether or not the command was successfully processed.</returns> /// <returns>Whether the command was successfully processed.</returns>
public bool ProcessCommand(string command) public bool ProcessCommand(string command)
{ {
if (this.Invoke?.Invoke(command) == true) if (this.Invoke?.Invoke(command) == true)
@ -374,7 +374,7 @@ internal partial class ConsoleManager : IServiceType
/// Execute this command. /// Execute this command.
/// </summary> /// </summary>
/// <param name="arguments">Arguments to invoke the entry with.</param> /// <param name="arguments">Arguments to invoke the entry with.</param>
/// <returns>Whether or not execution succeeded.</returns> /// <returns>Whether execution succeeded.</returns>
public abstract bool Invoke(IEnumerable<object> arguments); public abstract bool Invoke(IEnumerable<object> arguments);
/// <summary> /// <summary>

View file

@ -11,6 +11,47 @@ namespace Dalamud.Console;
#pragma warning disable Dalamud001 #pragma warning disable Dalamud001
/// <summary>
/// Utility functions for the console manager.
/// </summary>
internal static partial class ConsoleManagerPluginUtil
{
private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"];
/// <summary>
/// Get a sanitized namespace name from a plugin's internal name.
/// </summary>
/// <param name="pluginInternalName">The plugin's internal name.</param>
/// <returns>A sanitized namespace.</returns>
public static string GetSanitizedNamespaceName(string pluginInternalName)
{
// Must be lowercase
pluginInternalName = pluginInternalName.ToLowerInvariant();
// Remove all non-alphabetic characters
pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty);
// Remove reserved namespaces from the start or end
foreach (var reservedNamespace in ReservedNamespaces)
{
if (pluginInternalName.StartsWith(reservedNamespace))
{
pluginInternalName = pluginInternalName[reservedNamespace.Length..];
}
if (pluginInternalName.EndsWith(reservedNamespace))
{
pluginInternalName = pluginInternalName[..^reservedNamespace.Length];
}
}
return pluginInternalName;
}
[GeneratedRegex(@"[^a-z]")]
private static partial Regex NonAlphaRegex();
}
/// <summary> /// <summary>
/// Plugin-scoped version of the console service. /// Plugin-scoped version of the console service.
/// </summary> /// </summary>
@ -19,7 +60,7 @@ namespace Dalamud.Console;
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IConsole>] [ResolveVia<IConsole>]
#pragma warning restore SA1015 #pragma warning restore SA1015
public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
{ {
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get(); private readonly ConsoleManager console = Service<ConsoleManager>.Get();
@ -130,44 +171,3 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
return command; return command;
} }
} }
/// <summary>
/// Utility functions for the console manager.
/// </summary>
internal static partial class ConsoleManagerPluginUtil
{
private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"];
/// <summary>
/// Get a sanitized namespace name from a plugin's internal name.
/// </summary>
/// <param name="pluginInternalName">The plugin's internal name.</param>
/// <returns>A sanitized namespace.</returns>
public static string GetSanitizedNamespaceName(string pluginInternalName)
{
// Must be lowercase
pluginInternalName = pluginInternalName.ToLowerInvariant();
// Remove all non-alphabetic characters
pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty);
// Remove reserved namespaces from the start or end
foreach (var reservedNamespace in ReservedNamespaces)
{
if (pluginInternalName.StartsWith(reservedNamespace))
{
pluginInternalName = pluginInternalName[reservedNamespace.Length..];
}
if (pluginInternalName.EndsWith(reservedNamespace))
{
pluginInternalName = pluginInternalName[..^reservedNamespace.Length];
}
}
return pluginInternalName;
}
[GeneratedRegex(@"[^a-z]")]
private static partial Regex NonAlphaRegex();
}

View file

@ -91,7 +91,7 @@ internal sealed unsafe class Dalamud : IServiceType
return; return;
Util.Fatal( Util.Fatal(
"Dalamud failed to load all necessary services.\n\nThe game will continue, but you may not be able to use plugins.", $"Dalamud failed to load all necessary services.\nThe game will continue, but you may not be able to use plugins.\n\n{t.Exception}",
"Dalamud", false); "Dalamud", false);
} }

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>12.0.0.7</DalamudVersion> <DalamudVersion>12.0.1.4</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
@ -64,8 +64,8 @@
<PackageReference Include="BitFaster.Caching" Version="2.4.1" /> <PackageReference Include="BitFaster.Caching" Version="2.4.1" />
<PackageReference Include="CheapLoc" Version="1.1.8" /> <PackageReference Include="CheapLoc" Version="1.1.8" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="all" /> <PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="all" />
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" /> <PackageReference Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp5" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" /> <PackageReference Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp3" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" /> <PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" /> <PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" /> <PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
@ -130,6 +130,13 @@
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" /> <EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.DrawToPremul.ps.bin" />
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.DrawToPremul.vs.bin" />
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.MakeStraight.ps.bin" />
<EmbeddedResource WithCulture="false" Include="Interface\Textures\TextureWraps\Internal\DrawListTextureWrap\Renderer.MakeStraight.vs.bin" />
</ItemGroup>
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles"> <Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
<ItemGroup> <ItemGroup>
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" /> <ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />

View file

@ -321,7 +321,7 @@ public sealed class EntryPoint
Log.Information("User chose to disable plugins on next launch..."); Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get(); var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true; config.PluginSafeMode = true;
config.QueueSave(); config.ForceSave();
} }
Log.CloseAndFlush(); Log.CloseAndFlush();

View file

@ -0,0 +1,46 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Object representing data that is relevant in handling native events.
/// </summary>
public class AddonEventData
{
/// <summary>
/// Gets the AtkEventType for this event.
/// </summary>
public AddonEventType AtkEventType { get; internal set; }
/// <summary>
/// Gets the param field for this event.
/// </summary>
public uint Param { get; internal set; }
/// <summary>
/// Gets the pointer to the AtkEvent object for this event.
/// </summary>
/// <remarks>Note: This is not a pointer to the AtkEventData object.<br/><br/></remarks>
/// <remarks>Warning: AtkEvent->Node has been modified to be the AtkUnitBase*, and AtkEvent->Target has been modified to be the AtkResNode* that triggered this event.</remarks>
public nint AtkEventPointer { get; internal set; }
/// <summary>
/// Gets the pointer to the AtkEventData object for this event.
/// </summary>
/// <remarks>This field will contain relevant data such as left vs right click, scroll up vs scroll down.</remarks>
public nint AtkEventDataPointer { get; internal set; }
/// <summary>
/// Gets the pointer to the AtkUnitBase that is handling this event.
/// </summary>
public nint AddonPointer { get; internal set; }
/// <summary>
/// Gets the pointer to the AtkResNode that triggered this event.
/// </summary>
public nint NodeTargetPointer { get; internal set; }
/// <summary>
/// Gets or sets a pointer to the AtkEventListener responsible for handling this event.
/// Note: As the event listener is dalamud allocated, there's no reason to expose this field.
/// </summary>
internal nint AtkEventListener { get; set; }
}

View file

@ -1,5 +1,6 @@
using Dalamud.Memory; using Dalamud.Plugin.Services;
using Dalamud.Plugin.Services; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events; namespace Dalamud.Game.Addon.Events;
@ -35,7 +36,14 @@ internal unsafe class AddonEventEntry
/// <summary> /// <summary>
/// Gets the handler that gets called when this event is triggered. /// Gets the handler that gets called when this event is triggered.
/// </summary> /// </summary>
public required IAddonEventManager.AddonEventHandler Handler { get; init; } [Obsolete("Use AddonEventDelegate Delegate instead")]
public IAddonEventManager.AddonEventHandler Handler { get; init; }
/// <summary>
/// Gets the delegate that gets called when this event is triggered.
/// </summary>
[Api13ToDo("Make this field required")]
public IAddonEventManager.AddonEventDelegate Delegate { get; init; }
/// <summary> /// <summary>
/// Gets the unique id for this event. /// Gets the unique id for this event.

View file

@ -1,4 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@ -96,6 +96,29 @@ internal unsafe class AddonEventManager : IInternalDisposableService
return null; return null;
} }
/// <summary>
/// Registers an event handler for the specified addon, node, and type.
/// </summary>
/// <param name="pluginId">Unique ID for this plugin.</param>
/// <param name="atkUnitBase">The parent addon for this event.</param>
/// <param name="atkResNode">The node that will trigger this event.</param>
/// <param name="eventType">The event type for this event.</param>
/// <param name="eventDelegate">The delegate to call when event is triggered.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns>
internal IAddonEventHandle? AddEvent(Guid pluginId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate)
{
if (this.pluginEventControllers.TryGetValue(pluginId, out var controller))
{
return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventDelegate);
}
else
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was added.");
}
return null;
}
/// <summary> /// <summary>
/// Unregisters an event handler with the specified event id and event type. /// Unregisters an event handler with the specified event id and event type.
/// </summary> /// </summary>
@ -231,13 +254,20 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo
this.eventManagerService.ResetCursor(); this.eventManagerService.ResetCursor();
} }
Service<Framework>.Get().RunOnFrameworkThread(() =>
{
this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId);
}).Wait();
} }
/// <inheritdoc/> /// <inheritdoc/>
public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
=> this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler); => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler);
/// <inheritdoc/>
public IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate)
=> this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventDelegate);
/// <inheritdoc/> /// <inheritdoc/>
public void RemoveEvent(IAddonEventHandle eventHandle) public void RemoveEvent(IAddonEventHandle eventHandle)
=> this.eventManagerService.RemoveEvent(this.plugin.EffectiveWorkingPluginId, eventHandle); => this.eventManagerService.RemoveEvent(this.plugin.EffectiveWorkingPluginId, eventHandle);

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.Addon.Events; namespace Dalamud.Game.Addon.Events;
/// <summary> /// <summary>
/// Reimplementation of AtkEventType. /// Reimplementation of AtkEventType.
@ -30,16 +30,37 @@ public enum AddonEventType : byte
/// </summary> /// </summary>
MouseOut = 7, MouseOut = 7,
/// <summary>
/// Mouse Wheel.
/// </summary>
MouseWheel = 8,
/// <summary> /// <summary>
/// Mouse Click. /// Mouse Click.
/// </summary> /// </summary>
MouseClick = 9, MouseClick = 9,
/// <summary>
/// Mouse Double Click.
/// </summary>
MouseDoubleClick = 10,
/// <summary> /// <summary>
/// Input Received. /// Input Received.
/// </summary> /// </summary>
InputReceived = 12, InputReceived = 12,
/// <summary>
/// Input Navigation (LEFT, RIGHT, UP, DOWN, TAB_NEXT, TAB_PREV, TAB_BOTH_NEXT, TAB_BOTH_PREV, PAGEUP, PAGEDOWN).
/// </summary>
InputNavigation = 13,
/// <summary>
/// InputBase Input Received (AtkComponentTextInput and AtkComponentNumericInput).<br/>
/// For example, this is fired for moving the text cursor, deletion of a character and inserting a new line.
/// </summary>
InputBaseInputReceived = 15,
/// <summary> /// <summary>
/// Focus Start. /// Focus Start.
/// </summary> /// </summary>
@ -51,108 +72,199 @@ public enum AddonEventType : byte
FocusStop = 19, FocusStop = 19,
/// <summary> /// <summary>
/// Button Press, sent on MouseDown on Button. /// Resize (ChatLogPanel).
/// </summary>
Resize = 19,
/// <summary>
/// AtkComponentButton Press, sent on MouseDown on Button.
/// </summary> /// </summary>
ButtonPress = 23, ButtonPress = 23,
/// <summary> /// <summary>
/// Button Release, sent on MouseUp and MouseOut. /// AtkComponentButton Release, sent on MouseUp and MouseOut.
/// </summary> /// </summary>
ButtonRelease = 24, ButtonRelease = 24,
/// <summary> /// <summary>
/// Button Click, sent on MouseUp and MouseClick on button. /// AtkComponentButton Click, sent on MouseUp and MouseClick on button.
/// </summary> /// </summary>
ButtonClick = 25, ButtonClick = 25,
/// <summary> /// <summary>
/// List Item RollOver. /// Value Update (NumericInput, ScrollBar, etc.)
/// </summary>
ValueUpdate = 27,
/// <summary>
/// AtkComponentSlider Value Update.
/// </summary>
SliderValueUpdate = 29,
/// <summary>
/// AtkComponentSlider Released.
/// </summary>
SliderReleased = 30,
/// <summary>
/// AtkComponentList RollOver.
/// </summary> /// </summary>
ListItemRollOver = 33, ListItemRollOver = 33,
/// <summary> /// <summary>
/// List Item Roll Out. /// AtkComponentList Roll Out.
/// </summary> /// </summary>
ListItemRollOut = 34, ListItemRollOut = 34,
/// <summary> /// <summary>
/// List Item Toggle. /// AtkComponentList Click.
/// </summary> /// </summary>
ListItemClick = 35,
/// <summary>
/// AtkComponentList Toggle.
/// </summary>
[Obsolete("Use ListItemClick")]
ListItemToggle = 35, ListItemToggle = 35,
/// <summary> /// <summary>
/// Drag Drop Begin. /// AtkComponentList Double Click.
/// </summary>
ListItemDoubleClick = 36,
/// <summary>
/// AtkComponentList Select.
/// </summary>
ListItemSelect = 38,
/// <summary>
/// AtkComponentDragDrop Begin.
/// Sent on MouseDown over a draggable icon (will NOT send for a locked icon). /// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
/// </summary> /// </summary>
DragDropBegin = 47, DragDropBegin = 50,
/// <summary> /// <summary>
/// Drag Drop Insert. /// AtkComponentDragDrop End.
/// </summary>
DragDropEnd = 51,
/// <summary>
/// AtkComponentDragDrop Insert.
/// Sent when dropping an icon into a hotbar/inventory slot or similar. /// Sent when dropping an icon into a hotbar/inventory slot or similar.
/// </summary> /// </summary>
DragDropInsert = 50, DragDropInsert = 53,
/// <summary> /// <summary>
/// Drag Drop Roll Over. /// AtkComponentDragDrop Roll Over.
/// </summary> /// </summary>
DragDropRollOver = 52, DragDropRollOver = 55,
/// <summary> /// <summary>
/// Drag Drop Roll Out. /// AtkComponentDragDrop Roll Out.
/// </summary> /// </summary>
DragDropRollOut = 53, DragDropRollOut = 56,
/// <summary> /// <summary>
/// Drag Drop Discard. /// AtkComponentDragDrop Discard.
/// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar. /// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar.
/// </summary> /// </summary>
DragDropDiscard = 54, DragDropDiscard = 57,
/// <summary> /// <summary>
/// Drag Drop Unknown. /// Drag Drop Unknown.
/// </summary> /// </summary>
[Obsolete("Use DragDropDiscard")] [Obsolete("Use DragDropDiscard", true)]
DragDropUnk54 = 54, DragDropUnk54 = 54,
/// <summary> /// <summary>
/// Drag Drop Cancel. /// AtkComponentDragDrop Cancel.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon. /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// </summary> /// </summary>
DragDropCancel = 55, DragDropCancel = 58,
/// <summary> /// <summary>
/// Drag Drop Unknown. /// Drag Drop Unknown.
/// </summary> /// </summary>
[Obsolete("Use DragDropCancel")] [Obsolete("Use DragDropCancel", true)]
DragDropUnk55 = 55, DragDropUnk55 = 55,
/// <summary> /// <summary>
/// Icon Text Roll Over. /// AtkComponentIconText Roll Over.
/// </summary> /// </summary>
IconTextRollOver = 56, IconTextRollOver = 59,
/// <summary> /// <summary>
/// Icon Text Roll Out. /// AtkComponentIconText Roll Out.
/// </summary> /// </summary>
IconTextRollOut = 57, IconTextRollOut = 60,
/// <summary> /// <summary>
/// Icon Text Click. /// AtkComponentIconText Click.
/// </summary> /// </summary>
IconTextClick = 58, IconTextClick = 61,
/// <summary> /// <summary>
/// Window Roll Over. /// AtkDialogue Close.
/// </summary> /// </summary>
WindowRollOver = 67, DialogueClose = 62,
/// <summary> /// <summary>
/// Window Roll Out. /// AtkDialogue Submit.
/// </summary> /// </summary>
WindowRollOut = 68, DialogueSubmit = 63,
/// <summary> /// <summary>
/// Window Change Scale. /// AtkTimer Tick.
/// </summary> /// </summary>
WindowChangeScale = 69, TimerTick = 64,
/// <summary>
/// AtkTimer End.
/// </summary>
TimerEnd = 65,
/// <summary>
/// AtkSimpleTween Progress.
/// </summary>
TweenProgress = 67,
/// <summary>
/// AtkSimpleTween Complete.
/// </summary>
TweenComplete = 68,
/// <summary>
/// AtkAddonControl Child Addon Attached.
/// </summary>
ChildAddonAttached = 69,
/// <summary>
/// AtkComponentWindow Roll Over.
/// </summary>
WindowRollOver = 70,
/// <summary>
/// AtkComponentWindow Roll Out.
/// </summary>
WindowRollOut = 71,
/// <summary>
/// AtkComponentWindow Change Scale.
/// </summary>
WindowChangeScale = 72,
/// <summary>
/// AtkTextNode Link Mouse Click.
/// </summary>
LinkMouseClick = 75,
/// <summary>
/// AtkTextNode Link Mouse Over.
/// </summary>
LinkMouseOver = 76,
/// <summary>
/// AtkTextNode Link Mouse Out.
/// </summary>
LinkMouseOut = 77,
} }

View file

@ -1,10 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@ -37,6 +37,7 @@ internal unsafe class PluginEventController : IDisposable
/// <param name="atkEventType">The Event Type.</param> /// <param name="atkEventType">The Event Type.</param>
/// <param name="handler">The delegate to call when invoking this event.</param> /// <param name="handler">The delegate to call when invoking this event.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns> /// <returns>IAddonEventHandle used to remove the event.</returns>
[Obsolete("Use AddEvent that uses AddonEventDelegate instead of AddonEventHandler")]
public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler)
{ {
var node = (AtkResNode*)atkResNode; var node = (AtkResNode*)atkResNode;
@ -57,6 +58,49 @@ internal unsafe class PluginEventController : IDisposable
{ {
Addon = atkUnitBase, Addon = atkUnitBase,
Handler = handler, Handler = handler,
Delegate = null,
Node = atkResNode,
EventType = atkEventType,
ParamKey = eventId,
Handle = eventHandle,
};
Log.Verbose($"Adding Event. {eventEntry.LogString}");
this.EventListener.RegisterEvent(addon, node, eventType, eventId);
this.Events.Add(eventEntry);
return eventHandle;
}
/// <summary>
/// Adds a tracked event.
/// </summary>
/// <param name="atkUnitBase">The Parent addon for the event.</param>
/// <param name="atkResNode">The Node for the event.</param>
/// <param name="atkEventType">The Event Type.</param>
/// <param name="eventDelegate">The delegate to call when invoking this event.</param>
/// <returns>IAddonEventHandle used to remove the event.</returns>
public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventDelegate eventDelegate)
{
var node = (AtkResNode*)atkResNode;
var addon = (AtkUnitBase*)atkUnitBase;
var eventType = (AtkEventType)atkEventType;
var eventId = this.GetNextParamKey();
var eventGuid = Guid.NewGuid();
var eventHandle = new AddonEventHandle
{
AddonName = addon->NameString,
ParamKey = eventId,
EventType = atkEventType,
EventGuid = eventGuid,
};
var eventEntry = new AddonEventEntry
{
Addon = atkUnitBase,
Delegate = eventDelegate,
Handler = null,
Node = atkResNode, Node = atkResNode,
EventType = atkEventType, EventType = atkEventType,
ParamKey = eventId, ParamKey = eventId,
@ -139,17 +183,19 @@ internal unsafe class PluginEventController : IDisposable
// Is our stored addon pointer the same as the active addon pointer? // Is our stored addon pointer the same as the active addon pointer?
if (currentAddonPointer != eventEntry.Addon) return; if (currentAddonPointer != eventEntry.Addon) return;
// Does this addon contain the node this event is for? (by address) // Make sure the addon is not unloaded
var atkUnitBase = (AtkUnitBase*)currentAddonPointer; var atkUnitBase = (AtkUnitBase*)currentAddonPointer;
var nodeFound = false; if (atkUnitBase->UldManager.LoadedState == AtkLoadState.Unloaded) return;
foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount))
{
var node = atkUnitBase->UldManager.NodeList[index];
// Does this addon contain the node this event is for? (by address)
var nodeFound = false;
foreach (var node in atkUnitBase->UldManager.Nodes)
{
// If this node matches our node, then we know our node is still valid. // If this node matches our node, then we know our node is still valid.
if (node is not null && (nint)node == eventEntry.Node) if ((nint)node.Value == eventEntry.Node)
{ {
nodeFound = true; nodeFound = true;
break;
} }
} }
@ -184,6 +230,7 @@ internal unsafe class PluginEventController : IDisposable
this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey); this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey);
} }
[Api13ToDo("Remove invoke from eventInfo.Handler, and remove nullability from eventInfo.Delegate?.Invoke")]
private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr) private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr)
{ {
try try
@ -192,7 +239,18 @@ internal unsafe class PluginEventController : IDisposable
if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return; if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return;
// We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event. // We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event.
eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target); eventInfo.Handler?.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target);
eventInfo.Delegate?.Invoke((AddonEventType)eventType, new AddonEventData
{
AddonPointer = (nint)eventPtr->Node,
NodeTargetPointer = (nint)eventPtr->Target,
AtkEventDataPointer = (nint)eventDataPtr,
AtkEventListener = (nint)self,
AtkEventType = (AddonEventType)eventType,
Param = eventParam,
AtkEventPointer = (nint)eventPtr,
});
} }
catch (Exception exception) catch (Exception exception)
{ {

View file

@ -42,7 +42,7 @@ internal class AddonSetupHook<T> : IDisposable where T : Delegate
} }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is enabled. /// Gets a value indicating whether the hook is enabled.
/// </summary> /// </summary>
public bool IsEnabled => this.asmHook.IsEnabled; public bool IsEnabled => this.asmHook.IsEnabled;

View file

@ -8,6 +8,8 @@ using Dalamud.Configuration.Internal;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
@ -41,7 +43,7 @@ internal partial class ChatHandlers : IServiceType
public string? LastLink { get; private set; } public string? LastLink { get; private set; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not auto-updates have already completed this session. /// Gets a value indicating whether auto-updates have already completed this session.
/// </summary> /// </summary>
public bool IsAutoUpdateComplete { get; private set; } public bool IsAutoUpdateComplete { get; private set; }
@ -100,8 +102,6 @@ internal partial class ChatHandlers : IServiceType
if (chatGui == null || pluginManager == null || dalamudInterface == null) if (chatGui == null || pluginManager == null || dalamudInterface == null)
return; return;
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
if (this.configuration.PrintDalamudWelcomeMsg) if (this.configuration.PrintDalamudWelcomeMsg)
{ {
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
@ -116,15 +116,30 @@ internal partial class ChatHandlers : IServiceType
} }
} }
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
{ {
var linkPayload = chatGui.AddChatLinkHandler(
"dalamud",
8459324,
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
var updateMessage = new SeStringBuilder()
.AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!"))
.AddUiForeground(500)
.AddText(" [")
.Add(linkPayload)
.AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog."))
.Add(RawPayload.LinkTerminator)
.AddText("]")
.AddUiForegroundOff();
chatGui.Print(new XivChatEntry chatGui.Print(new XivChatEntry
{ {
Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."), Message = updateMessage.Build(),
Type = XivChatType.Notice, Type = XivChatType.Notice,
}); });
this.configuration.LastVersion = assemblyVersion; this.configuration.LastVersion = Util.AssemblyVersion;
this.configuration.QueueSave(); this.configuration.QueueSave();
} }

View file

@ -158,7 +158,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
ConditionFlag.NormalConditions, ConditionFlag.NormalConditions,
ConditionFlag.Jumping, ConditionFlag.Jumping,
ConditionFlag.Mounted, ConditionFlag.Mounted,
ConditionFlag.UsingParasol]); ConditionFlag.UsingFashionAccessory]);
blockingFlag = blockingConditions.FirstOrDefault(); blockingFlag = blockingConditions.FirstOrDefault();
return blockingFlag == 0; return blockingFlag == 0;

View file

@ -5,6 +5,8 @@ namespace Dalamud.Game.ClientState.Conditions;
/// ///
/// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to /// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to
/// LogMessage row 7700 and onwards, which can be checked by looking at the Condition sheet and looking at what column 2 maps to. /// LogMessage row 7700 and onwards, which can be checked by looking at the Condition sheet and looking at what column 2 maps to.
///
/// The first 24 conditions are the local players CharacterModes.
/// </summary> /// </summary>
public enum ConditionFlag public enum ConditionFlag
{ {
@ -176,11 +178,20 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Unable to execute command while occupied. /// Unable to execute command while occupied.
/// </summary> /// </summary>
/// <remarks>
/// Observed during Materialize (Desynthesis, Materia Extraction, Aetherial Reduction) and Repair.
/// </remarks>
Occupied39 = 39, Occupied39 = 39,
/// <summary> /// <summary>
/// Unable to execute command while crafting. /// Unable to execute command while crafting.
/// </summary> /// </summary>
ExecutingCraftingAction = 40,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
[Obsolete("Renamed to ExecutingCraftingAction.")]
Crafting40 = 40, Crafting40 = 40,
/// <summary> /// <summary>
@ -191,6 +202,13 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Unable to execute command while gathering. /// Unable to execute command while gathering.
/// </summary> /// </summary>
/// <remarks> Includes fishing. </remarks>
ExecutingGatheringAction = 42,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
[Obsolete("Renamed to ExecutingGatheringAction.")]
Gathering42 = 42, Gathering42 = 42,
/// <summary> /// <summary>
@ -220,8 +238,14 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Unable to execute command while auto-run is active. /// Unable to execute command while auto-run is active.
/// </summary> /// </summary>
[Obsolete("To avoid confusion, renamed to UsingChocoboTaxi.")]
AutorunActive = 49, AutorunActive = 49,
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
UsingChocoboTaxi = 49,
/// <summary> /// <summary>
/// Unable to execute command while occupied. /// Unable to execute command while occupied.
/// </summary> /// </summary>
@ -261,8 +285,14 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Unable to execute command at this time. /// Unable to execute command at this time.
/// </summary> /// </summary>
[Obsolete("Renamed to MountOrOrnamentTransition.")]
Unknown57 = 57, Unknown57 = 57,
/// <summary>
/// Unable to execute command at this time.
/// </summary>
MountOrOrnamentTransition = 57,
/// <summary> /// <summary>
/// Unable to execute command while watching a cutscene. /// Unable to execute command while watching a cutscene.
/// </summary> /// </summary>
@ -331,6 +361,9 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Unable to execute command while mounting. /// Unable to execute command while mounting.
/// </summary> /// </summary>
/// <remarks>
/// Observed in Cosmic Exploration while using the actions Astrodrill (only briefly) and Solar Flarethrower.
/// </remarks>
Mounting71 = 71, Mounting71 = 71,
/// <summary> /// <summary>
@ -398,7 +431,10 @@ public enum ConditionFlag
/// </summary> /// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84, ParticipatingInCrossWorldPartyOrAlliance = 84,
// Unknown85 = 85, /// <remarks>
/// Observed in Cosmic Exploration while gathering during a stellar mission.
/// </remarks>
Unknown85 = 85,
/// <summary> /// <summary>
/// Unable to execute command while playing duty record. /// Unable to execute command while playing duty record.
@ -450,8 +486,14 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Unable to execute command while using a parasol. /// Unable to execute command while using a parasol.
/// </summary> /// </summary>
[Obsolete("Renamed to UsingFashionAccessory.")]
UsingParasol = 94, UsingParasol = 94,
/// <summary>
/// Unable to execute command while using a fashion accessory.
/// </summary>
UsingFashionAccessory = 94,
/// <summary> /// <summary>
/// Unable to execute command while bound by duty. /// Unable to execute command while bound by duty.
/// </summary> /// </summary>
@ -460,6 +502,9 @@ public enum ConditionFlag
/// <summary> /// <summary>
/// Cannot execute at this time. /// Cannot execute at this time.
/// </summary> /// </summary>
/// <remarks>
/// Observed in Cosmic Exploration while participating in MechaEvent.
/// </remarks>
Unknown96 = 96, Unknown96 = 96,
/// <summary> /// <summary>
@ -481,4 +526,22 @@ public enum ConditionFlag
/// Unable to execute command while editing a portrait. /// Unable to execute command while editing a portrait.
/// </summary> /// </summary>
EditingPortrait = 100, EditingPortrait = 100,
/// <summary>
/// Cannot execute at this time.
/// </summary>
/// <remarks>
/// Observed in Cosmic Exploration, in mech flying to FATE or during Cosmoliner use. Maybe ClientPath related.
/// </remarks>
Unknown101 = 101,
/// <summary>
/// Unable to execute command while undertaking a duty.
/// </summary>
/// <remarks>
/// Used in Cosmic Exploration.
/// </remarks>
PilotingMech = 102,
// Unknown103 = 103,
} }

View file

@ -3,6 +3,7 @@ using System.Numerics;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
@ -69,13 +70,13 @@ public interface IFate : IEquatable<IFate>
byte Progress { get; } byte Progress { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus. /// Gets a value indicating whether this <see cref="Fate"/> has a EXP bonus.
/// </summary> /// </summary>
[Obsolete($"Use {nameof(HasBonus)} instead")] [Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; } bool HasExpBonus { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a bonus. /// Gets a value indicating whether this <see cref="Fate"/> has a bonus.
/// </summary> /// </summary>
bool HasBonus { get; } bool HasBonus { get; }
@ -222,8 +223,8 @@ internal unsafe partial class Fate : IFate
public byte Progress => this.Struct->Progress; public byte Progress => this.Struct->Progress;
/// <inheritdoc/> /// <inheritdoc/>
[Obsolete($"Use {nameof(HasBonus)} instead")] [Api13ToDo("Remove")]
public bool HasExpBonus => this.Struct->IsExpBonus; public bool HasExpBonus => this.HasBonus;
/// <inheritdoc/> /// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus; public bool HasBonus => this.Struct->IsBonus;
@ -249,5 +250,5 @@ internal unsafe partial class Fate : IFate
/// <summary> /// <summary>
/// Gets the territory this <see cref="Fate"/> is located in. /// Gets the territory this <see cref="Fate"/> is located in.
/// </summary> /// </summary>
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].TerritoryId); public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
} }

View file

@ -45,19 +45,19 @@ public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
public int AstralSoulStacks => this.Struct->AstralSoulStacks; public int AstralSoulStacks => this.Struct->AstralSoulStacks;
/// <summary> /// <summary>
/// Gets a value indicating whether or not the player is in Umbral Ice. /// Gets a value indicating whether the player is in Umbral Ice.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool InUmbralIce => this.Struct->ElementStance < 0; public bool InUmbralIce => this.Struct->ElementStance < 0;
/// <summary> /// <summary>
/// Gets a value indicating whether or not the player is in Astral fire. /// Gets a value indicating whether the player is in Astral fire.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool InAstralFire => this.Struct->ElementStance > 0; public bool InAstralFire => this.Struct->ElementStance > 0;
/// <summary> /// <summary>
/// Gets a value indicating whether or not Enochian is active. /// Gets a value indicating whether Enochian is active.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsEnochianActive => this.Struct->EnochianActive; public bool IsEnochianActive => this.Struct->EnochianActive;

View file

@ -30,27 +30,27 @@ public unsafe class PCTGauge : JobGaugeBase<PictomancerGauge>
public byte Paint => Struct->Paint; public byte Paint => Struct->Paint;
/// <summary> /// <summary>
/// Gets a value indicating whether or not a creature motif is drawn. /// Gets a value indicating whether a creature motif is drawn.
/// </summary> /// </summary>
public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn; public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn;
/// <summary> /// <summary>
/// Gets a value indicating whether or not a weapon motif is drawn. /// Gets a value indicating whether a weapon motif is drawn.
/// </summary> /// </summary>
public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn; public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn;
/// <summary> /// <summary>
/// Gets a value indicating whether or not a landscape motif is drawn. /// Gets a value indicating whether a landscape motif is drawn.
/// </summary> /// </summary>
public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn; public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn;
/// <summary> /// <summary>
/// Gets a value indicating whether or not a moogle portrait is ready. /// Gets a value indicating whether a moogle portrait is ready.
/// </summary> /// </summary>
public bool MooglePortraitReady => Struct->MooglePortraitReady; public bool MooglePortraitReady => Struct->MooglePortraitReady;
/// <summary> /// <summary>
/// Gets a value indicating whether or not a madeen portrait is ready. /// Gets a value indicating whether a madeen portrait is ready.
/// </summary> /// </summary>
public bool MadeenPortraitReady => Struct->MadeenPortraitReady; public bool MadeenPortraitReady => Struct->MadeenPortraitReady;

View file

@ -42,7 +42,7 @@ public enum CustomizeIndex
HairStyle = 0x06, HairStyle = 0x06,
/// <summary> /// <summary>
/// Whether or not the character has hair highlights. /// Whether the character has hair highlights.
/// </summary> /// </summary>
HasHighlights = 0x07, // negative to enable, positive to disable HasHighlights = 0x07, // negative to enable, positive to disable

View file

@ -45,6 +45,16 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
this.console.Invoke += this.ConsoleOnInvoke; this.console.Invoke += this.ConsoleOnInvoke;
} }
/// <summary>
/// Published whenever a command is registered
/// </summary>
public event EventHandler<CommandEventArgs>? CommandAdded;
/// <summary>
/// Published whenever a command is unregistered
/// </summary>
public event EventHandler<CommandEventArgs>? CommandRemoved;
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => new(this.commandMap); public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => new(this.commandMap);
@ -122,6 +132,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
return false; return false;
} }
this.CommandAdded?.Invoke(this, new CommandEventArgs
{
Command = command,
CommandInfo = info,
});
if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName)) if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName))
{ {
this.commandMap.Remove(command, out _); this.commandMap.Remove(command, out _);
@ -144,6 +160,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
return false; return false;
} }
this.CommandAdded?.Invoke(this, new CommandEventArgs
{
Command = command,
CommandInfo = info,
});
return true; return true;
} }
@ -155,7 +177,17 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
this.commandAssemblyNameMap.TryRemove(assemblyKeyValuePair.Key, out _); this.commandAssemblyNameMap.TryRemove(assemblyKeyValuePair.Key, out _);
} }
return this.commandMap.Remove(command, out _); var removed = this.commandMap.Remove(command, out var info);
if (removed)
{
this.CommandRemoved?.Invoke(this, new CommandEventArgs
{
Command = command,
CommandInfo = info,
});
}
return removed;
} }
/// <summary> /// <summary>
@ -204,6 +236,20 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
return this.ProcessCommand(command->ToString()) ? 0 : result; return this.ProcessCommand(command->ToString()) ? 0 : result;
} }
/// <inheritdoc />
public class CommandEventArgs : EventArgs
{
/// <summary>
/// Gets the command string.
/// </summary>
public string Command { get; init; }
/// <summary>
/// Gets the command info.
/// </summary>
public IReadOnlyCommandInfo CommandInfo { get; init; }
}
} }
/// <summary> /// <summary>
@ -268,7 +314,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
} }
else else
{ {
Log.Error($"Command {command} is already registered."); Log.Error("Command {Command} is already registered.", command);
} }
return false; return false;
@ -287,7 +333,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
} }
else else
{ {
Log.Error($"Command {command} not found."); Log.Error("Command {Command} not found.", command);
} }
return false; return false;

View file

@ -12,6 +12,7 @@ using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
@ -504,14 +505,18 @@ internal sealed class Framework : IInternalDisposableService, IFramework
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class FrameworkPluginScoped : IInternalDisposableService, IFramework internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
{ {
private readonly PluginErrorHandler pluginErrorHandler;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework frameworkService = Service<Framework>.Get(); private readonly Framework frameworkService = Service<Framework>.Get();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FrameworkPluginScoped"/> class. /// Initializes a new instance of the <see cref="FrameworkPluginScoped"/> class.
/// </summary> /// </summary>
internal FrameworkPluginScoped() /// <param name="pluginErrorHandler">Error handler instance.</param>
internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler)
{ {
this.pluginErrorHandler = pluginErrorHandler;
this.frameworkService.Update += this.OnUpdateForward; this.frameworkService.Update += this.OnUpdateForward;
} }
@ -604,7 +609,7 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
} }
else else
{ {
this.Update?.Invoke(framework); this.pluginErrorHandler.InvokeAndCatch(this.Update, $"{nameof(IFramework)}::{nameof(IFramework.Update)}", framework);
} }
} }
} }

View file

@ -220,6 +220,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
/// <param name="commandId">The ID of the command to run.</param> /// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param> /// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns> /// <returns>A payload for handling.</returns>
[Api13ToDo("Plugins should not specify their own command IDs here. We should assign them ourselves.")]
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction) internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{ {
var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId };

View file

@ -269,7 +269,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// Check whether an entry with the specified title exists. /// Check whether an entry with the specified title exists.
/// </summary> /// </summary>
/// <param name="title">The title to check for.</param> /// <param name="title">The title to check for.</param>
/// <returns>Whether or not an entry with that title is registered.</returns> /// <returns>Whether an entry with that title is registered.</returns>
internal bool HasEntry(string title) internal bool HasEntry(string title)
{ {
var found = false; var found = false;

View file

@ -39,7 +39,7 @@ public interface IReadOnlyDtrBarEntry
public bool Shown { get; } public bool Shown { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the user has hidden this entry from view through the Dalamud settings. /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings.
/// </summary> /// </summary>
public bool UserHidden { get; } public bool UserHidden { get; }
@ -145,7 +145,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
} }
/// <inheritdoc/> /// <inheritdoc/>
[Api12ToDo("Maybe make this config scoped to internalname?")] [Api13ToDo("Maybe make this config scoped to internalname?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <summary> /// <summary>

View file

@ -94,22 +94,37 @@ public enum FlyTextKind : int
/// <summary> /// <summary>
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// Added in 7.2, usage currently unknown.
/// </summary> /// </summary>
[Obsolete("Use Dataset instead", true)]
Unknown16 = 16, Unknown16 = 16,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// Added in 7.2, usage currently unknown.
/// </summary> /// </summary>
Dataset = 16,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use Knowledge instead", true)]
Unknown17 = 17, Unknown17 = 17,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Added in 7.2, usage currently unknown.
/// </summary> /// </summary>
Knowledge = 17,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use PhantomExp instead", true)]
Unknown18 = 18, Unknown18 = 18,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
PhantomExp = 18,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>

View file

@ -257,7 +257,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// <summary> /// <summary>
/// Indicates if the game is in the lobby scene (title screen, chara select, chara make, aesthetician etc.). /// Indicates if the game is in the lobby scene (title screen, chara select, chara make, aesthetician etc.).
/// </summary> /// </summary>
/// <returns>A value indicating whether or not the game is in the lobby scene.</returns> /// <returns>A value indicating whether the game is in the lobby scene.</returns>
internal bool IsInLobby() => RaptureAtkModule.Instance()->CurrentUIScene.StartsWith("LobbyMain"u8); internal bool IsInLobby() => RaptureAtkModule.Instance()->CurrentUIScene.StartsWith("LobbyMain"u8);
/// <summary> /// <summary>

View file

@ -6,7 +6,7 @@ namespace Dalamud.Game.Gui.NamePlate;
/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title). /// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title).
/// </summary> /// </summary>
/// <param name="field">The field type which should be set.</param> /// <param name="field">The field type which should be set.</param>
/// <param name="isFreeCompany">Whether or not this is a Free Company part.</param> /// <param name="isFreeCompany">Whether this is a Free Company part.</param>
/// <remarks> /// <remarks>
/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be /// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
/// performed. Only after all handler processing is complete does it write out any parts which were set to the /// performed. Only after all handler processing is complete does it write out any parts which were set to the
@ -53,7 +53,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany
return; return;
var sb = new SeStringBuilder(); var sb = new SeStringBuilder();
if (this.OuterWrap is { Item1: var outerLeft }) if (this.OuterWrap is { Item1: { } outerLeft })
{ {
sb.Append(outerLeft); sb.Append(outerLeft);
} }
@ -67,7 +67,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany
sb.Append(isFreeCompany ? " «" : "《"); sb.Append(isFreeCompany ? " «" : "《");
} }
if (this.TextWrap is { Item1: var left, Item2: var right }) if (this.TextWrap is { Item1: { } left, Item2: { } right })
{ {
sb.Append(left); sb.Append(left);
sb.Append(this.Text ?? this.GetStrippedField(handler)); sb.Append(this.Text ?? this.GetStrippedField(handler));
@ -87,7 +87,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany
sb.Append(isFreeCompany ? "»" : "》"); sb.Append(isFreeCompany ? "»" : "》");
} }
if (this.OuterWrap is { Item2: var outerRight }) if (this.OuterWrap is { Item2: { } outerRight })
{ {
sb.Append(outerRight); sb.Append(outerRight);
} }

View file

@ -35,7 +35,7 @@ public class NamePlateSimpleParts(NamePlateStringField field)
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
return; return;
if (this.TextWrap is { Item1: var left, Item2: var right }) if (this.TextWrap is { Item1: { } left, Item2: { } right })
{ {
var sb = new SeStringBuilder(); var sb = new SeStringBuilder();
sb.Append(left); sb.Append(left);

View file

@ -1,102 +0,0 @@
using System.Collections.Generic;
using Dalamud.Utility;
#if !DEBUG
using Dalamud.Configuration.Internal;
#endif
using Serilog;
namespace Dalamud.Game.Internal;
/// <summary>
/// This class disables anti-debug functionality in the game client.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class AntiDebug : IInternalDisposableService
{
private readonly byte[] nop = [0x31, 0xC0, 0x90, 0x90, 0x90, 0x90];
private byte[]? original;
private IntPtr debugCheckAddress;
[ServiceManager.ServiceConstructor]
private AntiDebug(TargetSigScanner sigScanner)
{
try
{
// This sig has to be the call site in Framework_Tick
this.debugCheckAddress = sigScanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 13 41");
}
catch (KeyNotFoundException)
{
this.debugCheckAddress = IntPtr.Zero;
}
Log.Verbose($"Debug check address {Util.DescribeAddress(this.debugCheckAddress)}");
if (!this.IsEnabled)
{
#if DEBUG
this.Enable();
#else
if (Service<DalamudConfiguration>.Get().IsAntiAntiDebugEnabled)
this.Enable();
#endif
}
}
/// <summary>Finalizes an instance of the <see cref="AntiDebug"/> class.</summary>
~AntiDebug() => ((IInternalDisposableService)this).DisposeService();
/// <summary>
/// Gets a value indicating whether the anti-debugging is enabled.
/// </summary>
public bool IsEnabled { get; private set; } = false;
/// <inheritdoc />
void IInternalDisposableService.DisposeService() => this.Disable();
/// <summary>
/// Enables the anti-debugging by overwriting code in memory.
/// </summary>
public void Enable()
{
if (this.IsEnabled)
return;
this.original = new byte[this.nop.Length];
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
{
Log.Information($"Overwriting debug check at {Util.DescribeAddress(this.debugCheckAddress)}");
SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original);
SafeMemory.WriteBytes(this.debugCheckAddress, this.nop);
}
else
{
Log.Information("Debug check already overwritten?");
}
this.IsEnabled = true;
}
/// <summary>
/// Disable the anti-debugging by reverting the overwritten code in memory.
/// </summary>
public void Disable()
{
if (!this.IsEnabled)
return;
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
{
Log.Information($"Reverting debug check at {Util.DescribeAddress(this.debugCheckAddress)}");
SafeMemory.WriteBytes(this.debugCheckAddress, this.original);
}
else
{
Log.Information("Debug check was not overwritten?");
}
this.IsEnabled = false;
}
}

View file

@ -0,0 +1,322 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Completion;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Internal;
/// <summary>
/// This class adds dalamud and plugin commands to the chat box's autocompletion.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class Completion : IInternalDisposableService
{
// 0xFF is a magic group number that causes CompletionModule's internals to treat entries
// as raw strings instead of as lookups into an EXD sheet
private const int GroupNumber = 0xFF;
[ServiceManager.ServiceDependency]
private readonly CommandManager commandManager = Service<CommandManager>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly Dictionary<string, EntryStrings> cachedCommands = [];
private readonly ConcurrentQueue<string> addedCommands = [];
private EntryStrings? dalamudCategory;
private Hook<CompletionModule.Delegates.GetSelection>? getSelection;
// This is marked volatile since we set and check it from different threads. Instead of using a synchronization
// primitive, a volatile is sufficient since the absolute worst case is that we delay one extra frame to reset
// the list, which is fine
private volatile bool needsClear;
private bool disposed;
private nint wantedVtblPtr;
/// <summary>
/// Initializes a new instance of the <see cref="Completion"/> class.
/// </summary>
[ServiceManager.ServiceConstructor]
internal Completion()
{
this.commandManager.CommandAdded += this.OnCommandAdded;
this.commandManager.CommandRemoved += this.OnCommandRemoved;
this.framework.Update += this.OnUpdate;
}
/// <summary>Finalizes an instance of the <see cref="Completion"/> class.</summary>
~Completion() => this.Dispose(false);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true);
private static AtkUnitBase* FindOwningAddon(AtkComponentTextInput* component)
{
if (component == null) return null;
var node = (AtkResNode*)component->OwnerNode;
if (node == null) return null;
while (node->ParentNode != null)
node = node->ParentNode;
foreach (var addon in RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Entries)
{
if (addon.Value->RootNode == node)
return addon;
}
return null;
}
private AtkComponentTextInput* GetActiveTextInput()
{
var mod = RaptureAtkModule.Instance();
if (mod == null) return null;
var basePtr = mod->TextInput.TargetTextInputEventInterface;
if (basePtr == null) return null;
// Once CS has an implementation for multiple inheritance, we can remove this sig from dalamud
// as well as the nasty pointer arithmetic below. But for now, we need to do this manually.
// The AtkTextInputEventInterface* is the secondary base class for AtkComponentTextInput*
// so the pointer is sizeof(AtkComponentInputBase) into the object. We verify that we're looking
// at the object we think we are by confirming the pointed-to vtbl matches the known secondary vtbl for
// AtkComponentTextInput, and if it does, we can shift the pointer back to get the start of our text input
if (this.wantedVtblPtr == 0)
{
this.wantedVtblPtr =
Service<TargetSigScanner>.Get().GetStaticAddressFromSig(
"48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 8B 48 68",
4);
}
var vtblPtr = *(nint*)basePtr;
if (vtblPtr != this.wantedVtblPtr) return null;
// This needs to be updated if the layout/base order of AtkComponentTextInput changes
return (AtkComponentTextInput*)((AtkComponentInputBase*)basePtr - 1);
}
private bool AllowCompletion(string cmd)
{
// this is one of our commands, let's see if we should allow this to be completed
var component = this.GetActiveTextInput();
// ContainingAddon or ContainingAddon2 aren't always populated, but they
// seem to be in any case where this is actually a completable AtkComponentTextInput
// In the worst case, we can walk the AtkNode tree- but let's try the easy pointers first
var addon = component->ContainingAddon;
if (addon == null) addon = component->ContainingAddon2;
if (addon == null) addon = FindOwningAddon(component);
if (addon == null || addon->NameString != "ChatLog")
{
// we don't know what addon is completing, or we know it isn't ChatLog
// either way, we should just reject this completion
return false;
}
// We're in ChatLog, so check if this is the start of the text input
// AtkComponentTextInput->UnkText1 is the evaluated version of the current text
// so if the command starts with that, then either it's empty or a prefix completion.
// In either case, we're happy to allow completion.
return cmd.StartsWith(component->UnkText1.StringPtr.ExtractText());
}
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.getSelection?.Disable();
this.getSelection?.Dispose();
this.framework.Update -= this.OnUpdate;
this.commandManager.CommandAdded -= this.OnCommandAdded;
this.commandManager.CommandRemoved -= this.OnCommandRemoved;
this.dalamudCategory?.Dispose();
this.ClearCachedCommands();
}
this.disposed = true;
}
private void OnCommandAdded(object? sender, CommandManager.CommandEventArgs e)
{
if (e.CommandInfo.ShowInHelp)
this.addedCommands.Enqueue(e.Command);
}
private void OnCommandRemoved(object? sender, CommandManager.CommandEventArgs e) => this.needsClear = true;
private void OnUpdate(IFramework fw)
{
var atkModule = RaptureAtkModule.Instance();
if (atkModule == null) return;
var textInput = &atkModule->TextInput;
if (textInput->CompletionModule == null) return;
// Before we change _anything_ we need to check the state of the UI- if the completion list is open
// changes to the underlying data are extremely unsafe, so we'll just wait until the next frame
// worst case, someone tries to complete a command that _just_ got unloaded so it won't do anything
// but that's the same as making a typo, really
if (textInput->CompletionDepth > 0) return;
// Create the category for Dalamud commands.
// This needs to be done here, since we cannot create Utf8Strings before the game
// has initialized (no allocator set up yet).
this.dalamudCategory ??= new EntryStrings("【Dalamud】");
this.LoadCommands(textInput->CompletionModule);
}
private CategoryData* EnsureCategoryData(CompletionModule* module)
{
if (module == null) return null;
if (this.getSelection == null)
{
this.getSelection = Hook<CompletionModule.Delegates.GetSelection>.FromAddress(
(IntPtr)module->VirtualTable->GetSelection,
this.GetSelectionDetour);
this.getSelection.Enable();
}
for (var i = 0; i < module->CategoryNames.Count; i++)
{
if (module->CategoryNames[i].AsReadOnlySeStringSpan().ContainsText("【Dalamud】"u8))
{
return module->CategoryData[i];
}
}
// Create the category since we don't have one
var categoryData = (CategoryData*)Memory.MemoryHelper.GameAllocateDefault((ulong)sizeof(CategoryData));
categoryData->Ctor(GroupNumber, 0xFF);
module->AddCategoryData(GroupNumber, this.dalamudCategory!.Display->StringPtr,
this.dalamudCategory.Match->StringPtr, categoryData);
return categoryData;
}
private void ClearCachedCommands()
{
if (this.cachedCommands.Count == 0)
return;
foreach (var entry in this.cachedCommands.Values)
{
entry.Dispose();
}
this.cachedCommands.Clear();
}
private void LoadCommands(CompletionModule* completionModule)
{
if (completionModule == null) return;
if (completionModule->CategoryNames.Count == 0) return; // We want this data populated first
if (this.needsClear && this.cachedCommands.Count > 0)
{
this.needsClear = false;
completionModule->ClearCompletionData();
this.ClearCachedCommands();
return;
}
var catData = this.EnsureCategoryData(completionModule);
if (catData == null) return;
if (catData->CompletionData.Count == 0)
{
var inputCommands = this.commandManager.Commands.Where(pair => pair.Value.ShowInHelp);
foreach (var (cmd, _) in inputCommands)
AddEntry(cmd);
catData->SortEntries();
return;
}
var needsSort = false;
while (this.addedCommands.TryDequeue(out var cmd))
{
needsSort = true;
AddEntry(cmd);
}
if (needsSort)
catData->SortEntries();
return;
void AddEntry(string cmd)
{
if (this.cachedCommands.ContainsKey(cmd)) return;
var cmdStr = new EntryStrings(cmd);
this.cachedCommands.Add(cmd, cmdStr);
completionModule->AddCompletionEntry(
GroupNumber,
0xFF,
cmdStr.Display->StringPtr,
cmdStr.Match->StringPtr,
0xFF);
}
}
private int GetSelectionDetour(CompletionModule* thisPtr, CategoryData.CompletionDataStruct* dataStructs, int index, Utf8String* outputString, Utf8String* outputDisplayString)
{
var ret = this.getSelection!.Original.Invoke(thisPtr, dataStructs, index, outputString, outputDisplayString);
if (ret != -2 || outputString == null) return ret;
// -2 means it was a plain text final selection, so it might be ours
// Unfortunately, the code that uses this string mangles the color macro for some reason...
// We'll just strip those out since we don't need the color in the chatbox
var txt = outputString->StringPtr.ExtractText();
if (!this.cachedCommands.ContainsKey(txt))
return ret;
if (!this.AllowCompletion(txt))
{
outputString->Clear();
if (outputDisplayString != null) outputDisplayString->Clear();
return ret;
}
outputString->SetString(txt + " ");
return ret;
}
private class EntryStrings(string command) : IDisposable
{
public Utf8String* Display { get; } =
Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).BuiltString.EncodeWithNullTerminator());
public Utf8String* Match { get; } = Utf8String.FromString(command);
public void Dispose()
{
this.Display->Dtor(true);
this.Match->Dtor(true);
}
}
}

View file

@ -21,7 +21,7 @@ internal class MarketBoardItemRequest
public uint Status { get; private set; } public uint Status { get; private set; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not this request was successful. /// Gets a value indicating whether this request was successful.
/// </summary> /// </summary>
public bool Ok => this.Status == 0; public bool Ok => this.Status == 0;

View file

@ -248,7 +248,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
/// <summary> /// <summary>
/// Disposes of managed and unmanaged resources. /// Disposes of managed and unmanaged resources.
/// </summary> /// </summary>
/// <param name="shouldDispose">Whether or not to execute the disposal.</param> /// <param name="shouldDispose">Whether to execute the disposal.</param>
protected void Dispose(bool shouldDispose) protected void Dispose(bool shouldDispose)
{ {
if (!shouldDispose) if (!shouldDispose)

View file

@ -31,7 +31,7 @@ public class SigScanner : IDisposable, ISigScanner
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SigScanner"/> class using the main module of the current process. /// Initializes a new instance of the <see cref="SigScanner"/> class using the main module of the current process.
/// </summary> /// </summary>
/// <param name="doCopy">Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param> /// <param name="doCopy">Whether to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param>
/// <param name="cacheFile">File used to cached signatures.</param> /// <param name="cacheFile">File used to cached signatures.</param>
public SigScanner(bool doCopy = false, FileInfo? cacheFile = null) public SigScanner(bool doCopy = false, FileInfo? cacheFile = null)
: this(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) : this(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile)
@ -42,7 +42,7 @@ public class SigScanner : IDisposable, ISigScanner
/// Initializes a new instance of the <see cref="SigScanner"/> class. /// Initializes a new instance of the <see cref="SigScanner"/> class.
/// </summary> /// </summary>
/// <param name="module">The ProcessModule to be used for scanning.</param> /// <param name="module">The ProcessModule to be used for scanning.</param>
/// <param name="doCopy">Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param> /// <param name="doCopy">Whether to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param>
/// <param name="cacheFile">File used to cached signatures.</param> /// <param name="cacheFile">File used to cached signatures.</param>
public SigScanner(ProcessModule module, bool doCopy = false, FileInfo? cacheFile = null) public SigScanner(ProcessModule module, bool doCopy = false, FileInfo? cacheFile = null)
{ {

View file

@ -19,7 +19,7 @@ internal class TargetSigScanner : SigScanner, IPublicDisposableService
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TargetSigScanner"/> class. /// Initializes a new instance of the <see cref="TargetSigScanner"/> class.
/// </summary> /// </summary>
/// <param name="doCopy">Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param> /// <param name="doCopy">Whether to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks.</param>
/// <param name="cacheFile">File used to cached signatures.</param> /// <param name="cacheFile">File used to cached signatures.</param>
public TargetSigScanner(bool doCopy = false, FileInfo? cacheFile = null) public TargetSigScanner(bool doCopy = false, FileInfo? cacheFile = null)
: base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) : base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile)

View file

@ -18,12 +18,12 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text; using FFXIVClientStructs.FFXIV.Component.Text;
using FFXIVClientStructs.Interop;
using Lumina.Data.Structs.Excel; using Lumina.Data.Structs.Excel;
using Lumina.Excel; using Lumina.Excel;
@ -35,6 +35,8 @@ using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using AddonSheet = Lumina.Excel.Sheets.Addon; using AddonSheet = Lumina.Excel.Sheets.Addon;
using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState;
using StatusSheet = Lumina.Excel.Sheets.Status;
namespace Dalamud.Game.Text.Evaluator; namespace Dalamud.Game.Text.Evaluator;
@ -50,6 +52,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
{ {
private static readonly ModuleLog Log = new("SeStringEvaluator"); private static readonly ModuleLog Log = new("SeStringEvaluator");
[ServiceManager.ServiceDependency]
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get(); private readonly DataManager dataManager = Service<DataManager>.Get();
@ -91,7 +96,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (str.IsTextOnly()) if (str.IsTextOnly())
return new(str); return new(str);
var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); var lang = language ?? this.GetEffectiveClientLanguage();
// TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append,
// and then remove try...finally block (discard builder from the pool on exception) // and then remove try...finally block (discard builder from the pool on exception)
@ -109,13 +114,22 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
} }
} }
/// <inheritdoc/>
public ReadOnlySeString EvaluateMacroString(
string macroString,
Span<SeStringParameter> localParameters = default,
ClientLanguage? language = null)
{
return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language);
}
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlySeString EvaluateFromAddon( public ReadOnlySeString EvaluateFromAddon(
uint addonId, uint addonId,
Span<SeStringParameter> localParameters = default, Span<SeStringParameter> localParameters = default,
ClientLanguage? language = null) ClientLanguage? language = null)
{ {
var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); var lang = language ?? this.GetEffectiveClientLanguage();
if (!this.dataManager.GetExcelSheet<AddonSheet>(lang).TryGetRow(addonId, out var addonRow)) if (!this.dataManager.GetExcelSheet<AddonSheet>(lang).TryGetRow(addonId, out var addonRow))
return default; return default;
@ -129,7 +143,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
Span<SeStringParameter> localParameters = default, Span<SeStringParameter> localParameters = default,
ClientLanguage? language = null) ClientLanguage? language = null)
{ {
var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); var lang = language ?? this.GetEffectiveClientLanguage();
if (!this.dataManager.GetExcelSheet<Lobby>(lang).TryGetRow(lobbyId, out var lobbyRow)) if (!this.dataManager.GetExcelSheet<Lobby>(lang).TryGetRow(lobbyId, out var lobbyRow))
return default; return default;
@ -143,7 +157,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
Span<SeStringParameter> localParameters = default, Span<SeStringParameter> localParameters = default,
ClientLanguage? language = null) ClientLanguage? language = null)
{ {
var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); var lang = language ?? this.GetEffectiveClientLanguage();
if (!this.dataManager.GetExcelSheet<LogMessage>(lang).TryGetRow(logMessageId, out var logMessageRow)) if (!this.dataManager.GetExcelSheet<LogMessage>(lang).TryGetRow(logMessageId, out var logMessageRow))
return default; return default;
@ -154,7 +168,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
/// <inheritdoc/> /// <inheritdoc/>
public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) => public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) =>
this.actStrCache.GetOrAdd( this.actStrCache.GetOrAdd(
new(actionKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), new(actionKind, id, language ?? this.GetEffectiveClientLanguage()),
static (key, t) => t.EvaluateFromAddon(2026, [key.Kind.GetActStrId(key.Id)], key.Language) static (key, t) => t.EvaluateFromAddon(2026, [key.Kind.GetActStrId(key.Id)], key.Language)
.ExtractText() .ExtractText()
.StripSoftHyphen(), .StripSoftHyphen(),
@ -163,7 +177,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
/// <inheritdoc/> /// <inheritdoc/>
public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) => public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) =>
this.objStrCache.GetOrAdd( this.objStrCache.GetOrAdd(
new(objectKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), new(objectKind, id, language ?? this.GetEffectiveClientLanguage()),
static (key, t) => t.EvaluateFromAddon(2025, [key.Kind.GetObjStrId(key.Id)], key.Language) static (key, t) => t.EvaluateFromAddon(2025, [key.Kind.GetObjStrId(key.Id)], key.Language)
.ExtractText() .ExtractText()
.StripSoftHyphen(), .StripSoftHyphen(),
@ -182,6 +196,18 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float y) private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float y)
=> ConvertRawToMapPos(map, map.OffsetY, y); => ConvertRawToMapPos(map, map.OffsetY, y);
private ClientLanguage GetEffectiveClientLanguage()
{
return this.dalamudConfiguration.EffectiveLanguage switch
{
"ja" => ClientLanguage.Japanese,
"en" => ClientLanguage.English,
"de" => ClientLanguage.German,
"fr" => ClientLanguage.French,
_ => this.clientState.ClientLanguage,
};
}
private SeStringBuilder EvaluateAndAppendTo( private SeStringBuilder EvaluateAndAppendTo(
SeStringBuilder builder, SeStringBuilder builder,
ReadOnlySeStringSpan str, ReadOnlySeStringSpan str,
@ -445,7 +471,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) && if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) &&
logCrossWorldName == 1) logCrossWorldName == 1)
context.Builder.Append((ReadOnlySeStringSpan)world.Name); context.Builder.Append(new ReadOnlySeStringSpan(world.Name.GetPointer(0)));
} }
return true; return true;
@ -635,7 +661,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
{ {
case false when digit == 0: case false when digit == 0:
continue; continue;
case true when i % 3 == 0: case true when MathF.Log10(i) % 3 == 2:
this.ResolveStringExpression(in context, eSep); this.ResolveStringExpression(in context, eSep);
break; break;
} }
@ -711,84 +737,186 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
this.TryResolveUInt(in context, enu.Current, out eColParamValue); this.TryResolveUInt(in context, enu.Current, out eColParamValue);
var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText(); var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText();
var originalRowIdValue = eRowIdValue;
this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue); var flags = this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue);
if (string.IsNullOrEmpty(resolvedSheetName)) if (string.IsNullOrEmpty(resolvedSheetName))
return false; return false;
if (!this.dataManager.Excel.SheetNames.Contains(resolvedSheetName)) var text = this.FormatSheetValue(context.Language, resolvedSheetName, eRowIdValue, eColIndexValue, eColParamValue);
if (text.IsEmpty)
return false; return false;
if (!this.dataManager.GetExcelSheet<RawRow>(context.Language, resolvedSheetName) this.AddSheetRedirectItemDecoration(context, ref text, flags, eRowIdValue);
.TryGetRow(eRowIdValue, out var row))
return false;
if (eColIndexValue >= row.Columns.Count) if (resolvedSheetName != "DescriptionString")
return false; eColParamValue = originalRowIdValue;
var column = row.Columns[(int)eColIndexValue]; // Note: The link marker symbol is added by RaptureLogMessage, probably somewhere in it's Update function.
switch (column.Type) // It is not part of this generated link.
this.CreateSheetLink(context, resolvedSheetName, text, eRowIdValue, eColParamValue);
return true;
}
private ReadOnlySeString FormatSheetValue(ClientLanguage language, string sheetName, uint rowId, uint colIndex, uint colParam)
{ {
case ExcelColumnDataType.String: if (!this.dataManager.Excel.SheetNames.Contains(sheetName))
context.Builder.Append(this.Evaluate(row.ReadString(column.Offset), [eColParamValue], context.Language)); return default;
return true;
case ExcelColumnDataType.Bool: if (!this.dataManager.GetExcelSheet<RawRow>(language, sheetName)
context.Builder.Append((row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); .TryGetRow(rowId, out var row))
return true; return default;
case ExcelColumnDataType.Int8:
context.Builder.Append(row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); if (colIndex >= row.Columns.Count)
return true; return default;
case ExcelColumnDataType.UInt8:
context.Builder.Append(row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); var column = row.Columns[(int)colIndex];
return true; return column.Type switch
case ExcelColumnDataType.Int16: {
context.Builder.Append(row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.String => this.Evaluate(row.ReadString(column.Offset), [colParam], language),
return true; ExcelColumnDataType.Bool => (row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
case ExcelColumnDataType.UInt16: ExcelColumnDataType.Int8 => row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture),
context.Builder.Append(row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.UInt8 => row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture),
return true; ExcelColumnDataType.Int16 => row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture),
case ExcelColumnDataType.Int32: ExcelColumnDataType.UInt16 => row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture),
context.Builder.Append(row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.Int32 => row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture),
return true; ExcelColumnDataType.UInt32 => row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture),
case ExcelColumnDataType.UInt32: ExcelColumnDataType.Float32 => row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture),
context.Builder.Append(row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.Int64 => row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture),
return true; ExcelColumnDataType.UInt64 => row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture),
case ExcelColumnDataType.Float32: ExcelColumnDataType.PackedBool0 => (row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
context.Builder.Append(row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.PackedBool1 => (row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
return true; ExcelColumnDataType.PackedBool2 => (row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
case ExcelColumnDataType.Int64: ExcelColumnDataType.PackedBool3 => (row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
context.Builder.Append(row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.PackedBool4 => (row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
return true; ExcelColumnDataType.PackedBool5 => (row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
case ExcelColumnDataType.UInt64: ExcelColumnDataType.PackedBool6 => (row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
context.Builder.Append(row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); ExcelColumnDataType.PackedBool7 => (row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
return true; _ => default,
case ExcelColumnDataType.PackedBool0: };
context.Builder.Append((row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); }
return true;
case ExcelColumnDataType.PackedBool1: private void AddSheetRedirectItemDecoration(in SeStringContext context, ref ReadOnlySeString text, SheetRedirectFlags flags, uint eRowIdValue)
context.Builder.Append((row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); {
return true; if (!flags.HasFlag(SheetRedirectFlags.Item))
case ExcelColumnDataType.PackedBool2: return;
context.Builder.Append((row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true; var rarity = 1u;
case ExcelColumnDataType.PackedBool3: var skipLink = false;
context.Builder.Append((row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true; if (flags.HasFlag(SheetRedirectFlags.EventItem))
case ExcelColumnDataType.PackedBool4: {
context.Builder.Append((row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); rarity = 8;
return true; skipLink = true;
case ExcelColumnDataType.PackedBool5: }
context.Builder.Append((row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true; var itemId = eRowIdValue;
case ExcelColumnDataType.PackedBool6:
context.Builder.Append((row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); if (this.dataManager.GetExcelSheet<Item>(context.Language).TryGetRow(itemId, out var itemRow))
return true; {
case ExcelColumnDataType.PackedBool7: rarity = itemRow.Rarity;
context.Builder.Append((row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); if (rarity == 0)
return true; rarity = 1;
if (itemRow.FilterGroup is 38 or 50)
skipLink = true;
}
if (flags.HasFlag(SheetRedirectFlags.Collectible))
{
itemId += 500000;
}
else if (flags.HasFlag(SheetRedirectFlags.HighQuality))
{
itemId += 1000000;
}
var sb = SeStringBuilder.SharedPool.Get();
sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language));
if (!skipLink)
sb.PushLink(LinkMacroPayloadType.Item, itemId, rarity, 0u); // arg3 = some LogMessage flag based on LogKind RowId? => "89 5C 24 20 E8 ?? ?? ?? ?? 48 8B 1F"
// there is code here for handling noun link markers (//), but i don't know why
sb.Append(text);
if (flags.HasFlag(SheetRedirectFlags.HighQuality)
&& this.dataManager.GetExcelSheet<AddonSheet>(context.Language).TryGetRow(9, out var hqSymbol))
{
sb.Append(hqSymbol.Text);
}
else if (flags.HasFlag(SheetRedirectFlags.Collectible)
&& this.dataManager.GetExcelSheet<AddonSheet>(context.Language).TryGetRow(150, out var collectibleSymbol))
{
sb.Append(collectibleSymbol.Text);
}
if (!skipLink)
sb.PopLink();
text = sb.ToReadOnlySeString();
SeStringBuilder.SharedPool.Return(sb);
}
private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue)
{
switch (resolvedSheetName)
{
case "Achievement":
context.Builder.PushLink(LinkMacroPayloadType.Achievement, eRowIdValue, 0u, 0u, text.AsSpan());
context.Builder.Append(text);
context.Builder.PopLink();
return;
case "HowTo":
context.Builder.PushLink(LinkMacroPayloadType.HowTo, eRowIdValue, 0u, 0u, text.AsSpan());
context.Builder.Append(text);
context.Builder.PopLink();
return;
case "Status" when this.dataManager.GetExcelSheet<StatusSheet>(context.Language).TryGetRow(eRowIdValue, out var statusRow):
context.Builder.PushLink(LinkMacroPayloadType.Status, eRowIdValue, 0u, 0u, []);
switch (statusRow.StatusCategory)
{
case 1: context.Builder.Append(this.EvaluateFromAddon(376)); break; // buff symbol
case 2: context.Builder.Append(this.EvaluateFromAddon(377)); break; // debuff symbol
}
context.Builder.Append(text);
context.Builder.PopLink();
return;
case "AkatsukiNoteString":
context.Builder.PushLink(LinkMacroPayloadType.AkatsukiNote, eColParamValue, 0u, 0u, text.AsSpan());
context.Builder.Append(text);
context.Builder.PopLink();
return;
case "DescriptionString" when eColParamValue > 0:
context.Builder.PushLink((LinkMacroPayloadType)11, eRowIdValue, eColParamValue, 0u, text.AsSpan());
context.Builder.Append(text);
context.Builder.PopLink();
return;
case "WKSPioneeringTrailString":
context.Builder.PushLink((LinkMacroPayloadType)12, eRowIdValue, eColParamValue, 0u, text.AsSpan());
context.Builder.Append(text);
context.Builder.PopLink();
return;
case "MKDLore":
context.Builder.PushLink((LinkMacroPayloadType)13, eRowIdValue, 0u, 0u, text.AsSpan());
context.Builder.Append(text);
context.Builder.PopLink();
return;
default: default:
return false; context.Builder.Append(text);
return;
} }
} }
@ -938,9 +1066,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (p.Type == ReadOnlySePayloadType.Text) if (p.Type == ReadOnlySePayloadType.Text)
{ {
context.Builder.Append( context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language));
context.CultureInfo.TextInfo.ToTitleCase(Encoding.UTF8.GetString(p.Body.Span)));
continue; continue;
} }
@ -1067,8 +1193,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt)) if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt))
return false; return false;
var instance = packedIds >> 0x10; var instance = packedIds >> 16;
var mapId = packedIds & 0xFF; var mapId = packedIds & 0xFFFF;
if (this.dataManager.GetExcelSheet<TerritoryType>(context.Language) if (this.dataManager.GetExcelSheet<TerritoryType>(context.Language)
.TryGetRow(territoryTypeId, out var territoryTypeRow)) .TryGetRow(territoryTypeId, out var territoryTypeRow))
@ -1356,8 +1482,6 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
var group = (uint)(e0Val + 1); var group = (uint)(e0Val + 1);
var rowId = (uint)e1Val; var rowId = (uint)e1Val;
using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55);
if (!this.dataManager.GetExcelSheet<Completion>(context.Language).TryGetFirst( if (!this.dataManager.GetExcelSheet<Completion>(context.Language).TryGetFirst(
row => row.Group == group && !row.LookupTable.IsEmpty, row => row.Group == group && !row.LookupTable.IsEmpty,
out var groupRow)) out var groupRow))
@ -1382,6 +1506,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return true; return true;
} }
using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55);
// CategoryDataCache // CategoryDataCache
if (lookupTable.Equals("#")) if (lookupTable.Equals("#"))
{ {

View file

@ -31,7 +31,7 @@ public class ItemPayload : Payload
/// Creates a payload representing an interactable item link for the specified item. /// Creates a payload representing an interactable item link for the specified item.
/// </summary> /// </summary>
/// <param name="itemId">The id of the item.</param> /// <param name="itemId">The id of the item.</param>
/// <param name="isHq">Whether or not the link should be for the high-quality variant of the item.</param> /// <param name="isHq">Whether the link should be for the high-quality variant of the item.</param>
/// <param name="displayNameOverride">An optional name to include in the item link. Typically this should /// <param name="displayNameOverride">An optional name to include in the item link. Typically this should
/// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent /// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent
/// TextPayload that is a part of a full item link in chat.</param> /// TextPayload that is a part of a full item link in chat.</param>
@ -75,7 +75,7 @@ public class ItemPayload : Payload
/// <summary> /// <summary>
/// Kinds of items that can be fetched from this payload. /// Kinds of items that can be fetched from this payload.
/// </summary> /// </summary>
[Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")] [Api13ToDo("Move this out of ItemPayload. It's used in other classes too.")]
public enum ItemKind : uint public enum ItemKind : uint
{ {
/// <summary> /// <summary>
@ -142,7 +142,7 @@ public class ItemPayload : Payload
: (RowRef)LuminaUtils.CreateRef<Item>(this.ItemId); : (RowRef)LuminaUtils.CreateRef<Item>(this.ItemId);
/// <summary> /// <summary>
/// Gets a value indicating whether or not this item link is for a high-quality version of the item. /// Gets a value indicating whether this item link is for a high-quality version of the item.
/// </summary> /// </summary>
[JsonProperty] [JsonProperty]
public bool IsHQ => this.Kind == ItemKind.Hq; public bool IsHQ => this.Kind == ItemKind.Hq;

View file

@ -46,7 +46,7 @@ public class UIForegroundPayload : Payload
public override PayloadType Type => PayloadType.UIForeground; public override PayloadType Type => PayloadType.UIForeground;
/// <summary> /// <summary>
/// Gets a value indicating whether or not this payload represents applying a foreground color, or disabling one. /// Gets a value indicating whether this payload represents applying a foreground color, or disabling one.
/// </summary> /// </summary>
public bool IsEnabled => this.ColorKey != 0; public bool IsEnabled => this.ColorKey != 0;

View file

@ -64,7 +64,7 @@ public class UIGlowPayload : Payload
} }
/// <summary> /// <summary>
/// Gets a value indicating whether or not this payload represents applying a glow color, or disabling one. /// Gets a value indicating whether this payload represents applying a glow color, or disabling one.
/// </summary> /// </summary>
public bool IsEnabled => this.ColorKey != 0; public bool IsEnabled => this.ColorKey != 0;

View file

@ -113,7 +113,7 @@ public class SeStringBuilder
/// Add an item link to the builder. /// Add an item link to the builder.
/// </summary> /// </summary>
/// <param name="itemId">The item ID.</param> /// <param name="itemId">The item ID.</param>
/// <param name="isHq">Whether or not the item is high quality.</param> /// <param name="isHq">Whether the item is high quality.</param>
/// <param name="itemNameOverride">Override for the item's name.</param> /// <param name="itemNameOverride">Override for the item's name.</param>
/// <returns>The current builder.</returns> /// <returns>The current builder.</returns>
public SeStringBuilder AddItemLink(uint itemId, bool isHq, string? itemNameOverride = null) => public SeStringBuilder AddItemLink(uint itemId, bool isHq, string? itemNameOverride = null) =>

View file

@ -89,7 +89,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook
} }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is enabled. /// Gets a value indicating whether the hook is enabled.
/// </summary> /// </summary>
public bool IsEnabled public bool IsEnabled
{ {
@ -101,7 +101,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook
} }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook has been disposed. /// Gets a value indicating whether the hook has been disposed.
/// </summary> /// </summary>
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }

View file

@ -59,12 +59,12 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
=> this.IsDisposed ? Marshal.GetDelegateForFunctionPointer<T>(this.address) : this.Original; => this.IsDisposed ? Marshal.GetDelegateForFunctionPointer<T>(this.address) : this.Original;
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is enabled. /// Gets a value indicating whether the hook is enabled.
/// </summary> /// </summary>
public virtual bool IsEnabled => throw new NotImplementedException(); public virtual bool IsEnabled => throw new NotImplementedException();
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook has been disposed. /// Gets a value indicating whether the hook has been disposed.
/// </summary> /// </summary>
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
@ -90,6 +90,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
/// <summary> /// <summary>
/// Starts intercepting a call to the function. /// Starts intercepting a call to the function.
/// </summary> /// </summary>
/// <exception cref="ObjectDisposedException">Hook is already disposed.</exception>
public virtual void Enable() => throw new NotImplementedException(); public virtual void Enable() => throw new NotImplementedException();
/// <summary> /// <summary>

View file

@ -11,12 +11,12 @@ public interface IDalamudHook : IDisposable
public IntPtr Address { get; } public IntPtr Address { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is enabled. /// Gets a value indicating whether the hook is enabled.
/// </summary> /// </summary>
public bool IsEnabled { get; } public bool IsEnabled { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is disposed. /// Gets a value indicating whether the hook is disposed.
/// </summary> /// </summary>
public bool IsDisposed { get; } public bool IsDisposed { get; }

View file

@ -15,7 +15,7 @@ namespace Dalamud.Hooking.Internal;
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. /// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
/// </summary> /// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam> /// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class CallHook<T> : IDisposable where T : Delegate internal class CallHook<T> : IDalamudHook where T : Delegate
{ {
private readonly Reloaded.Hooks.AsmHook asmHook; private readonly Reloaded.Hooks.AsmHook asmHook;
@ -29,7 +29,10 @@ internal class CallHook<T> : IDisposable where T : Delegate
/// <param name="detour">Delegate to invoke.</param> /// <param name="detour">Delegate to invoke.</param>
internal CallHook(nint address, T detour) internal CallHook(nint address, T detour)
{ {
ArgumentNullException.ThrowIfNull(detour);
this.detour = detour; this.detour = detour;
this.Address = address;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[] var code = new[]
@ -50,10 +53,19 @@ internal class CallHook<T> : IDisposable where T : Delegate
} }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is enabled. /// Gets a value indicating whether the hook is enabled.
/// </summary> /// </summary>
public bool IsEnabled => this.asmHook.IsEnabled; public bool IsEnabled => this.asmHook.IsEnabled;
/// <inheritdoc/>
public IntPtr Address { get; }
/// <inheritdoc/>
public string BackendName => "Reloaded AsmHook";
/// <inheritdoc/>
public bool IsDisposed => this.detour == null;
/// <summary> /// <summary>
/// Starts intercepting a call to the function. /// Starts intercepting a call to the function.
/// </summary> /// </summary>

View file

@ -116,14 +116,7 @@ internal unsafe class FunctionPointerVariableHook<T> : Hook<T>
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool IsEnabled public override bool IsEnabled => !this.IsDisposed && this.enabled;
{
get
{
this.CheckDisposed();
return this.enabled;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public override string BackendName => "MinHook"; public override string BackendName => "MinHook";
@ -132,9 +125,7 @@ internal unsafe class FunctionPointerVariableHook<T> : Hook<T>
public override void Dispose() public override void Dispose()
{ {
if (this.IsDisposed) if (this.IsDisposed)
{
return; return;
}
this.Disable(); this.Disable();
@ -148,16 +139,14 @@ internal unsafe class FunctionPointerVariableHook<T> : Hook<T>
/// <inheritdoc/> /// <inheritdoc/>
public override void Enable() public override void Enable()
{
lock (HookManager.HookEnableSyncRoot)
{ {
this.CheckDisposed(); this.CheckDisposed();
if (this.enabled) if (this.enabled)
{
return; return;
}
lock (HookManager.HookEnableSyncRoot)
{
Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnDetour); Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnDetour);
this.enabled = true; this.enabled = true;
} }
@ -166,15 +155,14 @@ internal unsafe class FunctionPointerVariableHook<T> : Hook<T>
/// <inheritdoc/> /// <inheritdoc/>
public override void Disable() public override void Disable()
{ {
this.CheckDisposed();
if (!this.enabled)
{
return;
}
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
if (this.IsDisposed)
return;
if (!this.enabled)
return;
Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnOriginal); Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnOriginal);
this.enabled = false; this.enabled = false;
} }

View file

@ -50,14 +50,7 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool IsEnabled public override bool IsEnabled => !this.IsDisposed && this.minHookImpl.Enabled;
{
get
{
this.CheckDisposed();
return this.minHookImpl.Enabled;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public override string BackendName => "MinHook"; public override string BackendName => "MinHook";
@ -83,29 +76,30 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
/// <inheritdoc/> /// <inheritdoc/>
public override void Enable() public override void Enable()
{
lock (HookManager.HookEnableSyncRoot)
{ {
this.CheckDisposed(); this.CheckDisposed();
if (!this.minHookImpl.Enabled) if (!this.minHookImpl.Enabled)
{ return;
lock (HookManager.HookEnableSyncRoot)
{
this.minHookImpl.Enable(); this.minHookImpl.Enable();
} }
} }
}
/// <inheritdoc/> /// <inheritdoc/>
public override void Disable() public override void Disable()
{
this.CheckDisposed();
if (this.minHookImpl.Enabled)
{ {
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
if (this.IsDisposed)
return;
if (!this.minHookImpl.Enabled)
return;
this.minHookImpl.Disable(); this.minHookImpl.Disable();
} }
} }
}
} }

View file

@ -45,14 +45,7 @@ internal class ReloadedHook<T> : Hook<T> where T : Delegate
} }
/// <inheritdoc/> /// <inheritdoc/>
public override bool IsEnabled public override bool IsEnabled => !this.IsDisposed && this.hookImpl.IsHookEnabled;
{
get
{
this.CheckDisposed();
return this.hookImpl.IsHookEnabled;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public override string BackendName => "Reloaded"; public override string BackendName => "Reloaded";
@ -73,10 +66,10 @@ internal class ReloadedHook<T> : Hook<T> where T : Delegate
/// <inheritdoc/> /// <inheritdoc/>
public override void Enable() public override void Enable()
{ {
this.CheckDisposed();
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
this.CheckDisposed();
if (!this.hookImpl.IsHookEnabled) if (!this.hookImpl.IsHookEnabled)
this.hookImpl.Enable(); this.hookImpl.Enable();
} }
@ -85,10 +78,11 @@ internal class ReloadedHook<T> : Hook<T> where T : Delegate
/// <inheritdoc/> /// <inheritdoc/>
public override void Disable() public override void Disable()
{ {
this.CheckDisposed();
lock (HookManager.HookEnableSyncRoot) lock (HookManager.HookEnableSyncRoot)
{ {
if (this.IsDisposed)
return;
if (!this.hookImpl.IsHookActivated) if (!this.hookImpl.IsHookActivated)
return; return;

View file

@ -85,12 +85,12 @@ public abstract class Easing
public TimeSpan Duration { get; set; } public TimeSpan Duration { get; set; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the animation is running. /// Gets a value indicating whether the animation is running.
/// </summary> /// </summary>
public bool IsRunning => this.animationTimer.IsRunning; public bool IsRunning => this.animationTimer.IsRunning;
/// <summary> /// <summary>
/// Gets a value indicating whether or not the animation is done. /// Gets a value indicating whether the animation is done.
/// </summary> /// </summary>
public bool IsDone => this.animationTimer.ElapsedMilliseconds > this.Duration.TotalMilliseconds; public bool IsDone => this.animationTimer.ElapsedMilliseconds > this.Duration.TotalMilliseconds;

View file

@ -21,9 +21,14 @@ public enum PluginInstallerOpenKind
UpdateablePlugins, UpdateablePlugins,
/// <summary> /// <summary>
/// Open to the "Changelogs" page. /// Open to the "Plugin Changelogs" page.
/// </summary> /// </summary>
Changelogs, Changelogs,
/// <summary>
/// Open to the "Dalamud Changelogs" page.
/// </summary>
DalamudChangelogs,
} }
/// <summary> /// <summary>

View file

@ -23,7 +23,7 @@ internal class DriveListLoader
public IReadOnlyList<DriveInfo> Drives { get; private set; } public IReadOnlyList<DriveInfo> Drives { get; private set; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not the loader is loading. /// Gets a value indicating whether the loader is loading.
/// </summary> /// </summary>
public bool Loading { get; private set; } public bool Loading { get; private set; }

View file

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Utility;
namespace Dalamud.Interface.ImGuiFileDialog; namespace Dalamud.Interface.ImGuiFileDialog;
/// <summary> /// <summary>
@ -13,11 +15,44 @@ public partial class FileDialog
private readonly DriveListLoader driveListLoader = new(); private readonly DriveListLoader driveListLoader = new();
private List<FileStruct> files = new(); private readonly List<FileStruct> files = [];
private List<FileStruct> filteredFiles = new(); private readonly List<FileStruct> filteredFiles = [];
private SortingField currentSortingField = SortingField.FileName; private SortingField currentSortingField = SortingField.FileName;
private bool[] sortDescending = { false, false, false, false };
/// <summary> Fired whenever the sorting field changes. </summary>
public event Action<SortingField>? SortOrderChanged;
/// <summary> The sorting type of the file selector. </summary>
public enum SortingField
{
/// <summary> No sorting specified. </summary>
None = 0,
/// <summary> Sort for ascending file names in culture-specific order. </summary>
FileName = 1,
/// <summary> Sort for ascending file types in culture-specific order. </summary>
Type = 2,
/// <summary> Sort for ascending file sizes. </summary>
Size = 3,
/// <summary> Sort for ascending last update dates. </summary>
Date = 4,
/// <summary> Sort for descending file names in culture-specific order. </summary>
FileNameDescending = 5,
/// <summary> Sort for descending file types in culture-specific order. </summary>
TypeDescending = 6,
/// <summary> Sort for descending file sizes. </summary>
SizeDescending = 7,
/// <summary> Sort for descending last update dates. </summary>
DateDescending = 8,
}
private enum FileStructType private enum FileStructType
{ {
@ -25,48 +60,64 @@ public partial class FileDialog
Directory, Directory,
} }
private enum SortingField /// <summary> Specify the current and subsequent sort order. </summary>
/// <param name="sortingField"> The new sort order. None is invalid and will not have any effect. </param>
public void SortFields(SortingField sortingField)
{ {
None, Comparison<FileStruct>? sortFunc = sortingField switch
FileName, {
Type, SortingField.FileName => SortByFileNameAsc,
Size, SortingField.FileNameDescending => SortByFileNameDesc,
Date, SortingField.Type => SortByTypeAsc,
SortingField.TypeDescending => SortByTypeDesc,
SortingField.Size => SortBySizeAsc,
SortingField.SizeDescending => SortBySizeDesc,
SortingField.Date => SortByDateAsc,
SortingField.DateDescending => SortByDateDesc,
_ => null,
};
if (sortFunc is null)
{
return;
} }
private static string ComposeNewPath(List<string> decomp) this.files.Sort(sortFunc);
this.currentSortingField = sortingField;
this.ApplyFilteringOnFileList();
this.SortOrderChanged?.InvokeSafely(this.currentSortingField);
}
private static string ComposeNewPath(List<string> decomposition)
{
switch (decomposition.Count)
{ {
// Handle UNC paths (network paths) // Handle UNC paths (network paths)
if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1])) case >= 2 when string.IsNullOrEmpty(decomposition[0]) && string.IsNullOrEmpty(decomposition[1]):
{ var pathParts = new List<string>(decomposition);
var pathParts = new List<string>(decomp);
pathParts.RemoveRange(0, 2); pathParts.RemoveRange(0, 2);
// Can not access server level or UNC root // Can not access server level or UNC root
if (pathParts.Count <= 1) if (pathParts.Count <= 1)
{ {
return string.Empty; return string.Empty;
} }
return $"\\\\{string.Join('\\', pathParts)}"; return $@"\\{string.Join('\\', pathParts)}";
} case 1:
var drivePath = decomposition[0];
if (decomp.Count == 1)
{
var drivePath = decomp[0];
if (drivePath[^1] != Path.DirectorySeparatorChar) if (drivePath[^1] != Path.DirectorySeparatorChar)
{ // turn C: into C:\ { // turn C: into C:\
drivePath += Path.DirectorySeparatorChar; drivePath += Path.DirectorySeparatorChar;
} }
return drivePath; return drivePath;
default: return Path.Combine(decomposition.ToArray());
} }
return Path.Combine(decomp.ToArray());
} }
private static FileStruct GetFile(FileInfo file, string path) private static FileStruct GetFile(FileInfo file, string path)
{ => new()
return new FileStruct
{ {
FileName = file.Name, FileName = file.Name,
FilePath = path, FilePath = path,
@ -76,11 +127,9 @@ public partial class FileDialog
Type = FileStructType.File, Type = FileStructType.File,
Ext = file.Extension.Trim('.'), Ext = file.Extension.Trim('.'),
}; };
}
private static FileStruct GetDir(DirectoryInfo dir, string path) private static FileStruct GetDir(DirectoryInfo dir, string path)
{ => new()
return new FileStruct
{ {
FileName = dir.Name, FileName = dir.Name,
FilePath = path, FilePath = path,
@ -90,136 +139,191 @@ public partial class FileDialog
Type = FileStructType.Directory, Type = FileStructType.Directory,
Ext = string.Empty, Ext = string.Empty,
}; };
}
private static int SortByFileNameDesc(FileStruct a, FileStruct b) private static int SortByFileNameDesc(FileStruct a, FileStruct b)
{ {
if (a.FileName[0] == '.' && b.FileName[0] != '.') switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.FileName[0] is '.')
{
if (b.FileName[0] is not '.')
{ {
return 1; return 1;
} }
if (a.FileName[0] != '.' && b.FileName[0] == '.') if (a.FileName.Length is 1)
{ {
return -1; return -1;
} }
if (a.FileName[0] == '.' && b.FileName[0] == '.') if (b.FileName.Length is 1)
{
if (a.FileName.Length == 1)
{
return -1;
}
if (b.FileName.Length == 1)
{ {
return 1; return 1;
} }
return -1 * string.Compare(a.FileName[1..], b.FileName[1..]); return -1 * string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture);
}
if (b.FileName[0] is '.')
{
return -1;
} }
if (a.Type != b.Type) if (a.Type != b.Type)
{ {
return a.Type == FileStructType.Directory ? 1 : -1; return a.Type is FileStructType.Directory ? 1 : -1;
} }
return -1 * string.Compare(a.FileName, b.FileName); return -string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture);
} }
private static int SortByFileNameAsc(FileStruct a, FileStruct b) private static int SortByFileNameAsc(FileStruct a, FileStruct b)
{ {
if (a.FileName[0] == '.' && b.FileName[0] != '.') switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.FileName[0] is '.')
{
if (b.FileName[0] is not '.')
{ {
return -1; return -1;
} }
if (a.FileName[0] != '.' && b.FileName[0] == '.') if (a.FileName.Length is 1)
{ {
return 1; return 1;
} }
if (a.FileName[0] == '.' && b.FileName[0] == '.') if (b.FileName.Length is 1)
{
if (a.FileName.Length == 1)
{
return 1;
}
if (b.FileName.Length == 1)
{ {
return -1; return -1;
} }
return string.Compare(a.FileName[1..], b.FileName[1..]); return string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture);
}
if (b.FileName[0] is '.')
{
return 1;
} }
if (a.Type != b.Type) if (a.Type != b.Type)
{ {
return a.Type == FileStructType.Directory ? -1 : 1; return a.Type is FileStructType.Directory ? -1 : 1;
} }
return string.Compare(a.FileName, b.FileName); return string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture);
} }
private static int SortByTypeDesc(FileStruct a, FileStruct b) private static int SortByTypeDesc(FileStruct a, FileStruct b)
{ {
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? 1 : -1; return (a.Type == FileStructType.Directory) ? 1 : -1;
} }
return string.Compare(a.Ext, b.Ext); return string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture);
} }
private static int SortByTypeAsc(FileStruct a, FileStruct b) private static int SortByTypeAsc(FileStruct a, FileStruct b)
{ {
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? -1 : 1; return (a.Type == FileStructType.Directory) ? -1 : 1;
} }
return -1 * string.Compare(a.Ext, b.Ext); return -string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture);
} }
private static int SortBySizeDesc(FileStruct a, FileStruct b) private static int SortBySizeDesc(FileStruct a, FileStruct b)
{ {
if (a.Type != b.Type) switch (a.FileName, b.FileName)
{ {
return (a.Type == FileStructType.Directory) ? 1 : -1; case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
} }
return (a.FileSize > b.FileSize) ? 1 : -1; if (a.Type != b.Type)
{
return (a.Type is FileStructType.Directory) ? 1 : -1;
}
return a.FileSize.CompareTo(b.FileSize);
} }
private static int SortBySizeAsc(FileStruct a, FileStruct b) private static int SortBySizeAsc(FileStruct a, FileStruct b)
{ {
if (a.Type != b.Type) switch (a.FileName, b.FileName)
{ {
return (a.Type == FileStructType.Directory) ? -1 : 1; case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
} }
return (a.FileSize > b.FileSize) ? -1 : 1; if (a.Type != b.Type)
{
return (a.Type is FileStructType.Directory) ? -1 : 1;
}
return -a.FileSize.CompareTo(b.FileSize);
} }
private static int SortByDateDesc(FileStruct a, FileStruct b) private static int SortByDateDesc(FileStruct a, FileStruct b)
{ {
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? 1 : -1; return (a.Type == FileStructType.Directory) ? 1 : -1;
} }
return string.Compare(a.FileModifiedDate, b.FileModifiedDate); return string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture);
} }
private static int SortByDateAsc(FileStruct a, FileStruct b) private static int SortByDateAsc(FileStruct a, FileStruct b)
{ {
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type) if (a.Type != b.Type)
{ {
return (a.Type == FileStructType.Directory) ? -1 : 1; return (a.Type == FileStructType.Directory) ? -1 : 1;
} }
return -1 * string.Compare(a.FileModifiedDate, b.FileModifiedDate); return -string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture);
} }
private bool CreateDir(string dirPath) private bool CreateDir(string dirPath)
@ -336,52 +440,17 @@ public partial class FileDialog
this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video)); this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video));
} }
private void SortFields(SortingField sortingField, bool canChangeOrder = false) private SortingField GetNewSorting(int column)
=> column switch
{ {
switch (sortingField) 0 when this.currentSortingField is SortingField.FileName => SortingField.FileNameDescending,
{ 0 => SortingField.FileName,
case SortingField.FileName: 1 when this.currentSortingField is SortingField.Type => SortingField.TypeDescending,
if (canChangeOrder && sortingField == this.currentSortingField) 1 => SortingField.Type,
{ 2 when this.currentSortingField is SortingField.Size => SortingField.SizeDescending,
this.sortDescending[0] = !this.sortDescending[0]; 2 => SortingField.Size,
} 3 when this.currentSortingField is SortingField.Date => SortingField.DateDescending,
3 => SortingField.Date,
this.files.Sort(this.sortDescending[0] ? SortByFileNameDesc : SortByFileNameAsc); _ => SortingField.None,
break; };
case SortingField.Type:
if (canChangeOrder && sortingField == this.currentSortingField)
{
this.sortDescending[1] = !this.sortDescending[1];
}
this.files.Sort(this.sortDescending[1] ? SortByTypeDesc : SortByTypeAsc);
break;
case SortingField.Size:
if (canChangeOrder && sortingField == this.currentSortingField)
{
this.sortDescending[2] = !this.sortDescending[2];
}
this.files.Sort(this.sortDescending[2] ? SortBySizeDesc : SortBySizeAsc);
break;
case SortingField.Date:
if (canChangeOrder && sortingField == this.currentSortingField)
{
this.sortDescending[3] = !this.sortDescending[3];
}
this.files.Sort(this.sortDescending[3] ? SortByDateDesc : SortByDateAsc);
break;
}
if (sortingField != SortingField.None)
{
this.currentSortingField = sortingField;
}
this.ApplyFilteringOnFileList();
}
} }

View file

@ -374,22 +374,8 @@ public partial class FileDialog
ImGui.PopID(); ImGui.PopID();
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
{ {
if (column == 0) var sorting = this.GetNewSorting(column);
{ this.SortFields(sorting);
this.SortFields(SortingField.FileName, true);
}
else if (column == 1)
{
this.SortFields(SortingField.Type, true);
}
else if (column == 2)
{
this.SortFields(SortingField.Size, true);
}
else
{
this.SortFields(SortingField.Date, true);
}
} }
} }

View file

@ -9,13 +9,21 @@ namespace Dalamud.Interface.ImGuiFileDialog;
/// </summary> /// </summary>
public class FileDialogManager public class FileDialogManager
{ {
/// <summary> Gets or sets a function that returns the desired default sort order in the file dialog. </summary>
public Func<FileDialog.SortingField>? GetDefaultSortOrder { get; set; }
/// <summary> Gets or sets an action to invoke when a file dialog changes its sort order. </summary>
public Action<FileDialog.SortingField>? SetDefaultSortOrder { get; set; }
#pragma warning disable SA1401 #pragma warning disable SA1401
/// <summary> Additional quick access items for the side bar.</summary> #pragma warning disable SA1201
public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = new(); /// <summary> Additional quick access items for the sidebar.</summary>
public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = [];
/// <summary> Additional flags with which to draw the window. </summary> /// <summary> Additional flags with which to draw the window. </summary>
public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None;
#pragma warning restore SA1401 #pragma warning restore SA1401
#pragma warning restore SA1201
private FileDialog? dialog; private FileDialog? dialog;
private Action<bool, string>? callback; private Action<bool, string>? callback;
@ -189,10 +197,41 @@ public class FileDialogManager
this.callback = callback as Action<bool, string>; this.callback = callback as Action<bool, string>;
} }
if (this.dialog is not null)
{
this.dialog.SortOrderChanged -= this.OnSortOrderChange;
}
this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags);
if (this.GetDefaultSortOrder is not null)
{
try
{
var order = this.GetDefaultSortOrder();
this.dialog.SortFields(order);
}
catch
{
// ignored.
}
}
this.dialog.SortOrderChanged += this.OnSortOrderChange;
this.dialog.WindowFlags |= this.AddedWindowFlags; this.dialog.WindowFlags |= this.AddedWindowFlags;
foreach (var (name, location, icon, position) in this.CustomSideBarItems) foreach (var (name, location, icon, position) in this.CustomSideBarItems)
this.dialog.SetQuickAccess(name, location, icon, position); this.dialog.SetQuickAccess(name, location, icon, position);
this.dialog.Show(); this.dialog.Show();
} }
private void OnSortOrderChange(FileDialog.SortingField sortOrder)
{
try
{
this.SetDefaultSortOrder?.Invoke(sortOrder);
}
catch
{
// ignored.
}
}
} }

View file

@ -14,8 +14,10 @@ internal sealed partial class ActiveNotification
/// <summary>Draws this notification.</summary> /// <summary>Draws this notification.</summary>
/// <param name="width">The maximum width of the notification window.</param> /// <param name="width">The maximum width of the notification window.</param>
/// <param name="offsetY">The offset from the bottom.</param> /// <param name="offsetY">The offset from the bottom.</param>
/// <param name="anchorPosition">Where notifications are anchored to on the screen.</param>
/// <param name="snapDirection">Direction of the screen which we are snapping to.</param>
/// <returns>The height of the notification.</returns> /// <returns>The height of the notification.</returns>
public float Draw(float width, float offsetY) public float Draw(float width, float offsetY, Vector2 anchorPosition, NotificationSnapDirection snapDirection)
{ {
var opacity = var opacity =
Math.Clamp( Math.Clamp(
@ -34,8 +36,8 @@ internal sealed partial class ActiveNotification
(NotificationConstants.ScaledWindowPadding * 2); (NotificationConstants.ScaledWindowPadding * 2);
var viewport = ImGuiHelpers.MainViewport; var viewport = ImGuiHelpers.MainViewport;
var viewportPos = viewport.WorkPos;
var viewportSize = viewport.WorkSize; var viewportSize = viewport.WorkSize;
var viewportPos = viewport.Pos;
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity);
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f);
@ -51,13 +53,78 @@ internal sealed partial class ActiveNotification
NotificationConstants.BackgroundOpacity)); NotificationConstants.BackgroundOpacity));
} }
Vector2 topLeft;
Vector2 pivot;
if (snapDirection is NotificationSnapDirection.Top or NotificationSnapDirection.Bottom)
{
// Top or bottom
var xPos = (viewportSize.X - width) * anchorPosition.X;
xPos = Math.Max(NotificationConstants.ScaledViewportEdgeMargin, Math.Min(viewportSize.X - width - NotificationConstants.ScaledViewportEdgeMargin, xPos));
if (snapDirection == NotificationSnapDirection.Top)
{
// Top
var yPos = NotificationConstants.ScaledViewportEdgeMargin - offsetY;
topLeft = new Vector2(xPos, yPos);
pivot = new(0, 0);
}
else
{
// Bottom
var yPos = viewportSize.Y - offsetY - NotificationConstants.ScaledViewportEdgeMargin;
topLeft = new Vector2(xPos, yPos);
pivot = new(0, 1);
}
}
else
{
// Left or Right
var yPos = (viewportSize.Y * anchorPosition.Y) - offsetY;
yPos = Math.Max(
NotificationConstants.ScaledViewportEdgeMargin,
Math.Min(viewportSize.Y - offsetY - NotificationConstants.ScaledViewportEdgeMargin, yPos));
if (snapDirection == NotificationSnapDirection.Left)
{
// Left
var xPos = NotificationConstants.ScaledViewportEdgeMargin;
if (anchorPosition.Y > 0.5f)
{
// Bottom
topLeft = new Vector2(xPos, yPos);
pivot = new(0, 1);
}
else
{
// Top
topLeft = new Vector2(xPos, yPos);
pivot = new(0, 0);
}
}
else
{
// Right
var xPos = viewportSize.X - width - NotificationConstants.ScaledViewportEdgeMargin;
if (anchorPosition.Y > 0.5f)
{
topLeft = new Vector2(xPos, yPos);
pivot = new(0, 1);
}
else
{
topLeft = new Vector2(xPos, yPos);
pivot = new(0, 0);
}
}
}
ImGuiHelpers.ForceNextWindowMainViewport(); ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos( ImGui.SetNextWindowPos(
(viewportPos + viewportSize) - topLeft + viewportPos,
new Vector2(NotificationConstants.ScaledViewportEdgeMargin) -
new Vector2(0, offsetY),
ImGuiCond.Always, ImGuiCond.Always,
Vector2.One); pivot);
ImGui.SetNextWindowSizeConstraints( ImGui.SetNextWindowSizeConstraints(
new(width, actionWindowHeight), new(width, actionWindowHeight),
new( new(
@ -145,7 +212,7 @@ internal sealed partial class ActiveNotification
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PopStyleVar(3); ImGui.PopStyleVar(3);
return windowSize.Y; return NotificationManager.ShouldScrollDownwards(anchorPosition) ? -windowSize.Y : windowSize.Y;
} }
/// <summary>Calculates the effective expiry, taking ImGui window state into account.</summary> /// <summary>Calculates the effective expiry, taking ImGui window state into account.</summary>

View file

@ -54,6 +54,11 @@ internal static class NotificationConstants
/// </summary> /// </summary>
public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// <summary>
/// The ratio of the screen at which the notification window will snap to the top or bottom of the screen.
/// </summary>
public const float NotificationTopBottomSnapMargin = 0.08f;
/// <summary>Default duration of the notification.</summary> /// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7); public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7);

View file

@ -1,7 +1,9 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
@ -21,9 +23,14 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get(); private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private readonly List<ActiveNotification> notifications = new(); private readonly List<ActiveNotification> notifications = new();
private readonly ConcurrentBag<ActiveNotification> pendingNotifications = new(); private readonly ConcurrentBag<ActiveNotification> pendingNotifications = new();
private NotificationPositionChooser? positionChooser;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private NotificationManager(FontAtlasFactory fontAtlasFactory) private NotificationManager(FontAtlasFactory fontAtlasFactory)
{ {
@ -47,6 +54,48 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe
/// <summary>Gets the private atlas for use with notification windows.</summary> /// <summary>Gets the private atlas for use with notification windows.</summary>
private IFontAtlas PrivateAtlas { get; } private IFontAtlas PrivateAtlas { get; }
/// <summary>
/// Calculate the width to be used to draw notifications.
/// </summary>
/// <returns>The width.</returns>
public static float CalculateNotificationWidth()
{
var viewportSize = ImGuiHelpers.MainViewport.WorkSize;
var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X;
width += NotificationConstants.ScaledWindowPadding * 3;
width += NotificationConstants.ScaledIconSize;
return Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth);
}
/// <summary>
/// Check if notifications should scroll downwards on the screen, based on the anchor position.
/// </summary>
/// <param name="anchorPosition">Where notifications are anchored to.</param>
/// <returns>A value indicating wether notifications should scroll downwards.</returns>
public static bool ShouldScrollDownwards(Vector2 anchorPosition)
{
return anchorPosition.Y < 0.5f;
}
/// <summary>
/// Choose the snap position for a notification based on the anchor position.
/// </summary>
/// <param name="anchorPosition">Where notifications are anchored to.</param>
/// <returns>The snap position.</returns>
public static NotificationSnapDirection ChooseSnapDirection(Vector2 anchorPosition)
{
if (anchorPosition.Y <= NotificationConstants.NotificationTopBottomSnapMargin)
return NotificationSnapDirection.Top;
if (anchorPosition.Y >= 1f - NotificationConstants.NotificationTopBottomSnapMargin)
return NotificationSnapDirection.Bottom;
if (anchorPosition.X <= 0.5f)
return NotificationSnapDirection.Left;
return NotificationSnapDirection.Right;
}
/// <inheritdoc/> /// <inheritdoc/>
public void DisposeService() public void DisposeService()
{ {
@ -97,25 +146,38 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe
/// <summary>Draw all currently queued notifications.</summary> /// <summary>Draw all currently queued notifications.</summary>
public void Draw() public void Draw()
{ {
var viewportSize = ImGuiHelpers.MainViewport.WorkSize;
var height = 0f; var height = 0f;
var uiHidden = this.gameGui.GameUiHidden; var uiHidden = this.gameGui.GameUiHidden;
while (this.pendingNotifications.TryTake(out var newNotification)) while (this.pendingNotifications.TryTake(out var newNotification))
this.notifications.Add(newNotification); this.notifications.Add(newNotification);
var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; var width = CalculateNotificationWidth();
width += NotificationConstants.ScaledWindowPadding * 3;
width += NotificationConstants.ScaledIconSize;
width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth);
this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal());
var scrollsDownwards = ShouldScrollDownwards(this.configuration.NotificationAnchorPosition);
var snapDirection = ChooseSnapDirection(this.configuration.NotificationAnchorPosition);
foreach (var tn in this.notifications) foreach (var tn in this.notifications)
{ {
if (uiHidden && tn.RespectUiHidden) if (uiHidden && tn.RespectUiHidden)
continue; continue;
height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap;
height += tn.Draw(width, height, this.configuration.NotificationAnchorPosition, snapDirection);
height += scrollsDownwards ? -NotificationConstants.ScaledWindowGap : NotificationConstants.ScaledWindowGap;
} }
this.positionChooser?.Draw();
}
/// <summary>
/// Starts the position chooser for notifications. Will block the UI until the user makes a selection.
/// </summary>
public void StartPositionChooser()
{
this.positionChooser = new NotificationPositionChooser(this.configuration);
this.positionChooser.SelectionMade += () => { this.positionChooser = null; };
} }
} }

View file

@ -0,0 +1,225 @@
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>
/// Class responsible for drawing UI that lets users choose the position of notifications.
/// </summary>
internal class NotificationPositionChooser
{
private readonly DalamudConfiguration configuration;
private readonly Vector2 previousAnchorPosition;
private Vector2 currentAnchorPosition;
/// <summary>
/// Initializes a new instance of the <see cref="NotificationPositionChooser"/> class.
/// </summary>
/// <param name="configuration">The configuration we are reading or writing from.</param>
public NotificationPositionChooser(DalamudConfiguration configuration)
{
this.configuration = configuration;
this.previousAnchorPosition = configuration.NotificationAnchorPosition;
}
/// <summary>
/// Gets or sets an action that is invoked when the user makes a selection.
/// </summary>
public event Action? SelectionMade;
/// <summary>
/// Draw the chooser UI.
/// </summary>
public void Draw()
{
using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f);
using var style2 = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 0f);
using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0));
var viewport = ImGuiHelpers.MainViewport;
var viewportSize = viewport.Size;
var viewportPos = viewport.Pos;
ImGui.SetNextWindowFocus();
ImGui.SetNextWindowPos(viewportPos);
ImGui.SetNextWindowSize(viewportSize);
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowBgAlpha(0.6f);
ImGui.Begin(
"###NotificationPositionChooser",
ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav);
var adjustedMousePos = ImGui.GetMousePos() - viewportPos;
var mousePosUnit = adjustedMousePos / viewportSize;
// Store the offset as a Vector2
this.currentAnchorPosition = mousePosUnit;
DrawPreview(this.previousAnchorPosition, 0.3f);
DrawPreview(this.currentAnchorPosition, 1f);
if (ImGui.IsMouseClicked(ImGuiMouseButton.Right))
{
this.SelectionMade?.Invoke();
}
else if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
this.configuration.NotificationAnchorPosition = this.currentAnchorPosition;
this.configuration.QueueSave();
this.SelectionMade?.Invoke();
}
// In the middle of the screen, draw some instructions
string[] instructions = ["Drag to move the notifications to where you would like them to appear.",
"Click to select the position.",
"Right-click to close without making changes."];
var dl = ImGui.GetWindowDrawList();
for (var i = 0; i < instructions.Length; i++)
{
var instruction = instructions[i];
var instructionSize = ImGui.CalcTextSize(instruction);
var instructionPos = new Vector2(
ImGuiHelpers.MainViewport.Size.X / 2 - instructionSize.X / 2,
ImGuiHelpers.MainViewport.Size.Y / 2 - instructionSize.Y / 2 + i * instructionSize.Y);
instructionPos += viewportPos;
dl.AddText(instructionPos, 0xFFFFFFFF, instruction);
}
ImGui.End();
}
private static void DrawPreview(Vector2 anchorPosition, float borderAlpha)
{
var dl = ImGui.GetWindowDrawList();
var width = NotificationManager.CalculateNotificationWidth();
var height = 100f * ImGuiHelpers.GlobalScale;
var smallBoxHeight = height * 0.4f;
var edgeMargin = NotificationConstants.ScaledViewportEdgeMargin;
var spacing = 10f * ImGuiHelpers.GlobalScale;
var viewport = ImGuiHelpers.MainViewport;
var viewportSize = viewport.Size;
var viewportPos = viewport.Pos;
var borderColor = ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, borderAlpha));
var borderThickness = 4.0f * ImGuiHelpers.GlobalScale;
var borderRounding = 4.0f * ImGuiHelpers.GlobalScale;
var backgroundColor = new Vector4(0, 0, 0, 0.5f); // Semi-transparent black
// Calculate positions based on the snap position
Vector2 topLeft, bottomRight, smallTopLeft, smallBottomRight;
var snapPos = NotificationManager.ChooseSnapDirection(anchorPosition);
if (snapPos is NotificationSnapDirection.Top or NotificationSnapDirection.Bottom)
{
// Calculate X position - same logic for top and bottom
var xPos = (viewportSize.X - width) * anchorPosition.X;
xPos = Math.Max(edgeMargin, Math.Min(viewportSize.X - width - edgeMargin, xPos));
if (snapPos == NotificationSnapDirection.Top)
{
// For top position: big box at top, small box below it
var yPos = edgeMargin;
topLeft = new Vector2(xPos, yPos);
bottomRight = new Vector2(xPos + width, yPos + height);
smallTopLeft = new Vector2(xPos, yPos + height + spacing);
smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight);
}
else
{
// For bottom position: big box at bottom, small box above it
var yPos = viewportSize.Y - height - edgeMargin;
topLeft = new Vector2(xPos, yPos);
bottomRight = new Vector2(xPos + width, yPos + height);
smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing);
smallBottomRight = new Vector2(xPos + width, yPos - spacing);
}
}
else
{
// For left and right positions, boxes are still stacked vertically (one above the other)
// Only the horizontal position changes
// Calculate Y position based on unit offset - used for both left and right positions
var yPos = (viewportSize.Y - height) * anchorPosition.Y;
yPos = Math.Max(edgeMargin, Math.Min(viewportSize.Y - height - edgeMargin, yPos));
if (snapPos == NotificationSnapDirection.Left)
{
// For left position: boxes are at the left edge of the screen
var xPos = edgeMargin;
if (anchorPosition.Y > 0.5f)
{
// Small box on top
smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing);
smallBottomRight = new Vector2(xPos + width, yPos - spacing);
// Big box below
topLeft = new Vector2(xPos, yPos);
bottomRight = new Vector2(xPos + width, yPos + height);
}
else
{
// Big box on top
topLeft = new Vector2(xPos, yPos);
bottomRight = new Vector2(xPos + width, yPos + height);
// Small box below
smallTopLeft = new Vector2(xPos, yPos + height + spacing);
smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight);
}
}
else
{
// For right position: boxes are at the right edge of the screen
var xPos = viewportSize.X - width - edgeMargin;
if (anchorPosition.Y > 0.5f)
{
// Small box on top
smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing);
smallBottomRight = new Vector2(xPos + width, yPos - spacing);
// Big box below
topLeft = new Vector2(xPos, yPos);
bottomRight = new Vector2(xPos + width, yPos + height);
}
else
{
// Big box on top
topLeft = new Vector2(xPos, yPos);
bottomRight = new Vector2(xPos + width, yPos + height);
// Small box below
smallTopLeft = new Vector2(xPos, yPos + height + spacing);
smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight);
}
}
}
topLeft += viewportPos;
bottomRight += viewportPos;
smallTopLeft += viewportPos;
smallBottomRight += viewportPos;
// Draw the big box
dl.AddRectFilled(topLeft, bottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll);
dl.AddRect(topLeft, bottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness);
// Draw the small box
dl.AddRectFilled(smallTopLeft, smallBottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll);
dl.AddRect(smallTopLeft, smallBottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness);
}
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Interface.ImGuiNotification.Internal;
/// <summary>
/// Where notifications should snap to on the screen when they are shown.
/// </summary>
public enum NotificationSnapDirection
{
/// <summary>
/// Snap to the top of the screen.
/// </summary>
Top,
/// <summary>
/// Snap to the bottom of the screen.
/// </summary>
Bottom,
/// <summary>
/// Snap to the left of the screen.
/// </summary>
Left,
/// <summary>
/// Snap to the right of the screen.
/// </summary>
Right,
}

View file

@ -146,6 +146,11 @@ internal class DalamudCommands : IServiceType
"DalamudCopyLogHelp", "DalamudCopyLogHelp",
"Copy the dalamud.log file to your clipboard."), "Copy the dalamud.log file to your clipboard."),
}); });
// Add the new command handler for toggling multi-monitor option
commandManager.AddHandler("/xltogglemultimonitor", new CommandInfo(this.OnToggleMultiMonitorCommand)
{
HelpMessage = Loc.Localize("DalamudToggleMultiMonitorHelp", "Toggle multi-monitor windows."),
});
} }
private void OnUnloadCommand(string command, string arguments) private void OnUnloadCommand(string command, string arguments)
@ -387,4 +392,19 @@ internal class DalamudCommands : IServiceType
: Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard."); : Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard.");
chatGui.Print(message); chatGui.Print(message);
} }
private void OnToggleMultiMonitorCommand(string command, string arguments)
{
var configuration = Service<DalamudConfiguration>.Get();
var chatGui = Service<ChatGui>.Get();
configuration.IsDisableViewport = !configuration.IsDisableViewport;
configuration.QueueSave();
var message = configuration.IsDisableViewport
? Loc.Localize("DalamudMultiMonitorDisabled", "Multi-monitor windows disabled.")
: Loc.Localize("DalamudMultiMonitorEnabled", "Multi-monitor windows enabled.");
chatGui.Print(message);
}
} }

View file

@ -11,13 +11,11 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImPlot; using Dalamud.Bindings.ImPlot;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Console; using Dalamud.Console;
using Dalamud.Data;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Internal;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
@ -305,8 +303,14 @@ internal class DalamudInterface : IInternalDisposableService
/// <summary> /// <summary>
/// Opens the <see cref="ConsoleWindow"/>. /// Opens the <see cref="ConsoleWindow"/>.
/// </summary> /// </summary>
public void OpenLogWindow() /// <param name="textFilter">The filter to set, if not null.</param>
public void OpenLogWindow(string? textFilter = "")
{ {
if (textFilter != null)
{
this.consoleWindow.TextFilter = textFilter;
}
this.consoleWindow.IsOpen = true; this.consoleWindow.IsOpen = true;
this.consoleWindow.BringToFront(); this.consoleWindow.BringToFront();
} }
@ -518,7 +522,7 @@ internal class DalamudInterface : IInternalDisposableService
/// <summary> /// <summary>
/// Toggle the screen darkening effect used for the credits. /// Toggle the screen darkening effect used for the credits.
/// </summary> /// </summary>
/// <param name="status">Whether or not to turn the effect on.</param> /// <param name="status">Whether to turn the effect on.</param>
public void SetCreditsDarkeningAnimation(bool status) public void SetCreditsDarkeningAnimation(bool status)
{ {
this.isCreditsDarkening = status; this.isCreditsDarkening = status;
@ -713,19 +717,6 @@ internal class DalamudInterface : IInternalDisposableService
this.dalamud.StartInfo.LogName); this.dalamud.StartInfo.LogName);
} }
var antiDebug = Service<AntiDebug>.Get();
if (ImGui.MenuItem("Disable Debugging Protections", (byte*)null, antiDebug.IsEnabled))
{
var newEnabled = !antiDebug.IsEnabled;
if (newEnabled)
antiDebug.Enable();
else
antiDebug.Disable();
this.configuration.IsAntiAntiDebugEnabled = newEnabled;
this.configuration.QueueSave();
}
ImGui.Separator(); ImGui.Separator();
if (ImGui.MenuItem("Open Data window")) if (ImGui.MenuItem("Open Data window"))
@ -1012,7 +1003,7 @@ internal class DalamudInterface : IInternalDisposableService
if (ImGui.MenuItem("Scan dev plugins")) if (ImGui.MenuItem("Scan dev plugins"))
{ {
pluginManager.ScanDevPlugins(); _ = pluginManager.ScanDevPluginsAsync();
} }
ImGui.Separator(); ImGui.Separator();

View file

@ -34,13 +34,14 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Timing; using Dalamud.Utility.Timing;
using FFXIVClientStructs.FFXIV.Client.Graphics.Environment;
using JetBrains.Annotations; using JetBrains.Annotations;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows; using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows; using static TerraFX.Interop.Windows.Windows;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
using DWMWINDOWATTRIBUTE = Windows.Win32.Graphics.Dwm.DWMWINDOWATTRIBUTE; using DWMWINDOWATTRIBUTE = Windows.Win32.Graphics.Dwm.DWMWINDOWATTRIBUTE;
// general dev notes, here because it's easiest // general dev notes, here because it's easiest
@ -198,7 +199,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public IImGuiBackend? Backend => this.backend; public IImGuiBackend? Backend => this.backend;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor.
/// </summary> /// </summary>
public bool OverrideGameCursor public bool OverrideGameCursor
{ {
@ -217,7 +218,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public bool IsReady => this.backend != null; public bool IsReady => this.backend != null;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not Draw events should be dispatched. /// Gets or sets a value indicating whether Draw events should be dispatched.
/// </summary> /// </summary>
public bool IsDispatchingEvents { get; set; } = true; public bool IsDispatchingEvents { get; set; } = true;
@ -515,7 +516,9 @@ internal partial class InterfaceManager : IInternalDisposableService
// Some graphics drivers seem to consider the game's shader cache as invalid if we hook too early. // Some graphics drivers seem to consider the game's shader cache as invalid if we hook too early.
// The game loads shader packages on the file thread and then compiles them. It will show the logo once it is done. // The game loads shader packages on the file thread and then compiles them. It will show the logo once it is done.
// This is a workaround, but it fixes an issue where the game would take a very long time to get to the title screen. // This is a workaround, but it fixes an issue where the game would take a very long time to get to the title screen.
if (EnvManager.Instance() == null) // NetworkModuleProxy is set up after lua scripts are loaded (EventFramework.LoadState >= 5), which can only happen
// after the shaders are compiled (if necessary) and loaded. AgentLobby.Update doesn't do much until this condition is met.
if (CSFramework.Instance()->GetNetworkModuleProxy() == null)
return; return;
this.SetupHooks(Service<TargetSigScanner>.Get(), Service<FontAtlasFactory>.Get()); this.SetupHooks(Service<TargetSigScanner>.Get(), Service<FontAtlasFactory>.Get());

View file

@ -0,0 +1,282 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Utility;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>Dedicated thread for OLE operations, and possibly more native thread-serialized operations.</summary>
[ServiceManager.EarlyLoadedService]
internal partial class StaThreadService : IInternalDisposableService
{
private readonly CancellationTokenSource cancellationTokenSource = new();
private readonly Thread thread;
private readonly ThreadBoundTaskScheduler taskScheduler;
private readonly TaskFactory taskFactory;
private readonly TaskCompletionSource<HWND> messageReceiverHwndTask =
new(TaskCreationOptions.RunContinuationsAsynchronously);
[ServiceManager.ServiceConstructor]
private StaThreadService()
{
try
{
this.thread = new(this.OleThreadBody);
this.thread.SetApartmentState(ApartmentState.STA);
this.taskScheduler = new(this.thread);
this.taskScheduler.TaskQueued += this.TaskSchedulerOnTaskQueued;
this.taskFactory = new(
this.cancellationTokenSource.Token,
TaskCreationOptions.None,
TaskContinuationOptions.None,
this.taskScheduler);
this.thread.Start();
this.messageReceiverHwndTask.Task.Wait();
}
catch (Exception e)
{
this.cancellationTokenSource.Cancel();
this.messageReceiverHwndTask.SetException(e);
throw;
}
}
/// <summary>Gets all the available clipboard formats.</summary>
public IReadOnlySet<uint> AvailableClipboardFormats { get; private set; } = ImmutableSortedSet<uint>.Empty;
/// <summary>Places a pointer to a specific data object onto the clipboard. This makes the data object accessible
/// to the <see cref="OleGetClipboard(IDataObject**)"/> function.</summary>
/// <param name="pdo">Pointer to the <see cref="IDataObject"/> interface on the data object from which the data to
/// be placed on the clipboard can be obtained. This parameter can be NULL; in which case the clipboard is emptied.
/// </param>
/// <returns>This function returns <see cref="S.S_OK"/> on success.</returns>
[LibraryImport("ole32.dll")]
public static unsafe partial int OleSetClipboard(IDataObject* pdo);
/// <inheritdoc cref="OleSetClipboard(IDataObject*)"/>
public static unsafe void OleSetClipboard(ComPtr<IDataObject> pdo) =>
Marshal.ThrowExceptionForHR(OleSetClipboard(pdo.Get()));
/// <summary>Retrieves a data object that you can use to access the contents of the clipboard.</summary>
/// <param name="pdo">Address of <see cref="IDataObject"/> pointer variable that receives the interface pointer to
/// the clipboard data object.</param>
/// <returns>This function returns <see cref="S.S_OK"/> on success.</returns>
[LibraryImport("ole32.dll")]
public static unsafe partial int OleGetClipboard(IDataObject** pdo);
/// <inheritdoc cref="OleGetClipboard(IDataObject**)"/>
public static unsafe ComPtr<IDataObject> OleGetClipboard()
{
var pdo = default(ComPtr<IDataObject>);
Marshal.ThrowExceptionForHR(OleGetClipboard(pdo.GetAddressOf()));
return pdo;
}
/// <summary>Calls the appropriate method or function to release the specified storage medium.</summary>
/// <param name="stgm">Address of <see cref="STGMEDIUM"/> to release.</param>
[LibraryImport("ole32.dll")]
public static unsafe partial void ReleaseStgMedium(STGMEDIUM* stgm);
/// <inheritdoc cref="ReleaseStgMedium(STGMEDIUM*)"/>
public static unsafe void ReleaseStgMedium(ref STGMEDIUM stgm)
{
fixed (STGMEDIUM* pstgm = &stgm)
ReleaseStgMedium(pstgm);
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.cancellationTokenSource.Cancel();
if (this.messageReceiverHwndTask.Task.IsCompletedSuccessfully)
SendMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_CLOSE, 0, 0);
this.thread.Join();
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <param name="action">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task"/> representating the state of the operation.</returns>
public async Task Run(Action action, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <typeparam name="T">Type of the return value.</typeparam>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task<T> Run<T>(Func<T> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task Run(Func<Task> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
/// <summary>Runs a given delegate in the messaging thread.</summary>
/// <typeparam name="T">Type of the return value.</typeparam>
/// <param name="func">Delegate to run.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>A <see cref="Task{T}"/> representating the state of the operation.</returns>
public async Task<T> Run<T>(Func<Task<T>> func, CancellationToken cancellationToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(
this.cancellationTokenSource.Token,
cancellationToken);
return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true);
}
[LibraryImport("ole32.dll")]
private static partial int OleInitialize(nint reserved);
[LibraryImport("ole32.dll")]
private static partial void OleUninitialize();
[LibraryImport("ole32.dll")]
private static partial int OleFlushClipboard();
private void TaskSchedulerOnTaskQueued() =>
PostMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_NULL, 0, 0);
private void UpdateAvailableClipboardFormats(HWND hWnd)
{
if (!OpenClipboard(hWnd))
{
this.AvailableClipboardFormats = ImmutableSortedSet<uint>.Empty;
return;
}
var formats = new SortedSet<uint>();
for (var cf = EnumClipboardFormats(0); cf != 0; cf = EnumClipboardFormats(cf))
formats.Add(cf);
this.AvailableClipboardFormats = formats;
CloseClipboard();
}
private LRESULT MessageReceiverWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
this.taskScheduler.Run();
switch (uMsg)
{
case WM.WM_CLIPBOARDUPDATE:
this.UpdateAvailableClipboardFormats(hWnd);
break;
case WM.WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}
private unsafe void OleThreadBody()
{
var hInstance = (HINSTANCE)Marshal.GetHINSTANCE(typeof(StaThreadService).Module);
ushort wndClassAtom = 0;
var gch = GCHandle.Alloc(this);
try
{
((HRESULT)OleInitialize(0)).ThrowOnError();
fixed (char* name = typeof(StaThreadService).FullName!)
{
var wndClass = new WNDCLASSEXW
{
cbSize = (uint)sizeof(WNDCLASSEXW),
lpfnWndProc = &MessageReceiverWndProcStatic,
hInstance = hInstance,
hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1),
lpszClassName = (ushort*)name,
};
wndClassAtom = RegisterClassExW(&wndClass);
if (wndClassAtom == 0)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
this.messageReceiverHwndTask.SetResult(
CreateWindowExW(
0,
(ushort*)wndClassAtom,
(ushort*)name,
0,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
default,
default,
hInstance,
(void*)GCHandle.ToIntPtr(gch)));
[UnmanagedCallersOnly]
static LRESULT MessageReceiverWndProcStatic(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam)
{
nint gchn;
if (uMsg == WM.WM_NCCREATE)
{
gchn = (nint)((CREATESTRUCTW*)lParam)->lpCreateParams;
SetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA, gchn);
}
else
{
gchn = GetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA);
}
if (gchn == 0)
return DefWindowProcW(hWnd, uMsg, wParam, lParam);
return ((StaThreadService)GCHandle.FromIntPtr(gchn).Target!)
.MessageReceiverWndProc(hWnd, uMsg, wParam, lParam);
}
}
AddClipboardFormatListener(this.messageReceiverHwndTask.Task.Result);
this.UpdateAvailableClipboardFormats(this.messageReceiverHwndTask.Task.Result);
for (MSG msg; GetMessageW(&msg, default, 0, 0);)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
catch (Exception e)
{
gch.Free();
_ = OleFlushClipboard();
OleUninitialize();
if (wndClassAtom != 0)
UnregisterClassW((ushort*)wndClassAtom, hInstance);
this.messageReceiverHwndTask.TrySetException(e);
}
}
}

View file

@ -429,17 +429,17 @@ internal unsafe class UiDebug
ImGui.SameLine(); ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText02); Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText02);
ImGui.Text("Text3: "); ImGui.Text("AvailableLines: ");
ImGui.SameLine(); ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText03); Service<SeStringRenderer>.Get().Draw(textInputComponent->AvailableLines);
ImGui.Text("Text4: "); ImGui.Text("HighlightedAutoTranslateOptionColorPrefix: ");
ImGui.SameLine(); ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText04); Service<SeStringRenderer>.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorPrefix);
ImGui.Text("Text5: "); ImGui.Text("HighlightedAutoTranslateOptionColorSuffix: ");
ImGui.SameLine(); ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText05); Service<SeStringRenderer>.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorSuffix);
break; break;
} }

View file

@ -58,7 +58,13 @@ internal unsafe class ComponentNodeTree : ResNodeTree
/// <inheritdoc/> /// <inheritdoc/>
private protected override void PrintChildNodes() private protected override void PrintChildNodes()
{ {
base.PrintChildNodes(); var prevNode = this.CompNode->Component->UldManager.RootNode;
while (prevNode != null)
{
GetOrCreate(prevNode, this.AddonTree).Print(null);
prevNode = prevNode->PrevSiblingNode;
}
var count = this.UldManager->NodeListCount; var count = this.UldManager->NodeListCount;
PrintNodeListAsTree(this.UldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f)); PrintNodeListAsTree(this.UldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f));
} }
@ -92,11 +98,11 @@ internal unsafe class ComponentNodeTree : ResNodeTree
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}"); $"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}");
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}"); $"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}");
ImGui.TextUnformatted( ImGui.TextUnformatted(
$"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}"); $"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}");
break; break;
case List: case List:
case TreeList: case TreeList:

View file

@ -84,7 +84,7 @@ internal unsafe partial class ResNodeTree : IDisposable
/// <returns>An existing or newly-created instance of <see cref="ResNodeTree"/>.</returns> /// <returns>An existing or newly-created instance of <see cref="ResNodeTree"/>.</returns>
internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) => internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) =>
addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree
: (int)node->Type > 1000 : (int)node->Type >= 1000
? new ComponentNodeTree(node, addonTree) ? new ComponentNodeTree(node, addonTree)
: node->Type switch : node->Type switch
{ {

View file

@ -51,9 +51,10 @@ public readonly unsafe partial struct TimelineTree
return; return;
} }
var count = this.Resource->AnimationCount; var animationCount = this.Resource->AnimationCount;
var labelSetCount = this.Resource->LabelSetCount;
if (count > 0) if (animationCount > 0)
{ {
using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth); using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth);
@ -65,6 +66,8 @@ public readonly unsafe partial struct TimelineTree
ShowStruct(this.NodeTimeline); ShowStruct(this.NodeTimeline);
if (this.Resource->Animations is not null)
{
PrintFieldValuePairs( PrintFieldValuePairs(
("Id", $"{this.NodeTimeline->Resource->Id}"), ("Id", $"{this.NodeTimeline->Resource->Id}"),
("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"), ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"),
@ -73,16 +76,27 @@ public readonly unsafe partial struct TimelineTree
PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}")); PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}"));
ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List"); ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List");
for (var a = 0; a < count; a++) for (var a = 0; a < animationCount; a++)
{ {
var animation = this.Resource->Animations[a]; var animation = this.Resource->Animations[a];
var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation; var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation;
this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation)))); this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + a));
} }
} }
} }
} }
if (labelSetCount > 0 && this.Resource->LabelSets is not null)
{
using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth);
if (tree.Success)
{
this.DrawLabelSets();
}
}
}
private static void GetFrameColumn(Span<AtkTimelineKeyGroup> keyGroups, List<IKeyGroupColumn> columns, ushort endFrame) private static void GetFrameColumn(Span<AtkTimelineKeyGroup> keyGroups, List<IKeyGroupColumn> columns, ushort endFrame)
{ {
for (var i = 0; i < keyGroups.Length; i++) for (var i = 0; i < keyGroups.Length; i++)
@ -380,4 +394,63 @@ public readonly unsafe partial struct TimelineTree
return columns; return columns;
} }
private void DrawLabelSets()
{
PrintFieldValuePair("LabelSet", $"{(nint)this.NodeTimeline->Resource->LabelSets:X}");
ImGui.SameLine();
ShowStruct(this.NodeTimeline->Resource->LabelSets);
PrintFieldValuePairs(
("StartFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->StartFrameIdx}"),
("EndFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->EndFrameIdx}"));
using var labelSetTable = ImRaii.TreeNode("Entries");
if (labelSetTable.Success)
{
var keyFrameGroup = this.Resource->LabelSets->LabelKeyGroup;
using var table = ImRaii.Table($"##{(nint)this.node}labelSetKeyFrameTable", 7, Borders | SizingFixedFit | RowBg | NoHostExtendX);
if (table.Success)
{
ImGui.TableSetupColumn("Frame ID", WidthFixed);
ImGui.TableSetupColumn("Speed Start", WidthFixed);
ImGui.TableSetupColumn("Speed End", WidthFixed);
ImGui.TableSetupColumn("Interpolation", WidthFixed);
ImGui.TableSetupColumn("Label ID", WidthFixed);
ImGui.TableSetupColumn("Jump Behavior", WidthFixed);
ImGui.TableSetupColumn("Target Label ID", WidthFixed);
ImGui.TableHeadersRow();
for (var l = 0; l < keyFrameGroup.KeyFrameCount; l++)
{
var keyFrame = keyFrameGroup.KeyFrames[l];
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.FrameIdx}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient1:F2}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient2:F2}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Interpolation}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Value.Label.LabelId}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpBehavior}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpLabelId}");
}
}
}
}
} }

View file

@ -121,6 +121,19 @@ internal class ConsoleWindow : Window, IDisposable
/// <summary>Gets the queue where log entries that are not processed yet are stored.</summary> /// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new(); public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
/// <summary>
/// Gets or sets the current text filter.
/// </summary>
public string TextFilter
{
get => this.textFilter;
set
{
this.textFilter = value;
this.RecompileLogFilter();
}
}
/// <inheritdoc/> /// <inheritdoc/>
public override void OnOpen() public override void OnOpen()
{ {
@ -619,6 +632,12 @@ internal class ConsoleWindow : Window, IDisposable
2048, 2048,
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
|| ImGui.IsItemDeactivatedAfterEdit()) || ImGui.IsItemDeactivatedAfterEdit())
{
this.RecompileLogFilter();
}
}
private void RecompileLogFilter()
{ {
this.compiledLogFilter = null; this.compiledLogFilter = null;
this.exceptionLogFilter = null; this.exceptionLogFilter = null;
@ -636,7 +655,6 @@ internal class ConsoleWindow : Window, IDisposable
foreach (var log in this.logText) foreach (var log in this.logText)
log.HighlightMatches = null; log.HighlightMatches = null;
} }
}
private void DrawSettingsPopup() private void DrawSettingsPopup()
{ {

View file

@ -1,4 +1,5 @@
using System.Numerics; using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game.Gui.FlyText; using Dalamud.Game.Gui.FlyText;
@ -38,13 +39,15 @@ internal class FlyTextWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public void Draw() public void Draw()
{ {
if (ImGui.BeginCombo("Kind", this.flyKind.ToString())) if (ImGui.BeginCombo("Kind", $"{this.flyKind} ({(int)this.flyKind})"))
{ {
var names = Enum.GetNames(typeof(FlyTextKind)); var values = Enum.GetValues<FlyTextKind>().Distinct();
for (var i = 0; i < names.Length; i++) foreach (var value in values)
{ {
if (ImGui.Selectable($"{names[i]} ({i})")) if (ImGui.Selectable($"{value} ({(int)value})"))
this.flyKind = (FlyTextKind)i; {
this.flyKind = value;
}
} }
ImGui.EndCombo(); ImGui.EndCombo();

View file

@ -1,7 +1,13 @@
using System.Runtime.InteropServices; using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Hooking; using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog; using Serilog;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging; using Windows.Win32.UI.WindowsAndMessaging;
@ -11,17 +17,40 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary> /// <summary>
/// Widget for displaying hook information. /// Widget for displaying hook information.
/// </summary> /// </summary>
internal class HookWidget : IDataWindowWidget internal unsafe class HookWidget : IDataWindowWidget
{ {
private readonly List<IDalamudHook> hookStressTestList = [];
private Hook<MessageBoxWDelegate>? messageBoxMinHook; private Hook<MessageBoxWDelegate>? messageBoxMinHook;
private bool hookUseMinHook; private bool hookUseMinHook;
private int hookStressTestCount = 0;
private int hookStressTestMax = 1000;
private int hookStressTestWait = 100;
private int hookStressTestMaxDegreeOfParallelism = 10;
private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random;
private bool hookStressTestRunning = false;
private MessageBoxWDelegate? messageBoxWOriginal;
private AddonFinalizeDelegate? addonFinalizeOriginal;
private AddonLifecycleAddressResolver? address;
private delegate int MessageBoxWDelegate( private delegate int MessageBoxWDelegate(
IntPtr hWnd, IntPtr hWnd,
[MarshalAs(UnmanagedType.LPWStr)] string text, [MarshalAs(UnmanagedType.LPWStr)] string text,
[MarshalAs(UnmanagedType.LPWStr)] string caption, [MarshalAs(UnmanagedType.LPWStr)] string caption,
MESSAGEBOX_STYLE type); MESSAGEBOX_STYLE type);
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
private enum StressTestHookTarget
{
MessageBoxW,
AddonFinalize,
Random,
}
/// <inheritdoc/> /// <inheritdoc/>
public string DisplayName { get; init; } = "Hook"; public string DisplayName { get; init; } = "Hook";
@ -35,6 +64,9 @@ internal class HookWidget : IDataWindowWidget
public void Load() public void Load()
{ {
this.Ready = true; this.Ready = true;
this.address = new AddonLifecycleAddressResolver();
this.address.Setup(Service<TargetSigScanner>.Get());
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -42,7 +74,9 @@ internal class HookWidget : IDataWindowWidget
{ {
try try
{ {
ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook); ImGui.Checkbox("Use MinHook (only for regular hooks, AsmHook is Reloaded-only)", ref this.hookUseMinHook);
ImGui.Separator();
if (ImGui.Button("Create")) if (ImGui.Button("Create"))
this.messageBoxMinHook = Hook<MessageBoxWDelegate>.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook); this.messageBoxMinHook = Hook<MessageBoxWDelegate>.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook);
@ -67,18 +101,94 @@ internal class HookWidget : IDataWindowWidget
if (this.messageBoxMinHook != null) if (this.messageBoxMinHook != null)
ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled); ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled);
ImGui.Separator();
ImGui.BeginDisabled(this.hookStressTestRunning);
ImGui.Text("Stress Test");
if (ImGui.InputInt("Max", ref this.hookStressTestMax))
this.hookStressTestCount = 0;
ImGui.InputInt("Wait (ms)", ref this.hookStressTestWait);
ImGui.InputInt("Max Degree of Parallelism", ref this.hookStressTestMaxDegreeOfParallelism);
if (ImGui.BeginCombo("Target", HookTargetToString(this.hookStressTestHookTarget)))
{
foreach (var target in Enum.GetValues<StressTestHookTarget>())
{
if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target))
this.hookStressTestHookTarget = target;
}
ImGui.EndCombo();
}
if (ImGui.Button("Stress Test"))
{
Task.Run(() =>
{
this.hookStressTestRunning = true;
this.hookStressTestCount = 0;
Parallel.For(
0,
this.hookStressTestMax,
new ParallelOptions
{
MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism,
},
_ =>
{
this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget));
this.hookStressTestCount++;
Thread.Sleep(this.hookStressTestWait);
});
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Stress test failed");
}
else
{
Log.Information("Stress test completed");
}
this.hookStressTestRunning = false;
this.hookStressTestList.ForEach(hook =>
{
hook.Dispose();
});
this.hookStressTestList.Clear();
});
}
ImGui.EndDisabled();
ImGui.TextUnformatted("Status: " + (this.hookStressTestRunning ? "Running" : "Idle"));
ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "MinHook error caught"); Log.Error(ex, "Hook error caught");
} }
} }
private static string HookTargetToString(StressTestHookTarget target)
{
return target switch
{
StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)",
StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)",
_ => target.ToString(),
};
}
private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, MESSAGEBOX_STYLE type) private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, MESSAGEBOX_STYLE type)
{ {
Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type); Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type);
var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, MESSAGEBOX_STYLE.MB_YESNO); var result = this.messageBoxWOriginal!(hwnd, "Cause Access Violation?", caption, MESSAGEBOX_STYLE.MB_YESNO);
if (result == (int)MESSAGEBOX_RESULT.IDYES) if (result == (int)MESSAGEBOX_RESULT.IDYES)
{ {
@ -87,4 +197,52 @@ internal class HookWidget : IDataWindowWidget
return result; return result;
} }
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
Log.Information("OnAddonFinalize");
this.addonFinalizeOriginal!(unitManager, atkUnitBase);
}
private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta)
{
Log.Information("OnAddonUpdate");
}
private IDalamudHook HookMessageBoxW()
{
var hook = Hook<MessageBoxWDelegate>.FromSymbol(
"User32",
"MessageBoxW",
this.MessageBoxWDetour,
this.hookUseMinHook);
this.messageBoxWOriginal = hook.Original;
hook.Enable();
return hook;
}
private IDalamudHook HookAddonFinalize()
{
var hook = Hook<AddonFinalizeDelegate>.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize);
this.addonFinalizeOriginal = hook.Original;
hook.Enable();
return hook;
}
private IDalamudHook HookTarget(StressTestHookTarget target)
{
if (target == StressTestHookTarget.Random)
{
target = (StressTestHookTarget)Random.Shared.Next(0, 2);
}
return target switch
{
StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(),
StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(),
_ => throw new ArgumentOutOfRangeException(nameof(target), target, null),
};
}
} }

View file

@ -23,7 +23,7 @@ internal class InventoryWidget : IDataWindowWidget
{ {
private DataManager dataManager; private DataManager dataManager;
private TextureManager textureManager; private TextureManager textureManager;
private InventoryType? selectedInventoryType = InventoryType.Inventory1; private GameInventoryType? selectedInventoryType = GameInventoryType.Inventory1;
/// <inheritdoc/> /// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["inv", "inventory"]; public string[]? CommandShortcuts { get; init; } = ["inv", "inventory"];
@ -53,7 +53,7 @@ internal class InventoryWidget : IDataWindowWidget
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
this.DrawInventoryType((InventoryType)this.selectedInventoryType); this.DrawInventoryType(this.selectedInventoryType.Value);
} }
private static string StripSoftHypen(string input) private static string StripSoftHypen(string input)
@ -71,9 +71,9 @@ internal class InventoryWidget : IDataWindowWidget
ImGui.TableSetupScrollFreeze(2, 1); ImGui.TableSetupScrollFreeze(2, 1);
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
foreach (var inventoryType in Enum.GetValues<InventoryType>()) foreach (var inventoryType in Enum.GetValues<GameInventoryType>())
{ {
var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType); var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType);
using var itemDisabled = ImRaii.Disabled(items.IsEmpty); using var itemDisabled = ImRaii.Disabled(items.IsEmpty);
@ -95,7 +95,7 @@ internal class InventoryWidget : IDataWindowWidget
if (ImGui.MenuItem("Copy Address")) if (ImGui.MenuItem("Copy Address"))
{ {
var container = InventoryManager.Instance()->GetInventoryContainer(inventoryType); var container = InventoryManager.Instance()->GetInventoryContainer((InventoryType)inventoryType);
ImGui.SetClipboardText($"0x{(nint)container:X}"); ImGui.SetClipboardText($"0x{(nint)container:X}");
} }
} }
@ -106,9 +106,9 @@ internal class InventoryWidget : IDataWindowWidget
} }
} }
private unsafe void DrawInventoryType(InventoryType inventoryType) private unsafe void DrawInventoryType(GameInventoryType inventoryType)
{ {
var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType); var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType);
if (items.IsEmpty) if (items.IsEmpty)
{ {
ImGui.TextUnformatted($"{inventoryType} is empty."); ImGui.TextUnformatted($"{inventoryType} is empty.");

View file

@ -20,7 +20,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
internal class NounProcessorWidget : IDataWindowWidget internal class NounProcessorWidget : IDataWindowWidget
{ {
/// <summary>A list of German grammatical cases.</summary> /// <summary>A list of German grammatical cases.</summary>
internal static readonly string[] GermanCases = ["Nominative", "Genitive", "Dative", "Accusative"]; internal static readonly string[] GermanCases = [string.Empty, "Nominative", "Genitive", "Dative", "Accusative"];
private static readonly Type[] NounSheets = [ private static readonly Type[] NounSheets = [
typeof(Aetheryte), typeof(Aetheryte),
@ -155,7 +155,7 @@ internal class NounProcessorWidget : IDataWindowWidget
GrammaticalCase = grammaticalCase, GrammaticalCase = grammaticalCase,
}; };
var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\""); var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\"");
var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase]}" : "1"; var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase + 1]}" : "1";
sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),"); sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),");
} }
} }

View file

@ -7,6 +7,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.Noun.Enums; using Dalamud.Game.Text.Noun.Enums;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
@ -90,6 +91,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
{ MacroCode.LowerHead, ["String"] }, { MacroCode.LowerHead, ["String"] },
{ MacroCode.ColorType, ["ColorType"] }, { MacroCode.ColorType, ["ColorType"] },
{ MacroCode.EdgeColorType, ["ColorType"] }, { MacroCode.EdgeColorType, ["ColorType"] },
{ MacroCode.Ruby, ["StandardText", "RubyText"] },
{ MacroCode.Digit, ["Value", "TargetLength"] }, { MacroCode.Digit, ["Value", "TargetLength"] },
{ MacroCode.Ordinal, ["Value"] }, { MacroCode.Ordinal, ["Value"] },
{ MacroCode.Sound, ["IsJingle", "SoundId"] }, { MacroCode.Sound, ["IsJingle", "SoundId"] },
@ -132,8 +134,8 @@ internal class SeStringCreatorWidget : IDataWindowWidget
new TextEntry(TextEntryType.Macro, "<colortype(17)>"), new TextEntry(TextEntryType.Macro, "<colortype(17)>"),
new TextEntry(TextEntryType.Macro, "<edgecolortype(19)>"), new TextEntry(TextEntryType.Macro, "<edgecolortype(19)>"),
new TextEntry(TextEntryType.String, "Dalamud"), new TextEntry(TextEntryType.String, "Dalamud"),
new TextEntry(TextEntryType.Macro, "<edgecolor(0)>"), new TextEntry(TextEntryType.Macro, "<edgecolor(stackcolor)>"),
new TextEntry(TextEntryType.Macro, "<colortype(0)>"), new TextEntry(TextEntryType.Macro, "<color(stackcolor)>"),
new TextEntry(TextEntryType.Macro, " <string(lstr1)>"), new TextEntry(TextEntryType.Macro, " <string(lstr1)>"),
]; ];
@ -165,7 +167,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public void Load() public void Load()
{ {
this.language = Service<DalamudConfiguration>.Get().EffectiveLanguage.ToClientLanguage(); this.language = Service<ClientState>.Get().ClientLanguage;
this.UpdateInputString(false); this.UpdateInputString(false);
this.Ready = true; this.Ready = true;
} }
@ -473,7 +475,12 @@ internal class SeStringCreatorWidget : IDataWindowWidget
} }
} }
RaptureLogModule.Instance()->PrintString(Service<SeStringEvaluator>.Get().Evaluate(sb.ToReadOnlySeString())); var evaluated = Service<SeStringEvaluator>.Get().Evaluate(
sb.ToReadOnlySeString(),
this.localParameters,
this.language);
RaptureLogModule.Instance()->PrintString(evaluated);
} }
if (this.entries.Count != 0) if (this.entries.Count != 0)
@ -1011,7 +1018,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32)); ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32));
} }
if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 3) if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 4)
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]); ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]);

View file

@ -237,27 +237,40 @@ internal class ServicesWidget : IDataWindowWidget
} }
} }
if (ImGui.CollapsingHeader("Plugin-facing Services")) if (ImGui.CollapsingHeader("Singleton Services"))
{ {
foreach (var instance in container.Instances) foreach (var instance in container.Instances)
{ {
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
var isPublic = instance.Key.IsPublic; var isPublic = instance.Key.IsPublic;
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
if (isPublic)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.Text("\t => PUBLIC!!!");
}
switch (instance.Value.Visibility)
{
case ObjectInstanceVisibility.Internal:
ImGui.Text("\t => Internally resolved");
break;
case ObjectInstanceVisibility.ExposedToPlugins:
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{ {
ImGui.Text("\t => Exposed to plugins!");
ImGui.Text( ImGui.Text(
hasInterface hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!"); : "\t => NO INTERFACE!!!");
} }
if (isPublic) break;
{ default:
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); throw new ArgumentOutOfRangeException();
ImGui.Text("\t => PUBLIC!!!");
} }
ImGuiHelpers.ScaledDummy(2); ImGuiHelpers.ScaledDummy(2);

View file

@ -180,6 +180,15 @@ internal class TexWidget : IDataWindowWidget
ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
if (!this.textureManager.HasClipboardImage())
{
ImGuiComponents.DisabledButton("Paste from Clipboard");
}
else if (ImGui.Button("Paste from Clipboard"))
{
this.addedTextures.Add(new(Api10: this.textureManager.CreateFromClipboardAsync()));
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon))) if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon)))
{ {
ImGui.PushID(nameof(this.DrawGetFromGameIcon)); ImGui.PushID(nameof(this.DrawGetFromGameIcon));

View file

@ -226,6 +226,7 @@ internal class PluginInstallerWindow : Window, IDisposable
IsInstallableOutdated = 1 << 5, IsInstallableOutdated = 1 << 5,
IsOrphan = 1 << 6, IsOrphan = 1 << 6,
IsTesting = 1 << 7, IsTesting = 1 << 7,
IsIncompatible = 1 << 8,
} }
private enum InstalledPluginListFilter private enum InstalledPluginListFilter
@ -281,11 +282,15 @@ internal class PluginInstallerWindow : Window, IDisposable
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync(); _ = pluginManager.ReloadPluginMastersAsync();
Service<PluginManager>.Get().ScanDevPlugins(); _ = pluginManager.ScanDevPluginsAsync();
if (!this.isSearchTextPrefilled) this.searchText = string.Empty; if (!this.isSearchTextPrefilled)
{
this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical; this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical; this.filterText = Locs.SortBy_Alphabetical;
}
this.adaptiveSort = true; this.adaptiveSort = true;
if (this.updateStatus == OperationStatus.Complete || this.updateStatus == OperationStatus.Idle) if (this.updateStatus == OperationStatus.Complete || this.updateStatus == OperationStatus.Idle)
@ -361,11 +366,20 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
this.isSearchTextPrefilled = false; this.isSearchTextPrefilled = false;
this.searchText = string.Empty; this.searchText = string.Empty;
if (this.sortKind == PluginSortKind.SearchScore)
{
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
this.ResortPlugins();
}
} }
else else
{ {
this.isSearchTextPrefilled = true; this.isSearchTextPrefilled = true;
this.searchText = text; this.searchText = text;
this.sortKind = PluginSortKind.SearchScore;
this.filterText = Locs.SortBy_SearchScore;
this.ResortPlugins();
} }
} }
@ -458,6 +472,36 @@ internal class PluginInstallerWindow : Window, IDisposable
configuration.QueueSave(); configuration.QueueSave();
} }
private static void DrawProgressBar<T>(IEnumerable<T> items, Func<T, bool> pendingFunc, Func<T, bool> totalFunc, Action<T> renderPending)
{
var windowSize = ImGui.GetWindowSize();
var numLoaded = 0;
var total = 0;
var itemsArray = items as T[] ?? items.ToArray();
var allPending = itemsArray.Where(pendingFunc)
.ToArray();
var allLoadedOrLoading = itemsArray.Count(totalFunc);
// Cap number of items we show to avoid clutter
const int maxShown = 3;
foreach (var repo in allPending.Take(maxShown))
{
renderPending(repo);
}
ImGuiHelpers.ScaledDummy(10);
numLoaded += allLoadedOrLoading - allPending.Length;
total += allLoadedOrLoading;
if (numLoaded != total)
{
ImGui.SetCursorPosX(windowSize.X / 3);
ImGui.ProgressBar(numLoaded / (float)total, new Vector2(windowSize.X / 3, 50), $"{numLoaded}/{total}");
}
}
private void SetOpenPage(PluginInstallerOpenKind kind) private void SetOpenPage(PluginInstallerOpenKind kind)
{ {
switch (kind) switch (kind)
@ -486,6 +530,12 @@ internal class PluginInstallerWindow : Window, IDisposable
// Plugins category // Plugins category
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
break; break;
case PluginInstallerOpenKind.DalamudChangelogs:
// Changelog group
this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog;
// Dalamud category
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.DalamudChangelogs;
break;
default: default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null); throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
} }
@ -523,7 +573,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.GetWindowDrawList().AddRectFilled( ImGui.GetWindowDrawList().AddRectFilled(
ImGui.GetWindowPos() + new Vector2(0, titleHeight), ImGui.GetWindowPos() + new Vector2(0, titleHeight),
ImGui.GetWindowPos() + windowSize, ImGui.GetWindowPos() + windowSize,
0xCC000000, ImGui.ColorConvertFloat4ToU32(new(0f, 0f, 0f, 0.8f * ImGui.GetStyle().Alpha)),
ImGui.GetStyle().WindowRounding, ImGui.GetStyle().WindowRounding,
ImDrawFlags.RoundCornersBottom); ImDrawFlags.RoundCornersBottom);
ImGui.PopClipRect(); ImGui.PopClipRect();
@ -555,40 +605,29 @@ internal class PluginInstallerWindow : Window, IDisposable
if (pluginManager.PluginsReady && !pluginManager.ReposReady) if (pluginManager.PluginsReady && !pluginManager.ReposReady)
{ {
ImGuiHelpers.CenteredText("Loading repositories..."); ImGuiHelpers.CenteredText("Loading repositories...");
ImGuiHelpers.ScaledDummy(10);
DrawProgressBar(pluginManager.Repos, x => x.State != PluginRepositoryState.Success &&
x.State != PluginRepositoryState.Fail &&
x.IsEnabled,
x => x.IsEnabled,
x => ImGuiHelpers.CenteredText($"Loading {x.PluginMasterUrl}"));
} }
else if (!pluginManager.PluginsReady && pluginManager.ReposReady) else if (!pluginManager.PluginsReady && pluginManager.ReposReady)
{ {
ImGuiHelpers.CenteredText("Loading installed plugins..."); ImGuiHelpers.CenteredText("Loading installed plugins...");
ImGuiHelpers.ScaledDummy(10);
DrawProgressBar(pluginManager.InstalledPlugins, x => x.State == PluginState.Loading,
x => x.State is PluginState.Loaded or
PluginState.LoadError or
PluginState.Loading,
x => ImGuiHelpers.CenteredText($"Loading {x.Name}"));
} }
else else
{ {
ImGuiHelpers.CenteredText("Loading repositories and plugins..."); ImGuiHelpers.CenteredText("Loading repositories and plugins...");
} }
var currentProgress = 0;
var total = 0;
var pendingRepos = pluginManager.Repos.ToArray()
.Where(x => (x.State != PluginRepositoryState.Success &&
x.State != PluginRepositoryState.Fail) &&
x.IsEnabled)
.ToArray();
var allRepoCount =
pluginManager.Repos.Count(x => x.State != PluginRepositoryState.Fail && x.IsEnabled);
foreach (var repo in pendingRepos)
{
ImGuiHelpers.CenteredText($"{repo.PluginMasterUrl}: {repo.State}");
}
currentProgress += allRepoCount - pendingRepos.Length;
total += allRepoCount;
if (currentProgress != total)
{
ImGui.SetCursorPosX(windowSize.X / 3);
ImGui.ProgressBar(currentProgress / (float)total, new Vector2(windowSize.X / 3, 50));
}
} }
break; break;
@ -599,11 +638,27 @@ internal class PluginInstallerWindow : Window, IDisposable
throw new ArgumentOutOfRangeException(); throw new ArgumentOutOfRangeException();
} }
if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(90) && !pluginManager.PluginsReady) if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(30) && !pluginManager.PluginsReady)
{ {
ImGuiHelpers.ScaledDummy(10);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer."); ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer.");
ImGuiHelpers.CenteredText("You can try restarting in safe mode, and deleting the plugin.");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGuiHelpers.BeginHorizontalButtonGroup()
.Add(
"Restart in Safe Mode",
() =>
{
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.ForceSave();
Dalamud.RestartGame();
})
.SetCentered(true)
.WithHeight(30)
.Draw();
} }
} }
} }
@ -761,7 +816,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button(Locs.FooterButton_ScanDevPlugins)) if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
{ {
pluginManager.ScanDevPlugins(); _ = pluginManager.ScanDevPluginsAsync();
} }
} }
@ -2137,7 +2192,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha);
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
ImGui.Image(this.imageCache.UpdateIcon.Handle, iconSize); ImGui.Image(this.imageCache.UpdateIcon.Handle, iconSize);
else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan)) else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan) || flags.HasFlag(PluginHeaderFlags.IsIncompatible))
ImGui.Image(this.imageCache.TroubleIcon.Handle, iconSize); ImGui.Image(this.imageCache.TroubleIcon.Handle, iconSize);
else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
ImGui.Image(this.imageCache.OutdatedInstallableIcon.Handle, iconSize); ImGui.Image(this.imageCache.OutdatedInstallableIcon.Handle, iconSize);
@ -2213,9 +2268,14 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPos(cursor); ImGui.SetCursorPos(cursor);
// Outdated warning // Outdated warning
if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) if (flags.HasFlag(PluginHeaderFlags.IsIncompatible))
{ {
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_Incompatible);
}
else if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
{
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
var bodyText = Locs.PluginBody_Outdated + " "; var bodyText = Locs.PluginBody_Outdated + " ";
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
@ -2224,7 +2284,6 @@ internal class PluginInstallerWindow : Window, IDisposable
bodyText += Locs.PluginBody_Outdated_WaitForUpdate; bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
ImGui.TextWrapped(bodyText); ImGui.TextWrapped(bodyText);
ImGui.PopStyleColor();
} }
else if (plugin is { IsBanned: true }) else if (plugin is { IsBanned: true })
{ {
@ -2393,6 +2452,14 @@ internal class PluginInstallerWindow : Window, IDisposable
var effectiveApiLevel = useTesting && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; var effectiveApiLevel = useTesting && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel;
var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel;
var isIncompatible = manifest.MinimumDalamudVersion != null &&
manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed;
var enableInstallButton = this.updateStatus != OperationStatus.InProgress &&
this.installStatus != OperationStatus.InProgress &&
!isOutdated &&
!isIncompatible;
// Check for valid versions // Check for valid versions
if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null) if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null)
{ {
@ -2417,6 +2484,11 @@ internal class PluginInstallerWindow : Window, IDisposable
label += Locs.PluginTitleMod_TestingAvailable; label += Locs.PluginTitleMod_TestingAvailable;
} }
if (isIncompatible)
{
label += Locs.PluginTitleMod_Incompatible;
}
var isThirdParty = manifest.SourceRepo.IsThirdParty; var isThirdParty = manifest.SourceRepo.IsThirdParty;
ImGui.PushID($"available{index}{manifest.InternalName}"); ImGui.PushID($"available{index}{manifest.InternalName}");
@ -2430,6 +2502,8 @@ internal class PluginInstallerWindow : Window, IDisposable
flags |= PluginHeaderFlags.IsInstallableOutdated; flags |= PluginHeaderFlags.IsInstallableOutdated;
if (useTesting || manifest.IsTestingExclusive) if (useTesting || manifest.IsTestingExclusive)
flags |= PluginHeaderFlags.IsTesting; flags |= PluginHeaderFlags.IsTesting;
if (isIncompatible)
flags |= PluginHeaderFlags.IsIncompatible;
if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index)) if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index))
{ {
@ -2457,9 +2531,6 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
// Controls
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress || isOutdated;
var versionString = useTesting var versionString = useTesting
? $"{manifest.TestingAssemblyVersion}" ? $"{manifest.TestingAssemblyVersion}"
: $"{manifest.AssemblyVersion}"; : $"{manifest.AssemblyVersion}";
@ -2468,7 +2539,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode); ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode);
} }
else if (disabled) else if (!enableInstallButton)
{ {
ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString)); ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString));
} }
@ -2713,7 +2784,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog; var applicableChangelog = plugin.IsTesting ? remoteManifest?.TestingChangelog : remoteManifest?.Changelog;
var hasChangelog = !applicableChangelog.IsNullOrWhitespace(); var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
var didDrawApplicableChangelogInsideCollapsible = false; var didDrawApplicableChangelogInsideCollapsible = false;
@ -3096,11 +3167,13 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle); ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle);
} }
else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled || pluginManager.SafeMode)
{ {
ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable);
if (inMultipleProfiles && ImGui.IsItemHovered()) if (pluginManager.SafeMode && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_SafeMode);
else if (inMultipleProfiles && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile); ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile);
else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered()) else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name)); ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name));
@ -3494,6 +3567,24 @@ internal class PluginInstallerWindow : Window, IDisposable
{ {
ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading); ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading);
} }
// Error Notifications
ImGui.PushStyleColor(ImGuiCol.Button, plugin.NotifyForErrors ? greenColor : redColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.NotifyForErrors ? greenColor : redColor);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Bolt))
{
plugin.NotifyForErrors ^= true;
configuration.QueueSave();
}
ImGui.PopStyleColor(2);
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(Locs.PluginButtonToolTip_NotifyForErrors);
}
} }
} }
@ -4047,6 +4138,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginTitleMod_TestingAvailable => Loc.Localize("InstallerTestingAvailable", " (has testing version)"); public static string PluginTitleMod_TestingAvailable => Loc.Localize("InstallerTestingAvailable", " (has testing version)");
public static string PluginTitleMod_Incompatible => Loc.Localize("InstallerTitleModIncompatible", " (incompatible)");
public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)"); public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)");
public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)"); public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)");
@ -4103,6 +4196,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible."); public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible.");
public static string PluginBody_Incompatible => Loc.Localize("InstallerIncompatiblePluginBody ", "This plugin is incompatible with your version of Dalamud. Please attempt to restart your game.");
public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author."); public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author.");
public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation."); public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation.");
@ -4160,6 +4255,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading"); public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading");
public static string PluginButtonToolTip_NotifyForErrors => Loc.Localize("InstallerNotifyForErrors", "Show Dalamud notifications when this plugin is creating errors");
public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin"); public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin");
public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game."); public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game.");
@ -4180,6 +4277,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection."); public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection.");
public static string PluginButtonToolTip_SafeMode => Loc.Localize("InstallerButtonSafeModeTooltip", "Cannot enable plugins in safe mode.");
public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name); public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name);
#endregion #endregion

View file

@ -224,6 +224,9 @@ internal class ProfileManagerWidget
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
ImGui.SameLine(); ImGui.SameLine();
// Center text in frame height
var textHeight = ImGui.CalcTextSize(profile.Name);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() / 2) - (textHeight.Y / 2));
ImGui.Text(profile.Name); ImGui.Text(profile.Name);
ImGui.SameLine(); ImGui.SameLine();
@ -263,6 +266,17 @@ internal class ProfileManagerWidget
didAny = true; didAny = true;
ImGuiHelpers.ScaledDummy(2); ImGuiHelpers.ScaledDummy(2);
// Separator if not the last item
if (profile != profman.Profiles.Last())
{
// Very light grey
ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f));
ImGui.Separator();
ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(2);
}
} }
if (toCloneGuid != null) if (toCloneGuid != null)
@ -386,10 +400,19 @@ internal class ProfileManagerWidget
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
var enableAtBoot = profile.AlwaysEnableAtBoot; ImGui.TextUnformatted(Locs.StartupBehavior);
if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot)) if (ImGui.BeginCombo("##startupBehaviorPicker", Locs.PolicyToLocalisedName(profile.StartupPolicy)))
{ {
profile.AlwaysEnableAtBoot = enableAtBoot; foreach (var policy in Enum.GetValues(typeof(ProfileModelV1.ProfileStartupPolicy)).Cast<ProfileModelV1.ProfileStartupPolicy>())
{
var name = Locs.PolicyToLocalisedName(policy);
if (ImGui.Selectable(name, profile.StartupPolicy == policy))
{
profile.StartupPolicy = policy;
}
}
ImGui.EndCombo();
} }
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
@ -514,6 +537,15 @@ internal class ProfileManagerWidget
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.RemovePlugin); ImGui.SetTooltip(Locs.RemovePlugin);
// Separator if not the last item
if (profileEntry != profile.Plugins.Last())
{
// Very light grey
ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f));
ImGui.Separator();
ImGui.PopStyleColor();
}
} }
if (wantRemovePluginGuid != null) if (wantRemovePluginGuid != null)
@ -554,6 +586,9 @@ internal class ProfileManagerWidget
private static class Locs private static class Locs
{ {
public static string StartupBehavior =>
Loc.Localize("ProfileManagerStartupBehavior", "Startup behavior");
public static string TooltipEnableDisable => public static string TooltipEnableDisable =>
Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection"); Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection");
@ -567,9 +602,6 @@ internal class ProfileManagerWidget
public static string NoPluginsInProfile => public static string NoPluginsInProfile =>
Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!"); Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!");
public static string AlwaysEnableAtBoot =>
Loc.Localize("ProfileManagerAlwaysEnableAtBoot", "Always enable when game starts");
public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection"); public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection");
public static string CopyToClipboardHint => public static string CopyToClipboardHint =>
@ -647,5 +679,22 @@ internal class ProfileManagerWidget
public static string NotInstalled(string name) => public static string NotInstalled(string name) =>
Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name); Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name);
public static string PolicyToLocalisedName(ProfileModelV1.ProfileStartupPolicy policy)
{
return policy switch
{
ProfileModelV1.ProfileStartupPolicy.RememberState => Loc.Localize(
"ProfileManagerRememberState",
"Remember state"),
ProfileModelV1.ProfileStartupPolicy.AlwaysEnable => Loc.Localize(
"ProfileManagerAlwaysEnableAtBoot",
"Always enable at boot"),
ProfileModelV1.ProfileStartupPolicy.AlwaysDisable => Loc.Localize(
"ProfileManagerAlwaysDisableAtBoot",
"Always disable at boot"),
_ => throw new ArgumentOutOfRangeException(nameof(policy), policy, null),
};
}
} }
} }

View file

@ -9,6 +9,7 @@ using Dalamud.Hooking.Internal;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
@ -44,10 +45,13 @@ internal class PluginStatWindow : Window
{ {
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
if (!ImGui.BeginTabBar("Stat Tabs")) using var tabBar = ImRaii.TabBar("Stat Tabs");
if (!tabBar)
return; return;
if (ImGui.BeginTabItem("Draw times")) using (var tabItem = ImRaii.TabItem("Draw times"))
{
if (tabItem)
{ {
var doStats = UiBuilder.DoStats; var doStats = UiBuilder.DoStats;
@ -88,7 +92,7 @@ internal class PluginStatWindow : Window
ref this.drawSearchText, ref this.drawSearchText,
500); 500);
if (ImGui.BeginTable( using var table = ImRaii.Table(
"##PluginStatsDrawTimes", "##PluginStatsDrawTimes",
4, 4,
ImGuiTableFlags.RowBg ImGuiTableFlags.RowBg
@ -97,7 +101,9 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable | ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY | ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable | ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable)) | ImGuiTableFlags.Hideable);
if (table)
{ {
ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Plugin"); ImGui.TableSetupColumn("Plugin");
@ -148,15 +154,14 @@ internal class PluginStatWindow : Window
: "-"); : "-");
} }
} }
}
ImGui.EndTable(); }
} }
} }
ImGui.EndTabItem(); using (var tabItem = ImRaii.TabItem("Framework times"))
} {
if (tabItem)
if (ImGui.BeginTabItem("Framework times"))
{ {
var doStats = Framework.StatsEnabled; var doStats = Framework.StatsEnabled;
@ -189,7 +194,7 @@ internal class PluginStatWindow : Window
ref this.frameworkSearchText, ref this.frameworkSearchText,
500); 500);
if (ImGui.BeginTable( using var table = ImRaii.Table(
"##PluginStatsFrameworkTimes", "##PluginStatsFrameworkTimes",
4, 4,
ImGuiTableFlags.RowBg ImGuiTableFlags.RowBg
@ -198,7 +203,8 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable | ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY | ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable | ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable)) | ImGuiTableFlags.Hideable);
if (table)
{ {
ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250); ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250);
@ -250,15 +256,14 @@ internal class PluginStatWindow : Window
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
} }
}
ImGui.EndTable(); }
} }
} }
ImGui.EndTabItem(); using (var tabItem = ImRaii.TabItem("Hooks"))
} {
if (tabItem)
if (ImGui.BeginTabItem("Hooks"))
{ {
ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks); ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks);
@ -268,7 +273,7 @@ internal class PluginStatWindow : Window
ref this.hookSearchText, ref this.hookSearchText,
500); 500);
if (ImGui.BeginTable( using var table = ImRaii.Table(
"##PluginStatsHooks", "##PluginStatsHooks",
4, 4,
ImGuiTableFlags.RowBg ImGuiTableFlags.RowBg
@ -276,7 +281,8 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable | ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY | ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable | ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable)) | ImGuiTableFlags.Hideable);
if (table)
{ {
ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250); ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250);
@ -345,11 +351,8 @@ internal class PluginStatWindow : Window
Log.Error(ex, "Error drawing hooks in plugin stats"); Log.Error(ex, "Error drawing hooks in plugin stats");
} }
} }
ImGui.EndTable();
} }
} }
}
ImGui.EndTabBar();
} }
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@ -54,6 +55,7 @@ internal class SelfTestWindow : Window
new SheetRedirectResolverSelfTestStep(), new SheetRedirectResolverSelfTestStep(),
new NounProcessorSelfTestStep(), new NounProcessorSelfTestStep(),
new SeStringEvaluatorSelfTestStep(), new SeStringEvaluatorSelfTestStep(),
new CompletionSelfTestStep(),
new LogoutEventSelfTestStep() new LogoutEventSelfTestStep()
]; ];
@ -61,7 +63,7 @@ internal class SelfTestWindow : Window
private bool selfTestRunning = false; private bool selfTestRunning = false;
private int currentStep = 0; private int currentStep = 0;
private int scrollToStep = -1;
private DateTimeOffset lastTestStart; private DateTimeOffset lastTestStart;
/// <summary> /// <summary>
@ -90,9 +92,10 @@ internal class SelfTestWindow : Window
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward)) if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
{ {
this.testIndexToResult.Add(this.currentStep, (SelfTestStepResult.NotRan, null)); this.testIndexToResult[this.currentStep] = (SelfTestStepResult.NotRan, null);
this.steps[this.currentStep].CleanUp(); this.steps[this.currentStep].CleanUp();
this.currentStep++; this.currentStep++;
this.scrollToStep = this.currentStep;
this.lastTestStart = DateTimeOffset.Now; this.lastTestStart = DateTimeOffset.Now;
if (this.currentStep >= this.steps.Count) if (this.currentStep >= this.steps.Count)
@ -107,6 +110,7 @@ internal class SelfTestWindow : Window
{ {
this.selfTestRunning = true; this.selfTestRunning = true;
this.currentStep = 0; this.currentStep = 0;
this.scrollToStep = this.currentStep;
this.testIndexToResult.Clear(); this.testIndexToResult.Clear();
this.lastTestStart = DateTimeOffset.Now; this.lastTestStart = DateTimeOffset.Now;
} }
@ -116,11 +120,11 @@ internal class SelfTestWindow : Window
ImGui.TextUnformatted($"Step: {this.currentStep} / {this.steps.Count}"); ImGui.TextUnformatted($"Step: {this.currentStep} / {this.steps.Count}");
ImGuiHelpers.ScaledDummy(10); ImGui.Spacing();
this.DrawResultTable(); this.DrawResultTable();
ImGuiHelpers.ScaledDummy(10); ImGui.Spacing();
if (this.currentStep >= this.steps.Count) if (this.currentStep >= this.steps.Count)
{ {
@ -131,11 +135,11 @@ internal class SelfTestWindow : Window
if (this.testIndexToResult.Any(x => x.Value.Result == SelfTestStepResult.Fail)) if (this.testIndexToResult.Any(x => x.Value.Result == SelfTestStepResult.Fail))
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, "One or more checks failed!"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "One or more checks failed!");
} }
else else
{ {
ImGui.TextColored(ImGuiColors.HealerGreen, "All checks passed!"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "All checks passed!");
} }
return; return;
@ -146,7 +150,8 @@ internal class SelfTestWindow : Window
return; return;
} }
ImGui.Separator(); using var resultChild = ImRaii.Child("SelfTestResultChild", ImGui.GetContentRegionAvail());
if (!resultChild) return;
var step = this.steps[this.currentStep]; var step = this.steps[this.currentStep];
ImGui.TextUnformatted($"Current: {step.Name}"); ImGui.TextUnformatted($"Current: {step.Name}");
@ -164,13 +169,12 @@ internal class SelfTestWindow : Window
result = SelfTestStepResult.Fail; result = SelfTestStepResult.Fail;
} }
ImGui.Separator();
if (result != SelfTestStepResult.Waiting) if (result != SelfTestStepResult.Waiting)
{ {
var duration = DateTimeOffset.Now - this.lastTestStart; var duration = DateTimeOffset.Now - this.lastTestStart;
this.testIndexToResult.Add(this.currentStep, (result, duration)); this.testIndexToResult[this.currentStep] = (result, duration);
this.currentStep++; this.currentStep++;
this.scrollToStep = this.currentStep;
this.lastTestStart = DateTimeOffset.Now; this.lastTestStart = DateTimeOffset.Now;
} }
@ -178,14 +182,24 @@ internal class SelfTestWindow : Window
private void DrawResultTable() private void DrawResultTable()
{ {
if (ImGui.BeginTable("agingResultTable", 5, ImGuiTableFlags.Borders)) var tableSize = ImGui.GetContentRegionAvail();
{
if (this.selfTestRunning)
tableSize -= new Vector2(0, 200);
tableSize.Y = Math.Min(tableSize.Y, ImGui.GetWindowViewport().Size.Y * 0.5f);
using var table = ImRaii.Table("agingResultTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, tableSize);
if (!table)
return;
ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Name"); ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
for (var i = 0; i < this.steps.Count; i++) for (var i = 0; i < this.steps.Count; i++)
@ -193,54 +207,68 @@ internal class SelfTestWindow : Window
var step = this.steps[i]; var step = this.steps[i];
ImGui.TableNextRow(); ImGui.TableNextRow();
if (this.selfTestRunning && this.currentStep == i)
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableRowBgAlt));
}
ImGui.TableSetColumnIndex(0); ImGui.TableSetColumnIndex(0);
ImGui.Text(i.ToString()); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(i.ToString());
if (this.selfTestRunning && this.scrollToStep == i)
{
ImGui.SetScrollHereY();
this.scrollToStep = -1;
}
ImGui.TableSetColumnIndex(1); ImGui.TableSetColumnIndex(1);
ImGui.Text(step.Name); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(step.Name);
if (this.testIndexToResult.TryGetValue(i, out var result)) if (this.testIndexToResult.TryGetValue(i, out var result))
{ {
ImGui.TableSetColumnIndex(2); ImGui.TableSetColumnIndex(2);
ImGui.PushFont(InterfaceManager.MonoFont); ImGui.AlignTextToFramePadding();
switch (result.Result) switch (result.Result)
{ {
case SelfTestStepResult.Pass: case SelfTestStepResult.Pass:
ImGui.TextColored(ImGuiColors.HealerGreen, "PASS"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "PASS");
break; break;
case SelfTestStepResult.Fail: case SelfTestStepResult.Fail:
ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "FAIL");
break; break;
default: default:
ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR");
break; break;
} }
ImGui.PopFont();
ImGui.TableSetColumnIndex(3); ImGui.TableSetColumnIndex(3);
if (result.Duration.HasValue) if (result.Duration.HasValue)
{ {
ImGui.TextUnformatted(result.Duration.Value.ToString("g")); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(this.FormatTimeSpan(result.Duration.Value));
} }
} }
else else
{ {
ImGui.TableSetColumnIndex(2); ImGui.TableSetColumnIndex(2);
ImGui.AlignTextToFramePadding();
if (this.selfTestRunning && this.currentStep == i) if (this.selfTestRunning && this.currentStep == i)
{ {
ImGui.TextColored(ImGuiColors.DalamudGrey, "WAIT"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "WAIT");
} }
else else
{ {
ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR");
} }
ImGui.TableSetColumnIndex(3); ImGui.TableSetColumnIndex(3);
ImGui.AlignTextToFramePadding();
if (this.selfTestRunning && this.currentStep == i) if (this.selfTestRunning && this.currentStep == i)
{ {
ImGui.TextUnformatted((DateTimeOffset.Now - this.lastTestStart).ToString("g")); ImGui.TextUnformatted(this.FormatTimeSpan(DateTimeOffset.Now - this.lastTestStart));
} }
} }
@ -260,9 +288,6 @@ internal class SelfTestWindow : Window
ImGui.SetTooltip("Jump to this test"); ImGui.SetTooltip("Jump to this test");
} }
} }
ImGui.EndTable();
}
} }
private void StopTests() private void StopTests()
@ -281,4 +306,11 @@ internal class SelfTestWindow : Window
} }
} }
} }
private string FormatTimeSpan(TimeSpan ts)
{
var str = ts.ToString("g", CultureInfo.InvariantCulture);
var commaPos = str.LastIndexOf('.');
return commaPos > -1 && commaPos + 5 < str.Length ? str[..(commaPos + 5)] : str;
}
} }

View file

@ -0,0 +1,94 @@
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
/// <summary>
/// Test setup for Chat.
/// </summary>
internal class CompletionSelfTestStep : ISelfTestStep
{
private int step = 0;
private bool registered;
private bool commandRun;
/// <inheritdoc/>
public string Name => "Test Completion";
/// <inheritdoc/>
public SelfTestStepResult RunStep()
{
var cmdManager = Service<CommandManager>.Get();
switch (this.step)
{
case 0:
this.step++;
break;
case 1:
ImGui.Text("[Chat Log]");
ImGui.TextWrapped("Use the category menus to navigate to [Dalamud], then complete a command from the list. Did it work?");
if (ImGui.Button("Yes"))
this.step++;
ImGui.SameLine();
if (ImGui.Button("No"))
return SelfTestStepResult.Fail;
break;
case 2:
ImGui.Text("[Chat Log]");
ImGui.Text("Type /xl into the chat log and tab-complete a dalamud command. Did it work?");
if (ImGui.Button("Yes"))
this.step++;
ImGui.SameLine();
if (ImGui.Button("No"))
return SelfTestStepResult.Fail;
break;
case 3:
ImGui.Text("[Chat Log]");
if (!this.registered)
{
cmdManager.AddHandler("/xlselftestcompletion", new CommandInfo((_, _) => this.commandRun = true));
this.registered = true;
}
ImGui.Text("Tab-complete /xlselftestcompletion in the chat log and send the command");
if (this.commandRun)
this.step++;
break;
case 4:
ImGui.Text("[Other text inputs]");
ImGui.Text("Open the party finder recruitment criteria dialog and try to tab-complete /xldev in the text box.");
ImGui.Text("Did the command appear in the text box? (It should not have)");
if (ImGui.Button("Yes"))
return SelfTestStepResult.Fail;
ImGui.SameLine();
if (ImGui.Button("No"))
this.step++;
break;
case 5:
return SelfTestStepResult.Pass;
}
return SelfTestStepResult.Waiting;
}
/// <inheritdoc/>
public void CleanUp()
{
Service<CommandManager>.Get().RemoveHandler("/completionselftest");
}
}

View file

@ -145,7 +145,7 @@ internal class ContextMenuSelfTestStep : ISelfTestStep
var targetItem = (a.Target as MenuTargetInventory)!.TargetItem; var targetItem = (a.Target as MenuTargetInventory)!.TargetItem;
if (targetItem is { } item) if (targetItem is { } item)
{ {
name = (this.itemSheet.GetRowOrDefault(item.ItemId)?.Name.ExtractText() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty); name = (this.itemSheet.GetRowOrDefault(item.BaseItemId)?.Name.ExtractText() ?? $"Unknown ({item.BaseItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
count = item.Quantity; count = item.Quantity;
} }
else else

View file

@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
/// Test setup for Lumina. /// Test setup for Lumina.
/// </summary> /// </summary>
/// <typeparam name="T">ExcelRow to run test on.</typeparam> /// <typeparam name="T">ExcelRow to run test on.</typeparam>
/// <param name="isLargeSheet">Whether or not the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken.</param> /// <param name="isLargeSheet">Whether the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken.</param>
internal class LuminaSelfTestStep<T>(bool isLargeSheet) : ISelfTestStep internal class LuminaSelfTestStep<T>(bool isLargeSheet) : ISelfTestStep
where T : struct, IExcelRow<T> where T : struct, IExcelRow<T>
{ {

View file

@ -1,4 +1,4 @@
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
@ -109,7 +109,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
} }
else else
{ {
ImGui.Text("Does this information match the purchase you made? This is testing the request to the server."); ImGui.TextWrapped("Does this information match the purchase you made? This is testing the request to the server.");
ImGui.Separator(); ImGui.Separator();
ImGui.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}"); ImGui.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}");
ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}"); ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}");
@ -135,7 +135,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
} }
else else
{ {
ImGui.Text("Does this information match the purchase you made? This is testing the response from the server."); ImGui.TextWrapped("Does this information match the purchase you made? This is testing the response from the server.");
ImGui.Separator(); ImGui.Separator();
ImGui.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}"); ImGui.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}");
ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}"); ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}");
@ -156,7 +156,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
case SubStep.Taxes: case SubStep.Taxes:
if (this.marketTaxRate == null) if (this.marketTaxRate == null)
{ {
ImGui.Text("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item."); ImGui.TextWrapped("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item.");
} }
else else
{ {

View file

@ -61,6 +61,10 @@ internal class SheetRedirectResolverSelfTestStep : ISelfTestStep
new("WeatherPlaceName", 40), new("WeatherPlaceName", 40),
new("WeatherPlaceName", 52), new("WeatherPlaceName", 52),
new("WeatherPlaceName", 2300), new("WeatherPlaceName", 2300),
new("InstanceContent", 1),
new("PartyContent", 2),
new("PublicContent", 1),
new("AkatsukiNote", 1),
]; ];
private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags); private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags);

View file

@ -11,12 +11,12 @@ public abstract class SettingsEntry
public string? Name { get; protected set; } public string? Name { get; protected set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not this entry is valid. /// Gets or sets a value indicating whether this entry is valid.
/// </summary> /// </summary>
public virtual bool IsValid { get; protected set; } = true; public virtual bool IsValid { get; protected set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not this entry is visible. /// Gets or sets a value indicating whether this entry is visible.
/// </summary> /// </summary>
public virtual bool IsVisible { get; protected set; } = true; public virtual bool IsVisible { get; protected set; } = true;

View file

@ -21,6 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
public class SettingsTabAutoUpdates : SettingsTab public class SettingsTabAutoUpdates : SettingsTab
{ {
private AutoUpdateBehavior behavior; private AutoUpdateBehavior behavior;
private bool updateDisabledPlugins;
private bool checkPeriodically; private bool checkPeriodically;
private bool chatNotification; private bool chatNotification;
private string pickerSearch = string.Empty; private string pickerSearch = string.Empty;
@ -65,6 +66,7 @@ public class SettingsTabAutoUpdates : SettingsTab
ImGuiHelpers.ScaledDummy(8); ImGuiHelpers.ScaledDummy(8);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateDisabledPlugins", "Auto-Update plugins that are currently disabled"), ref this.updateDisabledPlugins);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification);
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint", ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint",
@ -236,6 +238,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None; this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.updateDisabledPlugins = configuration.UpdateDisabledPlugins;
this.chatNotification = configuration.SendUpdateNotificationToChat; this.chatNotification = configuration.SendUpdateNotificationToChat;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates; this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences; this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
@ -248,6 +251,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior; configuration.AutoUpdateBehavior = this.behavior;
configuration.UpdateDisabledPlugins = this.updateDisabledPlugins;
configuration.SendUpdateNotificationToChat = this.chatNotification; configuration.SendUpdateNotificationToChat = this.chatNotification;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically; configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences; configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;

View file

@ -60,7 +60,8 @@ public class SettingsTabExperimental : SettingsTab
"Enable ImGui asserts"), "Enable ImGui asserts"),
Loc.Localize( Loc.Localize(
"DalamudSettingEnableImGuiAssertsHint", "DalamudSettingEnableImGuiAssertsHint",
"If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins."), "If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins. " +
"This setting does not persist and will reset when the game restarts.\nUse the setting below to enable it at startup."),
c => Service<InterfaceManager>.Get().ShowAsserts, c => Service<InterfaceManager>.Get().ShowAsserts,
(v, _) => Service<InterfaceManager>.Get().ShowAsserts = v), (v, _) => Service<InterfaceManager>.Get().ShowAsserts = v),

View file

@ -12,6 +12,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.ManagedFontAtlas.Internals;
@ -38,7 +39,7 @@ public class SettingsTabLook : SettingsTab
private IFontSpec defaultFontSpec = null!; private IFontSpec defaultFontSpec = null!;
public override SettingsEntry[] Entries { get; } = public override SettingsEntry[] Entries { get; } =
{ [
new GapSettingsEntry(5, true), new GapSettingsEntry(5, true),
new ButtonSettingsEntry( new ButtonSettingsEntry(
@ -46,6 +47,11 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."), Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."),
() => Service<DalamudInterface>.Get().OpenStyleEditor()), () => Service<DalamudInterface>.Get().OpenStyleEditor()),
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsOpenNotificationEditor", "Modify Notification Position"),
Loc.Localize("DalamudSettingsNotificationEditorHint", "Choose where Dalamud notifications appear on the screen."),
() => Service<NotificationManager>.Get().StartPositionChooser()),
new SettingsEntry<bool>( new SettingsEntry<bool>(
Loc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"), Loc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"),
Loc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."), Loc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."),
@ -167,8 +173,8 @@ public class SettingsTabLook : SettingsTab
ImGui.TextUnformatted("\uE020\uE021\uE022\uE023\uE024\uE025\uE026\uE027"); ImGui.TextUnformatted("\uE020\uE021\uE022\uE023\uE024\uE025\uE026\uE027");
ImGui.PopStyleVar(1); ImGui.PopStyleVar(1);
}, },
}, }
}; ];
public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel");

View file

@ -51,7 +51,7 @@ public class DevPluginsSettingsEntry : SettingsEntry
if (this.devPluginLocationsChanged) if (this.devPluginLocationsChanged)
{ {
Service<PluginManager>.Get().ScanDevPlugins(); _ = Service<PluginManager>.Get().ScanDevPluginsAsync();
this.devPluginLocationsChanged = false; this.devPluginLocationsChanged = false;
} }
} }

View file

@ -148,6 +148,10 @@ internal class TitleScreenMenuWindow : Window, IDisposable
{ {
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
if (this.state == State.Show)
ImGui.SetNextWindowFocus();
base.PreDraw(); base.PreDraw();
} }

View file

@ -101,8 +101,8 @@ public class StyleModelV1 : StyleModel
{ "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1) }, { "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1) },
{ "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1) }, { "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1) },
{ "PlotLinesHovered", new Vector4(1, 0.43f, 0.35f, 1) }, { "PlotLinesHovered", new Vector4(1, 0.43f, 0.35f, 1) },
{ "PlotHistogram", new Vector4(0.9f, 0.7f, 0, 1) }, { "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) },
{ "PlotHistogramHovered", new Vector4(1, 0.6f, 0, 1) }, { "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) },
{ "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1) }, { "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1) },
{ "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1) }, { "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1) },
{ "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1) }, { "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1) },
@ -220,8 +220,8 @@ public class StyleModelV1 : StyleModel
{ "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1f) }, { "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1f) },
{ "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1f) }, { "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1f) },
{ "PlotLinesHovered", new Vector4(1f, 0.43f, 0.35f, 1f) }, { "PlotLinesHovered", new Vector4(1f, 0.43f, 0.35f, 1f) },
{ "PlotHistogram", new Vector4(0.9f, 0.7f, 0f, 1f) }, { "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) },
{ "PlotHistogramHovered", new Vector4(1f, 0.6f, 0f, 1f) }, { "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) },
{ "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1f) }, { "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1f) },
{ "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1f) }, { "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1f) },
{ "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1f) }, { "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1f) },

View file

@ -0,0 +1,498 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Memory;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Textures.Internal;
/// <summary>Service responsible for loading and disposing ImGui texture wraps.</summary>
internal sealed partial class TextureManager
{
/// <inheritdoc/>
public async Task CopyToClipboardAsync(
IDalamudTextureWrap wrap,
string? preferredFileNameWithoutExtension = null,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
using var wrapAux = new WrapAux(wrap, leaveWrapOpen);
bool hasAlphaChannel;
switch (wrapAux.Desc.Format)
{
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
hasAlphaChannel = false;
break;
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM:
case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
hasAlphaChannel = true;
break;
default:
await this.CopyToClipboardAsync(
await this.CreateFromExistingTextureAsync(
wrap,
new() { Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM },
cancellationToken: cancellationToken),
preferredFileNameWithoutExtension,
false,
cancellationToken);
return;
}
// https://stackoverflow.com/questions/15689541/win32-clipboard-and-alpha-channel-images
// https://learn.microsoft.com/en-us/windows/win32/shell/clipboard
using var pdo = default(ComPtr<IDataObject>);
unsafe
{
fixed (Guid* piid = &IID.IID_IDataObject)
SHCreateDataObject(null, 1, null, null, piid, (void**)pdo.GetAddressOf()).ThrowOnError();
}
var ms = new MemoryStream();
{
ms.SetLength(ms.Position = 0);
await this.SaveToStreamAsync(
wrap,
GUID.GUID_ContainerFormatPng,
ms,
new Dictionary<string, object> { ["InterlaceOption"] = true },
true,
true,
cancellationToken);
unsafe
{
using var ims = default(ComPtr<IStream>);
fixed (byte* p = ms.GetBuffer())
ims.Attach(SHCreateMemStream(p, (uint)ms.Length));
if (ims.IsEmpty())
throw new OutOfMemoryException();
AddToDataObject(
pdo,
ClipboardFormats.Png,
new()
{
tymed = (uint)TYMED.TYMED_ISTREAM,
pstm = ims.Get(),
});
AddToDataObject(
pdo,
ClipboardFormats.FileContents,
new()
{
tymed = (uint)TYMED.TYMED_ISTREAM,
pstm = ims.Get(),
});
ims.Get()->AddRef();
ims.Detach();
}
if (preferredFileNameWithoutExtension is not null)
{
unsafe
{
preferredFileNameWithoutExtension += ".png";
if (preferredFileNameWithoutExtension.Length >= 260)
preferredFileNameWithoutExtension = preferredFileNameWithoutExtension[..^4] + ".png";
var namea = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8)
.GetBytes(preferredFileNameWithoutExtension);
if (namea.Length > 260)
{
namea.AsSpan()[^4..].CopyTo(namea.AsSpan(256, 4));
Array.Resize(ref namea, 260);
}
var fgda = new FILEGROUPDESCRIPTORA
{
cItems = 1,
fgd = new()
{
e0 = new()
{
dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE),
nFileSizeHigh = (uint)(ms.Length >> 32),
nFileSizeLow = (uint)ms.Length,
},
},
};
namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260));
AddToDataObject(
pdo,
ClipboardFormats.FileDescriptorA,
new()
{
tymed = (uint)TYMED.TYMED_HGLOBAL,
hGlobal = CreateHGlobalFromMemory<FILEGROUPDESCRIPTORA>(new(ref fgda)),
});
var fgdw = new FILEGROUPDESCRIPTORW
{
cItems = 1,
fgd = new()
{
e0 = new()
{
dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE),
nFileSizeHigh = (uint)(ms.Length >> 32),
nFileSizeLow = (uint)ms.Length,
},
},
};
preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260));
AddToDataObject(
pdo,
ClipboardFormats.FileDescriptorW,
new()
{
tymed = (uint)TYMED.TYMED_HGLOBAL,
hGlobal = CreateHGlobalFromMemory<FILEGROUPDESCRIPTORW>(new(ref fgdw)),
});
}
}
}
{
ms.SetLength(ms.Position = 0);
await this.SaveToStreamAsync(
wrap,
GUID.GUID_ContainerFormatBmp,
ms,
new Dictionary<string, object> { ["EnableV5Header32bppBGRA"] = false },
true,
true,
cancellationToken);
AddToDataObject(
pdo,
CF.CF_DIB,
new()
{
tymed = (uint)TYMED.TYMED_HGLOBAL,
hGlobal = CreateHGlobalFromMemory<byte>(
ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf<BITMAPFILEHEADER>()..]),
});
}
if (hasAlphaChannel)
{
ms.SetLength(ms.Position = 0);
await this.SaveToStreamAsync(
wrap,
GUID.GUID_ContainerFormatBmp,
ms,
new Dictionary<string, object> { ["EnableV5Header32bppBGRA"] = true },
true,
true,
cancellationToken);
AddToDataObject(
pdo,
CF.CF_DIBV5,
new()
{
tymed = (uint)TYMED.TYMED_HGLOBAL,
hGlobal = CreateHGlobalFromMemory<byte>(
ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf<BITMAPFILEHEADER>()..]),
});
}
var omts = await Service<StaThreadService>.GetAsync();
await omts.Run(() => StaThreadService.OleSetClipboard(pdo), cancellationToken);
return;
static unsafe void AddToDataObject(ComPtr<IDataObject> pdo, uint clipboardFormat, STGMEDIUM stg)
{
var fec = new FORMATETC
{
cfFormat = (ushort)clipboardFormat,
ptd = null,
dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
lindex = 0,
tymed = stg.tymed,
};
pdo.Get()->SetData(&fec, &stg, true).ThrowOnError();
}
static unsafe HGLOBAL CreateHGlobalFromMemory<T>(ReadOnlySpan<T> data) where T : unmanaged
{
var h = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)(data.Length * sizeof(T)));
if (h == 0)
throw new OutOfMemoryException("Failed to allocate.");
var p = GlobalLock(h);
data.CopyTo(new(p, data.Length));
GlobalUnlock(h);
return h;
}
}
/// <inheritdoc/>
public bool HasClipboardImage()
{
var acf = Service<StaThreadService>.Get().AvailableClipboardFormats;
return acf.Contains(CF.CF_DIBV5)
|| acf.Contains(CF.CF_DIB)
|| acf.Contains(ClipboardFormats.Png)
|| acf.Contains(ClipboardFormats.FileContents);
}
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromClipboardAsync(
string? debugName = null,
CancellationToken cancellationToken = default)
{
var omts = await Service<StaThreadService>.GetAsync();
var (stgm, clipboardFormat) = await omts.Run(GetSupportedClipboardData, cancellationToken);
try
{
return this.BlameSetName(
await this.DynamicPriorityTextureLoader.LoadAsync(
null,
ct =>
clipboardFormat is CF.CF_DIB or CF.CF_DIBV5
? CreateTextureFromStorageMediumDib(this, stgm, ct)
: CreateTextureFromStorageMedium(this, stgm, ct),
cancellationToken),
debugName ?? $"{nameof(this.CreateFromClipboardAsync)}({(TYMED)stgm.tymed})");
}
finally
{
StaThreadService.ReleaseStgMedium(ref stgm);
}
// Converts a CF_DIB/V5 format to a full BMP format, for WIC consumption.
static unsafe Task<IDalamudTextureWrap> CreateTextureFromStorageMediumDib(
TextureManager textureManager,
scoped in STGMEDIUM stgm,
CancellationToken ct)
{
var ms = new MemoryStream();
switch ((TYMED)stgm.tymed)
{
case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default:
{
var pMem = GlobalLock(stgm.hGlobal);
if (pMem is null)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
try
{
var size = (int)GlobalSize(stgm.hGlobal);
ms.SetLength(sizeof(BITMAPFILEHEADER) + size);
new ReadOnlySpan<byte>(pMem, size).CopyTo(ms.GetBuffer().AsSpan(sizeof(BITMAPFILEHEADER)));
}
finally
{
GlobalUnlock(stgm.hGlobal);
}
break;
}
case TYMED.TYMED_ISTREAM when stgm.pstm is not null:
{
STATSTG stat;
if (stgm.pstm->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME).SUCCEEDED && stat.cbSize.QuadPart > 0)
ms.SetLength(sizeof(BITMAPFILEHEADER) + (int)stat.cbSize.QuadPart);
else
ms.SetLength(8192);
var offset = (uint)sizeof(BITMAPFILEHEADER);
for (var read = 1u; read != 0;)
{
if (offset == ms.Length)
ms.SetLength(ms.Length * 2);
fixed (byte* pMem = ms.GetBuffer().AsSpan((int)offset))
{
stgm.pstm->Read(pMem, (uint)(ms.Length - offset), &read).ThrowOnError();
offset += read;
}
}
ms.SetLength(offset);
break;
}
default:
return Task.FromException<IDalamudTextureWrap>(new NotSupportedException());
}
ref var bfh = ref Unsafe.As<byte, BITMAPFILEHEADER>(ref ms.GetBuffer()[0]);
bfh.bfType = 0x4D42;
bfh.bfSize = (uint)ms.Length;
ref var bih = ref Unsafe.As<byte, BITMAPINFOHEADER>(ref ms.GetBuffer()[sizeof(BITMAPFILEHEADER)]);
bfh.bfOffBits = (uint)(sizeof(BITMAPFILEHEADER) + bih.biSize);
if (bih.biSize >= sizeof(BITMAPINFOHEADER))
{
if (bih.biBitCount > 8)
{
if (bih.biCompression == BI.BI_BITFIELDS)
bfh.bfOffBits += (uint)(3 * sizeof(RGBQUAD));
else if (bih.biCompression == 6 /* BI_ALPHABITFIELDS */)
bfh.bfOffBits += (uint)(4 * sizeof(RGBQUAD));
}
}
if (bih.biClrUsed > 0)
bfh.bfOffBits += (uint)(bih.biClrUsed * sizeof(RGBQUAD));
else if (bih.biBitCount <= 8)
bfh.bfOffBits += (uint)(sizeof(RGBQUAD) << bih.biBitCount);
using var pinned = ms.GetBuffer().AsMemory().Pin();
using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pinned, (int)ms.Length);
return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
}
// Interprets a data as an image file using WIC.
static unsafe Task<IDalamudTextureWrap> CreateTextureFromStorageMedium(
TextureManager textureManager,
scoped in STGMEDIUM stgm,
CancellationToken ct)
{
switch ((TYMED)stgm.tymed)
{
case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default:
{
var pMem = GlobalLock(stgm.hGlobal);
if (pMem is null)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
try
{
var size = (int)GlobalSize(stgm.hGlobal);
using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pMem, size);
return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
}
finally
{
GlobalUnlock(stgm.hGlobal);
}
}
case TYMED.TYMED_FILE when stgm.lpszFileName is not null:
{
var fileName = MemoryHelper.ReadString((nint)stgm.lpszFileName, Encoding.Unicode, short.MaxValue);
return textureManager.NoThrottleCreateFromFileAsync(fileName, ct);
}
case TYMED.TYMED_ISTREAM when stgm.pstm is not null:
{
using var strm = new ComPtr<IStream>(stgm.pstm);
return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct));
}
default:
return Task.FromException<IDalamudTextureWrap>(new NotSupportedException());
}
}
static unsafe bool TryGetClipboardDataAs(
ComPtr<IDataObject> pdo,
uint clipboardFormat,
uint tymed,
out STGMEDIUM stgm)
{
var fec = new FORMATETC
{
cfFormat = (ushort)clipboardFormat,
ptd = null,
dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
lindex = -1,
tymed = tymed,
};
fixed (STGMEDIUM* pstgm = &stgm)
return pdo.Get()->GetData(&fec, pstgm).SUCCEEDED;
}
// Takes a data from clipboard for use with WIC.
static unsafe (STGMEDIUM Stgm, uint ClipboardFormat) GetSupportedClipboardData()
{
using var pdo = StaThreadService.OleGetClipboard();
const uint tymeds = (uint)TYMED.TYMED_HGLOBAL |
(uint)TYMED.TYMED_FILE |
(uint)TYMED.TYMED_ISTREAM;
const uint sharedRead = STGM.STGM_READ | STGM.STGM_SHARE_DENY_WRITE;
// Try taking data from clipboard as-is.
if (TryGetClipboardDataAs(pdo, CF.CF_DIBV5, tymeds, out var stgm))
return (stgm, CF.CF_DIBV5);
if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileContents, tymeds, out stgm))
return (stgm, ClipboardFormats.FileContents);
if (TryGetClipboardDataAs(pdo, ClipboardFormats.Png, tymeds, out stgm))
return (stgm, ClipboardFormats.Png);
if (TryGetClipboardDataAs(pdo, CF.CF_DIB, tymeds, out stgm))
return (stgm, CF.CF_DIB);
// Try reading file from the path stored in clipboard.
if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameW, (uint)TYMED.TYMED_HGLOBAL, out stgm))
{
var pPath = GlobalLock(stgm.hGlobal);
try
{
IStream* pfs;
SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError();
var stgm2 = new STGMEDIUM
{
tymed = (uint)TYMED.TYMED_ISTREAM,
pstm = pfs,
pUnkForRelease = (IUnknown*)pfs,
};
return (stgm2, ClipboardFormats.FileContents);
}
finally
{
if (pPath is not null)
GlobalUnlock(stgm.hGlobal);
StaThreadService.ReleaseStgMedium(ref stgm);
}
}
if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameA, (uint)TYMED.TYMED_HGLOBAL, out stgm))
{
var pPath = GlobalLock(stgm.hGlobal);
try
{
IStream* pfs;
SHCreateStreamOnFileA((sbyte*)pPath, sharedRead, &pfs).ThrowOnError();
var stgm2 = new STGMEDIUM
{
tymed = (uint)TYMED.TYMED_ISTREAM,
pstm = pfs,
pUnkForRelease = (IUnknown*)pfs,
};
return (stgm2, ClipboardFormats.FileContents);
}
finally
{
if (pPath is not null)
GlobalUnlock(stgm.hGlobal);
StaThreadService.ReleaseStgMedium(ref stgm);
}
}
throw new InvalidOperationException("No compatible clipboard format found.");
}
}
}

View file

@ -6,7 +6,6 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -173,7 +172,7 @@ internal sealed partial class TextureManager
ReadOnlyMemory<byte> bytes, ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
try try
@ -204,7 +203,7 @@ internal sealed partial class TextureManager
string path, string path,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
try try
@ -359,11 +358,18 @@ internal sealed partial class TextureManager
/// <param name="handle">An instance of <see cref="MemoryHandle"/>.</param> /// <param name="handle">An instance of <see cref="MemoryHandle"/>.</param>
/// <param name="length">The number of bytes in the memory.</param> /// <param name="length">The number of bytes in the memory.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns> /// <returns>The new instance of <see cref="IStream"/>.</returns>
public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(MemoryHandle handle, int length) public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(MemoryHandle handle, int length) =>
this.CreateIStreamViewOfMemory((byte*)handle.Pointer, length);
/// <summary>Creates a new instance of <see cref="IStream"/> from a fixed memory allocation.</summary>
/// <param name="address">Address of the data.</param>
/// <param name="length">The number of bytes in the memory.</param>
/// <returns>The new instance of <see cref="IStream"/>.</returns>
public unsafe ComPtr<IStream> CreateIStreamViewOfMemory(void* address, int length)
{ {
using var wicStream = default(ComPtr<IWICStream>); using var wicStream = default(ComPtr<IWICStream>);
this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError();
wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError(); wicStream.Get()->InitializeFromMemory((byte*)address, checked((uint)length)).ThrowOnError();
var res = default(ComPtr<IStream>); var res = default(ComPtr<IStream>);
wicStream.As(ref res).ThrowOnError(); wicStream.As(ref res).ThrowOnError();

View file

@ -11,7 +11,9 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Interface.Textures.TextureWraps.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom; using Dalamud.Utility.TerraFxCom;
@ -48,10 +50,11 @@ internal sealed partial class TextureManager
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get(); private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly CancellationTokenSource disposeCts = new();
private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader; private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader;
private SharedTextureManager? sharedTextureManager; private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager; private WicManager? wicManager;
private bool disposing;
private ComPtr<ID3D11Device> device; private ComPtr<ID3D11Device> device;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
@ -104,10 +107,10 @@ internal sealed partial class TextureManager
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
if (this.disposing) if (this.disposeCts.IsCancellationRequested)
return; return;
this.disposing = true; this.disposeCts.Cancel();
Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose(); Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose();
Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose();
@ -269,6 +272,21 @@ internal sealed partial class TextureManager
return wrap; return wrap;
} }
/// <inheritdoc/>
public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => this.CreateDrawListTexture(null, debugName);
/// <summary><inheritdoc cref="CreateDrawListTexture(string?)" path="/summary"/></summary>
/// <param name="plugin">Plugin that created the draw list.</param>
/// <param name="debugName"><inheritdoc cref="CreateDrawListTexture(string?)" path="/param[name=debugName]"/></param>
/// <returns><inheritdoc cref="CreateDrawListTexture(string?)" path="/returns"/></returns>
public IDrawListTextureWrap CreateDrawListTexture(LocalPlugin? plugin, string? debugName = null) =>
new DrawListTextureWrap(
new(this.device),
this,
Service<DalamudAssetManager>.Get().Empty4X4,
plugin,
debugName ?? $"{nameof(this.CreateDrawListTexture)}");
/// <inheritdoc/> /// <inheritdoc/>
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
@ -330,7 +348,7 @@ internal sealed partial class TextureManager
/// <returns>The loaded texture.</returns> /// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
var buffer = file.TextureBuffer; var buffer = file.TextureBuffer;
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false);
@ -354,7 +372,7 @@ internal sealed partial class TextureManager
/// <returns>The loaded texture.</returns> /// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes) internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes)
{ {
ObjectDisposedException.ThrowIf(this.disposing, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes))
throw new InvalidDataException("The file is not a TexFile."); throw new InvalidDataException("The file is not a TexFile.");

View file

@ -151,6 +151,10 @@ internal sealed class TextureManagerPluginScoped
return textureWrap; return textureWrap;
} }
/// <inheritdoc/>
public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) =>
this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName);
/// <inheritdoc/> /// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromExistingTextureAsync( public async Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap, IDalamudTextureWrap wrap,
@ -267,6 +271,17 @@ internal sealed class TextureManagerPluginScoped
return textureWrap; return textureWrap;
} }
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromClipboardAsync(
string? debugName = null,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
var textureWrap = await manager.CreateFromClipboardAsync(debugName, cancellationToken);
manager.Blame(textureWrap, this.plugin);
return textureWrap;
}
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() => public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
@ -279,6 +294,9 @@ internal sealed class TextureManagerPluginScoped
return shared; return shared;
} }
/// <inheritdoc/>
public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage();
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture) public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture)
{ {
@ -411,6 +429,17 @@ internal sealed class TextureManagerPluginScoped
cancellationToken); cancellationToken);
} }
/// <inheritdoc/>
public async Task CopyToClipboardAsync(
IDalamudTextureWrap wrap,
string? preferredFileNameWithoutExtension = null,
bool leaveWrapOpen = false,
CancellationToken cancellationToken = default)
{
var manager = await this.ManagerTask;
await manager.CopyToClipboardAsync(wrap, preferredFileNameWithoutExtension, leaveWrapOpen, cancellationToken);
}
private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) => private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) =>
this.InterceptTexDataLoad?.Invoke(path, ref replacementPath); this.InterceptTexDataLoad?.Invoke(path, ref replacementPath);
} }

Some files were not shown because too many files have changed in this diff Show more