Merge remote-tracking branch 'origin/master' into v9-rollup

This commit is contained in:
github-actions[bot] 2023-07-27 12:07:53 +00:00
commit 5da34cbc81
91 changed files with 3849 additions and 1089 deletions

View file

@ -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

49
.github/workflows/rollup.yml vendored Normal file
View file

@ -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

View file

@ -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));

View file

@ -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.
/// </summary>
public bool IsDocking { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool EnablePluginUISoundEffects { get; set; }
/// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled.
@ -287,6 +293,11 @@ internal sealed class DalamudConfiguration : IServiceType
/// </summary>
public bool ProfilesEnabled { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not the user has seen the profiles tutorial.
/// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled.
/// </summary>

View file

@ -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<IDataManager>]
#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
}
}
/// <summary>
/// Gets the current game client language.
/// </summary>
/// <inheritdoc/>
public ClientLanguage Language { get; private set; }
/// <summary>
/// Gets the OpCodes sent by the server to the client.
/// </summary>
/// <inheritdoc/>
public ReadOnlyDictionary<string, ushort> ServerOpCodes { get; private set; }
/// <summary>
/// Gets the OpCodes sent by the client to the server.
/// </summary>
/// <inheritdoc/>
[UsedImplicitly]
public ReadOnlyDictionary<string, ushort> ClientOpCodes { get; private set; }
/// <summary>
/// Gets a <see cref="Lumina"/> object which gives access to any excel/game data.
/// </summary>
/// <inheritdoc/>
public GameData GameData { get; private set; }
/// <summary>
/// Gets an <see cref="ExcelModule"/> object which gives access to any of the game's sheet data.
/// </summary>
/// <inheritdoc/>
public ExcelModule Excel => this.GameData.Excel;
/// <summary>
/// Gets a value indicating whether Game Data is ready to be read.
/// </summary>
/// <inheritdoc/>
public bool IsDataReady { get; private set; }
/// <summary>
/// Gets a value indicating whether the game data files have been modified by another third-party tool.
/// </summary>
/// <inheritdoc/>
public bool HasModifiedGameDataFiles { get; private set; }
#region Lumina Wrappers
/// <summary>
/// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type.
/// </summary>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
/// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow
=> this.Excel.GetSheet<T>();
/// <summary>
/// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type with a specified language.
/// </summary>
/// <param name="language">Language of the sheet to get.</param>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
/// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow
=> this.Excel.GetSheet<T>(language.ToLumina());
/// <summary>
/// Get a <see cref="FileResource"/> with the given path.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
/// <inheritdoc/>
public FileResource? GetFile(string path)
=> this.GetFile<FileResource>(path);
/// <summary>
/// Get a <see cref="FileResource"/> with the given path, of the given type.
/// </summary>
/// <typeparam name="T">The type of resource.</typeparam>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
/// <inheritdoc/>
public T? GetFile<T>(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<T>(filePath.Category, filePath) : default;
}
/// <summary>
/// Check if the file with the given path exists within the game's index files.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>True if the file exists.</returns>
/// <inheritdoc/>
public bool FileExists(string path)
=> this.GameData.FileExists(path);
@ -217,25 +185,15 @@ public sealed class DataManager : IDisposable, IServiceType
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// 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);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// <inheritdoc/>
public TexFile? GetIcon(uint iconId, bool highResolution)
=> this.GetIcon(this.Language, iconId, highResolution);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// <inheritdoc/>
public TexFile? GetIcon(bool isHq, uint iconId)
{
var type = isHq ? "hq/" : string.Empty;
@ -248,17 +206,11 @@ public sealed class DataManager : IDisposable, IServiceType
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// 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);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// <inheritdoc/>
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
{
var type = iconLanguage switch
@ -279,17 +231,11 @@ public sealed class DataManager : IDisposable, IServiceType
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// 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);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
/// <inheritdoc/>
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <summary>
/// Get the passed <see cref="TexFile"/> as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTexture(TexFile? tex)
=> tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
/// <summary>
/// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
@ -339,59 +273,33 @@ public sealed class DataManager : IDisposable, IServiceType
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// 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));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return the high resolution version.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
=> this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
/// <inheritdoc/>
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion
/// <summary>
/// Dispose this DataManager.
/// </summary>
/// <inheritdoc/>
void IDisposable.Dispose()
{
this.luminaCancellationTokenSource.Cancel();

View file

@ -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;
}
}

View file

@ -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<IBuddyList>]
#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}");
}
/// <summary>
/// Gets the amount of battle buddies the local player has.
/// </summary>
/// <inheritdoc/>
public int Length
{
get
@ -56,16 +58,16 @@ public sealed partial class BuddyList : IServiceType
/// <summary>
/// Gets a value indicating whether the local player's companion is present.
/// </summary>
[Obsolete("Use CompanionBuddy != null", false)]
public bool CompanionBuddyPresent => this.CompanionBuddy != null;
/// <summary>
/// Gets a value indicating whether the local player's pet is present.
/// </summary>
[Obsolete("Use PetBuddy != null", false)]
public bool PetBuddyPresent => this.PetBuddy != null;
/// <summary>
/// Gets the active companion buddy.
/// </summary>
/// <inheritdoc/>
public BuddyMember? CompanionBuddy
{
get
@ -75,9 +77,7 @@ public sealed partial class BuddyList : IServiceType
}
}
/// <summary>
/// Gets the active pet buddy.
/// </summary>
/// <inheritdoc/>
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;
/// <summary>
/// Gets a battle buddy at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="BuddyMember"/> at the specified spawn index.</returns>
/// <inheritdoc/>
public BuddyMember? this[int index]
{
get
@ -110,29 +106,19 @@ public sealed partial class BuddyList : IServiceType
}
}
/// <summary>
/// Gets the address of the companion buddy.
/// </summary>
/// <returns>The memory address of the companion buddy.</returns>
/// <inheritdoc/>
public unsafe IntPtr GetCompanionBuddyMemberAddress()
{
return (IntPtr)(&this.BuddyListStruct->Companion);
}
/// <summary>
/// Gets the address of the pet buddy.
/// </summary>
/// <returns>The memory address of the pet buddy.</returns>
/// <inheritdoc/>
public unsafe IntPtr GetPetBuddyMemberAddress()
{
return (IntPtr)(&this.BuddyListStruct->Pet);
}
/// <summary>
/// Gets the address of the battle buddy at the specified index of the buddy list.
/// </summary>
/// <param name="index">The index of the battle buddy.</param>
/// <returns>The memory address of the battle buddy.</returns>
/// <inheritdoc/>
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));
}
/// <summary>
/// Create a reference to a buddy.
/// </summary>
/// <param name="address">The address of the buddy in memory.</param>
/// <returns><see cref="BuddyMember"/> object containing the requested data.</returns>
/// <inheritdoc/>
public BuddyMember? CreateBuddyMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
@ -165,7 +147,7 @@ public sealed partial class BuddyList : IServiceType
/// <summary>
/// This collection represents the buddies present in your squadron or trust party.
/// </summary>
public sealed partial class BuddyList : IReadOnlyCollection<BuddyMember>
public sealed partial class BuddyList
{
/// <inheritdoc/>
int IReadOnlyCollection<BuddyMember>.Count => this.Length;

View file

@ -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<IFateTable>]
#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}");
}
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
/// <inheritdoc/>
public IntPtr Address => this.address.FateTablePtr;
/// <summary>
/// Gets the amount of currently active Fates.
/// </summary>
/// <inheritdoc/>
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;
/// <summary>
/// Get an actor at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="Fate"/> at the specified spawn index.</returns>
/// <inheritdoc/>
public Fate? this[int index]
{
get
@ -83,11 +79,7 @@ public sealed partial class FateTable : IServiceType
}
}
/// <summary>
/// Gets the address of the Fate at the specified index of the fate table.
/// </summary>
/// <param name="index">The index of the Fate.</param>
/// <returns>The memory address of the Fate.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Create a reference to a FFXIV actor.
/// </summary>
/// <param name="offset">The offset of the actor in memory.</param>
/// <returns><see cref="Fate"/> object containing requested data.</returns>
/// <inheritdoc/>
public Fate? CreateFateReference(IntPtr offset)
{
var clientState = Service<ClientState>.Get();
@ -122,7 +110,7 @@ public sealed partial class FateTable : IServiceType
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
public sealed partial class FateTable : IReadOnlyCollection<Fate>
public sealed partial class FateTable
{
/// <inheritdoc/>
int IReadOnlyCollection<Fate>.Count => this.Length;

View file

@ -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<IGamepadState>]
#pragma warning restore SA1015
public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
{
private readonly Hook<ControllerPoll> gamepadPoll;
private readonly Hook<ControllerPoll>? gamepadPoll;
private bool isDisposed;
@ -42,44 +47,60 @@ public unsafe class GamepadState : IDisposable, IServiceType
/// </summary>
public IntPtr GamepadInputAddress { get; private set; }
/// <inheritdoc/>
public Vector2 LeftStick =>
new(this.leftStickX, this.leftStickY);
/// <inheritdoc/>
public Vector2 RightStick =>
new(this.rightStickX, this.rightStickY);
/// <summary>
/// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.X", false)]
public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.X", false)]
public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.Y", false)]
public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.LeftStick.Y", false)]
public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.X", false)]
public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.X", false)]
public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.Y", false)]
public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
[Obsolete("Use IGamepadState.RightStick.Y", false)]
public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
/// <summary>
@ -120,43 +141,16 @@ public unsafe class GamepadState : IDisposable, IServiceType
/// </summary>
internal bool NavEnableGamepad { get; set; }
/// <summary>
/// Gets whether <paramref name="button"/> has been pressed.
///
/// Only true on first frame of the press.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if pressed, 0 otherwise.</returns>
/// <inheritdoc/>
public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets whether <paramref name="button"/> is being pressed.
///
/// True in intervals if button is held down.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if still pressed during interval, 0 otherwise or in between intervals.</returns>
/// <inheritdoc/>
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets whether <paramref name="button"/> has been released.
///
/// Only true the frame after release.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if released, 0 otherwise.</returns>
/// <inheritdoc/>
public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets the raw state of <paramref name="button"/>.
///
/// Is set the entire time a button is pressed down.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 the whole time button is pressed, 0 otherwise.</returns>
/// <inheritdoc/>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
/// <summary>
@ -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;

View file

@ -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<IJobGauges>]
#pragma warning restore SA1015
public class JobGauges : IServiceType, IJobGauges
{
private Dictionary<Type, JobGaugeBase> cache = new();
@ -27,16 +31,10 @@ public class JobGauges : IServiceType
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
}
/// <summary>
/// Gets the address of the JobGauge data.
/// </summary>
/// <inheritdoc/>
public IntPtr Address { get; }
/// <summary>
/// Get the JobGauge for a given job.
/// </summary>
/// <typeparam name="T">A JobGauge struct from ClientState.Structs.JobGauge.</typeparam>
/// <returns>A JobGauge.</returns>
/// <inheritdoc/>
public T Get<T>() where T : JobGaugeBase
{
// This is cached to mitigate the effects of using activator for instantiation.

View file

@ -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<IObjectTable>]
#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}");
}
/// <summary>
/// Gets the address of the object table.
/// </summary>
/// <inheritdoc/>
public IntPtr Address => this.address.ObjectTable;
/// <summary>
/// Gets the length of the object table.
/// </summary>
/// <inheritdoc/>
public int Length => ObjectTableLength;
/// <summary>
/// Get an object at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>An <see cref="GameObject"/> at the specified spawn index.</returns>
/// <inheritdoc/>
public GameObject? this[int index]
{
get
@ -55,11 +51,7 @@ public sealed partial class ObjectTable : IServiceType
}
}
/// <summary>
/// Search for a game object by their Object ID.
/// </summary>
/// <param name="objectId">Object ID to find.</param>
/// <returns>A game object or null.</returns>
/// <inheritdoc/>
public GameObject? SearchById(ulong objectId)
{
if (objectId is GameObject.InvalidGameObjectId or 0)
@ -77,11 +69,7 @@ public sealed partial class ObjectTable : IServiceType
return null;
}
/// <summary>
/// Gets the address of the game object at the specified index of the object table.
/// </summary>
/// <param name="index">The index of the object.</param>
/// <returns>The memory address of the object.</returns>
/// <inheritdoc/>
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));
}
/// <summary>
/// Create a reference to an FFXIV game object.
/// </summary>
/// <param name="address">The address of the object in memory.</param>
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
/// <inheritdoc/>
public unsafe GameObject? CreateObjectReference(IntPtr address)
{
var clientState = Service<ClientState>.GetNullable();
@ -125,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType
/// <summary>
/// This collection represents the currently spawned FFXIV game objects.
/// </summary>
public sealed partial class ObjectTable : IReadOnlyCollection<GameObject>
public sealed partial class ObjectTable
{
/// <inheritdoc/>
int IReadOnlyCollection<GameObject>.Count => this.Length;

View file

@ -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<ITargetManager>]
#pragma warning restore SA1015
public sealed unsafe class TargetManager : IServiceType, ITargetManager
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
@ -28,50 +32,38 @@ public sealed unsafe class TargetManager : IServiceType
this.address = this.clientState.AddressResolver;
}
/// <summary>
/// Gets the address of the target manager.
/// </summary>
/// <inheritdoc/>
public IntPtr Address => this.address.TargetManager;
/// <summary>
/// Gets or sets the current target.
/// </summary>
/// <inheritdoc/>
public GameObject? Target
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value);
}
/// <summary>
/// Gets or sets the mouseover target.
/// </summary>
/// <inheritdoc/>
public GameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value);
}
/// <summary>
/// Gets or sets the focus target.
/// </summary>
/// <inheritdoc/>
public GameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value);
}
/// <summary>
/// Gets or sets the previous target.
/// </summary>
/// <inheritdoc/>
public GameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value);
}
/// <summary>
/// Gets or sets the soft target.
/// </summary>
/// <inheritdoc/>
public GameObject? SoftTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget);
@ -84,84 +76,99 @@ public sealed unsafe class TargetManager : IServiceType
/// Sets the current target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use Target Property", false)]
public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use MouseOverTarget Property", false)]
public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use FocusTarget Property", false)]
public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use PreviousTarget Property", false)]
public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actor">Actor to target.</param>
[Obsolete("Use SoftTarget Property", false)]
public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use Target Property", false)]
public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use MouseOverTarget Property", false)]
public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use FocusTarget Property", false)]
public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use PreviousTarget Property", false)]
public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
[Obsolete("Use SoftTarget Property", false)]
public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Clears the current target.
/// </summary>
[Obsolete("Use Target Property", false)]
public void ClearTarget() => this.SetTarget(IntPtr.Zero);
/// <summary>
/// Clears the mouseover target.
/// </summary>
[Obsolete("Use MouseOverTarget Property", false)]
public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
/// <summary>
/// Clears the focus target.
/// </summary>
[Obsolete("Use FocusTarget Property", false)]
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
/// <summary>
/// Clears the previous target.
/// </summary>
[Obsolete("Use PreviousTarget Property", false)]
public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
/// <summary>
/// Clears the soft target.
/// </summary>
[Obsolete("Use SoftTarget Property", false)]
public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
}

View file

@ -144,6 +144,11 @@ public unsafe partial class GameObject
/// </summary>
public bool IsDead => this.Struct->IsDead();
/// <summary>
/// Gets a value indicating whether the object is targetable.
/// </summary>
public bool IsTargetable => this.Struct->GetIsTargetable();
/// <summary>
/// Gets the position of this <see cref="GameObject" />.
/// </summary>

View file

@ -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<IPartyList>]
#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}");
}
/// <summary>
/// Gets the amount of party members the local player has.
/// </summary>
/// <inheritdoc/>
public int Length => this.GroupManagerStruct->MemberCount;
/// <summary>
/// Gets the index of the party leader.
/// </summary>
/// <inheritdoc/>
public uint PartyLeaderIndex => this.GroupManagerStruct->PartyLeaderIndex;
/// <summary>
/// Gets a value indicating whether this group is an alliance.
/// </summary>
/// <inheritdoc/>
public bool IsAlliance => this.GroupManagerStruct->AllianceFlags > 0;
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
/// <inheritdoc/>
public IntPtr GroupManagerAddress => this.address.GroupManager;
/// <summary>
/// Gets the address of the party list within the group manager.
/// </summary>
/// <inheritdoc/>
public IntPtr GroupListAddress => (IntPtr)GroupManagerStruct->PartyMembers;
/// <summary>
/// Gets the address of the alliance member list within the group manager.
/// </summary>
/// <inheritdoc/>
public IntPtr AllianceListAddress => (IntPtr)this.GroupManagerStruct->AllianceMembers;
/// <summary>
/// Gets the ID of the party.
/// </summary>
/// <inheritdoc/>
public long PartyId => this.GroupManagerStruct->PartyId;
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
/// <summary>
/// Get a party member at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="PartyMember"/> at the specified spawn index.</returns>
/// <inheritdoc/>
public PartyMember? this[int index]
{
get
@ -98,11 +84,7 @@ public sealed unsafe partial class PartyList : IServiceType
}
}
/// <summary>
/// Gets the address of the party member at the specified index of the party list.
/// </summary>
/// <param name="index">The index of the party member.</param>
/// <returns>The memory address of the party member.</returns>
/// <inheritdoc/>
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);
}
/// <summary>
/// Create a reference to an FFXIV party member.
/// </summary>
/// <param name="address">The address of the party member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
/// <inheritdoc/>
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);
}
/// <summary>
/// Gets the address of the alliance member at the specified index of the alliance list.
/// </summary>
/// <param name="index">The index of the alliance member.</param>
/// <returns>The memory address of the alliance member.</returns>
/// <inheritdoc/>
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);
}
/// <summary>
/// Create a reference to an FFXIV alliance member.
/// </summary>
/// <param name="address">The address of the alliance member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
/// <inheritdoc/>
public PartyMember? CreateAllianceMemberReference(IntPtr address)
{
if (this.clientState.LocalContentId == 0)
@ -160,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType
/// <summary>
/// This collection represents the party members present in your party or alliance.
/// </summary>
public sealed partial class PartyList : IReadOnlyCollection<PartyMember>
public sealed partial class PartyList
{
/// <inheritdoc/>
int IReadOnlyCollection<PartyMember>.Count => this.Length;

View file

@ -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<ICommandManager>]
#pragma warning restore SA1015
public sealed class CommandManager : IServiceType, IDisposable, ICommandManager
{
private readonly Dictionary<string, CommandInfo> commandMap = new();
private readonly ConcurrentDictionary<string, CommandInfo> commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe = new(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
@ -46,16 +51,10 @@ public sealed class CommandManager : IServiceType, IDisposable
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
}
/// <summary>
/// Gets a read-only list of all registered commands.
/// </summary>
/// <inheritdoc/>
public ReadOnlyDictionary<string, CommandInfo> Commands => new(this.commandMap);
/// <summary>
/// Process a command in full.
/// </summary>
/// <param name="content">The full command string.</param>
/// <returns>True if the command was found and dispatched.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Dispatch the handling of a command.
/// </summary>
/// <param name="command">The command to dispatch.</param>
/// <param name="argument">The provided arguments.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing this command.</param>
/// <inheritdoc/>
public void DispatchCommand(string command, string argument, CommandInfo info)
{
try
@ -116,37 +110,25 @@ public sealed class CommandManager : IServiceType, IDisposable
}
}
/// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary>
/// <param name="command">The command to register.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing the command.</param>
/// <returns>If adding was successful.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Remove a command from the command handlers.
/// </summary>
/// <param name="command">The command to remove.</param>
/// <returns>If the removal was successful.</returns>
/// <inheritdoc/>
public bool RemoveHandler(string command)
{
return this.commandMap.Remove(command);
return this.commandMap.Remove(command, out _);
}
/// <inheritdoc/>

View file

@ -0,0 +1,7 @@
using System;
namespace Dalamud.Game.Config;
public abstract record ConfigChangeEvent(Enum Option);
public record ConfigChangeEvent<T>(T ConfigOption) : ConfigChangeEvent(ConfigOption) where T : Enum;

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Config;
/// <summary>
/// Types of options used by the game config.
/// </summary>
public enum ConfigType
{
/// <summary>
/// Unused config index.
/// </summary>
Unused = 0,
/// <summary>
/// A label entry with no value.
/// </summary>
Category = 1,
/// <summary>
/// A config entry with an unsigned integer value.
/// </summary>
UInt = 2,
/// <summary>
/// A config entry with a float value.
/// </summary>
Float = 3,
/// <summary>
/// A config entry with a string value.
/// </summary>
String = 4,
}

View file

@ -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<IGameConfig>]
#pragma warning restore SA1015
public sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{
private readonly GameConfigAddressResolver address = new();
private Hook<ConfigChangeDelegate>? 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<ConfigChangeDelegate>.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged);
this.configChangeHook?.Enable();
});
}
/// <summary>
/// Gets the collection of config options that persist between characters.
/// </summary>
private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry);
/// <inheritdoc/>
public event EventHandler<ConfigChangeEvent> Changed;
/// <inheritdoc/>
public GameConfigSection System { get; private set; }
/// <summary>
/// Gets the collection of config options that are character specific.
/// </summary>
/// <inheritdoc/>
public GameConfigSection UiConfig { get; private set; }
/// <summary>
/// Gets the collection of config options that are control mode specific. (Mouse and Keyboard / Gamepad).
/// </summary>
/// <inheritdoc/>
public GameConfigSection UiControl { get; private set; }
/// <summary>
/// Attempts to get a boolean config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a uint config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out uint value) => this.System.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a float config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out float value) => this.System.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a string config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out string value) => this.System.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a boolean config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a uint config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out uint value) => this.UiConfig.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a float config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out float value) => this.UiConfig.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a string config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out string value) => this.UiConfig.TryGet(option.GetName(), out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out UIntConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out FloatConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out StringConfigProperties properties) => this.UiConfig.TryGetProperties(option.GetName(), out properties);
/// <summary>
/// Attempts to get a boolean config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out bool value) => this.UiControl.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a uint config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out uint value) => this.UiControl.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a float config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out float value) => this.UiControl.TryGet(option.GetName(), out value);
/// <summary>
/// Attempts to get a string config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out string value) => this.System.TryGet(option.GetName(), out value);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out string value) => this.UiControl.TryGet(option.GetName(), out value);
/// <summary>
/// Set a boolean config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out UIntConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out FloatConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(UiControlOption option, out StringConfigProperties properties) => this.UiControl.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public void Set(SystemConfigOption option, bool value) => this.System.Set(option.GetName(), value);
/// <summary>
/// Set a unsigned integer config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(SystemConfigOption option, uint value) => this.System.Set(option.GetName(), value);
/// <summary>
/// Set a float config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(SystemConfigOption option, float value) => this.System.Set(option.GetName(), value);
/// <summary>
/// Set a string config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(SystemConfigOption option, string value) => this.System.Set(option.GetName(), value);
/// <summary>
/// Set a boolean config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiConfigOption option, bool value) => this.UiConfig.Set(option.GetName(), value);
/// <summary>
/// Set a unsigned integer config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiConfigOption option, uint value) => this.UiConfig.Set(option.GetName(), value);
/// <summary>
/// Set a float config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiConfigOption option, float value) => this.UiConfig.Set(option.GetName(), value);
/// <summary>
/// Set a string config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiConfigOption option, string value) => this.UiConfig.Set(option.GetName(), value);
/// <summary>
/// Set a boolean config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiControlOption option, bool value) => this.UiControl.Set(option.GetName(), value);
/// <summary>
/// Set a uint config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiControlOption option, uint value) => this.UiControl.Set(option.GetName(), value);
/// <summary>
/// Set a float config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiControlOption option, float value) => this.UiControl.Set(option.GetName(), value);
/// <summary>
/// Set a string config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
/// <inheritdoc/>
public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value);
/// <inheritdoc/>
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<SystemConfigOption>(configEntry);
}
else if (configBase == this.UiConfig.GetConfigBase())
{
eventArgs = this.UiConfig.InvokeChange<UiConfigOption>(configEntry);
}
else if (configBase == this.UiControl.GetConfigBase())
{
eventArgs = this.UiControl.InvokeChange<UiControlOption>(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;
}
}

View file

@ -0,0 +1,18 @@
namespace Dalamud.Game.Config;
/// <summary>
/// Game config system address resolver.
/// </summary>
public sealed class GameConfigAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the method called when any config option is changed.
/// </summary>
public nint ConfigChangeAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner scanner)
{
this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E");
}
}

View file

@ -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;
/// </summary>
internal static class GameConfigEnumExtensions
{
private static readonly ConcurrentDictionary<SystemConfigOption, string> SystemNameCache = new();
private static readonly ConcurrentDictionary<UiConfigOption, string> UIConfigNameCache = new();
private static readonly ConcurrentDictionary<UiControlOption, string> UIControlNameCache = new();
/// <summary>
/// Gets the name of a SystemConfigOption from it's attribute.
/// </summary>
@ -14,7 +20,10 @@ internal static class GameConfigEnumExtensions
/// <returns>Name of the option.</returns>
public static string GetName(this SystemConfigOption systemConfigOption)
{
return systemConfigOption.GetAttribute<GameConfigOptionAttribute>()?.Name ?? $"{systemConfigOption}";
if (SystemNameCache.TryGetValue(systemConfigOption, out var name)) return name;
name = systemConfigOption.GetAttribute<GameConfigOptionAttribute>()?.Name ?? $"{systemConfigOption}";
SystemNameCache.TryAdd(systemConfigOption, name);
return name;
}
/// <summary>
@ -24,7 +33,10 @@ internal static class GameConfigEnumExtensions
/// <returns>Name of the option.</returns>
public static string GetName(this UiConfigOption uiConfigOption)
{
return uiConfigOption.GetAttribute<GameConfigOptionAttribute>()?.Name ?? $"{uiConfigOption}";
if (UIConfigNameCache.TryGetValue(uiConfigOption, out var name)) return name;
name = uiConfigOption.GetAttribute<GameConfigOptionAttribute>()?.Name ?? $"{uiConfigOption}";
UIConfigNameCache.TryAdd(uiConfigOption, name);
return name;
}
/// <summary>
@ -34,6 +46,9 @@ internal static class GameConfigEnumExtensions
/// <returns>Name of the option.</returns>
public static string GetName(this UiControlOption uiControlOption)
{
return uiControlOption.GetAttribute<GameConfigOptionAttribute>()?.Name ?? $"{uiControlOption}";
if (UIControlNameCache.TryGetValue(uiControlOption, out var name)) return name;
name = uiControlOption.GetAttribute<GameConfigOptionAttribute>()?.Name ?? $"{uiControlOption}";
UIControlNameCache.TryAdd(uiControlOption, name);
return name;
}
}

View file

@ -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<string, uint> indexMap = new();
private readonly Dictionary<uint, string> nameMap = new();
private readonly ConcurrentDictionary<string, uint> indexMap = new();
private readonly ConcurrentDictionary<uint, object> enumMap = new();
/// <summary>
/// Event which is fired when a game config option is changed within the section.
/// </summary>
public event EventHandler<ConfigChangeEvent> Changed;
/// <summary>
/// Initializes a new instance of the <see cref="GameConfigSection"/> class.
@ -59,7 +65,10 @@ public class GameConfigSection
/// </summary>
public string SectionName { get; }
private GetConfigBaseDelegate GetConfigBase { get; }
/// <summary>
/// Gets the pointer to the config section container.
/// </summary>
internal GetConfigBaseDelegate GetConfigBase { get; }
/// <summary>
/// Attempts to get a boolean config option.
@ -380,6 +389,128 @@ public class GameConfigSection
});
}
/// <summary>
/// Attempts to get the properties of a UInt option from the config section.
/// </summary>
/// <param name="name">Name of the option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
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;
}
/// <summary>
/// Attempts to get the properties of a Float option from the config section.
/// </summary>
/// <param name="name">Name of the option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
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;
}
/// <summary>
/// Attempts to get the properties of a String option from the config section.
/// </summary>
/// <param name="name">Name of the option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
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;
}
/// <summary>
/// Invokes a change event within the config section.
/// </summary>
/// <param name="entry">The config entry that was changed.</param>
/// <typeparam name="TEnum">SystemConfigOption, UiConfigOption, or UiControlOption.</typeparam>
/// <returns>The ConfigChangeEvent record.</returns>
internal unsafe ConfigChangeEvent? InvokeChange<TEnum>(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>((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;
}

View file

@ -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);

View file

@ -1,6 +1,4 @@
using FFXIVClientStructs.FFXIV.Common.Configuration;
namespace Dalamud.Game.Config;
namespace Dalamud.Game.Config;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo

View file

@ -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
/// </summary>
[GameConfigOption("PvPFrontlinesGCFree", ConfigType.UInt)]
PvPFrontlinesGCFree,
/// <summary>
/// System option with the internal name PetMirageTypeFairy.
/// This option is a UInt.
/// </summary>
[GameConfigOption("PetMirageTypeFairy", ConfigType.UInt)]
PetMirageTypeFairy,
/// <summary>
/// System option with the internal name ExHotbarChangeHotbar1IsFashion.
/// This option is a UInt.
/// </summary>
[GameConfigOption("ExHotbarChangeHotbar1IsFashion", ConfigType.UInt)]
ExHotbarChangeHotbar1IsFashion,
/// <summary>
/// System option with the internal name HotbarCrossUseExDirectionAutoSwitch.
/// This option is a UInt.
/// </summary>
[GameConfigOption("HotbarCrossUseExDirectionAutoSwitch", ConfigType.UInt)]
HotbarCrossUseExDirectionAutoSwitch,
/// <summary>
/// System option with the internal name NamePlateDispJobIcon.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispJobIcon", ConfigType.UInt)]
NamePlateDispJobIcon,
/// <summary>
/// System option with the internal name NamePlateDispJobIconType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispJobIconType", ConfigType.UInt)]
NamePlateDispJobIconType,
/// <summary>
/// System option with the internal name NamePlateSetRoleColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateSetRoleColor", ConfigType.UInt)]
NamePlateSetRoleColor,
/// <summary>
/// System option with the internal name NamePlateColorTank.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateColorTank", ConfigType.UInt)]
NamePlateColorTank,
/// <summary>
/// System option with the internal name NamePlateEdgeTank.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateEdgeTank", ConfigType.UInt)]
NamePlateEdgeTank,
/// <summary>
/// System option with the internal name NamePlateColorHealer.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateColorHealer", ConfigType.UInt)]
NamePlateColorHealer,
/// <summary>
/// System option with the internal name NamePlateEdgeHealer.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateEdgeHealer", ConfigType.UInt)]
NamePlateEdgeHealer,
/// <summary>
/// System option with the internal name NamePlateColorDps.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateColorDps", ConfigType.UInt)]
NamePlateColorDps,
/// <summary>
/// System option with the internal name NamePlateEdgeDps.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateEdgeDps", ConfigType.UInt)]
NamePlateEdgeDps,
/// <summary>
/// System option with the internal name NamePlateColorOtherClass.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateColorOtherClass", ConfigType.UInt)]
NamePlateColorOtherClass,
/// <summary>
/// System option with the internal name NamePlateEdgeOtherClass.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateEdgeOtherClass", ConfigType.UInt)]
NamePlateEdgeOtherClass,
/// <summary>
/// System option with the internal name NamePlateDispWorldTravel.
/// This option is a UInt.
/// </summary>
[GameConfigOption("NamePlateDispWorldTravel", ConfigType.UInt)]
NamePlateDispWorldTravel,
/// <summary>
/// System option with the internal name LogNameIconType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogNameIconType", ConfigType.UInt)]
LogNameIconType,
/// <summary>
/// System option with the internal name LogDispClassJobName.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogDispClassJobName", ConfigType.UInt)]
LogDispClassJobName,
/// <summary>
/// System option with the internal name LogSetRoleColor.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogSetRoleColor", ConfigType.UInt)]
LogSetRoleColor,
/// <summary>
/// System option with the internal name LogColorRoleTank.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogColorRoleTank", ConfigType.UInt)]
LogColorRoleTank,
/// <summary>
/// System option with the internal name LogColorRoleHealer.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogColorRoleHealer", ConfigType.UInt)]
LogColorRoleHealer,
/// <summary>
/// System option with the internal name LogColorRoleDPS.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogColorRoleDPS", ConfigType.UInt)]
LogColorRoleDPS,
/// <summary>
/// System option with the internal name LogColorOtherClass.
/// This option is a UInt.
/// </summary>
[GameConfigOption("LogColorOtherClass", ConfigType.UInt)]
LogColorOtherClass,
/// <summary>
/// System option with the internal name ItemInventryStoreEnd.
/// This option is a UInt.
/// </summary>
[GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)]
ItemInventryStoreEnd,
}

View file

@ -1,6 +1,4 @@
using FFXIVClientStructs.FFXIV.Common.Configuration;
namespace Dalamud.Game.Config;
namespace Dalamud.Game.Config;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo

View file

@ -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<IDutyState>]
#pragma warning restore SA1015
public unsafe class DutyState : IDisposable, IServiceType, IDutyState
{
private readonly DutyStateAddressResolver address;
private readonly Hook<SetupContentDirectNetworkMessageDelegate> contentDirectorNetworkMessageHook;
@ -44,42 +48,24 @@ public unsafe class DutyState : IDisposable, IServiceType
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3);
/// <summary>
/// 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.
/// </summary>
/// <inheritdoc/>
public event EventHandler<ushort> DutyStarted;
/// <summary>
/// Event that gets fired when everyone in the party dies and the screen fades to black.
/// </summary>
/// <inheritdoc/>
public event EventHandler<ushort> DutyWiped;
/// <summary>
/// Event that gets fired when the "Duty Recommence" message displays,
/// and on the remove the the ring at duty's spawn.
/// </summary>
/// <inheritdoc/>
public event EventHandler<ushort> DutyRecommenced;
/// <summary>
/// Event that gets fired when the duty is completed successfully.
/// </summary>
/// <inheritdoc/>
public event EventHandler<ushort> DutyCompleted;
/// <summary>
/// Gets a value indicating whether the current duty has been started.
/// </summary>
/// <inheritdoc/>
public bool IsDutyStarted { get; private set; }
/// <summary>
/// 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.
/// </summary>
private bool CompletedThisTerritory { get; set; }
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
/// <inheritdoc/>
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.
}
}

View file

@ -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<IGameLifecycle>]
#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
{
}
/// <summary>
/// Gets a token that is cancelled when Dalamud is unloading.
/// </summary>
/// <inheritdoc/>
public CancellationToken DalamudUnloadingToken => this.dalamudUnloadCts.Token;
/// <summary>
/// Gets a token that is cancelled when the game is shutting down.
/// </summary>
/// <inheritdoc/>
public CancellationToken GameShuttingDownToken => this.gameShutdownCts.Token;
/// <summary>
/// Gets a token that is cancelled when a character is logging out.
/// </summary>
/// <inheritdoc/>
public CancellationToken LogoutToken => this.logoutCts.Token;
/// <summary>

View file

@ -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<IDtrBar>]
#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();
}
/// <summary>
/// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
/// <inheritdoc/>
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);

View file

@ -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<IGameGui>]
#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);
/// <summary>
/// Event which is fired when the game UI hiding is toggled.
/// </summary>
/// <inheritdoc/>
public event EventHandler<bool> UiHideToggled;
/// <summary>
/// Event that is fired when the currently hovered item changes.
/// </summary>
/// <inheritdoc/>
public event EventHandler<ulong> HoveredItemChanged;
/// <summary>
/// Event that is fired when the currently hovered action changes.
/// </summary>
/// <inheritdoc/>
public event EventHandler<HoveredAction> HoveredActionChanged;
/// <summary>
/// Gets a value indicating whether the game UI is hidden.
/// </summary>
/// <inheritdoc/>
public bool GameUiHidden { get; private set; }
/// <summary>
/// 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.
/// </summary>
/// <inheritdoc/>
public ulong HoveredItem { get; set; }
/// <summary>
/// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
/// </summary>
/// <inheritdoc/>
public HoveredAction HoveredAction { get; } = new HoveredAction();
/// <summary>
/// Opens the in-game map with a flag on the location of the parameter.
/// </summary>
/// <param name="mapLink">Link to the map to be opened.</param>
/// <returns>True if there were no errors and it could open the map.</returns>
/// <inheritdoc/>
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);
}
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera and screenPos is in the viewport.</returns>
/// <inheritdoc/>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
=> this.WorldToScreen(worldPos, out screenPos, out var inView) && inView;
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <param name="inView">True if screenPos corresponds to a position inside the camera viewport.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Converts screen coordinates to in-world coordinates via raycasting.
/// </summary>
/// <param name="screenPos">Screen coordinates.</param>
/// <param name="worldPos">Converted coordinates.</param>
/// <param name="rayDistance">How far to search for a collision.</param>
/// <returns>True if successful. On false, worldPos's contents are undefined.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Gets a pointer to the game's UI module.
/// </summary>
/// <returns>IntPtr pointing to UI module.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Gets the pointer to the Addon with the given name and index.
/// </summary>
/// <param name="name">Name of addon to find.</param>
/// <param name="index">Index of addon to find (1-indexed).</param>
/// <returns>IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the addon.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addonName">The addon name.</param>
/// <returns>A pointer to the agent interface.</returns>
/// <inheritdoc/>
public IntPtr FindAgentInterface(string addonName)
{
var addon = this.GetAddonByName(addonName);
return this.FindAgentInterface(addon);
}
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addon">The addon address.</param>
/// <returns>A pointer to the agent interface.</returns>
/// <inheritdoc/>
public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon);
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addonPtr">The addon address.</param>
/// <returns>A pointer to the agent interface.</returns>
/// <inheritdoc/>
public IntPtr FindAgentInterface(IntPtr addonPtr)
{
if (addonPtr == IntPtr.Zero)

View file

@ -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<ILibcFunction>]
#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);
/// <summary>
/// Create a new string from the given bytes.
/// </summary>
/// <param name="content">The bytes to convert.</param>
/// <returns>An owned std string object.</returns>
/// <inheritdoc/>
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);
}
/// <summary>
/// Create a new string form the given bytes.
/// </summary>
/// <param name="content">The bytes to convert.</param>
/// <param name="encoding">A non-default encoding.</param>
/// <returns>An owned std string object.</returns>
public OwnedStdString NewString(string content, Encoding encoding = null)
/// <inheritdoc/>
public OwnedStdString NewString(string content, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;

View file

@ -15,12 +15,17 @@ using Serilog;
namespace Dalamud.Game;
// TODO(v9): There are static functions here that we can't keep due to interfaces
/// <summary>
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class SigScanner : IDisposable, IServiceType
#pragma warning disable SA1015
[ResolveVia<ISigScanner>]
#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();
}
/// <summary>
/// Gets a value indicating whether or not the search on this module is performed on a copy.
/// </summary>
/// <inheritdoc/>
public bool IsCopy { get; }
/// <summary>
/// Gets a value indicating whether or not the ProcessModule is 32-bit.
/// </summary>
/// <inheritdoc/>
public bool Is32BitProcess { get; }
/// <summary>
/// Gets the base address of the search area. When copied, this will be the address of the copy.
/// </summary>
/// <inheritdoc/>
public IntPtr SearchBase => this.IsCopy ? this.moduleCopyPtr : this.Module.BaseAddress;
/// <summary>
/// Gets the base address of the .text section search area.
/// </summary>
/// <inheritdoc/>
public IntPtr TextSectionBase => new(this.SearchBase.ToInt64() + this.TextSectionOffset);
/// <summary>
/// Gets the offset of the .text section from the base of the module.
/// </summary>
/// <inheritdoc/>
public long TextSectionOffset { get; private set; }
/// <summary>
/// Gets the size of the text section.
/// </summary>
/// <inheritdoc/>
public int TextSectionSize { get; private set; }
/// <summary>
/// Gets the base address of the .data section search area.
/// </summary>
/// <inheritdoc/>
public IntPtr DataSectionBase => new(this.SearchBase.ToInt64() + this.DataSectionOffset);
/// <summary>
/// Gets the offset of the .data section from the base of the module.
/// </summary>
/// <inheritdoc/>
public long DataSectionOffset { get; private set; }
/// <summary>
/// Gets the size of the .data section.
/// </summary>
/// <inheritdoc/>
public int DataSectionSize { get; private set; }
/// <summary>
/// Gets the base address of the .rdata section search area.
/// </summary>
/// <inheritdoc/>
public IntPtr RDataSectionBase => new(this.SearchBase.ToInt64() + this.RDataSectionOffset);
/// <summary>
/// Gets the offset of the .rdata section from the base of the module.
/// </summary>
/// <inheritdoc/>
public long RDataSectionOffset { get; private set; }
/// <summary>
/// Gets the size of the .rdata section.
/// </summary>
/// <inheritdoc/>
public int RDataSectionSize { get; private set; }
/// <summary>
/// Gets the ProcessModule on which the search is performed.
/// </summary>
/// <inheritdoc/>
public ProcessModule Module { get; }
private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize;
@ -229,11 +208,7 @@ public class SigScanner : IDisposable, IServiceType
}
}
/// <summary>
/// Scan for a byte signature in the .data section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <returns>The real offset of the found signature.</returns>
/// <inheritdoc/>
public IntPtr ScanData(string signature)
{
var scanRet = Scan(this.DataSectionBase, this.DataSectionSize, signature);
@ -244,12 +219,7 @@ public class SigScanner : IDisposable, IServiceType
return scanRet;
}
/// <summary>
/// Try scanning for a byte signature in the .data section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <param name="result">The real offset of the signature, if found.</param>
/// <returns>true if the signature was found.</returns>
/// <inheritdoc/>
public bool TryScanData(string signature, out IntPtr result)
{
try
@ -264,11 +234,7 @@ public class SigScanner : IDisposable, IServiceType
}
}
/// <summary>
/// Scan for a byte signature in the whole module search area.
/// </summary>
/// <param name="signature">The signature.</param>
/// <returns>The real offset of the found signature.</returns>
/// <inheritdoc/>
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;
}
/// <summary>
/// Try scanning for a byte signature in the whole module search area.
/// </summary>
/// <param name="signature">The signature.</param>
/// <param name="result">The real offset of the signature, if found.</param>
/// <returns>true if the signature was found.</returns>
/// <inheritdoc/>
public bool TryScanModule(string signature, out IntPtr result)
{
try
@ -299,23 +260,14 @@ public class SigScanner : IDisposable, IServiceType
}
}
/// <summary>
/// Resolve a RVA address.
/// </summary>
/// <param name="nextInstAddr">The address of the next instruction.</param>
/// <param name="relOffset">The relative offset.</param>
/// <returns>The calculated offset.</returns>
/// <inheritdoc/>
public IntPtr ResolveRelativeAddress(IntPtr nextInstAddr, int relOffset)
{
if (this.Is32BitProcess) throw new NotSupportedException("32 bit is not supported.");
return nextInstAddr + relOffset;
}
/// <summary>
/// Scan for a byte signature in the .text section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <returns>The real offset of the found signature.</returns>
/// <inheritdoc/>
public IntPtr ScanText(string signature)
{
if (this.textCache != null)
@ -347,12 +299,7 @@ public class SigScanner : IDisposable, IServiceType
return scanRet;
}
/// <summary>
/// Try scanning for a byte signature in the .text section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <param name="result">The real offset of the signature, if found.</param>
/// <returns>true if the signature was found.</returns>
/// <inheritdoc/>
public bool TryScanText(string signature, out IntPtr result)
{
try

View file

@ -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
/// <summary> Implements interop enums and function calls to interact with external drag and drop. </summary>
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

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
[PluginInterface]
[ServiceManager.EarlyLoadedService]
[ResolveVia<IDragDropManager>]
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<InterfaceManager.InterfaceManagerWithScene>.GetAsync()
.ContinueWith(t =>
{
this.windowHandlePtr = t.Result.Manager.WindowHandlePtr;
this.Enable();
});
}
/// <summary> Gets a value indicating whether external drag and drop is available at all. </summary>
public bool ServiceAvailable { get; private set; }
/// <summary> Gets a value indicating whether a valid external drag and drop is currently active and hovering over any FFXIV-related viewport. </summary>
public bool IsDragging { get; private set; }
/// <summary> Gets a value indicating whether there are any files or directories currently being dragged, or stored from the last drop. </summary>
public bool HasPaths
=> this.Files.Count + this.Directories.Count > 0;
/// <summary> Gets the list of file paths currently being dragged from an external application over any FFXIV-related viewport, or stored from the last drop. </summary>
public IReadOnlyList<string> Files { get; private set; } = Array.Empty<string>();
/// <summary> 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. </summary>
public IReadOnlySet<string> Extensions { get; private set; } = new HashSet<string>();
/// <summary> Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. </summary>
public IReadOnlyList<string> Directories { get; private set; } = Array.Empty<string>();
/// <summary> Enable external drag and drop. </summary>
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}");
}
}
/// <summary> Disable external drag and drop. </summary>
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;
}
/// <inheritdoc cref="Disable"/>
public void Dispose()
=> this.Disable();
/// <inheritdoc cref="IDragDropManager.CreateImGuiSource(string, Func{IDragDropManager, bool}, Func{IDragDropManager, bool})"/>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> 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();
}
/// <inheritdoc cref="IDragDropManager.CreateImGuiTarget"/>
public bool CreateImGuiTarget(string label, out IReadOnlyList<string> files, out IReadOnlyList<string> directories)
{
files = Array.Empty<string>();
directories = Array.Empty<string>();
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;
}
}

View file

@ -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;
/// <summary> Implements the IDropTarget interface to interact with external drag and dropping. </summary>
internal partial class DragDropManager : DragDropManager.IDropTarget
{
private int lastUpdateFrame = -1;
private DragDropInterop.ModifierKeys lastKeyState = DragDropInterop.ModifierKeys.MK_NONE;
/// <summary> Create the drag and drop formats we accept. </summary>
private FORMATETC formatEtc =
new()
{
cfFormat = (short)DragDropInterop.ClipboardFormat.CF_HDROP,
ptd = nint.Zero,
dwAspect = DVASPECT.DVASPECT_CONTENT,
lindex = -1,
tymed = TYMED.TYMED_HGLOBAL,
};
/// <summary>
/// Invoked whenever a drag and drop process drags files into any FFXIV-related viewport.
/// </summary>
/// <param name="pDataObj"> The drag and drop data. </param>
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
/// <param name="pt"> The global cursor position. </param>
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
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);
}
/// <summary> Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport. </summary>
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
/// <param name="pt"> The global cursor position. </param>
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
/// <remarks> Can be invoked more often than once a XIV frame, so we are keeping track of frames to skip unnecessary updates. </remarks>
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);
}
}
/// <summary> Invoked whenever a drag and drop process that hovered over any FFXIV-related viewport leaves all FFXIV-related viewports. </summary>
public void DragLeave()
{
this.IsDragging = false;
this.Files = Array.Empty<string>();
this.Directories = Array.Empty<string>();
this.Extensions = new HashSet<string>();
MouseDrop(this.lastKeyState);
Log.Debug("[DragDrop] Leaving external Drag and Drop.");
}
/// <summary> Invoked whenever a drag process ends by dropping over any FFXIV-related viewport. </summary>
/// <param name="pDataObj"> The drag and drop data. </param>
/// <param name="grfKeyState"> The mouse button used to drag as well as key modifiers. </param>
/// <param name="pt"> The global cursor position. </param>
/// <param name="pdwEffect"> Effects that can be used with this drag and drop process. </param>
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<string>(), Array.Empty<string>());
}
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<string>();
var directoryArray = directoryCount > 0 ? files.TakeLast(directoryCount).Reverse().ToArray() : Array.Empty<string>();
return (fileArray, directoryArray);
}
catch (Exception ex)
{
Log.Error($"Error obtaining data from drag & drop:\n{ex}");
}
return (Array.Empty<string>(), Array.Empty<string>());
}
}

View file

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
namespace Dalamud.Interface.DragDrop;
/// <summary>
/// A service to handle external drag and drop from WinAPI.
/// </summary>
public interface IDragDropManager
{
/// <summary> Gets a value indicating whether Drag and Drop functionality is available at all. </summary>
public bool ServiceAvailable { get; }
/// <summary> Gets a value indicating whether anything is being dragged from an external application and over any of the games viewports. </summary>
public bool IsDragging { get; }
/// <summary> Gets the list of files currently being dragged from an external application over any of the games viewports. </summary>
public IReadOnlyList<string> Files { get; }
/// <summary> Gets the set of file types by extension currently being dragged from an external application over any of the games viewports. </summary>
public IReadOnlySet<string> Extensions { get; }
/// <summary> Gets the list of directories currently being dragged from an external application over any of the games viewports. </summary>
public IReadOnlyList<string> Directories { get; }
/// <summary> Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. </summary>
/// <param name="label"> The label used for the drag & drop payload. </param>
/// <param name="validityCheck">A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.</param>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck)
=> this.CreateImGuiSource(label, validityCheck, _ => false);
/// <summary> Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. </summary>
/// <param name="label"> The label used for the drag & drop payload. </param>
/// <param name="validityCheck">A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged.</param>
/// <param name="tooltipBuilder">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.</param>
public void CreateImGuiSource(string label, Func<IDragDropManager, bool> validityCheck, Func<IDragDropManager, bool> tooltipBuilder);
/// <summary> Create an ImGui drag & drop target on the last ImGui object. </summary>
/// <param name="label">The label used for the drag & drop payload.</param>
/// <param name="files">On success, contains the list of file paths dropped onto the target.</param>
/// <param name="directories">On success, contains the list of directory paths dropped onto the target.</param>
/// <returns>True if items were dropped onto the target this frame, false otherwise.</returns>
public bool CreateImGuiTarget(string label, out IReadOnlyList<string> files, out IReadOnlyList<string> directories);
}

View file

@ -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;

View file

@ -154,5 +154,10 @@ internal enum DataKind
/// <summary>
/// Data Share.
/// </summary>
DataShare,
Data_Share,
/// <summary>
/// Network Monitor.
/// </summary>
Network_Monitor,
}

View file

@ -48,6 +48,7 @@ internal class DataWindow : Window
new DtrBarWidget(),
new UIColorWidget(),
new DataShareWidget(),
new NetworkMonitorWidget(),
};
private readonly Dictionary<DataKind, string> dataKindNames = new();

View file

@ -9,7 +9,7 @@ namespace Dalamud.Interface.Internal.Windows.Data;
internal class DataShareWidget : IDataWindowWidget
{
/// <inheritdoc/>
public DataKind DataKind { get; init; } = DataKind.DataShare;
public DataKind DataKind { get; init; } = DataKind.Data_Share;
/// <inheritdoc/>
public bool Ready { get; set; }

View file

@ -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"));
}

View file

@ -27,27 +27,6 @@ internal class GamepadWidget : IDataWindowWidget
{
var gamepadState = Service<GamepadState>.Get();
static void DrawHelper(string text, uint mask, Func<GamepadButtons, float> 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<GamepadButtons, float> 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)} ");
}
}

View file

@ -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;
/// <summary>
/// Widget to display the current packets.
/// </summary>
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<byte> Data = Array.Empty<byte>();
public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr)
: this(opCode, direction, sourceActorId, targetActorId)
=> this.Data = MemoryHelper.Read<byte>(dataPtr, widget.GetSizeFromOpCode(opCode), false);
}
private readonly ConcurrentQueue<NetworkPacketData> packets = new();
private readonly Dictionary<ushort, (string Name, int Size)> opCodeDict = new();
private bool trackNetwork;
private int trackedPackets;
private Regex? trackedOpCodes;
private string filterString = string.Empty;
private Regex? untrackedOpCodes;
private string negativeFilterString = string.Empty;
/// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary>
~NetworkMonitorWidget()
{
if (this.trackNetwork)
{
this.trackNetwork = false;
var network = Service<GameNetwork>.GetNullable();
if (network != null)
{
network.NetworkMessage -= this.OnNetworkMessage;
}
}
}
/// <inheritdoc/>
public DataKind DataKind { get; init; } = DataKind.Network_Monitor;
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
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<DataManager>.Get();
foreach (var (name, code) in dataManager.ClientOpCodes.Concat(dataManager.ServerOpCodes))
this.opCodeDict.TryAdd(code, (name, this.GetSizeFromName(name)));
}
/// <inheritdoc/>
public void Draw()
{
var network = Service<GameNetwork>.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;
/// <remarks> Add known packet-name -> packet struct size associations here to copy the byte data for such packets. </remarks>>
private int GetSizeFromName(string name)
=> name switch
{
_ => 0,
};
/// <remarks> The filter should find opCodes by number (decimal and hex) and name, if existing. </remarks>
private string OpCodeToString(ushort opCode)
=> this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{opCode}\0{opCode:X}\0{pair.Name}" : $"{opCode}\0{opCode:X}";
}

View file

@ -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
{

View file

@ -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
/// <param name="isThirdParty">If the plugin was third party sourced.</param>
/// <param name="iconTexture">Cached image textures, or an empty array.</param>
/// <returns>True if an entry exists, may be null if currently downloading.</returns>
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
/// <param name="isThirdParty">If the plugin was third party sourced.</param>
/// <param name="imageTextures">Cached image textures, or an empty array.</param>
/// <returns>True if the image array exists, may be empty if currently downloading.</returns>
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<TextureWrap?> DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty, ulong requestedFrame)
private async Task<TextureWrap?> 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<string?>? GetPluginImageUrls(PluginManifest manifest, bool isThirdParty, bool isTesting)
private List<string?>? GetPluginImageUrls(IPluginManifest manifest, bool isThirdParty, bool isTesting)
{
if (isThirdParty)
{
@ -698,14 +696,7 @@ internal class PluginImageCache : IDisposable, IServiceType
var output = new List<string>();
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;

View file

@ -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<PluginHistory>(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)

View file

@ -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;

View file

@ -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<PluginChangelogEntry>();
}
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<ProfileManager>.Get();
var config = Service<DalamudConfiguration>.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<ProfileManager>.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

View file

@ -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
/// </summary>
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<DalamudConfiguration>.Get();
if (!config.ProfilesHasSeenTutorial)
{
ImGui.OpenPopup(modalTitle);
config.ProfilesHasSeenTutorial = true;
config.QueueSave();
}
}
private void DrawOverview(uint tutorialId)
{
var didAny = false;
var profman = Service<ProfileManager>.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);
}

View file

@ -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++;

View file

@ -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<bool>(
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<bool>(
Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"),

View file

@ -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);

View file

@ -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
/// </summary>
public bool RespectCloseHotkey { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether this window should not generate sound effects when opening and closing.
/// </summary>
public bool DisableWindowSounds { get; set; } = false;
/// <summary>
/// Gets or sets a value representing the sound effect id to be played when the window is opened.
/// </summary>
public uint OnOpenSfxId { get; set; } = 23u;
/// <summary>
/// Gets or sets a value representing the sound effect id to be played when the window is closed.
/// </summary>
public uint OnCloseSfxId { get; set; } = 24u;
/// <summary>
/// Gets or sets the position of this window.
/// </summary>
@ -207,10 +223,12 @@ public abstract class Window
/// <summary>
/// Draw the window via ImGui.
/// </summary>
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<KeyState>.Get()[VirtualKey.ESCAPE];
var isAllowed = Service<DalamudConfiguration>.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<KeyState>.Get()[VirtualKey.ESCAPE];
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
{
this.IsOpen = false;
wasEscPressedLastFrame = true;
}
else if (!escapeDown && wasEscPressedLastFrame)
{
wasEscPressedLastFrame = false;
}
}
ImGui.End();

View file

@ -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<DalamudConfiguration>.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);

View file

@ -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
/// <summary>
/// Gets a list of installed plugins along with their current state.
/// </summary>
public IEnumerable<InstalledPluginState> InstalledPlugins => Service<PluginManager>.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.Manifest.EffectiveVersion));
public IEnumerable<InstalledPluginState> InstalledPlugins => Service<PluginManager>.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.EffectiveVersion));
/// <summary>
/// Opens the <see cref="PluginInstallerWindow"/> with the plugin name set as search target.

View file

@ -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
/// </summary>
/// <param name="manifest">The manifest to test.</param>
/// <returns>Whether or not a testing version is available.</returns>
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
/// </summary>
/// <param name="manifest">Manifest to check.</param>
/// <returns>A value indicating whether testing should be used.</returns>
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
/// </summary>
/// <param name="manifest">Manifest to check.</param>
/// <returns>A value indicating whether testing should be used.</returns>
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
/// <param name="repoManifest">The plugin definition.</param>
/// <param name="useTesting">If the testing version should be used.</param>
/// <param name="reason">The reason this plugin was loaded.</param>
/// <param name="inheritedWorkingPluginId">WorkingPluginId this plugin should inherit.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task<LocalPlugin> InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason)
public async Task<LocalPlugin> 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)
{

View file

@ -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
{

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Internal.Types.Manifest;
namespace Dalamud.Plugin.Internal.Types;
/// <summary>

View file

@ -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;

View file

@ -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;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
@ -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<PluginManager>.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");
}
/// <summary>
@ -168,9 +185,9 @@ internal class LocalPlugin : IDisposable
public FileInfo DllFile { get; }
/// <summary>
/// Gets the plugin manifest, if one exists.
/// Gets the plugin manifest.
/// </summary>
public LocalPluginManifest Manifest { get; private set; }
public ILocalPluginManifest Manifest => this.manifest;
/// <summary>
/// Gets or sets the current state of the plugin.
@ -186,12 +203,12 @@ internal class LocalPlugin : IDisposable
/// <summary>
/// Gets the plugin name from the manifest.
/// </summary>
public string Name => this.Manifest.Name;
public string Name => this.manifest.Name;
/// <summary>
/// Gets the plugin internal name from the manifest.
/// </summary>
public string InternalName => this.Manifest.InternalName;
public string InternalName => this.manifest.InternalName;
/// <summary>
/// Gets an optional reason, if the plugin is banned.
@ -213,23 +230,23 @@ internal class LocalPlugin : IDisposable
/// INCLUDES the default profile.
/// </summary>
public bool IsWantedByAnyProfile =>
Service<ProfileManager>.Get().GetWantStateAsync(this.Manifest.InternalName, false, false).GetAwaiter().GetResult();
Service<ProfileManager>.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult();
/// <summary>
/// Gets a value indicating whether this plugin's API level is out of date.
/// </summary>
public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
public bool IsOutdated => this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
/// <summary>
/// Gets a value indicating whether the plugin is for testing use only.
/// </summary>
public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
public bool IsTesting => this.manifest.IsTestingExclusive || this.manifest.Testing;
/// <summary>
/// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not.
/// </summary>
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;
/// <summary>
@ -237,7 +254,7 @@ internal class LocalPlugin : IDisposable
/// </summary>
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;
/// <summary>
/// Gets a value indicating whether this plugin has been banned.
@ -249,12 +266,23 @@ internal class LocalPlugin : IDisposable
/// </summary>
public bool IsDev => this is LocalDevPlugin;
/// <summary>
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
/// repo.
/// </summary>
public bool IsThirdParty => this.manifest.IsThirdParty;
/// <summary>
/// Gets a value indicating whether this plugin should be allowed to load.
/// </summary>
public bool ApplicableForLoad => !this.IsBanned && !this.IsDecommissioned && !this.IsOrphaned && !this.IsOutdated
&& !(!this.IsDev && this.State == PluginState.UnloadError) && this.CheckPolicy();
/// <summary>
/// Gets the effective version of this plugin.
/// </summary>
public Version EffectiveVersion => this.manifest.EffectiveVersion;
/// <summary>
/// Gets the service scope for this plugin.
/// </summary>
@ -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<InterfaceManager>.GetAsync();
await Service<GameFontManager>.GetAsync();
if (this.Manifest.LoadRequiredState == 0)
if (this.manifest.LoadRequiredState == 0)
_ = await Service<InterfaceManager.InterfaceManagerWithScene>.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<DalamudConfiguration>.Get();
var framework = Service<Framework>.GetNullable();
var ioc = await Service<ServiceContainer>.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
/// <param name="status">Schedule or cancel the deletion.</param>
public void ScheduleDeletion(bool status = true)
{
this.Manifest.ScheduledForDeletion = status;
this.SaveManifest();
this.manifest.ScheduledForDeletion = status;
this.SaveManifest("scheduling for deletion");
}
/// <summary>
@ -626,14 +656,14 @@ internal class LocalPlugin : IDisposable
/// </summary>
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<PluginManager>.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);
}

View file

@ -0,0 +1,26 @@
using System;
namespace Dalamud.Plugin.Internal.Types.Manifest;
/// <summary>
/// Public interface for the local plugin manifest.
/// </summary>
public interface ILocalPluginManifest : IPluginManifest
{
/// <summary>
/// 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.
/// </summary>
public string InstalledFromUrl { get; }
/// <summary>
/// Gets a value indicating whether the plugin should be deleted during the next cleanup.
/// </summary>
public bool ScheduledForDeletion { get; }
/// <summary>
/// Gets an ID uniquely identifying this specific installation of a plugin.
/// </summary>
public Guid WorkingPluginId { get; }
}

View file

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
namespace Dalamud.Plugin.Internal.Types.Manifest;
/// <summary>
/// Public interface for the base plugin manifest.
/// </summary>
public interface IPluginManifest
{
/// <summary>
/// Gets the internal name of the plugin, which should match the assembly name of the plugin.
/// </summary>
public string InternalName { get; }
/// <summary>
/// Gets the public name of the plugin.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a punchline of the plugins functions.
/// </summary>
public string? Punchline { get; }
/// <summary>
/// Gets the author/s of the plugin.
/// </summary>
public string Author { get; }
/// <summary>
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// </summary>
public bool CanUnloadAsync { get; }
/// <summary>
/// Gets the assembly version of the plugin.
/// </summary>
public Version AssemblyVersion { get; }
/// <summary>
/// Gets the assembly version of the plugin's testing variant.
/// </summary>
public Version? TestingAssemblyVersion { get; }
/// <summary>
/// Gets the DIP17 channel name.
/// </summary>
public string? Dip17Channel { get; }
/// <summary>
/// Gets the last time this plugin was updated.
/// </summary>
public long LastUpdate { get; }
/// <summary>
/// Gets a changelog, null if none exists.
/// </summary>
public string? Changelog { get; }
/// <summary>
/// Gets a list of tags that apply to this plugin.
/// </summary>
public List<string>? Tags { get; }
/// <summary>
/// Gets the API level of this plugin. For the current API level, please see <see cref="PluginManager.DalamudApiLevel"/>
/// for the currently used API level.
/// </summary>
public int DalamudApiLevel { get; }
/// <summary>
/// Gets the number of downloads this plugin has.
/// </summary>
public long DownloadCount { get; }
/// <summary>
/// Gets a value indicating whether the plugin supports profiles.
/// </summary>
public bool SupportsProfiles { get; }
/// <summary>
/// Gets an URL to the website or source code of the plugin.
/// </summary>
public string? RepoUrl { get; }
/// <summary>
/// Gets a description of the plugins functions.
/// </summary>
public string? Description { get; }
/// <summary>
/// Gets a message that is shown to users when sending feedback.
/// </summary>
public string? FeedbackMessage { get; }
/// <summary>
/// Gets a value indicating whether the plugin is only available for testing.
/// </summary>
public bool IsTestingExclusive { get; }
/// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>
public List<string>? ImageUrls { get; }
/// <summary>
/// Gets an URL for the plugin's icon.
/// </summary>
public string? IconUrl { get; }
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
internal record LocalPluginManifest : PluginManifest
internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest
{
/// <summary>
/// Flag indicating that a plugin was installed from the official repo.
@ -37,18 +38,15 @@ internal record LocalPluginManifest : PluginManifest
/// </summary>
public bool Testing { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the plugin should be deleted during the next cleanup.
/// </summary>
/// <inheritdoc/>
public bool ScheduledForDeletion { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <inheritdoc/>
public string InstalledFromUrl { get; set; } = string.Empty;
/// <inheritdoc/>
public Guid WorkingPluginId { get; set; } = Guid.Empty;
/// <summary>
/// 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
/// </summary>
public Version EffectiveVersion => this.Testing && this.TestingAssemblyVersion != null ? this.TestingAssemblyVersion : this.AssemblyVersion;
/// <summary>
/// Gets a value indicating whether this plugin is eligible for testing.
/// </summary>
public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion;
/// <summary>
/// Save a plugin manifest to file.
/// </summary>
/// <param name="manifestFile">Path to save at.</param>
public void Save(FileInfo manifestFile) => Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
/// <param name="reason">The reason the manifest was saved.</param>
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;
}
}
/// <summary>
/// Loads a plugin manifest from file.

View file

@ -1,7 +1,7 @@
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types;
namespace Dalamud.Plugin.Internal.Types.Manifest;
/// <summary>
/// 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
/// </summary>
[JsonIgnore]
public PluginRepository SourceRepo { get; set; } = null!;
/// <summary>
/// Gets a value indicating whether this plugin is eligible for testing.
/// </summary>
public bool IsAvailableForTesting => this.TestingAssemblyVersion != null && this.TestingAssemblyVersion > this.AssemblyVersion;
}

View file

@ -1,5 +1,7 @@
using System.IO;
using Dalamud.Plugin.Internal.Types.Manifest;
namespace Dalamud.Plugin.Internal.Types;
/// <summary>

View file

@ -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;
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL.
/// </summary>
internal record PluginManifest
internal record PluginManifest : IPluginManifest
{
/// <summary>
/// Gets the author/s of the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string? Author { get; init; }
/// <summary>
/// Gets or sets the public name of the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string Name { get; set; } = null!;
/// <summary>
/// Gets a punchline of the plugins functions.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string? Punchline { get; init; }
/// <summary>
/// Gets a description of the plugins functions.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string? Description { get; init; }
/// <summary>
/// Gets a changelog.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string? Changelog { get; init; }
/// <summary>
/// Gets a list of tags defined on the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public List<string>? Tags { get; init; }
@ -60,33 +49,23 @@ internal record PluginManifest
[JsonProperty]
public bool IsHide { get; init; }
/// <summary>
/// Gets the internal name of the plugin, which should match the assembly name of the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string InternalName { get; init; } = null!;
public string InternalName { get; set; } = null!;
/// <summary>
/// Gets the current assembly version of the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public Version AssemblyVersion { get; init; } = null!;
public Version AssemblyVersion { get; set; } = null!;
/// <summary>
/// Gets the current testing assembly version of the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public Version? TestingAssemblyVersion { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin is only available for testing.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public bool IsTestingExclusive { get; init; }
/// <summary>
/// Gets an URL to the website or source code of the plugin.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public string? RepoUrl { get; init; }
@ -97,24 +76,17 @@ internal record PluginManifest
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
/// <summary>
/// Gets the API level of this plugin. For the current API level, please see <see cref="PluginManager.DalamudApiLevel"/>
/// for the currently used API level.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
/// <summary>
/// Gets the number of downloads this plugin has.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public long DownloadCount { get; init; }
/// <summary>
/// Gets the last time this plugin was updated.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public long LastUpdate { get; init; }
public long LastUpdate { get; set; }
/// <summary>
/// Gets the download link used to install the plugin.
@ -156,26 +128,18 @@ internal record PluginManifest
[JsonProperty]
public int LoadPriority { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public bool CanUnloadAsync { get; init; }
public bool CanUnloadAsync { get; set; }
/// <summary>
/// Gets a value indicating whether the plugin supports profiles.
/// </summary>
/// <inheritdoc/>
[JsonProperty]
public bool SupportsProfiles { get; init; } = true;
/// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>
/// <inheritdoc/>
public List<string>? ImageUrls { get; init; }
/// <summary>
/// Gets an URL for the plugin's icon.
/// </summary>
/// <inheritdoc/>
public string? IconUrl { get; init; }
/// <summary>
@ -183,21 +147,10 @@ internal record PluginManifest
/// </summary>
public bool AcceptsFeedback { get; init; } = true;
/// <summary>
/// Gets a message that is shown to users when sending feedback.
/// </summary>
/// <inheritdoc/>
public string? FeedbackMessage { get; init; }
/// <summary>
/// Gets a value indicating whether this plugin is DIP17.
/// To be removed.
/// </summary>
[JsonProperty("_isDip17Plugin")]
public bool IsDip17Plugin { get; init; } = false;
/// <summary>
/// Gets the DIP17 channel name.
/// </summary>
/// <inheritdoc/>
[JsonProperty("_Dip17Channel")]
public string? Dip17Channel { get; init; }
}

View file

@ -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;

View file

@ -0,0 +1,61 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Dalamud.Game.ClientState.Buddy;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This collection represents the buddies present in your squadron or trust party.
/// It does not include the local player.
/// </summary>
public interface IBuddyList : IReadOnlyCollection<BuddyMember>
{
/// <summary>
/// Gets the amount of battle buddies the local player has.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the active companion buddy.
/// </summary>
public BuddyMember? CompanionBuddy { get; }
/// <summary>
/// Gets the active pet buddy.
/// </summary>
public BuddyMember? PetBuddy { get; }
/// <summary>
/// Gets a battle buddy at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="BuddyMember"/> at the specified spawn index.</returns>
public BuddyMember? this[int index] { get; }
/// <summary>
/// Gets the address of the companion buddy.
/// </summary>
/// <returns>The memory address of the companion buddy.</returns>
public nint GetCompanionBuddyMemberAddress();
/// <summary>
/// Gets the address of the pet buddy.
/// </summary>
/// <returns>The memory address of the pet buddy.</returns>
public nint GetPetBuddyMemberAddress();
/// <summary>
/// Gets the address of the battle buddy at the specified index of the buddy list.
/// </summary>
/// <param name="index">The index of the battle buddy.</param>
/// <returns>The memory address of the battle buddy.</returns>
public nint GetBattleBuddyMemberAddress(int index);
/// <summary>
/// Create a reference to a buddy.
/// </summary>
/// <param name="address">The address of the buddy in memory.</param>
/// <returns><see cref="BuddyMember"/> object containing the requested data.</returns>
public BuddyMember? CreateBuddyMemberReference(nint address);
}

View file

@ -0,0 +1,46 @@
using System.Collections.ObjectModel;
using Dalamud.Game.Command;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class manages registered in-game slash commands.
/// </summary>
public interface ICommandManager
{
/// <summary>
/// Gets a read-only list of all registered commands.
/// </summary>
public ReadOnlyDictionary<string, CommandInfo> Commands { get; }
/// <summary>
/// Process a command in full.
/// </summary>
/// <param name="content">The full command string.</param>
/// <returns>True if the command was found and dispatched.</returns>
public bool ProcessCommand(string content);
/// <summary>
/// Dispatch the handling of a command.
/// </summary>
/// <param name="command">The command to dispatch.</param>
/// <param name="argument">The provided arguments.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing this command.</param>
public void DispatchCommand(string command, string argument, CommandInfo info);
/// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary>
/// <param name="command">The command to register.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing the command.</param>
/// <returns>If adding was successful.</returns>
public bool AddHandler(string command, CommandInfo info);
/// <summary>
/// Remove a command from the command handlers.
/// </summary>
/// <param name="command">The command to remove.</param>
/// <returns>If the removal was successful.</returns>
public bool RemoveHandler(string command);
}

View file

@ -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;
/// <summary>
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
/// </summary>
public interface IDataManager
{
/// <summary>
/// Gets the current game client language.
/// </summary>
public ClientLanguage Language { get; }
/// <summary>
/// Gets the OpCodes sent by the server to the client.
/// </summary>
public ReadOnlyDictionary<string, ushort> ServerOpCodes { get; }
/// <summary>
/// Gets the OpCodes sent by the client to the server.
/// </summary>
public ReadOnlyDictionary<string, ushort> ClientOpCodes { get; }
/// <summary>
/// Gets a <see cref="Lumina"/> object which gives access to any excel/game data.
/// </summary>
public GameData GameData { get; }
/// <summary>
/// Gets an <see cref="ExcelModule"/> object which gives access to any of the game's sheet data.
/// </summary>
public ExcelModule Excel { get; }
/// <summary>
/// Gets a value indicating whether Game Data is ready to be read.
/// </summary>
public bool IsDataReady { get; }
/// <summary>
/// Gets a value indicating whether the game data files have been modified by another third-party tool.
/// </summary>
public bool HasModifiedGameDataFiles { get; }
/// <summary>
/// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type.
/// </summary>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow;
/// <summary>
/// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type with a specified language.
/// </summary>
/// <param name="language">Language of the sheet to get.</param>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow;
/// <summary>
/// Get a <see cref="FileResource"/> with the given path.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public FileResource? GetFile(string path);
/// <summary>
/// Get a <see cref="FileResource"/> with the given path, of the given type.
/// </summary>
/// <typeparam name="T">The type of resource.</typeparam>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public T? GetFile<T>(string path) where T : FileResource;
/// <summary>
/// Check if the file with the given path exists within the game's index files.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>True if the file exists.</returns>
public bool FileExists(string path);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(uint iconId, bool highResolution = false);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return high resolution version.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false);
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="highResolution">Return the high resolution version.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false);
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(bool isHq, uint iconId);
/// <summary>
/// Get a <see cref="TexFile"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetHqIcon(uint iconId);
/// <summary>
/// Get the passed <see cref="TexFile"/> as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(TexFile? tex);
/// <summary>
/// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(string path);
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId);
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId);
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId);
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureHqIcon(uint iconId);
}

View file

@ -0,0 +1,22 @@
using System;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text.SeStringHandling;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Class used to interface with the server info bar.
/// </summary>
public interface IDtrBar
{
/// <summary>
/// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
public DtrBarEntry Get(string title, SeString? text = null);
}

View file

@ -0,0 +1,36 @@
using System;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class represents the state of the currently occupied duty.
/// </summary>
public interface IDutyState
{
/// <summary>
/// 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.
/// </summary>
public event EventHandler<ushort> DutyStarted;
/// <summary>
/// Event that gets fired when everyone in the party dies and the screen fades to black.
/// </summary>
public event EventHandler<ushort> DutyWiped;
/// <summary>
/// Event that gets fired when the "Duty Recommence" message displays, and on the removal of the ring at duty's spawn.
/// </summary>
public event EventHandler<ushort> DutyRecommenced;
/// <summary>
/// Event that gets fired when the duty is completed successfully.
/// </summary>
public event EventHandler<ushort> DutyCompleted;
/// <summary>
/// Gets a value indicating whether the current duty has been started.
/// </summary>
public bool IsDutyStarted { get; }
}

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Fates;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
public interface IFateTable : IReadOnlyCollection<Fate>
{
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
public nint Address { get; }
/// <summary>
/// Gets the amount of currently active Fates.
/// </summary>
public int Length { get; }
/// <summary>
/// Get an actor at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="Fate"/> at the specified spawn index.</returns>
public Fate? this[int index] { get; }
/// <summary>
/// Gets the address of the Fate at the specified index of the fate table.
/// </summary>wo
/// <param name="index">The index of the Fate.</param>
/// <returns>The memory address of the Fate.</returns>
public nint GetFateAddress(int index);
/// <summary>
/// Create a reference to a FFXIV actor.
/// </summary>
/// <param name="offset">The offset of the actor in memory.</param>
/// <returns><see cref="Fate"/> object containing requested data.</returns>
public Fate? CreateFateReference(nint offset);
}

View file

@ -0,0 +1,321 @@
using System;
using System.Diagnostics;
using Dalamud.Game.Config;
using FFXIVClientStructs.FFXIV.Common.Configuration;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class represents the game's configuration.
/// </summary>
public interface IGameConfig
{
/// <summary>
/// Event which is fired when a game config option is changed.
/// </summary>
public event EventHandler<ConfigChangeEvent> Changed;
/// <summary>
/// Gets the collection of config options that persist between characters.
/// </summary>
public GameConfigSection System { get; }
/// <summary>
/// Gets the collection of config options that are character specific.
/// </summary>
public GameConfigSection UiConfig { get; }
/// <summary>
/// Gets the collection of config options that are control mode specific. (Mouse and Keyboard / Gamepad).
/// </summary>
public GameConfigSection UiControl { get; }
/// <summary>
/// Attempts to get a boolean config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out bool value);
/// <summary>
/// Attempts to get a uint config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out uint value);
/// <summary>
/// Attempts to get a float config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out float value);
/// <summary>
/// Attempts to get a string config value from the System section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out string value);
/// <summary>
/// Attempts to get the properties of a UInt option from the System section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties);
/// <summary>
/// Attempts to get the properties of a Float option from the System section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties);
/// <summary>
/// Attempts to get the properties of a String option from the System section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Default Value</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties);
/// <summary>
/// Attempts to get a boolean config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out bool value);
/// <summary>
/// Attempts to get a uint config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out uint value);
/// <summary>
/// Attempts to get a float config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out float value);
/// <summary>
/// Attempts to get a string config value from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out string value);
/// <summary>
/// Attempts to get the properties of a UInt option from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out UIntConfigProperties? properties);
/// <summary>
/// Attempts to get the properties of a Float option from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out FloatConfigProperties? properties);
/// <summary>
/// Attempts to get the properties of a String option from the UiConfig section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Default Value</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiConfigOption option, out StringConfigProperties? properties);
/// <summary>
/// Attempts to get a boolean config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out bool value);
/// <summary>
/// Attempts to get a uint config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out uint value);
/// <summary>
/// Attempts to get a float config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out float value);
/// <summary>
/// Attempts to get a string config value from the UiControl section.
/// </summary>
/// <param name="option">Option to get the value of.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out string value);
/// <summary>
/// Attempts to get the properties of a UInt option from the UiControl section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out UIntConfigProperties? properties);
/// <summary>
/// Attempts to get the properties of a Float option from the UiControl section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Minimum, Maximum, and Default values.</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out FloatConfigProperties? properties);
/// <summary>
/// Attempts to get the properties of a String option from the UiControl section.
/// </summary>
/// <param name="option">Option to get the properties of.</param>
/// <param name="properties">Details of the option: Default Value</param>
/// <returns>A value representing the success.</returns>
public bool TryGet(UiControlOption option, out StringConfigProperties? properties);
/// <summary>
/// Set a boolean config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(SystemConfigOption option, bool value);
/// <summary>
/// Set a unsigned integer config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(SystemConfigOption option, uint value);
/// <summary>
/// Set a float config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(SystemConfigOption option, float value);
/// <summary>
/// Set a string config option in the System config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(SystemConfigOption option, string value);
/// <summary>
/// Set a boolean config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiConfigOption option, bool value);
/// <summary>
/// Set a unsigned integer config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiConfigOption option, uint value);
/// <summary>
/// Set a float config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiConfigOption option, float value);
/// <summary>
/// Set a string config option in the UiConfig section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiConfigOption option, string value);
/// <summary>
/// Set a boolean config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiControlOption option, bool value);
/// <summary>
/// Set a uint config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiControlOption option, uint value);
/// <summary>
/// Set a float config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiControlOption option, float value);
/// <summary>
/// Set a string config option in the UiControl config section.
/// Note: Not all config options will be be immediately reflected in the game.
/// </summary>
/// <param name="option">Name of the config option.</param>
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public void Set(UiControlOption option, string value);
}

View file

@ -0,0 +1,112 @@
using System;
using System.Numerics;
using Dalamud.Game.Gui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
namespace Dalamud.Plugin.Services;
/// <summary>
/// A class handling many aspects of the in-game UI.
/// </summary>
public unsafe interface IGameGui
{
/// <summary>
/// Event which is fired when the game UI hiding is toggled.
/// </summary>
public event EventHandler<bool> UiHideToggled;
/// <summary>
/// Event that is fired when the currently hovered item changes.
/// </summary>
public event EventHandler<ulong> HoveredItemChanged;
/// <summary>
/// Event that is fired when the currently hovered action changes.
/// </summary>
public event EventHandler<HoveredAction> HoveredActionChanged;
/// <summary>
/// Gets a value indicating whether the game UI is hidden.
/// </summary>
public bool GameUiHidden { get; }
/// <summary>
/// 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.
/// </summary>
public ulong HoveredItem { get; set; }
/// <summary>
/// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
/// </summary>
public HoveredAction HoveredAction { get; }
/// <summary>
/// Opens the in-game map with a flag on the location of the parameter.
/// </summary>
/// <param name="mapLink">Link to the map to be opened.</param>
/// <returns>True if there were no errors and it could open the map.</returns>
public bool OpenMapWithMapLink(MapLinkPayload mapLink);
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera and screenPos is in the viewport.</returns>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos);
/// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary>
/// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates.</param>
/// <param name="inView">True if screenPos corresponds to a position inside the camera viewport.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera.</returns>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView);
/// <summary>
/// Converts screen coordinates to in-world coordinates via raycasting.
/// </summary>
/// <param name="screenPos">Screen coordinates.</param>
/// <param name="worldPos">Converted coordinates.</param>
/// <param name="rayDistance">How far to search for a collision.</param>
/// <returns>True if successful. On false, worldPos's contents are undefined.</returns>
public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f);
/// <summary>
/// Gets a pointer to the game's UI module.
/// </summary>
/// <returns>IntPtr pointing to UI module.</returns>
public nint GetUIModule();
/// <summary>
/// Gets the pointer to the Addon with the given name and index.
/// </summary>
/// <param name="name">Name of addon to find.</param>
/// <param name="index">Index of addon to find (1-indexed).</param>
/// <returns>nint.Zero if unable to find UI, otherwise nint pointing to the start of the addon.</returns>
public nint GetAddonByName(string name, int index = 1);
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addonName">The addon name.</param>
/// <returns>A pointer to the agent interface.</returns>
public nint FindAgentInterface(string addonName);
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addon">The addon address.</param>
/// <returns>A pointer to the agent interface.</returns>
public nint FindAgentInterface(void* addon);
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addonPtr">The addon address.</param>
/// <returns>A pointer to the agent interface.</returns>
public IntPtr FindAgentInterface(IntPtr addonPtr);
}

View file

@ -0,0 +1,24 @@
using System.Threading;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Class offering cancellation tokens for common gameplay events.
/// </summary>
public interface IGameLifecycle
{
/// <summary>
/// Gets a token that is cancelled when Dalamud is unloading.
/// </summary>
public CancellationToken DalamudUnloadingToken { get; }
/// <summary>
/// Gets a token that is cancelled when the game is shutting down.
/// </summary>
public CancellationToken GameShuttingDownToken { get; }
/// <summary>
/// Gets a token that is cancelled when a character is logging out.
/// </summary>
public CancellationToken LogoutToken { get; }
}

View file

@ -0,0 +1,68 @@
using System.Numerics;
using Dalamud.Game.ClientState.GamePad;
using ImGuiNET;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Exposes the game gamepad state to dalamud.
///
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary>
public interface IGamepadState
{
/// <summary>
/// Gets the pointer to the current instance of the GamepadInput struct.
/// </summary>
public nint GamepadInputAddress { get; }
/// <summary>
/// Gets the left analogue sticks tilt vector.
/// </summary>
public Vector2 LeftStick { get; }
/// <summary>
/// Gets the right analogue sticks tilt vector.
/// </summary>
public Vector2 RightStick { get; }
/// <summary>
/// Gets whether <paramref name="button"/> has been pressed.
///
/// Only true on first frame of the press.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if pressed, 0 otherwise.</returns>
public float Pressed(GamepadButtons button);
/// <summary>
/// Gets whether <paramref name="button"/> is being pressed.
///
/// True in intervals if button is held down.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if still pressed during interval, 0 otherwise or in between intervals.</returns>
public float Repeat(GamepadButtons button);
/// <summary>
/// Gets whether <paramref name="button"/> has been released.
///
/// Only true the frame after release.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if released, 0 otherwise.</returns>
public float Released(GamepadButtons button);
/// <summary>
/// Gets the raw state of <paramref name="button"/>.
///
/// Is set the entire time a button is pressed down.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 the whole time button is pressed, 0 otherwise.</returns>
public float Raw(GamepadButtons button);
}

View file

@ -0,0 +1,21 @@
using Dalamud.Game.ClientState.JobGauge.Types;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class converts in-memory Job gauge data to structs.
/// </summary>
public interface IJobGauges
{
/// <summary>
/// Gets the address of the JobGauge data.
/// </summary>
public nint Address { get; }
/// <summary>
/// Get the JobGauge for a given job.
/// </summary>
/// <typeparam name="T">A JobGauge struct from ClientState.Structs.JobGauge.</typeparam>
/// <returns>A JobGauge.</returns>
public T Get<T>() where T : JobGaugeBase;
}

View file

@ -0,0 +1,26 @@
using System.Text;
using Dalamud.Game.Libc;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class handles creating cstrings utilizing native game methods.
/// </summary>
public interface ILibcFunction
{
/// <summary>
/// Create a new string from the given bytes.
/// </summary>
/// <param name="content">The bytes to convert.</param>
/// <returns>An owned std string object.</returns>
public OwnedStdString NewString(byte[] content);
/// <summary>
/// Create a new string form the given bytes.
/// </summary>
/// <param name="content">The bytes to convert.</param>
/// <param name="encoding">A non-default encoding.</param>
/// <returns>An owned std string object.</returns>
public OwnedStdString NewString(string content, Encoding? encoding = null);
}

View file

@ -0,0 +1,49 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This collection represents the currently spawned FFXIV game objects.
/// </summary>
public interface IObjectTable : IReadOnlyCollection<GameObject>
{
/// <summary>
/// Gets the address of the object table.
/// </summary>
public nint Address { get; }
/// <summary>
/// Gets the length of the object table.
/// </summary>
public int Length { get; }
/// <summary>
/// Get an object at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>An <see cref="GameObject"/> at the specified spawn index.</returns>
public GameObject? this[int index] { get; }
/// <summary>
/// Search for a game object by their Object ID.
/// </summary>
/// <param name="objectId">Object ID to find.</param>
/// <returns>A game object or null.</returns>
public GameObject? SearchById(ulong objectId);
/// <summary>
/// Gets the address of the game object at the specified index of the object table.
/// </summary>
/// <param name="index">The index of the object.</param>
/// <returns>The memory address of the object.</returns>
public nint GetObjectAddress(int index);
/// <summary>
/// Create a reference to an FFXIV game object.
/// </summary>
/// <param name="address">The address of the object in memory.</param>
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
public GameObject? CreateObjectReference(nint address);
}

View file

@ -0,0 +1,81 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Party;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This collection represents the actors present in your party or alliance.
/// </summary>
public interface IPartyList : IReadOnlyCollection<PartyMember>
{
/// <summary>
/// Gets the amount of party members the local player has.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the index of the party leader.
/// </summary>
public uint PartyLeaderIndex { get; }
/// <summary>
/// Gets a value indicating whether this group is an alliance.
/// </summary>
public bool IsAlliance { get; }
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public nint GroupManagerAddress { get; }
/// <summary>
/// Gets the address of the party list within the group manager.
/// </summary>
public nint GroupListAddress { get; }
/// <summary>
/// Gets the address of the alliance member list within the group manager.
/// </summary>
public nint AllianceListAddress { get; }
/// <summary>
/// Gets the ID of the party.
/// </summary>
public long PartyId { get; }
/// <summary>
/// Get a party member at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="PartyMember"/> at the specified spawn index.</returns>
public PartyMember? this[int index] { get; }
/// <summary>
/// Gets the address of the party member at the specified index of the party list.
/// </summary>
/// <param name="index">The index of the party member.</param>
/// <returns>The memory address of the party member.</returns>
public nint GetPartyMemberAddress(int index);
/// <summary>
/// Create a reference to an FFXIV party member.
/// </summary>
/// <param name="address">The address of the party member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreatePartyMemberReference(nint address);
/// <summary>
/// Gets the address of the alliance member at the specified index of the alliance list.
/// </summary>
/// <param name="index">The index of the alliance member.</param>
/// <returns>The memory address of the alliance member.</returns>
public nint GetAllianceMemberAddress(int index);
/// <summary>
/// Create a reference to an FFXIV alliance member.
/// </summary>
/// <param name="address">The address of the alliance member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreateAllianceMemberReference(nint address);
}

View file

@ -0,0 +1,150 @@
using System;
using System.Diagnostics;
namespace Dalamud.Game;
/// <summary>
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
/// </summary>
public interface ISigScanner
{
/// <summary>
/// Gets a value indicating whether or not the search on this module is performed on a copy.
/// </summary>
public bool IsCopy { get; }
/// <summary>
/// Gets a value indicating whether or not the ProcessModule is 32-bit.
/// </summary>
public bool Is32BitProcess { get; }
/// <summary>
/// Gets the base address of the search area. When copied, this will be the address of the copy.
/// </summary>
public IntPtr SearchBase { get; }
/// <summary>
/// Gets the base address of the .text section search area.
/// </summary>
public IntPtr TextSectionBase { get; }
/// <summary>
/// Gets the offset of the .text section from the base of the module.
/// </summary>
public long TextSectionOffset { get; }
/// <summary>
/// Gets the size of the text section.
/// </summary>
public int TextSectionSize { get; }
/// <summary>
/// Gets the base address of the .data section search area.
/// </summary>
public IntPtr DataSectionBase { get; }
/// <summary>
/// Gets the offset of the .data section from the base of the module.
/// </summary>
public long DataSectionOffset { get; }
/// <summary>
/// Gets the size of the .data section.
/// </summary>
public int DataSectionSize { get; }
/// <summary>
/// Gets the base address of the .rdata section search area.
/// </summary>
public IntPtr RDataSectionBase { get; }
/// <summary>
/// Gets the offset of the .rdata section from the base of the module.
/// </summary>
public long RDataSectionOffset { get; }
/// <summary>
/// Gets the size of the .rdata section.
/// </summary>
public int RDataSectionSize { get; }
/// <summary>
/// Gets the ProcessModule on which the search is performed.
/// </summary>
public ProcessModule Module { get; }
/// <summary>
/// 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.
/// </summary>
/// <param name="signature">The signature of the function using the data.</param>
/// <param name="offset">The offset from function start of the instruction using the data.</param>
/// <returns>An IntPtr to the static memory location.</returns>
public nint GetStaticAddressFromSig(string signature, int offset = 0);
/// <summary>
/// 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.
/// </summary>
/// <param name="signature">The signature of the function using the data.</param>
/// <param name="result">An IntPtr to the static memory location, if found.</param>
/// <param name="offset">The offset from function start of the instruction using the data.</param>
/// <returns>true if the signature was found.</returns>
public bool TryGetStaticAddressFromSig(string signature, out nint result, int offset = 0);
/// <summary>
/// Scan for a byte signature in the .data section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <returns>The real offset of the found signature.</returns>
public nint ScanData(string signature);
/// <summary>
/// Try scanning for a byte signature in the .data section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <param name="result">The real offset of the signature, if found.</param>
/// <returns>true if the signature was found.</returns>
public bool TryScanData(string signature, out nint result);
/// <summary>
/// Scan for a byte signature in the whole module search area.
/// </summary>
/// <param name="signature">The signature.</param>
/// <returns>The real offset of the found signature.</returns>
public nint ScanModule(string signature);
/// <summary>
/// Try scanning for a byte signature in the whole module search area.
/// </summary>
/// <param name="signature">The signature.</param>
/// <param name="result">The real offset of the signature, if found.</param>
/// <returns>true if the signature was found.</returns>
public bool TryScanModule(string signature, out nint result);
/// <summary>
/// Resolve a RVA address.
/// </summary>
/// <param name="nextInstAddr">The address of the next instruction.</param>
/// <param name="relOffset">The relative offset.</param>
/// <returns>The calculated offset.</returns>
public nint ResolveRelativeAddress(nint nextInstAddr, int relOffset);
/// <summary>
/// Scan for a byte signature in the .text section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <returns>The real offset of the found signature.</returns>
public nint ScanText(string signature);
/// <summary>
/// Try scanning for a byte signature in the .text section.
/// </summary>
/// <param name="signature">The signature.</param>
/// <param name="result">The real offset of the signature, if found.</param>
/// <returns>true if the signature was found.</returns>
public bool TryScanText(string signature, out nint result);
}

View file

@ -0,0 +1,44 @@
using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Objects;
/// <summary>
/// Get and set various kinds of targets for the player.
/// </summary>
public interface ITargetManager
{
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public nint Address { get; }
/// <summary>
/// Gets or sets the current target.
/// Set to null to clear the target.
/// </summary>
public GameObject? Target { get; set; }
/// <summary>
/// Gets or sets the mouseover target.
/// Set to null to clear the target.
/// </summary>
public GameObject? MouseOverTarget { get; set; }
/// <summary>
/// Gets or sets the focus target.
/// Set to null to clear the target.
/// </summary>
public GameObject? FocusTarget { get; set; }
/// <summary>
/// Gets or sets the previous target.
/// Set to null to clear the target.
/// </summary>
public GameObject? PreviousTarget { get; set; }
/// <summary>
/// Gets or sets the soft target.
/// Set to null to clear the target.
/// </summary>
public GameObject? SoftTarget { get; set; }
}

View file

@ -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
/// <param name="reporter">The reporter name.</param>
/// <param name="includeException">Whether or not the most recent exception to occur should be included in the report.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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;

View file

@ -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,

View file

@ -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<char> needleSpan = ReadOnlySpan<char>.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<char> 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<char> 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<char> 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<char> 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
}

View file

@ -652,7 +652,7 @@ public static class Util
}
/// <summary>
/// Print formatted GameObject Information to ImGui
/// Print formatted GameObject Information to ImGui.
/// </summary>
/// <param name="actor">Game Object to Display.</param>
/// <param name="tag">Display Tag.</param>

View file

@ -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 "$@"

@ -1 +1 @@
Subproject commit f2abb4a11319b26b77cd29b69a52b34e1d56069d
Subproject commit a6f5d730c2fbf4a0521b0512fe41f4622d515218

71
sign.ps1 Normal file
View file

@ -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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Project>
<PropertyGroup>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Project>
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Platforms>x64</Platforms>
@ -10,42 +10,19 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AssemblySearchPaths>$(AssemblySearchPaths);$(DalamudLibPath)</AssemblySearchPaths>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.10" />
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Dalamud.Interface">
<HintPath>$(DalamudLibPath)Dalamud.Interface.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<PackageReference Include="DalamudPackager" Version="2.1.11" />
<Reference Include="FFXIVClientStructs" Private="false" />
<Reference Include="Newtonsoft.Json" Private="false" />
<Reference Include="Dalamud" Private="false" />
<Reference Include="Dalamud.Interface" Private="false" />
<Reference Include="ImGui.NET" Private="false" />
<Reference Include="ImGuiScene" Private="false" />
<Reference Include="Lumina" Private="false" />
<Reference Include="Lumina.Excel" Private="false" />
</ItemGroup>
<Target Name="Message" BeforeTargets="BeforeBuild">