mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-29 20:03:41 +01:00
Merge branch 'new_im_hooks' into feature/object-vtable-hook-with-swapchain
This commit is contained in:
commit
b241ffe008
20 changed files with 1175 additions and 146 deletions
|
|
@ -380,7 +380,7 @@ namespace Dalamud.Injector
|
|||
startInfo.BootShowConsole = args.Contains("--console");
|
||||
startInfo.BootEnableEtw = args.Contains("--etw");
|
||||
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
|
||||
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "clr_failfast_hijack", "prevent_icmphandle_crashes" };
|
||||
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" };
|
||||
startInfo.BootDotnetOpenProcessHookMode = 0;
|
||||
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
|
||||
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,12 @@ internal sealed class DalamudConfiguration : IServiceType
|
|||
/// </summary>
|
||||
public string LastVersion { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the last seen FTUE version.
|
||||
/// Unused for now, added to prevent existing users from seeing level 0 FTUE.
|
||||
/// </summary>
|
||||
public int SeenFtueLevel { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last loaded Dalamud version.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>7.9.0.0</DalamudVersion>
|
||||
<DalamudVersion>7.10.1.0</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
|
|
@ -16,9 +17,11 @@ using JetBrains.Annotations;
|
|||
using Lumina;
|
||||
using Lumina.Data;
|
||||
using Lumina.Data.Files;
|
||||
using Lumina.Data.Parsing.Tex.Buffers;
|
||||
using Lumina.Excel;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using SharpDX.DXGI;
|
||||
|
||||
namespace Dalamud.Data;
|
||||
|
||||
|
|
@ -185,15 +188,17 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
/// </summary>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
/// TODO(v9): remove in api9 in favor of GetIcon(uint iconId, bool highResolution)
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(uint iconId)
|
||||
=> this.GetIcon(this.Language, iconId, false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(uint iconId, bool highResolution)
|
||||
=> this.GetIcon(this.Language, iconId, highResolution);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(bool isHq, uint iconId)
|
||||
{
|
||||
var type = isHq ? "hq/" : string.Empty;
|
||||
|
|
@ -206,11 +211,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
/// <param name="iconLanguage">The requested language.</param>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
/// TODO(v9): remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
|
||||
=> this.GetIcon(iconLanguage, iconId, false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution)
|
||||
{
|
||||
var type = iconLanguage switch
|
||||
|
|
@ -231,11 +237,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
/// TODO(v9): remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution)
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(string? type, uint iconId)
|
||||
=> this.GetIcon(type, iconId, false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(string? type, uint iconId, bool highResolution)
|
||||
{
|
||||
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
|
||||
|
|
@ -257,14 +264,39 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetHqIcon(uint iconId)
|
||||
=> this.GetIcon(true, iconId);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
[return: NotNullIfNotNull(nameof(tex))]
|
||||
public TextureWrap? GetImGuiTexture(TexFile? tex)
|
||||
=> tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
|
||||
{
|
||||
if (tex is null)
|
||||
return null;
|
||||
|
||||
var im = Service<InterfaceManager>.Get();
|
||||
var buffer = tex.TextureBuffer;
|
||||
var bpp = 1 << (((int)tex.Header.Format & (int)TexFile.TextureFormat.BppMask) >>
|
||||
(int)TexFile.TextureFormat.BppShift);
|
||||
|
||||
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(tex.Header.Format, false);
|
||||
if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat))
|
||||
{
|
||||
dxgiFormat = (int)Format.B8G8R8A8_UNorm;
|
||||
buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8);
|
||||
bpp = 32;
|
||||
}
|
||||
|
||||
var pitch = buffer is BlockCompressionTextureBuffer
|
||||
? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp
|
||||
: ((buffer.Width * bpp) + 7) / 8;
|
||||
return im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTexture(string path)
|
||||
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
|
||||
|
||||
|
|
@ -274,26 +306,32 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager
|
|||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
|
||||
/// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution)
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(uint iconId)
|
||||
=> this.GetImGuiTexture(this.GetIcon(iconId, false));
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution)
|
||||
=> this.GetImGuiTexture(this.GetIcon(iconId, highResolution));
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
|
||||
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
|
||||
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
|
||||
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
|
||||
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
|
||||
|
||||
|
|
|
|||
|
|
@ -26,52 +26,52 @@ public unsafe class Character : GameObject
|
|||
/// <summary>
|
||||
/// Gets the current HP of this Chara.
|
||||
/// </summary>
|
||||
public uint CurrentHp => this.Struct->Health;
|
||||
public uint CurrentHp => this.Struct->CharacterData.Health;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum HP of this Chara.
|
||||
/// </summary>
|
||||
public uint MaxHp => this.Struct->MaxHealth;
|
||||
public uint MaxHp => this.Struct->CharacterData.MaxHealth;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current MP of this Chara.
|
||||
/// </summary>
|
||||
public uint CurrentMp => this.Struct->Mana;
|
||||
public uint CurrentMp => this.Struct->CharacterData.Mana;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum MP of this Chara.
|
||||
/// </summary>
|
||||
public uint MaxMp => this.Struct->MaxMana;
|
||||
public uint MaxMp => this.Struct->CharacterData.MaxMana;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current GP of this Chara.
|
||||
/// </summary>
|
||||
public uint CurrentGp => this.Struct->GatheringPoints;
|
||||
public uint CurrentGp => this.Struct->CharacterData.GatheringPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum GP of this Chara.
|
||||
/// </summary>
|
||||
public uint MaxGp => this.Struct->MaxGatheringPoints;
|
||||
public uint MaxGp => this.Struct->CharacterData.MaxGatheringPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current CP of this Chara.
|
||||
/// </summary>
|
||||
public uint CurrentCp => this.Struct->CraftingPoints;
|
||||
public uint CurrentCp => this.Struct->CharacterData.CraftingPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum CP of this Chara.
|
||||
/// </summary>
|
||||
public uint MaxCp => this.Struct->MaxCraftingPoints;
|
||||
public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ClassJob of this Chara.
|
||||
/// </summary>
|
||||
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->ClassJob);
|
||||
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->CharacterData.ClassJob);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the level of this Chara.
|
||||
/// </summary>
|
||||
public byte Level => this.Struct->Level;
|
||||
public byte Level => this.Struct->CharacterData.Level;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a byte array describing the visual appearance of this Chara.
|
||||
|
|
@ -97,7 +97,7 @@ public unsafe class Character : GameObject
|
|||
/// <summary>
|
||||
/// Gets the current online status of the character.
|
||||
/// </summary>
|
||||
public ExcelResolver<OnlineStatus> OnlineStatus => new(this.Struct->OnlineStatus);
|
||||
public ExcelResolver<OnlineStatus> OnlineStatus => new(this.Struct->CharacterData.OnlineStatus);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the status flags.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public static partial class ImGuiComponents
|
|||
|
||||
var text = icon.ToIconString();
|
||||
if (id.HasValue)
|
||||
text = $"{text}{id}";
|
||||
text = $"{text}##{id}";
|
||||
|
||||
var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,19 @@ 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 : TextureWrap
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safety harness for ImGuiScene textures that will defer destruction until
|
||||
/// the end of the frame.
|
||||
/// </summary>
|
||||
public class DalamudTextureWrap : TextureWrap
|
||||
public class DalamudTextureWrap : IDalamudTextureWrap
|
||||
{
|
||||
private readonly TextureWrap wrappedWrap;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ using ImGuiScene;
|
|||
using JetBrains.Annotations;
|
||||
using PInvoke;
|
||||
using Serilog;
|
||||
using SharpDX;
|
||||
using SharpDX.Direct3D;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
|
||||
// general dev notes, here because it's easiest
|
||||
|
||||
|
|
@ -323,6 +327,62 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the current D3D11 Device supports the given DXGI format.
|
||||
/// </summary>
|
||||
/// <param name="dxgiFormat">DXGI format to check.</param>
|
||||
/// <returns>Whether it is supported.</returns>
|
||||
public bool SupportsDxgiFormat(Format dxgiFormat) => this.scene is null
|
||||
? throw new InvalidOperationException("Scene isn't ready.")
|
||||
: this.scene.Device.CheckFormatSupport(dxgiFormat).HasFlag(FormatSupport.Texture2D);
|
||||
|
||||
/// <summary>
|
||||
/// Load an image from a span of bytes of specified format.
|
||||
/// </summary>
|
||||
/// <param name="data">The data to load.</param>
|
||||
/// <param name="pitch">The pitch(stride) in bytes.</param>
|
||||
/// <param name="width">The width in pixels.</param>
|
||||
/// <param name="height">The height in pixels.</param>
|
||||
/// <param name="dxgiFormat">Format of the texture.</param>
|
||||
/// <returns>A texture, ready to use in ImGui.</returns>
|
||||
public IDalamudTextureWrap LoadImageFromDxgiFormat(Span<byte> data, int pitch, int width, int height, Format dxgiFormat)
|
||||
{
|
||||
if (this.scene == null)
|
||||
throw new InvalidOperationException("Scene isn't ready.");
|
||||
|
||||
ShaderResourceView resView;
|
||||
unsafe
|
||||
{
|
||||
fixed (void* pData = data)
|
||||
{
|
||||
var texDesc = new Texture2DDescription
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
Format = dxgiFormat,
|
||||
SampleDescription = new(1, 0),
|
||||
Usage = ResourceUsage.Immutable,
|
||||
BindFlags = BindFlags.ShaderResource,
|
||||
CpuAccessFlags = CpuAccessFlags.None,
|
||||
OptionFlags = ResourceOptionFlags.None,
|
||||
};
|
||||
|
||||
using var texture = new Texture2D(this.Device, texDesc, new DataRectangle(new(pData), pitch));
|
||||
resView = new(this.Device, texture, new()
|
||||
{
|
||||
Format = texDesc.Format,
|
||||
Dimension = ShaderResourceViewDimension.Texture2D,
|
||||
Texture2D = { MipLevels = texDesc.MipLevels },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// no sampler for now because the ImGui implementation we copied doesn't allow for changing it
|
||||
return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height));
|
||||
}
|
||||
|
||||
#nullable restore
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -624,7 +684,11 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
}
|
||||
|
||||
this.RenderImGui();
|
||||
this.DisposeTextures();
|
||||
}
|
||||
|
||||
private void DisposeTextures()
|
||||
{
|
||||
if (this.deferredDisposeTextures.Count > 0)
|
||||
{
|
||||
Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count);
|
||||
|
|
|
|||
641
Dalamud/Interface/Internal/TextureManager.cs
Normal file
641
Dalamud/Interface/Internal/TextureManager.cs
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiScene;
|
||||
using Lumina.Data.Files;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for loading and disposing ImGui texture wraps.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<ITextureSubstitutionProvider>]
|
||||
#pragma warning restore SA1015
|
||||
internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionProvider
|
||||
{
|
||||
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
|
||||
private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex";
|
||||
|
||||
private const uint MillisecondsEvictionTime = 2000;
|
||||
|
||||
private static readonly ModuleLog Log = new("TEXM");
|
||||
|
||||
private readonly Framework framework;
|
||||
private readonly DataManager dataManager;
|
||||
private readonly InterfaceManager im;
|
||||
private readonly DalamudStartInfo startInfo;
|
||||
|
||||
private readonly Dictionary<string, TextureInfo> activeTextures = new();
|
||||
|
||||
private TextureWrap? fallbackTextureWrap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextureManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="framework">Framework instance.</param>
|
||||
/// <param name="dataManager">DataManager instance.</param>
|
||||
/// <param name="im">InterfaceManager instance.</param>
|
||||
/// <param name="startInfo">DalamudStartInfo instance.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public TextureManager(Framework framework, DataManager dataManager, InterfaceManager im, DalamudStartInfo startInfo)
|
||||
{
|
||||
this.framework = framework;
|
||||
this.dataManager = dataManager;
|
||||
this.im = im;
|
||||
this.startInfo = startInfo;
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
|
||||
Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync().ContinueWith(_ => this.CreateFallbackTexture());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad;
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for a specific icon.
|
||||
/// </summary>
|
||||
/// <param name="iconId">The ID of the icon to load.</param>
|
||||
/// <param name="flags">Options to be considered when loading the icon.</param>
|
||||
/// <param name="language">
|
||||
/// The language to be considered when loading the icon, if the icon has versions for multiple languages.
|
||||
/// If null, default to the game's current language.
|
||||
/// </param>
|
||||
/// <param name="keepAlive">
|
||||
/// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
|
||||
/// to render the icon.
|
||||
/// </returns>
|
||||
public TextureManagerTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false)
|
||||
{
|
||||
var path = this.GetIconPath(iconId, flags, language);
|
||||
return path == null ? null : this.CreateWrap(path, keepAlive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a path for a specific icon's .tex file.
|
||||
/// </summary>
|
||||
/// <param name="iconId">The ID of the icon to look up.</param>
|
||||
/// <param name="flags">Options to be considered when loading the icon.</param>
|
||||
/// <param name="language">
|
||||
/// The language to be considered when loading the icon, if the icon has versions for multiple languages.
|
||||
/// If null, default to the game's current language.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file,
|
||||
/// which can be loaded via IDataManager.
|
||||
/// </returns>
|
||||
public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null)
|
||||
{
|
||||
var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes);
|
||||
|
||||
// 1. Item
|
||||
var path = FormatIconPath(
|
||||
iconId,
|
||||
flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty,
|
||||
hiRes);
|
||||
if (this.dataManager.FileExists(path))
|
||||
return path;
|
||||
|
||||
language ??= this.startInfo.Language;
|
||||
var languageFolder = language switch
|
||||
{
|
||||
ClientLanguage.Japanese => "ja/",
|
||||
ClientLanguage.English => "en/",
|
||||
ClientLanguage.German => "de/",
|
||||
ClientLanguage.French => "fr/",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"),
|
||||
};
|
||||
|
||||
// 2. Regular icon, with language, hi-res
|
||||
path = FormatIconPath(
|
||||
iconId,
|
||||
languageFolder,
|
||||
hiRes);
|
||||
if (this.dataManager.FileExists(path))
|
||||
return path;
|
||||
|
||||
if (hiRes)
|
||||
{
|
||||
// 3. Regular icon, with language, no hi-res
|
||||
path = FormatIconPath(
|
||||
iconId,
|
||||
languageFolder,
|
||||
false);
|
||||
if (this.dataManager.FileExists(path))
|
||||
return path;
|
||||
}
|
||||
|
||||
// 4. Regular icon, without language, hi-res
|
||||
path = FormatIconPath(
|
||||
iconId,
|
||||
null,
|
||||
hiRes);
|
||||
if (this.dataManager.FileExists(path))
|
||||
return path;
|
||||
|
||||
// 4. Regular icon, without language, no hi-res
|
||||
if (hiRes)
|
||||
{
|
||||
path = FormatIconPath(
|
||||
iconId,
|
||||
null,
|
||||
false);
|
||||
if (this.dataManager.FileExists(path))
|
||||
return path;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for the texture at the specified path.
|
||||
/// You may only specify paths in the game's VFS.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the texture in the game's VFS.</param>
|
||||
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
|
||||
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
|
||||
public TextureManagerTextureWrap? GetTextureFromGame(string path, bool keepAlive = false)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
||||
if (Path.IsPathRooted(path))
|
||||
throw new ArgumentException("Use GetTextureFromFile() to load textures directly from a file.", nameof(path));
|
||||
|
||||
return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for the image or texture, specified by the passed FileInfo.
|
||||
/// You may only specify paths on the native file system.
|
||||
///
|
||||
/// This API can load .png and .tex files.
|
||||
/// </summary>
|
||||
/// <param name="file">The FileInfo describing the image or texture file.</param>
|
||||
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
|
||||
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
|
||||
public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(file);
|
||||
return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for the specified Lumina TexFile.
|
||||
/// </summary>
|
||||
/// <param name="file">The texture to obtain a handle to.</param>
|
||||
/// <returns>A texture wrap that can be used to render the texture.</returns>
|
||||
public IDalamudTextureWrap? GetTexture(TexFile file)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(file);
|
||||
|
||||
if (!this.im.IsReady)
|
||||
throw new InvalidOperationException("Cannot create textures before scene is ready");
|
||||
|
||||
#pragma warning disable CS0618
|
||||
return (IDalamudTextureWrap)this.dataManager.GetImGuiTexture(file);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSubstitutedPath(string originalPath)
|
||||
{
|
||||
if (this.InterceptTexDataLoad == null)
|
||||
return originalPath;
|
||||
|
||||
string? interceptPath = null;
|
||||
this.InterceptTexDataLoad.Invoke(originalPath, ref interceptPath);
|
||||
|
||||
if (interceptPath != null)
|
||||
{
|
||||
Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath);
|
||||
return interceptPath;
|
||||
}
|
||||
|
||||
return originalPath;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void InvalidatePaths(IEnumerable<string> paths)
|
||||
{
|
||||
lock (this.activeTextures)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (!this.activeTextures.TryGetValue(path, out var info) || info == null)
|
||||
continue;
|
||||
|
||||
info.Wrap?.Dispose();
|
||||
info.Wrap = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.fallbackTextureWrap?.Dispose();
|
||||
this.framework.Update -= this.FrameworkOnUpdate;
|
||||
|
||||
Log.Verbose("Disposing {Num} left behind textures.");
|
||||
|
||||
foreach (var activeTexture in this.activeTextures)
|
||||
{
|
||||
activeTexture.Value.Wrap?.Dispose();
|
||||
}
|
||||
|
||||
this.activeTextures.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get texture info.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the texture.</param>
|
||||
/// <param name="rethrow">
|
||||
/// If true, exceptions caused by texture load will not be caught.
|
||||
/// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles.
|
||||
/// </param>
|
||||
/// <returns>Info object storing texture metadata.</returns>
|
||||
internal TextureInfo GetInfo(string path, bool rethrow = false)
|
||||
{
|
||||
TextureInfo? info;
|
||||
lock (this.activeTextures)
|
||||
{
|
||||
if (!this.activeTextures.TryGetValue(path, out info))
|
||||
{
|
||||
Debug.Assert(rethrow, "This should never run when getting outside of creator");
|
||||
|
||||
info = new TextureInfo();
|
||||
this.activeTextures.Add(path, info);
|
||||
}
|
||||
|
||||
if (info == null)
|
||||
throw new Exception("null info in activeTextures");
|
||||
}
|
||||
|
||||
if (info.KeepAliveCount == 0)
|
||||
info.LastAccess = DateTime.UtcNow;
|
||||
|
||||
if (info is { Wrap: not null })
|
||||
return info;
|
||||
|
||||
if (!this.im.IsReady)
|
||||
throw new InvalidOperationException("Cannot create textures before scene is ready");
|
||||
|
||||
// Substitute the path here for loading, instead of when getting the respective TextureInfo
|
||||
path = this.GetSubstitutedPath(path);
|
||||
|
||||
TextureWrap? wrap;
|
||||
try
|
||||
{
|
||||
// We want to load this from the disk, probably, if the path has a root
|
||||
// Not sure if this can cause issues with e.g. network drives, might have to rethink
|
||||
// and add a flag instead if it does.
|
||||
if (Path.IsPathRooted(path))
|
||||
{
|
||||
if (Path.GetExtension(path) == ".tex")
|
||||
{
|
||||
// Attempt to load via Lumina
|
||||
var file = this.dataManager.GameData.GetFileFromDisk<TexFile>(path);
|
||||
wrap = this.GetTexture(file);
|
||||
Log.Verbose("Texture {Path} loaded FS via Lumina", path);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Attempt to load image
|
||||
wrap = this.im.LoadImage(path);
|
||||
Log.Verbose("Texture {Path} loaded FS via LoadImage", path);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load regularly from dats
|
||||
var file = this.dataManager.GetFile<TexFile>(path);
|
||||
if (file == null)
|
||||
throw new Exception("Could not load TexFile from dat.");
|
||||
|
||||
wrap = this.GetTexture(file);
|
||||
Log.Verbose("Texture {Path} loaded from SqPack", path);
|
||||
}
|
||||
|
||||
if (wrap == null)
|
||||
throw new Exception("Could not create texture");
|
||||
|
||||
// TODO: We could support this, but I don't think it's worth it at the moment.
|
||||
var extents = new Vector2(wrap.Width, wrap.Height);
|
||||
if (info.Extents != Vector2.Zero && info.Extents != extents)
|
||||
Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path);
|
||||
|
||||
info.Extents = extents;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Could not load texture from {Path}", path);
|
||||
|
||||
// When creating the texture initially, we want to be able to pass errors back to the plugin
|
||||
if (rethrow)
|
||||
throw;
|
||||
|
||||
// This means that the load failed due to circumstances outside of our control,
|
||||
// and we can't do anything about it. Return a dummy texture so that the plugin still
|
||||
// has something to draw.
|
||||
wrap = this.fallbackTextureWrap;
|
||||
|
||||
// Prevent divide-by-zero
|
||||
if (info.Extents == Vector2.Zero)
|
||||
info.Extents = Vector2.One;
|
||||
}
|
||||
|
||||
info.Wrap = wrap;
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify the system about an instance of a texture wrap being disposed.
|
||||
/// If required conditions are met, the texture will be unloaded at the next update.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the texture.</param>
|
||||
/// <param name="keepAlive">Whether or not this handle was created in keep-alive mode.</param>
|
||||
internal void NotifyTextureDisposed(string path, bool keepAlive)
|
||||
{
|
||||
lock (this.activeTextures)
|
||||
{
|
||||
if (!this.activeTextures.TryGetValue(path, out var info))
|
||||
{
|
||||
Log.Warning("Disposing texture that didn't exist: {Path}", path);
|
||||
return;
|
||||
}
|
||||
|
||||
info.RefCount--;
|
||||
|
||||
if (keepAlive)
|
||||
info.KeepAliveCount--;
|
||||
|
||||
// Clean it up by the next update. If it's re-requested in-between, we don't reload it.
|
||||
if (info.RefCount <= 0)
|
||||
info.LastAccess = default;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatIconPath(uint iconId, string? type, bool highResolution)
|
||||
{
|
||||
var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat;
|
||||
|
||||
type ??= string.Empty;
|
||||
if (type.Length > 0 && !type.EndsWith("/"))
|
||||
type += "/";
|
||||
|
||||
return string.Format(format, iconId / 1000, type, iconId);
|
||||
}
|
||||
|
||||
private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive)
|
||||
{
|
||||
lock (this.activeTextures)
|
||||
{
|
||||
// This will create the texture.
|
||||
// That's fine, it's probably used immediately and this will let the plugin catch load errors.
|
||||
var info = this.GetInfo(path, rethrow: true);
|
||||
|
||||
// We need to increase the refcounts here while locking the collection!
|
||||
// Otherwise, if this is loaded from a task, cleanup might already try to delete it
|
||||
// before it can be increased.
|
||||
info.RefCount++;
|
||||
|
||||
if (keepAlive)
|
||||
info.KeepAliveCount++;
|
||||
|
||||
return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void FrameworkOnUpdate(Framework fw)
|
||||
{
|
||||
lock (this.activeTextures)
|
||||
{
|
||||
var toRemove = new List<string>();
|
||||
|
||||
foreach (var texInfo in this.activeTextures)
|
||||
{
|
||||
if (texInfo.Value.RefCount == 0)
|
||||
{
|
||||
Log.Verbose("Evicting {Path} since no refs", texInfo.Key);
|
||||
|
||||
Debug.Assert(texInfo.Value.KeepAliveCount == 0, "texInfo.Value.KeepAliveCount == 0");
|
||||
|
||||
texInfo.Value.Wrap?.Dispose();
|
||||
texInfo.Value.Wrap = null;
|
||||
toRemove.Add(texInfo.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (texInfo.Value.KeepAliveCount > 0 || texInfo.Value.Wrap == null)
|
||||
continue;
|
||||
|
||||
if (DateTime.UtcNow - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime))
|
||||
{
|
||||
Log.Verbose("Evicting {Path} since too old", texInfo.Key);
|
||||
texInfo.Value.Wrap.Dispose();
|
||||
texInfo.Value.Wrap = null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var path in toRemove)
|
||||
{
|
||||
this.activeTextures.Remove(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateFallbackTexture()
|
||||
{
|
||||
var fallbackTexBytes = new byte[] { 0xFF, 0x00, 0xDC, 0xFF };
|
||||
this.fallbackTextureWrap = this.im.LoadImageRaw(fallbackTexBytes, 1, 1, 4);
|
||||
Debug.Assert(this.fallbackTextureWrap != null, "this.fallbackTextureWrap != null");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal representation of a managed texture.
|
||||
/// </summary>
|
||||
internal class TextureInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the actual texture wrap. May be unpopulated.
|
||||
/// </summary>
|
||||
public TextureWrap? Wrap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time the texture was last accessed.
|
||||
/// </summary>
|
||||
public DateTime LastAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of active holders of this texture.
|
||||
/// </summary>
|
||||
public uint RefCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of active holders that want this texture to stay alive forever.
|
||||
/// </summary>
|
||||
public uint KeepAliveCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extents of the texture.
|
||||
/// </summary>
|
||||
public Vector2 Extents { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plugin-scoped version of a texture manager.
|
||||
/// </summary>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
#pragma warning disable SA1015
|
||||
[ResolveVia<ITextureProvider>]
|
||||
#pragma warning restore SA1015
|
||||
internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDisposable
|
||||
{
|
||||
private readonly TextureManager textureManager;
|
||||
|
||||
private readonly List<TextureManagerTextureWrap> trackedTextures = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextureManagerPluginScoped"/> class.
|
||||
/// </summary>
|
||||
/// <param name="textureManager">TextureManager instance.</param>
|
||||
public TextureManagerPluginScoped(TextureManager textureManager)
|
||||
{
|
||||
this.textureManager = textureManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap? GetIcon(
|
||||
uint iconId,
|
||||
ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.ItemHighQuality,
|
||||
ClientLanguage? language = null,
|
||||
bool keepAlive = false)
|
||||
{
|
||||
var wrap = this.textureManager.GetIcon(iconId, flags, language, keepAlive);
|
||||
if (wrap == null)
|
||||
return null;
|
||||
|
||||
this.trackedTextures.Add(wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null)
|
||||
=> this.textureManager.GetIconPath(iconId, flags, language);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
||||
var wrap = this.textureManager.GetTextureFromGame(path, keepAlive);
|
||||
if (wrap == null)
|
||||
return null;
|
||||
|
||||
this.trackedTextures.Add(wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(file);
|
||||
|
||||
var wrap = this.textureManager.GetTextureFromFile(file, keepAlive);
|
||||
if (wrap == null)
|
||||
return null;
|
||||
|
||||
this.trackedTextures.Add(wrap);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap? GetTexture(TexFile file)
|
||||
=> this.textureManager.GetTexture(file);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// Dispose all leaked textures
|
||||
foreach (var textureWrap in this.trackedTextures.Where(x => !x.IsDisposed))
|
||||
{
|
||||
textureWrap.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap.
|
||||
/// </summary>
|
||||
internal class TextureManagerTextureWrap : IDalamudTextureWrap
|
||||
{
|
||||
private readonly TextureManager manager;
|
||||
private readonly string path;
|
||||
private readonly bool keepAlive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TextureManagerTextureWrap"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the texture.</param>
|
||||
/// <param name="extents">The extents of the texture.</param>
|
||||
/// <param name="keepAlive">Keep alive or not.</param>
|
||||
/// <param name="manager">Manager that we obtained this from.</param>
|
||||
internal TextureManagerTextureWrap(string path, Vector2 extents, bool keepAlive, TextureManager manager)
|
||||
{
|
||||
this.path = path;
|
||||
this.keepAlive = keepAlive;
|
||||
this.manager = manager;
|
||||
this.Width = (int)extents.X;
|
||||
this.Height = (int)extents.Y;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr ImGuiHandle => !this.IsDisposed ?
|
||||
this.manager.GetInfo(this.path).Wrap!.ImGuiHandle :
|
||||
throw new InvalidOperationException("Texture already disposed. You may not render it.");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Width { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Height { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this wrap has already been disposed.
|
||||
/// If true, the handle may be invalid.
|
||||
/// </summary>
|
||||
internal bool IsDisposed { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (!this.IsDisposed)
|
||||
this.manager.NotifyTextureDisposed(this.path, this.keepAlive);
|
||||
|
||||
this.IsDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
using Serilog;
|
||||
|
|
@ -14,13 +15,18 @@ namespace Dalamud.Interface.Internal.Windows.Data;
|
|||
/// </summary>
|
||||
internal class TexWidget : IDataWindowWidget
|
||||
{
|
||||
private readonly List<TextureWrap> addedTextures = new();
|
||||
|
||||
private string iconId = "18";
|
||||
private bool hiRes = true;
|
||||
private bool hq = false;
|
||||
private bool keepAlive = false;
|
||||
private string inputTexPath = string.Empty;
|
||||
private TextureWrap? debugTex;
|
||||
private Vector2 inputTexUv0 = Vector2.Zero;
|
||||
private Vector2 inputTexUv1 = Vector2.One;
|
||||
private Vector4 inputTintCol = Vector4.One;
|
||||
private Vector2 inputTexScale = Vector2.Zero;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataKind DataKind { get; init; } = DataKind.Tex;
|
||||
|
||||
|
|
@ -36,34 +42,87 @@ internal class TexWidget : IDataWindowWidget
|
|||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
var dataManager = Service<DataManager>.Get();
|
||||
var texManager = Service<TextureManager>.Get();
|
||||
|
||||
ImGui.InputText("Tex Path", ref this.inputTexPath, 255);
|
||||
ImGui.InputFloat2("UV0", ref this.inputTexUv0);
|
||||
ImGui.InputFloat2("UV1", ref this.inputTexUv1);
|
||||
ImGui.InputFloat4("Tint", ref this.inputTintCol);
|
||||
ImGui.InputFloat2("Scale", ref this.inputTexScale);
|
||||
|
||||
if (ImGui.Button("Load Tex"))
|
||||
ImGui.InputText("Icon ID", ref this.iconId, 32);
|
||||
ImGui.Checkbox("HQ Item", ref this.hq);
|
||||
ImGui.Checkbox("Hi-Res", ref this.hiRes);
|
||||
ImGui.Checkbox("Keep alive", ref this.keepAlive);
|
||||
if (ImGui.Button("Load Icon"))
|
||||
{
|
||||
try
|
||||
{
|
||||
this.debugTex = dataManager.GetImGuiTexture(this.inputTexPath);
|
||||
this.inputTexScale = new Vector2(this.debugTex?.Width ?? 0, this.debugTex?.Height ?? 0);
|
||||
var flags = ITextureProvider.IconFlags.None;
|
||||
if (this.hq)
|
||||
flags |= ITextureProvider.IconFlags.ItemHighQuality;
|
||||
|
||||
if (this.hiRes)
|
||||
flags |= ITextureProvider.IconFlags.HiRes;
|
||||
|
||||
this.addedTextures.Add(texManager.GetIcon(uint.Parse(this.iconId), flags, keepAlive: this.keepAlive));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not load tex");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.InputText("Tex Path", ref this.inputTexPath, 255);
|
||||
if (ImGui.Button("Load Tex"))
|
||||
{
|
||||
try
|
||||
{
|
||||
this.addedTextures.Add(texManager.GetTextureFromGame(this.inputTexPath, this.keepAlive));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not load tex");
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.Button("Load File"))
|
||||
{
|
||||
try
|
||||
{
|
||||
this.addedTextures.Add(texManager.GetTextureFromFile(new FileInfo(this.inputTexPath), this.keepAlive));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not load tex");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.InputFloat2("UV0", ref this.inputTexUv0);
|
||||
ImGui.InputFloat2("UV1", ref this.inputTexUv1);
|
||||
ImGui.InputFloat4("Tint", ref this.inputTintCol);
|
||||
ImGui.InputFloat2("Scale", ref this.inputTexScale);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(10);
|
||||
|
||||
if (this.debugTex != null)
|
||||
TextureWrap? toRemove = null;
|
||||
for (var i = 0; i < this.addedTextures.Count; i++)
|
||||
{
|
||||
ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol);
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
Util.ShowObject(this.debugTex);
|
||||
if (ImGui.CollapsingHeader($"Tex #{i}"))
|
||||
{
|
||||
var tex = this.addedTextures[i];
|
||||
|
||||
var scale = new Vector2(tex.Width, tex.Height);
|
||||
if (this.inputTexScale != Vector2.Zero)
|
||||
scale = this.inputTexScale;
|
||||
|
||||
ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol);
|
||||
|
||||
if (ImGui.Button($"X##{i}"))
|
||||
toRemove = tex;
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove != null)
|
||||
{
|
||||
toRemove.Dispose();
|
||||
this.addedTextures.Remove(toRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
|||
using Dalamud.Networking.Http;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Utility;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
|
||||
|
|
@ -44,27 +45,35 @@ internal class DalamudChangelogManager
|
|||
this.Changelogs = null;
|
||||
|
||||
var dalamudChangelogs = await client.GetFromJsonAsync<List<DalamudChangelog>>(DalamudChangelogUrl);
|
||||
var changelogs = dalamudChangelogs.Select(x => new DalamudChangelogEntry(x)).Cast<IChangelogEntry>();
|
||||
var changelogs = dalamudChangelogs.Select(x => new DalamudChangelogEntry(x)).Cast<IChangelogEntry>().ToList();
|
||||
|
||||
foreach (var plugin in this.manager.InstalledPlugins)
|
||||
{
|
||||
if (!plugin.IsThirdParty)
|
||||
if (!plugin.IsThirdParty && !plugin.IsDev)
|
||||
{
|
||||
var pluginChangelogs = await client.GetFromJsonAsync<PluginHistory>(string.Format(
|
||||
PluginChangelogUrl,
|
||||
plugin.Manifest.InternalName,
|
||||
plugin.Manifest.Dip17Channel));
|
||||
try
|
||||
{
|
||||
var pluginChangelogs = await client.GetFromJsonAsync<PluginHistory>(string.Format(
|
||||
PluginChangelogUrl,
|
||||
plugin.Manifest.InternalName,
|
||||
plugin.Manifest.Dip17Channel));
|
||||
|
||||
changelogs = changelogs.Concat(pluginChangelogs.Versions
|
||||
.Where(x => x.Dip17Track == plugin.Manifest.Dip17Channel)
|
||||
.Select(x => new PluginChangelogEntry(plugin, x)));
|
||||
changelogs.AddRange(pluginChangelogs.Versions
|
||||
.Where(x => x.Dip17Track ==
|
||||
plugin.Manifest.Dip17Channel)
|
||||
.Select(x => new PluginChangelogEntry(plugin, x)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to load changelog for {PluginName}", plugin.Manifest.Name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (plugin.Manifest.Changelog.IsNullOrWhitespace())
|
||||
continue;
|
||||
|
||||
changelogs = changelogs.Append(new PluginChangelogEntry(plugin));
|
||||
changelogs.Add(new PluginChangelogEntry(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -569,7 +569,12 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.filterText = selectable.Localization;
|
||||
|
||||
lock (this.listLock)
|
||||
{
|
||||
this.ResortPlugins();
|
||||
|
||||
// Positions of plugins within the list is likely to change
|
||||
this.openPluginCollapsibles.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -962,7 +967,14 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
this.dalamudChangelogRefreshTaskCts = new CancellationTokenSource();
|
||||
this.dalamudChangelogRefreshTask =
|
||||
Task.Run(this.dalamudChangelogManager.ReloadChangelogAsync, this.dalamudChangelogRefreshTaskCts.Token);
|
||||
Task.Run(this.dalamudChangelogManager.ReloadChangelogAsync, this.dalamudChangelogRefreshTaskCts.Token)
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully)
|
||||
{
|
||||
Log.Error(t.Exception, "Failed to load changelogs.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -2354,6 +2366,10 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var config = Service<DalamudConfiguration>.Get();
|
||||
|
||||
var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev;
|
||||
var profilesThatWantThisPlugin = profileManager.Profiles
|
||||
.Where(x => x.WantsPlugin(plugin.InternalName) != null)
|
||||
.ToArray();
|
||||
var isInSingleProfile = profilesThatWantThisPlugin.Length == 1;
|
||||
var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName);
|
||||
|
||||
// Disable everything if the updater is running or another plugin is operating
|
||||
|
|
@ -2437,6 +2453,10 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
var inMultipleProfiles = !isDefaultPlugin && !isInSingleProfile;
|
||||
var inSingleNonDefaultProfileWhichIsDisabled =
|
||||
isInSingleProfile && !profilesThatWantThisPlugin.First().IsEnabled;
|
||||
|
||||
if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev)
|
||||
{
|
||||
ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown);
|
||||
|
|
@ -2444,80 +2464,77 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed);
|
||||
}
|
||||
else if (disabled || !isDefaultPlugin)
|
||||
else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled)
|
||||
{
|
||||
ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable);
|
||||
|
||||
if (!isDefaultPlugin && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInDefault);
|
||||
if (inMultipleProfiles && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile);
|
||||
else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ImGuiComponents.ToggleButton(toggleId, ref isLoadedAndUnloadable))
|
||||
{
|
||||
// TODO: We can technically let profile manager take care of unloading/loading the plugin, but we should figure out error handling first.
|
||||
var applicableProfile = profilesThatWantThisPlugin.First();
|
||||
Log.Verbose("Switching {InternalName} in {Profile} to {State}",
|
||||
plugin.InternalName, applicableProfile, isLoadedAndUnloadable);
|
||||
|
||||
try
|
||||
{
|
||||
// Reload the devPlugin manifest if it's a dev plugin
|
||||
// The plugin might rely on changed values in the manifest
|
||||
if (plugin.IsDev)
|
||||
{
|
||||
plugin.ReloadManifest();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not reload DevPlugin manifest");
|
||||
}
|
||||
|
||||
// NOTE: We don't use the profile manager to actually handle loading/unloading here,
|
||||
// because that might cause us to show an error if a plugin we don't actually care about
|
||||
// fails to load/unload. Instead, we just do it ourselves and then update the profile.
|
||||
// There is probably a smarter way to handle this, but it's probably more code.
|
||||
if (!isLoadedAndUnloadable)
|
||||
{
|
||||
this.enableDisableStatus = OperationStatus.InProgress;
|
||||
this.loadingIndicatorKind = LoadingIndicatorKind.DisablingSingle;
|
||||
|
||||
Task.Run(() =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
if (plugin.IsDev)
|
||||
{
|
||||
plugin.ReloadManifest();
|
||||
}
|
||||
|
||||
var unloadTask = Task.Run(() => plugin.UnloadAsync())
|
||||
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
|
||||
|
||||
unloadTask.Wait();
|
||||
if (!unloadTask.Result)
|
||||
{
|
||||
this.enableDisableStatus = OperationStatus.Complete;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Work this out
|
||||
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, false, false))
|
||||
.GetAwaiter().GetResult();
|
||||
this.enableDisableStatus = OperationStatus.Complete;
|
||||
await plugin.UnloadAsync();
|
||||
await applicableProfile.AddOrUpdateAsync(
|
||||
plugin.Manifest.InternalName, false, false);
|
||||
|
||||
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
this.enableDisableStatus = OperationStatus.Complete;
|
||||
this.DisplayErrorContinuation(t, Locs.ErrorModal_UnloadFail(plugin.Name));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var enabler = new Task(() =>
|
||||
async Task Enabler()
|
||||
{
|
||||
this.enableDisableStatus = OperationStatus.InProgress;
|
||||
this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle;
|
||||
|
||||
if (plugin.IsDev)
|
||||
{
|
||||
plugin.ReloadManifest();
|
||||
}
|
||||
await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false);
|
||||
await plugin.LoadAsync(PluginLoadReason.Installer);
|
||||
|
||||
// TODO: Work this out
|
||||
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false))
|
||||
.GetAwaiter().GetResult();
|
||||
notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success);
|
||||
}
|
||||
|
||||
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
|
||||
.ContinueWith(
|
||||
this.DisplayErrorContinuation,
|
||||
Locs.ErrorModal_LoadFail(plugin.Name));
|
||||
|
||||
loadTask.Wait();
|
||||
var continuation = (Task t) =>
|
||||
{
|
||||
this.enableDisableStatus = OperationStatus.Complete;
|
||||
|
||||
if (!loadTask.Result)
|
||||
return;
|
||||
|
||||
notifications.AddNotification(
|
||||
Locs.Notifications_PluginEnabled(plugin.Manifest.Name),
|
||||
Locs.Notifications_PluginEnabledTitle,
|
||||
NotificationType.Success);
|
||||
});
|
||||
this.DisplayErrorContinuation(t, Locs.ErrorModal_LoadFail(plugin.Name));
|
||||
};
|
||||
|
||||
if (availableUpdate != default && !availableUpdate.InstalledPlugin.IsDev)
|
||||
{
|
||||
|
|
@ -2527,17 +2544,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
// We need to update the profile right here, because PM will not enable the plugin otherwise
|
||||
await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false);
|
||||
await this.UpdateSinglePlugin(availableUpdate);
|
||||
}
|
||||
else
|
||||
{
|
||||
enabler.Start();
|
||||
_ = Task.Run(Enabler).ContinueWith(continuation);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
enabler.Start();
|
||||
_ = Task.Run(Enabler).ContinueWith(continuation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2963,7 +2982,18 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
break;
|
||||
case PluginSortKind.LastUpdate:
|
||||
this.pluginListAvailable.Sort((p1, p2) => p2.LastUpdate.CompareTo(p1.LastUpdate));
|
||||
this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.LastUpdate.CompareTo(p1.Manifest.LastUpdate));
|
||||
this.pluginListInstalled.Sort((p1, p2) =>
|
||||
{
|
||||
// We need to get remote manifests here, as the local manifests will have the time when the current version is installed,
|
||||
// not the actual time of the last update, as the plugin may be pending an update
|
||||
IPluginManifest? p2Considered = this.pluginListAvailable.FirstOrDefault(x => x.InternalName == p2.InternalName);
|
||||
p2Considered ??= p2.Manifest;
|
||||
|
||||
IPluginManifest? p1Considered = this.pluginListAvailable.FirstOrDefault(x => x.InternalName == p1.InternalName);
|
||||
p1Considered ??= p1.Manifest;
|
||||
|
||||
return p2Considered.LastUpdate.CompareTo(p1Considered.LastUpdate);
|
||||
});
|
||||
break;
|
||||
case PluginSortKind.NewOrNot:
|
||||
this.pluginListAvailable.Sort((p1, p2) => this.WasPluginSeen(p1.InternalName)
|
||||
|
|
@ -3236,6 +3266,10 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerLoadUnloadFailedTooltip", "Plugin load/unload failed, please restart your game and try again.");
|
||||
|
||||
public static string PluginButtonToolTip_NeedsToBeInDefault => Loc.Localize("InstallerUnloadNeedsToBeInDefault", "This plugin is in one or more collections. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it manually, remove it from all collections.");
|
||||
|
||||
public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection.");
|
||||
|
||||
public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name);
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ namespace Dalamud.IoC.Internal;
|
|||
|
||||
/// <summary>
|
||||
/// A simple singleton-only IOC container that provides (optional) version-based dependency resolution.
|
||||
///
|
||||
/// This is only used to resolve dependencies for plugins.
|
||||
/// Dalamud services are constructed via Service{T}.ConstructObject at the moment.
|
||||
/// </summary>
|
||||
internal class ServiceContainer : IServiceProvider, IServiceType
|
||||
{
|
||||
|
|
@ -31,7 +34,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
/// Register a singleton object of any type into the current IOC container.
|
||||
/// </summary>
|
||||
/// <param name="instance">The existing instance to register in the container.</param>
|
||||
/// <typeparam name="T">The interface to register.</typeparam>
|
||||
/// <typeparam name="T">The type to register.</typeparam>
|
||||
public void RegisterSingleton<T>(Task<T> instance)
|
||||
{
|
||||
if (instance == null)
|
||||
|
|
@ -40,19 +43,27 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
}
|
||||
|
||||
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
|
||||
this.RegisterInterfaces(typeof(T));
|
||||
}
|
||||
|
||||
var resolveViaTypes = typeof(T)
|
||||
.GetCustomAttributes()
|
||||
.OfType<ResolveViaAttribute>()
|
||||
.Select(x => x.GetType().GetGenericArguments().First());
|
||||
/// <summary>
|
||||
/// Register the interfaces that can resolve this type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to register.</param>
|
||||
public void RegisterInterfaces(Type type)
|
||||
{
|
||||
var resolveViaTypes = type
|
||||
.GetCustomAttributes()
|
||||
.OfType<ResolveViaAttribute>()
|
||||
.Select(x => x.GetType().GetGenericArguments().First());
|
||||
foreach (var resolvableType in resolveViaTypes)
|
||||
{
|
||||
Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", typeof(T).FullName ?? "???");
|
||||
Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???");
|
||||
|
||||
Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed");
|
||||
Debug.Assert(typeof(T).IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type");
|
||||
Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type");
|
||||
|
||||
this.interfaceToTypeMap[resolvableType] = typeof(T);
|
||||
this.interfaceToTypeMap[resolvableType] = type;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,18 +106,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
parameters
|
||||
.Select(async p =>
|
||||
{
|
||||
if (p.parameterType.GetCustomAttribute<ServiceManager.ScopedService>() != null)
|
||||
{
|
||||
if (scopeImpl == null)
|
||||
{
|
||||
Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await scopeImpl.CreatePrivateScopedObject(p.parameterType, scopedObjects);
|
||||
}
|
||||
|
||||
var service = await this.GetService(p.parameterType, scopedObjects);
|
||||
var service = await this.GetService(p.parameterType, scopeImpl, scopedObjects);
|
||||
|
||||
if (service == null)
|
||||
{
|
||||
|
|
@ -168,22 +168,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
|
||||
foreach (var prop in props)
|
||||
{
|
||||
object service = null;
|
||||
|
||||
if (prop.propertyInfo.PropertyType.GetCustomAttribute<ServiceManager.ScopedService>() != null)
|
||||
{
|
||||
if (scopeImpl == null)
|
||||
{
|
||||
Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!);
|
||||
}
|
||||
else
|
||||
{
|
||||
service = await scopeImpl.CreatePrivateScopedObject(prop.propertyInfo.PropertyType, publicScopes);
|
||||
}
|
||||
}
|
||||
|
||||
service ??= await this.GetService(prop.propertyInfo.PropertyType, publicScopes);
|
||||
|
||||
var service = await this.GetService(prop.propertyInfo.PropertyType, scopeImpl, publicScopes);
|
||||
if (service == null)
|
||||
{
|
||||
Log.Error("Requested service type {TypeName} was not available (null)", prop.propertyInfo.PropertyType.FullName!);
|
||||
|
|
@ -203,7 +188,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
public IServiceScope GetScope() => new ServiceScopeImpl(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
object? IServiceProvider.GetService(Type serviceType) => this.GetService(serviceType);
|
||||
object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType);
|
||||
|
||||
private static bool CheckInterfaceVersion(RequiredVersionAttribute? requiredVersion, Type parameterType)
|
||||
{
|
||||
|
|
@ -228,9 +213,23 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
return false;
|
||||
}
|
||||
|
||||
private async Task<object?> GetService(Type serviceType, object[] scopedObjects)
|
||||
private async Task<object?> GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects)
|
||||
{
|
||||
var singletonService = await this.GetService(serviceType);
|
||||
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
|
||||
serviceType = implementingType;
|
||||
|
||||
if (serviceType.GetCustomAttribute<ServiceManager.ScopedService>() != null)
|
||||
{
|
||||
if (scope == null)
|
||||
{
|
||||
Log.Error("Failed to create {TypeName}, is scoped but no scope provided", serviceType.FullName!);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await scope.CreatePrivateScopedObject(serviceType, scopedObjects);
|
||||
}
|
||||
|
||||
var singletonService = await this.GetSingletonService(serviceType, false);
|
||||
if (singletonService != null)
|
||||
{
|
||||
return singletonService;
|
||||
|
|
@ -246,9 +245,9 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
return scoped;
|
||||
}
|
||||
|
||||
private async Task<object?> GetService(Type serviceType)
|
||||
private async Task<object?> GetSingletonService(Type serviceType, bool tryGetInterface = true)
|
||||
{
|
||||
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
|
||||
if (tryGetInterface && this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
|
||||
serviceType = implementingType;
|
||||
|
||||
if (!this.instances.TryGetValue(serviceType, out var service))
|
||||
|
|
@ -285,13 +284,24 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
|||
|
||||
private bool ValidateCtor(ConstructorInfo ctor, Type[] types)
|
||||
{
|
||||
bool IsTypeValid(Type type)
|
||||
{
|
||||
var contains = types.Any(x => x.IsAssignableTo(type));
|
||||
|
||||
// Scoped services are created on-demand
|
||||
return contains || type.GetCustomAttribute<ServiceManager.ScopedService>() != null;
|
||||
}
|
||||
|
||||
var parameters = ctor.GetParameters();
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
var contains = types.Any(x => x.IsAssignableTo(parameter.ParameterType));
|
||||
var valid = IsTypeValid(parameter.ParameterType);
|
||||
|
||||
// If this service is provided by an interface
|
||||
if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType))
|
||||
valid = IsTypeValid(implementationType);
|
||||
|
||||
// Scoped services are created on-demand
|
||||
if (!contains && parameter.ParameterType.GetCustomAttribute<ServiceManager.ScopedService>() == null)
|
||||
if (!valid)
|
||||
{
|
||||
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!);
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ public class ModuleLog
|
|||
/// <param name="exception">The exception that caused the error.</param>
|
||||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
public void Error(Exception exception, string messageTemplate, params object[] values)
|
||||
public void Error(Exception? exception, string messageTemplate, params object[] values)
|
||||
=> this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -232,4 +232,7 @@ internal class Profile
|
|||
if (apply)
|
||||
await this.manager.ApplyAllWantStatesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{this.Guid} ({this.Name})";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using ImGuiScene;
|
||||
using Lumina;
|
||||
|
|
@ -91,6 +93,7 @@ public interface IDataManager
|
|||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <param name="highResolution">Return high resolution version.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(uint iconId, bool highResolution = false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -100,6 +103,7 @@ public interface IDataManager
|
|||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <param name="highResolution">Return high resolution version.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -109,6 +113,7 @@ public interface IDataManager
|
|||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <param name="highResolution">Return high resolution version.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -117,6 +122,7 @@ public interface IDataManager
|
|||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <param name="highResolution">Return the high resolution version.</param>
|
||||
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -125,6 +131,7 @@ public interface IDataManager
|
|||
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetIcon(bool isHq, uint iconId);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -132,6 +139,7 @@ public interface IDataManager
|
|||
/// </summary>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TexFile? GetHqIcon(uint iconId);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -139,6 +147,8 @@ public interface IDataManager
|
|||
/// </summary>
|
||||
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
|
||||
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
[return: NotNullIfNotNull(nameof(tex))]
|
||||
public TextureWrap? GetImGuiTexture(TexFile? tex);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -146,6 +156,7 @@ public interface IDataManager
|
|||
/// </summary>
|
||||
/// <param name="path">The internal path to the texture.</param>
|
||||
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTexture(string path);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -154,6 +165,7 @@ public interface IDataManager
|
|||
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -162,6 +174,7 @@ public interface IDataManager
|
|||
/// <param name="iconLanguage">The requested language.</param>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -170,6 +183,7 @@ public interface IDataManager
|
|||
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -177,5 +191,6 @@ public interface IDataManager
|
|||
/// </summary>
|
||||
/// <param name="iconId">The icon ID.</param>
|
||||
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
|
||||
[Obsolete("Use ITextureProvider instead")]
|
||||
public TextureWrap? GetImGuiTextureHqIcon(uint iconId);
|
||||
}
|
||||
|
|
|
|||
96
Dalamud/Plugin/Services/ITextureProvider.cs
Normal file
96
Dalamud/Plugin/Services/ITextureProvider.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
using Dalamud.Interface.Internal;
|
||||
using Lumina.Data.Files;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that grants you access to textures you may render via ImGui.
|
||||
/// </summary>
|
||||
public interface ITextureProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Flags describing the icon you wish to receive.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum IconFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// Low-resolution, standard quality icon.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// If this icon is an item icon, and it has a high-quality variant, receive the high-quality version.
|
||||
/// Null if the item does not have a high-quality variant.
|
||||
/// </summary>
|
||||
ItemHighQuality = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Get the hi-resolution version of the icon, if it exists.
|
||||
/// </summary>
|
||||
HiRes = 1 << 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for a specific icon.
|
||||
/// </summary>
|
||||
/// <param name="iconId">The ID of the icon to load.</param>
|
||||
/// <param name="flags">Options to be considered when loading the icon.</param>
|
||||
/// <param name="language">
|
||||
/// The language to be considered when loading the icon, if the icon has versions for multiple languages.
|
||||
/// If null, default to the game's current language.
|
||||
/// </param>
|
||||
/// <param name="keepAlive">
|
||||
/// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used
|
||||
/// to render the icon.
|
||||
/// </returns>
|
||||
public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get a path for a specific icon's .tex file.
|
||||
/// </summary>
|
||||
/// <param name="iconId">The ID of the icon to look up.</param>
|
||||
/// <param name="flags">Options to be considered when loading the icon.</param>
|
||||
/// <param name="language">
|
||||
/// The language to be considered when loading the icon, if the icon has versions for multiple languages.
|
||||
/// If null, default to the game's current language.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file,
|
||||
/// which can be loaded via IDataManager.
|
||||
/// </returns>
|
||||
public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for the texture at the specified path.
|
||||
/// You may only specify paths in the game's VFS.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the texture in the game's VFS.</param>
|
||||
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
|
||||
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
|
||||
public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for the image or texture, specified by the passed FileInfo.
|
||||
/// You may only specify paths on the native file system.
|
||||
///
|
||||
/// This API can load .png and .tex files.
|
||||
/// </summary>
|
||||
/// <param name="file">The FileInfo describing the image or texture file.</param>
|
||||
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
|
||||
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
|
||||
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get a texture handle for the specified Lumina TexFile.
|
||||
/// </summary>
|
||||
/// <param name="file">The texture to obtain a handle to.</param>
|
||||
/// <returns>A texture wrap that can be used to render the texture.</returns>
|
||||
public IDalamudTextureWrap GetTexture(TexFile file);
|
||||
}
|
||||
37
Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs
Normal file
37
Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service that grants you the ability to replace texture data that is to be loaded by Dalamud.
|
||||
/// </summary>
|
||||
public interface ITextureSubstitutionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate describing a function that may be used to intercept and replace texture data.
|
||||
/// The path assigned may point to another texture inside the game's dats, or a .tex file or image on the disk.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the texture that is to be loaded.</param>
|
||||
/// <param name="replacementPath">The path that should be loaded instead.</param>
|
||||
public delegate void TextureDataInterceptorDelegate(string path, ref string? replacementPath);
|
||||
|
||||
/// <summary>
|
||||
/// Event that will be called once Dalamud wants to load texture data.
|
||||
/// </summary>
|
||||
public event TextureDataInterceptorDelegate? InterceptTexDataLoad;
|
||||
|
||||
/// <summary>
|
||||
/// Get a path that may be substituted by a subscriber to ITextureSubstitutionProvider.
|
||||
/// </summary>
|
||||
/// <param name="originalPath">The original path to substitute.</param>
|
||||
/// <returns>The original path, if no subscriber is registered or there is no substitution, or the substituted path.</returns>
|
||||
public string GetSubstitutedPath(string originalPath);
|
||||
|
||||
/// <summary>
|
||||
/// Notify Dalamud about substitution status for files at the specified VFS paths changing.
|
||||
/// You should call this with all paths that were either previously substituted and are no longer,
|
||||
/// and paths that are newly substituted.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths with a changed substitution status.</param>
|
||||
public void InvalidatePaths(IEnumerable<string> paths);
|
||||
}
|
||||
|
|
@ -132,11 +132,20 @@ internal static class ServiceManager
|
|||
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
|
||||
var getAsyncTaskMap = new Dictionary<Type, Task>();
|
||||
|
||||
var serviceContainer = Service<ServiceContainer>.Get();
|
||||
|
||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
|
||||
{
|
||||
var serviceKind = serviceType.GetServiceKind();
|
||||
if (serviceKind is ServiceKind.None or ServiceKind.ScopedService)
|
||||
if (serviceKind is ServiceKind.None)
|
||||
continue;
|
||||
|
||||
// Scoped service do not go through Service<T>, so we must let ServiceContainer know what their interfaces map to
|
||||
if (serviceKind is ServiceKind.ScopedService)
|
||||
{
|
||||
serviceContainer.RegisterInterfaces(serviceType);
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Assert(
|
||||
!serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService),
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 8962a47b95f96bec53e58680bd9d1e7f38610d40
|
||||
Subproject commit 9b2eab0f212030c062427b307b96118881d36b99
|
||||
Loading…
Add table
Add a link
Reference in a new issue