mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-17 05:17:42 +01:00
Merge remote-tracking branch 'origin/master' into net8-rollup
This commit is contained in:
commit
b68da56e74
99 changed files with 6308 additions and 1218 deletions
|
|
@ -5,6 +5,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.IoC.Internal;
|
||||
|
|
@ -145,7 +146,13 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use AXIS fonts from the game.
|
||||
/// </summary>
|
||||
public bool UseAxisFontsFromGame { get; set; } = false;
|
||||
[Obsolete($"See {nameof(DefaultFontSpec)}")]
|
||||
public bool UseAxisFontsFromGame { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default font spec.
|
||||
/// </summary>
|
||||
public IFontSpec? DefaultFontSpec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
|
||||
|
|
@ -208,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
/// </summary>
|
||||
public bool LogOpenAtStartup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of lines to keep for the Dalamud Console window.
|
||||
/// </summary>
|
||||
public int LogLinesLimit { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the dev bar should open at startup.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>9.0.0.17</DalamudVersion>
|
||||
<DalamudVersion>9.0.0.21</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
|
||||
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
|
||||
<PackageReference Include="Lumina" Version="3.15.2" />
|
||||
<PackageReference Include="Lumina" Version="3.16.0" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Dalamud.Hooking;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
|
@ -15,6 +17,11 @@ namespace Dalamud.Game.Config;
|
|||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
||||
{
|
||||
private readonly TaskCompletionSource tcsInitialization = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
|
||||
|
||||
private readonly GameConfigAddressResolver address = new();
|
||||
private Hook<ConfigChangeDelegate>? configChangeHook;
|
||||
|
||||
|
|
@ -23,16 +30,32 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
{
|
||||
framework.RunOnTick(() =>
|
||||
{
|
||||
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();
|
||||
try
|
||||
{
|
||||
Log.Verbose("[GameConfig] Initializing");
|
||||
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
|
||||
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
|
||||
this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
|
||||
this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
|
||||
this.tcsUiControl.SetResult(
|
||||
new(
|
||||
"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();
|
||||
this.tcsInitialization.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -58,14 +81,19 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
public event EventHandler<ConfigChangeEvent>? UiControlChanged;
|
||||
#pragma warning restore 67
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection System { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets a task representing the initialization state of this class.
|
||||
/// </summary>
|
||||
public Task InitializationTask => this.tcsInitialization.Task;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiConfig { get; private set; }
|
||||
public GameConfigSection System => this.tcsSystem.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiControl { get; private set; }
|
||||
public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
|
||||
|
|
@ -169,6 +197,11 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
var ode = new ObjectDisposedException(nameof(GameConfig));
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ode);
|
||||
this.tcsSystem.SetExceptionIfIncomplete(ode);
|
||||
this.tcsUiConfig.SetExceptionIfIncomplete(ode);
|
||||
this.tcsUiControl.SetExceptionIfIncomplete(ode);
|
||||
this.configChangeHook?.Disable();
|
||||
this.configChangeHook?.Dispose();
|
||||
}
|
||||
|
|
@ -220,15 +253,24 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
|
||||
|
||||
private readonly Task initializationTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class.
|
||||
/// </summary>
|
||||
internal GameConfigPluginScoped()
|
||||
{
|
||||
this.gameConfigService.Changed += this.ConfigChangedForward;
|
||||
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
|
||||
this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return r;
|
||||
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
|
||||
return Task.CompletedTask;
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -256,9 +298,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
public void Dispose()
|
||||
{
|
||||
this.gameConfigService.Changed -= this.ConfigChangedForward;
|
||||
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
|
||||
this.initializationTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
|
||||
});
|
||||
|
||||
this.Changed = null;
|
||||
this.SystemChanged = null;
|
||||
|
|
|
|||
560
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
560
Dalamud/Game/Gui/ContextMenu/ContextMenu.cs
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using FFXIVClientStructs.Interop;
|
||||
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// This class handles interacting with the game's (right-click) context menu.
|
||||
/// </summary>
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu
|
||||
{
|
||||
private static readonly ModuleLog Log = new("ContextMenu");
|
||||
|
||||
private readonly Hook<RaptureAtkModuleOpenAddonByAgentDelegate> raptureAtkModuleOpenAddonByAgentHook;
|
||||
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
|
||||
private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ContextMenu()
|
||||
{
|
||||
this.raptureAtkModuleOpenAddonByAgentHook = Hook<RaptureAtkModuleOpenAddonByAgentDelegate>.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour);
|
||||
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
|
||||
this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer<RaptureAtkModuleOpenAddonDelegate>((nint)RaptureAtkModule.Addresses.OpenAddon.Value);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Enable();
|
||||
this.addonContextMenuOnMenuSelectedHook.Enable();
|
||||
}
|
||||
|
||||
private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId);
|
||||
|
||||
private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
|
||||
|
||||
private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
private AgentInterface* SelectedAgent { get; set; }
|
||||
|
||||
private ContextMenuType? SelectedMenuType { get; set; }
|
||||
|
||||
private List<MenuItem>? SelectedItems { get; set; }
|
||||
|
||||
private HashSet<nint> SelectedEventInterfaces { get; } = new();
|
||||
|
||||
private AtkUnitBase* SelectedParentAddon { get; set; }
|
||||
|
||||
// -1 -> -inf: native items
|
||||
// 0 -> inf: selected items
|
||||
private List<int> MenuCallbackIds { get; } = new();
|
||||
|
||||
private IReadOnlyList<MenuItem>? SubmenuItems { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
var manager = RaptureAtkUnitManager.Instance();
|
||||
var menu = manager->GetAddonByName("ContextMenu");
|
||||
var submenu = manager->GetAddonByName("AddonContextSub");
|
||||
if (menu->IsVisible)
|
||||
menu->FireCallbackInt(-1);
|
||||
if (submenu->IsVisible)
|
||||
submenu->FireCallbackInt(-1);
|
||||
|
||||
this.raptureAtkModuleOpenAddonByAgentHook.Dispose();
|
||||
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
return false;
|
||||
return items.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
private AtkValue* ExpandContextMenuArray(Span<AtkValue> oldValues, int newSize)
|
||||
{
|
||||
// if the array has enough room, don't reallocate
|
||||
if (oldValues.Length >= newSize)
|
||||
return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]);
|
||||
|
||||
var size = (sizeof(AtkValue) * newSize) + 8;
|
||||
var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0);
|
||||
if (newArray == nint.Zero)
|
||||
throw new OutOfMemoryException();
|
||||
NativeMemory.Fill((void*)newArray, (nuint)size, 0);
|
||||
|
||||
*(ulong*)newArray = (ulong)newSize;
|
||||
|
||||
// copy old memory if existing
|
||||
if (!oldValues.IsEmpty)
|
||||
oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length));
|
||||
|
||||
return (AtkValue*)(newArray + 8);
|
||||
}
|
||||
|
||||
private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) =>
|
||||
IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8));
|
||||
|
||||
private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: String = Name
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0)
|
||||
// 7: UInt = 1
|
||||
|
||||
valueCount = 8;
|
||||
var values = this.ExpandContextMenuArray(Span<AtkValue>.Empty, valueCount);
|
||||
values[0].ChangeType(ValueType.UInt);
|
||||
values[0].UInt = 0;
|
||||
values[1].ChangeType(ValueType.String);
|
||||
values[1].SetString(name.Encode().NullTerminate());
|
||||
values[2].ChangeType(ValueType.Int);
|
||||
values[2].Int = x;
|
||||
values[3].ChangeType(ValueType.Int);
|
||||
values[3].Int = y;
|
||||
values[4].ChangeType(ValueType.Bool);
|
||||
values[4].Byte = 0;
|
||||
values[5].ChangeType(ValueType.UInt);
|
||||
values[5].UInt = 0;
|
||||
values[6].ChangeType(ValueType.UInt);
|
||||
values[6].UInt = 0;
|
||||
values[7].ChangeType(ValueType.UInt);
|
||||
values[7].UInt = 1;
|
||||
return values;
|
||||
}
|
||||
|
||||
private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority);
|
||||
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
|
||||
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
|
||||
|
||||
var nativeMenuSize = (int)values[sizeHeaderIdx].UInt;
|
||||
var prefixMenuSize = prefixItems.Length;
|
||||
var suffixMenuSize = suffixItems.Length;
|
||||
|
||||
var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0;
|
||||
|
||||
var hasCustomDisabled = items.Any(item => !item.IsEnabled);
|
||||
var hasAnyDisabled = hasGameDisabled || hasCustomDisabled;
|
||||
|
||||
values = this.ExpandContextMenuArray(
|
||||
new(values, valueCount),
|
||||
valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount);
|
||||
var offsetData = new Span<AtkValue>(values, headerCount);
|
||||
var nameData = new Span<AtkValue>(values + headerCount, nativeMenuSize + items.Count);
|
||||
var disabledData = hasAnyDisabled ? new Span<AtkValue>(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span<AtkValue>.Empty;
|
||||
|
||||
var returnMask = offsetData[returnHeaderIdx].UInt;
|
||||
var submenuMask = offsetData[submenuHeaderIdx].UInt;
|
||||
|
||||
nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
if (hasGameDisabled)
|
||||
{
|
||||
// copy old disabled data
|
||||
var oldDisabledData = new Span<AtkValue>(values + headerCount + nativeMenuSize, nativeMenuSize);
|
||||
oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
// enable all
|
||||
for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
returnMask <<= prefixMenuSize;
|
||||
submenuMask <<= prefixMenuSize;
|
||||
|
||||
void FillData(Span<AtkValue> disabledData, Span<AtkValue> nameData, int i, MenuItem item, int idx)
|
||||
{
|
||||
this.MenuCallbackIds.Add(idx);
|
||||
|
||||
if (hasAnyDisabled)
|
||||
{
|
||||
disabledData[i].ChangeType(ValueType.Int);
|
||||
disabledData[i].Int = item.IsEnabled ? 0 : 1;
|
||||
}
|
||||
|
||||
if (item.IsReturn)
|
||||
returnMask |= 1u << i;
|
||||
if (item.IsSubmenu)
|
||||
submenuMask |= 1u << i;
|
||||
|
||||
nameData[i].ChangeType(ValueType.String);
|
||||
nameData[i].SetString(item.PrefixedName.Encode().NullTerminate());
|
||||
}
|
||||
|
||||
for (var i = 0; i < prefixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = prefixItems[i];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1));
|
||||
|
||||
for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i)
|
||||
{
|
||||
var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize];
|
||||
FillData(disabledData, nameData, i, item, idx);
|
||||
}
|
||||
|
||||
offsetData[returnHeaderIdx].UInt = returnMask;
|
||||
offsetData[submenuHeaderIdx].UInt = submenuMask;
|
||||
|
||||
offsetData[sizeHeaderIdx].UInt += (uint)items.Count;
|
||||
}
|
||||
|
||||
private void SetupContextMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = Item Count
|
||||
// 1: UInt = 0 (probably window name, just unused)
|
||||
// 2: UInt = Return Mask (?)
|
||||
// 3: UInt = Submenu Mask
|
||||
// 4: UInt = OpenAtCursorPosition ? 2 : 1
|
||||
// 5: UInt = 0
|
||||
// 6: UInt = 0
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!item.Prefix.HasValue)
|
||||
{
|
||||
item.PrefixChar = 'D';
|
||||
item.PrefixColor = 539;
|
||||
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
|
||||
}
|
||||
}
|
||||
|
||||
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private void SetupContextSubMenu(IReadOnlyList<MenuItem> items, ref int valueCount, ref AtkValue* values)
|
||||
{
|
||||
// 0: UInt = ContextItemCount
|
||||
// 1: skipped?
|
||||
// 2: Int = PositionX
|
||||
// 3: Int = PositionY
|
||||
// 4: Bool = false
|
||||
// 5: UInt = ContextItemSubmenuMask
|
||||
// 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0
|
||||
// 7: UInt = 1
|
||||
|
||||
this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values);
|
||||
}
|
||||
|
||||
private ushort RaptureAtkModuleOpenAddonByAgentDetour(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId)
|
||||
{
|
||||
var oldValues = values;
|
||||
|
||||
if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
this.SelectedAgent = agent;
|
||||
this.SelectedParentAddon = module->RaptureAtkUnitManager.GetAddonById(parentAddonId);
|
||||
this.SelectedEventInterfaces.Clear();
|
||||
if (this.SelectedAgent == AgentInventoryContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Inventory;
|
||||
}
|
||||
else if (this.SelectedAgent == AgentContext.Instance())
|
||||
{
|
||||
this.SelectedMenuType = ContextMenuType.Default;
|
||||
|
||||
var menu = AgentContext.Instance()->CurrentContextMenu;
|
||||
var handlers = new Span<Pointer<AtkEventInterface>>(menu->EventHandlerArray, 32);
|
||||
var ids = new Span<byte>(menu->EventIdArray, 32);
|
||||
var count = (int)values[0].UInt;
|
||||
handlers = handlers.Slice(7, count);
|
||||
ids = ids.Slice(7, count);
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
if (ids[i] <= 106)
|
||||
continue;
|
||||
this.SelectedEventInterfaces.Add((nint)handlers[i].Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedMenuType = null;
|
||||
}
|
||||
|
||||
this.SubmenuItems = null;
|
||||
|
||||
if (this.SelectedMenuType is { } menuType)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.SelectedItems = new(items);
|
||||
else
|
||||
this.SelectedItems = new();
|
||||
}
|
||||
|
||||
var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces);
|
||||
this.OnMenuOpened?.InvokeSafely(args);
|
||||
this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt);
|
||||
this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SelectedItems = null;
|
||||
}
|
||||
}
|
||||
else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName))
|
||||
{
|
||||
this.MenuCallbackIds.Clear();
|
||||
if (this.SubmenuItems != null)
|
||||
{
|
||||
this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt);
|
||||
|
||||
this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values);
|
||||
Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items.");
|
||||
}
|
||||
}
|
||||
|
||||
var ret = this.raptureAtkModuleOpenAddonByAgentHook.Original(module, addonName, addon, valueCount, values, agent, a7, parentAddonId);
|
||||
if (values != oldValues)
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<MenuItem> FixupMenuList(List<MenuItem> items, int nativeMenuSize)
|
||||
{
|
||||
// The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow.
|
||||
// As such, we'll only work with 31 items.
|
||||
const int MaxMenuItems = 31;
|
||||
if (items.Count + nativeMenuSize > MaxMenuItems)
|
||||
{
|
||||
Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating.");
|
||||
var orderedItems = items.OrderBy(i => i.Priority).ToArray();
|
||||
var newItems = orderedItems[..(MaxMenuItems - nativeMenuSize - 1)];
|
||||
var submenuItems = orderedItems[(MaxMenuItems - nativeMenuSize - 1)..];
|
||||
return newItems.Append(new MenuItem
|
||||
{
|
||||
Prefix = SeIconChar.BoxedLetterD,
|
||||
PrefixColor = 539,
|
||||
IsSubmenu = true,
|
||||
Priority = int.MaxValue,
|
||||
Name = $"See More ({submenuItems.Length})",
|
||||
OnClicked = a => a.OpenSubmenu(submenuItems),
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> submenuItems, int posX, int posY)
|
||||
{
|
||||
if (submenuItems.Count == 0)
|
||||
throw new ArgumentException("Submenu must not be empty", nameof(submenuItems));
|
||||
|
||||
this.SubmenuItems = submenuItems;
|
||||
|
||||
var module = RaptureAtkModule.Instance();
|
||||
var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount);
|
||||
|
||||
switch (this.SelectedMenuType)
|
||||
{
|
||||
case ContextMenuType.Default:
|
||||
{
|
||||
var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
case ContextMenuType.Inventory:
|
||||
{
|
||||
var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId;
|
||||
this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu");
|
||||
break;
|
||||
}
|
||||
|
||||
this.FreeExpandedContextMenuArray(values, valueCount);
|
||||
}
|
||||
|
||||
private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3)
|
||||
{
|
||||
var items = this.SubmenuItems ?? this.SelectedItems;
|
||||
if (items == null)
|
||||
goto original;
|
||||
if (this.MenuCallbackIds.Count == 0)
|
||||
goto original;
|
||||
if (selectedIdx < 0)
|
||||
goto original;
|
||||
if (selectedIdx >= this.MenuCallbackIds.Count)
|
||||
goto original;
|
||||
|
||||
var callbackId = this.MenuCallbackIds[selectedIdx];
|
||||
|
||||
if (callbackId < 0)
|
||||
{
|
||||
selectedIdx = -callbackId - 1;
|
||||
goto original;
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = items[callbackId];
|
||||
var openedSubmenu = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (item.OnClicked == null)
|
||||
throw new InvalidOperationException("Item has no OnClicked handler");
|
||||
item.OnClicked.InvokeSafely(new(
|
||||
(name, items) =>
|
||||
{
|
||||
short x, y;
|
||||
addon->AtkUnitBase.GetPosition(&x, &y);
|
||||
this.OpenSubmenu(name ?? item.Name, items, x, y);
|
||||
openedSubmenu = true;
|
||||
},
|
||||
this.SelectedParentAddon,
|
||||
this.SelectedAgent,
|
||||
this.SelectedMenuType.Value,
|
||||
this.SelectedEventInterfaces));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Error while handling context menu click");
|
||||
}
|
||||
|
||||
// Close with clicky sound
|
||||
if (!openedSubmenu)
|
||||
addon->AtkUnitBase.FireCallbackInt(-2);
|
||||
return false;
|
||||
}
|
||||
|
||||
original:
|
||||
// Eventually handled by inventorycontext here: 14022BBD0 (6.51)
|
||||
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a <see cref="ContextMenu"/> service.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<IContextMenu>]
|
||||
#pragma warning restore SA1015
|
||||
internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly ContextMenu parentService = Service<ContextMenu>.Get();
|
||||
|
||||
private ContextMenuPluginScoped()
|
||||
{
|
||||
this.parentService.OnMenuOpened += this.OnMenuOpenedForward;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
|
||||
|
||||
private object MenuItemsLock { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.parentService.OnMenuOpened -= this.OnMenuOpenedForward;
|
||||
|
||||
this.OnMenuOpened = null;
|
||||
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
foreach (var (menuType, items) in this.MenuItems)
|
||||
{
|
||||
foreach (var item in items)
|
||||
this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (!this.MenuItems.TryGetValue(menuType, out var items))
|
||||
this.MenuItems[menuType] = items = new();
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
this.parentService.AddMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item)
|
||||
{
|
||||
lock (this.MenuItemsLock)
|
||||
{
|
||||
if (this.MenuItems.TryGetValue(menuType, out var items))
|
||||
items.Remove(item);
|
||||
}
|
||||
|
||||
return this.parentService.RemoveMenuItem(menuType, item);
|
||||
}
|
||||
|
||||
private void OnMenuOpenedForward(MenuOpenedArgs args) =>
|
||||
this.OnMenuOpened?.Invoke(args);
|
||||
}
|
||||
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
18
Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// The type of context menu.
|
||||
/// Each one has a different associated <see cref="MenuTarget"/>.
|
||||
/// </summary>
|
||||
public enum ContextMenuType
|
||||
{
|
||||
/// <summary>
|
||||
/// The default context menu.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// The inventory context menu. Used when right-clicked on an item.
|
||||
/// </summary>
|
||||
Inventory,
|
||||
}
|
||||
77
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
77
Dalamud/Game/Gui/ContextMenu/MenuArgs.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="IContextMenu"/> menu args.
|
||||
/// </summary>
|
||||
public abstract unsafe class MenuArgs
|
||||
{
|
||||
private IReadOnlySet<nint>? eventInterfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint>? eventInterfaces)
|
||||
{
|
||||
this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null;
|
||||
this.AddonPtr = (nint)addon;
|
||||
this.AgentPtr = (nint)agent;
|
||||
this.MenuType = type;
|
||||
this.eventInterfaces = eventInterfaces;
|
||||
this.Target = type switch
|
||||
{
|
||||
ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent),
|
||||
ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent),
|
||||
_ => throw new ArgumentException("Invalid context menu type", nameof(type)),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public string? AddonName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the addon that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AddonPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory pointer of the agent that opened the context menu.
|
||||
/// </summary>
|
||||
public nint AgentPtr { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the context menu.
|
||||
/// </summary>
|
||||
public ContextMenuType MenuType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target info of the context menu. The actual type depends on <see cref="MenuType"/>.
|
||||
/// <see cref="ContextMenuType.Default"/> signifies a <see cref="MenuTargetDefault"/>.
|
||||
/// <see cref="ContextMenuType.Inventory"/> signifies a <see cref="MenuTargetInventory"/>.
|
||||
/// </summary>
|
||||
public MenuTarget Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of AtkEventInterface pointers associated with the context menu.
|
||||
/// Only available with <see cref="ContextMenuType.Default"/>.
|
||||
/// Almost always an agent pointer. You can use this to find out what type of context menu it is.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the context menu is not a <see cref="ContextMenuType.Default"/>.</exception>
|
||||
public IReadOnlySet<nint> EventInterfaces =>
|
||||
this.MenuType != ContextMenuType.Default ?
|
||||
this.eventInterfaces :
|
||||
throw new InvalidOperationException("Not a default context menu");
|
||||
}
|
||||
91
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
91
Dalamud/Game/Gui/ContextMenu/MenuItem.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// A menu item that can be added to a context menu.
|
||||
/// </summary>
|
||||
public sealed record MenuItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the menu item.
|
||||
/// </summary>
|
||||
public SeString Name { get; set; } = SeString.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the prefix attached to the beginning of <see cref="Name"/>.
|
||||
/// </summary>
|
||||
public SeIconChar? Prefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the character to prefix the <see cref="Name"/> with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> must be an uppercase letter.</exception>
|
||||
public char? PrefixChar
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value is { } prefix)
|
||||
{
|
||||
if (!char.IsAsciiLetterUpper(prefix))
|
||||
throw new ArgumentException("Prefix must be an uppercase letter", nameof(value));
|
||||
|
||||
this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Prefix = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the color of the <see cref="Prefix"/>. Specifies a <see cref="UIColor"/> row id.
|
||||
/// </summary>
|
||||
public ushort PrefixColor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the callback to be invoked when the menu item is clicked.
|
||||
/// </summary>
|
||||
public Action<MenuItemClickedArgs>? OnClicked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority (or order) with which the menu item should be displayed in descending order.
|
||||
/// Priorities below 0 will be displayed above the native menu items.
|
||||
/// Other priorities will be displayed below the native menu items.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is enabled.
|
||||
/// Disabled items will be faded and cannot be clicked on.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a submenu.
|
||||
/// This value is purely visual. Submenu items will have an arrow to its right.
|
||||
/// </summary>
|
||||
public bool IsSubmenu { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the menu item is a return item.
|
||||
/// This value is purely visual. Return items will have a back arrow to its left.
|
||||
/// If both <see cref="IsSubmenu"/> and <see cref="IsReturn"/> are true, the return arrow will take precedence.
|
||||
/// </summary>
|
||||
public bool IsReturn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name with the given prefix.
|
||||
/// </summary>
|
||||
internal SeString PrefixedName =>
|
||||
this.Prefix is { } prefix
|
||||
? new SeStringBuilder()
|
||||
.AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor)
|
||||
.Append(this.Name)
|
||||
.Build()
|
||||
: this.Name;
|
||||
}
|
||||
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
44
Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is clicked.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuItemClickedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuItemClickedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="openSubmenu">Callback for opening a submenu.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuItemClickedArgs(Action<SeString?, IReadOnlyList<MenuItem>> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnOpenSubmenu = openSubmenu;
|
||||
}
|
||||
|
||||
private Action<SeString?, IReadOnlyList<MenuItem>> OnOpenSubmenu { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given name and items.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the submenu, displayed at the top.</param>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(SeString name, IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(name, items);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a submenu with the given items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items to display in the submenu.</param>
|
||||
public void OpenSubmenu(IReadOnlyList<MenuItem> items) =>
|
||||
this.OnOpenSubmenu(null, items);
|
||||
}
|
||||
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
34
Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Callback args used when a menu item is opened.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuOpenedArgs : MenuArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuOpenedArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="addMenuItem">Callback for adding a custom menu item.</param>
|
||||
/// <param name="addon">Addon associated with the context menu.</param>
|
||||
/// <param name="agent">Agent associated with the context menu.</param>
|
||||
/// <param name="type">The type of context menu.</param>
|
||||
/// <param name="eventInterfaces">List of AtkEventInterfaces associated with the context menu.</param>
|
||||
internal MenuOpenedArgs(Action<MenuItem> addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet<nint> eventInterfaces)
|
||||
: base(addon, agent, type, eventInterfaces)
|
||||
{
|
||||
this.OnAddMenuItem = addMenuItem;
|
||||
}
|
||||
|
||||
private Action<MenuItem> OnAddMenuItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom menu item to the context menu.
|
||||
/// </summary>
|
||||
/// <param name="item">The menu item to add.</param>
|
||||
public void AddMenuItem(MenuItem item) =>
|
||||
this.OnAddMenuItem(item);
|
||||
}
|
||||
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
9
Dalamud/Game/Gui/ContextMenu/MenuTarget.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="MenuArgs"/> contexts.
|
||||
/// Discriminated based on <see cref="ContextMenuType"/>.
|
||||
/// </summary>
|
||||
public abstract class MenuTarget
|
||||
{
|
||||
}
|
||||
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
67
Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on a default context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetDefault : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetDefault"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetDefault(AgentContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the target.
|
||||
/// </summary>
|
||||
public string TargetName => this.Context->TargetName.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetObjectId => this.Context->TargetObjectId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target object.
|
||||
/// </summary>
|
||||
public GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the target.
|
||||
/// </summary>
|
||||
public ulong TargetContentId => this.Context->TargetContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world id of the target.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.
|
||||
/// Just because this is <see langword="null"/> doesn't mean the target isn't a character.
|
||||
/// </summary>
|
||||
public CharacterData? TargetCharacter
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->CurrentContextMenuTarget;
|
||||
if (target != null)
|
||||
return new(target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentContext* Context { get; }
|
||||
}
|
||||
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
36
Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Dalamud.Game.Inventory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace Dalamud.Game.Gui.ContextMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Target information on an inventory context menu.
|
||||
/// </summary>
|
||||
public sealed unsafe class MenuTargetInventory : MenuTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MenuTargetInventory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The agent associated with the context menu.</param>
|
||||
internal MenuTargetInventory(AgentInventoryContext* context)
|
||||
{
|
||||
this.Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target item.
|
||||
/// </summary>
|
||||
public GameInventoryItem? TargetItem
|
||||
{
|
||||
get
|
||||
{
|
||||
var target = this.Context->TargetInventorySlot;
|
||||
if (target != null)
|
||||
return new(*target);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentInventoryContext* Context { get; }
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
|
||||
private IntPtr AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* arg)
|
||||
{
|
||||
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", Marshal.PtrToStringAnsi(new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
|
||||
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", MemoryHelper.ReadSeStringAsString(out _, new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
|
||||
|
||||
// "SendHotkey"
|
||||
// 3 == Close
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
namespace Dalamud.Game.Inventory;
|
||||
|
|
@ -103,8 +106,10 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// <summary>
|
||||
/// Gets the array of materia grades.
|
||||
/// </summary>
|
||||
// TODO: Replace with MateriaGradeBytes
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public ReadOnlySpan<ushort> MateriaGrade =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of native inventory item in the game.<br />
|
||||
|
|
@ -146,6 +151,9 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
|
|||
/// </summary>
|
||||
internal ulong CrafterContentId => this.InternalItem.CrafterContentID;
|
||||
|
||||
private ReadOnlySpan<byte> MateriaGradeBytes =>
|
||||
new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5);
|
||||
|
||||
public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r);
|
||||
|
||||
public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r);
|
||||
|
|
|
|||
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
197
Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Game.ClientState.Resolvers;
|
||||
using Dalamud.Memory;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Dalamud.Game.Network.Structures.InfoProxy;
|
||||
|
||||
/// <summary>
|
||||
/// Dalamud wrapper around a client structs <see cref="InfoProxyCommonList.CharacterData"/>.
|
||||
/// </summary>
|
||||
public unsafe class CharacterData
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CharacterData"/> class.
|
||||
/// </summary>
|
||||
/// <param name="data">Character data to wrap.</param>
|
||||
internal CharacterData(InfoProxyCommonList.CharacterData* data)
|
||||
{
|
||||
this.Address = (nint)data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the <see cref="InfoProxyCommonList.CharacterData"/> in memory.
|
||||
/// </summary>
|
||||
public nint Address { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content id of the character.
|
||||
/// </summary>
|
||||
public ulong ContentId => this.Struct->ContentId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status mask of the character.
|
||||
/// </summary>
|
||||
public ulong StatusMask => (ulong)this.Struct->State;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the applicable statues of the character.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ExcelResolver<OnlineStatus>> Statuses
|
||||
{
|
||||
get
|
||||
{
|
||||
var statuses = new List<ExcelResolver<OnlineStatus>>();
|
||||
for (var i = 0; i < 64; i++)
|
||||
{
|
||||
if ((this.StatusMask & (1UL << i)) != 0)
|
||||
statuses.Add(new((uint)i));
|
||||
}
|
||||
|
||||
return statuses;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display group of the character.
|
||||
/// </summary>
|
||||
public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the character's home world is different from the current world.
|
||||
/// </summary>
|
||||
public bool IsFromOtherServer => this.Struct->IsOtherServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sort order of the character.
|
||||
/// </summary>
|
||||
public byte Sort => this.Struct->Sort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home world of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the location of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<TerritoryType> Location => new(this.Struct->Location);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grand company of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<GrandCompany> GrandCompany => new((uint)this.Struct->GrandCompany);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the primary client language of the character.
|
||||
/// </summary>
|
||||
public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported language mask of the character.
|
||||
/// </summary>
|
||||
public byte LanguageMask => (byte)this.Struct->Languages;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported languages the character supports.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ClientLanguage> Languages
|
||||
{
|
||||
get
|
||||
{
|
||||
var languages = new List<ClientLanguage>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
if ((this.LanguageMask & (1 << i)) != 0)
|
||||
languages.Add((ClientLanguage)i);
|
||||
}
|
||||
|
||||
return languages;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the gender of the character.
|
||||
/// </summary>
|
||||
public byte Gender => this.Struct->Sex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the job of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->Job);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the character.
|
||||
/// </summary>
|
||||
public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free company tag of the character.
|
||||
/// </summary>
|
||||
public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying <see cref="InfoProxyCommonList.CharacterData"/> struct.
|
||||
/// </summary>
|
||||
internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display group of a character. Used for friends.
|
||||
/// </summary>
|
||||
public enum DisplayGroup : sbyte
|
||||
{
|
||||
/// <summary>
|
||||
/// All display groups.
|
||||
/// </summary>
|
||||
All = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No display group.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Star display group.
|
||||
/// </summary>
|
||||
Star,
|
||||
|
||||
/// <summary>
|
||||
/// Circle display group.
|
||||
/// </summary>
|
||||
Circle,
|
||||
|
||||
/// <summary>
|
||||
/// Triangle display group.
|
||||
/// </summary>
|
||||
Triangle,
|
||||
|
||||
/// <summary>
|
||||
/// Diamond display group.
|
||||
/// </summary>
|
||||
Diamond,
|
||||
|
||||
/// <summary>
|
||||
/// Heart display group.
|
||||
/// </summary>
|
||||
Heart,
|
||||
|
||||
/// <summary>
|
||||
/// Spade display group.
|
||||
/// </summary>
|
||||
Spade,
|
||||
|
||||
/// <summary>
|
||||
/// Club display group.
|
||||
/// </summary>
|
||||
Club,
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Storage.Assets;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font from Dalamud assets.
|
||||
/// </summary>
|
||||
public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DalamudAssetFontAndFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="asset">The font asset.</param>
|
||||
public DalamudAssetFontAndFamilyId(DalamudAsset asset)
|
||||
{
|
||||
if (asset.GetPurpose() != DalamudAssetPurpose.Font)
|
||||
throw new ArgumentOutOfRangeException(nameof(asset), asset, "The specified asset is not a font asset.");
|
||||
this.Asset = asset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font asset.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public DalamudAsset Asset { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public string EnglishName => $"Dalamud: {this.Asset}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IFontFamilyId Family => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
public static bool operator ==(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
|
||||
Equals(left, right);
|
||||
|
||||
public static bool operator !=(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) =>
|
||||
!Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => (int)this.Asset;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style) => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
|
||||
tk.AddDalamudAssetFont(this.Asset, config);
|
||||
|
||||
private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset;
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the default Dalamud font.
|
||||
/// </summary>
|
||||
public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId
|
||||
{
|
||||
/// <summary>
|
||||
/// The shared instance of <see cref="DalamudDefaultFontAndFamilyId"/>.
|
||||
/// </summary>
|
||||
public static readonly DalamudDefaultFontAndFamilyId Instance = new();
|
||||
|
||||
private DalamudDefaultFontAndFamilyId()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public string EnglishName => "(Default)";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IFontFamilyId Family => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
|
||||
|
||||
public static bool operator ==(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
|
||||
left is null == right is null;
|
||||
|
||||
public static bool operator !=(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) =>
|
||||
left is null != right is null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => 12345678;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => nameof(DalamudDefaultFontAndFamilyId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
|
||||
=> tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges);
|
||||
// TODO: mergeFont
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style) => 0;
|
||||
}
|
||||
81
Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs
Normal file
81
Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font from the game.
|
||||
/// </summary>
|
||||
public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameFontAndFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="family">The game font family.</param>
|
||||
public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the game font family.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public GameFontFamily GameFontFamily { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IFontFamilyId Family => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => new List<IFontId> { this }.AsReadOnly();
|
||||
|
||||
public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right);
|
||||
|
||||
public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => (int)this.GameFontFamily;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style) => 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) =>
|
||||
tk.AddGameGlyphs(new(this.GameFontFamily, config.SizePx), config.GlyphRanges, config.MergeFont);
|
||||
|
||||
private bool Equals(GameFontAndFamilyId other) => this.GameFontFamily == other.GameFontFamily;
|
||||
}
|
||||
102
Dalamud/Interface/FontIdentifier/IFontFamilyId.cs
Normal file
102
Dalamud/Interface/FontIdentifier/IFontFamilyId.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font family identifier.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontFamilyId : IObjectWithLocalizableName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the list of fonts under this family.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
IReadOnlyList<IFontId> Fonts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of the font inside <see cref="Fonts"/> that best matches the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="weight">The weight of the font.</param>
|
||||
/// <param name="stretch">The stretch of the font.</param>
|
||||
/// <param name="style">The style of the font.</param>
|
||||
/// <returns>The index of the font. Guaranteed to be a valid index.</returns>
|
||||
int FindBestMatch(int weight, int stretch, int style);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of Dalamud-provided fonts.
|
||||
/// </summary>
|
||||
/// <returns>The list of fonts.</returns>
|
||||
public static List<IFontFamilyId> ListDalamudFonts() =>
|
||||
new()
|
||||
{
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular),
|
||||
new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of Game-provided fonts.
|
||||
/// </summary>
|
||||
/// <returns>The list of fonts.</returns>
|
||||
public static List<IFontFamilyId> ListGameFonts() => new()
|
||||
{
|
||||
new GameFontAndFamilyId(GameFontFamily.Axis),
|
||||
new GameFontAndFamilyId(GameFontFamily.Jupiter),
|
||||
new GameFontAndFamilyId(GameFontFamily.JupiterNumeric),
|
||||
new GameFontAndFamilyId(GameFontFamily.Meidinger),
|
||||
new GameFontAndFamilyId(GameFontFamily.MiedingerMid),
|
||||
new GameFontAndFamilyId(GameFontFamily.TrumpGothic),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of System-provided fonts.
|
||||
/// </summary>
|
||||
/// <param name="refresh">If <c>true</c>, try to refresh the list.</param>
|
||||
/// <returns>The list of fonts.</returns>
|
||||
public static unsafe List<IFontFamilyId> ListSystemFonts(bool refresh)
|
||||
{
|
||||
using var dwf = default(ComPtr<IDWriteFactory>);
|
||||
fixed (Guid* piid = &IID.IID_IDWriteFactory)
|
||||
{
|
||||
DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
|
||||
piid,
|
||||
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
|
||||
}
|
||||
|
||||
using var sfc = default(ComPtr<IDWriteFontCollection>);
|
||||
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError();
|
||||
|
||||
var count = (int)sfc.Get()->GetFontFamilyCount();
|
||||
var result = new List<IFontFamilyId>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
using var ff = default(ComPtr<IDWriteFontFamily>);
|
||||
if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED)
|
||||
{
|
||||
// Ignore errors, if any
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.Add(SystemFontFamilyId.FromDWriteFamily(ff));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
40
Dalamud/Interface/FontIdentifier/IFontId.cs
Normal file
40
Dalamud/Interface/FontIdentifier/IFontId.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font identifier.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontId : IObjectWithLocalizableName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the associated font family.
|
||||
/// </summary>
|
||||
IFontFamilyId Family { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font weight, ranging from 1 to 999.
|
||||
/// </summary>
|
||||
int Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font stretch, ranging from 1 to 9.
|
||||
/// </summary>
|
||||
int Stretch { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font style. Treat as an opaque value.
|
||||
/// </summary>
|
||||
int Style { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds this font to the given font build toolkit.
|
||||
/// </summary>
|
||||
/// <param name="tk">The font build toolkit.</param>
|
||||
/// <param name="config">The font configuration. Some parameters may be ignored.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config);
|
||||
}
|
||||
52
Dalamud/Interface/FontIdentifier/IFontSpec.cs
Normal file
52
Dalamud/Interface/FontIdentifier/IFontSpec.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's choice of font(s).<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the font size in pixels.
|
||||
/// </summary>
|
||||
float SizePx { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font size in points.
|
||||
/// </summary>
|
||||
float SizePt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line height in pixels.
|
||||
/// </summary>
|
||||
float LineHeightPx { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a font handle corresponding to this font specification.
|
||||
/// </summary>
|
||||
/// <param name="atlas">The atlas to bind this font handle to.</param>
|
||||
/// <param name="callback">Optional callback to be called after creating the font handle.</param>
|
||||
/// <returns>The new font handle.</returns>
|
||||
/// <remarks><see cref="IFontAtlasBuildToolkit.Font"/> will be set when <paramref name="callback"/> is invoked.
|
||||
/// </remarks>
|
||||
IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds this font to the given font build toolkit.
|
||||
/// </summary>
|
||||
/// <param name="tk">The font build toolkit.</param>
|
||||
/// <param name="mergeFont">The font to merge to.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default);
|
||||
|
||||
/// <summary>
|
||||
/// Represents this font specification, preferrably in the requested locale.
|
||||
/// </summary>
|
||||
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
|
||||
/// <returns>The value.</returns>
|
||||
string ToLocalizedString(string localeCode);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an object with localizable names.
|
||||
/// </summary>
|
||||
public interface IObjectWithLocalizableName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name, preferrably in English.
|
||||
/// </summary>
|
||||
string EnglishName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the names per locales.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, string>? LocaleNames { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name in the requested locale if available; otherwise, <see cref="EnglishName"/>.
|
||||
/// </summary>
|
||||
/// <param name="localeCode">The locale code. Must be in lowercase(invariant).</param>
|
||||
/// <returns>The value.</returns>
|
||||
string GetLocalizedName(string localeCode)
|
||||
{
|
||||
if (this.LocaleNames is null)
|
||||
return this.EnglishName;
|
||||
if (this.LocaleNames.TryGetValue(localeCode, out var v))
|
||||
return v;
|
||||
foreach (var (a, b) in this.LocaleNames)
|
||||
{
|
||||
if (a.StartsWith(localeCode))
|
||||
return b;
|
||||
}
|
||||
|
||||
return this.EnglishName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves all names per locales.
|
||||
/// </summary>
|
||||
/// <param name="fn">The names.</param>
|
||||
/// <returns>A new dictionary mapping from locale code to localized names.</returns>
|
||||
internal static unsafe IReadOnlyDictionary<string, string> GetLocaleNames(IDWriteLocalizedStrings* fn)
|
||||
{
|
||||
var count = fn->GetCount();
|
||||
var maxStrLen = 0u;
|
||||
for (var i = 0u; i < count; i++)
|
||||
{
|
||||
var length = 0u;
|
||||
fn->GetStringLength(i, &length).ThrowOnError();
|
||||
maxStrLen = Math.Max(maxStrLen, length);
|
||||
fn->GetLocaleNameLength(i, &length).ThrowOnError();
|
||||
maxStrLen = Math.Max(maxStrLen, length);
|
||||
}
|
||||
|
||||
maxStrLen++;
|
||||
var buf = stackalloc char[(int)maxStrLen];
|
||||
var result = new Dictionary<string, string>((int)count);
|
||||
for (var i = 0u; i < count; i++)
|
||||
{
|
||||
fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError();
|
||||
var key = new string(buf);
|
||||
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
|
||||
var value = new string(buf);
|
||||
result[key.ToLowerInvariant()] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
154
Dalamud/Interface/FontIdentifier/SingleFontSpec.cs
Normal file
154
Dalamud/Interface/FontIdentifier/SingleFontSpec.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a user's choice of a single font.
|
||||
/// </summary>
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.OrderingRules",
|
||||
"SA1206:Declaration keywords should follow order",
|
||||
Justification = "public required")]
|
||||
public record SingleFontSpec : IFontSpec
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the font id.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public required IFontId FontId { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public float SizePx { get; init; } = 16;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public float SizePt
|
||||
{
|
||||
get => (this.SizePx * 3) / 4;
|
||||
init => this.SizePx = (value * 4) / 3;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line height ratio to the font size.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public float LineHeight { get; init; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glyph offset in pixels.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public Vector2 GlyphOffset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the letter spacing in pixels.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public float LetterSpacing { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glyph ranges.
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public ushort[]? GlyphRanges { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string ToLocalizedString(string localeCode)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(this.FontId.Family.GetLocalizedName(localeCode));
|
||||
sb.Append($"({this.FontId.GetLocalizedName(localeCode)}, {this.SizePt}pt");
|
||||
if (Math.Abs(this.LineHeight - 1f) > 0.000001f)
|
||||
sb.Append($", LH={this.LineHeight:0.##}");
|
||||
if (this.GlyphOffset != default)
|
||||
sb.Append($", O={this.GlyphOffset.X:0.##},{this.GlyphOffset.Y:0.##}");
|
||||
if (this.LetterSpacing != 0f)
|
||||
sb.Append($", LS={this.LetterSpacing:0.##}");
|
||||
sb.Append(')');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => this.ToLocalizedString("en");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) =>
|
||||
atlas.NewDelegateFontHandle(tk =>
|
||||
{
|
||||
tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e));
|
||||
callback?.Invoke(tk);
|
||||
});
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default)
|
||||
{
|
||||
var font = this.FontId.AddToBuildToolkit(
|
||||
tk,
|
||||
new()
|
||||
{
|
||||
SizePx = this.SizePx,
|
||||
GlyphRanges = this.GlyphRanges,
|
||||
MergeFont = mergeFont,
|
||||
});
|
||||
|
||||
tk.RegisterPostBuild(
|
||||
() =>
|
||||
{
|
||||
// Multiplication by scale will be done with global scale, outside of this handling.
|
||||
var scale = tk.GetFontScaleMode(font) == FontScaleMode.UndoGlobalScale ? 1 / tk.Scale : 1;
|
||||
var roundUnit = tk.GetFontScaleMode(font) == FontScaleMode.SkipHandling ? 1 : 1 / tk.Scale;
|
||||
var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit;
|
||||
var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit;
|
||||
var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit;
|
||||
|
||||
font.Ascent = newAscent;
|
||||
font.FontSize = newFontSize;
|
||||
font.Descent = newFontSize - font.Ascent;
|
||||
|
||||
var lookup = new BitArray(ushort.MaxValue + 1, this.GlyphRanges is null);
|
||||
if (this.GlyphRanges is not null)
|
||||
{
|
||||
for (var i = 0; i < this.GlyphRanges.Length && this.GlyphRanges[i] != 0; i += 2)
|
||||
{
|
||||
var to = (int)this.GlyphRanges[i + 1];
|
||||
for (var j = this.GlyphRanges[i]; j <= to; j++)
|
||||
lookup[j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
var dax = MathF.Round((this.LetterSpacing * scale) / roundUnit) * roundUnit;
|
||||
var dxy0 = this.GlyphOffset * scale;
|
||||
dxy0 /= roundUnit;
|
||||
dxy0 = new(MathF.Round(dxy0.X), MathF.Round(dxy0.Y));
|
||||
dxy0 *= roundUnit;
|
||||
|
||||
dxy0.Y += shiftDown;
|
||||
var dxy = new Vector4(dxy0, dxy0.X, dxy0.Y);
|
||||
foreach (ref var glyphReal in font.GlyphsWrapped().DataSpan)
|
||||
{
|
||||
if (!lookup[glyphReal.Codepoint])
|
||||
continue;
|
||||
|
||||
glyphReal.XY += dxy;
|
||||
glyphReal.AdvanceX += dax;
|
||||
}
|
||||
});
|
||||
|
||||
return font;
|
||||
}
|
||||
}
|
||||
181
Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
Normal file
181
Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font from system.
|
||||
/// </summary>
|
||||
public sealed class SystemFontFamilyId : IFontFamilyId
|
||||
{
|
||||
[JsonIgnore]
|
||||
private IReadOnlyList<IFontId>? fontsLazy;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="englishName">The font name in English.</param>
|
||||
/// <param name="localeNames">The localized font name for display purposes.</param>
|
||||
[JsonConstructor]
|
||||
internal SystemFontFamilyId(string englishName, IReadOnlyDictionary<string, string> localeNames)
|
||||
{
|
||||
this.EnglishName = englishName;
|
||||
this.LocaleNames = localeNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemFontFamilyId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localeNames">The localized font name for display purposes.</param>
|
||||
internal SystemFontFamilyId(IReadOnlyDictionary<string, string> localeNames)
|
||||
{
|
||||
if (localeNames.TryGetValue("en-us", out var name))
|
||||
this.EnglishName = name;
|
||||
else if (localeNames.TryGetValue("en", out name))
|
||||
this.EnglishName = name;
|
||||
else
|
||||
this.EnglishName = localeNames.Values.First();
|
||||
this.LocaleNames = localeNames;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public string EnglishName { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<IFontId> Fonts => this.fontsLazy ??= this.GetFonts();
|
||||
|
||||
public static bool operator ==(SystemFontFamilyId? left, SystemFontFamilyId? right) => Equals(left, right);
|
||||
|
||||
public static bool operator !=(SystemFontFamilyId? left, SystemFontFamilyId? right) => !Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int FindBestMatch(int weight, int stretch, int style)
|
||||
{
|
||||
using var matchingFont = default(ComPtr<IDWriteFont>);
|
||||
|
||||
var candidates = this.Fonts.ToList();
|
||||
var minGap = int.MaxValue;
|
||||
foreach (var c in candidates)
|
||||
minGap = Math.Min(minGap, Math.Abs(c.Weight - weight));
|
||||
candidates.RemoveAll(c => Math.Abs(c.Weight - weight) != minGap);
|
||||
|
||||
minGap = int.MaxValue;
|
||||
foreach (var c in candidates)
|
||||
minGap = Math.Min(minGap, Math.Abs(c.Stretch - stretch));
|
||||
candidates.RemoveAll(c => Math.Abs(c.Stretch - stretch) != minGap);
|
||||
|
||||
if (candidates.Any(x => x.Style == style))
|
||||
candidates.RemoveAll(x => x.Style != style);
|
||||
else if (candidates.Any(x => x.Style == (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL))
|
||||
candidates.RemoveAll(x => x.Style != (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL);
|
||||
|
||||
if (!candidates.Any())
|
||||
return 0;
|
||||
|
||||
for (var i = 0; i < this.Fonts.Count; i++)
|
||||
{
|
||||
if (Equals(this.Fonts[i], candidates[0]))
|
||||
return i;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.EnglishName.GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of <see cref="SystemFontFamilyId"/> from an <see cref="IDWriteFontFamily"/>.
|
||||
/// </summary>
|
||||
/// <param name="family">The family.</param>
|
||||
/// <returns>The new instance.</returns>
|
||||
internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr<IDWriteFontFamily> family)
|
||||
{
|
||||
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
|
||||
family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError();
|
||||
return new(IObjectWithLocalizableName.GetLocaleNames(fn));
|
||||
}
|
||||
|
||||
private unsafe IReadOnlyList<IFontId> GetFonts()
|
||||
{
|
||||
using var dwf = default(ComPtr<IDWriteFactory>);
|
||||
fixed (Guid* piid = &IID.IID_IDWriteFactory)
|
||||
{
|
||||
DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
|
||||
piid,
|
||||
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
|
||||
}
|
||||
|
||||
using var sfc = default(ComPtr<IDWriteFontCollection>);
|
||||
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
|
||||
|
||||
var familyIndex = 0u;
|
||||
BOOL exists = false;
|
||||
fixed (void* pName = this.EnglishName)
|
||||
sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
|
||||
if (!exists)
|
||||
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");
|
||||
|
||||
using var family = default(ComPtr<IDWriteFontFamily>);
|
||||
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
|
||||
|
||||
var fontCount = (int)family.Get()->GetFontCount();
|
||||
var fonts = new List<IFontId>(fontCount);
|
||||
for (var i = 0; i < fontCount; i++)
|
||||
{
|
||||
using var font = default(ComPtr<IDWriteFont>);
|
||||
if (family.Get()->GetFont((uint)i, font.GetAddressOf()).FAILED)
|
||||
{
|
||||
// Ignore errors, if any
|
||||
continue;
|
||||
}
|
||||
|
||||
if (font.Get()->GetSimulations() != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE)
|
||||
{
|
||||
// No simulation support
|
||||
continue;
|
||||
}
|
||||
|
||||
fonts.Add(new SystemFontId(this, font));
|
||||
}
|
||||
|
||||
fonts.Sort(
|
||||
(a, b) =>
|
||||
{
|
||||
var comp = a.Weight.CompareTo(b.Weight);
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
|
||||
comp = a.Stretch.CompareTo(b.Stretch);
|
||||
if (comp != 0)
|
||||
return comp;
|
||||
|
||||
return a.Style.CompareTo(b.Style);
|
||||
});
|
||||
return fonts;
|
||||
}
|
||||
|
||||
private bool Equals(SystemFontFamilyId other) => this.EnglishName == other.EnglishName;
|
||||
}
|
||||
163
Dalamud/Interface/FontIdentifier/SystemFontId.cs
Normal file
163
Dalamud/Interface/FontIdentifier/SystemFontId.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.FontIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a font installed in the system.
|
||||
/// </summary>
|
||||
public sealed class SystemFontId : IFontId
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemFontId"/> class.
|
||||
/// </summary>
|
||||
/// <param name="family">The parent font family.</param>
|
||||
/// <param name="font">The font.</param>
|
||||
internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr<IDWriteFont> font)
|
||||
{
|
||||
this.Family = family;
|
||||
this.Weight = (int)font.Get()->GetWeight();
|
||||
this.Stretch = (int)font.Get()->GetStretch();
|
||||
this.Style = (int)font.Get()->GetStyle();
|
||||
|
||||
using var fn = default(ComPtr<IDWriteLocalizedStrings>);
|
||||
font.Get()->GetFaceNames(fn.GetAddressOf()).ThrowOnError();
|
||||
this.LocaleNames = IObjectWithLocalizableName.GetLocaleNames(fn);
|
||||
if (this.LocaleNames.TryGetValue("en-us", out var name))
|
||||
this.EnglishName = name;
|
||||
else if (this.LocaleNames.TryGetValue("en", out name))
|
||||
this.EnglishName = name;
|
||||
else
|
||||
this.EnglishName = this.LocaleNames.Values.First();
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
private SystemFontId(string englishName, IReadOnlyDictionary<string, string> localeNames, IFontFamilyId family)
|
||||
{
|
||||
this.EnglishName = englishName;
|
||||
this.LocaleNames = localeNames;
|
||||
this.Family = family;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public string EnglishName { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public IReadOnlyDictionary<string, string>? LocaleNames { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public IFontFamilyId Family { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonProperty]
|
||||
public int Style { get; init; } = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL;
|
||||
|
||||
public static bool operator ==(SystemFontId? left, SystemFontId? right) => Equals(left, right);
|
||||
|
||||
public static bool operator !=(SystemFontId? left, SystemFontId? right) => !Equals(left, right);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) =>
|
||||
ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() =>
|
||||
$"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config)
|
||||
{
|
||||
var (path, index) = this.GetFileAndIndex();
|
||||
return tk.AddFontFromFile(path, config with { FontNo = index });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file containing this font, and the font index within.
|
||||
/// </summary>
|
||||
/// <returns>The path and index.</returns>
|
||||
public unsafe (string Path, int Index) GetFileAndIndex()
|
||||
{
|
||||
using var dwf = default(ComPtr<IDWriteFactory>);
|
||||
fixed (Guid* piid = &IID.IID_IDWriteFactory)
|
||||
{
|
||||
DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
|
||||
piid,
|
||||
(IUnknown**)dwf.GetAddressOf()).ThrowOnError();
|
||||
}
|
||||
|
||||
using var sfc = default(ComPtr<IDWriteFontCollection>);
|
||||
dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError();
|
||||
|
||||
var familyIndex = 0u;
|
||||
BOOL exists = false;
|
||||
fixed (void* name = this.Family.EnglishName)
|
||||
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
|
||||
if (!exists)
|
||||
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
|
||||
|
||||
using var family = default(ComPtr<IDWriteFontFamily>);
|
||||
sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError();
|
||||
|
||||
using var font = default(ComPtr<IDWriteFont>);
|
||||
family.Get()->GetFirstMatchingFont(
|
||||
(DWRITE_FONT_WEIGHT)this.Weight,
|
||||
(DWRITE_FONT_STRETCH)this.Stretch,
|
||||
(DWRITE_FONT_STYLE)this.Style,
|
||||
font.GetAddressOf()).ThrowOnError();
|
||||
|
||||
using var fface = default(ComPtr<IDWriteFontFace>);
|
||||
font.Get()->CreateFontFace(fface.GetAddressOf()).ThrowOnError();
|
||||
var fileCount = 0;
|
||||
fface.Get()->GetFiles((uint*)&fileCount, null).ThrowOnError();
|
||||
if (fileCount != 1)
|
||||
throw new NotSupportedException();
|
||||
|
||||
using var ffile = default(ComPtr<IDWriteFontFile>);
|
||||
fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError();
|
||||
void* refKey;
|
||||
var refKeySize = 0u;
|
||||
ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError();
|
||||
|
||||
using var floader = default(ComPtr<IDWriteFontFileLoader>);
|
||||
ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError();
|
||||
|
||||
using var flocal = default(ComPtr<IDWriteLocalFontFileLoader>);
|
||||
floader.As(&flocal).ThrowOnError();
|
||||
|
||||
var pathSize = 0u;
|
||||
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
|
||||
|
||||
var path = stackalloc char[(int)pathSize + 1];
|
||||
flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError();
|
||||
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
|
||||
}
|
||||
|
||||
private bool Equals(SystemFontId other) => this.Family.Equals(other.Family) && this.Weight == other.Weight &&
|
||||
this.Stretch == other.Stretch && this.Style == other.Style;
|
||||
}
|
||||
1115
Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
Normal file
1115
Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -141,6 +141,13 @@ internal class DalamudCommands : IServiceType
|
|||
"Toggle Dalamud UI display modes. Native UI modifications may also be affected by this, but that depends on the plugin."),
|
||||
});
|
||||
|
||||
commandManager.AddHandler("/xlprofiler", new CommandInfo(this.OnOpenProfilerCommand)
|
||||
{
|
||||
HelpMessage = Loc.Localize(
|
||||
"DalamudProfilerHelp",
|
||||
"Open Dalamud's startup timing profiler."),
|
||||
});
|
||||
|
||||
commandManager.AddHandler("/imdebug", new CommandInfo(this.OnDebugImInfoCommand)
|
||||
{
|
||||
HelpMessage = "ImGui DEBUG",
|
||||
|
|
@ -409,4 +416,9 @@ internal class DalamudCommands : IServiceType
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOpenProfilerCommand(string command, string arguments)
|
||||
{
|
||||
Service<DalamudInterface>.Get().ToggleProfilerWindow();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using System.Text.Unicode;
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Hooking.WndProcHook;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
|
|
@ -28,7 +29,6 @@ namespace Dalamud.Interface.Internal;
|
|||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed unsafe class DalamudIme : IDisposable, IServiceType
|
||||
{
|
||||
private const int ImGuiContextTextStateOffset = 0x4588;
|
||||
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
|
||||
private const int CImGuiStbTextUndoOffset = 0xB59C0;
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
|
|||
internal char InputModeIcon { get; private set; }
|
||||
|
||||
private static ImGuiInputTextState* TextState =>
|
||||
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextTextStateOffset);
|
||||
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Conditions;
|
|||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Internal;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Animation.EasingFunctions;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
|
@ -89,7 +90,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
private bool isImPlotDrawDemoWindow = false;
|
||||
private bool isImGuiTestWindowsInMonospace = false;
|
||||
private bool isImGuiDrawMetricsWindow = false;
|
||||
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private DalamudInterface(
|
||||
Dalamud dalamud,
|
||||
|
|
@ -188,7 +189,9 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
|
||||
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
|
||||
}
|
||||
|
||||
|
||||
private delegate nint CrashDebugDelegate(nint self);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of frames since Dalamud has loaded.
|
||||
/// </summary>
|
||||
|
|
@ -744,28 +747,48 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.MenuItem("Access Violation"))
|
||||
|
||||
if (ImGui.BeginMenu("Crash game"))
|
||||
{
|
||||
Marshal.ReadByte(IntPtr.Zero);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Crash game (nullptr)"))
|
||||
{
|
||||
unsafe
|
||||
if (ImGui.MenuItem("Access Violation"))
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Crash game (non-nullptr)"))
|
||||
{
|
||||
unsafe
|
||||
Marshal.ReadByte(IntPtr.Zero);
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Set UiModule to NULL"))
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0x12345678;
|
||||
unsafe
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Set UiModule to invalid ptr"))
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
framework->UIModule = (UIModule*)0x12345678;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Deref nullptr in Hook"))
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var hook = Hook<CrashDebugDelegate>.FromAddress(
|
||||
(nint)UIModule.StaticVTable.GetUIInputData,
|
||||
self =>
|
||||
{
|
||||
_ = *(byte*)0;
|
||||
return (nint)UIModule.Instance()->GetUIInputData();
|
||||
});
|
||||
hook.Enable();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes))
|
||||
|
|
|
|||
|
|
@ -1,41 +1,14 @@
|
|||
using System.Numerics;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiScene;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
|
||||
/// Used to avoid referencing ImGuiScene.
|
||||
/// </summary>
|
||||
public interface IDalamudTextureWrap : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a texture handle suitable for direct use with ImGui functions.
|
||||
/// </summary>
|
||||
IntPtr ImGuiHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the texture.
|
||||
/// </summary>
|
||||
int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the texture.
|
||||
/// </summary>
|
||||
int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size vector of the texture using Width, Height.
|
||||
/// </summary>
|
||||
Vector2 Size => new(this.Width, this.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safety harness for ImGuiScene textures that will defer destruction until
|
||||
/// the end of the frame.
|
||||
/// </summary>
|
||||
public class DalamudTextureWrap : IDalamudTextureWrap
|
||||
public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable
|
||||
{
|
||||
private readonly TextureWrap wrappedWrap;
|
||||
|
||||
|
|
@ -83,7 +56,7 @@ public class DalamudTextureWrap : IDalamudTextureWrap
|
|||
/// <summary>
|
||||
/// Actually dispose the wrapped texture.
|
||||
/// </summary>
|
||||
internal void RealDispose()
|
||||
void IDeferredDisposable.RealDispose()
|
||||
{
|
||||
this.wrappedWrap.Dispose();
|
||||
}
|
||||
|
|
|
|||
55
Dalamud/Interface/Internal/IDalamudTextureWrap.cs
Normal file
55
Dalamud/Interface/Internal/IDalamudTextureWrap.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System.Numerics;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Base TextureWrap interface for all Dalamud-owned texture wraps.
|
||||
/// Used to avoid referencing ImGuiScene.
|
||||
/// </summary>
|
||||
public interface IDalamudTextureWrap : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a texture handle suitable for direct use with ImGui functions.
|
||||
/// </summary>
|
||||
IntPtr ImGuiHandle { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the width of the texture.
|
||||
/// </summary>
|
||||
int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the height of the texture.
|
||||
/// </summary>
|
||||
int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size vector of the texture using Width, Height.
|
||||
/// </summary>
|
||||
Vector2 Size => new(this.Width, this.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new reference to the resource being pointed by this instance of <see cref="IDalamudTextureWrap"/>.
|
||||
/// </summary>
|
||||
/// <returns>The new reference to this texture wrap.</returns>
|
||||
/// <remarks>
|
||||
/// On calling this function, a new instance of <see cref="IDalamudTextureWrap"/> will be returned, but with
|
||||
/// the same <see cref="ImGuiHandle"/>. The new instance must be <see cref="IDisposable.Dispose"/>d, as the backing
|
||||
/// resource will stay alive until all the references are released. The old instance may be disposed as needed,
|
||||
/// once this function returns; the new instance will stay alive regardless of whether the old instance has been
|
||||
/// disposed.<br />
|
||||
/// Primary purpose of this function is to share textures across plugin boundaries. When texture wraps get passed
|
||||
/// across plugin boundaries for use for an indeterminate duration, the receiver should call this function to
|
||||
/// obtain a new reference to the texture received, so that it gets its own "copy" of the texture and the caller
|
||||
/// may dispose the texture anytime without any care for the receiver.<br />
|
||||
/// The default implementation will treat <see cref="ImGuiHandle"/> as an <see cref="IUnknown"/>.
|
||||
/// </remarks>
|
||||
unsafe IDalamudTextureWrap CreateWrapSharingLowLevelResource()
|
||||
{
|
||||
// Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView.
|
||||
var handle = (IUnknown*)this.ImGuiHandle;
|
||||
return new UnknownTextureWrap(handle, this.Width, this.Height, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis
|
|||
private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Effectively waiting for ImGui to become available.
|
||||
_ = imws;
|
||||
Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?");
|
||||
|
||||
var io = ImGui.GetIO();
|
||||
|
|
|
|||
133
Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs
Normal file
133
Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Fixes ImDrawList not correctly dealing with the current texture for that draw list not in tune with the global
|
||||
/// state. Currently, ImDrawList::AddPolyLine and ImDrawList::AddRectFilled are affected.
|
||||
///
|
||||
/// * The implementation for AddRectFilled is entirely replaced with the hook below.
|
||||
/// * The implementation for AddPolyLine is wrapped with Push/PopTextureID.
|
||||
///
|
||||
/// TODO:
|
||||
/// * imgui_draw.cpp:1433 ImDrawList::AddRectFilled
|
||||
/// The if block needs a PushTextureID(_Data->TexIdCommon)/PopTextureID() block,
|
||||
/// if _Data->TexIdCommon != _CmdHeader.TextureId.
|
||||
/// * imgui_draw.cpp:729 ImDrawList::AddPolyLine
|
||||
/// The if block always needs to call PushTextureID if the abovementioned condition is not met.
|
||||
/// Change push_texture_id to only have one condition.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable
|
||||
{
|
||||
private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0;
|
||||
private const int CImGuiImDrawListAddRectFilled = 0x59FD0;
|
||||
private const int CImGuiImDrawListSharedDataTexIdCommonOffset = 0;
|
||||
|
||||
private readonly Hook<ImDrawListAddPolyLine> hookImDrawListAddPolyline;
|
||||
private readonly Hook<ImDrawListAddRectFilled> hookImDrawListAddRectFilled;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Force cimgui.dll to be loaded.
|
||||
_ = ImGui.GetCurrentContext();
|
||||
var cimgui = Process.GetCurrentProcess().Modules.Cast<ProcessModule>()
|
||||
.First(x => x.ModuleName == "cimgui.dll")
|
||||
.BaseAddress;
|
||||
|
||||
this.hookImDrawListAddPolyline = Hook<ImDrawListAddPolyLine>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddPolyLineOffset,
|
||||
this.ImDrawListAddPolylineDetour);
|
||||
this.hookImDrawListAddRectFilled = Hook<ImDrawListAddRectFilled>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddRectFilled,
|
||||
this.ImDrawListAddRectFilledDetour);
|
||||
this.hookImDrawListAddPolyline.Enable();
|
||||
this.hookImDrawListAddRectFilled.Enable();
|
||||
}
|
||||
|
||||
private delegate void ImDrawListAddPolyLine(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 points,
|
||||
int pointsCount,
|
||||
uint color,
|
||||
ImDrawFlags flags,
|
||||
float thickness);
|
||||
|
||||
private delegate void ImDrawListAddRectFilled(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 min,
|
||||
ref Vector2 max,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.hookImDrawListAddPolyline.Dispose();
|
||||
this.hookImDrawListAddRectFilled.Dispose();
|
||||
}
|
||||
|
||||
private void ImDrawListAddRectFilledDetour(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 min,
|
||||
ref Vector2 max,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags)
|
||||
{
|
||||
// Skip drawing if we're drawing something with alpha value of 0.
|
||||
if ((col & 0xFF000000) == 0)
|
||||
return;
|
||||
|
||||
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask)
|
||||
{
|
||||
// Take the fast path of drawing two triangles if no rounded corners are required.
|
||||
|
||||
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
|
||||
if (pushTextureId)
|
||||
drawListPtr.PushTextureID(texIdCommon);
|
||||
|
||||
drawListPtr.PrimReserve(6, 4);
|
||||
drawListPtr.PrimRect(min, max, col);
|
||||
|
||||
if (pushTextureId)
|
||||
drawListPtr.PopTextureID();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Defer drawing rectangle with rounded corners to path drawing operations.
|
||||
// Note that this may have a slightly different extent behaviors from the above if case.
|
||||
// This is how it is in imgui_draw.cpp.
|
||||
drawListPtr.PathRect(min, max, rounding, flags);
|
||||
drawListPtr.PathFillConvex(col);
|
||||
}
|
||||
}
|
||||
|
||||
private void ImDrawListAddPolylineDetour(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 points,
|
||||
int pointsCount,
|
||||
uint color,
|
||||
ImDrawFlags flags,
|
||||
float thickness)
|
||||
{
|
||||
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
|
||||
if (pushTextureId)
|
||||
drawListPtr.PushTextureID(texIdCommon);
|
||||
|
||||
this.hookImDrawListAddPolyline.Original(drawListPtr, ref points, pointsCount, color, flags, thickness);
|
||||
|
||||
if (pushTextureId)
|
||||
drawListPtr.PopTextureID();
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
/// </summary>
|
||||
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
|
||||
|
||||
private readonly ConcurrentBag<DalamudTextureWrap> deferredDisposeTextures = new();
|
||||
private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new();
|
||||
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
|
|
@ -402,7 +402,7 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
/// Enqueue a texture to be disposed at the end of the frame.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The texture.</param>
|
||||
public void EnqueueDeferredDispose(DalamudTextureWrap wrap)
|
||||
public void EnqueueDeferredDispose(IDeferredDisposable wrap)
|
||||
{
|
||||
this.deferredDisposeTextures.Add(wrap);
|
||||
}
|
||||
|
|
@ -705,13 +705,13 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
using (this.dalamudAtlas.SuppressAutoRebuild())
|
||||
{
|
||||
this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx)));
|
||||
e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(-1)));
|
||||
this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(
|
||||
tk => tk.AddFontAwesomeIconFont(
|
||||
new()
|
||||
{
|
||||
SizePx = DefaultFontSizePx,
|
||||
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
|
||||
GlyphMinAdvanceX = DefaultFontSizePx,
|
||||
GlyphMaxAdvanceX = DefaultFontSizePx,
|
||||
})));
|
||||
|
|
@ -719,7 +719,10 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
e => e.OnPreBuild(
|
||||
tk => tk.AddDalamudAssetFont(
|
||||
DalamudAsset.InconsolataRegular,
|
||||
new() { SizePx = DefaultFontSizePx })));
|
||||
new()
|
||||
{
|
||||
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
|
||||
})));
|
||||
this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild(
|
||||
tk =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ internal static class ImGuiContextOffsets
|
|||
public const int FontStackOffset = 0x7A4;
|
||||
|
||||
public const int BeginPopupStackOffset = 0x7B8;
|
||||
|
||||
public const int TextStateOffset = 0x4588;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using ImGuiNET;
|
||||
|
|
@ -82,7 +83,7 @@ internal unsafe class UiDebug
|
|||
private void DrawUnitBase(AtkUnitBase* atkUnitBase)
|
||||
{
|
||||
var isVisible = (atkUnitBase->Flags & 0x20) == 0x20;
|
||||
var addonName = Marshal.PtrToStringAnsi(new IntPtr(atkUnitBase->Name));
|
||||
var addonName = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(atkUnitBase->Name));
|
||||
var agent = Service<GameGui>.Get().FindAgentInterface(atkUnitBase);
|
||||
|
||||
ImGui.Text($"{addonName}");
|
||||
|
|
@ -204,7 +205,7 @@ internal unsafe class UiDebug
|
|||
{
|
||||
case NodeType.Text:
|
||||
var textNode = (AtkTextNode*)node;
|
||||
ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(textNode->NodeText.StringPtr))}");
|
||||
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)textNode->NodeText.StringPtr)}");
|
||||
|
||||
ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize);
|
||||
|
||||
|
|
@ -231,7 +232,7 @@ internal unsafe class UiDebug
|
|||
break;
|
||||
case NodeType.Counter:
|
||||
var counterNode = (AtkCounterNode*)node;
|
||||
ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(counterNode->NodeText.StringPtr))}");
|
||||
ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)counterNode->NodeText.StringPtr)}");
|
||||
break;
|
||||
case NodeType.Image:
|
||||
var imageNode = (AtkImageNode*)node;
|
||||
|
|
@ -250,8 +251,8 @@ internal unsafe class UiDebug
|
|||
{
|
||||
var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
|
||||
var texString = texFileNameStdString->Length < 16
|
||||
? Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->Buffer)
|
||||
: Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->BufferPtr);
|
||||
? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
|
||||
: MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
|
||||
|
||||
ImGui.Text($"texture path: {texString}");
|
||||
var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
|
||||
|
|
@ -352,13 +353,13 @@ internal unsafe class UiDebug
|
|||
{
|
||||
case ComponentType.TextInput:
|
||||
var textInputComponent = (AtkComponentTextInput*)compNode->Component;
|
||||
ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText1.StringPtr))}");
|
||||
ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text3: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText3.StringPtr))}");
|
||||
ImGui.Text($"Text4: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText4.StringPtr))}");
|
||||
ImGui.Text($"Text5: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText5.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}");
|
||||
ImGui.Text($"InputBase Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText1.StringPtr))}");
|
||||
ImGui.Text($"Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText2.StringPtr))}");
|
||||
ImGui.Text($"Text3: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText3.StringPtr))}");
|
||||
ImGui.Text($"Text4: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText4.StringPtr))}");
|
||||
ImGui.Text($"Text5: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText5.StringPtr))}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -474,7 +475,7 @@ internal unsafe class UiDebug
|
|||
foundSelected = true;
|
||||
}
|
||||
|
||||
var name = Marshal.PtrToStringAnsi(new IntPtr(unitBase->Name));
|
||||
var name = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(unitBase->Name));
|
||||
if (searching)
|
||||
{
|
||||
if (name == null || !name.ToLower().Contains(searchStr.ToLower())) continue;
|
||||
|
|
|
|||
77
Dalamud/Interface/Internal/UnknownTextureWrap.cs
Normal file
77
Dalamud/Interface/Internal/UnknownTextureWrap.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System.Threading;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// A texture wrap that is created by cloning the underlying <see cref="IDalamudTextureWrap.ImGuiHandle"/>.
|
||||
/// </summary>
|
||||
internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable
|
||||
{
|
||||
private IntPtr imGuiHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UnknownTextureWrap"/> class.
|
||||
/// </summary>
|
||||
/// <param name="unknown">The pointer to <see cref="IUnknown"/> that is suitable for use with
|
||||
/// <see cref="IDalamudTextureWrap.ImGuiHandle"/>.</param>
|
||||
/// <param name="width">The width of the texture.</param>
|
||||
/// <param name="height">The height of the texture.</param>
|
||||
/// <param name="callAddRef">If <c>true</c>, call <see cref="IUnknown.AddRef"/>.</param>
|
||||
public UnknownTextureWrap(IUnknown* unknown, int width, int height, bool callAddRef)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(unknown is null, typeof(IUnknown));
|
||||
this.imGuiHandle = (nint)unknown;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
if (callAddRef)
|
||||
unknown->AddRef();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="UnknownTextureWrap"/> class.
|
||||
/// </summary>
|
||||
~UnknownTextureWrap() => this.Dispose(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public nint ImGuiHandle =>
|
||||
this.imGuiHandle == nint.Zero
|
||||
? throw new ObjectDisposedException(nameof(UnknownTextureWrap))
|
||||
: this.imGuiHandle;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Width { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queue the texture to be disposed once the frame ends.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actually dispose the wrapped texture.
|
||||
/// </summary>
|
||||
void IDeferredDisposable.RealDispose()
|
||||
{
|
||||
var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero);
|
||||
if (handle != nint.Zero)
|
||||
((IUnknown*)handle)->Release();
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
Service<InterfaceManager>.GetNullable()?.EnqueueDeferredDispose(this);
|
||||
else
|
||||
((IDeferredDisposable)this).RealDispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Numerics;
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.Command;
|
||||
|
|
@ -28,7 +29,11 @@ namespace Dalamud.Interface.Internal.Windows;
|
|||
/// </summary>
|
||||
internal class ConsoleWindow : Window, IDisposable
|
||||
{
|
||||
private readonly List<LogEntry> logText = new();
|
||||
private const int LogLinesMinimum = 100;
|
||||
private const int LogLinesMaximum = 1000000;
|
||||
|
||||
private readonly RollingList<LogEntry> logText;
|
||||
private volatile int newRolledLines;
|
||||
private readonly object renderLock = new();
|
||||
|
||||
private readonly List<string> history = new();
|
||||
|
|
@ -42,12 +47,14 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
private string pluginFilter = string.Empty;
|
||||
|
||||
private bool filterShowUncaughtExceptions;
|
||||
private bool settingsPopupWasOpen;
|
||||
private bool showFilterToolbar;
|
||||
private bool clearLog;
|
||||
private bool copyLog;
|
||||
private bool copyMode;
|
||||
private bool killGameArmed;
|
||||
private bool autoScroll;
|
||||
private int logLinesLimit;
|
||||
private bool autoOpen;
|
||||
private bool regexError;
|
||||
|
||||
|
|
@ -71,13 +78,20 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(600.0f, 200.0f),
|
||||
MaximumSize = new Vector2(9999.0f, 9999.0f),
|
||||
};
|
||||
|
||||
this.RespectCloseHotkey = false;
|
||||
|
||||
this.logLinesLimit = configuration.LogLinesLimit;
|
||||
|
||||
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
this.logText = new(limit);
|
||||
this.FilteredLogEntries = new(limit);
|
||||
|
||||
configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved;
|
||||
}
|
||||
|
||||
private List<LogEntry> FilteredLogEntries { get; set; } = new();
|
||||
private RollingList<LogEntry> FilteredLogEntries { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnOpen()
|
||||
|
|
@ -92,6 +106,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
|
||||
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -152,8 +167,11 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f);
|
||||
ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString);
|
||||
}
|
||||
|
||||
ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
|
||||
|
||||
var sendButtonSize = ImGui.CalcTextSize("Send") +
|
||||
((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale);
|
||||
var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y;
|
||||
ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar);
|
||||
|
||||
if (this.clearLog) this.Clear();
|
||||
|
||||
|
|
@ -173,9 +191,13 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
var childDrawList = ImGui.GetWindowDrawList();
|
||||
var childSize = ImGui.GetWindowSize();
|
||||
|
||||
var cursorDiv = ImGuiHelpers.GlobalScale * 93;
|
||||
var cursorLogLevel = ImGuiHelpers.GlobalScale * 100;
|
||||
var cursorLogLine = ImGuiHelpers.GlobalScale * 135;
|
||||
var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X;
|
||||
var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X;
|
||||
var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2);
|
||||
var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X;
|
||||
|
||||
var lastLinePosY = 0.0f;
|
||||
var logLineHeight = 0.0f;
|
||||
|
||||
lock (this.renderLock)
|
||||
{
|
||||
|
|
@ -184,7 +206,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
var line = this.FilteredLogEntries[i];
|
||||
var index = Math.Max(i - this.newRolledLines, 0); // Prevents flicker effect. Also workaround to avoid negative indexes.
|
||||
var line = this.FilteredLogEntries[index];
|
||||
|
||||
if (!line.IsMultiline && !this.copyLog)
|
||||
ImGui.Separator();
|
||||
|
|
@ -225,6 +248,10 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
ImGui.SetCursorPosX(cursorLogLine);
|
||||
ImGui.TextUnformatted(line.Line);
|
||||
|
||||
var currentLinePosY = ImGui.GetCursorPosY();
|
||||
logLineHeight = currentLinePosY - lastLinePosY;
|
||||
lastLinePosY = currentLinePosY;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,14 +263,19 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
var newRolledLinesCount = Interlocked.Exchange(ref this.newRolledLines, 0);
|
||||
if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY())
|
||||
{
|
||||
ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * newRolledLinesCount));
|
||||
}
|
||||
|
||||
if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
|
||||
{
|
||||
ImGui.SetScrollHereY(1.0f);
|
||||
}
|
||||
|
||||
// Draw dividing line
|
||||
var offset = ImGuiHelpers.GlobalScale * 127;
|
||||
childDrawList.AddLine(new Vector2(childPos.X + offset, childPos.Y), new Vector2(childPos.X + offset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
|
||||
childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
|
||||
|
||||
ImGui.EndChild();
|
||||
|
||||
|
|
@ -261,7 +293,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale));
|
||||
|
||||
var getFocus = false;
|
||||
unsafe
|
||||
|
|
@ -280,7 +312,7 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
if (hadColor) ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f)))
|
||||
if (ImGui.Button("Send", sendButtonSize))
|
||||
{
|
||||
this.ProcessCommand();
|
||||
}
|
||||
|
|
@ -361,21 +393,21 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
ImGui.SameLine();
|
||||
|
||||
this.autoScroll = configuration.LogAutoScroll;
|
||||
if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll))
|
||||
var settingsPopup = ImGui.BeginPopup("##console_settings");
|
||||
if (settingsPopup)
|
||||
{
|
||||
configuration.LogAutoScroll = !configuration.LogAutoScroll;
|
||||
configuration.QueueSave();
|
||||
this.DrawSettingsPopup(configuration);
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
else if (this.settingsPopupWasOpen)
|
||||
{
|
||||
// Prevent side effects in case Apply wasn't clicked
|
||||
this.logLinesLimit = configuration.LogLinesLimit;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
this.settingsPopupWasOpen = settingsPopup;
|
||||
|
||||
this.autoOpen = configuration.LogOpenAtStartup;
|
||||
if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen))
|
||||
{
|
||||
configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) ImGui.OpenPopup("##console_settings");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
|
|
@ -445,6 +477,33 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawSettingsPopup(DalamudConfiguration configuration)
|
||||
{
|
||||
if (ImGui.Checkbox("Open at startup", ref this.autoOpen))
|
||||
{
|
||||
configuration.LogOpenAtStartup = this.autoOpen;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
|
||||
{
|
||||
configuration.LogAutoScroll = this.autoScroll;
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted("Logs buffer");
|
||||
ImGui.SliderInt("lines", ref this.logLinesLimit, LogLinesMinimum, LogLinesMaximum);
|
||||
if (ImGui.Button("Apply"))
|
||||
{
|
||||
this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
|
||||
configuration.LogLinesLimit = this.logLinesLimit;
|
||||
configuration.QueueSave();
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFilterToolbar()
|
||||
{
|
||||
if (!this.showFilterToolbar) return;
|
||||
|
|
@ -684,8 +743,12 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
|
||||
this.logText.Add(entry);
|
||||
|
||||
var avoidScroll = this.FilteredLogEntries.Count == this.FilteredLogEntries.Size;
|
||||
if (this.IsFilterApplicable(entry))
|
||||
{
|
||||
this.FilteredLogEntries.Add(entry);
|
||||
if (avoidScroll) Interlocked.Increment(ref this.newRolledLines);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsFilterApplicable(LogEntry entry)
|
||||
|
|
@ -729,8 +792,6 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
this.regexError = false;
|
||||
|
||||
// else we couldn't find a filter for this entry, if we have any filters, we need to block this entry.
|
||||
return !this.pluginFilters.Any();
|
||||
}
|
||||
|
|
@ -739,7 +800,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
{
|
||||
lock (this.renderLock)
|
||||
{
|
||||
this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList();
|
||||
this.regexError = false;
|
||||
this.FilteredLogEntries = new RollingList<LogEntry>(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -788,6 +850,14 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
return result;
|
||||
}
|
||||
|
||||
private void OnDalamudConfigurationSaved(DalamudConfiguration dalamudConfiguration)
|
||||
{
|
||||
this.logLinesLimit = dalamudConfiguration.LogLinesLimit;
|
||||
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||
this.logText.Size = limit;
|
||||
this.FilteredLogEntries.Size = limit;
|
||||
}
|
||||
|
||||
private class LogEntry
|
||||
{
|
||||
public string Line { get; init; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ using System.Numerics;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ImGuiFontChooserDialog;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
|
@ -22,10 +25,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
/// </summary>
|
||||
internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
||||
{
|
||||
private static readonly string[] FontScaleModes =
|
||||
{
|
||||
nameof(FontScaleMode.Default),
|
||||
nameof(FontScaleMode.SkipHandling),
|
||||
nameof(FontScaleMode.UndoGlobalScale),
|
||||
};
|
||||
|
||||
private ImVectorWrapper<byte> testStringBuffer;
|
||||
private IFontAtlas? privateAtlas;
|
||||
private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance };
|
||||
private IFontHandle? fontDialogHandle;
|
||||
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> Handle)[]>? fontHandles;
|
||||
private bool useGlobalScale;
|
||||
private bool atlasScaleMode = true;
|
||||
private int fontScaleMode = (int)FontScaleMode.UndoGlobalScale;
|
||||
private bool useWordWrap;
|
||||
private bool useItalic;
|
||||
private bool useBold;
|
||||
|
|
@ -47,12 +60,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
public unsafe void Draw()
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
fixed (byte* labelPtr = "Global Scale"u8)
|
||||
if (ImGui.Combo("Global Scale per Font", ref this.fontScaleMode, FontScaleModes, FontScaleModes.Length))
|
||||
this.ClearAtlas();
|
||||
fixed (byte* labelPtr = "Global Scale for Atlas"u8)
|
||||
{
|
||||
var v = (byte)(this.useGlobalScale ? 1 : 0);
|
||||
var v = (byte)(this.atlasScaleMode ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
{
|
||||
this.useGlobalScale = v != 0;
|
||||
this.atlasScaleMode = v != 0;
|
||||
this.ClearAtlas();
|
||||
}
|
||||
}
|
||||
|
|
@ -111,36 +126,76 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
if (ImGui.Button("Test Lock"))
|
||||
Task.Run(this.TestLock);
|
||||
|
||||
fixed (byte* labelPtr = "Test Input"u8)
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Choose Editor Font"))
|
||||
{
|
||||
if (ImGuiNative.igInputTextMultiline(
|
||||
labelPtr,
|
||||
this.testStringBuffer.Data,
|
||||
(uint)this.testStringBuffer.Capacity,
|
||||
new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale),
|
||||
0,
|
||||
null,
|
||||
null) != 0)
|
||||
{
|
||||
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
|
||||
if (len + 4 >= this.testStringBuffer.Capacity)
|
||||
this.testStringBuffer.EnsureCapacityExponential(len + 4);
|
||||
if (len < this.testStringBuffer.Capacity)
|
||||
{
|
||||
this.testStringBuffer.LengthUnsafe = len;
|
||||
this.testStringBuffer.StorageSpan[len] = default;
|
||||
}
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
Service<FontAtlasFactory>.Get().CreateFontAtlas(
|
||||
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
|
||||
FontAtlasAutoRebuildMode.Async));
|
||||
fcd.SelectedFont = this.fontSpec;
|
||||
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
|
||||
Service<InterfaceManager>.Get().Draw += fcd.Draw;
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
|
||||
fcd.Dispose();
|
||||
|
||||
if (this.useMinimumBuild)
|
||||
_ = this.privateAtlas?.BuildFontsAsync();
|
||||
}
|
||||
_ = r.Exception;
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.fontSpec = r.Result;
|
||||
Log.Information("Selected font: {font}", this.fontSpec);
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
}));
|
||||
}
|
||||
|
||||
this.privateAtlas ??=
|
||||
Service<FontAtlasFactory>.Get().CreateFontAtlas(
|
||||
nameof(GamePrebakedFontsTestWidget),
|
||||
FontAtlasAutoRebuildMode.Async,
|
||||
this.useGlobalScale);
|
||||
this.atlasScaleMode);
|
||||
this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(
|
||||
this.privateAtlas,
|
||||
e => e.OnPreBuild(tk => tk.SetFontScaleMode(tk.Font, (FontScaleMode)this.fontScaleMode)));
|
||||
|
||||
fixed (byte* labelPtr = "Test Input"u8)
|
||||
{
|
||||
if (!this.atlasScaleMode)
|
||||
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
|
||||
using (this.fontDialogHandle.Push())
|
||||
{
|
||||
if (ImGuiNative.igInputTextMultiline(
|
||||
labelPtr,
|
||||
this.testStringBuffer.Data,
|
||||
(uint)this.testStringBuffer.Capacity,
|
||||
new(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 3),
|
||||
0,
|
||||
null,
|
||||
null) != 0)
|
||||
{
|
||||
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
|
||||
if (len + 4 >= this.testStringBuffer.Capacity)
|
||||
this.testStringBuffer.EnsureCapacityExponential(len + 4);
|
||||
if (len < this.testStringBuffer.Capacity)
|
||||
{
|
||||
this.testStringBuffer.LengthUnsafe = len;
|
||||
this.testStringBuffer.StorageSpan[len] = default;
|
||||
}
|
||||
|
||||
if (this.useMinimumBuild)
|
||||
_ = this.privateAtlas?.BuildFontsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.atlasScaleMode)
|
||||
ImGuiNative.igSetWindowFontScale(1);
|
||||
}
|
||||
|
||||
this.fontHandles ??=
|
||||
Enum.GetValues<GameFontFamilyAndSize>()
|
||||
.Where(x => x.GetAttribute<GameFontFamilyAndSizeAttribute>() is not null)
|
||||
|
|
@ -149,17 +204,29 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
.ToImmutableDictionary(
|
||||
x => x.Key,
|
||||
x => x.Select(
|
||||
y => (y, new Lazy<IFontHandle>(
|
||||
() => this.useMinimumBuild
|
||||
? this.privateAtlas.NewDelegateFontHandle(
|
||||
e =>
|
||||
e.OnPreBuild(
|
||||
tk => tk.AddGameGlyphs(
|
||||
y,
|
||||
Encoding.UTF8.GetString(
|
||||
this.testStringBuffer.DataSpan).ToGlyphRange(),
|
||||
default)))
|
||||
: this.privateAtlas.NewGameFontHandle(y))))
|
||||
y =>
|
||||
{
|
||||
var range = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan).ToGlyphRange();
|
||||
|
||||
Lazy<IFontHandle> l;
|
||||
if (this.useMinimumBuild
|
||||
|| (this.atlasScaleMode && this.fontScaleMode != (int)FontScaleMode.Default))
|
||||
{
|
||||
l = new(
|
||||
() => this.privateAtlas!.NewDelegateFontHandle(
|
||||
e =>
|
||||
e.OnPreBuild(
|
||||
tk => tk.SetFontScaleMode(
|
||||
tk.AddGameGlyphs(y, range, default),
|
||||
(FontScaleMode)this.fontScaleMode))));
|
||||
}
|
||||
else
|
||||
{
|
||||
l = new(() => this.privateAtlas!.NewGameFontHandle(y));
|
||||
}
|
||||
|
||||
return (y, l);
|
||||
})
|
||||
.ToArray());
|
||||
|
||||
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
|
||||
|
|
@ -187,7 +254,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
if (!this.useGlobalScale)
|
||||
if (!this.atlasScaleMode)
|
||||
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
|
||||
if (counter++ % 2 == 0)
|
||||
{
|
||||
|
|
@ -208,8 +275,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
ImGuiNative.igPopTextWrapPos();
|
||||
ImGuiNative.igSetWindowFontScale(1);
|
||||
ImGuiNative.igPopTextWrapPos();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,6 +294,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value))
|
||||
.AggregateToDisposable().Dispose();
|
||||
this.fontHandles = null;
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
this.privateAtlas?.Dispose();
|
||||
this.privateAtlas = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,10 @@ internal class TexWidget : IDataWindowWidget
|
|||
|
||||
if (ImGui.Button($"X##{i}"))
|
||||
toRemove = tex;
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button($"Clone##{i}"))
|
||||
this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = this.Size.Value,
|
||||
MaximumSize = new Vector2(5000, 5000),
|
||||
};
|
||||
|
||||
Service<PluginManager>.GetAsync().ContinueWith(pluginManagerTask =>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
/*using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Serilog;*/
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
|
||||
|
||||
|
|
@ -13,31 +20,22 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
|
|||
/// </summary>
|
||||
internal class ContextMenuAgingStep : IAgingStep
|
||||
{
|
||||
/*
|
||||
private SubStep currentSubStep;
|
||||
|
||||
private uint clickedItemId;
|
||||
private bool clickedItemHq;
|
||||
private uint clickedItemCount;
|
||||
private bool? targetInventorySubmenuOpened;
|
||||
private PlayerCharacter? targetCharacter;
|
||||
|
||||
private string? clickedPlayerName;
|
||||
private ushort? clickedPlayerWorld;
|
||||
private ulong? clickedPlayerCid;
|
||||
private uint? clickedPlayerId;
|
||||
|
||||
private bool multipleTriggerOne;
|
||||
private bool multipleTriggerTwo;
|
||||
private ExcelSheet<Item> itemSheet;
|
||||
private ExcelSheet<Materia> materiaSheet;
|
||||
private ExcelSheet<Stain> stainSheet;
|
||||
|
||||
private enum SubStep
|
||||
{
|
||||
Start,
|
||||
TestItem,
|
||||
TestGameObject,
|
||||
TestSubMenu,
|
||||
TestMultiple,
|
||||
TestInventoryAndSubmenu,
|
||||
TestDefault,
|
||||
Finish,
|
||||
}
|
||||
*/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => "Test Context Menu";
|
||||
|
|
@ -45,23 +43,24 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
/// <inheritdoc/>
|
||||
public SelfTestStepResult RunStep()
|
||||
{
|
||||
/*
|
||||
var contextMenu = Service<ContextMenu>.Get();
|
||||
var dataMgr = Service<DataManager>.Get();
|
||||
this.itemSheet = dataMgr.GetExcelSheet<Item>()!;
|
||||
this.materiaSheet = dataMgr.GetExcelSheet<Materia>()!;
|
||||
this.stainSheet = dataMgr.GetExcelSheet<Stain>()!;
|
||||
|
||||
ImGui.Text(this.currentSubStep.ToString());
|
||||
|
||||
switch (this.currentSubStep)
|
||||
{
|
||||
case SubStep.Start:
|
||||
contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
|
||||
contextMenu.OnMenuOpened += this.OnMenuOpened;
|
||||
this.currentSubStep++;
|
||||
break;
|
||||
case SubStep.TestItem:
|
||||
if (this.clickedItemId != 0)
|
||||
case SubStep.TestInventoryAndSubmenu:
|
||||
if (this.targetInventorySubmenuOpened == true)
|
||||
{
|
||||
var item = dataMgr.GetExcelSheet<Item>()!.GetRow(this.clickedItemId);
|
||||
ImGui.Text($"Did you click \"{item!.Name.RawString}\", hq:{this.clickedItemHq}, count:{this.clickedItemCount}?");
|
||||
ImGui.Text($"Is the data in the submenu correct?");
|
||||
|
||||
if (ImGui.Button("Yes"))
|
||||
this.currentSubStep++;
|
||||
|
|
@ -73,7 +72,7 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text("Right-click an item.");
|
||||
ImGui.Text("Right-click an item and select \"Self Test\".");
|
||||
|
||||
if (ImGui.Button("Skip"))
|
||||
this.currentSubStep++;
|
||||
|
|
@ -81,10 +80,10 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
|
||||
break;
|
||||
|
||||
case SubStep.TestGameObject:
|
||||
if (!this.clickedPlayerName.IsNullOrEmpty())
|
||||
case SubStep.TestDefault:
|
||||
if (this.targetCharacter is { } character)
|
||||
{
|
||||
ImGui.Text($"Did you click \"{this.clickedPlayerName}\", world:{this.clickedPlayerWorld}, cid:{this.clickedPlayerCid}, id:{this.clickedPlayerId}?");
|
||||
ImGui.Text($"Did you click \"{character.Name}\" ({character.ClassJob.GameData!.Abbreviation.ToDalamudString()})?");
|
||||
|
||||
if (ImGui.Button("Yes"))
|
||||
this.currentSubStep++;
|
||||
|
|
@ -103,149 +102,195 @@ internal class ContextMenuAgingStep : IAgingStep
|
|||
}
|
||||
|
||||
break;
|
||||
case SubStep.TestSubMenu:
|
||||
if (this.multipleTriggerOne && this.multipleTriggerTwo)
|
||||
{
|
||||
this.currentSubStep++;
|
||||
this.multipleTriggerOne = this.multipleTriggerTwo = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Text("Right-click a character and select both options in the submenu.");
|
||||
case SubStep.Finish:
|
||||
return SelfTestStepResult.Pass;
|
||||
|
||||
if (ImGui.Button("Skip"))
|
||||
this.currentSubStep++;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SubStep.TestMultiple:
|
||||
if (this.multipleTriggerOne && this.multipleTriggerTwo)
|
||||
{
|
||||
this.currentSubStep = SubStep.Finish;
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
ImGui.Text("Select both options on any context menu.");
|
||||
if (ImGui.Button("Skip"))
|
||||
this.currentSubStep++;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return SelfTestStepResult.Waiting;
|
||||
*/
|
||||
|
||||
return SelfTestStepResult.Pass;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CleanUp()
|
||||
{
|
||||
/*
|
||||
var contextMenu = Service<ContextMenu>.Get();
|
||||
contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
|
||||
contextMenu.OnMenuOpened -= this.OnMenuOpened;
|
||||
|
||||
this.currentSubStep = SubStep.Start;
|
||||
this.clickedItemId = 0;
|
||||
this.clickedPlayerName = null;
|
||||
this.multipleTriggerOne = this.multipleTriggerTwo = false;
|
||||
*/
|
||||
this.targetInventorySubmenuOpened = null;
|
||||
this.targetCharacter = null;
|
||||
}
|
||||
|
||||
/*
|
||||
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
|
||||
private void OnMenuOpened(MenuOpenedArgs args)
|
||||
{
|
||||
Log.Information("Got context menu with parent addon: {ParentAddonName}, title:{Title}, itemcnt:{ItemCount}", args.ParentAddonName, args.Title, args.Items.Count);
|
||||
if (args.GameObjectContext != null)
|
||||
{
|
||||
Log.Information(" => GameObject:{GameObjectName} world:{World} cid:{Cid} id:{Id}", args.GameObjectContext.Name, args.GameObjectContext.WorldId, args.GameObjectContext.ContentId, args.GameObjectContext.Id);
|
||||
}
|
||||
|
||||
if (args.InventoryItemContext != null)
|
||||
{
|
||||
Log.Information(" => Inventory:{ItemId} hq:{Hq} count:{Count}", args.InventoryItemContext.Id, args.InventoryItemContext.IsHighQuality, args.InventoryItemContext.Count);
|
||||
}
|
||||
LogMenuOpened(args);
|
||||
|
||||
switch (this.currentSubStep)
|
||||
{
|
||||
case SubStep.TestSubMenu:
|
||||
args.AddCustomSubMenu("Aging Submenu", openedArgs =>
|
||||
case SubStep.TestInventoryAndSubmenu:
|
||||
if (args.MenuType == ContextMenuType.Inventory)
|
||||
{
|
||||
openedArgs.AddCustomItem("Submenu Item 1", _ =>
|
||||
args.AddMenuItem(new()
|
||||
{
|
||||
this.multipleTriggerOne = true;
|
||||
});
|
||||
|
||||
openedArgs.AddCustomItem("Submenu Item 2", _ =>
|
||||
{
|
||||
this.multipleTriggerTwo = true;
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
case SubStep.TestMultiple:
|
||||
args.AddCustomItem("Aging Item 1", _ =>
|
||||
{
|
||||
this.multipleTriggerOne = true;
|
||||
});
|
||||
|
||||
args.AddCustomItem("Aging Item 2", _ =>
|
||||
{
|
||||
this.multipleTriggerTwo = true;
|
||||
});
|
||||
|
||||
return;
|
||||
case SubStep.Finish:
|
||||
return;
|
||||
|
||||
default:
|
||||
switch (args.ParentAddonName)
|
||||
{
|
||||
case "Inventory":
|
||||
if (this.currentSubStep != SubStep.TestItem)
|
||||
return;
|
||||
|
||||
args.AddCustomItem("Aging Item", _ =>
|
||||
Name = "Self Test",
|
||||
Prefix = SeIconChar.Hyadelyn,
|
||||
PrefixColor = 56,
|
||||
Priority = -1,
|
||||
IsSubmenu = true,
|
||||
OnClicked = (MenuItemClickedArgs a) =>
|
||||
{
|
||||
this.clickedItemId = args.InventoryItemContext!.Id;
|
||||
this.clickedItemHq = args.InventoryItemContext!.IsHighQuality;
|
||||
this.clickedItemCount = args.InventoryItemContext!.Count;
|
||||
Log.Warning("Clicked item: {Id} hq:{Hq} count:{Count}", this.clickedItemId, this.clickedItemHq, this.clickedItemCount);
|
||||
});
|
||||
break;
|
||||
SeString name;
|
||||
uint count;
|
||||
var targetItem = (a.Target as MenuTargetInventory).TargetItem;
|
||||
if (targetItem is { } item)
|
||||
{
|
||||
name = (this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
|
||||
count = item.Quantity;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = "None";
|
||||
count = 0;
|
||||
}
|
||||
|
||||
case null:
|
||||
case "_PartyList":
|
||||
case "ChatLog":
|
||||
case "ContactList":
|
||||
case "ContentMemberList":
|
||||
case "CrossWorldLinkshell":
|
||||
case "FreeCompany":
|
||||
case "FriendList":
|
||||
case "LookingForGroup":
|
||||
case "LinkShell":
|
||||
case "PartyMemberList":
|
||||
case "SocialList":
|
||||
if (this.currentSubStep != SubStep.TestGameObject || args.GameObjectContext == null || args.GameObjectContext.Name.IsNullOrEmpty())
|
||||
return;
|
||||
a.OpenSubmenu(new MenuItem[]
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "Name: " + name,
|
||||
IsEnabled = false,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = $"Count: {count}",
|
||||
IsEnabled = false,
|
||||
},
|
||||
});
|
||||
|
||||
args.AddCustomItem("Aging Character", _ =>
|
||||
{
|
||||
this.clickedPlayerName = args.GameObjectContext.Name!;
|
||||
this.clickedPlayerWorld = args.GameObjectContext.WorldId;
|
||||
this.clickedPlayerCid = args.GameObjectContext.ContentId;
|
||||
this.clickedPlayerId = args.GameObjectContext.Id;
|
||||
|
||||
Log.Warning("Clicked player: {Name} world:{World} cid:{Cid} id:{Id}", this.clickedPlayerName, this.clickedPlayerWorld, this.clickedPlayerCid, this.clickedPlayerId);
|
||||
});
|
||||
|
||||
break;
|
||||
this.targetInventorySubmenuOpened = true;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case SubStep.TestDefault:
|
||||
if (args.Target is MenuTargetDefault { TargetObject: PlayerCharacter { } character })
|
||||
this.targetCharacter = character;
|
||||
break;
|
||||
|
||||
case SubStep.Finish:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogMenuOpened(MenuOpenedArgs args)
|
||||
{
|
||||
Log.Verbose($"Got {args.MenuType} context menu with addon 0x{args.AddonPtr:X8} ({args.AddonName}) and agent 0x{args.AgentPtr:X8}");
|
||||
if (args.Target is MenuTargetDefault targetDefault)
|
||||
{
|
||||
{
|
||||
var b = new StringBuilder();
|
||||
b.AppendLine($"Target: {targetDefault.TargetName}");
|
||||
b.AppendLine($"Home World: {targetDefault.TargetHomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({targetDefault.TargetHomeWorld.Id})");
|
||||
b.AppendLine($"Content Id: 0x{targetDefault.TargetContentId:X8}");
|
||||
b.AppendLine($"Object Id: 0x{targetDefault.TargetObjectId:X8}");
|
||||
Log.Verbose(b.ToString());
|
||||
}
|
||||
|
||||
if (targetDefault.TargetCharacter is { } character)
|
||||
{
|
||||
var b = new StringBuilder();
|
||||
b.AppendLine($"Character: {character.Name}");
|
||||
|
||||
b.AppendLine($"Name: {character.Name}");
|
||||
b.AppendLine($"Content Id: 0x{character.ContentId:X8}");
|
||||
b.AppendLine($"FC Tag: {character.FCTag}");
|
||||
|
||||
b.AppendLine($"Job: {character.ClassJob.GameData?.Abbreviation.ToDalamudString() ?? "Unknown"} ({character.ClassJob.Id})");
|
||||
b.AppendLine($"Statuses: {string.Join(", ", character.Statuses.Select(s => s.GameData?.Name.ToDalamudString() ?? s.Id.ToString()))}");
|
||||
b.AppendLine($"Home World: {character.HomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.HomeWorld.Id})");
|
||||
b.AppendLine($"Current World: {character.CurrentWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.CurrentWorld.Id})");
|
||||
b.AppendLine($"Is From Other Server: {character.IsFromOtherServer}");
|
||||
|
||||
b.Append("Location: ");
|
||||
if (character.Location.GameData is { } location)
|
||||
b.Append($"{location.PlaceNameRegion.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceNameZone.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceName.Value?.Name.ToDalamudString() ?? "Unknown"}");
|
||||
else
|
||||
b.Append("Unknown");
|
||||
b.AppendLine($" ({character.Location.Id})");
|
||||
|
||||
b.AppendLine($"Grand Company: {character.GrandCompany.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.GrandCompany.Id})");
|
||||
b.AppendLine($"Client Language: {character.ClientLanguage}");
|
||||
b.AppendLine($"Languages: {string.Join(", ", character.Languages)}");
|
||||
b.AppendLine($"Gender: {character.Gender}");
|
||||
b.AppendLine($"Display Group: {character.DisplayGroup}");
|
||||
b.AppendLine($"Sort: {character.Sort}");
|
||||
|
||||
Log.Verbose(b.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"Character: null");
|
||||
}
|
||||
}
|
||||
else if (args.Target is MenuTargetInventory targetInventory)
|
||||
{
|
||||
if (targetInventory.TargetItem is { } item)
|
||||
{
|
||||
var b = new StringBuilder();
|
||||
b.AppendLine($"Item: {(item.IsEmpty ? "None" : this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString())} ({item.ItemId})");
|
||||
b.AppendLine($"Container: {item.ContainerType}");
|
||||
b.AppendLine($"Slot: {item.InventorySlot}");
|
||||
b.AppendLine($"Quantity: {item.Quantity}");
|
||||
b.AppendLine($"{(item.IsCollectable ? "Collectability" : "Spiritbond")}: {item.Spiritbond}");
|
||||
b.AppendLine($"Condition: {item.Condition / 300f:0.00}% ({item.Condition})");
|
||||
b.AppendLine($"Is HQ: {item.IsHq}");
|
||||
b.AppendLine($"Is Company Crest Applied: {item.IsCompanyCrestApplied}");
|
||||
b.AppendLine($"Is Relic: {item.IsRelic}");
|
||||
b.AppendLine($"Is Collectable: {item.IsCollectable}");
|
||||
|
||||
b.Append("Materia: ");
|
||||
var materias = new List<string>();
|
||||
foreach (var (materiaId, materiaGrade) in item.Materia.ToArray().Zip(item.MateriaGrade.ToArray()).Where(m => m.First != 0))
|
||||
{
|
||||
Log.Verbose($"{materiaId} {materiaGrade}");
|
||||
if (this.materiaSheet.GetRow(materiaId) is { } materia &&
|
||||
materia.Item[materiaGrade].Value is { } materiaItem)
|
||||
materias.Add($"{materiaItem.Name.ToDalamudString()}");
|
||||
else
|
||||
materias.Add($"Unknown (Id: {materiaId}, Grade: {materiaGrade})");
|
||||
}
|
||||
|
||||
if (materias.Count == 0)
|
||||
b.AppendLine("None");
|
||||
else
|
||||
b.AppendLine(string.Join(", ", materias));
|
||||
|
||||
b.Append($"Dye/Stain: ");
|
||||
if (item.Stain != 0)
|
||||
b.AppendLine($"{this.stainSheet.GetRow(item.Stain)?.Name.ToDalamudString() ?? "Unknown"} ({item.Stain})");
|
||||
else
|
||||
b.AppendLine("None");
|
||||
|
||||
b.Append("Glamoured Item: ");
|
||||
if (item.GlamourId != 0)
|
||||
b.AppendLine($"{this.itemSheet.GetRow(item.GlamourId)?.Name.ToDalamudString() ?? "Unknown"} ({item.GlamourId})");
|
||||
else
|
||||
b.AppendLine("None");
|
||||
|
||||
Log.Verbose(b.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose("Item: null");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"Target: Unknown ({args.Target?.GetType().Name ?? "null"})");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,11 +68,11 @@ internal class SettingsWindow : Window
|
|||
var interfaceManager = Service<InterfaceManager>.Get();
|
||||
var fontAtlasFactory = Service<FontAtlasFactory>.Get();
|
||||
|
||||
var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
|
||||
var rebuildFont = !Equals(fontAtlasFactory.DefaultFontSpec, configuration.DefaultFontSpec);
|
||||
rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale);
|
||||
|
||||
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
|
||||
fontAtlasFactory.UseAxisOverride = null;
|
||||
fontAtlasFactory.DefaultFontSpecOverride = null;
|
||||
|
||||
if (rebuildFont)
|
||||
interfaceManager.RebuildFonts();
|
||||
|
|
|
|||
|
|
@ -5,9 +5,14 @@ using System.Text;
|
|||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ImGuiFontChooserDialog;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -21,31 +26,19 @@ public class SettingsTabLook : SettingsTab
|
|||
{
|
||||
private static readonly (string, float)[] GlobalUiScalePresets =
|
||||
{
|
||||
("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt),
|
||||
("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt),
|
||||
("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt),
|
||||
("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt),
|
||||
("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt),
|
||||
("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt),
|
||||
("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f),
|
||||
("100%##DalamudSettingsGlobalUiScaleReset12", 1f),
|
||||
("117%##DalamudSettingsGlobalUiScaleReset14", 14 / 12f),
|
||||
("150%##DalamudSettingsGlobalUiScaleReset18", 1.5f),
|
||||
("200%##DalamudSettingsGlobalUiScaleReset24", 2f),
|
||||
("300%##DalamudSettingsGlobalUiScaleReset36", 3f),
|
||||
};
|
||||
|
||||
private float globalUiScale;
|
||||
private IFontSpec defaultFontSpec = null!;
|
||||
|
||||
public override SettingsEntry[] Entries { get; } =
|
||||
{
|
||||
new GapSettingsEntry(5),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"),
|
||||
Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font."),
|
||||
c => c.UseAxisFontsFromGame,
|
||||
(v, c) => c.UseAxisFontsFromGame = v,
|
||||
v =>
|
||||
{
|
||||
Service<FontAtlasFactory>.Get().UseAxisOverride = v;
|
||||
Service<InterfaceManager>.Get().RebuildFonts();
|
||||
}),
|
||||
|
||||
new GapSettingsEntry(5, true),
|
||||
|
||||
new ButtonSettingsEntry(
|
||||
|
|
@ -178,10 +171,10 @@ public class SettingsTabLook : SettingsTab
|
|||
}
|
||||
}
|
||||
|
||||
var globalUiScaleInPt = 12f * this.globalUiScale;
|
||||
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
|
||||
var globalUiScaleInPct = 100f * this.globalUiScale;
|
||||
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPct, 1f, 80f, 300f, "%.0f%%", ImGuiSliderFlags.AlwaysClamp))
|
||||
{
|
||||
this.globalUiScale = globalUiScaleInPt / 12f;
|
||||
this.globalUiScale = globalUiScaleInPct / 100f;
|
||||
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
|
||||
interfaceManager.RebuildFonts();
|
||||
}
|
||||
|
|
@ -201,12 +194,53 @@ public class SettingsTabLook : SettingsTab
|
|||
}
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
|
||||
{
|
||||
var faf = Service<FontAtlasFactory>.Get();
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
|
||||
fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
|
||||
fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
|
||||
interfaceManager.Draw += fcd.Draw;
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
interfaceManager.Draw -= fcd.Draw;
|
||||
fcd.Dispose();
|
||||
|
||||
_ = r.Exception;
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
faf.DefaultFontSpecOverride = this.defaultFontSpec = r.Result;
|
||||
interfaceManager.RebuildFonts();
|
||||
}));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (interfaceManager.MonoFontHandle?.Push())
|
||||
{
|
||||
if (ImGui.Button(Loc.Localize("DalamudSettingResetDefaultFont", "Reset Default Font")))
|
||||
{
|
||||
var faf = Service<FontAtlasFactory>.Get();
|
||||
faf.DefaultFontSpecOverride =
|
||||
this.defaultFontSpec =
|
||||
new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) };
|
||||
interfaceManager.RebuildFonts();
|
||||
}
|
||||
}
|
||||
|
||||
base.Draw();
|
||||
}
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
this.globalUiScale = Service<DalamudConfiguration>.Get().GlobalUiScale;
|
||||
this.defaultFontSpec = Service<FontAtlasFactory>.Get().DefaultFontSpec;
|
||||
|
||||
base.Load();
|
||||
}
|
||||
|
|
@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab
|
|||
public override void Save()
|
||||
{
|
||||
Service<DalamudConfiguration>.Get().GlobalUiScale = this.globalUiScale;
|
||||
Service<DalamudConfiguration>.Get().DefaultFontSpec = this.defaultFontSpec;
|
||||
|
||||
base.Save();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public class StyleEditorWindow : Window
|
|||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(890, 560),
|
||||
MaximumSize = new Vector2(10000, 10000),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
33
Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs
Normal file
33
Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how should global font scale affect a font.
|
||||
/// </summary>
|
||||
public enum FontScaleMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Do the default handling. Dalamud will load the sufficienty large font that will accomodate the global scale,
|
||||
/// and stretch the loaded glyphs so that they look pixel-perfect after applying global scale on drawing.
|
||||
/// Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
|
||||
/// </summary>
|
||||
Default,
|
||||
|
||||
/// <summary>
|
||||
/// Do nothing with the font. Dalamud will load the font with the size that is exactly as specified.
|
||||
/// On drawing, the font will look blurry due to stretching.
|
||||
/// Intended for use with custom scale handling.
|
||||
/// </summary>
|
||||
SkipHandling,
|
||||
|
||||
/// <summary>
|
||||
/// Stretch the glyphs of the loaded font by the inverse of the global scale.
|
||||
/// On drawing, the font will always render exactly as the requested size without blurring, as long as
|
||||
/// <see cref="ImGuiHelpers.GlobalScale"/> and <see cref="ImGui.SetWindowFontScale"/> do not affect the scale any
|
||||
/// further. Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes.
|
||||
/// </summary>
|
||||
UndoGlobalScale,
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@ using ImGuiNET;
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for <see cref="ImFontAtlasPtr"/>.
|
||||
/// Wrapper for <see cref="ImFontAtlasPtr"/>.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontAtlas : IDisposable
|
||||
{
|
||||
|
|
@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable
|
|||
/// </summary>
|
||||
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
|
||||
/// <returns>Handle to a font that may or may not be ready yet.</returns>
|
||||
/// <remarks>
|
||||
/// Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to support
|
||||
/// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <b>On initialization</b>:
|
||||
/// <code>
|
||||
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => {
|
||||
/// var config = new SafeFontConfig { SizePx = 16 };
|
||||
/// var config = new SafeFontConfig { SizePx = UiBuilder.DefaultFontSizePx };
|
||||
/// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config);
|
||||
/// tk.AddGameSymbol(config);
|
||||
/// tk.AddExtraGlyphsForDalamudLanguage(config);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ using ImGuiNET;
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.
|
||||
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkit
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether global scaling is ignored for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>True if ignored.</returns>
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
|
||||
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
|
||||
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a texture to be managed with the atlas.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -10,6 +12,7 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PreBuild"/>.<br />
|
||||
/// Not intended for plugins to implement.<br />
|
||||
/// <br />
|
||||
/// After <see cref="FontAtlasBuildStepDelegate"/> returns,
|
||||
/// either <see cref="IFontAtlasBuildToolkit.Font"/> must be set,
|
||||
|
|
@ -43,14 +46,43 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
|
|||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>Same <see cref="ImFontPtr"/> with <paramref name="fontPtr"/>.</returns>
|
||||
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr);
|
||||
[Obsolete(
|
||||
$"Use {nameof(this.SetFontScaleMode)} with {nameof(FontScaleMode)}.{nameof(FontScaleMode.UndoGlobalScale)}")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) => this.SetFontScaleMode(fontPtr, FontScaleMode.UndoGlobalScale);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether global scaling is ignored for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>True if ignored.</returns>
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
|
||||
[Obsolete($"Use {nameof(this.GetFontScaleMode)}")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the scaling mode for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font, returned from <see cref="AddFontFromFile"/> and alike.
|
||||
/// Note that <see cref="IFontAtlasBuildToolkit.Font"/> property is not guaranteed to be automatically updated upon
|
||||
/// calling font adding functions. Pass the return value from font adding functions, not
|
||||
/// <see cref="IFontAtlasBuildToolkit.Font"/> property.</param>
|
||||
/// <param name="mode">The scaling mode.</param>
|
||||
/// <returns><paramref name="fontPtr"/>.</returns>
|
||||
ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode mode);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scaling mode for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>The scaling mode.</returns>
|
||||
FontScaleMode GetFontScaleMode(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a function to be run after build.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run.</param>
|
||||
void RegisterPostBuild(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
|
||||
|
|
@ -134,7 +166,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
|
|||
/// As this involves adding multiple fonts, calling this function will set <see cref="IFontAtlasBuildToolkit.Font"/>
|
||||
/// as the return value of this function, if it was empty before.
|
||||
/// </summary>
|
||||
/// <param name="sizePx">Font size in pixels.</param>
|
||||
/// <param name="sizePx">
|
||||
/// Font size in pixels.
|
||||
/// If a negative value is supplied,
|
||||
/// (<see cref="UiBuilder.DefaultFontSpec"/>.<see cref="IFontSpec.SizePx"/> * <paramref name="sizePx"/>) will be
|
||||
/// used as the font size. Specify -1 to use the default font size.
|
||||
/// </param>
|
||||
/// <param name="glyphRanges">The glyph ranges. Use <see cref="FontAtlasBuildToolkitUtilities"/>.ToGlyphRange to build.</param>
|
||||
/// <returns>A font returned from <see cref="ImFontAtlasPtr.AddFont"/>.</returns>
|
||||
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ using ImGuiNET;
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference counting handle for fonts.
|
||||
/// Represents a reference counting handle for fonts.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface IFontHandle : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas;
|
|||
|
||||
/// <summary>
|
||||
/// The wrapper for <see cref="ImFontPtr"/>, guaranteeing that the associated data will be available as long as
|
||||
/// this struct is not disposed.
|
||||
/// this struct is not disposed.<br />
|
||||
/// Not intended for plugins to implement.
|
||||
/// </summary>
|
||||
public interface ILockedImFont : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
|
|||
using System.Text.Unicode;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
|
@ -42,6 +43,7 @@ internal sealed partial class FontAtlasFactory
|
|||
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
|
||||
private readonly FontAtlasFactory factory;
|
||||
private readonly FontAtlasBuiltData data;
|
||||
private readonly List<Action> registeredPostBuildActions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
|
||||
|
|
@ -81,9 +83,9 @@ internal sealed partial class FontAtlasFactory
|
|||
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of fonts to ignore global scale.
|
||||
/// Gets the font scale modes.
|
||||
/// </summary>
|
||||
public List<ImFontPtr> GlobalScaleExclusions { get; } = new();
|
||||
private Dictionary<ImFontPtr, FontScaleMode> FontScaleModes { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.disposeAfterBuild.Dispose();
|
||||
|
|
@ -149,19 +151,22 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
|
||||
public ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode)
|
||||
{
|
||||
this.GlobalScaleExclusions.Add(fontPtr);
|
||||
this.FontScaleModes[fontPtr] = scaleMode;
|
||||
return fontPtr;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
|
||||
public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) =>
|
||||
this.GlobalScaleExclusions.Contains(fontPtr);
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.GetFontScaleMode"/>
|
||||
public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) =>
|
||||
this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
|
||||
this.data.AddNewTexture(textureWrap, disposeOnError);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
|
||||
|
|
@ -180,6 +185,7 @@ internal sealed partial class FontAtlasFactory
|
|||
dataSize,
|
||||
debugTag);
|
||||
|
||||
var font = default(ImFontPtr);
|
||||
try
|
||||
{
|
||||
fontConfig.ThrowOnInvalidValues();
|
||||
|
|
@ -187,6 +193,7 @@ internal sealed partial class FontAtlasFactory
|
|||
var raw = fontConfig.Raw with
|
||||
{
|
||||
FontData = dataPointer,
|
||||
FontDataOwnedByAtlas = 1,
|
||||
FontDataSize = dataSize,
|
||||
};
|
||||
|
||||
|
|
@ -198,7 +205,7 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw);
|
||||
|
||||
var font = this.NewImAtlas.AddFont(&raw);
|
||||
font = this.NewImAtlas.AddFont(&raw);
|
||||
|
||||
var dataHash = default(HashCode);
|
||||
dataHash.AddBytes(new(dataPointer, dataSize));
|
||||
|
|
@ -235,8 +242,23 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
catch
|
||||
{
|
||||
if (!font.IsNull())
|
||||
{
|
||||
// Note that for both RemoveAt calls, corresponding destructors will be called.
|
||||
|
||||
var configIndex = this.data.ConfigData.FindIndex(x => x.DstFont == font.NativePtr);
|
||||
if (configIndex >= 0)
|
||||
this.data.ConfigData.RemoveAt(configIndex);
|
||||
|
||||
var index = this.Fonts.IndexOf(font);
|
||||
if (index >= 0)
|
||||
this.Fonts.RemoveAt(index);
|
||||
}
|
||||
|
||||
// ImFontConfig has no destructor, and does not free the data.
|
||||
if (freeOnException)
|
||||
ImGuiNative.igMemFree(dataPointer);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -314,18 +336,32 @@ internal sealed partial class FontAtlasFactory
|
|||
/// <inheritdoc/>
|
||||
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
|
||||
{
|
||||
ImFontPtr font;
|
||||
ImFontPtr font = default;
|
||||
glyphRanges ??= this.factory.DefaultGlyphRanges;
|
||||
if (this.factory.UseAxis)
|
||||
|
||||
var dfid = this.factory.DefaultFontSpec;
|
||||
if (sizePx < 0f)
|
||||
sizePx *= -dfid.SizePx;
|
||||
|
||||
if (dfid is SingleFontSpec sfs)
|
||||
{
|
||||
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
|
||||
if (sfs.FontId is DalamudDefaultFontAndFamilyId)
|
||||
{
|
||||
// invalid; calling sfs.AddToBuildToolkit calls this function, causing infinite recursion
|
||||
}
|
||||
else
|
||||
{
|
||||
sfs = sfs with { SizePx = sizePx };
|
||||
font = sfs.AddToBuildToolkit(this);
|
||||
if (sfs.FontId is not GameFontAndFamilyId { GameFontFamily: GameFontFamily.Axis })
|
||||
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (font.IsNull())
|
||||
{
|
||||
font = this.AddDalamudAssetFont(
|
||||
DalamudAsset.NotoSansJpMedium,
|
||||
new() { SizePx = sizePx, GlyphRanges = glyphRanges });
|
||||
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
|
||||
// fall back to AXIS fonts
|
||||
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
|
||||
}
|
||||
|
||||
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
|
||||
|
|
@ -460,17 +496,17 @@ internal sealed partial class FontAtlasFactory
|
|||
var configData = this.data.ConfigData;
|
||||
foreach (ref var config in configData.DataSpan)
|
||||
{
|
||||
if (this.GlobalScaleExclusions.Contains(new(config.DstFont)))
|
||||
if (this.GetFontScaleMode(config.DstFont) != FontScaleMode.Default)
|
||||
continue;
|
||||
|
||||
config.SizePixels *= this.Scale;
|
||||
|
||||
config.GlyphMaxAdvanceX *= this.Scale;
|
||||
if (float.IsInfinity(config.GlyphMaxAdvanceX))
|
||||
if (float.IsInfinity(config.GlyphMaxAdvanceX) || float.IsNaN(config.GlyphMaxAdvanceX))
|
||||
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
|
||||
|
||||
config.GlyphMinAdvanceX *= this.Scale;
|
||||
if (float.IsInfinity(config.GlyphMinAdvanceX))
|
||||
if (float.IsInfinity(config.GlyphMinAdvanceX) || float.IsNaN(config.GlyphMinAdvanceX))
|
||||
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
|
||||
|
||||
config.GlyphOffset *= this.Scale;
|
||||
|
|
@ -500,7 +536,7 @@ internal sealed partial class FontAtlasFactory
|
|||
var scale = this.Scale;
|
||||
foreach (ref var font in this.Fonts.DataSpan)
|
||||
{
|
||||
if (!this.GlobalScaleExclusions.Contains(font))
|
||||
if (this.GetFontScaleMode(font) != FontScaleMode.SkipHandling)
|
||||
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
|
||||
|
||||
foreach (var c in FallbackCodepoints)
|
||||
|
|
@ -531,6 +567,13 @@ internal sealed partial class FontAtlasFactory
|
|||
substance.OnPostBuild(this);
|
||||
}
|
||||
|
||||
public void PostBuildCallbacks()
|
||||
{
|
||||
foreach (var ac in this.registeredPostBuildActions)
|
||||
ac.InvokeSafely();
|
||||
this.registeredPostBuildActions.Clear();
|
||||
}
|
||||
|
||||
public unsafe void UploadTextures()
|
||||
{
|
||||
var buf = Array.Empty<byte>();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
private class FontAtlasBuiltData : IRefCountable
|
||||
{
|
||||
// Field for debugging.
|
||||
private static int numActiveInstances;
|
||||
|
||||
private readonly List<IDalamudTextureWrap> wraps;
|
||||
private readonly List<IFontHandleSubstance> substances;
|
||||
|
||||
|
|
@ -73,6 +76,9 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr));
|
||||
this.IsBuildInProgress = true;
|
||||
|
||||
Interlocked.Increment(ref numActiveInstances);
|
||||
this.Garbage.Add(() => Interlocked.Decrement(ref numActiveInstances));
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -658,7 +664,7 @@ internal sealed partial class FontAtlasFactory
|
|||
toolkit = res.CreateToolkit(this.factory, isAsync);
|
||||
|
||||
// PreBuildSubstances deals with toolkit.Add... function family. Do this first.
|
||||
var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null);
|
||||
var defaultFont = toolkit.AddDalamudDefaultFont(-1, null);
|
||||
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
toolkit.PreBuildSubstances();
|
||||
|
|
@ -679,6 +685,7 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
toolkit.PostBuild();
|
||||
toolkit.PostBuildSubstances();
|
||||
toolkit.PostBuildCallbacks();
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
|
||||
foreach (var font in toolkit.Fonts)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Storage.Assets;
|
||||
|
|
@ -108,14 +109,29 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to override configuration for UseAxis.
|
||||
/// Gets or sets a value indicating whether to override configuration for <see cref="DefaultFontSpec"/>.
|
||||
/// </summary>
|
||||
public bool? UseAxisOverride { get; set; } = null;
|
||||
public IFontSpec? DefaultFontSpecOverride { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use AXIS fonts.
|
||||
/// Gets the default font ID.
|
||||
/// </summary>
|
||||
public bool UseAxis => this.UseAxisOverride ?? Service<DalamudConfiguration>.Get().UseAxisFontsFromGame;
|
||||
public IFontSpec DefaultFontSpec =>
|
||||
this.DefaultFontSpecOverride
|
||||
?? Service<DalamudConfiguration>.Get().DefaultFontSpec
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
?? (Service<DalamudConfiguration>.Get().UseAxisFontsFromGame
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
? new()
|
||||
{
|
||||
FontId = new GameFontAndFamilyId(GameFontFamily.Axis),
|
||||
SizePx = InterfaceManager.DefaultFontSizePx,
|
||||
}
|
||||
: new SingleFontSpec
|
||||
{
|
||||
FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
|
||||
SizePx = InterfaceManager.DefaultFontSizePx + 1,
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service instance of <see cref="Framework"/>.
|
||||
|
|
@ -229,6 +245,25 @@ internal sealed partial class FontAtlasFactory
|
|||
|
||||
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
|
||||
/// texture behind.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
|
||||
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
|
||||
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
|
||||
{
|
||||
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
|
||||
using var res = srv.Resource;
|
||||
using var tex2D = res.QueryInterface<Texture2D>();
|
||||
var description = tex2D.Description;
|
||||
return new DalamudTextureWrap(
|
||||
new D3DTextureWrap(
|
||||
srv.QueryInterface<ShaderResourceView>(),
|
||||
description.Width,
|
||||
description.Height));
|
||||
}
|
||||
|
||||
private static unsafe void ExtractChannelFromB8G8R8A8(
|
||||
Span<byte> target,
|
||||
ReadOnlySpan<byte> source,
|
||||
|
|
@ -266,25 +301,6 @@ internal sealed partial class FontAtlasFactory
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
|
||||
/// texture behind.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
|
||||
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
|
||||
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
|
||||
{
|
||||
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
|
||||
using var res = srv.Resource;
|
||||
using var tex2D = res.QueryInterface<Texture2D>();
|
||||
var description = tex2D.Description;
|
||||
return new DalamudTextureWrap(
|
||||
new D3DTextureWrap(
|
||||
srv.QueryInterface<ShaderResourceView>(),
|
||||
description.Width,
|
||||
description.Height));
|
||||
}
|
||||
|
||||
private static unsafe void ExtractChannelFromB4G4R4A4(
|
||||
Span<byte> target,
|
||||
ReadOnlySpan<byte> source,
|
||||
|
|
@ -317,7 +333,7 @@ internal sealed partial class FontAtlasFactory
|
|||
v |= v << 4;
|
||||
*wptr = (uint)((v << 24) | 0x00FFFFFF);
|
||||
wptr++;
|
||||
rptr += 4;
|
||||
rptr += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -345,17 +345,36 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
{
|
||||
foreach (var (font, style, ranges) in this.attachments)
|
||||
{
|
||||
var effectiveStyle =
|
||||
toolkitPreBuild.IsGlobalScaleIgnored(font)
|
||||
? style.Scale(1 / toolkitPreBuild.Scale)
|
||||
: style;
|
||||
if (!this.fonts.TryGetValue(style, out var plan))
|
||||
{
|
||||
plan = new(
|
||||
effectiveStyle,
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
switch (toolkitPreBuild.GetFontScaleMode(font))
|
||||
{
|
||||
case FontScaleMode.Default:
|
||||
default:
|
||||
plan = new(
|
||||
style,
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
break;
|
||||
|
||||
case FontScaleMode.SkipHandling:
|
||||
plan = new(
|
||||
style,
|
||||
1f,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
break;
|
||||
|
||||
case FontScaleMode.UndoGlobalScale:
|
||||
plan = new(
|
||||
style.Scale(1 / toolkitPreBuild.Scale),
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
break;
|
||||
}
|
||||
|
||||
this.fonts[style] = plan;
|
||||
}
|
||||
|
||||
|
|
@ -620,15 +639,14 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
||||
{
|
||||
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
|
||||
var atlasScale = toolkitPostBuild.Scale;
|
||||
var round = 1 / atlasScale;
|
||||
|
||||
foreach (var (font, rangeBits) in this.Ranges)
|
||||
{
|
||||
if (font.NativePtr == this.FullRangeFont.NativePtr)
|
||||
continue;
|
||||
|
||||
var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font);
|
||||
var fontScaleMode = toolkitPostBuild.GetFontScaleMode(font);
|
||||
var round = fontScaleMode == FontScaleMode.SkipHandling ? 1 : 1 / toolkitPostBuild.Scale;
|
||||
|
||||
var lookup = font.IndexLookupWrapped();
|
||||
var glyphs = font.GlyphsWrapped();
|
||||
|
|
@ -649,7 +667,7 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
|
||||
ref var g = ref glyphs[glyphIndex];
|
||||
g = sourceGlyph;
|
||||
if (noGlobalScale)
|
||||
if (fontScaleMode == FontScaleMode.SkipHandling)
|
||||
{
|
||||
g.XY *= scale;
|
||||
g.AdvanceX *= scale;
|
||||
|
|
@ -673,7 +691,7 @@ internal class GamePrebakedFontHandle : FontHandle
|
|||
continue;
|
||||
if (!rangeBits[leftInt] || !rangeBits[rightInt])
|
||||
continue;
|
||||
if (noGlobalScale)
|
||||
if (fontScaleMode == FontScaleMode.SkipHandling)
|
||||
{
|
||||
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public struct SafeFontConfig
|
|||
this.PixelSnapH = true;
|
||||
this.GlyphMaxAdvanceX = float.MaxValue;
|
||||
this.RasterizerMultiply = 1f;
|
||||
this.RasterizerGamma = 1.4f;
|
||||
this.RasterizerGamma = 1.7f;
|
||||
this.EllipsisChar = unchecked((char)-1);
|
||||
this.Raw.FontDataOwnedByAtlas = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Dalamud.Game;
|
|||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
|
@ -173,12 +174,12 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the default Dalamud font size in points.
|
||||
/// </summary>
|
||||
public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
|
||||
public static float DefaultFontSizePt => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePt;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font size in pixels.
|
||||
/// </summary>
|
||||
public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
|
||||
public static float DefaultFontSizePx => Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
|
||||
|
|
@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable
|
|||
/// </summary>
|
||||
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default font specifications.
|
||||
/// </summary>
|
||||
public IFontSpec DefaultFontSpec => Service<FontAtlasFactory>.Get().DefaultFontSpec;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
|
||||
/// </summary>
|
||||
|
|
@ -206,7 +212,7 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <code>
|
||||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt)));
|
||||
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx)));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle DefaultFontHandle =>
|
||||
|
|
@ -225,6 +231,8 @@ public sealed class UiBuilder : IDisposable
|
|||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt })));
|
||||
/// // or use
|
||||
/// tk => tk.AddFontAwesomeIconFont(new() { SizePx = UiBuilder.DefaultFontSizePx })));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle IconFontHandle =>
|
||||
|
|
@ -245,6 +253,8 @@ public sealed class UiBuilder : IDisposable
|
|||
/// tk => tk.AddDalamudAssetFont(
|
||||
/// DalamudAsset.InconsolataRegular,
|
||||
/// new() { SizePt = UiBuilder.DefaultFontSizePt })));
|
||||
/// // or use
|
||||
/// new() { SizePx = UiBuilder.DefaultFontSizePx })));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle MonoFontHandle =>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using System.Numerics;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Unicode;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
|
|
@ -543,6 +544,24 @@ public static class ImGuiHelpers
|
|||
var pageIndex = unchecked((ushort)(codepoint / 4096));
|
||||
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the text for a text input, during the callback.
|
||||
/// </summary>
|
||||
/// <param name="data">The callback data.</param>
|
||||
/// <param name="s">The new text.</param>
|
||||
internal static unsafe void SetTextFromCallback(ImGuiInputTextCallbackData* data, string s)
|
||||
{
|
||||
if (data->BufTextLen != 0)
|
||||
ImGuiNative.ImGuiInputTextCallbackData_DeleteChars(data, 0, data->BufTextLen);
|
||||
|
||||
var len = Encoding.UTF8.GetByteCount(s);
|
||||
var buf = len < 1024 ? stackalloc byte[len] : new byte[len];
|
||||
Encoding.UTF8.GetBytes(s, buf);
|
||||
fixed (byte* pBuf = buf)
|
||||
ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len);
|
||||
ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the corresponding ImGui viewport ID for the given window handle.
|
||||
|
|
|
|||
|
|
@ -623,15 +623,38 @@ public abstract class Window
|
|||
/// </summary>
|
||||
public struct WindowSizeConstraints
|
||||
{
|
||||
private Vector2 internalMaxSize = new(float.MaxValue);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
|
||||
/// </summary>
|
||||
public WindowSizeConstraints()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum size of the window.
|
||||
/// </summary>
|
||||
public Vector2 MinimumSize { get; set; }
|
||||
|
||||
public Vector2 MinimumSize { get; set; } = new(0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum size of the window.
|
||||
/// </summary>
|
||||
public Vector2 MaximumSize { get; set; }
|
||||
public Vector2 MaximumSize
|
||||
{
|
||||
get => this.GetSafeMaxSize();
|
||||
set => this.internalMaxSize = value;
|
||||
}
|
||||
|
||||
private Vector2 GetSafeMaxSize()
|
||||
{
|
||||
var currentMin = this.MinimumSize;
|
||||
|
||||
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
|
||||
return new Vector2(float.MaxValue);
|
||||
|
||||
return this.internalMaxSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,21 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Memory.Exceptions;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using static Dalamud.NativeFunctions;
|
||||
|
||||
using LPayloadType = Lumina.Text.Payloads.PayloadType;
|
||||
using LSeString = Lumina.Text.SeString;
|
||||
|
||||
// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory)
|
||||
|
||||
namespace Dalamud.Memory;
|
||||
|
|
@ -19,6 +25,47 @@ namespace Dalamud.Memory;
|
|||
/// </summary>
|
||||
public static unsafe class MemoryHelper
|
||||
{
|
||||
private static readonly ObjectPool<StringBuilder> StringBuilderPool =
|
||||
ObjectPool.Create(new StringBuilderPooledObjectPolicy());
|
||||
|
||||
#region Cast
|
||||
|
||||
/// <summary>Casts the given memory address as the reference to the live object.</summary>
|
||||
/// <param name="memoryAddress">The memory address.</param>
|
||||
/// <typeparam name="T">The unmanaged type.</typeparam>
|
||||
/// <returns>The reference to the live object.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T Cast<T>(nint memoryAddress) where T : unmanaged => ref *(T*)memoryAddress;
|
||||
|
||||
/// <summary>Casts the given memory address as the span of the live object(s).</summary>
|
||||
/// <param name="memoryAddress">The memory address.</param>
|
||||
/// <param name="length">The number of items.</param>
|
||||
/// <typeparam name="T">The unmanaged type.</typeparam>
|
||||
/// <returns>The span containing reference to the live object(s).</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Span<T> Cast<T>(nint memoryAddress, int length) where T : unmanaged =>
|
||||
new((void*)memoryAddress, length);
|
||||
|
||||
/// <summary>Casts the given memory address as the span of the live object(s), until it encounters a zero.</summary>
|
||||
/// <param name="memoryAddress">The memory address.</param>
|
||||
/// <param name="maxLength">The maximum number of items.</param>
|
||||
/// <typeparam name="T">The unmanaged type.</typeparam>
|
||||
/// <returns>The span containing reference to the live object(s).</returns>
|
||||
/// <remarks>If <typeparamref name="T"/> is <c>byte</c> or <c>char</c> and <paramref name="maxLength"/> is not
|
||||
/// specified, consider using <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(byte*)"/> or
|
||||
/// <see cref="MemoryMarshal.CreateReadOnlySpanFromNullTerminated(char*)"/>.</remarks>
|
||||
public static Span<T> CastNullTerminated<T>(nint memoryAddress, int maxLength = int.MaxValue)
|
||||
where T : unmanaged, IEquatable<T>
|
||||
{
|
||||
var typedPointer = (T*)memoryAddress;
|
||||
var length = 0;
|
||||
while (length < maxLength && !default(T).Equals(*typedPointer++))
|
||||
length++;
|
||||
return new((void*)memoryAddress, length);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Read
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -27,7 +74,9 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in struct.</returns>
|
||||
public static T Read<T>(IntPtr memoryAddress) where T : unmanaged
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Read<T>(nint memoryAddress) where T : unmanaged
|
||||
=> Read<T>(memoryAddress, false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -37,12 +86,13 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
/// <returns>The read in struct.</returns>
|
||||
public static T Read<T>(IntPtr memoryAddress, bool marshal)
|
||||
{
|
||||
return marshal
|
||||
? Marshal.PtrToStructure<T>(memoryAddress)
|
||||
: Unsafe.Read<T>((void*)memoryAddress);
|
||||
}
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T Read<T>(nint memoryAddress, bool marshal) =>
|
||||
marshal
|
||||
? Marshal.PtrToStructure<T>(memoryAddress)
|
||||
: Unsafe.Read<T>((void*)memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a byte array from a specified memory address.
|
||||
|
|
@ -50,12 +100,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <returns>The read in byte array.</returns>
|
||||
public static byte[] ReadRaw(IntPtr memoryAddress, int length)
|
||||
{
|
||||
var value = new byte[length];
|
||||
Marshal.Copy(memoryAddress, value, 0, value.Length);
|
||||
return value;
|
||||
}
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] ReadRaw(nint memoryAddress, int length) => Cast<byte>(memoryAddress, length).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Reads a generic type array from a specified memory address.
|
||||
|
|
@ -64,8 +111,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <returns>The read in struct array.</returns>
|
||||
public static T[] Read<T>(IntPtr memoryAddress, int arrayLength) where T : unmanaged
|
||||
=> Read<T>(memoryAddress, arrayLength, false);
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T[] Read<T>(nint memoryAddress, int arrayLength) where T : unmanaged
|
||||
=> Cast<T>(memoryAddress, arrayLength).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Reads a generic type array from a specified memory address.
|
||||
|
|
@ -75,16 +124,18 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
/// <returns>The read in struct array.</returns>
|
||||
public static T[] Read<T>(IntPtr memoryAddress, int arrayLength, bool marshal)
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
public static T[] Read<T>(nint memoryAddress, int arrayLength, bool marshal)
|
||||
{
|
||||
var structSize = SizeOf<T>(marshal);
|
||||
var value = new T[arrayLength];
|
||||
|
||||
for (var i = 0; i < arrayLength; i++)
|
||||
{
|
||||
var address = memoryAddress + (structSize * i);
|
||||
Read(address, out T result, marshal);
|
||||
Read(memoryAddress, out T result, marshal);
|
||||
value[i] = result;
|
||||
memoryAddress += structSize;
|
||||
}
|
||||
|
||||
return value;
|
||||
|
|
@ -95,16 +146,10 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in byte array.</returns>
|
||||
public static unsafe byte[] ReadRawNullTerminated(IntPtr memoryAddress)
|
||||
{
|
||||
var byteCount = 0;
|
||||
while (*(byte*)(memoryAddress + byteCount) != 0x00)
|
||||
{
|
||||
byteCount++;
|
||||
}
|
||||
|
||||
return ReadRaw(memoryAddress, byteCount);
|
||||
}
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="CastNullTerminated{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] ReadRawNullTerminated(nint memoryAddress) =>
|
||||
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress).ToArray();
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -116,7 +161,9 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">Local variable to receive the read in struct.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, out T value) where T : unmanaged
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, out T value) where T : unmanaged
|
||||
=> value = Read<T>(memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -126,7 +173,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">Local variable to receive the read in struct.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, out T value, bool marshal)
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, out T value, bool marshal)
|
||||
=> value = Read<T>(memoryAddress, marshal);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -135,7 +185,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <param name="value">Local variable to receive the read in bytes.</param>
|
||||
public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value)
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadRaw(nint memoryAddress, int length, out byte[] value)
|
||||
=> value = ReadRaw(memoryAddress, length);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -145,7 +197,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <param name="value">The read in struct array.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged
|
||||
/// <remarks>If you do not need to make a copy, use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, int arrayLength, out T[] value) where T : unmanaged
|
||||
=> value = Read<T>(memoryAddress, arrayLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -156,7 +210,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="arrayLength">The amount of array items to read.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
/// <param name="value">The read in struct array.</param>
|
||||
public static void Read<T>(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value)
|
||||
/// <remarks>If you do not need to make a copy and <paramref name="marshal"/> is <c>false</c>,
|
||||
/// use <see cref="Cast{T}(nint,int)"/> instead.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Read<T>(nint memoryAddress, int arrayLength, bool marshal, out T[] value)
|
||||
=> value = Read<T>(memoryAddress, arrayLength, marshal);
|
||||
|
||||
#endregion
|
||||
|
|
@ -184,15 +241,27 @@ public static unsafe class MemoryHelper
|
|||
var length = 0;
|
||||
while (length < maxLength && pmem[length] != 0)
|
||||
length++;
|
||||
|
||||
|
||||
var mem = new Span<byte>(pmem, length);
|
||||
var memCharCount = encoding.GetCharCount(mem);
|
||||
if (memCharCount != charSpan.Length)
|
||||
return false;
|
||||
|
||||
Span<char> chars = stackalloc char[memCharCount];
|
||||
encoding.GetChars(mem, chars);
|
||||
return charSpan.SequenceEqual(chars);
|
||||
if (memCharCount < 1024)
|
||||
{
|
||||
Span<char> chars = stackalloc char[memCharCount];
|
||||
encoding.GetChars(mem, chars);
|
||||
return charSpan.SequenceEqual(chars);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rented = ArrayPool<char>.Shared.Rent(memCharCount);
|
||||
var chars = rented.AsSpan(0, memCharCount);
|
||||
encoding.GetChars(mem, chars);
|
||||
var equals = charSpan.SequenceEqual(chars);
|
||||
ArrayPool<char>.Shared.Return(rented);
|
||||
return equals;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -203,8 +272,9 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadStringNullTerminated(IntPtr memoryAddress)
|
||||
=> ReadStringNullTerminated(memoryAddress, Encoding.UTF8);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string ReadStringNullTerminated(nint memoryAddress)
|
||||
=> Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
|
||||
|
||||
/// <summary>
|
||||
/// Read a string with the given encoding from a specified memory address.
|
||||
|
|
@ -215,10 +285,25 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding)
|
||||
public static string ReadStringNullTerminated(nint memoryAddress, Encoding encoding)
|
||||
{
|
||||
var buffer = ReadRawNullTerminated(memoryAddress);
|
||||
return encoding.GetString(buffer);
|
||||
switch (encoding)
|
||||
{
|
||||
case UTF8Encoding:
|
||||
case var _ when encoding.IsSingleByte:
|
||||
return encoding.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
|
||||
case UnicodeEncoding:
|
||||
// Note that it may be in little or big endian, so using `new string(...)` is not always correct.
|
||||
return encoding.GetString(
|
||||
MemoryMarshal.Cast<char, byte>(
|
||||
MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)memoryAddress)));
|
||||
case UTF32Encoding:
|
||||
return encoding.GetString(MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress)));
|
||||
default:
|
||||
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
|
||||
// non-null character, then this branch can be merged with UTF8Encoding one.
|
||||
return encoding.GetString(ReadRawNullTerminated(memoryAddress));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -228,10 +313,12 @@ public static unsafe class MemoryHelper
|
|||
/// Attention! If this is an <see cref="SeString"/>, use the applicable helper methods to decode.
|
||||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="maxLength">The maximum number of bytes to read.
|
||||
/// Note that this is NOT the maximum length of the returned string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadString(IntPtr memoryAddress, int maxLength)
|
||||
=> ReadString(memoryAddress, Encoding.UTF8, maxLength);
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string ReadString(nint memoryAddress, int maxLength)
|
||||
=> Encoding.UTF8.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
|
||||
|
||||
/// <summary>
|
||||
/// Read a string with the given encoding from a specified memory address.
|
||||
|
|
@ -241,18 +328,32 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="maxLength">The maximum number of bytes to read.
|
||||
/// Note that this is NOT the maximum length of the returned string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength)
|
||||
public static string ReadString(nint memoryAddress, Encoding encoding, int maxLength)
|
||||
{
|
||||
if (maxLength <= 0)
|
||||
return string.Empty;
|
||||
|
||||
ReadRaw(memoryAddress, maxLength, out var buffer);
|
||||
|
||||
var data = encoding.GetString(buffer);
|
||||
var eosPos = data.IndexOf('\0');
|
||||
return eosPos >= 0 ? data.Substring(0, eosPos) : data;
|
||||
switch (encoding)
|
||||
{
|
||||
case UTF8Encoding:
|
||||
case var _ when encoding.IsSingleByte:
|
||||
return encoding.GetString(CastNullTerminated<byte>(memoryAddress, maxLength));
|
||||
case UnicodeEncoding:
|
||||
return encoding.GetString(
|
||||
MemoryMarshal.Cast<char, byte>(CastNullTerminated<char>(memoryAddress, maxLength / 2)));
|
||||
case UTF32Encoding:
|
||||
return encoding.GetString(
|
||||
MemoryMarshal.Cast<int, byte>(CastNullTerminated<int>(memoryAddress, maxLength / 4)));
|
||||
default:
|
||||
// For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a
|
||||
// non-null character, then this branch can be merged with UTF8Encoding one.
|
||||
var data = encoding.GetString(Cast<byte>(memoryAddress, maxLength));
|
||||
var eosPos = data.IndexOf('\0');
|
||||
return eosPos >= 0 ? data[..eosPos] : data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -260,11 +361,9 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static SeString ReadSeStringNullTerminated(IntPtr memoryAddress)
|
||||
{
|
||||
var buffer = ReadRawNullTerminated(memoryAddress);
|
||||
return SeString.Parse(buffer);
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SeString ReadSeStringNullTerminated(nint memoryAddress) =>
|
||||
SeString.Parse(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress));
|
||||
|
||||
/// <summary>
|
||||
/// Read an SeString from a specified memory address.
|
||||
|
|
@ -272,40 +371,165 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static SeString ReadSeString(IntPtr memoryAddress, int maxLength)
|
||||
{
|
||||
ReadRaw(memoryAddress, maxLength, out var buffer);
|
||||
|
||||
var eos = Array.IndexOf(buffer, (byte)0);
|
||||
if (eos < 0)
|
||||
{
|
||||
return SeString.Parse(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newBuffer = new byte[eos];
|
||||
Buffer.BlockCopy(buffer, 0, newBuffer, 0, eos);
|
||||
return SeString.Parse(newBuffer);
|
||||
}
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SeString ReadSeString(nint memoryAddress, int maxLength) =>
|
||||
// Note that a valid SeString never contains a null character, other than for the sequence terminator purpose.
|
||||
SeString.Parse(CastNullTerminated<byte>(memoryAddress, maxLength));
|
||||
|
||||
/// <summary>
|
||||
/// Read an SeString from a specified Utf8String structure.
|
||||
/// </summary>
|
||||
/// <param name="utf8String">The memory address to read from.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static unsafe SeString ReadSeString(Utf8String* utf8String)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static SeString ReadSeString(Utf8String* utf8String) =>
|
||||
utf8String == null ? string.Empty : SeString.Parse(utf8String->AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Reads an SeString from a specified memory address, and extracts the outermost string.<br />
|
||||
/// If the SeString is malformed, behavior is undefined.
|
||||
/// </summary>
|
||||
/// <param name="containsNonRepresentedPayload">Whether the SeString contained a non-represented payload.</param>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="stopOnFirstNonRepresentedPayload">Stop reading on encountering the first non-represented payload.
|
||||
/// What payloads are represented via this function may change.</param>
|
||||
/// <param name="nonRepresentedPayloadReplacement">Replacement for non-represented payloads.</param>
|
||||
/// <returns>The read in string.</returns>
|
||||
public static string ReadSeStringAsString(
|
||||
out bool containsNonRepresentedPayload,
|
||||
nint memoryAddress,
|
||||
int maxLength = int.MaxValue,
|
||||
bool stopOnFirstNonRepresentedPayload = false,
|
||||
string nonRepresentedPayloadReplacement = "*")
|
||||
{
|
||||
if (utf8String == null)
|
||||
return string.Empty;
|
||||
var sb = StringBuilderPool.Get();
|
||||
sb.EnsureCapacity(maxLength = CastNullTerminated<byte>(memoryAddress, maxLength).Length);
|
||||
|
||||
var ptr = utf8String->StringPtr;
|
||||
if (ptr == null)
|
||||
return string.Empty;
|
||||
// 1 utf-8 codepoint can spill up to 2 characters.
|
||||
Span<char> tmp = stackalloc char[2];
|
||||
|
||||
var len = Math.Max(utf8String->BufUsed, utf8String->StringLength);
|
||||
var pin = (byte*)memoryAddress;
|
||||
containsNonRepresentedPayload = false;
|
||||
while (*pin != 0 && maxLength > 0)
|
||||
{
|
||||
if (*pin != LSeString.StartByte)
|
||||
{
|
||||
var len = *pin switch
|
||||
{
|
||||
< 0x80 => 1,
|
||||
>= 0b11000000 and <= 0b11011111 => 2,
|
||||
>= 0b11100000 and <= 0b11101111 => 3,
|
||||
>= 0b11110000 and <= 0b11110111 => 4,
|
||||
_ => 0,
|
||||
};
|
||||
if (len == 0 || len > maxLength)
|
||||
break;
|
||||
|
||||
return ReadSeString((IntPtr)ptr, (int)len);
|
||||
var numChars = Encoding.UTF8.GetChars(new(pin, len), tmp);
|
||||
sb.Append(tmp[..numChars]);
|
||||
pin += len;
|
||||
maxLength -= len;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start byte
|
||||
++pin;
|
||||
--maxLength;
|
||||
|
||||
// Payload type
|
||||
var payloadType = (LPayloadType)(*pin++);
|
||||
|
||||
// Payload length
|
||||
if (!ReadIntExpression(ref pin, ref maxLength, out var expressionLength))
|
||||
break;
|
||||
if (expressionLength > maxLength)
|
||||
break;
|
||||
pin += expressionLength;
|
||||
maxLength -= unchecked((int)expressionLength);
|
||||
|
||||
// End byte
|
||||
if (*pin++ != LSeString.EndByte)
|
||||
break;
|
||||
--maxLength;
|
||||
|
||||
switch (payloadType)
|
||||
{
|
||||
case LPayloadType.NewLine:
|
||||
sb.AppendLine();
|
||||
break;
|
||||
case LPayloadType.Hyphen:
|
||||
sb.Append('–');
|
||||
break;
|
||||
case LPayloadType.SoftHyphen:
|
||||
sb.Append('\u00AD');
|
||||
break;
|
||||
default:
|
||||
sb.Append(nonRepresentedPayloadReplacement);
|
||||
containsNonRepresentedPayload = true;
|
||||
if (stopOnFirstNonRepresentedPayload)
|
||||
maxLength = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var res = sb.ToString();
|
||||
StringBuilderPool.Return(sb);
|
||||
return res;
|
||||
|
||||
static bool ReadIntExpression(ref byte* p, ref int maxLength, out uint value)
|
||||
{
|
||||
if (maxLength <= 0)
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var typeByte = *p++;
|
||||
--maxLength;
|
||||
|
||||
switch (typeByte)
|
||||
{
|
||||
case > 0 and < 0xD0:
|
||||
value = (uint)typeByte - 1;
|
||||
return true;
|
||||
case >= 0xF0 and <= 0xFE:
|
||||
++typeByte;
|
||||
value = 0u;
|
||||
if ((typeByte & 8) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= (uint)*p++ << 24;
|
||||
}
|
||||
|
||||
if ((typeByte & 4) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= (uint)*p++ << 16;
|
||||
}
|
||||
|
||||
if ((typeByte & 2) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= (uint)*p++ << 8;
|
||||
}
|
||||
|
||||
if ((typeByte & 1) != 0)
|
||||
{
|
||||
if (maxLength <= 0 || *p == 0)
|
||||
return false;
|
||||
value |= *p++;
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -320,7 +544,8 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
public static void ReadStringNullTerminated(IntPtr memoryAddress, out string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadStringNullTerminated(nint memoryAddress, out string value)
|
||||
=> value = ReadStringNullTerminated(memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -332,7 +557,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
public static void ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding, out string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadStringNullTerminated(nint memoryAddress, Encoding encoding, out string value)
|
||||
=> value = ReadStringNullTerminated(memoryAddress, encoding);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -344,7 +570,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
public static void ReadString(IntPtr memoryAddress, out string value, int maxLength)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadString(nint memoryAddress, out string value, int maxLength)
|
||||
=> value = ReadString(memoryAddress, maxLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -357,7 +584,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="encoding">The encoding to use to decode the string.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadString(nint memoryAddress, Encoding encoding, int maxLength, out string value)
|
||||
=> value = ReadString(memoryAddress, encoding, maxLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -365,7 +593,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in SeString.</param>
|
||||
public static void ReadSeStringNullTerminated(IntPtr memoryAddress, out SeString value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadSeStringNullTerminated(nint memoryAddress, out SeString value)
|
||||
=> value = ReadSeStringNullTerminated(memoryAddress);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -374,7 +603,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="maxLength">The maximum length of the string.</param>
|
||||
/// <param name="value">The read in SeString.</param>
|
||||
public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadSeString(nint memoryAddress, int maxLength, out SeString value)
|
||||
=> value = ReadSeString(memoryAddress, maxLength);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -382,6 +612,7 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="utf8String">The memory address to read from.</param>
|
||||
/// <param name="value">The read in string.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value)
|
||||
=> value = ReadSeString(utf8String);
|
||||
|
||||
|
|
@ -395,7 +626,8 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="item">The item to write to the address.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T item) where T : unmanaged
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Write<T>(nint memoryAddress, T item) where T : unmanaged
|
||||
=> Write(memoryAddress, item, false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -405,7 +637,7 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="item">The item to write to the address.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T item, bool marshal)
|
||||
public static void Write<T>(nint memoryAddress, T item, bool marshal)
|
||||
{
|
||||
if (marshal)
|
||||
Marshal.StructureToPtr(item, memoryAddress, false);
|
||||
|
|
@ -418,10 +650,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="data">The bytes to write to memoryAddress.</param>
|
||||
public static void WriteRaw(IntPtr memoryAddress, byte[] data)
|
||||
{
|
||||
Marshal.Copy(data, 0, memoryAddress, data.Length);
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteRaw(nint memoryAddress, byte[] data) => Marshal.Copy(data, 0, memoryAddress, data.Length);
|
||||
|
||||
/// <summary>
|
||||
/// Writes a generic type array to a specified memory address.
|
||||
|
|
@ -429,7 +659,8 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="items">The array of items to write to the address.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T[] items) where T : unmanaged
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Write<T>(nint memoryAddress, T[] items) where T : unmanaged
|
||||
=> Write(memoryAddress, items, false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -439,7 +670,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="items">The array of items to write to the address.</param>
|
||||
/// <param name="marshal">Set this to true to enable struct marshalling.</param>
|
||||
public static void Write<T>(IntPtr memoryAddress, T[] items, bool marshal)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Write<T>(nint memoryAddress, T[] items, bool marshal)
|
||||
{
|
||||
var structSize = SizeOf<T>(marshal);
|
||||
|
||||
|
|
@ -462,7 +694,8 @@ public static unsafe class MemoryHelper
|
|||
/// </remarks>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="value">The string to write.</param>
|
||||
public static void WriteString(IntPtr memoryAddress, string value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteString(nint memoryAddress, string? value)
|
||||
=> WriteString(memoryAddress, value, Encoding.UTF8);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -474,14 +707,12 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="value">The string to write.</param>
|
||||
/// <param name="encoding">The encoding to use.</param>
|
||||
public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding)
|
||||
public static void WriteString(nint memoryAddress, string? value, Encoding encoding)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return;
|
||||
|
||||
var bytes = encoding.GetBytes(value + '\0');
|
||||
|
||||
WriteRaw(memoryAddress, bytes);
|
||||
var ptr = 0;
|
||||
if (value is not null)
|
||||
ptr = encoding.GetBytes(value, Cast<byte>(memoryAddress, encoding.GetMaxByteCount(value.Length)));
|
||||
encoding.GetBytes("\0", Cast<byte>(memoryAddress + ptr, 4));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -489,7 +720,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="value">The SeString to write.</param>
|
||||
public static void WriteSeString(IntPtr memoryAddress, SeString value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteSeString(nint memoryAddress, SeString? value)
|
||||
{
|
||||
if (value is null)
|
||||
return;
|
||||
|
|
@ -507,15 +739,16 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="length">Amount of bytes to be allocated.</param>
|
||||
/// <returns>Address to the newly allocated memory.</returns>
|
||||
public static IntPtr Allocate(int length)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint Allocate(int length)
|
||||
{
|
||||
var address = VirtualAlloc(
|
||||
IntPtr.Zero,
|
||||
(UIntPtr)length,
|
||||
nint.Zero,
|
||||
(nuint)length,
|
||||
AllocationType.Commit | AllocationType.Reserve,
|
||||
MemoryProtection.ExecuteReadWrite);
|
||||
|
||||
if (address == IntPtr.Zero)
|
||||
if (address == nint.Zero)
|
||||
throw new MemoryAllocationException($"Unable to allocate {length} bytes.");
|
||||
|
||||
return address;
|
||||
|
|
@ -527,7 +760,8 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="length">Amount of bytes to be allocated.</param>
|
||||
/// <param name="memoryAddress">Address to the newly allocated memory.</param>
|
||||
public static void Allocate(int length, out IntPtr memoryAddress)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Allocate(int length, out nint memoryAddress)
|
||||
=> memoryAddress = Allocate(length);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -535,9 +769,10 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The address of the memory to free.</param>
|
||||
/// <returns>True if the operation is successful.</returns>
|
||||
public static bool Free(IntPtr memoryAddress)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Free(nint memoryAddress)
|
||||
{
|
||||
return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release);
|
||||
return VirtualFree(memoryAddress, nuint.Zero, AllocationType.Release);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -547,9 +782,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="length">The region size for which to change permissions for.</param>
|
||||
/// <param name="newPermissions">The new permissions to set.</param>
|
||||
/// <returns>The old page permissions.</returns>
|
||||
public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions)
|
||||
public static MemoryProtection ChangePermission(nint memoryAddress, int length, MemoryProtection newPermissions)
|
||||
{
|
||||
var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions);
|
||||
var result = VirtualProtect(memoryAddress, (nuint)length, newPermissions, out var oldPermissions);
|
||||
|
||||
if (!result)
|
||||
throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})");
|
||||
|
|
@ -568,7 +803,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="length">The region size for which to change permissions for.</param>
|
||||
/// <param name="newPermissions">The new permissions to set.</param>
|
||||
/// <param name="oldPermissions">The old page permissions.</param>
|
||||
public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ChangePermission(
|
||||
nint memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
|
||||
=> oldPermissions = ChangePermission(memoryAddress, length, newPermissions);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -580,7 +817,9 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="newPermissions">The new permissions to set.</param>
|
||||
/// <param name="marshal">Set to true to calculate the size of the struct after marshalling instead of before.</param>
|
||||
/// <returns>The old page permissions.</returns>
|
||||
public static MemoryProtection ChangePermission<T>(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static MemoryProtection ChangePermission<T>(
|
||||
nint memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
|
||||
=> ChangePermission(memoryAddress, SizeOf<T>(marshal), newPermissions);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -590,7 +829,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <returns>The read in bytes.</returns>
|
||||
public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static byte[] ReadProcessMemory(nint memoryAddress, int length)
|
||||
{
|
||||
var value = new byte[length];
|
||||
ReadProcessMemory(memoryAddress, ref value);
|
||||
|
|
@ -604,7 +844,8 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="length">The amount of bytes to read starting from the memoryAddress.</param>
|
||||
/// <param name="value">The read in bytes.</param>
|
||||
public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ReadProcessMemory(nint memoryAddress, int length, out byte[] value)
|
||||
=> value = ReadProcessMemory(memoryAddress, length);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -613,12 +854,12 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to read from.</param>
|
||||
/// <param name="value">The read in bytes.</param>
|
||||
public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value)
|
||||
public static void ReadProcessMemory(nint memoryAddress, ref byte[] value)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var length = value.Length;
|
||||
var result = NativeFunctions.ReadProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, value, length, out _);
|
||||
var result = NativeFunctions.ReadProcessMemory((nint)0xFFFFFFFF, memoryAddress, value, length, out _);
|
||||
|
||||
if (!result)
|
||||
throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
|
||||
|
|
@ -635,12 +876,12 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <param name="memoryAddress">The memory address to write to.</param>
|
||||
/// <param name="data">The bytes to write to memoryAddress.</param>
|
||||
public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data)
|
||||
public static void WriteProcessMemory(nint memoryAddress, byte[] data)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var length = data.Length;
|
||||
var result = NativeFunctions.WriteProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, data, length, out _);
|
||||
var result = NativeFunctions.WriteProcessMemory((nint)0xFFFFFFFF, memoryAddress, data, length, out _);
|
||||
|
||||
if (!result)
|
||||
throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
|
||||
|
|
@ -660,6 +901,7 @@ public static unsafe class MemoryHelper
|
|||
/// </summary>
|
||||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <returns>The size of the primitive or struct.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>()
|
||||
=> SizeOf<T>(false);
|
||||
|
||||
|
|
@ -669,6 +911,7 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
|
||||
/// <returns>The size of the primitive or struct.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>(bool marshal)
|
||||
=> marshal ? Marshal.SizeOf<T>() : Unsafe.SizeOf<T>();
|
||||
|
||||
|
|
@ -678,6 +921,7 @@ public static unsafe class MemoryHelper
|
|||
/// <typeparam name="T">An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.</typeparam>
|
||||
/// <param name="elementCount">The number of array elements present.</param>
|
||||
/// <returns>The size of the primitive or struct array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>(int elementCount) where T : unmanaged
|
||||
=> SizeOf<T>() * elementCount;
|
||||
|
||||
|
|
@ -688,6 +932,7 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="elementCount">The number of array elements present.</param>
|
||||
/// <param name="marshal">If set to true; will return the size of an element after marshalling.</param>
|
||||
/// <returns>The size of the primitive or struct array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int SizeOf<T>(int elementCount, bool marshal)
|
||||
=> SizeOf<T>(marshal) * elementCount;
|
||||
|
||||
|
|
@ -701,9 +946,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateUi(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateUi(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetUISpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetUISpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -712,9 +958,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateDefault(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateDefault(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -723,9 +970,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateAnimation(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateAnimation(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -734,9 +982,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateApricot(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateApricot(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetApricotSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -745,9 +994,10 @@ public static unsafe class MemoryHelper
|
|||
/// <param name="size">Amount of bytes to allocate.</param>
|
||||
/// <param name="alignment">The alignment of the allocation.</param>
|
||||
/// <returns>Pointer to the allocated region.</returns>
|
||||
public static IntPtr GameAllocateSound(ulong size, ulong alignment = 0)
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nint GameAllocateSound(ulong size, ulong alignment = 0)
|
||||
{
|
||||
return new IntPtr(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
|
||||
return new nint(IMemorySpace.GetSoundSpace()->Malloc(size, alignment));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -756,15 +1006,15 @@ public static unsafe class MemoryHelper
|
|||
/// <remarks>The memory you are freeing must be allocated with game allocators.</remarks>
|
||||
/// <param name="ptr">Position at which the memory to be freed is located.</param>
|
||||
/// <param name="size">Amount of bytes to free.</param>
|
||||
public static void GameFree(ref IntPtr ptr, ulong size)
|
||||
public static void GameFree(ref nint ptr, ulong size)
|
||||
{
|
||||
if (ptr == IntPtr.Zero)
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IMemorySpace.Free((void*)ptr, size);
|
||||
ptr = IntPtr.Zero;
|
||||
ptr = nint.Zero;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
37
Dalamud/Plugin/Services/IContextMenu.cs
Normal file
37
Dalamud/Plugin/Services/IContextMenu.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This class provides methods for interacting with the game's context menu.
|
||||
/// </summary>
|
||||
public interface IContextMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate type used for the <see cref="OnMenuOpened"/> event.
|
||||
/// </summary>
|
||||
/// <param name="args">Information about the currently opening menu.</param>
|
||||
public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// Event that gets fired every time the game framework updates.
|
||||
/// </summary>
|
||||
event OnMenuOpenedDelegate OnMenuOpened;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a menu item to a context menu.
|
||||
/// </summary>
|
||||
/// <param name="menuType">The type of context menu to add the item to.</param>
|
||||
/// <param name="item">The item to add.</param>
|
||||
void AddMenuItem(ContextMenuType menuType, MenuItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a menu item from a context menu.
|
||||
/// </summary>
|
||||
/// <param name="menuType">The type of context menu to remove the item from.</param>
|
||||
/// <param name="item">The item to add.</param>
|
||||
/// <returns><see langword="true"/> if the item was removed, <see langword="false"/> if it was not found.</returns>
|
||||
bool RemoveMenuItem(ContextMenuType menuType, MenuItem item);
|
||||
}
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game.Config;
|
||||
using FFXIVClientStructs.FFXIV.Common.Configuration;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the game's configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Accessing <see cref="GameConfigSection"/>-typed properties such as <see cref="System"/>, directly or indirectly
|
||||
/// via <see cref="TryGet(Game.Config.SystemConfigOption,out bool)"/>,
|
||||
/// <see cref="Set(Game.Config.SystemConfigOption,bool)"/>, or alike will block, if the game is not done loading.<br />
|
||||
/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets
|
||||
/// <see cref="PluginManifest.LoadRequiredState"/> to <c>2</c> and <see cref="PluginManifest.LoadSync"/> to <c>true</c>.
|
||||
/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via
|
||||
/// <see cref="IFramework.RunOnFrameworkThread{T}(Func{T})"/>; do not wait for the result right away.
|
||||
/// </remarks>
|
||||
public interface IGameConfig
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -194,12 +194,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
|
||||
try
|
||||
{
|
||||
await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write);
|
||||
await url.DownloadAsync(
|
||||
this.httpClient.SharedHttpClient,
|
||||
tempPathStream,
|
||||
this.cancellationTokenSource.Token);
|
||||
tempPathStream.Dispose();
|
||||
await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
await url.DownloadAsync(
|
||||
this.httpClient.SharedHttpClient,
|
||||
tempPathStream,
|
||||
this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
for (var j = RenameAttemptCount; ; j--)
|
||||
{
|
||||
try
|
||||
|
|
@ -265,7 +267,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
|
||||
ExtractResult(this.GetDalamudTextureWrapAsync(asset));
|
||||
this.GetDalamudTextureWrapAsync(asset).Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
|
|
@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
}
|
||||
}
|
||||
|
||||
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
|
||||
|
||||
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace Dalamud.Storage.Assets;
|
|||
/// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have
|
||||
/// externally visible state changes.
|
||||
/// </summary>
|
||||
internal interface IDalamudAssetManager
|
||||
public interface IDalamudAssetManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the shared texture wrap for <see cref="DalamudAsset.Empty4X4"/>.
|
||||
|
|
|
|||
|
|
@ -97,4 +97,76 @@ internal static class ArrayExtensions
|
|||
/// <returns><paramref name="array"/> casted as a <see cref="IReadOnlyCollection{T}"/> if it is one; otherwise the result of <see cref="Enumerable.ToArray{TSource}"/>.</returns>
|
||||
public static IReadOnlyCollection<T> AsReadOnlyCollection<T>(this IEnumerable<T> array) =>
|
||||
array as IReadOnlyCollection<T> ?? array.ToArray();
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindIndex(System.Predicate{T})"/>
|
||||
public static int FindIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
|
||||
=> list.FindIndex(0, list.Count, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindIndex(int,System.Predicate{T})"/>
|
||||
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
|
||||
=> list.FindIndex(startIndex, list.Count - startIndex, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindIndex(int,int,System.Predicate{T})"/>
|
||||
public static int FindIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> match)
|
||||
{
|
||||
if ((uint)startIndex > (uint)list.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
|
||||
|
||||
if (count < 0 || startIndex > list.Count - count)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, null);
|
||||
|
||||
if (match == null)
|
||||
throw new ArgumentNullException(nameof(match));
|
||||
|
||||
var endIndex = startIndex + count;
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
if (match(list[i])) return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindLastIndex(System.Predicate{T})"/>
|
||||
public static int FindLastIndex<T>(this IReadOnlyList<T> list, Predicate<T> match)
|
||||
=> list.FindLastIndex(list.Count - 1, list.Count, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindLastIndex(int,System.Predicate{T})"/>
|
||||
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, Predicate<T> match)
|
||||
=> list.FindLastIndex(startIndex, startIndex + 1, match);
|
||||
|
||||
/// <inheritdoc cref="List{T}.FindLastIndex(int,int,System.Predicate{T})"/>
|
||||
public static int FindLastIndex<T>(this IReadOnlyList<T> list, int startIndex, int count, Predicate<T> match)
|
||||
{
|
||||
if (match == null)
|
||||
throw new ArgumentNullException(nameof(match));
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
// Special case for 0 length List
|
||||
if (startIndex != -1)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Make sure we're not out of range
|
||||
if ((uint)startIndex >= (uint)list.Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null);
|
||||
}
|
||||
|
||||
// 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
|
||||
if (count < 0 || startIndex - count + 1 < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, null);
|
||||
|
||||
var endIndex = startIndex - count;
|
||||
for (var i = startIndex; i > endIndex; i--)
|
||||
{
|
||||
if (match(list[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,21 +39,23 @@ public static class DisposeSafety
|
|||
public static IDisposable ToDisposableIgnoreExceptions<T>(this Task<T> task)
|
||||
where T : IDisposable
|
||||
{
|
||||
return Disposable.Create(() => task.ContinueWith(r =>
|
||||
{
|
||||
_ = r.Exception;
|
||||
if (r.IsCompleted)
|
||||
{
|
||||
try
|
||||
return Disposable.Create(
|
||||
() => task.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}));
|
||||
_ = r.Exception;
|
||||
if (r.IsCompleted)
|
||||
{
|
||||
try
|
||||
{
|
||||
r.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -102,25 +104,26 @@ public static class DisposeSafety
|
|||
if (disposables is not T[] array)
|
||||
array = disposables?.ToArray() ?? Array.Empty<T>();
|
||||
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
List<Exception?> exceptions = null;
|
||||
foreach (var d in array)
|
||||
return Disposable.Create(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
List<Exception?> exceptions = null;
|
||||
foreach (var d in array)
|
||||
{
|
||||
d?.Dispose();
|
||||
try
|
||||
{
|
||||
d?.Dispose();
|
||||
}
|
||||
catch (Exception de)
|
||||
{
|
||||
exceptions ??= new();
|
||||
exceptions.Add(de);
|
||||
}
|
||||
}
|
||||
catch (Exception de)
|
||||
{
|
||||
exceptions ??= new();
|
||||
exceptions.Add(de);
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions is not null)
|
||||
throw new AggregateException(exceptions);
|
||||
});
|
||||
if (exceptions is not null)
|
||||
throw new AggregateException(exceptions);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -137,7 +140,11 @@ public static class DisposeSafety
|
|||
public event Action<IDisposeCallback, Exception?>? AfterDispose;
|
||||
|
||||
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
|
||||
public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity);
|
||||
public void EnsureCapacity(int capacity)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.EnsureCapacity(capacity);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Stack{T}.Push"/>
|
||||
/// <returns>The parameter.</returns>
|
||||
|
|
@ -145,7 +152,10 @@ public static class DisposeSafety
|
|||
public T? Add<T>(T? d) where T : IDisposable
|
||||
{
|
||||
if (d is not null)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -155,7 +165,10 @@ public static class DisposeSafety
|
|||
public Action? Add(Action? d)
|
||||
{
|
||||
if (d is not null)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -165,7 +178,10 @@ public static class DisposeSafety
|
|||
public Func<Task>? Add(Func<Task>? d)
|
||||
{
|
||||
if (d is not null)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -174,7 +190,10 @@ public static class DisposeSafety
|
|||
public GCHandle Add(GCHandle d)
|
||||
{
|
||||
if (d != default)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.Add(this.CheckAdd(d));
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
|
@ -183,29 +202,41 @@ public static class DisposeSafety
|
|||
/// Queue all the given <see cref="IDisposable"/> to be disposed later.
|
||||
/// </summary>
|
||||
/// <param name="ds">Disposables.</param>
|
||||
public void AddRange(IEnumerable<IDisposable?> ds) =>
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<IDisposable?> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue all the given <see cref="IDisposable"/> to be run later.
|
||||
/// </summary>
|
||||
/// <param name="ds">Actions.</param>
|
||||
public void AddRange(IEnumerable<Action?> ds) =>
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<Action?> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue all the given <see cref="Func{T}"/> returning <see cref="Task"/> to be run later.
|
||||
/// </summary>
|
||||
/// <param name="ds">Func{Task}s.</param>
|
||||
public void AddRange(IEnumerable<Func<Task>?> ds) =>
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<Func<Task>?> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue all the given <see cref="GCHandle"/> to be disposed later.
|
||||
/// </summary>
|
||||
/// <param name="ds">GCHandles.</param>
|
||||
public void AddRange(IEnumerable<GCHandle> ds) =>
|
||||
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
|
||||
public void AddRange(IEnumerable<GCHandle> ds)
|
||||
{
|
||||
lock (this.objects)
|
||||
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel all pending disposals.
|
||||
|
|
@ -213,9 +244,12 @@ public static class DisposeSafety
|
|||
/// <remarks>Use this after successful initialization of multiple disposables.</remarks>
|
||||
public void Cancel()
|
||||
{
|
||||
foreach (var o in this.objects)
|
||||
this.CheckRemove(o);
|
||||
this.objects.Clear();
|
||||
lock (this.objects)
|
||||
{
|
||||
foreach (var o in this.objects)
|
||||
this.CheckRemove(o);
|
||||
this.objects.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
|
||||
|
|
@ -264,11 +298,17 @@ public static class DisposeSafety
|
|||
this.BeforeDispose?.InvokeSafely(this);
|
||||
|
||||
List<Exception>? exceptions = null;
|
||||
while (this.objects.Any())
|
||||
while (true)
|
||||
{
|
||||
var obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
|
||||
object obj;
|
||||
lock (this.objects)
|
||||
{
|
||||
if (this.objects.Count == 0)
|
||||
break;
|
||||
obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (obj)
|
||||
|
|
@ -294,7 +334,8 @@ public static class DisposeSafety
|
|||
}
|
||||
}
|
||||
|
||||
this.objects.TrimExcess();
|
||||
lock (this.objects)
|
||||
this.objects.TrimExcess();
|
||||
|
||||
if (exceptions is not null)
|
||||
{
|
||||
|
|
@ -318,10 +359,16 @@ public static class DisposeSafety
|
|||
this.BeforeDispose?.InvokeSafely(this);
|
||||
|
||||
List<Exception>? exceptions = null;
|
||||
while (this.objects.Any())
|
||||
while (true)
|
||||
{
|
||||
var obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
object obj;
|
||||
lock (this.objects)
|
||||
{
|
||||
if (this.objects.Count == 0)
|
||||
break;
|
||||
obj = this.objects[^1];
|
||||
this.objects.RemoveAt(this.objects.Count - 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -351,7 +398,8 @@ public static class DisposeSafety
|
|||
}
|
||||
}
|
||||
|
||||
this.objects.TrimExcess();
|
||||
lock (this.objects)
|
||||
this.objects.TrimExcess();
|
||||
|
||||
if (exceptions is not null)
|
||||
{
|
||||
|
|
@ -386,7 +434,8 @@ public static class DisposeSafety
|
|||
private void OnItemDisposed(IDisposeCallback obj)
|
||||
{
|
||||
obj.BeforeDispose -= this.OnItemDisposed;
|
||||
this.objects.Remove(obj);
|
||||
lock (this.objects)
|
||||
this.objects.Remove(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -99,6 +100,23 @@ internal static class EventHandlerExtensions
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replacement for Invoke() on OnMenuOpenedDelegate to catch exceptions that stop event propagation in case
|
||||
/// of a thrown Exception inside of an invocation.
|
||||
/// </summary>
|
||||
/// <param name="openedDelegate">The OnMenuOpenedDelegate in question.</param>
|
||||
/// <param name="argument">Templated argument for Action.</param>
|
||||
public static void InvokeSafely(this IContextMenu.OnMenuOpenedDelegate? openedDelegate, MenuOpenedArgs argument)
|
||||
{
|
||||
if (openedDelegate == null)
|
||||
return;
|
||||
|
||||
foreach (var action in openedDelegate.GetInvocationList().Cast<IContextMenu.OnMenuOpenedDelegate>())
|
||||
{
|
||||
HandleInvoke(() => action(argument));
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleInvoke(Action act)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
12
Dalamud/Utility/IDeferredDisposable.cs
Normal file
12
Dalamud/Utility/IDeferredDisposable.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// An extension of <see cref="IDisposable"/> which makes <see cref="IDisposable.Dispose"/> queue
|
||||
/// <see cref="RealDispose"/> to be called at a later time.
|
||||
/// </summary>
|
||||
internal interface IDeferredDisposable : IDisposable
|
||||
{
|
||||
/// <summary>Actually dispose the object.</summary>
|
||||
/// <remarks>Not to be called from the code that uses the end object.</remarks>
|
||||
void RealDispose();
|
||||
}
|
||||
234
Dalamud/Utility/RollingList.cs
Normal file
234
Dalamud/Utility/RollingList.cs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Dalamud.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// A list with limited capacity holding items of type <typeparamref name="T"/>.
|
||||
/// Adding further items will result in the list rolling over.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Item type.</typeparam>
|
||||
/// <remarks>
|
||||
/// Implemented as a circular list using a <see cref="List{T}"/> internally.
|
||||
/// Insertions and Removals are not supported.
|
||||
/// Not thread-safe.
|
||||
/// </remarks>
|
||||
internal class RollingList<T> : IList<T>
|
||||
{
|
||||
private List<T> items;
|
||||
private int size;
|
||||
private int firstIndex;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
/// <param name="capacity">Internal <see cref="List{T}"/> initial capacity.</param>
|
||||
public RollingList(int size, int capacity)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0);
|
||||
capacity = Math.Min(capacity, size);
|
||||
this.size = size;
|
||||
this.items = new List<T>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
public RollingList(int size)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0);
|
||||
this.size = size;
|
||||
this.items = new();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="items">Collection where elements are copied from.</param>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
public RollingList(IEnumerable<T> items, int size)
|
||||
{
|
||||
if (!items.TryGetNonEnumeratedCount(out var capacity)) capacity = 4;
|
||||
capacity = Math.Min(capacity, size);
|
||||
this.size = size;
|
||||
this.items = new List<T>(capacity);
|
||||
this.AddRange(items);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="RollingList{T}"/> class.</summary>
|
||||
/// <param name="items">Collection where elements are copied from.</param>
|
||||
/// <param name="size"><see cref="RollingList{T}"/> size.</param>
|
||||
/// <param name="capacity">Internal <see cref="List{T}"/> initial capacity.</param>
|
||||
public RollingList(IEnumerable<T> items, int size, int capacity)
|
||||
{
|
||||
if (items.TryGetNonEnumeratedCount(out var count) && count > capacity) capacity = count;
|
||||
capacity = Math.Min(capacity, size);
|
||||
this.size = size;
|
||||
this.items = new List<T>(capacity);
|
||||
this.AddRange(items);
|
||||
}
|
||||
|
||||
/// <summary>Gets item count.</summary>
|
||||
public int Count => this.items.Count;
|
||||
|
||||
/// <summary>Gets or sets the internal list capacity.</summary>
|
||||
public int Capacity
|
||||
{
|
||||
get => this.items.Capacity;
|
||||
set => this.items.Capacity = Math.Min(value, this.size);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets rolling list size.</summary>
|
||||
public int Size
|
||||
{
|
||||
get => this.size;
|
||||
set
|
||||
{
|
||||
if (value == this.size) return;
|
||||
if (value > this.size)
|
||||
{
|
||||
if (this.firstIndex > 0)
|
||||
{
|
||||
this.items = new List<T>(this);
|
||||
this.firstIndex = 0;
|
||||
}
|
||||
}
|
||||
else // value < this._size
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0);
|
||||
if (value < this.Count)
|
||||
{
|
||||
this.items = new List<T>(this.TakeLast(value));
|
||||
this.firstIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.size = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets a value indicating whether the item is read only.</summary>
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <summary>Gets or sets an item by index.</summary>
|
||||
/// <param name="index">Item index.</param>
|
||||
/// <returns>Item at specified index.</returns>
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count);
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0);
|
||||
return this.items[this.GetRealIndex(index)];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count);
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0);
|
||||
this.items[this.GetRealIndex(index)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds an item to this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <param name="item">Item to add.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
if (this.size == 0) return;
|
||||
if (this.items.Count >= this.size)
|
||||
{
|
||||
this.items[this.firstIndex] = item;
|
||||
this.firstIndex = (this.firstIndex + 1) % this.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this.items.Count == this.items.Capacity)
|
||||
{
|
||||
// Manual list capacity resize
|
||||
var newCapacity = Math.Max(Math.Min(this.size, this.items.Capacity * 2), this.items.Capacity);
|
||||
this.items.Capacity = newCapacity;
|
||||
}
|
||||
|
||||
this.items.Add(item);
|
||||
}
|
||||
|
||||
Debug.Assert(this.items.Count <= this.size, "Item count should be less than Size");
|
||||
}
|
||||
|
||||
/// <summary>Add items to this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <param name="items">Items to add.</param>
|
||||
public void AddRange(IEnumerable<T> items)
|
||||
{
|
||||
if (this.size == 0) return;
|
||||
foreach (var item in items) this.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>Removes all elements from the <see cref="RollingList{T}"/></summary>
|
||||
public void Clear()
|
||||
{
|
||||
this.items.Clear();
|
||||
this.firstIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Find the index of a specific item.</summary>
|
||||
/// <param name="item">item to find.</param>
|
||||
/// <returns>Index where <paramref name="item"/> is found. -1 if not found.</returns>
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
var index = this.items.IndexOf(item);
|
||||
if (index == -1) return -1;
|
||||
return this.GetVirtualIndex(index);
|
||||
}
|
||||
|
||||
/// <summary>Not supported.</summary>
|
||||
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
|
||||
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Not supported.</summary>
|
||||
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
|
||||
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Find wether an item exists.</summary>
|
||||
/// <param name="item">item to find.</param>
|
||||
/// <returns>Wether <paramref name="item"/> is found.</returns>
|
||||
public bool Contains(T item) => this.items.Contains(item);
|
||||
|
||||
/// <summary>Copies the content of this list into an array.</summary>
|
||||
/// <param name="array">Array to copy into.</param>
|
||||
/// <param name="arrayIndex"><paramref name="array"/> index to start coping into.</param>
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(arrayIndex), arrayIndex, 0);
|
||||
if (array.Length - arrayIndex < this.Count) ThrowHelper.ThrowArgumentException("Not enough space");
|
||||
for (var index = 0; index < this.Count; index++)
|
||||
{
|
||||
array[arrayIndex++] = this[index];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Not supported.</summary>
|
||||
[SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")]
|
||||
[SuppressMessage("Documentation Rules", "SA1615", Justification = "Not supported")]
|
||||
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>Gets an enumerator for this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <returns><see cref="RollingList{T}"/> enumerator.</returns>
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
for (var index = 0; index < this.items.Count; index++)
|
||||
{
|
||||
yield return this.items[this.GetRealIndex(index)];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets an enumerator for this <see cref="RollingList{T}"/>.</summary>
|
||||
/// <returns><see cref="RollingList{T}"/> enumerator.</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int GetRealIndex(int index) => this.size > 0 ? (index + this.firstIndex) % this.size : 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int GetVirtualIndex(int index) => this.size > 0 ? (this.size + index - this.firstIndex) % this.size : 0;
|
||||
}
|
||||
}
|
||||
52
Dalamud/Utility/ThrowHelper.cs
Normal file
52
Dalamud/Utility/ThrowHelper.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Utility
|
||||
{
|
||||
/// <summary>Helper methods for throwing exceptions.</summary>
|
||||
internal static class ThrowHelper
|
||||
{
|
||||
/// <summary>Throws a <see cref="ArgumentException"/> with a specified <paramref name="message"/>.</summary>
|
||||
/// <param name="message">Message for the exception.</param>
|
||||
/// <exception cref="ArgumentException">Thrown by this method.</exception>
|
||||
[DoesNotReturn]
|
||||
public static void ThrowArgumentException(string message) => throw new ArgumentException(message);
|
||||
|
||||
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> with a specified <paramref name="message"/> for a specified <paramref name="paramName"/>.</summary>
|
||||
/// <param name="paramName">Parameter name.</param>
|
||||
/// <param name="message">Message for the exception.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method.</exception>
|
||||
[DoesNotReturn]
|
||||
public static void ThrowArgumentOutOfRangeException(string paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message);
|
||||
|
||||
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> if the specified <paramref name="value"/> is less than <paramref name="comparand"/>.</summary>
|
||||
/// <typeparam name="T"><see cref="IComparable{T}"/> value type.</typeparam>
|
||||
/// <param name="paramName">Parameter name.</param>
|
||||
/// <param name="value">Value to compare from.</param>
|
||||
/// <param name="comparand">Value to compare with.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method if <paramref name="value"/> is less than <paramref name="comparand"/>.</exception>
|
||||
public static void ThrowArgumentOutOfRangeExceptionIfLessThan<T>(string paramName, T value, T comparand) where T : IComparable<T>
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, comparand);
|
||||
#else
|
||||
if (Comparer<T>.Default.Compare(value, comparand) <= -1) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be greater than or equal {comparand}");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Throws a <see cref="ArgumentOutOfRangeException"/> if the specified <paramref name="value"/> is greater than or equal to <paramref name="comparand"/>.</summary>
|
||||
/// <typeparam name="T"><see cref="IComparable{T}"/> value type.</typeparam>
|
||||
/// <param name="paramName">Parameter name.</param>
|
||||
/// <param name="value">Value to compare from.</param>
|
||||
/// <param name="comparand">Value to compare with.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown by this method if <paramref name="value"/> is greater than or equal to<paramref name="comparand"/>.</exception>
|
||||
public static void ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual<T>(string paramName, T value, T comparand) where T : IComparable<T>
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, comparand);
|
||||
#else
|
||||
if (Comparer<T>.Default.Compare(value, comparand) >= 0) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be less than {comparand}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using System.Reflection.Emit;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
|
|
@ -22,6 +23,9 @@ using Dalamud.Logging.Internal;
|
|||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Serilog;
|
||||
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
using Windows.Win32.Storage.FileSystem;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
|
@ -684,6 +688,55 @@ public static class Util
|
|||
return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws a corresponding exception if <see cref="HRESULT.FAILED"/> is true.
|
||||
/// </summary>
|
||||
/// <param name="hr">The result value.</param>
|
||||
internal static void ThrowOnError(this HRESULT hr)
|
||||
{
|
||||
if (hr.FAILED)
|
||||
Marshal.ThrowExceptionForHR(hr.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
|
||||
/// </summary>
|
||||
/// <param name="t">The task.</param>
|
||||
/// <param name="ex">The exception to set.</param>
|
||||
internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex)
|
||||
{
|
||||
if (t.Task.IsCompleted)
|
||||
return;
|
||||
try
|
||||
{
|
||||
t.SetException(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <param name="t">The task.</param>
|
||||
/// <param name="ex">The exception to set.</param>
|
||||
internal static void SetExceptionIfIncomplete<T>(this TaskCompletionSource<T> t, Exception ex)
|
||||
{
|
||||
if (t.Task.IsCompleted)
|
||||
return;
|
||||
try
|
||||
{
|
||||
t.SetException(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print formatted GameObject Information to ImGui.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue