mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-17 05:17:42 +01:00
merge from master
This commit is contained in:
commit
6604678050
82 changed files with 2683 additions and 1371 deletions
|
|
@ -4,6 +4,7 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
|
|
@ -11,6 +12,7 @@ using Dalamud.Interface.FontIdentifier;
|
|||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Windowing.Persistence;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Internal.AutoUpdate;
|
||||
using Dalamud.Plugin.Internal.Profiles;
|
||||
|
|
@ -45,6 +47,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
[JsonIgnore]
|
||||
private bool isSaveQueued;
|
||||
|
||||
private Task? writeTask;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
|
||||
/// </summary>
|
||||
|
|
@ -243,13 +247,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
|
||||
/// </summary>
|
||||
public bool AssertsEnabledAtStartup { get; set; }
|
||||
public bool? ImGuiAssertsEnabledAtStartup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
|
||||
/// </summary>
|
||||
public bool IsDocking { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects.
|
||||
/// This setting is effected by the in-game "System Sounds" option and volume.
|
||||
|
|
@ -261,8 +265,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown
|
||||
/// on plugin title bars when using the Window System.
|
||||
/// </summary>
|
||||
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")]
|
||||
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
|
||||
public bool EnablePluginUiAdditionalOptions { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether viewports should always be disabled.
|
||||
|
|
@ -348,6 +351,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
/// </summary>
|
||||
public bool ProfilesHasSeenTutorial { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default UI preset.
|
||||
/// </summary>
|
||||
public PresetModel DefaultUiPreset { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the order of DTR elements, by title.
|
||||
/// </summary>
|
||||
|
|
@ -484,10 +492,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not users should be notified regularly about pending updates.
|
||||
/// Gets or sets a value indicating whether users should be notified regularly about pending updates.
|
||||
/// </summary>
|
||||
public bool CheckPeriodicallyForUpdates { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether users should be notified about updates in chat.
|
||||
/// </summary>
|
||||
public bool SendUpdateNotificationToChat { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Load a configuration from the provided path.
|
||||
/// </summary>
|
||||
|
|
@ -504,7 +517,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
{
|
||||
deserialized =
|
||||
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
|
||||
|
||||
|
||||
// If this reads as null, the file was empty, that's no good
|
||||
if (deserialized == null)
|
||||
throw new Exception("Read config was null.");
|
||||
|
|
@ -530,7 +543,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
{
|
||||
Log.Error(e, "Failed to set defaults for DalamudConfiguration");
|
||||
}
|
||||
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
|
|
@ -549,12 +562,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
{
|
||||
this.Save();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
// Make sure that we save, if a save is queued while we are shutting down
|
||||
this.Update();
|
||||
|
||||
// Wait for the write task to finish
|
||||
this.writeTask?.Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -595,22 +611,36 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
|||
this.ReduceMotions = winAnimEnabled == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Migrate old auto-update setting to new auto-update behavior
|
||||
this.AutoUpdateBehavior ??= this.AutoUpdatePlugins
|
||||
? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll
|
||||
: Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
|
||||
private void Save()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
if (this.configPath is null)
|
||||
throw new InvalidOperationException("configPath is not set.");
|
||||
|
||||
Service<ReliableFileStorage>.Get().WriteAllText(
|
||||
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
|
||||
// Wait for previous write to finish
|
||||
this.writeTask?.Wait();
|
||||
|
||||
this.writeTask = Task.Run(() =>
|
||||
{
|
||||
Service<ReliableFileStorage>.Get().WriteAllText(
|
||||
this.configPath,
|
||||
JsonConvert.SerializeObject(this, SerializerSettings));
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath);
|
||||
}
|
||||
});
|
||||
|
||||
this.DalamudConfigurationSaved?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>11.0.2.0</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<DalamudVersion>11.0.8.0</DalamudVersion>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
<FileVersion>$(DalamudVersion)</FileVersion>
|
||||
|
|
@ -43,10 +43,6 @@
|
|||
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Debug'">
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Release'">
|
||||
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
|
||||
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\</PathMap>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Warnings">
|
||||
<NoWarn>IDE0002;IDE0003;IDE1006;IDE0044;CA1822;CS1591;CS1701;CS1702</NoWarn>
|
||||
|
|
@ -67,14 +63,14 @@
|
|||
<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="2024.2.0" />
|
||||
<PackageReference Include="Lumina" Version="5.6.0" />
|
||||
<PackageReference Include="Lumina.Excel" Version="7.1.3" />
|
||||
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
|
||||
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
|
||||
<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>
|
||||
<PackageReference Include="MinSharp" Version="1.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
|
||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
|
|
|
|||
|
|
@ -178,6 +178,9 @@ public sealed class EntryPoint
|
|||
throw new Exception("Working directory was invalid");
|
||||
|
||||
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory);
|
||||
|
||||
// Apply common fixes for culture issues
|
||||
CultureFixes.Apply();
|
||||
|
||||
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
|
||||
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
|
|||
|
||||
private bool lastConditionNone = true;
|
||||
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
|
|||
};
|
||||
}
|
||||
|
||||
[Api11ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
|
||||
[Api12ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool WarnMultithreadedUsage()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ public interface IReadOnlyCommandInfo
|
|||
/// <param name="command">The command itself.</param>
|
||||
/// <param name="arguments">The arguments supplied to the command, ready for parsing.</param>
|
||||
public delegate void HandlerDelegate(string command, string arguments);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched.
|
||||
/// </summary>
|
||||
|
|
@ -26,6 +26,11 @@ public interface IReadOnlyCommandInfo
|
|||
/// Gets a value indicating whether if this command should be shown in the help output.
|
||||
/// </summary>
|
||||
bool ShowInHelp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display order of this command. Defaults to alphabetical ordering.
|
||||
/// </summary>
|
||||
int DisplayOrder { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -51,4 +56,7 @@ public sealed class CommandInfo : IReadOnlyCommandInfo
|
|||
|
||||
/// <inheritdoc/>
|
||||
public bool ShowInHelp { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int DisplayOrder { get; set; } = -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
|
|||
}
|
||||
|
||||
private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8);
|
||||
|
||||
|
||||
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
|
||||
|
||||
|
||||
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -92,16 +92,22 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
|
|||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
|
||||
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
||||
|
||||
var manager = RaptureAtkUnitManager.Instance();
|
||||
if (manager == null)
|
||||
return;
|
||||
|
||||
var menu = manager->GetAddonByName("ContextMenu");
|
||||
var submenu = manager->GetAddonByName("AddonContextSub");
|
||||
if (menu == null || submenu == null)
|
||||
return;
|
||||
|
||||
if (menu->IsVisible)
|
||||
menu->FireCallbackInt(-1);
|
||||
if (submenu->IsVisible)
|
||||
submenu->FireCallbackInt(-1);
|
||||
|
||||
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
|
||||
this.addonContextMenuOnMenuSelectedHook.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api11ToDo("Maybe make this config scoped to internalname?")]
|
||||
[Api12ToDo("Maybe make this config scoped to internalname?")]
|
||||
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
using Dalamud.Game.Network.Internal;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Game.Network.Internal;
|
||||
using Dalamud.Game.Network.Structures;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
||||
using static Dalamud.Plugin.Services.IMarketBoard;
|
||||
|
||||
namespace Dalamud.Game.MarketBoard;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -29,19 +35,19 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
|
||||
public event HistoryReceivedDelegate? HistoryReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
|
||||
public event ItemPurchasedDelegate? ItemPurchased;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
|
||||
public event OfferingsReceivedDelegate? OfferingsReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
|
||||
public event PurchaseRequestedDelegate? PurchaseRequested;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
|
||||
public event TaxRatesReceivedDelegate? TaxRatesReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeService()
|
||||
|
|
@ -89,35 +95,42 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
|
|||
#pragma warning restore SA1015
|
||||
internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
|
||||
{
|
||||
private static readonly ModuleLog Log = new(nameof(MarketBoardPluginScoped));
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get();
|
||||
|
||||
private readonly string owningPluginName;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MarketBoardPluginScoped"/> class.
|
||||
/// </summary>
|
||||
internal MarketBoardPluginScoped()
|
||||
/// <param name="plugin">The plugin owning this service.</param>
|
||||
internal MarketBoardPluginScoped(LocalPlugin? plugin)
|
||||
{
|
||||
this.marketBoardService.HistoryReceived += this.OnHistoryReceived;
|
||||
this.marketBoardService.ItemPurchased += this.OnItemPurchased;
|
||||
this.marketBoardService.OfferingsReceived += this.OnOfferingsReceived;
|
||||
this.marketBoardService.PurchaseRequested += this.OnPurchaseRequested;
|
||||
this.marketBoardService.TaxRatesReceived += this.OnTaxRatesReceived;
|
||||
|
||||
this.owningPluginName = plugin?.InternalName ?? "DalamudInternal";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
|
||||
public event HistoryReceivedDelegate? HistoryReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
|
||||
public event ItemPurchasedDelegate? ItemPurchased;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
|
||||
public event OfferingsReceivedDelegate? OfferingsReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
|
||||
public event PurchaseRequestedDelegate? PurchaseRequested;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
|
||||
public event TaxRatesReceivedDelegate? TaxRatesReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
|
|
@ -137,26 +150,96 @@ internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoar
|
|||
|
||||
private void OnHistoryReceived(IMarketBoardHistory history)
|
||||
{
|
||||
this.HistoryReceived?.Invoke(history);
|
||||
if (this.HistoryReceived == null) return;
|
||||
|
||||
foreach (var action in this.HistoryReceived.GetInvocationList().Cast<HistoryReceivedDelegate>())
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(history);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogInvocationError(ex, nameof(this.HistoryReceived));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemPurchased(IMarketBoardPurchase purchase)
|
||||
{
|
||||
this.ItemPurchased?.Invoke(purchase);
|
||||
if (this.ItemPurchased == null) return;
|
||||
|
||||
foreach (var action in this.ItemPurchased.GetInvocationList().Cast<ItemPurchasedDelegate>())
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(purchase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogInvocationError(ex, nameof(this.ItemPurchased));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOfferingsReceived(IMarketBoardCurrentOfferings currentOfferings)
|
||||
{
|
||||
this.OfferingsReceived?.Invoke(currentOfferings);
|
||||
if (this.OfferingsReceived == null) return;
|
||||
|
||||
foreach (var action in this.OfferingsReceived.GetInvocationList()
|
||||
.Cast<OfferingsReceivedDelegate>())
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(currentOfferings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogInvocationError(ex, nameof(this.OfferingsReceived));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPurchaseRequested(IMarketBoardPurchaseHandler purchaseHandler)
|
||||
{
|
||||
this.PurchaseRequested?.Invoke(purchaseHandler);
|
||||
if (this.PurchaseRequested == null) return;
|
||||
|
||||
foreach (var action in this.PurchaseRequested.GetInvocationList().Cast<PurchaseRequestedDelegate>())
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(purchaseHandler);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogInvocationError(ex, nameof(this.PurchaseRequested));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaxRatesReceived(IMarketTaxRates taxRates)
|
||||
{
|
||||
this.TaxRatesReceived?.Invoke(taxRates);
|
||||
if (this.TaxRatesReceived == null) return;
|
||||
|
||||
foreach (var action in this.TaxRatesReceived.GetInvocationList().Cast<TaxRatesReceivedDelegate>())
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(taxRates);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogInvocationError(ex, nameof(this.TaxRatesReceived));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LogInvocationError(Exception ex, string delegateName)
|
||||
{
|
||||
Log.Error(
|
||||
ex,
|
||||
"An error occured while invoking event `{evName}` for {plugin}",
|
||||
delegateName,
|
||||
this.owningPluginName);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,20 +13,26 @@ internal interface IMarketBoardUploader
|
|||
/// Upload data about an item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item request data being uploaded.</param>
|
||||
/// <param name="uploaderId">The uploaders ContentId.</param>
|
||||
/// <param name="worldId">The uploaders WorldId.</param>
|
||||
/// <returns>An async task.</returns>
|
||||
Task Upload(MarketBoardItemRequest item);
|
||||
Task Upload(MarketBoardItemRequest item, ulong uploaderId, uint worldId);
|
||||
|
||||
/// <summary>
|
||||
/// Upload tax rate data.
|
||||
/// </summary>
|
||||
/// <param name="taxRates">The tax rate data being uploaded.</param>
|
||||
/// <param name="uploaderId">The uploaders ContentId.</param>
|
||||
/// <param name="worldId">The uploaders WorldId.</param>
|
||||
/// <returns>An async task.</returns>
|
||||
Task UploadTax(MarketTaxRates taxRates);
|
||||
Task UploadTax(MarketTaxRates taxRates, ulong uploaderId, uint worldId);
|
||||
|
||||
/// <summary>
|
||||
/// Upload information about a purchase this client has made.
|
||||
/// </summary>
|
||||
/// <param name="purchaseHandler">The purchase handler data associated with the sale.</param>
|
||||
/// <param name="uploaderId">The uploaders ContentId.</param>
|
||||
/// <param name="worldId">The uploaders WorldId.</param>
|
||||
/// <returns>An async task.</returns>
|
||||
Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler);
|
||||
Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler, ulong uploaderId, uint worldId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -33,21 +32,16 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
|
|||
this.httpClient = happyHttpClient.SharedHttpClient;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task Upload(MarketBoardItemRequest request)
|
||||
public async Task Upload(MarketBoardItemRequest request, ulong uploaderId, uint worldId)
|
||||
{
|
||||
var clientState = Service<ClientState.ClientState>.GetNullable();
|
||||
if (clientState == null)
|
||||
return;
|
||||
|
||||
Log.Verbose("Starting Universalis upload");
|
||||
var uploader = clientState.LocalContentId;
|
||||
|
||||
// ====================================================================================
|
||||
|
||||
var uploadObject = new UniversalisItemUploadRequest
|
||||
{
|
||||
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0,
|
||||
UploaderId = uploader.ToString(),
|
||||
WorldId = worldId,
|
||||
UploaderId = uploaderId.ToString(),
|
||||
ItemId = request.CatalogId,
|
||||
Listings = [],
|
||||
Sales = [],
|
||||
|
|
@ -117,18 +111,12 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task UploadTax(MarketTaxRates taxRates)
|
||||
public async Task UploadTax(MarketTaxRates taxRates, ulong uploaderId, uint worldId)
|
||||
{
|
||||
var clientState = Service<ClientState.ClientState>.GetNullable();
|
||||
if (clientState == null)
|
||||
return;
|
||||
|
||||
// ====================================================================================
|
||||
|
||||
var taxUploadObject = new UniversalisTaxUploadRequest
|
||||
{
|
||||
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0,
|
||||
UploaderId = clientState.LocalContentId.ToString(),
|
||||
WorldId = worldId,
|
||||
UploaderId = uploaderId.ToString(),
|
||||
TaxData = new UniversalisTaxData
|
||||
{
|
||||
LimsaLominsa = taxRates.LimsaLominsaTax,
|
||||
|
|
@ -159,14 +147,9 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
|
|||
/// to track the available listings, that is done via the listings packet. All this does is remove
|
||||
/// a listing, or delete it, when a purchase has been made.
|
||||
/// </remarks>
|
||||
public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler)
|
||||
public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler, ulong uploaderId, uint worldId)
|
||||
{
|
||||
var clientState = Service<ClientState.ClientState>.GetNullable();
|
||||
if (clientState == null)
|
||||
return;
|
||||
|
||||
var itemId = purchaseHandler.CatalogId;
|
||||
var worldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0;
|
||||
|
||||
// ====================================================================================
|
||||
|
||||
|
|
@ -176,7 +159,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
|
|||
Quantity = purchaseHandler.ItemQuantity,
|
||||
ListingId = purchaseHandler.ListingId.ToString(),
|
||||
RetainerId = purchaseHandler.RetainerId.ToString(),
|
||||
UploaderId = clientState.LocalContentId.ToString(),
|
||||
UploaderId = uploaderId.ToString(),
|
||||
};
|
||||
|
||||
var deletePath = $"/api/{worldId}/{itemId}/delete";
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@ using Dalamud.Hooking;
|
|||
using Dalamud.Networking.Http;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.Network;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Serilog;
|
||||
|
|
@ -264,6 +267,33 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
this.cfPopHook.Dispose();
|
||||
}
|
||||
|
||||
private static (ulong UploaderId, uint WorldId) GetUploaderInfo()
|
||||
{
|
||||
var agentLobby = AgentLobby.Instance();
|
||||
|
||||
var uploaderId = agentLobby->LobbyData.ContentId;
|
||||
if (uploaderId == 0)
|
||||
{
|
||||
var playerState = PlayerState.Instance();
|
||||
if (playerState->IsLoaded == 1)
|
||||
{
|
||||
uploaderId = playerState->ContentId;
|
||||
}
|
||||
}
|
||||
|
||||
var worldId = agentLobby->LobbyData.CurrentWorldId;
|
||||
if (worldId == 0)
|
||||
{
|
||||
var localPlayer = Control.GetLocalPlayer();
|
||||
if (localPlayer != null)
|
||||
{
|
||||
worldId = localPlayer->CurrentWorld;
|
||||
}
|
||||
}
|
||||
|
||||
return (uploaderId, worldId);
|
||||
}
|
||||
|
||||
private unsafe nint CfPopDetour(PublicContentDirector.EnterContentInfoPacket* packetData)
|
||||
{
|
||||
var result = this.cfPopHook.OriginalDisposeSafe(packetData);
|
||||
|
|
@ -424,14 +454,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
startObservable
|
||||
.And(this.OnMarketBoardSalesBatch(startObservable))
|
||||
.And(this.OnMarketBoardListingsBatch(startObservable))
|
||||
.Then((request, sales, listings) => (request, sales, listings)))
|
||||
.Then((request, sales, listings) => (request, sales, listings, GetUploaderInfo())))
|
||||
.Where(this.ShouldUpload)
|
||||
.SubscribeOn(ThreadPoolScheduler.Instance)
|
||||
.Subscribe(
|
||||
data =>
|
||||
{
|
||||
var (request, sales, listings) = data;
|
||||
this.UploadMarketBoardData(request, sales, listings);
|
||||
var (request, sales, listings, uploaderInfo) = data;
|
||||
this.UploadMarketBoardData(request, sales, listings, uploaderInfo.UploaderId, uploaderInfo.WorldId);
|
||||
},
|
||||
ex => Log.Error(ex, "Failed to handle Market Board item request event"));
|
||||
}
|
||||
|
|
@ -439,7 +469,9 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
private void UploadMarketBoardData(
|
||||
MarketBoardItemRequest request,
|
||||
(uint CatalogId, ICollection<MarketBoardHistory.MarketBoardHistoryListing> Sales) sales,
|
||||
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings)
|
||||
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings,
|
||||
ulong uploaderId,
|
||||
uint worldId)
|
||||
{
|
||||
var catalogId = sales.CatalogId;
|
||||
if (listings.Count != request.AmountToArrive)
|
||||
|
|
@ -460,7 +492,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
request.Listings.AddRange(listings);
|
||||
request.History.AddRange(sales.Sales);
|
||||
|
||||
Task.Run(() => this.uploader.Upload(request))
|
||||
Task.Run(() => this.uploader.Upload(request, uploaderId, worldId))
|
||||
.ContinueWith(
|
||||
task => Log.Error(task.Exception, "Market Board offerings data upload failed"),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
|
@ -469,11 +501,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
private IDisposable HandleMarketTaxRates()
|
||||
{
|
||||
return this.MbTaxesObservable
|
||||
.Select((taxes) => (taxes, GetUploaderInfo()))
|
||||
.Where(this.ShouldUpload)
|
||||
.SubscribeOn(ThreadPoolScheduler.Instance)
|
||||
.Subscribe(
|
||||
taxes =>
|
||||
data =>
|
||||
{
|
||||
var (taxes, uploaderInfo) = data;
|
||||
|
||||
Log.Verbose(
|
||||
"MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5} sh#{6}",
|
||||
taxes.LimsaLominsaTax,
|
||||
|
|
@ -484,7 +519,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
taxes.CrystariumTax,
|
||||
taxes.SharlayanTax);
|
||||
|
||||
Task.Run(() => this.uploader.UploadTax(taxes))
|
||||
Task.Run(() => this.uploader.UploadTax(taxes, uploaderInfo.UploaderId, uploaderInfo.WorldId))
|
||||
.ContinueWith(
|
||||
task => Log.Error(task.Exception, "Market Board tax data upload failed"),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
|
@ -495,13 +530,13 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
private IDisposable HandleMarketBoardPurchaseHandler()
|
||||
{
|
||||
return this.MbPurchaseSentObservable
|
||||
.Zip(this.MbPurchaseObservable)
|
||||
.Zip(this.MbPurchaseObservable, (handler, purchase) => (handler, purchase, GetUploaderInfo()))
|
||||
.Where(this.ShouldUpload)
|
||||
.SubscribeOn(ThreadPoolScheduler.Instance)
|
||||
.Subscribe(
|
||||
data =>
|
||||
{
|
||||
var (handler, purchase) = data;
|
||||
var (handler, purchase, uploaderInfo) = data;
|
||||
|
||||
var sameQty = purchase.ItemQuantity == handler.ItemQuantity;
|
||||
var itemMatch = purchase.CatalogId == handler.CatalogId;
|
||||
|
|
@ -516,7 +551,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
|
|||
handler.CatalogId,
|
||||
handler.PricePerUnit * purchase.ItemQuantity,
|
||||
handler.ListingId);
|
||||
Task.Run(() => this.uploader.UploadPurchase(handler))
|
||||
Task.Run(() => this.uploader.UploadPurchase(handler, uploaderInfo.UploaderId, uploaderInfo.WorldId))
|
||||
.ContinueWith(
|
||||
task => Log.Error(task.Exception, "Market Board purchase data upload failed"),
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ public sealed class XivChatEntry
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the name payloads
|
||||
/// Gets or sets the name payloads.
|
||||
/// </summary>
|
||||
public byte[] NameBytes { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the message payloads.
|
||||
/// Gets or sets the message payloads.
|
||||
/// </summary>
|
||||
public byte[] MessageBytes { get; set; } = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Interface.Animation;
|
||||
|
||||
/// <summary>
|
||||
/// Base class facilitating the implementation of easing functions.
|
||||
/// </summary>
|
||||
[Api12ToDo("Re-apply https://github.com/goatcorp/Dalamud/commit/1aada983931d9e45a250eebbc17c8b782d07701b")]
|
||||
public abstract class Easing
|
||||
{
|
||||
// TODO: Use game delta time here instead
|
||||
|
|
|
|||
|
|
@ -182,7 +182,10 @@ public static partial class ImGuiComponents
|
|||
/// </summary>
|
||||
/// <param name="icon">Icon to show.</param>
|
||||
/// <param name="text">Text to show.</param>
|
||||
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon & text.</param>
|
||||
/// <param name="size">
|
||||
/// Sets the size of the button. If either dimension is set to 0,
|
||||
/// that dimension will conform to the size of the icon and text.
|
||||
/// </param>
|
||||
/// <returns>Indicator if button is clicked.</returns>
|
||||
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector2 size) => IconButtonWithText(icon, text, null, null, null, size);
|
||||
|
||||
|
|
@ -194,7 +197,10 @@ public static partial class ImGuiComponents
|
|||
/// <param name="defaultColor">The default color of the button.</param>
|
||||
/// <param name="activeColor">The color of the button when active.</param>
|
||||
/// <param name="hoveredColor">The color of the button when hovered.</param>
|
||||
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon & text.</param>
|
||||
/// <param name="size">
|
||||
/// Sets the size of the button. If either dimension is set to 0,
|
||||
/// that dimension will conform to the size of the icon and text.
|
||||
/// </param>
|
||||
/// <returns>Indicator if button is clicked.</returns>
|
||||
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null)
|
||||
{
|
||||
|
|
@ -272,15 +278,14 @@ public static partial class ImGuiComponents
|
|||
/// <returns>Width.</returns>
|
||||
public static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text)
|
||||
{
|
||||
Vector2 iconSize;
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
var iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
||||
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
|
||||
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
|
||||
|
||||
return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding;
|
||||
iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
||||
}
|
||||
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
|
||||
return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ using ImGuiNET;
|
|||
|
||||
namespace Dalamud.Interface.Components;
|
||||
|
||||
/// <summary>
|
||||
/// ImGui component used to create a radio-like input that uses icon buttons.
|
||||
/// </summary>
|
||||
public static partial class ImGuiComponents
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
228
Dalamud/Interface/Internal/Asserts/AssertHandler.cs
Normal file
228
Dalamud/Interface/Internal/Asserts/AssertHandler.cs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Asserts;
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for registering and handling ImGui asserts.
|
||||
/// </summary>
|
||||
internal class AssertHandler : IDisposable
|
||||
{
|
||||
private const int HideThreshold = 20;
|
||||
private const int HidePrintEvery = 500;
|
||||
|
||||
private readonly HashSet<string> ignoredAsserts = [];
|
||||
private readonly Dictionary<string, uint> assertCounts = new();
|
||||
|
||||
// Store callback to avoid it from being GC'd
|
||||
private readonly AssertCallbackDelegate callback;
|
||||
|
||||
private bool everShownAssertThisSession = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AssertHandler"/> class.
|
||||
/// </summary>
|
||||
public AssertHandler()
|
||||
{
|
||||
this.callback = (expr, file, line) => this.OnImGuiAssert(expr, file, line);
|
||||
}
|
||||
|
||||
private delegate void AssertCallbackDelegate(
|
||||
[MarshalAs(UnmanagedType.LPStr)] string expr,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string file,
|
||||
int line);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether ImGui asserts should be shown to the user.
|
||||
/// </summary>
|
||||
public bool ShowAsserts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether we want to hide asserts that occur frequently (= every update)
|
||||
/// and whether we want to log callstacks.
|
||||
/// </summary>
|
||||
public bool EnableVerboseLogging { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Register the cimgui assert handler with the native library.
|
||||
/// </summary>
|
||||
public void Setup()
|
||||
{
|
||||
CustomNativeFunctions.igCustom_SetAssertCallback(this.callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister the cimgui assert handler with the native library.
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
CustomNativeFunctions.igCustom_SetAssertCallback(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.Shutdown();
|
||||
}
|
||||
|
||||
private void OnImGuiAssert(string expr, string file, int line)
|
||||
{
|
||||
var key = $"{file}:{line}";
|
||||
if (this.ignoredAsserts.Contains(key))
|
||||
return;
|
||||
|
||||
// Don't log unless we've ever shown an assert this session
|
||||
if (!this.ShowAsserts && !this.everShownAssertThisSession)
|
||||
return;
|
||||
|
||||
Lazy<string> stackTrace = new(() => DiagnosticUtil.GetUsefulTrace(new StackTrace()).ToString());
|
||||
|
||||
if (!this.EnableVerboseLogging)
|
||||
{
|
||||
if (this.assertCounts.TryGetValue(key, out var count))
|
||||
{
|
||||
this.assertCounts[key] = count + 1;
|
||||
|
||||
if (count <= HideThreshold || count % HidePrintEvery == 0)
|
||||
{
|
||||
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)",
|
||||
expr,
|
||||
file,
|
||||
line,
|
||||
count);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.assertCounts[key] = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}",
|
||||
expr,
|
||||
file,
|
||||
line,
|
||||
stackTrace.Value);
|
||||
}
|
||||
|
||||
if (!this.ShowAsserts)
|
||||
return;
|
||||
|
||||
this.everShownAssertThisSession = true;
|
||||
|
||||
string? GetRepoUrl()
|
||||
{
|
||||
// TODO: implot, imguizmo?
|
||||
const string userName = "goatcorp";
|
||||
const string repoName = "gc-imgui";
|
||||
const string branch = "1.88-enhanced-abifix";
|
||||
|
||||
if (!file.Contains("imgui", StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
var lastSlash = file.LastIndexOf('\\');
|
||||
var fileName = file[(lastSlash + 1)..];
|
||||
return $"https://github.com/{userName}/{repoName}/blob/{branch}/{fileName}#L{line}";
|
||||
}
|
||||
|
||||
// grab the stack trace now that we've decided to show UI.
|
||||
_ = stackTrace.Value;
|
||||
|
||||
var gitHubUrl = GetRepoUrl();
|
||||
var showOnGitHubButton = new TaskDialogButton
|
||||
{
|
||||
Text = "Open GitHub",
|
||||
AllowCloseDialog = false,
|
||||
Enabled = !gitHubUrl.IsNullOrEmpty(),
|
||||
};
|
||||
showOnGitHubButton.Click += (_, _) =>
|
||||
{
|
||||
if (!gitHubUrl.IsNullOrEmpty())
|
||||
Util.OpenLink(gitHubUrl);
|
||||
};
|
||||
|
||||
var breakButton = new TaskDialogButton
|
||||
{
|
||||
Text = "Break",
|
||||
AllowCloseDialog = true,
|
||||
};
|
||||
|
||||
var disableButton = new TaskDialogButton
|
||||
{
|
||||
Text = "Disable for this session",
|
||||
AllowCloseDialog = true,
|
||||
};
|
||||
|
||||
var ignoreButton = TaskDialogButton.Ignore;
|
||||
|
||||
TaskDialogButton? result = null;
|
||||
void DialogThreadStart()
|
||||
{
|
||||
// TODO(goat): This is probably not gonna work if we showed the loading dialog
|
||||
// this session since it already loaded visual styles...
|
||||
Application.EnableVisualStyles();
|
||||
|
||||
var page = new TaskDialogPage
|
||||
{
|
||||
Heading = "ImGui assertion failed",
|
||||
Caption = "Dalamud",
|
||||
Expander = new TaskDialogExpander
|
||||
{
|
||||
CollapsedButtonText = "Show stack trace",
|
||||
ExpandedButtonText = "Hide stack trace",
|
||||
Text = stackTrace.Value,
|
||||
},
|
||||
Text = $"Some code in a plugin or Dalamud itself has caused an internal assertion in ImGui to fail. The game will most likely crash now.\n\n{expr}\nAt: {file}:{line}",
|
||||
Icon = TaskDialogIcon.Warning,
|
||||
Buttons =
|
||||
[
|
||||
showOnGitHubButton,
|
||||
breakButton,
|
||||
disableButton,
|
||||
ignoreButton,
|
||||
],
|
||||
DefaultButton = showOnGitHubButton,
|
||||
};
|
||||
|
||||
result = TaskDialog.ShowDialog(page);
|
||||
}
|
||||
|
||||
// Run in a separate thread because of STA and to not mess up other stuff
|
||||
var thread = new Thread(DialogThreadStart)
|
||||
{
|
||||
Name = "Dalamud ImGui Assert Dialog",
|
||||
};
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
|
||||
if (result == breakButton)
|
||||
{
|
||||
Debugger.Break();
|
||||
}
|
||||
else if (result == disableButton)
|
||||
{
|
||||
this.ShowAsserts = false;
|
||||
}
|
||||
else if (result == ignoreButton)
|
||||
{
|
||||
this.ignoredAsserts.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CustomNativeFunctions
|
||||
{
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
#pragma warning disable SA1300
|
||||
public static extern void igCustom_SetAssertCallback(AssertCallbackDelegate? callback);
|
||||
#pragma warning restore SA1300
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +178,7 @@ internal class DalamudCommands : IServiceType
|
|||
if (arguments.IsNullOrWhitespace())
|
||||
{
|
||||
chatGui.Print(Loc.Localize("DalamudCmdHelpAvailable", "Available commands:"));
|
||||
foreach (var cmd in commandManager.Commands)
|
||||
foreach (var cmd in commandManager.Commands.OrderBy(cInfo => cInfo.Key))
|
||||
{
|
||||
if (!cmd.Value.ShowInHelp)
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ using Dalamud.Game.Text;
|
|||
using Dalamud.Hooking.WndProcHook;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
|
|
@ -38,9 +37,6 @@ namespace Dalamud.Interface.Internal;
|
|||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class DalamudIme : IInternalDisposableService
|
||||
{
|
||||
private const int CImGuiStbTextCreateUndoOffset = 0xB57A0;
|
||||
private const int CImGuiStbTextUndoOffset = 0xB59C0;
|
||||
|
||||
private const int ImePageSize = 9;
|
||||
|
||||
private static readonly Dictionary<int, string> WmNames =
|
||||
|
|
@ -70,11 +66,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
UnicodeRanges.HangulJamoExtendedB,
|
||||
};
|
||||
|
||||
private static readonly delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, int, int, int, void>
|
||||
StbTextMakeUndoReplace;
|
||||
|
||||
private static readonly delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, void> StbTextUndo;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
|
|
@ -135,13 +126,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StbTextMakeUndoReplace =
|
||||
(delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, int, int, int, void>)
|
||||
(cimgui + CImGuiStbTextCreateUndoOffset);
|
||||
StbTextUndo =
|
||||
(delegate* unmanaged<ImGuiInputTextState*, StbTextEditState*, void>)
|
||||
(cimgui + CImGuiStbTextUndoOffset);
|
||||
}
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
|
|
@ -185,7 +169,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
return true;
|
||||
if (!ImGui.GetIO().ConfigInputTextCursorBlink)
|
||||
return true;
|
||||
var textState = TextState;
|
||||
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
|
||||
if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0)
|
||||
return true;
|
||||
if (textState->CursorAnim <= 0)
|
||||
|
|
@ -194,9 +178,6 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
|
||||
private static ImGuiInputTextState* TextState =>
|
||||
(ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset);
|
||||
|
||||
/// <summary>Gets a value indicating whether to display partial conversion status.</summary>
|
||||
private bool ShowPartialConversion => this.partialConversionFrom != 0 ||
|
||||
this.partialConversionTo != this.compositionString.Length;
|
||||
|
|
@ -341,7 +322,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
|
||||
try
|
||||
{
|
||||
var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
|
||||
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
|
||||
var invalidTarget = textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0;
|
||||
|
||||
#if IMEDEBUG
|
||||
switch (args.Message)
|
||||
|
|
@ -570,19 +552,20 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
|
||||
this.ReflectCharacterEncounters(newString);
|
||||
|
||||
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
|
||||
if (this.temporaryUndoSelection is not null)
|
||||
{
|
||||
TextState->Undo();
|
||||
TextState->SelectionTuple = this.temporaryUndoSelection.Value;
|
||||
textState->Undo();
|
||||
textState->SelectionTuple = this.temporaryUndoSelection.Value;
|
||||
this.temporaryUndoSelection = null;
|
||||
}
|
||||
|
||||
TextState->SanitizeSelectionRange();
|
||||
if (TextState->ReplaceSelectionAndPushUndo(newString))
|
||||
this.temporaryUndoSelection = TextState->SelectionTuple;
|
||||
textState->SanitizeSelectionRange();
|
||||
if (textState->ReplaceSelectionAndPushUndo(newString))
|
||||
this.temporaryUndoSelection = textState->SelectionTuple;
|
||||
|
||||
// Put the cursor at the beginning, so that the candidate window appears aligned with the text.
|
||||
TextState->SetSelectionRange(TextState->SelectionTuple.Start, newString.Length, 0);
|
||||
textState->SetSelectionRange(textState->SelectionTuple.Start, newString.Length, 0);
|
||||
|
||||
if (finalCommit)
|
||||
{
|
||||
|
|
@ -627,7 +610,10 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
this.partialConversionFrom = this.partialConversionTo = 0;
|
||||
this.compositionCursorOffset = 0;
|
||||
this.temporaryUndoSelection = null;
|
||||
TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd;
|
||||
|
||||
var textState = CustomNativeFunctions.igCustom_GetInputTextState();
|
||||
textState->Stb.SelectStart = textState->Stb.Cursor = textState->Stb.SelectEnd;
|
||||
|
||||
this.candidateStrings.Clear();
|
||||
this.immCandNative = default;
|
||||
if (invokeCancel)
|
||||
|
|
@ -1030,14 +1016,14 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
(s, e) = (e, s);
|
||||
}
|
||||
|
||||
public void Undo() => StbTextUndo(this.ThisPtr, &this.ThisPtr->Stb);
|
||||
public void Undo() => CustomNativeFunctions.igCustom_StbTextUndo(this.ThisPtr);
|
||||
|
||||
public bool MakeUndoReplace(int offset, int oldLength, int newLength)
|
||||
{
|
||||
if (oldLength == 0 && newLength == 0)
|
||||
return false;
|
||||
|
||||
StbTextMakeUndoReplace(this.ThisPtr, &this.ThisPtr->Stb, offset, oldLength, newLength);
|
||||
CustomNativeFunctions.igCustom_StbTextMakeUndoReplace(this.ThisPtr, offset, oldLength, newLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1113,6 +1099,20 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
|
||||
private static class CustomNativeFunctions
|
||||
{
|
||||
#pragma warning disable SA1300
|
||||
[DllImport("cimgui")]
|
||||
public static extern ImGuiInputTextState* igCustom_GetInputTextState();
|
||||
|
||||
[DllImport("cimgui")]
|
||||
public static extern void igCustom_StbTextMakeUndoReplace(ImGuiInputTextState* str, int where, int old_length, int new_length);
|
||||
|
||||
[DllImport("cimgui")]
|
||||
public static extern void igCustom_StbTextUndo(ImGuiInputTextState* str);
|
||||
#pragma warning restore SA1300
|
||||
}
|
||||
|
||||
#if IMEDEBUG
|
||||
private static class ImeDebug
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ using Dalamud.Game.Internal;
|
|||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Animation.EasingFunctions;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.Internal.Windows;
|
||||
using Dalamud.Interface.Internal.Windows.Data;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
|
|
@ -58,7 +57,6 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
private readonly Dalamud dalamud;
|
||||
private readonly DalamudConfiguration configuration;
|
||||
private readonly InterfaceManager interfaceManager;
|
||||
private readonly DataManager dataManager;
|
||||
|
||||
private readonly ChangelogWindow changelogWindow;
|
||||
private readonly ColorDemoWindow colorDemoWindow;
|
||||
|
|
@ -93,14 +91,13 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
private bool isImPlotDrawDemoWindow = false;
|
||||
private bool isImGuiTestWindowsInMonospace = false;
|
||||
private bool isImGuiDrawMetricsWindow = false;
|
||||
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private DalamudInterface(
|
||||
Dalamud dalamud,
|
||||
DalamudConfiguration configuration,
|
||||
FontAtlasFactory fontAtlasFactory,
|
||||
InterfaceManager interfaceManager,
|
||||
DataManager dataManager,
|
||||
PluginImageCache pluginImageCache,
|
||||
DalamudAssetManager dalamudAssetManager,
|
||||
Game.Framework framework,
|
||||
|
|
@ -113,10 +110,9 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
this.dalamud = dalamud;
|
||||
this.configuration = configuration;
|
||||
this.interfaceManager = interfaceManager;
|
||||
this.dataManager = dataManager;
|
||||
|
||||
this.WindowSystem = new WindowSystem("DalamudCore");
|
||||
|
||||
|
||||
this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false };
|
||||
this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false };
|
||||
this.dataWindow = new DataWindow() { IsOpen = false };
|
||||
|
|
@ -163,7 +159,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
this.WindowSystem.AddWindow(this.branchSwitcherWindow);
|
||||
this.WindowSystem.AddWindow(this.hitchSettingsWindow);
|
||||
|
||||
ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup;
|
||||
this.interfaceManager.ShowAsserts = configuration.ImGuiAssertsEnabledAtStartup ?? false;
|
||||
this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup;
|
||||
|
||||
this.interfaceManager.Draw += this.OnDraw;
|
||||
|
|
@ -197,7 +193,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
|
||||
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
|
||||
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
|
||||
|
||||
|
||||
// This is temporary, until we know the repercussions of vtable hooking mode
|
||||
consoleManager.AddCommand(
|
||||
"dalamud.interface.swapchain_mode",
|
||||
|
|
@ -216,14 +212,14 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
Log.Error("Unknown swapchain mode: {Mode}", mode);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
this.configuration.QueueSave();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private delegate nint CrashDebugDelegate(nint self);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of frames since Dalamud has loaded.
|
||||
/// </summary>
|
||||
|
|
@ -323,7 +319,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
this.pluginStatWindow.IsOpen = true;
|
||||
this.pluginStatWindow.BringToFront();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Opens the <see cref="PluginInstallerWindow"/> on the plugin installed.
|
||||
/// </summary>
|
||||
|
|
@ -388,7 +384,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
this.profilerWindow.IsOpen = true;
|
||||
this.profilerWindow.BringToFront();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Opens the <see cref="HitchSettingsWindow"/>.
|
||||
/// </summary>
|
||||
|
|
@ -700,7 +696,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
|
||||
var logSynchronously = this.configuration.LogSynchronously;
|
||||
if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously))
|
||||
{
|
||||
|
|
@ -792,14 +788,14 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
|
||||
if (ImGui.BeginMenu("Crash game"))
|
||||
{
|
||||
if (ImGui.MenuItem("Access Violation"))
|
||||
{
|
||||
Marshal.ReadByte(IntPtr.Zero);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Set UiModule to NULL"))
|
||||
{
|
||||
unsafe
|
||||
|
|
@ -808,7 +804,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
framework->UIModule = (UIModule*)0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ImGui.MenuItem("Set UiModule to invalid ptr"))
|
||||
{
|
||||
unsafe
|
||||
|
|
@ -817,7 +813,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
framework->UIModule = (UIModule*)0x12345678;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ImGui.MenuItem("Deref nullptr in Hook"))
|
||||
{
|
||||
unsafe
|
||||
|
|
@ -832,7 +828,13 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
hook.Enable();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (ImGui.MenuItem("Cause ImGui assert"))
|
||||
{
|
||||
ImGui.PopStyleVar();
|
||||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
}
|
||||
|
||||
|
|
@ -848,7 +850,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
{
|
||||
this.OpenBranchSwitcher();
|
||||
}
|
||||
|
||||
|
||||
ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false);
|
||||
ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false);
|
||||
ImGui.MenuItem($"CLR: {Environment.Version}", false);
|
||||
|
|
@ -865,18 +867,27 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
|
||||
ImGui.Separator();
|
||||
|
||||
var val = ImGuiManagedAsserts.AssertsEnabled;
|
||||
if (ImGui.MenuItem("Enable Asserts", string.Empty, ref val))
|
||||
var showAsserts = this.interfaceManager.ShowAsserts;
|
||||
if (ImGui.MenuItem("Enable assert popups", string.Empty, ref showAsserts))
|
||||
{
|
||||
ImGuiManagedAsserts.AssertsEnabled = val;
|
||||
this.interfaceManager.ShowAsserts = showAsserts;
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Enable asserts at startup", null, this.configuration.AssertsEnabledAtStartup))
|
||||
var enableVerboseAsserts = this.interfaceManager.EnableVerboseAssertLogging;
|
||||
if (ImGui.MenuItem("Enable verbose assert logging", string.Empty, ref enableVerboseAsserts))
|
||||
{
|
||||
this.configuration.AssertsEnabledAtStartup = !this.configuration.AssertsEnabledAtStartup;
|
||||
this.interfaceManager.EnableVerboseAssertLogging = enableVerboseAsserts;
|
||||
}
|
||||
|
||||
var assertsEnabled = this.configuration.ImGuiAssertsEnabledAtStartup ?? false;
|
||||
if (ImGui.MenuItem("Enable asserts at startup", null, assertsEnabled))
|
||||
{
|
||||
this.configuration.ImGuiAssertsEnabledAtStartup = !assertsEnabled;
|
||||
this.configuration.QueueSave();
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.MenuItem("Clear focus"))
|
||||
{
|
||||
ImGui.SetWindowFocus(null);
|
||||
|
|
@ -924,7 +935,7 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
{
|
||||
this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo;
|
||||
}
|
||||
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.MenuItem("Show loading window"))
|
||||
|
|
@ -1001,6 +1012,11 @@ internal class DalamudInterface : IInternalDisposableService
|
|||
pluginManager.LoadBannedPlugins = !pluginManager.LoadBannedPlugins;
|
||||
}
|
||||
|
||||
if (pluginManager.SafeMode && ImGui.MenuItem("Disable Safe Mode"))
|
||||
{
|
||||
pluginManager.SafeMode = false;
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false);
|
||||
ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count(), false);
|
||||
|
|
|
|||
|
|
@ -32,19 +32,21 @@ internal static partial class DalamudComponents
|
|||
var pm = Service<PluginManager>.GetNullable();
|
||||
if (pm == null)
|
||||
return 0;
|
||||
|
||||
|
||||
var addPluginToProfilePopupId = ImGui.GetID(id);
|
||||
using var popup = ImRaii.Popup(id);
|
||||
|
||||
if (popup.Success)
|
||||
{
|
||||
var width = ImGuiHelpers.GlobalScale * 300;
|
||||
|
||||
|
||||
ImGui.SetNextItemWidth(width);
|
||||
ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref pickerSearch, 255);
|
||||
|
||||
var currentSearchString = pickerSearch;
|
||||
if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
|
||||
|
||||
using var listBox = ImRaii.ListBox("###pluginPicker", new Vector2(width, width - 80));
|
||||
if (listBox.Success)
|
||||
{
|
||||
// TODO: Plugin searching should be abstracted... installer and this should use the same search
|
||||
var plugins = pm.InstalledPlugins.Where(
|
||||
|
|
@ -53,19 +55,15 @@ internal static partial class DalamudComponents
|
|||
currentSearchString,
|
||||
StringComparison.InvariantCultureIgnoreCase)))
|
||||
.Where(pluginFiltered ?? (_ => true));
|
||||
|
||||
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
using var disabled2 =
|
||||
ImRaii.Disabled(pluginDisabled(plugin));
|
||||
|
||||
using var disabled2 = ImRaii.Disabled(pluginDisabled(plugin));
|
||||
if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}"))
|
||||
{
|
||||
onClicked(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndListBox();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,222 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Fixes ImDrawList not correctly dealing with the current texture for that draw list not in tune with the global
|
||||
/// state. Currently, ImDrawList::AddPolyLine and ImDrawList::AddRectFilled are affected.
|
||||
///
|
||||
/// * The implementation for AddRectFilled is entirely replaced with the hook below.
|
||||
/// * The implementation for AddPolyLine is wrapped with Push/PopTextureID.
|
||||
///
|
||||
/// TODO:
|
||||
/// * imgui_draw.cpp:1433 ImDrawList::AddRectFilled
|
||||
/// The if block needs a PushTextureID(_Data->TexIdCommon)/PopTextureID() block,
|
||||
/// if _Data->TexIdCommon != _CmdHeader.TextureId.
|
||||
/// * imgui_draw.cpp:729 ImDrawList::AddPolyLine
|
||||
/// The if block always needs to call PushTextureID if the abovementioned condition is not met.
|
||||
/// Change push_texture_id to only have one condition.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal sealed unsafe class ImGuiDrawListFixProvider : IInternalDisposableService
|
||||
{
|
||||
private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0;
|
||||
private const int CImGuiImDrawListAddRectFilled = 0x59FD0;
|
||||
private const int CImGuiImDrawListAddImageRounded = 0x58390;
|
||||
private const int CImGuiImDrawListSharedDataTexIdCommonOffset = 0;
|
||||
|
||||
private readonly Hook<ImDrawListAddPolyLine> hookImDrawListAddPolyline;
|
||||
private readonly Hook<ImDrawListAddRectFilled> hookImDrawListAddRectFilled;
|
||||
private readonly Hook<ImDrawListAddImageRounded> hookImDrawListAddImageRounded;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws)
|
||||
{
|
||||
// Force cimgui.dll to be loaded.
|
||||
_ = ImGui.GetCurrentContext();
|
||||
var cimgui = Process.GetCurrentProcess().Modules.Cast<ProcessModule>()
|
||||
.First(x => x.ModuleName == "cimgui.dll")
|
||||
.BaseAddress;
|
||||
|
||||
this.hookImDrawListAddPolyline = Hook<ImDrawListAddPolyLine>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddPolyLineOffset,
|
||||
this.ImDrawListAddPolylineDetour);
|
||||
this.hookImDrawListAddRectFilled = Hook<ImDrawListAddRectFilled>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddRectFilled,
|
||||
this.ImDrawListAddRectFilledDetour);
|
||||
this.hookImDrawListAddImageRounded = Hook<ImDrawListAddImageRounded>.FromAddress(
|
||||
cimgui + CImGuiImDrawListAddImageRounded,
|
||||
this.ImDrawListAddImageRoundedDetour);
|
||||
this.hookImDrawListAddPolyline.Enable();
|
||||
this.hookImDrawListAddRectFilled.Enable();
|
||||
this.hookImDrawListAddImageRounded.Enable();
|
||||
}
|
||||
|
||||
private delegate void ImDrawListAddPolyLine(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 points,
|
||||
int pointsCount,
|
||||
uint color,
|
||||
ImDrawFlags flags,
|
||||
float thickness);
|
||||
|
||||
private delegate void ImDrawListAddRectFilled(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 min,
|
||||
ref Vector2 max,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags);
|
||||
|
||||
private delegate void ImDrawListAddImageRounded(
|
||||
ImDrawListPtr drawListPtr,
|
||||
nint userTextureId, ref Vector2 xy0,
|
||||
ref Vector2 xy1,
|
||||
ref Vector2 uv0,
|
||||
ref Vector2 uv1,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.hookImDrawListAddPolyline.Dispose();
|
||||
this.hookImDrawListAddRectFilled.Dispose();
|
||||
this.hookImDrawListAddImageRounded.Dispose();
|
||||
}
|
||||
|
||||
private static ImDrawFlags FixRectCornerFlags(ImDrawFlags flags)
|
||||
{
|
||||
#if !IMGUI_DISABLE_OBSOLETE_FUNCTIONS
|
||||
// Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All)
|
||||
// ~0 --> ImDrawFlags_RoundCornersAll or 0
|
||||
if ((int)flags == ~0)
|
||||
return ImDrawFlags.RoundCornersAll;
|
||||
|
||||
// Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations)
|
||||
// 0x01 --> ImDrawFlags_RoundCornersTopLeft (VALUE 0x01 OVERLAPS ImDrawFlags_Closed but ImDrawFlags_Closed is never valid in this path!)
|
||||
// 0x02 --> ImDrawFlags_RoundCornersTopRight
|
||||
// 0x03 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight
|
||||
// 0x04 --> ImDrawFlags_RoundCornersBotLeft
|
||||
// 0x05 --> ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBotLeft
|
||||
// ...
|
||||
// 0x0F --> ImDrawFlags_RoundCornersAll or 0
|
||||
// (See all values in ImDrawCornerFlags_)
|
||||
if ((int)flags >= 0x01 && (int)flags <= 0x0F)
|
||||
return (ImDrawFlags)((int)flags << 4);
|
||||
|
||||
// We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f'
|
||||
#endif
|
||||
|
||||
// If this triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values.
|
||||
// Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc...
|
||||
if (((int)flags & 0x0F) != 0)
|
||||
throw new ArgumentException("Misuse of legacy hardcoded ImDrawCornerFlags values!");
|
||||
|
||||
if ((flags & ImDrawFlags.RoundCornersMask) == 0)
|
||||
flags |= ImDrawFlags.RoundCornersAll;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
private void ImDrawListAddRectFilledDetour(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 min,
|
||||
ref Vector2 max,
|
||||
uint col,
|
||||
float rounding,
|
||||
ImDrawFlags flags)
|
||||
{
|
||||
// Skip drawing if we're drawing something with alpha value of 0.
|
||||
if ((col & 0xFF000000) == 0)
|
||||
return;
|
||||
|
||||
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask)
|
||||
{
|
||||
// Take the fast path of drawing two triangles if no rounded corners are required.
|
||||
|
||||
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
|
||||
if (pushTextureId)
|
||||
drawListPtr.PushTextureID(texIdCommon);
|
||||
|
||||
drawListPtr.PrimReserve(6, 4);
|
||||
drawListPtr.PrimRect(min, max, col);
|
||||
|
||||
if (pushTextureId)
|
||||
drawListPtr.PopTextureID();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Defer drawing rectangle with rounded corners to path drawing operations.
|
||||
// Note that this may have a slightly different extent behaviors from the above if case.
|
||||
// This is how it is in imgui_draw.cpp.
|
||||
drawListPtr.PathRect(min, max, rounding, flags);
|
||||
drawListPtr.PathFillConvex(col);
|
||||
}
|
||||
}
|
||||
|
||||
private void ImDrawListAddPolylineDetour(
|
||||
ImDrawListPtr drawListPtr,
|
||||
ref Vector2 points,
|
||||
int pointsCount,
|
||||
uint color,
|
||||
ImDrawFlags flags,
|
||||
float thickness)
|
||||
{
|
||||
var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId;
|
||||
if (pushTextureId)
|
||||
drawListPtr.PushTextureID(texIdCommon);
|
||||
|
||||
this.hookImDrawListAddPolyline.Original(drawListPtr, ref points, pointsCount, color, flags, thickness);
|
||||
|
||||
if (pushTextureId)
|
||||
drawListPtr.PopTextureID();
|
||||
}
|
||||
|
||||
private void ImDrawListAddImageRoundedDetour(ImDrawListPtr drawListPtr, nint userTextureId, ref Vector2 xy0, ref Vector2 xy1, ref Vector2 uv0, ref Vector2 uv1, uint col, float rounding, ImDrawFlags flags)
|
||||
{
|
||||
// Skip drawing if we're drawing something with alpha value of 0.
|
||||
if ((col & 0xFF000000) == 0)
|
||||
return;
|
||||
|
||||
// Handle non-rounded cases.
|
||||
flags = FixRectCornerFlags(flags);
|
||||
if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersNone)
|
||||
{
|
||||
drawListPtr.AddImage(userTextureId, xy0, xy1, uv0, uv1, col);
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporary provide the requested image as the common texture ID, so that the underlying
|
||||
// ImDrawList::AddConvexPolyFilled does not create a separate draw command and then revert back.
|
||||
// ImDrawList::AddImageRounded will temporarily push the texture ID provided by the user if the latest draw
|
||||
// command does not point to the texture we're trying to draw. Once pushed, ImDrawList::AddConvexPolyFilled
|
||||
// will leave the list of draw commands alone, so that ImGui::ShadeVertsLinearUV can safely work on the latest
|
||||
// draw command.
|
||||
ref var texIdCommon = ref *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset);
|
||||
var texIdCommonPrev = texIdCommon;
|
||||
texIdCommon = userTextureId;
|
||||
|
||||
this.hookImDrawListAddImageRounded.Original(
|
||||
drawListPtr,
|
||||
texIdCommon,
|
||||
ref xy0,
|
||||
ref xy1,
|
||||
ref uv0,
|
||||
ref uv1,
|
||||
col,
|
||||
rounding,
|
||||
flags);
|
||||
|
||||
texIdCommon = texIdCommonPrev;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,16 @@ using Dalamud.Hooking.Internal;
|
|||
using Dalamud.Hooking.WndProcHook;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||
using Dalamud.Interface.Internal.Asserts;
|
||||
using Dalamud.Interface.Internal.DesignSystem;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Interface.Windowing.Persistence;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
|
|
@ -60,6 +62,7 @@ namespace Dalamud.Interface.Internal;
|
|||
/// This class manages interaction with the ImGui interface.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
[InherentDependency<WindowSystemPersistence>] // Used by window system windows to restore state from the configuration
|
||||
internal partial class InterfaceManager : IInternalDisposableService
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -73,7 +76,7 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
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<IDisposable> deferredDisposeDisposables = new();
|
||||
|
||||
|
|
@ -94,6 +97,8 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
|
||||
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
|
||||
|
||||
private readonly AssertHandler assertHandler = new();
|
||||
|
||||
private RawDX11Scene? scene;
|
||||
|
||||
private Hook<SetCursorDelegate>? setCursorHook;
|
||||
|
|
@ -267,11 +272,27 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
/// </remarks>
|
||||
public long CumulativePresentCalls { get; private set; }
|
||||
|
||||
/// <inheritdoc cref="AssertHandler.ShowAsserts"/>
|
||||
public bool ShowAsserts
|
||||
{
|
||||
get => this.assertHandler.ShowAsserts;
|
||||
set => this.assertHandler.ShowAsserts = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="AssertHandler.EnableVerboseLogging"/>
|
||||
public bool EnableVerboseAssertLogging
|
||||
{
|
||||
get => this.assertHandler.EnableVerboseLogging;
|
||||
set => this.assertHandler.EnableVerboseLogging = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.assertHandler.Dispose();
|
||||
|
||||
// Unload hooks from the framework thread if possible.
|
||||
// We're currently off the framework thread, as this function can only be called from
|
||||
// ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread.
|
||||
|
|
@ -565,6 +586,7 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
{
|
||||
try
|
||||
{
|
||||
this.assertHandler.Setup();
|
||||
newScene = new RawDX11Scene((nint)swapChain);
|
||||
}
|
||||
catch (DllNotFoundException ex)
|
||||
|
|
@ -797,14 +819,14 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene.
|
||||
_ = this.dalamudAtlas.BuildFontsAsync();
|
||||
|
||||
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
|
||||
var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC);
|
||||
if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED)
|
||||
this.gameWindowHandle = swapChainDesc.OutputWindow;
|
||||
this.gameWindowHandle = swapChainDesc.OutputWindow;
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -947,7 +969,7 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
|
||||
switch (this.dalamudConfiguration.SwapChainHookMode)
|
||||
{
|
||||
case SwapChainHelper.HookMode.ByteCode:
|
||||
case SwapChainHelper.HookMode.ByteCode:
|
||||
default:
|
||||
{
|
||||
Log.Information("Hooking using bytecode...");
|
||||
|
|
@ -1128,15 +1150,22 @@ internal partial class InterfaceManager : IInternalDisposableService
|
|||
WindowSystem.HasAnyWindowSystemFocus = false;
|
||||
WindowSystem.FocusedWindowSystemNamespace = string.Empty;
|
||||
|
||||
var snap = ImGuiManagedAsserts.GetSnapshot();
|
||||
|
||||
if (this.IsDispatchingEvents)
|
||||
{
|
||||
this.Draw?.Invoke();
|
||||
try
|
||||
{
|
||||
this.Draw?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error when invoking global Draw");
|
||||
|
||||
// We should always handle this in the callbacks.
|
||||
Util.Fatal("An internal error occurred while drawing the Dalamud UI and the game must close.\nPlease report this error.", "Dalamud");
|
||||
}
|
||||
|
||||
Service<NotificationManager>.GetNullable()?.Draw();
|
||||
}
|
||||
|
||||
ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
namespace Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
||||
/// <summary>
|
||||
/// Offsets to various data in ImGui context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Last updated for ImGui 1.83.
|
||||
/// </remarks>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the unsage instead.")]
|
||||
internal static class ImGuiContextOffsets
|
||||
{
|
||||
public const int CurrentWindowStackOffset = 0x73A;
|
||||
|
||||
public const int ColorStackOffset = 0x79C;
|
||||
|
||||
public const int StyleVarStackOffset = 0x7A0;
|
||||
|
||||
public const int FontStackOffset = 0x7A4;
|
||||
|
||||
public const int BeginPopupStackOffset = 0x7B8;
|
||||
|
||||
public const int TextStateOffset = 0x4588;
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using static Dalamud.NativeFunctions;
|
||||
|
||||
namespace Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
||||
/// <summary>
|
||||
/// Report ImGui problems with a MessageBox dialog.
|
||||
/// </summary>
|
||||
internal static class ImGuiManagedAsserts
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether asserts are enabled for ImGui.
|
||||
/// </summary>
|
||||
public static bool AssertsEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a snapshot of the current ImGui context.
|
||||
/// Should be called before rendering an ImGui frame.
|
||||
/// </summary>
|
||||
/// <returns>A snapshot of the current context.</returns>
|
||||
public static unsafe ImGuiContextSnapshot GetSnapshot()
|
||||
{
|
||||
var contextPtr = ImGui.GetCurrentContext();
|
||||
|
||||
var styleVarStack = *((int*)contextPtr + ImGuiContextOffsets.StyleVarStackOffset); // ImVector.Size
|
||||
var colorStack = *((int*)contextPtr + ImGuiContextOffsets.ColorStackOffset); // ImVector.Size
|
||||
var fontStack = *((int*)contextPtr + ImGuiContextOffsets.FontStackOffset); // ImVector.Size
|
||||
var popupStack = *((int*)contextPtr + ImGuiContextOffsets.BeginPopupStackOffset); // ImVector.Size
|
||||
var windowStack = *((int*)contextPtr + ImGuiContextOffsets.CurrentWindowStackOffset); // ImVector.Size
|
||||
|
||||
return new ImGuiContextSnapshot
|
||||
{
|
||||
StyleVarStackSize = styleVarStack,
|
||||
ColorStackSize = colorStack,
|
||||
FontStackSize = fontStack,
|
||||
BeginPopupStackSize = popupStack,
|
||||
WindowStackSize = windowStack,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare a snapshot to the current post-draw state and report any errors in a MessageBox dialog.
|
||||
/// </summary>
|
||||
/// <param name="source">The source of any problems, something to blame.</param>
|
||||
/// <param name="before">ImGui context snapshot.</param>
|
||||
public static void ReportProblems(string source, ImGuiContextSnapshot before)
|
||||
{
|
||||
// TODO: Needs to be updated for ImGui 1.88
|
||||
return;
|
||||
|
||||
#pragma warning disable CS0162
|
||||
if (!AssertsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cSnap = GetSnapshot();
|
||||
|
||||
if (before.StyleVarStackSize != cSnap.StyleVarStackSize)
|
||||
{
|
||||
ShowAssert(source, $"You forgot to pop a style var!\n\nBefore: {before.StyleVarStackSize}, after: {cSnap.StyleVarStackSize}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (before.ColorStackSize != cSnap.ColorStackSize)
|
||||
{
|
||||
ShowAssert(source, $"You forgot to pop a color!\n\nBefore: {before.ColorStackSize}, after: {cSnap.ColorStackSize}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (before.FontStackSize != cSnap.FontStackSize)
|
||||
{
|
||||
ShowAssert(source, $"You forgot to pop a font!\n\nBefore: {before.FontStackSize}, after: {cSnap.FontStackSize}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (before.BeginPopupStackSize != cSnap.BeginPopupStackSize)
|
||||
{
|
||||
ShowAssert(source, $"You forgot to end a popup!\n\nBefore: {before.BeginPopupStackSize}, after: {cSnap.BeginPopupStackSize}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cSnap.WindowStackSize != 1)
|
||||
{
|
||||
if (cSnap.WindowStackSize > 1)
|
||||
{
|
||||
ShowAssert(source, $"Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?\n\ncSnap.WindowStackSize = {cSnap.WindowStackSize}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowAssert(source, $"Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?\n\ncSnap.WindowStackSize = {cSnap.WindowStackSize}");
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0162
|
||||
}
|
||||
|
||||
private static void ShowAssert(string source, string message)
|
||||
{
|
||||
var caption = $"You fucked up";
|
||||
message = $"{message}\n\nSource: {source}\n\nAsserts are now disabled. You may re-enable them.";
|
||||
var flags = MessageBoxType.Ok | MessageBoxType.IconError;
|
||||
|
||||
_ = MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
|
||||
AssertsEnabled = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A snapshot of various ImGui context properties.
|
||||
/// </summary>
|
||||
public class ImGuiContextSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the ImGui style var stack size.
|
||||
/// </summary>
|
||||
public int StyleVarStackSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ImGui color stack size.
|
||||
/// </summary>
|
||||
public int ColorStackSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ImGui font stack size.
|
||||
/// </summary>
|
||||
public int FontStackSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ImGui begin popup stack size.
|
||||
/// </summary>
|
||||
public int BeginPopupStackSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ImGui window stack size.
|
||||
/// </summary>
|
||||
public int WindowStackSize { get; init; }
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +60,7 @@ internal class DataWindow : Window, IDisposable
|
|||
new ToastWidget(),
|
||||
new UiColorWidget(),
|
||||
new UldWidget(),
|
||||
new VfsWidget(),
|
||||
};
|
||||
|
||||
private readonly IOrderedEnumerable<IDataWindowWidget> orderedModules;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
public class IconBrowserWidget : IDataWindowWidget
|
||||
{
|
||||
private const int MaxIconId = 250_000;
|
||||
|
||||
|
||||
private Vector2 iconSize = new(64.0f, 64.0f);
|
||||
private Vector2 editIconSize = new(64.0f, 64.0f);
|
||||
|
||||
|
|
@ -126,7 +126,6 @@ public class IconBrowserWidget : IDataWindowWidget
|
|||
this.valueRange = null;
|
||||
}
|
||||
|
||||
|
||||
ImGui.NextColumn();
|
||||
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0))
|
||||
|
|
@ -204,7 +203,7 @@ public class IconBrowserWidget : IDataWindowWidget
|
|||
ImGui.GetColorU32(ImGuiColors.DalamudRed),
|
||||
iconText);
|
||||
}
|
||||
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip($"{iconId}\n{exc}".Replace("%", "%%"));
|
||||
|
||||
|
|
@ -224,7 +223,7 @@ public class IconBrowserWidget : IDataWindowWidget
|
|||
cursor + ((this.iconSize - textSize) / 2),
|
||||
color,
|
||||
text);
|
||||
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(iconId.ToString());
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Game.Text;
|
||||
using ImGuiNET;
|
||||
using System.Linq;
|
||||
|
||||
using System.Linq;
|
||||
using Dalamud.Game.Text;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
|
|||
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Text;
|
||||
using Lumina.Text.Payloads;
|
||||
|
|
@ -31,7 +30,6 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue"];
|
||||
private ImVectorWrapper<byte> testStringBuffer;
|
||||
private string testString = string.Empty;
|
||||
private ExcelSheet<Addon> addons = null!;
|
||||
private ReadOnlySeString? logkind;
|
||||
private SeStringDrawParams style;
|
||||
private bool interactable;
|
||||
|
|
@ -51,7 +49,6 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
public void Load()
|
||||
{
|
||||
this.style = new() { GetEntity = this.GetEntity };
|
||||
this.addons = Service<DataManager>.Get().GetExcelSheet<Addon>();
|
||||
this.logkind = null;
|
||||
this.testString = string.Empty;
|
||||
this.interactable = this.useEntity = true;
|
||||
|
|
@ -193,13 +190,16 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
|
|||
ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA").X);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var addon = Service<DataManager>.GetNullable()?.GetExcelSheet<Addon>() ??
|
||||
throw new InvalidOperationException("Addon sheet not loaded.");
|
||||
|
||||
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||||
clipper.Begin(this.addons.Count);
|
||||
clipper.Begin(addon.Count);
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
var row = this.addons.GetRowAt(i);
|
||||
var row = addon.GetRowAt(i);
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.PushID(i);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
|
|
@ -7,12 +6,9 @@ using Dalamud.Data;
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Storage.Assets;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
|
@ -22,8 +18,6 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
/// </summary>
|
||||
internal class UiColorWidget : IDataWindowWidget
|
||||
{
|
||||
private ExcelSheet<UIColor> colors;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = ["uicolor"];
|
||||
|
||||
|
|
@ -37,12 +31,14 @@ internal class UiColorWidget : IDataWindowWidget
|
|||
public void Load()
|
||||
{
|
||||
this.Ready = true;
|
||||
this.colors = Service<DataManager>.Get().GetExcelSheet<UIColor>();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void Draw()
|
||||
{
|
||||
var colors = Service<DataManager>.GetNullable()?.GetExcelSheet<UIColor>()
|
||||
?? throw new InvalidOperationException("UIColor sheet not loaded.");
|
||||
|
||||
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(
|
||||
"· Color notation is #" +
|
||||
"<edgecolor(0xFFEEEE)><color(0xFF0000)>RR<color(stackcolor)><edgecolor(stackcolor)>" +
|
||||
|
|
@ -71,16 +67,16 @@ internal class UiColorWidget : IDataWindowWidget
|
|||
ImGui.TableHeadersRow();
|
||||
|
||||
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||||
clipper.Begin(this.colors.Count, ImGui.GetFrameHeightWithSpacing());
|
||||
clipper.Begin(colors.Count, ImGui.GetFrameHeightWithSpacing());
|
||||
while (clipper.Step())
|
||||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
var row = this.colors.GetRowAt(i);
|
||||
var row = colors.GetRowAt(i);
|
||||
UIColor? adjacentRow = null;
|
||||
if (i + 1 < this.colors.Count)
|
||||
if (i + 1 < colors.Count)
|
||||
{
|
||||
var adjRow = this.colors.GetRowAt(i + 1);
|
||||
var adjRow = colors.GetRowAt(i + 1);
|
||||
if (adjRow.RowId == row.RowId + 1)
|
||||
{
|
||||
adjacentRow = adjRow;
|
||||
|
|
|
|||
102
Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs
Normal file
102
Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Storage;
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// Widget for displaying configuration info.
|
||||
/// </summary>
|
||||
internal class VfsWidget : IDataWindowWidget
|
||||
{
|
||||
private int numBytes = 1024;
|
||||
private int reps = 1;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = { "vfs" };
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "VFS Performance";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load()
|
||||
{
|
||||
this.Ready = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
var service = Service<ReliableFileStorage>.Get();
|
||||
var dalamud = Service<Dalamud>.Get();
|
||||
|
||||
ImGui.InputInt("Num bytes", ref this.numBytes);
|
||||
ImGui.InputInt("Reps", ref this.reps);
|
||||
|
||||
var path = Path.Combine(dalamud.StartInfo.WorkingDirectory!, "test.bin");
|
||||
|
||||
if (ImGui.Button("Write"))
|
||||
{
|
||||
Log.Information("=== WRITING ===");
|
||||
var data = new byte[this.numBytes];
|
||||
var stopwatch = new Stopwatch();
|
||||
var acc = 0L;
|
||||
|
||||
for (var i = 0; i < this.reps; i++)
|
||||
{
|
||||
stopwatch.Restart();
|
||||
service.WriteAllBytes(path, data);
|
||||
stopwatch.Stop();
|
||||
acc += stopwatch.ElapsedMilliseconds;
|
||||
Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
Log.Information("Took {Ms}ms in total", acc);
|
||||
}
|
||||
|
||||
if (ImGui.Button("Read"))
|
||||
{
|
||||
Log.Information("=== READING ===");
|
||||
var stopwatch = new Stopwatch();
|
||||
var acc = 0L;
|
||||
|
||||
for (var i = 0; i < this.reps; i++)
|
||||
{
|
||||
stopwatch.Restart();
|
||||
service.ReadAllBytes(path);
|
||||
stopwatch.Stop();
|
||||
acc += stopwatch.ElapsedMilliseconds;
|
||||
Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
Log.Information("Took {Ms}ms in total", acc);
|
||||
}
|
||||
|
||||
if (ImGui.Button("Test Config"))
|
||||
{
|
||||
var config = Service<DalamudConfiguration>.Get();
|
||||
|
||||
Log.Information("=== READING ===");
|
||||
var stopwatch = new Stopwatch();
|
||||
var acc = 0L;
|
||||
|
||||
for (var i = 0; i < this.reps; i++)
|
||||
{
|
||||
stopwatch.Restart();
|
||||
config.ForceSave();
|
||||
stopwatch.Stop();
|
||||
acc += stopwatch.ElapsedMilliseconds;
|
||||
Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
Log.Information("Took {Ms}ms in total", acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +120,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
private List<AvailablePluginUpdate> pluginListUpdatable = new();
|
||||
private bool hasDevPlugins = false;
|
||||
private bool hasHiddenPlugins = false;
|
||||
|
||||
|
||||
private string searchText = string.Empty;
|
||||
private bool isSearchTextPrefilled = false;
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown;
|
||||
|
||||
private string verifiedCheckmarkHoveredPlugin = string.Empty;
|
||||
|
||||
|
||||
private string? staleDalamudNewVersion = null;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -215,18 +215,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ProfileOrNot,
|
||||
SearchScore,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
||||
[Flags]
|
||||
private enum PluginHeaderFlags
|
||||
{
|
||||
None = 0,
|
||||
IsThirdParty = 1 << 0,
|
||||
HasTrouble = 1 << 1,
|
||||
UpdateAvailable = 1 << 2,
|
||||
IsNew = 1 << 3,
|
||||
IsInstallableOutdated = 1 << 4,
|
||||
IsOrphan = 1 << 5,
|
||||
IsTesting = 1 << 6,
|
||||
MainRepoCrossUpdate = 1 << 3,
|
||||
IsNew = 1 << 4,
|
||||
IsInstallableOutdated = 1 << 5,
|
||||
IsOrphan = 1 << 6,
|
||||
IsTesting = 1 << 7,
|
||||
}
|
||||
|
||||
private enum InstalledPluginListFilter
|
||||
|
|
@ -236,7 +237,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
Updateable,
|
||||
Dev,
|
||||
}
|
||||
|
||||
|
||||
private bool AnyOperationInProgress => this.installStatus == OperationStatus.InProgress ||
|
||||
this.updateStatus == OperationStatus.InProgress ||
|
||||
this.enableDisableStatus == OperationStatus.InProgress;
|
||||
|
|
@ -282,6 +283,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var pluginManager = Service<PluginManager>.Get();
|
||||
|
||||
_ = pluginManager.ReloadPluginMastersAsync();
|
||||
Service<PluginManager>.Get().ScanDevPlugins();
|
||||
|
||||
if (!this.isSearchTextPrefilled) this.searchText = string.Empty;
|
||||
this.sortKind = PluginSortKind.Alphabetical;
|
||||
|
|
@ -304,7 +306,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
if (!t.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
|
||||
var versionInfo = t.Result;
|
||||
if (versionInfo.AssemblyVersion != Util.GetScmVersion() &&
|
||||
versionInfo.Track != "release" &&
|
||||
|
|
@ -413,7 +415,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
if (!task.IsFaulted && !task.IsCanceled)
|
||||
return true;
|
||||
|
||||
|
||||
var newErrorMessage = state as string;
|
||||
|
||||
if (task.Exception != null)
|
||||
|
|
@ -438,7 +440,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (task.IsCanceled)
|
||||
Log.Error("A task was cancelled");
|
||||
|
||||
|
|
@ -446,14 +448,14 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static void EnsureHaveTestingOptIn(IPluginManifest manifest)
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
|
||||
if (configuration.PluginTestingOptIns.Any(x => x.InternalName == manifest.InternalName))
|
||||
return;
|
||||
|
||||
|
||||
configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(manifest.InternalName));
|
||||
configuration.QueueSave();
|
||||
}
|
||||
|
|
@ -490,7 +492,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawProgressOverlay()
|
||||
{
|
||||
var pluginManager = Service<PluginManager>.Get();
|
||||
|
|
@ -733,7 +735,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawFooter()
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
|
@ -754,8 +756,9 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
Service<DalamudInterface>.Get().OpenSettings();
|
||||
}
|
||||
|
||||
// If any dev plugins are installed, allow a shortcut for the /xldev menu item
|
||||
if (this.hasDevPlugins)
|
||||
// If any dev plugin locations exist, allow a shortcut for the /xldev menu item
|
||||
var hasDevPluginLocations = configuration.DevPluginLoadLocations.Count > 0;
|
||||
if (hasDevPluginLocations)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
|
||||
|
|
@ -802,7 +805,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
this.updateStatus = OperationStatus.InProgress;
|
||||
this.loadingIndicatorKind = LoadingIndicatorKind.UpdatingAll;
|
||||
|
||||
|
||||
var toUpdate = this.pluginListUpdatable
|
||||
.Where(x => x.InstalledPlugin.IsWantedByAnyProfile)
|
||||
.ToList();
|
||||
|
|
@ -994,7 +997,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.Text(Locs.DeletePluginConfigWarningModal_ExplainTesting());
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
|
||||
ImGui.Text(Locs.DeletePluginConfigWarningModal_Body(this.deletePluginConfigWarningModalPluginName));
|
||||
ImGui.Spacing();
|
||||
|
||||
|
|
@ -1264,7 +1267,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
plugin.Manifest.RepoUrl == availableManifest.RepoUrl &&
|
||||
!plugin.IsDev);
|
||||
|
||||
// We "consumed" this plugin from the pile and remove it.
|
||||
// We "consumed" this plugin from the pile and remove it.
|
||||
if (plugin != null)
|
||||
{
|
||||
installedPlugins.Remove(plugin);
|
||||
|
|
@ -1296,7 +1299,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
return isHidden;
|
||||
return !isHidden;
|
||||
}
|
||||
|
||||
|
||||
// Filter out plugins that are not hidden
|
||||
proxies = proxies.Where(IsProxyHidden).ToList();
|
||||
|
||||
|
|
@ -1328,14 +1331,14 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
ImGui.PopID();
|
||||
}
|
||||
|
||||
|
||||
// Reset the category to "All" if we're on the "Hidden" category and there are no hidden plugins (we removed the last one)
|
||||
if (i == 0 && this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden)
|
||||
{
|
||||
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawInstalledPluginList(InstalledPluginListFilter filter)
|
||||
{
|
||||
var pluginList = this.pluginListInstalled;
|
||||
|
|
@ -1363,7 +1366,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
if (filter == InstalledPluginListFilter.Testing && !manager.HasTestingOptIn(plugin.Manifest))
|
||||
continue;
|
||||
|
||||
|
||||
// Find applicable update and manifest, if we have them
|
||||
AvailablePluginUpdate? update = null;
|
||||
RemotePluginManifest? remoteManifest = null;
|
||||
|
|
@ -1383,11 +1386,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
this.DrawInstalledPlugin(plugin, i++, remoteManifest, update);
|
||||
drewAny = true;
|
||||
}
|
||||
|
||||
|
||||
if (!drewAny)
|
||||
{
|
||||
var text = filter switch
|
||||
|
|
@ -1398,7 +1401,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
InstalledPluginListFilter.Dev => Locs.TabBody_NoPluginsDev,
|
||||
_ => throw new ArgumentException(null, nameof(filter)),
|
||||
};
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(60);
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
||||
|
|
@ -1490,7 +1493,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
foreach (var categoryKind in groupInfo.Categories)
|
||||
{
|
||||
var categoryInfo = this.categoryManager.CategoryList.First(x => x.CategoryKind == categoryKind);
|
||||
|
||||
|
||||
switch (categoryInfo.Condition)
|
||||
{
|
||||
case PluginCategoryManager.CategoryInfo.AppearCondition.None:
|
||||
|
|
@ -1549,7 +1552,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.PopFont();
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
||||
|
||||
void DrawLinesCentered(string text)
|
||||
{
|
||||
var lines = text.Split('\n');
|
||||
|
|
@ -1558,7 +1561,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGuiHelpers.CenteredText(line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var pm = Service<PluginManager>.Get();
|
||||
if (pm.SafeMode)
|
||||
{
|
||||
|
|
@ -1623,7 +1626,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
case PluginCategoryManager.CategoryKind.IsTesting:
|
||||
this.DrawInstalledPluginList(InstalledPluginListFilter.Testing);
|
||||
break;
|
||||
|
||||
|
||||
case PluginCategoryManager.CategoryKind.UpdateablePlugins:
|
||||
this.DrawInstalledPluginList(InstalledPluginListFilter.Updateable);
|
||||
break;
|
||||
|
|
@ -1631,7 +1634,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
case PluginCategoryManager.CategoryKind.PluginProfiles:
|
||||
this.profileManagerWidget.Draw();
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
ImGui.TextUnformatted("You found a secret category. Please feel a sense of pride and accomplishment.");
|
||||
break;
|
||||
|
|
@ -1652,7 +1655,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
case PluginCategoryManager.CategoryKind.PluginChangelogs:
|
||||
this.DrawChangelogList(false, true);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
ImGui.TextUnformatted("You found a quiet category. Please don't wake it up.");
|
||||
break;
|
||||
|
|
@ -1979,9 +1982,9 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
var sectionSize = ImGuiHelpers.GlobalScale * 66;
|
||||
var tapeCursor = ImGui.GetCursorPos();
|
||||
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
|
||||
var startCursor = ImGui.GetCursorPos();
|
||||
|
||||
if (flags.HasFlag(PluginHeaderFlags.IsTesting))
|
||||
|
|
@ -1992,9 +1995,9 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var scroll = new Vector2(ImGui.GetScrollX(), ImGui.GetScrollY());
|
||||
|
||||
|
||||
var adjustedPosition = windowPos + position - scroll;
|
||||
|
||||
|
||||
var yellow = ImGui.ColorConvertFloat4ToU32(new Vector4(1.0f, 0.9f, 0.0f, 0.10f));
|
||||
var numStripes = (int)(size.X / stripeWidth) + (int)(size.Y / skewAmount) + 1; // +1 to cover partial stripe
|
||||
|
||||
|
|
@ -2004,19 +2007,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var x1 = x0 + stripeWidth;
|
||||
var y0 = adjustedPosition.Y;
|
||||
var y1 = y0 + size.Y;
|
||||
|
||||
|
||||
var p0 = new Vector2(x0, y0);
|
||||
var p1 = new Vector2(x1, y0);
|
||||
var p2 = new Vector2(x1 - skewAmount, y1);
|
||||
var p3 = new Vector2(x0 - skewAmount, y1);
|
||||
|
||||
|
||||
if (i % 2 != 0)
|
||||
continue;
|
||||
|
||||
|
||||
wdl.AddQuadFilled(p0, p1, p2, p3, yellow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
DrawCautionTape(tapeCursor + new Vector2(0, 1), new Vector2(ImGui.GetWindowWidth(), sectionSize + ImGui.GetStyle().ItemSpacing.Y), ImGuiHelpers.GlobalScale * 40, 20);
|
||||
}
|
||||
|
||||
|
|
@ -2025,7 +2028,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0.5f, 0.5f, 0.5f, 0.2f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f));
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
|
||||
|
||||
|
||||
ImGui.SetCursorPos(tapeCursor);
|
||||
|
||||
if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize + ImGui.GetStyle().ItemSpacing.Y)))
|
||||
|
|
@ -2194,7 +2197,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
bodyText += " ";
|
||||
|
||||
if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable))
|
||||
bodyText += Locs.PluginBody_Outdated_CanNowUpdate;
|
||||
bodyText += "\n" + Locs.PluginBody_Outdated_CanNowUpdate;
|
||||
else
|
||||
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
|
||||
|
||||
|
|
@ -2217,7 +2220,12 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
else if (plugin is { IsDecommissioned: true, IsThirdParty: true })
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
ImGui.TextWrapped(Locs.PluginBody_NoServiceThird);
|
||||
|
||||
ImGui.TextWrapped(
|
||||
flags.HasFlag(PluginHeaderFlags.MainRepoCrossUpdate)
|
||||
? Locs.PluginBody_NoServiceThirdCrossUpdate
|
||||
: Locs.PluginBody_NoServiceThird);
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
else if (plugin != null && !plugin.CheckPolicy())
|
||||
|
|
@ -2368,11 +2376,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
label += Locs.PluginTitleMod_TestingAvailable;
|
||||
}
|
||||
|
||||
|
||||
var isThirdParty = manifest.SourceRepo.IsThirdParty;
|
||||
|
||||
ImGui.PushID($"available{index}{manifest.InternalName}");
|
||||
|
||||
|
||||
var flags = PluginHeaderFlags.None;
|
||||
if (isThirdParty)
|
||||
flags |= PluginHeaderFlags.IsThirdParty;
|
||||
|
|
@ -2382,7 +2390,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
flags |= PluginHeaderFlags.IsInstallableOutdated;
|
||||
if (useTesting || manifest.IsTestingExclusive)
|
||||
flags |= PluginHeaderFlags.IsTesting;
|
||||
|
||||
|
||||
if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index))
|
||||
{
|
||||
if (!wasSeen)
|
||||
|
|
@ -2444,7 +2452,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
ImGuiHelpers.ScaledDummy(3);
|
||||
}
|
||||
|
||||
if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback)
|
||||
if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback && !isOutdated)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
this.DrawSendFeedbackButton(manifest, false, true);
|
||||
|
|
@ -2478,7 +2486,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
EnsureHaveTestingOptIn(manifest);
|
||||
this.StartInstall(manifest, true);
|
||||
}
|
||||
|
||||
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
|
|
@ -2602,7 +2610,10 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
availablePluginUpdate = null;
|
||||
|
||||
// Update available
|
||||
if (availablePluginUpdate != default)
|
||||
var isMainRepoCrossUpdate = availablePluginUpdate != null &&
|
||||
availablePluginUpdate.UpdateManifest.RepoUrl != plugin.Manifest.RepoUrl &&
|
||||
availablePluginUpdate.UpdateManifest.RepoUrl == PluginRepository.MainRepoUrl;
|
||||
if (availablePluginUpdate != null)
|
||||
{
|
||||
label += Locs.PluginTitleMod_HasUpdate;
|
||||
}
|
||||
|
|
@ -2612,7 +2623,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (this.updatedPlugins != null && !plugin.IsDev)
|
||||
{
|
||||
var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName);
|
||||
if (update != default)
|
||||
if (update != null)
|
||||
{
|
||||
if (update.Status == PluginUpdateStatus.StatusKind.Success)
|
||||
{
|
||||
|
|
@ -2640,8 +2651,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
trouble = true;
|
||||
}
|
||||
|
||||
// Orphaned
|
||||
if (plugin.IsOrphaned)
|
||||
// Orphaned, if we don't have a cross-repo update
|
||||
if (plugin.IsOrphaned && !isMainRepoCrossUpdate)
|
||||
{
|
||||
label += Locs.PluginTitleMod_OrphanedError;
|
||||
trouble = true;
|
||||
|
|
@ -2670,15 +2681,15 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
string? availableChangelog = null;
|
||||
var didDrawAvailableChangelogInsideCollapsible = false;
|
||||
|
||||
if (availablePluginUpdate != default)
|
||||
if (availablePluginUpdate != null)
|
||||
{
|
||||
availablePluginUpdateVersion =
|
||||
availablePluginUpdate.UseTesting ?
|
||||
availablePluginUpdate.UpdateManifest.TestingAssemblyVersion :
|
||||
availablePluginUpdate.UpdateManifest.AssemblyVersion;
|
||||
|
||||
|
||||
availableChangelog =
|
||||
availablePluginUpdate.UseTesting ?
|
||||
availablePluginUpdate.UseTesting ?
|
||||
availablePluginUpdate.UpdateManifest.TestingChangelog :
|
||||
availablePluginUpdate.UpdateManifest.Changelog;
|
||||
}
|
||||
|
|
@ -2688,8 +2699,10 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
flags |= PluginHeaderFlags.IsThirdParty;
|
||||
if (trouble)
|
||||
flags |= PluginHeaderFlags.HasTrouble;
|
||||
if (availablePluginUpdate != default)
|
||||
if (availablePluginUpdate != null)
|
||||
flags |= PluginHeaderFlags.UpdateAvailable;
|
||||
if (isMainRepoCrossUpdate)
|
||||
flags |= PluginHeaderFlags.MainRepoCrossUpdate;
|
||||
if (plugin.IsOrphaned)
|
||||
flags |= PluginHeaderFlags.IsOrphan;
|
||||
if (plugin.IsTesting)
|
||||
|
|
@ -2724,8 +2737,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var canFeedback = !isThirdParty &&
|
||||
!plugin.IsDev &&
|
||||
!plugin.IsOrphaned &&
|
||||
(plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel
|
||||
|| plugin.Manifest.TestingDalamudApiLevel == PluginManager.DalamudApiLevel) &&
|
||||
(plugin.Manifest.DalamudApiLevel == PluginManager.DalamudApiLevel ||
|
||||
(plugin.Manifest.TestingDalamudApiLevel == PluginManager.DalamudApiLevel && hasTestingAvailable)) &&
|
||||
acceptsFeedback &&
|
||||
availablePluginUpdate == default;
|
||||
|
||||
|
|
@ -2762,13 +2775,14 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var commands = commandManager.Commands
|
||||
.Where(cInfo =>
|
||||
cInfo.Value is { ShowInHelp: true } &&
|
||||
commandManager.GetHandlerAssemblyName(cInfo.Key, cInfo.Value) == plugin.Manifest.InternalName)
|
||||
.ToArray();
|
||||
commandManager.GetHandlerAssemblyName(cInfo.Key, cInfo.Value) == plugin.Manifest.InternalName);
|
||||
|
||||
if (commands.Any())
|
||||
{
|
||||
ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f));
|
||||
foreach (var command in commands)
|
||||
foreach (var command in commands
|
||||
.OrderBy(cInfo => cInfo.Value.DisplayOrder)
|
||||
.ThenBy(cInfo => cInfo.Key))
|
||||
{
|
||||
ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}");
|
||||
}
|
||||
|
|
@ -2835,7 +2849,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
this.DrawInstalledPluginChangelog(applicableChangelog);
|
||||
}
|
||||
|
||||
|
||||
if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.UpdateablePlugins &&
|
||||
!availableChangelog.IsNullOrWhitespace() &&
|
||||
!didDrawAvailableChangelogInsideCollapsible)
|
||||
|
|
@ -3689,7 +3703,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
|
||||
this.ResortPlugins();
|
||||
}
|
||||
|
||||
|
||||
this.hasHiddenPlugins = this.pluginListAvailable.Any(x => configuration.HiddenPluginInternalName.Contains(x.InternalName));
|
||||
|
||||
this.UpdateCategoriesOnPluginsChange();
|
||||
|
|
@ -3943,16 +3957,16 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed.");
|
||||
|
||||
public static string TabBody_SafeMode => Loc.Localize("InstallerSafeMode", "Dalamud is running in Plugin Safe Mode, restart to activate plugins.");
|
||||
|
||||
|
||||
public static string TabBody_NoPluginsTesting => Loc.Localize("InstallerNoPluginsTesting", "You aren't testing any plugins at the moment!\nYou can opt in to testing versions in the plugin context menu.");
|
||||
|
||||
|
||||
public static string TabBody_NoPluginsInstalled =>
|
||||
string.Format(Loc.Localize("InstallerNoPluginsInstalled", "You don't have any plugins installed yet!\nYou can install them from the \"{0}\" tab."), PluginCategoryManager.Locs.Category_All);
|
||||
|
||||
|
||||
public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment.");
|
||||
|
||||
|
||||
public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings.");
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region Search text
|
||||
|
|
@ -4014,11 +4028,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
public static string PluginContext_TestingOptIn => Loc.Localize("InstallerTestingOptIn", "Receive plugin testing versions");
|
||||
|
||||
public static string PluginContext_InstallTestingVersion => Loc.Localize("InstallerInstallTestingVersion", "Install testing version");
|
||||
|
||||
|
||||
public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen");
|
||||
|
||||
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
|
||||
|
||||
|
||||
public static string PluginContext_UnhidePlugin => Loc.Localize("InstallerUnhidePlugin", "Unhide from installer");
|
||||
|
||||
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
|
||||
|
|
@ -4055,6 +4069,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
public static string PluginBody_NoServiceThird => Loc.Localize("InstallerNoServiceThirdPluginBody", "This plugin is no longer being serviced by its source repo. You may have to look for an updated version in another repo.");
|
||||
|
||||
public static string PluginBody_NoServiceThirdCrossUpdate => Loc.Localize("InstallerNoServiceThirdCrossUpdatePluginBody", "This plugin is no longer being serviced by its source repo. An update is available and will update it to a version from the official repository.");
|
||||
|
||||
public static string PluginBody_LoadFailed => Loc.Localize("InstallerLoadFailedPluginBody ", "This plugin failed to load. Please contact the author for more information.");
|
||||
|
||||
public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available.");
|
||||
|
|
|
|||
|
|
@ -625,13 +625,13 @@ internal class ProfileManagerWidget
|
|||
Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
|
||||
|
||||
public static string TutorialCommandsEnable =>
|
||||
Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(ProfileCommandHandler.CommandEnable);
|
||||
Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(PluginManagementCommandHandler.CommandEnableProfile);
|
||||
|
||||
public static string TutorialCommandsDisable =>
|
||||
Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(ProfileCommandHandler.CommandDisable);
|
||||
Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(PluginManagementCommandHandler.CommandDisableProfile);
|
||||
|
||||
public static string TutorialCommandsToggle =>
|
||||
Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(ProfileCommandHandler.CommandToggle);
|
||||
Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(PluginManagementCommandHandler.CommandToggleProfile);
|
||||
|
||||
public static string TutorialCommandsEnd =>
|
||||
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");
|
||||
|
|
|
|||
|
|
@ -23,10 +23,11 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
{
|
||||
private AutoUpdateBehavior behavior;
|
||||
private bool checkPeriodically;
|
||||
private bool chatNotification;
|
||||
private string pickerSearch = string.Empty;
|
||||
private List<AutoUpdatePreference> autoUpdatePreferences = [];
|
||||
|
||||
public override SettingsEntry[] Entries { get; } = Array.Empty<SettingsEntry>();
|
||||
|
||||
public override SettingsEntry[] Entries { get; } = [];
|
||||
|
||||
public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates");
|
||||
|
||||
|
|
@ -36,15 +37,15 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
"Dalamud can update your plugins automatically, making sure that you always " +
|
||||
"have the newest features and bug fixes. You can choose when and how auto-updates are run here."));
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
|
||||
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer1",
|
||||
"You can always update your plugins manually by clicking the update button in the plugin list. " +
|
||||
"You can also opt into updates for specific plugins by right-clicking them and selecting \"Always auto-update\"."));
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdateDisclaimer2",
|
||||
"Dalamud will only notify you about updates while you are idle."));
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(8);
|
||||
|
||||
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateBehavior",
|
||||
"When the game starts..."));
|
||||
var behaviorInt = (int)this.behavior;
|
||||
|
|
@ -62,20 +63,21 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
"These updates are not reviewed by the Dalamud team and may contain malicious code.");
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudOrange, warning);
|
||||
}
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(8);
|
||||
|
||||
|
||||
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification);
|
||||
ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically);
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint",
|
||||
"Plugins won't update automatically after startup, you will only receive a notification while you are not actively playing."));
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
ImGui.Separator();
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOptedIn",
|
||||
"Per-plugin overrides"));
|
||||
|
||||
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudWhite, Loc.Localize("DalamudSettingsAutoUpdateOverrideHint",
|
||||
"Here, you can choose to receive or not to receive updates for specific plugins. " +
|
||||
"This will override the settings above for the selected plugins."));
|
||||
|
|
@ -83,25 +85,25 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
if (this.autoUpdatePreferences.Count == 0)
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(20);
|
||||
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
||||
{
|
||||
ImGuiHelpers.CenteredText(Loc.Localize("DalamudSettingsAutoUpdateOptedInHint2",
|
||||
"You don't have auto-update rules for any plugins."));
|
||||
}
|
||||
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
|
||||
var pic = Service<PluginImageCache>.Get();
|
||||
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
|
||||
Guid? wantRemovePluginGuid = null;
|
||||
|
||||
|
||||
foreach (var preference in this.autoUpdatePreferences)
|
||||
{
|
||||
var pmPlugin = Service<PluginManager>.Get().InstalledPlugins
|
||||
|
|
@ -120,11 +122,12 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
if (pmPlugin.IsDev)
|
||||
{
|
||||
ImGui.SetCursorPos(cursorBeforeIcon);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f);
|
||||
ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight));
|
||||
ImGui.PopStyleVar();
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.7f))
|
||||
{
|
||||
ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}";
|
||||
|
|
@ -147,7 +150,7 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
|
||||
ImGui.SetCursorPos(before);
|
||||
}
|
||||
|
||||
|
|
@ -166,19 +169,18 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
}
|
||||
|
||||
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 250);
|
||||
if (ImGui.BeginCombo(
|
||||
$"###autoUpdateBehavior{preference.WorkingPluginId}",
|
||||
OptKindToString(preference.Kind)))
|
||||
using (var combo = ImRaii.Combo($"###autoUpdateBehavior{preference.WorkingPluginId}", OptKindToString(preference.Kind)))
|
||||
{
|
||||
foreach (var kind in Enum.GetValues<AutoUpdatePreference.OptKind>())
|
||||
if (combo.Success)
|
||||
{
|
||||
if (ImGui.Selectable(OptKindToString(kind)))
|
||||
foreach (var kind in Enum.GetValues<AutoUpdatePreference.OptKind>())
|
||||
{
|
||||
preference.Kind = kind;
|
||||
if (ImGui.Selectable(OptKindToString(kind)))
|
||||
{
|
||||
preference.Kind = kind;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndCombo();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -193,7 +195,7 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Loc.Localize("DalamudSettingsAutoUpdateOptInRemove", "Remove this override"));
|
||||
}
|
||||
|
||||
|
||||
if (wantRemovePluginGuid != null)
|
||||
{
|
||||
this.autoUpdatePreferences.RemoveAll(x => x.WorkingPluginId == wantRemovePluginGuid);
|
||||
|
|
@ -205,19 +207,19 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
var id = plugin.EffectiveWorkingPluginId;
|
||||
if (id == Guid.Empty)
|
||||
throw new InvalidOperationException("Plugin ID is empty.");
|
||||
|
||||
|
||||
this.autoUpdatePreferences.Add(new AutoUpdatePreference(id));
|
||||
}
|
||||
|
||||
|
||||
bool IsPluginDisabled(LocalPlugin plugin)
|
||||
=> this.autoUpdatePreferences.Any(x => x.WorkingPluginId == plugin.EffectiveWorkingPluginId);
|
||||
|
||||
|
||||
bool IsPluginFiltered(LocalPlugin plugin)
|
||||
=> !plugin.IsDev;
|
||||
|
||||
|
||||
var pickerId = DalamudComponents.DrawPluginPicker(
|
||||
"###autoUpdatePicker", ref this.pickerSearch, OnPluginPicked, IsPluginDisabled, IsPluginFiltered);
|
||||
|
||||
|
||||
const FontAwesomeIcon addButtonIcon = FontAwesomeIcon.Plus;
|
||||
var addButtonText = Loc.Localize("DalamudSettingsAutoUpdateOptInAdd", "Add new override");
|
||||
ImGuiHelpers.CenterCursorFor(ImGuiComponents.GetIconButtonWithTextWidth(addButtonIcon, addButtonText));
|
||||
|
|
@ -235,20 +237,22 @@ public class SettingsTabAutoUpdates : SettingsTab
|
|||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None;
|
||||
this.chatNotification = configuration.SendUpdateNotificationToChat;
|
||||
this.checkPeriodically = configuration.CheckPeriodicallyForUpdates;
|
||||
this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences;
|
||||
|
||||
|
||||
base.Load();
|
||||
}
|
||||
|
||||
public override void Save()
|
||||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
|
||||
configuration.AutoUpdateBehavior = this.behavior;
|
||||
configuration.SendUpdateNotificationToChat = this.chatNotification;
|
||||
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
|
||||
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;
|
||||
|
||||
|
||||
base.Save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,18 +39,6 @@ public class SettingsTabExperimental : SettingsTab
|
|||
|
||||
new GapSettingsEntry(5),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnablePluginUIAdditionalOptions",
|
||||
"Add a button to the title bar of plugin windows to open additional options"),
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
|
||||
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
|
||||
c => c.EnablePluginUiAdditionalOptions,
|
||||
(v, c) => c.EnablePluginUiAdditionalOptions = v),
|
||||
|
||||
new GapSettingsEntry(5),
|
||||
|
||||
new ButtonSettingsEntry(
|
||||
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
|
||||
Loc.Localize(
|
||||
|
|
@ -66,6 +54,26 @@ public class SettingsTabExperimental : SettingsTab
|
|||
|
||||
new DevPluginsSettingsEntry(),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnableImGuiAsserts",
|
||||
"Enable ImGui asserts"),
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnableImGuiAssertsHint",
|
||||
"If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins."),
|
||||
c => Service<InterfaceManager>.Get().ShowAsserts,
|
||||
(v, _) => Service<InterfaceManager>.Get().ShowAsserts = v),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnableImGuiAssertsAtStartup",
|
||||
"Always enable ImGui asserts at startup"),
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnableImGuiAssertsAtStartupHint",
|
||||
"This will enable ImGui asserts every time the game starts."),
|
||||
c => c.ImGuiAssertsEnabledAtStartup ?? false,
|
||||
(v, c) => c.ImGuiAssertsEnabledAtStartup = v),
|
||||
|
||||
new GapSettingsEntry(5, true),
|
||||
|
||||
new ThirdRepoSettingsEntry(),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
|
|||
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
|
||||
public class SettingsTabLook : SettingsTab
|
||||
{
|
||||
private static readonly (string, float)[] GlobalUiScalePresets =
|
||||
private static readonly (string, float)[] GlobalUiScalePresets =
|
||||
{
|
||||
("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f),
|
||||
("100%##DalamudSettingsGlobalUiScaleReset12", 1f),
|
||||
|
|
@ -107,7 +107,17 @@ public class SettingsTabLook : SettingsTab
|
|||
Loc.Localize("DalamudSettingToggleDockingHint", "This will allow you to fuse and tab plugin windows."),
|
||||
c => c.IsDocking,
|
||||
(v, c) => c.IsDocking = v),
|
||||
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnablePluginUIAdditionalOptions",
|
||||
"Add a button to the title bar of plugin windows to open additional options"),
|
||||
Loc.Localize(
|
||||
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
|
||||
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
|
||||
c => c.EnablePluginUiAdditionalOptions,
|
||||
(v, c) => c.EnablePluginUiAdditionalOptions = v),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize("DalamudSettingEnablePluginUISoundEffects", "Enable sound effects for plugin windows"),
|
||||
Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."),
|
||||
|
|
@ -125,19 +135,19 @@ public class SettingsTabLook : SettingsTab
|
|||
Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.\nDisabling this will also hide the Dalamud version text on the title screen."),
|
||||
c => c.ShowTsm,
|
||||
(v, c) => c.ShowTsm = v),
|
||||
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize("DalamudSettingInstallerOpenDefault", "Open the Plugin Installer to the \"Installed Plugins\" tab by default"),
|
||||
Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."),
|
||||
c => c.PluginInstallerOpen == PluginInstallerOpenKind.InstalledPlugins,
|
||||
(v, c) => c.PluginInstallerOpen = v ? PluginInstallerOpenKind.InstalledPlugins : PluginInstallerOpenKind.AllPlugins),
|
||||
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"),
|
||||
Loc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."),
|
||||
c => c.ReduceMotions ?? false,
|
||||
(v, c) => c.ReduceMotions = v),
|
||||
|
||||
|
||||
new SettingsEntry<float>(
|
||||
Loc.Localize("DalamudSettingImeStateIndicatorOpacity", "IME State Indicator Opacity (CJK only)"),
|
||||
Loc.Localize("DalamudSettingImeStateIndicatorOpacityHint", "When any of CJK IMEs is in use, the state of IME will be shown with the opacity specified here."),
|
||||
|
|
|
|||
|
|
@ -196,8 +196,17 @@ public class DevPluginsSettingsEntry : SettingsEntry
|
|||
}
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
this.fileDialogManager.Draw();
|
||||
}
|
||||
|
||||
private static bool ValidDevPluginPath(string path)
|
||||
=> Path.IsPathRooted(path) && Path.GetExtension(path) == ".dll";
|
||||
|
||||
private void AddDevPlugin()
|
||||
{
|
||||
this.devPluginTempLocation = this.devPluginTempLocation.Trim('"');
|
||||
if (this.devPluginLocations.Any(
|
||||
r => string.Equals(r.Path, this.devPluginTempLocation, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
|
|
@ -210,25 +219,21 @@ public class DevPluginsSettingsEntry : SettingsEntry
|
|||
"DalamudDevPluginInvalid",
|
||||
"The entered value is not a valid path to a potential Dev Plugin.\nDid you mean to enter it as a custom plugin repository in the fields below instead?");
|
||||
Task.Delay(5000).ContinueWith(t => this.devPluginLocationAddError = string.Empty);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.devPluginLocations.Add(
|
||||
new DevPluginLocationSettings
|
||||
{
|
||||
Path = this.devPluginTempLocation.Replace("\"", string.Empty),
|
||||
Path = this.devPluginTempLocation,
|
||||
IsEnabled = true,
|
||||
});
|
||||
this.devPluginLocationsChanged = true;
|
||||
this.devPluginTempLocation = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
this.fileDialogManager.Draw();
|
||||
// Enable ImGui asserts if a dev plugin is added, if no choice was made prior
|
||||
Service<DalamudConfiguration>.Get().ImGuiAssertsEnabledAtStartup ??= true;
|
||||
}
|
||||
|
||||
private static bool ValidDevPluginPath(string path)
|
||||
=> Path.IsPathRooted(path) && Path.GetExtension(path) == ".dll";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,11 +50,11 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
private readonly Lazy<IFontHandle> myFontHandle;
|
||||
private readonly Lazy<IDalamudTextureWrap> shadeTexture;
|
||||
private readonly AddonLifecycleEventListener versionStringListener;
|
||||
|
||||
|
||||
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
|
||||
private readonly Dictionary<Guid, InOutQuint> moveEasings = new();
|
||||
private readonly Dictionary<Guid, InOutCubic> logoEasings = new();
|
||||
|
||||
|
||||
private readonly IConsoleVariable<bool> showTsm;
|
||||
|
||||
private InOutCubic? fadeOutEasing;
|
||||
|
|
@ -62,7 +62,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
private State state = State.Hide;
|
||||
|
||||
private int lastLoadedPluginCount = -1;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TitleScreenMenuWindow"/> class.
|
||||
/// </summary>
|
||||
|
|
@ -91,7 +91,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus)
|
||||
{
|
||||
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);
|
||||
|
||||
|
||||
this.clientState = clientState;
|
||||
this.configuration = configuration;
|
||||
this.gameGui = gameGui;
|
||||
|
|
@ -124,7 +124,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
|
||||
framework.Update += this.FrameworkOnUpdate;
|
||||
this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate);
|
||||
|
||||
|
||||
this.versionStringListener = new AddonLifecycleEventListener(AddonEvent.PreDraw, "_TitleRevision", this.OnVersionStringDraw);
|
||||
addonLifecycle.RegisterListener(this.versionStringListener);
|
||||
this.scopedFinalizer.Add(() => addonLifecycle.UnregisterListener(this.versionStringListener));
|
||||
|
|
@ -136,7 +136,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
Show,
|
||||
FadeOut,
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether drawing is allowed.
|
||||
/// </summary>
|
||||
|
|
@ -165,7 +165,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
{
|
||||
if (!this.AllowDrawing || !this.showTsm.Value)
|
||||
return;
|
||||
|
||||
|
||||
var scale = ImGui.GetIO().FontGlobalScale;
|
||||
var entries = this.titleScreenMenu.PluginEntries;
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
|
||||
|
||||
Service<InterfaceManager>.Get().OverrideGameCursor = !hovered;
|
||||
|
||||
|
||||
switch (this.state)
|
||||
{
|
||||
case State.Show:
|
||||
|
|
@ -251,7 +251,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
|
||||
this.fadeOutEasing.Update();
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)Math.Max(this.fadeOutEasing.Value, 0)))
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var entry in entries)
|
||||
|
|
@ -392,21 +392,19 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
|
||||
if (overrideAlpha)
|
||||
{
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)Math.Min(logoEasing.Value, 1) : 0f);
|
||||
}
|
||||
|
||||
// Drop shadow
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, 0xFF000000))
|
||||
{
|
||||
for (int i = 0, to = (int)Math.Ceiling(1 * scale); i < to; i++)
|
||||
{
|
||||
ImGui.SetCursorPos(new Vector2(cursor.X, cursor.Y + i));
|
||||
ImGui.Text(entry.Name);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(cursor);
|
||||
ImGui.Text(entry.Name);
|
||||
ImGuiHelpers.SeStringWrapped(
|
||||
ReadOnlySeString.FromText(entry.Name),
|
||||
new()
|
||||
{
|
||||
FontSize = TargetFontSizePx * ImGui.GetIO().FontGlobalScale,
|
||||
Edge = true,
|
||||
Shadow = true,
|
||||
});
|
||||
|
||||
if (overrideAlpha)
|
||||
{
|
||||
|
|
@ -439,7 +437,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
|
||||
var addon = (AtkUnitBase*)drawArgs.Addon;
|
||||
var textNode = addon->GetTextNodeById(3);
|
||||
|
||||
|
||||
// look and feel init. should be harmless to set.
|
||||
textNode->TextFlags |= (byte)TextFlags.MultiLine;
|
||||
textNode->AlignmentType = AlignmentType.TopLeft;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ namespace Dalamud.Interface.Textures;
|
|||
/// <summary>A texture with a backing instance of <see cref="IDalamudTextureWrap"/> that is shared across multiple
|
||||
/// requesters.</summary>
|
||||
/// <remarks>
|
||||
/// <para>Calling <see cref="IDisposable.Dispose"/> on this interface is a no-op.</para>
|
||||
/// <para><see cref="GetWrapOrEmpty"/> and <see cref="TryGetWrap"/> may stop returning the intended texture at any point.
|
||||
/// Use <see cref="RentAsync"/> to lock the texture for use in any thread for any duration.</para>
|
||||
/// </remarks>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ using Dalamud.Game.ClientState.Conditions;
|
|||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
|
|
@ -713,8 +712,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
|
|||
ImGui.End();
|
||||
}
|
||||
|
||||
var snapshot = this.Draw is null ? null : ImGuiManagedAsserts.GetSnapshot();
|
||||
|
||||
try
|
||||
{
|
||||
this.Draw?.InvokeSafely();
|
||||
|
|
@ -728,10 +725,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder
|
|||
this.hasErrorWindow = true;
|
||||
}
|
||||
|
||||
// Only if Draw was successful
|
||||
if (this.Draw is not null && snapshot is not null)
|
||||
ImGuiManagedAsserts.ReportProblems(this.namespaceName, snapshot);
|
||||
|
||||
this.FrameCount++;
|
||||
|
||||
if (DoStats)
|
||||
|
|
|
|||
61
Dalamud/Interface/Windowing/Persistence/PresetModel.cs
Normal file
61
Dalamud/Interface/Windowing/Persistence/PresetModel.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Interface.Windowing.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a Window System preset.
|
||||
/// </summary>
|
||||
internal class PresetModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of this preset.
|
||||
/// </summary>
|
||||
[JsonProperty("id")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of this preset.
|
||||
/// </summary>
|
||||
[JsonProperty("n")]
|
||||
public string Name { get; set; } = "New Preset";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a dictionary containing the windows in the preset, mapping their ID to the preset.
|
||||
/// </summary>
|
||||
[JsonProperty("w")]
|
||||
public Dictionary<uint, PresetWindow> Windows { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Class representing a window in a preset.
|
||||
/// </summary>
|
||||
internal class PresetWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window is pinned.
|
||||
/// </summary>
|
||||
[JsonProperty("p")]
|
||||
public bool IsPinned { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the window is clickthrough.
|
||||
/// </summary>
|
||||
[JsonProperty("ct")]
|
||||
public bool IsClickThrough { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the window's opacity override.
|
||||
/// </summary>
|
||||
[JsonProperty("a")]
|
||||
public float? Alpha { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this preset is in the default state.
|
||||
/// </summary>
|
||||
public bool IsDefault =>
|
||||
!this.IsPinned &&
|
||||
!this.IsClickThrough &&
|
||||
!this.Alpha.HasValue;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using Dalamud.Configuration.Internal;
|
||||
|
||||
namespace Dalamud.Interface.Windowing.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Class handling persistence for window system windows.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class WindowSystemPersistence : IServiceType
|
||||
{
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WindowSystemPersistence"/> class.
|
||||
/// </summary>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public WindowSystemPersistence()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active window system preset.
|
||||
/// </summary>
|
||||
public PresetModel ActivePreset => this.config.DefaultUiPreset;
|
||||
|
||||
/// <summary>
|
||||
/// Get or add a window to the active preset.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the window.</param>
|
||||
/// <returns>The preset window instance, or null if the preset does not contain this window.</returns>
|
||||
public PresetModel.PresetWindow? GetWindow(uint id)
|
||||
{
|
||||
return this.ActivePreset.Windows.TryGetValue(id, out var window) ? window : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persist the state of a window to the active preset.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the window.</param>
|
||||
/// <param name="window">The preset window instance.</param>
|
||||
public void SaveWindow(uint id, PresetModel.PresetWindow window)
|
||||
{
|
||||
// If the window is in the default state, don't save it to avoid saving every possible window
|
||||
// if the user has not customized anything.
|
||||
if (window.IsDefault)
|
||||
{
|
||||
this.ActivePreset.Windows.Remove(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ActivePreset.Windows[id] = window;
|
||||
}
|
||||
|
||||
this.config.QueueSave();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Windowing.Persistence;
|
||||
using Dalamud.Logging.Internal;
|
||||
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using PInvoke;
|
||||
|
||||
namespace Dalamud.Interface.Windowing;
|
||||
|
|
@ -26,7 +31,7 @@ public abstract class Window
|
|||
private static readonly ModuleLog Log = new("WindowSystem");
|
||||
|
||||
private static bool wasEscPressedLastFrame = false;
|
||||
|
||||
|
||||
private bool internalLastIsOpen = false;
|
||||
private bool internalIsOpen = false;
|
||||
private bool internalIsPinned = false;
|
||||
|
|
@ -35,15 +40,19 @@ public abstract class Window
|
|||
private float? internalAlpha = null;
|
||||
private bool nextFrameBringToFront = false;
|
||||
|
||||
private bool hasInitializedFromPreset = false;
|
||||
private PresetModel.PresetWindow? presetWindow;
|
||||
private bool presetDirty = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Window"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name/ID of this window.
|
||||
/// If you have multiple windows with the same name, you will need to
|
||||
/// append an unique ID to it by specifying it after "###" behind the window title.
|
||||
/// append a unique ID to it by specifying it after "###" behind the window title.
|
||||
/// </param>
|
||||
/// <param name="flags">The <see cref="ImGuiWindowFlags"/> of this window.</param>
|
||||
/// <param name="forceMainWindow">Whether or not this window should be limited to the main game window.</param>
|
||||
/// <param name="forceMainWindow">Whether this window should be limited to the main game window.</param>
|
||||
protected Window(string name, ImGuiWindowFlags flags = ImGuiWindowFlags.None, bool forceMainWindow = false)
|
||||
{
|
||||
this.WindowName = name;
|
||||
|
|
@ -51,6 +60,33 @@ public abstract class Window
|
|||
this.ForceMainWindow = forceMainWindow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flags to control window behavior.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
internal enum WindowDrawFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// Nothing.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Enable window opening/closing sound effects.
|
||||
/// </summary>
|
||||
UseSoundEffects = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Hook into the game's focus management.
|
||||
/// </summary>
|
||||
UseFocusManagement = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Enable the built-in "additional options" menu on the title bar.
|
||||
/// </summary>
|
||||
UseAdditionalOptions = 1 << 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the namespace of the window.
|
||||
/// </summary>
|
||||
|
|
@ -87,7 +123,7 @@ public abstract class Window
|
|||
/// Gets or sets a value representing the sound effect id to be played when the window is closed.
|
||||
/// </summary>
|
||||
public uint OnCloseSfxId { get; set; } = 24u;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position of this window.
|
||||
/// </summary>
|
||||
|
|
@ -155,7 +191,7 @@ public abstract class Window
|
|||
|
||||
/// <summary>
|
||||
/// Gets or sets a list of available title bar buttons.
|
||||
///
|
||||
///
|
||||
/// If <see cref="AllowPinning"/> or <see cref="AllowClickthrough"/> are set to true, and this features is not
|
||||
/// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will
|
||||
/// not appear in this collection. If you wish to remove this button, set both of these values to false.
|
||||
|
|
@ -170,7 +206,7 @@ public abstract class Window
|
|||
get => this.internalIsOpen;
|
||||
set => this.internalIsOpen = value;
|
||||
}
|
||||
|
||||
|
||||
private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -267,17 +303,16 @@ public abstract class Window
|
|||
public virtual void Update()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Draw the window via ImGui.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Configuration instance used to check if certain window management features should be enabled.</param>
|
||||
internal void DrawInternal(DalamudConfiguration? configuration)
|
||||
/// <param name="internalDrawFlags">Flags controlling window behavior.</param>
|
||||
/// <param name="persistence">Handler for window persistence data.</param>
|
||||
internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence)
|
||||
{
|
||||
this.PreOpenCheck();
|
||||
|
||||
var doSoundEffects = configuration?.EnablePluginUISoundEffects ?? false;
|
||||
|
||||
if (!this.IsOpen)
|
||||
{
|
||||
if (this.internalIsOpen != this.internalLastIsOpen)
|
||||
|
|
@ -286,8 +321,9 @@ public abstract class Window
|
|||
this.OnClose();
|
||||
|
||||
this.IsFocused = false;
|
||||
|
||||
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
|
||||
|
||||
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
|
||||
UIGlobals.PlaySoundEffect(this.OnCloseSfxId);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -301,13 +337,16 @@ public abstract class Window
|
|||
|
||||
if (hasNamespace)
|
||||
ImGui.PushID(this.Namespace);
|
||||
|
||||
|
||||
this.PreHandlePreset(persistence);
|
||||
|
||||
if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen)
|
||||
{
|
||||
this.internalLastIsOpen = this.internalIsOpen;
|
||||
this.OnOpen();
|
||||
|
||||
if (doSoundEffects && !this.DisableWindowSounds) UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
|
||||
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseSoundEffects) && !this.DisableWindowSounds)
|
||||
UIGlobals.PlaySoundEffect(this.OnOpenSfxId);
|
||||
}
|
||||
|
||||
this.PreDraw();
|
||||
|
|
@ -340,6 +379,18 @@ public abstract class Window
|
|||
|
||||
if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags))
|
||||
{
|
||||
ImGuiNativeAdditions.igCustom_WindowSetInheritNoInputs(this.internalIsClickthrough);
|
||||
|
||||
// Not supported yet on non-main viewports
|
||||
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
|
||||
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
|
||||
{
|
||||
this.internalAlpha = null;
|
||||
this.internalIsPinned = false;
|
||||
this.internalIsClickthrough = false;
|
||||
this.presetDirty = true;
|
||||
}
|
||||
|
||||
// Draw the actual window contents
|
||||
try
|
||||
{
|
||||
|
|
@ -355,7 +406,7 @@ public abstract class Window
|
|||
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
|
||||
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
|
||||
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
|
||||
(configuration?.EnablePluginUiAdditionalOptions ?? true) &&
|
||||
internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) &&
|
||||
flagsApplicableForTitleBarIcons;
|
||||
if (showAdditions)
|
||||
{
|
||||
|
|
@ -364,10 +415,10 @@ public abstract class Window
|
|||
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
|
||||
{
|
||||
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
|
||||
|
||||
|
||||
if (!isAvailable)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
|
||||
if (this.internalIsClickthrough)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
|
|
@ -375,36 +426,51 @@ public abstract class Window
|
|||
{
|
||||
var showAsPinned = this.internalIsPinned || this.internalIsClickthrough;
|
||||
if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned))
|
||||
{
|
||||
this.internalIsPinned = showAsPinned;
|
||||
this.presetDirty = true;
|
||||
}
|
||||
|
||||
ImGuiComponents.HelpMarker(
|
||||
Loc.Localize("WindowSystemContextActionPinHint", "Pinned windows will not move or resize when you click and drag them, nor will they close when escape is pressed."));
|
||||
}
|
||||
|
||||
if (this.internalIsClickthrough)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
if (this.AllowClickthrough)
|
||||
ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), ref this.internalIsClickthrough);
|
||||
{
|
||||
if (ImGui.Checkbox(
|
||||
Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"),
|
||||
ref this.internalIsClickthrough))
|
||||
{
|
||||
this.presetDirty = true;
|
||||
}
|
||||
|
||||
ImGuiComponents.HelpMarker(
|
||||
Loc.Localize("WindowSystemContextActionClickthroughHint", "Clickthrough windows will not receive mouse input, move or resize. They are completely inert."));
|
||||
}
|
||||
|
||||
var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f;
|
||||
if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f,
|
||||
100f))
|
||||
{
|
||||
this.internalAlpha = alpha / 100f;
|
||||
this.presetDirty = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset")))
|
||||
{
|
||||
this.internalAlpha = null;
|
||||
this.presetDirty = true;
|
||||
}
|
||||
|
||||
if (isAvailable)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey,
|
||||
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
|
||||
"Open this menu again to disable clickthrough."));
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey,
|
||||
Loc.Localize("WindowSystemContextActionDisclaimer",
|
||||
"These options may not work for all plugins at the moment."));
|
||||
"Open this menu again by clicking the three dashes to disable clickthrough."));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -415,7 +481,7 @@ public abstract class Window
|
|||
|
||||
if (!isAvailable)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
|
|
@ -435,6 +501,7 @@ public abstract class Window
|
|||
Click = _ =>
|
||||
{
|
||||
this.internalIsClickthrough = false;
|
||||
this.presetDirty = false;
|
||||
ImGui.OpenPopup(additionsPopupName);
|
||||
},
|
||||
Priority = int.MinValue,
|
||||
|
|
@ -457,8 +524,7 @@ public abstract class Window
|
|||
|
||||
this.IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
||||
|
||||
var isAllowed = configuration?.IsFocusManagementEnabled ?? false;
|
||||
if (isAllowed)
|
||||
if (internalDrawFlags.HasFlag(WindowDrawFlags.UseFocusManagement) && !this.internalIsPinned)
|
||||
{
|
||||
var escapeDown = Service<KeyState>.Get()[VirtualKey.ESCAPE];
|
||||
if (escapeDown && this.IsFocused && !wasEscPressedLastFrame && this.RespectCloseHotkey)
|
||||
|
|
@ -476,6 +542,8 @@ public abstract class Window
|
|||
|
||||
this.PostDraw();
|
||||
|
||||
this.PostHandlePreset(persistence);
|
||||
|
||||
if (hasNamespace)
|
||||
ImGui.PopID();
|
||||
}
|
||||
|
|
@ -511,7 +579,7 @@ public abstract class Window
|
|||
{
|
||||
ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value);
|
||||
}
|
||||
|
||||
|
||||
// Manually set alpha takes precedence, if devs don't want that, they should turn it off
|
||||
if (this.internalAlpha.HasValue)
|
||||
{
|
||||
|
|
@ -519,21 +587,65 @@ public abstract class Window
|
|||
}
|
||||
}
|
||||
|
||||
private void PreHandlePreset(WindowSystemPersistence? persistence)
|
||||
{
|
||||
if (persistence == null || this.hasInitializedFromPreset)
|
||||
return;
|
||||
|
||||
var id = ImGui.GetID(this.WindowName);
|
||||
this.presetWindow = persistence.GetWindow(id);
|
||||
|
||||
this.hasInitializedFromPreset = true;
|
||||
|
||||
// Fresh preset - don't apply anything
|
||||
if (this.presetWindow == null)
|
||||
{
|
||||
this.presetWindow = new PresetModel.PresetWindow();
|
||||
this.presetDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.internalIsPinned = this.presetWindow.IsPinned;
|
||||
this.internalIsClickthrough = this.presetWindow.IsClickThrough;
|
||||
this.internalAlpha = this.presetWindow.Alpha;
|
||||
}
|
||||
|
||||
private void PostHandlePreset(WindowSystemPersistence? persistence)
|
||||
{
|
||||
if (persistence == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(this.presetWindow != null, "this.presetWindow != null");
|
||||
|
||||
if (this.presetDirty)
|
||||
{
|
||||
this.presetWindow.IsPinned = this.internalIsPinned;
|
||||
this.presetWindow.IsClickThrough = this.internalIsClickthrough;
|
||||
this.presetWindow.Alpha = this.internalAlpha;
|
||||
|
||||
var id = ImGui.GetID(this.WindowName);
|
||||
persistence.SaveWindow(id, this.presetWindow!);
|
||||
this.presetDirty = false;
|
||||
|
||||
Log.Verbose("Saved preset for {WindowName}", this.WindowName);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DrawTitleBarButtons(void* window, ImGuiWindowFlags flags, Vector4 titleBarRect, IEnumerable<TitleBarButton> buttons)
|
||||
{
|
||||
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
|
||||
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
var fontSize = ImGui.GetFontSize();
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
|
||||
var padR = 0f;
|
||||
var buttonSize = ImGui.GetFontSize();
|
||||
|
||||
var numNativeButtons = 0;
|
||||
if (this.CanShowCloseButton)
|
||||
numNativeButtons++;
|
||||
|
||||
|
||||
if (!flags.HasFlag(ImGuiWindowFlags.NoCollapse) && style.WindowMenuButtonPosition == ImGuiDir.Right)
|
||||
numNativeButtons++;
|
||||
|
||||
|
|
@ -543,15 +655,15 @@ public abstract class Window
|
|||
|
||||
// Pad to the left, to get out of the way of the native buttons
|
||||
padR += numNativeButtons * (buttonSize + style.ItemInnerSpacing.X);
|
||||
|
||||
Vector2 GetCenter(Vector4 rect) => new((rect.X + rect.Z) * 0.5f, (rect.Y + rect.W) * 0.5f);
|
||||
|
||||
Vector2 GetCenter(Vector4 rect) => new((rect.X + rect.Z) * 0.5f, (rect.Y + rect.W) * 0.5f);
|
||||
|
||||
var numButtons = 0;
|
||||
bool DrawButton(TitleBarButton button, Vector2 pos)
|
||||
{
|
||||
var id = ImGui.GetID($"###CustomTbButton{numButtons}");
|
||||
numButtons++;
|
||||
|
||||
|
||||
var min = pos;
|
||||
var max = pos + new Vector2(fontSize, fontSize);
|
||||
Vector4 bb = new(min.X, min.Y, max.X, max.Y);
|
||||
|
|
@ -563,12 +675,12 @@ public abstract class Window
|
|||
{
|
||||
hovered = false;
|
||||
held = false;
|
||||
|
||||
|
||||
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
|
||||
if (ImGui.IsMouseHoveringRect(min, max))
|
||||
{
|
||||
hovered = true;
|
||||
|
||||
|
||||
// We can't use ImGui native functions here, because they don't work with clickthrough
|
||||
if ((User32.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
|
||||
{
|
||||
|
|
@ -581,7 +693,7 @@ public abstract class Window
|
|||
{
|
||||
pressed = ImGuiNativeAdditions.igButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags.None);
|
||||
}
|
||||
|
||||
|
||||
if (isClipped)
|
||||
return pressed;
|
||||
|
||||
|
|
@ -590,10 +702,10 @@ public abstract class Window
|
|||
var textCol = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
if (hovered || held)
|
||||
drawList.AddCircleFilled(GetCenter(bb) + new Vector2(0.0f, -0.5f), (fontSize * 0.5f) + 1.0f, bgCol);
|
||||
|
||||
|
||||
var offset = button.IconOffset * ImGuiHelpers.GlobalScale;
|
||||
drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.X + offset.X, bb.Y + offset.Y), textCol, button.Icon.ToIconString());
|
||||
|
||||
drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.X + offset.X, bb.Y + offset.Y), textCol, button.Icon.ToIconString());
|
||||
|
||||
if (hovered)
|
||||
button.ShowTooltip?.Invoke();
|
||||
|
||||
|
|
@ -608,14 +720,14 @@ public abstract class Window
|
|||
{
|
||||
if (this.internalIsClickthrough && !button.AvailableClickthrough)
|
||||
return;
|
||||
|
||||
|
||||
Vector2 position = new(titleBarRect.Z - padR - buttonSize, titleBarRect.Y + style.FramePadding.Y);
|
||||
padR += buttonSize + style.ItemInnerSpacing.X;
|
||||
|
||||
|
||||
if (DrawButton(button, position))
|
||||
button.Click?.Invoke(ImGuiMouseButton.Left);
|
||||
}
|
||||
|
||||
|
||||
ImGui.PopClipRect();
|
||||
}
|
||||
|
||||
|
|
@ -625,7 +737,7 @@ public abstract class Window
|
|||
public struct WindowSizeConstraints
|
||||
{
|
||||
private Vector2 internalMaxSize = new(float.MaxValue);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WindowSizeConstraints"/> struct.
|
||||
/// </summary>
|
||||
|
|
@ -637,7 +749,7 @@ public abstract class Window
|
|||
/// Gets or sets the minimum size of the window.
|
||||
/// </summary>
|
||||
public Vector2 MinimumSize { get; set; } = new(0);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum size of the window.
|
||||
/// </summary>
|
||||
|
|
@ -646,12 +758,12 @@ public abstract class Window
|
|||
get => this.GetSafeMaxSize();
|
||||
set => this.internalMaxSize = value;
|
||||
}
|
||||
|
||||
|
||||
private Vector2 GetSafeMaxSize()
|
||||
{
|
||||
var currentMin = this.MinimumSize;
|
||||
|
||||
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
|
||||
if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y)
|
||||
return new Vector2(float.MaxValue);
|
||||
|
||||
return this.internalMaxSize;
|
||||
|
|
@ -667,53 +779,56 @@ public abstract class Window
|
|||
/// Gets or sets the icon of the button.
|
||||
/// </summary>
|
||||
public FontAwesomeIcon Icon { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a vector by which the position of the icon within the button shall be offset.
|
||||
/// Automatically scaled by the global font scale for you.
|
||||
/// </summary>
|
||||
public Vector2 IconOffset { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an action that is called when a tooltip shall be drawn.
|
||||
/// May be null if no tooltip shall be drawn.
|
||||
/// </summary>
|
||||
public Action? ShowTooltip { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an action that is called when the button is clicked.
|
||||
/// </summary>
|
||||
public Action<ImGuiMouseButton> Click { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority the button shall be shown in.
|
||||
/// Lower = closer to ImGui default buttons.
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not the button shall be clickable
|
||||
/// when the respective window is set to clickthrough.
|
||||
/// </summary>
|
||||
public bool AvailableClickthrough { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "imports")]
|
||||
private static unsafe class ImGuiNativeAdditions
|
||||
{
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags);
|
||||
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern bool igButtonBehavior(Vector4 bb, uint id, bool* outHovered, bool* outHeld, ImGuiButtonFlags flags);
|
||||
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void* igGetCurrentWindow();
|
||||
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void igStartMouseMovingWindow(void* window);
|
||||
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window);
|
||||
|
||||
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern void igCustom_WindowSetInheritNoInputs(bool inherit);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.Windowing.Persistence;
|
||||
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
|
@ -104,20 +104,28 @@ public class WindowSystem
|
|||
if (hasNamespace)
|
||||
ImGui.PushID(this.Namespace);
|
||||
|
||||
// These must be nullable, people are using stock WindowSystems and Windows without Dalamud for tests
|
||||
var config = Service<DalamudConfiguration>.GetNullable();
|
||||
var persistence = Service<WindowSystemPersistence>.GetNullable();
|
||||
|
||||
var flags = Window.WindowDrawFlags.None;
|
||||
|
||||
if (config?.EnablePluginUISoundEffects ?? false)
|
||||
flags |= Window.WindowDrawFlags.UseSoundEffects;
|
||||
|
||||
if (config?.EnablePluginUiAdditionalOptions ?? false)
|
||||
flags |= Window.WindowDrawFlags.UseAdditionalOptions;
|
||||
|
||||
if (config?.IsFocusManagementEnabled ?? false)
|
||||
flags |= Window.WindowDrawFlags.UseFocusManagement;
|
||||
|
||||
// Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating
|
||||
foreach (var window in this.windows.ToArray())
|
||||
{
|
||||
#if DEBUG
|
||||
// Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}");
|
||||
// Log.Verbose($"[WS{(hasNamespace ? "/" + this.Namespace : string.Empty)}] Drawing {window.WindowName}");
|
||||
#endif
|
||||
var snapshot = ImGuiManagedAsserts.GetSnapshot();
|
||||
|
||||
window.DrawInternal(config);
|
||||
|
||||
var source = ($"{this.Namespace}::" ?? string.Empty) + window.WindowName;
|
||||
ImGuiManagedAsserts.ReportProblems(source, snapshot);
|
||||
window.DrawInternal(flags, persistence);
|
||||
}
|
||||
|
||||
var focusedWindow = this.windows.FirstOrDefault(window => window.IsFocused && window.RespectCloseHotkey);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Core.Enrichers;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Dalamud.Logging.Internal;
|
||||
|
|
@ -11,7 +12,7 @@ public class ModuleLog
|
|||
{
|
||||
private readonly string moduleName;
|
||||
private readonly ILogger moduleLogger;
|
||||
|
||||
|
||||
// FIXME (v9): Deprecate this class in favor of using contextualized ILoggers with proper formatting.
|
||||
// We can keep this class around as a Serilog helper, but ModuleLog should no longer be a returned
|
||||
// type, instead returning a (prepared) ILogger appropriately.
|
||||
|
|
@ -27,6 +28,21 @@ public class ModuleLog
|
|||
this.moduleLogger = Log.ForContext("Dalamud.ModuleName", this.moduleName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModuleLog"/> class.
|
||||
/// This class will properly attach SourceContext and other attributes per Serilog standards.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of the class this logger is for.</param>
|
||||
public ModuleLog(Type type)
|
||||
{
|
||||
this.moduleName = type.Name;
|
||||
this.moduleLogger = Log.ForContext(
|
||||
[
|
||||
new PropertyEnricher(Constants.SourceContextPropertyName, type.FullName),
|
||||
new PropertyEnricher("Dalamud.ModuleName", this.moduleName)
|
||||
]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log a templated verbose message to the in-game debug log.
|
||||
/// </summary>
|
||||
|
|
@ -160,4 +176,11 @@ public class ModuleLog
|
|||
messageTemplate: $"[{this.moduleName}] {messageTemplate}",
|
||||
values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to create a new <see cref="ModuleLog"/> instance based on a type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The class to create this ModuleLog for.</typeparam>
|
||||
/// <returns>Returns a ModuleLog with name set.</returns>
|
||||
internal static ModuleLog Create<T>() => new(typeof(T));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Internal.Types.Manifest;
|
||||
|
||||
namespace Dalamud.Plugin;
|
||||
|
||||
|
|
@ -22,6 +23,47 @@ public interface IExposedPlugin
|
|||
/// </summary>
|
||||
bool IsLoaded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin's API level is out of date.
|
||||
/// </summary>
|
||||
bool IsOutdated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin is for testing use only.
|
||||
/// </summary>
|
||||
bool IsTesting { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not.
|
||||
/// </summary>
|
||||
bool IsOrphaned { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does).
|
||||
/// </summary>
|
||||
bool IsDecommissioned { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin has been banned.
|
||||
/// </summary>
|
||||
bool IsBanned { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin is dev plugin.
|
||||
/// </summary>
|
||||
bool IsDev { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
|
||||
/// repo.
|
||||
/// </summary>
|
||||
bool IsThirdParty { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin manifest.
|
||||
/// </summary>
|
||||
ILocalPluginManifest Manifest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the version of the plugin.
|
||||
/// </summary>
|
||||
|
|
@ -74,6 +116,30 @@ internal sealed class ExposedPlugin(LocalPlugin plugin) : IExposedPlugin
|
|||
/// <inheritdoc/>
|
||||
public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasConfigUi ?? false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsOutdated => plugin.IsOutdated;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsTesting => plugin.IsTesting;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsOrphaned => plugin.IsOrphaned;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsDecommissioned => plugin.IsDecommissioned;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsBanned => plugin.IsBanned;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsDev => plugin.IsDev;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsThirdParty => plugin.IsThirdParty;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILocalPluginManifest Manifest => plugin.Manifest;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OpenMainUi()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ using Dalamud.Console;
|
|||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.EventArgs;
|
||||
|
|
@ -31,17 +35,17 @@ namespace Dalamud.Plugin.Internal.AutoUpdate;
|
|||
internal class AutoUpdateManager : IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("AUTOUPDATE");
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Time we should wait after login to update.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Time we should wait between scheduled update checks.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Time we should wait between scheduled update checks if the user has dismissed the notification,
|
||||
/// instead of updating. We don't want to spam the user with notifications.
|
||||
|
|
@ -56,28 +60,30 @@ internal class AutoUpdateManager : IServiceType
|
|||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly PluginManager pluginManager = Service<PluginManager>.Get();
|
||||
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration config = Service<DalamudConfiguration>.Get();
|
||||
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly NotificationManager notificationManager = Service<NotificationManager>.Get();
|
||||
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudInterface dalamudInterface = Service<DalamudInterface>.Get();
|
||||
|
||||
|
||||
private readonly IConsoleVariable<bool> isDryRun;
|
||||
|
||||
|
||||
private readonly Task<DalamudLinkPayload> openInstallerWindowLinkTask;
|
||||
|
||||
private DateTime? loginTime;
|
||||
private DateTime? nextUpdateCheckTime;
|
||||
private DateTime? unblockedSince;
|
||||
|
||||
|
||||
private bool hasStartedInitialUpdateThisSession;
|
||||
|
||||
private IActiveNotification? updateNotification;
|
||||
|
||||
|
||||
private Task? autoUpdateTask;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AutoUpdateManager"/> class.
|
||||
/// </summary>
|
||||
|
|
@ -92,7 +98,17 @@ internal class AutoUpdateManager : IServiceType
|
|||
t.Result.Logout += (int type, int code) => this.OnLogout();
|
||||
});
|
||||
Service<Framework>.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
|
||||
|
||||
|
||||
this.openInstallerWindowLinkTask =
|
||||
Service<ChatGui>.GetAsync().ContinueWith(
|
||||
chatGuiTask => chatGuiTask.Result.AddChatLinkHandler(
|
||||
"Dalamud",
|
||||
1001,
|
||||
(_, _) =>
|
||||
{
|
||||
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerOpenKind.InstalledPlugins);
|
||||
}));
|
||||
|
||||
this.isDryRun = console.AddVariable("dalamud.autoupdate.dry_run", "Simulate updates instead", false);
|
||||
console.AddCommand("dalamud.autoupdate.trigger_login", "Trigger a login event", () =>
|
||||
{
|
||||
|
|
@ -106,36 +122,36 @@ internal class AutoUpdateManager : IServiceType
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private enum UpdateListingRestriction
|
||||
{
|
||||
Unrestricted,
|
||||
AllowNone,
|
||||
AllowMainRepo,
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not auto-updates have already completed this session.
|
||||
/// </summary>
|
||||
public bool IsAutoUpdateComplete { get; private set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time of the next scheduled update check.
|
||||
/// </summary>
|
||||
public DateTime? NextUpdateCheckTime => this.nextUpdateCheckTime;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time the auto-update was unblocked.
|
||||
/// </summary>
|
||||
public DateTime? UnblockedSince => this.unblockedSince;
|
||||
|
||||
|
||||
private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdateBehavior behavior)
|
||||
{
|
||||
return behavior switch
|
||||
{
|
||||
// We don't generally allow any updates in this mode, but specific opt-ins.
|
||||
AutoUpdateBehavior.None => UpdateListingRestriction.AllowNone,
|
||||
|
||||
|
||||
// If we're only notifying, I guess it's fine to list all plugins.
|
||||
AutoUpdateBehavior.OnlyNotify => UpdateListingRestriction.Unrestricted,
|
||||
|
||||
|
|
@ -144,7 +160,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
_ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification)
|
||||
{
|
||||
if (primary ?
|
||||
|
|
@ -179,7 +195,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
this.updateNotification = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we're blocked, we don't do anything.
|
||||
if (!isUnblocked)
|
||||
return;
|
||||
|
|
@ -199,16 +215,16 @@ internal class AutoUpdateManager : IServiceType
|
|||
if (!this.hasStartedInitialUpdateThisSession && DateTime.Now > this.loginTime.Value.Add(UpdateTimeAfterLogin))
|
||||
{
|
||||
this.hasStartedInitialUpdateThisSession = true;
|
||||
|
||||
|
||||
var currentlyUpdatablePlugins = this.GetAvailablePluginUpdates(DecideUpdateListingRestriction(behavior));
|
||||
if (currentlyUpdatablePlugins.Count == 0)
|
||||
{
|
||||
this.IsAutoUpdateComplete = true;
|
||||
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// TODO: This is not 100% what we want... Plugins that are opted-in should be updated regardless of the behavior,
|
||||
// and we should show a notification for the others afterwards.
|
||||
if (behavior == AutoUpdateBehavior.OnlyNotify)
|
||||
|
|
@ -241,6 +257,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
Log.Error(t.Exception!, "Failed to reload plugin masters for auto-update");
|
||||
}
|
||||
|
||||
Log.Verbose($"Available Updates: {string.Join(", ", this.pluginManager.UpdatablePlugins.Select(s => s.UpdateManifest.InternalName))}");
|
||||
var updatable = this.GetAvailablePluginUpdates(
|
||||
DecideUpdateListingRestriction(behavior));
|
||||
|
||||
|
|
@ -252,7 +269,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
{
|
||||
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
|
||||
Log.Verbose(
|
||||
"Auto update found nothing to do, next update at {Time}",
|
||||
"Auto update found nothing to do, next update at {Time}",
|
||||
this.nextUpdateCheckTime);
|
||||
}
|
||||
});
|
||||
|
|
@ -263,13 +280,13 @@ internal class AutoUpdateManager : IServiceType
|
|||
{
|
||||
if (this.updateNotification != null)
|
||||
throw new InvalidOperationException("Already showing a notification");
|
||||
|
||||
|
||||
this.updateNotification = this.notificationManager.AddNotification(notification);
|
||||
|
||||
this.updateNotification.Dismiss += _ =>
|
||||
{
|
||||
this.updateNotification = null;
|
||||
|
||||
|
||||
// Schedule the next update opportunistically for when this closes.
|
||||
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecks;
|
||||
};
|
||||
|
|
@ -291,7 +308,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
{
|
||||
Log.Warning("Auto-update task was canceled");
|
||||
}
|
||||
|
||||
|
||||
this.autoUpdateTask = null;
|
||||
this.IsAutoUpdateComplete = true;
|
||||
});
|
||||
|
|
@ -321,20 +338,20 @@ internal class AutoUpdateManager : IServiceType
|
|||
notification.Content = Locs.NotificationContentUpdating(updateProgress.CurrentPluginManifest.Name);
|
||||
notification.Progress = (float)updateProgress.PluginsProcessed / updateProgress.TotalPlugins;
|
||||
};
|
||||
|
||||
|
||||
var pluginStates = (await this.pluginManager.UpdatePluginsAsync(updatablePlugins, this.isDryRun.Value, true, progress)).ToList();
|
||||
this.pluginManager.PrintUpdatedPlugins(pluginStates, Loc.Localize("DalamudPluginAutoUpdate", "The following plugins were auto-updated:"));
|
||||
|
||||
notification.Progress = 1;
|
||||
notification.UserDismissable = true;
|
||||
notification.HardExpiry = DateTime.Now.AddSeconds(30);
|
||||
|
||||
|
||||
notification.DrawActions += _ =>
|
||||
{
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
DrawOpenInstallerNotificationButton(true, PluginInstallerOpenKind.InstalledPlugins, notification);
|
||||
};
|
||||
|
||||
|
||||
// Update the notification to show the final state
|
||||
if (pluginStates.All(x => x.Status == PluginUpdateStatus.StatusKind.Success))
|
||||
{
|
||||
|
|
@ -342,7 +359,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
|
||||
// Janky way to make sure the notification does not change before it's minimized...
|
||||
await Task.Delay(500);
|
||||
|
||||
|
||||
notification.Title = Locs.NotificationTitleUpdatesSuccessful;
|
||||
notification.MinimizedText = Locs.NotificationContentUpdatesSuccessfulMinimized;
|
||||
notification.Type = NotificationType.Success;
|
||||
|
|
@ -354,11 +371,11 @@ internal class AutoUpdateManager : IServiceType
|
|||
notification.MinimizedText = Locs.NotificationContentUpdatesFailedMinimized;
|
||||
notification.Type = NotificationType.Error;
|
||||
notification.Content = Locs.NotificationContentUpdatesFailed;
|
||||
|
||||
|
||||
var failedPlugins = pluginStates
|
||||
.Where(x => x.Status != PluginUpdateStatus.StatusKind.Success)
|
||||
.Select(x => x.Name).ToList();
|
||||
|
||||
|
||||
notification.Content += "\n" + Locs.NotificationContentFailedPlugins(failedPlugins);
|
||||
}
|
||||
}
|
||||
|
|
@ -367,7 +384,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
{
|
||||
if (updatablePlugins.Count == 0)
|
||||
return;
|
||||
|
||||
|
||||
var notification = this.GetBaseNotification(new Notification
|
||||
{
|
||||
Title = Locs.NotificationTitleUpdatesAvailable,
|
||||
|
|
@ -400,16 +417,44 @@ internal class AutoUpdateManager : IServiceType
|
|||
notification.Dismiss += args =>
|
||||
{
|
||||
if (args.Reason != NotificationDismissReason.Manual) return;
|
||||
|
||||
|
||||
this.nextUpdateCheckTime = DateTime.Now + TimeBetweenUpdateChecksIfDismissed;
|
||||
Log.Verbose("User dismissed update notification, next check at {Time}", this.nextUpdateCheckTime);
|
||||
};
|
||||
|
||||
// Send out a chat message only if the user requested so
|
||||
if (!this.config.SendUpdateNotificationToChat)
|
||||
return;
|
||||
|
||||
var chatGui = Service<ChatGui>.GetNullable();
|
||||
if (chatGui == null)
|
||||
{
|
||||
Log.Verbose("Unable to get chat gui, discard notification for chat.");
|
||||
return;
|
||||
}
|
||||
|
||||
chatGui.Print(new XivChatEntry
|
||||
{
|
||||
Message = new SeString(new List<Payload>
|
||||
{
|
||||
new TextPayload(Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count)),
|
||||
new TextPayload(" ["),
|
||||
new UIForegroundPayload(500),
|
||||
this.openInstallerWindowLinkTask.Result,
|
||||
new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
|
||||
RawPayload.LinkTerminator,
|
||||
new UIForegroundPayload(0),
|
||||
new TextPayload("]"),
|
||||
}),
|
||||
|
||||
Type = XivChatType.Urgent,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private List<AvailablePluginUpdate> GetAvailablePluginUpdates(UpdateListingRestriction restriction)
|
||||
{
|
||||
var optIns = this.config.PluginAutoUpdatePreferences.ToArray();
|
||||
|
||||
|
||||
// Get all of our updatable plugins and do some initial filtering that must apply to all plugins.
|
||||
var updateablePlugins = this.pluginManager.UpdatablePlugins
|
||||
.Where(
|
||||
|
|
@ -423,14 +468,14 @@ internal class AutoUpdateManager : IServiceType
|
|||
bool FilterPlugin(AvailablePluginUpdate availablePluginUpdate)
|
||||
{
|
||||
var optIn = optIns.FirstOrDefault(x => x.WorkingPluginId == availablePluginUpdate.InstalledPlugin.EffectiveWorkingPluginId);
|
||||
|
||||
|
||||
// If this is an opt-out, we don't update.
|
||||
if (optIn is { Kind: AutoUpdatePreference.OptKind.NeverUpdate })
|
||||
return false;
|
||||
|
||||
if (restriction == UpdateListingRestriction.AllowNone && optIn is not { Kind: AutoUpdatePreference.OptKind.AlwaysUpdate })
|
||||
return false;
|
||||
|
||||
|
||||
if (restriction == UpdateListingRestriction.AllowMainRepo && availablePluginUpdate.InstalledPlugin.IsThirdParty)
|
||||
return false;
|
||||
|
||||
|
|
@ -442,7 +487,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
{
|
||||
this.loginTime = DateTime.Now;
|
||||
}
|
||||
|
||||
|
||||
private void OnLogout()
|
||||
{
|
||||
this.loginTime = null;
|
||||
|
|
@ -452,7 +497,7 @@ internal class AutoUpdateManager : IServiceType
|
|||
{
|
||||
var condition = Service<Condition>.Get();
|
||||
return this.IsPluginManagerReady() &&
|
||||
!this.dalamudInterface.IsPluginInstallerOpen &&
|
||||
!this.dalamudInterface.IsPluginInstallerOpen &&
|
||||
condition.OnlyAny(ConditionFlag.NormalConditions,
|
||||
ConditionFlag.Jumping,
|
||||
ConditionFlag.Mounted,
|
||||
|
|
@ -469,21 +514,21 @@ internal class AutoUpdateManager : IServiceType
|
|||
public static string NotificationButtonOpenPluginInstaller => Loc.Localize("AutoUpdateOpenPluginInstaller", "Open installer");
|
||||
|
||||
public static string NotificationButtonUpdate => Loc.Localize("AutoUpdateUpdate", "Update");
|
||||
|
||||
|
||||
public static string NotificationTitleUpdatesAvailable => Loc.Localize("AutoUpdateUpdatesAvailable", "Updates available!");
|
||||
|
||||
|
||||
public static string NotificationTitleUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessful", "Updates successful!");
|
||||
|
||||
|
||||
public static string NotificationTitleUpdatingPlugins => Loc.Localize("AutoUpdateUpdatingPlugins", "Updating plugins...");
|
||||
|
||||
|
||||
public static string NotificationTitleUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailed", "Updates failed!");
|
||||
|
||||
|
||||
public static string NotificationContentUpdatesSuccessful => Loc.Localize("AutoUpdateUpdatesSuccessfulContent", "All plugins have been updated successfully.");
|
||||
|
||||
|
||||
public static string NotificationContentUpdatesSuccessfulMinimized => Loc.Localize("AutoUpdateUpdatesSuccessfulContentMinimized", "Plugins updated successfully.");
|
||||
|
||||
|
||||
public static string NotificationContentUpdatesFailed => Loc.Localize("AutoUpdateUpdatesFailedContent", "Some plugins failed to update. Please check the plugin installer for more information.");
|
||||
|
||||
|
||||
public static string NotificationContentUpdatesFailedMinimized => Loc.Localize("AutoUpdateUpdatesFailedContentMinimized", "Plugins failed to update.");
|
||||
|
||||
public static string NotificationContentUpdatesAvailable(ICollection<AvailablePluginUpdate> updatablePlugins)
|
||||
|
|
@ -497,20 +542,20 @@ internal class AutoUpdateManager : IServiceType
|
|||
"There are {0} plugins that can be updated:"),
|
||||
updatablePlugins.Count))
|
||||
+ "\n\n" + string.Join(", ", updatablePlugins.Select(x => x.InstalledPlugin.Manifest.Name));
|
||||
|
||||
|
||||
public static string NotificationContentUpdatesAvailableMinimized(int numUpdates)
|
||||
=> numUpdates == 1 ?
|
||||
Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") :
|
||||
Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedSingular", "1 plugin update available") :
|
||||
string.Format(Loc.Localize("AutoUpdateUpdatesAvailableContentMinimizedPlural", "{0} plugin updates available"), numUpdates);
|
||||
|
||||
|
||||
public static string NotificationContentPreparingToUpdate(int numPlugins)
|
||||
=> numPlugins == 1 ?
|
||||
Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") :
|
||||
Loc.Localize("AutoUpdatePreparingToUpdateSingular", "Preparing to update 1 plugin...") :
|
||||
string.Format(Loc.Localize("AutoUpdatePreparingToUpdatePlural", "Preparing to update {0} plugins..."), numPlugins);
|
||||
|
||||
|
||||
public static string NotificationContentUpdating(string name)
|
||||
=> string.Format(Loc.Localize("AutoUpdateUpdating", "Updating {0}..."), name);
|
||||
|
||||
|
||||
public static string NotificationContentFailedPlugins(IEnumerable<string> failedPlugins)
|
||||
=> string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
namespace Dalamud.Plugin.Internal.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// An exception to be thrown when policy blocks a plugin from loading.
|
||||
/// </summary>
|
||||
internal class InternalPluginStateException : InvalidPluginOperationException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InternalPluginStateException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to associate with this exception.</param>
|
||||
public InternalPluginStateException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -48,12 +48,12 @@ internal class PluginManager : IInternalDisposableService
|
|||
/// </summary>
|
||||
public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable
|
||||
|
||||
private static readonly ModuleLog Log = new("PLUGINM");
|
||||
private static readonly ModuleLog Log = ModuleLog.Create<PluginManager>();
|
||||
|
||||
private readonly object pluginListLock = new();
|
||||
private readonly DirectoryInfo pluginDirectory;
|
||||
private readonly BannedPlugin[]? bannedPlugins;
|
||||
|
||||
|
||||
private readonly List<LocalPlugin> installedPluginsList = new();
|
||||
private readonly List<RemotePluginManifest> availablePluginsList = new();
|
||||
private readonly List<AvailablePluginUpdate> updatablePluginsList = new();
|
||||
|
|
@ -134,9 +134,6 @@ internal class PluginManager : IInternalDisposableService
|
|||
this.configuration.PluginTestingOptIns ??= new();
|
||||
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
|
||||
|
||||
// NET8 CHORE
|
||||
// this.ApplyPatches();
|
||||
|
||||
registerStartupBlocker(
|
||||
Task.Run(this.LoadAndStartLoadSyncPlugins),
|
||||
"Waiting for plugins that asked to be loaded before the game.");
|
||||
|
|
@ -210,7 +207,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a copy of the list of all plugins with an available update.
|
||||
/// </summary>
|
||||
|
|
@ -246,9 +243,9 @@ internal class PluginManager : IInternalDisposableService
|
|||
public bool ReposReady { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the plugin manager started in safe mode.
|
||||
/// Gets or sets a value indicating whether the plugin manager started in safe mode.
|
||||
/// </summary>
|
||||
public bool SafeMode { get; init; }
|
||||
public bool SafeMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="PluginConfigurations"/> object used when initializing plugins.
|
||||
|
|
@ -264,7 +261,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
/// Gets or sets a value indicating whether banned plugins will be loaded.
|
||||
/// </summary>
|
||||
public bool LoadBannedPlugins { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a tracker for plugins that are loading at startup, used to display information to the user.
|
||||
/// </summary>
|
||||
|
|
@ -433,10 +430,6 @@ internal class PluginManager : IInternalDisposableService
|
|||
await Task.WhenAll(disposablePlugins.Select(plugin => plugin.DisposeAsync().AsTask()))
|
||||
.SuppressException();
|
||||
}
|
||||
|
||||
// NET8 CHORE
|
||||
// this.assemblyLocationMonoHook?.Dispose();
|
||||
// this.assemblyCodeBaseMonoHook?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -486,7 +479,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
Log.Error("No DLL found for plugin at {Path}", versionDir.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
|
||||
if (!manifestFile.Exists)
|
||||
{
|
||||
|
|
@ -513,7 +506,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
|
||||
this.configuration.QueueSave();
|
||||
|
||||
|
||||
if (versionsDefs.Count == 0)
|
||||
{
|
||||
Log.Verbose("No versions found for plugin: {Name}", pluginDir.Name);
|
||||
|
|
@ -559,7 +552,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
Log.Error("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var manifest = LocalPluginManifest.Load(manifestFile);
|
||||
if (manifest == null)
|
||||
{
|
||||
|
|
@ -768,7 +761,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
.SelectMany(repo => repo.PluginMaster)
|
||||
.Where(this.IsManifestEligible)
|
||||
.Where(IsManifestVisible));
|
||||
|
||||
|
||||
if (notify)
|
||||
{
|
||||
this.NotifyAvailablePluginsChanged();
|
||||
|
|
@ -790,7 +783,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
{
|
||||
if (!setting.IsEnabled)
|
||||
continue;
|
||||
|
||||
|
||||
Log.Verbose("Scanning dev plugins at {Path}", setting.Path);
|
||||
|
||||
if (File.Exists(setting.Path))
|
||||
|
|
@ -817,7 +810,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
Log.Error("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var manifest = LocalPluginManifest.Load(manifestFile);
|
||||
if (manifest == null)
|
||||
{
|
||||
|
|
@ -861,7 +854,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
var stream = await this.DownloadPluginAsync(repoManifest, useTesting);
|
||||
return await this.InstallPluginInternalAsync(repoManifest, useTesting, reason, stream, inheritedWorkingPluginId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin.
|
||||
/// </summary>
|
||||
|
|
@ -876,9 +869,6 @@ internal class PluginManager : IInternalDisposableService
|
|||
this.installedPluginsList.Remove(plugin);
|
||||
}
|
||||
|
||||
// NET8 CHORE
|
||||
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
|
||||
this.NotifyinstalledPluginsListChanged();
|
||||
this.NotifyAvailablePluginsChanged();
|
||||
}
|
||||
|
|
@ -1058,7 +1048,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
Status = PluginUpdateStatus.StatusKind.Success,
|
||||
HasChangelog = !metadata.UpdateManifest.Changelog.IsNullOrWhitespace(),
|
||||
};
|
||||
|
||||
|
||||
// Check if this plugin is already up to date (=> AvailablePluginUpdate was stale)
|
||||
lock (this.installedPluginsList)
|
||||
{
|
||||
|
|
@ -1085,7 +1075,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
updateStatus.Status = PluginUpdateStatus.StatusKind.FailedDownload;
|
||||
return updateStatus;
|
||||
}
|
||||
|
||||
|
||||
// Unload if loaded
|
||||
if (plugin.State is PluginState.Loaded or PluginState.LoadError or PluginState.DependencyResolutionFailed)
|
||||
{
|
||||
|
|
@ -1315,7 +1305,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
{
|
||||
if (serviceType == typeof(PluginManager))
|
||||
continue;
|
||||
|
||||
|
||||
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
|
||||
// Nonetheless, their direct dependencies must be considered.
|
||||
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
|
||||
|
|
@ -1323,19 +1313,19 @@ internal class PluginManager : IInternalDisposableService
|
|||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false);
|
||||
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
|
||||
|
||||
|
||||
foreach (var scopedDep in dependencies)
|
||||
{
|
||||
if (scopedDep == typeof(PluginManager))
|
||||
throw new Exception("Scoped plugin services cannot depend on PluginManager.");
|
||||
|
||||
|
||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
|
||||
yield return scopedDep;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
|
||||
if (pluginInterfaceAttribute == null)
|
||||
continue;
|
||||
|
|
@ -1346,12 +1336,12 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if there are any inconsistencies with our plugins, their IDs, and our profiles.
|
||||
/// Check if there are any inconsistencies with our plugins, their IDs, and our profiles.
|
||||
/// </summary>
|
||||
private void ParanoiaValidatePluginsAndProfiles()
|
||||
{
|
||||
var seenIds = new List<Guid>();
|
||||
|
||||
|
||||
foreach (var installedPlugin in this.InstalledPlugins)
|
||||
{
|
||||
if (installedPlugin.EffectiveWorkingPluginId == Guid.Empty)
|
||||
|
|
@ -1362,13 +1352,13 @@ internal class PluginManager : IInternalDisposableService
|
|||
throw new Exception(
|
||||
$"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.EffectiveWorkingPluginId}'");
|
||||
}
|
||||
|
||||
|
||||
seenIds.Add(installedPlugin.EffectiveWorkingPluginId);
|
||||
}
|
||||
|
||||
|
||||
this.profileManager.ParanoiaValidateProfiles();
|
||||
}
|
||||
|
||||
|
||||
private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting)
|
||||
{
|
||||
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
|
||||
|
|
@ -1401,7 +1391,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
{
|
||||
var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
|
||||
Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting}, version={version}, reason={reason})");
|
||||
|
||||
|
||||
// If this plugin is in the default profile for whatever reason, delete the state
|
||||
// If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there,
|
||||
// or the user removed the plugin manually in which case we don't care
|
||||
|
|
@ -1435,7 +1425,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
// If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed
|
||||
Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null");
|
||||
}
|
||||
|
||||
|
||||
// Ensure that we have a testing opt-in for this plugin if we are installing a testing version
|
||||
if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName))
|
||||
{
|
||||
|
|
@ -1544,7 +1534,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
this.NotifyinstalledPluginsListChanged();
|
||||
return plugin;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Load a plugin.
|
||||
/// </summary>
|
||||
|
|
@ -1572,13 +1562,17 @@ internal class PluginManager : IInternalDisposableService
|
|||
{
|
||||
Log.Information($"Loading dev plugin {name}");
|
||||
plugin = new LocalDevPlugin(dllFile, manifest);
|
||||
|
||||
// This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet
|
||||
// TODO(goat): Re-enable this when we have better tracing for what was rendering when
|
||||
// this.configuration.ImGuiAssertsEnabledAtStartup ??= true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information($"Loading plugin {name}");
|
||||
plugin = new LocalPlugin(dllFile, manifest);
|
||||
}
|
||||
|
||||
|
||||
// Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here.
|
||||
// This will also happen if you are installing a plugin with the installer, and that's intended!
|
||||
// It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will
|
||||
|
|
@ -1586,7 +1580,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
if (plugin.EffectiveWorkingPluginId == Guid.Empty)
|
||||
throw new Exception("Plugin should have a WorkingPluginId at this point");
|
||||
this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.EffectiveWorkingPluginId);
|
||||
|
||||
|
||||
var wantedByAnyProfile = false;
|
||||
|
||||
// Now, if this is a devPlugin, figure out if we want to load it
|
||||
|
|
@ -1602,11 +1596,11 @@ internal class PluginManager : IInternalDisposableService
|
|||
// We don't know about this plugin, so we don't want to do anything here.
|
||||
// The code below will take care of it and add it with the default value.
|
||||
Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName);
|
||||
|
||||
|
||||
// Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active.
|
||||
// Note that this will not add the plugin to the default profile. That's done below in any other case.
|
||||
wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, false, false);
|
||||
|
||||
|
||||
// If it is wanted by any other profile, we do want to load it.
|
||||
if (wantedByAnyProfile)
|
||||
loadPlugin = true;
|
||||
|
|
@ -1653,12 +1647,12 @@ internal class PluginManager : IInternalDisposableService
|
|||
#pragma warning disable CS0618
|
||||
var defaultState = manifest?.Disabled != true && loadPlugin;
|
||||
#pragma warning restore CS0618
|
||||
|
||||
|
||||
// Plugins that aren't in any profile will be added to the default profile with this call.
|
||||
// We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above.
|
||||
wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.EffectiveWorkingPluginId, plugin.Manifest.InternalName, defaultState);
|
||||
Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin);
|
||||
|
||||
|
||||
if (loadPlugin)
|
||||
{
|
||||
try
|
||||
|
|
@ -1674,8 +1668,6 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
catch (InvalidPluginException)
|
||||
{
|
||||
// NET8 CHORE
|
||||
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
throw;
|
||||
}
|
||||
catch (BannedPluginException)
|
||||
|
|
@ -1721,8 +1713,6 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
else
|
||||
{
|
||||
// NET8 CHORE
|
||||
// PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -1746,11 +1736,11 @@ internal class PluginManager : IInternalDisposableService
|
|||
private void DetectAvailablePluginUpdates()
|
||||
{
|
||||
Log.Debug("Starting plugin update check...");
|
||||
|
||||
|
||||
lock (this.pluginListLock)
|
||||
{
|
||||
this.updatablePluginsList.Clear();
|
||||
|
||||
|
||||
foreach (var plugin in this.installedPluginsList)
|
||||
{
|
||||
var installedVersion = plugin.IsTesting
|
||||
|
|
@ -1789,12 +1779,12 @@ internal class PluginManager : IInternalDisposableService
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count);
|
||||
}
|
||||
|
||||
private void NotifyAvailablePluginsChanged()
|
||||
{
|
||||
{
|
||||
this.DetectAvailablePluginUpdates();
|
||||
|
||||
this.OnAvailablePluginsChanged?.InvokeSafely();
|
||||
|
|
@ -1842,7 +1832,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
using (Timings.Start("PM Load Sync Plugins"))
|
||||
{
|
||||
var loadAllPlugins = Task.Run(this.LoadAllPlugins);
|
||||
|
||||
|
||||
// We wait for all blocking services and tasks to finish before kicking off the main thread in any mode.
|
||||
// This means that we don't want to block here if this stupid thing isn't enabled.
|
||||
if (this.configuration.IsResumeGameAfterPluginLoad)
|
||||
|
|
@ -1861,12 +1851,12 @@ internal class PluginManager : IInternalDisposableService
|
|||
Log.Error(ex, "Plugin load failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Class representing progress of an update operation.
|
||||
/// </summary>
|
||||
public record PluginUpdateProgress(int PluginsProcessed, int TotalPlugins, IPluginManifest CurrentPluginManifest);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Simple class that tracks the internal names and public names of plugins that we are planning to load at startup,
|
||||
/// and are still actively loading.
|
||||
|
|
@ -1876,12 +1866,12 @@ internal class PluginManager : IInternalDisposableService
|
|||
private readonly Dictionary<string, string> internalToPublic = new();
|
||||
private readonly ConcurrentBag<string> allInternalNames = new();
|
||||
private readonly ConcurrentBag<string> finishedInternalNames = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the total load progress.
|
||||
/// </summary>
|
||||
public float Progress => (float)this.finishedInternalNames.Count / this.allInternalNames.Count;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Calculate a set of internal names that are still pending.
|
||||
/// </summary>
|
||||
|
|
@ -1892,7 +1882,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
pending.ExceptWith(this.finishedInternalNames);
|
||||
return pending;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Track a new plugin.
|
||||
/// </summary>
|
||||
|
|
@ -1903,7 +1893,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
this.internalToPublic[internalName] = publicName;
|
||||
this.allInternalNames.Add(internalName);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Mark a plugin as finished loading.
|
||||
/// </summary>
|
||||
|
|
@ -1912,7 +1902,7 @@ internal class PluginManager : IInternalDisposableService
|
|||
{
|
||||
this.finishedInternalNames.Add(internalName);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the public name for a given internal name.
|
||||
/// </summary>
|
||||
|
|
@ -1931,114 +1921,3 @@ internal class PluginManager : IInternalDisposableService
|
|||
public static string DalamudPluginUpdateFailed(string name, Version version, string why) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed ({2}).").Format(name, version, why);
|
||||
}
|
||||
}
|
||||
|
||||
// NET8 CHORE
|
||||
/*
|
||||
/// <summary>
|
||||
/// Class responsible for loading and unloading plugins.
|
||||
/// This contains the assembly patching functionality to resolve assembly locations.
|
||||
/// </summary>
|
||||
internal partial class PluginManager
|
||||
{
|
||||
/// <summary>
|
||||
/// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
|
||||
/// plugins via byte[].
|
||||
/// </summary>
|
||||
internal static readonly ConcurrentDictionary<string, PluginPatchData> PluginLocations = new();
|
||||
|
||||
private MonoMod.RuntimeDetour.Hook? assemblyLocationMonoHook;
|
||||
private MonoMod.RuntimeDetour.Hook? assemblyCodeBaseMonoHook;
|
||||
|
||||
/// <summary>
|
||||
/// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location.
|
||||
/// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
|
||||
/// It should never be called manually.
|
||||
/// </summary>
|
||||
/// <param name="orig">A delegate that acts as the original method.</param>
|
||||
/// <param name="self">The equivalent of `this`.</param>
|
||||
/// <returns>The plugin location, or the result from the original method.</returns>
|
||||
private static string AssemblyLocationPatch(Func<Assembly, string?> orig, Assembly self)
|
||||
{
|
||||
var result = orig(self);
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
foreach (var assemblyName in GetStackFrameAssemblyNames())
|
||||
{
|
||||
if (PluginLocations.TryGetValue(assemblyName, out var data))
|
||||
{
|
||||
result = data.Location;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result ??= string.Empty;
|
||||
|
||||
Log.Verbose($"Assembly.Location // {self.FullName} // {result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase.
|
||||
/// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
|
||||
/// It should never be called manually.
|
||||
/// </summary>
|
||||
/// <param name="orig">A delegate that acts as the original method.</param>
|
||||
/// <param name="self">The equivalent of `this`.</param>
|
||||
/// <returns>The plugin code base, or the result from the original method.</returns>
|
||||
private static string AssemblyCodeBasePatch(Func<Assembly, string?> orig, Assembly self)
|
||||
{
|
||||
var result = orig(self);
|
||||
|
||||
if (string.IsNullOrEmpty(result))
|
||||
{
|
||||
foreach (var assemblyName in GetStackFrameAssemblyNames())
|
||||
{
|
||||
if (PluginLocations.TryGetValue(assemblyName, out var data))
|
||||
{
|
||||
result = data.CodeBase;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result ??= string.Empty;
|
||||
|
||||
Log.Verbose($"Assembly.CodeBase // {self.FullName} // {result}");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetStackFrameAssemblyNames()
|
||||
{
|
||||
var stackTrace = new StackTrace();
|
||||
var stackFrames = stackTrace.GetFrames();
|
||||
|
||||
foreach (var stackFrame in stackFrames)
|
||||
{
|
||||
var methodBase = stackFrame.GetMethod();
|
||||
if (methodBase == null)
|
||||
continue;
|
||||
|
||||
yield return methodBase.Module.Assembly.FullName!;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPatches()
|
||||
{
|
||||
var targetType = typeof(PluginManager).Assembly.GetType();
|
||||
|
||||
var locationTarget = targetType.GetProperty(nameof(Assembly.Location))!.GetGetMethod();
|
||||
var locationPatch = typeof(PluginManager).GetMethod(nameof(AssemblyLocationPatch), BindingFlags.NonPublic | BindingFlags.Static);
|
||||
this.assemblyLocationMonoHook = new MonoMod.RuntimeDetour.Hook(locationTarget, locationPatch);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
#pragma warning disable SYSLIB0012
|
||||
var codebaseTarget = targetType.GetProperty(nameof(Assembly.CodeBase))?.GetGetMethod();
|
||||
#pragma warning restore SYSLIB0012
|
||||
#pragma warning restore CS0618
|
||||
var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static);
|
||||
this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,388 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for profile-related chat commands.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class PluginManagementCommandHandler : IInternalDisposableService
|
||||
{
|
||||
#pragma warning disable SA1600
|
||||
public const string CommandEnableProfile = "/xlenablecollection";
|
||||
public const string CommandDisableProfile = "/xldisablecollection";
|
||||
public const string CommandToggleProfile = "/xltogglecollection";
|
||||
|
||||
public const string CommandEnablePlugin = "/xlenableplugin";
|
||||
public const string CommandDisablePlugin = "/xldisableplugin";
|
||||
public const string CommandTogglePlugin = "/xltoggleplugin";
|
||||
#pragma warning restore SA1600
|
||||
|
||||
private static readonly string LegacyCommandEnable = CommandEnableProfile.Replace("collection", "profile");
|
||||
private static readonly string LegacyCommandDisable = CommandDisableProfile.Replace("collection", "profile");
|
||||
private static readonly string LegacyCommandToggle = CommandToggleProfile.Replace("collection", "profile");
|
||||
|
||||
private readonly CommandManager cmd;
|
||||
private readonly ProfileManager profileManager;
|
||||
private readonly PluginManager pluginManager;
|
||||
private readonly ChatGui chat;
|
||||
private readonly Framework framework;
|
||||
|
||||
private List<(Target Target, PluginCommandOperation Operation)> commandQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginManagementCommandHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cmd">Command handler.</param>
|
||||
/// <param name="profileManager">Profile manager.</param>
|
||||
/// <param name="pluginManager">Plugin manager.</param>
|
||||
/// <param name="chat">Chat handler.</param>
|
||||
/// <param name="framework">Framework.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public PluginManagementCommandHandler(
|
||||
CommandManager cmd,
|
||||
ProfileManager profileManager,
|
||||
PluginManager pluginManager,
|
||||
ChatGui chat,
|
||||
Framework framework)
|
||||
{
|
||||
this.cmd = cmd;
|
||||
this.profileManager = profileManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.chat = chat;
|
||||
this.framework = framework;
|
||||
|
||||
this.cmd.AddHandler(CommandEnableProfile, new CommandInfo(this.OnEnableProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandDisableProfile, new CommandInfo(this.OnDisableProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandToggleProfile, new CommandInfo(this.OnToggleProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile)
|
||||
{
|
||||
ShowInHelp = false,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
|
||||
{
|
||||
ShowInHelp = false,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile)
|
||||
{
|
||||
ShowInHelp = false,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandEnablePlugin, new CommandInfo(this.OnEnablePlugin)
|
||||
{
|
||||
HelpMessage = Loc.Localize("PluginCommandsEnableHint", "Enable a plugin. Usage: /xlenableplugin \"Plugin Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandDisablePlugin, new CommandInfo(this.OnDisablePlugin)
|
||||
{
|
||||
HelpMessage = Loc.Localize("PluginCommandsDisableHint", "Disable a plugin. Usage: /xldisableplugin \"Plugin Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandTogglePlugin, new CommandInfo(this.OnTogglePlugin)
|
||||
{
|
||||
HelpMessage = Loc.Localize("PluginCommandsToggleHint", "Toggle a plugin. Usage: /xltoggleplugin \"Plugin Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
private enum PluginCommandOperation
|
||||
{
|
||||
Enable,
|
||||
Disable,
|
||||
Toggle,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.cmd.RemoveHandler(CommandEnableProfile);
|
||||
this.cmd.RemoveHandler(CommandDisableProfile);
|
||||
this.cmd.RemoveHandler(CommandToggleProfile);
|
||||
this.cmd.RemoveHandler(LegacyCommandEnable);
|
||||
this.cmd.RemoveHandler(LegacyCommandDisable);
|
||||
this.cmd.RemoveHandler(LegacyCommandToggle);
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
private void HandleProfileOperation(string profileName, PluginCommandOperation operation)
|
||||
{
|
||||
var profile = this.profileManager.Profiles.FirstOrDefault(
|
||||
x => x.Name == profileName);
|
||||
if (profile == null || profile.IsDefaultProfile)
|
||||
return;
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case PluginCommandOperation.Enable:
|
||||
if (!profile.IsEnabled)
|
||||
Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult();
|
||||
break;
|
||||
case PluginCommandOperation.Disable:
|
||||
if (profile.IsEnabled)
|
||||
Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult();
|
||||
break;
|
||||
case PluginCommandOperation.Toggle:
|
||||
Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(operation), operation, null);
|
||||
}
|
||||
|
||||
this.chat.Print(
|
||||
profile.IsEnabled
|
||||
? Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name)
|
||||
: Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
|
||||
|
||||
Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully && t.Exception != null)
|
||||
{
|
||||
Log.Error(t.Exception, "Could not apply profiles through commands");
|
||||
this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors."));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool HandlePluginOperation(Guid workingPluginId, PluginCommandOperation operation)
|
||||
{
|
||||
var plugin = this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.EffectiveWorkingPluginId == workingPluginId);
|
||||
if (plugin == null)
|
||||
return true;
|
||||
|
||||
switch (plugin.State)
|
||||
{
|
||||
// Ignore if the plugin is in a fail state
|
||||
case PluginState.LoadError or PluginState.UnloadError:
|
||||
this.chat.Print(Loc.Localize("PluginCommandsFailed", "Plugin \"{0}\" has previously failed to load/unload, not continuing.").Format(plugin.Name));
|
||||
return true;
|
||||
|
||||
case PluginState.Loaded when operation == PluginCommandOperation.Enable:
|
||||
this.chat.Print(Loc.Localize("PluginCommandsAlreadyEnabled", "Plugin \"{0}\" is already enabled.").Format(plugin.Name));
|
||||
return true;
|
||||
case PluginState.Unloaded when operation == PluginCommandOperation.Disable:
|
||||
this.chat.Print(Loc.Localize("PluginCommandsAlreadyDisabled", "Plugin \"{0}\" is already disabled.").Format(plugin.Name));
|
||||
return true;
|
||||
|
||||
// Defer if this plugin is busy right now
|
||||
case PluginState.Loading or PluginState.Unloading:
|
||||
return false;
|
||||
}
|
||||
|
||||
void Continuation(Task t, string onSuccess, string onError)
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully && t.Exception != null)
|
||||
{
|
||||
Log.Error(t.Exception, "Plugin command operation failed for plugin {PluginName}", plugin.Name);
|
||||
this.chat.PrintError(onError);
|
||||
return;
|
||||
}
|
||||
|
||||
this.chat.Print(onSuccess);
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case PluginCommandOperation.Enable:
|
||||
this.chat.Print(Loc.Localize("PluginCommandsEnabling", "Enabling plugin \"{0}\"...").Format(plugin.Name));
|
||||
Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
|
||||
.ContinueWith(t => Continuation(t,
|
||||
Loc.Localize("PluginCommandsEnableSuccess", "Plugin \"{0}\" enabled.").Format(plugin.Name),
|
||||
Loc.Localize("PluginCommandsEnableFailed", "Failed to enable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name)))
|
||||
.ConfigureAwait(false);
|
||||
break;
|
||||
case PluginCommandOperation.Disable:
|
||||
this.chat.Print(Loc.Localize("PluginCommandsDisabling", "Disabling plugin \"{0}\"...").Format(plugin.Name));
|
||||
Task.Run(() => plugin.UnloadAsync())
|
||||
.ContinueWith(t => Continuation(t,
|
||||
Loc.Localize("PluginCommandsDisableSuccess", "Plugin \"{0}\" disabled.").Format(plugin.Name),
|
||||
Loc.Localize("PluginCommandsDisableFailed", "Failed to disable plugin \"{0}\". Please check the console for errors.").Format(plugin.Name)))
|
||||
.ConfigureAwait(false);
|
||||
break;
|
||||
case PluginCommandOperation.Toggle:
|
||||
this.chat.Print(Loc.Localize("PluginCommandsToggling", "Toggling plugin \"{0}\"...").Format(plugin.Name));
|
||||
Task.Run(() => plugin.State == PluginState.Loaded ? plugin.UnloadAsync() : plugin.LoadAsync(PluginLoadReason.Installer))
|
||||
.ContinueWith(t => Continuation(t,
|
||||
Loc.Localize("PluginCommandsToggleSuccess", "Plugin \"{0}\" toggled.").Format(plugin.Name),
|
||||
Loc.Localize("PluginCommandsToggleFailed", "Failed to toggle plugin \"{0}\". Please check the console for errors.").Format(plugin.Name)))
|
||||
.ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(operation), operation, null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void FrameworkOnUpdate(IFramework framework1)
|
||||
{
|
||||
if (this.profileManager.IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.commandQueue.Count > 0)
|
||||
{
|
||||
var op = this.commandQueue[0];
|
||||
|
||||
var remove = true;
|
||||
switch (op.Target)
|
||||
{
|
||||
case PluginTarget pluginTarget:
|
||||
remove = this.HandlePluginOperation(pluginTarget.WorkingPluginId, op.Operation);
|
||||
break;
|
||||
case ProfileTarget profileTarget:
|
||||
this.HandleProfileOperation(profileTarget.ProfileName, op.Operation);
|
||||
break;
|
||||
}
|
||||
|
||||
if (remove)
|
||||
{
|
||||
this.commandQueue.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateProfileName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
var target = new ProfileTarget(name);
|
||||
this.commandQueue = this.commandQueue.Where(x => x.Target != target).ToList();
|
||||
this.commandQueue.Add((target, PluginCommandOperation.Enable));
|
||||
}
|
||||
|
||||
private void OnDisableProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateProfileName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
var target = new ProfileTarget(name);
|
||||
this.commandQueue = this.commandQueue.Where(x => x.Target != target).ToList();
|
||||
this.commandQueue.Add((target, PluginCommandOperation.Disable));
|
||||
}
|
||||
|
||||
private void OnToggleProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateProfileName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
var target = new ProfileTarget(name);
|
||||
this.commandQueue.Add((target, PluginCommandOperation.Toggle));
|
||||
}
|
||||
|
||||
private void OnEnablePlugin(string command, string arguments)
|
||||
{
|
||||
var plugin = this.ValidatePluginName(arguments);
|
||||
if (plugin == null)
|
||||
return;
|
||||
|
||||
var target = new PluginTarget(plugin.EffectiveWorkingPluginId);
|
||||
this.commandQueue
|
||||
.RemoveAll(x => x.Target == target);
|
||||
this.commandQueue.Add((target, PluginCommandOperation.Enable));
|
||||
}
|
||||
|
||||
private void OnDisablePlugin(string command, string arguments)
|
||||
{
|
||||
var plugin = this.ValidatePluginName(arguments);
|
||||
if (plugin == null)
|
||||
return;
|
||||
|
||||
var target = new PluginTarget(plugin.EffectiveWorkingPluginId);
|
||||
this.commandQueue
|
||||
.RemoveAll(x => x.Target == target);
|
||||
this.commandQueue.Add((target, PluginCommandOperation.Disable));
|
||||
}
|
||||
|
||||
private void OnTogglePlugin(string command, string arguments)
|
||||
{
|
||||
var plugin = this.ValidatePluginName(arguments);
|
||||
if (plugin == null)
|
||||
return;
|
||||
|
||||
var target = new PluginTarget(plugin.EffectiveWorkingPluginId);
|
||||
this.commandQueue
|
||||
.RemoveAll(x => x.Target == target);
|
||||
this.commandQueue.Add((target, PluginCommandOperation.Toggle));
|
||||
}
|
||||
|
||||
private string? ValidateProfileName(string arguments)
|
||||
{
|
||||
var name = arguments.Replace("\"", string.Empty);
|
||||
if (this.profileManager.Profiles.All(x => x.Name != name))
|
||||
{
|
||||
this.chat.PrintError(Loc.Localize("ProfileCommandsNotFound", "Collection \"{0}\" not found.").Format(name));
|
||||
return null;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private LocalPlugin? ValidatePluginName(string arguments)
|
||||
{
|
||||
var name = arguments.Replace("\"", string.Empty);
|
||||
var targetPlugin =
|
||||
this.pluginManager.InstalledPlugins.FirstOrDefault(x => x.InternalName == name || x.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
if (targetPlugin == null)
|
||||
{
|
||||
this.chat.PrintError(Loc.Localize("PluginCommandsNotFound", "Plugin \"{0}\" not found.").Format(name));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.profileManager.IsInDefaultProfile(targetPlugin.EffectiveWorkingPluginId))
|
||||
{
|
||||
this.chat.PrintError(Loc.Localize("PluginCommandsNotInDefaultProfile", "Plugin \"{0}\" is in a collection and can't be managed through commands. Manage the collection instead.")
|
||||
.Format(targetPlugin.Name));
|
||||
}
|
||||
|
||||
return targetPlugin;
|
||||
}
|
||||
|
||||
private abstract record Target;
|
||||
|
||||
private record PluginTarget(Guid WorkingPluginId) : Target;
|
||||
|
||||
private record ProfileTarget(string ProfileName) : Target;
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for profile-related chat commands.
|
||||
/// </summary>
|
||||
[ServiceManager.EarlyLoadedService]
|
||||
internal class ProfileCommandHandler : IInternalDisposableService
|
||||
{
|
||||
#pragma warning disable SA1600
|
||||
public const string CommandEnable = "/xlenablecollection";
|
||||
public const string CommandDisable = "/xldisablecollection";
|
||||
public const string CommandToggle = "/xltogglecollection";
|
||||
#pragma warning restore SA1600
|
||||
|
||||
private static readonly string LegacyCommandEnable = CommandEnable.Replace("collection", "profile");
|
||||
private static readonly string LegacyCommandDisable = CommandDisable.Replace("collection", "profile");
|
||||
private static readonly string LegacyCommandToggle = CommandToggle.Replace("collection", "profile");
|
||||
|
||||
private readonly CommandManager cmd;
|
||||
private readonly ProfileManager profileManager;
|
||||
private readonly ChatGui chat;
|
||||
private readonly Framework framework;
|
||||
|
||||
private List<(string, ProfileOp)> queue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProfileCommandHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cmd">Command handler.</param>
|
||||
/// <param name="profileManager">Profile manager.</param>
|
||||
/// <param name="chat">Chat handler.</param>
|
||||
/// <param name="framework">Framework.</param>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
public ProfileCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
|
||||
{
|
||||
this.cmd = cmd;
|
||||
this.profileManager = profileManager;
|
||||
this.chat = chat;
|
||||
this.framework = framework;
|
||||
|
||||
this.cmd.AddHandler(CommandEnable, new CommandInfo(this.OnEnableProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandDisable, new CommandInfo(this.OnDisableProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(CommandToggle, new CommandInfo(this.OnToggleProfile)
|
||||
{
|
||||
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile)
|
||||
{
|
||||
ShowInHelp = false,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
|
||||
{
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile)
|
||||
{
|
||||
ShowInHelp = true,
|
||||
});
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
private enum ProfileOp
|
||||
{
|
||||
Enable,
|
||||
Disable,
|
||||
Toggle,
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IInternalDisposableService.DisposeService()
|
||||
{
|
||||
this.cmd.RemoveHandler(CommandEnable);
|
||||
this.cmd.RemoveHandler(CommandDisable);
|
||||
this.cmd.RemoveHandler(CommandToggle);
|
||||
this.cmd.RemoveHandler(LegacyCommandEnable);
|
||||
this.cmd.RemoveHandler(LegacyCommandDisable);
|
||||
this.cmd.RemoveHandler(LegacyCommandToggle);
|
||||
|
||||
this.framework.Update += this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
private void FrameworkOnUpdate(IFramework framework1)
|
||||
{
|
||||
if (this.profileManager.IsBusy)
|
||||
return;
|
||||
|
||||
if (this.queue.Count > 0)
|
||||
{
|
||||
var op = this.queue[0];
|
||||
this.queue.RemoveAt(0);
|
||||
|
||||
var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1);
|
||||
if (profile == null || profile.IsDefaultProfile)
|
||||
return;
|
||||
|
||||
switch (op.Item2)
|
||||
{
|
||||
case ProfileOp.Enable:
|
||||
if (!profile.IsEnabled)
|
||||
Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult();
|
||||
break;
|
||||
case ProfileOp.Disable:
|
||||
if (profile.IsEnabled)
|
||||
Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult();
|
||||
break;
|
||||
case ProfileOp.Toggle:
|
||||
Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
if (profile.IsEnabled)
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
|
||||
}
|
||||
|
||||
Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
|
||||
{
|
||||
if (!t.IsCompletedSuccessfully && t.Exception != null)
|
||||
{
|
||||
Log.Error(t.Exception, "Could not apply profiles through commands");
|
||||
this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors."));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnableProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
|
||||
this.queue.Add((name, ProfileOp.Enable));
|
||||
}
|
||||
|
||||
private void OnDisableProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
|
||||
this.queue.Add((name, ProfileOp.Disable));
|
||||
}
|
||||
|
||||
private void OnToggleProfile(string command, string arguments)
|
||||
{
|
||||
var name = this.ValidateName(arguments);
|
||||
if (name == null)
|
||||
return;
|
||||
|
||||
this.queue.Add((name, ProfileOp.Toggle));
|
||||
}
|
||||
|
||||
private string? ValidateName(string arguments)
|
||||
{
|
||||
var name = arguments.Replace("\"", string.Empty);
|
||||
if (this.profileManager.Profiles.All(x => x.Name != name))
|
||||
{
|
||||
this.chat.PrintError($"No collection like \"{name}\".");
|
||||
return null;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
#pragma warning disable SA1401
|
||||
protected LocalPluginManifest manifest;
|
||||
#pragma warning restore SA1401
|
||||
|
||||
|
||||
private static readonly ModuleLog Log = new("LOCALPLUGIN");
|
||||
|
||||
private readonly FileInfo manifestFile;
|
||||
|
|
@ -281,7 +281,7 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
case PluginState.Unloaded:
|
||||
if (this.instance is not null)
|
||||
{
|
||||
throw new InvalidPluginOperationException(
|
||||
throw new InternalPluginStateException(
|
||||
"Plugin should have been unloaded but instance is not cleared");
|
||||
}
|
||||
|
||||
|
|
@ -314,7 +314,7 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
|
||||
this.State = PluginState.Loading;
|
||||
Log.Information($"Loading {this.DllFile.Name}");
|
||||
|
||||
|
||||
this.EnsureLoader();
|
||||
|
||||
if (this.DllFile.DirectoryName != null &&
|
||||
|
|
@ -382,10 +382,6 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// Update the location for the Location and CodeBase patches
|
||||
// NET8 CHORE
|
||||
// PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
|
||||
|
||||
this.dalamudInterface = new(this, reason);
|
||||
|
||||
this.serviceScope = ioc.GetScope();
|
||||
|
|
@ -413,9 +409,11 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.State = PluginState.LoadError;
|
||||
// These are "user errors", we don't want to mark the plugin as failed
|
||||
if (ex is not InvalidPluginOperationException)
|
||||
this.State = PluginState.LoadError;
|
||||
|
||||
// If a precondition fails, don't record it as an error, as it isn't really.
|
||||
// If a precondition fails, don't record it as an error, as it isn't really.
|
||||
if (ex is PluginPreconditionFailedException)
|
||||
Log.Warning(ex.Message);
|
||||
else
|
||||
|
|
@ -476,7 +474,10 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.State = PluginState.UnloadError;
|
||||
// These are "user errors", we don't want to mark the plugin as failed
|
||||
if (ex is not InvalidPluginOperationException)
|
||||
this.State = PluginState.UnloadError;
|
||||
|
||||
Log.Error(ex, "Error while unloading {PluginName}", this.InternalName);
|
||||
|
||||
throw;
|
||||
|
|
@ -509,9 +510,6 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
var startInfo = Service<Dalamud>.Get().StartInfo;
|
||||
var manager = Service<PluginManager>.Get();
|
||||
|
||||
if (startInfo.NoLoadPlugins)
|
||||
return false;
|
||||
|
||||
if (startInfo.NoLoadThirdPartyPlugins && this.manifest.IsThirdParty)
|
||||
return false;
|
||||
|
||||
|
|
@ -555,7 +553,7 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
/// </summary>
|
||||
/// <param name="reason">Why it should be saved.</param>
|
||||
protected void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called before a plugin is reloaded.
|
||||
/// </summary>
|
||||
|
|
@ -594,7 +592,7 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
// but plugins may load other versions of assemblies that Dalamud depends on.
|
||||
config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false));
|
||||
config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false));
|
||||
|
||||
|
||||
// Pin Lumina since we expose it as an API surface. Before anyone removes this again, please see #1598.
|
||||
// Changes to Lumina should be upstreamed if feasible, and if there is a desire to re-add unpinned Lumina we
|
||||
// will need to put this behind some kind of feature flag somewhere.
|
||||
|
|
@ -606,7 +604,7 @@ internal class LocalPlugin : IAsyncDisposable
|
|||
{
|
||||
if (this.loader != null)
|
||||
return;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ namespace Dalamud.Utility;
|
|||
/// Utility class for marking something to be changed for API 11, for ease of lookup.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
internal sealed class Api11ToDoAttribute : Attribute
|
||||
internal sealed class Api12ToDoAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks that this should be made internal.
|
||||
|
|
@ -12,11 +12,11 @@ internal sealed class Api11ToDoAttribute : Attribute
|
|||
public const string MakeInternal = "Make internal.";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Api11ToDoAttribute"/> class.
|
||||
/// Initializes a new instance of the <see cref="Api12ToDoAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="what">The explanation.</param>
|
||||
/// <param name="what2">The explanation 2.</param>
|
||||
public Api11ToDoAttribute(string what, string what2 = "")
|
||||
public Api12ToDoAttribute(string what, string what2 = "")
|
||||
{
|
||||
_ = what;
|
||||
_ = what2;
|
||||
52
Dalamud/Utility/CultureFixes.cs
Normal file
52
Dalamud/Utility/CultureFixes.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Class containing fixes for culture-specific issues.
|
||||
/// </summary>
|
||||
internal static class CultureFixes
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply all fixes.
|
||||
/// </summary>
|
||||
public static void Apply()
|
||||
{
|
||||
PatchFrenchNumberSeparator();
|
||||
}
|
||||
|
||||
private static void PatchFrenchNumberSeparator()
|
||||
{
|
||||
// Reset formatting specifier for the "digit grouping symbol" to an empty string
|
||||
// for cultures that use a narrow no-break space (U+202F).
|
||||
// This glyph is not present in any game fonts and not in the range for our Noto
|
||||
// so it will be rendered as a geta (=) instead. That's a hack, but it works and
|
||||
// doesn't look as weird.
|
||||
CultureInfo PatchCulture(CultureInfo info)
|
||||
{
|
||||
var newCulture = (CultureInfo)info.Clone();
|
||||
|
||||
const string invalidGroupSeparator = "\u202F";
|
||||
const string replacedGroupSeparator = " ";
|
||||
|
||||
if (info.NumberFormat.NumberGroupSeparator == invalidGroupSeparator)
|
||||
newCulture.NumberFormat.NumberGroupSeparator = replacedGroupSeparator;
|
||||
|
||||
if (info.NumberFormat.NumberDecimalSeparator == invalidGroupSeparator)
|
||||
newCulture.NumberFormat.NumberDecimalSeparator = replacedGroupSeparator;
|
||||
|
||||
if (info.NumberFormat.CurrencyGroupSeparator == invalidGroupSeparator)
|
||||
newCulture.NumberFormat.CurrencyGroupSeparator = replacedGroupSeparator;
|
||||
|
||||
if (info.NumberFormat.CurrencyDecimalSeparator == invalidGroupSeparator)
|
||||
newCulture.NumberFormat.CurrencyDecimalSeparator = replacedGroupSeparator;
|
||||
|
||||
return newCulture;
|
||||
}
|
||||
|
||||
CultureInfo.CurrentCulture = PatchCulture(CultureInfo.CurrentCulture);
|
||||
CultureInfo.CurrentUICulture = PatchCulture(CultureInfo.CurrentUICulture);
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CurrentCulture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CurrentUICulture;
|
||||
}
|
||||
}
|
||||
32
Dalamud/Utility/DiagnosticUtil.cs
Normal file
32
Dalamud/Utility/DiagnosticUtil.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// A set of utilities for diagnostics.
|
||||
/// </summary>
|
||||
public static class DiagnosticUtil
|
||||
{
|
||||
private static readonly string[] IgnoredNamespaces = [
|
||||
nameof(System),
|
||||
nameof(ImGuiNET.ImGuiNative)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a stack trace that filters out irrelevant frames.
|
||||
/// </summary>
|
||||
/// <param name="source">The source stacktrace to filter.</param>
|
||||
/// <returns>Returns a stack trace with "extra" frames removed.</returns>
|
||||
public static StackTrace GetUsefulTrace(StackTrace source)
|
||||
{
|
||||
var frames = source.GetFrames().SkipWhile(
|
||||
f =>
|
||||
{
|
||||
var frameNs = f.GetMethod()?.DeclaringType?.Namespace;
|
||||
return frameNs == null || IgnoredNamespaces.Any(i => frameNs.StartsWith(i, true, null));
|
||||
});
|
||||
|
||||
return new StackTrace(frames);
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ public static class SeStringExtensions
|
|||
/// <param name="macroString">Macro string in UTF-8 to compile and append to <paramref name="ssb"/>.</param>
|
||||
/// <returns><c>this</c> for method chaining.</returns>
|
||||
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)]
|
||||
[Api11ToDo("Remove")]
|
||||
[Api12ToDo("Remove")]
|
||||
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString) =>
|
||||
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ public static class SeStringExtensions
|
|||
/// <param name="macroString">Macro string in UTF-16 to compile and append to <paramref name="ssb"/>.</param>
|
||||
/// <returns><c>this</c> for method chaining.</returns>
|
||||
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.", true)]
|
||||
[Api11ToDo("Remove")]
|
||||
[Api12ToDo("Remove")]
|
||||
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString) =>
|
||||
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Support;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.Sheets;
|
||||
|
|
@ -28,8 +29,6 @@ using Windows.Win32.Storage.FileSystem;
|
|||
using Windows.Win32.System.Memory;
|
||||
using Windows.Win32.System.Ole;
|
||||
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
|
||||
using Win32_PInvoke = Windows.Win32.PInvoke;
|
||||
|
|
@ -66,7 +65,6 @@ public static class Util
|
|||
private static readonly Type GenericSpanType = typeof(Span<>);
|
||||
private static string? scmVersionInternal;
|
||||
private static string? gitHashInternal;
|
||||
private static int? gitCommitCountInternal;
|
||||
private static string? gitHashClientStructsInternal;
|
||||
|
||||
private static ulong moduleStartAddr;
|
||||
|
|
@ -78,58 +76,6 @@ public static class Util
|
|||
public static string AssemblyVersion { get; } =
|
||||
Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Check two byte arrays for equality.
|
||||
/// </summary>
|
||||
/// <param name="a1">The first byte array.</param>
|
||||
/// <param name="a2">The second byte array.</param>
|
||||
/// <returns>Whether or not the byte arrays are equal.</returns>
|
||||
public static unsafe bool FastByteArrayCompare(byte[]? a1, byte[]? a2)
|
||||
{
|
||||
// Copyright (c) 2008-2013 Hafthor Stefansson
|
||||
// Distributed under the MIT/X11 software license
|
||||
// Ref: http://www.opensource.org/licenses/mit-license.php.
|
||||
// https://stackoverflow.com/a/8808245
|
||||
|
||||
if (a1 == a2) return true;
|
||||
if (a1 == null || a2 == null || a1.Length != a2.Length)
|
||||
return false;
|
||||
fixed (byte* p1 = a1, p2 = a2)
|
||||
{
|
||||
byte* x1 = p1, x2 = p2;
|
||||
var l = a1.Length;
|
||||
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
|
||||
{
|
||||
if (*((long*)x1) != *((long*)x2))
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((l & 4) != 0)
|
||||
{
|
||||
if (*((int*)x1) != *((int*)x2))
|
||||
return false;
|
||||
x1 += 4;
|
||||
x2 += 4;
|
||||
}
|
||||
|
||||
if ((l & 2) != 0)
|
||||
{
|
||||
if (*((short*)x1) != *((short*)x2))
|
||||
return false;
|
||||
x1 += 2;
|
||||
x2 += 2;
|
||||
}
|
||||
|
||||
if ((l & 1) != 0)
|
||||
{
|
||||
if (*((byte*)x1) != *((byte*)x2))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return
|
||||
/// the <c>git describe</c> output for this build, which will be a raw version if this is a stable build or an
|
||||
|
|
@ -139,11 +85,11 @@ public static class Util
|
|||
public static string GetScmVersion()
|
||||
{
|
||||
if (scmVersionInternal != null) return scmVersionInternal;
|
||||
|
||||
|
||||
var asm = typeof(Util).Assembly;
|
||||
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
|
||||
|
||||
return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value
|
||||
return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value
|
||||
?? asm.GetName().Version!.ToString();
|
||||
}
|
||||
|
||||
|
|
@ -853,7 +799,7 @@ public static class Util
|
|||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Print formatted IGameObject Information to ImGui.
|
||||
/// </summary>
|
||||
|
|
@ -1051,7 +997,8 @@ public static class Util
|
|||
}
|
||||
}
|
||||
|
||||
private static unsafe void ShowSpanEntryPrivate<T>(ulong addr, IList<string> path, int offset, Span<T> spanobj) {
|
||||
private static unsafe void ShowSpanEntryPrivate<T>(ulong addr, IList<string> path, int offset, Span<T> spanobj)
|
||||
{
|
||||
const int batchSize = 20;
|
||||
if (spanobj.Length > batchSize)
|
||||
{
|
||||
|
|
@ -1221,6 +1168,7 @@ public static class Util
|
|||
ImGui.TextDisabled($"[0x{offset.Value:X}]");
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{f.FieldType.Name}");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue