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) {
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 },
{ "prevent_icmphandle_crashes", &prevent_icmphandle_crashes },
{ "symbol_load_patches", &symbol_load_patches },
{ "disable_game_debugging_protection", &disable_game_debugging_protection },
}
) {
try {

View file

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

View file

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

View file

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

View file

@ -3,12 +3,14 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
@ -67,12 +69,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public List<string>? BadWords { get; set; }
/// <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>
public bool DutyFinderTaskbarFlash { get; set; } = true;
/// <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>
public bool DutyFinderChatMessage { get; set; } = true;
@ -102,7 +104,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public XivChatType GeneralChatType { get; set; } = XivChatType.Debug;
/// <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>
public bool DoPluginTest { get; set; } = false;
@ -117,7 +119,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
/// <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>
public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null;
@ -175,38 +177,38 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public float ImeStateIndicatorOpacity { get; set; } = 1f;
/// <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>
public bool ToggleUiHide { get; set; } = true;
/// <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>
public bool ToggleUiHideDuringCutscenes { get; set; } = true;
/// <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>
public bool ToggleUiHideDuringGpose { get; set; } = true;
/// <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>
public bool PrintDalamudWelcomeMsg { get; set; } = true;
/// <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>
public bool PrintPluginsWelcomeMsg { get; set; } = true;
/// <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>
[Obsolete("Use AutoUpdateBehavior instead.")]
public bool AutoUpdatePlugins { get; set; }
/// <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>
public bool DoButtonsSystemMenu { get; set; } = true;
@ -221,12 +223,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public bool LogSynchronously { get; set; } = false;
/// <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>
public bool LogAutoScroll { get; set; } = true;
/// <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>
public bool LogOpenAtStartup { get; set; }
@ -241,29 +243,29 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public List<string> LogCommandHistory { get; set; } = new();
/// <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>
public bool DevBarOpenAtStartup { get; set; }
/// <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>
public bool? ImGuiAssertsEnabledAtStartup { get; set; }
/// <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>
public bool IsDocking { get; set; }
/// <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.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; }
/// <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.
/// </summary>
public bool EnablePluginUiAdditionalOptions { get; set; } = true;
@ -274,20 +276,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public bool IsDisableViewport { get; set; } = true;
/// <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>
public bool IsGamepadNavigationEnabled { get; set; } = true;
/// <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>
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>
/// Gets or sets a value indicating whether to resume game main thread after plugins load.
/// </summary>
@ -299,7 +296,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public string? DalamudBetaKind { get; set; }
/// <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.
/// </summary>
public bool PluginSafeMode { get; set; }
@ -311,7 +308,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public int? PluginWaitBeforeFree { get; set; }
/// <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>
public bool ReportShutdownCrashes { get; set; }
@ -343,12 +340,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public ProfileModel? DefaultProfile { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not profiles are enabled.
/// Gets or sets a value indicating whether profiles are enabled.
/// </summary>
public bool ProfilesEnabled { get; set; } = false;
/// <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>
public bool ProfilesHasSeenTutorial { get; set; } = false;
@ -392,7 +389,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public bool? ReduceMotions { get; set; }
/// <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>
public bool IsMbCollect { get; set; } = true;
@ -428,7 +425,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
}
/// <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>
public bool ShowDevBarInfo { get; set; } = true;
@ -502,6 +499,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
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>
/// Load a configuration from the provided path.
/// </summary>
@ -562,6 +569,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public void ForceSave()
{
this.Save();
this.isSaveQueued = false;
this.writeTask?.GetAwaiter().GetResult();
}
/// <inheritdoc/>

View file

@ -12,16 +12,22 @@ internal sealed class DevPluginSettings
/// </summary>
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>
/// Gets or sets a value indicating whether this plugin should automatically reload on file change.
/// </summary>
public bool AutomaticReloading { get; set; } = false;
/// <summary>
/// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary>
public Guid WorkingPluginId { get; set; } = Guid.Empty;
/// <summary>
/// Gets or sets a list of validation problems that have been dismissed by the user.
/// </summary>

View file

@ -21,7 +21,7 @@ internal class EnvironmentConfiguration
public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK");
/// <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>
public static bool DalamudDoContextMenu { get; } = GetEnvironmentVariable("DALAMUD_ENABLE_CONTEXTMENU");

View file

@ -11,13 +11,13 @@ public interface IConsoleEntry
/// Gets the name of the entry.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the description of the entry.
/// </summary>
public string Description { get; }
}
/// <summary>
/// Interface representing a command in the console.
/// </summary>
@ -27,7 +27,7 @@ public interface IConsoleCommand : IConsoleEntry
/// Execute this command.
/// </summary>
/// <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);
}

View file

@ -18,9 +18,9 @@ namespace Dalamud.Console;
internal partial class ConsoleManager : IServiceType
{
private static readonly ModuleLog Log = new("CON");
private Dictionary<string, IConsoleEntry> entries = new();
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleManager"/> class.
/// </summary>
@ -29,17 +29,17 @@ internal partial class ConsoleManager : IServiceType
{
this.AddCommand("toggle", "Toggle a boolean variable.", this.OnToggleVariable);
}
/// <summary>
/// Event that is triggered when a command is processed. Return true to stop the command from being processed any further.
/// </summary>
public event Func<string, bool>? Invoke;
/// <summary>
/// Gets a read-only dictionary of console entries.
/// </summary>
public IReadOnlyDictionary<string, IConsoleEntry> Entries => this.entries;
/// <summary>
/// Add a command to the console.
/// </summary>
@ -53,13 +53,13 @@ internal partial class ConsoleManager : IServiceType
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(description);
ArgumentNullException.ThrowIfNull(func);
if (this.FindEntry(name) != null)
throw new InvalidOperationException($"Entry '{name}' already exists.");
var command = new ConsoleCommand(name, description, func);
this.entries.Add(name, command);
return command;
}
@ -77,14 +77,14 @@ internal partial class ConsoleManager : IServiceType
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(description);
Traits.ThrowIfTIsNullableAndNull(defaultValue);
if (this.FindEntry(name) != null)
throw new InvalidOperationException($"Entry '{name}' already exists.");
var variable = new ConsoleVariable<T>(name, description);
variable.Value = defaultValue;
this.entries.Add(name, variable);
return variable;
}
@ -98,11 +98,11 @@ internal partial class ConsoleManager : IServiceType
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(alias);
var target = this.FindEntry(name);
if (target == null)
throw new EntryNotFoundException(name);
if (this.FindEntry(alias) != null)
throw new InvalidOperationException($"Entry '{alias}' already exists.");
@ -135,21 +135,21 @@ internal partial class ConsoleManager : IServiceType
public T GetVariable<T>(string name)
{
ArgumentNullException.ThrowIfNull(name);
var entry = this.FindEntry(name);
if (entry is ConsoleVariable<T> variable)
return variable.Value;
if (entry is ConsoleVariable)
throw new InvalidOperationException($"Variable '{name}' is not of type {typeof(T).Name}.");
if (entry is null)
throw new EntryNotFoundException(name);
throw new InvalidOperationException($"Command '{name}' is not a variable.");
}
/// <summary>
/// Set the value of a variable.
/// </summary>
@ -162,18 +162,18 @@ internal partial class ConsoleManager : IServiceType
{
ArgumentNullException.ThrowIfNull(name);
Traits.ThrowIfTIsNullableAndNull(value);
var entry = this.FindEntry(name);
if (entry is ConsoleVariable<T> variable)
variable.Value = value;
if (entry is ConsoleVariable)
throw new InvalidOperationException($"Variable '{name}' is not of type {typeof(T).Name}.");
if (entry is null)
throw new EntryNotFoundException(name);
throw new EntryNotFoundException(name);
throw new InvalidOperationException($"Command '{name}' is not a variable.");
}
@ -181,16 +181,16 @@ internal partial class ConsoleManager : IServiceType
/// Process a console command.
/// </summary>
/// <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)
{
if (this.Invoke?.Invoke(command) == true)
return true;
var matches = GetCommandParsingRegex().Matches(command);
if (matches.Count == 0)
return false;
var entryName = matches[0].Value;
if (string.IsNullOrEmpty(entryName) || entryName.Any(char.IsWhiteSpace))
{
@ -204,7 +204,7 @@ internal partial class ConsoleManager : IServiceType
Log.Error("Command {CommandName} not found", entryName);
return false;
}
var parsedArguments = new List<object>();
if (entry.ValidArguments != null)
@ -217,13 +217,13 @@ internal partial class ConsoleManager : IServiceType
PrintUsage(entry);
return false;
}
var argumentToMatch = entry.ValidArguments[i - 1];
var group = matches[i];
if (!group.Success)
continue;
var value = group.Value;
if (string.IsNullOrEmpty(value))
continue;
@ -262,15 +262,15 @@ internal partial class ConsoleManager : IServiceType
throw new Exception("Unhandled argument type.");
}
}
if (parsedArguments.Count != entry.ValidArguments.Count)
{
// Either fill in the default values or error out
for (var i = parsedArguments.Count; i < entry.ValidArguments.Count; i++)
{
var argument = entry.ValidArguments[i];
// If the default value is DBNull, we need to error out as that means it was not specified
if (argument.DefaultValue == DBNull.Value)
{
@ -281,7 +281,7 @@ internal partial class ConsoleManager : IServiceType
parsedArguments.Add(argument.DefaultValue);
}
if (parsedArguments.Count != entry.ValidArguments.Count)
{
Log.Error("Too many arguments for command {CommandName}", entryName);
@ -302,20 +302,20 @@ internal partial class ConsoleManager : IServiceType
return entry.Invoke(parsedArguments);
}
[GeneratedRegex("""("[^"]+"|[^\s"]+)""", RegexOptions.Compiled)]
private static partial Regex GetCommandParsingRegex();
private static void PrintUsage(ConsoleEntry entry, bool error = true)
{
Log.WriteLog(
error ? LogEventLevel.Error : LogEventLevel.Information,
error ? LogEventLevel.Error : LogEventLevel.Information,
"Usage: {CommandName} {Arguments}",
null,
entry.Name,
string.Join(" ", entry.ValidArguments?.Select(x => $"<{x.Type.ToString().ToLowerInvariant()}>") ?? Enumerable.Empty<string>()));
}
private ConsoleEntry? FindEntry(string name)
{
return this.entries.TryGetValue(name, out var entry) ? entry as ConsoleEntry : null;
@ -333,7 +333,7 @@ internal partial class ConsoleManager : IServiceType
return true;
}
private static class Traits
{
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
@ -364,17 +364,17 @@ internal partial class ConsoleManager : IServiceType
/// <inheritdoc/>
public string Description { get; }
/// <summary>
/// Gets or sets a list of valid argument types for this console entry.
/// </summary>
public IReadOnlyList<ArgumentInfo>? ValidArguments { get; protected set; }
/// <summary>
/// Execute this command.
/// </summary>
/// <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);
/// <summary>
@ -388,19 +388,19 @@ internal partial class ConsoleManager : IServiceType
{
if (type == typeof(string))
return new ArgumentInfo(ConsoleArgumentType.String, defaultValue);
if (type == typeof(int))
return new ArgumentInfo(ConsoleArgumentType.Integer, defaultValue);
if (type == typeof(float))
return new ArgumentInfo(ConsoleArgumentType.Float, defaultValue);
if (type == typeof(bool))
return new ArgumentInfo(ConsoleArgumentType.Bool, defaultValue);
throw new ArgumentException($"Invalid argument type: {type.Name}");
}
public record ArgumentInfo(ConsoleArgumentType Type, object? DefaultValue);
}
@ -436,7 +436,7 @@ internal partial class ConsoleManager : IServiceType
private class ConsoleCommand : ConsoleEntry, IConsoleCommand
{
private readonly Delegate func;
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleCommand"/> class.
/// </summary>
@ -447,17 +447,17 @@ internal partial class ConsoleManager : IServiceType
: base(name, description)
{
this.func = func;
if (func.Method.ReturnType != typeof(bool))
throw new ArgumentException("Console command functions must return a boolean indicating success.");
var validArguments = new List<ArgumentInfo>();
foreach (var parameterInfo in func.Method.GetParameters())
{
var paraT = parameterInfo.ParameterType;
validArguments.Add(TypeToArgument(paraT, parameterInfo.DefaultValue));
}
this.ValidArguments = validArguments;
}
@ -491,7 +491,7 @@ internal partial class ConsoleManager : IServiceType
{
this.ValidArguments = new List<ArgumentInfo> { TypeToArgument(typeof(T), null) };
}
/// <inheritdoc/>
public T Value { get; set; }
@ -507,16 +507,16 @@ internal partial class ConsoleManager : IServiceType
{
this.Value = (T)(object)!boolValue;
}
Log.WriteLog(LogEventLevel.Information, "{VariableName} = {VariableValue}", null, this.Name, this.Value);
return true;
}
if (first.GetType() != typeof(T))
throw new ArgumentException($"Console variable must be set with an argument of type {typeof(T).Name}.");
this.Value = (T)first;
return true;
}
}

View file

@ -11,6 +11,47 @@ namespace Dalamud.Console;
#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>
/// Plugin-scoped version of the console service.
/// </summary>
@ -19,11 +60,11 @@ namespace Dalamud.Console;
#pragma warning disable SA1015
[ResolveVia<IConsole>]
#pragma warning restore SA1015
public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
{
[ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
private readonly List<IConsoleEntry> trackedEntries = new();
/// <summary>
@ -38,7 +79,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
/// <inheritdoc/>
public string Prefix { get; private set; }
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -46,7 +87,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
{
this.console.RemoveEntry(trackedEntry);
}
this.trackedEntries.Clear();
}
@ -108,21 +149,21 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
this.console.RemoveEntry(entry);
this.trackedEntries.Remove(entry);
}
private string GetPrefixedName(string name)
{
ArgumentNullException.ThrowIfNull(name);
// If the name is empty, return the prefix to allow for a single command or variable to be top-level.
if (name.Length == 0)
return this.Prefix;
if (name.Any(char.IsWhiteSpace))
throw new ArgumentException("Name cannot contain whitespace.", nameof(name));
return $"{this.Prefix}.{name}";
}
private IConsoleCommand InternalAddCommand(string name, string description, Delegate func)
{
var command = this.console.AddCommand(this.GetPrefixedName(name), description, func);
@ -130,44 +171,3 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
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;
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);
}

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>12.0.0.7</DalamudVersion>
<DalamudVersion>12.0.1.4</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -64,8 +64,8 @@
<PackageReference Include="BitFaster.Caching" Version="2.4.1" />
<PackageReference Include="CheapLoc" Version="1.1.8" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="all" />
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp5" />
<PackageReference Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp3" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
@ -130,6 +130,13 @@
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
</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">
<ItemGroup>
<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...");
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.QueueSave();
config.ForceSave();
}
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;
namespace Dalamud.Game.Addon.Events;
@ -14,9 +15,9 @@ internal unsafe class AddonEventEntry
/// Name of an invalid addon.
/// </summary>
public const string InvalidAddonName = "NullAddon";
private string? addonName;
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
@ -35,18 +36,25 @@ internal unsafe class AddonEventEntry
/// <summary>
/// Gets the handler that gets called when this event is triggered.
/// </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>
/// Gets the unique id for this event.
/// </summary>
public required uint ParamKey { get; init; }
/// <summary>
/// Gets the event type for this event.
/// </summary>
public required AddonEventType EventType { get; init; }
/// <summary>
/// Gets the event handle for this event.
/// </summary>

View file

@ -1,4 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@ -24,21 +24,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// PluginName for Dalamud Internal use.
/// </summary>
public static readonly Guid DalamudInternalKey = Guid.NewGuid();
private static readonly ModuleLog Log = new("AddonEventManager");
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly AddonEventManagerAddressResolver address;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
private AddonCursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
private AddonEventManager(TargetSigScanner sigScanner)
{
@ -47,7 +47,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null;
this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
@ -69,7 +69,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
{
pluginEventController.Dispose();
}
this.addonLifecycle.UnregisterListener(this.finalizeEventListener);
}
@ -92,7 +92,30 @@ internal unsafe class AddonEventManager : IInternalDisposableService
{
Log.Verbose($"Unable to locate controller for {pluginId}. No event was added.");
}
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;
}
@ -112,7 +135,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed.");
}
}
/// <summary>
/// Force the game cursor to be the specified cursor.
/// </summary>
@ -167,21 +190,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService
pluginList.Value.RemoveForAddon(addonInfo.AddonName);
}
}
private nint UpdateCursorDetour(RaptureAtkModule* module)
{
try
{
var atkStage = AtkStage.Instance();
if (this.cursorOverride is not null && atkStage is not null)
{
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
if (cursor != this.cursorOverride)
if (cursor != this.cursorOverride)
{
AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
return nint.Zero;
}
}
@ -218,7 +241,7 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo
public AddonEventManagerPluginScoped(LocalPlugin plugin)
{
this.plugin = plugin;
this.eventManagerService.AddPluginEventController(plugin.EffectiveWorkingPluginId);
}
@ -230,31 +253,38 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo
{
this.eventManagerService.ResetCursor();
}
this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId);
Service<Framework>.Get().RunOnFrameworkThread(() =>
{
this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId);
}).Wait();
}
/// <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);
/// <inheritdoc/>
public IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate)
=> this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventDelegate);
/// <inheritdoc/>
public void RemoveEvent(IAddonEventHandle eventHandle)
=> this.eventManagerService.RemoveEvent(this.plugin.EffectiveWorkingPluginId, eventHandle);
/// <inheritdoc/>
public void SetCursor(AddonCursorType cursor)
{
this.isForcingCursor = true;
this.eventManagerService.SetCursor(cursor);
}
/// <inheritdoc/>
public void ResetCursor()
{
this.isForcingCursor = false;
this.eventManagerService.ResetCursor();
}
}

View file

@ -1,4 +1,4 @@
namespace Dalamud.Game.Addon.Events;
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// Reimplementation of AtkEventType.
@ -9,150 +9,262 @@ public enum AddonEventType : byte
/// Mouse Down.
/// </summary>
MouseDown = 3,
/// <summary>
/// Mouse Up.
/// </summary>
MouseUp = 4,
/// <summary>
/// Mouse Move.
/// </summary>
MouseMove = 5,
/// <summary>
/// Mouse Over.
/// </summary>
MouseOver = 6,
/// <summary>
/// Mouse Out.
/// </summary>
MouseOut = 7,
/// <summary>
/// Mouse Wheel.
/// </summary>
MouseWheel = 8,
/// <summary>
/// Mouse Click.
/// </summary>
MouseClick = 9,
/// <summary>
/// Mouse Double Click.
/// </summary>
MouseDoubleClick = 10,
/// <summary>
/// Input Received.
/// </summary>
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>
/// Focus Start.
/// </summary>
FocusStart = 18,
/// <summary>
/// Focus Stop.
/// </summary>
FocusStop = 19,
/// <summary>
/// Button Press, sent on MouseDown on Button.
/// Resize (ChatLogPanel).
/// </summary>
Resize = 19,
/// <summary>
/// AtkComponentButton Press, sent on MouseDown on Button.
/// </summary>
ButtonPress = 23,
/// <summary>
/// Button Release, sent on MouseUp and MouseOut.
/// AtkComponentButton Release, sent on MouseUp and MouseOut.
/// </summary>
ButtonRelease = 24,
/// <summary>
/// Button Click, sent on MouseUp and MouseClick on button.
/// AtkComponentButton Click, sent on MouseUp and MouseClick on button.
/// </summary>
ButtonClick = 25,
/// <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>
ListItemRollOver = 33,
/// <summary>
/// List Item Roll Out.
/// AtkComponentList Roll Out.
/// </summary>
ListItemRollOut = 34,
/// <summary>
/// List Item Toggle.
/// AtkComponentList Click.
/// </summary>
ListItemToggle = 35,
ListItemClick = 35,
/// <summary>
/// Drag Drop Begin.
/// AtkComponentList Toggle.
/// </summary>
[Obsolete("Use ListItemClick")]
ListItemToggle = 35,
/// <summary>
/// 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).
/// </summary>
DragDropBegin = 47,
DragDropBegin = 50,
/// <summary>
/// Drag Drop Insert.
/// AtkComponentDragDrop End.
/// </summary>
DragDropEnd = 51,
/// <summary>
/// AtkComponentDragDrop Insert.
/// Sent when dropping an icon into a hotbar/inventory slot or similar.
/// </summary>
DragDropInsert = 50,
DragDropInsert = 53,
/// <summary>
/// Drag Drop Roll Over.
/// AtkComponentDragDrop Roll Over.
/// </summary>
DragDropRollOver = 52,
DragDropRollOver = 55,
/// <summary>
/// Drag Drop Roll Out.
/// AtkComponentDragDrop Roll Out.
/// </summary>
DragDropRollOut = 53,
DragDropRollOut = 56,
/// <summary>
/// Drag Drop Discard.
/// AtkComponentDragDrop Discard.
/// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar.
/// </summary>
DragDropDiscard = 54,
DragDropDiscard = 57,
/// <summary>
/// Drag Drop Unknown.
/// </summary>
[Obsolete("Use DragDropDiscard")]
[Obsolete("Use DragDropDiscard", true)]
DragDropUnk54 = 54,
/// <summary>
/// Drag Drop Cancel.
/// AtkComponentDragDrop Cancel.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// </summary>
DragDropCancel = 55,
DragDropCancel = 58,
/// <summary>
/// Drag Drop Unknown.
/// </summary>
[Obsolete("Use DragDropCancel")]
[Obsolete("Use DragDropCancel", true)]
DragDropUnk55 = 55,
/// <summary>
/// Icon Text Roll Over.
/// AtkComponentIconText Roll Over.
/// </summary>
IconTextRollOver = 56,
IconTextRollOver = 59,
/// <summary>
/// Icon Text Roll Out.
/// AtkComponentIconText Roll Out.
/// </summary>
IconTextRollOut = 57,
IconTextRollOut = 60,
/// <summary>
/// Icon Text Click.
/// AtkComponentIconText Click.
/// </summary>
IconTextClick = 58,
IconTextClick = 61,
/// <summary>
/// Window Roll Over.
/// AtkDialogue Close.
/// </summary>
WindowRollOver = 67,
DialogueClose = 62,
/// <summary>
/// Window Roll Out.
/// AtkDialogue Submit.
/// </summary>
WindowRollOut = 68,
DialogueSubmit = 63,
/// <summary>
/// Window Change Scale.
/// AtkTimer Tick.
/// </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 Dalamud.Game.Gui;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -26,7 +26,7 @@ internal unsafe class PluginEventController : IDisposable
}
private AddonEventListener EventListener { get; init; }
private List<AddonEventEntry> Events { get; } = new();
/// <summary>
@ -37,6 +37,7 @@ internal unsafe class PluginEventController : IDisposable
/// <param name="atkEventType">The Event Type.</param>
/// <param name="handler">The delegate to call when invoking this event.</param>
/// <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)
{
var node = (AtkResNode*)atkResNode;
@ -44,7 +45,7 @@ internal unsafe class PluginEventController : IDisposable
var eventType = (AtkEventType)atkEventType;
var eventId = this.GetNextParamKey();
var eventGuid = Guid.NewGuid();
var eventHandle = new AddonEventHandle
{
AddonName = addon->NameString,
@ -52,11 +53,54 @@ internal unsafe class PluginEventController : IDisposable
EventType = atkEventType,
EventGuid = eventGuid,
};
var eventEntry = new AddonEventEntry
{
Addon = atkUnitBase,
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,
EventType = atkEventType,
ParamKey = eventId,
@ -92,14 +136,14 @@ internal unsafe class PluginEventController : IDisposable
if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events)
{
Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events.");
foreach (var registeredEvent in events)
{
this.RemoveEvent(registeredEvent.Handle);
}
}
}
/// <inheritdoc/>
public void Dispose()
{
@ -107,7 +151,7 @@ internal unsafe class PluginEventController : IDisposable
{
this.RemoveEvent(registeredEvent.Handle);
}
this.EventListener.Dispose();
}
@ -120,7 +164,7 @@ internal unsafe class PluginEventController : IDisposable
throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller.");
}
/// <summary>
/// Attempts to remove a tracked event from native UI.
/// This method performs several safety checks to only remove events from a still active addon.
@ -139,20 +183,22 @@ internal unsafe class PluginEventController : IDisposable
// Is our stored addon pointer the same as the active addon pointer?
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;
if (atkUnitBase->UldManager.LoadedState == AtkLoadState.Unloaded) return;
// Does this addon contain the node this event is for? (by address)
var nodeFound = false;
foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount))
foreach (var node in atkUnitBase->UldManager.Nodes)
{
var node = atkUnitBase->UldManager.NodeList[index];
// 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;
break;
}
}
// If we didn't find the node, we can't remove the event.
if (!nodeFound) return;
@ -166,33 +212,45 @@ internal unsafe class PluginEventController : IDisposable
var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey;
var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address;
var eventTypeMatches = currentEvent->State.EventType == eventType;
if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches)
{
eventFound = true;
break;
}
// Move to the next event.
currentEvent = currentEvent->NextEvent;
}
// If we didn't find the event, we can't remove the event.
if (!eventFound) return;
// We have a valid addon, valid node, valid event, and valid key.
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)
{
try
{
if (eventPtr is null) 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.
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)
{

View file

@ -11,7 +11,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
internal class AddonSetupHook<T> : IDisposable where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
@ -30,22 +30,22 @@ internal class AddonSetupHook<T> : IDisposable where T : Delegate
"use64",
$"mov r9, 0x{detourPtr:X8}",
};
var opt = new AsmHookOptions
{
PreferRelativeJump = true,
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
MaxOpcodeSize = 5,
};
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
}
/// <summary>
/// Gets a value indicating whether or not the hook is enabled.
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
@ -57,7 +57,7 @@ internal class AddonSetupHook<T> : IDisposable where T : Delegate
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}

View file

@ -8,6 +8,8 @@ using Dalamud.Configuration.Internal;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
@ -41,7 +43,7 @@ internal partial class ChatHandlers : IServiceType
public string? LastLink { get; private set; }
/// <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>
public bool IsAutoUpdateComplete { get; private set; }
@ -100,8 +102,6 @@ internal partial class ChatHandlers : IServiceType
if (chatGui == null || pluginManager == null || dalamudInterface == null)
return;
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
if (this.configuration.PrintDalamudWelcomeMsg)
{
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
{
Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."),
Message = updateMessage.Build(),
Type = XivChatType.Notice,
});
this.configuration.LastVersion = assemblyVersion;
this.configuration.LastVersion = Util.AssemblyVersion;
this.configuration.QueueSave();
}

View file

@ -158,7 +158,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.UsingParasol]);
ConditionFlag.UsingFashionAccessory]);
blockingFlag = blockingConditions.FirstOrDefault();
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
/// 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>
public enum ConditionFlag
{
@ -176,11 +178,20 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
/// <remarks>
/// Observed during Materialize (Desynthesis, Materia Extraction, Aetherial Reduction) and Repair.
/// </remarks>
Occupied39 = 39,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
ExecutingCraftingAction = 40,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
[Obsolete("Renamed to ExecutingCraftingAction.")]
Crafting40 = 40,
/// <summary>
@ -191,6 +202,13 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
/// <remarks> Includes fishing. </remarks>
ExecutingGatheringAction = 42,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
[Obsolete("Renamed to ExecutingGatheringAction.")]
Gathering42 = 42,
/// <summary>
@ -220,8 +238,14 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
[Obsolete("To avoid confusion, renamed to UsingChocoboTaxi.")]
AutorunActive = 49,
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
UsingChocoboTaxi = 49,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
@ -261,8 +285,14 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command at this time.
/// </summary>
[Obsolete("Renamed to MountOrOrnamentTransition.")]
Unknown57 = 57,
/// <summary>
/// Unable to execute command at this time.
/// </summary>
MountOrOrnamentTransition = 57,
/// <summary>
/// Unable to execute command while watching a cutscene.
/// </summary>
@ -331,6 +361,9 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command while mounting.
/// </summary>
/// <remarks>
/// Observed in Cosmic Exploration while using the actions Astrodrill (only briefly) and Solar Flarethrower.
/// </remarks>
Mounting71 = 71,
/// <summary>
@ -398,7 +431,10 @@ public enum ConditionFlag
/// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84,
// Unknown85 = 85,
/// <remarks>
/// Observed in Cosmic Exploration while gathering during a stellar mission.
/// </remarks>
Unknown85 = 85,
/// <summary>
/// Unable to execute command while playing duty record.
@ -430,7 +466,7 @@ public enum ConditionFlag
/// </summary>
[Obsolete("Use InDutyQueue")]
BoundToDuty97 = 91,
/// <summary>
/// Unable to execute command while bound by duty.
/// Specifically triggered when you are in a queue for a duty but not inside a duty.
@ -450,8 +486,14 @@ public enum ConditionFlag
/// <summary>
/// Unable to execute command while using a parasol.
/// </summary>
[Obsolete("Renamed to UsingFashionAccessory.")]
UsingParasol = 94,
/// <summary>
/// Unable to execute command while using a fashion accessory.
/// </summary>
UsingFashionAccessory = 94,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
@ -460,6 +502,9 @@ public enum ConditionFlag
/// <summary>
/// Cannot execute at this time.
/// </summary>
/// <remarks>
/// Observed in Cosmic Exploration while participating in MechaEvent.
/// </remarks>
Unknown96 = 96,
/// <summary>
@ -481,4 +526,22 @@ public enum ConditionFlag
/// Unable to execute command while editing a portrait.
/// </summary>
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.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Dalamud.Utility;
using Lumina.Excel;
@ -69,13 +70,13 @@ public interface IFate : IEquatable<IFate>
byte Progress { get; }
/// <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>
[Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; }
/// <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>
bool HasBonus { get; }
@ -222,8 +223,8 @@ internal unsafe partial class Fate : IFate
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
[Obsolete($"Use {nameof(HasBonus)} instead")]
public bool HasExpBonus => this.Struct->IsExpBonus;
[Api13ToDo("Remove")]
public bool HasExpBonus => this.HasBonus;
/// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus;
@ -249,5 +250,5 @@ internal unsafe partial class Fate : IFate
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </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;
/// <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>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool InUmbralIce => this.Struct->ElementStance < 0;
/// <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>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool InAstralFire => this.Struct->ElementStance > 0;
/// <summary>
/// Gets a value indicating whether or not Enochian is active.
/// Gets a value indicating whether Enochian is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsEnochianActive => this.Struct->EnochianActive;

View file

@ -14,7 +14,7 @@ public unsafe class PCTGauge : JobGaugeBase<PictomancerGauge>
/// Initializes a new instance of the <see cref="PCTGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal PCTGauge(IntPtr address)
internal PCTGauge(IntPtr address)
: base(address)
{
}
@ -28,29 +28,29 @@ public unsafe class PCTGauge : JobGaugeBase<PictomancerGauge>
/// Gets the amount of paint the player has.
/// </summary>
public byte Paint => Struct->Paint;
/// <summary>
/// Gets a value indicating whether or not a creature motif is drawn.
/// Gets a value indicating whether a creature motif is drawn.
/// </summary>
public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn;
/// <summary>
/// Gets a value indicating whether or not a weapon motif is drawn.
/// Gets a value indicating whether a weapon motif is drawn.
/// </summary>
public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn;
/// <summary>
/// Gets a value indicating whether or not a landscape motif is drawn.
/// Gets a value indicating whether a landscape motif is drawn.
/// </summary>
public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn;
/// <summary>
/// Gets a value indicating whether or not a moogle portrait is ready.
/// Gets a value indicating whether a moogle portrait is ready.
/// </summary>
public bool MooglePortraitReady => Struct->MooglePortraitReady;
/// <summary>
/// Gets a value indicating whether or not a madeen portrait is ready.
/// Gets a value indicating whether a madeen portrait is ready.
/// </summary>
public bool MadeenPortraitReady => Struct->MadeenPortraitReady;

View file

@ -42,7 +42,7 @@ public enum CustomizeIndex
HairStyle = 0x06,
/// <summary>
/// Whether or not the character has hair highlights.
/// Whether the character has hair highlights.
/// </summary>
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;
}
/// <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/>
public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => new(this.commandMap);
@ -122,6 +132,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
return false;
}
this.CommandAdded?.Invoke(this, new CommandEventArgs
{
Command = command,
CommandInfo = info,
});
if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName))
{
this.commandMap.Remove(command, out _);
@ -144,6 +160,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
return false;
}
this.CommandAdded?.Invoke(this, new CommandEventArgs
{
Command = command,
CommandInfo = info,
});
return true;
}
@ -155,7 +177,17 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
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>
@ -204,6 +236,20 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
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>
@ -268,7 +314,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
}
else
{
Log.Error($"Command {command} is already registered.");
Log.Error("Command {Command} is already registered.", command);
}
return false;
@ -287,7 +333,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
}
else
{
Log.Error($"Command {command} not found.");
Log.Error("Command {Command} not found.", command);
}
return false;

View file

@ -12,6 +12,7 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@ -47,7 +48,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private readonly ConcurrentDictionary<TaskCompletionSource, (ulong Expire, CancellationToken CancellationToken)>
tickDelayedTaskCompletionSources = new();
private ulong tickCounter;
private ulong tickCounter;
[ServiceManager.ServiceConstructor]
private unsafe Framework()
@ -504,14 +505,18 @@ internal sealed class Framework : IInternalDisposableService, IFramework
#pragma warning restore SA1015
internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
{
private readonly PluginErrorHandler pluginErrorHandler;
[ServiceManager.ServiceDependency]
private readonly Framework frameworkService = Service<Framework>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="FrameworkPluginScoped"/> class.
/// </summary>
internal FrameworkPluginScoped()
/// <param name="pluginErrorHandler">Error handler instance.</param>
internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler)
{
this.pluginErrorHandler = pluginErrorHandler;
this.frameworkService.Update += this.OnUpdateForward;
}
@ -604,7 +609,7 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework
}
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="commandAction">The command action itself.</param>
/// <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)
{
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.
/// </summary>
/// <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)
{
var found = false;

View file

@ -17,32 +17,32 @@ public interface IReadOnlyDtrBarEntry
/// Gets the title of this entry.
/// </summary>
public string Title { get; }
/// <summary>
/// Gets a value indicating whether this entry has a click action.
/// </summary>
public bool HasClickAction { get; }
/// <summary>
/// Gets the text of this entry.
/// </summary>
public SeString? Text { get; }
/// <summary>
/// Gets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
public SeString? Tooltip { get; }
/// <summary>
/// Gets a value indicating whether this entry should be shown.
/// </summary>
public bool Shown { get; }
/// <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>
public bool UserHidden { get; }
/// <summary>
/// Triggers the click action of this entry.
/// </summary>
@ -59,22 +59,22 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// Gets or sets the text of this entry.
/// </summary>
public new SeString? Text { get; set; }
/// <summary>
/// Gets or sets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
public new SeString? Tooltip { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public new bool Shown { get; set; }
/// <summary>
/// Gets or sets a action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action? OnClick { get; set; }
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.
@ -121,7 +121,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// <inheritdoc cref="IDtrBarEntry.Tooltip" />
public SeString? Tooltip { get; set; }
/// <summary>
/// Gets or sets a action to be invoked when the user clicks on the dtr entry.
/// </summary>
@ -145,14 +145,14 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
/// <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;
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>
internal AtkTextNode* TextNode { get; set; }
/// <summary>
/// Gets or sets the storage for the text of this entry.
/// </summary>
@ -171,7 +171,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// <summary>
/// Gets or sets a value indicating whether this entry has just been added.
/// </summary>
internal bool Added { get; set; }
internal bool Added { get; set; }
/// <summary>
/// Gets or sets the plugin that owns this entry.
@ -183,7 +183,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
{
if (this.OnClick == null)
return false;
this.OnClick.Invoke();
return true;
}

View file

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

View file

@ -257,7 +257,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// <summary>
/// Indicates if the game is in the lobby scene (title screen, chara select, chara make, aesthetician etc.).
/// </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);
/// <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).
/// </summary>
/// <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>
/// 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
@ -53,7 +53,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany
return;
var sb = new SeStringBuilder();
if (this.OuterWrap is { Item1: var outerLeft })
if (this.OuterWrap is { Item1: { } outerLeft })
{
sb.Append(outerLeft);
}
@ -67,7 +67,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool 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(this.Text ?? this.GetStrippedField(handler));
@ -87,7 +87,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany
sb.Append(isFreeCompany ? "»" : "》");
}
if (this.OuterWrap is { Item2: var outerRight })
if (this.OuterWrap is { Item2: { } outerRight })
{
sb.Append(outerRight);
}

View file

@ -35,7 +35,7 @@ public class NamePlateSimpleParts(NamePlateStringField field)
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
return;
if (this.TextWrap is { Item1: var left, Item2: var right })
if (this.TextWrap is { Item1: { } left, Item2: { } right })
{
var sb = new SeStringBuilder();
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; }
/// <summary>
/// Gets a value indicating whether or not this request was successful.
/// Gets a value indicating whether this request was successful.
/// </summary>
public bool Ok => this.Status == 0;

View file

@ -248,7 +248,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </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)
{
if (!shouldDispose)

View file

@ -31,7 +31,7 @@ public class SigScanner : IDisposable, ISigScanner
/// <summary>
/// Initializes a new instance of the <see cref="SigScanner"/> class using the main module of the current process.
/// </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>
public SigScanner(bool doCopy = false, FileInfo? cacheFile = null)
: 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.
/// </summary>
/// <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>
public SigScanner(ProcessModule module, bool doCopy = false, FileInfo? cacheFile = null)
{

View file

@ -19,7 +19,7 @@ internal class TargetSigScanner : SigScanner, IPublicDisposableService
/// <summary>
/// Initializes a new instance of the <see cref="TargetSigScanner"/> class.
/// </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>
public TargetSigScanner(bool doCopy = false, FileInfo? cacheFile = null)
: base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile)

View file

@ -18,12 +18,12 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using FFXIVClientStructs.Interop;
using Lumina.Data.Structs.Excel;
using Lumina.Excel;
@ -35,6 +35,8 @@ using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
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;
@ -50,6 +52,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
{
private static readonly ModuleLog Log = new("SeStringEvaluator");
[ServiceManager.ServiceDependency]
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
@ -91,7 +96,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (str.IsTextOnly())
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,
// 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/>
public ReadOnlySeString EvaluateFromAddon(
uint addonId,
Span<SeStringParameter> localParameters = default,
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))
return default;
@ -129,7 +143,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
Span<SeStringParameter> localParameters = default,
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))
return default;
@ -143,7 +157,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
Span<SeStringParameter> localParameters = default,
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))
return default;
@ -154,7 +168,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
/// <inheritdoc/>
public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) =>
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)
.ExtractText()
.StripSoftHyphen(),
@ -163,7 +177,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
/// <inheritdoc/>
public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) =>
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)
.ExtractText()
.StripSoftHyphen(),
@ -182,6 +196,18 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float 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(
SeStringBuilder builder,
ReadOnlySeStringSpan str,
@ -445,7 +471,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) &&
logCrossWorldName == 1)
context.Builder.Append((ReadOnlySeStringSpan)world.Name);
context.Builder.Append(new ReadOnlySeStringSpan(world.Name.GetPointer(0)));
}
return true;
@ -635,7 +661,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
{
case false when digit == 0:
continue;
case true when i % 3 == 0:
case true when MathF.Log10(i) % 3 == 2:
this.ResolveStringExpression(in context, eSep);
break;
}
@ -711,84 +737,186 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
this.TryResolveUInt(in context, enu.Current, out eColParamValue);
var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText();
this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue);
var originalRowIdValue = eRowIdValue;
var flags = this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue);
if (string.IsNullOrEmpty(resolvedSheetName))
return false;
if (!this.dataManager.Excel.SheetNames.Contains(resolvedSheetName))
var text = this.FormatSheetValue(context.Language, resolvedSheetName, eRowIdValue, eColIndexValue, eColParamValue);
if (text.IsEmpty)
return false;
if (!this.dataManager.GetExcelSheet<RawRow>(context.Language, resolvedSheetName)
.TryGetRow(eRowIdValue, out var row))
return false;
this.AddSheetRedirectItemDecoration(context, ref text, flags, eRowIdValue);
if (eColIndexValue >= row.Columns.Count)
return false;
if (resolvedSheetName != "DescriptionString")
eColParamValue = originalRowIdValue;
var column = row.Columns[(int)eColIndexValue];
switch (column.Type)
// Note: The link marker symbol is added by RaptureLogMessage, probably somewhere in it's Update function.
// 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)
{
if (!this.dataManager.Excel.SheetNames.Contains(sheetName))
return default;
if (!this.dataManager.GetExcelSheet<RawRow>(language, sheetName)
.TryGetRow(rowId, out var row))
return default;
if (colIndex >= row.Columns.Count)
return default;
var column = row.Columns[(int)colIndex];
return column.Type switch
{
case ExcelColumnDataType.String:
context.Builder.Append(this.Evaluate(row.ReadString(column.Offset), [eColParamValue], context.Language));
return true;
case ExcelColumnDataType.Bool:
context.Builder.Append((row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.Int8:
context.Builder.Append(row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.UInt8:
context.Builder.Append(row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.Int16:
context.Builder.Append(row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.UInt16:
context.Builder.Append(row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.Int32:
context.Builder.Append(row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.UInt32:
context.Builder.Append(row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.Float32:
context.Builder.Append(row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.Int64:
context.Builder.Append(row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.UInt64:
context.Builder.Append(row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool0:
context.Builder.Append((row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool1:
context.Builder.Append((row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool2:
context.Builder.Append((row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool3:
context.Builder.Append((row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool4:
context.Builder.Append((row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool5:
context.Builder.Append((row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool6:
context.Builder.Append((row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
case ExcelColumnDataType.PackedBool7:
context.Builder.Append((row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture));
return true;
ExcelColumnDataType.String => this.Evaluate(row.ReadString(column.Offset), [colParam], language),
ExcelColumnDataType.Bool => (row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.Int8 => row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.UInt8 => row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.Int16 => row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.UInt16 => row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.Int32 => row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.UInt32 => row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.Float32 => row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.Int64 => row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.UInt64 => row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool0 => (row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool1 => (row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool2 => (row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool3 => (row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool4 => (row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool5 => (row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool6 => (row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
ExcelColumnDataType.PackedBool7 => (row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture),
_ => default,
};
}
private void AddSheetRedirectItemDecoration(in SeStringContext context, ref ReadOnlySeString text, SheetRedirectFlags flags, uint eRowIdValue)
{
if (!flags.HasFlag(SheetRedirectFlags.Item))
return;
var rarity = 1u;
var skipLink = false;
if (flags.HasFlag(SheetRedirectFlags.EventItem))
{
rarity = 8;
skipLink = true;
}
var itemId = eRowIdValue;
if (this.dataManager.GetExcelSheet<Item>(context.Language).TryGetRow(itemId, out var itemRow))
{
rarity = itemRow.Rarity;
if (rarity == 0)
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:
return false;
context.Builder.Append(text);
return;
}
}
@ -938,9 +1066,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (p.Type == ReadOnlySePayloadType.Text)
{
context.Builder.Append(
context.CultureInfo.TextInfo.ToTitleCase(Encoding.UTF8.GetString(p.Body.Span)));
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language));
continue;
}
@ -1067,8 +1193,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt))
return false;
var instance = packedIds >> 0x10;
var mapId = packedIds & 0xFF;
var instance = packedIds >> 16;
var mapId = packedIds & 0xFFFF;
if (this.dataManager.GetExcelSheet<TerritoryType>(context.Language)
.TryGetRow(territoryTypeId, out var territoryTypeRow))
@ -1356,8 +1482,6 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
var group = (uint)(e0Val + 1);
var rowId = (uint)e1Val;
using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55);
if (!this.dataManager.GetExcelSheet<Completion>(context.Language).TryGetFirst(
row => row.Group == group && !row.LookupTable.IsEmpty,
out var groupRow))
@ -1382,6 +1506,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
return true;
}
using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55);
// CategoryDataCache
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.
/// </summary>
/// <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
/// 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>
@ -75,7 +75,7 @@ public class ItemPayload : Payload
/// <summary>
/// Kinds of items that can be fetched from this payload.
/// </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
{
/// <summary>
@ -142,7 +142,7 @@ public class ItemPayload : Payload
: (RowRef)LuminaUtils.CreateRef<Item>(this.ItemId);
/// <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>
[JsonProperty]
public bool IsHQ => this.Kind == ItemKind.Hq;

View file

@ -46,7 +46,7 @@ public class UIForegroundPayload : Payload
public override PayloadType Type => PayloadType.UIForeground;
/// <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>
public bool IsEnabled => this.ColorKey != 0;

View file

@ -64,7 +64,7 @@ public class UIGlowPayload : Payload
}
/// <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>
public bool IsEnabled => this.ColorKey != 0;

View file

@ -113,7 +113,7 @@ public class SeStringBuilder
/// Add an item link to the builder.
/// </summary>
/// <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>
/// <returns>The current builder.</returns>
public SeStringBuilder AddItemLink(uint itemId, bool isHq, string? itemNameOverride = null) =>

View file

@ -20,7 +20,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook
private bool isEnabled = false;
private DynamicMethod statsMethod;
private Guid hookId = Guid.NewGuid();
/// <summary>
@ -89,7 +89,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook
}
/// <summary>
/// Gets a value indicating whether or not the hook is enabled.
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled
{
@ -101,7 +101,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook
}
/// <summary>
/// Gets a value indicating whether or not the hook has been disposed.
/// Gets a value indicating whether the hook has been disposed.
/// </summary>
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;
/// <summary>
/// Gets a value indicating whether or not the hook is enabled.
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public virtual bool IsEnabled => throw new NotImplementedException();
/// <summary>
/// Gets a value indicating whether or not the hook has been disposed.
/// Gets a value indicating whether the hook has been disposed.
/// </summary>
public bool IsDisposed { get; private set; }
@ -90,6 +90,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
/// <exception cref="ObjectDisposedException">Hook is already disposed.</exception>
public virtual void Enable() => throw new NotImplementedException();
/// <summary>

View file

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

View file

@ -15,10 +15,10 @@ 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.
/// </summary>
/// <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 T? detour;
private bool activated;
@ -29,7 +29,10 @@ internal class CallHook<T> : IDisposable where T : Delegate
/// <param name="detour">Delegate to invoke.</param>
internal CallHook(nint address, T detour)
{
ArgumentNullException.ThrowIfNull(detour);
this.detour = detour;
this.Address = address;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
@ -38,22 +41,31 @@ internal class CallHook<T> : IDisposable where T : Delegate
$"mov rax, 0x{detourPtr:X8}",
"call rax",
};
var opt = new AsmHookOptions
{
PreferRelativeJump = true,
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
MaxOpcodeSize = 5,
};
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
}
/// <summary>
/// Gets a value indicating whether or not the hook is enabled.
/// Gets a value indicating whether the hook is enabled.
/// </summary>
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>
/// Starts intercepting a call to the function.
/// </summary>
@ -65,7 +77,7 @@ internal class CallHook<T> : IDisposable where T : Delegate
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}

View file

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

View file

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

View file

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

View file

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

View file

@ -14,16 +14,21 @@ public enum PluginInstallerOpenKind
/// Open to the "Installed Plugins" page.
/// </summary>
InstalledPlugins,
/// <summary>
/// Open to the "Can be updated" page.
/// </summary>
UpdateablePlugins,
/// <summary>
/// Open to the "Changelogs" page.
/// Open to the "Plugin Changelogs" page.
/// </summary>
Changelogs,
/// <summary>
/// Open to the "Dalamud Changelogs" page.
/// </summary>
DalamudChangelogs,
}
/// <summary>
@ -35,12 +40,12 @@ public enum SettingsOpenKind
/// Open to the "General" page.
/// </summary>
General,
/// <summary>
/// Open to the "Look &#038; Feel" page.
/// </summary>
LookAndFeel,
/// <summary>
/// Open to the "Auto Updates" page.
/// </summary>

View file

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

View file

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Utility;
namespace Dalamud.Interface.ImGuiFileDialog;
/// <summary>
@ -13,11 +15,44 @@ public partial class FileDialog
private readonly DriveListLoader driveListLoader = new();
private List<FileStruct> files = new();
private List<FileStruct> filteredFiles = new();
private readonly List<FileStruct> files = [];
private readonly List<FileStruct> filteredFiles = [];
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
{
@ -25,48 +60,64 @@ public partial class FileDialog
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,
FileName,
Type,
Size,
Date,
Comparison<FileStruct>? sortFunc = sortingField switch
{
SortingField.FileName => SortByFileNameAsc,
SortingField.FileNameDescending => SortByFileNameDesc,
SortingField.Type => SortByTypeAsc,
SortingField.TypeDescending => SortByTypeDesc,
SortingField.Size => SortBySizeAsc,
SortingField.SizeDescending => SortBySizeDesc,
SortingField.Date => SortByDateAsc,
SortingField.DateDescending => SortByDateDesc,
_ => null,
};
if (sortFunc is null)
{
return;
}
this.files.Sort(sortFunc);
this.currentSortingField = sortingField;
this.ApplyFilteringOnFileList();
this.SortOrderChanged?.InvokeSafely(this.currentSortingField);
}
private static string ComposeNewPath(List<string> decomp)
private static string ComposeNewPath(List<string> decomposition)
{
// Handle UNC paths (network paths)
if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1]))
switch (decomposition.Count)
{
var pathParts = new List<string>(decomp);
pathParts.RemoveRange(0, 2);
// Can not access server level or UNC root
if (pathParts.Count <= 1)
{
return string.Empty;
}
// Handle UNC paths (network paths)
case >= 2 when string.IsNullOrEmpty(decomposition[0]) && string.IsNullOrEmpty(decomposition[1]):
var pathParts = new List<string>(decomposition);
pathParts.RemoveRange(0, 2);
return $"\\\\{string.Join('\\', pathParts)}";
// Can not access server level or UNC root
if (pathParts.Count <= 1)
{
return string.Empty;
}
return $@"\\{string.Join('\\', pathParts)}";
case 1:
var drivePath = decomposition[0];
if (drivePath[^1] != Path.DirectorySeparatorChar)
{ // turn C: into C:\
drivePath += Path.DirectorySeparatorChar;
}
return drivePath;
default: return Path.Combine(decomposition.ToArray());
}
if (decomp.Count == 1)
{
var drivePath = decomp[0];
if (drivePath[^1] != Path.DirectorySeparatorChar)
{ // turn C: into C:\
drivePath += Path.DirectorySeparatorChar;
}
return drivePath;
}
return Path.Combine(decomp.ToArray());
}
private static FileStruct GetFile(FileInfo file, string path)
{
return new FileStruct
=> new()
{
FileName = file.Name,
FilePath = path,
@ -76,11 +127,9 @@ public partial class FileDialog
Type = FileStructType.File,
Ext = file.Extension.Trim('.'),
};
}
private static FileStruct GetDir(DirectoryInfo dir, string path)
{
return new FileStruct
=> new()
{
FileName = dir.Name,
FilePath = path,
@ -90,136 +139,191 @@ public partial class FileDialog
Type = FileStructType.Directory,
Ext = string.Empty,
};
}
private static int SortByFileNameDesc(FileStruct a, FileStruct b)
{
if (a.FileName[0] == '.' && b.FileName[0] != '.')
switch (a.FileName, b.FileName)
{
return 1;
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.FileName[0] != '.' && b.FileName[0] == '.')
if (a.FileName[0] is '.')
{
return -1;
}
if (a.FileName[0] == '.' && b.FileName[0] == '.')
{
if (a.FileName.Length == 1)
{
return -1;
}
if (b.FileName.Length == 1)
if (b.FileName[0] is not '.')
{
return 1;
}
return -1 * string.Compare(a.FileName[1..], b.FileName[1..]);
if (a.FileName.Length is 1)
{
return -1;
}
if (b.FileName.Length is 1)
{
return 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)
{
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)
{
if (a.FileName[0] == '.' && b.FileName[0] != '.')
switch (a.FileName, b.FileName)
{
return -1;
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.FileName[0] != '.' && b.FileName[0] == '.')
if (a.FileName[0] is '.')
{
return 1;
}
if (a.FileName[0] == '.' && b.FileName[0] == '.')
{
if (a.FileName.Length == 1)
{
return 1;
}
if (b.FileName.Length == 1)
if (b.FileName[0] is not '.')
{
return -1;
}
return string.Compare(a.FileName[1..], b.FileName[1..]);
if (a.FileName.Length is 1)
{
return 1;
}
if (b.FileName.Length is 1)
{
return -1;
}
return string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture);
}
if (b.FileName[0] is '.')
{
return 1;
}
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)
{
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type)
{
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)
{
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type)
{
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)
{
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)
{
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)
{
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type)
{
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)
{
switch (a.FileName, b.FileName)
{
case ("..", ".."): return 0;
case ("..", _): return -1;
case (_, ".."): return 1;
}
if (a.Type != b.Type)
{
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)
@ -336,52 +440,17 @@ public partial class FileDialog
this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video));
}
private void SortFields(SortingField sortingField, bool canChangeOrder = false)
{
switch (sortingField)
private SortingField GetNewSorting(int column)
=> column switch
{
case SortingField.FileName:
if (canChangeOrder && sortingField == this.currentSortingField)
{
this.sortDescending[0] = !this.sortDescending[0];
}
this.files.Sort(this.sortDescending[0] ? SortByFileNameDesc : SortByFileNameAsc);
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();
}
0 when this.currentSortingField is SortingField.FileName => SortingField.FileNameDescending,
0 => SortingField.FileName,
1 when this.currentSortingField is SortingField.Type => SortingField.TypeDescending,
1 => SortingField.Type,
2 when this.currentSortingField is SortingField.Size => SortingField.SizeDescending,
2 => SortingField.Size,
3 when this.currentSortingField is SortingField.Date => SortingField.DateDescending,
3 => SortingField.Date,
_ => SortingField.None,
};
}

View file

@ -374,22 +374,8 @@ public partial class FileDialog
ImGui.PopID();
if (ImGui.IsItemClicked())
{
if (column == 0)
{
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);
}
var sorting = this.GetNewSorting(column);
this.SortFields(sorting);
}
}

View file

@ -9,13 +9,21 @@ namespace Dalamud.Interface.ImGuiFileDialog;
/// </summary>
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
/// <summary> Additional quick access items for the side bar.</summary>
public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = new();
#pragma warning disable SA1201
/// <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>
public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None;
#pragma warning restore SA1401
#pragma warning restore SA1201
private FileDialog? dialog;
private Action<bool, string>? callback;
@ -189,10 +197,41 @@ public class FileDialogManager
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);
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;
foreach (var (name, location, icon, position) in this.CustomSideBarItems)
this.dialog.SetQuickAccess(name, location, icon, position);
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>
/// <param name="width">The maximum width of the notification window.</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>
public float Draw(float width, float offsetY)
public float Draw(float width, float offsetY, Vector2 anchorPosition, NotificationSnapDirection snapDirection)
{
var opacity =
Math.Clamp(
@ -34,8 +36,8 @@ internal sealed partial class ActiveNotification
(NotificationConstants.ScaledWindowPadding * 2);
var viewport = ImGuiHelpers.MainViewport;
var viewportPos = viewport.WorkPos;
var viewportSize = viewport.WorkSize;
var viewportPos = viewport.Pos;
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity);
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f);
@ -51,13 +53,78 @@ internal sealed partial class ActiveNotification
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();
ImGui.SetNextWindowPos(
(viewportPos + viewportSize) -
new Vector2(NotificationConstants.ScaledViewportEdgeMargin) -
new Vector2(0, offsetY),
topLeft + viewportPos,
ImGuiCond.Always,
Vector2.One);
pivot);
ImGui.SetNextWindowSizeConstraints(
new(width, actionWindowHeight),
new(
@ -145,7 +212,7 @@ internal sealed partial class ActiveNotification
ImGui.PopStyleColor();
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>

View file

@ -54,6 +54,11 @@ internal static class NotificationConstants
/// </summary>
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>
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7);

View file

@ -1,7 +1,9 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Gui;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
@ -21,9 +23,14 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private readonly List<ActiveNotification> notifications = new();
private readonly ConcurrentBag<ActiveNotification> pendingNotifications = new();
private NotificationPositionChooser? positionChooser;
[ServiceManager.ServiceConstructor]
private NotificationManager(FontAtlasFactory fontAtlasFactory)
{
@ -47,6 +54,48 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe
/// <summary>Gets the private atlas for use with notification windows.</summary>
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/>
public void DisposeService()
{
@ -97,25 +146,38 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe
/// <summary>Draw all currently queued notifications.</summary>
public void Draw()
{
var viewportSize = ImGuiHelpers.MainViewport.WorkSize;
var height = 0f;
var uiHidden = this.gameGui.GameUiHidden;
while (this.pendingNotifications.TryTake(out var newNotification))
this.notifications.Add(newNotification);
var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X;
width += NotificationConstants.ScaledWindowPadding * 3;
width += NotificationConstants.ScaledIconSize;
width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth);
var width = CalculateNotificationWidth();
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)
{
if (uiHidden && tn.RespectUiHidden)
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",
"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)
@ -387,4 +392,19 @@ internal class DalamudCommands : IServiceType
: Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard.");
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.Configuration.Internal;
using Dalamud.Console;
using Dalamud.Data;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Gui;
using Dalamud.Game.Internal;
using Dalamud.Hooking;
using Dalamud.Interface.Animation.EasingFunctions;
using Dalamud.Interface.Colors;
@ -305,8 +303,14 @@ internal class DalamudInterface : IInternalDisposableService
/// <summary>
/// Opens the <see cref="ConsoleWindow"/>.
/// </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.BringToFront();
}
@ -518,7 +522,7 @@ internal class DalamudInterface : IInternalDisposableService
/// <summary>
/// Toggle the screen darkening effect used for the credits.
/// </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)
{
this.isCreditsDarkening = status;
@ -713,19 +717,6 @@ internal class DalamudInterface : IInternalDisposableService
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();
if (ImGui.MenuItem("Open Data window"))
@ -1012,7 +1003,7 @@ internal class DalamudInterface : IInternalDisposableService
if (ImGui.MenuItem("Scan dev plugins"))
{
pluginManager.ScanDevPlugins();
_ = pluginManager.ScanDevPluginsAsync();
}
ImGui.Separator();

View file

@ -34,13 +34,14 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using FFXIVClientStructs.FFXIV.Client.Graphics.Environment;
using JetBrains.Annotations;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
using DWMWINDOWATTRIBUTE = Windows.Win32.Graphics.Dwm.DWMWINDOWATTRIBUTE;
// general dev notes, here because it's easiest
@ -198,7 +199,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public IImGuiBackend? Backend => this.backend;
/// <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>
public bool OverrideGameCursor
{
@ -217,7 +218,7 @@ internal partial class InterfaceManager : IInternalDisposableService
public bool IsReady => this.backend != null;
/// <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>
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.
// 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.
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;
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();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText02);
ImGui.Text("Text3: ");
ImGui.Text("AvailableLines: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText03);
Service<SeStringRenderer>.Get().Draw(textInputComponent->AvailableLines);
ImGui.Text("Text4: ");
ImGui.Text("HighlightedAutoTranslateOptionColorPrefix: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText04);
Service<SeStringRenderer>.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorPrefix);
ImGui.Text("Text5: ");
ImGui.Text("HighlightedAutoTranslateOptionColorSuffix: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText05);
Service<SeStringRenderer>.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorSuffix);
break;
}

View file

@ -58,7 +58,13 @@ internal unsafe class ComponentNodeTree : ResNodeTree
/// <inheritdoc/>
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;
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(
$"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}");
ImGui.TextUnformatted(
$"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}");
$"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}");
ImGui.TextUnformatted(
$"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}");
$"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}");
ImGui.TextUnformatted(
$"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}");
$"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}");
break;
case List:
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>
internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) =>
addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree
: (int)node->Type > 1000
: (int)node->Type >= 1000
? new ComponentNodeTree(node, addonTree)
: node->Type switch
{

View file

@ -51,9 +51,10 @@ public readonly unsafe partial struct TimelineTree
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);
@ -65,22 +66,35 @@ public readonly unsafe partial struct TimelineTree
ShowStruct(this.NodeTimeline);
PrintFieldValuePairs(
("Id", $"{this.NodeTimeline->Resource->Id}"),
("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"),
("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})"));
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");
for (var a = 0; a < count; a++)
if (this.Resource->Animations is not null)
{
var animation = this.Resource->Animations[a];
var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation;
this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation))));
PrintFieldValuePairs(
("Id", $"{this.NodeTimeline->Resource->Id}"),
("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"),
("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})"));
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");
for (var a = 0; a < animationCount; a++)
{
var animation = this.Resource->Animations[a];
var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation;
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)
@ -380,4 +394,63 @@ public readonly unsafe partial struct TimelineTree
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>
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/>
public override void OnOpen()
{
@ -620,24 +633,29 @@ internal class ConsoleWindow : Window, IDisposable
ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)
|| ImGui.IsItemDeactivatedAfterEdit())
{
this.compiledLogFilter = null;
this.exceptionLogFilter = null;
try
{
this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase);
this.QueueRefilter();
}
catch (Exception e)
{
this.exceptionLogFilter = e;
}
foreach (var log in this.logText)
log.HighlightMatches = null;
this.RecompileLogFilter();
}
}
private void RecompileLogFilter()
{
this.compiledLogFilter = null;
this.exceptionLogFilter = null;
try
{
this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase);
this.QueueRefilter();
}
catch (Exception e)
{
this.exceptionLogFilter = e;
}
foreach (var log in this.logText)
log.HighlightMatches = null;
}
private void DrawSettingsPopup()
{
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System.Linq;
using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Gui.FlyText;
@ -38,13 +39,15 @@ internal class FlyTextWidget : IDataWindowWidget
/// <inheritdoc/>
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));
for (var i = 0; i < names.Length; i++)
var values = Enum.GetValues<FlyTextKind>().Distinct();
foreach (var value in values)
{
if (ImGui.Selectable($"{names[i]} ({i})"))
this.flyKind = (FlyTextKind)i;
if (ImGui.Selectable($"{value} ({(int)value})"))
{
this.flyKind = value;
}
}
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.Game;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@ -11,17 +17,40 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying hook information.
/// </summary>
internal class HookWidget : IDataWindowWidget
internal unsafe class HookWidget : IDataWindowWidget
{
private readonly List<IDalamudHook> hookStressTestList = [];
private Hook<MessageBoxWDelegate>? messageBoxMinHook;
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(
IntPtr hWnd,
[MarshalAs(UnmanagedType.LPWStr)] string text,
[MarshalAs(UnmanagedType.LPWStr)] string caption,
MESSAGEBOX_STYLE type);
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
private enum StressTestHookTarget
{
MessageBoxW,
AddonFinalize,
Random,
}
/// <inheritdoc/>
public string DisplayName { get; init; } = "Hook";
@ -35,6 +64,9 @@ internal class HookWidget : IDataWindowWidget
public void Load()
{
this.Ready = true;
this.address = new AddonLifecycleAddressResolver();
this.address.Setup(Service<TargetSigScanner>.Get());
}
/// <inheritdoc/>
@ -42,7 +74,9 @@ internal class HookWidget : IDataWindowWidget
{
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"))
this.messageBoxMinHook = Hook<MessageBoxWDelegate>.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook);
@ -67,18 +101,94 @@ internal class HookWidget : IDataWindowWidget
if (this.messageBoxMinHook != null)
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)
{
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)
{
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)
{
@ -87,4 +197,52 @@ internal class HookWidget : IDataWindowWidget
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 TextureManager textureManager;
private InventoryType? selectedInventoryType = InventoryType.Inventory1;
private GameInventoryType? selectedInventoryType = GameInventoryType.Inventory1;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["inv", "inventory"];
@ -53,7 +53,7 @@ internal class InventoryWidget : IDataWindowWidget
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
this.DrawInventoryType((InventoryType)this.selectedInventoryType);
this.DrawInventoryType(this.selectedInventoryType.Value);
}
private static string StripSoftHypen(string input)
@ -71,9 +71,9 @@ internal class InventoryWidget : IDataWindowWidget
ImGui.TableSetupScrollFreeze(2, 1);
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);
@ -95,7 +95,7 @@ internal class InventoryWidget : IDataWindowWidget
if (ImGui.MenuItem("Copy Address"))
{
var container = InventoryManager.Instance()->GetInventoryContainer(inventoryType);
var container = InventoryManager.Instance()->GetInventoryContainer((InventoryType)inventoryType);
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)
{
ImGui.TextUnformatted($"{inventoryType} is empty.");

View file

@ -20,7 +20,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
internal class NounProcessorWidget : IDataWindowWidget
{
/// <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 = [
typeof(Aetheryte),
@ -155,7 +155,7 @@ internal class NounProcessorWidget : IDataWindowWidget
GrammaticalCase = grammaticalCase,
};
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}\"),");
}
}

View file

@ -7,6 +7,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.Noun.Enums;
using Dalamud.Game.Text.SeStringHandling;
@ -90,6 +91,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
{ MacroCode.LowerHead, ["String"] },
{ MacroCode.ColorType, ["ColorType"] },
{ MacroCode.EdgeColorType, ["ColorType"] },
{ MacroCode.Ruby, ["StandardText", "RubyText"] },
{ MacroCode.Digit, ["Value", "TargetLength"] },
{ MacroCode.Ordinal, ["Value"] },
{ MacroCode.Sound, ["IsJingle", "SoundId"] },
@ -132,8 +134,8 @@ internal class SeStringCreatorWidget : IDataWindowWidget
new TextEntry(TextEntryType.Macro, "<colortype(17)>"),
new TextEntry(TextEntryType.Macro, "<edgecolortype(19)>"),
new TextEntry(TextEntryType.String, "Dalamud"),
new TextEntry(TextEntryType.Macro, "<edgecolor(0)>"),
new TextEntry(TextEntryType.Macro, "<colortype(0)>"),
new TextEntry(TextEntryType.Macro, "<edgecolor(stackcolor)>"),
new TextEntry(TextEntryType.Macro, "<color(stackcolor)>"),
new TextEntry(TextEntryType.Macro, " <string(lstr1)>"),
];
@ -165,7 +167,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.language = Service<DalamudConfiguration>.Get().EffectiveLanguage.ToClientLanguage();
this.language = Service<ClientState>.Get().ClientLanguage;
this.UpdateInputString(false);
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)
@ -1011,7 +1018,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget
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.TextUnformatted(NounProcessorWidget.GermanCases[u32]);

View file

@ -237,29 +237,42 @@ internal class ServicesWidget : IDataWindowWidget
}
}
if (ImGui.CollapsingHeader("Plugin-facing Services"))
if (ImGui.CollapsingHeader("Singleton Services"))
{
foreach (var instance in container.Instances)
{
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
var isPublic = instance.Key.IsPublic;
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
{
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
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))
{
ImGui.Text("\t => Exposed to plugins!");
ImGui.Text(
hasInterface
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
: "\t => NO INTERFACE!!!");
}
break;
default:
throw new ArgumentOutOfRangeException();
}
ImGuiHelpers.ScaledDummy(2);
}
}

View file

@ -180,6 +180,15 @@ internal class TexWidget : IDataWindowWidget
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)))
{
ImGui.PushID(nameof(this.DrawGetFromGameIcon));

View file

@ -226,6 +226,7 @@ internal class PluginInstallerWindow : Window, IDisposable
IsInstallableOutdated = 1 << 5,
IsOrphan = 1 << 6,
IsTesting = 1 << 7,
IsIncompatible = 1 << 8,
}
private enum InstalledPluginListFilter
@ -281,11 +282,15 @@ internal class PluginInstallerWindow : Window, IDisposable
var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync();
Service<PluginManager>.Get().ScanDevPlugins();
_ = pluginManager.ScanDevPluginsAsync();
if (!this.isSearchTextPrefilled)
{
this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
}
if (!this.isSearchTextPrefilled) this.searchText = string.Empty;
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
this.adaptiveSort = true;
if (this.updateStatus == OperationStatus.Complete || this.updateStatus == OperationStatus.Idle)
@ -361,11 +366,20 @@ internal class PluginInstallerWindow : Window, IDisposable
{
this.isSearchTextPrefilled = false;
this.searchText = string.Empty;
if (this.sortKind == PluginSortKind.SearchScore)
{
this.sortKind = PluginSortKind.Alphabetical;
this.filterText = Locs.SortBy_Alphabetical;
this.ResortPlugins();
}
}
else
{
this.isSearchTextPrefilled = true;
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();
}
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)
{
switch (kind)
@ -486,6 +530,12 @@ internal class PluginInstallerWindow : Window, IDisposable
// Plugins category
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
break;
case PluginInstallerOpenKind.DalamudChangelogs:
// Changelog group
this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog;
// Dalamud category
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.DalamudChangelogs;
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
@ -523,7 +573,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.GetWindowDrawList().AddRectFilled(
ImGui.GetWindowPos() + new Vector2(0, titleHeight),
ImGui.GetWindowPos() + windowSize,
0xCC000000,
ImGui.ColorConvertFloat4ToU32(new(0f, 0f, 0f, 0.8f * ImGui.GetStyle().Alpha)),
ImGui.GetStyle().WindowRounding,
ImDrawFlags.RoundCornersBottom);
ImGui.PopClipRect();
@ -555,40 +605,29 @@ internal class PluginInstallerWindow : Window, IDisposable
if (pluginManager.PluginsReady && !pluginManager.ReposReady)
{
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)
{
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
{
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;
@ -599,11 +638,27 @@ internal class PluginInstallerWindow : Window, IDisposable
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);
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();
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();
if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
{
pluginManager.ScanDevPlugins();
_ = pluginManager.ScanDevPluginsAsync();
}
}
@ -2137,7 +2192,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha);
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
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);
else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated))
ImGui.Image(this.imageCache.OutdatedInstallableIcon.Handle, iconSize);
@ -2213,9 +2268,14 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPos(cursor);
// 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 + " ";
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
@ -2224,7 +2284,6 @@ internal class PluginInstallerWindow : Window, IDisposable
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
ImGui.TextWrapped(bodyText);
ImGui.PopStyleColor();
}
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 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
if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null)
{
@ -2417,6 +2484,11 @@ internal class PluginInstallerWindow : Window, IDisposable
label += Locs.PluginTitleMod_TestingAvailable;
}
if (isIncompatible)
{
label += Locs.PluginTitleMod_Incompatible;
}
var isThirdParty = manifest.SourceRepo.IsThirdParty;
ImGui.PushID($"available{index}{manifest.InternalName}");
@ -2430,6 +2502,8 @@ internal class PluginInstallerWindow : Window, IDisposable
flags |= PluginHeaderFlags.IsInstallableOutdated;
if (useTesting || manifest.IsTestingExclusive)
flags |= PluginHeaderFlags.IsTesting;
if (isIncompatible)
flags |= PluginHeaderFlags.IsIncompatible;
if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index))
{
@ -2457,9 +2531,6 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(5);
// Controls
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress || isOutdated;
var versionString = useTesting
? $"{manifest.TestingAssemblyVersion}"
: $"{manifest.AssemblyVersion}";
@ -2468,7 +2539,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode);
}
else if (disabled)
else if (!enableInstallButton)
{
ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString));
}
@ -2713,7 +2784,7 @@ internal class PluginInstallerWindow : Window, IDisposable
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 didDrawApplicableChangelogInsideCollapsible = false;
@ -3096,11 +3167,13 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle);
}
else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled)
else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled || pluginManager.SafeMode)
{
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);
else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name));
@ -3494,6 +3567,24 @@ internal class PluginInstallerWindow : Window, IDisposable
{
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_Incompatible => Loc.Localize("InstallerTitleModIncompatible", " (incompatible)");
public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)");
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_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_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_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_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_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);
#endregion

View file

@ -224,6 +224,9 @@ internal class ProfileManagerWidget
ImGuiHelpers.ScaledDummy(3);
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.SameLine();
@ -263,6 +266,17 @@ internal class ProfileManagerWidget
didAny = true;
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)
@ -386,10 +400,19 @@ internal class ProfileManagerWidget
ImGuiHelpers.ScaledDummy(5);
var enableAtBoot = profile.AlwaysEnableAtBoot;
if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot))
ImGui.TextUnformatted(Locs.StartupBehavior);
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);
@ -514,6 +537,15 @@ internal class ProfileManagerWidget
if (ImGui.IsItemHovered())
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)
@ -554,6 +586,9 @@ internal class ProfileManagerWidget
private static class Locs
{
public static string StartupBehavior =>
Loc.Localize("ProfileManagerStartupBehavior", "Startup behavior");
public static string TooltipEnableDisable =>
Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection");
@ -567,9 +602,6 @@ internal class ProfileManagerWidget
public static string NoPluginsInProfile =>
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 CopyToClipboardHint =>
@ -647,5 +679,22 @@ internal class ProfileManagerWidget
public static string NotInstalled(string 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.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
@ -44,51 +45,54 @@ internal class PluginStatWindow : Window
{
var pluginManager = Service<PluginManager>.Get();
if (!ImGui.BeginTabBar("Stat Tabs"))
using var tabBar = ImRaii.TabBar("Stat Tabs");
if (!tabBar)
return;
if (ImGui.BeginTabItem("Draw times"))
using (var tabItem = ImRaii.TabItem("Draw times"))
{
var doStats = UiBuilder.DoStats;
if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats))
if (tabItem)
{
UiBuilder.DoStats = doStats;
}
var doStats = UiBuilder.DoStats;
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats))
{
foreach (var plugin in pluginManager.InstalledPlugins)
{
if (plugin.DalamudInterface != null)
{
plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear();
}
}
UiBuilder.DoStats = doStats;
}
var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded);
var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0);
var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0);
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
foreach (var plugin in pluginManager.InstalledPlugins)
{
if (plugin.DalamudInterface != null)
{
plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1;
plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear();
}
}
}
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times");
var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded);
var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0);
var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0);
ImGui.InputTextWithHint(
"###PluginStatWindow_DrawSearch",
"Search",
ref this.drawSearchText,
500);
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times");
if (ImGui.BeginTable(
ImGui.InputTextWithHint(
"###PluginStatWindow_DrawSearch",
"Search",
ref this.drawSearchText,
500);
using var table = ImRaii.Table(
"##PluginStatsDrawTimes",
4,
ImGuiTableFlags.RowBg
@ -97,99 +101,100 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable))
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Plugin");
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort
ImGui.TableSetupColumn("Longest");
ImGui.TableSetupColumn("Average");
ImGui.TableHeadersRow();
| ImGuiTableFlags.Hideable);
var sortSpecs = ImGui.TableGetSortSpecs();
loadedPlugins = sortSpecs.Specs.ColumnIndex switch
if (table)
{
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.Name)
: loadedPlugins.OrderByDescending(plugin => plugin.Name),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0),
_ => loadedPlugins,
};
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Plugin");
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort
ImGui.TableSetupColumn("Longest");
ImGui.TableSetupColumn("Average");
ImGui.TableHeadersRow();
foreach (var plugin in loadedPlugins)
{
if (!this.drawSearchText.IsNullOrEmpty()
&& !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase))
var sortSpecs = ImGui.TableGetSortSpecs();
loadedPlugins = sortSpecs.Specs.ColumnIndex switch
{
continue;
}
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.Name)
: loadedPlugins.OrderByDescending(plugin => plugin.Name),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0),
_ => loadedPlugins,
};
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(plugin.Manifest.Name);
if (plugin.DalamudInterface != null)
foreach (var plugin in loadedPlugins)
{
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms");
if (!this.drawSearchText.IsNullOrEmpty()
&& !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase))
{
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms");
ImGui.Text(plugin.Manifest.Name);
ImGui.TableNextColumn();
ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0
? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms"
: "-");
if (plugin.DalamudInterface != null)
{
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms");
ImGui.TableNextColumn();
ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0
? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms"
: "-");
}
}
}
ImGui.EndTable();
}
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Framework times"))
using (var tabItem = ImRaii.TabItem("Framework times"))
{
var doStats = Framework.StatsEnabled;
if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats))
if (tabItem)
{
Framework.StatsEnabled = doStats;
}
var doStats = Framework.StatsEnabled;
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats))
{
Framework.StatsHistory.Clear();
Framework.StatsEnabled = doStats;
}
var statsHistory = Framework.StatsHistory.ToArray();
var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault());
var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average());
if (doStats)
{
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
Framework.StatsHistory.Clear();
}
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times");
var statsHistory = Framework.StatsHistory.ToArray();
var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault());
var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average());
ImGui.InputTextWithHint(
"###PluginStatWindow_FrameworkSearch",
"Search",
ref this.frameworkSearchText,
500);
ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together");
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times");
if (ImGui.BeginTable(
ImGui.InputTextWithHint(
"###PluginStatWindow_FrameworkSearch",
"Search",
ref this.frameworkSearchText,
500);
using var table = ImRaii.Table(
"##PluginStatsFrameworkTimes",
4,
ImGuiTableFlags.RowBg
@ -198,77 +203,77 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable))
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort
ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50);
ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50);
ImGui.TableHeadersRow();
var sortSpecs = ImGui.TableGetSortSpecs();
statsHistory = sortSpecs.Specs.ColumnIndex switch
| ImGuiTableFlags.Hideable);
if (table)
{
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Key).ToArray()
: statsHistory.OrderByDescending(handler => handler.Key).ToArray(),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(),
_ => statsHistory,
};
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort
ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50);
ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50);
ImGui.TableHeadersRow();
foreach (var handlerHistory in statsHistory)
{
if (!handlerHistory.Value.Any())
var sortSpecs = ImGui.TableGetSortSpecs();
statsHistory = sortSpecs.Specs.ColumnIndex switch
{
continue;
}
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Key).ToArray()
: statsHistory.OrderByDescending(handler => handler.Key).ToArray(),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray()
: statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(),
_ => statsHistory,
};
if (!this.frameworkSearchText.IsNullOrEmpty()
&& handlerHistory.Key != null
&& !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase))
foreach (var handlerHistory in statsHistory)
{
continue;
if (!handlerHistory.Value.Any())
{
continue;
}
if (!this.frameworkSearchText.IsNullOrEmpty()
&& handlerHistory.Key != null
&& !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase))
{
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Key}");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Last():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Max():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Key}");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Last():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Max():F4}ms");
ImGui.TableNextColumn();
ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
}
ImGui.EndTable();
}
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Hooks"))
using (var tabItem = ImRaii.TabItem("Hooks"))
{
ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks);
if (tabItem)
{
ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks);
ImGui.InputTextWithHint(
"###PluginStatWindow_HookSearch",
"Search",
ref this.hookSearchText,
500);
ImGui.InputTextWithHint(
"###PluginStatWindow_HookSearch",
"Search",
ref this.hookSearchText,
500);
if (ImGui.BeginTable(
using var table = ImRaii.Table(
"##PluginStatsHooks",
4,
ImGuiTableFlags.RowBg
@ -276,80 +281,78 @@ internal class PluginStatWindow : Window
| ImGuiTableFlags.Resizable
| ImGuiTableFlags.ScrollY
| ImGuiTableFlags.Reorderable
| ImGuiTableFlags.Hideable))
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100);
ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40);
ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40);
ImGui.TableHeadersRow();
foreach (var (guid, trackedHook) in HookManager.TrackedHooks)
| ImGuiTableFlags.Hideable);
if (table)
{
try
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250);
ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100);
ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40);
ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40);
ImGui.TableHeadersRow();
foreach (var (guid, trackedHook) in HookManager.TrackedHooks)
{
if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly())
continue;
if (!this.hookSearchText.IsNullOrEmpty())
try
{
if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
&& !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly())
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
ImGui.TableNextColumn();
if (!trackedHook.Hook.IsDisposed)
{
if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}"))
if (!this.hookSearchText.IsNullOrEmpty())
{
ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
Service<NotificationManager>.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success);
if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
&& !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase))
continue;
}
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue)
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
ImGui.TableNextColumn();
if (!trackedHook.Hook.IsDisposed)
{
if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}"))
if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}"))
{
ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
Service<NotificationManager>.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success);
ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
Service<NotificationManager>.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success);
}
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue)
{
if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}"))
{
ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
Service<NotificationManager>.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success);
}
}
}
ImGui.TableNextColumn();
if (trackedHook.Hook.IsDisposed)
{
ImGui.Text("Disposed");
}
else
{
ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled");
}
ImGui.TableNextColumn();
ImGui.Text(trackedHook.Hook.BackendName);
}
ImGui.TableNextColumn();
if (trackedHook.Hook.IsDisposed)
catch (Exception ex)
{
ImGui.Text("Disposed");
Log.Error(ex, "Error drawing hooks in plugin stats");
}
else
{
ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled");
}
ImGui.TableNextColumn();
ImGui.Text(trackedHook.Hook.BackendName);
}
catch (Exception ex)
{
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.Globalization;
using System.Linq;
using System.Numerics;
@ -54,6 +55,7 @@ internal class SelfTestWindow : Window
new SheetRedirectResolverSelfTestStep(),
new NounProcessorSelfTestStep(),
new SeStringEvaluatorSelfTestStep(),
new CompletionSelfTestStep(),
new LogoutEventSelfTestStep()
];
@ -61,7 +63,7 @@ internal class SelfTestWindow : Window
private bool selfTestRunning = false;
private int currentStep = 0;
private int scrollToStep = -1;
private DateTimeOffset lastTestStart;
/// <summary>
@ -90,9 +92,10 @@ internal class SelfTestWindow : Window
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.currentStep++;
this.scrollToStep = this.currentStep;
this.lastTestStart = DateTimeOffset.Now;
if (this.currentStep >= this.steps.Count)
@ -107,6 +110,7 @@ internal class SelfTestWindow : Window
{
this.selfTestRunning = true;
this.currentStep = 0;
this.scrollToStep = this.currentStep;
this.testIndexToResult.Clear();
this.lastTestStart = DateTimeOffset.Now;
}
@ -116,11 +120,11 @@ internal class SelfTestWindow : Window
ImGui.TextUnformatted($"Step: {this.currentStep} / {this.steps.Count}");
ImGuiHelpers.ScaledDummy(10);
ImGui.Spacing();
this.DrawResultTable();
ImGuiHelpers.ScaledDummy(10);
ImGui.Spacing();
if (this.currentStep >= this.steps.Count)
{
@ -131,11 +135,11 @@ internal class SelfTestWindow : Window
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
{
ImGui.TextColored(ImGuiColors.HealerGreen, "All checks passed!");
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "All checks passed!");
}
return;
@ -146,7 +150,8 @@ internal class SelfTestWindow : Window
return;
}
ImGui.Separator();
using var resultChild = ImRaii.Child("SelfTestResultChild", ImGui.GetContentRegionAvail());
if (!resultChild) return;
var step = this.steps[this.currentStep];
ImGui.TextUnformatted($"Current: {step.Name}");
@ -164,13 +169,12 @@ internal class SelfTestWindow : Window
result = SelfTestStepResult.Fail;
}
ImGui.Separator();
if (result != SelfTestStepResult.Waiting)
{
var duration = DateTimeOffset.Now - this.lastTestStart;
this.testIndexToResult.Add(this.currentStep, (result, duration));
this.testIndexToResult[this.currentStep] = (result, duration);
this.currentStep++;
this.scrollToStep = this.currentStep;
this.lastTestStart = DateTimeOffset.Now;
}
@ -178,90 +182,111 @@ internal class SelfTestWindow : Window
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("Name");
ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
for (var i = 0; i < this.steps.Count; i++)
{
ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale);
var step = this.steps[i];
ImGui.TableNextRow();
ImGui.TableHeadersRow();
for (var i = 0; i < this.steps.Count; i++)
if (this.selfTestRunning && this.currentStep == i)
{
var step = this.steps[i];
ImGui.TableNextRow();
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableRowBgAlt));
}
ImGui.TableSetColumnIndex(0);
ImGui.Text(i.ToString());
ImGui.TableSetColumnIndex(0);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(i.ToString());
ImGui.TableSetColumnIndex(1);
ImGui.Text(step.Name);
if (this.selfTestRunning && this.scrollToStep == i)
{
ImGui.SetScrollHereY();
this.scrollToStep = -1;
}
if (this.testIndexToResult.TryGetValue(i, out var result))
ImGui.TableSetColumnIndex(1);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(step.Name);
if (this.testIndexToResult.TryGetValue(i, out var result))
{
ImGui.TableSetColumnIndex(2);
ImGui.AlignTextToFramePadding();
switch (result.Result)
{
ImGui.TableSetColumnIndex(2);
ImGui.PushFont(InterfaceManager.MonoFont);
case SelfTestStepResult.Pass:
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "PASS");
break;
case SelfTestStepResult.Fail:
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "FAIL");
break;
default:
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR");
break;
}
switch (result.Result)
{
case SelfTestStepResult.Pass:
ImGui.TextColored(ImGuiColors.HealerGreen, "PASS");
break;
case SelfTestStepResult.Fail:
ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL");
break;
default:
ImGui.TextColored(ImGuiColors.DalamudGrey, "NR");
break;
}
ImGui.PopFont();
ImGui.TableSetColumnIndex(3);
if (result.Duration.HasValue)
{
ImGui.TextUnformatted(result.Duration.Value.ToString("g"));
}
ImGui.TableSetColumnIndex(3);
if (result.Duration.HasValue)
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(this.FormatTimeSpan(result.Duration.Value));
}
}
else
{
ImGui.TableSetColumnIndex(2);
ImGui.AlignTextToFramePadding();
if (this.selfTestRunning && this.currentStep == i)
{
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "WAIT");
}
else
{
ImGui.TableSetColumnIndex(2);
if (this.selfTestRunning && this.currentStep == i)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "WAIT");
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "NR");
}
ImGui.TableSetColumnIndex(3);
if (this.selfTestRunning && this.currentStep == i)
{
ImGui.TextUnformatted((DateTimeOffset.Now - this.lastTestStart).ToString("g"));
}
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR");
}
ImGui.TableSetColumnIndex(4);
using var id = ImRaii.PushId($"selfTest{i}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward))
ImGui.TableSetColumnIndex(3);
ImGui.AlignTextToFramePadding();
if (this.selfTestRunning && this.currentStep == i)
{
this.StopTests();
this.testIndexToResult.Remove(i);
this.currentStep = i;
this.selfTestRunning = true;
this.lastTestStart = DateTimeOffset.Now;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Jump to this test");
ImGui.TextUnformatted(this.FormatTimeSpan(DateTimeOffset.Now - this.lastTestStart));
}
}
ImGui.EndTable();
ImGui.TableSetColumnIndex(4);
using var id = ImRaii.PushId($"selfTest{i}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward))
{
this.StopTests();
this.testIndexToResult.Remove(i);
this.currentStep = i;
this.selfTestRunning = true;
this.lastTestStart = DateTimeOffset.Now;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Jump to this test");
}
}
}
@ -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;
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;
}
else

View file

@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps;
/// Test setup for Lumina.
/// </summary>
/// <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
where T : struct, IExcelRow<T>
{

View file

@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Linq;
using Dalamud.Bindings.ImGui;
@ -109,7 +109,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
}
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.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}");
ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}");
@ -135,7 +135,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
}
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.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}");
ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}");
@ -156,7 +156,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep
case SubStep.Taxes:
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
{

View file

@ -61,6 +61,10 @@ internal class SheetRedirectResolverSelfTestStep : ISelfTestStep
new("WeatherPlaceName", 40),
new("WeatherPlaceName", 52),
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);

View file

@ -11,12 +11,12 @@ public abstract class SettingsEntry
public string? Name { get; protected set; }
/// <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>
public virtual bool IsValid { get; protected set; } = true;
/// <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>
public virtual bool IsVisible { get; protected set; } = true;
@ -54,7 +54,7 @@ public abstract class SettingsEntry
{
// ignored
}
/// <summary>
/// Function to be called when the tab is closed.
/// </summary>

View file

@ -21,6 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
public class SettingsTabAutoUpdates : SettingsTab
{
private AutoUpdateBehavior behavior;
private bool updateDisabledPlugins;
private bool checkPeriodically;
private bool chatNotification;
private string pickerSearch = string.Empty;
@ -65,6 +66,7 @@ public class SettingsTabAutoUpdates : SettingsTab
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("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint",
@ -236,6 +238,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get();
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
this.updateDisabledPlugins = configuration.UpdateDisabledPlugins;
this.chatNotification = configuration.SendUpdateNotificationToChat;
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
@ -248,6 +251,7 @@ public class SettingsTabAutoUpdates : SettingsTab
var configuration = Service<DalamudConfiguration>.Get();
configuration.AutoUpdateBehavior = this.behavior;
configuration.UpdateDisabledPlugins = this.updateDisabledPlugins;
configuration.SendUpdateNotificationToChat = this.chatNotification;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;

View file

@ -60,7 +60,8 @@ public class SettingsTabExperimental : SettingsTab
"Enable ImGui asserts"),
Loc.Localize(
"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,
(v, _) => Service<InterfaceManager>.Get().ShowAsserts = v),

View file

@ -12,6 +12,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ImGuiFontChooserDialog;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.ManagedFontAtlas.Internals;
@ -38,7 +39,7 @@ public class SettingsTabLook : SettingsTab
private IFontSpec defaultFontSpec = null!;
public override SettingsEntry[] Entries { get; } =
{
[
new GapSettingsEntry(5, true),
new ButtonSettingsEntry(
@ -46,6 +47,11 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."),
() => 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>(
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."),
@ -167,8 +173,8 @@ public class SettingsTabLook : SettingsTab
ImGui.TextUnformatted("\uE020\uE021\uE022\uE023\uE024\uE025\uE026\uE027");
ImGui.PopStyleVar(1);
},
},
};
}
];
public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel");

View file

@ -51,7 +51,7 @@ public class DevPluginsSettingsEntry : SettingsEntry
if (this.devPluginLocationsChanged)
{
Service<PluginManager>.Get().ScanDevPlugins();
_ = Service<PluginManager>.Get().ScanDevPluginsAsync();
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.WindowPadding, new Vector2(0, 0));
if (this.state == State.Show)
ImGui.SetNextWindowFocus();
base.PreDraw();
}

View file

@ -101,8 +101,8 @@ public class StyleModelV1 : StyleModel
{ "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1) },
{ "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1) },
{ "PlotLinesHovered", new Vector4(1, 0.43f, 0.35f, 1) },
{ "PlotHistogram", new Vector4(0.9f, 0.7f, 0, 1) },
{ "PlotHistogramHovered", new Vector4(1, 0.6f, 0, 1) },
{ "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) },
{ "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) },
{ "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1) },
{ "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 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) },
{ "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1f) },
{ "PlotLinesHovered", new Vector4(1f, 0.43f, 0.35f, 1f) },
{ "PlotHistogram", new Vector4(0.9f, 0.7f, 0f, 1f) },
{ "PlotHistogramHovered", new Vector4(1f, 0.6f, 0f, 1f) },
{ "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) },
{ "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) },
{ "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1f) },
{ "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 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.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Plugin.Services;
@ -173,7 +172,7 @@ internal sealed partial class TextureManager
ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested();
try
@ -204,7 +203,7 @@ internal sealed partial class TextureManager
string path,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
cancellationToken.ThrowIfCancellationRequested();
try
@ -359,11 +358,18 @@ internal sealed partial class TextureManager
/// <param name="handle">An instance of <see cref="MemoryHandle"/>.</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(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>);
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>);
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.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
using Dalamud.Utility.TerraFxCom;
@ -48,10 +50,11 @@ internal sealed partial class TextureManager
[ServiceManager.ServiceDependency]
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly CancellationTokenSource disposeCts = new();
private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader;
private SharedTextureManager? sharedTextureManager;
private WicManager? wicManager;
private bool disposing;
private ComPtr<ID3D11Device> device;
[ServiceManager.ServiceConstructor]
@ -104,10 +107,10 @@ internal sealed partial class TextureManager
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
if (this.disposing)
if (this.disposeCts.IsCancellationRequested)
return;
this.disposing = true;
this.disposeCts.Cancel();
Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose();
Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose();
@ -269,6 +272,21 @@ internal sealed partial class TextureManager
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/>
bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) =>
this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat);
@ -330,7 +348,7 @@ internal sealed partial class TextureManager
/// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
var buffer = file.TextureBuffer;
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false);
@ -354,7 +372,7 @@ internal sealed partial class TextureManager
/// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes)
{
ObjectDisposedException.ThrowIf(this.disposing, this);
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes))
throw new InvalidDataException("The file is not a TexFile.");

View file

@ -151,6 +151,10 @@ internal sealed class TextureManagerPluginScoped
return textureWrap;
}
/// <inheritdoc/>
public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) =>
this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName);
/// <inheritdoc/>
public async Task<IDalamudTextureWrap> CreateFromExistingTextureAsync(
IDalamudTextureWrap wrap,
@ -267,6 +271,17 @@ internal sealed class TextureManagerPluginScoped
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/>
public IEnumerable<IBitmapCodecInfo> GetSupportedImageDecoderInfos() =>
this.ManagerOrThrow.Wic.GetSupportedDecoderInfos();
@ -279,6 +294,9 @@ internal sealed class TextureManagerPluginScoped
return shared;
}
/// <inheritdoc/>
public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage();
/// <inheritdoc/>
public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture)
{
@ -411,6 +429,17 @@ internal sealed class TextureManagerPluginScoped
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) =>
this.InterceptTexDataLoad?.Invoke(path, ref replacementPath);
}

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