From 04ab38cc59ed5427854458a56f762d0f2e792a8b Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 16 Feb 2025 15:53:27 +0100 Subject: [PATCH 01/18] Add extra waypoint for 'Big Trouble in Little Ala Mhigo' to prevent running into a table --- .../782_Big Trouble in Little Ala Mhigo.json | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A7-Southern Thanalan, Big Trouble in Little Ala Mhigo/782_Big Trouble in Little Ala Mhigo.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A7-Southern Thanalan, Big Trouble in Little Ala Mhigo/782_Big Trouble in Little Ala Mhigo.json index 7ea44492d..829b406c9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A7-Southern Thanalan, Big Trouble in Little Ala Mhigo/782_Big Trouble in Little Ala Mhigo.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A7-Southern Thanalan, Big Trouble in Little Ala Mhigo/782_Big Trouble in Little Ala Mhigo.json @@ -26,6 +26,28 @@ { "Sequence": 1, "Steps": [ + { + "Position": { + "X": -225.94685, + "Y": 26.139933, + "Z": -340.8984 + }, + "TerritoryId": 146, + "InteractionType": "WalkTo", + "Mount": true, + "SkipConditions": { + "StepIf": { + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 16 + ] + } + } + }, { "DataId": 2001980, "Position": { From 11cde2a2d665fd83c02c6bb65ac6c62780da93fb Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 18 Feb 2025 17:32:02 +0100 Subject: [PATCH 02/18] Minor ARR updates --- .../MSQ-2/E1-2.1/3874_Moving On.json | 2 +- .../MSQ-2/E3-2.3/1457_Recruiting the Realm.json | 9 +++++++++ .../Unlocks/Misc/1162_My Feisty Little Chocobo.json | 3 ++- Questionable/Controller/QuestController.cs | 4 ++++ Questionable/Controller/Steps/Common/Mount.cs | 4 ++-- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/3874_Moving On.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/3874_Moving On.json index a65faa630..1c8a147a7 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/3874_Moving On.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/3874_Moving On.json @@ -102,7 +102,7 @@ }, "TerritoryId": 137, "InteractionType": "WalkTo", - "Fly": true + "Mount": true }, { "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1457_Recruiting the Realm.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1457_Recruiting the Realm.json index 8f103b011..ab1bef2ad 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1457_Recruiting the Realm.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1457_Recruiting the Realm.json @@ -120,6 +120,15 @@ 8 ] }, + { + "Position": { + "X": -140.77458, + "Y": 39.99999, + "Z": 155.4174 + }, + "TerritoryId": 128, + "InteractionType": "WalkTo" + }, { "DataId": 1009133, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/Unlocks/Misc/1162_My Feisty Little Chocobo.json b/QuestPaths/2.x - A Realm Reborn/Unlocks/Misc/1162_My Feisty Little Chocobo.json index 55b1d5426..a1b454f61 100644 --- a/QuestPaths/2.x - A Realm Reborn/Unlocks/Misc/1162_My Feisty Little Chocobo.json +++ b/QuestPaths/2.x - A Realm Reborn/Unlocks/Misc/1162_My Feisty Little Chocobo.json @@ -80,7 +80,8 @@ }, "TerritoryId": 148, "InteractionType": "UseItem", - "ItemId": 4868 + "ItemId": 4868, + "Fly": true }, { "Position": { diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 331b1d61e..ce92099eb 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -757,6 +757,10 @@ internal sealed class QuestController : MiniTaskController if (ManualPriorityQuests.Contains(currentQuest.Quest)) return false; + // "ifrit bleeds, we can kill it" isn't listed as priority quest, as we accept it during the MSQ 'Moving On' + if (currentQuest.Quest.Id is QuestId { Value: 1048 }) + return false; + if (currentQuest.Quest.Info.AlliedSociety != EAlliedSociety.None) return false; diff --git a/Questionable/Controller/Steps/Common/Mount.cs b/Questionable/Controller/Steps/Common/Mount.cs index 4f35f4d97..898ca14f3 100644 --- a/Questionable/Controller/Steps/Common/Mount.cs +++ b/Questionable/Controller/Steps/Common/Mount.cs @@ -37,7 +37,7 @@ internal static class Mount private bool _mountTriggered; private DateTime _retryAt = DateTime.MinValue; - public MountResult EvaluateMountState() + public unsafe MountResult EvaluateMountState() { if (condition[ConditionFlag.Mounted]) return MountResult.DontMount; @@ -58,7 +58,7 @@ internal static class Mount { Vector3 playerPosition = clientState.LocalPlayer?.Position ?? Vector3.Zero; float distance = System.Numerics.Vector3.Distance(playerPosition, Task.Position.GetValueOrDefault()); - if (Task.TerritoryId == clientState.TerritoryType && distance < 30f && !Conditions.IsDiving) + if (Task.TerritoryId == clientState.TerritoryType && distance < 30f && !Conditions.Instance()->Diving) { logger.LogInformation("Not using mount, as we're close to the target"); return MountResult.DontMount; From b35ee13704d6bcc679cb33977361cdc45c2c8e22 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Tue, 18 Feb 2025 18:46:16 +0100 Subject: [PATCH 03/18] [GatheringPathRenderer] Replace ECommons/Splatoon with Pictomancy --- .gitmodules | 6 +- .../GatheringPathRenderer.csproj | 2 +- .../GatheringPathRenderer.json | 2 +- GatheringPathRenderer/RendererPlugin.cs | 223 ++-- GatheringPathRenderer/Windows/EditorWindow.cs | 3 - GatheringPathRenderer/packages.lock.json | 953 +++++++++++++++++- Questionable.sln | 14 +- vendor/ECommons | 1 - vendor/pictomancy | 1 + 9 files changed, 1059 insertions(+), 146 deletions(-) delete mode 160000 vendor/ECommons create mode 160000 vendor/pictomancy diff --git a/.gitmodules b/.gitmodules index 832da345f..54bf2732b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,9 @@ [submodule "LLib"] path = LLib url = https://git.carvel.li/liza/LLib.git -[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 +[submodule "vendor/pictomancy"] + path = vendor/pictomancy + url = https://github.com/sourpuh/ffxiv_pictomancy diff --git a/GatheringPathRenderer/GatheringPathRenderer.csproj b/GatheringPathRenderer/GatheringPathRenderer.csproj index 73d05cbeb..f13c6c208 100644 --- a/GatheringPathRenderer/GatheringPathRenderer.csproj +++ b/GatheringPathRenderer/GatheringPathRenderer.csproj @@ -9,7 +9,7 @@ - + diff --git a/GatheringPathRenderer/GatheringPathRenderer.json b/GatheringPathRenderer/GatheringPathRenderer.json index 56f917099..49678b342 100644 --- a/GatheringPathRenderer/GatheringPathRenderer.json +++ b/GatheringPathRenderer/GatheringPathRenderer.json @@ -2,6 +2,6 @@ "Name": "GatheringPathRenderer", "Author": "Liza Carvelli", "Punchline": "[Questionable dev plugin]: Renders gathering location.", - "Description": "[Questionable dev plugin]: Renders gathering location using Splatoon.", + "Description": "[Questionable dev plugin]: Renders gathering location using Pictomancy.", "RepoUrl": "https://git.carvel.li/liza/Questionable/src/branch/master/GatheringPathRenderer" } diff --git a/GatheringPathRenderer/RendererPlugin.cs b/GatheringPathRenderer/RendererPlugin.cs index aa88906c9..6553fdadd 100644 --- a/GatheringPathRenderer/RendererPlugin.cs +++ b/GatheringPathRenderer/RendererPlugin.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Numerics; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -13,11 +14,9 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using ECommons; -using ECommons.Schedulers; -using ECommons.SplatoonAPI; using GatheringPathRenderer.Windows; using LLib.GameData; +using Pictomancy; using Questionable.Model.Gathering; namespace GatheringPathRenderer; @@ -25,10 +24,8 @@ namespace GatheringPathRenderer; [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] public sealed class RendererPlugin : IDalamudPlugin { - private const long OnTerritoryChange = -2; - private readonly WindowSystem _windowSystem = new(nameof(RendererPlugin)); - private readonly List _colors = [0xFFFF2020, 0xFF20FF20, 0xFF2020FF, 0xFFFFFF20, 0xFFFF20FF, 0xFF20FFFF]; + private readonly List _colors = [0x40FF2020, 0x4020FF20, 0x402020FF, 0x40FFFF20, 0x40FF20FF, 0x4020FFFF]; private readonly IDalamudPluginInterface _pluginInterface; private readonly IClientState _clientState; @@ -58,7 +55,8 @@ public sealed class RendererPlugin : IDalamudPlugin _editorCommands = new EditorCommands(this, dataManager, commandManager, targetManager, clientState, chatGui, configuration); var configWindow = new ConfigWindow(pluginInterface, configuration); - _editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState, objectTable, configWindow) + _editorWindow = new EditorWindow(this, _editorCommands, dataManager, targetManager, clientState, objectTable, + configWindow) { IsOpen = true }; _windowSystem.AddWindow(configWindow); _windowSystem.AddWindow(_editorWindow); @@ -67,14 +65,12 @@ public sealed class RendererPlugin : IDalamudPlugin _pluginInterface.GetIpcSubscriber("Questionable.ReloadData") .Subscribe(Reload); - ECommonsMain.Init(pluginInterface, this, Module.SplatoonAPI); + PictoService.Initialize(pluginInterface); LoadGatheringLocationsFromDirectory(); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; - _clientState.TerritoryChanged += TerritoryChanged; + _pluginInterface.UiBuilder.Draw += Draw; _clientState.ClassJobChanged += ClassJobChanged; - if (_clientState.IsLoggedIn) - TerritoryChanged(_clientState.TerritoryType); } internal DirectoryInfo PathsDirectory @@ -93,7 +89,8 @@ public sealed class RendererPlugin : IDalamudPlugin throw new Exception($"Unable to resolve project path ({_pluginInterface.AssemblyLocation.Directory})"); #else - var allPluginsDirectory = _pluginInterface.ConfigFile.Directory ?? throw new Exception("Unknown directory for plugin configs"); + var allPluginsDirectory = + _pluginInterface.ConfigFile.Directory ?? throw new Exception("Unknown directory for plugin configs"); return allPluginsDirectory .CreateSubdirectory("Questionable") .CreateSubdirectory("GatheringPaths"); @@ -104,7 +101,6 @@ public sealed class RendererPlugin : IDalamudPlugin internal void Reload() { LoadGatheringLocationsFromDirectory(); - Redraw(); } private void LoadGatheringLocationsFromDirectory() @@ -124,7 +120,6 @@ public sealed class RendererPlugin : IDalamudPlugin _pluginLog.Information( $"Loaded {_gatheringLocations.Count} gathering root locations from {PathsDirectory.FullName} directory"); #endif - } catch (Exception e) { @@ -209,142 +204,114 @@ public sealed class RendererPlugin : IDalamudPlugin } } - private void TerritoryChanged(ushort territoryId) => Redraw(); - private void ClassJobChanged(uint classJobId) { _currentClassJob = (EClassJob)classJobId; - Redraw(_currentClassJob); } - internal void Redraw() => Redraw(_currentClassJob); - - private void Redraw(EClassJob classJob) + private void Draw() { - Splatoon.RemoveDynamicElements("GatheringPathRenderer"); - if (!classJob.IsGatherer()) + if (!_currentClassJob.IsGatherer()) return; - var elements = GetLocationsInTerritory(_clientState.TerritoryType) - .SelectMany(location => - location.Root.Groups.SelectMany(group => - group.Nodes.SelectMany(node => node.Locations - .SelectMany(x => - { - bool isUnsaved = false; - bool isCone = false; - int minimumAngle = 0; - int maximumAngle = 0; - if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) && - locationOverride != null) - { - isUnsaved = locationOverride.NeedsSave(); - if (locationOverride.IsCone()) - { - isCone = true; - minimumAngle = locationOverride.MinimumAngle.GetValueOrDefault(); - maximumAngle = locationOverride.MaximumAngle.GetValueOrDefault(); - } - } + using var drawList = PictoService.Draw(); + if (drawList == null) + return; - if (!isCone && x.IsCone()) + Vector3 position = _clientState.LocalPlayer?.Position ?? Vector3.Zero; + foreach (var location in GetLocationsInTerritory(_clientState.TerritoryType)) + { + if (!location.Root.Groups.Any(gr => + gr.Nodes.Any( + no => no.Locations.Any( + loc => Vector3.Distance(loc.Position, position) < 200f)))) + continue; + + foreach (var group in location.Root.Groups) + { + foreach (GatheringNode node in group.Nodes) + { + foreach (var x in node.Locations) + { + bool isUnsaved = false; + bool isCone = false; + float minimumAngle = 0; + float maximumAngle = 0; + if (_editorWindow.TryGetOverride(x.InternalId, out LocationOverride? locationOverride) && + locationOverride != null) + { + isUnsaved = locationOverride.NeedsSave(); + if (locationOverride.IsCone()) { isCone = true; - minimumAngle = x.MinimumAngle.GetValueOrDefault(); - maximumAngle = x.MaximumAngle.GetValueOrDefault(); + minimumAngle = locationOverride.MinimumAngle.GetValueOrDefault(); + maximumAngle = locationOverride.MaximumAngle.GetValueOrDefault(); } + } -#if false - var a = GatheringMath.CalculateLandingLocation(x, 0, 0); - var b = GatheringMath.CalculateLandingLocation(x, 1, 1); -#endif - return new List - { - new Element(isCone - ? ElementType.ConeAtFixedCoordinates - : ElementType.CircleAtFixedCoordinates) - { - refX = x.Position.X, - refY = x.Position.Z, - refZ = x.Position.Y, - Filled = true, - radius = locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance(), - Donut = (locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance()) - - (locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance()), - color = _colors[location.Root.Groups.IndexOf(group) % _colors.Count], - Enabled = true, - coneAngleMin = minimumAngle, - coneAngleMax = maximumAngle, - tether = false, - }, - new Element(ElementType.CircleAtFixedCoordinates) - { - refX = x.Position.X, - refY = x.Position.Z, - refZ = x.Position.Y, - color = 0xFFFFFFFF, - radius = 0.1f, - Enabled = true, - overlayText = - $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)}", - overlayBGColor = isUnsaved ? 0xFF2020FF : 0xFF000000, - }, -#if false - new Element(ElementType.CircleAtFixedCoordinates) - { - refX = a.X, - refY = a.Z, - refZ = a.Y, - color = _colors[0], - radius = 0.1f, - Enabled = true, - overlayText = "Min Angle" - }, - new Element(ElementType.CircleAtFixedCoordinates) - { - refX = b.X, - refY = b.Z, - refZ = b.Y, - color = _colors[1], - radius = 0.1f, - Enabled = true, - overlayText = "Max Angle" - } -#endif - }; - })))) - .ToList(); + if (!isCone && x.IsCone()) + { + isCone = true; + minimumAngle = x.MinimumAngle.GetValueOrDefault(); + maximumAngle = x.MaximumAngle.GetValueOrDefault(); + } - if (elements.Count == 0) - { - _pluginLog.Information("No new elements to render."); - return; + minimumAngle *= (float)Math.PI / 180; + maximumAngle *= (float)Math.PI / 180; + if (!isCone || maximumAngle - minimumAngle >= 2 * Math.PI) + { + minimumAngle = 0; + maximumAngle = (float)Math.PI * 2; + } + + uint color = _colors[location.Root.Groups.IndexOf(group) % _colors.Count]; + drawList.AddFanFilled(x.Position, + locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance(), + locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance(), + minimumAngle, maximumAngle, color); + drawList.AddFan(x.Position, + locationOverride?.MinimumDistance ?? x.CalculateMinimumDistance(), + locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance(), + minimumAngle, maximumAngle, color | 0xFF000000); + + drawList.AddText(x.Position, 0xFFFFFFFF, $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)} || {minimumAngle}, {maximumAngle}", 1f); +#if false + var a = GatheringMath.CalculateLandingLocation(x, 0, 0); + var b = GatheringMath.CalculateLandingLocation(x, 1, 1); + new Element(ElementType.CircleAtFixedCoordinates) + { + refX = a.X, + refY = a.Z, + refZ = a.Y, + color = _colors[0], + radius = 0.1f, + Enabled = true, + overlayText = "Min Angle" + }, + new Element(ElementType.CircleAtFixedCoordinates) + { + refX = b.X, + refY = b.Z, + refZ = b.Y, + color = _colors[1], + radius = 0.1f, + Enabled = true, + overlayText = "Max Angle" + } +#endif + } + } + } } - - _ = new TickScheduler(delegate - { - try - { - Splatoon.AddDynamicElements("GatheringPathRenderer", - elements.ToArray(), - new[] { OnTerritoryChange }); - _pluginLog.Information($"Created {elements.Count} splatoon elements."); - } - catch (Exception e) - { - _pluginLog.Error(e, "Unable to create splatoon layer"); - } - }); } public void Dispose() { _clientState.ClassJobChanged -= ClassJobChanged; - _clientState.TerritoryChanged -= TerritoryChanged; + _pluginInterface.UiBuilder.Draw -= Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; - Splatoon.RemoveDynamicElements("GatheringPathRenderer"); - ECommonsMain.Dispose(); + PictoService.Dispose(); _pluginInterface.GetIpcSubscriber("Questionable.ReloadData") .Unsubscribe(Reload); diff --git a/GatheringPathRenderer/Windows/EditorWindow.cs b/GatheringPathRenderer/Windows/EditorWindow.cs index c117f4a9b..08925f156 100644 --- a/GatheringPathRenderer/Windows/EditorWindow.cs +++ b/GatheringPathRenderer/Windows/EditorWindow.cs @@ -154,7 +154,6 @@ internal sealed class EditorWindow : Window { locationOverride.MinimumAngle = minAngle; locationOverride.MaximumAngle = maxAngle; - _plugin.Redraw(); } float minDistance = locationOverride.MinimumDistance ?? location.CalculateMinimumDistance(); @@ -163,7 +162,6 @@ internal sealed class EditorWindow : Window { locationOverride.MinimumDistance = minDistance; locationOverride.MaximumDistance = maxDistance; - _plugin.Redraw(); } bool unsaved = locationOverride.NeedsSave(); @@ -194,7 +192,6 @@ internal sealed class EditorWindow : Window if (ImGui.Button("Reset")) { _changes[location.InternalId] = new LocationOverride(); - _plugin.Redraw(); } ImGui.EndDisabled(); diff --git a/GatheringPathRenderer/packages.lock.json b/GatheringPathRenderer/packages.lock.json index 523f69697..8947d2aa3 100644 --- a/GatheringPathRenderer/packages.lock.json +++ b/GatheringPathRenderer/packages.lock.json @@ -35,6 +35,16 @@ "resolved": "8.0.0", "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, "Microsoft.SourceLink.AzureRepos.Git": { "type": "Transitive", "resolved": "1.1.1", @@ -76,13 +86,944 @@ "Microsoft.SourceLink.Common": "1.1.1" } }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SharpDX": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "3pv0LFMvfK/dv1qISJnn8xBeeT6R/FRvr0EV4KI2DGsL84Qlv6P7isWqxGyU0LCwlSVCJN3jgHJ4Bl0KI2PJww==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "SharpDX.D3DCompiler": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "Rnsd6Ilp127xbXqhTit8WKFQUrXwWxqVGpglyWDNkIBCk0tWXNQEjrJpsl0KAObzyZaa33+EXAikLVt5fnd3GA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "SharpDX.Direct2D1": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "Qs8LzDMaQf1u3KB8ArHu9pDv6itZ++QXs99a/bVAG+nKr0Hx5NG4mcN5vsfE0mVR2TkeHfeUm4PksRah6VUPtA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.Direct3D11": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "oTm/iT5X/IIuJ8kNYP+DTC/MhBhqtRF5dbgPPFgLBdQv0BKzNTzXQQXd7SveBFjQg6hXEAJ2jGCAzNYvGFc9LA==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0", + "SharpDX.DXGI": "4.2.0" + } + }, + "SharpDX.DXGI": { + "type": "Transitive", + "resolved": "4.2.0", + "contentHash": "UjKqkgWc8U+SP+j3LBzFP6OB6Ntapjih7Xo+g1rLcsGbIb5KwewBrBChaUu7sil8rWoeVU/k0EJd3SMN4VqNZw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "SharpDX": "4.2.0" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.Text.Json": { "type": "Transitive", "resolved": "8.0.5", "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==" }, - "ecommons": { - "type": "Project" + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } }, "llib": { "type": "Project", @@ -90,6 +1031,14 @@ "DalamudPackager": "[11.0.0, )" } }, + "pictomancy": { + "type": "Project", + "dependencies": { + "SharpDX.D3DCompiler": "[4.2.0, )", + "SharpDX.Direct2D1": "[4.2.0, )", + "SharpDX.Direct3D11": "[4.2.0, )" + } + }, "questionable.model": { "type": "Project", "dependencies": { diff --git a/Questionable.sln b/Questionable.sln index 5046322f0..0f59e4f56 100644 --- a/Questionable.sln +++ b/Questionable.sln @@ -19,8 +19,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GatheringPaths", "Gathering EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GatheringPathRenderer", "GatheringPathRenderer\GatheringPathRenderer.csproj", "{F514DA95-9867-4F3F-8062-ACE0C62E8740}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ECommons", "vendor\ECommons\ECommons\ECommons.csproj", "{A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BBFFC6EA-15B1-48FC-B4D3-D9491278C27F}" ProjectSection(SolutionItems) = preProject Directory.Build.targets = Directory.Build.targets @@ -30,6 +28,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "vendor", "vendor", "{8F5EC9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotificationMasterAPI", "vendor\NotificationMasterAPI\NotificationMasterAPI\NotificationMasterAPI.csproj", "{9BD494ED-22F2-487B-BCE1-435399A8720E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "vendor\pictomancy\Pictomancy\Pictomancy.csproj", "{D1AE2F8C-BDE7-457F-A369-973101044A25}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -68,20 +68,20 @@ Global {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Debug|x64.Build.0 = Debug|x64 {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Release|x64.ActiveCfg = Release|x64 {F514DA95-9867-4F3F-8062-ACE0C62E8740}.Release|x64.Build.0 = Release|x64 - {A12D7B4B-8E6E-4DCF-A41A-12F62E9FF94B}.Debug|x64.ActiveCfg = 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 + {D1AE2F8C-BDE7-457F-A369-973101044A25}.Debug|x64.ActiveCfg = Debug|x64 + {D1AE2F8C-BDE7-457F-A369-973101044A25}.Debug|x64.Build.0 = Debug|x64 + {D1AE2F8C-BDE7-457F-A369-973101044A25}.Release|x64.ActiveCfg = Release|x64 + {D1AE2F8C-BDE7-457F-A369-973101044A25}.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} + {D1AE2F8C-BDE7-457F-A369-973101044A25} = {8F5EC9D5-4CE7-433B-BB3A-782500E84DDB} EndGlobalSection EndGlobal diff --git a/vendor/ECommons b/vendor/ECommons deleted file mode 160000 index 6ea40a9ee..000000000 --- a/vendor/ECommons +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6ea40a9eea2e805f2f566fe0493749c7c0639ea3 diff --git a/vendor/pictomancy b/vendor/pictomancy new file mode 160000 index 000000000..d147acc0e --- /dev/null +++ b/vendor/pictomancy @@ -0,0 +1 @@ +Subproject commit d147acc0ea5eed00e25b12508bf5d3fb8eefed53 From 92873554cc838dca8e7e64db008063b2d676baf9 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 20 Feb 2025 01:34:59 +0100 Subject: [PATCH 04/18] Draft for auto-completing quest battles (HW MSQ) --- .../RoslynElements/QuestStepExtensions.cs | 3 + .../Class Quests/WAR/601_And My Axe.json | 4 +- .../1595_A Series of Unfortunate Events.json | 4 +- .../1597_Divine Intervention.json | 4 +- .../1601_Keeping the Flame Alive.json | 4 +- .../1606_Sounding Out the Amphitheatre.json | 17 ++- .../1626_Waiting for the Wind to Change.json | 17 ++- .../1630_A General Summons.json | 11 +- .../MSQ/A4-Ishgard/1639_Fire and Blood.json | 4 +- .../A5-Sea of Clouds/1644_Familiar Faces.json | 4 +- .../1657_An Illuminati Incident.json | 4 +- ...667_Close Encounters of the VIth Kind.json | 4 +- QuestPaths/quest-v1.json | 21 +++ Questionable.Model/Questing/QuestStep.cs | 1 + .../Controller/CombatModules/BossModModule.cs | 32 ++--- .../GameUi/InteractionUiController.cs | 17 ++- Questionable/Controller/QuestRegistry.cs | 3 +- .../Steps/Common/SendNotification.cs | 3 +- .../Steps/Interactions/SinglePlayerDuty.cs | 124 ++++++++++++++++++ .../Controller/Steps/Shared/WaitAtEnd.cs | 2 +- Questionable/Controller/Steps/TaskCreator.cs | 38 +++++- Questionable/Data/TerritoryData.cs | 2 +- Questionable/External/BossModIpc.cs | 73 +++++++++++ Questionable/QuestionablePlugin.cs | 9 ++ 24 files changed, 359 insertions(+), 46 deletions(-) create mode 100644 Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs create mode 100644 Questionable/External/BossModIpc.cs diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index 2d3ec8034..ae0517d28 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -123,6 +123,9 @@ internal static class QuestStepExtensions Assignment(nameof(QuestStep.AutoDutyEnabled), step.AutoDutyEnabled, emptyStep.AutoDutyEnabled) .AsSyntaxNodeOrToken(), + Assignment(nameof(QuestStep.BossModEnabled), + step.BossModEnabled, emptyStep.BossModEnabled) + .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions, emptyStep.SkipConditions) .AsSyntaxNodeOrToken(), diff --git a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json index fca23e000..dae6eb116 100644 --- a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json +++ b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json @@ -95,7 +95,9 @@ }, "TerritoryId": 138, "InteractionType": "SinglePlayerDuty", - "Fly": true + "Fly": true, + "ContentFinderConditionId": 393, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json index 06d28cf64..b4107b998 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json @@ -58,7 +58,9 @@ "Z": 349.96558 }, "TerritoryId": 401, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 395, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json index d8baf9d31..238d8e876 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json @@ -78,7 +78,9 @@ "AethernetShortcut": [ "[Ishgard] The Forgotten Knight", "[Ishgard] The Tribunal" - ] + ], + "ContentFinderConditionId": 396, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json index fe2d06748..a42a2a769 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json @@ -28,7 +28,9 @@ "Z": 388.63196 }, "TerritoryId": 145, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 400, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json index ff6cd6992..b00c7cc02 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json @@ -21,6 +21,16 @@ { "Sequence": 1, "Steps": [ + { + "Position": { + "X": 474.62885, + "Y": 200.2377, + "Z": 657.9519 + }, + "TerritoryId": 397, + "InteractionType": "WalkTo", + "AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest" + }, { "Position": { "X": 486.38373, @@ -28,8 +38,7 @@ "Z": 239.54294 }, "TerritoryId": 397, - "InteractionType": "WalkTo", - "AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest" + "InteractionType": "WalkTo" }, { "Position": { @@ -69,7 +78,9 @@ }, "TerritoryId": 397, "InteractionType": "SinglePlayerDuty", - "DisableNavmesh": true + "DisableNavmesh": true, + "ContentFinderConditionId": 397, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json index d38df9943..ea46e2441 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1626_Waiting for the Wind to Change.json @@ -59,7 +59,14 @@ "KillEnemyDataIds": [ 4015 ], - "$": "0 0 0 0 0 0 -> " + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 128 + ] }, { "Position": { @@ -72,6 +79,14 @@ "EnemySpawnType": "AutoOnEnterArea", "KillEnemyDataIds": [ 4015 + ], + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 64 ] } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json index 61828e2cf..cdca15510 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.3-The Churning Mists/1630_A General Summons.json @@ -89,6 +89,16 @@ "InteractionType": "WalkTo", "Mount": true }, + { + "Position": { + "X": -335.0186, + "Y": 13.983504, + "Z": -100.87753 + }, + "TerritoryId": 140, + "InteractionType": "WalkTo", + "Fly": true + }, { "DataId": 1004019, "Position": { @@ -98,7 +108,6 @@ }, "TerritoryId": 140, "InteractionType": "Interact", - "Fly": true, "TargetTerritoryId": 140 }, { diff --git a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json index ce1f3688a..d3573b34f 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json @@ -74,7 +74,9 @@ "Z": 37.247192 }, "TerritoryId": 418, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 398, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json index c65d68153..0122bc84d 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json @@ -56,7 +56,9 @@ "TerritoryId": 401, "InteractionType": "SinglePlayerDuty", "Emote": "lookout", - "StopDistance": 0.25 + "StopDistance": 0.25, + "ContentFinderConditionId": 401, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json index 9f25ebdab..f3e3acdb5 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json @@ -47,7 +47,9 @@ "AethernetShortcut": [ "[Idyllshire] Aetheryte Plaza", "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" - ] + ], + "ContentFinderConditionId": 422, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json index 907bdd453..792ea9613 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json @@ -68,7 +68,9 @@ "Z": 553.97876 }, "TerritoryId": 402, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "ContentFinderConditionId": 399, + "BossModEnabled": true } ] }, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 62ad57644..d078b4c83 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1257,6 +1257,27 @@ ] } }, + { + "if": { + "properties": { + "InteractionType": { + "const": "SinglePlayerDuty" + } + } + }, + "then": { + "properties": { + "ContentFinderConditionId": { + "type": "integer", + "exclusiveMinimum": 0, + "exclusiveMaximum": 3000 + }, + "BossModEnabled": { + "type": "boolean" + } + } + } + }, { "if": { "properties": { diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index bd1ce3040..fe7170d5b 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -75,6 +75,7 @@ public sealed class QuestStep public JumpDestination? JumpDestination { get; set; } public uint? ContentFinderConditionId { get; set; } public bool AutoDutyEnabled { get; set; } + public bool BossModEnabled { get; set; } public SkipConditions? SkipConditions { get; set; } public List?> RequiredQuestVariables { get; set; } = new(); diff --git a/Questionable/Controller/CombatModules/BossModModule.cs b/Questionable/Controller/CombatModules/BossModModule.cs index ee25967f4..a69237f3e 100644 --- a/Questionable/Controller/CombatModules/BossModModule.cs +++ b/Questionable/Controller/CombatModules/BossModModule.cs @@ -9,33 +9,26 @@ using Questionable.Model; using System; using System.IO; using System.Numerics; +using Questionable.External; namespace Questionable.Controller.CombatModules; internal sealed class BossModModule : ICombatModule, IDisposable { - private const string Name = "BossMod"; private readonly ILogger _logger; + private readonly BossModIpc _bossModIpc; private readonly Configuration _configuration; - private readonly ICallGateSubscriber _getPreset; - private readonly ICallGateSubscriber _createPreset; - private readonly ICallGateSubscriber _setPreset; - private readonly ICallGateSubscriber _clearPreset; private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!; public BossModModule( ILogger logger, - IDalamudPluginInterface pluginInterface, + BossModIpc bossModIpc, Configuration configuration) { _logger = logger; + _bossModIpc = bossModIpc; _configuration = configuration; - - _getPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Get"); - _createPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Create"); - _setPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.SetActive"); - _clearPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.ClearActive"); } public bool CanHandleFight(CombatController.CombatData combatData) @@ -43,26 +36,19 @@ internal sealed class BossModModule : ICombatModule, IDisposable if (_configuration.General.CombatModule != Configuration.ECombatModule.BossMod) return false; - try - { - return _getPreset.HasFunction; - } - catch (IpcError) - { - return false; - } + return _bossModIpc.IsSupported(); } public bool Start(CombatController.CombatData combatData) { try { - if (_getPreset.InvokeFunc("Questionable") == null) + if (_bossModIpc.GetPreset("Questionable") == null) { using var reader = new StreamReader(Preset); - _logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _createPreset.InvokeFunc(reader.ReadToEnd(), true)); + _logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _bossModIpc.CreatePreset(reader.ReadToEnd(), true)); } - _setPreset.InvokeFunc("Questionable"); + _bossModIpc.SetPreset("Questionable"); return true; } catch (IpcError e) @@ -76,7 +62,7 @@ internal sealed class BossModModule : ICombatModule, IDisposable { try { - _clearPreset.InvokeFunc(); + _bossModIpc.ClearPreset(); return true; } catch (IpcError e) diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 45333c2b7..3164a3bb2 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -598,14 +598,14 @@ internal sealed class InteractionUiController : IDisposable if (checkAllSteps) { var sequence = quest.FindSequence(currentQuest.Sequence); - if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest, - sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt)) + if (sequence != null && + sequence.Steps.Any(step => HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt))) return true; } else { var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step); - if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt)) + if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt)) return true; } @@ -619,7 +619,7 @@ internal sealed class InteractionUiController : IDisposable Yes = true }; - if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt)) + if (HandleDefaultYesNo(addonSelectYesno, quest, null, [dialogueChoice], actualPrompt)) return true; } @@ -630,7 +630,7 @@ internal sealed class InteractionUiController : IDisposable } private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest, - List dialogueChoices, string actualPrompt) + QuestStep? step, List dialogueChoices, string actualPrompt) { _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count); foreach (var dialogueChoice in dialogueChoices) @@ -659,6 +659,13 @@ internal sealed class InteractionUiController : IDisposable return true; } + if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true }) + { + _logger.LogTrace("DefaultYesNo: probably Single Player Duty"); + addonSelectYesno->AtkUnitBase.FireCallbackInt(0); + return true; + } + return false; } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 7808b0950..2c38b2410 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -150,7 +150,8 @@ internal sealed class QuestRegistry foreach (var quest in _quests.Values) { foreach (var dutyStep in quest.AllSteps().Where(x => - x.Step.InteractionType == EInteractionType.Duty && x.Step.ContentFinderConditionId != null)) + x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty + && x.Step.ContentFinderConditionId != null)) { _contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step); } diff --git a/Questionable/Controller/Steps/Common/SendNotification.cs b/Questionable/Controller/Steps/Common/SendNotification.cs index 6d8bbcec6..8bb4fa803 100644 --- a/Questionable/Controller/Steps/Common/SendNotification.cs +++ b/Questionable/Controller/Steps/Common/SendNotification.cs @@ -26,7 +26,8 @@ internal static class SendNotification new Task(step.InteractionType, step.ContentFinderConditionId.HasValue ? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name : step.Comment), - EInteractionType.SinglePlayerDuty => new Task(step.InteractionType, quest.Info.Name), + EInteractionType.SinglePlayerDuty when !step.BossModEnabled => + new Task(step.InteractionType, quest.Info.Name), _ => null, }; } diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs new file mode 100644 index 000000000..4cd79cec9 --- /dev/null +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using Dalamud.Plugin.Services; +using Questionable.Controller.Steps.Shared; +using Questionable.Data; +using Questionable.External; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Controller.Steps.Interactions; + +internal static class SinglePlayerDuty +{ + internal sealed class Factory : ITaskFactory + { + public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) + { + if (step.InteractionType != EInteractionType.SinglePlayerDuty) + yield break; + + if (step.BossModEnabled) + { + ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId); + + yield return new StartSinglePlayerDuty(step.ContentFinderConditionId.Value); + yield return new EnableAi(); + yield return new WaitSinglePlayerDuty(step.ContentFinderConditionId.Value); + yield return new DisableAi(); + yield return new WaitAtEnd.WaitNextStepOrSequence(); + } + } + } + + internal sealed record StartSinglePlayerDuty(uint ContentFinderConditionId) : ITask + { + public override string ToString() => $"Wait(BossMod, entered instance {ContentFinderConditionId})"; + } + + internal sealed class StartSinglePlayerDutyExecutor( + TerritoryData territoryData, + IClientState clientState) : TaskExecutor + { + protected override bool Start() => true; + + public override ETaskResult Update() + { + if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId, + out var cfcData)) + throw new TaskException("Failed to get territory ID for content finder condition"); + + return clientState.TerritoryType == cfcData.TerritoryId + ? ETaskResult.TaskComplete + : ETaskResult.StillRunning; + } + + public override bool ShouldInterruptOnDamage() => false; + } + + internal sealed record EnableAi : ITask + { + public override string ToString() => "BossMod.EnableAi"; + } + + internal sealed class EnableAiExecutor( + BossModIpc bossModIpc) : TaskExecutor + { + protected override bool Start() + { + bossModIpc.EnableAi(); + return true; + } + + public override ETaskResult Update() => ETaskResult.TaskComplete; + + public override bool ShouldInterruptOnDamage() => false; + } + + internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : ITask + { + public override string ToString() => $"Wait(BossMod, left instance {ContentFinderConditionId})"; + } + + internal sealed class WaitSinglePlayerDutyExecutor( + TerritoryData territoryData, + IClientState clientState, + BossModIpc bossModIpc) : TaskExecutor, IStoppableTaskExecutor + { + protected override bool Start() => true; + + public override ETaskResult Update() + { + if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId, + out var cfcData)) + throw new TaskException("Failed to get territory ID for content finder condition"); + + return clientState.TerritoryType != cfcData.TerritoryId + ? ETaskResult.TaskComplete + : ETaskResult.StillRunning; + } + + public void StopNow() => bossModIpc.DisableAi(); + + public override bool ShouldInterruptOnDamage() => false; + } + + internal sealed record DisableAi : ITask + { + public override string ToString() => "BossMod.DisableAi"; + } + + internal sealed class DisableAiExecutor( + BossModIpc bossModIpc) : TaskExecutor + { + protected override bool Start() + { + bossModIpc.DisableAi(); + return true; + } + + public override ETaskResult Update() => ETaskResult.TaskComplete; + + public override bool ShouldInterruptOnDamage() => false; + } +} diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index d39c7c2a3..59d108cdd 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -53,7 +53,7 @@ internal static class WaitAtEnd return [new WaitNextStepOrSequence()]; case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled): - case EInteractionType.SinglePlayerDuty: + case EInteractionType.SinglePlayerDuty when !step.BossModEnabled: return [new EndAutomation()]; case EInteractionType.WalkTo: diff --git a/Questionable/Controller/Steps/TaskCreator.cs b/Questionable/Controller/Steps/TaskCreator.cs index 915f7d570..997d40d1d 100644 --- a/Questionable/Controller/Steps/TaskCreator.cs +++ b/Questionable/Controller/Steps/TaskCreator.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Plugin.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Questionable.Controller.Steps.Interactions; +using Questionable.Data; using Questionable.Model; using Questionable.Model.Questing; @@ -11,11 +14,19 @@ namespace Questionable.Controller.Steps; internal sealed class TaskCreator { private readonly IServiceProvider _serviceProvider; + private readonly TerritoryData _territoryData; + private readonly IClientState _clientState; private readonly ILogger _logger; - public TaskCreator(IServiceProvider serviceProvider, ILogger logger) + public TaskCreator( + IServiceProvider serviceProvider, + TerritoryData territoryData, + IClientState clientState, + ILogger logger) { _serviceProvider = serviceProvider; + _territoryData = territoryData; + _clientState = clientState; _logger = logger; } @@ -40,6 +51,31 @@ internal sealed class TaskCreator return tasks; }) .ToList(); + + var singlePlayerDutyTask = newTasks + .Where(y => y is SinglePlayerDuty.StartSinglePlayerDuty) + .Cast() + .FirstOrDefault(); + if (singlePlayerDutyTask != null && + _territoryData.TryGetContentFinderCondition(singlePlayerDutyTask.ContentFinderConditionId, + out var cfcData)) + { + // if we have a single player duty in queue, we check if we're in the matching territory + // if yes, skip all steps before (e.g. teleporting, waiting for navmesh, moving, interacting) + if (_clientState.TerritoryType == cfcData.TerritoryId) + { + int index = newTasks.IndexOf(singlePlayerDutyTask); + _logger.LogWarning( + "Skipping {SkippedTaskCount} out of {TotalCount} tasks, questionable was started while in single player duty", + index + 1, newTasks.Count); + + newTasks.RemoveRange(0, index + 1); + _logger.LogInformation("Next actual task: {NextTask}, total tasks left: {RemainingTaskCount}", + newTasks.FirstOrDefault(), + newTasks.Count); + } + } + if (newTasks.Count == 0) _logger.LogInformation("Nothing to execute for step?"); else diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs index f269b138a..820ae6565 100644 --- a/Questionable/Data/TerritoryData.cs +++ b/Questionable/Data/TerritoryData.cs @@ -45,7 +45,7 @@ internal sealed class TerritoryData .ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToDalamudString().ToString()); _contentFinderConditions = dataManager.GetExcelSheet() - .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6) + .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType is 1 or 5 && x.ContentType.RowId != 6) .Select(x => new ContentFinderConditionData(x, dataManager.Language)) .ToImmutableDictionary(x => x.ContentFinderConditionId, x => x); } diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs new file mode 100644 index 000000000..d1d02f794 --- /dev/null +++ b/Questionable/External/BossModIpc.cs @@ -0,0 +1,73 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; +using Dalamud.Plugin.Services; + +namespace Questionable.External; + +internal sealed class BossModIpc +{ + private readonly ICommandManager _commandManager; + private const string Name = "BossMod"; + + private readonly ICallGateSubscriber _getPreset; + private readonly ICallGateSubscriber _createPreset; + private readonly ICallGateSubscriber _setPreset; + private readonly ICallGateSubscriber _clearPreset; + + public BossModIpc(IDalamudPluginInterface pluginInterface, ICommandManager commandManager) + { + _commandManager = commandManager; + _getPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Get"); + _createPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Create"); + _setPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.SetActive"); + _clearPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.ClearActive"); + } + + public bool IsSupported() + { + try + { + return _getPreset.HasFunction; + } + catch (IpcError) + { + return false; + } + } + + public string? GetPreset(string name) + { + return _getPreset.InvokeFunc(name); + } + + public bool CreatePreset(string name, bool overwrite) + { + return _createPreset.InvokeFunc(name, overwrite); + } + + public void SetPreset(string name) + { + _setPreset.InvokeFunc(name); + } + + public void ClearPreset() + { + _clearPreset.InvokeFunc(); + } + + // TODO this should use your actual rotation plugin, not always vbm + public void EnableAi(string presetName = "VBM Default") + { + _commandManager.ProcessCommand("/vbmai on"); + _commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles true"); + SetPreset(presetName); + } + + public void DisableAi() + { + _commandManager.ProcessCommand("/vbmai off"); + _commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles false"); + ClearPreset(); + } +} diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index e0794c2d7..28d1f66b6 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -131,6 +131,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } @@ -222,6 +223,14 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskFactory(); + serviceCollection + .AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection + .AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskFactory(); serviceCollection.AddTaskExecutor(); From 097c67ed5df428d880311c575d36b3eadd428f6a Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 20 Feb 2025 20:45:38 +0100 Subject: [PATCH 05/18] Second draft for auto-completing quest battles --- GatheringPathRenderer/RendererPlugin.cs | 2 +- .../RoslynElements/QuestStepExtensions.cs | 3 ++ .../BRD/76_The One That Got Away.json | 1 + .../Class Quests/DRG/439_Proof of Might.json | 1 + .../Class Quests/DRG/56_Lance of Destiny.json | 3 +- .../MNK/567_Return of the Holyfist.json | 1 + .../Class Quests/WAR/601_And My Axe.json | 1 - .../1595_A Series of Unfortunate Events.json | 1 - .../1597_Divine Intervention.json | 1 - .../1601_Keeping the Flame Alive.json | 1 - .../1606_Sounding Out the Amphitheatre.json | 1 - .../MSQ/A4-Ishgard/1639_Fire and Blood.json | 1 - .../A5-Sea of Clouds/1644_Familiar Faces.json | 1 - .../1657_An Illuminati Incident.json | 1 - ...667_Close Encounters of the VIth Kind.json | 1 - .../3895_Sleep Now in Sapphire.json | 1 + QuestPaths/quest-v1.json | 11 ++--- Questionable.Model/Questing/ElementId.cs | 2 + Questionable.Model/Questing/QuestStep.cs | 1 + Questionable.Model/common-aethernetshard.json | 2 +- Questionable.Model/common-aetheryte.json | 2 +- Questionable.Model/common-classjob.json | 2 +- .../common-completionflags.json | 2 +- Questionable.Model/common-vector3.json | 2 +- Questionable/Configuration.cs | 8 ++++ Questionable/Controller/QuestRegistry.cs | 15 +++++-- .../Steps/Common/SendNotification.cs | 3 +- .../Steps/Interactions/SinglePlayerDuty.cs | 36 ++++++---------- .../Controller/Steps/Shared/WaitAtEnd.cs | 5 ++- Questionable/Data/JournalData.cs | 6 +-- Questionable/Data/TerritoryData.cs | 43 +++++++++++++++++++ Questionable/External/AutoDutyIpc.cs | 4 +- Questionable/External/BossModIpc.cs | 32 +++++++++++++- Questionable/External/QuestionableIpc.cs | 5 ++- Questionable/Model/QuestInfo.cs | 19 ++++---- Questionable/Model/SatisfactionSupplyInfo.cs | 3 +- Questionable/QuestionablePlugin.cs | 1 + Questionable/Validation/EIssueType.cs | 1 + .../UniqueSinglePlayerInstanceValidator.cs | 32 ++++++++++++++ Questionable/Windows/QuestSelectionWindow.cs | 2 +- global.json | 7 +++ 41 files changed, 197 insertions(+), 70 deletions(-) create mode 100644 Questionable/Validation/Validators/UniqueSinglePlayerInstanceValidator.cs create mode 100644 global.json diff --git a/GatheringPathRenderer/RendererPlugin.cs b/GatheringPathRenderer/RendererPlugin.cs index 6553fdadd..1fa3548f5 100644 --- a/GatheringPathRenderer/RendererPlugin.cs +++ b/GatheringPathRenderer/RendererPlugin.cs @@ -274,7 +274,7 @@ public sealed class RendererPlugin : IDalamudPlugin locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance(), minimumAngle, maximumAngle, color | 0xFF000000); - drawList.AddText(x.Position, 0xFFFFFFFF, $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)} || {minimumAngle}, {maximumAngle}", 1f); + drawList.AddText(x.Position, isUnsaved ? 0xFFFF0000 : 0xFFFFFFFF, $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)} || {minimumAngle}, {maximumAngle}", 1f); #if false var a = GatheringMath.CalculateLandingLocation(x, 0, 0); var b = GatheringMath.CalculateLandingLocation(x, 1, 1); diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index ae0517d28..ca5591bd6 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -126,6 +126,9 @@ internal static class QuestStepExtensions Assignment(nameof(QuestStep.BossModEnabled), step.BossModEnabled, emptyStep.BossModEnabled) .AsSyntaxNodeOrToken(), + Assignment(nameof(QuestStep.SinglePlayerDutyIndex), + step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex) + .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions, emptyStep.SkipConditions) .AsSyntaxNodeOrToken(), diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json index d888625ae..f374ebd71 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json @@ -57,6 +57,7 @@ }, "TerritoryId": 153, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyIndex": 1, "Fly": true } ] diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json index 847348ed5..c9af0007b 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json @@ -62,6 +62,7 @@ }, "TerritoryId": 154, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyIndex": 1, "AetheryteShortcut": "North Shroud - Fallgourd Float", "Fly": true } diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json index a9288d208..b588274cc 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json @@ -119,7 +119,8 @@ "Z": 29.06836 }, "TerritoryId": 152, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyIndex": 1 } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json index 59a1d9060..b8d8505f5 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json @@ -92,6 +92,7 @@ }, "TerritoryId": 130, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyIndex": 1, "AetheryteShortcut": "Ul'dah" } ] diff --git a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json index dae6eb116..f0598d7b4 100644 --- a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json +++ b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json @@ -96,7 +96,6 @@ "TerritoryId": 138, "InteractionType": "SinglePlayerDuty", "Fly": true, - "ContentFinderConditionId": 393, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json index b4107b998..10c0755f4 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json @@ -59,7 +59,6 @@ }, "TerritoryId": 401, "InteractionType": "SinglePlayerDuty", - "ContentFinderConditionId": 395, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json index 238d8e876..e3a1d105d 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json @@ -79,7 +79,6 @@ "[Ishgard] The Forgotten Knight", "[Ishgard] The Tribunal" ], - "ContentFinderConditionId": 396, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json index a42a2a769..6e052e8b4 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json @@ -29,7 +29,6 @@ }, "TerritoryId": 145, "InteractionType": "SinglePlayerDuty", - "ContentFinderConditionId": 400, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json index b00c7cc02..3503cfee9 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json @@ -79,7 +79,6 @@ "TerritoryId": 397, "InteractionType": "SinglePlayerDuty", "DisableNavmesh": true, - "ContentFinderConditionId": 397, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json index d3573b34f..eb9876a68 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json @@ -75,7 +75,6 @@ }, "TerritoryId": 418, "InteractionType": "SinglePlayerDuty", - "ContentFinderConditionId": 398, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json index 0122bc84d..b156029fd 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json @@ -57,7 +57,6 @@ "InteractionType": "SinglePlayerDuty", "Emote": "lookout", "StopDistance": 0.25, - "ContentFinderConditionId": 401, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json index f3e3acdb5..31218140f 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json @@ -48,7 +48,6 @@ "[Idyllshire] Aetheryte Plaza", "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" ], - "ContentFinderConditionId": 422, "BossModEnabled": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json index 792ea9613..a498f657c 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json @@ -69,7 +69,6 @@ }, "TerritoryId": 402, "InteractionType": "SinglePlayerDuty", - "ContentFinderConditionId": 399, "BossModEnabled": true } ] diff --git a/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json b/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json index 2c5cc5169..160a2970d 100644 --- a/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json +++ b/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json @@ -104,6 +104,7 @@ "StopDistance": 5, "TerritoryId": 829, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyIndex": 1, "DialogueChoices": [ { "Type": "List", diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index d078b4c83..350b0bf13 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1267,13 +1267,14 @@ }, "then": { "properties": { - "ContentFinderConditionId": { - "type": "integer", - "exclusiveMinimum": 0, - "exclusiveMaximum": 3000 - }, "BossModEnabled": { "type": "boolean" + }, + "SinglePlayerDutyIndex": { + "type": "integer", + "minimum": 0, + "maximum": 1, + "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is" } } } diff --git a/Questionable.Model/Questing/ElementId.cs b/Questionable.Model/Questing/ElementId.cs index a553ff0d3..6fce25ca1 100644 --- a/Questionable.Model/Questing/ElementId.cs +++ b/Questionable.Model/Questing/ElementId.cs @@ -91,6 +91,8 @@ public abstract class ElementId : IComparable, IEquatable public sealed class QuestId(ushort value) : ElementId(value) { + public static QuestId FromRowId(uint rowId) => new((ushort)(rowId & 0xFFFF)); + public override string ToString() { return Value.ToString(CultureInfo.InvariantCulture); diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index fe7170d5b..98127cde7 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -76,6 +76,7 @@ public sealed class QuestStep public uint? ContentFinderConditionId { get; set; } public bool AutoDutyEnabled { get; set; } public bool BossModEnabled { get; set; } + public byte SinglePlayerDutyIndex { get; set; } public SkipConditions? SkipConditions { get; set; } public List?> RequiredQuestVariables { get; set; } = new(); diff --git a/Questionable.Model/common-aethernetshard.json b/Questionable.Model/common-aethernetshard.json index a2af4209e..15a85c5f1 100644 --- a/Questionable.Model/common-aethernetshard.json +++ b/Questionable.Model/common-aethernetshard.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-aethernetshard.json", + "$id": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-aethernetshard.json", "type": "string", "enum": [ "[Gridania] Aetheryte Plaza", diff --git a/Questionable.Model/common-aetheryte.json b/Questionable.Model/common-aetheryte.json index 6aa50781f..8b033a830 100644 --- a/Questionable.Model/common-aetheryte.json +++ b/Questionable.Model/common-aetheryte.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-aetheryte.json", + "$id": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-aetheryte.json", "type": "string", "enum": [ "Gridania", diff --git a/Questionable.Model/common-classjob.json b/Questionable.Model/common-classjob.json index 5a7749396..e5e0d3938 100644 --- a/Questionable.Model/common-classjob.json +++ b/Questionable.Model/common-classjob.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-classjob.json", + "$id": "https://git.carvel.li//liza/Questionable/raw/branch/master/Questionable.Model/common-classjob.json", "type": "string", "enum": [ "Gladiator", diff --git a/Questionable.Model/common-completionflags.json b/Questionable.Model/common-completionflags.json index eb77d70c7..b7212b1dc 100644 --- a/Questionable.Model/common-completionflags.json +++ b/Questionable.Model/common-completionflags.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-completionflags.json", + "$id": "https://git.carvel.li//liza/Questionable/raw/branch/master/Questionable.Model/common-completionflags.json", "type": "array", "description": "Quest Variables that dictate whether or not this step is skipped: null is don't check, positive values need to be set, negative values need to be unset", "items": { diff --git a/Questionable.Model/common-vector3.json b/Questionable.Model/common-vector3.json index cfae56379..028af1c71 100644 --- a/Questionable.Model/common-vector3.json +++ b/Questionable.Model/common-vector3.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-vector3.json", + "$id": "https://git.carvel.li//liza/Questionable/raw/branch/master/Questionable.Model/common-vector3.json", "type": "object", "description": "Position in the world", "properties": { diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index 90c42bb50..74bb05e6e 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -14,6 +14,7 @@ internal sealed class Configuration : IPluginConfiguration public int PluginSetupCompleteVersion { get; set; } public GeneralConfiguration General { get; } = new(); public DutyConfiguration Duties { get; } = new(); + public SoloDutyConfiguration SoloDuties { get; } = new(); public NotificationConfiguration Notifications { get; } = new(); public AdvancedConfiguration Advanced { get; } = new(); public WindowConfig DebugWindowConfig { get; } = new(); @@ -41,6 +42,13 @@ internal sealed class Configuration : IPluginConfiguration public HashSet BlacklistedDutyCfcIds { get; set; } = []; } + internal sealed class SoloDutyConfiguration + { + public bool RunSoloInstancesWithBossMod { get; set; } + public HashSet WhitelistedSoloDutyCfcIds { get; set; } = []; + public HashSet BlacklistedSoloDutyCfcIds { get; set; } = []; + } + internal sealed class NotificationConfiguration { public bool Enabled { get; set; } = true; diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 2c38b2410..5e948761d 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -27,6 +27,7 @@ internal sealed class QuestRegistry private readonly JsonSchemaValidator _jsonSchemaValidator; private readonly ILogger _logger; private readonly LeveData _leveData; + private readonly TerritoryData _territoryData; private readonly ICallGateProvider _reloadDataIpc; private readonly Dictionary _quests = []; @@ -34,7 +35,7 @@ internal sealed class QuestRegistry public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator, - ILogger logger, LeveData leveData) + ILogger logger, LeveData leveData, TerritoryData territoryData) { _pluginInterface = pluginInterface; _questData = questData; @@ -42,6 +43,7 @@ internal sealed class QuestRegistry _jsonSchemaValidator = jsonSchemaValidator; _logger = logger; _leveData = leveData; + _territoryData = territoryData; _reloadDataIpc = _pluginInterface.GetIpcProvider("Questionable.ReloadData"); } @@ -150,10 +152,15 @@ internal sealed class QuestRegistry foreach (var quest in _quests.Values) { foreach (var dutyStep in quest.AllSteps().Where(x => - x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty - && x.Step.ContentFinderConditionId != null)) + x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty)) { - _contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step); + if (dutyStep.Step is { InteractionType: EInteractionType.Duty, ContentFinderConditionId: not null }) + _contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = + (quest.Id, dutyStep.Step); + else if (dutyStep.Step.InteractionType == EInteractionType.SinglePlayerDuty && + _territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, + dutyStep.Step.SinglePlayerDutyIndex, out var cfcData)) + _contentFinderConditionIds[cfcData.ContentFinderConditionId] = (quest.Id, dutyStep.Step); } } } diff --git a/Questionable/Controller/Steps/Common/SendNotification.cs b/Questionable/Controller/Steps/Common/SendNotification.cs index 8bb4fa803..b2d146f00 100644 --- a/Questionable/Controller/Steps/Common/SendNotification.cs +++ b/Questionable/Controller/Steps/Common/SendNotification.cs @@ -14,6 +14,7 @@ internal static class SendNotification internal sealed class Factory( AutomatonIpc automatonIpc, AutoDutyIpc autoDutyIpc, + BossModIpc bossModIpc, TerritoryData territoryData) : SimpleTaskFactory { public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step) @@ -26,7 +27,7 @@ internal static class SendNotification new Task(step.InteractionType, step.ContentFinderConditionId.HasValue ? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name : step.Comment), - EInteractionType.SinglePlayerDuty when !step.BossModEnabled => + EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled) => new Task(step.InteractionType, quest.Info.Name), _ => null, }; diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs index 4cd79cec9..b8bf39fe6 100644 --- a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; using Questionable.Controller.Steps.Shared; using Questionable.Data; using Questionable.External; @@ -11,20 +12,23 @@ namespace Questionable.Controller.Steps.Interactions; internal static class SinglePlayerDuty { - internal sealed class Factory : ITaskFactory + internal sealed class Factory( + BossModIpc bossModIpc, + TerritoryData territoryData) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.SinglePlayerDuty) yield break; - if (step.BossModEnabled) + if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled)) { - ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId); + if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out var cfcData)) + throw new TaskException("Failed to get content finder condition for solo instance"); - yield return new StartSinglePlayerDuty(step.ContentFinderConditionId.Value); + yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId); yield return new EnableAi(); - yield return new WaitSinglePlayerDuty(step.ContentFinderConditionId.Value); + yield return new WaitSinglePlayerDuty(cfcData.ContentFinderConditionId); yield return new DisableAi(); yield return new WaitAtEnd.WaitNextStepOrSequence(); } @@ -36,19 +40,13 @@ internal static class SinglePlayerDuty public override string ToString() => $"Wait(BossMod, entered instance {ContentFinderConditionId})"; } - internal sealed class StartSinglePlayerDutyExecutor( - TerritoryData territoryData, - IClientState clientState) : TaskExecutor + internal sealed class StartSinglePlayerDutyExecutor : TaskExecutor { protected override bool Start() => true; - public override ETaskResult Update() + public override unsafe ETaskResult Update() { - if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId, - out var cfcData)) - throw new TaskException("Failed to get territory ID for content finder condition"); - - return clientState.TerritoryType == cfcData.TerritoryId + return GameMain.Instance()->CurrentContentFinderConditionId == Task.ContentFinderConditionId ? ETaskResult.TaskComplete : ETaskResult.StillRunning; } @@ -81,19 +79,13 @@ internal static class SinglePlayerDuty } internal sealed class WaitSinglePlayerDutyExecutor( - TerritoryData territoryData, - IClientState clientState, BossModIpc bossModIpc) : TaskExecutor, IStoppableTaskExecutor { protected override bool Start() => true; - public override ETaskResult Update() + public override unsafe ETaskResult Update() { - if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId, - out var cfcData)) - throw new TaskException("Failed to get territory ID for content finder condition"); - - return clientState.TerritoryType != cfcData.TerritoryId + return GameMain.Instance()->CurrentContentFinderConditionId != Task.ContentFinderConditionId ? ETaskResult.TaskComplete : ETaskResult.StillRunning; } diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index 59d108cdd..1476ed3c4 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -21,7 +21,8 @@ internal static class WaitAtEnd IClientState clientState, ICondition condition, TerritoryData territoryData, - AutoDutyIpc autoDutyIpc) + AutoDutyIpc autoDutyIpc, + BossModIpc bossModIpc) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) @@ -53,7 +54,7 @@ internal static class WaitAtEnd return [new WaitNextStepOrSequence()]; case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled): - case EInteractionType.SinglePlayerDuty when !step.BossModEnabled: + case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled): return [new EndAutomation()]; case EInteractionType.WalkTo: diff --git a/Questionable/Data/JournalData.cs b/Questionable/Data/JournalData.cs index c2983fe56..80f7560bd 100644 --- a/Questionable/Data/JournalData.cs +++ b/Questionable/Data/JournalData.cs @@ -23,17 +23,17 @@ internal sealed class JournalData var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1, new uint[] { 108, 109 }.Concat(limsaStart.QuestRedoParam.Select(x => x.Quest.RowId)) .Where(x => x != 0) - .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) + .Select(x => questData.GetQuestInfo(QuestId.FromRowId(x))) .ToList()); var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1, new uint[] { 85, 123, 124 }.Concat(gridaniaStart.QuestRedoParam.Select(x => x.Quest.RowId)) .Where(x => x != 0) - .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) + .Select(x => questData.GetQuestInfo(QuestId.FromRowId(x))) .ToList()); var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1, new uint[] { 568, 569, 570 }.Concat(uldahStart.QuestRedoParam.Select(x => x.Quest.RowId)) .Where(x => x != 0) - .Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF)))) + .Select(x => questData.GetQuestInfo(QuestId.FromRowId(x))) .ToList()); genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]); genres.Single(x => x.Id == 1) diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs index 820ae6565..c57ada43a 100644 --- a/Questionable/Data/TerritoryData.cs +++ b/Questionable/Data/TerritoryData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -7,6 +8,7 @@ using Dalamud.Game; using Dalamud.Plugin.Services; using Dalamud.Utility; using Lumina.Excel.Sheets; +using Questionable.Model.Questing; namespace Questionable.Data; @@ -17,6 +19,7 @@ internal sealed class TerritoryData private readonly ImmutableDictionary _dutyTerritories; private readonly ImmutableDictionary _instanceNames; private readonly ImmutableDictionary _contentFinderConditions; + private readonly ImmutableDictionary<(ElementId QuestId, byte Index), uint> _questsToCfc; public TerritoryData(IDataManager dataManager) { @@ -48,6 +51,13 @@ internal sealed class TerritoryData .Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType is 1 or 5 && x.ContentType.RowId != 6) .Select(x => new ContentFinderConditionData(x, dataManager.Language)) .ToImmutableDictionary(x => x.ContentFinderConditionId, x => x); + + _questsToCfc = dataManager.GetExcelSheet() + .Where(x => x is { RowId: > 0, IssuerLocation.RowId: > 0 }) + .SelectMany(GetQuestBattles) + .Select(x => (x.QuestId, x.Index, + CfcId: LookupContentFinderConditionForQuestBattle(dataManager, x.QuestBattleId))) + .ToImmutableDictionary(x => (x.QuestId, x.Index), x => x.CfcId); } public string? GetName(ushort territoryId) => _territoryNames.GetValueOrDefault(territoryId); @@ -77,6 +87,18 @@ internal sealed class TerritoryData [NotNullWhen(true)] out ContentFinderConditionData? contentFinderConditionData) => _contentFinderConditions.TryGetValue(cfcId, out contentFinderConditionData); + public bool TryGetContentFinderConditionForSoloInstance(ElementId questId, byte index, + [NotNullWhen(true)] out ContentFinderConditionData? contentFinderConditionData) + { + if (_questsToCfc.TryGetValue((questId, index), out uint cfcId)) + return _contentFinderConditions.TryGetValue(cfcId, out contentFinderConditionData); + else + { + contentFinderConditionData = null; + return false; + } + } + private static string FixName(string name, ClientLanguage language) { if (string.IsNullOrEmpty(name) || language != ClientLanguage.English) @@ -85,6 +107,27 @@ internal sealed class TerritoryData return string.Concat(name[0].ToString().ToUpper(CultureInfo.InvariantCulture), name.AsSpan(1)); } + private static IEnumerable<(ElementId QuestId, byte Index, uint QuestBattleId)> GetQuestBattles(Quest quest) + { + foreach (Quest.QuestParamsStruct t in quest.QuestParams) + { + if (t.ScriptInstruction == "QUESTBATTLE0") + yield return (QuestId.FromRowId(quest.RowId), 0, t.ScriptArg); + else if (t.ScriptInstruction == "QUESTBATTLE1") + yield return (QuestId.FromRowId(quest.RowId), 1, t.ScriptArg); + else if (t.ScriptInstruction.IsEmpty) + break; + } + } + + private static uint LookupContentFinderConditionForQuestBattle(IDataManager dataManager, uint questBattleId) + { + if (questBattleId >= 5000) + return dataManager.GetExcelSheet().GetRow(questBattleId).Order; + else + return dataManager.GetExcelSheet().GetRow(questBattleId).Unknown0; + } + public sealed record ContentFinderConditionData( uint ContentFinderConditionId, string Name, diff --git a/Questionable/External/AutoDutyIpc.cs b/Questionable/External/AutoDutyIpc.cs index 8d9a84828..67ea9fbdd 100644 --- a/Questionable/External/AutoDutyIpc.cs +++ b/Questionable/External/AutoDutyIpc.cs @@ -31,7 +31,7 @@ internal sealed class AutoDutyIpc _stop = pluginInterface.GetIpcSubscriber("AutoDuty.Stop"); } - public bool IsConfiguredToRunContent(uint? cfcId, bool autoDutyEnabled) + public bool IsConfiguredToRunContent(uint? cfcId, bool enabledByDefault) { if (cfcId == null) return false; @@ -46,7 +46,7 @@ internal sealed class AutoDutyIpc _territoryData.TryGetContentFinderCondition(cfcId.Value, out _)) return true; - return autoDutyEnabled && HasPath(cfcId.Value); + return enabledByDefault && HasPath(cfcId.Value); } public bool HasPath(uint cfcId) diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs index d1d02f794..82a3de68d 100644 --- a/Questionable/External/BossModIpc.cs +++ b/Questionable/External/BossModIpc.cs @@ -2,22 +2,33 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Services; +using Questionable.Data; +using Questionable.Model.Questing; namespace Questionable.External; internal sealed class BossModIpc { - private readonly ICommandManager _commandManager; private const string Name = "BossMod"; + private readonly Configuration _configuration; + private readonly ICommandManager _commandManager; + private readonly TerritoryData _territoryData; private readonly ICallGateSubscriber _getPreset; private readonly ICallGateSubscriber _createPreset; private readonly ICallGateSubscriber _setPreset; private readonly ICallGateSubscriber _clearPreset; - public BossModIpc(IDalamudPluginInterface pluginInterface, ICommandManager commandManager) + public BossModIpc( + IDalamudPluginInterface pluginInterface, + Configuration configuration, + ICommandManager commandManager, + TerritoryData territoryData) { + _configuration = configuration; _commandManager = commandManager; + _territoryData = territoryData; + _getPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Get"); _createPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.Create"); _setPreset = pluginInterface.GetIpcSubscriber($"{Name}.Presets.SetActive"); @@ -70,4 +81,21 @@ internal sealed class BossModIpc _commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles false"); ClearPreset(); } + + public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault) + { + if (!_configuration.SoloDuties.RunSoloInstancesWithBossMod) + return false; + + if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData)) + return false; + + if (_configuration.SoloDuties.BlacklistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) + return false; + + if (_configuration.SoloDuties.WhitelistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) + return true; + + return enabledByDefault; + } } diff --git a/Questionable/External/QuestionableIpc.cs b/Questionable/External/QuestionableIpc.cs index 2b80d66a8..0ff2b1836 100644 --- a/Questionable/External/QuestionableIpc.cs +++ b/Questionable/External/QuestionableIpc.cs @@ -41,10 +41,10 @@ internal sealed class QuestionableIpc : IDisposable eventInfoComponent.GetCurrentlyActiveEventQuests().Select(q => q.ToString()).ToList()); _startQuest = pluginInterface.GetIpcProvider(IpcStartQuest); - _startQuest.RegisterFunc((string questId) => StartQuest(questController, questRegistry, questId, false)); + _startQuest.RegisterFunc((questId) => StartQuest(questController, questRegistry, questId, false)); _startSingleQuest = pluginInterface.GetIpcProvider(IpcStartSingleQuest); - _startSingleQuest.RegisterFunc((string questId) => StartQuest(questController, questRegistry, questId, true)); + _startSingleQuest.RegisterFunc((questId) => StartQuest(questController, questRegistry, questId, true)); } private static bool StartQuest(QuestController qc, QuestRegistry qr, string questId, bool single) @@ -63,6 +63,7 @@ internal sealed class QuestionableIpc : IDisposable public void Dispose() { + _startSingleQuest.UnregisterFunc(); _startQuest.UnregisterFunc(); _getCurrentlyActiveEventQuests.UnregisterFunc(); _getCurrentQuestId.UnregisterFunc(); diff --git a/Questionable/Model/QuestInfo.cs b/Questionable/Model/QuestInfo.cs index 5f261275d..b7efde877 100644 --- a/Questionable/Model/QuestInfo.cs +++ b/Questionable/Model/QuestInfo.cs @@ -7,6 +7,7 @@ using Lumina.Excel.Sheets; using Questionable.Model.Questing; using ExcelQuest = Lumina.Excel.Sheets.Quest; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; +using QQuestId = Questionable.Model.Questing.QuestId; namespace Questionable.Model; @@ -14,7 +15,7 @@ internal sealed class QuestInfo : IQuestInfo { public QuestInfo(ExcelQuest quest, uint newGamePlusChapter, byte startingCity, JournalGenreOverrides journalGenreOverrides) { - QuestId = new QuestId((ushort)(quest.RowId & 0xFFFF)); + QuestId = QQuestId.FromRowId(quest.RowId); string suffix = QuestId.Value switch { @@ -41,15 +42,15 @@ internal sealed class QuestInfo : IQuestInfo PreviousQuests = new List { - new(ReplaceOldQuestIds((ushort)(quest.PreviousQuest[0].RowId & 0xFFFF)), quest.Unknown7), - new(ReplaceOldQuestIds((ushort)(quest.PreviousQuest[1].RowId & 0xFFFF))), - new(ReplaceOldQuestIds((ushort)(quest.PreviousQuest[2].RowId & 0xFFFF))) + new(ReplaceOldQuestIds(QQuestId.FromRowId(quest.PreviousQuest[0].RowId)), quest.Unknown7), + new(ReplaceOldQuestIds(QQuestId.FromRowId(quest.PreviousQuest[1].RowId))), + new(ReplaceOldQuestIds(QQuestId.FromRowId(quest.PreviousQuest[2].RowId))) } .Where(x => x.QuestId.Value != 0) .ToImmutableList(); PreviousQuestJoin = (EQuestJoin)quest.PreviousQuestJoin; QuestLocks = quest.QuestLock - .Select(x => new QuestId((ushort)(x.RowId & 0xFFFFF))) + .Select(x => QQuestId.FromRowId(x.RowId)) .Where(x => x.Value != 0) .ToImmutableList(); QuestLockJoin = (EQuestJoin)quest.QuestLockJoin; @@ -85,13 +86,13 @@ internal sealed class QuestInfo : IQuestInfo Expansion = (EExpansionVersion)quest.Expansion.RowId; } - private static QuestId ReplaceOldQuestIds(ushort questId) + private static QuestId ReplaceOldQuestIds(QuestId questId) { - return new QuestId(questId switch + return questId.Value switch { - 524 => 4522, + 524 => new QuestId(4522), _ => questId, - }); + }; } public ElementId QuestId { get; } diff --git a/Questionable/Model/SatisfactionSupplyInfo.cs b/Questionable/Model/SatisfactionSupplyInfo.cs index 21c929364..b1bba4a4b 100644 --- a/Questionable/Model/SatisfactionSupplyInfo.cs +++ b/Questionable/Model/SatisfactionSupplyInfo.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using LLib.GameData; using Lumina.Excel.Sheets; using Questionable.Model.Questing; +using QQuestId = Questionable.Model.Questing.QuestId; namespace Questionable.Model; @@ -16,7 +17,7 @@ internal sealed class SatisfactionSupplyInfo : IQuestInfo Level = npc.LevelUnlock; SortKey = QuestId.Value; Expansion = (EExpansionVersion)npc.QuestRequired.Value.Expansion.RowId; - PreviousQuests = [new PreviousQuestInfo(new QuestId((ushort)(npc.QuestRequired.RowId & 0xFFFF)))]; + PreviousQuests = [new PreviousQuestInfo(QQuestId.FromRowId(npc.QuestRequired.RowId))]; } public ElementId QuestId { get; } diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 28d1f66b6..ccd694af4 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -311,6 +311,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); } diff --git a/Questionable/Validation/EIssueType.cs b/Questionable/Validation/EIssueType.cs index 755124814..4d41f9970 100644 --- a/Questionable/Validation/EIssueType.cs +++ b/Questionable/Validation/EIssueType.cs @@ -18,4 +18,5 @@ public enum EIssueType InvalidAethernetShortcut, InvalidExcelRef, ClassQuestWithoutAetheryteShortcut, + DuplicateSinglePlayerInstance, } diff --git a/Questionable/Validation/Validators/UniqueSinglePlayerInstanceValidator.cs b/Questionable/Validation/Validators/UniqueSinglePlayerInstanceValidator.cs new file mode 100644 index 000000000..684e876b4 --- /dev/null +++ b/Questionable/Validation/Validators/UniqueSinglePlayerInstanceValidator.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Validation.Validators; + +internal sealed class UniqueSinglePlayerInstanceValidator : IQuestValidator +{ + public IEnumerable Validate(Quest quest) + { + var singlePlayerInstances = quest.AllSteps() + .Where(x => x.Step.InteractionType == EInteractionType.SinglePlayerDuty) + .Select(x => (x.Sequence, x.StepId, x.Step.SinglePlayerDutyIndex)) + .ToList(); + if (singlePlayerInstances.DistinctBy(x => x.SinglePlayerDutyIndex).Count() < singlePlayerInstances.Count) + { + foreach (var singlePlayerInstance in singlePlayerInstances) + { + yield return new ValidationIssue + { + ElementId = quest.Id, + Sequence = (byte)singlePlayerInstance.Sequence.Sequence, + Step = singlePlayerInstance.StepId, + Type = EIssueType.DuplicateSinglePlayerInstance, + Severity = EIssueSeverity.Error, + Description = $"Duplicate singleplayer duty index: {singlePlayerInstance.SinglePlayerDutyIndex}", + }; + } + } + } +} diff --git a/Questionable/Windows/QuestSelectionWindow.cs b/Questionable/Windows/QuestSelectionWindow.cs index a53b79e9d..481c5f1a0 100644 --- a/Questionable/Windows/QuestSelectionWindow.cs +++ b/Questionable/Windows/QuestSelectionWindow.cs @@ -119,7 +119,7 @@ internal sealed class QuestSelectionWindow : LWindow foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers) { - QuestId questId = new QuestId((ushort)(unacceptedQuest.ObjectiveId & 0xFFFF)); + QuestId questId = QuestId.FromRowId(unacceptedQuest.ObjectiveId); if (_quests.All(q => q.QuestId != questId)) _quests.Add(_questData.GetQuestInfo(questId)); } diff --git a/global.json b/global.json new file mode 100644 index 000000000..2ddda36c2 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file From 38206478275f08edb2600301556587056b691e03 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Thu, 20 Feb 2025 21:16:48 +0100 Subject: [PATCH 06/18] Restructure config window --- Questionable/QuestionablePlugin.cs | 6 + .../ConfigComponents/ConfigComponent.cs | 64 +++ .../ConfigComponents/DebugConfigComponent.cs | 49 ++ .../ConfigComponents/DutyConfigComponent.cs | 249 +++++++++ .../GeneralConfigComponent.cs | 114 +++++ .../NotificationConfigComponent.cs | 80 +++ Questionable/Windows/ConfigWindow.cs | 479 +----------------- 7 files changed, 584 insertions(+), 457 deletions(-) create mode 100644 Questionable/Windows/ConfigComponents/ConfigComponent.cs create mode 100644 Questionable/Windows/ConfigComponents/DebugConfigComponent.cs create mode 100644 Questionable/Windows/ConfigComponents/DutyConfigComponent.cs create mode 100644 Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs create mode 100644 Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index ccd694af4..0f776eb62 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -26,6 +26,7 @@ using Questionable.Functions; using Questionable.Validation; using Questionable.Validation.Validators; using Questionable.Windows; +using Questionable.Windows.ConfigComponents; using Questionable.Windows.JournalComponents; using Questionable.Windows.QuestComponents; using Action = Questionable.Controller.Steps.Interactions.Action; @@ -298,6 +299,11 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } private static void AddQuestValidators(ServiceCollection serviceCollection) diff --git a/Questionable/Windows/ConfigComponents/ConfigComponent.cs b/Questionable/Windows/ConfigComponents/ConfigComponent.cs new file mode 100644 index 000000000..0a5be627d --- /dev/null +++ b/Questionable/Windows/ConfigComponents/ConfigComponent.cs @@ -0,0 +1,64 @@ +using System.Text; +using Dalamud.Game.Text; +using Dalamud.Plugin; +using ImGuiNET; + +namespace Questionable.Windows.ConfigComponents; + +internal abstract class ConfigComponent +{ + protected const string DutyClipboardSeparator = ";"; + protected const string DutyWhitelistPrefix = "+"; + protected const string DutyBlacklistPrefix = "-"; + + protected readonly string[] SupportedCfcOptions = + [ + $"{SeIconChar.Circle.ToIconChar()} Enabled (Default)", + $"{SeIconChar.Circle.ToIconChar()} Enabled", + $"{SeIconChar.Cross.ToIconChar()} Disabled" + ]; + + protected readonly string[] UnsupportedCfcOptions = + [ + $"{SeIconChar.Cross.ToIconChar()} Disabled (Default)", + $"{SeIconChar.Circle.ToIconChar()} Enabled", + $"{SeIconChar.Cross.ToIconChar()} Disabled" + ]; + + private readonly IDalamudPluginInterface _pluginInterface; + + protected ConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration) + { + _pluginInterface = pluginInterface; + Configuration = configuration; + } + + protected Configuration Configuration { get; } + + public abstract void DrawTab(); + + protected void Save() => _pluginInterface.SavePluginConfig(Configuration); + + protected static string FormatLevel(int level) + { + if (level == 0) + return string.Empty; + + return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}"; + } + + /// + /// The default implementation for throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text. + /// + protected unsafe string? GetClipboardText() + { + byte* ptr = ImGuiNative.igGetClipboardText(); + if (ptr == null) + return null; + + int byteCount = 0; + while (ptr[byteCount] != 0) + ++byteCount; + return Encoding.UTF8.GetString(ptr, byteCount); + } +} diff --git a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs new file mode 100644 index 000000000..7d89efd07 --- /dev/null +++ b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs @@ -0,0 +1,49 @@ +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using ImGuiNET; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class DebugConfigComponent : ConfigComponent +{ + public DebugConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration) + : base(pluginInterface, configuration) + { + } + + public override void DrawTab() + { + 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."); + + ImGui.Separator(); + + bool debugOverlay = Configuration.Advanced.DebugOverlay; + if (ImGui.Checkbox("Enable debug overlay", ref debugOverlay)) + { + Configuration.Advanced.DebugOverlay = debugOverlay; + Save(); + } + + bool neverFly = Configuration.Advanced.NeverFly; + if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly)) + { + Configuration.Advanced.NeverFly = neverFly; + Save(); + } + + bool additionalStatusInformation = Configuration.Advanced.AdditionalStatusInformation; + if (ImGui.Checkbox("Draw additional status information", ref additionalStatusInformation)) + { + Configuration.Advanced.AdditionalStatusInformation = additionalStatusInformation; + Save(); + } + + ImGui.EndTabItem(); + } +} diff --git a/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs new file mode 100644 index 000000000..ffb6538a7 --- /dev/null +++ b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using System.Text; +using Dalamud.Game.Text; +using Dalamud.Interface; +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 Lumina.Excel.Sheets; +using Questionable.Controller; +using Questionable.Data; +using Questionable.External; +using Questionable.Model; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class DutyConfigComponent : ConfigComponent +{ + private const string DutyClipboardPrefix = "qst:duty:"; + + private readonly QuestRegistry _questRegistry; + private readonly AutoDutyIpc _autoDutyIpc; + private readonly Dictionary> _contentFinderConditionNames; + + public DutyConfigComponent( + IDalamudPluginInterface pluginInterface, + Configuration configuration, + IDataManager dataManager, + QuestRegistry questRegistry, + AutoDutyIpc autoDutyIpc, + TerritoryData territoryData) + : base(pluginInterface, configuration) + { + _questRegistry = questRegistry; + _autoDutyIpc = autoDutyIpc; + + _contentFinderConditionNames = dataManager.GetExcelSheet() + .Where(x => x is { RowId: > 0, Unknown16: false }) + .OrderBy(x => x.Unknown15) // SortKey for the support UI + .Select(x => x.Content.ValueNullable) + .Where(x => x != null) + .Select(x => x!.Value) + .Select(x => new + { + Expansion = (EExpansionVersion)x.TerritoryType.Value.ExVersion.RowId, + CfcId = x.RowId, + Name = territoryData.GetContentFinderCondition(x.RowId)?.Name ?? "?", + TerritoryId = x.TerritoryType.RowId, + ContentType = x.ContentType.RowId, + Level = x.ClassJobLevelRequired, + x.SortKey + }) + .GroupBy(x => x.Expansion) + .ToDictionary(x => x.Key, + x => x + .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, + $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}")) + .ToList()); + } + + public override void DrawTab() + { + using var tab = ImRaii.TabItem("Duties"); + if (!tab) + return; + + bool runInstancedContentWithAutoDuty = Configuration.Duties.RunInstancedContentWithAutoDuty; + if (ImGui.Checkbox("Run instanced content with AutoDuty and BossMod", ref runInstancedContentWithAutoDuty)) + { + Configuration.Duties.RunInstancedContentWithAutoDuty = runInstancedContentWithAutoDuty; + Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "The combat module used for this is configured by AutoDuty, ignoring whichever selection you've made in Questionable's \"General\" configuration."); + + ImGui.Separator(); + + using (ImRaii.Disabled(!runInstancedContentWithAutoDuty)) + { + ImGui.Text( + "Questionable includes a default list of duties that work if AutoDuty and BossMod are installed."); + + ImGui.Text( + "The included list of duties can change with each update, and is based on the following spreadsheet:"); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.GlobeEurope, "Open AutoDuty spreadsheet")) + Util.OpenLink( + "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0"); + + ImGui.Separator(); + ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:"); + + DrawConfigTable(runInstancedContentWithAutoDuty); + DrawClipboardButtons(); + + ImGui.SameLine(); + + using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) + { + if (ImGui.Button("Reset to default")) + { + Configuration.Duties.WhitelistedDutyCfcIds.Clear(); + Configuration.Duties.BlacklistedDutyCfcIds.Clear(); + Save(); + } + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Hold CTRL to enable this button."); + } + } + + private void DrawConfigTable(bool runInstancedContentWithAutoDuty) + { + using var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true); + if (!child) + return; + + foreach (EExpansionVersion expansion in Enum.GetValues()) + { + if (ImGui.CollapsingHeader(expansion.ToString())) + { + using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f); + + if (_contentFinderConditionNames.TryGetValue(expansion, out var cfcNames)) + { + foreach (var (cfcId, territoryId, name) in cfcNames) + { + if (_questRegistry.TryGetDutyByContentFinderConditionId(cfcId, + out bool autoDutyEnabledByDefault)) + { + ImGui.TableNextRow(); + + string[] labels = autoDutyEnabledByDefault + ? SupportedCfcOptions + : UnsupportedCfcOptions; + int value = 0; + if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId)) + value = 1; + if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId)) + value = 2; + + if (ImGui.TableNextColumn()) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(name); + if (ImGui.IsItemHovered() && + Configuration.Advanced.AdditionalStatusInformation) + { + using var tooltip = ImRaii.Tooltip(); + if (tooltip) + { + ImGui.TextUnformatted(name); + ImGui.Separator(); + ImGui.BulletText($"TerritoryId: {territoryId}"); + ImGui.BulletText($"ContentFinderConditionId: {cfcId}"); + } + } + + if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(cfcId)) + ImGuiComponents.HelpMarker("This duty is not supported by AutoDuty", + FontAwesomeIcon.Times, ImGuiColors.DalamudRed); + } + + if (ImGui.TableNextColumn()) + { + using var _ = ImRaii.PushId($"##Dungeon{cfcId}"); + ImGui.SetNextItemWidth(200); + if (ImGui.Combo(string.Empty, ref value, labels, labels.Length)) + { + Configuration.Duties.WhitelistedDutyCfcIds.Remove(cfcId); + Configuration.Duties.BlacklistedDutyCfcIds.Remove(cfcId); + + if (value == 1) + Configuration.Duties.WhitelistedDutyCfcIds.Add(cfcId); + else if (value == 2) + Configuration.Duties.BlacklistedDutyCfcIds.Add(cfcId); + + Save(); + } + } + } + } + } + } + } + } + } + + private void DrawClipboardButtons() + { + using (ImRaii.Disabled(Configuration.Duties.WhitelistedDutyCfcIds.Count + + Configuration.Duties.BlacklistedDutyCfcIds.Count == 0)) + { + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, "Export to clipboard")) + { + var whitelisted = + Configuration.Duties.WhitelistedDutyCfcIds.Select(x => $"{DutyWhitelistPrefix}{x}"); + var blacklisted = + Configuration.Duties.BlacklistedDutyCfcIds.Select(x => $"{DutyBlacklistPrefix}{x}"); + string text = DutyClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes( + string.Join(DutyClipboardSeparator, whitelisted.Concat(blacklisted)))); + ImGui.SetClipboardText(text); + } + } + + ImGui.SameLine(); + + string? clipboardText = GetClipboardText(); + using (ImRaii.Disabled(clipboardText == null || + !clipboardText.StartsWith(DutyClipboardPrefix, StringComparison.InvariantCulture))) + { + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Paste, "Import from Clipboard")) + { + clipboardText = clipboardText!.Substring(DutyClipboardPrefix.Length); + string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText)); + + Configuration.Duties.WhitelistedDutyCfcIds.Clear(); + Configuration.Duties.BlacklistedDutyCfcIds.Clear(); + foreach (string part in text.Split(DutyClipboardSeparator)) + { + if (part.StartsWith(DutyWhitelistPrefix, StringComparison.InvariantCulture) && + uint.TryParse(part.AsSpan(DutyWhitelistPrefix.Length), CultureInfo.InvariantCulture, + out uint whitelistedCfcId)) + Configuration.Duties.WhitelistedDutyCfcIds.Add(whitelistedCfcId); + + if (part.StartsWith(DutyBlacklistPrefix, StringComparison.InvariantCulture) && + uint.TryParse(part.AsSpan(DutyBlacklistPrefix.Length), CultureInfo.InvariantCulture, + out uint blacklistedCfcId)) + Configuration.Duties.WhitelistedDutyCfcIds.Add(blacklistedCfcId); + } + } + } + } + + private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name); +} diff --git a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs new file mode 100644 index 000000000..e58ce989e --- /dev/null +++ b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using ImGuiNET; +using Lumina.Excel.Sheets; +using Questionable.Controller; +using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class GeneralConfigComponent : ConfigComponent +{ + private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")]; + + private readonly CombatController _combatController; + + private readonly uint[] _mountIds; + private readonly string[] _mountNames; + private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"]; + + private readonly string[] _grandCompanyNames = + ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"]; + + public GeneralConfigComponent( + IDalamudPluginInterface pluginInterface, + Configuration configuration, + CombatController combatController, + IDataManager dataManager) + : base(pluginInterface, configuration) + { + _combatController = combatController; + + var mounts = dataManager.GetExcelSheet() + .Where(x => x is { RowId: > 0, Icon: > 0 }) + .Select(x => (MountId: x.RowId, Name: x.Singular.ToString())) + .Where(x => !string.IsNullOrEmpty(x.Name)) + .OrderBy(x => x.Name) + .ToList(); + _mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray(); + _mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray(); + } + + public override void DrawTab() + { + using var tab = ImRaii.TabItem("General"); + if (!tab) + return; + + using (ImRaii.Disabled(_combatController.IsRunning)) + { + int selectedCombatModule = (int)Configuration.General.CombatModule; + if (ImGui.Combo("Preferred Combat Module", ref selectedCombatModule, _combatModuleNames, + _combatModuleNames.Length)) + { + Configuration.General.CombatModule = (Configuration.ECombatModule)selectedCombatModule; + Save(); + } + } + + int selectedMount = Array.FindIndex(_mountIds, x => x == Configuration.General.MountId); + if (selectedMount == -1) + { + selectedMount = 0; + 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(); + } + + 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 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(); + } + } +} diff --git a/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs b/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs new file mode 100644 index 000000000..d0a4ba0dd --- /dev/null +++ b/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using Dalamud.Game.Text; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Dalamud.Utility; +using ImGuiNET; +using Questionable.External; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class NotificationConfigComponent : ConfigComponent +{ + private readonly NotificationMasterIpc _notificationMasterIpc; + + public NotificationConfigComponent( + IDalamudPluginInterface pluginInterface, + Configuration configuration, + NotificationMasterIpc notificationMasterIpc) + : base(pluginInterface, configuration) + { + _notificationMasterIpc = notificationMasterIpc; + } + + public override void DrawTab() + { + 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() + .Where(x => x != XivChatType.StandardEmote) + .ToArray(); + var selectedChatType = Array.IndexOf(xivChatTypes, Configuration.Notifications.ChatType); + string[] chatTypeNames = xivChatTypes + .Select(t => t.GetAttribute()?.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(); + } + } + } + } + } +} diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index 131f3726d..e2ac6c314 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -1,485 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Numerics; -using System.Text; -using Dalamud.Game.Text; -using Dalamud.Interface; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Components; -using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; -using Dalamud.Plugin.Services; -using Dalamud.Utility; using ImGuiNET; using LLib.ImGui; -using Lumina.Excel.Sheets; -using Questionable.Controller; -using Questionable.Data; -using Questionable.External; -using Questionable.Model; -using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; +using Questionable.Windows.ConfigComponents; namespace Questionable.Windows; internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig { - private const string DutyClipboardPrefix = "qst:duty:"; - private const string DutyClipboardSeparator = ";"; - private const string DutyWhitelistPrefix = "+"; - private const string DutyBlacklistPrefix = "-"; - - private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")]; - private readonly IDalamudPluginInterface _pluginInterface; - private readonly NotificationMasterIpc _notificationMasterIpc; + private readonly GeneralConfigComponent _generalConfigComponent; + private readonly DutyConfigComponent _dutyConfigComponent; + private readonly NotificationConfigComponent _notificationConfigComponent; + private readonly DebugConfigComponent _debugConfigComponent; private readonly Configuration _configuration; - private readonly CombatController _combatController; - private readonly QuestRegistry _questRegistry; - private readonly AutoDutyIpc _autoDutyIpc; - private readonly uint[] _mountIds; - private readonly string[] _mountNames; - - private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"]; - - private readonly string[] _grandCompanyNames = - ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"]; - - private readonly string[] _supportedCfcOptions = - [ - $"{SeIconChar.Circle.ToIconChar()} Enabled (Default)", - $"{SeIconChar.Circle.ToIconChar()} Enabled", - $"{SeIconChar.Cross.ToIconChar()} Disabled" - ]; - - private readonly string[] _unsupportedCfcOptions = - [ - $"{SeIconChar.Cross.ToIconChar()} Disabled (Default)", - $"{SeIconChar.Circle.ToIconChar()} Enabled", - $"{SeIconChar.Cross.ToIconChar()} Disabled" - ]; - - private readonly Dictionary> _contentFinderConditionNames; - - public ConfigWindow(IDalamudPluginInterface pluginInterface, - NotificationMasterIpc notificationMasterIpc, - Configuration configuration, - IDataManager dataManager, - CombatController combatController, - TerritoryData territoryData, - QuestRegistry questRegistry, - AutoDutyIpc autoDutyIpc) + public ConfigWindow( + IDalamudPluginInterface pluginInterface, + GeneralConfigComponent generalConfigComponent, + DutyConfigComponent dutyConfigComponent, + NotificationConfigComponent notificationConfigComponent, + DebugConfigComponent debugConfigComponent, + Configuration configuration) : base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize) { _pluginInterface = pluginInterface; - _notificationMasterIpc = notificationMasterIpc; + _generalConfigComponent = generalConfigComponent; + _dutyConfigComponent = dutyConfigComponent; + _notificationConfigComponent = notificationConfigComponent; + _debugConfigComponent = debugConfigComponent; _configuration = configuration; - _combatController = combatController; - _questRegistry = questRegistry; - _autoDutyIpc = autoDutyIpc; - - var mounts = dataManager.GetExcelSheet() - .Where(x => x is { RowId: > 0, Icon: > 0 }) - .Select(x => (MountId: x.RowId, Name: x.Singular.ToString())) - .Where(x => !string.IsNullOrEmpty(x.Name)) - .OrderBy(x => x.Name) - .ToList(); - _mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray(); - _mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray(); - - _contentFinderConditionNames = dataManager.GetExcelSheet() - .Where(x => x is { RowId: > 0, Unknown16: false }) - .OrderBy(x => x.Unknown15) // SortKey for the support UI - .Select(x => x.Content.ValueNullable) - .Where(x => x != null) - .Select(x => x!.Value) - .Select(x => new - { - Expansion = (EExpansionVersion)x.TerritoryType.Value.ExVersion.RowId, - CfcId = x.RowId, - Name = territoryData.GetContentFinderCondition(x.RowId)?.Name ?? "?", - TerritoryId = x.TerritoryType.RowId, - ContentType = x.ContentType.RowId, - Level = x.ClassJobLevelRequired, - x.SortKey - }) - .GroupBy(x => x.Expansion) - .ToDictionary(x => x.Key, - x => x - .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}")) - .ToList()); } public WindowConfig WindowConfig => _configuration.ConfigWindowConfig; - private static string FormatLevel(int level) - { - if (level == 0) - return string.Empty; - - return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}"; - } - public override void Draw() { using var tabBar = ImRaii.TabBar("QuestionableConfigTabs"); if (!tabBar) return; - DrawGeneralTab(); - DrawDutiesTab(); - DrawNotificationsTab(); - DrawAdvancedTab(); + _generalConfigComponent.DrawTab(); + _dutyConfigComponent.DrawTab(); + _notificationConfigComponent.DrawTab(); + _debugConfigComponent.DrawTab(); } - private void DrawGeneralTab() - { - using var tab = ImRaii.TabItem("General"); - if (!tab) - return; - - using (ImRaii.Disabled(_combatController.IsRunning)) - { - int selectedCombatModule = (int)_configuration.General.CombatModule; - if (ImGui.Combo("Preferred Combat Module", ref selectedCombatModule, _combatModuleNames, - _combatModuleNames.Length)) - { - _configuration.General.CombatModule = (Configuration.ECombatModule)selectedCombatModule; - Save(); - } - } - - int selectedMount = Array.FindIndex(_mountIds, x => x == _configuration.General.MountId); - if (selectedMount == -1) - { - selectedMount = 0; - _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(); - } - - 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 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(); - } - } - - private void DrawDutiesTab() - { - using var tab = ImRaii.TabItem("Duties"); - if (!tab) - return; - - bool runInstancedContentWithAutoDuty = _configuration.Duties.RunInstancedContentWithAutoDuty; - if (ImGui.Checkbox("Run instanced content with AutoDuty and BossMod", ref runInstancedContentWithAutoDuty)) - { - _configuration.Duties.RunInstancedContentWithAutoDuty = runInstancedContentWithAutoDuty; - Save(); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "The combat module used for this is configured by AutoDuty, ignoring whichever selection you've made in Questionable's \"General\" configuration."); - - ImGui.Separator(); - - using (ImRaii.Disabled(!runInstancedContentWithAutoDuty)) - { - ImGui.Text( - "Questionable includes a default list of duties that work if AutoDuty and BossMod are installed."); - - ImGui.Text("The included list of duties can change with each update, and is based on the following spreadsheet:"); - if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.GlobeEurope, "Open AutoDuty spreadsheet")) - Util.OpenLink( - "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0"); - - ImGui.Separator(); - ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:"); - - using (var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true)) - { - if (child) - { - foreach (EExpansionVersion expansion in Enum.GetValues()) - { - if (ImGui.CollapsingHeader(expansion.ToString())) - { - using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit); - if (table) - { - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f); - - if (_contentFinderConditionNames.TryGetValue(expansion, out var cfcNames)) - { - foreach (var (cfcId, territoryId, name) in cfcNames) - { - if (_questRegistry.TryGetDutyByContentFinderConditionId(cfcId, - out bool autoDutyEnabledByDefault)) - { - ImGui.TableNextRow(); - - string[] labels = autoDutyEnabledByDefault - ? _supportedCfcOptions - : _unsupportedCfcOptions; - int value = 0; - if (_configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId)) - value = 1; - if (_configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId)) - value = 2; - - if (ImGui.TableNextColumn()) - { - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(name); - if (ImGui.IsItemHovered() && _configuration.Advanced.AdditionalStatusInformation) - { - using var tooltip = ImRaii.Tooltip(); - if (tooltip) - { - ImGui.TextUnformatted(name); - ImGui.Separator(); - ImGui.BulletText($"TerritoryId: {territoryId}"); - ImGui.BulletText($"ContentFinderConditionId: {cfcId}"); - } - } - - if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(cfcId)) - ImGuiComponents.HelpMarker("This duty is not supported by AutoDuty", FontAwesomeIcon.Times, ImGuiColors.DalamudRed); - } - - if (ImGui.TableNextColumn()) - { - using var _ = ImRaii.PushId($"##Dungeon{cfcId}"); - ImGui.SetNextItemWidth(200); - if (ImGui.Combo(string.Empty, ref value, labels, labels.Length)) - { - _configuration.Duties.WhitelistedDutyCfcIds.Remove(cfcId); - _configuration.Duties.BlacklistedDutyCfcIds.Remove(cfcId); - - if (value == 1) - _configuration.Duties.WhitelistedDutyCfcIds.Add(cfcId); - else if (value == 2) - _configuration.Duties.BlacklistedDutyCfcIds.Add(cfcId); - - Save(); - } - } - } - } - } - } - } - } - } - } - - using (ImRaii.Disabled(_configuration.Duties.WhitelistedDutyCfcIds.Count + - _configuration.Duties.BlacklistedDutyCfcIds.Count == 0)) - { - if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, "Export to clipboard")) - { - var whitelisted = - _configuration.Duties.WhitelistedDutyCfcIds.Select(x => $"{DutyWhitelistPrefix}{x}"); - var blacklisted = - _configuration.Duties.BlacklistedDutyCfcIds.Select(x => $"{DutyBlacklistPrefix}{x}"); - string text = DutyClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes( - string.Join(DutyClipboardSeparator, whitelisted.Concat(blacklisted)))); - ImGui.SetClipboardText(text); - } - } - - ImGui.SameLine(); - - string? clipboardText = GetClipboardText(); - using (ImRaii.Disabled(clipboardText == null || !clipboardText.StartsWith(DutyClipboardPrefix, StringComparison.InvariantCulture))) - { - if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Paste, "Import from Clipboard")) - { - clipboardText = clipboardText!.Substring(DutyClipboardPrefix.Length); - string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText)); - - _configuration.Duties.WhitelistedDutyCfcIds.Clear(); - _configuration.Duties.BlacklistedDutyCfcIds.Clear(); - foreach (string part in text.Split(DutyClipboardSeparator)) - { - if (part.StartsWith(DutyWhitelistPrefix, StringComparison.InvariantCulture) && - uint.TryParse(part.AsSpan(DutyWhitelistPrefix.Length), CultureInfo.InvariantCulture, - out uint whitelistedCfcId)) - _configuration.Duties.WhitelistedDutyCfcIds.Add(whitelistedCfcId); - - if (part.StartsWith(DutyBlacklistPrefix, StringComparison.InvariantCulture) && - uint.TryParse(part.AsSpan(DutyBlacklistPrefix.Length), CultureInfo.InvariantCulture, - out uint blacklistedCfcId)) - _configuration.Duties.WhitelistedDutyCfcIds.Add(blacklistedCfcId); - } - } - } - - ImGui.SameLine(); - - using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) - { - if (ImGui.Button("Reset to default")) - { - _configuration.Duties.WhitelistedDutyCfcIds.Clear(); - _configuration.Duties.BlacklistedDutyCfcIds.Clear(); - Save(); - } - } - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("Hold CTRL to enable this button."); - } - } - - 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() - .Where(x => x != XivChatType.StandardEmote) - .ToArray(); - var selectedChatType = Array.IndexOf(xivChatTypes, _configuration.Notifications.ChatType); - string[] chatTypeNames = xivChatTypes - .Select(t => t.GetAttribute()?.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."); - - ImGui.Separator(); - - bool debugOverlay = _configuration.Advanced.DebugOverlay; - if (ImGui.Checkbox("Enable debug overlay", ref debugOverlay)) - { - _configuration.Advanced.DebugOverlay = debugOverlay; - Save(); - } - - bool neverFly = _configuration.Advanced.NeverFly; - if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly)) - { - _configuration.Advanced.NeverFly = neverFly; - Save(); - } - - 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); - - public void SaveWindowConfig() => Save(); - - /// - /// The default implementation for throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text. - /// - private unsafe string? GetClipboardText() - { - byte* ptr = ImGuiNative.igGetClipboardText(); - if (ptr == null) - return null; - - int byteCount = 0; - while (ptr[byteCount] != 0) - ++byteCount; - return Encoding.UTF8.GetString(ptr, byteCount); - } - - private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name); + public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration); } From a75286e92745c03bb1a212f171ebec8f4acc99c7 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 21 Feb 2025 02:17:47 +0100 Subject: [PATCH 07/18] Add UI to enable/disable quest battles --- LLib | 2 +- .../Class Quests/DRG/2914_Dragon Sound.json | 2 +- .../2900_Curious Gorge Meets His Match.json | 2 +- Questionable/Configuration.cs | 8 +- Questionable/Controller/QuestController.cs | 7 +- Questionable/Controller/QuestRegistry.cs | 6 +- Questionable/Data/QuestData.cs | 25 +- Questionable/Data/TerritoryData.cs | 5 + Questionable/External/BossModIpc.cs | 6 +- Questionable/QuestionablePlugin.cs | 3 + Questionable/Validation/EIssueType.cs | 1 + .../SinglePlayerInstanceValidator.cs | 44 ++ .../ConfigComponents/ConfigComponent.cs | 4 +- .../ConfigComponents/DebugConfigComponent.cs | 2 +- .../ConfigComponents/DutyConfigComponent.cs | 43 +- .../GeneralConfigComponent.cs | 2 +- .../NotificationConfigComponent.cs | 2 +- .../SinglePlayerDutyConfigComponent.cs | 464 ++++++++++++++++++ Questionable/Windows/ConfigWindow.cs | 5 + vendor/pictomancy | 2 +- 20 files changed, 588 insertions(+), 47 deletions(-) create mode 100644 Questionable/Validation/Validators/SinglePlayerInstanceValidator.cs create mode 100644 Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs diff --git a/LLib b/LLib index 746d14681..edab3c7ec 160000 --- a/LLib +++ b/LLib @@ -1 +1 @@ -Subproject commit 746d14681baa91132784ab17f8f49671e86ea211 +Subproject commit edab3c7ecc6bd66ac07e3c3938eb9c8a835a1c42 diff --git a/QuestPaths/4.x - Stormblood/Class Quests/DRG/2914_Dragon Sound.json b/QuestPaths/4.x - Stormblood/Class Quests/DRG/2914_Dragon Sound.json index 3561d3c05..fe27abf7a 100644 --- a/QuestPaths/4.x - Stormblood/Class Quests/DRG/2914_Dragon Sound.json +++ b/QuestPaths/4.x - Stormblood/Class Quests/DRG/2914_Dragon Sound.json @@ -34,7 +34,7 @@ "Z": -509.51404 }, "TerritoryId": 622, - "InteractionType": "Interact", + "InteractionType": "SinglePlayerDuty", "Fly": true } ] diff --git a/QuestPaths/4.x - Stormblood/Class Quests/WAR/2900_Curious Gorge Meets His Match.json b/QuestPaths/4.x - Stormblood/Class Quests/WAR/2900_Curious Gorge Meets His Match.json index d17c860b1..12c31df17 100644 --- a/QuestPaths/4.x - Stormblood/Class Quests/WAR/2900_Curious Gorge Meets His Match.json +++ b/QuestPaths/4.x - Stormblood/Class Quests/WAR/2900_Curious Gorge Meets His Match.json @@ -35,7 +35,7 @@ "Z": 686.427 }, "TerritoryId": 135, - "InteractionType": "Interact", + "InteractionType": "SinglePlayerDuty", "AetheryteShortcut": "Lower La Noscea - Moraby Drydocks" } ] diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index 74bb05e6e..a4126ed6f 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -14,7 +14,7 @@ internal sealed class Configuration : IPluginConfiguration public int PluginSetupCompleteVersion { get; set; } public GeneralConfiguration General { get; } = new(); public DutyConfiguration Duties { get; } = new(); - public SoloDutyConfiguration SoloDuties { get; } = new(); + public SinglePlayerDutyConfiguration SinglePlayerDuties { get; } = new(); public NotificationConfiguration Notifications { get; } = new(); public AdvancedConfiguration Advanced { get; } = new(); public WindowConfig DebugWindowConfig { get; } = new(); @@ -42,11 +42,11 @@ internal sealed class Configuration : IPluginConfiguration public HashSet BlacklistedDutyCfcIds { get; set; } = []; } - internal sealed class SoloDutyConfiguration + internal sealed class SinglePlayerDutyConfiguration { public bool RunSoloInstancesWithBossMod { get; set; } - public HashSet WhitelistedSoloDutyCfcIds { get; set; } = []; - public HashSet BlacklistedSoloDutyCfcIds { get; set; } = []; + public HashSet WhitelistedSinglePlayerDutyCfcIds { get; set; } = []; + public HashSet BlacklistedSinglePlayerDutyCfcIds { get; set; } = []; } internal sealed class NotificationConfiguration diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index ce92099eb..9fbaadfdd 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -15,6 +15,7 @@ using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; +using Questionable.Windows.ConfigComponents; using Quest = Questionable.Model.Quest; namespace Questionable.Controller; @@ -35,6 +36,7 @@ internal sealed class QuestController : MiniTaskController private readonly Configuration _configuration; private readonly YesAlreadyIpc _yesAlreadyIpc; private readonly TaskCreator _taskCreator; + private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent; private readonly ILogger _logger; private readonly object _progressLock = new(); @@ -76,7 +78,8 @@ internal sealed class QuestController : MiniTaskController TaskCreator taskCreator, IServiceProvider serviceProvider, InterruptHandler interruptHandler, - IDataManager dataManager) + IDataManager dataManager, + SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent) : base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger) { _clientState = clientState; @@ -93,6 +96,7 @@ internal sealed class QuestController : MiniTaskController _configuration = configuration; _yesAlreadyIpc = yesAlreadyIpc; _taskCreator = taskCreator; + _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; _logger = logger; _condition.ConditionChange += OnConditionChange; @@ -169,6 +173,7 @@ internal sealed class QuestController : MiniTaskController DebugState = null; _questRegistry.Reload(); + _singlePlayerDutyConfigComponent.Reload(); } } diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs index 5e948761d..0e066c96b 100644 --- a/Questionable/Controller/QuestRegistry.cs +++ b/Questionable/Controller/QuestRegistry.cs @@ -240,11 +240,11 @@ internal sealed class QuestRegistry public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest) => _quests.TryGetValue(questId, out quest); - public List GetKnownClassJobQuests(EClassJob classJob) + public List GetKnownClassJobQuests(EClassJob classJob, bool includeRoleQuests = true) { - List allQuests = [.._questData.GetClassJobQuests(classJob)]; + List allQuests = [.._questData.GetClassJobQuests(classJob, includeRoleQuests)]; if (classJob.AsJob() != classJob) - allQuests.AddRange(_questData.GetClassJobQuests(classJob.AsJob())); + allQuests.AddRange(_questData.GetClassJobQuests(classJob.AsJob(), includeRoleQuests)); return allQuests .Where(x => IsKnownQuest(x.QuestId)) diff --git a/Questionable/Data/QuestData.cs b/Questionable/Data/QuestData.cs index f84edb959..e3cc60f19 100644 --- a/Questionable/Data/QuestData.cs +++ b/Questionable/Data/QuestData.cs @@ -247,8 +247,8 @@ internal sealed class QuestData private void AddPreviousQuest(QuestId questToUpdate, QuestId requiredQuestId) { - QuestInfo quest = (QuestInfo)_quests[questToUpdate]; - quest.AddPreviousQuest(new PreviousQuestInfo(requiredQuestId)); + if (_quests.TryGetValue(questToUpdate, out IQuestInfo? quest) && quest is QuestInfo questInfo) + questInfo.AddPreviousQuest(new PreviousQuestInfo(requiredQuestId)); } private void AddGcFollowUpQuests() @@ -300,7 +300,7 @@ internal sealed class QuestData .ToList(); } - public List GetClassJobQuests(EClassJob classJob) + public List GetClassJobQuests(EClassJob classJob, bool includeRoleQuests = false) { List chapterIds = classJob switch { @@ -367,7 +367,20 @@ internal sealed class QuestData _ => throw new ArgumentOutOfRangeException(nameof(classJob)), }; - chapterIds.AddRange(classJob switch + if (includeRoleQuests) + { + chapterIds.AddRange(GetRoleQuestIds(classJob)); + } + + return GetQuestsInNewGamePlusChapters(chapterIds); + } + + public List GetRoleQuests(EClassJob referenceClassJob) => + GetQuestsInNewGamePlusChapters(GetRoleQuestIds(referenceClassJob).ToList()); + + private static IEnumerable GetRoleQuestIds(EClassJob classJob) + { + return classJob switch { _ when classJob.IsTank() => TankRoleQuests, _ when classJob.IsHealer() => HealerRoleQuests, @@ -375,9 +388,7 @@ internal sealed class QuestData _ when classJob.IsPhysicalRanged() => PhysicalRangedRoleQuests, _ when classJob.IsCaster() && classJob != EClassJob.BlueMage => CasterRoleQuests, _ => [] - }); - - return GetQuestsInNewGamePlusChapters(chapterIds); + }; } private List GetQuestsInNewGamePlusChapters(List chapterIds) diff --git a/Questionable/Data/TerritoryData.cs b/Questionable/Data/TerritoryData.cs index c57ada43a..f35f9cee7 100644 --- a/Questionable/Data/TerritoryData.cs +++ b/Questionable/Data/TerritoryData.cs @@ -99,6 +99,11 @@ internal sealed class TerritoryData } } + public IEnumerable<(ElementId QuestId, byte Index, ContentFinderConditionData Data)> GetAllQuestsWithQuestBattles() + { + return _questsToCfc.Select(x => (x.Key.QuestId, x.Key.Index, _contentFinderConditions[x.Value])); + } + private static string FixName(string name, ClientLanguage language) { if (string.IsNullOrEmpty(name) || language != ClientLanguage.English) diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs index 82a3de68d..939a35d7a 100644 --- a/Questionable/External/BossModIpc.cs +++ b/Questionable/External/BossModIpc.cs @@ -84,16 +84,16 @@ internal sealed class BossModIpc public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault) { - if (!_configuration.SoloDuties.RunSoloInstancesWithBossMod) + if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod) return false; if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData)) return false; - if (_configuration.SoloDuties.BlacklistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) + if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) return false; - if (_configuration.SoloDuties.WhitelistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) + if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) return true; return enabledByDefault; diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 0f776eb62..e6a82af2d 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -302,6 +302,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } @@ -317,6 +318,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); @@ -326,6 +328,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin { serviceProvider.GetRequiredService().Reload(); serviceProvider.GetRequiredService().Reload(); + serviceProvider.GetRequiredService().Reload(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); serviceProvider.GetRequiredService(); diff --git a/Questionable/Validation/EIssueType.cs b/Questionable/Validation/EIssueType.cs index 4d41f9970..3f725738a 100644 --- a/Questionable/Validation/EIssueType.cs +++ b/Questionable/Validation/EIssueType.cs @@ -19,4 +19,5 @@ public enum EIssueType InvalidExcelRef, ClassQuestWithoutAetheryteShortcut, DuplicateSinglePlayerInstance, + UnusedSinglePlayerInstance, } diff --git a/Questionable/Validation/Validators/SinglePlayerInstanceValidator.cs b/Questionable/Validation/Validators/SinglePlayerInstanceValidator.cs new file mode 100644 index 000000000..60a838f52 --- /dev/null +++ b/Questionable/Validation/Validators/SinglePlayerInstanceValidator.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Questionable.Data; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Validation.Validators; + +internal sealed class SinglePlayerInstanceValidator : IQuestValidator +{ + private readonly Dictionary> _questIdToDutyIndexes; + + public SinglePlayerInstanceValidator(TerritoryData territoryData) + { + _questIdToDutyIndexes = territoryData.GetAllQuestsWithQuestBattles() + .GroupBy(x => x.QuestId) + .ToDictionary(x => x.Key, x => x.Select(y => y.Index).ToList()); + } + + public IEnumerable Validate(Quest quest) + { + if (_questIdToDutyIndexes.TryGetValue(quest.Id, out var indexes)) + { + foreach (var index in indexes) + { + if (quest.AllSteps().Any(x => + x.Step.InteractionType == EInteractionType.SinglePlayerDuty && + x.Step.SinglePlayerDutyIndex == index)) + continue; + + yield return new ValidationIssue + { + ElementId = quest.Id, + Sequence = null, + Step = null, + Type = EIssueType.UnusedSinglePlayerInstance, + Severity = EIssueSeverity.Error, + Description = $"Single player instance {index} not used", + }; + } + } + } +} diff --git a/Questionable/Windows/ConfigComponents/ConfigComponent.cs b/Questionable/Windows/ConfigComponents/ConfigComponent.cs index 0a5be627d..e82cf07b6 100644 --- a/Questionable/Windows/ConfigComponents/ConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/ConfigComponent.cs @@ -39,12 +39,12 @@ internal abstract class ConfigComponent protected void Save() => _pluginInterface.SavePluginConfig(Configuration); - protected static string FormatLevel(int level) + protected static string FormatLevel(int level, bool includePrefix = true) { if (level == 0) return string.Empty; - return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}"; + return $"{(includePrefix ? SeIconChar.LevelEn.ToIconString() : string.Empty)}{FormatLevel(level / 10, false)}{(SeIconChar.Number0 + level % 10).ToIconChar()}"; } /// diff --git a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs index 7d89efd07..c410f3fbc 100644 --- a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs @@ -14,7 +14,7 @@ internal sealed class DebugConfigComponent : ConfigComponent public override void DrawTab() { - using var tab = ImRaii.TabItem("Advanced"); + using var tab = ImRaii.TabItem("Advanced###Debug"); if (!tab) return; diff --git a/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs index ffb6538a7..a04319e1f 100644 --- a/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs @@ -60,14 +60,13 @@ internal sealed class DutyConfigComponent : ConfigComponent .GroupBy(x => x.Expansion) .ToDictionary(x => x.Key, x => x - .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, - $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}")) + .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, $"{FormatLevel(y.Level)} {y.Name}")) .ToList()); } public override void DrawTab() { - using var tab = ImRaii.TabItem("Duties"); + using var tab = ImRaii.TabItem("Duties###Duties"); if (!tab) return; @@ -96,37 +95,25 @@ internal sealed class DutyConfigComponent : ConfigComponent "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0"); ImGui.Separator(); - ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:"); + ImGui.Text("You can override the settings for each individual dungeon/trial:"); DrawConfigTable(runInstancedContentWithAutoDuty); + DrawClipboardButtons(); - ImGui.SameLine(); - - using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) - { - if (ImGui.Button("Reset to default")) - { - Configuration.Duties.WhitelistedDutyCfcIds.Clear(); - Configuration.Duties.BlacklistedDutyCfcIds.Clear(); - Save(); - } - } - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("Hold CTRL to enable this button."); + DrawResetButton(); } } private void DrawConfigTable(bool runInstancedContentWithAutoDuty) { - using var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true); + using var child = ImRaii.Child("DutyConfiguration", new Vector2(650, 400), true); if (!child) return; foreach (EExpansionVersion expansion in Enum.GetValues()) { - if (ImGui.CollapsingHeader(expansion.ToString())) + if (ImGui.CollapsingHeader(expansion.ToFriendlyString())) { using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit); if (table) @@ -245,5 +232,21 @@ internal sealed class DutyConfigComponent : ConfigComponent } } + private void DrawResetButton() + { + using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) + { + if (ImGui.Button("Reset to default")) + { + Configuration.Duties.WhitelistedDutyCfcIds.Clear(); + Configuration.Duties.BlacklistedDutyCfcIds.Clear(); + Save(); + } + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Hold CTRL to enable this button."); + } + private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name); } diff --git a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs index e58ce989e..9f71d7a60 100644 --- a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs @@ -45,7 +45,7 @@ internal sealed class GeneralConfigComponent : ConfigComponent public override void DrawTab() { - using var tab = ImRaii.TabItem("General"); + using var tab = ImRaii.TabItem("General###General"); if (!tab) return; diff --git a/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs b/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs index d0a4ba0dd..5df122a56 100644 --- a/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs @@ -25,7 +25,7 @@ internal sealed class NotificationConfigComponent : ConfigComponent public override void DrawTab() { - using var tab = ImRaii.TabItem("Notifications"); + using var tab = ImRaii.TabItem("Notifications###Notifications"); if (!tab) return; diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs new file mode 100644 index 000000000..443ccd87a --- /dev/null +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Dalamud.Game.Text; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using ImGuiNET; +using LLib.GameData; +using Lumina.Excel.Sheets; +using Microsoft.Extensions.Logging; +using Questionable.Controller; +using Questionable.Controller.Steps.Interactions; +using Questionable.Data; +using Questionable.Model; +using Questionable.Model.Common; +using Questionable.Model.Questing; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent +{ + private readonly TerritoryData _territoryData; + private readonly QuestRegistry _questRegistry; + private readonly QuestData _questData; + private readonly IDataManager _dataManager; + private readonly ILogger _logger; + + private static readonly List<(EClassJob ClassJob, string Name)> RoleQuestCategories = + [ + (EClassJob.Paladin, "Tank Role Quests"), + (EClassJob.WhiteMage, "Healer Role Quests"), + (EClassJob.Lancer, "Melee Role Quests"), + (EClassJob.Bard, "Physical Ranged Role Quests"), + (EClassJob.BlackMage, "Magical Ranged Role Quests"), + ]; + + private ImmutableDictionary> _startingCityBattles = ImmutableDictionary>.Empty; + private ImmutableDictionary> _mainScenarioBattles = ImmutableDictionary>.Empty; + private ImmutableDictionary> _jobQuestBattles = ImmutableDictionary>.Empty; + private ImmutableDictionary> _roleQuestBattles = ImmutableDictionary>.Empty; + private ImmutableList _otherRoleQuestBattles = ImmutableList.Empty; + private ImmutableList<(string Label, List)> _otherQuestBattles = ImmutableList<(string Label, List)>.Empty; + + public SinglePlayerDutyConfigComponent( + IDalamudPluginInterface pluginInterface, + Configuration configuration, + TerritoryData territoryData, + QuestRegistry questRegistry, + QuestData questData, + IDataManager dataManager, + ILogger logger) + : base(pluginInterface, configuration) + { + _territoryData = territoryData; + _questRegistry = questRegistry; + _questData = questData; + _dataManager = dataManager; + _logger = logger; + } + + public void Reload() + { + List questsWithMultipleBattles = _territoryData.GetAllQuestsWithQuestBattles() + .GroupBy(x => x.QuestId) + .Where(x => x.Count() > 1) + .Select(x => x.Key) + .ToList(); + + List mainScenarioBattles = []; + Dictionary> startingCityBattles = + new() + { + { EAetheryteLocation.Limsa, [] }, + { EAetheryteLocation.Gridania, [] }, + { EAetheryteLocation.Uldah, [] }, + }; + + List otherBattles = []; + + Dictionary questIdsToJob = Enum.GetValues() + .Where(x => x != EClassJob.Adventurer && !x.IsCrafter() && !x.IsGatherer()) + .Where(x => x.IsClass() || !x.HasBaseClass()) + .SelectMany(x => _questRegistry.GetKnownClassJobQuests(x, false).Select(y => (y.QuestId, ClassJob: x))) + .ToDictionary(x => x.QuestId, x => x.ClassJob); + Dictionary> jobQuestBattles = questIdsToJob.Values.Distinct() + .ToDictionary(x => x, _ => new List()); + + Dictionary> questIdToRole = RoleQuestCategories + .SelectMany(x => _questData.GetRoleQuests(x.ClassJob).Select(y => (y.QuestId, x.ClassJob))) + .GroupBy(x => x.QuestId) + .ToDictionary(x => x.Key, x => x.Select(y => y.ClassJob).ToList()); + Dictionary> roleQuestBattles = RoleQuestCategories + .ToDictionary(x => x.ClassJob, _ => new List()); + List otherRoleQuestBattles = []; + + foreach (var (questId, index, cfcData) in _territoryData.GetAllQuestsWithQuestBattles()) + { + IQuestInfo questInfo = _questData.GetQuestInfo(questId); + QuestStep questStep = new QuestStep + { + SinglePlayerDutyIndex = 0, + BossModEnabled = false, + }; + bool enabled; + if (_questRegistry.TryGetQuest(questId, out var quest)) + { + if (quest.Root.Disabled) + { + _logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId); + enabled = false; + } + else + { + var foundStep = quest.AllSteps().FirstOrDefault(x => + x.Step.InteractionType == EInteractionType.SinglePlayerDuty && + x.Step.SinglePlayerDutyIndex == index); + if (foundStep == default) + { + _logger.LogWarning("Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, index); + enabled = false; + } + else + { + questStep = foundStep.Step; + enabled = true; + } + } + } + else + { + _logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId); + enabled = false; + } + + string name = $"{FormatLevel(questInfo.Level)} {questInfo.Name}"; + if (!string.IsNullOrEmpty(cfcData.Name) && !questInfo.Name.EndsWith(cfcData.Name, StringComparison.Ordinal)) + name += $" ({cfcData.Name})"; + + if (questsWithMultipleBattles.Contains(questId)) + name += $" (Part {questStep.SinglePlayerDutyIndex + 1})"; + else if (cfcData.ContentFinderConditionId is 674 or 691) + name += " (Melee/Phys. Ranged)"; + + var dutyInfo = new SinglePlayerDutyInfo( + cfcData.ContentFinderConditionId, + cfcData.TerritoryId, + name, + questInfo.Expansion, + questInfo.JournalGenre ?? uint.MaxValue, + questInfo.SortKey, + questStep.SinglePlayerDutyIndex, + enabled, + questStep.BossModEnabled); + + if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334) + startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo); + else if (cfcData.ContentFinderConditionId is 296 or 297 or 299 or 298) + startingCityBattles[EAetheryteLocation.Gridania].Add(dutyInfo); + else if (cfcData.ContentFinderConditionId is 335 or 312 or 337 or 336) + startingCityBattles[EAetheryteLocation.Uldah].Add(dutyInfo); + else if (questInfo.IsMainScenarioQuest) + mainScenarioBattles.Add(dutyInfo); + else if (questIdsToJob.TryGetValue(questId, out EClassJob classJob)) + jobQuestBattles[classJob].Add(dutyInfo); + else if (questIdToRole.TryGetValue(questId, out var classJobs)) + { + foreach (var roleClassJob in classJobs) + roleQuestBattles[roleClassJob].Add(dutyInfo); + } + else if (dutyInfo.CfcId is 845 or 1016) + otherRoleQuestBattles.Add(dutyInfo); + else + otherBattles.Add(dutyInfo); + } + + _startingCityBattles = startingCityBattles + .ToImmutableDictionary(x => x.Key, + x => x.Value.OrderBy(y => y.SortKey) + .ToList()); + _mainScenarioBattles = mainScenarioBattles + .GroupBy(x => x.Expansion) + .ToImmutableDictionary(x => x.Key, + x => + x.OrderBy(y => y.JournalGenreId) + .ThenBy(y => y.SortKey) + .ThenBy(y => y.Index) + .ToList()); + _jobQuestBattles = jobQuestBattles + .Where(x => x.Value.Count > 0) + .ToImmutableDictionary(x => x.Key, + x => + x.Value + // level 10 quests use the same quest battle for [you started as this class] and [you picked this class up later] + .DistinctBy(y => y.CfcId) + .OrderBy(y => y.JournalGenreId) + .ThenBy(y => y.SortKey) + .ThenBy(y => y.Index) + .ToList()); + _roleQuestBattles = roleQuestBattles + .ToImmutableDictionary(x => x.Key, + x => + x.Value.OrderBy(y => y.JournalGenreId) + .ThenBy(y => y.SortKey) + .ThenBy(y => y.Index) + .ToList()); + _otherRoleQuestBattles = otherRoleQuestBattles.ToImmutableList(); + _otherQuestBattles = otherBattles + .OrderBy(x => x.JournalGenreId) + .ThenBy(x => x.SortKey) + .ThenBy(x => x.Index) + .GroupBy(x => x.JournalGenreId) + .Select(x => (BuildJournalGenreLabel(x.Key), x.ToList())) + .ToImmutableList(); + } + + private string BuildJournalGenreLabel(uint journalGenreId) + { + var journalGenre = _dataManager.GetExcelSheet().GetRow(journalGenreId); + var journalCategory = journalGenre.JournalCategory.Value; + + string genreName = journalGenre.Name.ExtractText(); + string categoryName = journalCategory.Name.ExtractText(); + + return $"{categoryName} {SeIconChar.ArrowRight.ToIconString()} {genreName}"; + } + + public override void DrawTab() + { + using var tab = ImRaii.TabItem("Quest Battles###QuestBattles"); + if (!tab) + return; + + bool runSoloInstancesWithBossMod = Configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod; + if (ImGui.Checkbox("Run quest battles with BossMod", ref runSoloInstancesWithBossMod)) + { + Configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod = runSoloInstancesWithBossMod; + Save(); + } + + ImGui.TextColored(ImGuiColors.DalamudRed, + "Work in Progress: For now, this will always use BossMod for combat."); + + ImGui.Separator(); + + using (ImRaii.Disabled(!runSoloInstancesWithBossMod)) + { + ImGui.Text( + "Questionable includes a default list of quest battles that work if BossMod is installed."); + ImGui.Text("The included list of quest battles can change with each update."); + + ImGui.Separator(); + ImGui.Text("You can override the settings for each individual quest battle:"); + + + using var tabBar = ImRaii.TabBar("QuestionableConfigTabs"); + if (tabBar) + { + DrawMainScenarioConfigTable(); + DrawJobQuestConfigTable(); + DrawRoleQuestConfigTable(); + DrawOtherQuestConfigTable(); + } + + DrawResetButton(); + } + } + + private void DrawMainScenarioConfigTable() + { + using var tab = ImRaii.TabItem("MSQ###MSQ"); + if (!tab) + return; + + using var child = BeginChildArea(); + if (!child) + return; + + if (ImGui.CollapsingHeader($"Limsa Lominsa ({FormatLevel(5)} - {FormatLevel(14)})")) + DrawQuestTable("LimsaLominsa", _startingCityBattles[EAetheryteLocation.Limsa]); + + if (ImGui.CollapsingHeader($"Gridania ({FormatLevel(5)} - {FormatLevel(14)})")) + DrawQuestTable("Gridania", _startingCityBattles[EAetheryteLocation.Gridania]); + + if (ImGui.CollapsingHeader($"Ul'dah ({FormatLevel(4)} - {FormatLevel(14)})")) + DrawQuestTable("Uldah", _startingCityBattles[EAetheryteLocation.Uldah]); + + foreach (EExpansionVersion expansion in Enum.GetValues()) + { + if (_mainScenarioBattles.TryGetValue(expansion, out var dutyInfos)) + { + if (ImGui.CollapsingHeader(expansion.ToFriendlyString())) + DrawQuestTable($"Duties{expansion}", dutyInfos); + } + } + } + + private void DrawJobQuestConfigTable() + { + using var tab = ImRaii.TabItem("Class/Job Quests###JobQuests"); + if (!tab) + return; + + using var child = BeginChildArea(); + if (!child) + return; + + foreach (EClassJob classJob in Enum.GetValues()) + { + if (_jobQuestBattles.TryGetValue(classJob, out var dutyInfos)) + { + string jobName = classJob.ToFriendlyString(); + if (classJob.IsClass()) + jobName += $" / {classJob.AsJob().ToFriendlyString()}"; + + if (ImGui.CollapsingHeader(jobName)) + DrawQuestTable($"JobQuests{classJob}", dutyInfos); + } + } + } + + private void DrawRoleQuestConfigTable() + { + using var tab = ImRaii.TabItem("Role Quests###RoleQuests"); + if (!tab) + return; + + using var child = BeginChildArea(); + if (!child) + return; + + foreach (var (classJob, label) in RoleQuestCategories) + { + if (_roleQuestBattles.TryGetValue(classJob, out var dutyInfos)) + { + if (ImGui.CollapsingHeader(label)) + DrawQuestTable($"RoleQuests{classJob}", dutyInfos); + } + } + + if(ImGui.CollapsingHeader("General Role Quests")) + DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles); + } + + private void DrawOtherQuestConfigTable() + { + using var tab = ImRaii.TabItem("Other Quests###MiscQuests"); + if (!tab) + return; + + using var child = BeginChildArea(); + if (!child) + return; + + foreach (var (label, dutyInfos) in _otherQuestBattles) + { + if (ImGui.CollapsingHeader(label)) + DrawQuestTable($"Other{label}", dutyInfos); + } + } + + private void DrawQuestTable(string label, IReadOnlyList dutyInfos) + { + using var table = ImRaii.Table(label, 2, ImGuiTableFlags.SizingFixedFit); + if (table) + { + ImGui.TableSetupColumn("Quest", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f); + + foreach (var dutyInfo in dutyInfos) + { + ImGui.TableNextRow(); + + string[] labels = dutyInfo.BossModEnabledByDefault + ? SupportedCfcOptions + : UnsupportedCfcOptions; + int value = 0; + if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(dutyInfo.CfcId)) + value = 1; + if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(dutyInfo.CfcId)) + value = 2; + + if (ImGui.TableNextColumn()) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(dutyInfo.Name); + + if (ImGui.IsItemHovered() && Configuration.Advanced.AdditionalStatusInformation) + { + using var tooltip = ImRaii.Tooltip(); + if (tooltip) + { + ImGui.TextUnformatted(dutyInfo.Name); + ImGui.Separator(); + ImGui.BulletText($"TerritoryId: {dutyInfo.TerritoryId}"); + ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.CfcId}"); + } + } + + if (!dutyInfo.Enabled) + { + ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.", + FontAwesomeIcon.Times, ImGuiColors.DalamudRed); + } + } + + if (ImGui.TableNextColumn()) + { + using var _ = ImRaii.PushId($"##Duty{dutyInfo.CfcId}"); + using (ImRaii.Disabled(!dutyInfo.Enabled)) + { + ImGui.SetNextItemWidth(200); + if (ImGui.Combo(string.Empty, ref value, labels, labels.Length)) + { + Configuration.Duties.WhitelistedDutyCfcIds.Remove(dutyInfo.CfcId); + Configuration.Duties.BlacklistedDutyCfcIds.Remove(dutyInfo.CfcId); + + if (value == 1) + Configuration.Duties.WhitelistedDutyCfcIds.Add(dutyInfo.CfcId); + else if (value == 2) + Configuration.Duties.BlacklistedDutyCfcIds.Add(dutyInfo.CfcId); + + Save(); + } + } + } + } + } + } + + private static ImRaii.IEndObject BeginChildArea() => ImRaii.Child("DutyConfiguration", new Vector2(650, 400), true); + + private void DrawResetButton() + { + using (ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl))) + { + if (ImGui.Button("Reset to default")) + { + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Clear(); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Clear(); + Save(); + } + } + + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Hold CTRL to enable this button."); + } + + private sealed record SinglePlayerDutyInfo( + uint CfcId, + uint TerritoryId, + string Name, + EExpansionVersion Expansion, + uint JournalGenreId, + ushort SortKey, + byte Index, + bool Enabled, + bool BossModEnabledByDefault); +} diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index e2ac6c314..f3611a5bb 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using ImGuiNET; using LLib.ImGui; +using Questionable.Controller.Steps.Interactions; using Questionable.Windows.ConfigComponents; namespace Questionable.Windows; @@ -11,6 +12,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig private readonly IDalamudPluginInterface _pluginInterface; private readonly GeneralConfigComponent _generalConfigComponent; private readonly DutyConfigComponent _dutyConfigComponent; + private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent; private readonly NotificationConfigComponent _notificationConfigComponent; private readonly DebugConfigComponent _debugConfigComponent; private readonly Configuration _configuration; @@ -19,6 +21,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig IDalamudPluginInterface pluginInterface, GeneralConfigComponent generalConfigComponent, DutyConfigComponent dutyConfigComponent, + SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, NotificationConfigComponent notificationConfigComponent, DebugConfigComponent debugConfigComponent, Configuration configuration) @@ -27,6 +30,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig _pluginInterface = pluginInterface; _generalConfigComponent = generalConfigComponent; _dutyConfigComponent = dutyConfigComponent; + _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; _notificationConfigComponent = notificationConfigComponent; _debugConfigComponent = debugConfigComponent; _configuration = configuration; @@ -42,6 +46,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig _generalConfigComponent.DrawTab(); _dutyConfigComponent.DrawTab(); + _singlePlayerDutyConfigComponent.DrawTab(); _notificationConfigComponent.DrawTab(); _debugConfigComponent.DrawTab(); } diff --git a/vendor/pictomancy b/vendor/pictomancy index d147acc0e..70c0e31aa 160000 --- a/vendor/pictomancy +++ b/vendor/pictomancy @@ -1 +1 @@ -Subproject commit d147acc0ea5eed00e25b12508bf5d3fb8eefed53 +Subproject commit 70c0e31aabfbc7067c5b57fd02ee0c72ebc7a22e From 31eb121cf043d4bb5079fbeab93a99b69d0c383e Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 21 Feb 2025 03:22:47 +0100 Subject: [PATCH 08/18] Add quest battle notes --- .../RoslynElements/QuestStepExtensions.cs | 3 + .../4522_The Ultimate Weapon.json | 6 +- ...3682_Vows of Virtue, Deeds of Cruelty.json | 29 +++++--- QuestPaths/quest-v1.json | 6 ++ Questionable.Model/Questing/QuestStep.cs | 1 + .../GameUi/InteractionUiController.cs | 21 +++++- Questionable/External/BossModIpc.cs | 3 + .../SinglePlayerDutyConfigComponent.cs | 71 ++++++++++++++----- 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index ca5591bd6..7e57e1ae1 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -126,6 +126,9 @@ internal static class QuestStepExtensions Assignment(nameof(QuestStep.BossModEnabled), step.BossModEnabled, emptyStep.BossModEnabled) .AsSyntaxNodeOrToken(), + Assignment(nameof(QuestStep.BossModNotes), + step.BossModNotes, emptyStep.BossModNotes) + .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.SinglePlayerDutyIndex), step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex) .AsSyntaxNodeOrToken(), diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json index 1a788c09f..cd396c519 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json @@ -103,7 +103,11 @@ "Z": 479.9724 }, "TerritoryId": 1053, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "BossModEnabled": false, + "BossModNotes": [ + "Doesn't handle death properly" + ] } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json index c8a671c33..51863a11f 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json @@ -61,7 +61,19 @@ "TerritoryId": 156, "InteractionType": "Interact", "AetheryteShortcut": "Mor Dhona", - "TargetTerritoryId": 351 + "TargetTerritoryId": 351, + "SkipConditions": { + "AetheryteShortcutIf": { + "InTerritory": [ + 351 + ] + }, + "StepIf": { + "InTerritory": [ + 351 + ] + } + } }, { "DataId": 1032081, @@ -73,13 +85,14 @@ "TerritoryId": 351, "InteractionType": "SinglePlayerDuty", "Comment": "Estinien vs. Arch Ultima", - "DialogueChoices": [ - { - "Type": "YesNo", - "Prompt": "TEXT_LUCKMG110_03682_Q1_100_125", - "Yes": true - } - ] + "BossModEnabled": false, + "BossModNotes": [ + "AI doesn't move automatically for the first boss", + "AI doesn't move automatically for the dialogue with gaius on the bridge", + "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)", + "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking" + ], + "$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one" } ] }, diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 350b0bf13..f5df18c8a 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1270,6 +1270,12 @@ "BossModEnabled": { "type": "boolean" }, + "BossModNotes": { + "type": "array", + "items": { + "type": "string" + } + }, "SinglePlayerDutyIndex": { "type": "integer", "minimum": 0, diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index 98127cde7..8d9d31ecb 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -76,6 +76,7 @@ public sealed class QuestStep public uint? ContentFinderConditionId { get; set; } public bool AutoDutyEnabled { get; set; } public bool BossModEnabled { get; set; } + public List BossModNotes { get; set; } = []; public byte SinglePlayerDutyIndex { get; set; } public SkipConditions? SkipConditions { get; set; } diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 3164a3bb2..0c7e4d0d2 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -19,6 +19,7 @@ using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Interactions; using Questionable.Data; +using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Gathering; @@ -45,6 +46,7 @@ internal sealed class InteractionUiController : IDisposable private readonly ITargetManager _targetManager; private readonly IClientState _clientState; private readonly ShopController _shopController; + private readonly BossModIpc _bossModIpc; private readonly ILogger _logger; private readonly Regex _returnRegex; private readonly Regex _purchaseItemRegex; @@ -68,6 +70,7 @@ internal sealed class InteractionUiController : IDisposable IPluginLog pluginLog, IClientState clientState, ShopController shopController, + BossModIpc bossModIpc, ILogger logger) { _addonLifecycle = addonLifecycle; @@ -85,6 +88,7 @@ internal sealed class InteractionUiController : IDisposable _targetManager = targetManager; _clientState = clientState; _shopController = shopController; + _bossModIpc = bossModIpc; _logger = logger; _returnRegex = _dataManager.GetExcelSheet().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!; @@ -176,7 +180,10 @@ internal sealed class InteractionUiController : IDisposable int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps) ?? HandleInstanceListChoice(actualPrompt); if (answer != null) + { + _logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt); addonSelectString->AtkUnitBase.FireCallbackInt(answer.Value); + } } private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args) @@ -224,6 +231,7 @@ internal sealed class InteractionUiController : IDisposable int? answer = HandleListChoice(actualPrompt, answers, checkAllSteps); if (answer != null) { + _logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", answer, actualPrompt); addonSelectIconString->AtkUnitBase.FireCallbackInt(answer.Value); return; } @@ -266,6 +274,7 @@ internal sealed class InteractionUiController : IDisposable int questSelection = answers.FindIndex(x => GameFunctions.GameStringEquals(questName, x)); if (questSelection >= 0) { + _logger.LogInformation("Selecting quest {QuestName}", questName); addonSelectIconString->AtkUnitBase.FireCallbackInt(questSelection); return true; } @@ -655,13 +664,21 @@ internal sealed class InteractionUiController : IDisposable continue; } + _logger.LogInformation("Returning {YesNo} for '{Prompt}'", dialogueChoice.Yes ? "Yes" : "No", actualPrompt); addonSelectYesno->AtkUnitBase.FireCallbackInt(dialogueChoice.Yes ? 0 : 1); return true; } - if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true }) + if (step is { InteractionType: EInteractionType.SinglePlayerDuty } && + _bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled)) { - _logger.LogTrace("DefaultYesNo: probably Single Player Duty"); + // Most of these are yes/no dialogs "Duty calls, ...". + // + // For 'Vows of Virtue, Deeds of Cruelty', there's no such dialog, and it just puts you into the instance + // after you confirm 'Wait for Krile?'. However, if you fail that duty, you'll get a DifficultySelectYesNo. + + // DifficultySelectYesNo → [0, 2] for very easy + _logger.LogInformation("DefaultYesNo: probably Single Player Duty"); addonSelectYesno->AtkUnitBase.FireCallbackInt(0); return true; } diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs index 939a35d7a..e73e1863d 100644 --- a/Questionable/External/BossModIpc.cs +++ b/Questionable/External/BossModIpc.cs @@ -84,6 +84,9 @@ internal sealed class BossModIpc public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault) { + if (!IsSupported()) + return false; + if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod) return false; diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 443ccd87a..263a3b54d 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -40,12 +40,22 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent (EClassJob.BlackMage, "Magical Ranged Role Quests"), ]; - private ImmutableDictionary> _startingCityBattles = ImmutableDictionary>.Empty; - private ImmutableDictionary> _mainScenarioBattles = ImmutableDictionary>.Empty; - private ImmutableDictionary> _jobQuestBattles = ImmutableDictionary>.Empty; - private ImmutableDictionary> _roleQuestBattles = ImmutableDictionary>.Empty; + private ImmutableDictionary> _startingCityBattles = + ImmutableDictionary>.Empty; + + private ImmutableDictionary> _mainScenarioBattles = + ImmutableDictionary>.Empty; + + private ImmutableDictionary> _jobQuestBattles = + ImmutableDictionary>.Empty; + + private ImmutableDictionary> _roleQuestBattles = + ImmutableDictionary>.Empty; + private ImmutableList _otherRoleQuestBattles = ImmutableList.Empty; - private ImmutableList<(string Label, List)> _otherQuestBattles = ImmutableList<(string Label, List)>.Empty; + + private ImmutableList<(string Label, List)> _otherQuestBattles = + ImmutableList<(string Label, List)>.Empty; public SinglePlayerDutyConfigComponent( IDalamudPluginInterface pluginInterface, @@ -103,10 +113,10 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent { IQuestInfo questInfo = _questData.GetQuestInfo(questId); QuestStep questStep = new QuestStep - { - SinglePlayerDutyIndex = 0, - BossModEnabled = false, - }; + { + SinglePlayerDutyIndex = 0, + BossModEnabled = false, + }; bool enabled; if (_questRegistry.TryGetQuest(questId, out var quest)) { @@ -122,7 +132,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent x.Step.SinglePlayerDutyIndex == index); if (foundStep == default) { - _logger.LogWarning("Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, index); + _logger.LogWarning( + "Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, + index); enabled = false; } else @@ -156,7 +168,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent questInfo.SortKey, questStep.SinglePlayerDutyIndex, enabled, - questStep.BossModEnabled); + questStep.BossModEnabled, + questStep.BossModNotes); if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334) startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo); @@ -343,7 +356,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent } } - if(ImGui.CollapsingHeader("General Role Quests")) + if (ImGui.CollapsingHeader("General Role Quests")) DrawQuestTable("RoleQuestsGeneral", _otherRoleQuestBattles); } @@ -380,9 +393,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ? SupportedCfcOptions : UnsupportedCfcOptions; int value = 0; - if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(dutyInfo.CfcId)) + if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId)) value = 1; - if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(dutyInfo.CfcId)) + if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId)) value = 2; if (ImGui.TableNextColumn()) @@ -407,6 +420,25 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ImGuiComponents.HelpMarker("Questionable doesn't include support for this quest.", FontAwesomeIcon.Times, ImGuiColors.DalamudRed); } + else if (dutyInfo.Notes.Count > 0) + { + using var color = new ImRaii.Color(); + color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString()); + } + + if (ImGui.IsItemHovered()) + { + using var _ = ImRaii.Tooltip(); + + ImGui.TextColored(ImGuiColors.DalamudYellow, "While testing, the following issues have been found:"); + foreach (string note in dutyInfo.Notes) + ImGui.BulletText(note); + } + } } if (ImGui.TableNextColumn()) @@ -417,13 +449,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ImGui.SetNextItemWidth(200); if (ImGui.Combo(string.Empty, ref value, labels, labels.Length)) { - Configuration.Duties.WhitelistedDutyCfcIds.Remove(dutyInfo.CfcId); - Configuration.Duties.BlacklistedDutyCfcIds.Remove(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId); if (value == 1) - Configuration.Duties.WhitelistedDutyCfcIds.Add(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId); else if (value == 2) - Configuration.Duties.BlacklistedDutyCfcIds.Add(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId); Save(); } @@ -460,5 +492,6 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ushort SortKey, byte Index, bool Enabled, - bool BossModEnabledByDefault); + bool BossModEnabledByDefault, + List Notes); } From 71e0b01dbce051122f95560db0a290e111883302 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Fri, 21 Feb 2025 12:21:01 +0100 Subject: [PATCH 09/18] Add quest battle difficulty selection; UI tweaks --- .../MSQ-1/Gridania/445_Chasing Shadows.json | 6 +- .../MSQ/H-5.2/3765_A Sleep Disturbed.json | 4 ++ Questionable/Configuration.cs | 1 + .../GameUi/InteractionUiController.cs | 65 ++++++++++++++++++- .../SinglePlayerDutyConfigComponent.cs | 57 ++++++++++++---- 5 files changed, 118 insertions(+), 15 deletions(-) diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json index d59a446c9..bb4aa6554 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json @@ -28,7 +28,11 @@ "Z": -309.55975 }, "TerritoryId": 148, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "BossModEnabled": false, + "BossModNotes": [ + "AI doesn't automatically target newly spawning adds and dies until after the boss died (tested on CNJ)" + ] } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json index 5cd92e470..49a098916 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json @@ -46,6 +46,10 @@ }, "TerritoryId": 817, "InteractionType": "SinglePlayerDuty", + "BossModEnabled": false, + "BossModNotes": [ + "Doesn't walk to the teleporter to finish the duty" + ], "Fly": true, "Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)", "$": "The dialogue choices and data ids here are recycled", diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index a4126ed6f..07b20f4d0 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -45,6 +45,7 @@ internal sealed class Configuration : IPluginConfiguration internal sealed class SinglePlayerDutyConfiguration { public bool RunSoloInstancesWithBossMod { get; set; } + public byte RetryDifficulty { get; set; } = 2; public HashSet WhitelistedSinglePlayerDutyCfcIds { get; set; } = []; public HashSet BlacklistedSinglePlayerDutyCfcIds { get; set; } = []; } diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 0c7e4d0d2..1825b4f79 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -47,6 +47,7 @@ internal sealed class InteractionUiController : IDisposable private readonly IClientState _clientState; private readonly ShopController _shopController; private readonly BossModIpc _bossModIpc; + private readonly Configuration _configuration; private readonly ILogger _logger; private readonly Regex _returnRegex; private readonly Regex _purchaseItemRegex; @@ -71,6 +72,7 @@ internal sealed class InteractionUiController : IDisposable IClientState clientState, ShopController shopController, BossModIpc bossModIpc, + Configuration configuration, ILogger logger) { _addonLifecycle = addonLifecycle; @@ -89,6 +91,7 @@ internal sealed class InteractionUiController : IDisposable _clientState = clientState; _shopController = shopController; _bossModIpc = bossModIpc; + _configuration = configuration; _logger = logger; _returnRegex = _dataManager.GetExcelSheet().GetRow(196).GetRegex(addon => addon.Text, pluginLog)!; @@ -98,6 +101,7 @@ internal sealed class InteractionUiController : IDisposable _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup); + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup); @@ -144,6 +148,12 @@ internal sealed class InteractionUiController : IDisposable SelectYesnoPostSetup(addonSelectYesno, true); } + if (_gameGui.TryGetAddonByName("DifficultySelectYesNo", out AtkUnitBase* addonDifficultySelectYesNo)) + { + _logger.LogInformation("DifficultySelectYesNo window is open"); + DifficultySelectYesNoPostSetup(addonDifficultySelectYesNo, true); + } + if (_gameGui.TryGetAddonByName("PointMenu", out AtkUnitBase* addonPointMenu)) { _logger.LogInformation("PointMenu is open"); @@ -669,8 +679,19 @@ internal sealed class InteractionUiController : IDisposable return true; } + if (CheckSinglePlayerDutyYesNo(quest.Id, step)) + { + addonSelectYesno->AtkUnitBase.FireCallbackInt(0); + return true; + } + + return false; + } + + private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step) + { if (step is { InteractionType: EInteractionType.SinglePlayerDuty } && - _bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled)) + _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyIndex, step.BossModEnabled)) { // Most of these are yes/no dialogs "Duty calls, ...". // @@ -678,8 +699,7 @@ internal sealed class InteractionUiController : IDisposable // after you confirm 'Wait for Krile?'. However, if you fail that duty, you'll get a DifficultySelectYesNo. // DifficultySelectYesNo → [0, 2] for very easy - _logger.LogInformation("DefaultYesNo: probably Single Player Duty"); - addonSelectYesno->AtkUnitBase.FireCallbackInt(0); + _logger.LogInformation("SinglePlayerDutyYesNo: probably Single Player Duty"); return true; } @@ -716,6 +736,44 @@ internal sealed class InteractionUiController : IDisposable return false; } + + private unsafe void DifficultySelectYesNoPostSetup(AddonEvent type, AddonArgs args) + { + AtkUnitBase* addonDifficultySelectYesNo = (AtkUnitBase*)args.Addon; + DifficultySelectYesNoPostSetup(addonDifficultySelectYesNo, false); + } + + private unsafe void DifficultySelectYesNoPostSetup(AtkUnitBase* addonDifficultySelectYesNo, bool checkAllSteps) + { + var currentQuest = _questController.StartedQuest; + if (currentQuest == null) + return; + + var quest = currentQuest.Quest; + bool autoConfirm; + if (checkAllSteps) + { + var sequence = quest.FindSequence(currentQuest.Sequence); + autoConfirm = sequence != null && sequence.Steps.Any(step => CheckSinglePlayerDutyYesNo(quest.Id, step)); + } + else + { + var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step); + autoConfirm = step != null && CheckSinglePlayerDutyYesNo(quest.Id, step); + } + + if (autoConfirm) + { + _logger.LogInformation("Confirming difficulty ({Difficulty}) for quest battle", _configuration.SinglePlayerDuties.RetryDifficulty); + var selectChoice = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 0 }, + new() { Type = ValueType.Int, Int = _configuration.SinglePlayerDuties.RetryDifficulty } + }; + addonDifficultySelectYesNo->FireCallback(2, selectChoice); + } + } + private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest) { // this can be triggered either manually (in which case we should increase the step counter), or automatically @@ -888,6 +946,7 @@ internal sealed class InteractionUiController : IDisposable { _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup); + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup); diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 263a3b54d..07149deba 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -25,12 +25,6 @@ namespace Questionable.Windows.ConfigComponents; internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent { - private readonly TerritoryData _territoryData; - private readonly QuestRegistry _questRegistry; - private readonly QuestData _questData; - private readonly IDataManager _dataManager; - private readonly ILogger _logger; - private static readonly List<(EClassJob ClassJob, string Name)> RoleQuestCategories = [ (EClassJob.Paladin, "Tank Role Quests"), @@ -40,6 +34,15 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent (EClassJob.BlackMage, "Magical Ranged Role Quests"), ]; + private readonly string[] _retryDifficulties = ["Normal", "Easy", "Very Easy"]; + + private readonly TerritoryData _territoryData; + private readonly QuestRegistry _questRegistry; + private readonly QuestData _questData; + private readonly IDataManager _dataManager; + private readonly ILogger _logger; + private readonly List<(EClassJob ClassJob, int Category)> _sortedClassJobs; + private ImmutableDictionary> _startingCityBattles = ImmutableDictionary>.Empty; @@ -72,6 +75,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent _questData = questData; _dataManager = dataManager; _logger = logger; + + _sortedClassJobs = dataManager.GetExcelSheet() + .Where(x => x is { RowId: > 0, UIPriority: < 100 }) + .Select(x => (ClassJob: (EClassJob)x.RowId, Priority: x.UIPriority)) + .OrderBy(x => x.Priority) + .Select(x => (x.ClassJob, x.Priority / 10)) + .ToList(); } public void Reload() @@ -256,8 +266,23 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent Save(); } - ImGui.TextColored(ImGuiColors.DalamudRed, - "Work in Progress: For now, this will always use BossMod for combat."); + using (ImRaii.PushIndent(ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X)) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(ImGuiColors.DalamudRed, + "Work in Progress: For now, this will always use BossMod for combat."); + + using (ImRaii.Disabled(!runSoloInstancesWithBossMod)) + { + int retryDifficulty = Configuration.SinglePlayerDuties.RetryDifficulty; + if (ImGui.Combo("Difficulty when retrying a quest battle", ref retryDifficulty, _retryDifficulties, + _retryDifficulties.Length)) + { + Configuration.SinglePlayerDuties.RetryDifficulty = (byte)retryDifficulty; + Save(); + } + } + } ImGui.Separator(); @@ -286,7 +311,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent private void DrawMainScenarioConfigTable() { - using var tab = ImRaii.TabItem("MSQ###MSQ"); + using var tab = ImRaii.TabItem("Main Scenario Quests###MSQ"); if (!tab) return; @@ -323,10 +348,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent if (!child) return; - foreach (EClassJob classJob in Enum.GetValues()) + int oldPriority = 0; + foreach (var (classJob, priority) in _sortedClassJobs) { if (_jobQuestBattles.TryGetValue(classJob, out var dutyInfos)) { + if (priority != oldPriority) + { + oldPriority = priority; + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + } + string jobName = classJob.ToFriendlyString(); if (classJob.IsClass()) jobName += $" / {classJob.AsJob().ToFriendlyString()}"; @@ -434,7 +468,8 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent { using var _ = ImRaii.Tooltip(); - ImGui.TextColored(ImGuiColors.DalamudYellow, "While testing, the following issues have been found:"); + ImGui.TextColored(ImGuiColors.DalamudYellow, + "While testing, the following issues have been found:"); foreach (string note in dutyInfo.Notes) ImGui.BulletText(note); } From a70e195a93cd450854d7e28adad78761dc6619ec Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 00:03:06 +0100 Subject: [PATCH 10/18] Configure a few early MSQ battles --- .../RoslynElements/QuestStepExtensions.cs | 12 +- .../SinglePlayerDutyOptionsExtensions.cs | 31 ++++ QuestPathGenerator/RoslynShortcuts.cs | 1 + .../BRD/76_The One That Got Away.json | 4 +- .../Class Quests/DRG/439_Proof of Might.json | 4 +- .../Class Quests/DRG/56_Lance of Destiny.json | 4 +- .../MNK/558_The Spirit Is Willing.json | 3 + .../MNK/567_Return of the Holyfist.json | 4 +- .../WAR/1054_How to Quit You.json | 3 + .../Class Quests/WHM/147_Trial by Wind.json | 3 + .../Class Quests/WHM/91_Trial by Wind.json | 8 +- .../Class Quests/WHM/92_Trial by Water.json | 43 ++++- .../MSQ-1/Gridania/129_Spirithold Broken.json | 5 +- .../MSQ-1/Gridania/161_Leia's Legacy.json | 8 +- .../MSQ-1/Gridania/445_Chasing Shadows.json | 10 +- .../Gridania/447_To Guard a Guardian.json | 6 + .../MSQ-1/Limsa/414_Victory in Peril.json | 6 + .../MSQ-1/Limsa/466_Double Dealing.json | 7 +- .../MSQ-1/Limsa/469_Just Deserts.json | 5 +- .../Limsa/543_Lurkers in the Grotto.json | 8 +- .../MSQ-1/Limsa/544_Feint and Strike.json | 6 + .../MSQ-1/Shared/343_Lord of the Inferno.json | 5 +- .../MSQ-1/Shared/660_Into a Copper Hell.json | 6 + ...80_The Company You Keep (Twin Adders).json | 25 ++- .../681_The Company You Keep (Maelstrom).json | 3 + ...he Company You Keep (Immortal Flames).json | 3 + .../MSQ-1/Ul'dah/303_Step Nine.json | 3 +- .../Ul'dah/320_Way Down in the Hole.json | 5 +- .../Ul'dah/334_Storms on the Horizon.json | 9 + .../Ul'dah/336_Oh Captain, My Captain.json | 5 +- .../3853_Heir Today, Gone Tomorrow.json | 21 ++- .../Ul'dah/550_Underneath the Sultantree.json | 5 +- .../Ul'dah/551_Duty, Honor, Country.json | 8 +- .../3856_We Come in Peace.json | 22 ++- .../724_Brotherly Love.json | 5 +- .../3862_Nouveau Riche.json | 8 +- .../4522_The Ultimate Weapon.json | 10 +- .../Class Quests/WAR/601_And My Axe.json | 4 +- .../1595_A Series of Unfortunate Events.json | 4 +- .../1597_Divine Intervention.json | 4 +- .../1601_Keeping the Flame Alive.json | 4 +- .../1606_Sounding Out the Amphitheatre.json | 4 +- .../MSQ/A4-Ishgard/1639_Fire and Blood.json | 4 +- .../A5-Sea of Clouds/1644_Familiar Faces.json | 4 +- .../1657_An Illuminati Incident.json | 4 +- ...667_Close Encounters of the VIth Kind.json | 4 +- ...3682_Vows of Virtue, Deeds of Cruelty.json | 18 +- .../MSQ/H-5.2/3765_A Sleep Disturbed.json | 10 +- .../3895_Sleep Now in Sapphire.json | 4 +- QuestPaths/quest-v1.json | 40 +++-- Questionable.Model/Questing/QuestStep.cs | 5 +- .../Questing/SinglePlayerDutyOptions.cs | 10 ++ .../GameUi/InteractionUiController.cs | 2 +- .../Steps/Common/SendNotification.cs | 2 +- .../Steps/Interactions/SinglePlayerDuty.cs | 2 +- .../Controller/Steps/Shared/WaitAtEnd.cs | 2 +- Questionable/External/BossModIpc.cs | 7 +- .../SinglePlayerDutyConfigComponent.cs | 166 ++++++++++-------- 58 files changed, 462 insertions(+), 166 deletions(-) create mode 100644 QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs create mode 100644 Questionable.Model/Questing/SinglePlayerDutyOptions.cs diff --git a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs index 7e57e1ae1..1ff4fbc1b 100644 --- a/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs +++ b/QuestPathGenerator/RoslynElements/QuestStepExtensions.cs @@ -108,7 +108,7 @@ internal static class QuestStepExtensions AssignmentList(nameof(QuestStep.ComplexCombatData), step.ComplexCombatData) .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.CombatItemUse), step.CombatItemUse, - emptyStep.CombatItemUse) + emptyStep.CombatItemUse) .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.CombatDelaySecondsAtStart), step.CombatDelaySecondsAtStart, @@ -123,14 +123,8 @@ internal static class QuestStepExtensions Assignment(nameof(QuestStep.AutoDutyEnabled), step.AutoDutyEnabled, emptyStep.AutoDutyEnabled) .AsSyntaxNodeOrToken(), - Assignment(nameof(QuestStep.BossModEnabled), - step.BossModEnabled, emptyStep.BossModEnabled) - .AsSyntaxNodeOrToken(), - Assignment(nameof(QuestStep.BossModNotes), - step.BossModNotes, emptyStep.BossModNotes) - .AsSyntaxNodeOrToken(), - Assignment(nameof(QuestStep.SinglePlayerDutyIndex), - step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex) + Assignment(nameof(QuestStep.SinglePlayerDutyOptions), step.SinglePlayerDutyOptions, + emptyStep.SinglePlayerDutyOptions) .AsSyntaxNodeOrToken(), Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions, emptyStep.SkipConditions) diff --git a/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs b/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs new file mode 100644 index 000000000..7a545318f --- /dev/null +++ b/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Questionable.Model.Questing; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Questionable.QuestPathGenerator.RoslynShortcuts; + +namespace Questionable.QuestPathGenerator.RoslynElements; + +internal static class SinglePlayerDutyOptionsExtensions +{ + public static ExpressionSyntax ToExpressionSyntax(this SinglePlayerDutyOptions dutyOptions) + { + var emptyOptions = new SinglePlayerDutyOptions(); + return ObjectCreationExpression( + IdentifierName(nameof(SinglePlayerDutyOptions))) + .WithInitializer( + InitializerExpression( + SyntaxKind.ObjectInitializerExpression, + SeparatedList( + SyntaxNodeList( + Assignment(nameof(SinglePlayerDutyOptions.Enabled), + dutyOptions.Enabled, emptyOptions.Enabled) + .AsSyntaxNodeOrToken(), + Assignment(nameof(SinglePlayerDutyOptions.Notes), + dutyOptions.Notes, emptyOptions.Notes) + .AsSyntaxNodeOrToken(), + Assignment(nameof(SinglePlayerDutyOptions.Index), + dutyOptions.Index, emptyOptions.Index) + .AsSyntaxNodeOrToken())))); + } +} diff --git a/QuestPathGenerator/RoslynShortcuts.cs b/QuestPathGenerator/RoslynShortcuts.cs index 2c1df09fb..70cae35db 100644 --- a/QuestPathGenerator/RoslynShortcuts.cs +++ b/QuestPathGenerator/RoslynShortcuts.cs @@ -62,6 +62,7 @@ public static class RoslynShortcuts ComplexCombatData complexCombatData => complexCombatData.ToExpressionSyntax(), QuestWorkValue questWorkValue => questWorkValue.ToExpressionSyntax(), List list => list.ToExpressionSyntax(), // TODO fix in AssignmentList + SinglePlayerDutyOptions dutyOptions => dutyOptions.ToExpressionSyntax(), SkipConditions skipConditions => skipConditions.ToExpressionSyntax(), SkipStepConditions skipStepConditions => skipStepConditions.ToExpressionSyntax(), SkipItemConditions skipItemCondition => skipItemCondition.ToExpressionSyntax(), diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json index f374ebd71..3e3fc39bf 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/BRD/76_The One That Got Away.json @@ -57,7 +57,9 @@ }, "TerritoryId": 153, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyIndex": 1, + "SinglePlayerDutyOptions": { + "Index": 1 + }, "Fly": true } ] diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json index c9af0007b..bbbf6dedc 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/439_Proof of Might.json @@ -62,7 +62,9 @@ }, "TerritoryId": 154, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyIndex": 1, + "SinglePlayerDutyOptions": { + "Index": 1 + }, "AetheryteShortcut": "North Shroud - Fallgourd Float", "Fly": true } diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json index b588274cc..0b0d45596 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/DRG/56_Lance of Destiny.json @@ -120,7 +120,9 @@ }, "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyIndex": 1 + "SinglePlayerDutyOptions": { + "Index": 1 + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json index 8924e6fdb..f45e4e63b 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json @@ -140,6 +140,9 @@ }, "TerritoryId": 141, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "Fly": true } ] diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json index b8d8505f5..507069443 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/567_Return of the Holyfist.json @@ -92,7 +92,9 @@ }, "TerritoryId": 130, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyIndex": 1, + "SinglePlayerDutyOptions": { + "Index": 1 + }, "AetheryteShortcut": "Ul'dah" } ] diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json index 08758166b..6abfe2689 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json @@ -35,6 +35,9 @@ }, "TerritoryId": 137, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "AetheryteShortcut": "Eastern La Noscea - Wineport", "Fly": true } diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json index 0dfda1454..b54256d6d 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json @@ -116,6 +116,9 @@ }, "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "Fly": true } ] diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json index a176c3b22..c110a643e 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json @@ -65,7 +65,8 @@ "AetheryteShortcut": "East Shroud - Hawthorne Hut", "SkipConditions": { "AetheryteShortcutIf": { - "InSameTerritory": true + "InSameTerritory": true, + "AetheryteLocked": "East Shroud - Hawthorne Hut" } } } @@ -116,7 +117,10 @@ "Z": 35.568726 }, "TerritoryId": 152, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json index cc3a74915..aebc91387 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json @@ -33,6 +33,39 @@ { "Sequence": 1, "Steps": [ + { + "DataId": 1001263, + "Position": { + "X": 181.41443, + "Y": -2.3519497, + "Z": -240.40594 + }, + "TerritoryId": 133, + "InteractionType": "Interact", + "TargetTerritoryId": 152, + "AethernetShortcut": [ + "[Gridania] Conjurers' Guild", + "[Gridania] Lancers' Guild" + ], + "SkipConditions": { + "StepIf": { + "AetheryteUnlocked": "East Shroud - Hawthorne Hut", + "InTerritory": [ + 152 + ] + } + } + }, + { + "TerritoryId": 152, + "InteractionType": "AttuneAetheryte", + "Aetheryte": "East Shroud - Hawthorne Hut", + "SkipConditions": { + "StepIf": { + "AetheryteUnlocked": "East Shroud - Hawthorne Hut" + } + } + }, { "Position": { "X": -276.804, @@ -42,7 +75,12 @@ "TerritoryId": 152, "InteractionType": "WalkTo", "AetheryteShortcut": "East Shroud - Hawthorne Hut", - "Fly": true + "Fly": true, + "SkipConditions": { + "AetheryteShortcutIf": { + "AetheryteLocked": "East Shroud - Hawthorne Hut" + } + } }, { "DataId": 2000889, @@ -212,6 +250,9 @@ }, "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "Fly": true } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json index cdb594ab5..3bc3081b6 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json @@ -138,7 +138,10 @@ "Z": 192.2179 }, "TerritoryId": 148, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json index 9bd7daf81..981c73c5e 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json @@ -111,7 +111,13 @@ "Z": 295.52136 }, "TerritoryId": 148, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "Notes": [ + "Healer NPC is only killed after the boss dies; all NPCs need to be killed for the duty to complete" + ] + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json index bb4aa6554..769e2f5f7 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json @@ -29,10 +29,12 @@ }, "TerritoryId": 148, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": false, - "BossModNotes": [ - "AI doesn't automatically target newly spawning adds and dies until after the boss died (tested on CNJ)" - ] + "SinglePlayerDutyOptions": { + "Enabled": false, + "Notes": [ + "AI doesn't automatically target newly spawning adds until after the boss died, and dies (tested on CNJ)" + ] + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json index aa50f006f..d8b6848a2 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json @@ -77,6 +77,12 @@ }, "TerritoryId": 148, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "Notes": [ + "(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete" + ] + }, "AetheryteShortcut": "Central Shroud - Bentbranch Meadows" } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json index 05886053a..236b0f94b 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json @@ -69,6 +69,12 @@ }, "TerritoryId": 135, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": false, + "Notes": [ + "(Phase 1, second enemy group) Stuck with enemy being out of sight -- but still able to attack you (tested on ACN)" + ] + }, "AetheryteShortcut": "Lower La Noscea - Moraby Drydocks" } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/466_Double Dealing.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/466_Double Dealing.json index 72ba9d7a0..ee2f686b9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/466_Double Dealing.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/466_Double Dealing.json @@ -45,8 +45,11 @@ "TerritoryId": 134, "InteractionType": "Combat", "EnemySpawnType": "AutoOnEnterArea", - "KillEnemyDataIds": [ - 52 + "ComplexCombatData": [ + { + "DataId": 52, + "IgnoreQuestMarker": true + } ] }, { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json index 62476d4e7..54199db01 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json @@ -73,7 +73,10 @@ "Z": -432.15082 }, "TerritoryId": 134, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json index 8e6aad406..901d40782 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json @@ -28,7 +28,13 @@ "Z": -141.7716 }, "TerritoryId": 134, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": false, + "Notes": [ + "AI doesn't automatically target newly spawning adds until after the boss died (requires healing luck on ACN)" + ] + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json index e812ae0f1..6ffa3d588 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json @@ -58,6 +58,12 @@ }, "TerritoryId": 138, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "Notes": [ + "(Phase 1) Kills PGL NPCs and then the boss - allied NPCs will kill most other NPCs eventually; all NPCs need to be killed for the duty to complete" + ] + }, "AetheryteShortcut": "Western La Noscea - Swiftperch" } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json index cab6933d0..c236b60bb 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json @@ -44,7 +44,10 @@ "Z": -242.51166 }, "TerritoryId": 145, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json index e5c21738f..4141d3200 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json @@ -79,6 +79,9 @@ }, "TerritoryId": 130, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "AetheryteShortcut": "Ul'dah", "AethernetShortcut": [ "[Ul'dah] Aetheryte Plaza", @@ -87,6 +90,9 @@ } ] }, + { + "Sequence": 5 + }, { "Sequence": 255, "Steps": [ diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json index 4874da87d..310b20cc8 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json @@ -63,12 +63,22 @@ "AethernetShortcut": [ "[Gridania] Aetheryte Plaza", "[Gridania] Lancers' Guild" - ] + ], + "SkipConditions": { + "StepIf": { + "AetheryteUnlocked": "East Shroud - Hawthorne Hut" + } + } }, { "TerritoryId": 152, "InteractionType": "AttuneAetheryte", - "Aetheryte": "East Shroud - Hawthorne Hut" + "Aetheryte": "East Shroud - Hawthorne Hut", + "SkipConditions": { + "StepIf": { + "AetheryteUnlocked": "East Shroud - Hawthorne Hut" + } + } }, { "DataId": 1004886, @@ -78,7 +88,16 @@ "Z": 475.30322 }, "TerritoryId": 152, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, + "AetheryteShortcut": "East Shroud - Hawthorne Hut", + "SkipConditions": { + "AetheryteShortcutIf": { + "InSameTerritory": true + } + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json index 4d2c5e406..dae94b36b 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json @@ -64,6 +64,9 @@ }, "TerritoryId": 135, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "AethernetShortcut": [ "[Limsa Lominsa] The Aftcastle", "[Limsa Lominsa] Tempest Gate (Lower La Noscea)" diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json index ba6b4a767..9bcf3680e 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json @@ -59,6 +59,9 @@ }, "TerritoryId": 140, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + }, "AetheryteShortcut": "Western Thanalan - Horizon" } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/303_Step Nine.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/303_Step Nine.json index 0e262818a..4c8290b14 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/303_Step Nine.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/303_Step Nine.json @@ -46,7 +46,8 @@ }, "StopDistance": 7, "TerritoryId": 141, - "InteractionType": "Interact" + "InteractionType": "Interact", + "DelaySecondsAtStart": 2 } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json index 42a2b633a..a61bf9782 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json @@ -158,7 +158,10 @@ "Z": 117.29602 }, "TerritoryId": 141, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/334_Storms on the Horizon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/334_Storms on the Horizon.json index 7c6a109b0..0074bc2bc 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/334_Storms on the Horizon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/334_Storms on the Horizon.json @@ -21,6 +21,15 @@ { "Sequence": 255, "Steps": [ + { + "Position": { + "X": -174.73444, + "Y": 15.450659, + "Z": -266.76144 + }, + "TerritoryId": 140, + "InteractionType": "WalkTo" + }, { "Position": { "X": -289.1099, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json index b35b8ea32..0b89ddcc7 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json @@ -37,7 +37,10 @@ "Z": -293.1411 }, "TerritoryId": 140, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3853_Heir Today, Gone Tomorrow.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3853_Heir Today, Gone Tomorrow.json index 143056172..6cc4dc5b0 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3853_Heir Today, Gone Tomorrow.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/3853_Heir Today, Gone Tomorrow.json @@ -29,7 +29,7 @@ }, "TerritoryId": 141, "InteractionType": "Combat", - "EnemySpawnType": "OverworldEnemies", + "EnemySpawnType": "FinishCombatIfAny", "KillEnemyDataIds": [ 352, 353 @@ -53,6 +53,25 @@ { "Sequence": 255, "Steps": [ + { + "Position": { + "X": 131.78122, + "Y": 20.119337, + "Z": -115.27284 + }, + "TerritoryId": 141, + "InteractionType": "WalkTo" + }, + { + "Position": { + "X": 127.7017, + "Y": -0.15994573, + "Z": -161.89238 + }, + "TerritoryId": 141, + "InteractionType": "WalkTo", + "DisableNavmesh": true + }, { "DataId": 1001605, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json index c12bda58a..c211fef2b 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json @@ -28,7 +28,10 @@ "Z": 536.88855 }, "TerritoryId": 141, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json index c16c22b97..841ded188 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json @@ -64,7 +64,13 @@ "Z": -131.48706 }, "TerritoryId": 141, - "InteractionType": "Interact", + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "Notes": [ + "(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete" + ] + }, "AetheryteShortcut": "Central Thanalan - Black Brush Station" } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A0-Gridania to East Shroud/3856_We Come in Peace.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A0-Gridania to East Shroud/3856_We Come in Peace.json index a7fddd2e9..b0e58b23b 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A0-Gridania to East Shroud/3856_We Come in Peace.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A0-Gridania to East Shroud/3856_We Come in Peace.json @@ -73,13 +73,23 @@ }, "TerritoryId": 133, "InteractionType": "Interact", - "TargetTerritoryId": 152 + "TargetTerritoryId": 152, + "SkipConditions": { + "StepIf": { + "AetheryteUnlocked": "East Shroud - Hawthorne Hut" + } + } }, { "TerritoryId": 152, "InteractionType": "AttuneAetheryte", "Aetheryte": "East Shroud - Hawthorne Hut", - "StopDistance": 5 + "StopDistance": 5, + "SkipConditions": { + "StepIf": { + "AetheryteUnlocked": "East Shroud - Hawthorne Hut" + } + } }, { "DataId": 1006188, @@ -89,7 +99,13 @@ "Z": 283.4973 }, "TerritoryId": 152, - "InteractionType": "CompleteQuest" + "InteractionType": "CompleteQuest", + "AetheryteShortcut": "East Shroud - Hawthorne Hut", + "SkipConditions": { + "AetheryteShortcutIf": { + "InSameTerritory": true + } + } } ] } diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json index 2b4fbd24c..0a54ca732 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json @@ -64,7 +64,10 @@ "Z": -39.383606 }, "TerritoryId": 152, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json index dfda0e548..3b4137858 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json @@ -83,7 +83,13 @@ "Z": -12.985474 }, "TerritoryId": 153, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "Notes": [ + "AI will kill initial adds before the boss, but not switch target whenever new enemies spawn; all NPCs need to be killed for the duty to complete" + ] + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json index cd396c519..8d6f72e7c 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json @@ -104,10 +104,12 @@ }, "TerritoryId": 1053, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": false, - "BossModNotes": [ - "Doesn't handle death properly" - ] + "SinglePlayerDutyOptions": { + "Enabled": false, + "Notes": [ + "Doesn't handle death properly" + ] + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json index f0598d7b4..b4f80942e 100644 --- a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json +++ b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json @@ -96,7 +96,9 @@ "TerritoryId": 138, "InteractionType": "SinglePlayerDuty", "Fly": true, - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json index 10c0755f4..a72b23340 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json @@ -59,7 +59,9 @@ }, "TerritoryId": 401, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json index e3a1d105d..d1e8cd475 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json @@ -79,7 +79,9 @@ "[Ishgard] The Forgotten Knight", "[Ishgard] The Tribunal" ], - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json index 6e052e8b4..92b63e22e 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json @@ -29,7 +29,9 @@ }, "TerritoryId": 145, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json index 3503cfee9..c7fd5ede3 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json @@ -79,7 +79,9 @@ "TerritoryId": 397, "InteractionType": "SinglePlayerDuty", "DisableNavmesh": true, - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json index eb9876a68..15f79ef81 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json @@ -75,7 +75,9 @@ }, "TerritoryId": 418, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json index b156029fd..30809e01a 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json @@ -57,7 +57,9 @@ "InteractionType": "SinglePlayerDuty", "Emote": "lookout", "StopDistance": 0.25, - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json index 31218140f..0b5e5d49a 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json @@ -48,7 +48,9 @@ "[Idyllshire] Aetheryte Plaza", "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" ], - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json index a498f657c..18809a5e5 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json @@ -69,7 +69,9 @@ }, "TerritoryId": 402, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": true + "SinglePlayerDutyOptions": { + "Enabled": true + } } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json index 51863a11f..2465f30c9 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json @@ -85,14 +85,16 @@ "TerritoryId": 351, "InteractionType": "SinglePlayerDuty", "Comment": "Estinien vs. Arch Ultima", - "BossModEnabled": false, - "BossModNotes": [ - "AI doesn't move automatically for the first boss", - "AI doesn't move automatically for the dialogue with gaius on the bridge", - "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)", - "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking" - ], - "$.1": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one" + "SinglePlayerDutyOptions": { + "Enabled": false, + "Notes": [ + "AI doesn't move automatically for the first boss", + "AI doesn't move automatically for the dialogue with gaius on the bridge", + "After walking downstairs automatically, AI tries to run back towards the stairs (ignoring the arena boudnary)", + "After moving from the arena boundary, AI doesn't move into melee range and stops too far away when initially attacking" + ] + }, + "$": "This doesn't have a duty confirmation dialog, so we're treating TEXT_LUCKMG110_03682_Q1_100_125 as one" } ] }, diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json index 49a098916..4fab7756d 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json @@ -46,10 +46,12 @@ }, "TerritoryId": 817, "InteractionType": "SinglePlayerDuty", - "BossModEnabled": false, - "BossModNotes": [ - "Doesn't walk to the teleporter to finish the duty" - ], + "SinglePlayerDutyOptions": { + "Enabled": false, + "Notes": [ + "Doesn't walk to the teleporter to finish the duty" + ] + }, "Fly": true, "Comment": "A Sleep Disturbed (Opo-Opo, Wolf, Serpent)", "$": "The dialogue choices and data ids here are recycled", diff --git a/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json b/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json index 160a2970d..206d7457b 100644 --- a/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json +++ b/QuestPaths/5.x - Shadowbringers/Trial Quests/3895_Sleep Now in Sapphire.json @@ -104,7 +104,9 @@ "StopDistance": 5, "TerritoryId": 829, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyIndex": 1, + "SinglePlayerDutyOptions": { + "Index": 1 + }, "DialogueChoices": [ { "Type": "List", diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index f5df18c8a..40204b393 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1267,20 +1267,32 @@ }, "then": { "properties": { - "BossModEnabled": { - "type": "boolean" - }, - "BossModNotes": { - "type": "array", - "items": { - "type": "string" - } - }, - "SinglePlayerDutyIndex": { - "type": "integer", - "minimum": 0, - "maximum": 1, - "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is" + "SinglePlayerDutyOptions": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean" + }, + "Notes": { + "type": "array", + "items": { + "type": "string" + } + }, + "Index": { + "type": "integer", + "minimum": 0, + "maximum": 1, + "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is" + }, + "$": { + "type": "string" + } + }, + "TODO_required": [ + "Enabled" + ], + "additionalProperties": false } } } diff --git a/Questionable.Model/Questing/QuestStep.cs b/Questionable.Model/Questing/QuestStep.cs index 8d9d31ecb..b96a30a20 100644 --- a/Questionable.Model/Questing/QuestStep.cs +++ b/Questionable.Model/Questing/QuestStep.cs @@ -75,9 +75,8 @@ public sealed class QuestStep public JumpDestination? JumpDestination { get; set; } public uint? ContentFinderConditionId { get; set; } public bool AutoDutyEnabled { get; set; } - public bool BossModEnabled { get; set; } - public List BossModNotes { get; set; } = []; - public byte SinglePlayerDutyIndex { get; set; } + public SinglePlayerDutyOptions? SinglePlayerDutyOptions { get; set; } + public byte SinglePlayerDutyIndex => SinglePlayerDutyOptions?.Index ?? 0; public SkipConditions? SkipConditions { get; set; } public List?> RequiredQuestVariables { get; set; } = new(); diff --git a/Questionable.Model/Questing/SinglePlayerDutyOptions.cs b/Questionable.Model/Questing/SinglePlayerDutyOptions.cs new file mode 100644 index 000000000..874fc5c03 --- /dev/null +++ b/Questionable.Model/Questing/SinglePlayerDutyOptions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Questionable.Model.Questing; + +public sealed class SinglePlayerDutyOptions +{ + public bool Enabled { get; set; } + public List Notes { get; set; } = []; + public byte Index { get; set; } +} diff --git a/Questionable/Controller/GameUi/InteractionUiController.cs b/Questionable/Controller/GameUi/InteractionUiController.cs index 1825b4f79..c4c70b982 100644 --- a/Questionable/Controller/GameUi/InteractionUiController.cs +++ b/Questionable/Controller/GameUi/InteractionUiController.cs @@ -691,7 +691,7 @@ internal sealed class InteractionUiController : IDisposable private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step) { if (step is { InteractionType: EInteractionType.SinglePlayerDuty } && - _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyIndex, step.BossModEnabled)) + _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyOptions)) { // Most of these are yes/no dialogs "Duty calls, ...". // diff --git a/Questionable/Controller/Steps/Common/SendNotification.cs b/Questionable/Controller/Steps/Common/SendNotification.cs index b2d146f00..97089936c 100644 --- a/Questionable/Controller/Steps/Common/SendNotification.cs +++ b/Questionable/Controller/Steps/Common/SendNotification.cs @@ -27,7 +27,7 @@ internal static class SendNotification new Task(step.InteractionType, step.ContentFinderConditionId.HasValue ? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name : step.Comment), - EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled) => + EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions) => new Task(step.InteractionType, quest.Info.Name), _ => null, }; diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs index b8bf39fe6..188729be4 100644 --- a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -21,7 +21,7 @@ internal static class SinglePlayerDuty if (step.InteractionType != EInteractionType.SinglePlayerDuty) yield break; - if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled)) + if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions)) { if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out var cfcData)) throw new TaskException("Failed to get content finder condition for solo instance"); diff --git a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs index 1476ed3c4..86e5d47d4 100644 --- a/Questionable/Controller/Steps/Shared/WaitAtEnd.cs +++ b/Questionable/Controller/Steps/Shared/WaitAtEnd.cs @@ -54,7 +54,7 @@ internal static class WaitAtEnd return [new WaitNextStepOrSequence()]; case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled): - case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled): + case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions): return [new EndAutomation()]; case EInteractionType.WalkTo: diff --git a/Questionable/External/BossModIpc.cs b/Questionable/External/BossModIpc.cs index e73e1863d..d157622eb 100644 --- a/Questionable/External/BossModIpc.cs +++ b/Questionable/External/BossModIpc.cs @@ -82,7 +82,7 @@ internal sealed class BossModIpc ClearPreset(); } - public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault) + public bool IsConfiguredToRunSoloInstance(ElementId questId, SinglePlayerDutyOptions? dutyOptions) { if (!IsSupported()) return false; @@ -90,7 +90,8 @@ internal sealed class BossModIpc if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod) return false; - if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData)) + dutyOptions ??= new(); + if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyOptions.Index, out var cfcData)) return false; if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) @@ -99,6 +100,6 @@ internal sealed class BossModIpc if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(cfcData.ContentFinderConditionId)) return true; - return enabledByDefault; + return dutyOptions.Enabled; } } diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 07149deba..c49a2c814 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -122,70 +122,24 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent foreach (var (questId, index, cfcData) in _territoryData.GetAllQuestsWithQuestBattles()) { IQuestInfo questInfo = _questData.GetQuestInfo(questId); - QuestStep questStep = new QuestStep - { - SinglePlayerDutyIndex = 0, - BossModEnabled = false, - }; - bool enabled; - if (_questRegistry.TryGetQuest(questId, out var quest)) - { - if (quest.Root.Disabled) - { - _logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId); - enabled = false; - } - else - { - var foundStep = quest.AllSteps().FirstOrDefault(x => - x.Step.InteractionType == EInteractionType.SinglePlayerDuty && - x.Step.SinglePlayerDutyIndex == index); - if (foundStep == default) - { - _logger.LogWarning( - "Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, - index); - enabled = false; - } - else - { - questStep = foundStep.Step; - enabled = true; - } - } - } - else - { - _logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId); - enabled = false; - } + (bool enabled, SinglePlayerDutyOptions options) = FindDutyOptions(questId, index); string name = $"{FormatLevel(questInfo.Level)} {questInfo.Name}"; if (!string.IsNullOrEmpty(cfcData.Name) && !questInfo.Name.EndsWith(cfcData.Name, StringComparison.Ordinal)) name += $" ({cfcData.Name})"; if (questsWithMultipleBattles.Contains(questId)) - name += $" (Part {questStep.SinglePlayerDutyIndex + 1})"; + name += $" (Part {options.Index + 1})"; else if (cfcData.ContentFinderConditionId is 674 or 691) name += " (Melee/Phys. Ranged)"; - var dutyInfo = new SinglePlayerDutyInfo( - cfcData.ContentFinderConditionId, - cfcData.TerritoryId, - name, - questInfo.Expansion, - questInfo.JournalGenre ?? uint.MaxValue, - questInfo.SortKey, - questStep.SinglePlayerDutyIndex, - enabled, - questStep.BossModEnabled, - questStep.BossModNotes); + var dutyInfo = new SinglePlayerDutyInfo(name, questInfo, cfcData, options, enabled); - if (cfcData.ContentFinderConditionId is 332 or 333 or 313 or 334) + if (dutyInfo.IsLimsaStart) startingCityBattles[EAetheryteLocation.Limsa].Add(dutyInfo); - else if (cfcData.ContentFinderConditionId is 296 or 297 or 299 or 298) + else if (dutyInfo.IsGridaniaStart) startingCityBattles[EAetheryteLocation.Gridania].Add(dutyInfo); - else if (cfcData.ContentFinderConditionId is 335 or 312 or 337 or 336) + else if (dutyInfo.IsUldahStart) startingCityBattles[EAetheryteLocation.Uldah].Add(dutyInfo); else if (questInfo.IsMainScenarioQuest) mainScenarioBattles.Add(dutyInfo); @@ -196,7 +150,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent foreach (var roleClassJob in classJobs) roleQuestBattles[roleClassJob].Add(dutyInfo); } - else if (dutyInfo.CfcId is 845 or 1016) + else if (dutyInfo.IsOtherRoleQuest) otherRoleQuestBattles.Add(dutyInfo); else otherBattles.Add(dutyInfo); @@ -220,7 +174,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent x => x.Value // level 10 quests use the same quest battle for [you started as this class] and [you picked this class up later] - .DistinctBy(y => y.CfcId) + .DistinctBy(y => y.ContentFinderConditionId) .OrderBy(y => y.JournalGenreId) .ThenBy(y => y.SortKey) .ThenBy(y => y.Index) @@ -242,6 +196,47 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent .ToImmutableList(); } + private (bool Enabled, SinglePlayerDutyOptions Options) FindDutyOptions(ElementId questId, byte index) + { + SinglePlayerDutyOptions options = new() + { + Index = 0, + Enabled = false, + }; + if (_questRegistry.TryGetQuest(questId, out var quest)) + { + if (quest.Root.Disabled) + { + _logger.LogDebug("Disabling quest battle for quest {QuestId}, quest is disabled", questId); + return (false, options); + } + else + { + var foundStep = quest.AllSteps() + .Select(x => x.Step) + .FirstOrDefault(x => + x.InteractionType == EInteractionType.SinglePlayerDuty && + x.SinglePlayerDutyIndex == index); + if (foundStep == null) + { + _logger.LogWarning( + "Disabling quest battle for quest {QuestId}, no battle with index {Index} found", questId, + index); + return (false, options); + } + else + { + return (true, foundStep.SinglePlayerDutyOptions ?? options); + } + } + } + else + { + _logger.LogDebug("Disabling quest battle for quest {QuestId}, unknown quest", questId); + return (false, options); + } + } + private string BuildJournalGenreLabel(uint journalGenreId) { var journalGenre = _dataManager.GetExcelSheet().GetRow(journalGenreId); @@ -250,7 +245,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent string genreName = journalGenre.Name.ExtractText(); string categoryName = journalCategory.Name.ExtractText(); - return $"{categoryName} {SeIconChar.ArrowRight.ToIconString()} {genreName}"; + return $"{categoryName} \u203B {genreName}"; } public override void DrawTab() @@ -423,13 +418,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent { ImGui.TableNextRow(); - string[] labels = dutyInfo.BossModEnabledByDefault + string[] labels = dutyInfo.EnabledByDefault ? SupportedCfcOptions : UnsupportedCfcOptions; int value = 0; - if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId)) + if (Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(dutyInfo.ContentFinderConditionId)) value = 1; - if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.CfcId)) + if (Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(dutyInfo.ContentFinderConditionId)) value = 2; if (ImGui.TableNextColumn()) @@ -445,7 +440,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ImGui.TextUnformatted(dutyInfo.Name); ImGui.Separator(); ImGui.BulletText($"TerritoryId: {dutyInfo.TerritoryId}"); - ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.CfcId}"); + ImGui.BulletText($"ContentFinderConditionId: {dutyInfo.ContentFinderConditionId}"); } } @@ -457,11 +452,18 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent else if (dutyInfo.Notes.Count > 0) { using var color = new ImRaii.Color(); - color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow); + if (!dutyInfo.EnabledByDefault) + color.Push(ImGuiCol.TextDisabled, ImGuiColors.DalamudYellow); + else + color.Push(ImGuiCol.TextDisabled, ImGuiColors.ParsedBlue); + ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) { - ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString()); + if (!dutyInfo.EnabledByDefault) + ImGui.TextDisabled(FontAwesomeIcon.ExclamationTriangle.ToIconString()); + else + ImGui.TextDisabled(FontAwesomeIcon.InfoCircle.ToIconString()); } if (ImGui.IsItemHovered()) @@ -478,19 +480,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent if (ImGui.TableNextColumn()) { - using var _ = ImRaii.PushId($"##Duty{dutyInfo.CfcId}"); + using var _ = ImRaii.PushId($"##Duty{dutyInfo.ContentFinderConditionId}"); using (ImRaii.Disabled(!dutyInfo.Enabled)) { ImGui.SetNextItemWidth(200); if (ImGui.Combo(string.Empty, ref value, labels, labels.Length)) { - Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId); - Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Remove(dutyInfo.ContentFinderConditionId); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Remove(dutyInfo.ContentFinderConditionId); if (value == 1) - Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Add(dutyInfo.ContentFinderConditionId); else if (value == 2) - Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.CfcId); + Configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Add(dutyInfo.ContentFinderConditionId); Save(); } @@ -519,14 +521,28 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent } private sealed record SinglePlayerDutyInfo( - uint CfcId, - uint TerritoryId, string Name, - EExpansionVersion Expansion, - uint JournalGenreId, - ushort SortKey, - byte Index, - bool Enabled, - bool BossModEnabledByDefault, - List Notes); + IQuestInfo QuestInfo, + TerritoryData.ContentFinderConditionData ContentFinderConditionData, + SinglePlayerDutyOptions Options, + bool Enabled) + { + public EExpansionVersion Expansion => QuestInfo.Expansion; + public uint JournalGenreId => QuestInfo.JournalGenre ?? uint.MaxValue; + public ushort SortKey => QuestInfo.SortKey; + public uint ContentFinderConditionId => ContentFinderConditionData.ContentFinderConditionId; + public uint TerritoryId => ContentFinderConditionData.TerritoryId; + public byte Index => Options.Index; + public bool EnabledByDefault => Options.Enabled; + public IReadOnlyList Notes => Options.Notes; + + public bool IsLimsaStart => ContentFinderConditionId is 332 or 333 or 313 or 334; + public bool IsGridaniaStart => ContentFinderConditionId is 296 or 297 or 299 or 298; + public bool IsUldahStart => ContentFinderConditionId is 335 or 312 or 337 or 336; + + /// + /// 'Other' role quest is the post-EW/DT role quests. + /// + public bool IsOtherRoleQuest => ContentFinderConditionId is 845 or 1016; + } } From dcdc288b081a6779ee22d3c1da1d9b43ac417331 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 01:06:41 +0100 Subject: [PATCH 11/18] Add special handling for Lahabrea fight --- .../4522_The Ultimate Weapon.json | 5 +- Questionable/Controller/QuestController.cs | 9 +++- .../Steps/Interactions/SinglePlayerDuty.cs | 47 ++++++++++++++++++- Questionable/Functions/GameFunctions.cs | 6 +++ Questionable/QuestionablePlugin.cs | 1 + 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json index 8d6f72e7c..ffa07f49d 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json @@ -105,10 +105,7 @@ "TerritoryId": 1053, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": false, - "Notes": [ - "Doesn't handle death properly" - ] + "Enabled": true } } ] diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 9fbaadfdd..b509de18a 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -10,6 +10,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; +using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Shared; using Questionable.External; using Questionable.Functions; @@ -200,7 +201,13 @@ internal sealed class QuestController : MiniTaskController if (!_clientState.IsLoggedIn || _condition[ConditionFlag.Unconscious]) { - if (!_taskQueue.AllTasksComplete) + if (_condition[ConditionFlag.Unconscious] && + _condition[ConditionFlag.SufferingStatusAffliction63] && + _clientState.TerritoryType == SinglePlayerDuty.LahabreaTerritoryId) + { + // ignore, we're in the lahabrea fight + } + else if (!_taskQueue.AllTasksComplete) { Stop("HP = 0"); _movementController.Stop(); diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs index 188729be4..7bf43b810 100644 --- a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -1,7 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; +using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Shared; using Questionable.Data; using Questionable.External; @@ -12,9 +17,13 @@ namespace Questionable.Controller.Steps.Interactions; internal static class SinglePlayerDuty { + public const int LahabreaTerritoryId = 1052; + internal sealed class Factory( BossModIpc bossModIpc, - TerritoryData territoryData) : ITaskFactory + TerritoryData territoryData, + ICondition condition, + IClientState clientState) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { @@ -28,6 +37,14 @@ internal static class SinglePlayerDuty yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId); yield return new EnableAi(); + if (cfcData.TerritoryId == LahabreaTerritoryId) + { + yield return new SetTarget(14643); + yield return new WaitCondition.Task(() => condition[ConditionFlag.Unconscious] || clientState.TerritoryType != LahabreaTerritoryId, "Wait(death)"); + yield return new DisableAi(); + yield return new WaitCondition.Task(() => !condition[ConditionFlag.Unconscious] || clientState.TerritoryType != LahabreaTerritoryId, "Wait(resurrection)"); + yield return new EnableAi(); + } yield return new WaitSinglePlayerDuty(cfcData.ContentFinderConditionId); yield return new DisableAi(); yield return new WaitAtEnd.WaitNextStepOrSequence(); @@ -113,4 +130,32 @@ internal static class SinglePlayerDuty public override bool ShouldInterruptOnDamage() => false; } + + // TODO this should be handled in VBM + internal sealed record SetTarget(uint DataId) : ITask + { + public override string ToString() => $"SetTarget({DataId})"; + } + + internal sealed class SetTargetExecutor( + ITargetManager targetManager, + IObjectTable objectTable) : TaskExecutor + { + protected override bool Start() => true; + + public override ETaskResult Update() + { + if (targetManager.Target?.DataId == Task.DataId) + return ETaskResult.TaskComplete; + + IGameObject? gameObject = objectTable.FirstOrDefault(x => x.DataId == Task.DataId); + if (gameObject == null) + return ETaskResult.StillRunning; + + targetManager.Target = gameObject; + return ETaskResult.StillRunning; + } + + public override bool ShouldInterruptOnDamage() => false; + } } diff --git a/Questionable/Functions/GameFunctions.cs b/Questionable/Functions/GameFunctions.cs index 09a9be25c..27c5fc868 100644 --- a/Questionable/Functions/GameFunctions.cs +++ b/Questionable/Functions/GameFunctions.cs @@ -16,6 +16,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; +using Questionable.Controller.Steps.Interactions; using Questionable.Model; using Questionable.Model.Questing; using Action = Lumina.Excel.Sheets.Action; @@ -427,6 +428,11 @@ internal sealed unsafe class GameFunctions return true; } + if (_condition[ConditionFlag.Unconscious] && + _condition[ConditionFlag.SufferingStatusAffliction63] && + _clientState.TerritoryType == SinglePlayerDuty.LahabreaTerritoryId) + return false; // needed to process the tasks + return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] || _condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] || _condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] || diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index e6a82af2d..753ddb13c 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -231,6 +231,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection .AddTaskExecutor(); serviceCollection.AddTaskExecutor(); + serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskExecutor(); serviceCollection.AddTaskFactory(); From 22aa81cf7501c64a1ff54ca3cbbbd04adfaa35e7 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 19:52:10 +0100 Subject: [PATCH 12/18] Fix Crystal Tower quests ignoring 'NextQuestId' --- Questionable/Controller/Steps/Common/NextQuest.cs | 6 ++++-- Questionable/Functions/QuestFunctions.cs | 15 ++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Questionable/Controller/Steps/Common/NextQuest.cs b/Questionable/Controller/Steps/Common/NextQuest.cs index 3ac7758d7..8262a9ec6 100644 --- a/Questionable/Controller/Steps/Common/NextQuest.cs +++ b/Questionable/Controller/Steps/Common/NextQuest.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Logging; +using System.Linq; +using Microsoft.Extensions.Logging; +using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; @@ -21,7 +23,7 @@ internal static class NextQuest return null; // probably irrelevant, since pick up is handled elsewhere (and, in particular, checks for aetherytes and stuff) - if (questFunctions.GetPriorityQuests().Contains(step.NextQuestId)) + if (questFunctions.GetPriorityQuests(onlyClassAndRoleQuests: true).Contains(step.NextQuestId)) return null; return new SetQuestTask(step.NextQuestId, quest.Id); diff --git a/Questionable/Functions/QuestFunctions.cs b/Questionable/Functions/QuestFunctions.cs index f0e6d1591..6107e2570 100644 --- a/Questionable/Functions/QuestFunctions.cs +++ b/Questionable/Functions/QuestFunctions.cs @@ -401,14 +401,15 @@ internal sealed unsafe class QuestFunctions return 1000 * quest.AllSteps().Count(x => x.Step.AetheryteShortcut != null); } - public List GetPriorityQuests() + public List GetPriorityQuests(bool onlyClassAndRoleQuests = false) { - List priorityQuests = - [ - new QuestId(1157), // Garuda (Hard) - new QuestId(1158), // Titan (Hard) - ..QuestData.CrystalTowerQuests - ]; + List priorityQuests = []; + if (!onlyClassAndRoleQuests) + { + priorityQuests.Add(new QuestId(1157)); // Garuda (Hard) + priorityQuests.Add(new QuestId(1158)); // Titan (Hard) + priorityQuests.AddRange(QuestData.CrystalTowerQuests); + } EClassJob classJob = (EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId ?? EClassJob.Adventurer; uint[] shadowbringersRoleQuestChapters = QuestData.AllRoleQuestChapters.Select(x => x[0]).ToArray(); From ed797143b3cb984b944d9d40d56ebf78e4c38e51 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 22:28:04 +0100 Subject: [PATCH 13/18] Update quest battle metadata --- .../MNK/558_The Spirit Is Willing.json | 3 ++- .../Class Quests/WAR/1054_How to Quit You.json | 3 ++- .../Class Quests/WHM/147_Trial by Wind.json | 3 ++- .../Class Quests/WHM/91_Trial by Wind.json | 3 ++- .../Class Quests/WHM/92_Trial by Water.json | 3 ++- .../MSQ-1/Gridania/129_Spirithold Broken.json | 3 ++- .../MSQ-1/Gridania/161_Leia's Legacy.json | 1 + .../MSQ-1/Gridania/445_Chasing Shadows.json | 1 + .../MSQ-1/Gridania/447_To Guard a Guardian.json | 1 + .../MSQ-1/Limsa/414_Victory in Peril.json | 1 + .../MSQ-1/Limsa/469_Just Deserts.json | 3 ++- .../MSQ-1/Limsa/543_Lurkers in the Grotto.json | 1 + .../MSQ-1/Limsa/544_Feint and Strike.json | 1 + .../MSQ-1/Shared/343_Lord of the Inferno.json | 3 ++- .../MSQ-1/Shared/660_Into a Copper Hell.json | 3 ++- .../680_The Company You Keep (Twin Adders).json | 3 ++- .../681_The Company You Keep (Maelstrom).json | 3 ++- ...2_The Company You Keep (Immortal Flames).json | 3 ++- .../MSQ-1/Ul'dah/320_Way Down in the Hole.json | 3 ++- .../MSQ-1/Ul'dah/336_Oh Captain, My Captain.json | 3 ++- .../Ul'dah/550_Underneath the Sultantree.json | 3 ++- .../MSQ-1/Ul'dah/551_Duty, Honor, Country.json | 1 + .../724_Brotherly Love.json | 3 ++- .../3862_Nouveau Riche.json | 1 + .../4521_Operation Archon.json | 6 +++++- .../4522_The Ultimate Weapon.json | 3 ++- .../E1-2.1/1190_You Have Selected Regicide.json | 9 +++++++++ .../MSQ-2/E3-2.3/1456_Guardian of Eorzea.json | 4 ++++ .../MSQ-2/E4-2.4/88_The Reason Roaille.json | 8 ++++++++ .../Class Quests/WAR/601_And My Axe.json | 3 ++- .../1595_A Series of Unfortunate Events.json | 3 ++- .../1597_Divine Intervention.json | 3 ++- .../A2-Raubahn/1601_Keeping the Flame Alive.json | 3 ++- .../1606_Sounding Out the Amphitheatre.json | 3 ++- .../MSQ/A4-Ishgard/1639_Fire and Blood.json | 3 ++- .../A5-Sea of Clouds/1644_Familiar Faces.json | 3 ++- .../1657_An Illuminati Incident.json | 3 ++- .../1667_Close Encounters of the VIth Kind.json | 3 ++- .../3682_Vows of Virtue, Deeds of Cruelty.json | 1 + .../MSQ/H-5.2/3765_A Sleep Disturbed.json | 1 + QuestPaths/quest-v1.json | 4 ++++ .../Validators/UniqueStartStopValidator.cs | 2 +- .../SinglePlayerDutyConfigComponent.cs | 16 +++++++++++++--- 43 files changed, 106 insertions(+), 31 deletions(-) diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json index f45e4e63b..ebee0edac 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/MNK/558_The Spirit Is Willing.json @@ -141,7 +141,8 @@ "TerritoryId": 141, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "Fly": true } diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json index 6abfe2689..ce4bc552e 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WAR/1054_How to Quit You.json @@ -36,7 +36,8 @@ "TerritoryId": 137, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "AetheryteShortcut": "Eastern La Noscea - Wineport", "Fly": true diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json index b54256d6d..02e891b3f 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/147_Trial by Wind.json @@ -117,7 +117,8 @@ "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "Fly": true } diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json index c110a643e..1348cd351 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/91_Trial by Wind.json @@ -119,7 +119,8 @@ "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json index aebc91387..7a06c8454 100644 --- a/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json +++ b/QuestPaths/2.x - A Realm Reborn/Class Quests/WHM/92_Trial by Water.json @@ -251,7 +251,8 @@ "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "Fly": true } diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json index 3bc3081b6..bbe047160 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/129_Spirithold Broken.json @@ -140,7 +140,8 @@ "TerritoryId": 148, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json index 981c73c5e..8706b50a0 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/161_Leia's Legacy.json @@ -114,6 +114,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": true, + "TestedBossModVersion": 292, "Notes": [ "Healer NPC is only killed after the boss dies; all NPCs need to be killed for the duty to complete" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json index 769e2f5f7..be848e395 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/445_Chasing Shadows.json @@ -31,6 +31,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": false, + "TestedBossModVersion": 292, "Notes": [ "AI doesn't automatically target newly spawning adds until after the boss died, and dies (tested on CNJ)" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json index d8b6848a2..0c1c2e8fa 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Gridania/447_To Guard a Guardian.json @@ -79,6 +79,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": true, + "TestedBossModVersion": 292, "Notes": [ "(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json index 236b0f94b..be519bb93 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/414_Victory in Peril.json @@ -71,6 +71,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": false, + "TestedBossModVersion": 292, "Notes": [ "(Phase 1, second enemy group) Stuck with enemy being out of sight -- but still able to attack you (tested on ACN)" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json index 54199db01..ffd07a9de 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/469_Just Deserts.json @@ -75,7 +75,8 @@ "TerritoryId": 134, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json index 901d40782..bb1e80d8d 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/543_Lurkers in the Grotto.json @@ -31,6 +31,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": false, + "TestedBossModVersion": 292, "Notes": [ "AI doesn't automatically target newly spawning adds until after the boss died (requires healing luck on ACN)" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json index 6ffa3d588..527a16105 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Limsa/544_Feint and Strike.json @@ -60,6 +60,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": true, + "TestedBossModVersion": 292, "Notes": [ "(Phase 1) Kills PGL NPCs and then the boss - allied NPCs will kill most other NPCs eventually; all NPCs need to be killed for the duty to complete" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json index c236b60bb..ea9fd4bab 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/343_Lord of the Inferno.json @@ -46,7 +46,8 @@ "TerritoryId": 145, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json index 4141d3200..ba1604a44 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/660_Into a Copper Hell.json @@ -80,7 +80,8 @@ "TerritoryId": 130, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "AetheryteShortcut": "Ul'dah", "AethernetShortcut": [ diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json index 310b20cc8..7afc73a39 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/680_The Company You Keep (Twin Adders).json @@ -90,7 +90,8 @@ "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "AetheryteShortcut": "East Shroud - Hawthorne Hut", "SkipConditions": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json index dae94b36b..87ab602c4 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/681_The Company You Keep (Maelstrom).json @@ -65,7 +65,8 @@ "TerritoryId": 135, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "AethernetShortcut": [ "[Limsa Lominsa] The Aftcastle", diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json index 9bcf3680e..2696f1a70 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Shared/682_The Company You Keep (Immortal Flames).json @@ -60,7 +60,8 @@ "TerritoryId": 140, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 }, "AetheryteShortcut": "Western Thanalan - Horizon" } diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json index a61bf9782..29b7f66c9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/320_Way Down in the Hole.json @@ -160,7 +160,8 @@ "TerritoryId": 141, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json index 0b89ddcc7..d69bd0e19 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/336_Oh Captain, My Captain.json @@ -39,7 +39,8 @@ "TerritoryId": 140, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json index c211fef2b..91ccee4d9 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/550_Underneath the Sultantree.json @@ -30,7 +30,8 @@ "TerritoryId": 141, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json index 841ded188..3622d62c2 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-1/Ul'dah/551_Duty, Honor, Country.json @@ -67,6 +67,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": true, + "TestedBossModVersion": 292, "Notes": [ "(Phase 1) Healer NPCs are only killed after the boss dies - allied NPCs will kill them eventually; all NPCs need to be killed for the duty to complete" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json index 0a54ca732..0f856627f 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A2-East Shroud to South Shroud/724_Brotherly Love.json @@ -66,7 +66,8 @@ "TerritoryId": 152, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json index 3b4137858..88e38d619 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/A3-South Shroud, Buscarron’s Druthers/3862_Nouveau Riche.json @@ -86,6 +86,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": true, + "TestedBossModVersion": 292, "Notes": [ "AI will kill initial adds before the boss, but not switch target whenever new enemies spawn; all NPCs need to be killed for the duty to complete" ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4521_Operation Archon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4521_Operation Archon.json index 2f33376f4..3af001c02 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4521_Operation Archon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4521_Operation Archon.json @@ -159,7 +159,11 @@ "Z": -805.478 }, "TerritoryId": 140, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "TestedBossModVersion": 292 + } } ] }, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json index ffa07f49d..e9c620671 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/C9-Ultimate Weapon/4522_The Ultimate Weapon.json @@ -105,7 +105,8 @@ "TerritoryId": 1053, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/1190_You Have Selected Regicide.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/1190_You Have Selected Regicide.json index bf6395ceb..18ccd2d00 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/1190_You Have Selected Regicide.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E1-2.1/1190_You Have Selected Regicide.json @@ -68,6 +68,15 @@ { "Sequence": 3, "Steps": [ + { + "Position": { + "X": -561.9863, + "Y": 9.919454, + "Z": 66.29564 + }, + "TerritoryId": 152, + "InteractionType": "WalkTo" + }, { "DataId": 1008276, "Position": { diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1456_Guardian of Eorzea.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1456_Guardian of Eorzea.json index 4f6424b0f..977457fea 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1456_Guardian of Eorzea.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1456_Guardian of Eorzea.json @@ -78,6 +78,10 @@ "StopDistance": 1, "TerritoryId": 156, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "TestedBossModVersion": 292 + }, "Fly": true } ] diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/88_The Reason Roaille.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/88_The Reason Roaille.json index 49bf11b4b..ef6c35411 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/88_The Reason Roaille.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E4-2.4/88_The Reason Roaille.json @@ -71,6 +71,14 @@ }, "TerritoryId": 147, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "TestedBossModVersion": 292, + "Notes": [ + "Will target Eline first (other NPCs later), and move to some -other- group of NPCs; only re-targets once they're at 1 HP (for Eline) or die", + "If the target isn't in melee range but other NPCs are, whether any AOEs are used for nearby enemies seems random" + ] + }, "Fly": true, "AetheryteShortcut": "Northern Thanalan - Ceruleum Processing Plant" } diff --git a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json index b4f80942e..14cf5ecd2 100644 --- a/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json +++ b/QuestPaths/3.x - Heavensward/Class Quests/WAR/601_And My Axe.json @@ -97,7 +97,8 @@ "InteractionType": "SinglePlayerDuty", "Fly": true, "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json index a72b23340..39e74a09f 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1595_A Series of Unfortunate Events.json @@ -60,7 +60,8 @@ "TerritoryId": 401, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json index d1e8cd475..e03080cc3 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1597_Divine Intervention.json @@ -80,7 +80,8 @@ "[Ishgard] The Tribunal" ], "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json index 92b63e22e..2ab1b1a9c 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json @@ -30,7 +30,8 @@ "TerritoryId": 145, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json index c7fd5ede3..8cec31a79 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A3.1-Coerthas Western Highlands 2/1606_Sounding Out the Amphitheatre.json @@ -80,7 +80,8 @@ "InteractionType": "SinglePlayerDuty", "DisableNavmesh": true, "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json index 15f79ef81..66c6232c3 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A4-Ishgard/1639_Fire and Blood.json @@ -76,7 +76,8 @@ "TerritoryId": 418, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json index 30809e01a..d47363bf5 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A5-Sea of Clouds/1644_Familiar Faces.json @@ -58,7 +58,8 @@ "Emote": "lookout", "StopDistance": 0.25, "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json index 0b5e5d49a..799f242fe 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A6-The Dravanian Hinterlands/1657_An Illuminati Incident.json @@ -49,7 +49,8 @@ "[Idyllshire] Epilogue Gate (Eastern Hinterlands)" ], "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json index 18809a5e5..56776f285 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A7-Azys Lla/1667_Close Encounters of the VIth Kind.json @@ -70,7 +70,8 @@ "TerritoryId": 402, "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { - "Enabled": true + "Enabled": true, + "TestedBossModVersion": 292 } } ] diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json index 2465f30c9..cf937a672 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/G-5.1/3682_Vows of Virtue, Deeds of Cruelty.json @@ -87,6 +87,7 @@ "Comment": "Estinien vs. Arch Ultima", "SinglePlayerDutyOptions": { "Enabled": false, + "TestedBossModVersion": 292, "Notes": [ "AI doesn't move automatically for the first boss", "AI doesn't move automatically for the dialogue with gaius on the bridge", diff --git a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json index 4fab7756d..0255c3224 100644 --- a/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json +++ b/QuestPaths/5.x - Shadowbringers/MSQ/H-5.2/3765_A Sleep Disturbed.json @@ -48,6 +48,7 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": false, + "TestedBossModVersion": 292, "Notes": [ "Doesn't walk to the teleporter to finish the duty" ] diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 40204b393..ede8e6c66 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1285,6 +1285,10 @@ "maximum": 1, "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is" }, + "TestedBossModVersion": { + "type": "number", + "minimum": 292 + }, "$": { "type": "string" } diff --git a/Questionable/Validation/Validators/UniqueStartStopValidator.cs b/Questionable/Validation/Validators/UniqueStartStopValidator.cs index b7e088133..3c9651f67 100644 --- a/Questionable/Validation/Validators/UniqueStartStopValidator.cs +++ b/Questionable/Validation/Validators/UniqueStartStopValidator.cs @@ -9,7 +9,7 @@ internal sealed class UniqueStartStopValidator : IQuestValidator { public IEnumerable Validate(Quest quest) { - if (quest.Id is SatisfactionSupplyNpcId) + if (quest.Id is SatisfactionSupplyNpcId or AlliedSocietyDailyId) yield break; var questAccepts = diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index c49a2c814..18f7012c4 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -34,7 +34,9 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent (EClassJob.BlackMage, "Magical Ranged Role Quests"), ]; +#if false private readonly string[] _retryDifficulties = ["Normal", "Easy", "Very Easy"]; +#endif private readonly TerritoryData _territoryData; private readonly QuestRegistry _questRegistry; @@ -263,12 +265,19 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent using (ImRaii.PushIndent(ImGui.GetFrameHeight() + ImGui.GetStyle().ItemInnerSpacing.X)) { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(ImGuiColors.DalamudRed, - "Work in Progress: For now, this will always use BossMod for combat."); + using (_ = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + { + ImGui.TextUnformatted("Work in Progress:"); + ImGui.BulletText("Will always use BossMod for combat (ignoring the configured combat module)."); + ImGui.BulletText("Only a small subset of quest battles have been tested - most of which are in the MSQ."); + ImGui.BulletText("When retrying a failed battle, it will always start at 'Normal' difficulty."); + ImGui.BulletText("Please don't enable this option when using a BossMod fork (such as Reborn);\nwith the combat changes, it is unlikely to be compatible."); + } +#if false using (ImRaii.Disabled(!runSoloInstancesWithBossMod)) { + ImGui.Spacing(); int retryDifficulty = Configuration.SinglePlayerDuties.RetryDifficulty; if (ImGui.Combo("Difficulty when retrying a quest battle", ref retryDifficulty, _retryDifficulties, _retryDifficulties.Length)) @@ -277,6 +286,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent Save(); } } +#endif } ImGui.Separator(); From 224825b0719cdd7fe91540e054e8cd91583ff0d7 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 23:08:58 +0100 Subject: [PATCH 14/18] Check for unexpected party members when entering instanced duties --- .../Controller/Utils/PartyWatchDog.cs | 167 ++++++++++++++++++ Questionable/DalamudInitializer.cs | 5 + Questionable/QuestionablePlugin.cs | 3 + 3 files changed, 175 insertions(+) create mode 100644 Questionable/Controller/Utils/PartyWatchDog.cs diff --git a/Questionable/Controller/Utils/PartyWatchDog.cs b/Questionable/Controller/Utils/PartyWatchDog.cs new file mode 100644 index 000000000..9e7a0cc72 --- /dev/null +++ b/Questionable/Controller/Utils/PartyWatchDog.cs @@ -0,0 +1,167 @@ +using System; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Group; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; + +namespace Questionable.Controller.Utils; + +internal sealed class PartyWatchDog : IDisposable +{ + private readonly QuestController _questController; + private readonly IClientState _clientState; + private readonly IChatGui _chatGui; + private readonly ILogger _logger; + + private ushort? _uncheckedTeritoryId; + + public PartyWatchDog(QuestController questController, IClientState clientState, IChatGui chatGui, + ILogger logger) + { + _questController = questController; + _clientState = clientState; + _chatGui = chatGui; + _logger = logger; + + _clientState.TerritoryChanged += TerritoryChanged; + } + + private unsafe void TerritoryChanged(ushort newTerritoryId) + { + var intendedUse = (ETerritoryIntendedUseEnum)GameMain.Instance()->CurrentTerritoryIntendedUseId; + switch (intendedUse) + { + case ETerritoryIntendedUseEnum.Gaol: + case ETerritoryIntendedUseEnum.Frontline: + case ETerritoryIntendedUseEnum.LordOfVerminion: + case ETerritoryIntendedUseEnum.Diadem: + case ETerritoryIntendedUseEnum.CrystallineConflict: + case ETerritoryIntendedUseEnum.Battlehall: + case ETerritoryIntendedUseEnum.CrystallineConflict2: + case ETerritoryIntendedUseEnum.DeepDungeon: + case ETerritoryIntendedUseEnum.TreasureMapDuty: + case ETerritoryIntendedUseEnum.Diadem2: + case ETerritoryIntendedUseEnum.RivalWings: + case ETerritoryIntendedUseEnum.Eureka: + case ETerritoryIntendedUseEnum.LeapOfFaith: + case ETerritoryIntendedUseEnum.OceanFishing: + case ETerritoryIntendedUseEnum.Diadem3: + case ETerritoryIntendedUseEnum.Bozja: + case ETerritoryIntendedUseEnum.Battlehall2: + case ETerritoryIntendedUseEnum.Battlehall3: + case ETerritoryIntendedUseEnum.LargeScaleRaid: + case ETerritoryIntendedUseEnum.LargeScaleSavageRaid: + case ETerritoryIntendedUseEnum.Blunderville: + StopIfRunning($"Unsupported Area entered ({newTerritoryId})"); + break; + + case ETerritoryIntendedUseEnum.Dungeon: + case ETerritoryIntendedUseEnum.VariantDungeon: + case ETerritoryIntendedUseEnum.AllianceRaid: + case ETerritoryIntendedUseEnum.Trial: + case ETerritoryIntendedUseEnum.Raid: + case ETerritoryIntendedUseEnum.Raid2: + case ETerritoryIntendedUseEnum.SeasonalEvent: + case ETerritoryIntendedUseEnum.SeasonalEvent2: + case ETerritoryIntendedUseEnum.CriterionDuty: + case ETerritoryIntendedUseEnum.CriterionSavageDuty: + _uncheckedTeritoryId = newTerritoryId; + _logger.LogInformation("Will check territory {TerritoryId} after loading", newTerritoryId); + break; + } + } + + public unsafe void Update() + { + if (_uncheckedTeritoryId == _clientState.TerritoryType && GameMain.Instance()->TerritoryLoadState == 2) + { + var groupManager = GroupManager.Instance(); + if (groupManager == null) + return; + + byte memberCount = groupManager->MainGroup.MemberCount; + _logger.LogDebug("Terrritory {TerritoryId} with {MemberCount} members", _uncheckedTeritoryId, memberCount); + if (memberCount > 1) + StopIfRunning("Other party members present"); + + _uncheckedTeritoryId = null; + } + } + + private void StopIfRunning(string reason) + { + if (_questController.IsRunning || _questController.AutomationType != QuestController.EAutomationType.Manual) + { + _chatGui.PrintError( + $"Stopping Questionable: {reason}. If you believe this to be correct, please restart Questionable manually.", + CommandHandler.MessageTag, CommandHandler.TagColor); + _questController.Stop(reason); + } + } + + public void Dispose() + { + _clientState.TerritoryChanged -= TerritoryChanged; + } + + // from https://github.com/NightmareXIV/ECommons/blob/f69e460e95134c72592654059843b138b4c01a9e/ECommons/ExcelServices/TerritoryIntendedUseEnum.cs#L5 + [UsedImplicitly(ImplicitUseTargetFlags.Members, Reason = "game data")] + private enum ETerritoryIntendedUseEnum : byte + { + CityArea = 0, + OpenWorld = 1, + Inn = 2, + Dungeon = 3, + VariantDungeon = 4, + Gaol = 5, + StartingArea = 6, + QuestArea = 7, + AllianceRaid = 8, + QuestBattle = 9, + Trial = 10, + QuestArea2 = 12, + ResidentialArea = 13, + HousingInstances = 14, + QuestArea3 = 15, + Raid = 16, + Raid2 = 17, + Frontline = 18, + ChocoboSquare = 20, + RestorationEvent = 21, + Sanctum = 22, + GoldSaucer = 23, + LordOfVerminion = 25, + Diadem = 26, + HallOfTheNovice = 27, + CrystallineConflict = 28, + QuestBattle2 = 29, + Barracks = 30, + DeepDungeon = 31, + SeasonalEvent = 32, + TreasureMapDuty = 33, + SeasonalEventDuty = 34, + Battlehall = 35, + CrystallineConflict2 = 37, + Diadem2 = 38, + RivalWings = 39, + Unknown1 = 40, + Eureka = 41, + SeasonalEvent2 = 43, + LeapOfFaith = 44, + MaskedCarnivale = 45, + OceanFishing = 46, + Diadem3 = 47, + Bozja = 48, + IslandSanctuary = 49, + Battlehall2 = 50, + Battlehall3 = 51, + LargeScaleRaid = 52, + LargeScaleSavageRaid = 53, + QuestArea4 = 54, + TribalInstance = 56, + CriterionDuty = 57, + CriterionSavageDuty = 58, + Blunderville = 59, + } +} diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index a4b228d92..1c6f8a667 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -7,6 +7,7 @@ using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Controller; using Questionable.Controller.GameUi; +using Questionable.Controller.Utils; using Questionable.Windows; namespace Questionable; @@ -23,6 +24,7 @@ internal sealed class DalamudInitializer : IDisposable private readonly ConfigWindow _configWindow; private readonly IToastGui _toastGui; private readonly Configuration _configuration; + private readonly PartyWatchDog _partyWatchDog; private readonly ILogger _logger; public DalamudInitializer( @@ -42,6 +44,7 @@ internal sealed class DalamudInitializer : IDisposable PriorityWindow priorityWindow, IToastGui toastGui, Configuration configuration, + PartyWatchDog partyWatchDog, ILogger logger) { _pluginInterface = pluginInterface; @@ -54,6 +57,7 @@ internal sealed class DalamudInitializer : IDisposable _configWindow = configWindow; _toastGui = toastGui; _configuration = configuration; + _partyWatchDog = partyWatchDog; _logger = logger; _windowSystem.AddWindow(oneTimeSetupWindow); @@ -77,6 +81,7 @@ internal sealed class DalamudInitializer : IDisposable private void FrameworkUpdate(IFramework framework) { + _partyWatchDog.Update(); _questController.Update(); try diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 753ddb13c..04d69bcf2 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -20,6 +20,7 @@ using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Gathering; using Questionable.Controller.Steps.Interactions; using Questionable.Controller.Steps.Leves; +using Questionable.Controller.Utils; using Questionable.Data; using Questionable.External; using Questionable.Functions; @@ -260,6 +261,8 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); From 71b40496fbdd96f17a9c15fac28fb16f52e23e37 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 23:11:08 +0100 Subject: [PATCH 15/18] Fix interrupts in interactions --- .../Controller/Steps/Interactions/Interact.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Questionable/Controller/Steps/Interactions/Interact.cs b/Questionable/Controller/Steps/Interactions/Interact.cs index 0073349b3..2e44caf3b 100644 --- a/Questionable/Controller/Steps/Interactions/Interact.cs +++ b/Questionable/Controller/Steps/Interactions/Interact.cs @@ -96,6 +96,12 @@ internal static class Interact private EInteractionState _interactionState = EInteractionState.None; private DateTime _continueAt = DateTime.MinValue; + /// + /// A slight delay when we think an interaction has ended, to make sure that we're processing "Action cancelled" + /// prior to the next step (in case we're attacked). + /// + private bool delayedFinalCheck; + public Quest? Quest => Task.Quest; public EInteractionType InteractionType { get; set; } @@ -179,7 +185,14 @@ internal static class Interact return ETaskResult.StillRunning; else if (ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed) - return ETaskResult.TaskComplete; + { + if (delayedFinalCheck) + return ETaskResult.TaskComplete; + + _continueAt = DateTime.Now.AddSeconds(0.2); + delayedFinalCheck = true; + return ETaskResult.StillRunning; + } } IGameObject? gameObject = gameFunctions.FindObjectByDataId(Task.DataId); From fe1d86bf5b0d6a7e722651e84ecc26f47e84f8a6 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 22 Feb 2025 23:11:28 +0100 Subject: [PATCH 16/18] Add previously missing change in config class --- Questionable/Configuration.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index 07b20f4d0..4a13462ea 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Dalamud.Configuration; using Dalamud.Game.Text; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -45,7 +46,10 @@ internal sealed class Configuration : IPluginConfiguration internal sealed class SinglePlayerDutyConfiguration { public bool RunSoloInstancesWithBossMod { get; set; } - public byte RetryDifficulty { get; set; } = 2; + + [SuppressMessage("Performance", "CA1822", Justification = "Will be fixed when no longer WIP")] + public byte RetryDifficulty => 0; + public HashSet WhitelistedSinglePlayerDutyCfcIds { get; set; } = []; public HashSet BlacklistedSinglePlayerDutyCfcIds { get; set; } = []; } From 2ada2fa9dc5461a2c76d4c317daf43fef09c4920 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 23 Feb 2025 00:50:15 +0100 Subject: [PATCH 17/18] Update for post-ARR quest battles --- Directory.Build.targets | 2 +- .../SinglePlayerDutyOptionsExtensions.cs | 3 +-- .../E3-2.3/1460_Brave New Companions.json | 23 ++++++++++++++++++- .../E6-2.55/4591_The Steps of Faith.json | 11 ++++++++- .../WAR/586_Duty and the Beast.json | 4 ++++ .../1601_Keeping the Flame Alive.json | 5 +++- .../Controller/Utils/PartyWatchDog.cs | 6 +++-- .../SinglePlayerDutyConfigComponent.cs | 2 +- 8 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index 807158990..028e926c8 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - 4.19 + 4.20 diff --git a/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs b/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs index 7a545318f..d37a85a26 100644 --- a/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs +++ b/QuestPathGenerator/RoslynElements/SinglePlayerDutyOptionsExtensions.cs @@ -21,8 +21,7 @@ internal static class SinglePlayerDutyOptionsExtensions Assignment(nameof(SinglePlayerDutyOptions.Enabled), dutyOptions.Enabled, emptyOptions.Enabled) .AsSyntaxNodeOrToken(), - Assignment(nameof(SinglePlayerDutyOptions.Notes), - dutyOptions.Notes, emptyOptions.Notes) + AssignmentList(nameof(SinglePlayerDutyOptions.Notes), dutyOptions.Notes) .AsSyntaxNodeOrToken(), Assignment(nameof(SinglePlayerDutyOptions.Index), dutyOptions.Index, emptyOptions.Index) diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1460_Brave New Companions.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1460_Brave New Companions.json index d20209778..86eb28043 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1460_Brave New Companions.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E3-2.3/1460_Brave New Companions.json @@ -100,6 +100,28 @@ 2 ] }, + { + "Position": { + "X": 86.662384, + "Y": 28.34813, + "Z": -627.5218 + }, + "TerritoryId": 156, + "InteractionType": "WalkTo", + "Fly": true, + "SkipConditions": { + "StepIf": { + "CompletionQuestVariablesFlags": [ + null, + null, + null, + null, + null, + 32 + ] + } + } + }, { "DataId": 1009143, "Position": { @@ -109,7 +131,6 @@ }, "TerritoryId": 156, "InteractionType": "Interact", - "Fly": true, "$": "1 112 0 0 0 2 -> 2 96 0 0 0 34", "CompletionQuestVariablesFlags": [ null, diff --git a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json index eb05bcad2..90686f33b 100644 --- a/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json +++ b/QuestPaths/2.x - A Realm Reborn/MSQ-2/E6-2.55/4591_The Steps of Faith.json @@ -28,7 +28,16 @@ "Z": -328.66406 }, "TerritoryId": 155, - "InteractionType": "SinglePlayerDuty" + "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": false, + "TestedBossModVersion": 292, + "Notes": [ + "WIP: Needs to be re-tested", + "AI doesn't move after starting the instance, so enemies won't be triggered", + "(First Barrier) If the player is too far south, after being stunned by Vishap's roar, AI doesn't move out of the AOE and dies to the Cauterize" + ] + } } ] }, diff --git a/QuestPaths/3.x - Heavensward/Class Quests/WAR/586_Duty and the Beast.json b/QuestPaths/3.x - Heavensward/Class Quests/WAR/586_Duty and the Beast.json index a883ac643..7112086e9 100644 --- a/QuestPaths/3.x - Heavensward/Class Quests/WAR/586_Duty and the Beast.json +++ b/QuestPaths/3.x - Heavensward/Class Quests/WAR/586_Duty and the Beast.json @@ -46,6 +46,10 @@ }, "TerritoryId": 155, "InteractionType": "SinglePlayerDuty", + "SinglePlayerDutyOptions": { + "Enabled": true, + "TestedBossModVersion": 292 + }, "Fly": true } ] diff --git a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json index 2ab1b1a9c..1fc5d8336 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A2-Raubahn/1601_Keeping the Flame Alive.json @@ -31,7 +31,10 @@ "InteractionType": "SinglePlayerDuty", "SinglePlayerDutyOptions": { "Enabled": true, - "TestedBossModVersion": 292 + "TestedBossModVersion": 292, + "Notes": [ + "Will not move into melee range to kill the gate; Alphinaud will kill it after a while" + ] } } ] diff --git a/Questionable/Controller/Utils/PartyWatchDog.cs b/Questionable/Controller/Utils/PartyWatchDog.cs index 9e7a0cc72..c44395401 100644 --- a/Questionable/Controller/Utils/PartyWatchDog.cs +++ b/Questionable/Controller/Utils/PartyWatchDog.cs @@ -81,8 +81,10 @@ internal sealed class PartyWatchDog : IDisposable return; byte memberCount = groupManager->MainGroup.MemberCount; - _logger.LogDebug("Terrritory {TerritoryId} with {MemberCount} members", _uncheckedTeritoryId, memberCount); - if (memberCount > 1) + bool isInAlliance = groupManager->MainGroup.IsAlliance; + _logger.LogDebug("Territory {TerritoryId} with {MemberCount} members, alliance: {IsInAlliance}", + _uncheckedTeritoryId, memberCount, isInAlliance); + if (memberCount > 1 || isInAlliance) StopIfRunning("Other party members present"); _uncheckedTeritoryId = null; diff --git a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs index 18f7012c4..c51de1e5e 100644 --- a/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs +++ b/Questionable/Windows/ConfigComponents/SinglePlayerDutyConfigComponent.cs @@ -271,7 +271,7 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent ImGui.BulletText("Will always use BossMod for combat (ignoring the configured combat module)."); ImGui.BulletText("Only a small subset of quest battles have been tested - most of which are in the MSQ."); ImGui.BulletText("When retrying a failed battle, it will always start at 'Normal' difficulty."); - ImGui.BulletText("Please don't enable this option when using a BossMod fork (such as Reborn);\nwith the combat changes, it is unlikely to be compatible."); + ImGui.BulletText("Please don't enable this option when using a BossMod fork (such as Reborn);\nwith the missing combat module configuration, it is unlikely to be compatible."); } #if false From 59793d19dc9a8adbf1692058f29aa5406e628575 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sun, 23 Feb 2025 01:06:55 +0100 Subject: [PATCH 18/18] Update 'At the End of our Hope' --- .../1588_At the End of Our Hope.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1588_At the End of Our Hope.json b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1588_At the End of Our Hope.json index 35fcdd9d3..d2e57f370 100644 --- a/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1588_At the End of Our Hope.json +++ b/QuestPaths/3.x - Heavensward/MSQ/A1-Coerthas Western Highlands 1, Sea of Clouds 1/1588_At the End of Our Hope.json @@ -30,7 +30,11 @@ }, "TerritoryId": 397, "InteractionType": "SinglePlayerDuty", - "Comment": "Walk straight to Gorgagne Mills basement, ignore footprints" + "Comment": "Walk straight to Gorgagne Mills basement, ignore footprints", + "SinglePlayerDutyOptions": { + "Enabled": true, + "TestedBossModVersion": 292 + } } ] },