using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Services; using Dalamud.Utility; using Lumina.Data.Files; using TerraFX.Interop.DirectX; namespace Dalamud.Interface.Textures.Internal; /// Plugin-scoped version of . [PluginInterface] [ServiceManager.ScopedService] #pragma warning disable SA1015 [ResolveVia] [ResolveVia] [ResolveVia] #pragma warning restore SA1015 internal sealed class TextureManagerPluginScoped : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider, ITextureReadbackProvider { private readonly LocalPlugin plugin; private readonly bool nonAsyncFunctionAccessDuringLoadIsError; private Task? managerTaskNullable; /// Initializes a new instance of the class. /// The plugin. [ServiceManager.ServiceConstructor] public TextureManagerPluginScoped(LocalPlugin plugin) { this.plugin = plugin; if (plugin.Manifest is LocalPluginManifest lpm) this.nonAsyncFunctionAccessDuringLoadIsError = lpm.LoadSync && lpm.LoadRequiredState != 0; this.managerTaskNullable = Service .GetAsync() .ContinueWith( r => { if (r.IsCompletedSuccessfully) r.Result.InterceptTexDataLoad += this.ResultOnInterceptTexDataLoad; return r; }) .Unwrap(); } /// public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; /// Gets the task resulting in an instance of . /// Thrown if disposed. private Task ManagerTask { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.managerTaskNullable ?? throw new ObjectDisposedException(this.ToString()); } /// Gets an instance of . /// Thrown if disposed. /// Thrown if called at an unfortune time. private TextureManager ManagerOrThrow { get { var task = this.ManagerTask; // Check for IMWS too, as TextureManager is constructed after IMWS, and UiBuilder.RunWhenUiPrepared gets // resolved when IMWS is constructed. if (!task.IsCompleted && Service.GetNullable() is null) { if (this.nonAsyncFunctionAccessDuringLoadIsError && this.plugin.State != PluginState.Loaded) { throw new InvalidOperationException( "The function you've called will wait for the drawing facilities to be available, and as " + "Dalamud is already waiting for your plugin to be fully constructed before even attempting " + "to initialize the drawing facilities, calling this function will stall the game until and " + "is forbidden until your plugin has been fully loaded.\n" + $"Consider using {nameof(UiBuilder.RunWhenUiPrepared)} to wait for the right moment.\n" + "\n" + $"Note that your plugin has {nameof(LocalPluginManifest.LoadSync)} set and " + $"{nameof(LocalPluginManifest.LoadRequiredState)} that is nonzero."); } if (ThreadSafety.IsMainThread) { throw new InvalidOperationException( "The function you've called will wait for the drawing facilities to be available, and as " + "the drawing facilities are initialized from the main thread, calling this function will " + "stall the game until and is forbidden until your plugin has been fully loaded.\n" + $"Consider using {nameof(UiBuilder.RunWhenUiPrepared)} to wait for the right moment."); } } return task.Result; } } /// void IInternalDisposableService.DisposeService() { if (Interlocked.Exchange(ref this.managerTaskNullable, null) is not { } task) return; task.ContinueWith( r => { if (r.IsCompletedSuccessfully) r.Result.InterceptTexDataLoad -= this.ResultOnInterceptTexDataLoad; }); } /// public override string ToString() { return this.managerTaskNullable is null ? $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name}, disposed)" : $"{nameof(TextureManagerPluginScoped)}({this.plugin.Name})"; } /// public IDalamudTextureWrap CreateEmpty( RawImageSpecification specs, bool cpuRead, bool cpuWrite, string? debugName = null) { var manager = this.ManagerOrThrow; var textureWrap = manager.CreateEmpty(specs, cpuRead, cpuWrite, debugName); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, TextureModificationArgs args = default, bool leaveWrapOpen = false, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromExistingTextureAsync( wrap, args, leaveWrapOpen, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromImageAsync( ReadOnlyMemory bytes, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromImageAsync(bytes, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromImageAsync( Stream stream, bool leaveOpen = false, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromImageAsync(stream, leaveOpen, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes, string? debugName = null) { var manager = this.ManagerOrThrow; var textureWrap = manager.CreateFromRaw(specs, bytes, debugName); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromRawAsync( RawImageSpecification specs, ReadOnlyMemory bytes, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromRawAsync(specs, bytes, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromRawAsync( RawImageSpecification specs, Stream stream, bool leaveOpen = false, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromRawAsync(specs, stream, leaveOpen, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public IDalamudTextureWrap CreateFromTexFile(TexFile file) { var manager = this.ManagerOrThrow; var textureWrap = manager.CreateFromTexFile(file); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public async Task CreateFromTexFileAsync( TexFile file, string? debugName = null, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromTexFileAsync(file, debugName, cancellationToken); manager.Blame(textureWrap, this.plugin); return textureWrap; } /// public IEnumerable GetSupportedImageDecoderInfos() => this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); /// public ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup) { var shared = this.ManagerOrThrow.Shared.GetFromGameIcon(lookup); shared.AddOwnerPlugin(this.plugin); return shared; } /// public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture) { if (this.ManagerOrThrow.Shared.TryGetFromGameIcon(lookup, out var shared)) { shared.AddOwnerPlugin(this.plugin); texture = shared; return true; } texture = null; return false; } /// public ISharedImmediateTexture GetFromGame(string path) { var shared = this.ManagerOrThrow.Shared.GetFromGame(path); shared.AddOwnerPlugin(this.plugin); return shared; } /// public ISharedImmediateTexture GetFromFile(string path) { var shared = this.ManagerOrThrow.Shared.GetFromFile(path); shared.AddOwnerPlugin(this.plugin); return shared; } /// public ISharedImmediateTexture GetFromFile(FileInfo file) { var shared = this.ManagerOrThrow.Shared.GetFromFile(file); shared.AddOwnerPlugin(this.plugin); return shared; } /// public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name) { var shared = this.ManagerOrThrow.Shared.GetFromManifestResource(assembly, name); shared.AddOwnerPlugin(this.plugin); return shared; } /// public string GetIconPath(in GameIconLookup lookup) => this.ManagerOrThrow.GetIconPath(lookup); /// public bool TryGetIconPath(in GameIconLookup lookup, out string? path) => this.ManagerOrThrow.TryGetIconPath(lookup, out path); /// public bool IsDxgiFormatSupported(int dxgiFormat) => this.ManagerOrThrow.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); /// public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int dxgiFormat) => this.ManagerOrThrow.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); /// public string GetSubstitutedPath(string originalPath) => this.ManagerOrThrow.GetSubstitutedPath(originalPath); /// public void InvalidatePaths(IEnumerable paths) => this.ManagerOrThrow.InvalidatePaths(paths); /// public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( IDalamudTextureWrap wrap, TextureModificationArgs args = default, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; return await manager.GetRawImageAsync(wrap, args, leaveWrapOpen, cancellationToken); } /// public IEnumerable GetSupportedImageEncoderInfos() => this.ManagerOrThrow.Wic.GetSupportedEncoderInfos(); /// public async Task SaveToStreamAsync( IDalamudTextureWrap wrap, Guid containerGuid, Stream stream, IReadOnlyDictionary? props = null, bool leaveWrapOpen = false, bool leaveStreamOpen = false, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; await manager.SaveToStreamAsync( wrap, containerGuid, stream, props, leaveWrapOpen, leaveStreamOpen, cancellationToken); } /// public async Task SaveToFileAsync( IDalamudTextureWrap wrap, Guid containerGuid, string path, IReadOnlyDictionary? props = null, bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { var manager = await this.ManagerTask; await manager.SaveToFileAsync( wrap, containerGuid, path, props, leaveWrapOpen, cancellationToken); } private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) => this.InterceptTexDataLoad?.Invoke(path, ref replacementPath); }