Merge pull request #1766 from goatcorp/new_im_hooks-rollup

[new_im_hooks] Rollup changes from master
This commit is contained in:
KazWolfe 2024-04-28 11:51:15 -07:00 committed by GitHub
commit b2b894b1cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 376 additions and 114 deletions

2
.gitmodules vendored
View file

@ -3,7 +3,7 @@
url = https://github.com/goatcorp/ImGuiScene
[submodule "lib/FFXIVClientStructs"]
path = lib/FFXIVClientStructs
url = https://github.com/aers/FFXIVClientStructs.git
url = https://github.com/aers/FFXIVClientStructs
[submodule "lib/Nomade040-nmd"]
path = lib/Nomade040-nmd
url = https://github.com/Nomade040/nmd.git

View file

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="3.16.0" />
<PackageReference Include="Lumina" Version="3.17.0" />
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>9.1.0.5</DalamudVersion>
<DalamudVersion>9.1.0.6</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
@ -68,9 +68,9 @@
<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.16.0" />
<PackageReference Include="Lumina" Version="3.17.0" />
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using System.IO;
using System.Threading;
@ -52,15 +51,12 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
DefaultExcelLanguage = this.Language.ToLumina(),
};
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
this.GameData = new(
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
luminaOptions)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName)!, "sqpack"), luminaOptions);
}
else
{
throw new Exception("Could not main module.");
}
StreamPool = new(),
};
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
@ -107,7 +103,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
Log.Error(ex, "Could not initialize Lumina");
throw;
}
}
@ -161,6 +158,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
void IInternalDisposableService.DisposeService()
{
this.luminaCancellationTokenSource.Cancel();
this.GameData.Dispose();
}
private class LauncherTroubleshootingInfo

View file

@ -47,14 +47,14 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
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 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 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);
private 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;
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();
@ -171,7 +171,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
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 itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority).ToArray();
var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray();
var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray();
@ -268,10 +268,10 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
foreach (var item in items)
{
if (!item.Prefix.HasValue)
if (!item.Prefix.HasValue && !item.UseDefaultPrefix)
{
item.PrefixChar = 'D';
item.PrefixColor = 539;
item.Prefix = MenuItem.DalamudDefaultPrefix;
item.PrefixColor = MenuItem.DalamudDefaultPrefixColor;
Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix.");
}
}
@ -378,13 +378,13 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
{
// 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)
const int maxMenuItems = 31;
if (items.Count + nativeMenuSize > maxMenuItems)
{
Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating.");
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)..];
var newItems = orderedItems[..(maxMenuItems - nativeMenuSize - 1)];
var submenuItems = orderedItems[(maxMenuItems - nativeMenuSize - 1)..];
return newItems.Append(new MenuItem
{
Prefix = SeIconChar.BoxedLetterD,
@ -450,7 +450,6 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
if (callbackId < 0)
{
selectedIdx = -callbackId - 1;
goto original;
}
else
{
@ -461,17 +460,17 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
{
if (item.OnClicked == null)
throw new InvalidOperationException("Item has no OnClicked handler");
item.OnClicked.InvokeSafely(new(
(name, items) =>
item.OnClicked.InvokeSafely(new MenuItemClickedArgs(
(name, submenuItems) =>
{
short x, y;
addon->AtkUnitBase.GetPosition(&x, &y);
this.OpenSubmenu(name ?? item.Name, items, x, y);
this.OpenSubmenu(name ?? item.Name, submenuItems, x, y);
openedSubmenu = true;
},
this.SelectedParentAddon,
this.SelectedAgent,
this.SelectedMenuType.Value,
this.SelectedMenuType ?? default,
this.SelectedEventInterfaces));
}
catch (Exception e)
@ -479,14 +478,14 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
Log.Error(e, "Error while handling context menu click");
}
// Close with clicky sound
// Close with click sound
if (!openedSubmenu)
addon->AtkUnitBase.FireCallbackInt(-2);
return false;
}
original:
// Eventually handled by inventorycontext here: 14022BBD0 (6.51)
// Eventually handled by inventory context here: 14022BBD0 (6.51)
return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3);
}
}
@ -511,7 +510,7 @@ internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMen
}
/// <inheritdoc/>
public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened;
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
private Dictionary<ContextMenuType, List<MenuItem>> MenuItems { get; } = new();

View file

@ -70,8 +70,18 @@ public abstract unsafe class MenuArgs
/// 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");
public IReadOnlySet<nint> EventInterfaces
{
get
{
if (this.MenuType is ContextMenuType.Default)
{
return this.eventInterfaces ?? new HashSet<nint>();
}
else
{
throw new InvalidOperationException("Not a default context menu");
}
}
}
}

View file

@ -10,6 +10,16 @@ namespace Dalamud.Game.Gui.ContextMenu;
/// </summary>
public sealed record MenuItem
{
/// <summary>
/// The default prefix used if no specific preset is specified.
/// </summary>
public const SeIconChar DalamudDefaultPrefix = SeIconChar.BoxedLetterD;
/// <summary>
/// The default prefix color used if no specific preset is specified.
/// </summary>
public const ushort DalamudDefaultPrefixColor = 539;
/// <summary>
/// Gets or sets the display name of the menu item.
/// </summary>
@ -46,6 +56,11 @@ public sealed record MenuItem
/// 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 a value indicating whether the dev wishes to intentionally use the default prefix symbol and color.
/// </summary>
public bool UseDefaultPrefix { get; set; }
/// <summary>
/// Gets or sets the callback to be invoked when the menu item is clicked.

View file

@ -73,13 +73,16 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.configuration.QueueSave();
}
/// <inheritdoc/>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.entries;
/// <inheritdoc/>
public DtrBarEntry Get(string title, SeString? text = null)
{
if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
var entry = new DtrBarEntry(title, null);
var entry = new DtrBarEntry(this.configuration, title, null);
entry.Text = text;
// Add the entry to the end of the order list, if it's not there already.
@ -196,7 +199,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
foreach (var data in this.entries)
{
var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown;
var isHide = data.UserHidden || !data.Shown;
if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null })
{
@ -499,6 +502,9 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
private readonly Dictionary<string, DtrBarEntry> pluginEntries = new();
/// <inheritdoc/>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -510,7 +516,7 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
this.pluginEntries.Clear();
}
/// <inheritdoc/>
public DtrBarEntry Get(string title, SeString? text = null)
{

View file

@ -1,37 +1,115 @@
using System;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// Interface representing a read-only entry in the server info bar.
/// </summary>
public interface IReadOnlyDtrBarEntry
{
/// <summary>
/// Gets the title of this entry.
/// </summary>
public string Title { get; }
/// <summary>
/// Gets a value indicating whether this entry has a click action.
/// </summary>
public bool HasClickAction { get; }
/// <summary>
/// Gets the text of this entry.
/// </summary>
public SeString Text { get; }
/// <summary>
/// Gets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
public SeString Tooltip { get; }
/// <summary>
/// Gets a value indicating whether this entry should be shown.
/// </summary>
public bool Shown { get; }
/// <summary>
/// Gets a value indicating whether or not the user has hidden this entry from view through the Dalamud settings.
/// </summary>
public bool UserHidden { get; }
/// <summary>
/// Triggers the click action of this entry.
/// </summary>
/// <returns>True, if a click action was registered and executed.</returns>
public bool TriggerClickAction();
}
/// <summary>
/// Interface representing an entry in the server info bar.
/// </summary>
public interface IDtrBarEntry : IReadOnlyDtrBarEntry
{
/// <summary>
/// Gets or sets the text of this entry.
/// </summary>
public new SeString? Text { get; set; }
/// <summary>
/// Gets or sets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
public new SeString? Tooltip { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public new bool Shown { get; set; }
/// <summary>
/// Gets or sets a action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action? OnClick { get; set; }
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.
/// </summary>
public void Remove();
}
/// <summary>
/// Class representing an entry in the server info bar.
/// </summary>
public sealed unsafe class DtrBarEntry : IDisposable
public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
{
private readonly DalamudConfiguration configuration;
private bool shownBacking = true;
private SeString? textBacking = null;
/// <summary>
/// Initializes a new instance of the <see cref="DtrBarEntry"/> class.
/// </summary>
/// <param name="configuration">Dalamud configuration, used to check if the entry is hidden by the user.</param>
/// <param name="title">The title of the bar entry.</param>
/// <param name="textNode">The corresponding text node.</param>
internal DtrBarEntry(string title, AtkTextNode* textNode)
internal DtrBarEntry(DalamudConfiguration configuration, string title, AtkTextNode* textNode)
{
this.configuration = configuration;
this.Title = title;
this.TextNode = textNode;
}
/// <summary>
/// Gets the title of this entry.
/// </summary>
/// <inheritdoc/>
public string Title { get; init; }
/// <summary>
/// Gets or sets the text of this entry.
/// </summary>
/// <inheritdoc cref="IDtrBarEntry.Text" />
public SeString? Text
{
get => this.textBacking;
@ -41,10 +119,8 @@ public sealed unsafe class DtrBarEntry : IDisposable
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
/// <inheritdoc cref="IDtrBarEntry.Tooltip" />
public SeString? Tooltip { get; set; }
/// <summary>
@ -52,9 +128,10 @@ public sealed unsafe class DtrBarEntry : IDisposable
/// </summary>
public Action? OnClick { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
/// <inheritdoc/>
public bool HasClickAction => this.OnClick != null;
/// <inheritdoc cref="IDtrBarEntry.Shown" />
public bool Shown
{
get => this.shownBacking;
@ -65,6 +142,10 @@ public sealed unsafe class DtrBarEntry : IDisposable
}
}
/// <inheritdoc/>
[Api10ToDo("Maybe make this config scoped to internalname?")]
public bool UserHidden => this.configuration.DtrIgnore?.Any(x => x == this.Title) ?? false;
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>
@ -84,6 +165,16 @@ public sealed unsafe class DtrBarEntry : IDisposable
/// Gets or sets a value indicating whether this entry has just been added.
/// </summary>
internal bool Added { get; set; } = false;
/// <inheritdoc/>
public bool TriggerClickAction()
{
if (this.OnClick == null)
return false;
this.OnClick.Invoke();
return true;
}
/// <summary>
/// Remove this entry from the bar.

View file

@ -23,13 +23,16 @@ using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
using ImGuiScene;
using JetBrains.Annotations;
using PInvoke;
using Serilog;
using SharpDX;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
@ -67,6 +70,8 @@ internal class InterfaceManager : IInternalDisposableService
/// </summary>
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f;
private static readonly ModuleLog Log = new("INTERFACE");
private readonly ConcurrentBag<IDeferredDisposable> deferredDisposeTextures = new();
private readonly ConcurrentBag<ILockedImFont> deferredDisposeImFontLockeds = new();
@ -146,6 +151,13 @@ internal class InterfaceManager : IInternalDisposableService
public static ImFontPtr IconFont =>
WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets an included FontAwesome icon font with fixed width.
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
/// </summary>
public static ImFontPtr IconFontFixedWidth =>
WhenFontsReady().IconFontFixedWidthHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault);
/// <summary>
/// Gets an included monospaced font.<br />
/// <strong>Accessing this static property outside of the main thread is dangerous and not supported.</strong>
@ -163,6 +175,11 @@ internal class InterfaceManager : IInternalDisposableService
/// </summary>
public FontHandle? IconFontHandle { get; private set; }
/// <summary>
/// Gets the icon font handle with fixed width.
/// </summary>
public FontHandle? IconFontFixedWidthHandle { get; private set; }
/// <summary>
/// Gets the mono font handle.
/// </summary>
@ -396,7 +413,7 @@ internal class InterfaceManager : IInternalDisposableService
});
}
}
// no sampler for now because the ImGui implementation we copied doesn't allow for changing it
return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height));
}
@ -492,7 +509,7 @@ internal class InterfaceManager : IInternalDisposableService
atlas.BuildTask.GetAwaiter().GetResult();
return im;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void RenderImGui(RawDX11Scene scene)
{
@ -806,6 +823,14 @@ internal class InterfaceManager : IInternalDisposableService
GlyphMinAdvanceX = DefaultFontSizePx,
GlyphMaxAdvanceX = DefaultFontSizePx,
})));
this.IconFontFixedWidthHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(tk => tk.AddDalamudAssetFont(
DalamudAsset.FontAwesomeFreeSolid,
new()
{
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
GlyphRanges = new ushort[] { 0x20, 0x20, 0x00 },
})));
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
e => e.OnPreBuild(
tk => tk.AddDalamudAssetFont(
@ -822,6 +847,13 @@ internal class InterfaceManager : IInternalDisposableService
tk.GetFont(this.DefaultFontHandle),
tk.GetFont(this.MonoFontHandle),
missingOnly: true);
// Fill missing glyphs in IconFontFixedWidth with IconFont and fit ratio
tk.CopyGlyphsAcrossFonts(
tk.GetFont(this.IconFontHandle),
tk.GetFont(this.IconFontFixedWidthHandle),
missingOnly: true);
tk.FitRatio(tk.GetFont(this.IconFontFixedWidthHandle));
});
this.DefaultFontHandle.ImFontChanged += (_, font) =>
{
@ -844,7 +876,7 @@ internal class InterfaceManager : IInternalDisposableService
});
};
}
// This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene.
_ = this.dalamudAtlas.BuildFontsAsync();
}

View file

@ -18,6 +18,7 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
private int selectedIconCategory;
private string iconSearchInput = string.Empty;
private bool iconSearchChanged = true;
private bool useFixedWidth = false;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" };
@ -80,6 +81,8 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
{
this.iconSearchChanged = true;
}
ImGui.Checkbox("Use fixed width font", ref this.useFixedWidth);
ImGuiHelpers.ScaledDummy(10f);
for (var i = 0; i < this.icons?.Count; i++)
@ -88,7 +91,7 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledRelativeSameLine(50f);
ImGui.Text($"{this.iconNames?[i]}");
ImGuiHelpers.ScaledRelativeSameLine(280f);
ImGui.PushFont(UiBuilder.IconFont);
ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
ImGui.Text(this.icons[i].ToIconString());
ImGui.PopFont();
ImGuiHelpers.ScaledDummy(2f);

View file

@ -1128,34 +1128,33 @@ internal class PluginInstallerWindow : Window, IDisposable
this.DrawChangelog(logEntry);
}
}
private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin);
#pragma warning disable SA1201
private void DrawAvailablePluginList()
#pragma warning restore SA1201
private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin);
private IEnumerable<PluginInstallerAvailablePluginProxy> GatherProxies()
{
var proxies = new List<PluginInstallerAvailablePluginProxy>();
var availableManifests = this.pluginListAvailable;
var installedPlugins = this.pluginListInstalled.ToList(); // Copy intended
if (availableManifests.Count == 0)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible);
return;
return proxies;
}
var filteredAvailableManifests = availableManifests
.Where(rm => !this.IsManifestFiltered(rm))
.ToList();
.Where(rm => !this.IsManifestFiltered(rm))
.ToList();
if (filteredAvailableManifests.Count == 0)
{
ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
return;
return proxies;
}
var proxies = new List<PluginInstallerAvailablePluginProxy>();
// Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile
foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast<RemotePluginManifest>())
{
@ -1168,7 +1167,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (plugin != null)
{
installedPlugins.Remove(plugin);
proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin));
proxies.Add(new PluginInstallerAvailablePluginProxy(availableManifest, plugin));
continue;
}
@ -1187,8 +1186,14 @@ internal class PluginInstallerWindow : Window, IDisposable
proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin));
}
return proxies;
}
#pragma warning restore SA1201
private void DrawAvailablePluginList()
{
var i = 0;
foreach (var proxy in proxies)
foreach (var proxy in this.GatherProxies())
{
IPluginManifest applicableManifest = proxy.LocalPlugin != null ? proxy.LocalPlugin.Manifest : proxy.RemoteManifest;
@ -1199,7 +1204,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (proxy.LocalPlugin != null)
{
this.DrawInstalledPlugin(proxy.LocalPlugin, i++, true);
this.DrawInstalledPlugin(proxy.LocalPlugin, i++, proxy.RemoteManifest, true);
}
else if (proxy.RemoteManifest != null)
{
@ -1237,7 +1242,12 @@ internal class PluginInstallerWindow : Window, IDisposable
if (filterTesting && !manager.HasTestingOptIn(plugin.Manifest))
continue;
this.DrawInstalledPlugin(plugin, i++);
// Find the applicable remote manifest
var remoteManifest = this.pluginListAvailable
.FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName &&
rm.RepoUrl == plugin.Manifest.RepoUrl);
this.DrawInstalledPlugin(plugin, i++, remoteManifest);
}
}
@ -1266,7 +1276,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var i = 0;
foreach (var plugin in filteredList)
{
this.DrawInstalledPlugin(plugin, i++);
this.DrawInstalledPlugin(plugin, i++, null);
}
}
@ -2173,17 +2183,18 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGuiHelpers.ScaledDummy(10);
ImGui.SameLine();
this.DrawVisitRepoUrlButton(manifest.RepoUrl, true);
if (this.DrawVisitRepoUrlButton(manifest.RepoUrl, true))
{
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(3);
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(3);
ImGui.SameLine();
if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback)
{
ImGui.SameLine();
this.DrawSendFeedbackButton(manifest, false, true);
}
ImGuiHelpers.ScaledDummy(5);
if (this.DrawPluginImages(null, manifest, isThirdParty, index))
@ -2251,7 +2262,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
private void DrawInstalledPlugin(LocalPlugin plugin, int index, bool showInstalled = false)
private void DrawInstalledPlugin(LocalPlugin plugin, int index, RemotePluginManifest? remoteManifest, bool showInstalled = false)
{
var configuration = Service<DalamudConfiguration>.Get();
var commandManager = Service<CommandManager>.Get();
@ -2376,7 +2387,9 @@ internal class PluginInstallerWindow : Window, IDisposable
}
ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}");
var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty();
var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog;
var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
var didDrawChangelogInsideCollapsible = false;
if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index))
@ -2473,11 +2486,15 @@ internal class PluginInstallerWindow : Window, IDisposable
if (canFeedback)
{
ImGui.SameLine();
this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting, false);
}
if (availablePluginUpdate != default && !plugin.IsDev)
{
ImGui.SameLine();
this.DrawUpdateSinglePluginButton(availablePluginUpdate);
}
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.EffectiveVersion}");
@ -2494,7 +2511,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion)))
{
didDrawChangelogInsideCollapsible = true;
this.DrawInstalledPluginChangelog(plugin.Manifest);
this.DrawInstalledPluginChangelog(applicableChangelog);
ImGui.TreePop();
}
}
@ -2502,9 +2519,10 @@ internal class PluginInstallerWindow : Window, IDisposable
if (availablePluginUpdate != default && !availablePluginUpdate.UpdateManifest.Changelog.IsNullOrWhitespace())
{
var availablePluginUpdateVersion = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : availablePluginUpdate.UpdateManifest.AssemblyVersion;
if (ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion)))
var availableChangelog = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingChangelog : availablePluginUpdate.UpdateManifest.Changelog;
if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion)))
{
this.DrawInstalledPluginChangelog(availablePluginUpdate.UpdateManifest);
this.DrawInstalledPluginChangelog(availableChangelog);
ImGui.TreePop();
}
}
@ -2512,13 +2530,13 @@ internal class PluginInstallerWindow : Window, IDisposable
if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible)
{
this.DrawInstalledPluginChangelog(plugin.Manifest);
this.DrawInstalledPluginChangelog(applicableChangelog);
}
ImGui.PopID();
}
private void DrawInstalledPluginChangelog(IPluginManifest manifest)
private void DrawInstalledPluginChangelog(string changelog)
{
ImGuiHelpers.ScaledDummy(5);
@ -2531,7 +2549,7 @@ internal class PluginInstallerWindow : Window, IDisposable
{
ImGui.Text("Changelog:");
ImGuiHelpers.ScaledDummy(2);
ImGuiHelpers.SafeTextWrapped(manifest.Changelog!);
ImGuiHelpers.SafeTextWrapped(changelog!);
}
ImGui.EndChild();
@ -2878,8 +2896,6 @@ internal class PluginInstallerWindow : Window, IDisposable
private void DrawUpdateSinglePluginButton(AvailablePluginUpdate update)
{
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Download))
{
Task.Run(() => this.UpdateSinglePlugin(update));
@ -2949,8 +2965,6 @@ internal class PluginInstallerWindow : Window, IDisposable
private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting, bool big)
{
ImGui.SameLine();
var clicked = big ?
ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Comment, Locs.FeedbackModal_Title) :
ImGuiComponents.IconButton(FontAwesomeIcon.Comment);
@ -3195,7 +3209,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
private void DrawVisitRepoUrlButton(string? repoUrl, bool big)
private bool DrawVisitRepoUrlButton(string? repoUrl, bool big)
{
if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://"))
{
@ -3222,7 +3236,11 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_VisitPluginUrl);
return true;
}
return false;
}
private bool DrawPluginImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, int index)

View file

@ -142,6 +142,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable
var scale = ImGui.GetIO().FontGlobalScale;
var entries = this.titleScreenMenu.Entries;
var hovered = ImGui.IsWindowHovered(
ImGuiHoveredFlags.RootAndChildWindows |
ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
Service<InterfaceManager>.Get().OverrideGameCursor = !hovered;
switch (this.state)
{
case State.Show:
@ -187,8 +193,11 @@ internal class TitleScreenMenuWindow : Window, IDisposable
i++;
}
if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows |
ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
// Don't check for hover if we're in the middle of an animation, as it will cause flickering.
if (this.moveEasings.Any(x => !x.Value.IsDone))
break;
if (!hovered)
{
this.state = State.FadeOut;
}

View file

@ -1,4 +1,4 @@
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using ImGuiNET;
@ -27,6 +27,13 @@ public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
/// <returns>The texture index.</returns>
int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError);
/// <summary>
/// Fits a font to a fixed 1:1 ratio adjusting glyph positions horizontally and vertically to fit within font size boundaries.
/// </summary>
/// <param name="font">The font to fit.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
void FitRatio(ImFontPtr font, bool rebuildLookupTable = true);
/// <summary>
/// Copies glyphs across fonts, in a safer way.<br />
/// If the font does not belong to the current atlas, this function is a no-op.

View file

@ -1,4 +1,4 @@
using System.Buffers;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -166,7 +166,7 @@ internal sealed partial class FontAtlasFactory
/// <inheritdoc/>
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
this.data.AddNewTexture(textureWrap, disposeOnError);
/// <inheritdoc/>
public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action);
@ -391,12 +391,10 @@ internal sealed partial class FontAtlasFactory
});
case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile:
{
return this.AddGameGlyphs(
new(GameFontFamily.Axis, fontConfig.SizePx),
fontConfig.GlyphRanges,
fontConfig.MergeFont);
}
default:
return this.factory.AddFont(
@ -858,5 +856,30 @@ internal sealed partial class FontAtlasFactory
}
}
}
/// <inheritdoc/>
public void FitRatio(ImFontPtr font, bool rebuildLookupTable = true)
{
var nsize = font.FontSize;
var glyphs = font.GlyphsWrapped();
foreach (ref var glyph in glyphs.DataSpan)
{
var ratio = 1f;
if (glyph.X1 - glyph.X0 > nsize)
ratio = Math.Max(ratio, (glyph.X1 - glyph.X0) / nsize);
if (glyph.Y1 - glyph.Y0 > nsize)
ratio = Math.Max(ratio, (glyph.Y1 - glyph.Y0) / nsize);
var w = MathF.Round((glyph.X1 - glyph.X0) / ratio, MidpointRounding.ToZero);
var h = MathF.Round((glyph.Y1 - glyph.Y0) / ratio, MidpointRounding.AwayFromZero);
glyph.X0 = MathF.Round((nsize - w) / 2f, MidpointRounding.ToZero);
glyph.Y0 = MathF.Round((nsize - h) / 2f, MidpointRounding.AwayFromZero);
glyph.X1 = glyph.X0 + w;
glyph.Y1 = glyph.Y0 + h;
glyph.AdvanceX = nsize;
}
if (rebuildLookupTable)
this.BuildLookupTable(font);
}
}
}

View file

@ -21,9 +21,13 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using ImGuiNET;
using ImGuiScene;
using Serilog;
using SharpDX.Direct3D11;
namespace Dalamud.Interface;
@ -53,6 +57,7 @@ public sealed class UiBuilder : IDisposable
private IFontHandle? defaultFontHandle;
private IFontHandle? iconFontHandle;
private IFontHandle? monoFontHandle;
private IFontHandle? iconFontFixedWidthHandle;
/// <summary>
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
@ -106,7 +111,7 @@ public sealed class UiBuilder : IDisposable
/// Event that is fired when the plugin should open its configuration interface.
/// </summary>
public event Action OpenConfigUi;
/// <summary>
/// Event that is fired when the plugin should open its main interface.
/// </summary>
@ -251,6 +256,16 @@ public sealed class UiBuilder : IDisposable
this.InterfaceManagerWithScene?.IconFontHandle
?? throw new InvalidOperationException("Scene is not yet ready.")));
/// <summary>
/// Gets the default Dalamud icon font based on FontAwesome 5 free solid with a fixed width and vertically centered glyphs.
/// </summary>
public IFontHandle IconFontFixedWidthHandle =>
this.iconFontFixedWidthHandle ??=
this.scopedFinalizer.Add(
new FontHandleWrapper(
this.InterfaceManagerWithScene?.IconFontFixedWidthHandle
?? throw new InvalidOperationException("Scene is not yet ready.")));
/// <summary>
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.
/// </summary>
@ -266,7 +281,7 @@ public sealed class UiBuilder : IDisposable
/// new() { SizePx = UiBuilder.DefaultFontSizePx })));
/// </code>
/// </remarks>
public IFontHandle MonoFontHandle =>
public IFontHandle MonoFontHandle =>
this.monoFontHandle ??=
this.scopedFinalizer.Add(
new FontHandleWrapper(
@ -630,7 +645,7 @@ public sealed class UiBuilder : IDisposable
{
this.OpenConfigUi?.InvokeSafely();
}
/// <summary>
/// Open the registered configuration UI, if it exists.
/// </summary>
@ -838,5 +853,5 @@ public sealed class UiBuilder : IDisposable
private void WrappedOnImFontChanged(IFontHandle obj, ILockedImFont lockedFont) =>
this.ImFontChanged?.Invoke(obj, lockedFont);
}
}
}

View file

@ -1,5 +1,8 @@
using System;
using Dalamud.Utility;
namespace Dalamud.Plugin;
[Api10ToDo("Refactor into an interface, add wrappers for OpenMainUI and OpenConfigUI")]
public record InstalledPluginState(string Name, string InternalName, bool IsLoaded, Version Version);

View file

@ -16,6 +16,11 @@ internal record RemotePluginManifest : PluginManifest
/// </summary>
[JsonIgnore]
public PluginRepository SourceRepo { get; set; } = null!;
/// <summary>
/// Gets or sets the changelog to be shown when obtaining the testing version of the plugin.
/// </summary>
public string? TestingChangelog { get; set; }
/// <summary>
/// Gets a value indicating whether this plugin is eligible for testing.

View file

@ -166,7 +166,12 @@ internal class CallGateChannel
if (arg == null)
{
if (paramType.IsValueType)
{
if (paramType.IsGenericType && paramType.GetGenericTypeDefinition() == typeof(Nullable<>))
continue;
throw new IpcValueNullError(this.Name, paramType, i);
}
continue;
}

View file

@ -1,6 +1,4 @@
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
namespace Dalamud.Plugin.Services;
@ -16,8 +14,9 @@ public interface IContextMenu
public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args);
/// <summary>
/// Event that gets fired every time the game framework updates.
/// Event that gets fired whenever any context menu is opened.
/// </summary>
/// <remarks>Use this event and then check if the triggering addon is the desired addon, then add custom context menu items to the provided args.</remarks>
event OnMenuOpenedDelegate OnMenuOpened;
/// <summary>
@ -25,6 +24,7 @@ public interface IContextMenu
/// </summary>
/// <param name="menuType">The type of context menu to add the item to.</param>
/// <param name="item">The item to add.</param>
/// <remarks>Used to add a context menu entry to <em>all</em> context menus.</remarks>
void AddMenuItem(ContextMenuType menuType, MenuItem item);
/// <summary>
@ -32,6 +32,7 @@ public interface IContextMenu
/// </summary>
/// <param name="menuType">The type of context menu to remove the item from.</param>
/// <param name="item">The item to add.</param>
/// <remarks>Used to remove a context menu entry from <em>all</em> context menus.</remarks>
/// <returns><see langword="true"/> if the item was removed, <see langword="false"/> if it was not found.</returns>
bool RemoveMenuItem(ContextMenuType menuType, MenuItem item);
}

View file

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
namespace Dalamud.Plugin.Services;
@ -10,6 +12,11 @@ namespace Dalamud.Plugin.Services;
/// </summary>
public interface IDtrBar
{
/// <summary>
/// Gets a read-only list of all DTR bar entries.
/// </summary>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries { get; }
/// <summary>
/// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
@ -18,6 +25,7 @@ public interface IDtrBar
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
[Api10ToDo("Return IDtrBarEntry instead of DtrBarEntry")]
public DtrBarEntry Get(string title, SeString? text = null);
/// <summary>

View file

@ -43,6 +43,7 @@ internal static class ServiceManager
#endif
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
private static readonly CancellationTokenSource UnloadCancellationTokenSource = new();
private static ManualResetEvent unloadResetEvent = new(false);
@ -107,6 +108,12 @@ internal static class ServiceManager
/// Gets task that gets completed when all blocking early loading services are done loading.
/// </summary>
public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task;
/// <summary>
/// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state
/// during initialization or during regular operation.
/// </summary>
public static CancellationToken UnloadCancellationToken => UnloadCancellationTokenSource.Token;
/// <summary>
/// Initializes Provided Services and FFXIVClientStructs.
@ -374,6 +381,8 @@ internal static class ServiceManager
}
catch (Exception e)
{
UnloadCancellationTokenSource.Cancel();
Log.Error(e, "Failed resolving services");
try
{
@ -401,6 +410,8 @@ internal static class ServiceManager
/// </summary>
public static void UnloadAllServices()
{
UnloadCancellationTokenSource.Cancel();
var framework = Service<Framework>.GetNullable(Service<Framework>.ExceptionPropagationMode.None);
if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false })
{

View file

@ -116,7 +116,7 @@ internal static class Service<T> where T : IServiceType
#endif
if (!instanceTcs.Task.IsCompleted)
instanceTcs.Task.Wait();
instanceTcs.Task.Wait(ServiceManager.UnloadCancellationToken);
return instanceTcs.Task.Result;
}

View file

@ -344,7 +344,10 @@ public static class Util
_ = NativeFunctions.MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
if (exit)
{
Log.CloseAndFlush();
Environment.Exit(-1);
}
}
/// <summary>

@ -1 +1 @@
Subproject commit 98c1de8b94bcdfce4dc79a61cc0e8b17773777f0
Subproject commit 3d572e5301fbcb3194d59d7c995db067eba5cc0b