[submodule "vendor/ECommons"]
path = vendor/ECommons
url = https://github.com/NightmareXIV/ECommons.git
+[submodule "vendor/NotificationMasterAPI"]
+ path = vendor/NotificationMasterAPI
+ url = https://github.com/NightmareXIV/NotificationMasterAPI.git
<Project>
<PropertyGroup>
- <Version>3.12</Version>
+ <Version>3.13</Version>
</PropertyGroup>
</Project>
Directory.Build.targets = Directory.Build.targets
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "vendor", "vendor", "{8F5EC9D5-4CE7-433B-BB3A-782500E84DDB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NotificationMasterAPI", "vendor\NotificationMasterAPI\NotificationMasterAPI\NotificationMasterAPI.csproj", "{9BD494ED-22F2-487B-BCE1-435399A8720E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Debug|x64.Build.0 = Debug|x64
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Release|x64.ActiveCfg = Release|x64
{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Release|x64.Build.0 = Release|x64
+ {9BD494ED-22F2-487B-BCE1-435399A8720E}.Debug|x64.ActiveCfg = Debug|x64
+ {9BD494ED-22F2-487B-BCE1-435399A8720E}.Debug|x64.Build.0 = Debug|x64
+ {9BD494ED-22F2-487B-BCE1-435399A8720E}.Release|x64.ActiveCfg = Release|x64
+ {9BD494ED-22F2-487B-BCE1-435399A8720E}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B} = {8F5EC9D5-4CE7-433B-BB3A-782500E84DDB}
+ {9BD494ED-22F2-487B-BCE1-435399A8720E} = {8F5EC9D5-4CE7-433B-BB3A-782500E84DDB}
+ EndGlobalSection
EndGlobal
using Dalamud.Configuration;
+using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.ImGui;
{
public const int PluginSetupVersion = 1;
- public int Version { get; set; } = 1;
+ public int Version { get; set; } =1 ;
public int PluginSetupCompleteVersion { get; set; }
public GeneralConfiguration General { get; } = new();
+ public NotificationConfiguration Notifications { get; } = new();
public AdvancedConfiguration Advanced { get; } = new();
public WindowConfig DebugWindowConfig { get; } = new();
public WindowConfig ConfigWindowConfig { get; } = new();
public bool ConfigureTextAdvance { get; set; } = true;
}
+ internal sealed class NotificationConfiguration
+ {
+ public bool Enabled { get; set; } = true;
+ public XivChatType ChatType { get; set; } = XivChatType.Debug;
+ public bool ShowTrayMessage { get; set; }
+ public bool FlashTaskbar { get; set; }
+ }
+
internal sealed class AdvancedConfiguration
{
public bool DebugOverlay { get; set; }
--- /dev/null
+using Dalamud.Game.Text;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Plugin.Services;
+using Questionable.External;
+using Questionable.Model.Questing;
+
+namespace Questionable.Controller.Steps.Common;
+
+internal static class SendNotification
+{
+ internal sealed record Task(EInteractionType InteractionType, string? Comment) : ITask
+ {
+ public override string ToString() => "SendNotification";
+ }
+
+ internal sealed class Executor(
+ NotificationMasterIpc notificationMasterIpc,
+ IChatGui chatGui,
+ Configuration configuration) : TaskExecutor<Task>
+ {
+ protected override bool Start()
+ {
+ if (!configuration.Notifications.Enabled)
+ return false;
+
+ string text = Task.InteractionType switch
+ {
+ EInteractionType.Duty => "Duty",
+ EInteractionType.SinglePlayerDuty => "Single player duty",
+ EInteractionType.Instruction or EInteractionType.WaitForManualProgress or EInteractionType.Snipe =>
+ "Manual interaction required",
+ _ => $"{Task.InteractionType}",
+ };
+
+ if (!string.IsNullOrEmpty(Task.Comment))
+ text += $" - {Task.Comment}";
+
+ if (configuration.Notifications.ChatType != XivChatType.None)
+ {
+ var message = configuration.Notifications.ChatType switch
+ {
+ XivChatType.Say
+ or XivChatType.Shout
+ or XivChatType.TellOutgoing
+ or XivChatType.TellIncoming
+ or XivChatType.Party
+ or XivChatType.Alliance
+ or (>= XivChatType.Ls1 and <= XivChatType.Ls8)
+ or XivChatType.FreeCompany
+ or XivChatType.NoviceNetwork
+ or XivChatType.Yell
+ or XivChatType.CrossParty
+ or XivChatType.PvPTeam
+ or XivChatType.CrossLinkShell1
+ or XivChatType.NPCDialogue
+ or XivChatType.NPCDialogueAnnouncements
+ or (>= XivChatType.CrossLinkShell2 and <= XivChatType.CrossLinkShell8)
+ => new XivChatEntry
+ {
+ Message = text,
+ Type = configuration.Notifications.ChatType,
+ Name = new SeStringBuilder()
+ .AddUiForeground(CommandHandler.MessageTag, CommandHandler.TagColor)
+ .Build(),
+ },
+ _ => new XivChatEntry
+ {
+ Message = new SeStringBuilder()
+ .AddUiForeground($"[{CommandHandler.MessageTag}] ", CommandHandler.TagColor)
+ .Append(text)
+ .Build(),
+ Type = configuration.Notifications.ChatType,
+ }
+ };
+ chatGui.Print(message);
+ }
+
+ notificationMasterIpc.Notify(text);
+ return true;
+ }
+
+ public override ETaskResult Update() => ETaskResult.TaskComplete;
+ }
+}
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Questionable.Controller.Steps.Common;
+using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.Functions;
internal sealed class Factory(
IClientState clientState,
ICondition condition,
- TerritoryData territoryData)
+ TerritoryData territoryData,
+ Configuration configuration)
: ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
case EInteractionType.WaitForManualProgress:
case EInteractionType.Instruction:
- case EInteractionType.Snipe:
return [new WaitNextStepOrSequence()];
+ case EInteractionType.Snipe:
+ if (configuration.General.AutomaticallyCompleteSnipeTasks)
+ return [new WaitNextStepOrSequence()];
+ else
+ return [
+ new SendNotification.Task(step.InteractionType, step.Comment),
+ new WaitNextStepOrSequence()
+ ];
+
case EInteractionType.Duty:
+ return [
+ new SendNotification.Task(step.InteractionType, step.ContentFinderConditionId.HasValue ? territoryData.GetContentFinderConditionName(step.ContentFinderConditionId.Value) : step.Comment),
+ new EndAutomation(),
+ ];
+
case EInteractionType.SinglePlayerDuty:
- return [new EndAutomation()];
+ return [
+ new SendNotification.Task(step.InteractionType, quest.Info.Name),
+ new EndAutomation()
+ ];
case EInteractionType.WalkTo:
case EInteractionType.Jump:
private readonly ImmutableHashSet<ushort> _territoriesWithMount;
private readonly ImmutableDictionary<ushort, uint> _dutyTerritories;
private readonly ImmutableDictionary<ushort, string> _instanceNames;
+ private readonly ImmutableDictionary<uint, string> _contentFinderConditionNames;
public TerritoryData(IDataManager dataManager)
{
_instanceNames = dataManager.GetExcelSheet<ContentFinderCondition>()!
.Where(x => x.RowId > 0 && x.Content != 0 && x.ContentLinkType == 1 && x.ContentType.Row != 6)
.ToImmutableDictionary(x => x.Content, x => x.Name.ToString());
+
+ _contentFinderConditionNames = dataManager.GetExcelSheet<ContentFinderCondition>()!
+ .Where(x => x.RowId > 0 && x.Content != 0 && x.ContentLinkType == 1 && x.ContentType.Row != 6)
+ .ToImmutableDictionary(x => x.RowId, x => x.Name.ToString());
}
public string? GetName(ushort territoryId) => _territoryNames.GetValueOrDefault(territoryId);
_dutyTerritories.TryGetValue(territoryId, out uint contentType) && contentType == 7;
public string? GetInstanceName(ushort instanceId) => _instanceNames.GetValueOrDefault(instanceId);
+
+ public string? GetContentFinderConditionName(uint cfcId) => _contentFinderConditionNames.GetValueOrDefault(cfcId);
}
--- /dev/null
+using Dalamud.Plugin;
+using NotificationMasterAPI;
+
+namespace Questionable.External;
+
+internal sealed class NotificationMasterIpc(IDalamudPluginInterface pluginInterface, Configuration configuration)
+{
+ private readonly NotificationMasterApi _api = new(pluginInterface);
+
+ public bool Enabled => _api.IsIPCReady();
+
+ public void Notify(string message)
+ {
+ var config = configuration.Notifications;
+ if (!config.Enabled)
+ return;
+
+ if (config.ShowTrayMessage)
+ _api.DisplayTrayNotification("Questionable", message);
+
+ if (config.FlashTaskbar)
+ _api.FlashTaskbarIcon();
+ }
+}
<ProjectReference Include="..\LLib\LLib.csproj"/>
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj"/>
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj"/>
+ <ProjectReference Include="..\vendor\NotificationMasterAPI\NotificationMasterAPI\NotificationMasterAPI.csproj" />
</ItemGroup>
</Project>
serviceCollection.AddSingleton<ArtisanIpc>();
serviceCollection.AddSingleton<QuestionableIpc>();
serviceCollection.AddSingleton<TextAdvanceIpc>();
+ serviceCollection.AddSingleton<NotificationMasterIpc>();
}
private static void AddTaskFactories(ServiceCollection serviceCollection)
serviceCollection.AddTaskExecutor<InitiateLeve.Initiate, InitiateLeve.InitiateExecutor>();
serviceCollection.AddTaskExecutor<InitiateLeve.SelectDifficulty, InitiateLeve.SelectDifficultyExecutor>();
+ serviceCollection.AddTaskExecutor<SendNotification.Task, SendNotification.Executor>();
serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Dalamud.Game.Text;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
+using Dalamud.Utility;
using ImGuiNET;
using LLib.ImGui;
using Lumina.Excel.GeneratedSheets;
+using Questionable.External;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Questionable.Windows;
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
{
private readonly IDalamudPluginInterface _pluginInterface;
+ private readonly NotificationMasterIpc _notificationMasterIpc;
private readonly Configuration _configuration;
private readonly uint[] _mountIds;
["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
[SuppressMessage("Performance", "CA1861", Justification = "One time initialization")]
- public ConfigWindow(IDalamudPluginInterface pluginInterface, Configuration configuration, IDataManager dataManager)
+ public ConfigWindow(IDalamudPluginInterface pluginInterface, NotificationMasterIpc notificationMasterIpc, Configuration configuration, IDataManager dataManager)
: base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize)
{
_pluginInterface = pluginInterface;
+ _notificationMasterIpc = notificationMasterIpc;
_configuration = configuration;
var mounts = dataManager.GetExcelSheet<Mount>()!
public override void Draw()
{
- if (ImGui.BeginTabBar("QuestionableConfigTabs"))
+ using var tabBar = ImRaii.TabBar("QuestionableConfigTabs");
+ if (!tabBar)
+ return;
+
+ DrawGeneralTab();
+ DrawNotificationsTab();
+ DrawAdvancedTab();
+ }
+
+ private void DrawGeneralTab()
+ {
+ using var tab = ImRaii.TabItem("General");
+ if (!tab)
+ return;
+ int selectedMount = Array.FindIndex(_mountIds, x => x == _configuration.General.MountId);
+ if (selectedMount == -1)
{
- if (ImGui.BeginTabItem("General"))
- {
- int selectedMount = Array.FindIndex(_mountIds, x => x == _configuration.General.MountId);
- if (selectedMount == -1)
- {
- selectedMount = 0;
- _configuration.General.MountId = _mountIds[selectedMount];
- Save();
- }
+ selectedMount = 0;
+ _configuration.General.MountId = _mountIds[selectedMount];
+ Save();
+ }
- if (ImGui.Combo("Preferred Mount", ref selectedMount, _mountNames, _mountNames.Length))
- {
- _configuration.General.MountId = _mountIds[selectedMount];
- Save();
- }
+ if (ImGui.Combo("Preferred Mount", ref selectedMount, _mountNames, _mountNames.Length))
+ {
+ _configuration.General.MountId = _mountIds[selectedMount];
+ Save();
+ }
- int grandCompany = (int)_configuration.General.GrandCompany;
- if (ImGui.Combo("Preferred Grand Company", ref grandCompany, _grandCompanyNames,
- _grandCompanyNames.Length))
- {
- _configuration.General.GrandCompany = (GrandCompany)grandCompany;
- Save();
- }
+ int grandCompany = (int)_configuration.General.GrandCompany;
+ if (ImGui.Combo("Preferred Grand Company", ref grandCompany, _grandCompanyNames,
+ _grandCompanyNames.Length))
+ {
+ _configuration.General.GrandCompany = (GrandCompany)grandCompany;
+ Save();
+ }
- bool hideInAllInstances = _configuration.General.HideInAllInstances;
- if (ImGui.Checkbox("Hide quest window in all instanced duties", ref hideInAllInstances))
- {
- _configuration.General.HideInAllInstances = hideInAllInstances;
- Save();
- }
+ bool hideInAllInstances = _configuration.General.HideInAllInstances;
+ if (ImGui.Checkbox("Hide quest window in all instanced duties", ref hideInAllInstances))
+ {
+ _configuration.General.HideInAllInstances = hideInAllInstances;
+ Save();
+ }
- bool useEscToCancelQuesting = _configuration.General.UseEscToCancelQuesting;
- if (ImGui.Checkbox("Use ESC to cancel questing/movement", ref useEscToCancelQuesting))
- {
- _configuration.General.UseEscToCancelQuesting = useEscToCancelQuesting;
- Save();
- }
+ bool useEscToCancelQuesting = _configuration.General.UseEscToCancelQuesting;
+ if (ImGui.Checkbox("Use ESC to cancel questing/movement", ref useEscToCancelQuesting))
+ {
+ _configuration.General.UseEscToCancelQuesting = useEscToCancelQuesting;
+ Save();
+ }
- bool showIncompleteSeasonalEvents = _configuration.General.ShowIncompleteSeasonalEvents;
- if (ImGui.Checkbox("Show details for incomplete seasonal events", ref showIncompleteSeasonalEvents))
- {
- _configuration.General.ShowIncompleteSeasonalEvents = showIncompleteSeasonalEvents;
- Save();
- }
+ bool showIncompleteSeasonalEvents = _configuration.General.ShowIncompleteSeasonalEvents;
+ if (ImGui.Checkbox("Show details for incomplete seasonal events", ref showIncompleteSeasonalEvents))
+ {
+ _configuration.General.ShowIncompleteSeasonalEvents = showIncompleteSeasonalEvents;
+ Save();
+ }
+
+ bool configureTextAdvance = _configuration.General.ConfigureTextAdvance;
+ if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings",
+ ref configureTextAdvance))
+ {
+ _configuration.General.ConfigureTextAdvance = configureTextAdvance;
+ Save();
+ }
+
+ if (ImGui.CollapsingHeader("Cheats"))
+ {
+ ImGui.TextColored(ImGuiColors.DalamudRed,
+ "This setting will be removed in a future version, and will be\navailable through TextAdvance instead.");
+ bool automaticallyCompleteSnipeTasks = _configuration.General.AutomaticallyCompleteSnipeTasks;
+ if (ImGui.Checkbox("Automatically complete snipe tasks", ref automaticallyCompleteSnipeTasks))
+ {
+ _configuration.General.AutomaticallyCompleteSnipeTasks = automaticallyCompleteSnipeTasks;
+ Save();
+ }
+ }
+ }
+
+ private void DrawNotificationsTab()
+ {
+ using var tab = ImRaii.TabItem("Notifications");
+ if (!tab)
+ return;
- bool configureTextAdvance = _configuration.General.ConfigureTextAdvance;
- if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref configureTextAdvance))
+ bool enabled = _configuration.Notifications.Enabled;
+ if (ImGui.Checkbox("Enable notifications when manual interaction is required", ref enabled))
+ {
+ _configuration.Notifications.Enabled = enabled;
+ Save();
+ }
+
+ using (ImRaii.Disabled(!_configuration.Notifications.Enabled))
+ {
+ using (ImRaii.PushIndent())
+ {
+ var xivChatTypes = Enum.GetValues<XivChatType>()
+ .Where(x => x != XivChatType.StandardEmote)
+ .ToArray();
+ var selectedChatType = Array.IndexOf(xivChatTypes, _configuration.Notifications.ChatType);
+ string[] chatTypeNames = xivChatTypes
+ .Select(t => t.GetAttribute<XivChatTypeInfoAttribute>()?.FancyName ?? t.ToString())
+ .ToArray();
+ if (ImGui.Combo("Chat channel", ref selectedChatType, chatTypeNames,
+ chatTypeNames.Length))
{
- _configuration.General.ConfigureTextAdvance = configureTextAdvance;
+ _configuration.Notifications.ChatType = xivChatTypes[selectedChatType];
Save();
}
- if (ImGui.CollapsingHeader("Cheats"))
+ ImGui.Separator();
+ ImGui.Text("NotificationMaster settings");
+ ImGui.SameLine();
+ ImGuiComponents.HelpMarker("Requires the plugin 'NotificationMaster' to be installed.");
+ using (ImRaii.Disabled(!_notificationMasterIpc.Enabled))
{
- ImGui.TextColored(ImGuiColors.DalamudRed, "This setting will be removed in a future version, and will be\navailable through TextAdvance instead.");
- bool automaticallyCompleteSnipeTasks = _configuration.General.AutomaticallyCompleteSnipeTasks;
- if (ImGui.Checkbox("Automatically complete snipe tasks", ref automaticallyCompleteSnipeTasks))
+ bool showTrayMessage = _configuration.Notifications.ShowTrayMessage;
+ if (ImGui.Checkbox("Show tray notification", ref showTrayMessage))
{
- _configuration.General.AutomaticallyCompleteSnipeTasks = automaticallyCompleteSnipeTasks;
+ _configuration.Notifications.ShowTrayMessage = showTrayMessage;
Save();
}
- }
- ImGui.EndTabItem();
+ bool flashTaskbar = _configuration.Notifications.FlashTaskbar;
+ if (ImGui.Checkbox("Flash taskbar icon", ref flashTaskbar))
+ {
+ _configuration.Notifications.FlashTaskbar = flashTaskbar;
+ Save();
+ }
+ }
}
+ }
+ }
- if (ImGui.BeginTabItem("Advanced"))
- {
- ImGui.TextColored(ImGuiColors.DalamudRed,
- "Enabling any option here may cause unexpected behavior. Use at your own risk.");
-
- ImGui.Separator();
+ private void DrawAdvancedTab()
+ {
+ using var tab = ImRaii.TabItem("Advanced");
+ if (!tab)
+ return;
- bool debugOverlay = _configuration.Advanced.DebugOverlay;
- if (ImGui.Checkbox("Enable debug overlay", ref debugOverlay))
- {
- _configuration.Advanced.DebugOverlay = debugOverlay;
- Save();
- }
+ ImGui.TextColored(ImGuiColors.DalamudRed,
+ "Enabling any option here may cause unexpected behavior. Use at your own risk.");
- bool neverFly = _configuration.Advanced.NeverFly;
- if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly))
- {
- _configuration.Advanced.NeverFly = neverFly;
- Save();
- }
+ ImGui.Separator();
- bool additionalStatusInformation = _configuration.Advanced.AdditionalStatusInformation;
- if (ImGui.Checkbox("Draw additional status information", ref additionalStatusInformation))
- {
- _configuration.Advanced.AdditionalStatusInformation = additionalStatusInformation;
- Save();
- }
+ bool debugOverlay = _configuration.Advanced.DebugOverlay;
+ if (ImGui.Checkbox("Enable debug overlay", ref debugOverlay))
+ {
+ _configuration.Advanced.DebugOverlay = debugOverlay;
+ Save();
+ }
- ImGui.EndTabItem();
- }
+ bool neverFly = _configuration.Advanced.NeverFly;
+ if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly))
+ {
+ _configuration.Advanced.NeverFly = neverFly;
+ Save();
+ }
- ImGui.EndTabBar();
+ bool additionalStatusInformation = _configuration.Advanced.AdditionalStatusInformation;
+ if (ImGui.Checkbox("Draw additional status information", ref additionalStatusInformation))
+ {
+ _configuration.Advanced.AdditionalStatusInformation = additionalStatusInformation;
+ Save();
}
+
+ ImGui.EndTabItem();
}
private void Save() => _pluginInterface.SavePluginConfig(_configuration);
during quests, including being interrupted by mobs.
""",
new Uri("https://github.com/FFXIV-CombatReborn/RotationSolverReborn")),
+ new("NotificationMaster",
+ """
+ Sends a configurable out-of-game notification if a quest
+ requires manual actions.
+ """,
+ new Uri("https://github.com/NightmareXIV/NotificationMaster")),
];
private readonly Configuration _configuration;
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Finish Setup"))
{
+ _logger.LogInformation("Marking setup as complete");
_configuration.MarkPluginSetupComplete();
_pluginInterface.SavePluginConfig(_configuration);
IsOpen = false;
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Close window & don't enable Questionable"))
{
+ _logger.LogWarning("Closing window without all required plugins installed");
IsOpen = false;
}
}
"gatheringpaths": {
"type": "Project",
"dependencies": {
- "Questionable.Model": "[3.10.0, )"
+ "Questionable.Model": "[3.12.0, )"
}
},
"llib": {
"DalamudPackager": "[2.1.13, )"
}
},
+ "notificationmasterapi": {
+ "type": "Project"
+ },
"questionable.model": {
"type": "Project",
"dependencies": {
"questpaths": {
"type": "Project",
"dependencies": {
- "Questionable.Model": "[3.10.0, )"
+ "Questionable.Model": "[3.12.0, )"
}
}
}
--- /dev/null
+Subproject commit 05b1ba788d5cb940ed8e82599eb88778c9cecdb0