Add notification settings for when manual interactions are required

This commit is contained in:
Liza 2024-11-03 22:25:03 +01:00
parent eb81c74930
commit f42540bd66
Signed by: liza
GPG Key ID: 7199F8D727D55F67
14 changed files with 341 additions and 96 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[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

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>3.12</Version>
<Version>3.13</Version>
</PropertyGroup>
</Project>

View File

@ -26,6 +26,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
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
@ -68,8 +72,16 @@ Global
{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

View File

@ -1,4 +1,5 @@
using Dalamud.Configuration;
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.ImGui;
@ -11,6 +12,7 @@ internal sealed class Configuration : IPluginConfiguration
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();
@ -30,6 +32,14 @@ internal sealed class Configuration : IPluginConfiguration
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; }

View File

@ -0,0 +1,84 @@
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;
}
}

View File

@ -6,6 +6,7 @@ using System.Numerics;
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;
@ -19,7 +20,8 @@ internal static class WaitAtEnd
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)
@ -47,12 +49,28 @@ internal static class WaitAtEnd
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:

View File

@ -14,6 +14,7 @@ internal sealed class TerritoryData
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)
{
@ -40,6 +41,10 @@ internal sealed class TerritoryData
_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);
@ -61,4 +66,6 @@ internal sealed class TerritoryData
_dutyTerritories.TryGetValue(territoryId, out uint contentType) && contentType == 7;
public string? GetInstanceName(ushort instanceId) => _instanceNames.GetValueOrDefault(instanceId);
public string? GetContentFinderConditionName(uint cfcId) => _contentFinderConditionNames.GetValueOrDefault(cfcId);
}

View File

@ -0,0 +1,24 @@
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();
}
}

View File

@ -21,5 +21,6 @@
<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>

View File

@ -127,6 +127,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<ArtisanIpc>();
serviceCollection.AddSingleton<QuestionableIpc>();
serviceCollection.AddSingleton<TextAdvanceIpc>();
serviceCollection.AddSingleton<NotificationMasterIpc>();
}
private static void AddTaskFactories(ServiceCollection serviceCollection)
@ -205,6 +206,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
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>();

View File

@ -1,12 +1,17 @@
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;
@ -14,6 +19,7 @@ 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;
@ -23,10 +29,11 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
["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>()!
@ -43,10 +50,20 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
public override void Draw()
{
if (ImGui.BeginTabBar("QuestionableConfigTabs"))
{
if (ImGui.BeginTabItem("General"))
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)
{
@ -91,7 +108,8 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
}
bool configureTextAdvance = _configuration.General.ConfigureTextAdvance;
if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref configureTextAdvance))
if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings",
ref configureTextAdvance))
{
_configuration.General.ConfigureTextAdvance = configureTextAdvance;
Save();
@ -99,7 +117,8 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
if (ImGui.CollapsingHeader("Cheats"))
{
ImGui.TextColored(ImGuiColors.DalamudRed, "This setting will be removed in a future version, and will be\navailable through TextAdvance instead.");
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))
{
@ -107,12 +126,69 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
Save();
}
}
ImGui.EndTabItem();
}
if (ImGui.BeginTabItem("Advanced"))
private void DrawNotificationsTab()
{
using var tab = ImRaii.TabItem("Notifications");
if (!tab)
return;
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.Notifications.ChatType = xivChatTypes[selectedChatType];
Save();
}
ImGui.Separator();
ImGui.Text("NotificationMaster settings");
ImGui.SameLine();
ImGuiComponents.HelpMarker("Requires the plugin 'NotificationMaster' to be installed.");
using (ImRaii.Disabled(!_notificationMasterIpc.Enabled))
{
bool showTrayMessage = _configuration.Notifications.ShowTrayMessage;
if (ImGui.Checkbox("Show tray notification", ref showTrayMessage))
{
_configuration.Notifications.ShowTrayMessage = showTrayMessage;
Save();
}
bool flashTaskbar = _configuration.Notifications.FlashTaskbar;
if (ImGui.Checkbox("Flash taskbar icon", ref flashTaskbar))
{
_configuration.Notifications.FlashTaskbar = flashTaskbar;
Save();
}
}
}
}
}
private void DrawAdvancedTab()
{
using var tab = ImRaii.TabItem("Advanced");
if (!tab)
return;
ImGui.TextColored(ImGuiColors.DalamudRed,
"Enabling any option here may cause unexpected behavior. Use at your own risk.");
@ -142,10 +218,6 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
ImGui.EndTabItem();
}
ImGui.EndTabBar();
}
}
private void Save() => _pluginInterface.SavePluginConfig(_configuration);
public void SaveWindowConfig() => Save();

View File

@ -44,6 +44,12 @@ internal sealed class OneTimeSetupWindow : LWindow, IDisposable
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;
@ -109,6 +115,7 @@ internal sealed class OneTimeSetupWindow : LWindow, IDisposable
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Finish Setup"))
{
_logger.LogInformation("Marking setup as complete");
_configuration.MarkPluginSetupComplete();
_pluginInterface.SavePluginConfig(_configuration);
IsOpen = false;
@ -128,6 +135,7 @@ internal sealed class OneTimeSetupWindow : LWindow, IDisposable
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Close window & don't enable Questionable"))
{
_logger.LogWarning("Closing window without all required plugins installed");
IsOpen = false;
}
}

View File

@ -174,7 +174,7 @@
"gatheringpaths": {
"type": "Project",
"dependencies": {
"Questionable.Model": "[3.10.0, )"
"Questionable.Model": "[3.12.0, )"
}
},
"llib": {
@ -183,6 +183,9 @@
"DalamudPackager": "[2.1.13, )"
}
},
"notificationmasterapi": {
"type": "Project"
},
"questionable.model": {
"type": "Project",
"dependencies": {
@ -192,7 +195,7 @@
"questpaths": {
"type": "Project",
"dependencies": {
"Questionable.Model": "[3.10.0, )"
"Questionable.Model": "[3.12.0, )"
}
}
}

1
vendor/NotificationMasterAPI vendored Submodule

@ -0,0 +1 @@
Subproject commit 05b1ba788d5cb940ed8e82599eb88778c9cecdb0