From f7ef68d65e215afba1cf24cbfba9e0bb21e595c6 Mon Sep 17 00:00:00 2001
From: goat
Date: Tue, 17 Dec 2024 21:50:03 +0100
Subject: [PATCH 01/11] build: 11.0.3.0
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 39bc837626..75afaae1b4 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -9,7 +9,7 @@
- 11.0.2.0
+ 11.0.3.0
XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
From 2e6cb6ef006da80a1d053d7693530444463d6d34 Mon Sep 17 00:00:00 2001
From: Infi
Date: Thu, 19 Dec 2024 22:19:50 +0100
Subject: [PATCH 02/11] - Add chat notification back to AutoUpdate (#2146)
- ImRaii UI elements in AutoUpdate tab
- Fix scoping in IconButton
---
.../Internal/DalamudConfiguration.cs | 19 ++-
.../Components/ImGuiComponents.IconButton.cs | 13 +-
.../DalamudComponents.PluginPicker.cs | 16 +-
.../Settings/Tabs/SettingsTabAutoUpdate.cs | 76 +++++----
.../Internal/AutoUpdate/AutoUpdateManager.cs | 156 ++++++++++++------
5 files changed, 166 insertions(+), 114 deletions(-)
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 5c2378f689..4df38d6dfc 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -249,7 +249,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
///
public bool IsDocking { get; set; }
-
+
///
/// 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.
@@ -484,10 +484,15 @@ public string EffectiveLanguage
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
///
- /// 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.
///
public bool CheckPeriodicallyForUpdates { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether users should be notified about updates in chat.
+ ///
+ public bool SendUpdateNotificationToChat { get; set; } = false;
+
///
/// Load a configuration from the provided path.
///
@@ -504,7 +509,7 @@ public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
{
deserialized =
JsonConvert.DeserializeObject(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 +535,7 @@ public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
{
Log.Error(e, "Failed to set defaults for DalamudConfiguration");
}
-
+
return deserialized;
}
@@ -549,7 +554,7 @@ public void ForceSave()
{
this.Save();
}
-
+
///
void IInternalDisposableService.DisposeService()
{
@@ -595,14 +600,14 @@ private void SetDefaults()
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();
diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
index d2b1b4a36d..10f177590d 100644
--- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
+++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
@@ -272,15 +272,14 @@ public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4
/// Width.
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;
}
}
diff --git a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs
index f0ce6bc824..3d31bbda69 100644
--- a/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs
+++ b/Dalamud/Interface/Internal/DesignSystem/DalamudComponents.PluginPicker.cs
@@ -32,19 +32,21 @@ internal static uint DrawPluginPicker(string id, ref string pickerSearch, Action
var pm = Service.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 uint DrawPluginPicker(string id, ref string pickerSearch, Action
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();
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs
index 77c79c96d4..9356131ad3 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs
@@ -23,10 +23,11 @@ public class SettingsTabAutoUpdates : SettingsTab
{
private AutoUpdateBehavior behavior;
private bool checkPeriodically;
+ private bool chatNotification;
private string pickerSearch = string.Empty;
private List autoUpdatePreferences = [];
-
- public override SettingsEntry[] Entries { get; } = Array.Empty();
+
+ public override SettingsEntry[] Entries { get; } = [];
public override string Title => Loc.Localize("DalamudSettingsAutoUpdates", "Auto-Updates");
@@ -36,15 +37,15 @@ public override void Draw()
"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 override void Draw()
"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 override void Draw()
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.Get();
var windowSize = ImGui.GetWindowSize();
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
Guid? wantRemovePluginGuid = null;
-
+
foreach (var preference in this.autoUpdatePreferences)
{
var pmPlugin = Service.Get().InstalledPlugins
@@ -120,11 +122,12 @@ public override void Draw()
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 override void Draw()
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
ImGui.TextUnformatted(text);
-
+
ImGui.SetCursorPos(before);
}
@@ -166,19 +169,18 @@ string OptKindToString(AutoUpdatePreference.OptKind kind)
}
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())
+ if (combo.Success)
{
- if (ImGui.Selectable(OptKindToString(kind)))
+ foreach (var kind in Enum.GetValues())
{
- preference.Kind = kind;
+ if (ImGui.Selectable(OptKindToString(kind)))
+ {
+ preference.Kind = kind;
+ }
}
}
-
- ImGui.EndCombo();
}
ImGui.SameLine();
@@ -193,7 +195,7 @@ string OptKindToString(AutoUpdatePreference.OptKind kind)
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 @@ void OnPluginPicked(LocalPlugin plugin)
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 override void Load()
var configuration = Service.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.Get();
-
+
configuration.AutoUpdateBehavior = this.behavior;
+ configuration.SendUpdateNotificationToChat = this.chatNotification;
configuration.CheckPeriodicallyForUpdates = this.checkPeriodically;
configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences;
-
+
base.Save();
}
}
diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
index c25ec4ee41..99850ddb49 100644
--- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
+++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs
@@ -9,6 +9,10 @@
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");
-
+
///
/// Time we should wait after login to update.
///
private static readonly TimeSpan UpdateTimeAfterLogin = TimeSpan.FromSeconds(20);
-
+
///
/// Time we should wait between scheduled update checks.
///
private static readonly TimeSpan TimeBetweenUpdateChecks = TimeSpan.FromHours(2);
-
+
///
/// 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.Get();
-
+
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration config = Service.Get();
-
+
[ServiceManager.ServiceDependency]
private readonly NotificationManager notificationManager = Service.Get();
-
+
[ServiceManager.ServiceDependency]
private readonly DalamudInterface dalamudInterface = Service.Get();
-
+
private readonly IConsoleVariable isDryRun;
-
+
private DateTime? loginTime;
private DateTime? nextUpdateCheckTime;
private DateTime? unblockedSince;
-
+
private bool hasStartedInitialUpdateThisSession;
private IActiveNotification? updateNotification;
-
+
private Task? autoUpdateTask;
-
+
+ private readonly Task openInstallerWindowLink;
+
///
/// Initializes a new instance of the class.
///
@@ -92,7 +98,18 @@ public AutoUpdateManager(ConsoleManager console)
t.Result.Logout += (int type, int code) => this.OnLogout();
});
Service.GetAsync().ContinueWith(t => { t.Result.Update += this.OnUpdate; });
-
+
+ this.openInstallerWindowLink =
+ Service.GetAsync().ContinueWith(
+ chatGuiTask => chatGuiTask.Result.AddChatLinkHandler(
+ "Dalamud",
+ 1001,
+ (_, _) =>
+ {
+ Service.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 +123,36 @@ public AutoUpdateManager(ConsoleManager console)
return true;
});
}
-
+
private enum UpdateListingRestriction
{
Unrestricted,
AllowNone,
AllowMainRepo,
}
-
+
///
/// Gets a value indicating whether or not auto-updates have already completed this session.
///
public bool IsAutoUpdateComplete { get; private set; }
-
+
///
/// Gets the time of the next scheduled update check.
///
public DateTime? NextUpdateCheckTime => this.nextUpdateCheckTime;
-
+
///
/// Gets the time the auto-update was unblocked.
///
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 +161,7 @@ private static UpdateListingRestriction DecideUpdateListingRestriction(AutoUpdat
_ => throw new ArgumentOutOfRangeException(nameof(behavior), behavior, null),
};
}
-
+
private static void DrawOpenInstallerNotificationButton(bool primary, PluginInstallerOpenKind kind, IActiveNotification notification)
{
if (primary ?
@@ -179,7 +196,7 @@ private void OnUpdate(IFramework framework)
this.updateNotification = null;
}
}
-
+
// If we're blocked, we don't do anything.
if (!isUnblocked)
return;
@@ -199,16 +216,16 @@ private void OnUpdate(IFramework framework)
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 +258,7 @@ private void OnUpdate(IFramework framework)
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 +270,7 @@ private void OnUpdate(IFramework framework)
{
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 +281,13 @@ private IActiveNotification GetBaseNotification(Notification notification)
{
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 +309,7 @@ private void KickOffAutoUpdates(ICollection updatablePlug
{
Log.Warning("Auto-update task was canceled");
}
-
+
this.autoUpdateTask = null;
this.IsAutoUpdateComplete = true;
});
@@ -321,20 +339,20 @@ private async Task RunAutoUpdates(ICollection updatablePl
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 +360,7 @@ private async Task RunAutoUpdates(ICollection updatablePl
// 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 +372,11 @@ private async Task RunAutoUpdates(ICollection updatablePl
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 +385,7 @@ private void NotifyUpdatesAreAvailable(ICollection updata
{
if (updatablePlugins.Count == 0)
return;
-
+
var notification = this.GetBaseNotification(new Notification
{
Title = Locs.NotificationTitleUpdatesAvailable,
@@ -400,16 +418,44 @@ void DrawNotificationContent(INotificationDrawArgs args)
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.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
+ {
+ new TextPayload(Locs.NotificationContentUpdatesAvailableMinimized(updatablePlugins.Count)),
+ new TextPayload(" ["),
+ new UIForegroundPayload(500),
+ this.openInstallerWindowLink.Result,
+ new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
+ RawPayload.LinkTerminator,
+ new UIForegroundPayload(0),
+ new TextPayload("]"),
+ }),
+
+ Type = XivChatType.Urgent,
+ });
}
-
+
private List 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 +469,14 @@ private List GetAvailablePluginUpdates(UpdateListingRestr
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 +488,7 @@ private void OnLogin()
{
this.loginTime = DateTime.Now;
}
-
+
private void OnLogout()
{
this.loginTime = null;
@@ -452,7 +498,7 @@ private bool CanUpdateOrNag()
{
var condition = Service.Get();
return this.IsPluginManagerReady() &&
- !this.dalamudInterface.IsPluginInstallerOpen &&
+ !this.dalamudInterface.IsPluginInstallerOpen &&
condition.OnlyAny(ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
@@ -469,21 +515,21 @@ private static class Locs
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 updatablePlugins)
@@ -497,20 +543,20 @@ public static string NotificationContentUpdatesAvailable(ICollection 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 failedPlugins)
=> string.Format(Loc.Localize("AutoUpdateFailedPlugins", "Failed plugin(s): {0}"), string.Join(", ", failedPlugins));
}
From 87d069267bf848424530900add96f37dbab7c4d4 Mon Sep 17 00:00:00 2001
From: bleatbot <106497096+bleatbot@users.noreply.github.com>
Date: Fri, 20 Dec 2024 15:33:54 +0100
Subject: [PATCH 03/11] Update ClientStructs (#2148)
Co-authored-by: github-actions[bot]
---
lib/FFXIVClientStructs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index df03181ccb..cc98a564d0 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit df03181ccbbbfead3db116b59359dae4a31cb07d
+Subproject commit cc98a564d0787813d4be082bf75f5bb98e0ed12f
From 2d6689b9d3d390abaaf9a9dd24d2a9eb4fdcd666 Mon Sep 17 00:00:00 2001
From: goat
Date: Sun, 22 Dec 2024 19:14:18 +0100
Subject: [PATCH 04/11] move lumina version out of csproj
---
Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 6 +++---
Dalamud/Dalamud.csproj | 8 ++++----
Directory.Build.props | 9 ++++++++-
3 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
index b85607f0fe..ca78f09add 100644
--- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
+++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
@@ -27,9 +27,9 @@
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 75afaae1b4..e1bb20db61 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -9,8 +9,8 @@
- 11.0.3.0
XIV Launcher addon framework
+ 11.0.3.0
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
@@ -71,14 +71,14 @@
-
-
+
+
all
-
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 0c5af2e375..6ee6f4b11a 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,5 +1,12 @@
-
+
+
+ 5.6.0
+ 7.1.3
+ 13.0.3
+
+
+
From c647d07d94f7f6e2f192d22d3900f18e583df529 Mon Sep 17 00:00:00 2001
From: goat
Date: Sun, 22 Dec 2024 19:19:56 +0100
Subject: [PATCH 05/11] build: 11.0.4.0
---
Dalamud/Dalamud.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index e1bb20db61..a6ac07227c 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -10,7 +10,7 @@
XIV Launcher addon framework
- 11.0.3.0
+ 11.0.4.0
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
From bc21621d9ac1549a12e02854797be7a4234cd400 Mon Sep 17 00:00:00 2001
From: goat
Date: Mon, 23 Dec 2024 16:50:06 +0100
Subject: [PATCH 06/11] add release script
---
release.ps1 | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
create mode 100644 release.ps1
diff --git a/release.ps1 b/release.ps1
new file mode 100644
index 0000000000..8863a12149
--- /dev/null
+++ b/release.ps1
@@ -0,0 +1,31 @@
+param(
+ [string]$VersionString
+)
+
+if (-not $VersionString) {
+ Write-Error "Version string is required as the first argument."
+ exit 1
+}
+
+$csprojPath = "Dalamud/Dalamud.csproj"
+
+if (-not (Test-Path $csprojPath)) {
+ Write-Error "Cannot find Dalamud.csproj at the specified path."
+ exit 1
+}
+
+# Update the version in the csproj file
+(Get-Content $csprojPath) -replace '.*?', "$VersionString" | Set-Content $csprojPath
+
+# Commit the change
+git add $csprojPath
+git commit -m "build: $VersionString"
+
+# Get the current branch
+$currentBranch = git rev-parse --abbrev-ref HEAD
+
+# Create a tag
+git tag -a -m "v$VersionString" $VersionString
+
+# Push atomically
+git push origin $currentBranch $VersionString
\ No newline at end of file
From 8572ed8b1ef34c6fb0f961e7a073b6ed0d9c001b Mon Sep 17 00:00:00 2001
From: goat
Date: Mon, 23 Dec 2024 17:05:35 +0100
Subject: [PATCH 07/11] ProfileCommandHandler -> PluginManagementCommandHandler
---
.../Windows/PluginInstaller/ProfileManagerWidget.cs | 6 +++---
...eCommandHandler.cs => PluginManagementCommandHandler.cs} | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
rename Dalamud/Plugin/Internal/Profiles/{ProfileCommandHandler.cs => PluginManagementCommandHandler.cs} (95%)
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
index 95315dbd3f..ee93d20420 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
@@ -625,13 +625,13 @@ private static class Locs
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.CommandEnable);
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.CommandDisable);
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.CommandToggle);
public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");
diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs
similarity index 95%
rename from Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
rename to Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs
index 7b7b4cfd0c..d219b659ba 100644
--- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
+++ b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs
@@ -16,7 +16,7 @@ namespace Dalamud.Plugin.Internal.Profiles;
/// Service responsible for profile-related chat commands.
///
[ServiceManager.EarlyLoadedService]
-internal class ProfileCommandHandler : IInternalDisposableService
+internal class PluginManagementCommandHandler : IInternalDisposableService
{
#pragma warning disable SA1600
public const string CommandEnable = "/xlenablecollection";
@@ -36,14 +36,14 @@ internal class ProfileCommandHandler : IInternalDisposableService
private List<(string, ProfileOp)> queue = new();
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// Command handler.
/// Profile manager.
/// Chat handler.
/// Framework.
[ServiceManager.ServiceConstructor]
- public ProfileCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
+ public PluginManagementCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
{
this.cmd = cmd;
this.profileManager = profileManager;
From 8276c19c6f016d58da414916074c4a5a764ad26e Mon Sep 17 00:00:00 2001
From: goat
Date: Mon, 23 Dec 2024 20:55:57 +0100
Subject: [PATCH 08/11] add plugin enable/disable/toggle commands
---
.../PluginInstaller/ProfileManagerWidget.cs | 6 +-
.../PluginManagementCommandHandler.cs | 316 ++++++++++++++----
2 files changed, 253 insertions(+), 69 deletions(-)
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
index ee93d20420..ddb89d38c0 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
@@ -625,13 +625,13 @@ private static class Locs
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(PluginManagementCommandHandler.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(PluginManagementCommandHandler.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(PluginManagementCommandHandler.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.");
diff --git a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs
index d219b659ba..ad5aad286d 100644
--- a/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs
+++ b/Dalamud/Plugin/Internal/Profiles/PluginManagementCommandHandler.cs
@@ -6,6 +6,7 @@
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;
@@ -19,50 +20,62 @@ namespace Dalamud.Plugin.Internal.Profiles;
internal class PluginManagementCommandHandler : IInternalDisposableService
{
#pragma warning disable SA1600
- public const string CommandEnable = "/xlenablecollection";
- public const string CommandDisable = "/xldisablecollection";
- public const string CommandToggle = "/xltogglecollection";
+ 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 = CommandEnable.Replace("collection", "profile");
- private static readonly string LegacyCommandDisable = CommandDisable.Replace("collection", "profile");
- private static readonly string LegacyCommandToggle = CommandToggle.Replace("collection", "profile");
+ 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<(string, ProfileOp)> queue = new();
-
+ private List<(Target Target, PluginCommandOperation Operation)> commandQueue = new();
+
///
/// Initializes a new instance of the class.
///
/// Command handler.
/// Profile manager.
+ /// Plugin manager.
/// Chat handler.
/// Framework.
[ServiceManager.ServiceConstructor]
- public PluginManagementCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
+ 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(CommandEnable, new CommandInfo(this.OnEnableProfile)
+ this.cmd.AddHandler(CommandEnableProfile, 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)
+ this.cmd.AddHandler(CommandDisableProfile, 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)
+ this.cmd.AddHandler(CommandToggleProfile, new CommandInfo(this.OnToggleProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true,
@@ -75,18 +88,36 @@ public PluginManagementCommandHandler(CommandManager cmd, ProfileManager profile
this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
{
- ShowInHelp = true,
+ 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 ProfileOp
+ private enum PluginCommandOperation
{
Enable,
Disable,
@@ -96,109 +127,262 @@ private enum ProfileOp
///
void IInternalDisposableService.DisposeService()
{
- this.cmd.RemoveHandler(CommandEnable);
- this.cmd.RemoveHandler(CommandDisable);
- this.cmd.RemoveHandler(CommandToggle);
+ 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 FrameworkOnUpdate(IFramework framework1)
+
+ private void HandleProfileOperation(string profileName, PluginCommandOperation operation)
{
- if (this.profileManager.IsBusy)
+ var profile = this.profileManager.Profiles.FirstOrDefault(
+ x => x.Name == profileName);
+ if (profile == null || profile.IsDefaultProfile)
return;
- if (this.queue.Count > 0)
+ switch (operation)
{
- var op = this.queue[0];
- this.queue.RemoveAt(0);
+ 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);
+ }
- var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1);
- if (profile == null || profile.IsDefaultProfile)
- return;
+ this.chat.Print(
+ profile.IsEnabled
+ ? Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name)
+ : Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
- switch (op.Item2)
+ Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
+ {
+ if (!t.IsCompletedSuccessfully && t.Exception != null)
{
- 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();
+ 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."));
}
-
- if (profile.IsEnabled)
+ else
{
- this.chat.Print(Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name));
+ this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
}
- else
+ });
+ }
+
+ 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)
{
- this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
+ 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);
+ }
- Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t =>
+ 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)
{
- 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."));
- }
- });
+ 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.ValidateName(arguments);
+ var name = this.ValidateProfileName(arguments);
if (name == null)
return;
- this.queue = this.queue.Where(x => x.Item1 != name).ToList();
- this.queue.Add((name, ProfileOp.Enable));
+ 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.ValidateName(arguments);
+ var name = this.ValidateProfileName(arguments);
if (name == null)
return;
- this.queue = this.queue.Where(x => x.Item1 != name).ToList();
- this.queue.Add((name, ProfileOp.Disable));
+ 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.ValidateName(arguments);
+ var name = this.ValidateProfileName(arguments);
if (name == null)
return;
- this.queue.Add((name, ProfileOp.Toggle));
+ 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;
- private string? ValidateName(string arguments)
+ 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($"No collection like \"{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;
}
From a1ae33bfee3f5a8f61c7617d59bdafa4279887ef Mon Sep 17 00:00:00 2001
From: goat
Date: Mon, 23 Dec 2024 21:03:31 +0100
Subject: [PATCH 09/11] don't set plugin to fail state if we threw
InvalidPluginOperationException These are supposed to indicate to the user
that they called a function at the wrong point in time. We don't want to
actually mutate the state in that case.
---
.../Exceptions/InternalPluginStateException.cs | 16 ++++++++++++++++
Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 11 ++++++++---
2 files changed, 24 insertions(+), 3 deletions(-)
create mode 100644 Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs
diff --git a/Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs b/Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs
new file mode 100644
index 0000000000..03e37afcfa
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/InternalPluginStateException.cs
@@ -0,0 +1,16 @@
+namespace Dalamud.Plugin.Internal.Exceptions;
+
+///
+/// An exception to be thrown when policy blocks a plugin from loading.
+///
+internal class InternalPluginStateException : InvalidPluginOperationException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message to associate with this exception.
+ public InternalPluginStateException(string message)
+ : base(message)
+ {
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
index 59f6b23c19..ed3a94994f 100644
--- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs
@@ -281,7 +281,7 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
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");
}
@@ -413,7 +413,9 @@ public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
}
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 (ex is PluginPreconditionFailedException)
@@ -476,7 +478,10 @@ public async Task UnloadAsync(PluginLoaderDisposalMode disposalMode = PluginLoad
}
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;
From 667ae312c56b217f8245f1683de7ca76f3f5c8dd Mon Sep 17 00:00:00 2001
From: goat <16760685+goaaats@users.noreply.github.com>
Date: Mon, 23 Dec 2024 21:15:03 +0100
Subject: [PATCH 10/11] some readme housekeeping
---
README.md | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 8a925f1c61..bbf9ee1ce3 100644
--- a/README.md
+++ b/README.md
@@ -4,13 +4,18 @@
-Dalamud is a plugin development framework for FINAL FANTASY XIV that provides access to game data and native interoperability with the game itself to add functionality and quality-of-life.
+Dalamud is a plugin development framework for FFXIV that provides access to game data and native interoperability with the game itself to add functionality and quality-of-life.
-It is meant to be used in conjunction with [FFXIVQuickLauncher](https://github.com/goatcorp/FFXIVQuickLauncher), which manages and launches Dalamud for you. __It is generally not recommended for users to try to run Dalamud manually as there are multiple dependencies and assumed folder paths.__
+It is meant to be used in conjunction with [XIVLauncher](https://github.com/goatcorp/FFXIVQuickLauncher), which manages and launches Dalamud for you. __It is generally not recommended for end users to try to run Dalamud manually as XIVLauncher manages multiple required dependencies.__
## Hold Up!
+
If you are just trying to **use** Dalamud, you don't need to do anything on this page - please [download XIVLauncher](https://goatcorp.github.io/) from its official page and follow the setup instructions.
+## Building and testing locally
+
+Please check the [docs page on building Dalamud](https://dalamud.dev/building) for more information and required dependencies.
+
## Plugin development
Dalamud features a growing API for in-game plugin development with game data and chat access and overlays.
Please see our [Developer FAQ](https://goatcorp.github.io/faq/development) and the [API documentation](https://dalamud.dev) for more details.
@@ -34,15 +39,6 @@ Dalamud can be loaded via DLL injection, or by rewriting a process' entrypoint.
| *Dalamud* (C#) | Core API, game bindings, plugin framework |
| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features |
-## Branches
-
-We are currently working from the following branches.
-
-| Name | API Level | Purpose | .NET Version | Track |
-|----------|-----------|------------------------------------------------------------|----------------------------|-------------------|
-| *master* | **9** | Current release branch | .NET 8.0.0 (November 2023) | Release & Staging |
-| *api10* | **10** | Next major version, slated for release alongside Patch 7.0 | .NET 8.0.0 (November 2023) | api10 |
-
##### Final Fantasy XIV © 2010-2021 SQUARE ENIX CO., LTD. All Rights Reserved. We are not affiliated with SQUARE ENIX CO., LTD. in any way.
From 5a2473293d28a076835d02e9ec1f446924109952 Mon Sep 17 00:00:00 2001
From: goat
Date: Mon, 23 Dec 2024 22:00:57 +0100
Subject: [PATCH 11/11] reorganize solution a bit
---
Dalamud.sln | 23 ++++++++++++++++-------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/Dalamud.sln b/Dalamud.sln
index 22cc59a8d9..5d4b0737f0 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -6,11 +6,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
+ tools\BannedSymbols.txt = tools\BannedSymbols.txt
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
- Directory.Build.props = Directory.Build.props
- tools\BannedSymbols.txt = tools\BannedSymbols.txt
tools\dalamud.ruleset = tools\dalamud.ruleset
+ Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
@@ -25,7 +25,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Da
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
EndProject
@@ -49,6 +49,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -97,6 +101,10 @@ Global
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.Build.0 = Release|Any CPU
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.ActiveCfg = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64
@@ -117,19 +125,20 @@ Global
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6AA1C3F-9470-4922-9D3F-D4549657AB22}.Release|Any CPU.Build.0 = Release|Any CPU
- {E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E0D51896-604F-4B40-8CFE-51941607B3A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E0D51896-604F-4B40-8CFE-51941607B3A1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
+ {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {E0D51896-604F-4B40-8CFE-51941607B3A1} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{3620414C-7DFC-423E-929F-310E19F5D930} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{A6AA1C3F-9470-4922-9D3F-D4549657AB22} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
EndGlobalSection