From b2e30f7cc172f596d71a1503642d39819a83c0be Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Wed, 10 Jul 2024 02:38:03 +0900 Subject: [PATCH 01/79] Add NamePlateGui --- Dalamud/Game/Gui/NamePlate/NamePlateGui.cs | 270 ++++++++ .../Game/Gui/NamePlate/NamePlateInfoView.cs | 93 +++ Dalamud/Game/Gui/NamePlate/NamePlateKind.cs | 57 ++ .../Gui/NamePlate/NamePlatePartsContainer.cs | 46 ++ .../Gui/NamePlate/NamePlateQuotedParts.cs | 73 +++ .../Gui/NamePlate/NamePlateSimpleParts.cs | 44 ++ .../Gui/NamePlate/NamePlateStringField.cs | 38 ++ .../Gui/NamePlate/NamePlateUpdateContext.cs | 146 +++++ .../Gui/NamePlate/NamePlateUpdateHandler.cs | 603 ++++++++++++++++++ Dalamud/Plugin/Services/INamePlateGui.cs | 37 ++ 10 files changed, 1407 insertions(+) create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateGui.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateKind.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs create mode 100644 Dalamud/Plugin/Services/INamePlateGui.cs diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs new file mode 100644 index 000000000..0ea2a5c56 --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs @@ -0,0 +1,270 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Game.ClientState.Objects; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// Class used to modify the data used when rendering nameplates. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui +{ + /// + /// The index for the number array used by the NamePlate addon. + /// + public const int NumberArrayIndex = 5; + + /// + /// The index for the string array used by the NamePlate addon. + /// + public const int StringArrayIndex = 4; + + /// + /// The index for of the FullUpdate entry in the NamePlate number array. + /// + internal const int NumberArrayFullUpdateIndex = 4; + + /// + /// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields. + /// + internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer(); + + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle addonLifecycle = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ObjectTable objectTable = Service.Get(); + + private readonly AddonLifecycleEventListener preRequestedUpdateListener; + + private NamePlateUpdateContext? context; + + private NamePlateUpdateHandler[] updateHandlers = []; + + [ServiceManager.ServiceConstructor] + private NamePlateGui() + { + this.preRequestedUpdateListener = new AddonLifecycleEventListener( + AddonEvent.PreRequestedUpdate, + "NamePlate", + this.OnPreRequestedUpdate); + + this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener); + } + + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate; + + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate; + + /// + public unsafe void RequestRedraw() + { + var addon = this.gameGui.GetAddonByName("NamePlate"); + if (addon != 0) + { + var raptureAtkModule = RaptureAtkModule.Instance(); + if (raptureAtkModule == null) + { + return; + } + + ((AddonNamePlate*)addon)->DoFullUpdate = 1; + var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex]; + namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1); + } + } + + /// + void IInternalDisposableService.DisposeService() + { + this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener); + } + + private static nint CreateEmptyStringPointer() + { + var pointer = Marshal.AllocHGlobal(1); + Marshal.WriteByte(pointer, 0, 0); + return pointer; + } + + private void CreateHandlers(NamePlateUpdateContext createdContext) + { + var handlers = new List(); + for (var i = 0; i < AddonNamePlate.NumNamePlateObjects; i++) + { + handlers.Add(new NamePlateUpdateHandler(createdContext, i)); + } + + this.updateHandlers = handlers.ToArray(); + } + + private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args) + { + if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null) + { + return; + } + + var reqArgs = (AddonRequestedUpdateArgs)args; + if (this.context == null) + { + this.context = new NamePlateUpdateContext(this.objectTable, reqArgs); + this.CreateHandlers(this.context); + } + else + { + this.context.ResetState(reqArgs); + } + + var activeNamePlateCount = this.context.ActiveNamePlateCount; + if (activeNamePlateCount == 0) + return; + + var activeHandlers = this.updateHandlers[..activeNamePlateCount]; + + if (this.context.IsFullUpdate) + { + foreach (var handler in activeHandlers) + { + handler.ResetState(); + } + + this.OnDataUpdate?.Invoke(this.context, activeHandlers); + this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers); + if (this.context.HasParts) + this.ApplyBuilders(activeHandlers); + } + else + { + var udpatedHandlers = new List(activeNamePlateCount); + foreach (var handler in activeHandlers) + { + handler.ResetState(); + if (handler.IsUpdating) + udpatedHandlers.Add(handler); + } + + if (this.OnDataUpdate is not null) + { + this.OnDataUpdate?.Invoke(this.context, activeHandlers); + this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers); + if (this.context.HasParts) + this.ApplyBuilders(activeHandlers); + } + else if (udpatedHandlers.Count != 0) + { + var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan(); + this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers); + if (this.context.HasParts) + this.ApplyBuilders(changedHandlersSpan); + } + } + } + + private void ApplyBuilders(Span handlers) + { + foreach (var handler in handlers) + { + if (handler.PartsContainer is { } container) + { + container.ApplyBuilders(handler); + } + } + } +} + +/// +/// Plugin-scoped version of a AddonEventManager service. +/// +[PluginInterface] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlateGui +{ + [ServiceManager.ServiceDependency] + private readonly NamePlateGui parentService = Service.Get(); + + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate + { + add + { + if (this.OnNamePlateUpdateScoped == null) + this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward; + this.OnNamePlateUpdateScoped += value; + } + + remove + { + this.OnNamePlateUpdateScoped -= value; + if (this.OnNamePlateUpdateScoped == null) + this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward; + } + } + + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate + { + add + { + if (this.OnDataUpdateScoped == null) + this.parentService.OnDataUpdate += this.OnDataUpdateForward; + this.OnDataUpdateScoped += value; + } + + remove + { + this.OnDataUpdateScoped -= value; + if (this.OnDataUpdateScoped == null) + this.parentService.OnDataUpdate -= this.OnDataUpdateForward; + } + } + + private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped; + + private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped; + + /// + public void RequestRedraw() + { + this.parentService.RequestRedraw(); + } + + /// + public void DisposeService() + { + this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward; + this.OnNamePlateUpdateScoped = null; + + this.parentService.OnDataUpdate -= this.OnDataUpdateForward; + this.OnDataUpdateScoped = null; + } + + private void OnNamePlateUpdateForward( + INamePlateUpdateContext context, IReadOnlyList handlers) + { + this.OnNamePlateUpdateScoped?.Invoke(context, handlers); + } + + private void OnDataUpdateForward( + INamePlateUpdateContext context, IReadOnlyList handlers) + { + this.OnDataUpdateScoped?.Invoke(context, handlers); + } +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs new file mode 100644 index 000000000..a51ed20c3 --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs @@ -0,0 +1,93 @@ +using Dalamud.Game.Text.SeStringHandling; + +using FFXIVClientStructs.FFXIV.Client.UI; + +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to +/// fields do not affect this data. +/// +public interface INamePlateInfoView +{ + /// + /// Gets the displayed name for this nameplate according to the nameplate info object. + /// + SeString Name { get; } + + /// + /// Gets the displayed free company tag for this nameplate according to the nameplate info object. + /// + SeString FreeCompanyTag { get; } + + /// + /// Gets the displayed title for this nameplate according to the nameplate info object. In this field, the quote + /// characters which appear on either side of the title are NOT included. + /// + SeString Title { get; } + + /// + /// Gets the displayed title for this nameplate according to the nameplate info object. In this field, the quote + /// characters which appear on either side of the title ARE included. + /// + SeString DisplayTitle { get; } + + /// + /// Gets the displayed level text for this nameplate according to the nameplate info object. + /// + SeString LevelText { get; } + + /// + /// Gets the flags for this nameplate according to the nameplate info object. + /// + int Flags { get; } + + /// + /// Gets a value indicating whether this nameplate is considered 'dirty' or not according to the nameplate + /// info object. + /// + bool IsDirty { get; } + + /// + /// Gets a value indicating whether the title for this nameplate is a prefix title or not according to the nameplate + /// info object. This value is derived from the field. + /// + bool IsPrefixTitle { get; } +} + +/// +/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to +/// fields do not affect this data. +/// +internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : INamePlateInfoView +{ + private SeString? name; + private SeString? freeCompanyTag; + private SeString? title; + private SeString? displayTitle; + private SeString? levelText; + + /// + public SeString Name => this.name ??= SeString.Parse(info->Name); + + /// + public SeString FreeCompanyTag => this.freeCompanyTag ??= SeString.Parse(info->FcName); + + /// + public SeString Title => this.title ??= SeString.Parse(info->Title); + + /// + public SeString DisplayTitle => this.displayTitle ??= SeString.Parse(info->DisplayTitle); + + /// + public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText); + + /// + public int Flags => info->Flags; + + /// + public bool IsDirty => info->IsDirty; + + /// + public bool IsPrefixTitle => ((info->Flags >> (8 * 3)) & 0xFF) == 1; +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs b/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs new file mode 100644 index 000000000..af41ae199 --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs @@ -0,0 +1,57 @@ +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// An enum describing what kind of game object this nameplate represents. +/// +public enum NamePlateKind : byte +{ + /// + /// A player character. + /// + PlayerCharacter = 0, + + /// + /// An event NPC or companion. + /// + EventNpcCompanion = 1, + + /// + /// A retainer. + /// + Retainer = 2, + + /// + /// An enemy battle NPC. + /// + BattleNpcEnemy = 3, + + /// + /// A friendly battle NPC. + /// + BattleNpcFriendly = 4, + + /// + /// An event object. + /// + EventObject = 5, + + /// + /// Treasure. + /// + Treasure = 6, + + /// + /// A gathering point. + /// + GatheringPoint = 7, + + /// + /// A battle NPC with subkind 6. + /// + BattleNpcSubkind6 = 8, + + /// + /// Something else. + /// + Other = 9, +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs b/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs new file mode 100644 index 000000000..c6f443c91 --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs @@ -0,0 +1,46 @@ +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// A container for parts. +/// +internal class NamePlatePartsContainer +{ + private NamePlateSimpleParts? nameParts; + private NamePlateQuotedParts? titleParts; + private NamePlateQuotedParts? freeCompanyTagParts; + + /// + /// Initializes a new instance of the class. + /// + /// The currently executing update context. + public NamePlatePartsContainer(NamePlateUpdateContext context) + { + context.HasParts = true; + } + + /// + /// Gets a parts object for constructing a nameplate name. + /// + internal NamePlateSimpleParts Name => this.nameParts ??= new NamePlateSimpleParts(NamePlateStringField.Name); + + /// + /// Gets a parts object for constructing a nameplate title. + /// + internal NamePlateQuotedParts Title => this.titleParts ??= new NamePlateQuotedParts(NamePlateStringField.Title, false); + + /// + /// Gets a parts object for constructing a nameplate free company tag. + /// + internal NamePlateQuotedParts FreeCompanyTag => this.freeCompanyTagParts ??= new NamePlateQuotedParts(NamePlateStringField.FreeCompanyTag, true); + + /// + /// Applies all container parts. + /// + /// The handler to apply the builders to. + internal void ApplyBuilders(NamePlateUpdateHandler handler) + { + this.nameParts?.Apply(handler); + this.freeCompanyTagParts?.Apply(handler); + this.titleParts?.Apply(handler); + } +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs new file mode 100644 index 000000000..684578fcd --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -0,0 +1,73 @@ +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title). +/// +/// The field type which should be set. +public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany) +{ + /// + /// Gets or sets the opening quote string which appears before the text and opening text-wrap. + /// + public SeString? LeftQuote { get; set; } + + /// + /// Gets or sets the closing quote string which appears after the text and closing text-wrap. + /// + public SeString? RightQuote { get; set; } + + /// + /// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or + /// styling to the field's text. + /// + public (SeString, SeString)? TextWrap { get; set; } + + /// + /// Gets or sets this field's text. + /// + public SeString? Text { get; set; } + + /// + /// Applies the changes from this builder to the actual field. + /// + /// The handler to perform the changes on. + internal unsafe void Apply(NamePlateUpdateHandler handler) + { + if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) + return; + + var sb = new SeStringBuilder(); + if (this.LeftQuote is not null) + { + sb.Append(this.LeftQuote); + } + else + { + sb.Append(isFreeCompany ? " «" : "《"); + } + + if (this.TextWrap is { Item1: var left, Item2: var right }) + { + sb.Append(left); + sb.Append(this.Text ?? handler.GetFieldAsSeString(field)); + sb.Append(right); + } + else + { + sb.Append(this.Text ?? handler.GetFieldAsSeString(field)); + } + + if (this.RightQuote is not null) + { + sb.Append(this.RightQuote); + } + else + { + sb.Append(isFreeCompany ? "»" : "》"); + } + + handler.SetField(field, sb.Build()); + } +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs new file mode 100644 index 000000000..444f0e390 --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs @@ -0,0 +1,44 @@ +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// A part builder for constructing and setting a simple (unquoted) nameplate field. +/// +/// The field type which should be set. +public class NamePlateSimpleParts(NamePlateStringField field) +{ + /// + /// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or + /// styling to the field's text. + /// + public (SeString, SeString)? TextWrap { get; set; } + + /// + /// Gets or sets this field's text. + /// + public SeString? Text { get; set; } + + /// + /// Applies the changes from this builder to the actual field. + /// + /// The handler to perform the changes on. + internal unsafe void Apply(NamePlateUpdateHandler handler) + { + if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) + return; + + if (this.TextWrap is { Item1: var left, Item2: var right }) + { + var sb = new SeStringBuilder(); + sb.Append(left); + sb.Append(this.Text ?? handler.GetFieldAsSeString(field)); + sb.Append(right); + handler.SetField(field, sb.Build()); + } + else if (this.Text is not null) + { + handler.SetField(field, this.Text); + } + } +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs b/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs new file mode 100644 index 000000000..022935216 --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs @@ -0,0 +1,38 @@ +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// An enum describing the string fields available in nameplate data. The and various flags +/// determine which fields will actually be rendered. +/// +public enum NamePlateStringField +{ + /// + /// The object's name. + /// + Name = 0, + + /// + /// The object's title. + /// + Title = 50, + + /// + /// The object's free company tag. + /// + FreeCompanyTag = 100, + + /// + /// The object's status prefix. + /// + StatusPrefix = 150, + + /// + /// The object's target suffix. + /// + TargetSuffix = 200, + + /// + /// The object's level prefix. + /// + LevelPrefix = 250, +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs new file mode 100644 index 000000000..2164d7bee --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs @@ -0,0 +1,146 @@ +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Game.ClientState.Objects; + +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should +/// not be kept across frames. +/// +public interface INamePlateUpdateContext +{ + /// + /// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some + /// nameplates are hidden by default (based on in-game "Display Name Settings" and so on). + /// + int ActiveNamePlateCount { get; } + + /// + /// Gets a value indicating whether the game is currently performing a full update of all active nameplates. + /// + bool IsFullUpdate { get; } + + /// + /// Gets the address of the NamePlate addon. + /// + nint AddonAddress { get; } + + /// + /// Gets the address of the NamePlate addon's number array data container. + /// + nint NumberArrayDataAddress { get; } + + /// + /// Gets the address of the NamePlate addon's string array data container. + /// + nint StringArrayDataAddress { get; } + + /// + /// Gets the address of the first entry in the NamePlate addon's int array. + /// + nint NumberArrayDataEntryAddress { get; } +} + +/// +/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should +/// not be kept across frames. +/// +internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext +{ + /// + /// Initializes a new instance of the class. + /// + /// An object table. + /// The addon lifecycle arguments for the update request. + internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args) + { + this.ObjectTable = objectTable; + this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance(); + this.ResetState(args); + } + + /// + /// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some + /// nameplates are hidden by default (based on in-game "Display Name Settings" and so on). + /// + public int ActiveNamePlateCount { get; private set; } + + /// + /// Gets a value indicating whether the game is currently performing a full update of all active nameplates. + /// + public bool IsFullUpdate { get; private set; } + + /// + /// Gets the address of the NamePlate addon. + /// + public nint AddonAddress => (nint)this.Addon; + + /// + /// Gets the address of the NamePlate addon's number array data container. + /// + public nint NumberArrayDataAddress => (nint)this.NumberData; + + /// + /// Gets the address of the NamePlate addon's string array data container. + /// + public nint StringArrayDataAddress => (nint)this.StringData; + + /// + /// Gets the address of the first entry in the NamePlate addon's int array. + /// + public nint NumberArrayDataEntryAddress => (nint)this.NumberStruct; + + /// + /// Gets the RaptureAtkModule. + /// + internal RaptureAtkModule* RaptureAtkModule { get; } + + /// + /// Gets the ObjectTable. + /// + internal ObjectTable ObjectTable { get; } + + /// + /// Gets a pointer to the NamePlate addon. + /// + internal AddonNamePlate* Addon { get; private set; } + + /// + /// Gets a pointer to the NamePlate addon's number array data container. + /// + internal NumberArrayData* NumberData { get; private set; } + + /// + /// Gets a pointer to the NamePlate addon's string array data container. + /// + internal StringArrayData* StringData { get; private set; } + + /// + /// Gets a pointer to the NamePlate addon's number array entries as a struct. + /// + internal AddonNamePlate.NamePlateIntArrayData* NumberStruct { get; private set; } + + /// + /// Gets or sets a value indicating whether any handler in the current context has instantiated a part builder. + /// + internal bool HasParts { get; set; } + + /// + /// Resets the state of the context based on the provided addon lifecycle arguments. + /// + /// The addon lifecycle arguments for the update request. + internal void ResetState(AddonRequestedUpdateArgs args) + { + this.Addon = (AddonNamePlate*)args.Addon; + this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex]; + this.NumberStruct = (AddonNamePlate.NamePlateIntArrayData*)this.NumberData->IntArray; + this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex]; + this.HasParts = false; + + this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount; + this.IsFullUpdate = this.Addon->DoFullUpdate != 0; + } +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs new file mode 100644 index 000000000..14f8101ad --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -0,0 +1,603 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Text.SeStringHandling; + +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.Interop; + +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the +/// nameplate and allows for modification of various backing fields in number and string array data, which in turn +/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame +/// and should not be kept across frames. +/// +public interface INamePlateUpdateHandler +{ + /// + /// Gets the GameObjectId of the game object associated with this nameplate. + /// + ulong GameObjectId { get; } + + /// + /// Gets the associated with this nameplate, if possible. Performs an object table scan + /// and caches the result if successful. + /// + IGameObject? GameObject { get; } + + /// + /// Gets a read-only view of the nameplate info object data for a nameplate. Modifications to + /// fields do not affect fields in the returned view. + /// + INamePlateInfoView InfoView { get; } + + /// + /// Gets the index for this nameplate data in the backing number and string array data. This is not the same as the + /// rendered or object index, which can be retrieved from . + /// + int ArrayIndex { get; } + + /// + /// Gets the associated with this nameplate, if possible. Returns null if the nameplate + /// has an associated , but that object cannot be assigned to . + /// + IBattleChara? BattleChara { get; } + + /// + /// Gets the associated with this nameplate, if possible. Returns null if the + /// nameplate has an associated , but that object cannot be assigned to + /// . + /// + IPlayerCharacter? PlayerCharacter { get; } + + /// + /// Gets the address of the nameplate info struct. + /// + nint NamePlateInfoAddress { get; } + + /// + /// Gets the address of the first entry associated with this nameplate in the NamePlate addon's int array. + /// + nint NamePlateObjectAddress { get; } + + /// + /// Gets a value indicating what kind of nameplate this is, based on the kind of object it is associated with. + /// + NamePlateKind NamePlateKind { get; } + + /// + /// Gets the update flags for this nameplate. + /// + int UpdateFlags { get; } + + /// + /// Gets or sets the overall text color for this nameplate. If this value is changed, the appropriate update flag + /// will be set so that the game will reflect this change immediately. + /// + uint TextColor { get; set; } + + /// + /// Gets or sets the overall text edge color for this nameplate. If this value is changed, the appropriate update + /// flag will be set so that the game will reflect this change immediately. + /// + uint EdgeColor { get; set; } + + /// + /// Gets or sets the icon ID for the nameplate's marker icon, which is the large icon used to indicate quest + /// availability and so on. This value is read from and reset by the game every frame, not just when a nameplate + /// changes. + /// + int MarkerIconId { get; set; } + + /// + /// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name. + /// + int NameIconId { get; set; } + + /// + /// Gets the nameplate index, which is the index used for rendering and looking up entries in the object array. For + /// number and string array data, is used. + /// + int NamePlateIndex { get; } + + /// + /// Gets the draw flags for this nameplate. + /// + int DrawFlags { get; } + + /// + /// Gets or sets the visibility flags for this nameplate. + /// + int VisibilityFlags { get; set; } + + /// + /// Gets a value indicating whether this nameplate is undergoing a major update or not. This is usually true when a + /// nameplate has just appeared or something meaningful about the entity has changed (e.g. its job or status). This + /// flag is reset by the game during the update process (during requested update and before draw). + /// + bool IsUpdating { get; } + + /// + /// Gets or sets a value indicating whether the title (when visible) will be displayed above the object's name (a + /// prefix title) instead of below the object's name (a suffix title). + /// + bool IsPrefixTitle { get; set; } + + /// + /// Gets or sets a value indicating whether the title should be displayed at all. + /// + bool DisplayTitle { get; set; } + + /// + /// Gets or sets the name for this nameplate. + /// + SeString Name { get; set; } + + /// + /// Gets a builder which can be used to help cooperatively build a new name for this nameplate even when other + /// plugins modifying the name are present. Specifically, this builder allows setting text and text-wrapping + /// payloads (e.g. for setting text color) separately. + /// + NamePlateSimpleParts NameParts { get; } + + /// + /// Gets or sets the title for this nameplate. + /// + SeString Title { get; set; } + + /// + /// Gets a builder which can be used to help cooperatively build a new title for this nameplate even when other + /// plugins modifying the title are present. Specifically, this builder allows setting text, text-wrapping + /// payloads (e.g. for setting text color), and opening and closing quote sequences separately. + /// + NamePlateQuotedParts TitleParts { get; } + + /// + /// Gets or sets the free company tag for this nameplate. + /// + SeString FreeCompanyTag { get; set; } + + /// + /// Gets a builder which can be used to help cooperatively build a new FC tag for this nameplate even when other + /// plugins modifying the FC tag are present. Specifically, this builder allows setting text, text-wrapping + /// payloads (e.g. for setting text color), and opening and closing quote sequences separately. + /// + NamePlateQuotedParts FreeCompanyTagParts { get; } + + /// + /// Gets or sets the status prefix for this nameplate. This prefix is used by the game to add BitmapFontIcon-based + /// online status icons to player nameplates. + /// + SeString StatusPrefix { get; set; } + + /// + /// Gets or sets the target suffix for this nameplate. This suffix is used by the game to add the squared-letter + /// target tags to the end of combat target nameplates. + /// + SeString TargetSuffix { get; set; } + + /// + /// Gets or sets the level prefix for this nameplate. This "Lv60" style prefix is added to enemy and friendly battle + /// NPC nameplates to indicate the NPC level. + /// + SeString LevelPrefix { get; set; } + + /// + /// Removes the contents of the name field for this nameplate. This differs from simply setting the field + /// to an empty string because it writes a special value to memory, and other setters (except SetField variants) + /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed. + /// + void RemoveName(); + + /// + /// Removes the contents of the title field for this nameplate. This differs from simply setting the field + /// to an empty string because it writes a special value to memory, and other setters (except SetField variants) + /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed. + /// + void RemoveTitle(); + + /// + /// Removes the contents of the FC tag field for this nameplate. This differs from simply setting the field + /// to an empty string because it writes a special value to memory, and other setters (except SetField variants) + /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed. + /// + void RemoveFreeCompanyTag(); + + /// + /// Removes the contents of the status prefix field for this nameplate. This differs from simply setting the field + /// to an empty string because it writes a special value to memory, and other setters (except SetField variants) + /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed. + /// + void RemoveStatusPrefix(); + + /// + /// Removes the contents of the target suffix field for this nameplate. This differs from simply setting the field + /// to an empty string because it writes a special value to memory, and other setters (except SetField variants) + /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed. + /// + void RemoveTargetSuffix(); + + /// + /// Removes the contents of the level prefix field for this nameplate. This differs from simply setting the field + /// to an empty string because it writes a special value to memory, and other setters (except SetField variants) + /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed. + /// + void RemoveLevelPrefix(); + + /// + /// Gets a pointer to the string array value in the provided field. + /// + /// The field to read from. + /// A pointer to a sequence of non-null bytes. + unsafe byte* GetFieldAsPointer(NamePlateStringField field); + + /// + /// Gets a byte span containing the string array value in the provided field. + /// + /// The field to read from. + /// A ReadOnlySpan containing a sequence of non-null bytes. + ReadOnlySpan GetFieldAsSpan(NamePlateStringField field); + + /// + /// Gets a UTF8 string copy of the string array value in the provided field. + /// + /// The field to read from. + /// A copy of the string array value as a string. + string GetFieldAsString(NamePlateStringField field); + + /// + /// Gets a parsed SeString copy of the string array value in the provided field. + /// + /// The field to read from. + /// A copy of the string array value as a parsed SeString. + SeString GetFieldAsSeString(NamePlateStringField field); + + /// + /// Sets the string array value for the provided field. + /// + /// The field to write to. + /// The string to write. + void SetField(NamePlateStringField field, string value); + + /// + /// Sets the string array value for the provided field. + /// + /// The field to write to. + /// The SeString to write. + void SetField(NamePlateStringField field, SeString value); + + /// + /// Sets the string array value for the provided field. + /// + /// The field to write to. + /// The ReadOnlySpan of bytes to write. + void SetField(NamePlateStringField field, ReadOnlySpan value); + + /// + /// Sets the string array value for the provided field. + /// + /// The field to write to. + /// The pointer to a null-terminated sequence of bytes to write. + unsafe void SetField(NamePlateStringField field, byte* value); + + /// + /// Sets the string array value for the provided field to a fixed pointer to an empty string in unmanaged memory. + /// Other methods may notice this fixed pointer and refuse to overwrite it, preserving the emptiness of the field. + /// + /// The field to write to. + void RemoveField(NamePlateStringField field); +} + +/// +/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the +/// nameplate and allows for modification of various backing fields in number and string array data, which in turn +/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame +/// and should not be kept across frames. +/// +internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler +{ + private readonly NamePlateUpdateContext context; + + private ulong? gameObjectId; + private IGameObject? gameObject; + private NamePlateInfoView? infoView; + private NamePlatePartsContainer? partsContainer; + + /// + /// Initializes a new instance of the class. + /// + /// The current update context. + /// The index for this nameplate data in the backing number and string array data. This is + /// not the same as the rendered index, which can be retrieved from . + internal NamePlateUpdateHandler(NamePlateUpdateContext context, int arrayIndex) + { + this.context = context; + this.ArrayIndex = arrayIndex; + } + + /// + public int ArrayIndex { get; } + + /// + public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId; + + /// + public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.SearchById(this.GameObjectId); + + /// + public IBattleChara? BattleChara => this.GameObject as IBattleChara; + + /// + public IPlayerCharacter? PlayerCharacter => this.GameObject as IPlayerCharacter; + + /// + public INamePlateInfoView InfoView => this.infoView ??= new NamePlateInfoView(this.NamePlateInfo); + + /// + public nint NamePlateInfoAddress => (nint)this.NamePlateInfo; + + /// + public nint NamePlateObjectAddress => (nint)this.NamePlateObject; + + /// + public NamePlateKind NamePlateKind => (NamePlateKind)this.ObjectData->NamePlateKind; + + /// + public int UpdateFlags + { + get => this.ObjectData->UpdateFlags; + private set => this.ObjectData->UpdateFlags = value; + } + + /// + public uint TextColor + { + get => this.ObjectData->NameTextColor; + set + { + if (value != this.TextColor) this.UpdateFlags |= 2; + this.ObjectData->NameTextColor = value; + } + } + + /// + public uint EdgeColor + { + get => this.ObjectData->NameEdgeColor; + set + { + if (value != this.EdgeColor) this.UpdateFlags |= 2; + this.ObjectData->NameEdgeColor = value; + } + } + + /// + public int MarkerIconId + { + get => this.ObjectData->MarkerIconId; + set => this.ObjectData->MarkerIconId = value; + } + + /// + public int NameIconId + { + get => this.ObjectData->NameIconId; + set => this.ObjectData->NameIconId = value; + } + + /// + public int NamePlateIndex => this.ObjectData->NamePlateObjectIndex; + + /// + public int DrawFlags + { + get => this.ObjectData->DrawFlags; + private set => this.ObjectData->DrawFlags = value; + } + + /// + public int VisibilityFlags + { + get => ObjectData->VisibilityFlags; + set => ObjectData->VisibilityFlags = value; + } + + /// + public bool IsUpdating => (this.UpdateFlags & 1) != 0; + + /// + public bool IsPrefixTitle + { + get => (this.DrawFlags & 1) != 0; + set => this.DrawFlags = value ? this.DrawFlags | 1 : this.DrawFlags & ~1; + } + + /// + public bool DisplayTitle + { + get => (this.DrawFlags & 0x80) == 0; + set => this.DrawFlags = value ? this.DrawFlags & ~0x80 : this.DrawFlags | 0x80; + } + + /// + public SeString Name + { + get => this.GetFieldAsSeString(NamePlateStringField.Name); + set => this.WeakSetField(NamePlateStringField.Name, value); + } + + /// + public NamePlateSimpleParts NameParts => this.PartsContainer.Name; + + /// + public SeString Title + { + get => this.GetFieldAsSeString(NamePlateStringField.Title); + set => this.WeakSetField(NamePlateStringField.Title, value); + } + + /// + public NamePlateQuotedParts TitleParts => this.PartsContainer.Title; + + /// + public SeString FreeCompanyTag + { + get => this.GetFieldAsSeString(NamePlateStringField.FreeCompanyTag); + set => this.WeakSetField(NamePlateStringField.FreeCompanyTag, value); + } + + /// + public NamePlateQuotedParts FreeCompanyTagParts => this.PartsContainer.FreeCompanyTag; + + /// + public SeString StatusPrefix + { + get => this.GetFieldAsSeString(NamePlateStringField.StatusPrefix); + set => this.WeakSetField(NamePlateStringField.StatusPrefix, value); + } + + /// + public SeString TargetSuffix + { + get => this.GetFieldAsSeString(NamePlateStringField.TargetSuffix); + set => this.WeakSetField(NamePlateStringField.TargetSuffix, value); + } + + /// + public SeString LevelPrefix + { + get => this.GetFieldAsSeString(NamePlateStringField.LevelPrefix); + set => this.WeakSetField(NamePlateStringField.LevelPrefix, value); + } + + /// + /// Gets or (lazily) creates a part builder container for this nameplate. + /// + internal NamePlatePartsContainer PartsContainer => + this.partsContainer ??= new NamePlatePartsContainer(this.context); + + private RaptureAtkModule.NamePlateInfo* NamePlateInfo => + this.context.RaptureAtkModule->NamePlateInfoEntries.GetPointer(this.NamePlateIndex); + + private AddonNamePlate.NamePlateObject* NamePlateObject => + &this.context.Addon->NamePlateObjectArray[this.NamePlateIndex]; + + private AddonNamePlate.NamePlateIntArrayData.NamePlateObjectIntArrayData* ObjectData => + this.context.NumberStruct->ObjectData.GetPointer(this.ArrayIndex); + + /// + public void RemoveName() => this.RemoveField(NamePlateStringField.Name); + + /// + public void RemoveTitle() => this.RemoveField(NamePlateStringField.Title); + + /// + public void RemoveFreeCompanyTag() => this.RemoveField(NamePlateStringField.FreeCompanyTag); + + /// + public void RemoveStatusPrefix() => this.RemoveField(NamePlateStringField.StatusPrefix); + + /// + public void RemoveTargetSuffix() => this.RemoveField(NamePlateStringField.TargetSuffix); + + /// + public void RemoveLevelPrefix() => this.RemoveField(NamePlateStringField.LevelPrefix); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte* GetFieldAsPointer(NamePlateStringField field) + { + return this.context.StringData->StringArray[this.ArrayIndex + (int)field]; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan GetFieldAsSpan(NamePlateStringField field) + { + return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(this.GetFieldAsPointer(field)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string GetFieldAsString(NamePlateStringField field) + { + return Encoding.UTF8.GetString(this.GetFieldAsSpan(field)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SeString GetFieldAsSeString(NamePlateStringField field) + { + return SeString.Parse(this.GetFieldAsSpan(field)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetField(NamePlateStringField field, string value) + { + this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetField(NamePlateStringField field, SeString value) + { + this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetField(NamePlateStringField field, ReadOnlySpan value) + { + this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetField(NamePlateStringField field, byte* value) + { + this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveField(NamePlateStringField field) + { + this.context.StringData->SetValue( + this.ArrayIndex + (int)field, + (byte*)NamePlateGui.EmptyStringPointer, + true, + false, + true); + } + + /// + /// Resets the state of this handler for re-use in a new update. + /// + internal void ResetState() + { + this.gameObjectId = null; + this.gameObject = null; + this.infoView = null; + this.partsContainer = null; + } + + /// + /// Sets the string array value for the provided field, unless it was already set to the special empty string + /// pointer used by the Remove methods. + /// + /// The field to write to. + /// The SeString to write. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WeakSetField(NamePlateStringField field, SeString value) + { + if ((nint)this.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) + return; + this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true); + } +} diff --git a/Dalamud/Plugin/Services/INamePlateGui.cs b/Dalamud/Plugin/Services/INamePlateGui.cs new file mode 100644 index 000000000..1f6c4ed6f --- /dev/null +++ b/Dalamud/Plugin/Services/INamePlateGui.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +using Dalamud.Game.Gui.NamePlate; + +namespace Dalamud.Plugin.Services; + +/// +/// Class used to modify the data used when rendering nameplates. +/// +public interface INamePlateGui +{ + /// + /// The delegate used for receiving nameplate update events. + /// + /// An object containing information about the pending data update. + /// A list of handlers used for updating nameplate data. + public delegate void OnPlateUpdateDelegate( + INamePlateUpdateContext context, IReadOnlyList handlers); + + /// + /// An event which fires when nameplate data is updated and at least one nameplate has important updates. The + /// subscriber is provided with a list of handlers for nameplates with important updates. + /// + event OnPlateUpdateDelegate? OnNamePlateUpdate; + + /// + /// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all + /// nameplates. This event is likely to fire every frame even when no nameplates are actually updated, so in most + /// cases is preferred. + /// + event OnPlateUpdateDelegate? OnDataUpdate; + + /// + /// Requests that all nameplates should be redrawn on the following frame. + /// + void RequestRedraw(); +} From 9a1a32c03ed68c7d417a8f9de87171cd2f5adf74 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:50:43 +0900 Subject: [PATCH 02/79] Improve handling of quote parts when no text part is provided --- Dalamud/Game/Gui/NamePlate/NamePlateGui.cs | 32 +++++++++++++++++++ .../Game/Gui/NamePlate/NamePlateInfoView.cs | 26 +++++++++++---- .../Gui/NamePlate/NamePlateQuotedParts.cs | 12 +++++-- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs index 0ea2a5c56..029e9716c 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs @@ -94,6 +94,38 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener); } + /// + /// Strips the surrounding quotes from a free company tag. If the quotes are not present in the expected location, + /// no modifications will be made. + /// + /// A quoted free company tag. + /// A span containing the free company tag without its surrounding quote characters. + internal static ReadOnlySpan StripFreeCompanyTagQuotes(ReadOnlySpan text) + { + if (text.Length > 4 && text[..3].SequenceEqual(" «"u8) && text[^2..].SequenceEqual("»"u8)) + { + return text[3..^2]; + } + + return text; + } + + /// + /// Strips the surrounding quotes from a title. If the quotes are not present in the expected location, no + /// modifications will be made. + /// + /// A quoted title. + /// A span containing the title without its surrounding quote characters. + internal static ReadOnlySpan StripTitleQuotes(ReadOnlySpan text) + { + if (text.Length > 5 && text[..3].SequenceEqual("《"u8) && text[^3..].SequenceEqual("》"u8)) + { + return text[3..^3]; + } + + return text; + } + private static nint CreateEmptyStringPointer() { var pointer = Marshal.AllocHGlobal(1); diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs index a51ed20c3..020905422 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs @@ -16,21 +16,28 @@ public interface INamePlateInfoView SeString Name { get; } /// - /// Gets the displayed free company tag for this nameplate according to the nameplate info object. + /// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field, + /// the quote characters which appear on either side of the title are NOT included. /// SeString FreeCompanyTag { get; } /// - /// Gets the displayed title for this nameplate according to the nameplate info object. In this field, the quote + /// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field, + /// the quote characters which appear on either side of the title ARE included. + /// + SeString QuotedFreeCompanyTag { get; } + + /// + /// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote /// characters which appear on either side of the title are NOT included. /// SeString Title { get; } /// - /// Gets the displayed title for this nameplate according to the nameplate info object. In this field, the quote + /// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote /// characters which appear on either side of the title ARE included. /// - SeString DisplayTitle { get; } + SeString QuotedTitle { get; } /// /// Gets the displayed level text for this nameplate according to the nameplate info object. @@ -63,21 +70,26 @@ internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : { private SeString? name; private SeString? freeCompanyTag; + private SeString? quotedFreeCompanyTag; private SeString? title; - private SeString? displayTitle; + private SeString? quotedTitle; private SeString? levelText; /// public SeString Name => this.name ??= SeString.Parse(info->Name); /// - public SeString FreeCompanyTag => this.freeCompanyTag ??= SeString.Parse(info->FcName); + public SeString FreeCompanyTag => this.freeCompanyTag ??= + SeString.Parse(NamePlateGui.StripFreeCompanyTagQuotes(info->FcName)); + + /// + public SeString QuotedFreeCompanyTag => this.quotedFreeCompanyTag ??= SeString.Parse(info->FcName); /// public SeString Title => this.title ??= SeString.Parse(info->Title); /// - public SeString DisplayTitle => this.displayTitle ??= SeString.Parse(info->DisplayTitle); + public SeString QuotedTitle => this.quotedTitle ??= SeString.Parse(info->DisplayTitle); /// public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText); diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index 684578fcd..fed0dd144 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -51,12 +51,12 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany if (this.TextWrap is { Item1: var left, Item2: var right }) { sb.Append(left); - sb.Append(this.Text ?? handler.GetFieldAsSeString(field)); + sb.Append(this.Text ?? this.GetStrippedField(handler)); sb.Append(right); } else { - sb.Append(this.Text ?? handler.GetFieldAsSeString(field)); + sb.Append(this.Text ?? this.GetStrippedField(handler)); } if (this.RightQuote is not null) @@ -70,4 +70,12 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany handler.SetField(field, sb.Build()); } + + private SeString GetStrippedField(NamePlateUpdateHandler handler) + { + return SeString.Parse( + isFreeCompany + ? NamePlateGui.StripFreeCompanyTagQuotes(handler.GetFieldAsSpan(field)) + : NamePlateGui.StripTitleQuotes(handler.GetFieldAsSpan(field))); + } } From bceca96998993675ea334244b2ff8cb899beb5fb Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Thu, 11 Jul 2024 01:37:01 +0900 Subject: [PATCH 03/79] Use UI3DModule for GameObject lookups, avoiding object table scan --- Dalamud/Game/Gui/NamePlate/NamePlateGui.cs | 4 ++-- Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs | 6 ++++++ Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs index 029e9716c..28e2c36eb 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs @@ -102,7 +102,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui /// A span containing the free company tag without its surrounding quote characters. internal static ReadOnlySpan StripFreeCompanyTagQuotes(ReadOnlySpan text) { - if (text.Length > 4 && text[..3].SequenceEqual(" «"u8) && text[^2..].SequenceEqual("»"u8)) + if (text.Length > 4 && text.StartsWith(" «"u8) && text.EndsWith("»"u8)) { return text[3..^2]; } @@ -118,7 +118,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui /// A span containing the title without its surrounding quote characters. internal static ReadOnlySpan StripTitleQuotes(ReadOnlySpan text) { - if (text.Length > 5 && text[..3].SequenceEqual("《"u8) && text[^3..].SequenceEqual("》"u8)) + if (text.Length > 5 && text.StartsWith("《"u8) && text.EndsWith("》"u8)) { return text[3..^3]; } diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs index 2164d7bee..b8a4a9bd8 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs @@ -59,6 +59,7 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext { this.ObjectTable = objectTable; this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance(); + this.Ui3DModule = UIModule.Instance()->GetUI3DModule(); this.ResetState(args); } @@ -98,6 +99,11 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext /// internal RaptureAtkModule* RaptureAtkModule { get; } + /// + /// Gets the Ui3DModule. + /// + internal UI3DModule* Ui3DModule { get; } + /// /// Gets the ObjectTable. /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs index 14f8101ad..d34aba904 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -327,7 +327,9 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId; /// - public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.SearchById(this.GameObjectId); + public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.CreateObjectReference( + (nint)this.context.Ui3DModule->NamePlateObjectInfoPointers[ + this.ArrayIndex].Value->GameObject); /// public IBattleChara? BattleChara => this.GameObject as IBattleChara; From a78296c6033a9b1eecb4562efb47910a8f27ce43 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:27:24 +0900 Subject: [PATCH 04/79] Add NamePlateQuotedParts.OuterWrap --- .../Game/Gui/NamePlate/NamePlateQuotedParts.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index fed0dd144..40edee0fb 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -8,6 +8,12 @@ namespace Dalamud.Game.Gui.NamePlate; /// The field type which should be set. public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany) { + /// + /// Gets or sets the opening and closing SeStrings which will wrap the entire contents, which can be used to apply + /// colors or styling to the entire field. + /// + public (SeString, SeString)? OuterWrap { get; set; } + /// /// Gets or sets the opening quote string which appears before the text and opening text-wrap. /// @@ -39,6 +45,11 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany return; var sb = new SeStringBuilder(); + if (this.OuterWrap is { Item1: var outerLeft }) + { + sb.Append(outerLeft); + } + if (this.LeftQuote is not null) { sb.Append(this.LeftQuote); @@ -68,6 +79,11 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany sb.Append(isFreeCompany ? "»" : "》"); } + if (this.OuterWrap is { Item2: var outerRight }) + { + sb.Append(outerRight); + } + handler.SetField(field, sb.Build()); } From 15034298a53e322a9b3299d454cfb296622d82a0 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Sun, 14 Jul 2024 19:42:57 +0900 Subject: [PATCH 05/79] Small improvements to NamePlateGui doc comments --- Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs | 7 +++++++ Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs | 7 +++++++ Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs | 3 ++- Dalamud/Plugin/Services/INamePlateGui.cs | 10 ++++++++-- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index 40edee0fb..e05e553cd 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -6,6 +6,13 @@ namespace Dalamud.Game.Gui.NamePlate; /// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title). /// /// The field type which should be set. +/// +/// 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 +/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the +/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using +/// . +/// public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany) { /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs index 444f0e390..2906005da 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs @@ -6,6 +6,13 @@ namespace Dalamud.Game.Gui.NamePlate; /// A part builder for constructing and setting a simple (unquoted) nameplate field. /// /// The field type which should be set. +/// +/// 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 +/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the +/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using +/// . +/// public class NamePlateSimpleParts(NamePlateStringField field) { /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs index d34aba904..565f04846 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -90,12 +90,13 @@ public interface INamePlateUpdateHandler /// /// Gets or sets the icon ID for the nameplate's marker icon, which is the large icon used to indicate quest /// availability and so on. This value is read from and reset by the game every frame, not just when a nameplate - /// changes. + /// changes. Setting this to 0 disables the icon. /// int MarkerIconId { get; set; } /// /// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name. + /// Setting this to -1 disables the icon. /// int NameIconId { get; set; } diff --git a/Dalamud/Plugin/Services/INamePlateGui.cs b/Dalamud/Plugin/Services/INamePlateGui.cs index 1f6c4ed6f..6fdc02bd3 100644 --- a/Dalamud/Plugin/Services/INamePlateGui.cs +++ b/Dalamud/Plugin/Services/INamePlateGui.cs @@ -21,13 +21,19 @@ public interface INamePlateGui /// An event which fires when nameplate data is updated and at least one nameplate has important updates. The /// subscriber is provided with a list of handlers for nameplates with important updates. /// + /// + /// Fires after . + /// event OnPlateUpdateDelegate? OnNamePlateUpdate; /// /// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all - /// nameplates. This event is likely to fire every frame even when no nameplates are actually updated, so in most - /// cases is preferred. + /// nameplates. /// + /// + /// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases + /// is preferred. Fires before . + /// event OnPlateUpdateDelegate? OnDataUpdate; /// From c0968a61f6680d9090131a868e00e7624b528e1e Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:20:55 +0900 Subject: [PATCH 06/79] Use SeString.EncodeWithNullTerminator and improve doc comments for raw setters --- .../Gui/NamePlate/NamePlateUpdateHandler.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs index 565f04846..99429d932 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs @@ -273,14 +273,14 @@ public interface INamePlateUpdateHandler void SetField(NamePlateStringField field, SeString value); /// - /// Sets the string array value for the provided field. + /// Sets the string array value for the provided field. The provided byte sequence must be null-terminated. /// /// The field to write to. /// The ReadOnlySpan of bytes to write. void SetField(NamePlateStringField field, ReadOnlySpan value); /// - /// Sets the string array value for the provided field. + /// Sets the string array value for the provided field. The provided byte sequence must be null-terminated. /// /// The field to write to. /// The pointer to a null-terminated sequence of bytes to write. @@ -550,7 +550,12 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetField(NamePlateStringField field, SeString value) { - this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true); + this.context.StringData->SetValue( + this.ArrayIndex + (int)field, + value.EncodeWithNullTerminator(), + true, + true, + true); } /// @@ -601,6 +606,11 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler { if ((nint)this.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) return; - this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true); + this.context.StringData->SetValue( + this.ArrayIndex + (int)field, + value.EncodeWithNullTerminator(), + true, + true, + true); } } From 2b96f2187c915e7c674fce04073e7897a87a5c44 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 14 Jul 2024 11:07:36 -0700 Subject: [PATCH 07/79] feat: Use new versioning strategy - Rename BuildHash to SCMVersion - Expose the actual commit hash as a new `commit_hash.txt` - Update GetGitHash() to actually return the git hash --- .github/workflows/main.yml | 3 +- Dalamud/Dalamud.csproj | 76 ++++++++----------- Dalamud/EntryPoint.cs | 2 +- Dalamud/Interface/Internal/DalamudCommands.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 4 +- .../PluginInstaller/PluginInstallerWindow.cs | 4 +- Dalamud/Support/BugBait.cs | 2 +- Dalamud/Support/Troubleshooting.cs | 4 +- Dalamud/Utility/Util.cs | 31 ++++++-- 9 files changed, 67 insertions(+), 61 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d78c87d68..a8d27140d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -137,6 +137,7 @@ jobs: $newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt") $revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt") + $commitHash = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\commit_hash.txt") Remove-Item -Force -Recurse .\scratch if (Test-Path -Path $branchName) { @@ -147,7 +148,7 @@ jobs: } else { Move-Item -Force ".\canary.zip" ".\${branchName}\latest.zip" $versionData.AssemblyVersion = $newVersion - $versionData | add-member -Force -Name "GitSha" $newVersion -MemberType NoteProperty + $versionData | add-member -Force -Name "GitSha" $commitHash -MemberType NoteProperty $versionData | add-member -Force -Name "Revision" $revision -MemberType NoteProperty $versionData | ConvertTo-Json -Compress | Out-File ".\${branchName}\version" } diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 2b575f617..a5391631a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -124,57 +124,45 @@ $(OutputPath)TEMP_gitver.txt + $(OutputPath)commit_hash.txt $(OutputPath)revision.txt - + - - - - $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", "")) - - - - - - - - - + + - - + + - - + - $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", "")) - $([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", "")) + $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", "")) + $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", "")) + $([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", "")) + $([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", "")) - - - - - - - - Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss)) - $(LocalBuildText) - ??? - - - - + + + - + + + + Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss)) + ??? + + + + $(IntermediateOutputPath)CustomAssemblyInfo.cs @@ -185,21 +173,21 @@ - - <_Parameter1>GitHash - <_Parameter2>$(BuildHash) + + <_Parameter1>SCMVersion + <_Parameter2>$(SCMVersion) - + <_Parameter1>GitCommitCount <_Parameter2>$(CommitCount) - + <_Parameter1>GitHashClientStructs - <_Parameter2>$(BuildHashClientStructs) + <_Parameter2>$(CommitHashClientStructs) - - <_Parameter1>FullGitHash - <_Parameter2>$(DalamudFullGitCommitHash) + + <_Parameter1>GitHash + <_Parameter2>$(CommitHash) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 4b0adc9d8..d2bf69e16 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -185,7 +185,7 @@ public sealed class EntryPoint var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", - Util.GetGitHash(), + Util.GetScmVersion(), Util.GetGitHashClientStructs(), FFXIVClientStructs.ThisAssembly.Git.Commits); diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index cc63d887f..c19e4d120 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -329,7 +329,7 @@ internal class DalamudCommands : IServiceType chatGui.Print(new SeStringBuilder() .AddItalics("Dalamud:") - .AddText($" D{Util.AssemblyVersion}({Util.GetGitHash()}") + .AddText($" D{Util.AssemblyVersion}({Util.GetScmVersion()}") .Build()); chatGui.Print(new SeStringBuilder() diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 71ba2b071..9eac68c9b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -821,7 +821,7 @@ internal class DalamudInterface : IInternalDisposableService ImGui.MenuItem(Util.AssemblyVersion, false); ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); - ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false); + ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); ImGui.EndMenu(); @@ -1020,7 +1020,7 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu($"{Util.GetGitHash()}({Util.GetGitCommitCount()})", false); + ImGui.BeginMenu(Util.GetScmVersion(), false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index d4700ddb5..cb8a2da87 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -304,7 +304,7 @@ internal class PluginInstallerWindow : Window, IDisposable return; var versionInfo = t.Result; - if (versionInfo.AssemblyVersion != Util.GetGitHash() && + if (versionInfo.AssemblyVersion != Util.GetScmVersion() && versionInfo.Track != "release" && string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase)) this.staleDalamudNewVersion = versionInfo.AssemblyVersion; @@ -1540,7 +1540,7 @@ internal class PluginInstallerWindow : Window, IDisposable DrawWarningIcon(); DrawLinesCentered("A new version of Dalamud is available.\n" + "Please restart the game to ensure compatibility with updated plugins.\n" + - $"old: {Util.GetGitHash()} new: {this.staleDalamudNewVersion}"); + $"old: {Util.GetScmVersion()} new: {this.staleDalamudNewVersion}"); ImGuiHelpers.ScaledDummy(10); } diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index 22628303e..c82e5e652 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -36,7 +36,7 @@ internal static class BugBait Reporter = reporter, Name = plugin.InternalName, Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(), - DalamudHash = Util.GetGitHash(), + DalamudHash = Util.GetScmVersion(), }; if (includeException) diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 3ba088c66..4af8d5ffc 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -69,8 +69,8 @@ public static class Troubleshooting LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), - DalamudVersion = Util.AssemblyVersion, - DalamudGitHash = Util.GetGitHash(), + DalamudVersion = Util.GetScmVersion(), + DalamudGitHash = Util.GetGitHash() ?? "Unknown", GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), BetaKey = configuration.DalamudBetaKey, diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 927f7b310..745df597e 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -61,6 +61,7 @@ public static class Util ]; private static readonly Type GenericSpanType = typeof(Span<>); + private static string? scmVersionInternal; private static string? gitHashInternal; private static int? gitCommitCountInternal; private static string? gitHashClientStructsInternal; @@ -127,11 +128,28 @@ public static class Util } /// - /// Gets the git hash value from the assembly - /// or null if it cannot be found. + /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return + /// the git describe output for this build, which will be a raw version if this is a stable build or an + /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. + /// + /// The SCM version of the assembly. + public static string GetScmVersion() + { + if (scmVersionInternal != null) return scmVersionInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value + ?? asm.GetName().Version!.ToString(); + } + + /// + /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, + /// and will be suffixed with `-dirty` if in release with pending changes. /// /// The git hash of the assembly. - public static string GetGitHash() + public static string? GetGitHash() { if (gitHashInternal != null) return gitHashInternal; @@ -139,15 +157,14 @@ public static class Util var asm = typeof(Util).Assembly; var attrs = asm.GetCustomAttributes(); - gitHashInternal = attrs.First(a => a.Key == "GitHash").Value; - - return gitHashInternal; + return gitHashInternal = attrs.First(a => a.Key == "GitHash").Value; } /// /// Gets the amount of commits in the current branch, or null if undetermined. /// /// The amount of commits in the current branch. + [Obsolete($"Planned for removal in API 11. Use {nameof(GetScmVersion)} for version tracking.")] public static int? GetGitCommitCount() { if (gitCommitCountInternal != null) @@ -169,7 +186,7 @@ public static class Util /// or null if it cannot be found. /// /// The git hash of the assembly. - public static string GetGitHashClientStructs() + public static string? GetGitHashClientStructs() { if (gitHashClientStructsInternal != null) return gitHashClientStructsInternal; From 2bdd0dbe7b2d2cdd6a210d18dcb8177b89c3d1a7 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Thu, 18 Jul 2024 21:00:41 -0700 Subject: [PATCH 08/79] Use new version format in a few other places --- Dalamud/Interface/Internal/DalamudCommands.cs | 2 +- Dalamud/Interface/Internal/DalamudInterface.cs | 3 +-- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 2 +- Dalamud/Networking/Http/HappyHttpClient.cs | 2 +- Dalamud/Plugin/Internal/Types/PluginRepository.cs | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index c19e4d120..fb64ad979 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -329,7 +329,7 @@ internal class DalamudCommands : IServiceType chatGui.Print(new SeStringBuilder() .AddItalics("Dalamud:") - .AddText($" D{Util.AssemblyVersion}({Util.GetScmVersion()}") + .AddText($" {Util.GetScmVersion()}") .Build()); chatGui.Print(new SeStringBuilder() diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 9eac68c9b..4a280a1e3 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -818,8 +818,7 @@ internal class DalamudInterface : IInternalDisposableService { this.OpenBranchSwitcher(); } - - ImGui.MenuItem(Util.AssemblyVersion, false); + ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 67685f38a..d42dc3669 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -360,7 +360,7 @@ internal sealed class ChangelogWindow : Window, IDisposable { case State.WindowFadeIn: case State.ExplainerIntro: - ImGui.TextWrapped($"Welcome to Dalamud v{Util.AssemblyVersion}!"); + ImGui.TextWrapped($"Welcome to Dalamud v{Util.GetScmVersion()}!"); ImGuiHelpers.ScaledDummy(5); ImGui.TextWrapped(ChangeLog); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index aeed98695..4db44f1cf 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -36,7 +36,7 @@ internal class HappyHttpClient : IInternalDisposableService { UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + new ProductInfoHeaderValue("Dalamud", Util.GetScmVersion()), }, }, }; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 2f63070c3..9caa40f2e 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -59,7 +59,7 @@ internal class PluginRepository }, UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + new ProductInfoHeaderValue("Dalamud", Util.GetScmVersion()), }, }, }; From 19464ec08019b72ffb1e942b92656b7af0ca7fc3 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 20 Jul 2024 00:04:11 +0900 Subject: [PATCH 09/79] use what already was working instead (#1950) --- Dalamud.Boot/hooks.cpp | 9 ++ Dalamud/Support/CurrentProcessModules.cs | 129 ++--------------------- 2 files changed, 17 insertions(+), 121 deletions(-) diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp index 1b1280cf0..295d427ae 100644 --- a/Dalamud.Boot/hooks.cpp +++ b/Dalamud.Boot/hooks.cpp @@ -5,6 +5,14 @@ #include "ntdll.h" #include "logging.h" +namespace { + int s_dllChanged = 0; +} + +extern "C" __declspec(dllexport) int* GetDllChangedStorage() { + return &s_dllChanged; +} + hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook() : m_pfnGetProcAddress(GetProcAddress) , m_thunk("kernel32!GetProcAddress(Singleton Import Hook)", @@ -71,6 +79,7 @@ void hooks::getprocaddress_singleton_import_hook::initialize() { m_getProcAddressHandler = set_handler(L"kernel32.dll", "GetProcAddress", m_thunk.get_thunk(), [this](void*) {}); LdrRegisterDllNotification(0, [](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* context) { + s_dllChanged = 1; if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) { const auto dllName = unicode::convert(pData->Loaded.FullDllName->Buffer); diff --git a/Dalamud/Support/CurrentProcessModules.cs b/Dalamud/Support/CurrentProcessModules.cs index bc0abcf50..e1e3465b3 100644 --- a/Dalamud/Support/CurrentProcessModules.cs +++ b/Dalamud/Support/CurrentProcessModules.cs @@ -1,145 +1,32 @@ -using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using Serilog; -using TerraFX.Interop.Windows; - namespace Dalamud.Support; /// Tracks the loaded process modules. -[ServiceManager.EarlyLoadedService] -internal sealed unsafe partial class CurrentProcessModules : IInternalDisposableService +internal static unsafe partial class CurrentProcessModules { - private static readonly ConcurrentQueue LogQueue = new(); - private static readonly SemaphoreSlim LogSemaphore = new(0); - private static Process? process; - private static nint cookie; - - private readonly CancellationTokenSource logTaskStop = new(); - private readonly Task logTask; - - [ServiceManager.ServiceConstructor] - private CurrentProcessModules() - { - var res = LdrRegisterDllNotification(0, &DllNotificationCallback, 0, out cookie); - if (res != STATUS.STATUS_SUCCESS) - { - Log.Error("{what}: LdrRegisterDllNotification failure: 0x{err}", nameof(CurrentProcessModules), res); - cookie = 0; - this.logTask = Task.CompletedTask; - return; - } - - this.logTask = Task.Factory.StartNew( - () => - { - while (!this.logTaskStop.IsCancellationRequested) - { - LogSemaphore.Wait(); - while (LogQueue.TryDequeue(out var log)) - Log.Verbose(log); - } - }, - this.logTaskStop.Token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - } - - private enum LdrDllNotificationReason : uint - { - Loaded = 1, - Unloaded = 2, - } /// Gets all the loaded modules, up to date. public static ProcessModuleCollection ModuleCollection { get { - if (cookie == 0) + ref var t = ref *GetDllChangedStorage(); + if (t != 0) { - // This service has not been initialized; return a fresh copy without storing it. - return Process.GetCurrentProcess().Modules; - } - - if (process is null) + t = 0; + process = null; Log.Verbose("{what}: Fetchling fresh copy of current process modules.", nameof(CurrentProcessModules)); + } return (process ??= Process.GetCurrentProcess()).Modules; } } - /// - void IInternalDisposableService.DisposeService() - { - if (Interlocked.Exchange(ref cookie, 0) is var copy and not 0) - LdrUnregisterDllNotification(copy); - if (!this.logTask.IsCompleted) - { - this.logTaskStop.Cancel(); - LogSemaphore.Release(); - this.logTask.Wait(); - } - } - - [UnmanagedCallersOnly] - private static void DllNotificationCallback( - LdrDllNotificationReason reason, - LdrDllNotificationData* data, - nint context) => process = null; - - /// - /// Registers for notification when a DLL is first loaded. - /// This notification occurs before dynamic linking takes place.

- /// Docs. - ///
- /// This parameter must be zero. - /// A pointer to a callback function to call when the DLL is loaded. - /// A pointer to context data for the callback function. - /// A pointer to a variable to receive an identifier for the callback function. - /// This identifier is used to unregister the notification callback function. - /// Returns an NTSTATUS or error code. - [LibraryImport("ntdll.dll", SetLastError = true)] - private static partial int LdrRegisterDllNotification( - uint flags, - delegate* unmanaged - notificationFunction, - nint context, - out nint cookie); - - /// - /// Cancels DLL load notification previously registered by calling the LdrRegisterDllNotification function.
- ///
- /// Docs. - ///
- /// A pointer to the callback identifier received from the LdrRegisterDllNotification call - /// that registered for notification. - /// - /// Returns an NTSTATUS or error code. - [LibraryImport("ntdll.dll", SetLastError = true)] - private static partial int LdrUnregisterDllNotification(nint cookie); - - [StructLayout(LayoutKind.Sequential)] - private struct LdrDllNotificationData - { - /// Reserved. - public uint Flags; - - /// The full path name of the DLL module. - public UNICODE_STRING* FullDllName; - - /// The base file name of the DLL module. - public UNICODE_STRING* BaseDllName; - - /// A pointer to the base address for the DLL in memory. - public nint DllBase; - - /// The size of the DLL image, in bytes. - public uint SizeOfImage; - } + [LibraryImport("Dalamud.Boot.dll")] + private static partial int* GetDllChangedStorage(); } From 25cf7fde10a61da259e606dd65714f1a0153dfa9 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 19 Jul 2024 18:50:15 +0200 Subject: [PATCH 10/79] logging: limit main log file size to 100mb --- Dalamud.Injector/EntryPoint.cs | 3 ++- Dalamud/EntryPoint.cs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index f22c2923e..6507436c2 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -198,9 +198,10 @@ namespace Dalamud.Injector CullLogFile(logPath, 1 * 1024 * 1024); + const long maxLogSize = 100 * 1024 * 1024; // 100MB Log.Logger = new LoggerConfiguration() .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug) - .WriteTo.File(logPath, fileSizeLimitBytes: null) + .WriteTo.File(logPath, fileSizeLimitBytes: maxLogSize) .MinimumLevel.ControlledBy(levelSwitch) .CreateLogger(); diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 4b0adc9d8..1e6fccd8b 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -107,15 +107,16 @@ public sealed class EntryPoint .WriteTo.Sink(SerilogEventSink.Instance) .MinimumLevel.ControlledBy(LogLevelSwitch); + const long maxLogSize = 100 * 1024 * 1024; // 100MB if (logSynchronously) { - config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: null); + config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: maxLogSize); } else { config = config.WriteTo.Async(a => a.File( logPath.FullName, - fileSizeLimitBytes: null, + fileSizeLimitBytes: maxLogSize, buffered: false, flushToDiskInterval: TimeSpan.FromSeconds(1))); } From c88689a79add846f2a2908e6706d80a6e531dd5f Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 19 Jul 2024 21:31:56 +0200 Subject: [PATCH 11/79] build: 10.0.0.5 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 2b575f617..97be8b600 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -9,7 +9,7 @@ - 10.0.0.4 + 10.0.0.5 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 05ec0b8c114ebd55e5547639b3a3cdb55a38bd6a Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 20 Jul 2024 01:11:09 +0200 Subject: [PATCH 12/79] pi: show release date on changelog entries --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index d4700ddb5..99dc33e44 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2276,6 +2276,16 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.TextColored(ImGuiColors.DalamudGrey3, Locs.PluginBody_AuthorWithoutDownloadCount(log.Author)); } + if (log.Date != DateTime.MinValue) + { + var whenText = log.Date.LocRelativePastLong(); + var whenSize = ImGui.CalcTextSize(whenText); + ImGui.SameLine(ImGui.GetWindowWidth() - whenSize.X - (25 * ImGuiHelpers.GlobalScale)); + ImGui.TextColored(ImGuiColors.DalamudGrey3, whenText); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Published on " + log.Date.LocAbsolute()); + } + cursor.Y += ImGui.GetTextLineHeightWithSpacing(); ImGui.SetCursorPos(cursor); From 58753a27258a0f2bc634244fd18c4370fff354c1 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 20 Jul 2024 01:37:34 +0200 Subject: [PATCH 13/79] pi: always show available changelogs in "updateable" tab --- .../PluginInstaller/PluginInstallerWindow.cs | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 99dc33e44..fd9ef1044 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2624,7 +2624,24 @@ internal class PluginInstallerWindow : Window, IDisposable var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog; var hasChangelog = !applicableChangelog.IsNullOrWhitespace(); - var didDrawChangelogInsideCollapsible = false; + var didDrawApplicableChangelogInsideCollapsible = false; + + Version? availablePluginUpdateVersion = null; + string? availableChangelog = null; + var didDrawAvailableChangelogInsideCollapsible = false; + + if (availablePluginUpdate != default) + { + availablePluginUpdateVersion = + availablePluginUpdate.UseTesting ? + availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : + availablePluginUpdate.UpdateManifest.AssemblyVersion; + + availableChangelog = + availablePluginUpdate.UseTesting ? + availablePluginUpdate.UpdateManifest.TestingChangelog : + availablePluginUpdate.UpdateManifest.Changelog; + } var flags = PluginHeaderFlags.None; if (plugin.IsThirdParty) @@ -2758,28 +2775,33 @@ internal class PluginInstallerWindow : Window, IDisposable { if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion))) { - didDrawChangelogInsideCollapsible = true; + didDrawApplicableChangelogInsideCollapsible = true; this.DrawInstalledPluginChangelog(applicableChangelog); ImGui.TreePop(); } } - if (availablePluginUpdate != default && !availablePluginUpdate.UpdateManifest.Changelog.IsNullOrWhitespace()) + if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion))) { - var availablePluginUpdateVersion = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : availablePluginUpdate.UpdateManifest.AssemblyVersion; - var availableChangelog = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingChangelog : availablePluginUpdate.UpdateManifest.Changelog; - if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion))) - { - this.DrawInstalledPluginChangelog(availableChangelog); - ImGui.TreePop(); - } + this.DrawInstalledPluginChangelog(availableChangelog); + ImGui.TreePop(); + didDrawAvailableChangelogInsideCollapsible = true; } } - if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible) + if (thisWasUpdated && + hasChangelog && + !didDrawApplicableChangelogInsideCollapsible) { this.DrawInstalledPluginChangelog(applicableChangelog); } + + if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.UpdateablePlugins && + !availableChangelog.IsNullOrWhitespace() && + !didDrawAvailableChangelogInsideCollapsible) + { + this.DrawInstalledPluginChangelog(availableChangelog); + } ImGui.PopID(); } From 3584f2cf3d6b82468b0730221c3adbe532cf2728 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 20 Jul 2024 02:17:04 +0200 Subject: [PATCH 14/79] fix warning --- Dalamud/Utility/Util.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index e6bf8c1f3..b9c81acfd 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -19,6 +19,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; +using Dalamud.Support; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -27,8 +28,6 @@ using Windows.Win32.Storage.FileSystem; using Windows.Win32.System.Memory; using Windows.Win32.System.Ole; -using Dalamud.Support; - using static TerraFX.Interop.Windows.Windows; using Win32_PInvoke = Windows.Win32.PInvoke; From 21063217a500f5d509758d3357b9e91e1b3ffdf5 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 20 Jul 2024 02:17:27 +0200 Subject: [PATCH 15/79] pi: add "hidden" tab if there are any hidden plugins --- .../Internal/PluginCategoryManager.cs | 21 +++++++- .../PluginInstaller/PluginInstallerWindow.cs | 48 ++++++++++++++++--- Dalamud/Plugin/Internal/PluginManager.cs | 6 --- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index ec2a1c15b..71c869ede 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -23,6 +23,7 @@ internal class PluginCategoryManager new(CategoryKind.All, "special.all", () => Locs.Category_All), new(CategoryKind.IsTesting, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest), new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest), + new(CategoryKind.Hidden, "special.hidden", () => Locs.Category_Hidden, CategoryInfo.AppearCondition.AnyHiddenPlugins), new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled), new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester), new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud), @@ -106,6 +107,11 @@ internal class PluginCategoryManager /// AvailableForTesting = 2, + /// + /// Plugins that were hidden. + /// + Hidden = 3, + /// /// Installed dev plugins. /// @@ -309,6 +315,9 @@ internal class PluginCategoryManager { groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryKind); } + + // Hidden at the end + groupAvail.Categories.Add(CategoryKind.Hidden); // compare with prev state and mark as dirty if needed var noCategoryChanges = prevCategoryIds.SequenceEqual(groupAvail.Categories); @@ -332,7 +341,10 @@ internal class PluginCategoryManager { var groupInfo = this.groupList[this.currentGroupIdx]; - var includeAll = (this.currentCategoryKind == CategoryKind.All) || (groupInfo.GroupKind != GroupKind.Available); + var includeAll = this.currentCategoryKind == CategoryKind.All || + this.currentCategoryKind == CategoryKind.Hidden || + groupInfo.GroupKind != GroupKind.Available; + if (includeAll) { result.AddRange(plugins); @@ -455,6 +467,11 @@ internal class PluginCategoryManager /// Check if plugin testing is enabled. /// DoPluginTest, + + /// + /// Check if there are any hidden plugins. + /// + AnyHiddenPlugins, } /// @@ -529,6 +546,8 @@ internal class PluginCategoryManager public static string Category_AvailableForTesting => Loc.Localize("InstallerCategoryAvailableForTesting", "Testing Available"); + public static string Category_Hidden => Loc.Localize("InstallerCategoryHidden", "Hidden"); + public static string Category_DevInstalled => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins"); public static string Category_IconTester => "Image/Icon Tester"; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index fd9ef1044..466277a2f 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -118,7 +118,8 @@ internal class PluginInstallerWindow : Window, IDisposable private List pluginListInstalled = new(); private List pluginListUpdatable = new(); private bool hasDevPlugins = false; - + private bool hasHiddenPlugins = false; + private string searchText = string.Empty; private bool isSearchTextPrefilled = false; @@ -1277,6 +1278,19 @@ internal class PluginInstallerWindow : Window, IDisposable proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin)); } + var configuration = Service.Get(); + bool IsProxyHidden(PluginInstallerAvailablePluginProxy proxy) + { + var isHidden = + configuration.HiddenPluginInternalName.Contains(proxy.RemoteManifest?.InternalName); + if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden) + return isHidden; + return !isHidden; + } + + // Filter out plugins that are not hidden + proxies = proxies.Where(IsProxyHidden).ToList(); + return proxies; } #pragma warning restore SA1201 @@ -1305,6 +1319,12 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PopID(); } + + // Reset the category to "All" if we're on the "Hidden" category and there are no hidden plugins (we removed the last one) + if (i == 0 && this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden) + { + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; + } } private void DrawInstalledPluginList(InstalledPluginListFilter filter) @@ -1471,6 +1491,10 @@ internal class PluginInstallerWindow : Window, IDisposable if (!Service.Get().DoPluginTest) continue; break; + case PluginCategoryManager.CategoryInfo.AppearCondition.AnyHiddenPlugins: + if (!this.hasHiddenPlugins) + continue; + break; default: throw new ArgumentOutOfRangeException(); } @@ -2456,12 +2480,19 @@ internal class PluginInstallerWindow : Window, IDisposable pluginManager.RefilterPluginMasters(); } - if (ImGui.Selectable(Locs.PluginContext_HidePlugin)) + var isHidden = configuration.HiddenPluginInternalName.Contains(manifest.InternalName); + switch (isHidden) { - Log.Debug($"Adding {manifest.InternalName} to hidden plugins"); - configuration.HiddenPluginInternalName.Add(manifest.InternalName); - configuration.QueueSave(); - pluginManager.RefilterPluginMasters(); + case false when ImGui.Selectable(Locs.PluginContext_HidePlugin): + configuration.HiddenPluginInternalName.Add(manifest.InternalName); + configuration.QueueSave(); + pluginManager.RefilterPluginMasters(); + break; + case true when ImGui.Selectable(Locs.PluginContext_UnhidePlugin): + configuration.HiddenPluginInternalName.Remove(manifest.InternalName); + configuration.QueueSave(); + pluginManager.RefilterPluginMasters(); + break; } if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig)) @@ -3638,6 +3669,7 @@ internal class PluginInstallerWindow : Window, IDisposable private void OnAvailablePluginsChanged() { var pluginManager = Service.Get(); + var configuration = Service.Get(); lock (this.listLock) { @@ -3647,6 +3679,8 @@ internal class PluginInstallerWindow : Window, IDisposable this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList(); this.ResortPlugins(); } + + this.hasHiddenPlugins = this.pluginListAvailable.Any(x => configuration.HiddenPluginInternalName.Contains(x.InternalName)); this.UpdateCategoriesOnPluginsChange(); } @@ -3974,6 +4008,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen"); public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer"); + + public static string PluginContext_UnhidePlugin => Loc.Localize("InstallerUnhidePlugin", "Unhide from installer"); public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data"); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index ca9e4a922..5d365d66c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -258,12 +258,6 @@ internal class PluginManager : IInternalDisposableService /// If the manifest is visible. public static bool IsManifestVisible(RemotePluginManifest manifest) { - var configuration = Service.Get(); - - // Hidden by user - if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName)) - return false; - // Hidden by manifest return !manifest.IsHide; } From 21a9cf215a9e611e8f768bedca7ea970480b66ac Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Sun, 21 Jul 2024 00:58:34 +0900 Subject: [PATCH 16/79] Add doc remark to RequestRedraw --- Dalamud/Plugin/Services/INamePlateGui.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Plugin/Services/INamePlateGui.cs b/Dalamud/Plugin/Services/INamePlateGui.cs index 6fdc02bd3..713d9120b 100644 --- a/Dalamud/Plugin/Services/INamePlateGui.cs +++ b/Dalamud/Plugin/Services/INamePlateGui.cs @@ -39,5 +39,10 @@ public interface INamePlateGui /// /// Requests that all nameplates should be redrawn on the following frame. /// + /// + /// This causes extra work for the game, and should not need to be called every frame. However, it is acceptable to + /// call frequently when needed (e.g. in response to a manual settings change by the user) or when necessary (e.g. + /// after a change of zone, party type, etc.). + /// void RequestRedraw(); } From 7d2ac511db2e2b27b58cac7a213e2e13ad96fcd3 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Sun, 21 Jul 2024 00:58:22 +0900 Subject: [PATCH 17/79] Add NamePlateAgingStep --- .../SelfTest/AgingSteps/NamePlateAgingStep.cs | 123 ++++++++++++++++++ .../Windows/SelfTest/SelfTestWindow.cs | 1 + 2 files changed, 124 insertions(+) create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs new file mode 100644 index 000000000..5a03a6dc2 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; + +using Dalamud.Game.Gui.NamePlate; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Tests for nameplates. +/// +internal class NamePlateAgingStep : IAgingStep +{ + private SubStep currentSubStep; + private Dictionary? updateCount; + + private enum SubStep + { + Start, + Confirm, + } + + /// + public string Name => "Test Nameplates"; + + /// + public SelfTestStepResult RunStep() + { + var namePlateGui = Service.Get(); + + switch (this.currentSubStep) + { + case SubStep.Start: + namePlateGui.OnNamePlateUpdate += this.OnNamePlateUpdate; + namePlateGui.OnDataUpdate += this.OnDataUpdate; + namePlateGui.RequestRedraw(); + this.updateCount = new Dictionary(); + this.currentSubStep++; + break; + + case SubStep.Confirm: + ImGui.Text("Click to redraw all visible nameplates"); + if (ImGui.Button("Request redraw")) + namePlateGui.RequestRedraw(); + + ImGui.TextUnformatted("Can you see marker icons above nameplates, and does\n" + + "the update count increase when using request redraw?"); + + if (ImGui.Button("Yes")) + { + this.CleanUp(); + return SelfTestStepResult.Pass; + } + + ImGui.SameLine(); + + if (ImGui.Button("No")) + { + this.CleanUp(); + return SelfTestStepResult.Fail; + } + + break; + } + + return SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + var namePlateGui = Service.Get(); + namePlateGui.OnNamePlateUpdate -= this.OnNamePlateUpdate; + namePlateGui.OnDataUpdate -= this.OnDataUpdate; + namePlateGui.RequestRedraw(); + this.updateCount = null; + this.currentSubStep = SubStep.Start; + } + + private void OnDataUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) + { + foreach (var handler in handlers) + { + // Force nameplates to be visible + handler.VisibilityFlags |= 1; + + // Set marker icon based on nameplate kind, and flicker when updating + if (handler.IsUpdating || context.IsFullUpdate) + { + handler.MarkerIconId = 66181 + (int)handler.NamePlateKind; + } + else + { + handler.MarkerIconId = 66161 + (int)handler.NamePlateKind; + } + } + } + + private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) + { + foreach (var handler in handlers) + { + // Append GameObject address to name + var gameObjectAddress = handler.GameObject?.Address ?? 0; + + handler.Name = handler.Name.Append(new SeString(new UIForegroundPayload(9))) + .Append($" (0x{gameObjectAddress:X})") + .Append(new SeString(UIForegroundPayload.UIForegroundOff)); + + // Track update count and set it as title + var count = this.updateCount!.GetValueOrDefault(handler.GameObjectId); + this.updateCount[handler.GameObjectId] = count + 1; + + handler.TitleParts.Text = $"Updates: {count}"; + handler.TitleParts.TextWrap = (new SeString(new UIForegroundPayload(43)), + new SeString(UIForegroundPayload.UIForegroundOff)); + handler.DisplayTitle = true; + handler.IsPrefixTitle = false; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index e3172d5c2..a17e6c077 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -28,6 +28,7 @@ internal class SelfTestWindow : Window new EnterTerritoryAgingStep(148, "Central Shroud"), new ItemPayloadAgingStep(), new ContextMenuAgingStep(), + new NamePlateAgingStep(), new ActorTableAgingStep(), new FateTableAgingStep(), new AetheryteListAgingStep(), From 45e765eb5cc52b0705ac31a0d6a8f2dd19accb23 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Sun, 21 Jul 2024 00:59:08 +0900 Subject: [PATCH 18/79] Run CleanUp when when skipping a SelfTest step --- Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index a17e6c077..51c9b35f6 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -83,6 +83,7 @@ internal class SelfTestWindow : Window if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward)) { this.stepResults.Add((SelfTestStepResult.NotRan, null)); + this.steps[this.currentStep].CleanUp(); this.currentStep++; this.lastTestStart = DateTimeOffset.Now; From d16e78346621e17e1506a21ef00cf9835e7df499 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 15:30:22 +0900 Subject: [PATCH 19/79] Isolate plugin constructors to separate threads --- Dalamud/IoC/Internal/ServiceContainer.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 673dba29b..7a0b4347d 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Logging.Internal; @@ -127,7 +128,26 @@ internal class ServiceContainer : IServiceProvider, IServiceType return null; } - ctor.Invoke(instance, resolvedParams); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var thr = new Thread( + () => + { + try + { + ctor.Invoke(instance, resolvedParams); + } + catch (Exception e) + { + tcs.SetException(e); + return; + } + + tcs.SetResult(); + }); + + thr.Start(); + await tcs.Task.ConfigureAwait(false); + thr.Join(); return instance; } From 0d66c7fd75163b7084d1ad8bb94a0dbec801bb9d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 15:34:29 +0900 Subject: [PATCH 20/79] Fix load order --- Dalamud/Interface/Internal/InterfaceManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 06a93e453..e2993f911 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -817,8 +817,12 @@ internal class InterfaceManager : IInternalDisposableService // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. _ = this.dalamudAtlas.BuildFontsAsync(); + SwapChainHelper.BusyWaitForGameDeviceSwapChain(); + SwapChainHelper.DetectReShade(); + try { + // Requires that game window to be there, which will be the case once game swap chain is initialized. if (Service.Get().WindowIsImmersive) this.SetImmersiveMode(true); } @@ -834,9 +838,6 @@ internal class InterfaceManager : IInternalDisposableService 0, this.SetCursorDetour); - SwapChainHelper.BusyWaitForGameDeviceSwapChain(); - SwapChainHelper.DetectReShade(); - Log.Verbose("===== S W A P C H A I N ====="); this.resizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, From d70ae468e818a739cfc461b62203c836de7aa1bd Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 18:34:17 +0900 Subject: [PATCH 21/79] CurrentProcessModules: suppress sporadic errors --- Dalamud/Support/CurrentProcessModules.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Dalamud/Support/CurrentProcessModules.cs b/Dalamud/Support/CurrentProcessModules.cs index e1e3465b3..cd73ceb04 100644 --- a/Dalamud/Support/CurrentProcessModules.cs +++ b/Dalamud/Support/CurrentProcessModules.cs @@ -20,10 +20,18 @@ internal static unsafe partial class CurrentProcessModules { t = 0; process = null; - Log.Verbose("{what}: Fetchling fresh copy of current process modules.", nameof(CurrentProcessModules)); + Log.Verbose("{what}: Fetching fresh copy of current process modules.", nameof(CurrentProcessModules)); } - return (process ??= Process.GetCurrentProcess()).Modules; + try + { + return (process ??= Process.GetCurrentProcess()).Modules; + } + catch (Exception e) + { + Log.Verbose(e, "{what}: Failed to fetch module list.", nameof(CurrentProcessModules)); + return new([]); + } } } From aa99be6ccba01be05b263b8395037eade582d987 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 18:34:31 +0900 Subject: [PATCH 22/79] DalamudAssetManager: suppress ignorable error --- Dalamud/Storage/Assets/DalamudAssetManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 514785823..0109339fe 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -85,7 +85,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud .Where(x => x is not DalamudAsset.Empty4X4) .Where(x => x.GetAttribute()?.Required is false) .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())) + .Select(x => x.ToContentDisposedTask(true))) .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } From efaa346d5e41e2fd45620fce47528f1fdf8d951d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 18:35:35 +0900 Subject: [PATCH 23/79] Localization: change to a provided service --- Dalamud/Dalamud.cs | 7 ++++++- Dalamud/Localization.cs | 29 +++++++++++++++++------------ Dalamud/Service/ServiceManager.cs | 10 +++++++++- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 9ea96a45c..93de4c64d 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -65,7 +65,12 @@ internal sealed class Dalamud : IServiceType true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json"))); } - ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner); + ServiceManager.InitializeProvidedServices( + this, + fs, + configuration, + scanner, + Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride)); // Set up FFXIVClientStructs this.SetupClientStructsResolver(cacheDir); diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 3ed2ad519..84e8437b3 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Reflection; using CheapLoc; -using Dalamud.Configuration.Internal; using Serilog; @@ -13,7 +12,7 @@ namespace Dalamud; /// /// Class handling localization. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.ProvidedService] public class Localization : IServiceType { /// @@ -43,16 +42,6 @@ public class Localization : IServiceType this.assembly = Assembly.GetCallingAssembly(); } - [ServiceManager.ServiceConstructor] - private Localization(Dalamud dalamud, DalamudConfiguration configuration) - : this(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_") - { - if (!string.IsNullOrEmpty(configuration.LanguageOverride)) - this.SetupWithLangCode(configuration.LanguageOverride); - else - this.SetupWithUiCulture(); - } - /// /// Delegate for the event that occurs when the language is changed. /// @@ -167,6 +156,22 @@ public class Localization : IServiceType Loc.ExportLocalizableForAssembly(this.assembly, ignoreInvalidFunctions); } + /// + /// Creates a new instance of the class. + /// + /// Path to Dalamud assets. + /// Optional language override. + /// A new instance. + internal static Localization FromAssets(string assetDirectory, string? languageOverride) + { + var t = new Localization(Path.Combine(assetDirectory, "UIRes", "loc", "dalamud"), "dalamud_"); + if (!string.IsNullOrEmpty(languageOverride)) + t.SetupWithLangCode(languageOverride); + else + t.SetupWithUiCulture(); + return t; + } + private string ReadLocData(string langCode) { if (this.useEmbedded) diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 7483b0a27..3f8a55614 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -126,7 +126,13 @@ internal static class ServiceManager /// Instance of . /// Instance of . /// Instance of . - public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) + /// Instance of . + public static void InitializeProvidedServices( + Dalamud dalamud, + ReliableFileStorage fs, + DalamudConfiguration configuration, + TargetSigScanner scanner, + Localization localization) { #if DEBUG lock (LoadedServices) @@ -136,6 +142,7 @@ internal static class ServiceManager ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); + ProvideService(localization); } return; @@ -152,6 +159,7 @@ internal static class ServiceManager ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); + ProvideService(localization); return; void ProvideService(T service) where T : IServiceType => Service.Provide(service); From 9db4e2f3a13f8d5ddffef3a9144f4885f8a3da20 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 18:38:01 +0900 Subject: [PATCH 24/79] LoadingDialog: support localization, remove winforms, prevent invalid thread association errors --- Dalamud.Boot/Dalamud.Boot.rc | 18 ++ Dalamud.Boot/Dalamud.Boot.vcxproj | 5 +- Dalamud.Boot/Dalamud.Boot.vcxproj.filters | 3 + Dalamud.Boot/themes.manifest | 9 + Dalamud/Dalamud.csproj | 1 - Dalamud/Service/LoadingDialog.cs | 376 ++++++++++++++-------- Dalamud/Service/ServiceManager.cs | 2 +- 7 files changed, 272 insertions(+), 142 deletions(-) create mode 100644 Dalamud.Boot/themes.manifest diff --git a/Dalamud.Boot/Dalamud.Boot.rc b/Dalamud.Boot/Dalamud.Boot.rc index daa41a282..b46e81caf 100644 --- a/Dalamud.Boot/Dalamud.Boot.rc +++ b/Dalamud.Boot/Dalamud.Boot.rc @@ -12,6 +12,24 @@ ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// RT_MANIFEST +// + +RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + ///////////////////////////////////////////////////////////////////////////// // English (United Kingdom) resources diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index 298edbcbc..80435cd67 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -197,8 +197,11 @@ + + + - + \ No newline at end of file diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index 87eaf6fcc..7c26b28ff 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -163,4 +163,7 @@ Dalamud.Boot DLL + + + \ No newline at end of file diff --git a/Dalamud.Boot/themes.manifest b/Dalamud.Boot/themes.manifest new file mode 100644 index 000000000..11c048abd --- /dev/null +++ b/Dalamud.Boot/themes.manifest @@ -0,0 +1,9 @@ + + + Windows Forms Common Control manifest + + + + + + \ No newline at end of file diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 97be8b600..9ed0aa991 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -32,7 +32,6 @@ - true true true portable diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs index 64af02171..eebeb5b1c 100644 --- a/Dalamud/Service/LoadingDialog.cs +++ b/Dalamud/Service/LoadingDialog.cs @@ -1,34 +1,44 @@ -using System.Drawing; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; + +using CheapLoc; using Dalamud.Plugin.Internal; using Dalamud.Utility; -using Windows.Win32.Foundation; -using Windows.Win32.UI.WindowsAndMessaging; + +using Serilog; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.TASKDIALOG_FLAGS; +using static TerraFX.Interop.Windows.Windows; namespace Dalamud; /// /// Class providing an early-loading dialog. /// -internal class LoadingDialog +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +internal sealed unsafe class LoadingDialog { - // TODO: We can't localize any of what's in here at the moment, because Localization is an EarlyLoadedService. - - private static int wasGloballyHidden = 0; - + private static int wasGloballyHidden; + private Thread? thread; - private TaskDialogButton? inProgressHideButton; - private TaskDialogPage? page; - private bool canHide; - private State currentState = State.LoadingDalamud; + private HWND hwndTaskDialog; private DateTime firstShowTime; - + private State currentState = State.LoadingDalamud; + private bool canHide; + /// /// Enum representing the state of the dialog. /// @@ -38,18 +48,18 @@ internal class LoadingDialog /// Show a message stating that Dalamud is currently loading. /// LoadingDalamud, - + /// /// Show a message stating that Dalamud is currently loading plugins. /// LoadingPlugins, - + /// /// Show a message stating that Dalamud is currently updating plugins. /// AutoUpdatePlugins, } - + /// /// Gets or sets the current state of the dialog. /// @@ -58,13 +68,16 @@ internal class LoadingDialog get => this.currentState; set { + if (this.currentState == value) + return; + this.currentState = value; - this.UpdatePage(); + this.UpdateMainInstructionText(); } } - + /// - /// Gets or sets a value indicating whether or not the dialog can be hidden by the user. + /// Gets or sets a value indicating whether the dialog can be hidden by the user. /// /// Thrown if called before the dialog has been created. public bool CanHide @@ -72,8 +85,11 @@ internal class LoadingDialog get => this.canHide; set { + if (this.canHide == value) + return; + this.canHide = value; - this.UpdatePage(); + this.UpdateButtonEnabled(); } } @@ -84,17 +100,17 @@ internal class LoadingDialog { if (Volatile.Read(ref wasGloballyHidden) == 1) return; - + if (this.thread?.IsAlive == true) return; - + this.thread = new Thread(this.ThreadStart) { Name = "Dalamud Loading Dialog", }; this.thread.SetApartmentState(ApartmentState.STA); this.thread.Start(); - + this.firstShowTime = DateTime.Now; } @@ -103,150 +119,232 @@ internal class LoadingDialog /// public void HideAndJoin() { - if (this.thread == null || !this.thread.IsAlive) + if (this.thread?.IsAlive is not true) return; - - this.inProgressHideButton?.PerformClick(); - this.thread!.Join(); + + SendMessageW(this.hwndTaskDialog, WM.WM_CLOSE, default, default); + this.thread.Join(); } - private void UpdatePage() + private void UpdateMainInstructionText() { - if (this.page == null) + if (this.hwndTaskDialog == default) return; - this.page.Heading = this.currentState switch + fixed (void* pszText = this.currentState switch + { + State.LoadingDalamud => Loc.Localize( + "LoadingDialogMainInstructionLoadingDalamud", + "Dalamud is loading..."), + State.LoadingPlugins => Loc.Localize( + "LoadingDialogMainInstructionLoadingPlugins", + "Waiting for plugins to load..."), + State.AutoUpdatePlugins => Loc.Localize( + "LoadingDialogMainInstructionAutoUpdatePlugins", + "Updating plugins..."), + _ => string.Empty, // should not happen + }) { - State.LoadingDalamud => "Dalamud is loading...", - State.LoadingPlugins => "Waiting for plugins to load...", - State.AutoUpdatePlugins => "Updating plugins...", - _ => throw new ArgumentOutOfRangeException(), - }; + SendMessageW( + this.hwndTaskDialog, + (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT, + (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_MAIN_INSTRUCTION, + (LPARAM)pszText); + } + } - var context = string.Empty; - if (this.currentState == State.LoadingPlugins) + private void UpdateContentText() + { + if (this.hwndTaskDialog == default) + return; + + var contentBuilder = new StringBuilder( + Loc.Localize( + "LoadingDialogContentInfo", + "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" + + "This is likely normal, please wait a little while longer.")); + + if (this.CurrentState == State.LoadingPlugins) { - context = "\nPreparing..."; - var tracker = Service.GetNullable()?.StartupLoadTracking; if (tracker != null) { - var nameString = tracker.GetPendingInternalNames() - .Select(x => tracker.GetPublicName(x)) - .Where(x => x != null) - .Aggregate(string.Empty, (acc, x) => acc + x + ", "); - + var nameString = string.Join( + ", ", + tracker.GetPendingInternalNames() + .Select(x => tracker.GetPublicName(x)) + .Where(x => x != null)); + if (!nameString.IsNullOrEmpty()) - context = $"\nWaiting for: {nameString[..^2]}"; + { + contentBuilder + .AppendLine() + .AppendLine() + .Append( + string.Format( + Loc.Localize("LoadingDialogContentCurrentPlugin", "Waiting for: {0}"), + nameString)); + } } } - + // Add some text if loading takes more than a few minutes if (DateTime.Now - this.firstShowTime > TimeSpan.FromMinutes(3)) - context += "\nIt's been a while now. Please report this issue on our Discord server."; - - this.page.Text = this.currentState switch { - State.LoadingDalamud => "Please wait while Dalamud loads...", - State.LoadingPlugins => "Please wait while Dalamud loads plugins...", - State.AutoUpdatePlugins => "Please wait while Dalamud updates your plugins...", - _ => throw new ArgumentOutOfRangeException(), -#pragma warning disable SA1513 - } + context; -#pragma warning restore SA1513 - - this.inProgressHideButton!.Enabled = this.canHide; - } - - private async Task DialogStatePeriodicUpdate(CancellationToken token) - { - using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50)); - while (!token.IsCancellationRequested) - { - await timer.WaitForNextTickAsync(token); - this.UpdatePage(); + contentBuilder + .AppendLine() + .AppendLine() + .Append( + Loc.Localize( + "LoadingDialogContentTakingTooLong", + "It's been a while now. Please report this issue on our Discord server.")); } + + fixed (void* pszText = contentBuilder.ToString()) + { + SendMessageW( + this.hwndTaskDialog, + (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT, + (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_CONTENT, + (LPARAM)pszText); + } + } + + private void UpdateButtonEnabled() + { + if (this.hwndTaskDialog == default) + return; + + SendMessageW(this.hwndTaskDialog, (uint)TASKDIALOG_MESSAGES.TDM_ENABLE_BUTTON, IDOK, this.canHide ? 1 : 0); + } + + private HRESULT TaskDialogCallback(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam) + { + switch ((TASKDIALOG_NOTIFICATIONS)msg) + { + case TASKDIALOG_NOTIFICATIONS.TDN_CREATED: + this.hwndTaskDialog = hwnd; + + this.UpdateMainInstructionText(); + this.UpdateContentText(); + this.UpdateButtonEnabled(); + SendMessageW(hwnd, (int)TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_MARQUEE, 1, 0); + + // Bring to front + SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE); + SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE); + ShowWindow(hwnd, SW.SW_SHOW); + SetForegroundWindow(hwnd); + SetFocus(hwnd); + SetActiveWindow(hwnd); + return S.S_OK; + + case TASKDIALOG_NOTIFICATIONS.TDN_DESTROYED: + this.hwndTaskDialog = default; + return S.S_OK; + + case TASKDIALOG_NOTIFICATIONS.TDN_TIMER: + this.UpdateContentText(); + return S.S_OK; + } + + return S.S_OK; } private void ThreadStart() { - Application.EnableVisualStyles(); - - this.inProgressHideButton = new TaskDialogButton("Hide", this.canHide); - // We don't have access to the asset service here. var workingDirectory = Service.Get().StartInfo.WorkingDirectory; - TaskDialogIcon? dialogIcon = null; - if (!workingDirectory.IsNullOrEmpty()) + using var extractedIcon = + string.IsNullOrEmpty(workingDirectory) + ? null + : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe")); + + fixed (void* pszWindowTitle = "Dalamud") + fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) + fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") + fixed (void* pszDalamudBoot = "Dalamud.Boot.dll") { - var extractedIcon = Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe")); - if (extractedIcon != null) + var taskDialogButton = new TASKDIALOG_BUTTON { - dialogIcon = new TaskDialogIcon(extractedIcon); + nButtonID = IDOK, + pszButtonText = (ushort*)pszHide, + }; + var taskDialogConfig = new TASKDIALOGCONFIG + { + cbSize = (uint)sizeof(TASKDIALOGCONFIG), + hwndParent = default, + hInstance = (HINSTANCE)Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().ManifestModule), + dwFlags = (int)TDF_CAN_BE_MINIMIZED | + (int)TDF_SHOW_MARQUEE_PROGRESS_BAR | + (int)TDF_CALLBACK_TIMER | + (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN), + dwCommonButtons = 0, + pszWindowTitle = (ushort*)pszWindowTitle, + pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle, + pszMainInstruction = null, + pszContent = null, + cButtons = 1, + pButtons = &taskDialogButton, + nDefaultButton = IDOK, + cRadioButtons = 0, + pRadioButtons = null, + nDefaultRadioButton = 0, + pszVerificationText = null, + pszExpandedInformation = null, + pszExpandedControlText = null, + pszCollapsedControlText = null, + pszFooterIcon = null, + pszFooter = null, + pfCallback = &HResultFuncBinder, + lpCallbackData = 0, + cxWidth = 0, + }; + + HANDLE hActCtx = default; + GCHandle gch = default; + nuint cookie = 0; + try + { + var actctx = new ACTCTXW + { + cbSize = (uint)sizeof(ACTCTXW), + dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID, + lpResourceName = (ushort*)pszThemesManifestResourceName, + hModule = GetModuleHandleW((ushort*)pszDalamudBoot), + }; + hActCtx = CreateActCtxW(&actctx); + if (hActCtx == default) + throw new Win32Exception("CreateActCtxW failure."); + + if (!ActivateActCtx(hActCtx, &cookie)) + throw new Win32Exception("ActivateActCtx failure."); + + gch = GCHandle.Alloc((Func)this.TaskDialogCallback); + taskDialogConfig.lpCallbackData = GCHandle.ToIntPtr(gch); + TaskDialogIndirect(&taskDialogConfig, null, null, null).ThrowOnError(); + } + catch (Exception e) + { + Log.Error(e, "TaskDialogIndirect failure."); + } + finally + { + if (gch.IsAllocated) + gch.Free(); + if (cookie != 0) + DeactivateActCtx(0, cookie); + ReleaseActCtx(hActCtx); } } - dialogIcon ??= TaskDialogIcon.Information; - this.page = new TaskDialogPage - { - ProgressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.Marquee), - Caption = "Dalamud", - Icon = dialogIcon, - Buttons = { this.inProgressHideButton }, - AllowMinimize = false, - AllowCancel = false, - Expander = new TaskDialogExpander - { - CollapsedButtonText = "What does this mean?", - ExpandedButtonText = "What does this mean?", - Text = "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" + - "This is likely normal, please wait a little while longer.", - }, - SizeToContent = true, - }; - - this.UpdatePage(); - - // Call private TaskDialog ctor - var ctor = typeof(TaskDialog).GetConstructor( - BindingFlags.Instance | BindingFlags.NonPublic, - null, - Array.Empty(), - null); - - var taskDialog = (TaskDialog)ctor!.Invoke(Array.Empty())!; - - this.page.Created += (_, _) => - { - var hwnd = new HWND(taskDialog.Handle); - - // Bring to front - Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, - SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE); - Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, - SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW | SET_WINDOW_POS_FLAGS.SWP_NOSIZE | - SET_WINDOW_POS_FLAGS.SWP_NOMOVE); - Windows.Win32.PInvoke.SetForegroundWindow(hwnd); - Windows.Win32.PInvoke.SetFocus(hwnd); - Windows.Win32.PInvoke.SetActiveWindow(hwnd); - }; - - // Call private "ShowDialogInternal" - var showDialogInternal = typeof(TaskDialog).GetMethod( - "ShowDialogInternal", - BindingFlags.Instance | BindingFlags.NonPublic, - null, - [typeof(IntPtr), typeof(TaskDialogPage), typeof(TaskDialogStartupLocation)], - null); - - var cts = new CancellationTokenSource(); - _ = this.DialogStatePeriodicUpdate(cts.Token); - - showDialogInternal!.Invoke( - taskDialog, - [IntPtr.Zero, this.page, TaskDialogStartupLocation.CenterScreen]); - Interlocked.Exchange(ref wasGloballyHidden, 1); - cts.Cancel(); + + return; + + [UnmanagedCallersOnly] + static HRESULT HResultFuncBinder(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, nint user) => + ((Func)GCHandle.FromIntPtr(user).Target!) + .Invoke(hwnd, msg, wParam, lParam); } } diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 3f8a55614..446e404dc 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -282,6 +282,7 @@ internal static class ServiceManager async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable, LoadingDialog.State state) { + loadingDialog.CurrentState = state; var tasks = tasksEnumerable.AsReadOnlyCollection(); if (tasks.Count == 0) return; @@ -294,7 +295,6 @@ internal static class ServiceManager { loadingDialog.Show(); loadingDialog.CanHide = true; - loadingDialog.CurrentState = state; } } }).ConfigureAwait(false); From 877906ae15e58c606429c5517f3bc193826ea409 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 19:09:05 +0900 Subject: [PATCH 25/79] Show logs on loading dialog --- Dalamud/EntryPoint.cs | 5 ++ .../Internal/Windows/ConsoleWindow.cs | 21 ++---- Dalamud/Service/LoadingDialog.cs | 72 ++++++++++++++++--- Dalamud/Service/ServiceManager.cs | 11 ++- 4 files changed, 81 insertions(+), 28 deletions(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 1e6fccd8b..fcf33fe28 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; +using Dalamud.Interface.Internal.Windows; using Dalamud.Logging.Internal; using Dalamud.Logging.Retention; using Dalamud.Plugin.Internal; @@ -232,6 +233,10 @@ public sealed class EntryPoint private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev) { + if (!LoadingDialog.IsGloballyHidden) + LoadingDialog.NewLogEntries.Enqueue(ev); + ConsoleWindow.NewLogEntries.Enqueue(ev); + if (ev.LogEvent.Exception == null) return; diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 8f7c0e36c..f7ce5d145 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -18,7 +18,6 @@ using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -39,9 +38,6 @@ internal class ConsoleWindow : Window, IDisposable private const int LogLinesMaximum = 1000000; private const int HistorySize = 50; - // Only this field may be touched from any thread. - private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries; - // Fields below should be touched only from the main thread. private readonly RollingList logText; private readonly RollingList filteredLogEntries; @@ -94,7 +90,6 @@ internal class ConsoleWindow : Window, IDisposable this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; - SerilogEventSink.Instance.LogLine += this.OnLogLine; Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate); @@ -114,7 +109,6 @@ internal class ConsoleWindow : Window, IDisposable this.logLinesLimit = configuration.LogLinesLimit; var limit = Math.Max(LogLinesMinimum, this.logLinesLimit); - this.newLogEntries = new(); this.logText = new(limit); this.filteredLogEntries = new(limit); @@ -126,6 +120,9 @@ internal class ConsoleWindow : Window, IDisposable } } + /// Gets the queue where log entries that are not processed yet are stored. + public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new(); + /// public override void OnOpen() { @@ -136,7 +133,6 @@ internal class ConsoleWindow : Window, IDisposable /// public void Dispose() { - SerilogEventSink.Instance.LogLine -= this.OnLogLine; this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; if (Service.GetNullable() is { } framework) framework.Update -= this.FrameworkOnUpdate; @@ -324,7 +320,7 @@ internal class ConsoleWindow : Window, IDisposable ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit, this.CommandInputCallback)) { - this.newLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), []))); + NewLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), []))); this.ProcessCommand(); getFocus = true; } @@ -372,7 +368,7 @@ internal class ConsoleWindow : Window, IDisposable this.pendingClearLog = false; this.logText.Clear(); this.filteredLogEntries.Clear(); - this.newLogEntries.Clear(); + NewLogEntries.Clear(); } if (this.pendingRefilter) @@ -388,7 +384,7 @@ internal class ConsoleWindow : Window, IDisposable var numPrevFilteredLogEntries = this.filteredLogEntries.Count; var addedLines = 0; - while (this.newLogEntries.TryDequeue(out var logLine)) + while (NewLogEntries.TryDequeue(out var logLine)) addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent); this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries); } @@ -1062,11 +1058,6 @@ internal class ConsoleWindow : Window, IDisposable /// Queues filtering the log entries again, before next call to . private void QueueRefilter() => this.pendingRefilter = true; - /// Enqueues the new log line to the log-to-be-processed queue. - /// See for the handler for the queued log entries. - private void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent) => - this.newLogEntries.Enqueue(logEvent); - private bool DrawToggleButtonWithTooltip( string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState) { diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs index eebeb5b1c..42676386c 100644 --- a/Dalamud/Service/LoadingDialog.cs +++ b/Dalamud/Service/LoadingDialog.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Concurrent; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; @@ -14,6 +15,7 @@ using Dalamud.Plugin.Internal; using Dalamud.Utility; using Serilog; +using Serilog.Events; using TerraFX.Interop.Windows; @@ -31,7 +33,7 @@ namespace Dalamud; Justification = "Multiple fixed blocks")] internal sealed unsafe class LoadingDialog { - private static int wasGloballyHidden; + private readonly RollingList logs = new(20); private Thread? thread; private HWND hwndTaskDialog; @@ -60,6 +62,13 @@ internal sealed unsafe class LoadingDialog AutoUpdatePlugins, } + /// Gets the queue where log entries that are not processed yet are stored. + public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new(); + + /// Gets a value indicating whether the initial Dalamud loading dialog will not show again until next + /// game restart. + public static bool IsGloballyHidden { get; private set; } + /// /// Gets or sets the current state of the dialog. /// @@ -98,7 +107,7 @@ internal sealed unsafe class LoadingDialog /// public void Show() { - if (Volatile.Read(ref wasGloballyHidden) == 1) + if (IsGloballyHidden) return; if (this.thread?.IsAlive == true) @@ -119,6 +128,7 @@ internal sealed unsafe class LoadingDialog /// public void HideAndJoin() { + IsGloballyHidden = true; if (this.thread?.IsAlive is not true) return; @@ -210,6 +220,42 @@ internal sealed unsafe class LoadingDialog } } + private void UpdateExpandedInformation() + { + const int maxCharactersPerLine = 80; + + if (NewLogEntries.IsEmpty) + return; + while (NewLogEntries.TryDequeue(out var e)) + { + var t = e.Line.AsSpan(); + while (!t.IsEmpty) + { + var i = t.IndexOfAny('\r', '\n'); + var line = i == -1 ? t : t[..i]; + t = i == -1 ? ReadOnlySpan.Empty : t[(i + 1)..]; + if (line.IsEmpty) + continue; + + this.logs.Add( + line.Length < maxCharactersPerLine ? line.ToString() : $"{line[..(maxCharactersPerLine - 3)]}..."); + } + } + + var sb = new StringBuilder(); + foreach (var l in this.logs) + sb.AppendLine(l); + + fixed (void* pszText = sb.ToString()) + { + SendMessageW( + this.hwndTaskDialog, + (uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT, + (WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_EXPANDED_INFORMATION, + (LPARAM)pszText); + } + } + private void UpdateButtonEnabled() { if (this.hwndTaskDialog == default) @@ -227,6 +273,7 @@ internal sealed unsafe class LoadingDialog this.UpdateMainInstructionText(); this.UpdateContentText(); + this.UpdateExpandedInformation(); this.UpdateButtonEnabled(); SendMessageW(hwnd, (int)TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_MARQUEE, 1, 0); @@ -245,6 +292,7 @@ internal sealed unsafe class LoadingDialog case TASKDIALOG_NOTIFICATIONS.TDN_TIMER: this.UpdateContentText(); + this.UpdateExpandedInformation(); return S.S_OK; } @@ -260,10 +308,13 @@ internal sealed unsafe class LoadingDialog ? null : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe")); + fixed (void* pszEmpty = "-") fixed (void* pszWindowTitle = "Dalamud") - fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) - fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") fixed (void* pszDalamudBoot = "Dalamud.Boot.dll") + fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") + fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) + fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs")) + fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs")) { var taskDialogButton = new TASKDIALOG_BUTTON { @@ -277,6 +328,7 @@ internal sealed unsafe class LoadingDialog hInstance = (HINSTANCE)Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().ManifestModule), dwFlags = (int)TDF_CAN_BE_MINIMIZED | (int)TDF_SHOW_MARQUEE_PROGRESS_BAR | + (int)TDF_EXPAND_FOOTER_AREA | (int)TDF_CALLBACK_TIMER | (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN), dwCommonButtons = 0, @@ -291,14 +343,14 @@ internal sealed unsafe class LoadingDialog pRadioButtons = null, nDefaultRadioButton = 0, pszVerificationText = null, - pszExpandedInformation = null, - pszExpandedControlText = null, - pszCollapsedControlText = null, + pszExpandedInformation = (ushort*)pszEmpty, + pszExpandedControlText = (ushort*)pszShowLatestLogs, + pszCollapsedControlText = (ushort*)pszHideLatestLogs, pszFooterIcon = null, pszFooter = null, pfCallback = &HResultFuncBinder, lpCallbackData = 0, - cxWidth = 0, + cxWidth = 360, }; HANDLE hActCtx = default; @@ -338,7 +390,7 @@ internal sealed unsafe class LoadingDialog } } - Interlocked.Exchange(ref wasGloballyHidden, 1); + IsGloballyHidden = true; return; diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 446e404dc..5e22ed0c1 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -250,19 +250,20 @@ internal static class ServiceManager try { // Wait for all blocking constructors to complete first. - await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]), + await WaitWithTimeoutConsent( + blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]), LoadingDialog.State.LoadingDalamud); // All the BlockingEarlyLoadedService constructors have been run, // and blockerTasks now will not change. Now wait for them. // Note that ServiceManager.CallWhenServicesReady does not get to register a blocker. - await WaitWithTimeoutConsent(blockerTasks, + await WaitWithTimeoutConsent( + blockerTasks, LoadingDialog.State.LoadingPlugins); Log.Verbose("=============== BLOCKINGSERVICES & TASKS INITIALIZED ==============="); Timings.Event("BlockingServices Initialized"); BlockingServicesLoadedTaskCompletionSource.SetResult(); - loadingDialog.HideAndJoin(); } catch (Exception e) { @@ -277,6 +278,10 @@ internal static class ServiceManager Log.Error(e, "Failed resolving blocking services"); } + finally + { + loadingDialog.HideAndJoin(); + } return; From 856c1989348d0fdc1fa699f920f731f3d71a6d4d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 19:16:34 +0900 Subject: [PATCH 26/79] Display timestamp on logs --- Dalamud/Service/LoadingDialog.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs index 42676386c..f788ffb71 100644 --- a/Dalamud/Service/LoadingDialog.cs +++ b/Dalamud/Service/LoadingDialog.cs @@ -226,9 +226,12 @@ internal sealed unsafe class LoadingDialog if (NewLogEntries.IsEmpty) return; + + var sb = new StringBuilder(); while (NewLogEntries.TryDequeue(out var e)) { var t = e.Line.AsSpan(); + var first = true; while (!t.IsEmpty) { var i = t.IndexOfAny('\r', '\n'); @@ -236,13 +239,22 @@ internal sealed unsafe class LoadingDialog t = i == -1 ? ReadOnlySpan.Empty : t[(i + 1)..]; if (line.IsEmpty) continue; - - this.logs.Add( - line.Length < maxCharactersPerLine ? line.ToString() : $"{line[..(maxCharactersPerLine - 3)]}..."); + + sb.Clear(); + if (first) + sb.Append($"{e.LogEvent.Timestamp:HH:mm:ss} | "); + else + sb.Append(" | "); + first = false; + if (line.Length < maxCharactersPerLine) + sb.Append(line); + else + sb.Append($"{line[..(maxCharactersPerLine - 3)]}..."); + this.logs.Add(sb.ToString()); } } - var sb = new StringBuilder(); + sb.Clear(); foreach (var l in this.logs) sb.AppendLine(l); From 1be5cd452d14c4f581823134d25fcddd97b8a4d8 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jul 2024 20:49:41 +0900 Subject: [PATCH 27/79] Make all TCS RunContinuationsAsynchronously --- Dalamud/Game/Config/GameConfig.cs | 15 +++++++++++---- Dalamud/Game/Framework.cs | 2 +- .../SingleFontChooserDialog.cs | 2 +- Dalamud/Interface/Internal/InterfaceManager.cs | 8 ++++---- .../Internal/Windows/PluginImageCache.cs | 4 ++-- .../PluginInstaller/PluginInstallerWindow.cs | 4 ++-- .../Internals/FontAtlasFactory.Implementation.cs | 2 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 2 +- .../TextureWraps/Internal/ViewportTextureWrap.cs | 3 ++- .../Utility/Internal/DevTextureSaveMenu.cs | 4 ++-- Dalamud/Service/ServiceManager.cs | 4 +++- Dalamud/Service/Service{T}.cs | 2 +- Dalamud/Utility/AsyncUtils.cs | 2 +- Dalamud/Utility/DynamicPriorityQueueLoader.cs | 2 +- 14 files changed, 33 insertions(+), 23 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 1aeb42488..bfb58fd3c 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -16,10 +16,17 @@ namespace Dalamud.Game.Config; [ServiceManager.EarlyLoadedService] internal sealed class GameConfig : IInternalDisposableService, IGameConfig { - private readonly TaskCompletionSource tcsInitialization = new(); - private readonly TaskCompletionSource tcsSystem = new(); - private readonly TaskCompletionSource tcsUiConfig = new(); - private readonly TaskCompletionSource tcsUiControl = new(); + private readonly TaskCompletionSource tcsInitialization = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + private readonly TaskCompletionSource tcsSystem = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + private readonly TaskCompletionSource tcsUiConfig = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + private readonly TaskCompletionSource tcsUiControl = + new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly GameConfigAddressResolver address = new(); private Hook? configChangeHook; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 4f9c8d6c6..07942f780 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -139,7 +139,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework if (numTicks <= 0) return Task.CompletedTask; - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken); return tcs.Task; } diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index c5c4581e7..f03518ada 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -59,7 +59,7 @@ public sealed class SingleFontChooserDialog : IDisposable private readonly int counter; private readonly byte[] fontPreviewText = new byte[2048]; - private readonly TaskCompletionSource tcs = new(); + private readonly TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly IFontAtlas atlas; private string popupImGuiName; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e2993f911..cbbf63075 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -336,7 +336,7 @@ internal class InterfaceManager : IInternalDisposableService /// A that resolves once is run. public Task RunBeforeImGuiRender(Action action) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.runBeforeImGuiRender.Enqueue( () => { @@ -359,7 +359,7 @@ internal class InterfaceManager : IInternalDisposableService /// A that resolves once is run. public Task RunBeforeImGuiRender(Func func) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.runBeforeImGuiRender.Enqueue( () => { @@ -380,7 +380,7 @@ internal class InterfaceManager : IInternalDisposableService /// A that resolves once is run. public Task RunAfterImGuiRender(Action action) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.runAfterImGuiRender.Enqueue( () => { @@ -403,7 +403,7 @@ internal class InterfaceManager : IInternalDisposableService /// A that resolves once is run. public Task RunAfterImGuiRender(Func func) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.runAfterImGuiRender.Enqueue( () => { diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index e67ff3cf5..e95d2e1b8 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -315,7 +315,7 @@ internal class PluginImageCache : IInternalDisposableService private Task RunInDownloadQueue(Func> func, ulong requestedFrame) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.downloadQueue.Add(Tuple.Create(requestedFrame, async () => { try @@ -332,7 +332,7 @@ internal class PluginImageCache : IInternalDisposableService private Task RunInLoadQueue(Func> func) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.loadQueue.Add(async () => { try diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 466277a2f..ccf7b8226 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3774,7 +3774,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.errorModalMessage = message; this.errorModalDrawing = true; this.errorModalOnNextFrame = true; - this.errorModalTaskCompletionSource = new TaskCompletionSource(); + this.errorModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); return this.errorModalTaskCompletionSource.Task; } @@ -3782,7 +3782,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.updateModalOnNextFrame = true; this.updateModalPlugin = plugin; - this.updateModalTaskCompletionSource = new TaskCompletionSource(); + this.updateModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); return this.updateModalTaskCompletionSource.Task; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index ef92ffd65..61ac00faf 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -497,7 +497,7 @@ internal sealed partial class FontAtlasFactory $"{nameof(FontAtlasAutoRebuildMode.Async)}."); } - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); try { var rebuildIndex = Interlocked.Increment(ref this.buildIndex); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 0e26145f0..b84a857da 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -242,7 +242,7 @@ internal abstract class FontHandle : IFontHandle if (this.Available) return Task.FromResult(this); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.ImFontChanged += OnImFontChanged; this.Disposed += OnDisposed; if (this.Available) diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs index ad3188925..6e21bc0e8 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs @@ -24,7 +24,8 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos private readonly string? debugName; private readonly LocalPlugin? ownerPlugin; private readonly CancellationToken cancellationToken; - private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); + private readonly TaskCompletionSource firstUpdateTaskCompletionSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); private ImGuiViewportTextureArgs args; private D3D11_TEXTURE2D_DESC desc; diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs index 2ff42bc2a..a6584f9aa 100644 --- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -59,7 +59,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService { var first = true; var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); Service.Get().Draw += DrawChoices; encoder = await tcs.Task; @@ -108,7 +108,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService string path; { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.fileDialogManager.SaveFileDialog( "Save texture...", $"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}", diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 5e22ed0c1..29016bc69 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -44,7 +44,9 @@ internal static class ServiceManager private static readonly List LoadedServices = new(); #endif - private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); + private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = + new(TaskCreationOptions.RunContinuationsAsynchronously); + private static readonly CancellationTokenSource UnloadCancellationTokenSource = new(); private static ManualResetEvent unloadResetEvent = new(false); diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index 57acd2ccf..b4bfff917 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -332,7 +332,7 @@ internal static class Service where T : IServiceType break; } - instanceTcs = new TaskCompletionSource(); + instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); instanceTcs.SetException(new UnloadedException()); } diff --git a/Dalamud/Utility/AsyncUtils.cs b/Dalamud/Utility/AsyncUtils.cs index 9533f2ab0..4de561275 100644 --- a/Dalamud/Utility/AsyncUtils.cs +++ b/Dalamud/Utility/AsyncUtils.cs @@ -21,7 +21,7 @@ public static class AsyncUtils /// Returns the first task that completes, according to . public static Task FirstSuccessfulTask(ICollection> tasks) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var remainingTasks = tasks.Count; foreach (var task in tasks) diff --git a/Dalamud/Utility/DynamicPriorityQueueLoader.cs b/Dalamud/Utility/DynamicPriorityQueueLoader.cs index 8109d2e94..83fd366bb 100644 --- a/Dalamud/Utility/DynamicPriorityQueueLoader.cs +++ b/Dalamud/Utility/DynamicPriorityQueueLoader.cs @@ -238,7 +238,7 @@ internal class DynamicPriorityQueueLoader : IDisposable params IDisposable?[] disposables) : base(basis, cancellationToken, disposables) { - this.taskCompletionSource = new(); + this.taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); this.immediateLoadFunction = immediateLoadFunction; } From 488cd13b26ee88502b018f4123c7ab3b80a4a9fa Mon Sep 17 00:00:00 2001 From: NotNite Date: Sun, 21 Jul 2024 09:43:15 -0400 Subject: [PATCH 28/79] Add WinForms back --- Dalamud/Dalamud.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9ed0aa991..97be8b600 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -32,6 +32,7 @@ + true true true portable From 521cbf472e9a3606ff510fe0d389f4fa87546dc8 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 21 Jul 2024 17:31:37 +0200 Subject: [PATCH 29/79] Update ClientStructs (#1942) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 07f7b3fda..47dc5beef 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 07f7b3fda2da0f9f8891241ca70839c2acdf2c4a +Subproject commit 47dc5beefe9b5fb44bfb35edc768d866d322cb09 From 42c728ec6c7e61dd22aeed67db4ddbe0e80692a9 Mon Sep 17 00:00:00 2001 From: ItsBexy <103910869+ItsBexy@users.noreply.github.com> Date: Sun, 21 Jul 2024 09:40:15 -0600 Subject: [PATCH 30/79] Update Addon Inspector for variant image node types (#1941) Now prints texture information for `AtkNineGridNode` and `AtkClippingMaskNode` --- Dalamud/Interface/Internal/UiDebug.cs | 108 ++++++++++++++++---------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index da5dc0d98..f18b132dc 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -189,6 +189,7 @@ internal unsafe class UiDebug case NodeType.Image: Util.ShowStruct(*(AtkImageNode*)node, (ulong)node); break; case NodeType.Collision: Util.ShowStruct(*(AtkCollisionNode*)node, (ulong)node); break; case NodeType.NineGrid: Util.ShowStruct(*(AtkNineGridNode*)node, (ulong)node); break; + case NodeType.ClippingMask: Util.ShowStruct(*(AtkClippingMaskNode*)node, (ulong)node); break; case NodeType.Counter: Util.ShowStruct(*(AtkCounterNode*)node, (ulong)node); break; default: Util.ShowStruct(*node, (ulong)node); break; } @@ -233,48 +234,15 @@ internal unsafe class UiDebug break; case NodeType.Image: var imageNode = (AtkImageNode*)node; - if (imageNode->PartsList != null) - { - if (imageNode->PartId > imageNode->PartsList->PartCount) - { - ImGui.Text("part id > part count?"); - } - else - { - var textureInfo = imageNode->PartsList->Parts[imageNode->PartId].UldAsset; - var texType = textureInfo->AtkTexture.TextureType; - ImGui.Text($"texture type: {texType} part_id={imageNode->PartId} part_id_count={imageNode->PartsList->PartCount}"); - if (texType == TextureType.Resource) - { - var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName; - var texString = texFileNameStdString->Length < 16 - ? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer) - : MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr); - - ImGui.Text($"texture path: {texString}"); - var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject; - - if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}")) - { - ImGui.Image(new IntPtr(kernelTexture->D3D11ShaderResourceView), new Vector2(kernelTexture->Width, kernelTexture->Height)); - ImGui.TreePop(); - } - } - else if (texType == TextureType.KernelTexture) - { - if (ImGui.TreeNode($"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}")) - { - ImGui.Image(new IntPtr(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView), new Vector2(textureInfo->AtkTexture.KernelTexture->Width, textureInfo->AtkTexture.KernelTexture->Height)); - ImGui.TreePop(); - } - } - } - } - else - { - ImGui.Text("no texture loaded"); - } - + PrintTextureInfo(imageNode->PartsList, imageNode->PartId); + break; + case NodeType.NineGrid: + var ngNode = (AtkNineGridNode*)node; + PrintTextureInfo(ngNode->PartsList, ngNode->PartId); + break; + case NodeType.ClippingMask: + var cmNode = (AtkClippingMaskNode*)node; + PrintTextureInfo(cmNode->PartsList, cmNode->PartId); break; } @@ -287,8 +255,64 @@ internal unsafe class UiDebug if (isVisible && !popped) ImGui.PopStyleColor(); + + static void PrintTextureInfo(AtkUldPartsList* partsList, uint partId) + { + if (partsList != null) + { + if (partId > partsList->PartCount) + { + ImGui.Text("part id > part count?"); + } + else + { + var textureInfo = partsList->Parts[partId].UldAsset; + var texType = textureInfo->AtkTexture.TextureType; + ImGui.Text( + $"texture type: {texType} part_id={partId} part_id_count={partsList->PartCount}"); + if (texType == TextureType.Resource) + { + var texFileNameStdString = + &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName; + var texString = texFileNameStdString->Length < 16 + ? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer) + : MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr); + + ImGui.Text($"texture path: {texString}"); + var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject; + + if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}")) + { + ImGui.Image( + new IntPtr(kernelTexture->D3D11ShaderResourceView), + new Vector2(kernelTexture->Width, kernelTexture->Height)); + ImGui.TreePop(); + } + } + else if (texType == TextureType.KernelTexture) + { + if (ImGui.TreeNode( + $"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}")) + { + ImGui.Image( + new IntPtr(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView), + new Vector2( + textureInfo->AtkTexture.KernelTexture->Width, + textureInfo->AtkTexture.KernelTexture->Height)); + ImGui.TreePop(); + } + } + } + } + else + { + ImGui.Text("no texture loaded"); + } + } } + + private void PrintComponentNode(AtkResNode* node, string treePrefix) { var compNode = (AtkComponentNode*)node; From 1f315be94e3c8df3e27b0dede77f25654529909f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 22 Jul 2024 05:42:18 +0900 Subject: [PATCH 31/79] Implement ReShade addon interface --- .../Internal/InterfaceManager.AsHook.cs | 75 + .../InterfaceManager.AsReShadeAddon.cs | 86 + .../Interface/Internal/InterfaceManager.cs | 172 +- .../ReShadeAddonInterface.AddonEvent.cs | 1706 +++++++++++++++++ .../Internal/ReShadeAddonInterface.Exports.cs | 59 + .../Internal/ReShadeAddonInterface.cs | 176 ++ Dalamud/Interface/Internal/SwapChainHelper.cs | 110 -- 7 files changed, 2140 insertions(+), 244 deletions(-) create mode 100644 Dalamud/Interface/Internal/InterfaceManager.AsHook.cs create mode 100644 Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs create mode 100644 Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs create mode 100644 Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs create mode 100644 Dalamud/Interface/Internal/ReShadeAddonInterface.cs diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs new file mode 100644 index 000000000..b2afb970f --- /dev/null +++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs @@ -0,0 +1,75 @@ +using System.Diagnostics; + +using Dalamud.Utility; + +namespace Dalamud.Interface.Internal; + +/// +/// This class manages interaction with the ImGui interface. +/// +internal partial class InterfaceManager +{ + private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) + { + if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) + return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + + Debug.Assert(this.dxgiPresentHook is not null, "How did PresentDetour get called when presentHook is null?"); + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + + if (this.scene == null) + this.InitScene(swapChain); + + Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + { + if (this.dalamudAtlas.BuildTask.Exception != null) + { + // TODO: Can we do something more user-friendly here? Unload instead? + Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); + Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); + } + + return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + } + + this.CumulativePresentCalls++; + this.IsMainThreadInPresent = true; + + while (this.runBeforeImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); + + RenderImGui(this.scene!); + this.PostImGuiRender(); + this.IsMainThreadInPresent = false; + + return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + } + + private IntPtr AsHookResizeBuffersDetour( + IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) + { + if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) + return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + +#if DEBUG + Log.Verbose( + $"Calling resizebuffers swap@{swapChain.ToInt64():X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); +#endif + + this.ResizeBuffers?.InvokeSafely(); + + this.scene?.OnPreResize(); + + var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + if (ret.ToInt64() == 0x887A0001) + { + Log.Error("invalid call to resizeBuffers"); + } + + this.scene?.OnPostResize((int)width, (int)height); + + return ret; + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs new file mode 100644 index 000000000..0f1eeb707 --- /dev/null +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -0,0 +1,86 @@ +using System.Diagnostics; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.Internal; + +/// +/// This class manages interaction with the ImGui interface. +/// +internal partial class InterfaceManager +{ + private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapchain) + { + var swapChain = swapchain.GetNative(); + if (this.scene?.SwapChain.NativePointer != (nint)swapChain) + return; + + this.scene?.OnPreResize(); + } + + private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapchain) + { + var swapChain = swapchain.GetNative(); + if (this.scene?.SwapChain.NativePointer != (nint)swapChain) + return; + + DXGI_SWAP_CHAIN_DESC desc; + if (swapChain->GetDesc(&desc).FAILED) + return; + + this.scene?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height); + } + + private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime) + { + var swapChain = runtime.GetNative(); + + if (this.scene == null) + this.InitScene(swapChain); + + if (this.scene?.SwapChain.NativePointer != swapChain) + return; + + Debug.Assert(this.dalamudAtlas is not null, "this.dalamudAtlas is not null"); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + { + if (this.dalamudAtlas.BuildTask.Exception != null) + { + // TODO: Can we do something more user-friendly here? Unload instead? + Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); + Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); + } + + return; + } + + this.CumulativePresentCalls++; + this.IsMainThreadInPresent = true; + + while (this.runBeforeImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); + + RenderImGui(this.scene!); + this.PostImGuiRender(); + this.IsMainThreadInPresent = false; + } + + private nint AsReShadeAddonResizeBuffersDetour( + nint swapChain, + uint bufferCount, + uint width, + uint height, + uint newFormat, + uint swapChainFlags) + { + // Hooked vtbl instead of registering ReShade event. This check is correct. + if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) + return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + + this.ResizeBuffers?.InvokeSafely(); + return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index cbbf63075..10d508d99 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -13,6 +13,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; @@ -29,10 +30,11 @@ using ImGuiNET; using ImGuiScene; +using JetBrains.Annotations; + using PInvoke; -using SharpDX; -using SharpDX.DXGI; +using TerraFX.Interop.Windows; // general dev notes, here because it's easiest @@ -52,7 +54,7 @@ namespace Dalamud.Interface.Internal; /// This class manages interaction with the ImGui interface. /// [ServiceManager.EarlyLoadedService] -internal class InterfaceManager : IInternalDisposableService +internal partial class InterfaceManager : IInternalDisposableService { /// /// The default font size, in points. @@ -75,6 +77,11 @@ internal class InterfaceManager : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + // ReShadeAddonInterface requires hooks to be alive to unregister itself. + [ServiceManager.ServiceDependency] + [UsedImplicitly] + private readonly HookManager hookManager = Service.Get(); + private readonly ConcurrentQueue runBeforeImGuiRender = new(); private readonly ConcurrentQueue runAfterImGuiRender = new(); @@ -82,8 +89,8 @@ internal class InterfaceManager : IInternalDisposableService private Hook? setCursorHook; private Hook? dxgiPresentHook; - private Hook? reshadeOnPresentHook; private Hook? resizeBuffersHook; + private ReShadeAddonInterface? reShadeAddonInterface; private IFontAtlas? dalamudAtlas; private ILockedImFont? defaultFontResourceLock; @@ -101,9 +108,6 @@ internal class InterfaceManager : IInternalDisposableService [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr DxgiPresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate void ReshadeOnPresentDelegate(nint swapChain, uint flags, nint presentParams); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); @@ -299,8 +303,8 @@ internal class InterfaceManager : IInternalDisposableService this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); Interlocked.Exchange(ref this.dxgiPresentHook, null)?.Dispose(); - Interlocked.Exchange(ref this.reshadeOnPresentHook, null)?.Dispose(); Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); + Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose(); } } @@ -431,11 +435,11 @@ internal class InterfaceManager : IInternalDisposableService try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -464,11 +468,11 @@ internal class InterfaceManager : IInternalDisposableService if (this.GameWindowHandle == 0) throw new InvalidOperationException("Game window is not yet ready."); var value = enabled ? 1 : 0; - ((Result)NativeFunctions.DwmSetWindowAttribute( + ((HRESULT)NativeFunctions.DwmSetWindowAttribute( this.GameWindowHandle, NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, ref value, - sizeof(int))).CheckError(); + sizeof(int))).ThrowOnError(); } private static InterfaceManager WhenFontsReady() @@ -632,86 +636,6 @@ internal class InterfaceManager : IInternalDisposableService args.SuppressWithValue(r.Value); } - private void ReshadeOnPresentDetour(nint swapChain, uint flags, nint presentParams) - { - if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - { - this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams); - return; - } - - Debug.Assert(this.reshadeOnPresentHook is not null, "this.reshadeOnPresentHook is not null"); - Debug.Assert(this.dalamudAtlas is not null, "this.dalamudAtlas is not null"); - - if (this.scene == null) - this.InitScene(swapChain); - - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - { - if (this.dalamudAtlas.BuildTask.Exception != null) - { - // TODO: Can we do something more user-friendly here? Unload instead? - Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); - Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); - } - - this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams); - return; - } - - this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; - } - - private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) - { - if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); - - Debug.Assert(this.dxgiPresentHook is not null, "How did PresentDetour get called when presentHook is null?"); - Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); - - if (this.scene == null) - this.InitScene(swapChain); - - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - { - if (this.dalamudAtlas.BuildTask.Exception != null) - { - // TODO: Can we do something more user-friendly here? Unload instead? - Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); - Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); - } - - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); - } - - this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; - - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); - } - private void PostImGuiRender() { while (this.runAfterImGuiRender.TryDequeue(out var action)) @@ -799,10 +723,7 @@ internal class InterfaceManager : IInternalDisposableService () => { // Update the ImGui default font. - unsafe - { - ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont; - } + ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont; // Update the reference to the resources of the default font. this.defaultFontResourceLock?.Dispose(); @@ -818,7 +739,6 @@ internal class InterfaceManager : IInternalDisposableService _ = this.dalamudAtlas.BuildFontsAsync(); SwapChainHelper.BusyWaitForGameDeviceSwapChain(); - SwapChainHelper.DetectReShade(); try { @@ -839,52 +759,36 @@ internal class InterfaceManager : IInternalDisposableService this.SetCursorDetour); Log.Verbose("===== S W A P C H A I N ====="); - this.resizeBuffersHook = Hook.FromAddress( - (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, - this.ResizeBuffersDetour); - Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}"); - - if (SwapChainHelper.ReshadeOnPresent is null) + if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) { - var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present; - this.dxgiPresentHook = Hook.FromAddress(addr, this.PresentDetour); - Log.Verbose($"ReShade::DXGISwapChain::on_present address {Util.DescribeAddress(addr)}"); + this.resizeBuffersHook = Hook.FromAddress( + (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, + this.AsReShadeAddonResizeBuffersDetour); + Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}"); + + Log.Verbose( + "Registered as a ReShade({name}: 0x{addr:X}) addon.", + ReShadeAddonInterface.ReShadeModule!.FileName, + ReShadeAddonInterface.ReShadeModule!.BaseAddress); + this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; + this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; + this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; } else { - var addr = (nint)SwapChainHelper.ReshadeOnPresent; - this.reshadeOnPresentHook = Hook.FromAddress(addr, this.ReshadeOnPresentDetour); + this.resizeBuffersHook = Hook.FromAddress( + (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, + this.AsHookResizeBuffersDetour); + Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}"); + + var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present; + this.dxgiPresentHook = Hook.FromAddress(addr, this.PresentDetour); Log.Verbose($"IDXGISwapChain::Present address {Util.DescribeAddress(addr)}"); } this.setCursorHook.Enable(); this.dxgiPresentHook?.Enable(); - this.reshadeOnPresentHook?.Enable(); - this.resizeBuffersHook.Enable(); - } - - private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) - { - if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); - -#if DEBUG - Log.Verbose($"Calling resizebuffers swap@{swapChain.ToInt64():X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); -#endif - - this.ResizeBuffers?.InvokeSafely(); - - this.scene?.OnPreResize(); - - var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); - if (ret.ToInt64() == 0x887A0001) - { - Log.Error("invalid call to resizeBuffers"); - } - - this.scene?.OnPostResize((int)width, (int)height); - - return ret; + this.resizeBuffersHook?.Enable(); } private IntPtr SetCursorDetour(IntPtr hCursor) diff --git a/Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs b/Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs new file mode 100644 index 000000000..23f01875d --- /dev/null +++ b/Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs @@ -0,0 +1,1706 @@ +namespace Dalamud.Interface.Internal; + +/// ReShade interface. +internal sealed partial class ReShadeAddonInterface +{ + /// Supported events emitted by ReShade. + private enum AddonEvent : uint + { +#pragma warning disable + + /// + /// Called after successful device creation, from: + /// + /// IDirect3D9::CreateDevice + /// IDirect3D9Ex::CreateDeviceEx + /// IDirect3DDevice9::Reset + /// IDirect3DDevice9Ex::ResetEx + /// D3D10CreateDevice + /// D3D10CreateDevice1 + /// D3D10CreateDeviceAndSwapChain + /// D3D10CreateDeviceAndSwapChain1 + /// D3D11CreateDevice + /// D3D11CreateDeviceAndSwapChain + /// D3D12CreateDevice + /// glMakeCurrent + /// vkCreateDevice + /// + /// Callback function signature: void (api::device *device) + /// + InitDevice, + + /// + /// Called on device destruction, before: + /// + /// IDirect3DDevice9::Reset + /// IDirect3DDevice9Ex::ResetEx + /// IDirect3DDevice9::Release + /// ID3D10Device::Release + /// ID3D11Device::Release + /// ID3D12Device::Release + /// wglDeleteContext + /// vkDestroyDevice + /// + /// Callback function signature: void (api::device *device) + /// + DestroyDevice, + + /// + /// Called after successful command list creation, from: + /// + /// ID3D11Device::CreateDeferredContext + /// ID3D11Device1::CreateDeferredContext1 + /// ID3D11Device2::CreateDeferredContext2 + /// ID3D11Device3::CreateDeferredContext3 + /// ID3D12Device::CreateCommandList + /// ID3D12Device4::CreateCommandList1 + /// vkAllocateCommandBuffers + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + /// + /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit immediate command list was created. + /// + InitCommandList, + + /// + /// Called on command list destruction, before: + /// + /// ID3D11CommandList::Release + /// ID3D12CommandList::Release + /// vkFreeCommandBuffers + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + DestroyCommandList, + + /// + /// Called after successful command queue creation, from: + /// + /// ID3D12Device::CreateCommandQueue + /// vkCreateDevice (for every queue associated with the device) + /// + /// Callback function signature: void (api::command_queue *queue) + /// + /// + /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit command queue was created. + /// + InitCommandQueue, + + /// + /// Called on command queue destruction, before: + /// + /// ID3D12CommandQueue::Release + /// vkDestroyDevice (for every queue associated with the device) + /// + /// Callback function signature: void (api::command_queue *queue) + /// + DestroyCommandQueue, + + /// + /// Called after successful swap chain creation, from: + /// + /// IDirect3D9::CreateDevice (for the implicit swap chain) + /// IDirect3D9Ex::CreateDeviceEx (for the implicit swap chain) + /// IDirect3D9Device::CreateAdditionalSwapChain + /// IDXGIFactory::CreateSwapChain + /// IDXGIFactory2::CreateSwapChain(...) + /// wglMakeCurrent + /// wglSwapBuffers (after window was resized) + /// vkCreateSwapchainKHR + /// xrCreateSession + /// + /// In addition, called when swap chain is resized, after: + /// + /// IDirect3DDevice9::Reset (for the implicit swap chain) + /// IDirect3DDevice9Ex::ResetEx (for the implicit swap chain) + /// IDXGISwapChain::ResizeBuffers + /// IDXGISwapChain3::ResizeBuffers1 + /// + /// Callback function signature: void (api::swapchain *swapchain) + /// + InitSwapChain, + + /// + /// Called on swap chain creation, before: + /// + /// IDirect3D9::CreateDevice (for the implicit swap chain) + /// IDirect3D9Ex::CreateDeviceEx (for the implicit swap chain) + /// IDirect3D9Device::CreateAdditionalSwapChain + /// IDirect3D9Device::Reset (for the implicit swap chain) + /// IDirect3D9DeviceEx::ResetEx (for the implicit swap chain) + /// IDXGIFactory::CreateSwapChain + /// IDXGIFactory2::CreateSwapChain(...) + /// IDXGISwapChain::ResizeBuffers + /// IDXGISwapChain3::ResizeBuffers1 + /// wglSetPixelFormat + /// vkCreateSwapchainKHR + /// + /// Callback function signature: bool (api::swapchain_desc &desc, void *hwnd) + /// + /// + /// To overwrite the swap chain description, modify desc in the callback and return , otherwise return . + /// + CreateSwapChain, + + /// + /// Called on swap chain destruction, before: + /// + /// IDirect3DDevice9::Release (for the implicit swap chain) + /// IDirect3DSwapChain9::Release + /// IDXGISwapChain::Release + /// wglDeleteContext + /// wglSwapBuffers (after window was resized) + /// vkDestroySwapchainKHR + /// xrDestroySession + /// + /// In addition, called when swap chain is resized, before: + /// + /// IDirect3DDevice9::Reset (for the implicit swap chain) + /// IDirect3DDevice9Ex::ResetEx (for the implicit swap chain) + /// IDXGISwapChain::ResizeBuffers + /// IDXGISwapChain1::ResizeBuffers1 + /// + /// Callback function signature: void (api::swapchain *swapchain) + /// + DestroySwapChain, + + /// + /// Called after effect runtime initialization (which happens after swap chain creation or a swap chain buffer resize). + /// Callback function signature: void (api::effect_runtime *runtime) + /// + InitEffectRuntime, + + /// + /// Called when an effect runtime is reset or destroyed. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + DestroyEffectRuntime, + + /// + /// Called after successful sampler creation from: + /// + /// ID3D10Device::CreateSamplerState + /// ID3D11Device::CreateSamplerState + /// ID3D12Device::CreateSampler + /// vkCreateSampler + /// + /// Callback function signature: void (api::device *device, const api::sampler_desc &desc, api::sampler sampler) + /// + /// + /// Is not called in D3D9 (since samplers are loose state there) or OpenGL. + /// + InitSampler, + + /// + /// Called on sampler creation, before: + /// + /// ID3D10Device::CreateSamplerState + /// ID3D11Device::CreateSamplerState + /// ID3D12Device::CreateSampler + /// ID3D12Device::CreateRootSignature + /// vkCreateSampler + /// + /// Callback function signature: bool (api::device *device, api::sampler_desc &desc) + /// + /// + /// To overwrite the sampler description, modify desc in the callback and return , otherwise return . + /// Is not called in D3D9 (since samplers are loose state there) or OpenGL. + /// + CreateSampler, + + /// + /// Called on sampler destruction, before: + /// + /// ID3D10SamplerState::Release + /// ID3D11SamplerState::Release + /// glDeleteSamplers + /// vkDestroySampler + /// + /// Callback function signature: void (api::device *device, api::sampler sampler) + /// + /// + /// Is not called in D3D9 (since samplers are loose state there), D3D12 (since samplers are descriptor handles instead of objects there) or OpenGL. + /// + DestroySampler, + + /// + /// Called after successful resource creation from: + /// + /// IDirect3DDevice9::CreateVertexBuffer + /// IDirect3DDevice9::CreateIndexBuffer + /// IDirect3DDevice9::CreateTexture + /// IDirect3DDevice9::CreateCubeTexture + /// IDirect3DDevice9::CreateVolumeTexture + /// IDirect3DDevice9::CreateRenderTargetSurface + /// IDirect3DDevice9::CreateDepthStencilSurface + /// IDirect3DDevice9::CreateOffscreenPlainSurface + /// IDirect3DDevice9Ex::CreateRenderTargetSurfaceEx + /// IDirect3DDevice9Ex::CreateDepthStencilSurfaceEx + /// IDirect3DDevice9Ex::CreateOffscreenPlainSurfaceEx + /// ID3D10Device::CreateBuffer + /// ID3D10Device::CreateTexture1D + /// ID3D10Device::CreateTexture2D + /// ID3D10Device::CreateTexture2D + /// ID3D11Device::CreateBuffer + /// ID3D11Device::CreateTexture1D + /// ID3D11Device::CreateTexture2D + /// ID3D11Device::CreateTexture3D + /// ID3D11Device3::CreateTexture2D + /// ID3D11Device3::CreateTexture3D + /// ID3D12Device::CreateCommittedResource + /// ID3D12Device::CreatePlacedResource + /// ID3D12Device::CreateReservedResource + /// ID3D12Device4::CreateCommittedResource1 + /// ID3D12Device4::CreateReservedResource1 + /// glBufferData + /// glBufferStorage + /// glNamedBufferData + /// glNamedBufferStorage + /// glTexImage1D + /// glTexImage2D + /// glTexImage2DMultisample + /// glTexImage3D + /// glTexImage3DMultisample + /// glCompressedTexImage1D + /// glCompressedTexImage2D + /// glCompressedTexImage3D + /// glTexStorage1D + /// glTexStorage2D + /// glTexStorage2DMultisample + /// glTexStorage3D + /// glTexStorage3DMultisample + /// glTextureStorage1D + /// glTextureStorage2D + /// glTextureStorage2DMultisample + /// glTextureStorage3D + /// glTextureStorage3DMultisample + /// glRenderbufferStorage + /// glRenderbufferStorageMultisample + /// glNamedRenderbufferStorage + /// glNamedRenderbufferStorageMultisample + /// vkBindBufferMemory + /// vkBindBufferMemory2 + /// vkBindImageMemory + /// vkBindImageMemory2 + /// + /// Callback function signature: void (api::device *device, const api::resource_desc &desc, const api::subresource_data *initial_data, api::resource_usage initial_state, api::resource resource) + /// + /// + /// May be called multiple times with the same resource handle (whenever the resource is updated or its reference count is incremented). + /// + InitResource, + + /// + /// Called on resource creation, before: + /// + /// IDirect3DDevice9::CreateVertexBuffer + /// IDirect3DDevice9::CreateIndexBuffer + /// IDirect3DDevice9::CreateTexture + /// IDirect3DDevice9::CreateCubeTexture + /// IDirect3DDevice9::CreateVolumeTexture + /// IDirect3DDevice9::CreateRenderTargetSurface + /// IDirect3DDevice9::CreateDepthStencilSurface + /// IDirect3DDevice9::CreateOffscreenPlainSurface + /// IDirect3DDevice9Ex::CreateRenderTargetSurfaceEx + /// IDirect3DDevice9Ex::CreateDepthStencilSurfaceEx + /// IDirect3DDevice9Ex::CreateOffscreenPlainSurfaceEx + /// ID3D10Device::CreateBuffer + /// ID3D10Device::CreateTexture1D + /// ID3D10Device::CreateTexture2D + /// ID3D10Device::CreateTexture2D + /// ID3D11Device::CreateBuffer + /// ID3D11Device::CreateTexture1D + /// ID3D11Device::CreateTexture2D + /// ID3D11Device::CreateTexture3D + /// ID3D11Device3::CreateTexture2D + /// ID3D11Device3::CreateTexture3D + /// ID3D12Device::CreateCommittedResource + /// ID3D12Device::CreatePlacedResource + /// ID3D12Device::CreateReservedResource + /// ID3D12Device4::CreateCommittedResource1 + /// ID3D12Device4::CreateReservedResource1 + /// glBufferData + /// glBufferStorage + /// glNamedBufferData + /// glNamedBufferStorage + /// glTexImage1D + /// glTexImage2D + /// glTexImage2DMultisample + /// glTexImage3D + /// glTexImage3DMultisample + /// glCompressedTexImage1D + /// glCompressedTexImage2D + /// glCompressedTexImage3D + /// glTexStorage1D + /// glTexStorage2D + /// glTexStorage2DMultisample + /// glTexStorage3D + /// glTexStorage3DMultisample + /// glTextureStorage1D + /// glTextureStorage2D + /// glTextureStorage2DMultisample + /// glTextureStorage3D + /// glTextureStorage3DMultisample + /// glRenderbufferStorage + /// glRenderbufferStorageMultisample + /// glNamedRenderbufferStorage + /// glNamedRenderbufferStorageMultisample + /// vkCreateBuffer + /// vkCreateImage + /// + /// Callback function signature: bool (api::device *device, api::resource_desc &desc, api::subresource_data *initial_data, api::resource_usage initial_state) + /// + /// + /// To overwrite the resource description, modify desc in the callback and return , otherwise return . + /// + CreateResource, + + /// + /// Called on resource destruction, before: + /// + /// IDirect3DResource9::Release + /// ID3D10Resource::Release + /// ID3D11Resource::Release + /// ID3D12Resource::Release + /// glDeleteBuffers + /// glDeleteTextures + /// glDeleteRenderbuffers + /// vkDestroyBuffer + /// vkDestroyImage + /// + /// Callback function signature: void (api::device *device, api::resource resource) + /// + DestroyResource, + + /// + /// Called after successful resource view creation from: + /// + /// IDirect3DDevice9::CreateTexture + /// IDirect3DDevice9::CreateCubeTexture + /// IDirect3DDevice9::CreateVolumeTexture + /// ID3D10Device::CreateShaderResourceView + /// ID3D10Device::CreateRenderTargetView + /// ID3D10Device::CreateDepthStencilView + /// ID3D10Device1::CreateShaderResourceView1 + /// ID3D11Device::CreateShaderResourceView + /// ID3D11Device::CreateUnorderedAccessView + /// ID3D11Device::CreateRenderTargetView + /// ID3D11Device::CreateDepthStencilView + /// ID3D11Device3::CreateShaderResourceView1 + /// ID3D11Device3::CreateUnorderedAccessView1 + /// ID3D11Device3::CreateRenderTargetView1 + /// ID3D12Device::CreateShaderResourceView + /// ID3D12Device::CreateUnorderedAccessView + /// ID3D12Device::CreateRenderTargetView + /// ID3D12Device::CreateDepthStencilView + /// glTexBuffer + /// glTextureBuffer + /// glTextureView + /// vkCreateBufferView + /// vkCreateImageView + /// vkCreateAccelerationStructureKHR + /// + /// Callback function signature: void (api::device *device, api::resource resource, api::resource_usage usage_type, const api::resource_view_desc &desc, api::resource_view view) + /// + /// + /// May be called multiple times with the same resource view handle (whenever the resource view is updated). + /// + InitResourceView, + + /// + /// Called on resource view creation, before: + /// + /// ID3D10Device::CreateShaderResourceView + /// ID3D10Device::CreateRenderTargetView + /// ID3D10Device::CreateDepthStencilView + /// ID3D10Device1::CreateShaderResourceView1 + /// ID3D11Device::CreateShaderResourceView + /// ID3D11Device::CreateUnorderedAccessView + /// ID3D11Device::CreateRenderTargetView + /// ID3D11Device::CreateDepthStencilView + /// ID3D11Device3::CreateShaderResourceView1 + /// ID3D11Device3::CreateUnorderedAccessView1 + /// ID3D11Device3::CreateRenderTargetView1 + /// ID3D12Device::CreateShaderResourceView + /// ID3D12Device::CreateUnorderedAccessView + /// ID3D12Device::CreateRenderTargetView + /// ID3D12Device::CreateDepthStencilView + /// glTexBuffer + /// glTextureBuffer + /// glTextureView + /// vkCreateBufferView + /// vkCreateImageView + /// vkCreateAccelerationStructureKHR + /// + /// Callback function signature: bool (api::device *device, api::resource resource, api::resource_usage usage_type, api::resource_view_desc &desc) + /// + /// + /// To overwrite the resource view description, modify desc in the callback and return , otherwise return . + /// Is not called in D3D9 (since resource views are tied to resources there). + /// + CreateResourceView, + + /// + /// Called on resource view destruction, before: + /// + /// IDirect3DResource9::Release + /// ID3D10View::Release + /// ID3D11View::Release + /// glDeleteTextures + /// vkDestroyBufferView + /// vkDestroyImageView + /// vkDestroyAccelerationStructureKHR + /// + /// Callback function signature: void (api::device *device, api::resource_view view) + /// + /// + /// Is not called in D3D12 (since resource views are descriptor handles instead of objects there). + /// + DestroyResourceView, + + /// + /// Called after: + /// + /// IDirect3DVertexBuffer9::Lock + /// IDirect3DIndexBuffer9::Lock + /// ID3D10Resource::Map + /// ID3D11DeviceContext::Map + /// ID3D12Resource::Map + /// glMapBuffer + /// glMapBufferRange + /// glMapNamedBuffer + /// glMapNamedBufferRange + /// + /// Callback function signature: void (api::device *device, api::resource resource, uint64_t offset, uint64_t size, api::map_access access, void **data) + /// + MapBufferRegion, + + /// + /// Called before: + /// + /// IDirect3DVertexBuffer9::Unlock + /// IDirect3DIndexBuffer9::Unlock + /// ID3D10Resource::Unmap + /// ID3D11DeviceContext::Unmap + /// ID3D12Resource::Unmap + /// glUnmapBuffer + /// glUnmapNamedBuffer + /// + /// Callback function signature: void (api::device *device, api::resource resource) + /// + UnmapBufferRegion, + + /// + /// Called after: + /// + /// IDirect3DSurface9::LockRect + /// IDirect3DVolume9::LockBox + /// IDirect3DTexture9::LockRect + /// IDirect3DVolumeTexture9::LockBox + /// IDirect3DCubeTexture9::LockRect + /// ID3D10Resource::Map + /// ID3D11DeviceContext::Map + /// ID3D12Resource::Map + /// + /// Callback function signature: void (api::device *device, api::resource resource, uint32_t subresource, const api::subresource_box *box, api::map_access access, api::subresource_data *data) + /// + MapTextureRegion, + + /// + /// Called before: + /// + /// IDirect3DSurface9::UnlockRect + /// IDirect3DVolume9::UnlockBox + /// IDirect3DTexture9::UnlockRect + /// IDirect3DVolumeTexture9::UnlockBox + /// IDirect3DCubeTexture9::UnlockRect + /// ID3D10Resource::Unmap + /// ID3D11DeviceContext::Unmap + /// ID3D12Resource::Unmap + /// + /// Callback function signature: void (api::device *device, api::resource resource, uint32_t subresource) + /// + UnmapTextureRegion, + + /// + /// Called before: + /// + /// ID3D10Device::UpdateSubresource + /// ID3D11DeviceContext::UpdateSubresource + /// glBufferSubData + /// glNamedBufferSubData + /// + /// Callback function signature: bool (api::device *device, const void *data, api::resource resource, uint64_t offset, uint64_t size) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Destination resource will be in the state. + /// + UpdateBufferRegion, + + /// + /// Called before: + /// + /// ID3D10Device::UpdateSubresource + /// ID3D11DeviceContext::UpdateSubresource + /// glTexSubData1D + /// glTexSubData2D + /// glTexSubData3D + /// glTextureSubData1D + /// glTextureSubData2D + /// glTextureSubData3D + /// glCompressedTexSubData1D + /// glCompressedTexSubData2D + /// glCompressedTexSubData3D + /// glCompressedTextureSubData1D + /// glCompressedTextureSubData2D + /// glCompressedTextureSubData3D + /// + /// Callback function signature: bool (api::device *device, const api::subresource_data &data, api::resource resource, uint32_t subresource, const api::subresource_box *box) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Destination resource will be in the state. + /// + UpdateTextureRegion, + + /// + /// Called after successful pipeline creation from: + /// + /// IDirect3DDevice9::CreateVertexShader + /// IDirect3DDevice9::CreatePixelShader + /// IDirect3DDevice9::CreateVertexDeclaration + /// ID3D10Device::CreateVertexShader + /// ID3D10Device::CreateGeometryShader + /// ID3D10Device::CreateGeometryShaderWithStreamOutput + /// ID3D10Device::CreatePixelShader + /// ID3D10Device::CreateInputLayout + /// ID3D10Device::CreateBlendState + /// ID3D10Device::CreateDepthStencilState + /// ID3D10Device::CreateRasterizerState + /// ID3D10Device1::CreateBlendState1 + /// ID3D11Device::CreateVertexShader + /// ID3D11Device::CreateHullShader + /// ID3D11Device::CreateDomainShader + /// ID3D11Device::CreateGeometryShader + /// ID3D11Device::CreateGeometryShaderWithStreamOutput + /// ID3D11Device::CreatePixelShader + /// ID3D11Device::CreateComputeShader + /// ID3D11Device::CreateInputLayout + /// ID3D11Device::CreateBlendState + /// ID3D11Device::CreateDepthStencilState + /// ID3D11Device::CreateRasterizerState + /// ID3D11Device1::CreateBlendState1 + /// ID3D11Device1::CreateRasterizerState1 + /// ID3D11Device3::CreateRasterizerState2 + /// ID3D12Device::CreateComputePipelineState + /// ID3D12Device::CreateGraphicsPipelineState + /// ID3D12Device2::CreatePipelineState + /// ID3D12Device5::CreateStateObject + /// ID3D12Device7::AddToStateObject + /// ID3D12PipelineLibrary::LoadComputePipeline + /// ID3D12PipelineLibrary::LoadGraphicsPipeline + /// ID3D12PipelineLibrary1::LoadPipeline + /// glLinkProgram + /// vkCreateComputePipelines + /// vkCreateGraphicsPipelines + /// + /// Callback function signature: void (api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects, api::pipeline pipeline) + /// + /// + /// May be called multiple times with the same pipeline handle (whenever the pipeline is updated or its reference count is incremented). + /// + InitPipeline, + + /// + /// Called on pipeline creation, before: + /// + /// IDirect3DDevice9::CreateVertexShader + /// IDirect3DDevice9::CreatePixelShader + /// IDirect3DDevice9::CreateVertexDeclaration + /// ID3D10Device::CreateVertexShader + /// ID3D10Device::CreateGeometryShader + /// ID3D10Device::CreateGeometryShaderWithStreamOutput + /// ID3D10Device::CreatePixelShader + /// ID3D10Device::CreateInputLayout + /// ID3D10Device::CreateBlendState + /// ID3D10Device::CreateDepthStencilState + /// ID3D10Device::CreateRasterizerState + /// ID3D10Device1::CreateBlendState1 + /// ID3D11Device::CreateVertexShader + /// ID3D11Device::CreateHullShader + /// ID3D11Device::CreateDomainShader + /// ID3D11Device::CreateGeometryShader + /// ID3D11Device::CreateGeometryShaderWithStreamOutput + /// ID3D11Device::CreatePixelShader + /// ID3D11Device::CreateComputeShader + /// ID3D11Device::CreateInputLayout + /// ID3D11Device::CreateBlendState + /// ID3D11Device::CreateDepthStencilState + /// ID3D11Device::CreateRasterizerState + /// ID3D11Device1::CreateBlendState1 + /// ID3D11Device1::CreateRasterizerState1 + /// ID3D11Device3::CreateRasterizerState2 + /// ID3D12Device::CreateComputePipelineState + /// ID3D12Device::CreateGraphicsPipelineState + /// ID3D12Device2::CreatePipelineState + /// ID3D12Device5::CreateStateObject + /// glShaderSource + /// vkCreateComputePipelines + /// vkCreateGraphicsPipelines + /// + /// Callback function signature: bool (api::device *device, api::pipeline_layout layout, uint32_t subobject_count, const api::pipeline_subobject *subobjects) + /// + /// + /// To overwrite the pipeline description, modify desc in the callback and return , otherwise return . + /// + CreatePipeline, + + /// + /// Called on pipeline destruction, before: + /// + /// ID3D10VertexShader::Release + /// ID3D10GeometryShader::Release + /// ID3D10PixelShader::Release + /// ID3D10InputLayout::Release + /// ID3D10BlendState::Release + /// ID3D10DepthStencilState::Release + /// ID3D10RasterizerState::Release + /// ID3D11VertexShader::Release + /// ID3D11HullShader::Release + /// ID3D11DomainShader::Release + /// ID3D11GeometryShader::Release + /// ID3D11PixelShader::Release + /// ID3D11ComputeShader::Release + /// ID3D11InputLayout::Release + /// ID3D11BlendState::Release + /// ID3D11DepthStencilState::Release + /// ID3D11RasterizerState::Release + /// ID3D12PipelineState::Release + /// ID3D12StateObject::Release + /// glDeleteProgram + /// vkDestroyPipeline + /// + /// Callback function signature: void (api::device *device, api::pipeline pipeline) + /// + /// + /// Is not called in D3D9. + /// + DestroyPipeline, + + /// + /// Called after successful pipeline layout creation from: + /// + /// ID3D12Device::CreateRootSignature + /// vkCreatePipelineLayout + /// + /// Callback function signature: void (api::device *device, uint32_t param_count, const api::pipeline_layout_param *params, api::pipeline_layout layout) + /// + /// + /// In case of D3D9, D3D10, D3D11 and OpenGL this is called during device initialization as well and behaves as if an implicit global pipeline layout was created. + /// + InitPipelineLayout, + + /// + /// Called on pipeline layout creation, before: + /// + /// ID3D12Device::CreateRootSignature + /// vkCreatePipelineLayout + /// + /// Callback function signature: bool (api::device *device, uint32_t &param_count, api::pipeline_layout_param *&params) + /// + /// + /// Is not called in D3D9, D3D10, D3D11 or OpenGL. + /// + CreatePipelineLayout, + + /// + /// Called on pipeline layout destruction, before: + /// + /// ID3D12RootSignature::Release + /// VkDestroyPipelineLayout + /// + /// Callback function signature: void (api::device *device, api::pipeline_layout layout) + /// + DestroyPipelineLayout, + + /// + /// Called before: + /// + /// ID3D12Device::CopyDescriptors + /// ID3D12Device::CopyDescriptorsSimple + /// vkUpdateDescriptorSets + /// + /// Callback function signature: bool (api::device *device, uint32_t count, const api::descriptor_table_copy *copies) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + CopyDescriptorTables, + + /// + /// Called before: + /// + /// ID3D12Device::CreateConstantBufferView + /// ID3D12Device::CreateShaderResourceView + /// ID3D12Device::CreateUnorderedAccessView + /// ID3D12Device::CreateSampler + /// vkUpdateDescriptorSets + /// + /// Callback function signature: bool (api::device *device, uint32_t count, const api::descriptor_table_update *updates) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + UpdateDescriptorTables, + + /// + /// Called after successful query heap creation from: + /// + /// ID3D12Device::CreateQueryHeap + /// vkCreateQueryPool + /// + /// Callback function signature: void (api::device *device, api::query_type type, uint32_t size, api::query_heap heap) + /// + InitQueryHeap, + + /// + /// Called on query heap creation, before: + /// + /// ID3D12Device::CreateQueryHeap + /// vkCreateQueryPool + /// + /// Callback function signature: bool (api::device *device, api::query_type type, uint32_t &size) + /// + CreateQueryHeap, + + /// + /// Called on query heap destruction, before: + /// + /// ID3D12QueryHeap::Release + /// vkDestroyQueryPool + /// + /// Callback function signature: void (api::device *device, api::query_heap heap) + /// + DestroyQueryHeap, + + /// + /// Called before: + /// + /// vkGetQueryPoolResults + /// + /// Callback function signature: bool (api::device *device, api::query_heap heap, uint32_t first, uint32_t count, void *results, uint32_t stride) + /// + GetQueryHeapResults, + + /// + /// Called after: + /// + /// ID3D12GraphicsCommandList::ResourceBarrier + /// ID3D12GraphicsCommandList7::Barrier + /// vkCmdPipelineBarrier + /// vkCmdPipelineBarrier2 + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::resource *resources, const api::resource_usage *old_states, const api::resource_usage *new_states) + /// + Barrier, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList4::BeginRenderPass + /// vkCmdBeginRenderPass + /// vkCmdBeginRenderPass2 + /// vkCmdNextSubpass + /// vkCmdNextSubpass2 + /// vkCmdBeginRendering + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::render_pass_render_target_desc *rts, const api::render_pass_depth_stencil_desc *ds) + /// + /// + /// The depth-stencil description argument is optional and may be (which indicates that no depth-stencil is used). + /// + BeginRenderPass, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList4::EndRenderPass + /// vkCmdEndRenderPass + /// vkCmdEndRenderPass2 + /// vkCmdNextSubpass + /// vkCmdNextSubpass2 + /// vkCmdEndRendering + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + EndRenderPass, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetRenderTarget + /// IDirect3DDevice9::SetDepthStencilSurface + /// ID3D10Device::OMSetRenderTargets + /// ID3D11DeviceContext::OMSetRenderTargets + /// ID3D11DeviceContext::OMSetRenderTargetsAndUnorderedAccessViews + /// ID3D12GraphicsCommandList::OMSetRenderTargets + /// glBindFramebuffer + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::resource_view *rtvs, api::resource_view dsv) + /// + BindRenderTargetsAndDepthStencil, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetVertexShader + /// IDirect3DDevice9::SetPixelShader + /// IDirect3DDevice9::SetVertexDeclaration + /// IDirect3DDevice9::ProcessVertices + /// ID3D10Device::VSSetShader + /// ID3D10Device::GSSetShader + /// ID3D10Device::PSSetShader + /// ID3D10Device::IASetInputLayout + /// ID3D10Device::OMSetBlendState + /// ID3D10Device::OMSetDepthStencilState + /// ID3D10Device::RSSetState + /// ID3D11DeviceContext::VSSetShader + /// ID3D11DeviceContext::HSSetShader + /// ID3D11DeviceContext::DSSetShader + /// ID3D11DeviceContext::GSSetShader + /// ID3D11DeviceContext::PSSetShader + /// ID3D11DeviceContext::CSSetShader + /// ID3D11DeviceContext::IASetInputLayout + /// ID3D11DeviceContext::OMSetBlendState + /// ID3D11DeviceContext::OMSetDepthStencilState + /// ID3D11DeviceContext::RSSetState + /// ID3D12GraphicsCommandList::Reset + /// ID3D12GraphicsCommandList::SetPipelineState + /// ID3D12GraphicsCommandList4::SetPipelineState1 + /// glUseProgram + /// glBindVertexArray + /// vkCmdBindPipeline + /// + /// Callback function signature: void (api::command_list *cmd_list, api::pipeline_stage stages, api::pipeline pipeline) + /// + BindPipeline, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetRenderState + /// ID3D10Device::IASetPrimitiveTopology + /// ID3D10Device::OMSetBlendState + /// ID3D10Device::OMSetDepthStencilState + /// ID3D11DeviceContext::IASetPrimitiveTopology + /// ID3D11DeviceContext::OMSetBlendState + /// ID3D11DeviceContext::OMSetDepthStencilState + /// ID3D12GraphicsCommandList::IASetPrimitiveTopology + /// ID3D12GraphicsCommandList::OMSetBlendFactor + /// ID3D12GraphicsCommandList::OMSetStencilRef + /// gl(...) + /// vkCmdSetDepthBias + /// vkCmdSetBlendConstants + /// vkCmdSetStencilCompareMask + /// vkCmdSetStencilWriteMask + /// vkCmdSetStencilReference + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t count, const api::dynamic_state *states, const uint32_t *values) + /// + BindPipelineStates, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetViewport + /// IDirect3DDevice9::SetRenderTarget (implicitly updates the viewport) + /// ID3D10Device::RSSetViewports + /// ID3D11DeviceContext::RSSetViewports + /// ID3D12GraphicsCommandList::RSSetViewports + /// glViewport + /// glViewportArrayv + /// glViewportIndexedf + /// glViewportIndexedfv + /// vkCmdSetViewport + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::viewport *viewports) + /// + BindViewports, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetScissorRect + /// ID3D10Device::RSSetScissorRects + /// ID3D11DeviceContext::RSSetScissorRects + /// ID3D12GraphicsCommandList::RSSetScissorRects + /// glScissor + /// glScissorArrayv + /// glScissorIndexed + /// glScissorIndexedv + /// vkCmdSetScissor + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::rect *rects) + /// + BindScissorRects, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetVertexShaderConstantF + /// IDirect3DDevice9::SetPixelShaderConstantF + /// ID3D12GraphicsCommandList::SetComputeRoot32BitConstant + /// ID3D12GraphicsCommandList::SetComputeRoot32BitConstants + /// ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstant + /// ID3D12GraphicsCommandList::SetGraphicsRoot32BitConstants + /// glUniform(...) + /// vkCmdPushConstants + /// + /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, uint32_t first, uint32_t count, const void *values) + /// + PushConstants, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetTexture + /// ID3D10Device::VSSetSamplers + /// ID3D10Device::VSSetShaderResources + /// ID3D10Device::VSSetConstantBuffers + /// ID3D10Device::GSSetSamplers + /// ID3D10Device::GSSetShaderResources + /// ID3D10Device::GSSetConstantBuffers + /// ID3D10Device::PSSetSamplers + /// ID3D10Device::PSSetShaderResources + /// ID3D10Device::PSSetConstantBuffers + /// ID3D11DeviceContext::VSSetSamplers + /// ID3D11DeviceContext::VSSetShaderResources + /// ID3D11DeviceContext::VSSetConstantBuffers + /// ID3D11DeviceContext::HSSetSamplers + /// ID3D11DeviceContext::HSSetShaderResources + /// ID3D11DeviceContext::HSSetConstantBuffers + /// ID3D11DeviceContext::DSSetSamplers + /// ID3D11DeviceContext::DSSetShaderResources + /// ID3D11DeviceContext::DSSetConstantBuffers + /// ID3D11DeviceContext::GSSetSamplers + /// ID3D11DeviceContext::GSSetShaderResources + /// ID3D11DeviceContext::GSSetConstantBuffers + /// ID3D11DeviceContext::PSSetSamplers + /// ID3D11DeviceContext::PSSetShaderResources + /// ID3D11DeviceContext::PSSetConstantBuffers + /// ID3D11DeviceContext::CSSetSamplers + /// ID3D11DeviceContext::CSSetShaderResources + /// ID3D11DeviceContext::CSSetUnorderedAccessViews + /// ID3D11DeviceContext::CSSetConstantBuffers + /// ID3D12GraphicsCommandList::SetComputeRootConstantBufferView + /// ID3D12GraphicsCommandList::SetGraphicsRootConstantBufferView + /// ID3D12GraphicsCommandList::SetComputeRootShaderResourceView + /// ID3D12GraphicsCommandList::SetGraphicsRootShaderResourceView + /// ID3D12GraphicsCommandList::SetComputeRootUnorderedAccessView + /// ID3D12GraphicsCommandList::SetGraphicsRootUnorderedAccessView + /// glBindBufferBase + /// glBindBufferRange + /// glBindBuffersBase + /// glBindBuffersRange + /// glBindTexture + /// glBindImageTexture + /// glBindTextures + /// glBindImageTextures + /// glBindTextureUnit + /// glBindMultiTextureEXT + /// vkCmdPushDescriptorSetKHR + /// + /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t layout_param, const api::descriptor_table_update &update) + /// + PushDescriptors, + + /// + /// Called after: + /// + /// ID3D12GraphicsCommandList::SetComputeRootSignature + /// ID3D12GraphicsCommandList::SetGraphicsRootSignature + /// ID3D12GraphicsCommandList::SetComputeRootDescriptorTable + /// ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable + /// vkCmdBindDescriptorSets + /// + /// Callback function signature: void (api::command_list *cmd_list, api::shader_stage stages, api::pipeline_layout layout, uint32_t first, uint32_t count, const api::descriptor_table *tables) + /// + BindDescriptorTables, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetIndices + /// ID3D10Device::IASetIndexBuffer + /// ID3D11DeviceContext::IASetIndexBuffer + /// ID3D12GraphicsCommandList::IASetIndexBuffer + /// glBindBuffer + /// vkCmdBindIndexBuffer + /// + /// Callback function signature: void (api::command_list *cmd_list, api::resource buffer, uint64_t offset, uint32_t index_size) + /// + BindIndexBuffer, + + /// + /// Called after: + /// + /// IDirect3DDevice9::SetStreamSource + /// ID3D10Device::IASetVertexBuffers + /// ID3D11DeviceContext::IASetVertexBuffers + /// ID3D12GraphicsCommandList::IASetVertexBuffers + /// glBindBuffer + /// glBindVertexBuffer + /// glBindVertexBuffers + /// vkCmdBindVertexBuffers + /// vkCmdBindVertexBuffers2 + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint32_t *strides) + /// + /// + /// The strides argument is optional and may be . + /// + BindVertexBuffers, + + /// + /// Called after: + /// + /// IDirect3DDevice9::ProcessVertices + /// ID3D10Device::SOSetTargets + /// ID3D11DeviceContext::SOSetTargets + /// ID3D12GraphicsCommandList::SOSetTargets + /// glBindBufferBase + /// glBindBufferRange + /// glBindBuffersBase + /// glBindBuffersRange + /// vkCmdBindTransformFeedbackBuffersEXT + /// + /// Callback function signature: void (api::command_list *cmd_list, uint32_t first, uint32_t count, const api::resource *buffers, const uint64_t *offsets, const uint64_t *max_sizes, const api::resource *counter_buffers, const uint64_t *counter_offsets) + /// + /// + /// The counter arguments are optional and may be . + /// + BindStreamOutputBuffers, + + /// + /// Called before: + /// + /// IDirect3DDevice9::DrawPrimitive + /// IDirect3DDevice9::DrawPrimitiveUP + /// IDirect3DDevice9::ProcessVertices + /// ID3D10Device::Draw + /// ID3D10Device::DrawInstanced + /// ID3D11DeviceContext::Draw + /// ID3D11DeviceContext::DrawInstanced + /// ID3D12GraphicsCommandList::DrawInstanced + /// glDrawArrays + /// glDrawArraysInstanced + /// glDrawArraysInstancedBaseInstance + /// glMultiDrawArrays + /// vkCmdDraw + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t vertex_count, uint32_t instance_count, uint32_t first_vertex, uint32_t first_instance) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + Draw, + + /// + /// Called before: + /// + /// IDirect3DDevice9::DrawIndexedPrimitive + /// IDirect3DDevice9::DrawIndexedPrimitiveUP + /// ID3D10Device::DrawIndexed + /// ID3D10Device::DrawIndexedInstanced + /// ID3D11DeviceContext::DrawIndexed + /// ID3D11DeviceContext::DrawIndexedInstanced + /// ID3D12GraphicsCommandList::DrawIndexedInstanced + /// glDrawElements + /// glDrawElementsBaseVertex + /// glDrawElementsInstanced + /// glDrawElementsInstancedBaseVertex + /// glDrawElementsInstancedBaseInstance + /// glDrawElementsInstancedBaseVertexBaseInstance + /// glMultiDrawElements + /// glMultiDrawElementsBaseVertex + /// vkCmdDrawIndexed + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t index_count, uint32_t instance_count, uint32_t first_index, int32_t vertex_offset, uint32_t first_instance) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + DrawIndexed, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::Dispatch + /// ID3D12GraphicsCommandList::Dispatch + /// glDispatchCompute + /// vkCmdDispatch + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + Dispatch = 54, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::DispatchMesh + /// vkCmdDrawMeshTasksEXT + /// + /// Callback function signature: bool (api::command_list *cmd_list, uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + DispatchMesh = 89, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::DispatchRays + /// vkCmdTraceRaysKHR + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource raygen, uint64_t raygen_offset, uint64_t raygen_size, api::resource miss, uint64_t miss_offset, uint64_t miss_size, uint64_t miss_stride, api::resource hit_group, uint64_t hit_group_offset, uint64_t hit_group_size, uint64_t hit_group_stride, api::resource callable, uint64_t callable_offset, uint64_t callable_size, uint64_t callable_stride, uint32_t width, uint32_t height, uint32_t depth) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// In case of D3D12 and Vulkan, the shader handle buffer handles may be zero with the buffers instead referred to via a device address passed in the related offset argument. + /// + DispatchRays = 90, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::DrawInstancedIndirect + /// ID3D11DeviceContext::DrawIndexedInstancedIndirect + /// ID3D11DeviceContext::DispatchIndirect + /// ID3D12GraphicsCommandList::ExecuteIndirect + /// glDrawArraysIndirect + /// glDrawElementsIndirect + /// glMultiDrawArraysIndirect + /// glMultiDrawElementsIndirect + /// glDispatchComputeIndirect + /// vkCmdDrawIndirect + /// vkCmdDrawIndexedIndirect + /// vkCmdDispatchIndirect + /// vkCmdTraceRaysIndirect2KHR + /// vkCmdDrawMeshTasksIndirectEXT + /// vkCmdDrawMeshTasksIndirectCountEXT + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::indirect_command type, api::resource buffer, uint64_t offset, uint32_t draw_count, uint32_t stride) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + DrawOrDispatchIndirect = 55, + + /// + /// Called before: + /// + /// IDirect3DDevice9::UpdateTexture + /// IDirect3DDevice9::GetRenderTargetData + /// ID3D10Device::CopyResource + /// ID3D11DeviceContext::CopyResource + /// ID3D12GraphicsCommandList::CopyResource + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, api::resource dest) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// + CopyResource, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::CopyBufferRegion + /// glCopyBufferSubData + /// glCopyNamedBufferSubData + /// vkCmdCopyBuffer + /// vkCmdCopyBuffer2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint64_t source_offset, api::resource dest, uint64_t dest_offset, uint64_t size) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// + CopyBufferRegion, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::CopyTextureRegion + /// vkCmdCopyBufferToImage + /// vkCmdCopyBufferToImage2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint64_t source_offset, uint32_t row_length, uint32_t slice_height, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box argument is optional and may be (which indicates the entire subresource is referenced). + /// + CopyBufferToTexture, + + /// + /// Called before: + /// + /// IDirect3DDevice9::UpdateSurface + /// IDirect3DDevice9::StretchRect + /// ID3D10Device::CopySubresourceRegion + /// ID3D11DeviceContext::CopySubresourceRegion + /// ID3D12GraphicsCommandList::CopyTextureRegion + /// glBlitFramebuffer + /// glBlitNamedFramebuffer + /// glCopyImageSubData + /// glCopyTexSubImage1D + /// glCopyTexSubImage2D + /// glCopyTexSubImage3D + /// glCopyTextureSubImage1D + /// glCopyTextureSubImage2D + /// glCopyTextureSubImage3D + /// vkCmdBlitImage + /// vkCmdBlitImage2 + /// vkCmdCopyImage + /// vkCmdCopyImage2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, const api::subresource_box *dest_box, api::filter_mode filter) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box arguments are optional and may be (which indicates the entire subresource is used). + /// + CopyTextureRegion, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::CopyTextureRegion + /// vkCmdCopyImageToBuffer + /// vkCmdCopyImageToBuffer2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint64_t dest_offset, uint32_t row_length, uint32_t slice_height) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box argument is optional and may be (which indicates the entire subresource is used). + /// + CopyTextureToBuffer, + + /// + /// Called before: + /// + /// IDirect3DDevice9::StretchRect + /// ID3D10Device::ResolveSubresource + /// ID3D11DeviceContext::ResolveSubresource + /// ID3D12GraphicsCommandList::ResolveSubresource + /// ID3D12GraphicsCommandList1::ResolveSubresourceRegion + /// glBlitFramebuffer + /// glBlitNamedFramebuffer + /// vkCmdResolveImage + /// vkCmdResolveImage2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource source, uint32_t source_subresource, const api::subresource_box *source_box, api::resource dest, uint32_t dest_subresource, int32_t dest_x, int32_t dest_y, int32_t dest_z, api::format format) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Source resource will be in the state. + /// Destination resource will be in the state. + /// The subresource box argument is optional and may be (which indicates the entire subresource is used). + /// + ResolveTextureRegion, + + /// + /// Called before: + /// + /// IDirect3DDevice9::Clear + /// ID3D10Device::ClearDepthStencilView + /// ID3D11DeviceContext::ClearDepthStencilView + /// ID3D11DeviceContext1::ClearView (for depth-stencil views) + /// ID3D12GraphicsCommandList::ClearDepthStencilView + /// glClear + /// glClearBufferfi + /// glClearBufferfv + /// glClearNamedFramebufferfi + /// glClearNamedFramebufferfv + /// vkCmdClearDepthStencilImage + /// vkCmdClearAttachments + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view dsv, const float *depth, const uint8_t *stencil, uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resource will be in the state. + /// One of the depth or stencil clear value arguments may be when the respective component is not cleared. + /// + ClearDepthStencilView, + + /// + /// Called before: + /// + /// IDirect3DDevice9::Clear + /// IDirect3DDevice9::ColorFill + /// ID3D10Device::ClearRenderTargetView + /// ID3D11DeviceContext::ClearRenderTargetView + /// ID3D11DeviceContext1::ClearView (for render target views) + /// ID3D12GraphicsCommandList::ClearRenderTargetView + /// glClear + /// glClearBufferfv + /// glClearNamedFramebufferfv + /// vkCmdClearColorImage + /// vkCmdClearAttachments + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view rtv, const float color[4], uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resources will be in the state. + /// + ClearRenderTargetView, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::ClearUnorderedAccessViewUint + /// ID3D12GraphicsCommandList::ClearUnorderedAccessViewUint + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view uav, const uint32_t values[4], uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resource will be in the state. + /// + ClearUnorderedAccessViewUint, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::ClearUnorderedAccessViewFloat + /// ID3D11DeviceContext1::ClearView (for unordered access views) + /// ID3D12GraphicsCommandList::ClearUnorderedAccessViewFloat + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view uav, const float values[4], uint32_t rect_count, const api::rect *rects) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// Resource will be in the state. + /// + ClearUnorderedAccessViewFloat, + + /// + /// Called before: + /// + /// ID3D10Device::GenerateMips + /// ID3D11DeviceContext::GenerateMips + /// glGenerateMipmap + /// glGenerateTextureMipmap + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view srv) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + GenerateMipmaps, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::BeginQuery + /// vkCmdBeginQuery + /// vkCmdBeginQueryIndexedEXT + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + BeginQuery, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::EndQuery + /// vkCmdEndQuery + /// vkCmdEndQueryIndexedEXT + /// vkCmdWriteTimestamp + /// vkCmdWriteTimestamp2 + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t index) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + EndQuery, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::ResolveQueryData + /// vkCmdCopyQueryPoolResults + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::query_heap heap, api::query_type type, uint32_t first, uint32_t count, api::resource dest, uint64_t dest_offset, uint32_t stride) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + CopyQueryHeapResults = 69, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList4::CopyRaytracingAccelerationStructure + /// vkCmdCopyAccelerationStructureKHR + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::resource_view source, api::resource_view dest, api::acceleration_structure_copy_mode mode) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// + CopyAccelerationStructure = 91, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList4::BuildRaytracingAccelerationStructure + /// vkCmdBuildAccelerationStructuresKHR + /// + /// Callback function signature: bool (api::command_list *cmd_list, api::acceleration_structure_type type, api::acceleration_structure_build_flags flags, uint32_t input_count, const api::acceleration_structure_build_input *inputs, api::resource scratch, uint64_t scratch_offset, api::resource_view source, api::resource_view dest, api::acceleration_structure_build_mode mode) + /// + /// + /// To prevent this command from being executed, return , otherwise return . + /// In case of D3D12 and Vulkan, the scratch buffer handle may be zero with the buffer instead referred to via a device address passed in the related offset argument. + /// Scratch buffer will be in the resource state. + /// + BuildAccelerationStructure = 92, + + /// + /// Called before: + /// + /// ID3D12GraphicsCommandList::Reset + /// vkBeginCommandBuffer + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + /// + /// Is not called for immediate command lists (since they cannot be reset). + /// + ResetCommandList = 70, + + /// + /// Called before: + /// + /// ID3D11DeviceContext::FinishCommandList + /// ID3D12GraphicsCommandList::Close + /// vkEndCommandBuffer + /// + /// Callback function signature: void (api::command_list *cmd_list) + /// + /// + /// Is not called for immediate command lists (since they cannot be closed). + /// + CloseCommandList, + + /// + /// Called when a command list is submitted to a command queue (or an immediate command list is flushed), before: + /// + /// IDirect3DDevice9::EndScene + /// ID3D10Device::Flush + /// ID3D11DeviceContext::Flush + /// ID3D11DeviceContext3::Flush1 + /// ID3D12CommandQueue::ExecuteCommandLists + /// glFlush + /// vkQueueSubmit + /// + /// Callback function signature: void (api::command_queue *queue, api::command_list *cmd_list) + /// + ExecuteCommandList, + + /// + /// Called when a secondary command list is executed on a primary command list, before: + /// + /// ID3D11DeviceContext::ExecuteCommandList + /// ID3D12GraphicsCommandList::ExecuteBundle + /// vkCmdExecuteCommands + /// + /// In addition, called after: + /// + /// ID3D11DeviceContext::FinishCommandList + /// + /// Callback function signature: void (api::command_list *cmd_list, api::command_list *secondary_cmd_list) + /// + ExecuteSecondaryCommandList, + + /// + /// Called before: + /// + /// IDirect3DDevice9::Present + /// IDirect3DDevice9Ex::PresentEx + /// IDirect3DSwapChain9::Present + /// IDXGISwapChain::Present + /// IDXGISwapChain3::Present1 + /// ID3D12CommandQueueDownlevel::Present + /// wglSwapBuffers + /// vkQueuePresentKHR + /// IVRCompositor::Submit + /// xrEndFrame + /// + /// Callback function signature: void (api::command_queue *queue, api::swapchain *swapchain, const api::rect *source_rect, const api::rect *dest_rect, uint32_t dirty_rect_count, const api::rect *dirty_rects) + /// + /// + /// The source and destination rectangle arguments are optional and may be (which indicates the swap chain is presented in its entirety). + /// + Present, + + /// + /// Called before: + /// + /// IDXGISwapChain::SetFullscreenState + /// vkAcquireFullScreenExclusiveModeEXT + /// vkReleaseFullScreenExclusiveModeEXT + /// + /// Callback function signature: bool (api::swapchain *swapchain, bool fullscreen, void *hmonitor) + /// + /// + /// To prevent the fullscreen state from being changed, return , otherwise return . + /// + SetFullscreenState = 93, + + /// + /// Called after ReShade has rendered its overlay. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + ReShadePresent = 75, + + /// + /// Called right before ReShade effects are rendered. + /// Callback function signature: void (api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb) + /// + ReShadeBeginEffects, + + /// + /// Called right after ReShade effects were rendered. + /// Callback function signature: void (api::effect_runtime *runtime, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb) + /// + ReShadeFinishEffects, + + /// + /// Called right after all ReShade effects were reloaded. + /// This occurs during effect runtime initialization or because the user pressed the "Reload" button in the overlay. + /// Any , and handles are invalidated when this event occurs and need to be queried again. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + ReShadeReloadedEffects, + + /// + /// Called before a uniform variable is changed, with the new value. + /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_uniform_variable variable, const void *new_value, size_t new_value_size) + /// + /// + /// To prevent the variable value from being changed, return , otherwise return . + /// The new value has the data type reported by . The new value size is in bytes. + /// + ReShadeSetUniformValue, + + /// + /// Called before a technique is enabled or disabled, with the new state. + /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_technique technique, bool enabled) + /// + /// + /// To prevent the technique state from being changed, return , otherwise return . + /// + ReShadeSetTechniqueState, + + /// + /// Called between the ImGui::NewFrame and ImGui::EndFrame calls for the ReShade overlay. + /// Can be used to perform custom Dear ImGui calls, but it is recommended to instead use to register a dedicated overlay. + /// Callback function signature: void (api::effect_runtime *runtime) + /// + /// + /// This is not called for effect runtimes in VR. + /// + ReShadeOverlay, + + /// + /// Called after a screenshot was taken and saved to disk, with the path to the saved image file. + /// Callback function signature: void (api::effect_runtime *runtime, const char *path) + /// + ReShadeScreenshot, + + /// + /// Called for each technique after it was rendered, usually between and . + /// Callback function signature: void (api::effect_runtime *runtime, api::effect_technique technique, api::command_list *cmd_list, api::resource_view rtv, api::resource_view rtv_srgb) + /// + ReShadeRenderTechnique, + + /// + /// Called when all effects are about to be enabled or disabled. + /// Callback function signature: bool (api::effect_runtime *runtime, bool enabled) + /// + /// + /// To prevent the effects state from being changed, return , otherwise return . + /// + ReShadeSetEffectsState = 94, + + /// + /// Called after a preset was loaded and applied. + /// This occurs after effect reloading or when the user chooses a new preset in the overlay. + /// Callback function signature: void (api::effect_runtime *runtime, const char *path) + /// + ReShadeSetCurrentPresetPath = 84, + + /// + /// Called when the rendering order of loaded techniques is changed, with a handle array specifying the new order. + /// Callback function signature: bool (api::effect_runtime *runtime, size_t count, api::effect_technique *techniques) + /// + /// + /// To prevent the order from being changed, return , otherwise return . + /// + ReShadeReorderTechniques, + + /// + /// Called when the ReShade overlay is about to be opened or closed. + /// Callback function signature: bool (api::effect_runtime *runtime, bool open, api::input_source source) + /// + /// + /// To prevent the overlay state from being changed, return , otherwise return . + /// + ReShadeOpenOverlay, + + /// + /// Called when a uniform variable widget is added to the variable list in the overlay. + /// Can be used to replace with custom one or add widgets for specific uniform variables. + /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_uniform_variable variable) + /// + /// + /// To prevent the normal widget from being added to the overlay, return , otherwise return . + /// + ReShadeOverlayUniformVariable, + + /// + /// Called when a technique is added to the technique list in the overlay. + /// Can be used to replace with custom one or add widgets for specific techniques. + /// Callback function signature: bool (api::effect_runtime *runtime, api::effect_technique technique) + /// + /// + /// To prevent the normal widget from being added to the overlay, return , otherwise return . + /// + ReShadeOverlayTechnique, + } +} diff --git a/Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs new file mode 100644 index 000000000..3dce1b79b --- /dev/null +++ b/Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// ReShade interface. +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +internal sealed unsafe partial class ReShadeAddonInterface +{ + private static readonly ExportsStruct Exports; + + static ReShadeAddonInterface() + { + foreach (var m in Process.GetCurrentProcess().Modules.Cast()) + { + ExportsStruct e; + if (!GetProcAddressInto(m, nameof(e.ReShadeRegisterAddon), &e.ReShadeRegisterAddon) || + !GetProcAddressInto(m, nameof(e.ReShadeUnregisterAddon), &e.ReShadeUnregisterAddon) || + !GetProcAddressInto(m, nameof(e.ReShadeRegisterEvent), &e.ReShadeRegisterEvent) || + !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent)) + continue; + + ReShadeModule = m; + Exports = e; + return; + } + + return; + + bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) + { + Span name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1]; + name8[Encoding.UTF8.GetBytes(name, name8)] = 0; + *(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0])); + return *(nint*)res != 0; + } + } + + /// Gets the active ReShade module. + public static ProcessModule? ReShadeModule { get; private set; } + + private struct ExportsStruct + { + public delegate* unmanaged ReShadeRegisterAddon; + public delegate* unmanaged ReShadeUnregisterAddon; + public delegate* unmanaged ReShadeRegisterEvent; + public delegate* unmanaged ReShadeUnregisterEvent; + } +} diff --git a/Dalamud/Interface/Internal/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeAddonInterface.cs new file mode 100644 index 000000000..156688c27 --- /dev/null +++ b/Dalamud/Interface/Internal/ReShadeAddonInterface.cs @@ -0,0 +1,176 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; + +using JetBrains.Annotations; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// ReShade interface. +internal sealed unsafe partial class ReShadeAddonInterface : IDisposable +{ + private const int ReShadeApiVersion = 12; + + private readonly HMODULE hDalamudModule; + + private readonly Hook addonModuleResolverHook; + + private readonly DelegateStorage reShadeOverlayDelegate; + private readonly DelegateStorage initSwapChainDelegate; + private readonly DelegateStorage destroySwapChainDelegate; + + private ReShadeAddonInterface() + { + this.hDalamudModule = (HMODULE)Marshal.GetHINSTANCE(typeof(ReShadeAddonInterface).Assembly.ManifestModule); + if (!Exports.ReShadeRegisterAddon(this.hDalamudModule, ReShadeApiVersion)) + throw new InvalidOperationException("ReShadeRegisterAddon failure."); + + this.addonModuleResolverHook = Hook.FromImport( + ReShadeModule!, + "kernel32.dll", + nameof(GetModuleHandleExW), + 0, + this.GetModuleHandleExWDetour); + + this.addonModuleResolverHook.Enable(); + Exports.ReShadeRegisterEvent( + AddonEvent.ReShadeOverlay, + this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt))); + Exports.ReShadeRegisterEvent( + AddonEvent.InitSwapChain, + this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt))); + Exports.ReShadeRegisterEvent( + AddonEvent.DestroySwapChain, + this.destroySwapChainDelegate = new((ref ApiObject rt) => this.DestroySwapChain?.Invoke(ref rt))); + } + + /// Finalizes an instance of the class. + ~ReShadeAddonInterface() => this.ReleaseUnmanagedResources(); + + /// Delegate for . + /// Reference to the ReShade runtime. + public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime); + + /// Delegate for . + /// Reference to the ReShade SwapChain wrapper. + public delegate void ReShadeInitSwapChain(ref ApiObject swapChain); + + /// Delegate for . + /// Reference to the ReShade SwapChain wrapper. + public delegate void ReShadeDestroySwapChain(ref ApiObject swapChain); + + private delegate BOOL GetModuleHandleExWDelegate(uint dwFlags, ushort* lpModuleName, HMODULE* phModule); + + /// Called on . + public event ReShadeOverlayDelegate? ReShadeOverlay; + + /// Called on . + public event ReShadeInitSwapChain? InitSwapChain; + + /// Called on . + public event ReShadeDestroySwapChain? DestroySwapChain; + + /// Registers Dalamud as a ReShade addon. + /// Initialized interface. + /// true on success. + public static bool TryRegisterAddon([NotNullWhen(true)] out ReShadeAddonInterface? r) + { + try + { + r = Exports.ReShadeRegisterAddon is null ? null : new(); + return r is not null; + } + catch + { + r = null; + return false; + } + } + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() + { + Exports.ReShadeUnregisterEvent(AddonEvent.InitSwapChain, this.initSwapChainDelegate); + Exports.ReShadeUnregisterEvent(AddonEvent.DestroySwapChain, this.destroySwapChainDelegate); + Exports.ReShadeUnregisterEvent(AddonEvent.ReShadeOverlay, this.reShadeOverlayDelegate); + Exports.ReShadeUnregisterAddon(this.hDalamudModule); + this.addonModuleResolverHook.Disable(); + this.addonModuleResolverHook.Dispose(); + } + + private BOOL GetModuleHandleExWDetour(uint dwFlags, ushort* lpModuleName, HMODULE* phModule) + { + if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) == 0) + return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule); + if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT) == 0) + return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule); + if (lpModuleName == this.initSwapChainDelegate || + lpModuleName == this.destroySwapChainDelegate || + lpModuleName == this.reShadeOverlayDelegate) + { + *phModule = this.hDalamudModule; + return BOOL.TRUE; + } + + return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule); + } + + /// ReShade effect runtime object. + [StructLayout(LayoutKind.Sequential)] + public struct ApiObject + { + /// The vtable. + public VTable* Vtbl; + + /// Gets this object as a typed pointer. + /// Address of this instance. + /// This call is invalid if this object is not already fixed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ApiObject* AsPointer() => (ApiObject*)Unsafe.AsPointer(ref this); + + /// Gets the native object. + /// The native object. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public nint GetNative() => this.Vtbl->GetNative(this.AsPointer()); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T* GetNative() where T : unmanaged => (T*)this.GetNative(); + + /// VTable of . + [StructLayout(LayoutKind.Sequential)] + public struct VTable + { + /// + public delegate* unmanaged GetNative; + } + } + + private readonly struct DelegateStorage where T : Delegate + { + [UsedImplicitly] + public readonly T Delegate; + + public readonly void* Address; + + public DelegateStorage(T @delegate) + { + this.Delegate = @delegate; + this.Address = (void*)Marshal.GetFunctionPointerForDelegate(@delegate); + } + + public static implicit operator void*(DelegateStorage sto) => sto.Address; + } +} diff --git a/Dalamud/Interface/Internal/SwapChainHelper.cs b/Dalamud/Interface/Internal/SwapChainHelper.cs index 09c1f52fd..e0c3f21a8 100644 --- a/Dalamud/Interface/Internal/SwapChainHelper.cs +++ b/Dalamud/Interface/Internal/SwapChainHelper.cs @@ -1,13 +1,7 @@ -using System.Diagnostics; using System.Threading; -using Dalamud.Game; -using Dalamud.Utility; - using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Serilog; - using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -16,12 +10,6 @@ namespace Dalamud.Interface.Internal; /// Helper for dealing with swap chains. internal static unsafe class SwapChainHelper { - /// - /// Gets the function pointer for ReShade's DXGISwapChain::on_present. - /// Source. - /// - public static delegate* unmanaged ReshadeOnPresent { get; private set; } - /// Gets the game's active instance of IDXGISwapChain that is initialized. /// Address of the game's instance of IDXGISwapChain, or null if not available (yet.) public static IDXGISwapChain* GameDeviceSwapChain @@ -92,102 +80,4 @@ internal static unsafe class SwapChainHelper while (GameDeviceSwapChain is null) Thread.Yield(); } - - /// Detects ReShade and populate . - public static void DetectReShade() - { - var modules = Process.GetCurrentProcess().Modules; - foreach (ProcessModule processModule in modules) - { - if (!processModule.FileName.EndsWith("game\\dxgi.dll", StringComparison.InvariantCultureIgnoreCase)) - continue; - - try - { - var fileInfo = FileVersionInfo.GetVersionInfo(processModule.FileName); - - if (fileInfo.FileDescription == null) - break; - - if (!fileInfo.FileDescription.Contains("GShade") && !fileInfo.FileDescription.Contains("ReShade")) - break; - - // warning: these comments may no longer be accurate. - // reshade master@4232872 RVA - // var p = processModule.BaseAddress + 0x82C7E0; // DXGISwapChain::Present - // var p = processModule.BaseAddress + 0x82FAC0; // DXGISwapChain::runtime_present - // DXGISwapChain::handle_device_loss =>df DXGISwapChain::Present => DXGISwapChain::runtime_present - // 5.2+ - F6 C2 01 0F 85 - // 6.0+ - F6 C2 01 0F 85 88 - - var scanner = new SigScanner(processModule); - var reShadeDxgiPresent = nint.Zero; - - if (fileInfo.FileVersion?.StartsWith("6.") == true) - { - // No Addon - if (scanner.TryScanText("F6 C2 01 0F 85 A8", out reShadeDxgiPresent)) - { - Log.Information("Hooking present for ReShade 6 No-Addon"); - } - - // Addon - else if (scanner.TryScanText("F6 C2 01 0F 85 88", out reShadeDxgiPresent)) - { - Log.Information("Hooking present for ReShade 6 Addon"); - } - - // Fallback - else - { - Log.Error("Failed to get ReShade 6 DXGISwapChain::on_present offset!"); - } - } - - // Looks like this sig only works for GShade 4 - if (reShadeDxgiPresent == nint.Zero && fileInfo.FileDescription?.Contains("GShade 4.") == true) - { - if (scanner.TryScanText("E8 ?? ?? ?? ?? 45 0F B6 5E ??", out reShadeDxgiPresent)) - { - Log.Information("Hooking present for GShade 4"); - } - else - { - Log.Error("Failed to find GShade 4 DXGISwapChain::on_present offset!"); - } - } - - if (reShadeDxgiPresent == nint.Zero) - { - if (scanner.TryScanText("F6 C2 01 0F 85", out reShadeDxgiPresent)) - { - Log.Information("Hooking present for ReShade with fallback 5.X sig"); - } - else - { - Log.Error("Failed to find ReShade DXGISwapChain::on_present offset with fallback sig!"); - } - } - - Log.Information( - "ReShade DLL: {FileName} ({Info} - {Version}) with DXGISwapChain::on_present at {Address}", - processModule.FileName, - fileInfo.FileDescription ?? "Unknown", - fileInfo.FileVersion ?? "Unknown", - Util.DescribeAddress(reShadeDxgiPresent)); - - if (reShadeDxgiPresent != nint.Zero) - { - ReshadeOnPresent = (delegate* unmanaged)reShadeDxgiPresent; - } - - break; - } - catch (Exception e) - { - Log.Error(e, "Failed to get ReShade version info"); - break; - } - } - } } From 7830d7651b528946cdd840b0896db9616f96e122 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 22 Jul 2024 00:34:49 +0200 Subject: [PATCH 32/79] pi: force-reload pluginmasters when toggling testing on a plugin --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ccf7b8226..92afe56a6 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2889,6 +2889,7 @@ internal class PluginInstallerWindow : Window, IDisposable } configuration.QueueSave(); + _ = pluginManager.ReloadPluginMastersAsync(); } if (repoManifest?.IsTestingExclusive == true) From d44baa1f49e8b4f7a5d7d790a4eda13ba72797d4 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Mon, 22 Jul 2024 08:00:15 +0900 Subject: [PATCH 33/79] Small efficiency improvements for DtrBar (#1954) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 72 ++++++++++++++--------------- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 9 ++-- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 2d8bb064b..4e1ad1533 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -131,9 +131,12 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// internal void HandleRemovedNodes() { - foreach (var data in this.entries.Where(d => d.ShouldBeRemoved)) + foreach (var data in this.entries) { - this.RemoveEntry(data); + if (data.ShouldBeRemoved) + { + this.RemoveEntry(data); + } } this.entries.RemoveAll(d => d.ShouldBeRemoved); @@ -210,7 +213,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar // If we have an unmodified DTR but still have entries, we need to // work to reset our state. - if (!this.CheckForDalamudNodes()) + if (!this.CheckForDalamudNodes(dtr)) this.RecreateNodes(); var collisionNode = dtr->GetNodeById(17); @@ -223,40 +226,35 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar foreach (var data in this.entries) { - var isHide = data.UserHidden || !data.Shown; - - if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null }) - { - var node = data.TextNode; - - if (data.Storage == null) - { - data.Storage = Utf8String.CreateEmpty(); - } - - data.Storage->SetString(data.Text.EncodeWithNullTerminator()); - node->SetText(data.Storage->StringPtr); - - ushort w = 0, h = 0; - - if (!isHide) - { - node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); - node->AtkResNode.SetWidth(w); - } - - node->AtkResNode.ToggleVisibility(!isHide); - - data.Dirty = false; - } - if (!data.Added) { data.Added = this.AddNode(data.TextNode); } + var isHide = !data.Shown || data.UserHidden; + var node = data.TextNode; + var nodeHidden = !node->AtkResNode.IsVisible(); + if (!isHide) { + if (nodeHidden) + node->AtkResNode.ToggleVisibility(true); + + if (data is { Added: true, Text: not null, TextNode: not null } && (data.Dirty || nodeHidden)) + { + if (data.Storage == null) + { + data.Storage = Utf8String.CreateEmpty(); + } + + data.Storage->SetString(data.Text.EncodeWithNullTerminator()); + node->SetText(data.Storage->StringPtr); + + ushort w = 0, h = 0; + node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); + node->AtkResNode.SetWidth(w); + } + var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing; if (this.configuration.DtrSwapDirection) @@ -270,17 +268,20 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); } } - else + else if (!nodeHidden) { // If we want the node hidden, shift it up, to prevent collision conflicts - data.TextNode->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX); + node->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX); + node->AtkResNode.ToggleVisibility(false); } + + data.Dirty = false; } } private void HandleAddedNodes() { - if (this.newEntries.Any()) + if (!this.newEntries.IsEmpty) { foreach (var newEntry in this.newEntries) { @@ -354,11 +355,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// Checks if there are any Dalamud nodes in the DTR. /// /// True if there are nodes with an ID > 1000. - private bool CheckForDalamudNodes() + private bool CheckForDalamudNodes(AtkUnitBase* dtr) { - var dtr = this.GetDtr(); - if (dtr == null || dtr->RootNode == null) return false; - for (var i = 0; i < dtr->UldManager.NodeListCount; i++) { if (dtr->UldManager.NodeList[i]->NodeId > 1000) diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index 33e9b26e3..fc5210fda 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -137,14 +137,17 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry get => this.shownBacking; set { - this.shownBacking = value; - this.Dirty = true; + if (value != this.shownBacking) + { + this.shownBacking = value; + this.Dirty = true; + } } } /// [Api10ToDo("Maybe make this config scoped to internalname?")] - public bool UserHidden => this.configuration.DtrIgnore?.Any(x => x == this.Title) ?? false; + public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// /// Gets or sets the internal text node of this entry. From 2335c498c59d80ad7770b65caffa4d2648c7e3f3 Mon Sep 17 00:00:00 2001 From: Tykku Date: Sun, 21 Jul 2024 19:00:48 -0400 Subject: [PATCH 34/79] fix: nin gauge missing kazematoi (#1904) Added Kazematoi to nin gauge from structs Co-authored-by: KazWolfe --- Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs index 4d0b08556..1123fa8b3 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs @@ -18,4 +18,9 @@ public unsafe class NINGauge : JobGaugeBase public byte Ninki => this.Struct->Ninki; + + /// + /// Gets the current charges for Kazematoi + /// + public byte Kazematoi => this.Struct->Kazematoi; } From 80ac97fea874cccffe907b8fd203dab53d7dec53 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 22 Jul 2024 19:24:34 +0900 Subject: [PATCH 35/79] Move ReShadeAddonInterface into sep. ns. --- .../Internal/InterfaceManager.AsReShadeAddon.cs | 6 +++--- Dalamud/Interface/Internal/InterfaceManager.cs | 8 ++++---- .../ReShadeAddonInterface.AddonEvent.cs | 2 +- .../ReShadeAddonInterface.Exports.cs | 2 +- .../{ => ReShadeHandling}/ReShadeAddonInterface.cs | 14 +++++++------- 5 files changed, 16 insertions(+), 16 deletions(-) rename Dalamud/Interface/Internal/{ => ReShadeHandling}/ReShadeAddonInterface.AddonEvent.cs (99%) rename Dalamud/Interface/Internal/{ => ReShadeHandling}/ReShadeAddonInterface.Exports.cs (97%) rename Dalamud/Interface/Internal/{ => ReShadeHandling}/ReShadeAddonInterface.cs (90%) diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index 0f1eeb707..9c08aaf06 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -11,7 +11,7 @@ namespace Dalamud.Interface.Internal; /// internal partial class InterfaceManager { - private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapchain) + private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeHandling.ReShadeAddonInterface.ApiObject swapchain) { var swapChain = swapchain.GetNative(); if (this.scene?.SwapChain.NativePointer != (nint)swapChain) @@ -20,7 +20,7 @@ internal partial class InterfaceManager this.scene?.OnPreResize(); } - private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapchain) + private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeHandling.ReShadeAddonInterface.ApiObject swapchain) { var swapChain = swapchain.GetNative(); if (this.scene?.SwapChain.NativePointer != (nint)swapChain) @@ -33,7 +33,7 @@ internal partial class InterfaceManager this.scene?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height); } - private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime) + private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeHandling.ReShadeAddonInterface.ApiObject runtime) { var swapChain = runtime.GetNative(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 10d508d99..6bbded0f9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -90,7 +90,7 @@ internal partial class InterfaceManager : IInternalDisposableService private Hook? setCursorHook; private Hook? dxgiPresentHook; private Hook? resizeBuffersHook; - private ReShadeAddonInterface? reShadeAddonInterface; + private ReShadeHandling.ReShadeAddonInterface? reShadeAddonInterface; private IFontAtlas? dalamudAtlas; private ILockedImFont? defaultFontResourceLock; @@ -759,7 +759,7 @@ internal partial class InterfaceManager : IInternalDisposableService this.SetCursorDetour); Log.Verbose("===== S W A P C H A I N ====="); - if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) + if (ReShadeHandling.ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) { this.resizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, @@ -768,8 +768,8 @@ internal partial class InterfaceManager : IInternalDisposableService Log.Verbose( "Registered as a ReShade({name}: 0x{addr:X}) addon.", - ReShadeAddonInterface.ReShadeModule!.FileName, - ReShadeAddonInterface.ReShadeModule!.BaseAddress); + ReShadeHandling.ReShadeAddonInterface.ReShadeModule!.FileName, + ReShadeHandling.ReShadeAddonInterface.ReShadeModule!.BaseAddress); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; diff --git a/Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.AddonEvent.cs similarity index 99% rename from Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs rename to Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.AddonEvent.cs index 23f01875d..c68cf4fb6 100644 --- a/Dalamud/Interface/Internal/ReShadeAddonInterface.AddonEvent.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.AddonEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface.Internal.ReShadeHandling; /// ReShade interface. internal sealed partial class ReShadeAddonInterface diff --git a/Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs similarity index 97% rename from Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs rename to Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index 3dce1b79b..46d3cc1af 100644 --- a/Dalamud/Interface/Internal/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -8,7 +8,7 @@ using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface.Internal.ReShadeHandling; /// ReShade interface. [SuppressMessage( diff --git a/Dalamud/Interface/Internal/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs similarity index 90% rename from Dalamud/Interface/Internal/ReShadeAddonInterface.cs rename to Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs index 156688c27..df324941e 100644 --- a/Dalamud/Interface/Internal/ReShadeAddonInterface.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs @@ -10,7 +10,7 @@ using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; -namespace Dalamud.Interface.Internal; +namespace Dalamud.Interface.Internal.ReShadeHandling; /// ReShade interface. internal sealed unsafe partial class ReShadeAddonInterface : IDisposable @@ -53,27 +53,27 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable /// Finalizes an instance of the class. ~ReShadeAddonInterface() => this.ReleaseUnmanagedResources(); - /// Delegate for . + /// Delegate for . /// Reference to the ReShade runtime. public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime); - /// Delegate for . + /// Delegate for . /// Reference to the ReShade SwapChain wrapper. public delegate void ReShadeInitSwapChain(ref ApiObject swapChain); - /// Delegate for . + /// Delegate for . /// Reference to the ReShade SwapChain wrapper. public delegate void ReShadeDestroySwapChain(ref ApiObject swapChain); private delegate BOOL GetModuleHandleExWDelegate(uint dwFlags, ushort* lpModuleName, HMODULE* phModule); - /// Called on . + /// Called on . public event ReShadeOverlayDelegate? ReShadeOverlay; - /// Called on . + /// Called on . public event ReShadeInitSwapChain? InitSwapChain; - /// Called on . + /// Called on . public event ReShadeDestroySwapChain? DestroySwapChain; /// Registers Dalamud as a ReShade addon. From d71fbc52fb72a9afc6774db00bbfd39f7f720dda Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 22 Jul 2024 20:29:26 +0900 Subject: [PATCH 36/79] Add reshade unwrapping options --- .../Internal/DalamudConfiguration.cs | 4 + .../Interface/Internal/InterfaceManager.cs | 34 ++- .../ReShadeHandling/ReShadeHandlingMode.cs | 14 ++ .../ReShadeHandling/ReShadeUnwrapper.cs | 205 ++++++++++++++++++ Dalamud/Interface/Internal/SwapChainHelper.cs | 23 +- .../Settings/Tabs/SettingsTabExperimental.cs | 78 ++++++- .../Settings/Tabs/SettingsTabGeneral.cs | 2 +- .../Settings/Widgets/EnumSettingsEntry{T}.cs | 175 +++++++++++++++ .../Settings/Widgets/SettingsEntry{T}.cs | 53 +---- 9 files changed, 513 insertions(+), 75 deletions(-) create mode 100644 Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs create mode 100644 Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs create mode 100644 Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 6bff5720f..9d54f4562 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Style; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.AutoUpdate; @@ -441,6 +442,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool WindowIsImmersive { get; set; } = false; + /// Gets or sets the mode specifying how to handle ReShade. + public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.ReShadeAddon; + /// /// Gets or sets hitch threshold for game network up in milliseconds. /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6bbded0f9..b7c2f8765 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -17,6 +17,7 @@ using Dalamud.Hooking.Internal; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; +using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; @@ -72,7 +73,7 @@ internal partial class InterfaceManager : IInternalDisposableService private readonly ConcurrentBag deferredDisposeDisposables = new(); [ServiceManager.ServiceDependency] - private readonly WndProcHookManager wndProcHookManager = Service.Get(); + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -82,6 +83,9 @@ internal partial class InterfaceManager : IInternalDisposableService [UsedImplicitly] private readonly HookManager hookManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly WndProcHookManager wndProcHookManager = Service.Get(); + private readonly ConcurrentQueue runBeforeImGuiRender = new(); private readonly ConcurrentQueue runAfterImGuiRender = new(); @@ -90,7 +94,7 @@ internal partial class InterfaceManager : IInternalDisposableService private Hook? setCursorHook; private Hook? dxgiPresentHook; private Hook? resizeBuffersHook; - private ReShadeHandling.ReShadeAddonInterface? reShadeAddonInterface; + private ReShadeAddonInterface? reShadeAddonInterface; private IFontAtlas? dalamudAtlas; private ILockedImFont? defaultFontResourceLock; @@ -759,17 +763,23 @@ internal partial class InterfaceManager : IInternalDisposableService this.SetCursorDetour); Log.Verbose("===== S W A P C H A I N ====="); - if (ReShadeHandling.ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) + if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade) + { + if (SwapChainHelper.UnwrapReShade()) + Log.Verbose("Unwrapped ReShade."); + } + + if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon && + ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) { this.resizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, this.AsReShadeAddonResizeBuffersDetour); - Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}"); Log.Verbose( "Registered as a ReShade({name}: 0x{addr:X}) addon.", - ReShadeHandling.ReShadeAddonInterface.ReShadeModule!.FileName, - ReShadeHandling.ReShadeAddonInterface.ReShadeModule!.BaseAddress); + ReShadeAddonInterface.ReShadeModule!.FileName, + ReShadeAddonInterface.ReShadeModule!.BaseAddress); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; @@ -779,16 +789,18 @@ internal partial class InterfaceManager : IInternalDisposableService this.resizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, this.AsHookResizeBuffersDetour); - Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}"); - var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present; - this.dxgiPresentHook = Hook.FromAddress(addr, this.PresentDetour); - Log.Verbose($"IDXGISwapChain::Present address {Util.DescribeAddress(addr)}"); + this.dxgiPresentHook = Hook.FromAddress( + (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, + this.PresentDetour); } + Log.Verbose($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); + Log.Verbose($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}"); + this.setCursorHook.Enable(); this.dxgiPresentHook?.Enable(); - this.resizeBuffersHook?.Enable(); + this.resizeBuffersHook.Enable(); } private IntPtr SetCursorDetour(IntPtr hCursor) diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs new file mode 100644 index 000000000..e4561ca46 --- /dev/null +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs @@ -0,0 +1,14 @@ +namespace Dalamud.Interface.Internal.ReShadeHandling; + +/// Available handling modes for working with ReShade. +internal enum ReShadeHandlingMode +{ + /// Register as a ReShade addon, and draw on reshade_overlay event. + ReShadeAddon, + + /// Unwraps ReShade from the swap chain obtained from the game. + UnwrapReShade, + + /// Do not do anything special about it. ReShade will process Dalamud rendered stuff. + None = -1, +} diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs new file mode 100644 index 000000000..5e6cd28a6 --- /dev/null +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs @@ -0,0 +1,205 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal.ReShadeHandling; + +/// +/// Peels ReShade off stuff. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +internal static unsafe class ReShadeUnwrapper +{ + /// Unwraps if it is wrapped by ReShade. + /// [inout] The COM pointer to an instance of . + /// A COM type that is or extends . + /// true if peeled. + public static bool Unwrap(ComPtr* comptr) + where T : unmanaged, IUnknown.Interface + { + if (typeof(T).GetNestedType("Vtbl`1") is not { } vtblType) + return false; + + nint vtblSize = vtblType.GetFields().Length * sizeof(nint); + var changed = false; + while (comptr->Get() != null && IsReShadedComObject(comptr->Get())) + { + // Expectation: the pointer to the underlying object should come early after the overriden vtable. + for (nint i = sizeof(nint); i <= 0x20; i += sizeof(nint)) + { + var ppObjectBehind = (nint)comptr->Get() + i; + + // Is the thing directly pointed from the address an actual something in the memory? + if (!IsValidReadableMemoryAddress(ppObjectBehind, 8)) + continue; + + var pObjectBehind = *(nint*)ppObjectBehind; + + // Is the address of vtable readable? + if (!IsValidReadableMemoryAddress(pObjectBehind, sizeof(nint))) + continue; + var pObjectBehindVtbl = *(nint*)pObjectBehind; + + // Is the vtable itself readable? + if (!IsValidReadableMemoryAddress(pObjectBehindVtbl, vtblSize)) + continue; + + // Are individual functions in vtable executable? + var valid = true; + for (var j = 0; valid && j < vtblSize; j += sizeof(nint)) + valid &= IsValidExecutableMemoryAddress(*(nint*)(pObjectBehindVtbl + j), 1); + if (!valid) + continue; + + // Interpret the object as an IUnknown. + // Note that `using` is not used, and `Attach` is used. We do not alter the reference count yet. + var punk = default(ComPtr); + punk.Attach((IUnknown*)pObjectBehind); + + // Is the IUnknown object also the type we want? + using var comptr2 = default(ComPtr); + if (punk.As(&comptr2).FAILED) + continue; + + comptr2.Swap(comptr); + changed = true; + break; + } + + if (!changed) + break; + } + + return changed; + } + + private static bool BelongsInReShadeDll(nint ptr) + { + foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules) + { + if (ptr < processModule.BaseAddress || ptr >= processModule.BaseAddress + processModule.ModuleMemorySize) + continue; + + fixed (byte* pfn0 = "ReShadeRegisterAddon"u8) + fixed (byte* pfn1 = "ReShadeUnregisterAddon"u8) + fixed (byte* pfn2 = "ReShadeRegisterEvent"u8) + fixed (byte* pfn3 = "ReShadeUnregisterEvent"u8) + { + if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn0) == 0) + continue; + if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn1) == 0) + continue; + if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn2) == 0) + continue; + if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn3) == 0) + continue; + } + + return true; + } + + return false; + } + + private static bool IsReShadedComObject(T* obj) + where T : unmanaged, IUnknown.Interface + { + if (!IsValidReadableMemoryAddress((nint)obj, sizeof(nint))) + return false; + + try + { + var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj); + if (!IsValidReadableMemoryAddress((nint)vtbl, sizeof(nint) * 3)) + return false; + + for (var i = 0; i < 3; i++) + { + var pfn = Marshal.ReadIntPtr((nint)(vtbl + i)); + if (!IsValidExecutableMemoryAddress(pfn, 1)) + return false; + if (!BelongsInReShadeDll(pfn)) + return false; + } + + return true; + } + catch + { + return false; + } + } + + private static bool IsValidReadableMemoryAddress(nint p, nint size) + { + while (size > 0) + { + if (!IsValidUserspaceMemoryAddress(p)) + return false; + + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0) + return false; + + if (mbi is not + { + State: MEM.MEM_COMMIT, + Protect: PAGE.PAGE_READONLY or PAGE.PAGE_READWRITE or PAGE.PAGE_EXECUTE_READ + or PAGE.PAGE_EXECUTE_READWRITE, + }) + return false; + + var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL); + var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p; + size -= checkedSize; + p += checkedSize; + } + + return true; + } + + private static bool IsValidExecutableMemoryAddress(nint p, nint size) + { + while (size > 0) + { + if (!IsValidUserspaceMemoryAddress(p)) + return false; + + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0) + return false; + + if (mbi is not + { + State: MEM.MEM_COMMIT, + Protect: PAGE.PAGE_EXECUTE or PAGE.PAGE_EXECUTE_READ or PAGE.PAGE_EXECUTE_READWRITE + or PAGE.PAGE_EXECUTE_WRITECOPY, + }) + return false; + + var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL); + var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p; + size -= checkedSize; + p += checkedSize; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidUserspaceMemoryAddress(nint p) + { + // https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces + // A 64-bit process on 64-bit Windows has a virtual address space within the 128-terabyte range + // 0x000'00000000 through 0x7FFF'FFFFFFFF. + return p >= 0x10000 && p <= unchecked((nint)0x7FFF_FFFFFFFFUL); + } +} diff --git a/Dalamud/Interface/Internal/SwapChainHelper.cs b/Dalamud/Interface/Internal/SwapChainHelper.cs index e0c3f21a8..051e348e0 100644 --- a/Dalamud/Interface/Internal/SwapChainHelper.cs +++ b/Dalamud/Interface/Internal/SwapChainHelper.cs @@ -1,5 +1,7 @@ using System.Threading; +using Dalamud.Interface.Internal.ReShadeHandling; + using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using TerraFX.Interop.DirectX; @@ -10,12 +12,17 @@ namespace Dalamud.Interface.Internal; /// Helper for dealing with swap chains. internal static unsafe class SwapChainHelper { + private static IDXGISwapChain* foundGameDeviceSwapChain; + /// Gets the game's active instance of IDXGISwapChain that is initialized. /// Address of the game's instance of IDXGISwapChain, or null if not available (yet.) public static IDXGISwapChain* GameDeviceSwapChain { get { + if (foundGameDeviceSwapChain is not null) + return foundGameDeviceSwapChain; + var kernelDev = Device.Instance(); if (kernelDev == null) return null; @@ -29,7 +36,7 @@ internal static unsafe class SwapChainHelper if (swapChain->BackBuffer == null) return null; - return (IDXGISwapChain*)swapChain->DXGISwapChain; + return foundGameDeviceSwapChain = (IDXGISwapChain*)swapChain->DXGISwapChain; } } @@ -80,4 +87,18 @@ internal static unsafe class SwapChainHelper while (GameDeviceSwapChain is null) Thread.Yield(); } + + /// + /// Make store address of unwrapped swap chain, if it was wrapped with ReShade. + /// + /// true if it was wrapped with ReShade. + public static bool UnwrapReShade() + { + using var swapChain = new ComPtr(GameDeviceSwapChain); + if (!ReShadeUnwrapper.Unwrap(&swapChain)) + return false; + + foundGameDeviceSwapChain = swapChain.Get(); + return true; + } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 2707f67df..c51f465f9 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -1,8 +1,10 @@ using System.Diagnostics.CodeAnalysis; using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.Utility; @@ -11,28 +13,39 @@ using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; -[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] +[SuppressMessage( + "StyleCop.CSharp.DocumentationRules", + "SA1600:Elements should be documented", + Justification = "Internals")] public class SettingsTabExperimental : SettingsTab { public override SettingsEntry[] Entries { get; } = - { + [ new SettingsEntry( Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"), string.Format( - Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."), + Loc.Localize( + "DalamudSettingsPluginTestHint", + "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."), PluginCategoryManager.Locs.Group_Installed, PluginInstallerWindow.Locs.PluginContext_TestingOptIn), c => c.DoPluginTest, (v, c) => c.DoPluginTest = v), new HintSettingsEntry( - Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), + Loc.Localize( + "DalamudSettingsPluginTestWarning", + "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), ImGuiColors.DalamudRed), - + new GapSettingsEntry(5), - + new SettingsEntry( - Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"), - Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), + Loc.Localize( + "DalamudSettingEnablePluginUIAdditionalOptions", + "Add a button to the title bar of plugin windows to open additional options"), + Loc.Localize( + "DalamudSettingEnablePluginUIAdditionalOptionsHint", + "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), c => c.EnablePluginUiAdditionalOptions, (v, c) => c.EnablePluginUiAdditionalOptions = v), @@ -40,7 +53,9 @@ public class SettingsTabExperimental : SettingsTab new ButtonSettingsEntry( Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"), - Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."), + Loc.Localize( + "DalamudSettingsClearHiddenHint", + "Restore plugins you have previously hidden from the plugin installer."), () => { Service.Get().HiddenPluginInternalName.Clear(); @@ -55,6 +70,45 @@ public class SettingsTabExperimental : SettingsTab new ThirdRepoSettingsEntry(), + new GapSettingsEntry(5, true), + + new EnumSettingsEntry( + Loc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"), + Loc.Localize( + "DalamudSettingsReShadeHandlingModeHint", + "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."), + c => c.ReShadeHandlingMode, + (v, c) => c.ReShadeHandlingMode = v, + fallbackValue: ReShadeHandlingMode.ReShadeAddon) + { + FriendlyEnumNameGetter = x => x switch + { + ReShadeHandlingMode.ReShadeAddon => Loc.Localize( + "DalamudSettingsReShadeHandlingModeReShadeAddon", + "ReShade addon"), + ReShadeHandlingMode.UnwrapReShade => Loc.Localize( + "DalamudSettingsReShadeHandlingModeUnwrapReShade", + "Unwrap ReShade"), + ReShadeHandlingMode.None => Loc.Localize( + "DalamudSettingsReShadeHandlingModeNone", + "Do not handle"), + _ => "", + }, + FriendlyEnumDescriptionGetter = x => x switch + { + ReShadeHandlingMode.ReShadeAddon => Loc.Localize( + "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", + "Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option won't work too well."), + ReShadeHandlingMode.UnwrapReShade => Loc.Localize( + "DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription", + "Dalamud will exclude itself from all ReShade handling. Multi-monitor windows should work fine with this mode, but it may not be supported and crash in future ReShade versions."), + ReShadeHandlingMode.None => Loc.Localize( + "DalamudSettingsReShadeHandlingModeNoneDescription", + "No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."), + _ => "", + }, + }, + /* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles. new GapSettingsEntry(5, true), @@ -64,7 +118,7 @@ public class SettingsTabExperimental : SettingsTab c => c.ProfilesEnabled, (v, c) => c.ProfilesEnabled = v), */ - }; + ]; public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental"); @@ -72,7 +126,9 @@ public class SettingsTabExperimental : SettingsTab { base.Draw(); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false))); + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudGrey, + "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false))); ImGuiHelpers.ScaledDummy(15); } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index c991907ec..5e3648ac6 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -15,7 +15,7 @@ public class SettingsTabGeneral : SettingsTab new GapSettingsEntry(5), - new SettingsEntry( + new EnumSettingsEntry( Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"), Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."), c => c.GeneralChatType, diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs new file mode 100644 index 000000000..f40654542 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/EnumSettingsEntry{T}.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using Dalamud.Configuration.Internal; + +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; + +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] +internal sealed class EnumSettingsEntry : SettingsEntry + where T : struct, Enum +{ + private readonly LoadSettingDelegate load; + private readonly SaveSettingDelegate save; + private readonly Action? change; + + private readonly T fallbackValue; + + private T valueBacking; + + public EnumSettingsEntry( + string name, + string description, + LoadSettingDelegate load, + SaveSettingDelegate save, + Action? change = null, + Func? warning = null, + Func? validity = null, + Func? visibility = null, + T fallbackValue = default) + { + this.load = load; + this.save = save; + this.change = change; + this.Name = name; + this.Description = description; + this.CheckWarning = warning; + this.CheckValidity = validity; + this.CheckVisibility = visibility; + + this.fallbackValue = fallbackValue; + } + + public delegate T LoadSettingDelegate(DalamudConfiguration config); + + public delegate void SaveSettingDelegate(T value, DalamudConfiguration config); + + public T Value + { + get => this.valueBacking; + set + { + if (Equals(value, this.valueBacking)) + return; + this.valueBacking = value; + this.change?.Invoke(value); + } + } + + public string Description { get; } + + public Action>? CustomDraw { get; init; } + + public Func? CheckValidity { get; init; } + + public Func? CheckWarning { get; init; } + + public Func? CheckVisibility { get; init; } + + public Func FriendlyEnumNameGetter { get; init; } = x => x.ToString(); + + public Func FriendlyEnumDescriptionGetter { get; init; } = _ => string.Empty; + + public override bool IsVisible => this.CheckVisibility?.Invoke() ?? true; + + public override void Draw() + { + Debug.Assert(this.Name != null, "this.Name != null"); + + if (this.CustomDraw is not null) + { + this.CustomDraw.Invoke(this); + } + else + { + ImGuiHelpers.SafeTextWrapped(this.Name); + + var idx = this.valueBacking; + var values = Enum.GetValues(); + + if (!values.Contains(idx)) + { + idx = Enum.IsDefined(this.fallbackValue) + ? this.fallbackValue + : throw new InvalidOperationException("No fallback value for enum"); + this.valueBacking = idx; + } + + if (ImGui.BeginCombo($"###{this.Id.ToString()}", this.FriendlyEnumNameGetter(idx))) + { + foreach (var value in values) + { + if (ImGui.Selectable(this.FriendlyEnumNameGetter(value), idx.Equals(value))) + { + this.valueBacking = value; + } + } + + ImGui.EndCombo(); + } + } + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + var desc = this.FriendlyEnumDescriptionGetter(this.valueBacking); + if (!string.IsNullOrWhiteSpace(desc)) + { + ImGuiHelpers.SafeTextWrapped(desc); + ImGuiHelpers.ScaledDummy(2); + } + + ImGuiHelpers.SafeTextWrapped(this.Description); + } + + if (this.CheckValidity != null) + { + var validityMsg = this.CheckValidity.Invoke(this.Value); + this.IsValid = string.IsNullOrEmpty(validityMsg); + + if (!this.IsValid) + { + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + { + ImGui.Text(validityMsg); + } + } + } + else + { + this.IsValid = true; + } + + var warningMessage = this.CheckWarning?.Invoke(this.Value); + + if (warningMessage != null) + { + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + { + ImGui.Text(warningMessage); + } + } + } + + public override void Load() + { + this.valueBacking = this.load(Service.Get()); + + if (this.CheckValidity != null) + { + this.IsValid = this.CheckValidity(this.Value) == null; + } + else + { + this.IsValid = true; + } + } + + public override void Save() => this.save(this.Value, Service.Get()); +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs index 2ac4187cf..cffe0a5da 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs @@ -1,15 +1,13 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; -using System.Numerics; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; -using Dalamud.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; @@ -22,7 +20,6 @@ internal sealed class SettingsEntry : SettingsEntry private readonly Action? change; private object? valueBacking; - private object? fallbackValue; public SettingsEntry( string name, @@ -32,8 +29,7 @@ internal sealed class SettingsEntry : SettingsEntry Action? change = null, Func? warning = null, Func? validity = null, - Func? visibility = null, - object? fallbackValue = null) + Func? visibility = null) { this.load = load; this.save = save; @@ -43,8 +39,6 @@ internal sealed class SettingsEntry : SettingsEntry this.CheckWarning = warning; this.CheckValidity = validity; this.CheckVisibility = visibility; - - this.fallbackValue = fallbackValue; } public delegate T? LoadSettingDelegate(DalamudConfiguration config); @@ -118,34 +112,6 @@ internal sealed class SettingsEntry : SettingsEntry this.change?.Invoke(this.Value); } } - else if (type.IsEnum) - { - ImGuiHelpers.SafeTextWrapped(this.Name); - - var idx = (Enum)(this.valueBacking ?? 0); - var values = Enum.GetValues(type); - var descriptions = - values.Cast().ToDictionary(x => x, x => x.GetAttribute() ?? new SettingsAnnotationAttribute(x.ToString(), string.Empty)); - - if (!descriptions.ContainsKey(idx)) - { - idx = (Enum)this.fallbackValue ?? throw new Exception("No fallback value for enum"); - this.valueBacking = idx; - } - - if (ImGui.BeginCombo($"###{this.Id.ToString()}", descriptions[idx].FriendlyName)) - { - foreach (Enum value in values) - { - if (ImGui.Selectable(descriptions[value].FriendlyName, idx.Equals(value))) - { - this.valueBacking = value; - } - } - - ImGui.EndCombo(); - } - } using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) { @@ -197,18 +163,3 @@ internal sealed class SettingsEntry : SettingsEntry public override void Save() => this.save(this.Value, Service.Get()); } - -[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] -[AttributeUsage(AttributeTargets.Field)] -internal class SettingsAnnotationAttribute : Attribute -{ - public SettingsAnnotationAttribute(string friendlyName, string description) - { - this.FriendlyName = friendlyName; - this.Description = description; - } - - public string FriendlyName { get; set; } - - public string Description { get; set; } -} From 6c8ec0ab4d2fb14d30d60161ece8443d74ca3a66 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 22 Jul 2024 20:34:43 +0900 Subject: [PATCH 37/79] Fix random typo --- Dalamud/Utility/Util.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index b9c81acfd..7d4de1a02 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -269,7 +269,7 @@ public static class Util { if ((mbi.Protect & (1 << i)) == 0) continue; - if (c++ == 0) + if (c++ != 0) sb.Append(" | "); sb.Append(PageProtectionFlagNames[i]); } From e1a7caa2cffb0e2e0e68440081045aaffc8ad411 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 22 Jul 2024 22:43:02 +0900 Subject: [PATCH 38/79] fix cpm rc --- Dalamud/Support/CurrentProcessModules.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Support/CurrentProcessModules.cs b/Dalamud/Support/CurrentProcessModules.cs index cd73ceb04..b89d2eb63 100644 --- a/Dalamud/Support/CurrentProcessModules.cs +++ b/Dalamud/Support/CurrentProcessModules.cs @@ -8,7 +8,7 @@ namespace Dalamud.Support; /// Tracks the loaded process modules. internal static unsafe partial class CurrentProcessModules { - private static Process? process; + private static ProcessModuleCollection? moduleCollection; /// Gets all the loaded modules, up to date. public static ProcessModuleCollection ModuleCollection @@ -19,13 +19,13 @@ internal static unsafe partial class CurrentProcessModules if (t != 0) { t = 0; - process = null; + moduleCollection = null; Log.Verbose("{what}: Fetching fresh copy of current process modules.", nameof(CurrentProcessModules)); } try { - return (process ??= Process.GetCurrentProcess()).Modules; + return moduleCollection ??= Process.GetCurrentProcess().Modules; } catch (Exception e) { From 849f84e859f7be45c3dadab3adb9dae8763b89b4 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 22 Jul 2024 20:50:05 +0200 Subject: [PATCH 39/79] ExposedPlugin: correctly indicate whether a plugin has a config UI --- Dalamud/Plugin/InstalledPluginState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/InstalledPluginState.cs b/Dalamud/Plugin/InstalledPluginState.cs index f92cc0c2c..6700a1f8d 100644 --- a/Dalamud/Plugin/InstalledPluginState.cs +++ b/Dalamud/Plugin/InstalledPluginState.cs @@ -72,7 +72,7 @@ internal sealed class ExposedPlugin(LocalPlugin plugin) : IExposedPlugin public bool HasMainUi => plugin.DalamudInterface?.LocalUiBuilder.HasMainUi ?? false; /// - public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasMainUi ?? false; + public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasConfigUi ?? false; /// public void OpenMainUi() From 18b6596aaa9ad74cf839c1610f88858d96a4d12c Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 23 Jul 2024 00:36:04 +0200 Subject: [PATCH 40/79] build: 10.0.0.6 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 97be8b600..dfaab870c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -9,7 +9,7 @@ - 10.0.0.5 + 10.0.0.6 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 6fd19638e958e852bedb50a72517635819cd71c9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jul 2024 10:14:34 +0900 Subject: [PATCH 41/79] Reviewed changed --- .../Internal/ReShadeHandling/ReShadeAddonInterface.cs | 2 ++ .../Internal/ReShadeHandling/ReShadeUnwrapper.cs | 9 +-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs index df324941e..de7629276 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs @@ -31,6 +31,8 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable if (!Exports.ReShadeRegisterAddon(this.hDalamudModule, ReShadeApiVersion)) throw new InvalidOperationException("ReShadeRegisterAddon failure."); + // https://github.com/crosire/reshade/commit/eaaa2a2c5adf5749ad17b358305da3f2d0f6baf4 + // TODO: when ReShade gets a proper release with this commit, make this hook optional this.addonModuleResolverHook = Hook.FromImport( ReShadeModule!, "kernel32.dll", diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs index 5e6cd28a6..3682b03c0 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,13 +8,7 @@ using static TerraFX.Interop.Windows.Windows; namespace Dalamud.Interface.Internal.ReShadeHandling; -/// -/// Peels ReShade off stuff. -/// -[SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] +/// Unwraps IUnknown wrapped by ReShade. internal static unsafe class ReShadeUnwrapper { /// Unwraps if it is wrapped by ReShade. From 3215b6dddf5da60fa9a7b634f8511a424d01f939 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jul 2024 10:57:09 +0900 Subject: [PATCH 42/79] Add optional vtable swapchain hook mode --- .../Internal/DalamudConfiguration.cs | 4 + Dalamud/Hooking/Internal/ObjectVTableHook.cs | 286 ++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 62 +++- .../ReShadeHandling/ReShadeUnwrapper.cs | 28 +- Dalamud/Interface/Internal/SwapChainHelper.cs | 10 + .../Settings/Tabs/SettingsTabExperimental.cs | 13 +- 6 files changed, 374 insertions(+), 29 deletions(-) create mode 100644 Dalamud/Hooking/Internal/ObjectVTableHook.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 9d54f4562..d5f1299fd 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Style; using Dalamud.IoC.Internal; @@ -445,6 +446,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// Gets or sets the mode specifying how to handle ReShade. public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.ReShadeAddon; + /// Gets or sets the swap chain hook mode. + public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode; + /// /// Gets or sets hitch threshold for game network up in milliseconds. /// diff --git a/Dalamud/Hooking/Internal/ObjectVTableHook.cs b/Dalamud/Hooking/Internal/ObjectVTableHook.cs new file mode 100644 index 000000000..b4500bb5f --- /dev/null +++ b/Dalamud/Hooking/Internal/ObjectVTableHook.cs @@ -0,0 +1,286 @@ +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Utility; + +using Serilog; + +namespace Dalamud.Hooking.Internal; + +/// Manages a hook that works by replacing the vtable of target object. +internal unsafe class ObjectVTableHook : IDisposable +{ + private readonly nint** ppVtbl; + private readonly int numMethods; + + private readonly nint* pVtblOriginal; + private readonly nint[] vtblOverriden; + + /// Extra data for overriden vtable entries, primarily for keeping references to delegates that are used + /// with . + private readonly object?[] vtblOverridenTag; + + private bool released; + + /// Initializes a new instance of the class. + /// Address to vtable. Usually the address of the object itself. + /// Number of methods in this vtable. + public ObjectVTableHook(nint ppVtbl, int numMethods) + { + this.ppVtbl = (nint**)ppVtbl; + this.numMethods = numMethods; + this.vtblOverridenTag = new object?[numMethods]; + this.pVtblOriginal = *this.ppVtbl; + this.vtblOverriden = GC.AllocateArray(numMethods, true); + this.OriginalVTableSpan.CopyTo(this.vtblOverriden); + } + + /// Initializes a new instance of the class. + /// Address to vtable. Usually the address of the object itself. + /// Number of methods in this vtable. + public ObjectVTableHook(void* ppVtbl, int numMethods) + : this((nint)ppVtbl, numMethods) + { + } + + /// Finalizes an instance of the class. + ~ObjectVTableHook() => this.ReleaseUnmanagedResources(); + + /// Gets the span view of original vtable. + public ReadOnlySpan OriginalVTableSpan => new(this.pVtblOriginal, this.numMethods); + + /// Gets the span view of overriden vtable. + public ReadOnlySpan OverridenVTableSpan => this.vtblOverriden.AsSpan(); + + /// Disables the hook. + public void Disable() + { + // already disabled + if (*this.ppVtbl == this.pVtblOriginal) + return; + + if (*this.ppVtbl != Unsafe.AsPointer(ref this.vtblOverriden[0])) + { + Log.Warning( + "[{who}]: the object was hooked by something else; disabling may result in a crash.", + this.GetType().Name); + } + + *this.ppVtbl = this.pVtblOriginal; + } + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// Enables the hook. + public void Enable() + { + // already enabled + if (*this.ppVtbl == Unsafe.AsPointer(ref this.vtblOverriden[0])) + return; + + if (*this.ppVtbl != this.pVtblOriginal) + { + Log.Warning( + "[{who}]: the object was hooked by something else; enabling may result in a crash.", + this.GetType().Name); + } + + *this.ppVtbl = (nint*)Unsafe.AsPointer(ref this.vtblOverriden[0]); + } + + /// Gets the original method address of the given method index. + /// Index of the method. + /// Address of the original method. + public nint GetOriginalMethodAddress(int methodIndex) + { + this.EnsureMethodIndex(methodIndex); + return this.pVtblOriginal[methodIndex]; + } + + /// Gets the original method of the given method index, as a delegate of given type. + /// Index of the method. + /// Type of delegate. + /// Delegate to the original method. + public T GetOriginalMethodDelegate(int methodIndex) + where T : Delegate + { + this.EnsureMethodIndex(methodIndex); + return Marshal.GetDelegateForFunctionPointer(this.pVtblOriginal[methodIndex]); + } + + /// Resets a method to the original function. + /// Index of the method. + public void ResetVtableEntry(int methodIndex) + { + this.EnsureMethodIndex(methodIndex); + this.vtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex]; + this.vtblOverridenTag[methodIndex] = null; + } + + /// Sets a method in vtable to the given address of function. + /// Index of the method. + /// Address of the detour function. + /// Additional reference to keep in memory. + public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep) + { + this.EnsureMethodIndex(methodIndex); + this.vtblOverriden[methodIndex] = pfn; + this.vtblOverridenTag[methodIndex] = refkeep; + } + + /// Sets a method in vtable to the given delegate. + /// Index of the method. + /// Detour delegate. + /// Type of delegate. + public void SetVtableEntry(int methodIndex, T detourDelegate) + where T : Delegate => + this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate); + + /// Sets a method in vtable to the given delegate. + /// Index of the method. + /// Detour delegate. + /// Original method delegate. + /// Type of delegate. + public void SetVtableEntry(int methodIndex, T detourDelegate, out T originalMethodDelegate) + where T : Delegate + { + originalMethodDelegate = this.GetOriginalMethodDelegate(methodIndex); + this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate); + } + + /// Creates a new instance of that manages one entry in the vtable hook. + /// Index of the method. + /// Detour delegate. + /// Type of delegate. + /// A new instance of . + /// Even if a single hook is enabled, without , the hook will remain disabled. + /// + public Hook CreateHook(int methodIndex, T detourDelegate) where T : Delegate => + new SingleHook(this, methodIndex, detourDelegate); + + private void EnsureMethodIndex(int methodIndex) + { + ArgumentOutOfRangeException.ThrowIfNegative(methodIndex); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(methodIndex, this.numMethods); + } + + private void ReleaseUnmanagedResources() + { + if (!this.released) + { + this.Disable(); + this.released = true; + } + } + + private sealed class SingleHook(ObjectVTableHook hook, int methodIndex, T detourDelegate) + : Hook((nint)hook.ppVtbl) + where T : Delegate + { + /// + public override T Original { get; } = hook.GetOriginalMethodDelegate(methodIndex); + + /// + public override bool IsEnabled => + hook.OriginalVTableSpan[methodIndex] != hook.OverridenVTableSpan[methodIndex]; + + /// + public override string BackendName => nameof(ObjectVTableHook); + + /// + public override void Enable() => hook.SetVtableEntry(methodIndex, detourDelegate); + + /// + public override void Disable() => hook.ResetVtableEntry(methodIndex); + } +} + +/// Typed version of . +/// VTable struct. +internal unsafe class ObjectVTableHook : ObjectVTableHook + where TVTable : unmanaged +{ + private static readonly string[] Fields = + typeof(TVTable).GetFields(BindingFlags.Instance | BindingFlags.Public).Select(x => x.Name).ToArray(); + + /// Initializes a new instance of the class. + /// Address to vtable. Usually the address of the object itself. + public ObjectVTableHook(void* ppVtbl) + : base(ppVtbl, Fields.Length) + { + } + + /// Gets the original vtable. + public ref readonly TVTable OriginalVTable => ref MemoryMarshal.Cast(this.OriginalVTableSpan)[0]; + + /// Gets the overriden vtable. + public ref readonly TVTable OverridenVTable => ref MemoryMarshal.Cast(this.OverridenVTableSpan)[0]; + + /// Gets the index of the method by method name. + /// Name of the method. + /// Index of the method. + public int GetMethodIndex(string methodName) => Fields.IndexOf(methodName); + + /// Gets the original method address of the given method index. + /// Name of the method. + /// Address of the original method. + public nint GetOriginalMethodAddress(string methodName) => + this.GetOriginalMethodAddress(this.GetMethodIndex(methodName)); + + /// Gets the original method of the given method index, as a delegate of given type. + /// Name of the method. + /// Type of delegate. + /// Delegate to the original method. + public T GetOriginalMethodDelegate(string methodName) + where T : Delegate + => this.GetOriginalMethodDelegate(this.GetMethodIndex(methodName)); + + /// Resets a method to the original function. + /// Name of the method. + public void ResetVtableEntry(string methodName) + => this.ResetVtableEntry(this.GetMethodIndex(methodName)); + + /// Sets a method in vtable to the given address of function. + /// Name of the method. + /// Address of the detour function. + /// Additional reference to keep in memory. + public void SetVtableEntry(string methodName, nint pfn, object? refkeep) + => this.SetVtableEntry(this.GetMethodIndex(methodName), pfn, refkeep); + + /// Sets a method in vtable to the given delegate. + /// Name of the method. + /// Detour delegate. + /// Type of delegate. + public void SetVtableEntry(string methodName, T detourDelegate) + where T : Delegate => + this.SetVtableEntry( + this.GetMethodIndex(methodName), + Marshal.GetFunctionPointerForDelegate(detourDelegate), + detourDelegate); + + /// Sets a method in vtable to the given delegate. + /// Name of the method. + /// Detour delegate. + /// Original method delegate. + /// Type of delegate. + public void SetVtableEntry(string methodName, T detourDelegate, out T originalMethodDelegate) + where T : Delegate + => this.SetVtableEntry(this.GetMethodIndex(methodName), detourDelegate, out originalMethodDelegate); + + /// Creates a new instance of that manages one entry in the vtable hook. + /// Name of the method. + /// Detour delegate. + /// Type of delegate. + /// A new instance of . + /// Even if a single hook is enabled, without , the hook will remain + /// disabled. + public Hook CreateHook(string methodName, T detourDelegate) where T : Delegate => + this.CreateHook(this.GetMethodIndex(methodName), detourDelegate); +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index b7c2f8765..4941ea46c 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -35,6 +35,7 @@ using JetBrains.Annotations; using PInvoke; +using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; // general dev notes, here because it's easiest @@ -94,6 +95,7 @@ internal partial class InterfaceManager : IInternalDisposableService private Hook? setCursorHook; private Hook? dxgiPresentHook; private Hook? resizeBuffersHook; + private ObjectVTableHook>? swapChainHook; private ReShadeAddonInterface? reShadeAddonInterface; private IFontAtlas? dalamudAtlas; @@ -308,6 +310,7 @@ internal partial class InterfaceManager : IInternalDisposableService Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); Interlocked.Exchange(ref this.dxgiPresentHook, null)?.Dispose(); Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); + Interlocked.Exchange(ref this.swapChainHook, null)?.Dispose(); Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose(); } } @@ -769,12 +772,13 @@ internal partial class InterfaceManager : IInternalDisposableService Log.Verbose("Unwrapped ReShade."); } + ResizeBuffersDelegate resizeBuffersDelegate; + DxgiPresentDelegate? dxgiPresentDelegate; if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon && ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) { - this.resizeBuffersHook = Hook.FromAddress( - (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, - this.AsReShadeAddonResizeBuffersDetour); + resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour; + dxgiPresentDelegate = null; Log.Verbose( "Registered as a ReShade({name}: 0x{addr:X}) addon.", @@ -786,21 +790,55 @@ internal partial class InterfaceManager : IInternalDisposableService } else { - this.resizeBuffersHook = Hook.FromAddress( - (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, - this.AsHookResizeBuffersDetour); - - this.dxgiPresentHook = Hook.FromAddress( - (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, - this.PresentDetour); + resizeBuffersDelegate = this.AsHookResizeBuffersDetour; + dxgiPresentDelegate = this.PresentDetour; } - Log.Verbose($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); + switch (this.dalamudConfiguration.SwapChainHookMode) + { + case SwapChainHelper.HookMode.ByteCode: + default: + { + this.resizeBuffersHook = Hook.FromAddress( + (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, + resizeBuffersDelegate); + + if (dxgiPresentDelegate is not null) + { + this.dxgiPresentHook = Hook.FromAddress( + (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, + dxgiPresentDelegate); + } + + break; + } + + case SwapChainHelper.HookMode.VTable: + { + this.swapChainHook = new(SwapChainHelper.GameDeviceSwapChain); + this.resizeBuffersHook = this.swapChainHook.CreateHook( + nameof(IDXGISwapChain.ResizeBuffers), + resizeBuffersDelegate); + + if (dxgiPresentDelegate is not null) + { + this.dxgiPresentHook = this.swapChainHook.CreateHook( + nameof(IDXGISwapChain.Present), + dxgiPresentDelegate); + } + + break; + } + } + + Log.Verbose( + $"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); Log.Verbose($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}"); this.setCursorHook.Enable(); - this.dxgiPresentHook?.Enable(); this.resizeBuffersHook.Enable(); + this.dxgiPresentHook?.Enable(); + this.swapChainHook?.Enable(); } private IntPtr SetCursorDetour(IntPtr hCursor) diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs index 3682b03c0..f1210425d 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs @@ -78,28 +78,24 @@ internal static unsafe class ReShadeUnwrapper { foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules) { - if (ptr < processModule.BaseAddress || ptr >= processModule.BaseAddress + processModule.ModuleMemorySize) + if (ptr < processModule.BaseAddress || + ptr >= processModule.BaseAddress + processModule.ModuleMemorySize || + !HasProcExported(processModule, "ReShadeRegisterAddon"u8) || + !HasProcExported(processModule, "ReShadeUnregisterAddon"u8) || + !HasProcExported(processModule, "ReShadeRegisterEvent"u8) || + !HasProcExported(processModule, "ReShadeUnregisterEvent"u8)) continue; - fixed (byte* pfn0 = "ReShadeRegisterAddon"u8) - fixed (byte* pfn1 = "ReShadeUnregisterAddon"u8) - fixed (byte* pfn2 = "ReShadeRegisterEvent"u8) - fixed (byte* pfn3 = "ReShadeUnregisterEvent"u8) - { - if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn0) == 0) - continue; - if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn1) == 0) - continue; - if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn2) == 0) - continue; - if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn3) == 0) - continue; - } - return true; } return false; + + static bool HasProcExported(ProcessModule m, ReadOnlySpan name) + { + fixed (byte* p = name) + return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0; + } } private static bool IsReShadedComObject(T* obj) diff --git a/Dalamud/Interface/Internal/SwapChainHelper.cs b/Dalamud/Interface/Internal/SwapChainHelper.cs index 051e348e0..4a336ee9f 100644 --- a/Dalamud/Interface/Internal/SwapChainHelper.cs +++ b/Dalamud/Interface/Internal/SwapChainHelper.cs @@ -14,6 +14,16 @@ internal static unsafe class SwapChainHelper { private static IDXGISwapChain* foundGameDeviceSwapChain; + /// Describes how to hook methods. + public enum HookMode + { + /// Hooks by rewriting the native bytecode. + ByteCode, + + /// Hooks by providing an alternative vtable. + VTable, + } + /// Gets the game's active instance of IDXGISwapChain that is initialized. /// Address of the game's instance of IDXGISwapChain, or null if not available (yet.) public static IDXGISwapChain* GameDeviceSwapChain diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index c51f465f9..faefe418c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -98,7 +98,7 @@ public class SettingsTabExperimental : SettingsTab { ReShadeHandlingMode.ReShadeAddon => Loc.Localize( "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", - "Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option won't work too well."), + "Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), ReShadeHandlingMode.UnwrapReShade => Loc.Localize( "DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription", "Dalamud will exclude itself from all ReShade handling. Multi-monitor windows should work fine with this mode, but it may not be supported and crash in future ReShade versions."), @@ -109,6 +109,17 @@ public class SettingsTabExperimental : SettingsTab }, }, + new GapSettingsEntry(5, true), + + new EnumSettingsEntry( + Loc.Localize("DalamudSettingsSwapChainHookMode", "Swap chain hooking mode"), + Loc.Localize( + "DalamudSettingsSwapChainHookModeHint", + "Depending on addons aside from Dalamud you use, you may have to use different options for Dalamud and other addons to cooperate.\nRestart is required for changes to take effect."), + c => c.SwapChainHookMode, + (v, c) => c.SwapChainHookMode = v, + fallbackValue: SwapChainHelper.HookMode.ByteCode), + /* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles. new GapSettingsEntry(5, true), From 4ddd9c99fba7255174e765cd05aabedc13d24631 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 23 Jul 2024 06:22:05 +0200 Subject: [PATCH 43/79] Update ClientStructs (#1956) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 47dc5beef..a969dec3a 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 47dc5beefe9b5fb44bfb35edc768d866d322cb09 +Subproject commit a969dec3a5a05fd6d5070452c74720cf38af6567 From 97069dff270c6dd59911e955543db9852ccbf885 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jul 2024 17:48:11 +0900 Subject: [PATCH 44/79] Detect and warn reshade w/o addon support --- .../Interface/Internal/InterfaceManager.cs | 58 ++++++++++++++----- .../ReShadeAddonInterface.Exports.cs | 31 ++++++++++ 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 4941ea46c..cfbc23bc9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -8,6 +8,8 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using CheapLoc; + using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.GamePad; @@ -15,6 +17,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Hooking; using Dalamud.Hooking.Internal; using Dalamud.Hooking.WndProcHook; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.ReShadeHandling; @@ -746,6 +749,9 @@ internal partial class InterfaceManager : IInternalDisposableService _ = this.dalamudAtlas.BuildFontsAsync(); SwapChainHelper.BusyWaitForGameDeviceSwapChain(); + var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC); + if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED) + this.gameWindowHandle = swapChainDesc.OutputWindow; try { @@ -765,6 +771,28 @@ internal partial class InterfaceManager : IInternalDisposableService 0, this.SetCursorDetour); + if (ReShadeAddonInterface.ReShadeHasSignature) + { + Log.Warning("Signed ReShade binary detected."); + Service + .GetAsync() + .ContinueWith( + nmt => nmt.Result.AddNotification( + new() + { + MinimizedText = Loc.Localize( + "ReShadeNoAddonSupportNotificationMinimizedText", + "Wrong ReShade installation"), + Content = Loc.Localize( + "ReShadeNoAddonSupportNotificationContent", + "Your installation of ReShade does not have full addon support, and may not work with Dalamud and/or the game.\n" + + "Download and install ReShade with full addon-support."), + Type = NotificationType.Warning, + InitialDuration = TimeSpan.MaxValue, + ShowIndeterminateIfNoExpiry = false, + })); + } + Log.Verbose("===== S W A P C H A I N ====="); if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade) { @@ -772,23 +800,25 @@ internal partial class InterfaceManager : IInternalDisposableService Log.Verbose("Unwrapped ReShade."); } - ResizeBuffersDelegate resizeBuffersDelegate; - DxgiPresentDelegate? dxgiPresentDelegate; - if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon && - ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) + ResizeBuffersDelegate? resizeBuffersDelegate = null; + DxgiPresentDelegate? dxgiPresentDelegate = null; + if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon) { - resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour; - dxgiPresentDelegate = null; + if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) + { + resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour; - Log.Verbose( - "Registered as a ReShade({name}: 0x{addr:X}) addon.", - ReShadeAddonInterface.ReShadeModule!.FileName, - ReShadeAddonInterface.ReShadeModule!.BaseAddress); - this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; - this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; - this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; + Log.Verbose( + "Registered as a ReShade({name}: 0x{addr:X}) addon.", + ReShadeAddonInterface.ReShadeModule!.FileName, + ReShadeAddonInterface.ReShadeModule!.BaseAddress); + this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; + this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; + this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; + } } - else + + if (resizeBuffersDelegate is null) { resizeBuffersDelegate = this.AsHookResizeBuffersDetour; dxgiPresentDelegate = this.PresentDetour; diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index 46d3cc1af..60bbc37cd 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -30,8 +30,35 @@ internal sealed unsafe partial class ReShadeAddonInterface !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent)) continue; + fixed (void* pwszFile = m.FileName) + fixed (Guid* pguid = &WINTRUST_ACTION_GENERIC_VERIFY_V2) + { + var wtfi = new WINTRUST_FILE_INFO + { + cbStruct = (uint)sizeof(WINTRUST_FILE_INFO), + pcwszFilePath = (ushort*)pwszFile, + hFile = default, + pgKnownSubject = null, + }; + var wtd = new WINTRUST_DATA + { + cbStruct = (uint)sizeof(WINTRUST_DATA), + pPolicyCallbackData = null, + pSIPClientData = null, + dwUIChoice = WTD.WTD_UI_NONE, + fdwRevocationChecks = WTD.WTD_REVOKE_NONE, + dwUnionChoice = WTD.WTD_STATEACTION_VERIFY, + hWVTStateData = default, + pwszURLReference = null, + dwUIContext = 0, + pFile = &wtfi, + }; + ReShadeHasSignature = WinVerifyTrust(default, pguid, &wtd) != TRUST.TRUST_E_NOSIGNATURE; + } + ReShadeModule = m; Exports = e; + return; } @@ -49,6 +76,10 @@ internal sealed unsafe partial class ReShadeAddonInterface /// Gets the active ReShade module. public static ProcessModule? ReShadeModule { get; private set; } + /// Gets a value indicating whether the loaded ReShade has signatures. + /// ReShade without addon support is signed, but may not pass signature verification. + public static bool ReShadeHasSignature { get; private set; } + private struct ExportsStruct { public delegate* unmanaged ReShadeRegisterAddon; From 55056aee3fc63039e66cc0c14cede45e2ecbbf86 Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 23 Jul 2024 10:11:14 -0700 Subject: [PATCH 45/79] [Plugin Installer] typo fix (#1962) --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 92afe56a6..4a936a795 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3942,7 +3942,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment."); - public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them some the settings."); + public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings."); #endregion From 7e709d98cfe4e108eed62c3e1c7c348a9d264fac Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 23 Jul 2024 19:37:19 +0200 Subject: [PATCH 46/79] interface: make swapchain hooking mode into a console command instead --- .../Interface/Internal/DalamudInterface.cs | 22 +++++++++++++++++++ .../Settings/Tabs/SettingsTabExperimental.cs | 2 ++ 2 files changed, 24 insertions(+) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 71ba2b071..d2ffe488a 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -190,6 +190,28 @@ internal class DalamudInterface : IInternalDisposableService this.creditsDarkeningAnimation.Point1 = Vector2.Zero; this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha); + + // This is temporary, until we know the repercussions of vtable hooking mode + consoleManager.AddCommand( + "dalamud.interface.swapchain_mode", + "Set swapchain hooking mode", + (string mode) => + { + switch (mode) + { + case "vtable": + this.configuration.SwapChainHookMode = SwapChainHelper.HookMode.VTable; + break; + case "bytecode": + this.configuration.SwapChainHookMode = SwapChainHelper.HookMode.ByteCode; + break; + default: + Log.Error("Unknown swapchain mode: {Mode}", mode); + break; + } + + this.configuration.QueueSave(); + }); } private delegate nint CrashDebugDelegate(nint self); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index faefe418c..3f6ec783e 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -109,6 +109,7 @@ public class SettingsTabExperimental : SettingsTab }, }, + /* // Making this a console command instead, for now new GapSettingsEntry(5, true), new EnumSettingsEntry( @@ -119,6 +120,7 @@ public class SettingsTabExperimental : SettingsTab c => c.SwapChainHookMode, (v, c) => c.SwapChainHookMode = v, fallbackValue: SwapChainHelper.HookMode.ByteCode), + */ /* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles. new GapSettingsEntry(5, true), From 69f490619f4f505d8b02be147c90fc56619a6764 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 23 Jul 2024 19:43:01 +0200 Subject: [PATCH 47/79] interface: add learn more button to ReShade addon notification --- Dalamud/Interface/Internal/InterfaceManager.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index cfbc23bc9..79c76b80d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -19,6 +19,7 @@ using Dalamud.Hooking.Internal; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.DesignSystem; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.ManagedFontAtlas; @@ -790,7 +791,18 @@ internal partial class InterfaceManager : IInternalDisposableService Type = NotificationType.Warning, InitialDuration = TimeSpan.MaxValue, ShowIndeterminateIfNoExpiry = false, - })); + })).ContinueWith( + t => + { + t.Result.DrawActions += _ => + { + ImGuiHelpers.ScaledDummy(2); + if (DalamudComponents.PrimaryButton(Loc.Localize("LearnMore", "Learn more..."))) + { + Util.OpenLink("https://dalamud.dev/news/2024/07/23/reshade/"); + } + }; + }); } Log.Verbose("===== S W A P C H A I N ====="); From f0ef73f96b37de7ec02136d43b648fa80221cf8f Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 23 Jul 2024 19:45:03 +0200 Subject: [PATCH 48/79] fix bad console command usage, whoops --- Dalamud/Interface/Internal/DalamudInterface.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index d2ffe488a..c88246f51 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -207,10 +207,11 @@ internal class DalamudInterface : IInternalDisposableService break; default: Log.Error("Unknown swapchain mode: {Mode}", mode); - break; + return false; } this.configuration.QueueSave(); + return true; }); } From 4d0ab8e254bda99c408cdb62b3d9df6789af8c31 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 23 Jul 2024 19:46:02 +0200 Subject: [PATCH 49/79] fix warnings --- Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs | 2 +- Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs | 1 + Dalamud/Interface/Internal/UiDebug.cs | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs index 1123fa8b3..aee8019e4 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/NINGauge.cs @@ -20,7 +20,7 @@ public unsafe class NINGauge : JobGaugeBase this.Struct->Ninki; /// - /// Gets the current charges for Kazematoi + /// Gets the current charges for Kazematoi. /// public byte Kazematoi => this.Struct->Kazematoi; } diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index e05e553cd..a398bdb82 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -6,6 +6,7 @@ namespace Dalamud.Game.Gui.NamePlate; /// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title). /// /// The field type which should be set. +/// Whether or not this is a Free Company part. /// /// 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 diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index f18b132dc..9d8b3b764 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -311,8 +311,6 @@ internal unsafe class UiDebug } } - - private void PrintComponentNode(AtkResNode* node, string treePrefix) { var compNode = (AtkComponentNode*)node; From ec269f483e3e4ab0976d307873b0d694fe2b7664 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 23 Jul 2024 20:36:30 +0200 Subject: [PATCH 50/79] interface: bump log levels during init for now --- .../Interface/Internal/InterfaceManager.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 79c76b80d..b7267ea94 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -774,7 +774,7 @@ internal partial class InterfaceManager : IInternalDisposableService if (ReShadeAddonInterface.ReShadeHasSignature) { - Log.Warning("Signed ReShade binary detected."); + Log.Warning("Signed ReShade binary detected"); Service .GetAsync() .ContinueWith( @@ -809,7 +809,7 @@ internal partial class InterfaceManager : IInternalDisposableService if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade) { if (SwapChainHelper.UnwrapReShade()) - Log.Verbose("Unwrapped ReShade."); + Log.Information("Unwrapped ReShade"); } ResizeBuffersDelegate? resizeBuffersDelegate = null; @@ -820,14 +820,18 @@ internal partial class InterfaceManager : IInternalDisposableService { resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour; - Log.Verbose( - "Registered as a ReShade({name}: 0x{addr:X}) addon.", + Log.Information( + "Registered as a ReShade({Name}: 0x{Addr:X}) addon", ReShadeAddonInterface.ReShadeModule!.FileName, ReShadeAddonInterface.ReShadeModule!.BaseAddress); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; } + else + { + Log.Information("Could not register as ReShade addon"); + } } if (resizeBuffersDelegate is null) @@ -841,6 +845,7 @@ internal partial class InterfaceManager : IInternalDisposableService case SwapChainHelper.HookMode.ByteCode: default: { + Log.Information("Hooking using bytecode..."); this.resizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, resizeBuffersDelegate); @@ -850,6 +855,7 @@ internal partial class InterfaceManager : IInternalDisposableService this.dxgiPresentHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, dxgiPresentDelegate); + Log.Information("Hooked present using bytecode"); } break; @@ -857,6 +863,7 @@ internal partial class InterfaceManager : IInternalDisposableService case SwapChainHelper.HookMode.VTable: { + Log.Information("Hooking using VTable..."); this.swapChainHook = new(SwapChainHelper.GameDeviceSwapChain); this.resizeBuffersHook = this.swapChainHook.CreateHook( nameof(IDXGISwapChain.ResizeBuffers), @@ -867,15 +874,15 @@ internal partial class InterfaceManager : IInternalDisposableService this.dxgiPresentHook = this.swapChainHook.CreateHook( nameof(IDXGISwapChain.Present), dxgiPresentDelegate); + Log.Information("Hooked present using VTable"); } break; } } - Log.Verbose( - $"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); - Log.Verbose($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}"); + Log.Information($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); + Log.Information($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}"); this.setCursorHook.Enable(); this.resizeBuffersHook.Enable(); From 1380a5593522f923396eb0d870bd86d719d07f32 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 24 Jul 2024 04:01:41 +0900 Subject: [PATCH 51/79] ReShade related fixes (#1963) * Handle ReShadeAddonInterface ctor exceptions * Lower ReShadeApiVersion to 1 * fixes --- .../ReShadeHandling/ReShadeAddonInterface.cs | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs index de7629276..372928725 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs @@ -15,7 +15,7 @@ namespace Dalamud.Interface.Internal.ReShadeHandling; /// ReShade interface. internal sealed unsafe partial class ReShadeAddonInterface : IDisposable { - private const int ReShadeApiVersion = 12; + private const int ReShadeApiVersion = 1; private readonly HMODULE hDalamudModule; @@ -25,6 +25,8 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable private readonly DelegateStorage initSwapChainDelegate; private readonly DelegateStorage destroySwapChainDelegate; + private bool requiresFinalize; + private ReShadeAddonInterface() { this.hDalamudModule = (HMODULE)Marshal.GetHINSTANCE(typeof(ReShadeAddonInterface).Assembly.ManifestModule); @@ -40,16 +42,37 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable 0, this.GetModuleHandleExWDetour); - this.addonModuleResolverHook.Enable(); - Exports.ReShadeRegisterEvent( - AddonEvent.ReShadeOverlay, - this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt))); - Exports.ReShadeRegisterEvent( - AddonEvent.InitSwapChain, - this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt))); - Exports.ReShadeRegisterEvent( - AddonEvent.DestroySwapChain, - this.destroySwapChainDelegate = new((ref ApiObject rt) => this.DestroySwapChain?.Invoke(ref rt))); + try + { + this.addonModuleResolverHook.Enable(); + Exports.ReShadeRegisterEvent( + AddonEvent.ReShadeOverlay, + this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt))); + Exports.ReShadeRegisterEvent( + AddonEvent.InitSwapChain, + this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt))); + Exports.ReShadeRegisterEvent( + AddonEvent.DestroySwapChain, + this.destroySwapChainDelegate = new((ref ApiObject rt) => this.DestroySwapChain?.Invoke(ref rt))); + } + catch (Exception e1) + { + Exports.ReShadeUnregisterAddon(this.hDalamudModule); + + try + { + this.addonModuleResolverHook.Disable(); + this.addonModuleResolverHook.Dispose(); + } + catch (Exception e2) + { + throw new AggregateException(e1, e2); + } + + throw; + } + + this.requiresFinalize = true; } /// Finalizes an instance of the class. @@ -104,9 +127,10 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable private void ReleaseUnmanagedResources() { - Exports.ReShadeUnregisterEvent(AddonEvent.InitSwapChain, this.initSwapChainDelegate); - Exports.ReShadeUnregisterEvent(AddonEvent.DestroySwapChain, this.destroySwapChainDelegate); - Exports.ReShadeUnregisterEvent(AddonEvent.ReShadeOverlay, this.reShadeOverlayDelegate); + if (!this.requiresFinalize) + return; + this.requiresFinalize = false; + // This will also unregister addon event registrations. Exports.ReShadeUnregisterAddon(this.hDalamudModule); this.addonModuleResolverHook.Disable(); this.addonModuleResolverHook.Dispose(); From 4383a5747dff64bb9a24d8622a869686dae15563 Mon Sep 17 00:00:00 2001 From: attick Date: Wed, 24 Jul 2024 00:17:02 -0400 Subject: [PATCH 52/79] Add SerpentCombo to VPRGauge (#1960) Co-authored-by: KazWolfe --- .../JobGauge/Enums/SerpentCombo.cs | 42 +++++++++++++++++++ .../ClientState/JobGauge/Types/VPRGauge.cs | 8 +++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs diff --git a/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs b/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs new file mode 100644 index 000000000..0fc50d87a --- /dev/null +++ b/Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs @@ -0,0 +1,42 @@ +namespace Dalamud.Game.ClientState.JobGauge.Enums; + +/// +/// Enum representing the SerpentCombo actions for the VPR job gauge. +/// +public enum SerpentCombo : byte +{ + /// + /// No Serpent combo is active. + /// + NONE = 0, + + /// + /// Death Rattle action. + /// + DEATHRATTLE = 1, + + /// + /// Last Lash action. + /// + LASTLASH = 2, + + /// + /// First Legacy action. + /// + FIRSTLEGACY = 3, + + /// + /// Second Legacy action. + /// + SECONDLEGACY = 4, + + /// + /// Third Legacy action. + /// + THIRDLEGACY = 5, + + /// + /// Fourth Legacy action. + /// + FOURTHLEGACY = 6, +} diff --git a/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs index a889c3482..3c822c7d7 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/VPRGauge.cs @@ -1,8 +1,9 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Gauge; +using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using Reloaded.Memory; using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo; +using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo; namespace Dalamud.Game.ClientState.JobGauge.Types; @@ -39,4 +40,9 @@ public unsafe class VPRGauge : JobGaugeBase /// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo. /// public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo; + + /// + /// Gets current ability for Serpent's Tail. + /// + public SerpentCombo SerpentCombo => (SerpentCombo)Struct->SerpentCombo; } From 32b24b3b5a23b627bdabc2db6d6739ed5b6d8c69 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 24 Jul 2024 18:39:24 +0900 Subject: [PATCH 53/79] Fix ServiceScope.CreatePrivateScopedObject concurrency --- Dalamud/IoC/Internal/ServiceScope.cs | 60 ++++++++++++++++------------ 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index c21e73f34..c243eca28 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Serilog; + namespace Dalamud.IoC.Internal; /// @@ -41,8 +44,8 @@ internal class ServiceScopeImpl : IServiceScope { private readonly ServiceContainer container; - private readonly List privateScopedObjects = new(); - private readonly List scopeCreatedObjects = new(); + private readonly List privateScopedObjects = []; + private readonly ConcurrentDictionary> scopeCreatedObjects = new(); /// /// Initializes a new instance of the class. @@ -77,34 +80,39 @@ internal class ServiceScopeImpl : IServiceScope /// The type of object to create. /// Additional scoped objects. /// The created object, or null. - public async Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) - { - var instance = this.scopeCreatedObjects.FirstOrDefault(x => x.GetType() == objectType); - if (instance != null) - return instance; - - instance = - await this.container.CreateAsync(objectType, scopedObjects.Concat(this.privateScopedObjects).ToArray()); - if (instance != null) - this.scopeCreatedObjects.Add(instance); - - return instance; - } + public Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) => + this.scopeCreatedObjects.GetOrAdd( + objectType, + static (objectType, p) => p.Scope.container.CreateAsync( + objectType, + p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()), + (Scope: this, Objects: scopedObjects)); /// public void Dispose() { - foreach (var createdObject in this.scopeCreatedObjects) + foreach (var objectTask in this.scopeCreatedObjects) { - switch (createdObject) - { - case IInternalDisposableService d: - d.DisposeService(); - break; - case IDisposable d: - d.Dispose(); - break; - } + objectTask.Value.ContinueWith( + static r => + { + if (!r.IsCompletedSuccessfully) + { + if (r.Exception is { } e) + Log.Error(e, "{what}: Failed to load.", nameof(ServiceScopeImpl)); + return; + } + + switch (r.Result) + { + case IInternalDisposableService d: + d.DisposeService(); + break; + case IDisposable d: + d.Dispose(); + break; + } + }); } } } From 4b98f4e60aa9a864886d18240aa95ad6f6617c1b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 24 Jul 2024 18:55:27 +0900 Subject: [PATCH 54/79] Remove nullability from IServiceScope.CreateAsync --- Dalamud/IoC/Internal/ServiceContainer.cs | 14 ++++++------ Dalamud/IoC/Internal/ServiceScope.cs | 8 +++---- Dalamud/Plugin/DalamudPluginInterface.cs | 24 ++++++++++++-------- Dalamud/Plugin/IDalamudPluginInterface.cs | 11 ++++++++- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 22 ++++++------------ 5 files changed, 42 insertions(+), 37 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 7a0b4347d..06e2ff14d 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -84,15 +84,15 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Scoped objects to be included in the constructor. /// The scope to be used to create scoped services. /// The created object. - public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) + public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) { var scopeImpl = scope as ServiceScopeImpl; var ctor = this.FindApplicableCtor(objectType, scopedObjects); if (ctor == null) { - Log.Error("Failed to create {TypeName}, an eligible ctor with satisfiable services could not be found", objectType.FullName!); - return null; + throw new InvalidOperationException( + $"Failed to create {objectType.FullName ?? objectType.Name}; an eligible ctor with satisfiable services could not be found"); } // validate dependency versions (if they exist) @@ -116,16 +116,16 @@ internal class ServiceContainer : IServiceProvider, IServiceType var hasNull = resolvedParams.Any(p => p == null); if (hasNull) { - Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName!); - return null; + throw new InvalidOperationException( + $"Failed to create {objectType.FullName ?? objectType.Name}; a requested service type could not be satisfied"); } var instance = RuntimeHelpers.GetUninitializedObject(objectType); if (!await this.InjectProperties(instance, scopedObjects, scope)) { - Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!); - return null; + throw new InvalidOperationException( + $"Failed to create {objectType.FullName ?? objectType.Name}; a requested property service type could not be satisfied"); } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index c243eca28..33a4d3f54 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -25,7 +25,7 @@ internal interface IServiceScope : IDisposable /// The type of object to create. /// Scoped objects to be included in the constructor. /// The created object. - public Task CreateAsync(Type objectType, params object[] scopedObjects); + public Task CreateAsync(Type objectType, params object[] scopedObjects); /// /// Inject interfaces into public or static properties on the provided object. @@ -45,7 +45,7 @@ internal class ServiceScopeImpl : IServiceScope private readonly ServiceContainer container; private readonly List privateScopedObjects = []; - private readonly ConcurrentDictionary> scopeCreatedObjects = new(); + private readonly ConcurrentDictionary> scopeCreatedObjects = new(); /// /// Initializes a new instance of the class. @@ -63,7 +63,7 @@ internal class ServiceScopeImpl : IServiceScope } /// - public Task CreateAsync(Type objectType, params object[] scopedObjects) + public Task CreateAsync(Type objectType, params object[] scopedObjects) { return this.container.CreateAsync(objectType, scopedObjects, this); } @@ -80,7 +80,7 @@ internal class ServiceScopeImpl : IServiceScope /// The type of object to create. /// Additional scoped objects. /// The created object, or null. - public Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) => + public Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) => this.scopeCreatedObjects.GetOrAdd( objectType, static (objectType, p) => p.Scope.container.CreateAsync( diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index bb92b6b0c..9fb73fbe1 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Dalamud.Configuration; using Dalamud.Configuration.Internal; @@ -26,6 +27,8 @@ using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; +using Serilog; + namespace Dalamud.Plugin; /// @@ -458,21 +461,22 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa #region Dependency Injection - /// - /// Create a new object of the provided type using its default constructor, then inject objects and properties. - /// - /// Objects to inject additionally. - /// The type to create. - /// The created and initialized type. + /// public T? Create(params object[] scopedObjects) where T : class { - var svcContainer = Service.Get(); + var t = this.CreateAsync(scopedObjects); + t.Wait(); - return (T)this.plugin.ServiceScope!.CreateAsync( - typeof(T), - this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); + if (t.Exception is { } e) + Log.Error(e, "{who}: Failed to initialize {what}", this.plugin.Name, typeof(T).FullName ?? typeof(T).Name); + + return t.IsCompletedSuccessfully ? t.Result : null; } + /// + public async Task CreateAsync(params object[] scopedObjects) where T : class => + (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects)); + /// /// Inject services into properties on the provided object instance. /// diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index 6393dc5ab..b5257c033 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading.Tasks; using Dalamud.Configuration; using Dalamud.Game.Text; @@ -304,9 +305,17 @@ public interface IDalamudPluginInterface /// /// Objects to inject additionally. /// The type to create. - /// The created and initialized type. + /// The created and initialized type, or null on failure. T? Create(params object[] scopedObjects) where T : class; + /// + /// Create a new object of the provided type using its default constructor, then inject objects and properties. + /// + /// Objects to inject additionally. + /// The type to create. + /// A task representing the created and initialized type. + Task CreateAsync(params object[] scopedObjects) where T : class; + /// /// Inject services into properties on the provided object instance. /// diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index f7bb3495c..00fa9d243 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -417,24 +417,16 @@ internal class LocalPlugin : IDisposable try { - if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1) - { - var newInstance = await framework.RunOnFrameworkThread( - () => this.ServiceScope.CreateAsync( - this.pluginType!, - this.DalamudInterface!)).ConfigureAwait(false); - - this.instance = newInstance as IDalamudPlugin; - } - else - { - this.instance = - await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; - } + var forceFrameworkThread = this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1; + var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); + this.instance = await newInstanceTask.ConfigureAwait(false); + + async Task Create() => + (IDalamudPlugin)await this.ServiceScope!.CreateAsync(this.pluginType!, this.DalamudInterface!); } catch (Exception ex) { - Log.Error(ex, "Exception in plugin constructor"); + Log.Error(ex, "Exception during plugin initialization"); this.instance = null; } From db3e9a4171bc353f8e86987b29cc272a0fb68de4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 24 Jul 2024 19:17:31 +0900 Subject: [PATCH 55/79] Add IDalamudPluginInterface.InjectAsync --- Dalamud/IoC/Internal/ServiceContainer.cs | 134 +++++++++------------- Dalamud/IoC/Internal/ServiceScope.cs | 10 +- Dalamud/Plugin/DalamudPluginInterface.cs | 37 ++++-- Dalamud/Plugin/IDalamudPluginInterface.cs | 8 ++ 4 files changed, 92 insertions(+), 97 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 06e2ff14d..39c2007f3 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -86,70 +86,55 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// The created object. public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) { - var scopeImpl = scope as ServiceScopeImpl; + var errorStep = "constructor lookup"; - var ctor = this.FindApplicableCtor(objectType, scopedObjects); - if (ctor == null) + try { - throw new InvalidOperationException( - $"Failed to create {objectType.FullName ?? objectType.Name}; an eligible ctor with satisfiable services could not be found"); - } + var scopeImpl = scope as ServiceScopeImpl; - // validate dependency versions (if they exist) - var parameterTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList(); + var ctor = this.FindApplicableCtor(objectType, scopedObjects) + ?? throw new InvalidOperationException("An eligible ctor with satisfiable services could not be found"); - var resolvedParams = - await Task.WhenAll( - parameterTypes - .Select(async type => + errorStep = "requested service resolution"; + var resolvedParams = + await Task.WhenAll( + ctor.GetParameters() + .Select(p => p.ParameterType) + .Select(type => this.GetService(type, scopeImpl, scopedObjects))); + + var instance = RuntimeHelpers.GetUninitializedObject(objectType); + + errorStep = "property injection"; + await this.InjectProperties(instance, scopedObjects, scope); + + errorStep = "ctor invocation"; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var thr = new Thread( + () => + { + try { - var service = await this.GetService(type, scopeImpl, scopedObjects); + ctor.Invoke(instance, resolvedParams); + } + catch (Exception e) + { + tcs.SetException(e); + return; + } - if (service == null) - { - Log.Error("Requested ctor service type {TypeName} was not available (null)", type.FullName!); - } + tcs.SetResult(); + }); - return service; - })); + thr.Start(); + await tcs.Task.ConfigureAwait(false); + thr.Join(); - var hasNull = resolvedParams.Any(p => p == null); - if (hasNull) - { - throw new InvalidOperationException( - $"Failed to create {objectType.FullName ?? objectType.Name}; a requested service type could not be satisfied"); + return instance; } - - var instance = RuntimeHelpers.GetUninitializedObject(objectType); - - if (!await this.InjectProperties(instance, scopedObjects, scope)) + catch (Exception e) { - throw new InvalidOperationException( - $"Failed to create {objectType.FullName ?? objectType.Name}; a requested property service type could not be satisfied"); + throw new AggregateException($"Failed to create {objectType.FullName ?? objectType.Name} ({errorStep})", e); } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var thr = new Thread( - () => - { - try - { - ctor.Invoke(instance, resolvedParams); - } - catch (Exception e) - { - tcs.SetException(e); - return; - } - - tcs.SetResult(); - }); - - thr.Start(); - await tcs.Task.ConfigureAwait(false); - thr.Join(); - - return instance; } /// @@ -159,28 +144,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// The object instance. /// Scoped objects to be injected. /// The scope to be used to create scoped services. - /// Whether or not the injection was successful. - public async Task InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) + /// A representing the operation. + public async ValueTask InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) { var scopeImpl = scope as ServiceScopeImpl; var objectType = instance.GetType(); - var props = objectType.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | - BindingFlags.NonPublic).Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any()).ToArray(); + var props = + objectType + .GetProperties( + BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any()) + .ToArray(); foreach (var prop in props) - { - var service = await this.GetService(prop.PropertyType, scopeImpl, publicScopes); - if (service == null) - { - Log.Error("Requested service type {TypeName} was not available (null)", prop.PropertyType.FullName!); - return false; - } - - prop.SetValue(instance, service); - } - - return true; + prop.SetValue(instance, await this.GetService(prop.PropertyType, scopeImpl, publicScopes)); } /// @@ -192,7 +170,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType); - private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) + private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) { if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; @@ -201,8 +179,8 @@ internal class ServiceContainer : IServiceProvider, IServiceType { if (scope == null) { - Log.Error("Failed to create {TypeName}, is scoped but no scope provided", serviceType.FullName!); - return null; + throw new InvalidOperationException( + $"Failed to create {serviceType.FullName ?? serviceType.Name}, is scoped but no scope provided"); } return await scope.CreatePrivateScopedObject(serviceType, scopedObjects); @@ -210,18 +188,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType var singletonService = await this.GetSingletonService(serviceType, false); if (singletonService != null) - { return singletonService; - } // resolve dependency from scoped objects - var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)); - if (scoped == default) - { - return null; - } - - return scoped; + return scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)) + ?? throw new InvalidOperationException( + $"Requested type {serviceType.FullName ?? serviceType.Name} could not be found from {nameof(scopedObjects)}"); } private async Task GetSingletonService(Type serviceType, bool tryGetInterface = true) diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 33a4d3f54..fb06ec75c 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -33,8 +33,8 @@ internal interface IServiceScope : IDisposable /// /// The object instance. /// Scoped objects to be injected. - /// Whether or not the injection was successful. - public Task InjectPropertiesAsync(object instance, params object[] scopedObjects); + /// A representing the status of the operation. + public ValueTask InjectPropertiesAsync(object instance, params object[] scopedObjects); } /// @@ -69,10 +69,8 @@ internal class ServiceScopeImpl : IServiceScope } /// - public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) - { - return this.container.InjectProperties(instance, scopedObjects, this); - } + public ValueTask InjectPropertiesAsync(object instance, params object[] scopedObjects) => + this.container.InjectProperties(instance, scopedObjects, this); /// /// Create a service scoped to this scope, with private scoped objects. diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 9fb73fbe1..f0882c6fe 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -468,7 +468,14 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa t.Wait(); if (t.Exception is { } e) - Log.Error(e, "{who}: Failed to initialize {what}", this.plugin.Name, typeof(T).FullName ?? typeof(T).Name); + { + Log.Error( + e, + "{who}: Exception during {where}: {what}", + this.plugin.Name, + nameof(this.Create), + typeof(T).FullName ?? typeof(T).Name); + } return t.IsCompletedSuccessfully ? t.Result : null; } @@ -477,19 +484,29 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa public async Task CreateAsync(params object[] scopedObjects) where T : class => (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects)); - /// - /// Inject services into properties on the provided object instance. - /// - /// The instance to inject services into. - /// Objects to inject additionally. - /// Whether or not the injection succeeded. + /// public bool Inject(object instance, params object[] scopedObjects) { - return this.plugin.ServiceScope!.InjectPropertiesAsync( - instance, - this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); + var t = this.InjectAsync(instance, scopedObjects).AsTask(); + t.Wait(); + + if (t.Exception is { } e) + { + Log.Error( + e, + "{who}: Exception during {where}: {what}", + this.plugin.Name, + nameof(this.Inject), + instance.GetType().FullName ?? instance.GetType().Name); + } + + return t.IsCompletedSuccessfully; } + /// + public ValueTask InjectAsync(object instance, params object[] scopedObjects) => + this.plugin.ServiceScope!.InjectPropertiesAsync(instance, this.GetPublicIocScopes(scopedObjects)); + #endregion /// Unregister the plugin and dispose all references. diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index b5257c033..5205c3ed1 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -323,4 +323,12 @@ public interface IDalamudPluginInterface /// Objects to inject additionally. /// Whether or not the injection succeeded. bool Inject(object instance, params object[] scopedObjects); + + /// + /// Inject services into properties on the provided object instance. + /// + /// The instance to inject services into. + /// Objects to inject additionally. + /// A representing the status of the operation. + ValueTask InjectAsync(object instance, params object[] scopedObjects); } From a725bbf8e06c093d16aee3036551c242d48c7e11 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 24 Jul 2024 19:20:39 +0900 Subject: [PATCH 56/79] cleanup --- Dalamud/IoC/Internal/ServiceContainer.cs | 2 +- Dalamud/IoC/Internal/ServiceScope.cs | 27 ++++++++--------------- Dalamud/Plugin/DalamudPluginInterface.cs | 4 ++-- Dalamud/Plugin/IDalamudPluginInterface.cs | 4 ++-- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 39c2007f3..a8eacb02d 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -145,7 +145,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Scoped objects to be injected. /// The scope to be used to create scoped services. /// A representing the operation. - public async ValueTask InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) + public async Task InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) { var scopeImpl = scope as ServiceScopeImpl; var objectType = instance.GetType(); diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index fb06ec75c..4fc299f6e 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -17,7 +17,7 @@ internal interface IServiceScope : IDisposable /// but not directly to created objects. /// /// The scopes to add. - public void RegisterPrivateScopes(params object[] scopes); + void RegisterPrivateScopes(params object[] scopes); /// /// Create an object. @@ -25,7 +25,7 @@ internal interface IServiceScope : IDisposable /// The type of object to create. /// Scoped objects to be included in the constructor. /// The created object. - public Task CreateAsync(Type objectType, params object[] scopedObjects); + Task CreateAsync(Type objectType, params object[] scopedObjects); /// /// Inject interfaces into public or static properties on the provided object. @@ -34,7 +34,7 @@ internal interface IServiceScope : IDisposable /// The object instance. /// Scoped objects to be injected. /// A representing the status of the operation. - public ValueTask InjectPropertiesAsync(object instance, params object[] scopedObjects); + Task InjectPropertiesAsync(object instance, params object[] scopedObjects); } /// @@ -47,29 +47,20 @@ internal class ServiceScopeImpl : IServiceScope private readonly List privateScopedObjects = []; private readonly ConcurrentDictionary> scopeCreatedObjects = new(); - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The container this scope will use to create services. - public ServiceScopeImpl(ServiceContainer container) - { - this.container = container; - } + public ServiceScopeImpl(ServiceContainer container) => this.container = container; /// - public void RegisterPrivateScopes(params object[] scopes) - { + public void RegisterPrivateScopes(params object[] scopes) => this.privateScopedObjects.AddRange(scopes); - } /// - public Task CreateAsync(Type objectType, params object[] scopedObjects) - { - return this.container.CreateAsync(objectType, scopedObjects, this); - } + public Task CreateAsync(Type objectType, params object[] scopedObjects) => + this.container.CreateAsync(objectType, scopedObjects, this); /// - public ValueTask InjectPropertiesAsync(object instance, params object[] scopedObjects) => + public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) => this.container.InjectProperties(instance, scopedObjects, this); /// diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index f0882c6fe..ecd2e0799 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -487,7 +487,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public bool Inject(object instance, params object[] scopedObjects) { - var t = this.InjectAsync(instance, scopedObjects).AsTask(); + var t = this.InjectAsync(instance, scopedObjects); t.Wait(); if (t.Exception is { } e) @@ -504,7 +504,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa } /// - public ValueTask InjectAsync(object instance, params object[] scopedObjects) => + public Task InjectAsync(object instance, params object[] scopedObjects) => this.plugin.ServiceScope!.InjectPropertiesAsync(instance, this.GetPublicIocScopes(scopedObjects)); #endregion diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index 5205c3ed1..100d4570e 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -321,7 +321,7 @@ public interface IDalamudPluginInterface /// /// The instance to inject services into. /// Objects to inject additionally. - /// Whether or not the injection succeeded. + /// Whether the injection succeeded. bool Inject(object instance, params object[] scopedObjects); /// @@ -330,5 +330,5 @@ public interface IDalamudPluginInterface /// The instance to inject services into. /// Objects to inject additionally. /// A representing the status of the operation. - ValueTask InjectAsync(object instance, params object[] scopedObjects); + Task InjectAsync(object instance, params object[] scopedObjects); } From f1a1f176c3c4e12a5ecb3d24ca6c6ec5a25e31ec Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 25 Jul 2024 02:52:57 +0900 Subject: [PATCH 57/79] Change ReShade related detections (#1965) * Changed ReShade w/o addon support detection to compare the name of the signer to the string "ReShade", so that any false positives stemming from use of other injector do not trigger warnings. * Changed main SwapChain detection to be done by comparing the HWND of window that the SwapChain is attached to. --- .../Interface/Internal/InterfaceManager.cs | 2 +- .../ReShadeAddonInterface.Exports.cs | 131 ++++++++++++++---- Dalamud/Interface/Internal/SwapChainHelper.cs | 36 ++--- 3 files changed, 127 insertions(+), 42 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index b7267ea94..eeddb8334 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -772,7 +772,7 @@ internal partial class InterfaceManager : IInternalDisposableService 0, this.SetCursorDetour); - if (ReShadeAddonInterface.ReShadeHasSignature) + if (ReShadeAddonInterface.ReShadeIsSignedByReShade) { Log.Warning("Signed ReShade binary detected"); Service diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index 60bbc37cd..b3add07e7 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -1,9 +1,12 @@ +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using Serilog; + using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; @@ -30,30 +33,19 @@ internal sealed unsafe partial class ReShadeAddonInterface !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent)) continue; - fixed (void* pwszFile = m.FileName) - fixed (Guid* pguid = &WINTRUST_ACTION_GENERIC_VERIFY_V2) + try { - var wtfi = new WINTRUST_FILE_INFO - { - cbStruct = (uint)sizeof(WINTRUST_FILE_INFO), - pcwszFilePath = (ushort*)pwszFile, - hFile = default, - pgKnownSubject = null, - }; - var wtd = new WINTRUST_DATA - { - cbStruct = (uint)sizeof(WINTRUST_DATA), - pPolicyCallbackData = null, - pSIPClientData = null, - dwUIChoice = WTD.WTD_UI_NONE, - fdwRevocationChecks = WTD.WTD_REVOKE_NONE, - dwUnionChoice = WTD.WTD_STATEACTION_VERIFY, - hWVTStateData = default, - pwszURLReference = null, - dwUIContext = 0, - pFile = &wtfi, - }; - ReShadeHasSignature = WinVerifyTrust(default, pguid, &wtd) != TRUST.TRUST_E_NOSIGNATURE; + var signerName = GetSignatureSignerNameWithoutVerification(m.FileName); + ReShadeIsSignedByReShade = signerName == "ReShade"; + Log.Information( + "ReShade DLL is signed by {signerName}. {vn}={v}", + signerName, + nameof(ReShadeIsSignedByReShade), + ReShadeIsSignedByReShade); + } + catch (Exception ex) + { + Log.Information(ex, "ReShade DLL did not had a valid signature."); } ReShadeModule = m; @@ -78,7 +70,98 @@ internal sealed unsafe partial class ReShadeAddonInterface /// Gets a value indicating whether the loaded ReShade has signatures. /// ReShade without addon support is signed, but may not pass signature verification. - public static bool ReShadeHasSignature { get; private set; } + public static bool ReShadeIsSignedByReShade { get; private set; } + + /// Gets the name of the signer of a file that has a certificate embedded within, without verifying if the + /// file has a valid signature. + /// Path to the file. + /// Name of the signer. + // https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/get-information-authenticode-signed-executables + private static string GetSignatureSignerNameWithoutVerification(ReadOnlySpan path) + { + var hCertStore = default(HCERTSTORE); + var hMsg = default(HCRYPTMSG); + var pCertContext = default(CERT_CONTEXT*); + try + { + fixed (void* pwszFile = path) + { + uint dwMsgAndCertEncodingType; + uint dwContentType; + uint dwFormatType; + void* pvContext; + if (!CryptQueryObject( + CERT.CERT_QUERY_OBJECT_FILE, + pwszFile, + CERT.CERT_QUERY_CONTENT_FLAG_ALL, + CERT.CERT_QUERY_FORMAT_FLAG_ALL, + 0, + &dwMsgAndCertEncodingType, + &dwContentType, + &dwFormatType, + &hCertStore, + &hMsg, + &pvContext)) + { + throw new Win32Exception("CryptQueryObject"); + } + } + + var pcb = 0u; + if (!CryptMsgGetParam(hMsg, CMSG.CMSG_SIGNER_INFO_PARAM, 0, null, &pcb)) + throw new Win32Exception("CryptMsgGetParam(1)"); + + var signerInfo = GC.AllocateArray((int)pcb, true); + var pSignerInfo = (CMSG_SIGNER_INFO*)Unsafe.AsPointer(ref signerInfo[0]); + if (!CryptMsgGetParam(hMsg, CMSG.CMSG_SIGNER_INFO_PARAM, 0, pSignerInfo, &pcb)) + throw new Win32Exception("CryptMsgGetParam(2)"); + + var certInfo = new CERT_INFO + { + Issuer = pSignerInfo->Issuer, + SerialNumber = pSignerInfo->SerialNumber, + }; + pCertContext = CertFindCertificateInStore( + hCertStore, + X509.X509_ASN_ENCODING | PKCS.PKCS_7_ASN_ENCODING, + 0, + CERT.CERT_FIND_SUBJECT_CERT, + &certInfo, + null); + if (pCertContext == default) + throw new Win32Exception("CertFindCertificateInStore"); + + pcb = CertGetNameStringW( + pCertContext, + CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT.CERT_NAME_ISSUER_FLAG, + null, + null, + pcb); + if (pcb == 0) + throw new Win32Exception("CertGetNameStringW(1)"); + + var issuerName = GC.AllocateArray((int)pcb, true); + pcb = CertGetNameStringW( + pCertContext, + CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT.CERT_NAME_ISSUER_FLAG, + null, + (ushort*)Unsafe.AsPointer(ref issuerName[0]), + pcb); + if (pcb == 0) + throw new Win32Exception("CertGetNameStringW(2)"); + + // The string is null-terminated. + return new(issuerName.AsSpan()[..^1]); + } + finally + { + if (pCertContext != default) CertFreeCertificateContext(pCertContext); + if (hCertStore != default) CertCloseStore(hCertStore, 0); + if (hMsg != default) CryptMsgClose(hMsg); + } + } private struct ExportsStruct { diff --git a/Dalamud/Interface/Internal/SwapChainHelper.cs b/Dalamud/Interface/Internal/SwapChainHelper.cs index 4a336ee9f..6e9254680 100644 --- a/Dalamud/Interface/Internal/SwapChainHelper.cs +++ b/Dalamud/Interface/Internal/SwapChainHelper.cs @@ -69,26 +69,28 @@ internal static unsafe class SwapChainHelper /// true if the object is the game's swap chain. public static bool IsGameDeviceSwapChain(T* punk) where T : unmanaged, IUnknown.Interface { - // https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void) - // For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any - // of the object's interfaces must always return the same pointer value. + using var psc = default(ComPtr); + fixed (Guid* piid = &IID.IID_IDXGISwapChain) + { + if (punk->QueryInterface(piid, (void**)psc.GetAddressOf()).FAILED) + return false; + } - var gdsc = GameDeviceSwapChain; - if (gdsc is null || punk is null) + return IsGameDeviceSwapChain(psc.Get()); + } + + /// + public static bool IsGameDeviceSwapChain(IDXGISwapChain* punk) + { + DXGI_SWAP_CHAIN_DESC desc1; + if (punk->GetDesc(&desc1).FAILED) + return false; + + DXGI_SWAP_CHAIN_DESC desc2; + if (GameDeviceSwapChain->GetDesc(&desc2).FAILED) return false; - fixed (Guid* iid = &IID.IID_IUnknown) - { - using var u1 = default(ComPtr); - if (gdsc->QueryInterface(iid, (void**)u1.GetAddressOf()).FAILED) - return false; - - using var u2 = default(ComPtr); - if (punk->QueryInterface(iid, (void**)u2.GetAddressOf()).FAILED) - return false; - - return u1.Get() == u2.Get(); - } + return desc1.OutputWindow == desc2.OutputWindow; } /// Wait for the game to have finished initializing the IDXGISwapChain. From 426eaec0f2dc87ade31f223e876812abc9bfc102 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Thu, 25 Jul 2024 03:06:45 +0900 Subject: [PATCH 58/79] Mark DtrBar entries as dirty when added (#1967) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 4e1ad1533..779dbf7f9 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -229,6 +229,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar if (!data.Added) { data.Added = this.AddNode(data.TextNode); + data.Dirty = true; } var isHide = !data.Shown || data.UserHidden; From def28b37c7511b0d479216e9eac1692db71cfe21 Mon Sep 17 00:00:00 2001 From: RyouBakura Date: Wed, 24 Jul 2024 20:07:23 +0200 Subject: [PATCH 59/79] GetFromFile without FileInfo (#1913) * GetFromFile without FileInfo * Implement missing interface member --- .../Internal/TextureManager.SharedTextures.cs | 11 ++++++++++- .../Textures/Internal/TextureManagerPluginScoped.cs | 8 ++++++++ Dalamud/Plugin/Services/ITextureProvider.cs | 11 ++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs index 92152b1fb..156ffa56f 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.SharedTextures.cs @@ -45,6 +45,10 @@ internal sealed partial class TextureManager ISharedImmediateTexture ITextureProvider.GetFromFile(FileInfo file) => this.Shared.GetFromFile(file); + /// + public ISharedImmediateTexture GetFromFileAbsolute(string fullPath) => + this.Shared.GetFromFileAbsolute(fullPath); + /// ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) => this.Shared.GetFromManifestResource(assembly, name); @@ -141,7 +145,12 @@ internal sealed partial class TextureManager /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public SharedImmediateTexture.PureImpl GetFromFile(FileInfo file) => - this.fileDict.GetOrAdd(file.FullName, FileSystemSharedImmediateTexture.CreatePlaceholder) + this.GetFromFileAbsolute(file.FullName); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public SharedImmediateTexture.PureImpl GetFromFileAbsolute(string fullPath) => + this.fileDict.GetOrAdd(fullPath, FileSystemSharedImmediateTexture.CreatePlaceholder) .PublicUseInstance; /// diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 27f97168e..68e2dde47 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -313,6 +313,14 @@ internal sealed class TextureManagerPluginScoped return shared; } + /// + public ISharedImmediateTexture GetFromFileAbsolute(string fullPath) + { + var shared = this.ManagerOrThrow.Shared.GetFromFileAbsolute(fullPath); + shared.AddOwnerPlugin(this.plugin); + return shared; + } + /// public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) { diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index d75899bd4..d914b1091 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; @@ -232,6 +232,15 @@ public interface ITextureProvider /// ISharedImmediateTexture GetFromFile(FileInfo file); + /// Gets a shared texture corresponding to the given file on the filesystem. + /// The file on the filesystem to load. Requires a full path. + /// The shared texture that you may use to obtain the loaded texture wrap and load states. + /// + /// This function does not throw exceptions. + /// Caching the returned object is not recommended. Performance benefit will be minimal. + /// + ISharedImmediateTexture GetFromFileAbsolute(string fullPath); + /// Gets a shared texture corresponding to the given file of the assembly manifest resources. /// The assembly containing manifest resources. /// The case-sensitive name of the manifest resource being requested. From 56f7da5e0c6940e38da0a7eb12c6c2feeb9ebb06 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 24 Jul 2024 13:23:54 -0700 Subject: [PATCH 60/79] feat: Add helper to get the character's current mount --- .../Game/ClientState/Objects/Types/Character.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index 67f8c8b62..c4c91f5b0 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -93,6 +93,11 @@ public interface ICharacter : IGameObject /// Gets the status flags. /// public StatusFlags StatusFlags { get; } + + /// + /// Gets the current mount for this character. Will be null if the character doesn't have a mount. + /// + public ExcelResolver? CurrentMount { get; } } /// @@ -172,6 +177,18 @@ internal unsafe class Character : GameObject, ICharacter (this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) | (this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) | (this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None); + + /// + public ExcelResolver? CurrentMount + { + get + { + if (this.Struct->IsNotMounted()) return null; // safety i guess? + + var mountId = this.Struct->Mount.MountId; + return mountId == 0 ? null : new ExcelResolver(mountId); + } + } /// /// Gets the underlying structure. From 35520fab8c7d7cb3bd47f1e4d0a6090cf1c737eb Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 24 Jul 2024 13:36:33 -0700 Subject: [PATCH 61/79] feat: Add CurrentMinion to Character --- .../ClientState/Objects/Types/Character.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index c4c91f5b0..72f6a9950 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -98,6 +98,13 @@ public interface ICharacter : IGameObject /// Gets the current mount for this character. Will be null if the character doesn't have a mount. /// public ExcelResolver? CurrentMount { get; } + + /// + /// Gets the current minion summoned for this character. Will be null if the character doesn't have a minion. + /// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a + /// mount. + /// + public ExcelResolver? CurrentMinion { get; } } /// @@ -183,13 +190,27 @@ internal unsafe class Character : GameObject, ICharacter { get { - if (this.Struct->IsNotMounted()) return null; // safety i guess? + if (this.Struct->IsNotMounted()) return null; // just for safety. var mountId = this.Struct->Mount.MountId; return mountId == 0 ? null : new ExcelResolver(mountId); } } + /// + public ExcelResolver? CurrentMinion + { + get + { + if (this.Struct->CompanionObject != null) + return new ExcelResolver(this.Struct->CompanionObject->BaseId); + + // this is only present if a minion is summoned but hidden (e.g. the player's on a mount). + var hiddenCompanionId = this.Struct->CompanionData.CompanionId; + return hiddenCompanionId == 0 ? null : new ExcelResolver(hiddenCompanionId); + } + } + /// /// Gets the underlying structure. /// From 818eec91a0cfc8b25718a892d7bb99e0faf533e6 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 25 Jul 2024 07:33:46 +0900 Subject: [PATCH 62/79] Use ReShade addon event Present (#1970) --- .../InterfaceManager.AsReShadeAddon.cs | 29 ++++++---- .../Interface/Internal/InterfaceManager.cs | 2 +- .../ReShadeHandling/ReShadeAddonInterface.cs | 53 ++++++++++++++++--- 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index 9c08aaf06..486a3874b 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -1,8 +1,10 @@ using System.Diagnostics; +using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Utility; using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; namespace Dalamud.Interface.Internal; @@ -11,36 +13,41 @@ namespace Dalamud.Interface.Internal; /// internal partial class InterfaceManager { - private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeHandling.ReShadeAddonInterface.ApiObject swapchain) + private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { - var swapChain = swapchain.GetNative(); - if (this.scene?.SwapChain.NativePointer != (nint)swapChain) + var swapChainNative = swapChain.GetNative(); + if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) return; this.scene?.OnPreResize(); } - private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeHandling.ReShadeAddonInterface.ApiObject swapchain) + private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { - var swapChain = swapchain.GetNative(); - if (this.scene?.SwapChain.NativePointer != (nint)swapChain) + var swapChainNative = swapChain.GetNative(); + if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) return; DXGI_SWAP_CHAIN_DESC desc; - if (swapChain->GetDesc(&desc).FAILED) + if (swapChainNative->GetDesc(&desc).FAILED) return; this.scene?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height); } - private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeHandling.ReShadeAddonInterface.ApiObject runtime) + private void ReShadeAddonInterfaceOnPresent( + ref ReShadeAddonInterface.ApiObject runtime, + ref ReShadeAddonInterface.ApiObject swapChain, + ReadOnlySpan sourceRect, + ReadOnlySpan destRect, + ReadOnlySpan dirtyRects) { - var swapChain = runtime.GetNative(); + var swapChainNative = swapChain.GetNative(); if (this.scene == null) - this.InitScene(swapChain); + this.InitScene(swapChainNative); - if (this.scene?.SwapChain.NativePointer != swapChain) + if (this.scene?.SwapChain.NativePointer != swapChainNative) return; Debug.Assert(this.dalamudAtlas is not null, "this.dalamudAtlas is not null"); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index eeddb8334..0491fed1e 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -826,7 +826,7 @@ internal partial class InterfaceManager : IInternalDisposableService ReShadeAddonInterface.ReShadeModule!.BaseAddress); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; - this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; + this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent; } else { diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs index 372928725..cb4c04afa 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs @@ -21,7 +21,7 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable private readonly Hook addonModuleResolverHook; - private readonly DelegateStorage reShadeOverlayDelegate; + private readonly DelegateStorage presentDelegate; private readonly DelegateStorage initSwapChainDelegate; private readonly DelegateStorage destroySwapChainDelegate; @@ -46,8 +46,21 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable { this.addonModuleResolverHook.Enable(); Exports.ReShadeRegisterEvent( - AddonEvent.ReShadeOverlay, - this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt))); + AddonEvent.Present, + this.presentDelegate = new( + ( + ref ApiObject commandQueue, + ref ApiObject swapChain, + RECT* pSourceRect, + RECT* pDestRect, + uint dirtyRectCount, + void* pDirtyRects) => + this.Present?.Invoke( + ref commandQueue, + ref swapChain, + pSourceRect is null ? default : new(pSourceRect, 1), + pDestRect is null ? default : new(pDestRect, 1), + new(pDirtyRects, (int)dirtyRectCount)))); Exports.ReShadeRegisterEvent( AddonEvent.InitSwapChain, this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt))); @@ -79,8 +92,17 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable ~ReShadeAddonInterface() => this.ReleaseUnmanagedResources(); /// Delegate for . - /// Reference to the ReShade runtime. - public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime); + /// Current command queue. Type: api::command_queue. + /// Current swap chain. Type: api::swapchain. + /// Optional; source rectangle. May contain up to 1 element. + /// Optional; target rectangle. May contain up to 1 element. + /// Dirty rectangles. + public delegate void PresentDelegate( + ref ApiObject commandQueue, + ref ApiObject swapChain, + ReadOnlySpan sourceRect, + ReadOnlySpan destRect, + ReadOnlySpan dirtyRects); /// Delegate for . /// Reference to the ReShade SwapChain wrapper. @@ -90,10 +112,25 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable /// Reference to the ReShade SwapChain wrapper. public delegate void ReShadeDestroySwapChain(ref ApiObject swapChain); + /// Delegate for . + /// Current command queue. Type: api::command_queue. + /// Current swap chain. Type: api::swapchain. + /// Optional; source rectangle. + /// Optional; target rectangle. + /// Number of dirty rectangles. + /// Optional; dirty rectangles. + private delegate void UnsafePresentDelegate( + ref ApiObject commandQueue, + ref ApiObject swapChain, + RECT* pSourceRect, + RECT* pDestRect, + uint dirtyRectCount, + void* pDirtyRects); + private delegate BOOL GetModuleHandleExWDelegate(uint dwFlags, ushort* lpModuleName, HMODULE* phModule); - /// Called on . - public event ReShadeOverlayDelegate? ReShadeOverlay; + /// Called on . + public event PresentDelegate? Present; /// Called on . public event ReShadeInitSwapChain? InitSwapChain; @@ -144,7 +181,7 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule); if (lpModuleName == this.initSwapChainDelegate || lpModuleName == this.destroySwapChainDelegate || - lpModuleName == this.reShadeOverlayDelegate) + lpModuleName == this.presentDelegate) { *phModule = this.hDalamudModule; return BOOL.TRUE; From 0684c31ec1f5efdd5e6b69988b72c34e876ca5fb Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Wed, 24 Jul 2024 15:58:22 -0700 Subject: [PATCH 63/79] ci: Fixups (#1971) - Disable rollup job as it's not needed at the moment - Ignore some compat changes. - CP0006: Allow members to be added to interfaces. --- .github/workflows/main.yml | 2 +- .github/workflows/rollup.yml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d78c87d68..73fe25562 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: foreach ($file in $FILES_TO_VALIDATE) { $testout = "" Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ===" - apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout + apicompat -l "left\${file}" -r "right\${file}" --noWarn "$(NoWarn);CP0006" | Tee-Object -Variable testout Write-Output "::endgroup::" if ($testout -ne "APICompat ran successfully without finding any breaking changes.") { Write-Output "::error::${file} did not pass. Please review it for problems." diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index c3aba2a63..70a8fc7b7 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -11,8 +11,7 @@ jobs: strategy: matrix: branches: - - new_im_hooks -# - apiX + - WORKFLOW_DISABLED_REMOVE_BEFORE_RUNNING defaults: run: From 5650bf3ae00789a8196891d64ae4f5c94936b8d3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 25 Jul 2024 00:59:34 +0200 Subject: [PATCH 64/79] Properly remove DtrEntries from DtrBarPluginScoped (#1969) fixes #1862 --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 779dbf7f9..55b2573f0 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -76,6 +76,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar this.configuration.QueueSave(); } + /// + /// Event type fired each time a DtrEntry was removed. + /// + /// The title of the bar entry. + internal delegate void DtrEntryRemovedDelegate(string title); + + /// + /// Event fired each time a DtrEntry was removed. + /// + internal event DtrEntryRemovedDelegate? DtrEntryRemoved; + /// public IReadOnlyList Entries => this.entries; @@ -136,6 +147,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar if (data.ShouldBeRemoved) { this.RemoveEntry(data); + this.DtrEntryRemoved?.Invoke(data.Title); } } @@ -525,7 +537,6 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// Plugin-scoped version of a AddonEventManager service. /// [PluginInterface] - [ServiceManager.ScopedService] #pragma warning disable SA1015 [ResolveVia] @@ -536,13 +547,23 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar private readonly DtrBar dtrBarService = Service.Get(); private readonly Dictionary pluginEntries = new(); - + + /// + /// Initializes a new instance of the class. + /// + internal DtrBarPluginScoped() + { + this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved; + } + /// public IReadOnlyList Entries => this.dtrBarService.Entries; /// void IInternalDisposableService.DisposeService() { + this.dtrBarService.DtrEntryRemoved -= this.OnDtrEntryRemoved; + foreach (var entry in this.pluginEntries) { entry.Value.Remove(); @@ -569,4 +590,9 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar this.pluginEntries.Remove(title); } } + + private void OnDtrEntryRemoved(string title) + { + this.pluginEntries.Remove(title); + } } From ebee2f151ec42acea4d026ec72746566a3a41641 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 25 Jul 2024 18:05:21 +0900 Subject: [PATCH 65/79] add back DXGISwapChain::on_present hook as default for now --- Dalamud/Hooking/Internal/ObjectVTableHook.cs | 9 + .../Internal/InterfaceManager.AsHook.cs | 149 ++++++++--- .../InterfaceManager.AsReShadeAddon.cs | 49 +--- .../Interface/Internal/InterfaceManager.cs | 240 +++++++++++++----- .../ReShadeAddonInterface.Exports.cs | 61 +++-- .../ReShadeHandling/ReShadeHandlingMode.cs | 11 +- .../Settings/Tabs/SettingsTabExperimental.cs | 32 ++- 7 files changed, 379 insertions(+), 172 deletions(-) diff --git a/Dalamud/Hooking/Internal/ObjectVTableHook.cs b/Dalamud/Hooking/Internal/ObjectVTableHook.cs index b4500bb5f..8b2f24de2 100644 --- a/Dalamud/Hooking/Internal/ObjectVTableHook.cs +++ b/Dalamud/Hooking/Internal/ObjectVTableHook.cs @@ -54,6 +54,15 @@ internal unsafe class ObjectVTableHook : IDisposable /// Gets the span view of overriden vtable. public ReadOnlySpan OverridenVTableSpan => this.vtblOverriden.AsSpan(); + /// Gets the address of the pointer to the vtable. + public nint Address => (nint)this.ppVtbl; + + /// Gets the address of the original vtable. + public nint OriginalVTableAddress => (nint)this.pVtblOriginal; + + /// Gets the address of the overriden vtable. + public nint OverridenVTableAddress => (nint)Unsafe.AsPointer(ref this.vtblOverriden[0]); + /// Disables the hook. public void Disable() { diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs index b2afb970f..3dc8b30cd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs @@ -1,75 +1,142 @@ using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.Utility; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + namespace Dalamud.Interface.Internal; /// /// This class manages interaction with the ImGui interface. /// -internal partial class InterfaceManager +internal unsafe partial class InterfaceManager { - private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) + // NOTE: Do not use HRESULT as return value type. It appears that .NET marshaller thinks HRESULT needs to be still + // treated as a type that does not fit into RAX. + + /// Delegate for DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in + /// dxgi_swapchain.cpp. + /// Pointer to an instance of DXGISwapChain, which happens to be an + /// . + /// An integer value that contains swap-chain presentation options. These options are defined by + /// the DXGI_PRESENT constants. + /// Optional; DXGI present parameters. + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void ReShadeDxgiSwapChainPresentDelegate( + ReShadeDxgiSwapChain* swapChain, + uint flags, + DXGI_PRESENT_PARAMETERS* presentParams); + + /// Delegate for . + /// Microsoft + /// Learn. + /// Pointer to an instance of . + /// An integer that specifies how to synchronize presentation of a frame with the + /// vertical blank. + /// An integer value that contains swap-chain presentation options. These options are defined by + /// the DXGI_PRESENT constants. + /// A representing the result of the operation. + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate int DxgiSwapChainPresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint flags); + + /// Detour function for . + /// + /// Microsoft Learn. + /// Pointer to an instance of . + /// The number of buffers in the swap chain (including all back and front buffers). + /// This number can be different from the number of buffers with which you created the swap chain. This number + /// can't be greater than . Set this number to zero to preserve the + /// existing number of buffers in the swap chain. You can't specify less than two buffers for the flip presentation + /// model. + /// The new width of the back buffer. If you specify zero, DXGI will use the width of the client + /// area of the target window. You can't specify the width as zero if you called the + /// method to create the swap chain for a composition + /// surface. + /// The new height of the back buffer. If you specify zero, DXGI will use the height of the + /// client area of the target window. You can't specify the height as zero if you called the + /// method to create the swap chain for a composition + /// surface. + /// A DXGI_FORMAT-typed value for the new format of the back buffer. Set this value to + /// to preserve the existing format of the back buffer. The flip + /// presentation model supports a more restricted set of formats than the bit-block transfer (bitblt) model. + /// A combination of -typed values that are combined + /// by using a bitwise OR operation. The resulting value specifies options for swap-chain behavior. + /// A representing the result of the operation. + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate int ResizeBuffersDelegate( + IDXGISwapChain* swapChain, + uint bufferCount, + uint width, + uint height, + DXGI_FORMAT newFormat, + uint swapChainFlags); + + private void ReShadeDxgiSwapChainOnPresentDetour( + ReShadeDxgiSwapChain* swapChain, + uint flags, + DXGI_PRESENT_PARAMETERS* presentParams) { - if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + Debug.Assert( + this.reShadeDxgiSwapChainPresentHook is not null, + "this.reShadeDxgiSwapChainPresentHook is not null"); - Debug.Assert(this.dxgiPresentHook is not null, "How did PresentDetour get called when presentHook is null?"); - Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + // Call this first to draw Dalamud over ReShade. + this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams); - if (this.scene == null) - this.InitScene(swapChain); + if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain()) is { } activeScene) + this.RenderDalamudDraw(activeScene); - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - { - if (this.dalamudAtlas.BuildTask.Exception != null) - { - // TODO: Can we do something more user-friendly here? Unload instead? - Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); - Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); - } - - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); - } - - this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; - - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + // Upstream call to system IDXGISwapChain::Present will be called by ReShade. } - private IntPtr AsHookResizeBuffersDetour( - IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) + private int DxgiSwapChainPresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint flags) + { + Debug.Assert(this.dxgiSwapChainPresentHook is not null, "this.dxgiSwapChainPresentHook is not null"); + + if (this.RenderDalamudCheckAndInitialize(swapChain) is { } activeScene) + this.RenderDalamudDraw(activeScene); + + return this.dxgiSwapChainPresentHook!.Original(swapChain, syncInterval, flags); + } + + private int AsHookDxgiSwapChainResizeBuffersDetour( + IDXGISwapChain* swapChain, + uint bufferCount, + uint width, + uint height, + DXGI_FORMAT newFormat, + uint swapChainFlags) { if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); #if DEBUG Log.Verbose( - $"Calling resizebuffers swap@{swapChain.ToInt64():X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); + $"Calling resizebuffers swap@{(nint)swapChain:X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); #endif this.ResizeBuffers?.InvokeSafely(); this.scene?.OnPreResize(); - var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); - if (ret.ToInt64() == 0x887A0001) - { + var ret = this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + if (ret == DXGI.DXGI_ERROR_INVALID_CALL) Log.Error("invalid call to resizeBuffers"); - } this.scene?.OnPostResize((int)width, (int)height); return ret; } + + /// Represents DXGISwapChain in ReShade. + [StructLayout(LayoutKind.Sequential)] + private struct ReShadeDxgiSwapChain + { + // DXGISwapChain only implements IDXGISwapChain4. The only vtable should be that. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDXGISwapChain* AsIDxgiSwapChain() => (IDXGISwapChain*)Unsafe.AsPointer(ref this); + } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index 486a3874b..817a935ee 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -11,9 +11,9 @@ namespace Dalamud.Interface.Internal; /// /// This class manages interaction with the ImGui interface. /// -internal partial class InterfaceManager +internal unsafe partial class InterfaceManager { - private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain) + private void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { var swapChainNative = swapChain.GetNative(); if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) @@ -22,7 +22,7 @@ internal partial class InterfaceManager this.scene?.OnPreResize(); } - private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) + private void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { var swapChainNative = swapChain.GetNative(); if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) @@ -42,52 +42,25 @@ internal partial class InterfaceManager ReadOnlySpan destRect, ReadOnlySpan dirtyRects) { - var swapChainNative = swapChain.GetNative(); + var swapChainNative = swapChain.GetNative(); - if (this.scene == null) - this.InitScene(swapChainNative); - - if (this.scene?.SwapChain.NativePointer != swapChainNative) - return; - - Debug.Assert(this.dalamudAtlas is not null, "this.dalamudAtlas is not null"); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - { - if (this.dalamudAtlas.BuildTask.Exception != null) - { - // TODO: Can we do something more user-friendly here? Unload instead? - Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); - Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); - } - - return; - } - - this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; + if (this.RenderDalamudCheckAndInitialize(swapChainNative) is { } activeScene) + this.RenderDalamudDraw(activeScene); } - private nint AsReShadeAddonResizeBuffersDetour( - nint swapChain, + private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour( + IDXGISwapChain* swapChain, uint bufferCount, uint width, uint height, - uint newFormat, + DXGI_FORMAT newFormat, uint swapChainFlags) { // Hooked vtbl instead of registering ReShade event. This check is correct. if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); this.ResizeBuffers?.InvokeSafely(); - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 0491fed1e..45cba5c76 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -97,9 +97,10 @@ internal partial class InterfaceManager : IInternalDisposableService private RawDX11Scene? scene; private Hook? setCursorHook; - private Hook? dxgiPresentHook; - private Hook? resizeBuffersHook; - private ObjectVTableHook>? swapChainHook; + private Hook? reShadeDxgiSwapChainPresentHook; + private Hook? dxgiSwapChainPresentHook; + private Hook? dxgiSwapChainResizeBuffersHook; + private ObjectVTableHook>? dxgiSwapChainHook; private ReShadeAddonInterface? reShadeAddonInterface; private IFontAtlas? dalamudAtlas; @@ -115,12 +116,6 @@ internal partial class InterfaceManager : IInternalDisposableService { } - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr DxgiPresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); @@ -231,7 +226,7 @@ internal partial class InterfaceManager : IInternalDisposableService /// public bool IsDispatchingEvents { get; set; } = true; - /// Gets a value indicating whether the main thread is executing . + /// Gets a value indicating whether the main thread is executing . /// This still will be true even when queried off the main thread. public bool IsMainThreadInPresent { get; private set; } @@ -265,7 +260,7 @@ internal partial class InterfaceManager : IInternalDisposableService /// public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; - /// Gets the number of calls to so far. + /// Gets the number of calls to so far. /// /// The value increases even when Dalamud is hidden via "/xlui hide". /// does not. @@ -312,9 +307,10 @@ internal partial class InterfaceManager : IInternalDisposableService { this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); - Interlocked.Exchange(ref this.dxgiPresentHook, null)?.Dispose(); - Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); - Interlocked.Exchange(ref this.swapChainHook, null)?.Dispose(); + Interlocked.Exchange(ref this.dxgiSwapChainPresentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.reShadeDxgiSwapChainPresentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.dxgiSwapChainResizeBuffersHook, null)?.Dispose(); + Interlocked.Exchange(ref this.dxgiSwapChainHook, null)?.Dispose(); Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose(); } } @@ -497,31 +493,72 @@ internal partial class InterfaceManager : IInternalDisposableService return im; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RenderImGui(RawDX11Scene scene) + /// Checks if the provided swap chain is the target that Dalamud should draw its interface onto, + /// and initializes ImGui for drawing. + /// The swap chain to test and initialize ImGui with if conditions are met. + /// An initialized instance of , or null if + /// is not the main swap chain. + private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain) { - var conf = Service.Get(); + if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) + return null; + + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + + var activeScene = this.scene ?? this.InitScene(swapChain); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + { + if (this.dalamudAtlas.BuildTask.Exception != null) + { + // TODO: Can we do something more user-friendly here? Unload instead? + Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); + Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); + } + + return null; + } + + return activeScene; + } + + /// Draws Dalamud to the given scene representing the ImGui context. + /// The scene to draw to. + private void RenderDalamudDraw(RawDX11Scene activeScene) + { + this.CumulativePresentCalls++; + this.IsMainThreadInPresent = true; + + while (this.runBeforeImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); // Process information needed by ImGuiHelpers each frame. ImGuiHelpers.NewFrame(); // Enable viewports if there are no issues. - if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + var viewportsEnable = this.dalamudConfiguration.IsDisableViewport || + activeScene.SwapChain.IsFullScreen || + ImGui.GetPlatformIO().Monitors.Size == 1; + if (viewportsEnable) ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; else ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - scene.Render(); + // Call drawing functions, which in turn will call Draw event. + activeScene.Render(); + + this.PostImGuiRender(); + this.IsMainThreadInPresent = false; } - private void InitScene(IntPtr swapChain) + private unsafe RawDX11Scene InitScene(IDXGISwapChain* swapChain) { RawDX11Scene newScene; using (Timings.Start("IM Scene Init")) { try { - newScene = new RawDX11Scene(swapChain); + newScene = new RawDX11Scene((nint)swapChain); } catch (DllNotFoundException ex) { @@ -547,7 +584,7 @@ internal partial class InterfaceManager : IInternalDisposableService Environment.Exit(-1); // Doesn't reach here, but to make the compiler not complain - return; + throw new InvalidOperationException(); } var startInfo = Service.Get().StartInfo; @@ -638,6 +675,7 @@ internal partial class InterfaceManager : IInternalDisposableService Service.Provide(new(this)); this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + return newScene; } private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) @@ -805,39 +843,84 @@ internal partial class InterfaceManager : IInternalDisposableService }); } - Log.Verbose("===== S W A P C H A I N ====="); - if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade) + Log.Information("===== S W A P C H A I N ====="); + var sb = new StringBuilder(); + foreach (var m in ReShadeAddonInterface.AllReShadeModules) { - if (SwapChainHelper.UnwrapReShade()) - Log.Information("Unwrapped ReShade"); + sb.Clear(); + sb.Append("ReShade detected: "); + sb.Append(m.FileName).Append('('); + sb.Append(m.FileVersionInfo.OriginalFilename); + sb.Append("; ").Append(m.FileVersionInfo.ProductName); + sb.Append("; ").Append(m.FileVersionInfo.ProductVersion); + sb.Append("; ").Append(m.FileVersionInfo.FileDescription); + sb.Append("; ").Append(m.FileVersionInfo.FileVersion); + sb.Append($"@ 0x{m.BaseAddress:X}"); + if (!ReferenceEquals(m, ReShadeAddonInterface.ReShadeModule)) + sb.Append(" [ignored by Dalamud]"); + Log.Information(sb.ToString()); } - ResizeBuffersDelegate? resizeBuffersDelegate = null; - DxgiPresentDelegate? dxgiPresentDelegate = null; - if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon) - { - if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) - { - resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour; + if (ReShadeAddonInterface.AllReShadeModules.Length > 1) + Log.Warning("Multiple ReShade dlls are detected."); - Log.Information( - "Registered as a ReShade({Name}: 0x{Addr:X}) addon", - ReShadeAddonInterface.ReShadeModule!.FileName, - ReShadeAddonInterface.ReShadeModule!.BaseAddress); + ResizeBuffersDelegate dxgiSwapChainResizeBuffersDelegate; + ReShadeDxgiSwapChainPresentDelegate? reShadeDxgiSwapChainPresentDelegate = null; + DxgiSwapChainPresentDelegate? dxgiSwapChainPresentDelegate = null; + nint pfnReShadeDxgiSwapChainPresent = 0; + switch (this.dalamudConfiguration.ReShadeHandlingMode) + { + // This is the only mode honored when SwapChainHookMode is set to VTable. + case ReShadeHandlingMode.UnwrapReShade when ReShadeAddonInterface.ReShadeModule is not null: + if (SwapChainHelper.UnwrapReShade()) + Log.Information("Unwrapped ReShade"); + else + Log.Warning("Could not unwrap ReShade"); + goto default; + + // Do no special ReShade handling. + // If ReShade is not found or SwapChainHookMode is set to VTable, also do nothing special. + case ReShadeHandlingMode.None: + case var _ when ReShadeAddonInterface.ReShadeModule is null: + case var _ when this.dalamudConfiguration.SwapChainHookMode == SwapChainHelper.HookMode.VTable: + default: + dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour; + dxgiSwapChainPresentDelegate = this.DxgiSwapChainPresentDetour; + break; + + // Register Dalamud as a ReShade addon. + case ReShadeHandlingMode.ReShadeAddon: + if (!ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) + { + Log.Warning("Could not register as ReShade addon"); + goto default; + } + + Log.Information("Registered as a ReShade addon"); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent; - } - else - { - Log.Information("Could not register as ReShade addon"); - } - } - if (resizeBuffersDelegate is null) - { - resizeBuffersDelegate = this.AsHookResizeBuffersDetour; - dxgiPresentDelegate = this.PresentDetour; + dxgiSwapChainResizeBuffersDelegate = this.AsReShadeAddonDxgiSwapChainResizeBuffersDetour; + break; + + // Hook ReShade's DXGISwapChain::on_present. This is the legacy and the default option. + case ReShadeHandlingMode.Default: + case ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent: + pfnReShadeDxgiSwapChainPresent = ReShadeAddonInterface.FindReShadeDxgiSwapChainOnPresent(); + + if (pfnReShadeDxgiSwapChainPresent == 0) + { + Log.Warning("ReShade::DXGISwapChain::on_present could not be found"); + goto default; + } + + Log.Information( + "Found ReShade::DXGISwapChain::on_present at {addr}", + Util.DescribeAddress(pfnReShadeDxgiSwapChainPresent)); + reShadeDxgiSwapChainPresentDelegate = this.ReShadeDxgiSwapChainOnPresentDetour; + dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour; + break; } switch (this.dalamudConfiguration.SwapChainHookMode) @@ -846,16 +929,31 @@ internal partial class InterfaceManager : IInternalDisposableService default: { Log.Information("Hooking using bytecode..."); - this.resizeBuffersHook = Hook.FromAddress( + this.dxgiSwapChainResizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, - resizeBuffersDelegate); + dxgiSwapChainResizeBuffersDelegate); + Log.Information( + "Hooked IDXGISwapChain::ResizeBuffers using bytecode: {addr}", + Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address)); - if (dxgiPresentDelegate is not null) + if (dxgiSwapChainPresentDelegate is not null) { - this.dxgiPresentHook = Hook.FromAddress( + this.dxgiSwapChainPresentHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, - dxgiPresentDelegate); - Log.Information("Hooked present using bytecode"); + dxgiSwapChainPresentDelegate); + Log.Information( + "Hooked IDXGISwapChain::Present using bytecode: {addr}", + Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address)); + } + + if (reShadeDxgiSwapChainPresentDelegate is not null && pfnReShadeDxgiSwapChainPresent != 0) + { + this.reShadeDxgiSwapChainPresentHook = Hook.FromAddress( + pfnReShadeDxgiSwapChainPresent, + reShadeDxgiSwapChainPresentDelegate); + Log.Information( + "Hooked ReShade::DXGISwapChain::on_present using bytecode: {addr}", + Util.DescribeAddress(this.reShadeDxgiSwapChainPresentHook.Address)); } break; @@ -864,30 +962,38 @@ internal partial class InterfaceManager : IInternalDisposableService case SwapChainHelper.HookMode.VTable: { Log.Information("Hooking using VTable..."); - this.swapChainHook = new(SwapChainHelper.GameDeviceSwapChain); - this.resizeBuffersHook = this.swapChainHook.CreateHook( + this.dxgiSwapChainHook = new(SwapChainHelper.GameDeviceSwapChain); + this.dxgiSwapChainResizeBuffersHook = this.dxgiSwapChainHook.CreateHook( nameof(IDXGISwapChain.ResizeBuffers), - resizeBuffersDelegate); + dxgiSwapChainResizeBuffersDelegate); + Log.Information( + "Hooked IDXGISwapChain::ResizeBuffers using VTable: {addr}", + Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address)); - if (dxgiPresentDelegate is not null) + if (dxgiSwapChainPresentDelegate is not null) { - this.dxgiPresentHook = this.swapChainHook.CreateHook( + this.dxgiSwapChainPresentHook = this.dxgiSwapChainHook.CreateHook( nameof(IDXGISwapChain.Present), - dxgiPresentDelegate); - Log.Information("Hooked present using VTable"); + dxgiSwapChainPresentDelegate); + Log.Information( + "Hooked IDXGISwapChain::Present using VTable: {addr}", + Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address)); } + Log.Information( + "Detouring vtable at {addr}: {prev} to {new}", + Util.DescribeAddress(this.dxgiSwapChainHook.Address), + Util.DescribeAddress(this.dxgiSwapChainHook.OriginalVTableAddress), + Util.DescribeAddress(this.dxgiSwapChainHook.OverridenVTableAddress)); break; } } - Log.Information($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); - Log.Information($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}"); - this.setCursorHook.Enable(); - this.resizeBuffersHook.Enable(); - this.dxgiPresentHook?.Enable(); - this.swapChainHook?.Enable(); + this.reShadeDxgiSwapChainPresentHook?.Enable(); + this.dxgiSwapChainResizeBuffersHook.Enable(); + this.dxgiSwapChainPresentHook?.Enable(); + this.dxgiSwapChainHook?.Enable(); } private IntPtr SetCursorDetour(IntPtr hCursor) diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index b3add07e7..d8d210076 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -24,6 +26,7 @@ internal sealed unsafe partial class ReShadeAddonInterface static ReShadeAddonInterface() { + var modules = new List(); foreach (var m in Process.GetCurrentProcess().Modules.Cast()) { ExportsStruct e; @@ -33,27 +36,31 @@ internal sealed unsafe partial class ReShadeAddonInterface !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent)) continue; - try + modules.Add(m); + if (modules.Count == 1) { - var signerName = GetSignatureSignerNameWithoutVerification(m.FileName); - ReShadeIsSignedByReShade = signerName == "ReShade"; - Log.Information( - "ReShade DLL is signed by {signerName}. {vn}={v}", - signerName, - nameof(ReShadeIsSignedByReShade), - ReShadeIsSignedByReShade); - } - catch (Exception ex) - { - Log.Information(ex, "ReShade DLL did not had a valid signature."); - } + try + { + var signerName = GetSignatureSignerNameWithoutVerification(m.FileName); + ReShadeIsSignedByReShade = signerName == "ReShade"; + Log.Information( + "ReShade DLL is signed by {signerName}. {vn}={v}", + signerName, + nameof(ReShadeIsSignedByReShade), + ReShadeIsSignedByReShade); + } + catch (Exception ex) + { + Log.Information(ex, "ReShade DLL did not had a valid signature."); + } - ReShadeModule = m; - Exports = e; - - return; + ReShadeModule = m; + Exports = e; + } } + AllReShadeModules = [..modules]; + return; bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) @@ -68,10 +75,30 @@ internal sealed unsafe partial class ReShadeAddonInterface /// Gets the active ReShade module. public static ProcessModule? ReShadeModule { get; private set; } + /// Gets all the detected ReShade modules. + public static ImmutableArray AllReShadeModules { get; private set; } + /// Gets a value indicating whether the loaded ReShade has signatures. /// ReShade without addon support is signed, but may not pass signature verification. public static bool ReShadeIsSignedByReShade { get; private set; } + /// Finds the address of DXGISwapChain::on_present in . + /// Address of the function, or 0 if not found. + public static nint FindReShadeDxgiSwapChainOnPresent() + { + if (ReShadeModule is not { } rsm) + return 0; + + var m = new ReadOnlySpan((void*)rsm.BaseAddress, rsm.ModuleMemorySize); + + // Signature validated against 5.0.0 to 6.2.0 + var i = m.IndexOf(new byte[] { 0xCC, 0xF6, 0xC2, 0x01, 0x0F, 0x85 }); + if (i == -1) + return 0; + + return rsm.BaseAddress + i + 1; + } + /// Gets the name of the signer of a file that has a certificate embedded within, without verifying if the /// file has a valid signature. /// Path to the file. diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs index e4561ca46..6ffba3878 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs @@ -3,11 +3,18 @@ namespace Dalamud.Interface.Internal.ReShadeHandling; /// Available handling modes for working with ReShade. internal enum ReShadeHandlingMode { + /// Use the default method, whatever it is for the current Dalamud version. + Default = 0, + + /// Unwrap ReShade from the swap chain obtained from the game. + UnwrapReShade, + /// Register as a ReShade addon, and draw on reshade_overlay event. ReShadeAddon, - /// Unwraps ReShade from the swap chain obtained from the game. - UnwrapReShade, + /// Hook DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in + /// dxgi_swapchain.cpp. + HookReShadeDxgiSwapChainOnPresent, /// Do not do anything special about it. ReShade will process Dalamud rendered stuff. None = -1, diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 3f6ec783e..6da89004b 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -79,16 +79,28 @@ public class SettingsTabExperimental : SettingsTab "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."), c => c.ReShadeHandlingMode, (v, c) => c.ReShadeHandlingMode = v, - fallbackValue: ReShadeHandlingMode.ReShadeAddon) + fallbackValue: ReShadeHandlingMode.ReShadeAddon, + warning: static rshm => + rshm is ReShadeHandlingMode.UnwrapReShade or ReShadeHandlingMode.None + ? null + : Loc.Localize( + "DalamudSettingsReShadeHandlingModeIgnoredVTableHookMode", + "Current option will be ignored and no special ReShade handling will be done, because SwapChain vtable hook mode is set.")) { FriendlyEnumNameGetter = x => x switch { - ReShadeHandlingMode.ReShadeAddon => Loc.Localize( - "DalamudSettingsReShadeHandlingModeReShadeAddon", - "ReShade addon"), + ReShadeHandlingMode.Default => Loc.Localize( + "DalamudSettingsReShadeHandlingModeDefault", + "Default"), ReShadeHandlingMode.UnwrapReShade => Loc.Localize( "DalamudSettingsReShadeHandlingModeUnwrapReShade", "Unwrap ReShade"), + ReShadeHandlingMode.ReShadeAddon => Loc.Localize( + "DalamudSettingsReShadeHandlingModeReShadeAddon", + "ReShade addon"), + ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize( + "DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresent", + "Hook ReShade DXGISwapChain::OnPresent"), ReShadeHandlingMode.None => Loc.Localize( "DalamudSettingsReShadeHandlingModeNone", "Do not handle"), @@ -96,12 +108,18 @@ public class SettingsTabExperimental : SettingsTab }, FriendlyEnumDescriptionGetter = x => x switch { - ReShadeHandlingMode.ReShadeAddon => Loc.Localize( - "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", - "Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), + ReShadeHandlingMode.Default => Loc.Localize( + "DalamudSettingsReShadeHandlingModeDefaultDescription", + "Dalamud will use the developer-recommend settings. If nothing's wrong, keeping this option is recommended."), ReShadeHandlingMode.UnwrapReShade => Loc.Localize( "DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription", "Dalamud will exclude itself from all ReShade handling. Multi-monitor windows should work fine with this mode, but it may not be supported and crash in future ReShade versions."), + ReShadeHandlingMode.ReShadeAddon => Loc.Localize( + "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", + "Dalamud will register itself as a ReShade addon. Multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), + ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize( + "DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresentDescription", + "Dalamud will use an unsupported method of detouring an internal ReShade function. Multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), ReShadeHandlingMode.None => Loc.Localize( "DalamudSettingsReShadeHandlingModeNoneDescription", "No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."), From 1f34db5039e532833aa740376addf11e9c25104a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 25 Jul 2024 19:05:59 +0900 Subject: [PATCH 66/79] Do not handle DXGI_PRESENT_TEST --- Dalamud/Interface/Internal/InterfaceManager.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 45cba5c76..79fe41046 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -498,8 +498,14 @@ internal partial class InterfaceManager : IInternalDisposableService /// The swap chain to test and initialize ImGui with if conditions are met. /// An initialized instance of , or null if /// is not the main swap chain. - private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain) + private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain, uint flags) { + // Quoting ReShade dxgi_swapchain.cpp DXGISwapChain::on_present: + // > Some D3D11 games test presentation for timing and composition purposes + // > These calls are not rendering related, but rather a status request for the D3D runtime and as such should be ignored + if ((flags & DXGI.DXGI_PRESENT_TEST) != 0) + return null; + if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) return null; From ef64a67d105dc18a2db38efa7c9e114012edc58a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 25 Jul 2024 20:19:22 +0900 Subject: [PATCH 67/79] fix --- Dalamud/Interface/Internal/InterfaceManager.AsHook.cs | 4 ++-- Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs | 4 +--- Dalamud/Interface/Internal/InterfaceManager.cs | 1 + 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs index 3dc8b30cd..2cc948270 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs @@ -86,7 +86,7 @@ internal unsafe partial class InterfaceManager // Call this first to draw Dalamud over ReShade. this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams); - if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain()) is { } activeScene) + if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain(), flags) is { } activeScene) this.RenderDalamudDraw(activeScene); // Upstream call to system IDXGISwapChain::Present will be called by ReShade. @@ -96,7 +96,7 @@ internal unsafe partial class InterfaceManager { Debug.Assert(this.dxgiSwapChainPresentHook is not null, "this.dxgiSwapChainPresentHook is not null"); - if (this.RenderDalamudCheckAndInitialize(swapChain) is { } activeScene) + if (this.RenderDalamudCheckAndInitialize(swapChain, flags) is { } activeScene) this.RenderDalamudDraw(activeScene); return this.dxgiSwapChainPresentHook!.Original(swapChain, syncInterval, flags); diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index 817a935ee..c5131d3a0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Utility; @@ -44,7 +42,7 @@ internal unsafe partial class InterfaceManager { var swapChainNative = swapChain.GetNative(); - if (this.RenderDalamudCheckAndInitialize(swapChainNative) is { } activeScene) + if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activeScene) this.RenderDalamudDraw(activeScene); } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 79fe41046..10703ad8d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -496,6 +496,7 @@ internal partial class InterfaceManager : IInternalDisposableService /// Checks if the provided swap chain is the target that Dalamud should draw its interface onto, /// and initializes ImGui for drawing. /// The swap chain to test and initialize ImGui with if conditions are met. + /// Flags passed to . /// An initialized instance of , or null if /// is not the main swap chain. private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain, uint flags) From d8861ec7daaec599de09aedb94abb95a2fc64ec0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 25 Jul 2024 21:07:49 +0900 Subject: [PATCH 68/79] write code --- .../Internal/DalamudConfiguration.cs | 2 +- .../InterfaceManager.AsReShadeAddon.cs | 8 +++ .../Interface/Internal/InterfaceManager.cs | 10 ++-- .../ReShadeHandling/ReShadeAddonInterface.cs | 14 +++++- .../ReShadeHandling/ReShadeHandlingMode.cs | 9 +++- .../Settings/Tabs/SettingsTabExperimental.cs | 49 ++++--------------- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index d5f1299fd..45b49cc8a 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -444,7 +444,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool WindowIsImmersive { get; set; } = false; /// Gets or sets the mode specifying how to handle ReShade. - public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.ReShadeAddon; + public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.Default; /// Gets or sets the swap chain hook mode. public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode; diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index c5131d3a0..73c0a4d15 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -46,6 +46,14 @@ internal unsafe partial class InterfaceManager this.RenderDalamudDraw(activeScene); } + private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime) + { + var swapChainNative = runtime.GetNative(); + + if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activeScene) + this.RenderDalamudDraw(activeScene); + } + private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour( IDXGISwapChain* swapChain, uint bufferCount, diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 10703ad8d..313d0ae94 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -878,6 +878,7 @@ internal partial class InterfaceManager : IInternalDisposableService switch (this.dalamudConfiguration.ReShadeHandlingMode) { // This is the only mode honored when SwapChainHookMode is set to VTable. + case ReShadeHandlingMode.Default: case ReShadeHandlingMode.UnwrapReShade when ReShadeAddonInterface.ReShadeModule is not null: if (SwapChainHelper.UnwrapReShade()) Log.Information("Unwrapped ReShade"); @@ -896,7 +897,8 @@ internal partial class InterfaceManager : IInternalDisposableService break; // Register Dalamud as a ReShade addon. - case ReShadeHandlingMode.ReShadeAddon: + case ReShadeHandlingMode.ReShadeAddonPresent: + case ReShadeHandlingMode.ReShadeAddonReShadeOverlay: if (!ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) { Log.Warning("Could not register as ReShade addon"); @@ -906,13 +908,15 @@ internal partial class InterfaceManager : IInternalDisposableService Log.Information("Registered as a ReShade addon"); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; - this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent; + if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddonPresent) + this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent; + else + this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay; dxgiSwapChainResizeBuffersDelegate = this.AsReShadeAddonDxgiSwapChainResizeBuffersDetour; break; // Hook ReShade's DXGISwapChain::on_present. This is the legacy and the default option. - case ReShadeHandlingMode.Default: case ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent: pfnReShadeDxgiSwapChainPresent = ReShadeAddonInterface.FindReShadeDxgiSwapChainOnPresent(); diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs index cb4c04afa..9062a6a1b 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.cs @@ -22,6 +22,7 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable private readonly Hook addonModuleResolverHook; private readonly DelegateStorage presentDelegate; + private readonly DelegateStorage reShadeOverlayDelegate; private readonly DelegateStorage initSwapChainDelegate; private readonly DelegateStorage destroySwapChainDelegate; @@ -61,6 +62,9 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable pSourceRect is null ? default : new(pSourceRect, 1), pDestRect is null ? default : new(pDestRect, 1), new(pDirtyRects, (int)dirtyRectCount)))); + Exports.ReShadeRegisterEvent( + AddonEvent.ReShadeOverlay, + this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt))); Exports.ReShadeRegisterEvent( AddonEvent.InitSwapChain, this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt))); @@ -104,6 +108,10 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable ReadOnlySpan destRect, ReadOnlySpan dirtyRects); + /// Delegate for . + /// Reference to the ReShade runtime. + public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime); + /// Delegate for . /// Reference to the ReShade SwapChain wrapper. public delegate void ReShadeInitSwapChain(ref ApiObject swapChain); @@ -132,6 +140,9 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable /// Called on . public event PresentDelegate? Present; + /// Called on . + public event ReShadeOverlayDelegate? ReShadeOverlay; + /// Called on . public event ReShadeInitSwapChain? InitSwapChain; @@ -181,7 +192,8 @@ internal sealed unsafe partial class ReShadeAddonInterface : IDisposable return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule); if (lpModuleName == this.initSwapChainDelegate || lpModuleName == this.destroySwapChainDelegate || - lpModuleName == this.presentDelegate) + lpModuleName == this.presentDelegate || + lpModuleName == this.reShadeOverlayDelegate) { *phModule = this.hDalamudModule; return BOOL.TRUE; diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs index 6ffba3878..b02fd630d 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs @@ -9,8 +9,13 @@ internal enum ReShadeHandlingMode /// Unwrap ReShade from the swap chain obtained from the game. UnwrapReShade, - /// Register as a ReShade addon, and draw on reshade_overlay event. - ReShadeAddon, + /// Register as a ReShade addon, and draw on event. + /// + ReShadeAddonPresent, + + /// Register as a ReShade addon, and draw on + /// event. + ReShadeAddonReShadeOverlay, /// Hook DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in /// dxgi_swapchain.cpp. diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 6da89004b..d3298f61a 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -79,50 +79,21 @@ public class SettingsTabExperimental : SettingsTab "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."), c => c.ReShadeHandlingMode, (v, c) => c.ReShadeHandlingMode = v, - fallbackValue: ReShadeHandlingMode.ReShadeAddon, + fallbackValue: ReShadeHandlingMode.Default, warning: static rshm => - rshm is ReShadeHandlingMode.UnwrapReShade or ReShadeHandlingMode.None + rshm is ReShadeHandlingMode.UnwrapReShade or ReShadeHandlingMode.None || + Service.Get().SwapChainHookMode == SwapChainHelper.HookMode.ByteCode ? null - : Loc.Localize( - "DalamudSettingsReShadeHandlingModeIgnoredVTableHookMode", - "Current option will be ignored and no special ReShade handling will be done, because SwapChain vtable hook mode is set.")) + : "Current option will be ignored and no special ReShade handling will be done, because SwapChain vtable hook mode is set.") { FriendlyEnumNameGetter = x => x switch { - ReShadeHandlingMode.Default => Loc.Localize( - "DalamudSettingsReShadeHandlingModeDefault", - "Default"), - ReShadeHandlingMode.UnwrapReShade => Loc.Localize( - "DalamudSettingsReShadeHandlingModeUnwrapReShade", - "Unwrap ReShade"), - ReShadeHandlingMode.ReShadeAddon => Loc.Localize( - "DalamudSettingsReShadeHandlingModeReShadeAddon", - "ReShade addon"), - ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize( - "DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresent", - "Hook ReShade DXGISwapChain::OnPresent"), - ReShadeHandlingMode.None => Loc.Localize( - "DalamudSettingsReShadeHandlingModeNone", - "Do not handle"), - _ => "", - }, - FriendlyEnumDescriptionGetter = x => x switch - { - ReShadeHandlingMode.Default => Loc.Localize( - "DalamudSettingsReShadeHandlingModeDefaultDescription", - "Dalamud will use the developer-recommend settings. If nothing's wrong, keeping this option is recommended."), - ReShadeHandlingMode.UnwrapReShade => Loc.Localize( - "DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription", - "Dalamud will exclude itself from all ReShade handling. Multi-monitor windows should work fine with this mode, but it may not be supported and crash in future ReShade versions."), - ReShadeHandlingMode.ReShadeAddon => Loc.Localize( - "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", - "Dalamud will register itself as a ReShade addon. Multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), - ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize( - "DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresentDescription", - "Dalamud will use an unsupported method of detouring an internal ReShade function. Multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), - ReShadeHandlingMode.None => Loc.Localize( - "DalamudSettingsReShadeHandlingModeNoneDescription", - "No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."), + ReShadeHandlingMode.Default => "Default", + ReShadeHandlingMode.UnwrapReShade => "Unwrap", + ReShadeHandlingMode.ReShadeAddonPresent => "ReShade Addon (present)", + ReShadeHandlingMode.ReShadeAddonReShadeOverlay => "ReShade Addon (reshade_overlay)", + ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => "Hook ReShade::DXGISwapChain::OnPresent", + ReShadeHandlingMode.None => "Do not handle", _ => "", }, }, From f0777106d3703e19d02dba61235a09116d32df9e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 25 Jul 2024 21:39:07 +0900 Subject: [PATCH 69/79] Render reshade over dalamud on hook mode --- Dalamud/Interface/Internal/InterfaceManager.AsHook.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs index 2cc948270..3ad25f97d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs @@ -83,12 +83,11 @@ internal unsafe partial class InterfaceManager this.reShadeDxgiSwapChainPresentHook is not null, "this.reShadeDxgiSwapChainPresentHook is not null"); - // Call this first to draw Dalamud over ReShade. - this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams); - if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain(), flags) is { } activeScene) this.RenderDalamudDraw(activeScene); + this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams); + // Upstream call to system IDXGISwapChain::Present will be called by ReShade. } From 32607ed6ba310c21ad0a9f7a4a2250c9c4ff17fa Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 25 Jul 2024 12:22:54 -0700 Subject: [PATCH 70/79] fix/ci: don't reference a non-existent env var (#1975) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d3d225293..2c564cc81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: foreach ($file in $FILES_TO_VALIDATE) { $testout = "" Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ===" - apicompat -l "left\${file}" -r "right\${file}" --noWarn "$(NoWarn);CP0006" | Tee-Object -Variable testout + apicompat -l "left\${file}" -r "right\${file}" --noWarn "CP0006" | Tee-Object -Variable testout Write-Output "::endgroup::" if ($testout -ne "APICompat ran successfully without finding any breaking changes.") { Write-Output "::error::${file} did not pass. Please review it for problems." From 79971fc8d69f7db640fd1d3b5ae52d3a905820b2 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 25 Jul 2024 12:30:36 -0700 Subject: [PATCH 71/79] fix: Use AssemblyVersion for UserAgent header (#1974) - Fixes a bug with local builds not being able to make HTTP requests --- Dalamud/Networking/Http/HappyHttpClient.cs | 2 +- Dalamud/Plugin/Internal/Types/PluginRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index 4db44f1cf..aeed98695 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -36,7 +36,7 @@ internal class HappyHttpClient : IInternalDisposableService { UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.GetScmVersion()), + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), }, }, }; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 9caa40f2e..2f63070c3 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -59,7 +59,7 @@ internal class PluginRepository }, UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.GetScmVersion()), + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), }, }, }; From 2265b389b1916eae0847da764be63d9ab88fc6d9 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 25 Jul 2024 21:36:53 +0200 Subject: [PATCH 72/79] config: rename ReShade handling mode property to force new default --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 45b49cc8a..5b49f5c72 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -444,6 +444,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool WindowIsImmersive { get; set; } = false; /// Gets or sets the mode specifying how to handle ReShade. + [JsonProperty("ReShadeHandlingModeV2")] public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.Default; /// Gets or sets the swap chain hook mode. From 9643f32a492cbafc6a7cd3a82cfe4044618a2a15 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:39:32 +0200 Subject: [PATCH 73/79] [master] Update ClientStructs (#1961) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index a969dec3a..ced63e0ad 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a969dec3a5a05fd6d5070452c74720cf38af6567 +Subproject commit ced63e0ad17eee63d23405ab1c35aab59487035e From b523acd5009785792855dc8ce8a1e60706d8c523 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Fri, 26 Jul 2024 04:39:54 +0900 Subject: [PATCH 74/79] Use origins when calculating scaled node position (#1972) --- Dalamud/Interface/Internal/UiDebug.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 9d8b3b764..97eec1ee1 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -563,11 +563,13 @@ internal unsafe class UiDebug private Vector2 GetNodePosition(AtkResNode* node) { var pos = new Vector2(node->X, node->Y); + pos -= new Vector2(node->OriginX * (node->ScaleX - 1), node->OriginY * (node->ScaleY - 1)); var par = node->ParentNode; while (par != null) { pos *= new Vector2(par->ScaleX, par->ScaleY); pos += new Vector2(par->X, par->Y); + pos -= new Vector2(par->OriginX * (par->ScaleX - 1), par->OriginY * (par->ScaleY - 1)); par = par->ParentNode; } From 7b7d5ad9c711f4ee37467122dd1694cc2c183763 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 26 Jul 2024 04:39:49 +0900 Subject: [PATCH 75/79] Fix case order --- Dalamud/Interface/Internal/InterfaceManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 313d0ae94..aeff893a3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -877,9 +877,13 @@ internal partial class InterfaceManager : IInternalDisposableService nint pfnReShadeDxgiSwapChainPresent = 0; switch (this.dalamudConfiguration.ReShadeHandlingMode) { + // If ReShade is not found, do no special handling. + case var _ when ReShadeAddonInterface.ReShadeModule is null: + goto default; + // This is the only mode honored when SwapChainHookMode is set to VTable. case ReShadeHandlingMode.Default: - case ReShadeHandlingMode.UnwrapReShade when ReShadeAddonInterface.ReShadeModule is not null: + case ReShadeHandlingMode.UnwrapReShade: if (SwapChainHelper.UnwrapReShade()) Log.Information("Unwrapped ReShade"); else @@ -887,9 +891,8 @@ internal partial class InterfaceManager : IInternalDisposableService goto default; // Do no special ReShade handling. - // If ReShade is not found or SwapChainHookMode is set to VTable, also do nothing special. + // If SwapChainHookMode is set to VTable, do no special handling. case ReShadeHandlingMode.None: - case var _ when ReShadeAddonInterface.ReShadeModule is null: case var _ when this.dalamudConfiguration.SwapChainHookMode == SwapChainHelper.HookMode.VTable: default: dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour; From 9a259100b4fbffc28455c18819f83e736d497905 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:02:29 -0400 Subject: [PATCH 76/79] Better AddonLifecycle AddonEvent docs (#1964) Co-authored-by: Kaz Wolfe Co-authored-by: Haselnussbomber --- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 81 +++++++++++++++++----- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 7cbc93eb2..91b9dd51f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -1,4 +1,8 @@ -namespace Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon.Lifecycle; /// /// Enumeration for available AddonLifecycle events. @@ -6,67 +10,112 @@ public enum AddonEvent { /// - /// Event that is fired before an addon begins it's setup process. + /// An event that is fired prior to an addon being setup with its implementation of + /// . This event is useful for modifying the initial data contained within + /// prior to the addon being created. /// + /// PreSetup, - + /// - /// Event that is fired after an addon has completed it's setup process. + /// An event that is fired after an addon has finished its initial setup. This event is particularly useful for + /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data + /// placed in the AtkValues by the game during the setup process. + /// See for more information. /// PostSetup, /// - /// Event that is fired before an addon begins update. + /// An event that is fired before an addon begins its update cycle via . This event + /// is fired every frame that an addon is loaded, regardless of visibility. /// + /// PreUpdate, /// - /// Event that is fired after an addon has completed update. + /// An event that is fired after an addon has finished its update. + /// See for more information. /// PostUpdate, /// - /// Event that is fired before an addon begins draw. + /// An event that is fired before an addon begins drawing to screen via . Unlike + /// , this event is only fired if an addon is visible or otherwise drawing to screen. /// + /// PreDraw, /// - /// Event that is fired after an addon has completed draw. + /// An event that is fired after an addon has finished its draw to screen. + /// See for more information. /// PostDraw, /// - /// Event that is fired before an addon is finalized. + /// An event that is fired immediately before an addon is finalized via and + /// destroyed. After this event, the addon will destruct its UI node data as well as free any allocated memory. + /// This event can be used for cleanup and tracking tasks. /// + /// + /// This event is NOT fired when the addon is being hidden, but tends to be fired when it's being properly + /// closed. + ///
+ /// As this is part of the destruction process for an addon, this event does not have an associated Post event. + ///
+ /// PreFinalize, /// - /// Event that is fired before an addon begins a requested update. + /// An event that is fired before a call to is made in response to a + /// change in the subscribed or + /// backing this addon. This generally occurs in response to + /// receiving data from the game server, but can happen in other cases as well. This event is useful for modifying + /// the data received before it's passed to the UI for display. Contrast to which tends to + /// be in response to client-driven interactions. /// + /// + /// + /// + /// A developer would use this event to intercept free company information after it's received from the server, but + /// before it's displayed to the user. This would allow the developer to add user-driven notes or other information + /// to the Free Company's overview. + /// PreRequestedUpdate, /// - /// Event that is fired after an addon finishes a requested update. + /// An event that is fired after an addon has finished processing an ArrayData update. + /// See for more information. /// PostRequestedUpdate, /// - /// Event that is fired before an addon begins a refresh. - /// + /// An event that is fired before an addon calls its method. Refreshes are + /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to + /// update the AtkValues present in this addon. Contrast to which is called + /// in response to ArrayData updates.
+ /// + /// PreRefresh, /// - /// Event that is fired after an addon has finished a refresh. + /// An event that is fired after an addon has finished its refresh. + /// See for more information. /// PostRefresh, /// - /// Event that is fired before an addon begins processing an event. + /// An event that is fired before an addon begins processing a user-driven event via + /// , such as mousing over an element or clicking a button. This event + /// is only valid for addons that actually override the ReceiveEvent method of the underlying + /// AtkEventListener. /// + /// + /// PreReceiveEvent, /// - /// Event that is fired after an addon has processed an event. + /// An event that is fired after an addon finishes calling its method. + /// See for more information. /// PostReceiveEvent, } From 90c9705ea2ac99ec763eba314b2890ee8af79254 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:27:52 +0200 Subject: [PATCH 77/79] Update ClientStructs (#1976) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index ced63e0ad..731e3ab00 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ced63e0ad17eee63d23405ab1c35aab59487035e +Subproject commit 731e3ab0006ce56c4fe789aee148bc967965b914 From eec755b1f33e90428bb0cbe125fd497375596bb4 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 27 Jul 2024 13:36:26 +0200 Subject: [PATCH 78/79] chat: print SCM version instead of git hash when logging in --- Dalamud/Game/ChatHandlers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index cd0cf9097..a41c6ff7a 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -179,7 +179,7 @@ internal class ChatHandlers : IServiceType if (this.configuration.PrintDalamudWelcomeMsg) { - chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetGitHash()) + chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); } From b1fe875c6eefb2834d2e478164e1ad5c101ec41a Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 27 Jul 2024 13:37:24 +0200 Subject: [PATCH 79/79] build: 10.0.0.7 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a89ba992e..c8cce8a8c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -9,7 +9,7 @@ - 10.0.0.6 + 10.0.0.7 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion)