merge from master

This commit is contained in:
goaaats 2025-03-08 16:10:25 +01:00
commit 6604678050
82 changed files with 2683 additions and 1371 deletions

View file

@ -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);
}
}

View file

@ -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" />

View file

@ -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;

View file

@ -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)
{

View file

@ -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()
{

View file

@ -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;
}

View file

@ -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/>

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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";

View file

@ -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);

View file

@ -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; } = [];

View file

@ -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

View file

@ -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;
}
}

View file

@ -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>

View 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
}
}

View file

@ -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;

View file

@ -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
{

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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; }
}
}

View file

@ -60,6 +60,7 @@ internal class DataWindow : Window, IDisposable
new ToastWidget(),
new UiColorWidget(),
new UldWidget(),
new VfsWidget(),
};
private readonly IOrderedEnumerable<IDataWindowWidget> orderedModules;

View file

@ -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());

View file

@ -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;

View file

@ -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);

View file

@ -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;

View 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);
}
}
}

View file

@ -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.");

View file

@ -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.");

View file

@ -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();
}
}

View file

@ -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(),

View file

@ -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."),

View file

@ -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";
}

View file

@ -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;

View file

@ -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>

View file

@ -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)

View 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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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));
}

View file

@ -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()
{

View file

@ -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));
}

View file

@ -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)
{
}
}

View file

@ -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);
}
}
*/

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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;

View 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;
}
}

View 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);
}
}

View file

@ -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 });

View file

@ -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}");
}