diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d780df2fc..7ada48e50 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -29,6 +29,12 @@ jobs:
run: .\build.ps1 compile
- name: Test Dalamud
run: .\build.ps1 test
+ - name: Sign Dalamud
+ if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}
+ env:
+ CODESIGN_CERT_PFX: ${{ secrets.CODESIGN_CERT_PFX }}
+ CODESIGN_CERT_PASSWORD: ${{ secrets.CODESIGN_CERT_PASSWORD }}
+ run: .\sign.ps1 .\bin\Release
- name: Create hashlist
run: .\CreateHashList.ps1 .\bin\Release
- name: Upload artifact
diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml
new file mode 100644
index 000000000..25b558711
--- /dev/null
+++ b/.github/workflows/rollup.yml
@@ -0,0 +1,49 @@
+name: Rollup changes to next version
+on:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ branches:
+ - v9
+
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ submodules: true
+ fetch-depth: 0
+ ref: ${{ matrix.branches }}
+ token: ${{ secrets.UPDATE_PAT }}
+ - name: Create update branch
+ run: git checkout -b ${{ matrix.branches }}-rollup
+ - name: Initialize mandatory git config
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email noreply@github.com
+ git config --global pull.rebase false
+ - name: Update submodule
+ run: |
+ git fetch origin master
+ git merge -s recursive -X ours origin/master
+ git push origin ${{ matrix.branches }}-rollup --force
+ - name: Create PR
+ run: |
+ echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token
+ prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.branches }}-rollup --state open --json number --template "{{range .}}{{.number}}{{end}}")
+ if [ -z "$prNumber" ]; then
+ echo "No PR found, creating one"
+ gh pr create --head ${{ matrix.branches }}-rollup --title "[${{ matrix.branches }}] Rollup changes from master" --body "" --base ${{ matrix.branches }}
+ else
+ echo "PR already exists, ignoring"
+ fi
diff --git a/Dalamud.Interface/Raii/EndObjects.cs b/Dalamud.Interface/Raii/EndObjects.cs
index 1edc9f518..032f09621 100644
--- a/Dalamud.Interface/Raii/EndObjects.cs
+++ b/Dalamud.Interface/Raii/EndObjects.cs
@@ -35,6 +35,15 @@ public static partial class ImRaii
public static IEndObject Popup(string id, ImGuiWindowFlags flags)
=> new EndConditionally(ImGui.EndPopup, ImGui.BeginPopup(id, flags));
+
+ public static IEndObject PopupModal(string id)
+ => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id));
+
+ public static IEndObject PopupModal(string id, ref bool open)
+ => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id, ref open));
+
+ public static IEndObject PopupModal(string id, ref bool open, ImGuiWindowFlags flags)
+ => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupModal(id, ref open, flags));
public static IEndObject ContextPopup(string id)
=> new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextWindow(id));
@@ -42,6 +51,12 @@ public static partial class ImRaii
public static IEndObject ContextPopup(string id, ImGuiPopupFlags flags)
=> new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextWindow(id, flags));
+ public static IEndObject ContextPopupItem(string id)
+ => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextItem(id));
+
+ public static IEndObject ContextPopupItem(string id, ImGuiPopupFlags flags)
+ => new EndConditionally(ImGui.EndPopup, ImGui.BeginPopupContextItem(id, flags));
+
public static IEndObject Combo(string label, string previewValue)
=> new EndConditionally(ImGui.EndCombo, ImGui.BeginCombo(label, previewValue));
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index cb33e7070..39c53c3cb 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -207,6 +207,12 @@ internal sealed class DalamudConfiguration : IServiceType
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
///
public bool IsDocking { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects.
+ /// This setting is effected by the in-game "System Sounds" option and volume.
+ ///
+ public bool EnablePluginUISoundEffects { get; set; }
///
/// Gets or sets a value indicating whether viewports should always be disabled.
@@ -287,6 +293,11 @@ internal sealed class DalamudConfiguration : IServiceType
///
public bool ProfilesEnabled { get; set; } = false;
+ ///
+ /// Gets or sets a value indicating whether or not the user has seen the profiles tutorial.
+ ///
+ public bool ProfilesHasSeenTutorial { get; set; } = false;
+
///
/// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled.
///
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index 9d6a352ca..407a1b0da 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -8,6 +8,7 @@ using System.Threading;
using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiScene;
@@ -27,7 +28,10 @@ namespace Dalamud.Data;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed class DataManager : IDisposable, IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed class DataManager : IDisposable, IServiceType, IDataManager
{
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
@@ -127,75 +131,43 @@ public sealed class DataManager : IDisposable, IServiceType
}
}
- ///
- /// Gets the current game client language.
- ///
+ ///
public ClientLanguage Language { get; private set; }
- ///
- /// Gets the OpCodes sent by the server to the client.
- ///
+ ///
public ReadOnlyDictionary ServerOpCodes { get; private set; }
- ///
- /// Gets the OpCodes sent by the client to the server.
- ///
+ ///
[UsedImplicitly]
public ReadOnlyDictionary ClientOpCodes { get; private set; }
- ///
- /// Gets a object which gives access to any excel/game data.
- ///
+ ///
public GameData GameData { get; private set; }
- ///
- /// Gets an object which gives access to any of the game's sheet data.
- ///
+ ///
public ExcelModule Excel => this.GameData.Excel;
- ///
- /// Gets a value indicating whether Game Data is ready to be read.
- ///
+ ///
public bool IsDataReady { get; private set; }
- ///
- /// Gets a value indicating whether the game data files have been modified by another third-party tool.
- ///
+ ///
public bool HasModifiedGameDataFiles { get; private set; }
#region Lumina Wrappers
- ///
- /// Get an with the given Excel sheet row type.
- ///
- /// The excel sheet type to get.
- /// The , giving access to game rows.
+ ///
public ExcelSheet? GetExcelSheet() where T : ExcelRow
=> this.Excel.GetSheet();
- ///
- /// Get an with the given Excel sheet row type with a specified language.
- ///
- /// Language of the sheet to get.
- /// The excel sheet type to get.
- /// The , giving access to game rows.
+ ///
public ExcelSheet? GetExcelSheet(ClientLanguage language) where T : ExcelRow
=> this.Excel.GetSheet(language.ToLumina());
- ///
- /// Get a with the given path.
- ///
- /// The path inside of the game files.
- /// The of the file.
+ ///
public FileResource? GetFile(string path)
=> this.GetFile(path);
- ///
- /// Get a with the given path, of the given type.
- ///
- /// The type of resource.
- /// The path inside of the game files.
- /// The of the file.
+ ///
public T? GetFile(string path) where T : FileResource
{
var filePath = GameData.ParseFilePath(path);
@@ -204,11 +176,7 @@ public sealed class DataManager : IDisposable, IServiceType
return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile(filePath.Category, filePath) : default;
}
- ///
- /// Check if the file with the given path exists within the game's index files.
- ///
- /// The path inside of the game files.
- /// True if the file exists.
+ ///
public bool FileExists(string path)
=> this.GameData.FileExists(path);
@@ -217,25 +185,15 @@ public sealed class DataManager : IDisposable, IServiceType
///
/// The icon ID.
/// The containing the icon.
- /// todo: remove in api9 in favor of GetIcon(uint iconId, bool highResolution)
+ /// TODO(v9): remove in api9 in favor of GetIcon(uint iconId, bool highResolution)
public TexFile? GetIcon(uint iconId)
=> this.GetIcon(this.Language, iconId, false);
- ///
- /// Get a containing the icon with the given ID.
- ///
- /// The icon ID.
- /// Return high resolution version.
- /// The containing the icon.
+ ///
public TexFile? GetIcon(uint iconId, bool highResolution)
=> this.GetIcon(this.Language, iconId, highResolution);
-
- ///
- /// Get a containing the icon with the given ID, of the given quality.
- ///
- /// A value indicating whether the icon should be HQ.
- /// The icon ID.
- /// The containing the icon.
+
+ ///
public TexFile? GetIcon(bool isHq, uint iconId)
{
var type = isHq ? "hq/" : string.Empty;
@@ -248,17 +206,11 @@ public sealed class DataManager : IDisposable, IServiceType
/// The requested language.
/// The icon ID.
/// The containing the icon.
- /// todo: remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
+ /// TODO(v9): remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetIcon(iconLanguage, iconId, false);
- ///
- /// Get a containing the icon with the given ID, of the given language.
- ///
- /// The requested language.
- /// The icon ID.
- /// Return high resolution version.
- /// The containing the icon.
+ ///
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
{
var type = iconLanguage switch
@@ -279,17 +231,11 @@ public sealed class DataManager : IDisposable, IServiceType
/// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
/// The icon ID.
/// The containing the icon.
- /// todo: remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution)
+ /// TODO(v9): remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution)
public TexFile? GetIcon(string? type, uint iconId)
=> this.GetIcon(type, iconId, false);
- ///
- /// Get a containing the icon with the given ID, of the given type.
- ///
- /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
- /// The icon ID.
- /// Return high resolution version.
- /// The containing the icon.
+ ///
public TexFile? GetIcon(string? type, uint iconId, bool highResolution)
{
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
@@ -310,27 +256,15 @@ public sealed class DataManager : IDisposable, IServiceType
return file;
}
- ///
- /// Get a containing the HQ icon with the given ID.
- ///
- /// The icon ID.
- /// The containing the icon.
+ ///
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
- ///
- /// Get the passed as a drawable ImGui TextureWrap.
- ///
- /// The Lumina .
- /// A that can be used to draw the texture.
+ ///
public TextureWrap? GetImGuiTexture(TexFile? tex)
=> tex == null ? null : Service.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
- ///
- /// Get the passed texture path as a drawable ImGui TextureWrap.
- ///
- /// The internal path to the texture.
- /// A that can be used to draw the texture.
+ ///
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile(path));
@@ -339,59 +273,33 @@ public sealed class DataManager : IDisposable, IServiceType
///
/// The icon ID.
/// The containing the icon.
- /// todo: remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution)
+ /// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution)
public TextureWrap? GetImGuiTextureIcon(uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconId, false));
- ///
- /// Get a containing the icon with the given ID.
- ///
- /// The icon ID.
- /// Return the high resolution version.
- /// The containing the icon.
+ ///
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
=> this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
- ///
- /// Get a containing the icon with the given ID, of the given quality.
- ///
- /// A value indicating whether the icon should be HQ.
- /// The icon ID.
- /// The containing the icon.
+ ///
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
- ///
- /// Get a containing the icon with the given ID, of the given language.
- ///
- /// The requested language.
- /// The icon ID.
- /// The containing the icon.
+ ///
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
- ///
- /// Get a containing the icon with the given ID, of the given type.
- ///
- /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
- /// The icon ID.
- /// The containing the icon.
+ ///
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
- ///
- /// Get a containing the HQ icon with the given ID.
- ///
- /// The icon ID.
- /// The containing the icon.
+ ///
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion
- ///
- /// Dispose this DataManager.
- ///
+ ///
void IDisposable.Dispose()
{
this.luminaCancellationTokenSource.Cancel();
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 19b4f841c..1975505a8 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -263,7 +263,7 @@ public sealed class EntryPoint
{
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
- if (plugin.Manifest.IsThirdParty)
+ if (plugin.IsThirdParty)
supportText = string.Empty;
}
}
diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
index 4c8de5586..dc2cb9fae 100644
--- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs
+++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.Buddy;
@@ -16,7 +17,10 @@ namespace Dalamud.Game.ClientState.Buddy;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed partial class BuddyList : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed partial class BuddyList : IServiceType, IBuddyList
{
private const uint InvalidObjectID = 0xE0000000;
@@ -33,9 +37,7 @@ public sealed partial class BuddyList : IServiceType
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
}
- ///
- /// Gets the amount of battle buddies the local player has.
- ///
+ ///
public int Length
{
get
@@ -56,16 +58,16 @@ public sealed partial class BuddyList : IServiceType
///
/// Gets a value indicating whether the local player's companion is present.
///
+ [Obsolete("Use CompanionBuddy != null", false)]
public bool CompanionBuddyPresent => this.CompanionBuddy != null;
///
/// Gets a value indicating whether the local player's pet is present.
///
+ [Obsolete("Use PetBuddy != null", false)]
public bool PetBuddyPresent => this.PetBuddy != null;
- ///
- /// Gets the active companion buddy.
- ///
+ ///
public BuddyMember? CompanionBuddy
{
get
@@ -75,9 +77,7 @@ public sealed partial class BuddyList : IServiceType
}
}
- ///
- /// Gets the active pet buddy.
- ///
+ ///
public BuddyMember? PetBuddy
{
get
@@ -96,11 +96,7 @@ public sealed partial class BuddyList : IServiceType
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress;
- ///
- /// Gets a battle buddy at the specified spawn index.
- ///
- /// Spawn index.
- /// A at the specified spawn index.
+ ///
public BuddyMember? this[int index]
{
get
@@ -110,29 +106,19 @@ public sealed partial class BuddyList : IServiceType
}
}
- ///
- /// Gets the address of the companion buddy.
- ///
- /// The memory address of the companion buddy.
+ ///
public unsafe IntPtr GetCompanionBuddyMemberAddress()
{
return (IntPtr)(&this.BuddyListStruct->Companion);
}
- ///
- /// Gets the address of the pet buddy.
- ///
- /// The memory address of the pet buddy.
+ ///
public unsafe IntPtr GetPetBuddyMemberAddress()
{
return (IntPtr)(&this.BuddyListStruct->Pet);
}
- ///
- /// Gets the address of the battle buddy at the specified index of the buddy list.
- ///
- /// The index of the battle buddy.
- /// The memory address of the battle buddy.
+ ///
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
{
if (index < 0 || index >= 3)
@@ -141,11 +127,7 @@ public sealed partial class BuddyList : IServiceType
return (IntPtr)(this.BuddyListStruct->BattleBuddies + (index * BuddyMemberSize));
}
- ///
- /// Create a reference to a buddy.
- ///
- /// The address of the buddy in memory.
- /// object containing the requested data.
+ ///
public BuddyMember? CreateBuddyMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
@@ -165,7 +147,7 @@ public sealed partial class BuddyList : IServiceType
///
/// This collection represents the buddies present in your squadron or trust party.
///
-public sealed partial class BuddyList : IReadOnlyCollection
+public sealed partial class BuddyList
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs
index dfd4bcaee..53196d5df 100644
--- a/Dalamud/Game/ClientState/Fates/FateTable.cs
+++ b/Dalamud/Game/ClientState/Fates/FateTable.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.Fates;
@@ -14,7 +15,10 @@ namespace Dalamud.Game.ClientState.Fates;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed partial class FateTable : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed partial class FateTable : IServiceType, IFateTable
{
private readonly ClientStateAddressResolver address;
@@ -26,14 +30,10 @@ public sealed partial class FateTable : IServiceType
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
}
- ///
- /// Gets the address of the Fate table.
- ///
+ ///
public IntPtr Address => this.address.FateTablePtr;
- ///
- /// Gets the amount of currently active Fates.
- ///
+ ///
public unsafe int Length
{
get
@@ -69,11 +69,7 @@ public sealed partial class FateTable : IServiceType
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress;
- ///
- /// Get an actor at the specified spawn index.
- ///
- /// Spawn index.
- /// A at the specified spawn index.
+ ///
public Fate? this[int index]
{
get
@@ -83,11 +79,7 @@ public sealed partial class FateTable : IServiceType
}
}
- ///
- /// Gets the address of the Fate at the specified index of the fate table.
- ///
- /// The index of the Fate.
- /// The memory address of the Fate.
+ ///
public unsafe IntPtr GetFateAddress(int index)
{
if (index >= this.Length)
@@ -100,11 +92,7 @@ public sealed partial class FateTable : IServiceType
return (IntPtr)this.Struct->Fates.Get((ulong)index).Value;
}
- ///
- /// Create a reference to a FFXIV actor.
- ///
- /// The offset of the actor in memory.
- /// object containing requested data.
+ ///
public Fate? CreateFateReference(IntPtr offset)
{
var clientState = Service.Get();
@@ -122,7 +110,7 @@ public sealed partial class FateTable : IServiceType
///
/// This collection represents the currently available Fate events.
///
-public sealed partial class FateTable : IReadOnlyCollection
+public sealed partial class FateTable
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs
index c72e9c1de..bc5744047 100644
--- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs
+++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs
@@ -1,8 +1,10 @@
using System;
+using System.Numerics;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using ImGuiNET;
using Serilog;
@@ -16,9 +18,12 @@ namespace Dalamud.Game.ClientState.GamePad;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public unsafe class GamepadState : IDisposable, IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
{
- private readonly Hook gamepadPoll;
+ private readonly Hook? gamepadPoll;
private bool isDisposed;
@@ -42,44 +47,60 @@ public unsafe class GamepadState : IDisposable, IServiceType
///
public IntPtr GamepadInputAddress { get; private set; }
+ ///
+ public Vector2 LeftStick =>
+ new(this.leftStickX, this.leftStickY);
+
+ ///
+ public Vector2 RightStick =>
+ new(this.rightStickX, this.rightStickY);
+
///
/// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.LeftStick.X", false)]
public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
///
/// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.LeftStick.X", false)]
public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
///
/// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.LeftStick.Y", false)]
public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
///
/// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.LeftStick.Y", false)]
public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
///
/// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.RightStick.X", false)]
public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
///
/// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.RightStick.X", false)]
public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
///
/// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.RightStick.Y", false)]
public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
///
/// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
///
+ [Obsolete("Use IGamepadState.RightStick.Y", false)]
public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
///
@@ -120,43 +141,16 @@ public unsafe class GamepadState : IDisposable, IServiceType
///
internal bool NavEnableGamepad { get; set; }
- ///
- /// Gets whether has been pressed.
- ///
- /// Only true on first frame of the press.
- /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
- ///
- /// The button to check for.
- /// 1 if pressed, 0 otherwise.
+ ///
public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0;
- ///
- /// Gets whether is being pressed.
- ///
- /// True in intervals if button is held down.
- /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
- ///
- /// The button to check for.
- /// 1 if still pressed during interval, 0 otherwise or in between intervals.
+ ///
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
- ///
- /// Gets whether has been released.
- ///
- /// Only true the frame after release.
- /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
- ///
- /// The button to check for.
- /// 1 if released, 0 otherwise.
+ ///
public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0;
- ///
- /// Gets the raw state of .
- ///
- /// Is set the entire time a button is pressed down.
- ///
- /// The button to check for.
- /// 1 the whole time button is pressed, 0 otherwise.
+ ///
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
///
@@ -171,12 +165,12 @@ public unsafe class GamepadState : IDisposable, IServiceType
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
- this.gamepadPoll.Enable();
+ this.gamepadPoll?.Enable();
}
private int GamepadPollDetour(IntPtr gamepadInput)
{
- var original = this.gamepadPoll.Original(gamepadInput);
+ var original = this.gamepadPoll!.Original(gamepadInput);
try
{
this.GamepadInputAddress = gamepadInput;
diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs
index bf5c4b525..683f5c61f 100644
--- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs
+++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs
@@ -5,6 +5,7 @@ using System.Reflection;
using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.JobGauge;
@@ -15,7 +16,10 @@ namespace Dalamud.Game.ClientState.JobGauge;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public class JobGauges : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public class JobGauges : IServiceType, IJobGauges
{
private Dictionary cache = new();
@@ -27,16 +31,10 @@ public class JobGauges : IServiceType
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
}
- ///
- /// Gets the address of the JobGauge data.
- ///
+ ///
public IntPtr Address { get; }
- ///
- /// Get the JobGauge for a given job.
- ///
- /// A JobGauge struct from ClientState.Structs.JobGauge.
- /// A JobGauge.
+ ///
public T Get() where T : JobGaugeBase
{
// This is cached to mitigate the effects of using activator for instantiation.
diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
index 70c17fd83..16cf7c277 100644
--- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs
+++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs
@@ -7,6 +7,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.Objects;
@@ -17,7 +18,10 @@ namespace Dalamud.Game.ClientState.Objects;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed partial class ObjectTable : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed partial class ObjectTable : IServiceType, IObjectTable
{
private const int ObjectTableLength = 596;
@@ -31,21 +35,13 @@ public sealed partial class ObjectTable : IServiceType
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
}
- ///
- /// Gets the address of the object table.
- ///
+ ///
public IntPtr Address => this.address.ObjectTable;
- ///
- /// Gets the length of the object table.
- ///
+ ///
public int Length => ObjectTableLength;
- ///
- /// Get an object at the specified spawn index.
- ///
- /// Spawn index.
- /// An at the specified spawn index.
+ ///
public GameObject? this[int index]
{
get
@@ -55,11 +51,7 @@ public sealed partial class ObjectTable : IServiceType
}
}
- ///
- /// Search for a game object by their Object ID.
- ///
- /// Object ID to find.
- /// A game object or null.
+ ///
public GameObject? SearchById(ulong objectId)
{
if (objectId is GameObject.InvalidGameObjectId or 0)
@@ -77,11 +69,7 @@ public sealed partial class ObjectTable : IServiceType
return null;
}
- ///
- /// Gets the address of the game object at the specified index of the object table.
- ///
- /// The index of the object.
- /// The memory address of the object.
+ ///
public unsafe IntPtr GetObjectAddress(int index)
{
if (index < 0 || index >= ObjectTableLength)
@@ -90,11 +78,7 @@ public sealed partial class ObjectTable : IServiceType
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
}
- ///
- /// Create a reference to an FFXIV game object.
- ///
- /// The address of the object in memory.
- /// object or inheritor containing the requested data.
+ ///
public unsafe GameObject? CreateObjectReference(IntPtr address)
{
var clientState = Service.GetNullable();
@@ -125,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType
///
/// This collection represents the currently spawned FFXIV game objects.
///
-public sealed partial class ObjectTable : IReadOnlyCollection
+public sealed partial class ObjectTable
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs
index ccd89e6a3..ff1bdc5ba 100644
--- a/Dalamud/Game/ClientState/Objects/TargetManager.cs
+++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs
@@ -3,6 +3,7 @@ using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+#pragma warning disable CS0618
namespace Dalamud.Game.ClientState.Objects;
@@ -12,7 +13,10 @@ namespace Dalamud.Game.ClientState.Objects;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed unsafe class TargetManager : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed unsafe class TargetManager : IServiceType, ITargetManager
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service.Get();
@@ -28,50 +32,38 @@ public sealed unsafe class TargetManager : IServiceType
this.address = this.clientState.AddressResolver;
}
- ///
- /// Gets the address of the target manager.
- ///
+ ///
public IntPtr Address => this.address.TargetManager;
- ///
- /// Gets or sets the current target.
- ///
+ ///
public GameObject? Target
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value);
}
- ///
- /// Gets or sets the mouseover target.
- ///
+ ///
public GameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value);
}
- ///
- /// Gets or sets the focus target.
- ///
+ ///
public GameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value);
}
- ///
- /// Gets or sets the previous target.
- ///
+ ///
public GameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value);
}
- ///
- /// Gets or sets the soft target.
- ///
+ ///
public GameObject? SoftTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget);
@@ -84,84 +76,99 @@ public sealed unsafe class TargetManager : IServiceType
/// Sets the current target.
///
/// Actor to target.
+ [Obsolete("Use Target Property", false)]
public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
///
/// Sets the mouseover target.
///
/// Actor to target.
+ [Obsolete("Use MouseOverTarget Property", false)]
public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
///
/// Sets the focus target.
///
/// Actor to target.
+ [Obsolete("Use FocusTarget Property", false)]
public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
///
/// Sets the previous target.
///
/// Actor to target.
+ [Obsolete("Use PreviousTarget Property", false)]
public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
///
/// Sets the soft target.
///
/// Actor to target.
+ [Obsolete("Use SoftTarget Property", false)]
public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
///
/// Sets the current target.
///
/// Actor (address) to target.
+ [Obsolete("Use Target Property", false)]
public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
///
/// Sets the mouseover target.
///
/// Actor (address) to target.
+ [Obsolete("Use MouseOverTarget Property", false)]
public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
///
/// Sets the focus target.
///
/// Actor (address) to target.
+ [Obsolete("Use FocusTarget Property", false)]
public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
///
/// Sets the previous target.
///
/// Actor (address) to target.
+ [Obsolete("Use PreviousTarget Property", false)]
public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
///
/// Sets the soft target.
///
/// Actor (address) to target.
+ [Obsolete("Use SoftTarget Property", false)]
public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
///
/// Clears the current target.
///
+ [Obsolete("Use Target Property", false)]
public void ClearTarget() => this.SetTarget(IntPtr.Zero);
///
/// Clears the mouseover target.
///
+ [Obsolete("Use MouseOverTarget Property", false)]
public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
///
/// Clears the focus target.
///
+ [Obsolete("Use FocusTarget Property", false)]
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
///
/// Clears the previous target.
///
+ [Obsolete("Use PreviousTarget Property", false)]
public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
///
/// Clears the soft target.
///
+ [Obsolete("Use SoftTarget Property", false)]
public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
}
diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs
index 62b69dcec..292430b27 100644
--- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs
+++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs
@@ -144,6 +144,11 @@ public unsafe partial class GameObject
///
public bool IsDead => this.Struct->IsDead();
+ ///
+ /// Gets a value indicating whether the object is targetable.
+ ///
+ public bool IsTargetable => this.Struct->GetIsTargetable();
+
///
/// Gets the position of this .
///
diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs
index 80fe7d41f..529b57b6f 100644
--- a/Dalamud/Game/ClientState/Party/PartyList.cs
+++ b/Dalamud/Game/ClientState/Party/PartyList.cs
@@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.ClientState.Party;
@@ -15,7 +16,10 @@ namespace Dalamud.Game.ClientState.Party;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed unsafe partial class PartyList : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed unsafe partial class PartyList : IServiceType, IPartyList
{
private const int GroupLength = 8;
private const int AllianceLength = 20;
@@ -33,50 +37,32 @@ public sealed unsafe partial class PartyList : IServiceType
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
}
- ///
- /// Gets the amount of party members the local player has.
- ///
+ ///
public int Length => this.GroupManagerStruct->MemberCount;
- ///
- /// Gets the index of the party leader.
- ///
+ ///
public uint PartyLeaderIndex => this.GroupManagerStruct->PartyLeaderIndex;
- ///
- /// Gets a value indicating whether this group is an alliance.
- ///
+ ///
public bool IsAlliance => this.GroupManagerStruct->AllianceFlags > 0;
- ///
- /// Gets the address of the Group Manager.
- ///
+ ///
public IntPtr GroupManagerAddress => this.address.GroupManager;
- ///
- /// Gets the address of the party list within the group manager.
- ///
+ ///
public IntPtr GroupListAddress => (IntPtr)GroupManagerStruct->PartyMembers;
- ///
- /// Gets the address of the alliance member list within the group manager.
- ///
+ ///
public IntPtr AllianceListAddress => (IntPtr)this.GroupManagerStruct->AllianceMembers;
- ///
- /// Gets the ID of the party.
- ///
+ ///
public long PartyId => this.GroupManagerStruct->PartyId;
private static int PartyMemberSize { get; } = Marshal.SizeOf();
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
- ///
- /// Get a party member at the specified spawn index.
- ///
- /// Spawn index.
- /// A at the specified spawn index.
+ ///
public PartyMember? this[int index]
{
get
@@ -98,11 +84,7 @@ public sealed unsafe partial class PartyList : IServiceType
}
}
- ///
- /// Gets the address of the party member at the specified index of the party list.
- ///
- /// The index of the party member.
- /// The memory address of the party member.
+ ///
public IntPtr GetPartyMemberAddress(int index)
{
if (index < 0 || index >= GroupLength)
@@ -111,11 +93,7 @@ public sealed unsafe partial class PartyList : IServiceType
return this.GroupListAddress + (index * PartyMemberSize);
}
- ///
- /// Create a reference to an FFXIV party member.
- ///
- /// The address of the party member in memory.
- /// The party member object containing the requested data.
+ ///
public PartyMember? CreatePartyMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
@@ -127,11 +105,7 @@ public sealed unsafe partial class PartyList : IServiceType
return new PartyMember(address);
}
- ///
- /// Gets the address of the alliance member at the specified index of the alliance list.
- ///
- /// The index of the alliance member.
- /// The memory address of the alliance member.
+ ///
public IntPtr GetAllianceMemberAddress(int index)
{
if (index < 0 || index >= AllianceLength)
@@ -140,11 +114,7 @@ public sealed unsafe partial class PartyList : IServiceType
return this.AllianceListAddress + (index * PartyMemberSize);
}
- ///
- /// Create a reference to an FFXIV alliance member.
- ///
- /// The address of the alliance member in memory.
- /// The party member object containing the requested data.
+ ///
public PartyMember? CreateAllianceMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
@@ -160,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType
///
/// This collection represents the party members present in your party or alliance.
///
-public sealed partial class PartyList : IReadOnlyCollection
+public sealed partial class PartyList
{
///
int IReadOnlyCollection.Count => this.Length;
diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs
index 7bb429063..63a1a3d09 100644
--- a/Dalamud/Game/Command/CommandManager.cs
+++ b/Dalamud/Game/Command/CommandManager.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
@@ -8,6 +9,7 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Serilog;
namespace Dalamud.Game.Command;
@@ -18,9 +20,12 @@ namespace Dalamud.Game.Command;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed class CommandManager : IServiceType, IDisposable
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
{
- private readonly Dictionary commandMap = new();
+ private readonly ConcurrentDictionary commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe = new(@"^„(?.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
@@ -46,16 +51,10 @@ public sealed class CommandManager : IServiceType, IDisposable
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
}
- ///
- /// Gets a read-only list of all registered commands.
- ///
+ ///
public ReadOnlyDictionary Commands => new(this.commandMap);
- ///
- /// Process a command in full.
- ///
- /// The full command string.
- /// True if the command was found and dispatched.
+ ///
public bool ProcessCommand(string content)
{
string command;
@@ -91,19 +90,14 @@ public sealed class CommandManager : IServiceType, IDisposable
argument = content[argStart..];
}
- if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found.
+ if (!this.commandMap.TryGetValue(command, out var handler)) // Command was not found.
return false;
this.DispatchCommand(command, argument, handler);
return true;
}
- ///
- /// Dispatch the handling of a command.
- ///
- /// The command to dispatch.
- /// The provided arguments.
- /// A object describing this command.
+ ///
public void DispatchCommand(string command, string argument, CommandInfo info)
{
try
@@ -116,37 +110,25 @@ public sealed class CommandManager : IServiceType, IDisposable
}
}
- ///
- /// Add a command handler, which you can use to add your own custom commands to the in-game chat.
- ///
- /// The command to register.
- /// A object describing the command.
- /// If adding was successful.
+ ///
public bool AddHandler(string command, CommandInfo info)
{
if (info == null)
throw new ArgumentNullException(nameof(info), "Command handler is null.");
- try
- {
- this.commandMap.Add(command, info);
- return true;
- }
- catch (ArgumentException)
+ if (!this.commandMap.TryAdd(command, info))
{
Log.Error("Command {CommandName} is already registered.", command);
return false;
}
+
+ return true;
}
- ///
- /// Remove a command from the command handlers.
- ///
- /// The command to remove.
- /// If the removal was successful.
+ ///
public bool RemoveHandler(string command)
{
- return this.commandMap.Remove(command);
+ return this.commandMap.Remove(command, out _);
}
///
diff --git a/Dalamud/Game/Config/ConfigChangeEvent.cs b/Dalamud/Game/Config/ConfigChangeEvent.cs
new file mode 100644
index 000000000..941033c61
--- /dev/null
+++ b/Dalamud/Game/Config/ConfigChangeEvent.cs
@@ -0,0 +1,7 @@
+using System;
+
+namespace Dalamud.Game.Config;
+
+public abstract record ConfigChangeEvent(Enum Option);
+
+public record ConfigChangeEvent(T ConfigOption) : ConfigChangeEvent(ConfigOption) where T : Enum;
diff --git a/Dalamud/Game/Config/ConfigType.cs b/Dalamud/Game/Config/ConfigType.cs
new file mode 100644
index 000000000..731b26cc8
--- /dev/null
+++ b/Dalamud/Game/Config/ConfigType.cs
@@ -0,0 +1,32 @@
+namespace Dalamud.Game.Config;
+
+///
+/// Types of options used by the game config.
+///
+public enum ConfigType
+{
+ ///
+ /// Unused config index.
+ ///
+ Unused = 0,
+
+ ///
+ /// A label entry with no value.
+ ///
+ Category = 1,
+
+ ///
+ /// A config entry with an unsigned integer value.
+ ///
+ UInt = 2,
+
+ ///
+ /// A config entry with a float value.
+ ///
+ Float = 3,
+
+ ///
+ /// A config entry with a string value.
+ ///
+ String = 4,
+}
diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs
index 81112cd79..dfdb8b5d2 100644
--- a/Dalamud/Game/Config/GameConfig.cs
+++ b/Dalamud/Game/Config/GameConfig.cs
@@ -1,7 +1,10 @@
-using System.Diagnostics;
-
+using System;
+using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+using FFXIVClientStructs.FFXIV.Common.Configuration;
using Serilog;
namespace Dalamud.Game.Config;
@@ -12,250 +15,181 @@ namespace Dalamud.Game.Config;
[InterfaceVersion("1.0")]
[PluginInterface]
[ServiceManager.EarlyLoadedService]
-public sealed class GameConfig : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{
+ private readonly GameConfigAddressResolver address = new();
+ private Hook? configChangeHook;
+
[ServiceManager.ServiceConstructor]
- private unsafe GameConfig(Framework framework)
+ private unsafe GameConfig(Framework framework, SigScanner sigScanner)
{
framework.RunOnTick(() =>
{
- Log.Verbose("[GameConfig] Initalizing");
+ Log.Verbose("[GameConfig] Initializing");
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase);
this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig);
this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig);
+
+ this.address.Setup(sigScanner);
+ this.configChangeHook = Hook.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged);
+ this.configChangeHook?.Enable();
});
}
- ///
- /// Gets the collection of config options that persist between characters.
- ///
+ private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry);
+
+ ///
+ public event EventHandler Changed;
+
+ ///
public GameConfigSection System { get; private set; }
- ///
- /// Gets the collection of config options that are character specific.
- ///
+ ///
public GameConfigSection UiConfig { get; private set; }
- ///
- /// Gets the collection of config options that are control mode specific. (Mouse and Keyboard / Gamepad).
- ///
+ ///
public GameConfigSection UiControl { get; private set; }
- ///
- /// Attempts to get a boolean config value from the System section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a uint config value from the System section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(SystemConfigOption option, out uint value) => this.System.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a float config value from the System section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(SystemConfigOption option, out float value) => this.System.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a string config value from the System section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(SystemConfigOption option, out string value) => this.System.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a boolean config value from the UiConfig section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
+ public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
+
+ ///
+ public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
+
+ ///
public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a uint config value from the UiConfig section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(UiConfigOption option, out uint value) => this.UiConfig.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a float config value from the UiConfig section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(UiConfigOption option, out float value) => this.UiConfig.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a string config value from the UiConfig section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
- public bool TryGet(UiConfigOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value);
+ ///
+ public bool TryGet(UiConfigOption option, out string value) => this.UiConfig.TryGet(option.GetName(), out value);
+
+ ///
+ public bool TryGet(UiConfigOption option, out UIntConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties);
+
+ ///
+ public bool TryGet(UiConfigOption option, out FloatConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties);
+
+ ///
+ public bool TryGet(UiConfigOption option, out StringConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties);
- ///
- /// Attempts to get a boolean config value from the UiControl section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(UiControlOption option, out bool value) => this.UiControl.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a uint config value from the UiControl section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(UiControlOption option, out uint value) => this.UiControl.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a float config value from the UiControl section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
+ ///
public bool TryGet(UiControlOption option, out float value) => this.UiControl.TryGet(option.GetName(), out value);
- ///
- /// Attempts to get a string config value from the UiControl section.
- ///
- /// Option to get the value of.
- /// The returned value of the config option.
- /// A value representing the success.
- public bool TryGet(UiControlOption option, out string value) => this.System.TryGet(option.GetName(), out value);
+ ///
+ public bool TryGet(UiControlOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value);
- ///
- /// Set a boolean config option in the System config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
+ public bool TryGet(UiControlOption option, out UIntConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties);
+
+ ///
+ public bool TryGet(UiControlOption option, out FloatConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties);
+
+ ///
+ public bool TryGet(UiControlOption option, out StringConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties);
+
+ ///
public void Set(SystemConfigOption option, bool value) => this.System.Set(option.GetName(), value);
- ///
- /// Set a unsigned integer config option in the System config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
public void Set(SystemConfigOption option, uint value) => this.System.Set(option.GetName(), value);
- ///
- /// Set a float config option in the System config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
public void Set(SystemConfigOption option, float value) => this.System.Set(option.GetName(), value);
- ///
- /// Set a string config option in the System config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
public void Set(SystemConfigOption option, string value) => this.System.Set(option.GetName(), value);
- ///
- /// Set a boolean config option in the UiConfig section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
public void Set(UiConfigOption option, bool value) => this.UiConfig.Set(option.GetName(), value);
-
- ///
- /// Set a unsigned integer config option in the UiConfig section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+
+ ///
public void Set(UiConfigOption option, uint value) => this.UiConfig.Set(option.GetName(), value);
- ///
- /// Set a float config option in the UiConfig section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
public void Set(UiConfigOption option, float value) => this.UiConfig.Set(option.GetName(), value);
- ///
- /// Set a string config option in the UiConfig section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+ ///
public void Set(UiConfigOption option, string value) => this.UiConfig.Set(option.GetName(), value);
-
- ///
- /// Set a boolean config option in the UiControl config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+
+ ///
public void Set(UiControlOption option, bool value) => this.UiControl.Set(option.GetName(), value);
-
- ///
- /// Set a uint config option in the UiControl config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+
+ ///
public void Set(UiControlOption option, uint value) => this.UiControl.Set(option.GetName(), value);
-
- ///
- /// Set a float config option in the UiControl config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+
+ ///
public void Set(UiControlOption option, float value) => this.UiControl.Set(option.GetName(), value);
-
- ///
- /// Set a string config option in the UiControl config section.
- /// Note: Not all config options will be be immediately reflected in the game.
- ///
- /// Name of the config option.
- /// New value of the config option.
- /// Throw if the config option is not found.
- /// Thrown if the name of the config option is found, but the struct was not.
+
+ ///
public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value);
+
+ ///
+ void IDisposable.Dispose()
+ {
+ this.configChangeHook?.Disable();
+ this.configChangeHook?.Dispose();
+ }
+
+ private unsafe nint OnConfigChanged(ConfigBase* configBase, ConfigEntry* configEntry)
+ {
+ var returnValue = this.configChangeHook!.Original(configBase, configEntry);
+ try
+ {
+ ConfigChangeEvent? eventArgs = null;
+
+ if (configBase == this.System.GetConfigBase())
+ {
+ eventArgs = this.System.InvokeChange(configEntry);
+ }
+ else if (configBase == this.UiConfig.GetConfigBase())
+ {
+ eventArgs = this.UiConfig.InvokeChange(configEntry);
+ }
+ else if (configBase == this.UiControl.GetConfigBase())
+ {
+ eventArgs = this.UiControl.InvokeChange(configEntry);
+ }
+
+ if (eventArgs == null) return returnValue;
+
+ this.Changed?.InvokeSafely(this, eventArgs);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Exception thrown handing {nameof(this.OnConfigChanged)} events.");
+ }
+
+ return returnValue;
+ }
}
diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs
new file mode 100644
index 000000000..6a207807a
--- /dev/null
+++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs
@@ -0,0 +1,18 @@
+namespace Dalamud.Game.Config;
+
+///
+/// Game config system address resolver.
+///
+public sealed class GameConfigAddressResolver : BaseAddressResolver
+{
+ ///
+ /// Gets the address of the method called when any config option is changed.
+ ///
+ public nint ConfigChangeAddress { get; private set; }
+
+ ///
+ protected override void Setup64Bit(SigScanner scanner)
+ {
+ this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E");
+ }
+}
diff --git a/Dalamud/Game/Config/GameConfigEnumExtensions.cs b/Dalamud/Game/Config/GameConfigEnumExtensions.cs
index f880ee1b2..69087350b 100644
--- a/Dalamud/Game/Config/GameConfigEnumExtensions.cs
+++ b/Dalamud/Game/Config/GameConfigEnumExtensions.cs
@@ -1,4 +1,6 @@
-using Dalamud.Utility;
+using System.Collections.Concurrent;
+
+using Dalamud.Utility;
namespace Dalamud.Game.Config;
@@ -7,6 +9,10 @@ namespace Dalamud.Game.Config;
///
internal static class GameConfigEnumExtensions
{
+ private static readonly ConcurrentDictionary SystemNameCache = new();
+ private static readonly ConcurrentDictionary UIConfigNameCache = new();
+ private static readonly ConcurrentDictionary UIControlNameCache = new();
+
///
/// Gets the name of a SystemConfigOption from it's attribute.
///
@@ -14,7 +20,10 @@ internal static class GameConfigEnumExtensions
/// Name of the option.
public static string GetName(this SystemConfigOption systemConfigOption)
{
- return systemConfigOption.GetAttribute()?.Name ?? $"{systemConfigOption}";
+ if (SystemNameCache.TryGetValue(systemConfigOption, out var name)) return name;
+ name = systemConfigOption.GetAttribute()?.Name ?? $"{systemConfigOption}";
+ SystemNameCache.TryAdd(systemConfigOption, name);
+ return name;
}
///
@@ -24,7 +33,10 @@ internal static class GameConfigEnumExtensions
/// Name of the option.
public static string GetName(this UiConfigOption uiConfigOption)
{
- return uiConfigOption.GetAttribute()?.Name ?? $"{uiConfigOption}";
+ if (UIConfigNameCache.TryGetValue(uiConfigOption, out var name)) return name;
+ name = uiConfigOption.GetAttribute()?.Name ?? $"{uiConfigOption}";
+ UIConfigNameCache.TryAdd(uiConfigOption, name);
+ return name;
}
///
@@ -34,6 +46,9 @@ internal static class GameConfigEnumExtensions
/// Name of the option.
public static string GetName(this UiControlOption uiControlOption)
{
- return uiControlOption.GetAttribute()?.Name ?? $"{uiControlOption}";
+ if (UIControlNameCache.TryGetValue(uiControlOption, out var name)) return name;
+ name = uiControlOption.GetAttribute()?.Name ?? $"{uiControlOption}";
+ UIControlNameCache.TryAdd(uiControlOption, name);
+ return name;
}
}
diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs
index 107b0d4a8..6c87ad3cf 100644
--- a/Dalamud/Game/Config/GameConfigSection.cs
+++ b/Dalamud/Game/Config/GameConfigSection.cs
@@ -1,8 +1,9 @@
using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
using System.Diagnostics;
using Dalamud.Memory;
+using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Common.Configuration;
using Serilog;
@@ -14,8 +15,13 @@ namespace Dalamud.Game.Config;
public class GameConfigSection
{
private readonly Framework framework;
- private readonly Dictionary indexMap = new();
- private readonly Dictionary nameMap = new();
+ private readonly ConcurrentDictionary indexMap = new();
+ private readonly ConcurrentDictionary enumMap = new();
+
+ ///
+ /// Event which is fired when a game config option is changed within the section.
+ ///
+ public event EventHandler Changed;
///
/// Initializes a new instance of the class.
@@ -59,7 +65,10 @@ public class GameConfigSection
///
public string SectionName { get; }
- private GetConfigBaseDelegate GetConfigBase { get; }
+ ///
+ /// Gets the pointer to the config section container.
+ ///
+ internal GetConfigBaseDelegate GetConfigBase { get; }
///
/// Attempts to get a boolean config option.
@@ -380,6 +389,128 @@ public class GameConfigSection
});
}
+ ///
+ /// Attempts to get the properties of a UInt option from the config section.
+ ///
+ /// Name of the option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public unsafe bool TryGetProperties(string name, out UIntConfigProperties? properties)
+ {
+ if (!this.TryGetIndex(name, out var index))
+ {
+ properties = null;
+ return false;
+ }
+
+ if (!this.TryGetEntry(index, out var entry))
+ {
+ properties = null;
+ return false;
+ }
+
+ if ((ConfigType)entry->Type != ConfigType.UInt)
+ {
+ properties = null;
+ return false;
+ }
+
+ var prop = &entry->Properties.UInt;
+ properties = new UIntConfigProperties(prop->DefaultValue, prop->MinValue, prop->MaxValue);
+ return true;
+ }
+
+ ///
+ /// Attempts to get the properties of a Float option from the config section.
+ ///
+ /// Name of the option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public unsafe bool TryGetProperties(string name, out FloatConfigProperties? properties)
+ {
+ if (!this.TryGetIndex(name, out var index))
+ {
+ properties = null;
+ return false;
+ }
+
+ if (!this.TryGetEntry(index, out var entry))
+ {
+ properties = null;
+ return false;
+ }
+
+ if ((ConfigType)entry->Type != ConfigType.Float)
+ {
+ properties = null;
+ return false;
+ }
+
+ var prop = &entry->Properties.Float;
+ properties = new FloatConfigProperties(prop->DefaultValue, prop->MinValue, prop->MaxValue);
+ return true;
+ }
+
+ ///
+ /// Attempts to get the properties of a String option from the config section.
+ ///
+ /// Name of the option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public unsafe bool TryGetProperties(string name, out StringConfigProperties? properties)
+ {
+ if (!this.TryGetIndex(name, out var index))
+ {
+ properties = null;
+ return false;
+ }
+
+ if (!this.TryGetEntry(index, out var entry))
+ {
+ properties = null;
+ return false;
+ }
+
+ if ((ConfigType)entry->Type != ConfigType.String)
+ {
+ properties = null;
+ return false;
+ }
+
+ var prop = entry->Properties.String;
+ properties = new StringConfigProperties(prop.DefaultValue == null ? null : MemoryHelper.ReadSeString(prop.DefaultValue));
+ return true;
+ }
+
+ ///
+ /// Invokes a change event within the config section.
+ ///
+ /// The config entry that was changed.
+ /// SystemConfigOption, UiConfigOption, or UiControlOption.
+ /// The ConfigChangeEvent record.
+ internal unsafe ConfigChangeEvent? InvokeChange(ConfigEntry* entry) where TEnum : Enum
+ {
+ if (!this.enumMap.TryGetValue(entry->Index, out var enumObject))
+ {
+ if (entry->Name == null) return null;
+ var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name));
+ if (Enum.TryParse(typeof(TEnum), name, out enumObject))
+ {
+ this.enumMap.TryAdd(entry->Index, enumObject);
+ }
+ else
+ {
+ enumObject = null;
+ this.enumMap.TryAdd(entry->Index, null);
+ }
+ }
+
+ if (enumObject == null) return null;
+ var eventArgs = new ConfigChangeEvent((TEnum)enumObject);
+ this.Changed?.InvokeSafely(this, eventArgs);
+ return eventArgs;
+ }
+
private unsafe bool TryGetIndex(string name, out uint index)
{
if (this.indexMap.TryGetValue(name, out index))
@@ -400,7 +531,6 @@ public class GameConfigSection
if (eName.Equals(name))
{
this.indexMap.TryAdd(name, i);
- this.nameMap.TryAdd(i, name);
index = i;
return true;
}
diff --git a/Dalamud/Game/Config/Properties.cs b/Dalamud/Game/Config/Properties.cs
new file mode 100644
index 000000000..b43a44a47
--- /dev/null
+++ b/Dalamud/Game/Config/Properties.cs
@@ -0,0 +1,7 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Game.Config;
+
+public record StringConfigProperties(SeString? Default);
+public record UIntConfigProperties(uint Default, uint Minimum, uint Maximum);
+public record FloatConfigProperties(float Default, float Minimum, float Maximum);
diff --git a/Dalamud/Game/Config/SystemConfigOption.cs b/Dalamud/Game/Config/SystemConfigOption.cs
index 40d6b2fc9..70982311a 100644
--- a/Dalamud/Game/Config/SystemConfigOption.cs
+++ b/Dalamud/Game/Config/SystemConfigOption.cs
@@ -1,6 +1,4 @@
-using FFXIVClientStructs.FFXIV.Common.Configuration;
-
-namespace Dalamud.Game.Config;
+namespace Dalamud.Game.Config;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs
index 44be0d380..82f823ffe 100644
--- a/Dalamud/Game/Config/UiConfigOption.cs
+++ b/Dalamud/Game/Config/UiConfigOption.cs
@@ -1,6 +1,4 @@
-using FFXIVClientStructs.FFXIV.Common.Configuration;
-
-namespace Dalamud.Game.Config;
+namespace Dalamud.Game.Config;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
@@ -3314,4 +3312,165 @@ public enum UiConfigOption
///
[GameConfigOption("PvPFrontlinesGCFree", ConfigType.UInt)]
PvPFrontlinesGCFree,
+
+ ///
+ /// System option with the internal name PetMirageTypeFairy.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("PetMirageTypeFairy", ConfigType.UInt)]
+ PetMirageTypeFairy,
+
+ ///
+ /// System option with the internal name ExHotbarChangeHotbar1IsFashion.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("ExHotbarChangeHotbar1IsFashion", ConfigType.UInt)]
+ ExHotbarChangeHotbar1IsFashion,
+
+ ///
+ /// System option with the internal name HotbarCrossUseExDirectionAutoSwitch.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("HotbarCrossUseExDirectionAutoSwitch", ConfigType.UInt)]
+ HotbarCrossUseExDirectionAutoSwitch,
+
+ ///
+ /// System option with the internal name NamePlateDispJobIcon.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispJobIcon", ConfigType.UInt)]
+ NamePlateDispJobIcon,
+
+ ///
+ /// System option with the internal name NamePlateDispJobIconType.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispJobIconType", ConfigType.UInt)]
+ NamePlateDispJobIconType,
+
+ ///
+ /// System option with the internal name NamePlateSetRoleColor.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateSetRoleColor", ConfigType.UInt)]
+ NamePlateSetRoleColor,
+
+ ///
+ /// System option with the internal name NamePlateColorTank.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateColorTank", ConfigType.UInt)]
+ NamePlateColorTank,
+
+ ///
+ /// System option with the internal name NamePlateEdgeTank.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateEdgeTank", ConfigType.UInt)]
+ NamePlateEdgeTank,
+
+ ///
+ /// System option with the internal name NamePlateColorHealer.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateColorHealer", ConfigType.UInt)]
+ NamePlateColorHealer,
+
+ ///
+ /// System option with the internal name NamePlateEdgeHealer.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateEdgeHealer", ConfigType.UInt)]
+ NamePlateEdgeHealer,
+
+ ///
+ /// System option with the internal name NamePlateColorDps.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateColorDps", ConfigType.UInt)]
+ NamePlateColorDps,
+
+ ///
+ /// System option with the internal name NamePlateEdgeDps.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateEdgeDps", ConfigType.UInt)]
+ NamePlateEdgeDps,
+
+ ///
+ /// System option with the internal name NamePlateColorOtherClass.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateColorOtherClass", ConfigType.UInt)]
+ NamePlateColorOtherClass,
+
+ ///
+ /// System option with the internal name NamePlateEdgeOtherClass.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateEdgeOtherClass", ConfigType.UInt)]
+ NamePlateEdgeOtherClass,
+
+ ///
+ /// System option with the internal name NamePlateDispWorldTravel.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("NamePlateDispWorldTravel", ConfigType.UInt)]
+ NamePlateDispWorldTravel,
+
+ ///
+ /// System option with the internal name LogNameIconType.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogNameIconType", ConfigType.UInt)]
+ LogNameIconType,
+
+ ///
+ /// System option with the internal name LogDispClassJobName.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogDispClassJobName", ConfigType.UInt)]
+ LogDispClassJobName,
+
+ ///
+ /// System option with the internal name LogSetRoleColor.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogSetRoleColor", ConfigType.UInt)]
+ LogSetRoleColor,
+
+ ///
+ /// System option with the internal name LogColorRoleTank.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogColorRoleTank", ConfigType.UInt)]
+ LogColorRoleTank,
+
+ ///
+ /// System option with the internal name LogColorRoleHealer.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogColorRoleHealer", ConfigType.UInt)]
+ LogColorRoleHealer,
+
+ ///
+ /// System option with the internal name LogColorRoleDPS.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogColorRoleDPS", ConfigType.UInt)]
+ LogColorRoleDPS,
+
+ ///
+ /// System option with the internal name LogColorOtherClass.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("LogColorOtherClass", ConfigType.UInt)]
+ LogColorOtherClass,
+
+ ///
+ /// System option with the internal name ItemInventryStoreEnd.
+ /// This option is a UInt.
+ ///
+ [GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)]
+ ItemInventryStoreEnd,
}
diff --git a/Dalamud/Game/Config/UiControlOption.cs b/Dalamud/Game/Config/UiControlOption.cs
index 742df6b9f..5d36ee84d 100644
--- a/Dalamud/Game/Config/UiControlOption.cs
+++ b/Dalamud/Game/Config/UiControlOption.cs
@@ -1,6 +1,4 @@
-using FFXIVClientStructs.FFXIV.Common.Configuration;
-
-namespace Dalamud.Game.Config;
+namespace Dalamud.Game.Config;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs
index 11eeff9f3..49fc874e3 100644
--- a/Dalamud/Game/DutyState/DutyState.cs
+++ b/Dalamud/Game/DutyState/DutyState.cs
@@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Dalamud.Utility;
namespace Dalamud.Game.DutyState;
@@ -15,7 +16,10 @@ namespace Dalamud.Game.DutyState;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
-public unsafe class DutyState : IDisposable, IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public unsafe class DutyState : IDisposable, IServiceType, IDutyState
{
private readonly DutyStateAddressResolver address;
private readonly Hook contentDirectorNetworkMessageHook;
@@ -44,42 +48,24 @@ public unsafe class DutyState : IDisposable, IServiceType
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3);
- ///
- /// Event that gets fired when the duty starts. Triggers when the "Duty Start"
- /// message displays, and on the remove of the ring at duty's spawn.
- ///
+ ///
public event EventHandler DutyStarted;
- ///
- /// Event that gets fired when everyone in the party dies and the screen fades to black.
- ///
+ ///
public event EventHandler DutyWiped;
-
- ///
- /// Event that gets fired when the "Duty Recommence" message displays,
- /// and on the remove the the ring at duty's spawn.
- ///
+
+ ///
public event EventHandler DutyRecommenced;
-
- ///
- /// Event that gets fired when the duty is completed successfully.
- ///
+
+ ///
public event EventHandler DutyCompleted;
-
- ///
- /// Gets a value indicating whether the current duty has been started.
- ///
+
+ ///
public bool IsDutyStarted { get; private set; }
- ///
- /// Gets or sets a value indicating whether the current duty has been completed or not.
- /// Prevents DutyStarted from triggering if combat is entered after receiving a duty complete network event.
- ///
private bool CompletedThisTerritory { get; set; }
- ///
- /// Dispose of managed and unmanaged resources.
- ///
+ ///
void IDisposable.Dispose()
{
this.contentDirectorNetworkMessageHook.Dispose();
@@ -171,9 +157,6 @@ public unsafe class DutyState : IDisposable, IServiceType
else if (!this.IsBoundByDuty() && this.IsDutyStarted)
{
this.IsDutyStarted = false;
-
- // Could potentially add a call to DutyCompleted here since this
- // should only be reached if we are actually no longer in a duty, and missed the network event.
}
}
diff --git a/Dalamud/Game/GameLifecycle.cs b/Dalamud/Game/GameLifecycle.cs
index 3a6733512..5c1acc989 100644
--- a/Dalamud/Game/GameLifecycle.cs
+++ b/Dalamud/Game/GameLifecycle.cs
@@ -2,6 +2,7 @@
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
namespace Dalamud.Game;
@@ -11,7 +12,10 @@ namespace Dalamud.Game;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public class GameLifecycle : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public class GameLifecycle : IServiceType, IGameLifecycle
{
private readonly CancellationTokenSource dalamudUnloadCts = new();
private readonly CancellationTokenSource gameShutdownCts = new();
@@ -26,19 +30,13 @@ public class GameLifecycle : IServiceType
{
}
- ///
- /// Gets a token that is cancelled when Dalamud is unloading.
- ///
+ ///
public CancellationToken DalamudUnloadingToken => this.dalamudUnloadCts.Token;
- ///
- /// Gets a token that is cancelled when the game is shutting down.
- ///
+ ///
public CancellationToken GameShuttingDownToken => this.gameShutdownCts.Token;
- ///
- /// Gets a token that is cancelled when a character is logging out.
- ///
+ ///
public CancellationToken LogoutToken => this.logoutCts.Token;
///
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index d77b406f0..dd1e7aa30 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -6,6 +6,7 @@ using Dalamud.Configuration.Internal;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
@@ -18,7 +19,10 @@ namespace Dalamud.Game.Gui.Dtr;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed unsafe class DtrBar : IDisposable, IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
{
private const uint BaseNodeId = 1000;
@@ -44,14 +48,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType
this.configuration.QueueSave();
}
- ///
- /// Get a DTR bar entry.
- /// This allows you to add your own text, and users to sort it.
- ///
- /// A user-friendly name for sorting.
- /// The text the entry shows.
- /// The entry object used to update, hide and remove the entry.
- /// Thrown when an entry with the specified title exists.
+ ///
public DtrBarEntry Get(string title, SeString? text = null)
{
if (this.entries.Any(x => x.Title == title))
@@ -134,7 +131,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType
});
}
- private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer();
+ private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer();
private void Update(Framework unused)
{
@@ -293,7 +290,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType
newTextNode->AtkResNode.NodeID = nodeId;
newTextNode->AtkResNode.Type = NodeType.Text;
- newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop);
+ newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop;
newTextNode->AtkResNode.DrawFlags = 12;
newTextNode->AtkResNode.SetWidth(22);
newTextNode->AtkResNode.SetHeight(22);
diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs
index 59c136416..0235bef5a 100644
--- a/Dalamud/Game/Gui/GameGui.cs
+++ b/Dalamud/Game/Gui/GameGui.cs
@@ -7,6 +7,7 @@ using Dalamud.Hooking;
using Dalamud.Interface;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.String;
@@ -28,7 +29,10 @@ namespace Dalamud.Game.Gui;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed unsafe class GameGui : IDisposable, IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
{
private readonly GameGuiAddressResolver address;
@@ -112,43 +116,26 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte);
-
- ///
- /// Event which is fired when the game UI hiding is toggled.
- ///
+
+ ///
public event EventHandler UiHideToggled;
- ///
- /// Event that is fired when the currently hovered item changes.
- ///
+ ///
public event EventHandler HoveredItemChanged;
- ///
- /// Event that is fired when the currently hovered action changes.
- ///
+ ///
public event EventHandler HoveredActionChanged;
- ///
- /// Gets a value indicating whether the game UI is hidden.
- ///
+ ///
public bool GameUiHidden { get; private set; }
- ///
- /// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered.
- /// If > 1.000.000, subtract 1.000.000 and treat it as HQ.
- ///
+ ///
public ulong HoveredItem { get; set; }
- ///
- /// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
- ///
+ ///
public HoveredAction HoveredAction { get; } = new HoveredAction();
- ///
- /// Opens the in-game map with a flag on the location of the parameter.
- ///
- /// Link to the map to be opened.
- /// True if there were no errors and it could open the map.
+ ///
public bool OpenMapWithMapLink(MapLinkPayload mapLink)
{
var uiModule = this.GetUIModule();
@@ -178,22 +165,11 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
return this.openMapWithFlag(uiMapObjectPtr, mapLinkString);
}
- ///
- /// Converts in-world coordinates to screen coordinates (upper left corner origin).
- ///
- /// Coordinates in the world.
- /// Converted coordinates.
- /// True if worldPos corresponds to a position in front of the camera and screenPos is in the viewport.
+ ///
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
=> this.WorldToScreen(worldPos, out screenPos, out var inView) && inView;
- ///
- /// Converts in-world coordinates to screen coordinates (upper left corner origin).
- ///
- /// Coordinates in the world.
- /// Converted coordinates.
- /// True if screenPos corresponds to a position inside the camera viewport.
- /// True if worldPos corresponds to a position in front of the camera.
+ ///
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView)
{
// Get base object with matrices
@@ -220,13 +196,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
return inFront;
}
- ///
- /// Converts screen coordinates to in-world coordinates via raycasting.
- ///
- /// Screen coordinates.
- /// Converted coordinates.
- /// How far to search for a collision.
- /// True if successful. On false, worldPos's contents are undefined.
+ ///
public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f)
{
// The game is only visible in the main viewport, so if the cursor is outside
@@ -290,10 +260,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
return isSuccess;
}
- ///
- /// Gets a pointer to the game's UI module.
- ///
- /// IntPtr pointing to UI module.
+ ///
public IntPtr GetUIModule()
{
var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
@@ -307,12 +274,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
return (IntPtr)uiModule;
}
- ///
- /// Gets the pointer to the Addon with the given name and index.
- ///
- /// Name of addon to find.
- /// Index of addon to find (1-indexed).
- /// IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the addon.
+ ///
public IntPtr GetAddonByName(string name, int index = 1)
{
var atkStage = AtkStage.GetSingleton();
@@ -330,29 +292,17 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
return (IntPtr)addon;
}
- ///
- /// Find the agent associated with an addon, if possible.
- ///
- /// The addon name.
- /// A pointer to the agent interface.
+ ///
public IntPtr FindAgentInterface(string addonName)
{
var addon = this.GetAddonByName(addonName);
return this.FindAgentInterface(addon);
}
- ///
- /// Find the agent associated with an addon, if possible.
- ///
- /// The addon address.
- /// A pointer to the agent interface.
+ ///
public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon);
-
- ///
- /// Find the agent associated with an addon, if possible.
- ///
- /// The addon address.
- /// A pointer to the agent interface.
+
+ ///
public IntPtr FindAgentInterface(IntPtr addonPtr)
{
if (addonPtr == IntPtr.Zero)
diff --git a/Dalamud/Game/Libc/LibcFunction.cs b/Dalamud/Game/Libc/LibcFunction.cs
index 4c58376f2..7dfc26b3b 100644
--- a/Dalamud/Game/Libc/LibcFunction.cs
+++ b/Dalamud/Game/Libc/LibcFunction.cs
@@ -4,6 +4,7 @@ using System.Text;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
namespace Dalamud.Game.Libc;
@@ -13,7 +14,10 @@ namespace Dalamud.Game.Libc;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-public sealed class LibcFunction : IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public sealed class LibcFunction : IServiceType, ILibcFunction
{
private readonly LibcFunctionAddressResolver address;
private readonly StdStringFromCStringDelegate stdStringCtorCString;
@@ -37,11 +41,7 @@ public sealed class LibcFunction : IServiceType
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr StdStringDeallocateDelegate(IntPtr address);
- ///
- /// Create a new string from the given bytes.
- ///
- /// The bytes to convert.
- /// An owned std string object.
+ ///
public OwnedStdString NewString(byte[] content)
{
// While 0x70 bytes in the memory should be enough in DX11 version,
@@ -56,14 +56,9 @@ public sealed class LibcFunction : IServiceType
return new OwnedStdString(pReallocString, this.DeallocateStdString);
}
-
- ///
- /// Create a new string form the given bytes.
- ///
- /// The bytes to convert.
- /// A non-default encoding.
- /// An owned std string object.
- public OwnedStdString NewString(string content, Encoding encoding = null)
+
+ ///
+ public OwnedStdString NewString(string content, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs
index b19024098..b5fe0b5b3 100644
--- a/Dalamud/Game/SigScanner.cs
+++ b/Dalamud/Game/SigScanner.cs
@@ -15,12 +15,17 @@ using Serilog;
namespace Dalamud.Game;
+// TODO(v9): There are static functions here that we can't keep due to interfaces
+
///
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
///
[PluginInterface]
[InterfaceVersion("1.0")]
-public class SigScanner : IDisposable, IServiceType
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public class SigScanner : IDisposable, IServiceType, ISigScanner
{
private readonly FileInfo? cacheFile;
@@ -65,69 +70,43 @@ public class SigScanner : IDisposable, IServiceType
this.Load();
}
- ///
- /// Gets a value indicating whether or not the search on this module is performed on a copy.
- ///
+ ///
public bool IsCopy { get; }
- ///
- /// Gets a value indicating whether or not the ProcessModule is 32-bit.
- ///
+ ///
public bool Is32BitProcess { get; }
- ///
- /// Gets the base address of the search area. When copied, this will be the address of the copy.
- ///
+ ///
public IntPtr SearchBase => this.IsCopy ? this.moduleCopyPtr : this.Module.BaseAddress;
- ///
- /// Gets the base address of the .text section search area.
- ///
+ ///
public IntPtr TextSectionBase => new(this.SearchBase.ToInt64() + this.TextSectionOffset);
- ///
- /// Gets the offset of the .text section from the base of the module.
- ///
+ ///
public long TextSectionOffset { get; private set; }
- ///
- /// Gets the size of the text section.
- ///
+ ///
public int TextSectionSize { get; private set; }
- ///
- /// Gets the base address of the .data section search area.
- ///
+ ///
public IntPtr DataSectionBase => new(this.SearchBase.ToInt64() + this.DataSectionOffset);
- ///
- /// Gets the offset of the .data section from the base of the module.
- ///
+ ///
public long DataSectionOffset { get; private set; }
- ///
- /// Gets the size of the .data section.
- ///
+ ///
public int DataSectionSize { get; private set; }
- ///
- /// Gets the base address of the .rdata section search area.
- ///
+ ///
public IntPtr RDataSectionBase => new(this.SearchBase.ToInt64() + this.RDataSectionOffset);
- ///
- /// Gets the offset of the .rdata section from the base of the module.
- ///
+ ///
public long RDataSectionOffset { get; private set; }
- ///
- /// Gets the size of the .rdata section.
- ///
+ ///
public int RDataSectionSize { get; private set; }
- ///
- /// Gets the ProcessModule on which the search is performed.
- ///
+ ///
public ProcessModule Module { get; }
private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize;
@@ -229,11 +208,7 @@ public class SigScanner : IDisposable, IServiceType
}
}
- ///
- /// Scan for a byte signature in the .data section.
- ///
- /// The signature.
- /// The real offset of the found signature.
+ ///
public IntPtr ScanData(string signature)
{
var scanRet = Scan(this.DataSectionBase, this.DataSectionSize, signature);
@@ -244,12 +219,7 @@ public class SigScanner : IDisposable, IServiceType
return scanRet;
}
- ///
- /// Try scanning for a byte signature in the .data section.
- ///
- /// The signature.
- /// The real offset of the signature, if found.
- /// true if the signature was found.
+ ///
public bool TryScanData(string signature, out IntPtr result)
{
try
@@ -264,11 +234,7 @@ public class SigScanner : IDisposable, IServiceType
}
}
- ///
- /// Scan for a byte signature in the whole module search area.
- ///
- /// The signature.
- /// The real offset of the found signature.
+ ///
public IntPtr ScanModule(string signature)
{
var scanRet = Scan(this.SearchBase, this.Module.ModuleMemorySize, signature);
@@ -279,12 +245,7 @@ public class SigScanner : IDisposable, IServiceType
return scanRet;
}
- ///
- /// Try scanning for a byte signature in the whole module search area.
- ///
- /// The signature.
- /// The real offset of the signature, if found.
- /// true if the signature was found.
+ ///
public bool TryScanModule(string signature, out IntPtr result)
{
try
@@ -299,23 +260,14 @@ public class SigScanner : IDisposable, IServiceType
}
}
- ///
- /// Resolve a RVA address.
- ///
- /// The address of the next instruction.
- /// The relative offset.
- /// The calculated offset.
+ ///
public IntPtr ResolveRelativeAddress(IntPtr nextInstAddr, int relOffset)
{
if (this.Is32BitProcess) throw new NotSupportedException("32 bit is not supported.");
return nextInstAddr + relOffset;
}
- ///
- /// Scan for a byte signature in the .text section.
- ///
- /// The signature.
- /// The real offset of the found signature.
+ ///
public IntPtr ScanText(string signature)
{
if (this.textCache != null)
@@ -347,12 +299,7 @@ public class SigScanner : IDisposable, IServiceType
return scanRet;
}
- ///
- /// Try scanning for a byte signature in the .text section.
- ///
- /// The signature.
- /// The real offset of the signature, if found.
- /// true if the signature was found.
+ ///
public bool TryScanText(string signature, out IntPtr result)
{
try
diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs
new file mode 100644
index 000000000..28a2644a5
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+
+// ReSharper disable UnusedMember.Local
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+namespace Dalamud.Interface.DragDrop;
+
+#pragma warning disable SA1600 // Elements should be documented
+/// Implements interop enums and function calls to interact with external drag and drop.
+internal partial class DragDropManager
+{
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("00000122-0000-0000-C000-000000000046")]
+ [ComImport]
+ public interface IDropTarget
+ {
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void DragEnter([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void DragOver([ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void DragLeave();
+
+ [MethodImpl(MethodImplOptions.InternalCall)]
+ void Drop([MarshalAs(UnmanagedType.Interface), In] IDataObject pDataObj, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In] uint grfKeyState, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.POINTL"), In] POINTL pt, [ComAliasName("Microsoft.VisualStudio.OLE.Interop.DWORD"), In, Out] ref uint pdwEffect);
+ }
+
+ internal struct POINTL
+ {
+ [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")]
+ public int x;
+ [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")]
+ public int y;
+ }
+
+ private static class DragDropInterop
+ {
+ [Flags]
+ public enum ModifierKeys
+ {
+ MK_NONE = 0x00,
+ MK_LBUTTON = 0x01,
+ MK_RBUTTON = 0x02,
+ MK_SHIFT = 0x04,
+ MK_CONTROL = 0x08,
+ MK_MBUTTON = 0x10,
+ MK_ALT = 0x20,
+ }
+
+ public enum ClipboardFormat
+ {
+ CF_TEXT = 1,
+ CF_BITMAP = 2,
+ CF_DIB = 3,
+ CF_UNICODETEXT = 13,
+ CF_HDROP = 15,
+ }
+
+ [Flags]
+ public enum DVAspect
+ {
+ DVASPECT_CONTENT = 0x01,
+ DVASPECT_THUMBNAIL = 0x02,
+ DVASPECT_ICON = 0x04,
+ DVASPECT_DOCPRINT = 0x08,
+ }
+
+ [Flags]
+ public enum TYMED
+ {
+ TYMED_NULL = 0x00,
+ TYMED_HGLOBAL = 0x01,
+ TYMED_FILE = 0x02,
+ TYMED_ISTREAM = 0x04,
+ TYMED_ISTORAGE = 0x08,
+ TYMED_GDI = 0x10,
+ TYMED_MFPICT = 0x20,
+ TYMED_ENHMF = 0x40,
+ }
+
+ [Flags]
+ public enum DropEffects : uint
+ {
+ None = 0x00_0000_00,
+ Copy = 0x00_0000_01,
+ Move = 0x00_0000_02,
+ Link = 0x00_0000_04,
+ Scroll = 0x80_0000_00,
+ }
+
+ [DllImport("ole32.dll")]
+ public static extern int RegisterDragDrop(nint hwnd, IDropTarget pDropTarget);
+
+ [DllImport("ole32.dll")]
+ public static extern int RevokeDragDrop(nint hwnd);
+
+ [DllImport("shell32.dll")]
+ public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch);
+ }
+}
+#pragma warning restore SA1600 // Elements should be documented
diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs
new file mode 100644
index 000000000..8336edc11
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/DragDropManager.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Dalamud.Interface.Internal;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using ImGuiNET;
+using Serilog;
+
+namespace Dalamud.Interface.DragDrop;
+
+///
+/// A manager that keeps state of external windows drag and drop events,
+/// and can be used to create ImGui drag and drop sources and targets for those external events.
+///
+[PluginInterface]
+[ServiceManager.EarlyLoadedService]
+[ResolveVia]
+internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType
+{
+ private nint windowHandlePtr = nint.Zero;
+
+ private int lastDropFrame = -2;
+ private int lastTooltipFrame = -1;
+
+ [ServiceManager.ServiceConstructor]
+ private DragDropManager()
+ {
+ Service.GetAsync()
+ .ContinueWith(t =>
+ {
+ this.windowHandlePtr = t.Result.Manager.WindowHandlePtr;
+ this.Enable();
+ });
+ }
+
+ /// Gets a value indicating whether external drag and drop is available at all.
+ public bool ServiceAvailable { get; private set; }
+
+ /// Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport.
+ public bool IsDragging { get; private set; }
+
+ /// Gets a value indicating whether there are any files or directories currently being dragged, or stored from the last drop.
+ public bool HasPaths
+ => this.Files.Count + this.Directories.Count > 0;
+
+ /// Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport, or stored from the last drop.
+ public IReadOnlyList Files { get; private set; } = Array.Empty();
+
+ /// Gets a set of all extensions available in the paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop.
+ public IReadOnlySet Extensions { get; private set; } = new HashSet();
+
+ /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop.
+ public IReadOnlyList Directories { get; private set; } = Array.Empty();
+
+ /// Enable external drag and drop.
+ public void Enable()
+ {
+ if (this.ServiceAvailable || this.windowHandlePtr == nint.Zero)
+ {
+ return;
+ }
+
+ try
+ {
+ var ret = DragDropInterop.RegisterDragDrop(this.windowHandlePtr, this);
+ Log.Information($"[DragDrop] Registered window 0x{this.windowHandlePtr:X} for external drag and drop operations. ({ret})");
+ Marshal.ThrowExceptionForHR(ret);
+ this.ServiceAvailable = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Could not create windows drag and drop utility for window 0x{this.windowHandlePtr:X}:\n{ex}");
+ }
+ }
+
+ /// Disable external drag and drop.
+ public void Disable()
+ {
+ if (!this.ServiceAvailable)
+ {
+ return;
+ }
+
+ try
+ {
+ var ret = DragDropInterop.RevokeDragDrop(this.windowHandlePtr);
+ Log.Information($"[DragDrop] Disabled external drag and drop operations for window 0x{this.windowHandlePtr:X}. ({ret})");
+ Marshal.ThrowExceptionForHR(ret);
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Could not disable windows drag and drop utility for window 0x{this.windowHandlePtr:X}:\n{ex}");
+ }
+
+ this.ServiceAvailable = false;
+ }
+
+ ///
+ public void Dispose()
+ => this.Disable();
+
+ ///
+ public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder)
+ {
+ if (!this.IsDragging && !this.IsDropping())
+ {
+ return;
+ }
+
+ if (!validityCheck(this) || !ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceExtern))
+ {
+ return;
+ }
+
+ ImGui.SetDragDropPayload(label, nint.Zero, 0);
+ if (this.CheckTooltipFrame(out var frame) && tooltipBuilder(this))
+ {
+ this.lastTooltipFrame = frame;
+ }
+
+ ImGui.EndDragDropSource();
+ }
+
+ ///
+ public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories)
+ {
+ files = Array.Empty();
+ directories = Array.Empty();
+ if (!this.HasPaths || !ImGui.BeginDragDropTarget())
+ {
+ return false;
+ }
+
+ unsafe
+ {
+ if (ImGui.AcceptDragDropPayload(label, ImGuiDragDropFlags.AcceptBeforeDelivery).NativePtr != null && this.IsDropping())
+ {
+ this.lastDropFrame = -2;
+ files = this.Files;
+ directories = this.Directories;
+ return true;
+ }
+ }
+
+ ImGui.EndDragDropTarget();
+ return false;
+ }
+
+ private bool CheckTooltipFrame(out int frame)
+ {
+ frame = ImGui.GetFrameCount();
+ return this.lastTooltipFrame < frame;
+ }
+
+ private bool IsDropping()
+ {
+ var frame = ImGui.GetFrameCount();
+ return this.lastDropFrame == frame || this.lastDropFrame == frame - 1;
+ }
+}
diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs
new file mode 100644
index 000000000..5e7166fb3
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+
+using Dalamud.Utility;
+using ImGuiNET;
+using Serilog;
+
+namespace Dalamud.Interface.DragDrop;
+
+/// Implements the IDropTarget interface to interact with external drag and dropping.
+internal partial class DragDropManager : DragDropManager.IDropTarget
+{
+ private int lastUpdateFrame = -1;
+ private DragDropInterop.ModifierKeys lastKeyState = DragDropInterop.ModifierKeys.MK_NONE;
+
+ /// Create the drag and drop formats we accept.
+ private FORMATETC formatEtc =
+ new()
+ {
+ cfFormat = (short)DragDropInterop.ClipboardFormat.CF_HDROP,
+ ptd = nint.Zero,
+ dwAspect = DVASPECT.DVASPECT_CONTENT,
+ lindex = -1,
+ tymed = TYMED.TYMED_HGLOBAL,
+ };
+
+ ///
+ /// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport.
+ ///
+ /// The drag and drop data.
+ /// The mouse button used to drag as well as key modifiers.
+ /// The global cursor position.
+ /// Effects that can be used with this drag and drop process.
+ public void DragEnter(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect)
+ {
+ this.IsDragging = true;
+ this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, true);
+
+ if (pDataObj.QueryGetData(ref this.formatEtc) != 0)
+ {
+ pdwEffect = 0;
+ }
+ else
+ {
+ pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
+ (this.Files, this.Directories) = this.GetPaths(pDataObj);
+ this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet();
+ }
+
+ Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y, this.Files.Count + this.Directories.Count);
+ }
+
+ /// Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport.
+ /// The mouse button used to drag as well as key modifiers.
+ /// The global cursor position.
+ /// Effects that can be used with this drag and drop process.
+ /// Can be invoked more often than once a XIV frame, so we are keeping track of frames to skip unnecessary updates.
+ public void DragOver(uint grfKeyState, POINTL pt, ref uint pdwEffect)
+ {
+ var frame = ImGui.GetFrameCount();
+ if (frame != this.lastUpdateFrame)
+ {
+ this.lastUpdateFrame = frame;
+ this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false);
+ pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
+ Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y);
+ }
+ }
+
+ /// Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports.
+ public void DragLeave()
+ {
+ this.IsDragging = false;
+ this.Files = Array.Empty();
+ this.Directories = Array.Empty();
+ this.Extensions = new HashSet();
+ MouseDrop(this.lastKeyState);
+ Log.Debug("[DragDrop] Leaving external Drag and Drop.");
+ }
+
+ /// Invoked whenever a drag process ends by dropping over any FFXIV-related viewport.
+ /// The drag and drop data.
+ /// The mouse button used to drag as well as key modifiers.
+ /// The global cursor position.
+ /// Effects that can be used with this drag and drop process.
+ public void Drop(IDataObject pDataObj, uint grfKeyState, POINTL pt, ref uint pdwEffect)
+ {
+ MouseDrop(this.lastKeyState);
+ this.lastDropFrame = ImGui.GetFrameCount();
+ this.IsDragging = false;
+ if (this.HasPaths)
+ {
+ pdwEffect &= (uint)DragDropInterop.DropEffects.Copy;
+ }
+ else
+ {
+ pdwEffect = 0;
+ }
+
+ Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y);
+ }
+
+ private static DragDropInterop.ModifierKeys UpdateIo(DragDropInterop.ModifierKeys keys, bool entering)
+ {
+ var io = ImGui.GetIO();
+ void UpdateMouse(int mouseIdx)
+ {
+ if (entering)
+ {
+ io.MouseDownDuration[mouseIdx] = 1f;
+ }
+
+ io.MouseDown[mouseIdx] = true;
+ io.AddMouseButtonEvent(mouseIdx, true);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON))
+ {
+ UpdateMouse(0);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON))
+ {
+ UpdateMouse(1);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON))
+ {
+ UpdateMouse(2);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_CONTROL))
+ {
+ io.KeyCtrl = true;
+ io.AddKeyEvent(ImGuiKey.LeftCtrl, true);
+ }
+ else
+ {
+ io.KeyCtrl = false;
+ io.AddKeyEvent(ImGuiKey.LeftCtrl, false);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_ALT))
+ {
+ io.KeyAlt = true;
+ io.AddKeyEvent(ImGuiKey.LeftAlt, true);
+ }
+ else
+ {
+ io.KeyAlt = false;
+ io.AddKeyEvent(ImGuiKey.LeftAlt, false);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_SHIFT))
+ {
+ io.KeyShift = true;
+ io.AddKeyEvent(ImGuiKey.LeftShift, true);
+ }
+ else
+ {
+ io.KeyShift = false;
+ io.AddKeyEvent(ImGuiKey.LeftShift, false);
+ }
+
+ return keys;
+ }
+
+ private static void MouseDrop(DragDropInterop.ModifierKeys keys)
+ {
+ var io = ImGui.GetIO();
+ void UpdateMouse(int mouseIdx)
+ {
+ io.AddMouseButtonEvent(mouseIdx, false);
+ io.MouseDown[mouseIdx] = false;
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_LBUTTON))
+ {
+ UpdateMouse(0);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_RBUTTON))
+ {
+ UpdateMouse(1);
+ }
+
+ if (keys.HasFlag(DragDropInterop.ModifierKeys.MK_MBUTTON))
+ {
+ UpdateMouse(2);
+ }
+ }
+
+ private (string[] Files, string[] Directories) GetPaths(IDataObject data)
+ {
+ if (!this.IsDragging)
+ {
+ return (Array.Empty(), Array.Empty());
+ }
+
+ try
+ {
+ data.GetData(ref this.formatEtc, out var stgMedium);
+ var numFiles = DragDropInterop.DragQueryFile(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0);
+ var files = new string[numFiles];
+ var sb = new StringBuilder(1024);
+ var directoryCount = 0;
+ var fileCount = 0;
+ for (var i = 0u; i < numFiles; ++i)
+ {
+ sb.Clear();
+ var ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity);
+ if (ret >= sb.Capacity)
+ {
+ sb.Capacity = ret + 1;
+ ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity);
+ }
+
+ if (ret > 0 && ret < sb.Capacity)
+ {
+ var s = sb.ToString();
+ if (Directory.Exists(s))
+ {
+ files[^(++directoryCount)] = s;
+ }
+ else
+ {
+ files[fileCount++] = s;
+ }
+ }
+ }
+
+ var fileArray = fileCount > 0 ? files.Take(fileCount).ToArray() : Array.Empty();
+ var directoryArray = directoryCount > 0 ? files.TakeLast(directoryCount).Reverse().ToArray() : Array.Empty();
+
+ return (fileArray, directoryArray);
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Error obtaining data from drag & drop:\n{ex}");
+ }
+
+ return (Array.Empty(), Array.Empty());
+ }
+}
diff --git a/Dalamud/Interface/DragDrop/IDragDropManager.cs b/Dalamud/Interface/DragDrop/IDragDropManager.cs
new file mode 100644
index 000000000..736c8af24
--- /dev/null
+++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+
+namespace Dalamud.Interface.DragDrop;
+
+///
+/// A service to handle external drag and drop from WinAPI.
+///
+public interface IDragDropManager
+{
+ /// Gets a value indicating whether Drag and Drop functionality is available at all.
+ public bool ServiceAvailable { get; }
+
+ /// Gets a value indicating whether anything is being dragged from an external application and over any of the games viewports.
+ public bool IsDragging { get; }
+
+ /// Gets the list of files currently being dragged from an external application over any of the games viewports.
+ public IReadOnlyList Files { get; }
+
+ /// Gets the set of file types by extension currently being dragged from an external application over any of the games viewports.
+ public IReadOnlySet Extensions { get; }
+
+ /// Gets the list of directories currently being dragged from an external application over any of the games viewports.
+ public IReadOnlyList Directories { get; }
+
+ /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source.
+ /// The label used for the drag & drop payload.
+ /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.
+ public void CreateImGuiSource(string label, Func validityCheck)
+ => this.CreateImGuiSource(label, validityCheck, _ => false);
+
+ /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source.
+ /// The label used for the drag & drop payload.
+ /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.
+ /// Executes ImGui functions to build a tooltip. Should return true if it creates any tooltip and false otherwise. If multiple sources are active, only the first non-empty tooltip type drawn in a frame will be used.
+ public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder);
+
+ /// Create an ImGui drag & drop target on the last ImGui object.
+ /// The label used for the drag & drop payload.
+ /// On success, contains the list of file paths dropped onto the target.
+ /// On success, contains the list of directory paths dropped onto the target.
+ /// True if items were dropped onto the target this frame, false otherwise.
+ public bool CreateImGuiTarget(string label, out IReadOnlyList files, out IReadOnlyList directories);
+}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 5de5f52de..6bb45b325 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -1147,10 +1147,10 @@ internal class InterfaceManager : IDisposable, IServiceType
var dPadRight = gamepadState.Raw(GamepadButtons.DpadRight) != 0;
var dPadDown = gamepadState.Raw(GamepadButtons.DpadDown) != 0;
var dPadLeft = gamepadState.Raw(GamepadButtons.DpadLeft) != 0;
- var leftStickUp = gamepadState.LeftStickUp;
- var leftStickRight = gamepadState.LeftStickRight;
- var leftStickDown = gamepadState.LeftStickDown;
- var leftStickLeft = gamepadState.LeftStickLeft;
+ var leftStickUp = gamepadState.LeftStick.Y > 0 ? gamepadState.LeftStick.Y / 100f : 0;
+ var leftStickRight = gamepadState.LeftStick.X > 0 ? gamepadState.LeftStick.X / 100f : 0;
+ var leftStickDown = gamepadState.LeftStick.Y < 0 ? -gamepadState.LeftStick.Y / 100f : 0;
+ var leftStickLeft = gamepadState.LeftStick.X < 0 ? -gamepadState.LeftStick.X / 100f : 0;
var l1Button = gamepadState.Raw(GamepadButtons.L1) != 0;
var l2Button = gamepadState.Raw(GamepadButtons.L2) != 0;
var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0;
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs
index 99c6cb6e9..d7c4eb095 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs
@@ -154,5 +154,10 @@ internal enum DataKind
///
/// Data Share.
///
- DataShare,
+ Data_Share,
+
+ ///
+ /// Network Monitor.
+ ///
+ Network_Monitor,
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index f392d3912..9d8dc1e93 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -48,6 +48,7 @@ internal class DataWindow : Window
new DtrBarWidget(),
new UIColorWidget(),
new DataShareWidget(),
+ new NetworkMonitorWidget(),
};
private readonly Dictionary dataKindNames = new();
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs
index 6ec741fe8..ec7124042 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs
@@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data;
internal class DataShareWidget : IDataWindowWidget
{
///
- public DataKind DataKind { get; init; } = DataKind.DataShare;
+ public DataKind DataKind { get; init; } = DataKind.Data_Share;
///
public bool Ready { get; set; }
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs
index 78a93c1cc..779032f1d 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs
@@ -58,7 +58,7 @@ internal class FateTableWidget : IDataWindowWidget
ImGui.TextUnformatted(fateString);
ImGui.SameLine();
- if (ImGui.Button("C"))
+ if (ImGui.Button($"C##{fate.Address.ToInt64():X}"))
{
ImGui.SetClipboardText(fate.Address.ToString("X"));
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs
index 5c92e3ad1..1a4408d53 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs
@@ -27,27 +27,6 @@ internal class GamepadWidget : IDataWindowWidget
{
var gamepadState = Service.Get();
- static void DrawHelper(string text, uint mask, Func resolve)
- {
- ImGui.Text($"{text} {mask:X4}");
- ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
- $"DPadUp {resolve(GamepadButtons.DpadUp)} " +
- $"DPadRight {resolve(GamepadButtons.DpadRight)} " +
- $"DPadDown {resolve(GamepadButtons.DpadDown)} ");
- ImGui.Text($"West {resolve(GamepadButtons.West)} " +
- $"North {resolve(GamepadButtons.North)} " +
- $"East {resolve(GamepadButtons.East)} " +
- $"South {resolve(GamepadButtons.South)} ");
- ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
- $"L2 {resolve(GamepadButtons.L2)} " +
- $"R1 {resolve(GamepadButtons.R1)} " +
- $"R2 {resolve(GamepadButtons.R2)} ");
- ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
- $"Start {resolve(GamepadButtons.Start)} " +
- $"L3 {resolve(GamepadButtons.L3)} " +
- $"R3 {resolve(GamepadButtons.R3)} ");
- }
-
ImGui.Text($"GamepadInput 0x{gamepadState.GamepadInputAddress.ToInt64():X}");
#if DEBUG
@@ -58,29 +37,44 @@ internal class GamepadWidget : IDataWindowWidget
ImGui.SetClipboardText($"0x{gamepadState.GamepadInputAddress.ToInt64():X}");
#endif
- DrawHelper(
+ this.DrawHelper(
"Buttons Raw",
gamepadState.ButtonsRaw,
gamepadState.Raw);
- DrawHelper(
+ this.DrawHelper(
"Buttons Pressed",
gamepadState.ButtonsPressed,
gamepadState.Pressed);
- DrawHelper(
+ this.DrawHelper(
"Buttons Repeat",
gamepadState.ButtonsRepeat,
gamepadState.Repeat);
- DrawHelper(
+ this.DrawHelper(
"Buttons Released",
gamepadState.ButtonsReleased,
gamepadState.Released);
- ImGui.Text($"LeftStickLeft {gamepadState.LeftStickLeft:0.00} " +
- $"LeftStickUp {gamepadState.LeftStickUp:0.00} " +
- $"LeftStickRight {gamepadState.LeftStickRight:0.00} " +
- $"LeftStickDown {gamepadState.LeftStickDown:0.00} ");
- ImGui.Text($"RightStickLeft {gamepadState.RightStickLeft:0.00} " +
- $"RightStickUp {gamepadState.RightStickUp:0.00} " +
- $"RightStickRight {gamepadState.RightStickRight:0.00} " +
- $"RightStickDown {gamepadState.RightStickDown:0.00} ");
+ ImGui.Text($"LeftStick {gamepadState.LeftStick}");
+ ImGui.Text($"RightStick {gamepadState.RightStick}");
+ }
+
+ private void DrawHelper(string text, uint mask, Func resolve)
+ {
+ ImGui.Text($"{text} {mask:X4}");
+ ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
+ $"DPadUp {resolve(GamepadButtons.DpadUp)} " +
+ $"DPadRight {resolve(GamepadButtons.DpadRight)} " +
+ $"DPadDown {resolve(GamepadButtons.DpadDown)} ");
+ ImGui.Text($"West {resolve(GamepadButtons.West)} " +
+ $"North {resolve(GamepadButtons.North)} " +
+ $"East {resolve(GamepadButtons.East)} " +
+ $"South {resolve(GamepadButtons.South)} ");
+ ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
+ $"L2 {resolve(GamepadButtons.L2)} " +
+ $"R1 {resolve(GamepadButtons.R1)} " +
+ $"R2 {resolve(GamepadButtons.R2)} ");
+ ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
+ $"Start {resolve(GamepadButtons.Start)} " +
+ $"L3 {resolve(GamepadButtons.L3)} " +
+ $"R3 {resolve(GamepadButtons.R3)} ");
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
new file mode 100644
index 000000000..01d0b1759
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.RegularExpressions;
+
+using Dalamud.Data;
+using Dalamud.Game.Network;
+using Dalamud.Interface.Raii;
+using Dalamud.Memory;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Data;
+
+///
+/// Widget to display the current packets.
+///
+internal class NetworkMonitorWidget : IDataWindowWidget
+{
+#pragma warning disable SA1313
+ private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId)
+#pragma warning restore SA1313
+ {
+ public readonly IReadOnlyList Data = Array.Empty();
+
+ public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr)
+ : this(opCode, direction, sourceActorId, targetActorId)
+ => this.Data = MemoryHelper.Read(dataPtr, widget.GetSizeFromOpCode(opCode), false);
+ }
+
+ private readonly ConcurrentQueue packets = new();
+ private readonly Dictionary opCodeDict = new();
+
+ private bool trackNetwork;
+ private int trackedPackets;
+ private Regex? trackedOpCodes;
+ private string filterString = string.Empty;
+ private Regex? untrackedOpCodes;
+ private string negativeFilterString = string.Empty;
+
+ /// Finalizes an instance of the class.
+ ~NetworkMonitorWidget()
+ {
+ if (this.trackNetwork)
+ {
+ this.trackNetwork = false;
+ var network = Service.GetNullable();
+ if (network != null)
+ {
+ network.NetworkMessage -= this.OnNetworkMessage;
+ }
+ }
+ }
+
+ ///
+ public DataKind DataKind { get; init; } = DataKind.Network_Monitor;
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.trackNetwork = false;
+ this.trackedPackets = 20;
+ this.trackedOpCodes = null;
+ this.filterString = string.Empty;
+ this.packets.Clear();
+ this.Ready = true;
+ var dataManager = Service.Get();
+ foreach (var (name, code) in dataManager.ClientOpCodes.Concat(dataManager.ServerOpCodes))
+ this.opCodeDict.TryAdd(code, (name, this.GetSizeFromName(name)));
+ }
+
+ ///
+ public void Draw()
+ {
+ var network = Service.Get();
+ if (ImGui.Checkbox("Track Network Packets", ref this.trackNetwork))
+ {
+ if (this.trackNetwork)
+ {
+ network.NetworkMessage += this.OnNetworkMessage;
+ }
+ else
+ {
+ network.NetworkMessage -= this.OnNetworkMessage;
+ }
+ }
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2);
+ if (ImGui.DragInt("Stored Number of Packets", ref this.trackedPackets, 0.1f, 1, 512))
+ {
+ this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512);
+ }
+
+ this.DrawFilterInput();
+ this.DrawNegativeFilterInput();
+
+ ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "Known Name", "OpCode", "Hex", "Target", "Source", "Data");
+ }
+
+ private void DrawNetworkPacket(NetworkPacketData data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Direction.ToString());
+
+ ImGui.TableNextColumn();
+ if (this.opCodeDict.TryGetValue(data.OpCode, out var pair))
+ {
+ ImGui.TextUnformatted(pair.Name);
+ }
+ else
+ {
+ ImGui.Dummy(new Vector2(150 * ImGuiHelpers.GlobalScale, 0));
+ }
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.OpCode.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted($"0x{data.OpCode:X4}");
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty);
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty);
+
+ ImGui.TableNextColumn();
+ if (data.Data.Count > 0)
+ {
+ ImGui.TextUnformatted(string.Join(" ", data.Data.Select(b => b.ToString("X2"))));
+ }
+ else
+ {
+ ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 });
+ }
+ }
+
+ private void DrawFilterInput()
+ {
+ var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == null;
+ using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
+ using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ if (!ImGui.InputTextWithHint("##Filter", "Regex Filter OpCodes...", ref this.filterString, 1024))
+ {
+ return;
+ }
+
+ if (this.filterString.Length == 0)
+ {
+ this.trackedOpCodes = null;
+ }
+ else
+ {
+ try
+ {
+ this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ }
+ catch
+ {
+ this.trackedOpCodes = null;
+ }
+ }
+ }
+
+ private void DrawNegativeFilterInput()
+ {
+ var invalidRegEx = this.negativeFilterString.Length > 0 && this.untrackedOpCodes == null;
+ using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
+ using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ if (!ImGui.InputTextWithHint("##NegativeFilter", "Regex Filter Against OpCodes...", ref this.negativeFilterString, 1024))
+ {
+ return;
+ }
+
+ if (this.negativeFilterString.Length == 0)
+ {
+ this.untrackedOpCodes = null;
+ }
+ else
+ {
+ try
+ {
+ this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+ }
+ catch
+ {
+ this.untrackedOpCodes = null;
+ }
+ }
+ }
+
+ private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
+ {
+ if ((this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode)))
+ && (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode))))
+ {
+ this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr));
+ while (this.packets.Count > this.trackedPackets)
+ {
+ this.packets.TryDequeue(out _);
+ }
+ }
+ }
+
+ private int GetSizeFromOpCode(ushort opCode)
+ => this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Size : 0;
+
+ /// Add known packet-name -> packet struct size associations here to copy the byte data for such packets. >
+ private int GetSizeFromName(string name)
+ => name switch
+ {
+ _ => 0,
+ };
+
+ /// The filter should find opCodes by number (decimal and hex) and name, if existing.
+ private string OpCodeToString(ushort opCode)
+ => this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{opCode}\0{opCode:X}\0{pair.Name}" : $"{opCode}\0{opCode:X}";
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs
index 07d6e8f72..57fd03300 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs
@@ -65,20 +65,20 @@ internal class TargetWidget : IDataWindowWidget
Util.PrintGameObject(targetMgr.SoftTarget, "SoftTarget", this.resolveGameData);
if (ImGui.Button("Clear CT"))
- targetMgr.ClearTarget();
+ targetMgr.Target = null;
if (ImGui.Button("Clear FT"))
- targetMgr.ClearFocusTarget();
+ targetMgr.FocusTarget = null;
var localPlayer = clientState.LocalPlayer;
if (localPlayer != null)
{
if (ImGui.Button("Set CT"))
- targetMgr.SetTarget(localPlayer);
+ targetMgr.Target = localPlayer;
if (ImGui.Button("Set FT"))
- targetMgr.SetFocusTarget(localPlayer);
+ targetMgr.FocusTarget = localPlayer;
}
else
{
diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
index 0aeb0722d..766f80b23 100644
--- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
@@ -11,6 +11,7 @@ using Dalamud.Game;
using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
using ImGuiScene;
using Serilog;
@@ -232,7 +233,7 @@ internal class PluginImageCache : IDisposable, IServiceType
/// If the plugin was third party sourced.
/// Cached image textures, or an empty array.
/// True if an entry exists, may be null if currently downloading.
- public bool TryGetIcon(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture)
+ public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture)
{
iconTexture = null;
@@ -274,7 +275,7 @@ internal class PluginImageCache : IDisposable, IServiceType
/// If the plugin was third party sourced.
/// Cached image textures, or an empty array.
/// True if the image array exists, may be empty if currently downloading.
- public bool TryGetImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures)
+ public bool TryGetImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures)
{
if (!this.pluginImagesMap.TryAdd(manifest.InternalName, null))
{
@@ -307,7 +308,7 @@ internal class PluginImageCache : IDisposable, IServiceType
byte[]? bytes,
string name,
string? loc,
- PluginManifest manifest,
+ IPluginManifest manifest,
int maxWidth,
int maxHeight,
bool requireSquare)
@@ -491,7 +492,7 @@ internal class PluginImageCache : IDisposable, IServiceType
Log.Debug("Plugin image loader has shutdown");
}
- private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame)
+ private async Task DownloadPluginIconAsync(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame)
{
if (plugin is { IsDev: true })
{
@@ -558,7 +559,7 @@ internal class PluginImageCache : IDisposable, IServiceType
return icon;
}
- private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame)
+ private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame)
{
if (plugin is { IsDev: true })
{
@@ -671,18 +672,15 @@ internal class PluginImageCache : IDisposable, IServiceType
}
}
- private string? GetPluginIconUrl(PluginManifest manifest, bool isThirdParty, bool isTesting)
+ private string? GetPluginIconUrl(IPluginManifest manifest, bool isThirdParty, bool isTesting)
{
if (isThirdParty)
return manifest.IconUrl;
- if (manifest.IsDip17Plugin)
- return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
-
- return MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, "icon.png");
+ return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
}
- private List? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting)
+ private List? GetPluginImageUrls(IPluginManifest manifest, bool isThirdParty, bool isTesting)
{
if (isThirdParty)
{
@@ -698,14 +696,7 @@ internal class PluginImageCache : IDisposable, IServiceType
var output = new List();
for (var i = 1; i <= 5; i++)
{
- if (manifest.IsDip17Plugin)
- {
- output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png"));
- }
- else
- {
- output.Add(MainRepoImageUrl.Format(isTesting ? "testing" : "plugins", manifest.InternalName, $"image{i}.png"));
- }
+ output.Add(MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, $"image{i}.png"));
}
return output;
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs
index 2dc182e9a..984732509 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs
@@ -48,15 +48,12 @@ internal class DalamudChangelogManager
foreach (var plugin in this.manager.InstalledPlugins)
{
- if (!plugin.Manifest.IsThirdParty)
+ if (!plugin.IsThirdParty)
{
- if (!plugin.Manifest.IsDip17Plugin)
- continue;
-
var pluginChangelogs = await client.GetFromJsonAsync(string.Format(
- PluginChangelogUrl,
- plugin.Manifest.InternalName,
- plugin.Manifest.Dip17Channel));
+ PluginChangelogUrl,
+ plugin.Manifest.InternalName,
+ plugin.Manifest.Dip17Channel));
changelogs = changelogs.Concat(pluginChangelogs.Versions
.Where(x => x.Dip17Track == plugin.Manifest.Dip17Channel)
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs
index 247e2d353..b4048536e 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs
@@ -33,7 +33,7 @@ internal class PluginChangelogEntry : IChangelogEntry
{
this.Plugin = plugin;
- this.Version = plugin.Manifest.EffectiveVersion.ToString();
+ this.Version = plugin.EffectiveVersion.ToString();
this.Text = plugin.Manifest.Changelog ?? Loc.Localize("ChangelogNoText", "No changelog for this version.");
this.Author = plugin.Manifest.Author;
this.Date = DateTimeOffset.FromUnixTimeSeconds(this.Plugin.Manifest.LastUpdate).DateTime;
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index ba249e051..a3787aaab 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -24,6 +24,7 @@ using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Support;
using Dalamud.Utility;
using ImGuiNET;
@@ -88,7 +89,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private string feedbackModalBody = string.Empty;
private string feedbackModalContact = string.Empty;
private bool feedbackModalIncludeException = false;
- private PluginManifest? feedbackPlugin = null;
+ private IPluginManifest? feedbackPlugin = null;
private bool feedbackIsTesting = false;
private int updatePluginCount = 0;
@@ -183,6 +184,7 @@ internal class PluginInstallerWindow : Window, IDisposable
NewOrNot,
NotInstalled,
EnabledDisabled,
+ ProfileOrNot,
}
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
@@ -504,6 +506,7 @@ internal class PluginInstallerWindow : Window, IDisposable
(Locs.SortBy_NewOrNot, PluginSortKind.NewOrNot),
(Locs.SortBy_NotInstalled, PluginSortKind.NotInstalled),
(Locs.SortBy_EnabledDisabled, PluginSortKind.EnabledDisabled),
+ (Locs.SortBy_ProfileOrNot, PluginSortKind.ProfileOrNot),
};
var longestSelectableWidth = sortSelectables.Select(t => ImGui.CalcTextSize(t.Localization).X).Max();
var selectableWidth = longestSelectableWidth + (style.FramePadding.X * 2); // This does not include the label
@@ -533,7 +536,8 @@ internal class PluginInstallerWindow : Window, IDisposable
"###XlPluginInstaller_Search",
Locs.Header_SearchPlaceholder,
ref this.searchText,
- 100);
+ 100,
+ ImGuiInputTextFlags.AutoSelectAll);
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
@@ -978,7 +982,7 @@ internal class PluginInstallerWindow : Window, IDisposable
changelogs = this.dalamudChangelogManager.Changelogs.OfType();
}
- var sortedChangelogs = changelogs?.Where(x => this.searchText.IsNullOrWhitespace() || x.Title.ToLowerInvariant().Contains(this.searchText.ToLowerInvariant()))
+ var sortedChangelogs = changelogs?.Where(x => this.searchText.IsNullOrWhitespace() || new FuzzyMatcher(this.searchText.ToLowerInvariant(), MatchMode.FuzzyParts).Matches(x.Title.ToLowerInvariant()) > 0)
.OrderByDescending(x => x.Date).ToList();
if (sortedChangelogs == null || !sortedChangelogs.Any())
@@ -1606,7 +1610,7 @@ internal class PluginInstallerWindow : Window, IDisposable
return ready;
}
- private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index)
+ private bool DrawPluginCollapsingHeader(string label, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, bool trouble, bool updateAvailable, bool isNew, bool installableOutdated, bool isOrphan, Action drawContextMenuAction, int index)
{
ImGui.Separator();
@@ -1741,13 +1745,13 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.TextWrapped(Locs.PluginBody_Orphaned);
ImGui.PopStyleColor();
}
- else if (plugin is { IsDecommissioned: true } && !plugin.Manifest.IsThirdParty)
+ else if (plugin is { IsDecommissioned: true, IsThirdParty: false })
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_NoServiceOfficial);
ImGui.PopStyleColor();
}
- else if (plugin is { IsDecommissioned: true } && plugin.Manifest.IsThirdParty)
+ else if (plugin is { IsDecommissioned: true, IsThirdParty: true })
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Locs.PluginBody_NoServiceThird);
@@ -1808,7 +1812,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (log is PluginChangelogEntry pluginLog)
{
icon = this.imageCache.DefaultIcon;
- var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex);
+ var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.IsThirdParty, out var cachedIconTex);
if (hasIcon && cachedIconTex != null)
{
icon = cachedIconTex;
@@ -2031,12 +2035,14 @@ internal class PluginInstallerWindow : Window, IDisposable
}
// Testing
- if (plugin.Manifest.Testing)
+ if (plugin.IsTesting)
{
label += Locs.PluginTitleMod_TestingVersion;
}
- if (plugin.Manifest.IsAvailableForTesting && configuration.DoPluginTest && testingOptIn == null)
+ var hasTestingAvailable = this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName &&
+ x.IsAvailableForTesting);
+ if (hasTestingAvailable && configuration.DoPluginTest && testingOptIn == null)
{
label += Locs.PluginTitleMod_TestingAvailable;
}
@@ -2132,7 +2138,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty();
- if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.Manifest.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
+ if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
{
if (!this.WasPluginSeen(plugin.Manifest.InternalName))
configuration.SeenPluginInternalName.Add(plugin.Manifest.InternalName);
@@ -2154,12 +2160,15 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText);
- var isThirdParty = manifest.IsThirdParty;
+ var acceptsFeedback =
+ this.pluginListAvailable.Any(x => x.InternalName == plugin.InternalName && x.AcceptsFeedback);
+
+ var isThirdParty = plugin.IsThirdParty;
var canFeedback = !isThirdParty &&
!plugin.IsDev &&
!plugin.IsOrphaned &&
plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel &&
- plugin.Manifest.AcceptsFeedback &&
+ acceptsFeedback &&
availablePluginUpdate == default;
// Installed from
@@ -2180,6 +2189,14 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.SafeTextWrapped(manifest.Description);
}
+ // Working Plugin ID
+ if (this.hasDevPlugins)
+ {
+ ImGuiHelpers.ScaledDummy(3);
+ ImGui.TextColored(ImGuiColors.DalamudGrey, $"WorkingPluginId: {manifest.WorkingPluginId}");
+ ImGuiHelpers.ScaledDummy(3);
+ }
+
// Available commands (if loaded)
if (plugin.IsLoaded)
{
@@ -2215,7 +2232,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.DrawUpdateSinglePluginButton(availablePluginUpdate);
ImGui.SameLine();
- ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.EffectiveVersion}");
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.EffectiveVersion}");
ImGuiHelpers.ScaledDummy(5);
@@ -2226,7 +2243,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (hasChangelog)
{
- if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.Manifest.EffectiveVersion)))
+ if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion)))
{
this.DrawInstalledPluginChangelog(plugin.Manifest);
ImGui.TreePop();
@@ -2252,7 +2269,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.PopID();
}
- private void DrawInstalledPluginChangelog(PluginManifest manifest)
+ private void DrawInstalledPluginChangelog(IPluginManifest manifest)
{
ImGuiHelpers.ScaledDummy(5);
@@ -2265,7 +2282,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGui.Text("Changelog:");
ImGuiHelpers.ScaledDummy(2);
- ImGuiHelpers.SafeTextWrapped(manifest.Changelog);
+ ImGuiHelpers.SafeTextWrapped(manifest.Changelog!);
}
ImGui.EndChild();
@@ -2336,7 +2353,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var profileManager = Service.Get();
var config = Service.Get();
- var applicableForProfiles = plugin.Manifest.SupportsProfiles;
+ var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev;
var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName);
// Disable everything if the updater is running or another plugin is operating
@@ -2363,7 +2380,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var isLoadedAndUnloadable = plugin.State == PluginState.Loaded ||
plugin.State == PluginState.DependencyResolutionFailed;
- //StyleModelV1.DalamudStandard.Push();
+ // StyleModelV1.DalamudStandard.Push();
var profileChooserPopupName = $"###pluginProfileChooser{plugin.Manifest.InternalName}";
if (ImGui.BeginPopup(profileChooserPopupName))
@@ -2526,7 +2543,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
- //StyleModelV1.DalamudStandard.Pop();
+ // StyleModelV1.DalamudStandard.Pop();
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(15, 0);
@@ -2621,7 +2638,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
- private void DrawSendFeedbackButton(PluginManifest manifest, bool isTesting)
+ private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting)
{
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Comment))
@@ -2796,7 +2813,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
- private bool DrawPluginImages(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, int index)
+ private bool DrawPluginImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, int index)
{
var hasImages = this.imageCache.TryGetImages(plugin, manifest, isThirdParty, out var imageTextures);
if (!hasImages || imageTextures.All(x => x == null))
@@ -2871,10 +2888,10 @@ internal class PluginInstallerWindow : Window, IDisposable
return true;
}
- private bool IsManifestFiltered(PluginManifest manifest)
+ private bool IsManifestFiltered(IPluginManifest manifest)
{
- var searchString = this.searchText.ToLowerInvariant();
- var hasSearchString = !string.IsNullOrWhiteSpace(searchString);
+ var matcher = new FuzzyMatcher(this.searchText.ToLowerInvariant(), MatchMode.FuzzyParts);
+ var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText);
var oldApi = manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
var installed = this.IsManifestInstalled(manifest).IsInstalled;
@@ -2882,14 +2899,14 @@ internal class PluginInstallerWindow : Window, IDisposable
return true;
return hasSearchString && !(
- (!manifest.Name.IsNullOrEmpty() && manifest.Name.ToLowerInvariant().Contains(searchString)) ||
- (!manifest.InternalName.IsNullOrEmpty() && manifest.InternalName.ToLowerInvariant().Contains(searchString)) ||
- (!manifest.Author.IsNullOrEmpty() && manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase)) ||
- (!manifest.Punchline.IsNullOrEmpty() && manifest.Punchline.ToLowerInvariant().Contains(searchString)) ||
- (manifest.Tags != null && manifest.Tags.Any(tag => tag.ToLowerInvariant().Contains(searchString))));
+ (!manifest.Name.IsNullOrEmpty() && matcher.Matches(manifest.Name.ToLowerInvariant()) > 0) ||
+ (!manifest.InternalName.IsNullOrEmpty() && matcher.Matches(manifest.InternalName.ToLowerInvariant()) > 0) ||
+ (!manifest.Author.IsNullOrEmpty() && matcher.Matches(manifest.Author.ToLowerInvariant()) > 0) ||
+ // (!manifest.Punchline.IsNullOrEmpty() && matcher.Matches(manifest.Punchline.ToLowerInvariant()) > 0) || // Removed because fuzzy match gets a little too excited with lots of random words
+ (manifest.Tags != null && matcher.MatchesAny(manifest.Tags.Select(term => term.ToLowerInvariant()).ToArray()) > 0));
}
- private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(PluginManifest? manifest)
+ private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(IPluginManifest? manifest)
{
if (manifest == null) return (false, default);
@@ -2970,6 +2987,12 @@ internal class PluginInstallerWindow : Window, IDisposable
});
this.pluginListInstalled.Sort((p1, p2) => (p2.State == PluginState.Loaded).CompareTo(p1.State == PluginState.Loaded));
break;
+ case PluginSortKind.ProfileOrNot:
+ this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));
+
+ var profman = Service.Get();
+ this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName)));
+ break;
default:
throw new InvalidEnumArgumentException("Unknown plugin sort type.");
}
@@ -3048,6 +3071,8 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string SortBy_EnabledDisabled => Loc.Localize("InstallerEnabledDisabled", "Enabled/Disabled");
+ public static string SortBy_ProfileOrNot => Loc.Localize("InstallerProfileOrNot", "In a collection");
+
public static string SortBy_Label => Loc.Localize("InstallerSortBy", "Sort By");
#endregion
@@ -3124,9 +3149,9 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
- public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin configuration");
+ public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
- public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfigReload", "Reset plugin configuration and reload");
+ public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfigReload", "Reset plugin data and reload");
#endregion
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
index b43d70e7d..301e43473 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
@@ -4,6 +4,7 @@ using System.Numerics;
using System.Threading.Tasks;
using CheapLoc;
+using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
@@ -48,10 +49,14 @@ internal class ProfileManagerWidget
///
public void Draw()
{
+ var tutorialTitle = Locs.TutorialTitle + "###collectionsTutorWindow";
+ var tutorialId = ImGui.GetID(tutorialTitle);
+ this.DrawTutorial(tutorialTitle);
+
switch (this.mode)
{
case Mode.Overview:
- this.DrawOverview();
+ this.DrawOverview(tutorialId);
break;
case Mode.EditSingleProfile:
@@ -70,7 +75,62 @@ internal class ProfileManagerWidget
this.pickerSearch = string.Empty;
}
- private void DrawOverview()
+ private void DrawTutorial(string modalTitle)
+ {
+ var open = true;
+ ImGui.SetNextWindowSize(new Vector2(450, 350), ImGuiCond.Appearing);
+ using (var popup = ImRaii.PopupModal(modalTitle, ref open))
+ {
+ if (popup)
+ {
+ using var scrolling = ImRaii.Child("###scrolling", new Vector2(-1, -1));
+ if (scrolling)
+ {
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphOne);
+ ImGuiHelpers.ScaledDummy(5);
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphTwo);
+ ImGuiHelpers.ScaledDummy(5);
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphThree);
+ ImGuiHelpers.ScaledDummy(5);
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphFour);
+ ImGuiHelpers.ScaledDummy(5);
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommands);
+
+ ImGui.Bullet();
+ ImGui.SameLine();
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnable);
+
+ ImGui.Bullet();
+ ImGui.SameLine();
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsDisable);
+
+ ImGui.Bullet();
+ ImGui.SameLine();
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsToggle);
+
+ ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd);
+ ImGuiHelpers.ScaledDummy(5);
+
+ var buttonWidth = 120f;
+ ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2);
+ if (ImGui.Button("OK", new Vector2(buttonWidth, 40)))
+ {
+ ImGui.CloseCurrentPopup();
+ }
+ }
+ }
+ }
+
+ var config = Service.Get();
+ if (!config.ProfilesHasSeenTutorial)
+ {
+ ImGui.OpenPopup(modalTitle);
+ config.ProfilesHasSeenTutorial = true;
+ config.QueueSave();
+ }
+ }
+
+ private void DrawOverview(uint tutorialId)
{
var didAny = false;
var profman = Service.Get();
@@ -101,6 +161,16 @@ internal class ProfileManagerWidget
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.ImportProfileHint);
+
+ ImGui.SameLine();
+ ImGuiHelpers.ScaledDummy(5);
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Question))
+ ImGui.OpenPopup(tutorialId);
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.TutorialHint);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
@@ -221,7 +291,7 @@ internal class ProfileManagerWidget
if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
{
// TODO: Plugin searching should be abstracted... installer and this should use the same search
- foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles &&
+ foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev &&
(this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant()))))
{
using var disabled2 =
@@ -340,7 +410,7 @@ internal class ProfileManagerWidget
if (pmPlugin != null)
{
- pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.Manifest.IsThirdParty, out var icon);
+ pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon);
icon ??= pic.DefaultIcon;
ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
@@ -488,6 +558,9 @@ internal class ProfileManagerWidget
public static string ImportProfileHint =>
Loc.Localize("ProfileManagerImportProfile", "Import a shared collection from your clipboard");
+ public static string TutorialHint =>
+ Loc.Localize("ProfileManagerTutorialHint", "Learn more about collections");
+
public static string AddProfile => Loc.Localize("ProfileManagerAddProfile", "Add a new collection");
public static string NotificationImportSuccess =>
@@ -502,6 +575,36 @@ internal class ProfileManagerWidget
public static string ErrorCouldNotChangeState =>
Loc.Localize("ProfileManagerCouldNotChangeState", "Could not change plugin state.");
+ public static string TutorialTitle =>
+ Loc.Localize("ProfileManagerTutorial", "About Collections");
+
+ public static string TutorialParagraphOne =>
+ Loc.Localize("ProfileManagerTutorialParagraphOne", "Collections are shareable lists of plugins that can be enabled or disabled in the plugin installer or via chat commands.\nWhen a plugin is part of a collection, it will be enabled if the collection is enabled. If a plugin is part of multiple collections, it will be enabled if one or more collections it is a part of are enabled.");
+
+ public static string TutorialParagraphTwo =>
+ Loc.Localize("ProfileManagerTutorialParagraphTwo", "You can add plugins to collections by clicking the plus button when editing a collection on this screen, or by using the button with the toolbox icon on the \"Installed Plugins\" screen.");
+
+ public static string TutorialParagraphThree =>
+ Loc.Localize("ProfileManagerTutorialParagraphThree", "If a collection's \"Start on boot\" checkbox is ticked, the collection and the plugins within will be enabled every time the game starts up, even if it has been manually disabled in a prior session.");
+
+ public static string TutorialParagraphFour =>
+ Loc.Localize("ProfileManagerTutorialParagraphFour", "Individual plugins inside a collection also have a checkbox next to them. This indicates if a plugin is active within that collection - if the checkbox is not ticked, the plugin will not be enabled if that collection is active. Mind that it will still be enabled if the plugin is an active part of any other collection.");
+
+ public static string TutorialCommands =>
+ Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
+
+ public static string TutorialCommandsEnable =>
+ Loc.Localize("ProfileManagerTutorialCommandsEnable", "/xlenableprofile \"Collection Name\" - Enable a collection");
+
+ public static string TutorialCommandsDisable =>
+ Loc.Localize("ProfileManagerTutorialCommandsDisable", "/xldisableprofile \"Collection Name\" - Disable a collection");
+
+ public static string TutorialCommandsToggle =>
+ Loc.Localize("ProfileManagerTutorialCommandsToggle", "/xltoggleprofile \"Collection Name\" - Toggle a collection's state");
+
+ public static string TutorialCommandsEnd =>
+ Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");
+
public static string NotInstalled(string name) =>
Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name);
}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs
index 1e66ac19e..0a1b4d91d 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/TargetAgingStep.cs
@@ -23,8 +23,8 @@ internal class TargetAgingStep : IAgingStep
switch (this.step)
{
case 0:
- targetManager.ClearTarget();
- targetManager.ClearFocusTarget();
+ targetManager.Target = null;
+ targetManager.FocusTarget = null;
this.step++;
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
index 13adccffd..3e801a8c3 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
@@ -101,6 +101,12 @@ public class SettingsTabLook : SettingsTab
Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."),
c => c.IsDocking,
(v, c) => c.IsDocking = v),
+
+ new SettingsEntry(
+ Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"),
+ Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."),
+ c => c.EnablePluginUISoundEffects,
+ (v, c) => c.EnablePluginUISoundEffects = v),
new SettingsEntry(
Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"),
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index 143fda6ab..10180f0c3 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -45,7 +45,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
{
this.IsOpen = true;
-
+ this.DisableWindowSounds = true;
this.ForceMainWindow = true;
this.Position = new Vector2(0, 200);
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index 2a8beb639..39c61566b 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -2,6 +2,7 @@ using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Keys;
+using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET;
namespace Dalamud.Interface.Windowing;
@@ -55,6 +56,21 @@ public abstract class Window
///
public bool RespectCloseHotkey { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether this window should not generate sound effects when opening and closing.
+ ///
+ public bool DisableWindowSounds { get; set; } = false;
+
+ ///
+ /// Gets or sets a value representing the sound effect id to be played when the window is opened.
+ ///
+ public uint OnOpenSfxId { get; set; } = 23u;
+
+ ///
+ /// Gets or sets a value representing the sound effect id to be played when the window is closed.
+ ///
+ public uint OnCloseSfxId { get; set; } = 24u;
+
///
/// Gets or sets the position of this window.
///
@@ -207,10 +223,12 @@ public abstract class Window
///
/// Draw the window via ImGui.
///
- internal void DrawInternal()
+ internal void DrawInternal(DalamudConfiguration? configuration)
{
this.PreOpenCheck();
+ var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false;
+
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
@@ -219,6 +237,8 @@ public abstract class Window
this.OnClose();
this.IsFocused = false;
+
+ if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnCloseSfxId, 0, 0, 0);
}
return;
@@ -243,6 +263,8 @@ public abstract class Window
{
this.internalLastIsOpen = this.internalIsOpen;
this.OnOpen();
+
+ if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnOpenSfxId, 0, 0, 0);
}
var wasFocused = this.IsFocused;
@@ -272,16 +294,19 @@ public abstract class Window
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
- var escapeDown = Service.Get()[VirtualKey.ESCAPE];
- var isAllowed = Service.Get().IsFocusManagementEnabled;
- if (escapeDown && this.IsFocused && isAllowed && !wasEscPressedLastFrame && this.RespectCloseHotkey)
+ var isAllowed = configuration?.IsFocusManagementEnabled ?? false;
+ if (isAllowed)
{
- this.IsOpen = false;
- wasEscPressedLastFrame = true;
- }
- else if (!escapeDown && wasEscPressedLastFrame)
- {
- wasEscPressedLastFrame = false;
+ var escapeDown = Service.Get()[VirtualKey.ESCAPE];
+ if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
+ {
+ this.IsOpen = false;
+ wasEscPressedLastFrame = true;
+ }
+ else if (!escapeDown && wasEscPressedLastFrame)
+ {
+ wasEscPressedLastFrame = false;
+ }
}
ImGui.End();
diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs
index 516f3c21b..8e12d8f68 100644
--- a/Dalamud/Interface/Windowing/WindowSystem.cs
+++ b/Dalamud/Interface/Windowing/WindowSystem.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
+using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using ImGuiNET;
using Serilog;
@@ -111,6 +112,8 @@ public class WindowSystem
if (hasNamespace)
ImGui.PushID(this.Namespace);
+ var config = Service.GetNullable();
+
// Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating
foreach (var window in this.windows.ToArray())
{
@@ -119,7 +122,7 @@ public class WindowSystem
#endif
var snapshot = ImGuiManagedAsserts.GetSnapshot();
- window.DrawInternal();
+ window.DrawInternal(config);
var source = ($"{this.Namespace}::" ?? string.Empty) + window.WindowName;
ImGuiManagedAsserts.ReportProblems(source, snapshot);
diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs
index 76e5a65d5..acfa68dfb 100644
--- a/Dalamud/Plugin/DalamudPluginInterface.cs
+++ b/Dalamud/Plugin/DalamudPluginInterface.cs
@@ -21,6 +21,7 @@ using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Ipc.Internal;
@@ -208,7 +209,7 @@ public sealed class DalamudPluginInterface : IDisposable
///
/// Gets a list of installed plugins along with their current state.
///
- public IEnumerable InstalledPlugins => Service.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.Manifest.EffectiveVersion));
+ public IEnumerable InstalledPlugins => Service.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.EffectiveVersion));
///
/// Opens the with the plugin name set as search target.
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs
index 94bb9467a..887994f30 100644
--- a/Dalamud/Plugin/Internal/PluginManager.cs
+++ b/Dalamud/Plugin/Internal/PluginManager.cs
@@ -25,6 +25,7 @@ using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
@@ -247,7 +248,7 @@ internal partial class PluginManager : IDisposable, IServiceType
///
/// The manifest to test.
/// Whether or not a testing version is available.
- public static bool HasTestingVersion(PluginManifest manifest)
+ public static bool HasTestingVersion(IPluginManifest manifest)
{
var av = manifest.AssemblyVersion;
var tv = manifest.TestingAssemblyVersion;
@@ -317,7 +318,7 @@ internal partial class PluginManager : IDisposable, IServiceType
///
/// Manifest to check.
/// A value indicating whether testing should be used.
- public bool HasTestingOptIn(PluginManifest manifest)
+ public bool HasTestingOptIn(IPluginManifest manifest)
{
return this.configuration.PluginTestingOptIns!.Any(x => x.InternalName == manifest.InternalName);
}
@@ -328,7 +329,7 @@ internal partial class PluginManager : IDisposable, IServiceType
///
/// Manifest to check.
/// A value indicating whether testing should be used.
- public bool UseTesting(PluginManifest manifest)
+ public bool UseTesting(IPluginManifest manifest)
{
if (!this.configuration.DoPluginTest)
return false;
@@ -746,8 +747,9 @@ internal partial class PluginManager : IDisposable, IServiceType
/// The plugin definition.
/// If the testing version should be used.
/// The reason this plugin was loaded.
+ /// WorkingPluginId this plugin should inherit.
/// A representing the asynchronous operation.
- public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason)
+ public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Guid? inheritedWorkingPluginId = null)
{
Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})");
@@ -834,6 +836,9 @@ internal partial class PluginManager : IDisposable, IServiceType
// Reload as a local manifest, add some attributes, and save again.
var manifest = LocalPluginManifest.Load(manifestFile);
+ if (manifest == null)
+ throw new Exception("Plugin had no valid manifest");
+
if (manifest.InternalName != repoManifest.InternalName)
{
Directory.Delete(outputDir.FullName, true);
@@ -841,6 +846,11 @@ internal partial class PluginManager : IDisposable, IServiceType
$"Distributed internal name does not match repo internal name: {manifest.InternalName} - {repoManifest.InternalName}");
}
+ if (manifest.WorkingPluginId != Guid.Empty)
+ throw new Exception("Plugin shall not specify a WorkingPluginId");
+
+ manifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid();
+
if (useTesting)
{
manifest.Testing = true;
@@ -849,7 +859,7 @@ internal partial class PluginManager : IDisposable, IServiceType
// Document the url the plugin was installed from
manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : LocalPluginManifest.FlagMainRepo;
- manifest.Save(manifestFile);
+ manifest.Save(manifestFile, "installation");
Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})");
@@ -1023,6 +1033,10 @@ internal partial class PluginManager : IDisposable, IServiceType
{
var plugin = metadata.InstalledPlugin;
+ var workingPluginId = metadata.InstalledPlugin.Manifest.WorkingPluginId;
+ if (workingPluginId == Guid.Empty)
+ throw new Exception("Existing plugin had no WorkingPluginId");
+
var updateStatus = new PluginUpdateStatus
{
InternalName = plugin.Manifest.InternalName,
@@ -1082,7 +1096,7 @@ internal partial class PluginManager : IDisposable, IServiceType
try
{
- await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update);
+ await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update, workingPluginId);
}
catch (Exception ex)
{
diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs
index 71feff0c2..61d521e89 100644
--- a/Dalamud/Plugin/Internal/Profiles/Profile.cs
+++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs
@@ -219,7 +219,7 @@ internal class Profile
{
if (!this.IsDefaultProfile)
{
- await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, entry.IsEnabled, false);
+ await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false);
}
else
{
diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
index 13523a379..36823b389 100644
--- a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
+++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
@@ -1,3 +1,5 @@
+using Dalamud.Plugin.Internal.Types.Manifest;
+
namespace Dalamud.Plugin.Internal.Types;
///
diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
index 498bea874..98784ce64 100644
--- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Internal.Types.Manifest;
namespace Dalamud.Plugin.Internal.Types;
diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
index 14ae4a0c0..115ab0f8d 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
@@ -11,11 +11,11 @@ using Dalamud.Game.Gui.Dtr;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
-using Dalamud.Logging;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Profiles;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
namespace Dalamud.Plugin.Internal.Types;
@@ -39,6 +39,8 @@ internal class LocalPlugin : IDisposable
private Type? pluginType;
private IDalamudPlugin? instance;
+ private LocalPluginManifest manifest;
+
///
/// Initializes a new instance of the class.
///
@@ -111,7 +113,7 @@ internal class LocalPlugin : IDisposable
// If the parameter manifest was null
if (manifest == null)
{
- this.Manifest = new LocalPluginManifest()
+ this.manifest = new LocalPluginManifest()
{
Author = "developer",
Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
@@ -125,36 +127,51 @@ internal class LocalPlugin : IDisposable
// Save the manifest to disk so there won't be any problems later.
// We'll update the name property after it can be retrieved from the instance.
- this.Manifest.Save(this.manifestFile);
+ this.manifest.Save(this.manifestFile, "manifest was null");
}
else
{
- this.Manifest = manifest;
+ this.manifest = manifest;
}
+ var needsSaveDueToLegacyFiles = false;
+
// This converts from the ".disabled" file feature to the manifest instead.
this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
if (this.disabledFile.Exists)
{
#pragma warning disable CS0618
- this.Manifest.Disabled = true;
+ this.manifest.Disabled = true;
#pragma warning restore CS0618
this.disabledFile.Delete();
+
+ needsSaveDueToLegacyFiles = true;
}
// This converts from the ".testing" file feature to the manifest instead.
this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
if (this.testingFile.Exists)
{
- this.Manifest.Testing = true;
+ this.manifest.Testing = true;
this.testingFile.Delete();
+
+ needsSaveDueToLegacyFiles = true;
+ }
+
+ // Create an installation instance ID for this plugin, if it doesn't have one yet
+ if (this.manifest.WorkingPluginId == Guid.Empty)
+ {
+ this.manifest.WorkingPluginId = Guid.NewGuid();
+
+ needsSaveDueToLegacyFiles = true;
}
var pluginManager = Service.Get();
- this.IsBanned = pluginManager.IsManifestBanned(this.Manifest) && !this.IsDev;
- this.BanReason = pluginManager.GetBanReason(this.Manifest);
+ this.IsBanned = pluginManager.IsManifestBanned(this.manifest) && !this.IsDev;
+ this.BanReason = pluginManager.GetBanReason(this.manifest);
- this.SaveManifest();
+ if (needsSaveDueToLegacyFiles)
+ this.SaveManifest("legacy");
}
///
@@ -168,9 +185,9 @@ internal class LocalPlugin : IDisposable
public FileInfo DllFile { get; }
///
- /// Gets the plugin manifest, if one exists.
+ /// Gets the plugin manifest.
///
- public LocalPluginManifest Manifest { get; private set; }
+ public ILocalPluginManifest Manifest => this.manifest;
///
/// Gets or sets the current state of the plugin.
@@ -186,12 +203,12 @@ internal class LocalPlugin : IDisposable
///
/// Gets the plugin name from the manifest.
///
- public string Name => this.Manifest.Name;
+ public string Name => this.manifest.Name;
///
/// Gets the plugin internal name from the manifest.
///
- public string InternalName => this.Manifest.InternalName;
+ public string InternalName => this.manifest.InternalName;
///
/// Gets an optional reason, if the plugin is banned.
@@ -213,23 +230,23 @@ internal class LocalPlugin : IDisposable
/// INCLUDES the default profile.
///
public bool IsWantedByAnyProfile =>
- Service.Get().GetWantStateAsync(this.Manifest.InternalName, false, false).GetAwaiter().GetResult();
+ Service.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult();
///
/// Gets a value indicating whether this plugin's API level is out of date.
///
- public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
+ public bool IsOutdated => this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
///
/// Gets a value indicating whether the plugin is for testing use only.
///
- public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
+ public bool IsTesting => this.manifest.IsTestingExclusive || this.manifest.Testing;
///
/// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not.
///
public bool IsOrphaned => !this.IsDev &&
- !this.Manifest.InstalledFromUrl.IsNullOrEmpty() && // TODO(api8): Remove this, all plugins will have a proper flag
+ !this.manifest.InstalledFromUrl.IsNullOrEmpty() && // TODO(api8): Remove this, all plugins will have a proper flag
this.GetSourceRepository() == null;
///
@@ -237,7 +254,7 @@ internal class LocalPlugin : IDisposable
///
public bool IsDecommissioned => !this.IsDev &&
this.GetSourceRepository()?.State == PluginRepositoryState.Success &&
- this.GetSourceRepository()?.PluginMaster?.FirstOrDefault(x => x.InternalName == this.Manifest.InternalName) == null;
+ this.GetSourceRepository()?.PluginMaster?.FirstOrDefault(x => x.InternalName == this.manifest.InternalName) == null;
///
/// Gets a value indicating whether this plugin has been banned.
@@ -249,12 +266,23 @@ internal class LocalPlugin : IDisposable
///
public bool IsDev => this is LocalDevPlugin;
+ ///
+ /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
+ /// repo.
+ ///
+ public bool IsThirdParty => this.manifest.IsThirdParty;
+
///
/// Gets a value indicating whether this plugin should be allowed to load.
///
public bool ApplicableForLoad => !this.IsBanned && !this.IsDecommissioned && !this.IsOrphaned && !this.IsOutdated
&& !(!this.IsDev && this.State == PluginState.UnloadError) && this.CheckPolicy();
+ ///
+ /// Gets the effective version of this plugin.
+ ///
+ public Version EffectiveVersion => this.manifest.EffectiveVersion;
+
///
/// Gets the service scope for this plugin.
///
@@ -270,7 +298,7 @@ internal class LocalPlugin : IDisposable
if (this.instance != null)
{
didPluginDispose = true;
- if (this.Manifest.CanUnloadAsync || framework == null)
+ if (this.manifest.CanUnloadAsync || framework == null)
this.instance.Dispose();
else
framework.RunOnFrameworkThread(() => this.instance.Dispose()).Wait();
@@ -309,7 +337,7 @@ internal class LocalPlugin : IDisposable
await Service.GetAsync();
await Service.GetAsync();
- if (this.Manifest.LoadRequiredState == 0)
+ if (this.manifest.LoadRequiredState == 0)
_ = await Service.GetAsync();
await this.pluginLoadStateLock.WaitAsync();
@@ -322,8 +350,11 @@ internal class LocalPlugin : IDisposable
}
// If we reload a plugin we don't want to delete it. Makes sense, right?
- this.Manifest.ScheduledForDeletion = false;
- this.SaveManifest();
+ if (this.manifest.ScheduledForDeletion)
+ {
+ this.manifest.ScheduledForDeletion = false;
+ this.SaveManifest("Scheduled for deletion, but loading");
+ }
switch (this.State)
{
@@ -353,13 +384,13 @@ internal class LocalPlugin : IDisposable
throw new ArgumentOutOfRangeException(this.State.ToString());
}
- if (pluginManager.IsManifestBanned(this.Manifest) && !this.IsDev)
+ if (pluginManager.IsManifestBanned(this.manifest) && !this.IsDev)
throw new BannedPluginException($"Unable to load {this.Name}, banned");
- if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
+ if (this.manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
- if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels)
+ if (this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
// We might want to throw here?
@@ -380,8 +411,8 @@ internal class LocalPlugin : IDisposable
{
Log.Error(
"==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====",
- this.Manifest.Author!,
- this.Manifest.InternalName);
+ this.manifest.Author!,
+ this.manifest.InternalName);
Log.Error(
"YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
Log.Error(
@@ -449,7 +480,7 @@ internal class LocalPlugin : IDisposable
this.ServiceScope = ioc.GetScope();
this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it
- if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1)
+ if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1)
{
this.instance = await framework.RunOnFrameworkThread(
() => this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin;
@@ -470,10 +501,10 @@ internal class LocalPlugin : IDisposable
}
// In-case the manifest name was a placeholder. Can occur when no manifest was included.
- if (this.Manifest.Name.IsNullOrEmpty())
+ if (this.manifest.Name.IsNullOrEmpty() && !this.IsDev)
{
- this.Manifest.Name = this.instance.Name;
- this.Manifest.Save(this.manifestFile);
+ this.manifest.Name = this.instance.Name;
+ this.manifest.Save(this.manifestFile, "manifest name null or empty");
}
this.State = PluginState.Loaded;
@@ -505,7 +536,6 @@ internal class LocalPlugin : IDisposable
{
var configuration = Service.Get();
var framework = Service.GetNullable();
- var ioc = await Service.GetAsync();
await this.pluginLoadStateLock.WaitAsync();
try
@@ -534,7 +564,7 @@ internal class LocalPlugin : IDisposable
this.State = PluginState.Unloading;
Log.Information($"Unloading {this.DllFile.Name}");
- if (this.Manifest.CanUnloadAsync || framework == null)
+ if (this.manifest.CanUnloadAsync || framework == null)
this.instance?.Dispose();
else
await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
@@ -602,7 +632,7 @@ internal class LocalPlugin : IDisposable
if (startInfo.NoLoadPlugins)
return false;
- if (startInfo.NoLoadThirdPartyPlugins && this.Manifest.IsThirdParty)
+ if (startInfo.NoLoadThirdPartyPlugins && this.manifest.IsThirdParty)
return false;
if (manager.SafeMode)
@@ -617,8 +647,8 @@ internal class LocalPlugin : IDisposable
/// Schedule or cancel the deletion.
public void ScheduleDeletion(bool status = true)
{
- this.Manifest.ScheduledForDeletion = status;
- this.SaveManifest();
+ this.manifest.ScheduledForDeletion = status;
+ this.SaveManifest("scheduling for deletion");
}
///
@@ -626,14 +656,14 @@ internal class LocalPlugin : IDisposable
///
public void ReloadManifest()
{
- var manifest = LocalPluginManifest.GetManifestFile(this.DllFile);
- if (manifest.Exists)
+ var manifestPath = LocalPluginManifest.GetManifestFile(this.DllFile);
+ if (manifestPath.Exists)
{
// var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted
- this.Manifest = LocalPluginManifest.Load(manifest) ?? throw new Exception("Could not reload manifest.");
- // this.Manifest.Disabled = isDisabled;
+ this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest.");
+ // this.manifest.Disabled = isDisabled;
- this.SaveManifest();
+ this.SaveManifest("dev reload");
}
}
@@ -649,10 +679,10 @@ internal class LocalPlugin : IDisposable
var repos = Service.Get().Repos;
return repos.FirstOrDefault(x =>
{
- if (!x.IsThirdParty && !this.Manifest.IsThirdParty)
+ if (!x.IsThirdParty && !this.manifest.IsThirdParty)
return true;
- return x.PluginMasterUrl == this.Manifest.InstalledFromUrl;
+ return x.PluginMasterUrl == this.manifest.InstalledFromUrl;
});
}
@@ -665,5 +695,5 @@ internal class LocalPlugin : IDisposable
config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
}
- private void SaveManifest() => this.Manifest.Save(this.manifestFile);
+ private void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason);
}
diff --git a/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs
new file mode 100644
index 000000000..5b147dde1
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/Manifest/ILocalPluginManifest.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Types.Manifest;
+
+///
+/// Public interface for the local plugin manifest.
+///
+public interface ILocalPluginManifest : IPluginManifest
+{
+ ///
+ /// Gets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
+ /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
+ /// when installed from the main repo.
+ ///
+ public string InstalledFromUrl { get; }
+
+ ///
+ /// Gets a value indicating whether the plugin should be deleted during the next cleanup.
+ ///
+ public bool ScheduledForDeletion { get; }
+
+ ///
+ /// Gets an ID uniquely identifying this specific installation of a plugin.
+ ///
+ public Guid WorkingPluginId { get; }
+}
diff --git a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs
new file mode 100644
index 000000000..9e052efad
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+
+namespace Dalamud.Plugin.Internal.Types.Manifest;
+
+///
+/// Public interface for the base plugin manifest.
+///
+public interface IPluginManifest
+{
+ ///
+ /// Gets the internal name of the plugin, which should match the assembly name of the plugin.
+ ///
+ public string InternalName { get; }
+
+ ///
+ /// Gets the public name of the plugin.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets a punchline of the plugins functions.
+ ///
+ public string? Punchline { get; }
+
+ ///
+ /// Gets the author/s of the plugin.
+ ///
+ public string Author { get; }
+
+ ///
+ /// Gets a value indicating whether the plugin can be unloaded asynchronously.
+ ///
+ public bool CanUnloadAsync { get; }
+
+ ///
+ /// Gets the assembly version of the plugin.
+ ///
+ public Version AssemblyVersion { get; }
+
+ ///
+ /// Gets the assembly version of the plugin's testing variant.
+ ///
+ public Version? TestingAssemblyVersion { get; }
+
+ ///
+ /// Gets the DIP17 channel name.
+ ///
+ public string? Dip17Channel { get; }
+
+ ///
+ /// Gets the last time this plugin was updated.
+ ///
+ public long LastUpdate { get; }
+
+ ///
+ /// Gets a changelog, null if none exists.
+ ///
+ public string? Changelog { get; }
+
+ ///
+ /// Gets a list of tags that apply to this plugin.
+ ///
+ public List? Tags { get; }
+
+ ///
+ /// Gets the API level of this plugin. For the current API level, please see
+ /// for the currently used API level.
+ ///
+ public int DalamudApiLevel { get; }
+
+ ///
+ /// Gets the number of downloads this plugin has.
+ ///
+ public long DownloadCount { get; }
+
+ ///
+ /// Gets a value indicating whether the plugin supports profiles.
+ ///
+ public bool SupportsProfiles { get; }
+
+ ///
+ /// Gets an URL to the website or source code of the plugin.
+ ///
+ public string? RepoUrl { get; }
+
+ ///
+ /// Gets a description of the plugins functions.
+ ///
+ public string? Description { get; }
+
+ ///
+ /// Gets a message that is shown to users when sending feedback.
+ ///
+ public string? FeedbackMessage { get; }
+
+ ///
+ /// Gets a value indicating whether the plugin is only available for testing.
+ ///
+ public bool IsTestingExclusive { get; }
+
+ ///
+ /// Gets a list of screenshot image URLs to show in the plugin installer.
+ ///
+ public List? ImageUrls { get; }
+
+ ///
+ /// Gets an URL for the plugin's icon.
+ ///
+ public string? IconUrl { get; }
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs
similarity index 82%
rename from Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
rename to Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs
index e142f9cb0..8afbe1aea 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs
@@ -3,14 +3,15 @@ using System.IO;
using Dalamud.Utility;
using Newtonsoft.Json;
+using Serilog;
-namespace Dalamud.Plugin.Internal.Types;
+namespace Dalamud.Plugin.Internal.Types.Manifest;
///
/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
///
-internal record LocalPluginManifest : PluginManifest
+internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest
{
///
/// Flag indicating that a plugin was installed from the official repo.
@@ -37,18 +38,15 @@ internal record LocalPluginManifest : PluginManifest
///
public bool Testing { get; set; }
- ///
- /// Gets or sets a value indicating whether the plugin should be deleted during the next cleanup.
- ///
+ ///
public bool ScheduledForDeletion { get; set; }
- ///
- /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
- /// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
- /// when installed from the main repo.
- ///
+ ///
public string InstalledFromUrl { get; set; } = string.Empty;
+ ///
+ public Guid WorkingPluginId { get; set; } = Guid.Empty;
+
///
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
/// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null.
@@ -60,16 +58,25 @@ internal record LocalPluginManifest : PluginManifest
///
public Version EffectiveVersion => this.Testing && this.TestingAssemblyVersion != null ? this.TestingAssemblyVersion : this.AssemblyVersion;
- ///
- /// Gets a value indicating whether this plugin is eligible for testing.
- ///
- public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion;
-
///
/// Save a plugin manifest to file.
///
/// Path to save at.
- public void Save(FileInfo manifestFile) => Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
+ /// The reason the manifest was saved.
+ public void Save(FileInfo manifestFile, string reason)
+ {
+ Log.Verbose("Saving manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason);
+
+ try
+ {
+ Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
+ }
+ catch
+ {
+ Log.Error("Could not write out manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason);
+ throw;
+ }
+ }
///
/// Loads a plugin manifest from file.
diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs
similarity index 71%
rename from Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
rename to Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs
index 09084d569..952650c72 100644
--- a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs
@@ -1,7 +1,7 @@
using JetBrains.Annotations;
using Newtonsoft.Json;
-namespace Dalamud.Plugin.Internal.Types;
+namespace Dalamud.Plugin.Internal.Types.Manifest;
///
/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
@@ -16,4 +16,9 @@ internal record RemotePluginManifest : PluginManifest
///
[JsonIgnore]
public PluginRepository SourceRepo { get; set; } = null!;
+
+ ///
+ /// Gets a value indicating whether this plugin is eligible for testing.
+ ///
+ public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion;
}
diff --git a/Dalamud/Plugin/Internal/Types/PluginDef.cs b/Dalamud/Plugin/Internal/Types/PluginDef.cs
index c5dbede0d..049e58d7d 100644
--- a/Dalamud/Plugin/Internal/Types/PluginDef.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginDef.cs
@@ -1,5 +1,7 @@
using System.IO;
+using Dalamud.Plugin.Internal.Types.Manifest;
+
namespace Dalamud.Plugin.Internal.Types;
///
diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
index 71051e666..0b5ec26fc 100644
--- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Dalamud.Game;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types;
@@ -9,41 +10,29 @@ namespace Dalamud.Plugin.Internal.Types;
///
/// Information about a plugin, packaged in a json file with the DLL.
///
-internal record PluginManifest
+internal record PluginManifest : IPluginManifest
{
- ///
- /// Gets the author/s of the plugin.
- ///
+ ///
[JsonProperty]
public string? Author { get; init; }
- ///
- /// Gets or sets the public name of the plugin.
- ///
+ ///
[JsonProperty]
public string Name { get; set; } = null!;
- ///
- /// Gets a punchline of the plugins functions.
- ///
+ ///
[JsonProperty]
public string? Punchline { get; init; }
- ///
- /// Gets a description of the plugins functions.
- ///
+ ///
[JsonProperty]
public string? Description { get; init; }
- ///
- /// Gets a changelog.
- ///
+ ///
[JsonProperty]
public string? Changelog { get; init; }
- ///
- /// Gets a list of tags defined on the plugin.
- ///
+ ///
[JsonProperty]
public List? Tags { get; init; }
@@ -60,33 +49,23 @@ internal record PluginManifest
[JsonProperty]
public bool IsHide { get; init; }
- ///
- /// Gets the internal name of the plugin, which should match the assembly name of the plugin.
- ///
+ ///
[JsonProperty]
- public string InternalName { get; init; } = null!;
+ public string InternalName { get; set; } = null!;
- ///
- /// Gets the current assembly version of the plugin.
- ///
+ ///
[JsonProperty]
- public Version AssemblyVersion { get; init; } = null!;
+ public Version AssemblyVersion { get; set; } = null!;
- ///
- /// Gets the current testing assembly version of the plugin.
- ///
+ ///
[JsonProperty]
public Version? TestingAssemblyVersion { get; init; }
- ///
- /// Gets a value indicating whether the plugin is only available for testing.
- ///
+ ///
[JsonProperty]
public bool IsTestingExclusive { get; init; }
- ///
- /// Gets an URL to the website or source code of the plugin.
- ///
+ ///
[JsonProperty]
public string? RepoUrl { get; init; }
@@ -97,24 +76,17 @@ internal record PluginManifest
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
- ///
- /// Gets the API level of this plugin. For the current API level, please see
- /// for the currently used API level.
- ///
+ ///
[JsonProperty]
public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
- ///
- /// Gets the number of downloads this plugin has.
- ///
+ ///
[JsonProperty]
public long DownloadCount { get; init; }
- ///
- /// Gets the last time this plugin was updated.
- ///
+ ///
[JsonProperty]
- public long LastUpdate { get; init; }
+ public long LastUpdate { get; set; }
///
/// Gets the download link used to install the plugin.
@@ -156,26 +128,18 @@ internal record PluginManifest
[JsonProperty]
public int LoadPriority { get; init; }
- ///
- /// Gets a value indicating whether the plugin can be unloaded asynchronously.
- ///
+ ///
[JsonProperty]
- public bool CanUnloadAsync { get; init; }
+ public bool CanUnloadAsync { get; set; }
- ///
- /// Gets a value indicating whether the plugin supports profiles.
- ///
+ ///
[JsonProperty]
public bool SupportsProfiles { get; init; } = true;
- ///
- /// Gets a list of screenshot image URLs to show in the plugin installer.
- ///
+ ///
public List? ImageUrls { get; init; }
- ///
- /// Gets an URL for the plugin's icon.
- ///
+ ///
public string? IconUrl { get; init; }
///
@@ -183,21 +147,10 @@ internal record PluginManifest
///
public bool AcceptsFeedback { get; init; } = true;
- ///
- /// Gets a message that is shown to users when sending feedback.
- ///
+ ///
public string? FeedbackMessage { get; init; }
- ///
- /// Gets a value indicating whether this plugin is DIP17.
- /// To be removed.
- ///
- [JsonProperty("_isDip17Plugin")]
- public bool IsDip17Plugin { get; init; } = false;
-
- ///
- /// Gets the DIP17 channel name.
- ///
+ ///
[JsonProperty("_Dip17Channel")]
public string? Dip17Channel { get; init; }
}
diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs
index b51a3355c..a1097abce 100644
--- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs
+++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Http;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
using Newtonsoft.Json;
diff --git a/Dalamud/Plugin/Services/IBuddyList.cs b/Dalamud/Plugin/Services/IBuddyList.cs
new file mode 100644
index 000000000..f273d71c9
--- /dev/null
+++ b/Dalamud/Plugin/Services/IBuddyList.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Game.ClientState.Buddy;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This collection represents the buddies present in your squadron or trust party.
+/// It does not include the local player.
+///
+public interface IBuddyList : IReadOnlyCollection
+{
+ ///
+ /// Gets the amount of battle buddies the local player has.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets the active companion buddy.
+ ///
+ public BuddyMember? CompanionBuddy { get; }
+
+ ///
+ /// Gets the active pet buddy.
+ ///
+ public BuddyMember? PetBuddy { get; }
+
+ ///
+ /// Gets a battle buddy at the specified spawn index.
+ ///
+ /// Spawn index.
+ /// A at the specified spawn index.
+ public BuddyMember? this[int index] { get; }
+
+ ///
+ /// Gets the address of the companion buddy.
+ ///
+ /// The memory address of the companion buddy.
+ public nint GetCompanionBuddyMemberAddress();
+
+ ///
+ /// Gets the address of the pet buddy.
+ ///
+ /// The memory address of the pet buddy.
+ public nint GetPetBuddyMemberAddress();
+
+ ///
+ /// Gets the address of the battle buddy at the specified index of the buddy list.
+ ///
+ /// The index of the battle buddy.
+ /// The memory address of the battle buddy.
+ public nint GetBattleBuddyMemberAddress(int index);
+
+ ///
+ /// Create a reference to a buddy.
+ ///
+ /// The address of the buddy in memory.
+ /// object containing the requested data.
+ public BuddyMember? CreateBuddyMemberReference(nint address);
+}
diff --git a/Dalamud/Plugin/Services/ICommandManager.cs b/Dalamud/Plugin/Services/ICommandManager.cs
new file mode 100644
index 000000000..ead7723eb
--- /dev/null
+++ b/Dalamud/Plugin/Services/ICommandManager.cs
@@ -0,0 +1,46 @@
+using System.Collections.ObjectModel;
+
+using Dalamud.Game.Command;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class manages registered in-game slash commands.
+///
+public interface ICommandManager
+{
+ ///
+ /// Gets a read-only list of all registered commands.
+ ///
+ public ReadOnlyDictionary Commands { get; }
+
+ ///
+ /// Process a command in full.
+ ///
+ /// The full command string.
+ /// True if the command was found and dispatched.
+ public bool ProcessCommand(string content);
+
+ ///
+ /// Dispatch the handling of a command.
+ ///
+ /// The command to dispatch.
+ /// The provided arguments.
+ /// A object describing this command.
+ public void DispatchCommand(string command, string argument, CommandInfo info);
+
+ ///
+ /// Add a command handler, which you can use to add your own custom commands to the in-game chat.
+ ///
+ /// The command to register.
+ /// A object describing the command.
+ /// If adding was successful.
+ public bool AddHandler(string command, CommandInfo info);
+
+ ///
+ /// Remove a command from the command handlers.
+ ///
+ /// The command to remove.
+ /// If the removal was successful.
+ public bool RemoveHandler(string command);
+}
diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs
new file mode 100644
index 000000000..fa8c5bf43
--- /dev/null
+++ b/Dalamud/Plugin/Services/IDataManager.cs
@@ -0,0 +1,181 @@
+using System.Collections.ObjectModel;
+
+using ImGuiScene;
+using Lumina;
+using Lumina.Data;
+using Lumina.Data.Files;
+using Lumina.Excel;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
+///
+public interface IDataManager
+{
+ ///
+ /// Gets the current game client language.
+ ///
+ public ClientLanguage Language { get; }
+
+ ///
+ /// Gets the OpCodes sent by the server to the client.
+ ///
+ public ReadOnlyDictionary ServerOpCodes { get; }
+
+ ///
+ /// Gets the OpCodes sent by the client to the server.
+ ///
+ public ReadOnlyDictionary ClientOpCodes { get; }
+
+ ///
+ /// Gets a object which gives access to any excel/game data.
+ ///
+ public GameData GameData { get; }
+
+ ///
+ /// Gets an object which gives access to any of the game's sheet data.
+ ///
+ public ExcelModule Excel { get; }
+
+ ///
+ /// Gets a value indicating whether Game Data is ready to be read.
+ ///
+ public bool IsDataReady { get; }
+
+ ///
+ /// Gets a value indicating whether the game data files have been modified by another third-party tool.
+ ///
+ public bool HasModifiedGameDataFiles { get; }
+
+ ///
+ /// Get an with the given Excel sheet row type.
+ ///
+ /// The excel sheet type to get.
+ /// The , giving access to game rows.
+ public ExcelSheet? GetExcelSheet() where T : ExcelRow;
+
+ ///
+ /// Get an with the given Excel sheet row type with a specified language.
+ ///
+ /// Language of the sheet to get.
+ /// The excel sheet type to get.
+ /// The , giving access to game rows.
+ public ExcelSheet? GetExcelSheet(ClientLanguage language) where T : ExcelRow;
+
+ ///
+ /// Get a with the given path.
+ ///
+ /// The path inside of the game files.
+ /// The of the file.
+ public FileResource? GetFile(string path);
+
+ ///
+ /// Get a with the given path, of the given type.
+ ///
+ /// The type of resource.
+ /// The path inside of the game files.
+ /// The of the file.
+ public T? GetFile(string path) where T : FileResource;
+
+ ///
+ /// Check if the file with the given path exists within the game's index files.
+ ///
+ /// The path inside of the game files.
+ /// True if the file exists.
+ public bool FileExists(string path);
+
+ ///
+ /// Get a containing the icon with the given ID.
+ ///
+ /// The icon ID.
+ /// Return high resolution version.
+ /// The containing the icon.
+ public TexFile? GetIcon(uint iconId, bool highResolution = false);
+
+ ///
+ /// Get a containing the icon with the given ID, of the given language.
+ ///
+ /// The requested language.
+ /// The icon ID.
+ /// Return high resolution version.
+ /// The containing the icon.
+ public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false);
+
+ ///
+ /// Get a containing the icon with the given ID, of the given type.
+ ///
+ /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
+ /// The icon ID.
+ /// Return high resolution version.
+ /// The containing the icon.
+ public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false);
+
+ ///
+ /// Get a containing the icon with the given ID.
+ ///
+ /// The icon ID.
+ /// Return the high resolution version.
+ /// The containing the icon.
+ public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false);
+
+ ///
+ /// Get a containing the icon with the given ID, of the given quality.
+ ///
+ /// A value indicating whether the icon should be HQ.
+ /// The icon ID.
+ /// The containing the icon.
+ public TexFile? GetIcon(bool isHq, uint iconId);
+
+ ///
+ /// Get a containing the HQ icon with the given ID.
+ ///
+ /// The icon ID.
+ /// The containing the icon.
+ public TexFile? GetHqIcon(uint iconId);
+
+ ///
+ /// Get the passed as a drawable ImGui TextureWrap.
+ ///
+ /// The Lumina .
+ /// A that can be used to draw the texture.
+ public TextureWrap? GetImGuiTexture(TexFile? tex);
+
+ ///
+ /// Get the passed texture path as a drawable ImGui TextureWrap.
+ ///
+ /// The internal path to the texture.
+ /// A that can be used to draw the texture.
+ public TextureWrap? GetImGuiTexture(string path);
+
+ ///
+ /// Get a containing the icon with the given ID, of the given quality.
+ ///
+ /// A value indicating whether the icon should be HQ.
+ /// The icon ID.
+ /// The containing the icon.
+ public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId);
+
+ ///
+ /// Get a containing the icon with the given ID, of the given language.
+ ///
+ /// The requested language.
+ /// The icon ID.
+ /// The containing the icon.
+ public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId);
+
+ ///
+ /// Get a containing the icon with the given ID, of the given type.
+ ///
+ /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).
+ /// The icon ID.
+ /// The containing the icon.
+ public TextureWrap? GetImGuiTextureIcon(string type, uint iconId);
+
+ ///
+ /// Get a containing the HQ icon with the given ID.
+ ///
+ /// The icon ID.
+ /// The containing the icon.
+ public TextureWrap? GetImGuiTextureHqIcon(uint iconId);
+}
diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs
new file mode 100644
index 000000000..6c2b8ad1e
--- /dev/null
+++ b/Dalamud/Plugin/Services/IDtrBar.cs
@@ -0,0 +1,22 @@
+using System;
+
+using Dalamud.Game.Gui.Dtr;
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Class used to interface with the server info bar.
+///
+public interface IDtrBar
+{
+ ///
+ /// Get a DTR bar entry.
+ /// This allows you to add your own text, and users to sort it.
+ ///
+ /// A user-friendly name for sorting.
+ /// The text the entry shows.
+ /// The entry object used to update, hide and remove the entry.
+ /// Thrown when an entry with the specified title exists.
+ public DtrBarEntry Get(string title, SeString? text = null);
+}
diff --git a/Dalamud/Plugin/Services/IDutyState.cs b/Dalamud/Plugin/Services/IDutyState.cs
new file mode 100644
index 000000000..a2331364c
--- /dev/null
+++ b/Dalamud/Plugin/Services/IDutyState.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class represents the state of the currently occupied duty.
+///
+public interface IDutyState
+{
+ ///
+ /// Event that gets fired when the duty starts.
+ /// Triggers when the "Duty Start" message displays, and on the removal of the ring at duty's spawn.
+ /// Does not trigger when loading into a duty that was in progress, or from loading in after a disconnect.
+ ///
+ public event EventHandler DutyStarted;
+
+ ///
+ /// Event that gets fired when everyone in the party dies and the screen fades to black.
+ ///
+ public event EventHandler DutyWiped;
+
+ ///
+ /// Event that gets fired when the "Duty Recommence" message displays, and on the removal of the ring at duty's spawn.
+ ///
+ public event EventHandler DutyRecommenced;
+
+ ///
+ /// Event that gets fired when the duty is completed successfully.
+ ///
+ public event EventHandler DutyCompleted;
+
+ ///
+ /// Gets a value indicating whether the current duty has been started.
+ ///
+ public bool IsDutyStarted { get; }
+}
diff --git a/Dalamud/Plugin/Services/IFateTable.cs b/Dalamud/Plugin/Services/IFateTable.cs
new file mode 100644
index 000000000..ba243ec04
--- /dev/null
+++ b/Dalamud/Plugin/Services/IFateTable.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.ClientState.Fates;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This collection represents the currently available Fate events.
+///
+public interface IFateTable : IReadOnlyCollection
+{
+ ///
+ /// Gets the address of the Fate table.
+ ///
+ public nint Address { get; }
+
+ ///
+ /// Gets the amount of currently active Fates.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Get an actor at the specified spawn index.
+ ///
+ /// Spawn index.
+ /// A at the specified spawn index.
+ public Fate? this[int index] { get; }
+
+ ///
+ /// Gets the address of the Fate at the specified index of the fate table.
+ /// wo
+ /// The index of the Fate.
+ /// The memory address of the Fate.
+ public nint GetFateAddress(int index);
+
+ ///
+ /// Create a reference to a FFXIV actor.
+ ///
+ /// The offset of the actor in memory.
+ /// object containing requested data.
+ public Fate? CreateFateReference(nint offset);
+}
diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs
new file mode 100644
index 000000000..98f6160cc
--- /dev/null
+++ b/Dalamud/Plugin/Services/IGameConfig.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Diagnostics;
+
+using Dalamud.Game.Config;
+using FFXIVClientStructs.FFXIV.Common.Configuration;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class represents the game's configuration.
+///
+public interface IGameConfig
+{
+ ///
+ /// Event which is fired when a game config option is changed.
+ ///
+ public event EventHandler Changed;
+
+ ///
+ /// Gets the collection of config options that persist between characters.
+ ///
+ public GameConfigSection System { get; }
+
+ ///
+ /// Gets the collection of config options that are character specific.
+ ///
+ public GameConfigSection UiConfig { get; }
+
+ ///
+ /// Gets the collection of config options that are control mode specific. (Mouse and Keyboard / Gamepad).
+ ///
+ public GameConfigSection UiControl { get; }
+
+ ///
+ /// Attempts to get a boolean config value from the System section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out bool value);
+
+ ///
+ /// Attempts to get a uint config value from the System section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out uint value);
+
+ ///
+ /// Attempts to get a float config value from the System section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out float value);
+
+ ///
+ /// Attempts to get a string config value from the System section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out string value);
+
+ ///
+ /// Attempts to get the properties of a UInt option from the System section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties);
+
+ ///
+ /// Attempts to get the properties of a Float option from the System section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties);
+
+ ///
+ /// Attempts to get the properties of a String option from the System section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Default Value
+ /// A value representing the success.
+ public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties);
+
+ ///
+ /// Attempts to get a boolean config value from the UiConfig section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out bool value);
+
+ ///
+ /// Attempts to get a uint config value from the UiConfig section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out uint value);
+
+ ///
+ /// Attempts to get a float config value from the UiConfig section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out float value);
+
+ ///
+ /// Attempts to get a string config value from the UiConfig section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out string value);
+
+ ///
+ /// Attempts to get the properties of a UInt option from the UiConfig section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out UIntConfigProperties? properties);
+
+ ///
+ /// Attempts to get the properties of a Float option from the UiConfig section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out FloatConfigProperties? properties);
+
+ ///
+ /// Attempts to get the properties of a String option from the UiConfig section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Default Value
+ /// A value representing the success.
+ public bool TryGet(UiConfigOption option, out StringConfigProperties? properties);
+
+ ///
+ /// Attempts to get a boolean config value from the UiControl section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out bool value);
+
+ ///
+ /// Attempts to get a uint config value from the UiControl section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out uint value);
+
+ ///
+ /// Attempts to get a float config value from the UiControl section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out float value);
+
+ ///
+ /// Attempts to get a string config value from the UiControl section.
+ ///
+ /// Option to get the value of.
+ /// The returned value of the config option.
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out string value);
+
+ ///
+ /// Attempts to get the properties of a UInt option from the UiControl section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out UIntConfigProperties? properties);
+
+ ///
+ /// Attempts to get the properties of a Float option from the UiControl section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Minimum, Maximum, and Default values.
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out FloatConfigProperties? properties);
+
+ ///
+ /// Attempts to get the properties of a String option from the UiControl section.
+ ///
+ /// Option to get the properties of.
+ /// Details of the option: Default Value
+ /// A value representing the success.
+ public bool TryGet(UiControlOption option, out StringConfigProperties? properties);
+
+ ///
+ /// Set a boolean config option in the System config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(SystemConfigOption option, bool value);
+
+ ///
+ /// Set a unsigned integer config option in the System config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(SystemConfigOption option, uint value);
+
+ ///
+ /// Set a float config option in the System config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(SystemConfigOption option, float value);
+
+ ///
+ /// Set a string config option in the System config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(SystemConfigOption option, string value);
+
+ ///
+ /// Set a boolean config option in the UiConfig section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiConfigOption option, bool value);
+
+ ///
+ /// Set a unsigned integer config option in the UiConfig section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiConfigOption option, uint value);
+
+ ///
+ /// Set a float config option in the UiConfig section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiConfigOption option, float value);
+
+ ///
+ /// Set a string config option in the UiConfig section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiConfigOption option, string value);
+
+ ///
+ /// Set a boolean config option in the UiControl config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiControlOption option, bool value);
+
+ ///
+ /// Set a uint config option in the UiControl config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiControlOption option, uint value);
+
+ ///
+ /// Set a float config option in the UiControl config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiControlOption option, float value);
+
+ ///
+ /// Set a string config option in the UiControl config section.
+ /// Note: Not all config options will be be immediately reflected in the game.
+ ///
+ /// Name of the config option.
+ /// New value of the config option.
+ /// Throw if the config option is not found.
+ /// Thrown if the name of the config option is found, but the struct was not.
+ public void Set(UiControlOption option, string value);
+}
diff --git a/Dalamud/Plugin/Services/IGameGui.cs b/Dalamud/Plugin/Services/IGameGui.cs
new file mode 100644
index 000000000..ddb0ec67c
--- /dev/null
+++ b/Dalamud/Plugin/Services/IGameGui.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Numerics;
+
+using Dalamud.Game.Gui;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// A class handling many aspects of the in-game UI.
+///
+public unsafe interface IGameGui
+{
+ ///
+ /// Event which is fired when the game UI hiding is toggled.
+ ///
+ public event EventHandler UiHideToggled;
+
+ ///
+ /// Event that is fired when the currently hovered item changes.
+ ///
+ public event EventHandler HoveredItemChanged;
+
+ ///
+ /// Event that is fired when the currently hovered action changes.
+ ///
+ public event EventHandler HoveredActionChanged;
+
+ ///
+ /// Gets a value indicating whether the game UI is hidden.
+ ///
+ public bool GameUiHidden { get; }
+
+ ///
+ /// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered.
+ /// If > 1.000.000, subtract 1.000.000 and treat it as HQ.
+ ///
+ public ulong HoveredItem { get; set; }
+
+ ///
+ /// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
+ ///
+ public HoveredAction HoveredAction { get; }
+
+ ///
+ /// Opens the in-game map with a flag on the location of the parameter.
+ ///
+ /// Link to the map to be opened.
+ /// True if there were no errors and it could open the map.
+ public bool OpenMapWithMapLink(MapLinkPayload mapLink);
+
+ ///
+ /// Converts in-world coordinates to screen coordinates (upper left corner origin).
+ ///
+ /// Coordinates in the world.
+ /// Converted coordinates.
+ /// True if worldPos corresponds to a position in front of the camera and screenPos is in the viewport.
+ public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos);
+
+ ///
+ /// Converts in-world coordinates to screen coordinates (upper left corner origin).
+ ///
+ /// Coordinates in the world.
+ /// Converted coordinates.
+ /// True if screenPos corresponds to a position inside the camera viewport.
+ /// True if worldPos corresponds to a position in front of the camera.
+ public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView);
+
+ ///
+ /// Converts screen coordinates to in-world coordinates via raycasting.
+ ///
+ /// Screen coordinates.
+ /// Converted coordinates.
+ /// How far to search for a collision.
+ /// True if successful. On false, worldPos's contents are undefined.
+ public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f);
+
+ ///
+ /// Gets a pointer to the game's UI module.
+ ///
+ /// IntPtr pointing to UI module.
+ public nint GetUIModule();
+
+ ///
+ /// Gets the pointer to the Addon with the given name and index.
+ ///
+ /// Name of addon to find.
+ /// Index of addon to find (1-indexed).
+ /// nint.Zero if unable to find UI, otherwise nint pointing to the start of the addon.
+ public nint GetAddonByName(string name, int index = 1);
+
+ ///
+ /// Find the agent associated with an addon, if possible.
+ ///
+ /// The addon name.
+ /// A pointer to the agent interface.
+ public nint FindAgentInterface(string addonName);
+
+ ///
+ /// Find the agent associated with an addon, if possible.
+ ///
+ /// The addon address.
+ /// A pointer to the agent interface.
+ public nint FindAgentInterface(void* addon);
+
+ ///
+ /// Find the agent associated with an addon, if possible.
+ ///
+ /// The addon address.
+ /// A pointer to the agent interface.
+ public IntPtr FindAgentInterface(IntPtr addonPtr);
+}
diff --git a/Dalamud/Plugin/Services/IGameLifecycle.cs b/Dalamud/Plugin/Services/IGameLifecycle.cs
new file mode 100644
index 000000000..caa64ed23
--- /dev/null
+++ b/Dalamud/Plugin/Services/IGameLifecycle.cs
@@ -0,0 +1,24 @@
+using System.Threading;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Class offering cancellation tokens for common gameplay events.
+///
+public interface IGameLifecycle
+{
+ ///
+ /// Gets a token that is cancelled when Dalamud is unloading.
+ ///
+ public CancellationToken DalamudUnloadingToken { get; }
+
+ ///
+ /// Gets a token that is cancelled when the game is shutting down.
+ ///
+ public CancellationToken GameShuttingDownToken { get; }
+
+ ///
+ /// Gets a token that is cancelled when a character is logging out.
+ ///
+ public CancellationToken LogoutToken { get; }
+}
diff --git a/Dalamud/Plugin/Services/IGamepadState.cs b/Dalamud/Plugin/Services/IGamepadState.cs
new file mode 100644
index 000000000..c349923f3
--- /dev/null
+++ b/Dalamud/Plugin/Services/IGamepadState.cs
@@ -0,0 +1,68 @@
+using System.Numerics;
+
+using Dalamud.Game.ClientState.GamePad;
+using ImGuiNET;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Exposes the game gamepad state to dalamud.
+///
+/// Will block game's gamepad input if is set.
+///
+public interface IGamepadState
+{
+ ///
+ /// Gets the pointer to the current instance of the GamepadInput struct.
+ ///
+ public nint GamepadInputAddress { get; }
+
+ ///
+ /// Gets the left analogue sticks tilt vector.
+ ///
+ public Vector2 LeftStick { get; }
+
+ ///
+ /// Gets the right analogue sticks tilt vector.
+ ///
+ public Vector2 RightStick { get; }
+
+ ///
+ /// Gets whether has been pressed.
+ ///
+ /// Only true on first frame of the press.
+ /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
+ ///
+ /// The button to check for.
+ /// 1 if pressed, 0 otherwise.
+ public float Pressed(GamepadButtons button);
+
+ ///
+ /// Gets whether is being pressed.
+ ///
+ /// True in intervals if button is held down.
+ /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
+ ///
+ /// The button to check for.
+ /// 1 if still pressed during interval, 0 otherwise or in between intervals.
+ public float Repeat(GamepadButtons button);
+
+ ///
+ /// Gets whether has been released.
+ ///
+ /// Only true the frame after release.
+ /// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
+ ///
+ /// The button to check for.
+ /// 1 if released, 0 otherwise.
+ public float Released(GamepadButtons button);
+
+ ///
+ /// Gets the raw state of .
+ ///
+ /// Is set the entire time a button is pressed down.
+ ///
+ /// The button to check for.
+ /// 1 the whole time button is pressed, 0 otherwise.
+ public float Raw(GamepadButtons button);
+}
diff --git a/Dalamud/Plugin/Services/IJobGauges.cs b/Dalamud/Plugin/Services/IJobGauges.cs
new file mode 100644
index 000000000..4489a7be7
--- /dev/null
+++ b/Dalamud/Plugin/Services/IJobGauges.cs
@@ -0,0 +1,21 @@
+using Dalamud.Game.ClientState.JobGauge.Types;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class converts in-memory Job gauge data to structs.
+///
+public interface IJobGauges
+{
+ ///
+ /// Gets the address of the JobGauge data.
+ ///
+ public nint Address { get; }
+
+ ///
+ /// Get the JobGauge for a given job.
+ ///
+ /// A JobGauge struct from ClientState.Structs.JobGauge.
+ /// A JobGauge.
+ public T Get() where T : JobGaugeBase;
+}
diff --git a/Dalamud/Plugin/Services/ILibcFunction.cs b/Dalamud/Plugin/Services/ILibcFunction.cs
new file mode 100644
index 000000000..bebd62936
--- /dev/null
+++ b/Dalamud/Plugin/Services/ILibcFunction.cs
@@ -0,0 +1,26 @@
+using System.Text;
+
+using Dalamud.Game.Libc;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class handles creating cstrings utilizing native game methods.
+///
+public interface ILibcFunction
+{
+ ///
+ /// Create a new string from the given bytes.
+ ///
+ /// The bytes to convert.
+ /// An owned std string object.
+ public OwnedStdString NewString(byte[] content);
+
+ ///
+ /// Create a new string form the given bytes.
+ ///
+ /// The bytes to convert.
+ /// A non-default encoding.
+ /// An owned std string object.
+ public OwnedStdString NewString(string content, Encoding? encoding = null);
+}
diff --git a/Dalamud/Plugin/Services/IObjectTable.cs b/Dalamud/Plugin/Services/IObjectTable.cs
new file mode 100644
index 000000000..d029045fa
--- /dev/null
+++ b/Dalamud/Plugin/Services/IObjectTable.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.ClientState.Objects.Types;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This collection represents the currently spawned FFXIV game objects.
+///
+public interface IObjectTable : IReadOnlyCollection
+{
+ ///
+ /// Gets the address of the object table.
+ ///
+ public nint Address { get; }
+
+ ///
+ /// Gets the length of the object table.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Get an object at the specified spawn index.
+ ///
+ /// Spawn index.
+ /// An at the specified spawn index.
+ public GameObject? this[int index] { get; }
+
+ ///
+ /// Search for a game object by their Object ID.
+ ///
+ /// Object ID to find.
+ /// A game object or null.
+ public GameObject? SearchById(ulong objectId);
+
+ ///
+ /// Gets the address of the game object at the specified index of the object table.
+ ///
+ /// The index of the object.
+ /// The memory address of the object.
+ public nint GetObjectAddress(int index);
+
+ ///
+ /// Create a reference to an FFXIV game object.
+ ///
+ /// The address of the object in memory.
+ /// object or inheritor containing the requested data.
+ public GameObject? CreateObjectReference(nint address);
+}
diff --git a/Dalamud/Plugin/Services/IPartyList.cs b/Dalamud/Plugin/Services/IPartyList.cs
new file mode 100644
index 000000000..fbf663a00
--- /dev/null
+++ b/Dalamud/Plugin/Services/IPartyList.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.ClientState.Party;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This collection represents the actors present in your party or alliance.
+///
+public interface IPartyList : IReadOnlyCollection
+{
+ ///
+ /// Gets the amount of party members the local player has.
+ ///
+ public int Length { get; }
+
+ ///
+ /// Gets the index of the party leader.
+ ///
+ public uint PartyLeaderIndex { get; }
+
+ ///
+ /// Gets a value indicating whether this group is an alliance.
+ ///
+ public bool IsAlliance { get; }
+
+ ///
+ /// Gets the address of the Group Manager.
+ ///
+ public nint GroupManagerAddress { get; }
+
+ ///
+ /// Gets the address of the party list within the group manager.
+ ///
+ public nint GroupListAddress { get; }
+
+ ///
+ /// Gets the address of the alliance member list within the group manager.
+ ///
+ public nint AllianceListAddress { get; }
+
+ ///
+ /// Gets the ID of the party.
+ ///
+ public long PartyId { get; }
+
+ ///
+ /// Get a party member at the specified spawn index.
+ ///
+ /// Spawn index.
+ /// A at the specified spawn index.
+ public PartyMember? this[int index] { get; }
+
+ ///
+ /// Gets the address of the party member at the specified index of the party list.
+ ///
+ /// The index of the party member.
+ /// The memory address of the party member.
+ public nint GetPartyMemberAddress(int index);
+
+ ///
+ /// Create a reference to an FFXIV party member.
+ ///
+ /// The address of the party member in memory.
+ /// The party member object containing the requested data.
+ public PartyMember? CreatePartyMemberReference(nint address);
+
+ ///
+ /// Gets the address of the alliance member at the specified index of the alliance list.
+ ///
+ /// The index of the alliance member.
+ /// The memory address of the alliance member.
+ public nint GetAllianceMemberAddress(int index);
+
+ ///
+ /// Create a reference to an FFXIV alliance member.
+ ///
+ /// The address of the alliance member in memory.
+ /// The party member object containing the requested data.
+ public PartyMember? CreateAllianceMemberReference(nint address);
+}
diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs
new file mode 100644
index 000000000..c3bb7d6c1
--- /dev/null
+++ b/Dalamud/Plugin/Services/ISigScanner.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Diagnostics;
+
+namespace Dalamud.Game;
+
+///
+/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
+///
+public interface ISigScanner
+{
+ ///
+ /// Gets a value indicating whether or not the search on this module is performed on a copy.
+ ///
+ public bool IsCopy { get; }
+
+ ///
+ /// Gets a value indicating whether or not the ProcessModule is 32-bit.
+ ///
+ public bool Is32BitProcess { get; }
+
+ ///
+ /// Gets the base address of the search area. When copied, this will be the address of the copy.
+ ///
+ public IntPtr SearchBase { get; }
+
+ ///
+ /// Gets the base address of the .text section search area.
+ ///
+ public IntPtr TextSectionBase { get; }
+
+ ///
+ /// Gets the offset of the .text section from the base of the module.
+ ///
+ public long TextSectionOffset { get; }
+
+ ///
+ /// Gets the size of the text section.
+ ///
+ public int TextSectionSize { get; }
+
+ ///
+ /// Gets the base address of the .data section search area.
+ ///
+ public IntPtr DataSectionBase { get; }
+
+ ///
+ /// Gets the offset of the .data section from the base of the module.
+ ///
+ public long DataSectionOffset { get; }
+
+ ///
+ /// Gets the size of the .data section.
+ ///
+ public int DataSectionSize { get; }
+
+ ///
+ /// Gets the base address of the .rdata section search area.
+ ///
+ public IntPtr RDataSectionBase { get; }
+
+ ///
+ /// Gets the offset of the .rdata section from the base of the module.
+ ///
+ public long RDataSectionOffset { get; }
+
+ ///
+ /// Gets the size of the .rdata section.
+ ///
+ public int RDataSectionSize { get; }
+
+ ///
+ /// Gets the ProcessModule on which the search is performed.
+ ///
+ public ProcessModule Module { get; }
+
+ ///
+ /// Scan for a .data address using a .text function.
+ /// This is intended to be used with IDA sigs.
+ /// Place your cursor on the line calling a static address, and create and IDA sig.
+ /// The signature and offset should not break through instruction boundaries.
+ ///
+ /// The signature of the function using the data.
+ /// The offset from function start of the instruction using the data.
+ /// An IntPtr to the static memory location.
+ public nint GetStaticAddressFromSig(string signature, int offset = 0);
+
+ ///
+ /// Try scanning for a .data address using a .text function.
+ /// This is intended to be used with IDA sigs.
+ /// Place your cursor on the line calling a static address, and create and IDA sig.
+ ///
+ /// The signature of the function using the data.
+ /// An IntPtr to the static memory location, if found.
+ /// The offset from function start of the instruction using the data.
+ /// true if the signature was found.
+ public bool TryGetStaticAddressFromSig(string signature, out nint result, int offset = 0);
+
+ ///
+ /// Scan for a byte signature in the .data section.
+ ///
+ /// The signature.
+ /// The real offset of the found signature.
+ public nint ScanData(string signature);
+
+ ///
+ /// Try scanning for a byte signature in the .data section.
+ ///
+ /// The signature.
+ /// The real offset of the signature, if found.
+ /// true if the signature was found.
+ public bool TryScanData(string signature, out nint result);
+
+ ///
+ /// Scan for a byte signature in the whole module search area.
+ ///
+ /// The signature.
+ /// The real offset of the found signature.
+ public nint ScanModule(string signature);
+
+ ///
+ /// Try scanning for a byte signature in the whole module search area.
+ ///
+ /// The signature.
+ /// The real offset of the signature, if found.
+ /// true if the signature was found.
+ public bool TryScanModule(string signature, out nint result);
+
+ ///
+ /// Resolve a RVA address.
+ ///
+ /// The address of the next instruction.
+ /// The relative offset.
+ /// The calculated offset.
+ public nint ResolveRelativeAddress(nint nextInstAddr, int relOffset);
+
+ ///
+ /// Scan for a byte signature in the .text section.
+ ///
+ /// The signature.
+ /// The real offset of the found signature.
+ public nint ScanText(string signature);
+
+ ///
+ /// Try scanning for a byte signature in the .text section.
+ ///
+ /// The signature.
+ /// The real offset of the signature, if found.
+ /// true if the signature was found.
+ public bool TryScanText(string signature, out nint result);
+}
diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs
new file mode 100644
index 000000000..108b1ca03
--- /dev/null
+++ b/Dalamud/Plugin/Services/ITargetManager.cs
@@ -0,0 +1,44 @@
+using Dalamud.Game.ClientState.Objects.Types;
+
+namespace Dalamud.Game.ClientState.Objects;
+
+///
+/// Get and set various kinds of targets for the player.
+///
+public interface ITargetManager
+{
+ ///
+ /// Gets the address of the target manager.
+ ///
+ public nint Address { get; }
+
+ ///
+ /// Gets or sets the current target.
+ /// Set to null to clear the target.
+ ///
+ public GameObject? Target { get; set; }
+
+ ///
+ /// Gets or sets the mouseover target.
+ /// Set to null to clear the target.
+ ///
+ public GameObject? MouseOverTarget { get; set; }
+
+ ///
+ /// Gets or sets the focus target.
+ /// Set to null to clear the target.
+ ///
+ public GameObject? FocusTarget { get; set; }
+
+ ///
+ /// Gets or sets the previous target.
+ /// Set to null to clear the target.
+ ///
+ public GameObject? PreviousTarget { get; set; }
+
+ ///
+ /// Gets or sets the soft target.
+ /// Set to null to clear the target.
+ ///
+ public GameObject? SoftTarget { get; set; }
+}
diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs
index cdbf94616..22628303e 100644
--- a/Dalamud/Support/BugBait.cs
+++ b/Dalamud/Support/BugBait.cs
@@ -3,7 +3,7 @@ using System.Text;
using System.Threading.Tasks;
using Dalamud.Networking.Http;
-using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
using Newtonsoft.Json;
@@ -25,7 +25,7 @@ internal static class BugBait
/// The reporter name.
/// Whether or not the most recent exception to occur should be included in the report.
/// A representing the asynchronous operation.
- public static async Task SendFeedback(PluginManifest plugin, bool isTesting, string content, string reporter, bool includeException)
+ public static async Task SendFeedback(IPluginManifest plugin, bool isTesting, string content, string reporter, bool includeException)
{
if (content.IsNullOrWhitespace())
return;
diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs
index ef1897eeb..9893451f4 100644
--- a/Dalamud/Support/Troubleshooting.cs
+++ b/Dalamud/Support/Troubleshooting.cs
@@ -7,7 +7,7 @@ using Dalamud.Configuration;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal;
-using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Internal.Types.Manifest;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
@@ -67,7 +67,7 @@ public static class Troubleshooting
{
var payload = new TroubleshootingPayload
{
- LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest)?.OrderByDescending(x => x.InternalName).ToArray(),
+ 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,
diff --git a/Dalamud/Utility/FuzzyMatcher.cs b/Dalamud/Utility/FuzzyMatcher.cs
new file mode 100644
index 000000000..647c9586d
--- /dev/null
+++ b/Dalamud/Utility/FuzzyMatcher.cs
@@ -0,0 +1,273 @@
+#define BORDER_MATCHING
+
+namespace Dalamud.Utility;
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+internal readonly ref struct FuzzyMatcher
+{
+ private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>();
+
+ private readonly string needleString = string.Empty;
+ private readonly ReadOnlySpan needleSpan = ReadOnlySpan.Empty;
+ private readonly int needleFinalPosition = -1;
+ private readonly (int start, int end)[] needleSegments = EmptySegArray;
+ private readonly MatchMode mode = MatchMode.Simple;
+
+ public FuzzyMatcher(string term, MatchMode matchMode)
+ {
+ needleString = term;
+ needleSpan = needleString.AsSpan();
+ needleFinalPosition = needleSpan.Length - 1;
+ mode = matchMode;
+
+ switch (matchMode)
+ {
+ case MatchMode.FuzzyParts:
+ needleSegments = FindNeedleSegments(needleSpan);
+ break;
+ case MatchMode.Fuzzy:
+ case MatchMode.Simple:
+ needleSegments = EmptySegArray;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, null);
+ }
+ }
+
+ private static (int start, int end)[] FindNeedleSegments(ReadOnlySpan span)
+ {
+ var segments = new List<(int, int)>();
+ var wordStart = -1;
+
+ for (var i = 0; i < span.Length; i++)
+ {
+ if (span[i] is not ' ' and not '\u3000')
+ {
+ if (wordStart < 0)
+ {
+ wordStart = i;
+ }
+ }
+ else if (wordStart >= 0)
+ {
+ segments.Add((wordStart, i - 1));
+ wordStart = -1;
+ }
+ }
+
+ if (wordStart >= 0)
+ {
+ segments.Add((wordStart, span.Length - 1));
+ }
+
+ return segments.ToArray();
+ }
+
+ public int Matches(string value)
+ {
+ if (needleFinalPosition < 0)
+ {
+ return 0;
+ }
+
+ if (mode == MatchMode.Simple)
+ {
+ return value.Contains(needleString) ? 1 : 0;
+ }
+
+ var haystack = value.AsSpan();
+
+ if (mode == MatchMode.Fuzzy)
+ {
+ return GetRawScore(haystack, 0, needleFinalPosition);
+ }
+
+ if (mode == MatchMode.FuzzyParts)
+ {
+ if (needleSegments.Length < 2)
+ {
+ return GetRawScore(haystack, 0, needleFinalPosition);
+ }
+
+ var total = 0;
+ for (var i = 0; i < needleSegments.Length; i++)
+ {
+ var (start, end) = needleSegments[i];
+ var cur = GetRawScore(haystack, start, end);
+ if (cur == 0)
+ {
+ return 0;
+ }
+
+ total += cur;
+ }
+
+ return total;
+ }
+
+ return 8;
+ }
+
+ public int MatchesAny(params string[] values)
+ {
+ var max = 0;
+ for (var i = 0; i < values.Length; i++)
+ {
+ var cur = Matches(values[i]);
+ if (cur > max)
+ {
+ max = cur;
+ }
+ }
+
+ return max;
+ }
+
+ private int GetRawScore(ReadOnlySpan haystack, int needleStart, int needleEnd)
+ {
+ var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd);
+ if (startPos < 0)
+ {
+ return 0;
+ }
+
+ var needleSize = needleEnd - needleStart + 1;
+
+ var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
+ // PluginLog.Debug(
+ // $"['{needleString.Substring(needleStart, needleEnd - needleStart + 1)}' in '{haystack}'] fwd: needleSize={needleSize} startPos={startPos} gaps={gaps} consecutive={consecutive} borderMatches={borderMatches} score={score}");
+
+ (startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd);
+ var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
+ // PluginLog.Debug(
+ // $"['{needleString.Substring(needleStart, needleEnd - needleStart + 1)}' in '{haystack}'] rev: needleSize={needleSize} startPos={startPos} gaps={gaps} consecutive={consecutive} borderMatches={borderMatches} score={revScore}");
+
+ return int.Max(score, revScore);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches)
+ {
+ var score = 100
+ + needleSize * 3
+ + borderMatches * 3
+ + consecutive * 5
+ - startPos
+ - gaps * 2;
+ if (startPos == 0)
+ score += 5;
+ return score < 1 ? 1 : score;
+ }
+
+ private (int startPos, int gaps, int consecutive, int borderMatches, int haystackIndex) FindForward(
+ ReadOnlySpan haystack, int needleStart, int needleEnd)
+ {
+ var needleIndex = needleStart;
+ var lastMatchIndex = -10;
+
+ var startPos = 0;
+ var gaps = 0;
+ var consecutive = 0;
+ var borderMatches = 0;
+
+ for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++)
+ {
+ if (haystack[haystackIndex] == needleSpan[needleIndex])
+ {
+#if BORDER_MATCHING
+ if (haystackIndex > 0)
+ {
+ if (!char.IsLetterOrDigit(haystack[haystackIndex - 1]))
+ {
+ borderMatches++;
+ }
+ }
+#endif
+
+ needleIndex++;
+
+ if (haystackIndex == lastMatchIndex + 1)
+ {
+ consecutive++;
+ }
+
+ if (needleIndex > needleEnd)
+ {
+ return (startPos, gaps, consecutive, borderMatches, haystackIndex);
+ }
+
+ lastMatchIndex = haystackIndex;
+ }
+ else
+ {
+ if (needleIndex > needleStart)
+ {
+ gaps++;
+ }
+ else
+ {
+ startPos++;
+ }
+ }
+ }
+
+ return (-1, 0, 0, 0, 0);
+ }
+
+ private (int startPos, int gaps, int consecutive, int borderMatches) FindReverse(ReadOnlySpan haystack,
+ int haystackLastMatchIndex, int needleStart, int needleEnd)
+ {
+ var needleIndex = needleEnd;
+ var revLastMatchIndex = haystack.Length + 10;
+
+ var gaps = 0;
+ var consecutive = 0;
+ var borderMatches = 0;
+
+ for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--)
+ {
+ if (haystack[haystackIndex] == needleSpan[needleIndex])
+ {
+#if BORDER_MATCHING
+ if (haystackIndex > 0)
+ {
+ if (!char.IsLetterOrDigit(haystack[haystackIndex - 1]))
+ {
+ borderMatches++;
+ }
+ }
+#endif
+
+ needleIndex--;
+
+ if (haystackIndex == revLastMatchIndex - 1)
+ {
+ consecutive++;
+ }
+
+ if (needleIndex < needleStart)
+ {
+ return (haystackIndex, gaps, consecutive, borderMatches);
+ }
+
+ revLastMatchIndex = haystackIndex;
+ }
+ else
+ {
+ gaps++;
+ }
+ }
+
+ return (-1, 0, 0, 0);
+ }
+}
+
+public enum MatchMode
+{
+ Simple,
+ Fuzzy,
+ FuzzyParts
+}
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index bf816cbc8..53c570e54 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -652,7 +652,7 @@ public static class Util
}
///
- /// Print formatted GameObject Information to ImGui
+ /// Print formatted GameObject Information to ImGui.
///
/// Game Object to Display.
/// Display Tag.
diff --git a/build.sh b/build.sh
index eca8e308c..a4c346c80 100644
--- a/build.sh
+++ b/build.sh
@@ -58,5 +58,5 @@ fi
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
-"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
-"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false /p:EnableWindowsTargeting=true -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- /p:EnableWindowsTargeting=true "$@"
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index f2abb4a11..a6f5d730c 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit f2abb4a11319b26b77cd29b69a52b34e1d56069d
+Subproject commit a6f5d730c2fbf4a0521b0512fe41f4622d515218
diff --git a/sign.ps1 b/sign.ps1
new file mode 100644
index 000000000..73fb6cc67
--- /dev/null
+++ b/sign.ps1
@@ -0,0 +1,71 @@
+# Get the certificate and password from environment variables
+$certificateBase64 = $env:CODESIGN_CERT_PFX
+$certificatePassword = $env:CODESIGN_CERT_PASSWORD
+
+# Write the certificate to a file
+$certificatePath = Join-Path -Path $env:TEMP -ChildPath 'certificate.pfx'
+$certificateBytes = [Convert]::FromBase64String($certificateBase64)
+[System.IO.File]::WriteAllBytes($certificatePath, $certificateBytes)
+
+# Define the function to find the path to signtool.exe
+function Get-SignToolPath {
+ # Array of common installation directories for Windows SDK
+ $sdkInstallationDirs = @(
+ "$env:ProgramFiles (x86)\Windows Kits\10\bin\x64",
+ "$env:ProgramFiles\Windows Kits\10\bin\x64",
+ "$env:ProgramFiles (x86)\Windows Kits\10\App Certification Kit"
+ )
+
+ foreach ($dir in $sdkInstallationDirs) {
+ $path = Join-Path -Path $dir -ChildPath 'signtool.exe'
+ #Write-Host $path
+ if (Test-Path -Path $path) {
+ return $path
+ }
+ }
+
+ throw "Could not find signtool.exe. Make sure the Windows SDK is installed."
+}
+
+# Find the path to signtool.exe
+$signtoolPath = Get-SignToolPath
+
+# Define the function to code-sign a file
+function Sign-File {
+ param (
+ [Parameter(Mandatory=$true)]
+ [String]$FilePath
+ )
+
+ # Check if the file is already code-signed
+ $signature = Get-AuthenticodeSignature -FilePath $FilePath -ErrorAction SilentlyContinue
+ if ($signature.status -ne "NotSigned") {
+ Write-Host "File '$FilePath' is already code-signed. Skipping."
+ return
+ }
+
+ # Code-sign the file using signtool
+ Write-Host "Code-signing file '$FilePath'..."
+ & $signtoolPath sign /tr http://timestamp.digicert.com /td sha256 /v /fd sha256 /f $certificatePath /p $certificatePassword $FilePath
+}
+
+# Define the function to recursively code-sign files in a directory
+function Sign-FilesRecursively {
+ param (
+ [Parameter(Mandatory=$true)]
+ [String]$DirectoryPath
+ )
+
+ Write-Host $DirectoryPath
+
+ # Get all exe and dll files recursively
+ dir $DirectoryPath -recurse | where {$_.extension -in ".exe",".dll"} | ForEach-Object {
+ Sign-File -FilePath $_.FullName
+ }
+}
+
+# Usage: Provide the directory path as an argument to sign files recursively
+Sign-FilesRecursively -DirectoryPath $args[0]
+
+# Remove the temporary certificate file
+Remove-Item -Path $certificatePath
diff --git a/targets/Dalamud.Plugin.Bootstrap.targets b/targets/Dalamud.Plugin.Bootstrap.targets
index 16f78517c..c30a5acba 100644
--- a/targets/Dalamud.Plugin.Bootstrap.targets
+++ b/targets/Dalamud.Plugin.Bootstrap.targets
@@ -1,5 +1,5 @@
-
+
$(appdata)\XIVLauncher\addon\Hooks\dev\
diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets
index 9234f5064..4a5f9e97e 100644
--- a/targets/Dalamud.Plugin.targets
+++ b/targets/Dalamud.Plugin.targets
@@ -1,5 +1,5 @@
-
+
net7.0-windows
x64
@@ -10,42 +10,19 @@
false
true
true
+ $(AssemblySearchPaths);$(DalamudLibPath)
-
-
- $(DalamudLibPath)FFXIVClientStructs.dll
- false
-
-
- $(DalamudLibPath)Newtonsoft.Json.dll
- false
-
-
- $(DalamudLibPath)Dalamud.dll
- false
-
-
- $(DalamudLibPath)Dalamud.Interface.dll
- false
-
-
- $(DalamudLibPath)ImGui.NET.dll
- false
-
-
- $(DalamudLibPath)ImGuiScene.dll
- false
-
-
- $(DalamudLibPath)Lumina.dll
- false
-
-
- $(DalamudLibPath)Lumina.Excel.dll
- false
-
+
+
+
+
+
+
+
+
+