Use ServiceHost/ILogger
authorLiza Carvelli <liza@carvel.li>
Sat, 8 Jun 2024 19:16:57 +0000 (21:16 +0200)
committerLiza Carvelli <liza@carvel.li>
Sat, 8 Jun 2024 19:23:10 +0000 (21:23 +0200)
35 files changed:
Questionable/.editorconfig
Questionable/Controller/GameUiController.cs
Questionable/Controller/MovementController.cs
Questionable/Controller/NavigationShortcutController.cs [new file with mode: 0644]
Questionable/Controller/QuestController.cs
Questionable/Controller/QuestRegistry.cs [new file with mode: 0644]
Questionable/DalamudInitializer.cs [new file with mode: 0644]
Questionable/GameFunctions.cs
Questionable/GlobalSuppressions.cs [new file with mode: 0644]
Questionable/Model/V1/AethernetShortcut.cs
Questionable/Model/V1/ChatMessage.cs
Questionable/Model/V1/Converter/AethernetShortcutConverter.cs
Questionable/Model/V1/Converter/AetheryteConverter.cs
Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs
Questionable/Model/V1/Converter/EmoteConverter.cs
Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs
Questionable/Model/V1/Converter/EnumConverter.cs
Questionable/Model/V1/Converter/InteractionTypeConverter.cs
Questionable/Model/V1/Converter/SkipConditionConverter.cs
Questionable/Model/V1/Converter/VectorConverter.cs
Questionable/Model/V1/DialogueChoice.cs
Questionable/Model/V1/EAetheryteLocation.cs
Questionable/Model/V1/EDialogChoiceType.cs
Questionable/Model/V1/EEmote.cs
Questionable/Model/V1/EEnemySpawnType.cs
Questionable/Model/V1/EInteractionType.cs
Questionable/Model/V1/ESkipCondition.cs
Questionable/Model/V1/JumpDestination.cs
Questionable/Model/V1/QuestData.cs
Questionable/Model/V1/QuestSequence.cs
Questionable/Model/V1/QuestStep.cs
Questionable/Questionable.csproj
Questionable/QuestionablePlugin.cs
Questionable/Windows/DebugWindow.cs
Questionable/packages.lock.json

index 1e0e3b2f48a87d3185ab616900e0aa08a62580dc..140b119dd538107a6a520b9ec85e25eb76f801b5 100644 (file)
@@ -290,7 +290,7 @@ dotnet_diagnostic.CA1806.severity = warning
 dotnet_diagnostic.CA1810.severity = warning
 
 # CA1812: Avoid uninstantiated internal classes
-dotnet_diagnostic.CA1812.severity = warning
+dotnet_diagnostic.CA1812.severity = suggestion
 
 # CA1813: Avoid unsealed attributes
 dotnet_diagnostic.CA1813.severity = warning
@@ -392,7 +392,7 @@ dotnet_diagnostic.CA1846.severity = warning
 dotnet_diagnostic.CA1847.severity = warning
 
 # CA1848: Use the LoggerMessage delegates
-dotnet_diagnostic.CA1848.severity = warning
+dotnet_diagnostic.CA1848.severity = suggestion
 
 # CA1849: Call async methods when in an async method
 dotnet_diagnostic.CA1849.severity = warning
index 131814db37a44a6713c189905ba5b85b1486ae4e..eb74e4f23cac663bf7805fe1dcf4600e859dbdaa 100644 (file)
@@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
 using FFXIVClientStructs.FFXIV.Component.GUI;
 using LLib.GameUI;
 using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.Logging;
 using Questionable.Model.V1;
 using Quest = Questionable.Model.Quest;
 
@@ -21,17 +22,17 @@ internal sealed class GameUiController : IDisposable
     private readonly GameFunctions _gameFunctions;
     private readonly QuestController _questController;
     private readonly IGameGui _gameGui;
-    private readonly IPluginLog _pluginLog;
+    private readonly ILogger<GameUiController> _logger;
 
     public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions,
-        QuestController questController, IGameGui gameGui, IPluginLog pluginLog)
+        QuestController questController, IGameGui gameGui, ILogger<GameUiController> logger)
     {
         _addonLifecycle = addonLifecycle;
         _dataManager = dataManager;
         _gameFunctions = gameFunctions;
         _questController = questController;
         _gameGui = gameGui;
-        _pluginLog = pluginLog;
+        _logger = logger;
 
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
         _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
@@ -45,26 +46,26 @@ internal sealed class GameUiController : IDisposable
     {
         if (_gameGui.TryGetAddonByName("SelectString", out AddonSelectString* addonSelectString))
         {
-            _pluginLog.Information("SelectString window is open");
+            _logger.LogInformation("SelectString window is open");
             SelectStringPostSetup(addonSelectString, true);
         }
 
         if (_gameGui.TryGetAddonByName("CutSceneSelectString",
                 out AddonCutSceneSelectString* addonCutSceneSelectString))
         {
-            _pluginLog.Information("CutSceneSelectString window is open");
+            _logger.LogInformation("CutSceneSelectString window is open");
             CutsceneSelectStringPostSetup(addonCutSceneSelectString, true);
         }
 
         if (_gameGui.TryGetAddonByName("SelectIconString", out AddonSelectIconString* addonSelectIconString))
         {
-            _pluginLog.Information("SelectIconString window is open");
+            _logger.LogInformation("SelectIconString window is open");
             SelectIconStringPostSetup(addonSelectIconString, true);
         }
 
         if (_gameGui.TryGetAddonByName("SelectYesno", out AddonSelectYesno* addonSelectYesno))
         {
-            _pluginLog.Information("SelectYesno window is open");
+            _logger.LogInformation("SelectYesno window is open");
             SelectYesnoPostSetup(addonSelectYesno, true);
         }
     }
@@ -151,7 +152,7 @@ internal sealed class GameUiController : IDisposable
         var currentQuest = _questController.CurrentQuest;
         if (currentQuest == null)
         {
-            _pluginLog.Information("Ignoring list choice, no active quest");
+            _logger.LogInformation("Ignoring list choice, no active quest");
             return null;
         }
 
@@ -167,7 +168,7 @@ internal sealed class GameUiController : IDisposable
             var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
             if (step == null)
             {
-                _pluginLog.Information("Ignoring list choice, no active step");
+                _logger.LogInformation("Ignoring list choice, no active step");
                 return null;
             }
 
@@ -178,7 +179,7 @@ internal sealed class GameUiController : IDisposable
         {
             if (dialogueChoice.Answer == null)
             {
-                _pluginLog.Information("Ignoring entry in DialogueChoices, no answer");
+                _logger.LogInformation("Ignoring entry in DialogueChoices, no answer");
                 continue;
             }
 
@@ -212,28 +213,31 @@ internal sealed class GameUiController : IDisposable
 
             if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
             {
-                _pluginLog.Information($"Unexpected excelPrompt: {excelPrompt}");
+                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
                 continue;
             }
 
             if (actualPrompt != null && (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)))
             {
-                _pluginLog.Information($"Unexpected excelPrompt: {excelPrompt}, actualPrompt: {actualPrompt}");
+                _logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
+                    excelPrompt, actualPrompt);
                 continue;
             }
 
             for (int i = 0; i < answers.Count; ++i)
             {
-                _pluginLog.Verbose($"Checking if {answers[i]} == {excelAnswer}");
+                _logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
+                    answers[i], excelAnswer);
                 if (GameStringEquals(answers[i], excelAnswer))
                 {
-                    _pluginLog.Information($"Returning {i}: '{answers[i]}' for '{actualPrompt}'");
+                    _logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
+                        i, answers[i], actualPrompt);
                     return i;
                 }
             }
         }
 
-        _pluginLog.Information($"No matching answer found for {actualPrompt}.");
+        _logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
         return null;
     }
 
@@ -249,7 +253,7 @@ internal sealed class GameUiController : IDisposable
         if (actualPrompt == null)
             return;
 
-        _pluginLog.Verbose($"Prompt: '{actualPrompt}'");
+        _logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
 
         var currentQuest = _questController.CurrentQuest;
         if (currentQuest == null)
@@ -277,7 +281,7 @@ internal sealed class GameUiController : IDisposable
     private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
         IList<DialogueChoice> dialogueChoices, string actualPrompt, bool checkAllSteps)
     {
-        _pluginLog.Verbose($"DefaultYesNo: Choice count: {dialogueChoices.Count}");
+        _logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
         foreach (var dialogueChoice in dialogueChoices)
         {
             string? excelPrompt;
@@ -325,21 +329,22 @@ internal sealed class GameUiController : IDisposable
         bool increaseStepCount = true;
         QuestStep? step = sequence.FindStep(currentQuest.Step);
         if (step != null)
-            _pluginLog.Verbose($"Current step: {step.TerritoryId}, {step.TargetTerritoryId}");
+            _logger.LogTrace("Current step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId,
+                step.TargetTerritoryId);
 
         if (step == null || step.TargetTerritoryId == null)
         {
-            _pluginLog.Verbose("TravelYesNo: Checking previous step...");
+            _logger.LogTrace("TravelYesNo: Checking previous step...");
             step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
             increaseStepCount = false;
 
             if (step != null)
-                _pluginLog.Verbose($"Previous step: {step.TerritoryId}, {step.TargetTerritoryId}");
+                _logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, step.TargetTerritoryId);
         }
 
         if (step == null || step.TargetTerritoryId == null)
         {
-            _pluginLog.Verbose("TravelYesNo: Not found");
+            _logger.LogTrace("TravelYesNo: Not found");
             return;
         }
 
@@ -351,11 +356,11 @@ internal sealed class GameUiController : IDisposable
             string? excelPrompt = entry.Question?.ToString();
             if (excelPrompt == null || !GameStringEquals(excelPrompt, actualPrompt))
             {
-                _pluginLog.Information($"Ignoring prompt '{excelPrompt}'");
+                _logger.LogDebug("Ignoring prompt '{Prompt}'", excelPrompt);
                 continue;
             }
 
-            _pluginLog.Information($"Using warp {entry.RowId}, {excelPrompt}");
+            _logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt);
             addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
             if (increaseStepCount)
                 _questController.IncreaseStepCount();
@@ -365,7 +370,7 @@ internal sealed class GameUiController : IDisposable
 
     private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
     {
-        _pluginLog.Information("Closing Credits sequence");
+        _logger.LogInformation("Closing Credits sequence");
         AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
         addon->FireCallbackInt(-2);
     }
@@ -374,7 +379,7 @@ internal sealed class GameUiController : IDisposable
     {
         if (_questController.CurrentQuest?.Quest.QuestId == 4526)
         {
-            _pluginLog.Information("Closing Unending Codex");
+            _logger.LogInformation("Closing Unending Codex");
             AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
             addon->FireCallbackInt(-2);
         }
index 0c58119dfe1c96931be59286b2c0627fd9de4832..bacea45cc60bd46deeda8f95654b913396689e7b 100644 (file)
@@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Objects.Types;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Client.Game;
 using FFXIVClientStructs.FFXIV.Client.Game.Control;
+using Microsoft.Extensions.Logging;
 using Questionable.External;
 using Questionable.Model;
 using Questionable.Model.V1;
@@ -26,18 +27,18 @@ internal sealed class MovementController : IDisposable
     private readonly IClientState _clientState;
     private readonly GameFunctions _gameFunctions;
     private readonly ICondition _condition;
-    private readonly IPluginLog _pluginLog;
+    private readonly ILogger<MovementController> _logger;
     private CancellationTokenSource? _cancellationTokenSource;
     private Task<List<Vector3>>? _pathfindTask;
 
     public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions,
-        ICondition condition, IPluginLog pluginLog)
+        ICondition condition, ILogger<MovementController> logger)
     {
         _navmeshIpc = navmeshIpc;
         _clientState = clientState;
         _gameFunctions = gameFunctions;
         _condition = condition;
-        _pluginLog = pluginLog;
+        _logger = logger;
     }
 
     public bool IsNavmeshReady => _navmeshIpc.IsReady;
@@ -51,9 +52,8 @@ internal sealed class MovementController : IDisposable
         {
             if (_pathfindTask.IsCompletedSuccessfully)
             {
-                _pluginLog.Information(
-                    string.Create(CultureInfo.InvariantCulture,
-                        $"Pathfinding complete, route: [{string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString()))}]"));
+                _logger.LogInformation("Pathfinding complete, route: [{Route}]",
+                    string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
 
                 var navPoints = _pathfindTask.Result.Skip(1).ToList();
                 Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
@@ -83,7 +83,7 @@ internal sealed class MovementController : IDisposable
                         if (actualDistance > 100f &&
                             ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4) == 0)
                         {
-                            _pluginLog.Information("Triggering Sprint");
+                            _logger.LogInformation("Triggering Sprint");
                             ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4);
                         }
                     }
@@ -94,7 +94,7 @@ internal sealed class MovementController : IDisposable
             }
             else if (_pathfindTask.IsCompleted)
             {
-                _pluginLog.Information("Unable to complete pathfinding task");
+                _logger.LogWarning("Unable to complete pathfinding task");
                 ResetPathfinding();
             }
         }
@@ -170,7 +170,7 @@ internal sealed class MovementController : IDisposable
     {
         fly |= _condition[ConditionFlag.Diving];
         PrepareNavigation(type, dataId, to, fly, sprint, stopDistance);
-        _pluginLog.Information($"Pathfinding to {Destination}");
+        _logger.LogInformation("Pathfinding to {Destination}", Destination);
 
         _cancellationTokenSource = new();
         _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
@@ -183,7 +183,7 @@ internal sealed class MovementController : IDisposable
         fly |= _condition[ConditionFlag.Diving];
         PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance);
 
-        _pluginLog.Information($"Moving to {Destination}");
+        _logger.LogInformation("Moving to {Destination}", Destination);
         _navmeshIpc.MoveTo(to, fly);
     }
 
diff --git a/Questionable/Controller/NavigationShortcutController.cs b/Questionable/Controller/NavigationShortcutController.cs
new file mode 100644 (file)
index 0000000..fcdd8fa
--- /dev/null
@@ -0,0 +1,38 @@
+using System.Numerics;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using Questionable.Model;
+
+namespace Questionable.Controller;
+
+internal sealed class NavigationShortcutController
+{
+    private readonly IGameGui _gameGui;
+    private readonly MovementController _movementController;
+    private readonly GameFunctions _gameFunctions;
+
+    public NavigationShortcutController(IGameGui gameGui, MovementController movementController,
+        GameFunctions gameFunctions)
+    {
+        _gameGui = gameGui;
+        _movementController = movementController;
+        _gameFunctions = gameFunctions;
+    }
+
+    public unsafe void HandleNavigationShortcut()
+    {
+        var inputData = UIInputData.Instance();
+        if (inputData == null)
+            return;
+
+        if (inputData->IsGameWindowFocused &&
+            inputData->UIFilteredMouseButtonReleasedFlags.HasFlag(MouseButtonFlags.LBUTTON) &&
+            inputData->GetKeyState(SeVirtualKey.MENU).HasFlag(KeyStateFlags.Down) &&
+            _gameGui.ScreenToWorld(new Vector2(inputData->CursorXPosition, inputData->CursorYPosition),
+                out Vector3 worldPos))
+        {
+            _movementController.NavigateTo(EMovementType.Shortcut, null, worldPos,
+                _gameFunctions.IsFlyingUnlockedInCurrentZone(), true);
+        }
+    }
+}
index 9c9778586600fe618bd7a128ba308ebe20cded1a..34f7d72272079442516bb72f554eadd4833b1773 100644 (file)
@@ -1,15 +1,11 @@
 using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
 using System.Numerics;
-using System.Text.Json;
 using Dalamud.Game.ClientState.Conditions;
 using Dalamud.Game.ClientState.Objects.Types;
-using Dalamud.Plugin;
 using Dalamud.Plugin.Services;
 using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
 using FFXIVClientStructs.FFXIV.Client.Game;
+using Microsoft.Extensions.Logging;
 using Questionable.Data;
 using Questionable.External;
 using Questionable.Model;
@@ -20,39 +16,34 @@ namespace Questionable.Controller;
 
 internal sealed class QuestController
 {
-    private readonly DalamudPluginInterface _pluginInterface;
-    private readonly IDataManager _dataManager;
     private readonly IClientState _clientState;
     private readonly GameFunctions _gameFunctions;
     private readonly MovementController _movementController;
-    private readonly IPluginLog _pluginLog;
+    private readonly ILogger<QuestController> _logger;
     private readonly ICondition _condition;
     private readonly IChatGui _chatGui;
     private readonly IFramework _framework;
     private readonly AetheryteData _aetheryteData;
     private readonly LifestreamIpc _lifestreamIpc;
     private readonly TerritoryData _territoryData;
-    private readonly Dictionary<ushort, Quest> _quests = new();
+    private readonly QuestRegistry _questRegistry;
 
-    public QuestController(DalamudPluginInterface pluginInterface, IDataManager dataManager, IClientState clientState,
-        GameFunctions gameFunctions, MovementController movementController, IPluginLog pluginLog, ICondition condition,
-        IChatGui chatGui, IFramework framework, AetheryteData aetheryteData, LifestreamIpc lifestreamIpc)
+    public QuestController(IClientState clientState, GameFunctions gameFunctions, MovementController movementController,
+        ILogger<QuestController> logger, ICondition condition, IChatGui chatGui, IFramework framework,
+        AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, TerritoryData territoryData,
+        QuestRegistry questRegistry)
     {
-        _pluginInterface = pluginInterface;
-        _dataManager = dataManager;
         _clientState = clientState;
         _gameFunctions = gameFunctions;
         _movementController = movementController;
-        _pluginLog = pluginLog;
+        _logger = logger;
         _condition = condition;
         _chatGui = chatGui;
         _framework = framework;
         _aetheryteData = aetheryteData;
         _lifestreamIpc = lifestreamIpc;
-        _territoryData = new TerritoryData(dataManager);
-
-        Reload();
-        _gameFunctions.QuestController = this;
+        _territoryData = territoryData;
+        _questRegistry = questRegistry;
     }
 
 
@@ -62,88 +53,10 @@ internal sealed class QuestController
 
     public void Reload()
     {
-        _quests.Clear();
-
         CurrentQuest = null;
         DebugState = null;
 
-#if RELEASE
-        _pluginLog.Information("Loading quests from assembly");
-        QuestPaths.AssemblyQuestLoader.LoadQuestsFromEmbeddedResources(LoadQuestFromStream);
-#else
-        DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation?.Directory?.Parent?.Parent;
-        if (solutionDirectory != null)
-        {
-            DirectoryInfo pathProjectDirectory =
-                new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "QuestPaths"));
-            if (pathProjectDirectory.Exists)
-            {
-                LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Shadowbringers")));
-                LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Endwalker")));
-            }
-        }
-#endif
-        LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
-
-        foreach (var (questId, quest) in _quests)
-        {
-            var questData =
-                _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
-            if (questData == null)
-                continue;
-
-            quest.Name = questData.Name.ToString();
-        }
-    }
-
-    private void LoadQuestFromStream(string fileName, Stream stream)
-    {
-        _pluginLog.Verbose($"Loading quest from '{fileName}'");
-        var (questId, name) = ExtractQuestDataFromName(fileName);
-        Quest quest = new Quest
-        {
-            QuestId = questId,
-            Name = name,
-            Data = JsonSerializer.Deserialize<QuestData>(stream)!,
-        };
-        _quests[questId] = quest;
-    }
-
-    public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId);
-
-    private void LoadFromDirectory(DirectoryInfo directory)
-    {
-        if (!directory.Exists)
-        {
-            _pluginLog.Information($"Not loading quests from {directory} (doesn't exist)");
-            return;
-        }
-
-        _pluginLog.Information($"Loading quests from {directory}");
-        foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
-        {
-            try
-            {
-                using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
-                LoadQuestFromStream(fileInfo.Name, stream);
-            }
-            catch (Exception e)
-            {
-                throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
-            }
-        }
-
-        foreach (DirectoryInfo childDirectory in directory.GetDirectories())
-            LoadFromDirectory(childDirectory);
-    }
-
-    private static (ushort QuestId, string Name) ExtractQuestDataFromName(string resourceName)
-    {
-        string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
-        name = name.Substring(name.LastIndexOf('.') + 1);
-
-        string[] parts = name.Split('_', 2);
-        return (ushort.Parse(parts[0], CultureInfo.InvariantCulture), parts[1]);
+        _questRegistry.Reload();
     }
 
     public void Update()
@@ -158,7 +71,7 @@ internal sealed class QuestController
         }
         else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
         {
-            if (_quests.TryGetValue(currentQuestId, out var quest))
+            if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
                 CurrentQuest = new QuestProgress(quest, currentSequence, 0);
             else if (CurrentQuest != null)
                 CurrentQuest = null;
@@ -244,7 +157,7 @@ internal sealed class QuestController
         (QuestSequence? seq, QuestStep? step) = GetNextStep();
         if (CurrentQuest == null || seq == null || step == null)
         {
-            _pluginLog.Warning("Unable to retrieve next quest step, not increasing step count");
+            _logger.LogWarning("Unable to retrieve next quest step, not increasing step count");
             return;
         }
 
@@ -271,7 +184,7 @@ internal sealed class QuestController
         (QuestSequence? seq, QuestStep? step) = GetNextStep();
         if (CurrentQuest == null || seq == null || step == null)
         {
-            _pluginLog.Warning("Unable to retrieve next quest step, not increasing dialogue choice count");
+            _logger.LogWarning("Unable to retrieve next quest step, not increasing dialogue choice count");
             return;
         }
 
@@ -292,13 +205,13 @@ internal sealed class QuestController
         (QuestSequence? seq, QuestStep? step) = GetNextStep();
         if (CurrentQuest == null || seq == null || step == null)
         {
-            _pluginLog.Warning("Could not retrieve next quest step, not doing anything");
+            _logger.LogWarning("Could not retrieve next quest step, not doing anything");
             return;
         }
 
         if (step.Disabled)
         {
-            _pluginLog.Information("Skipping step, as it is disabled");
+            _logger.LogInformation("Skipping step, as it is disabled");
             IncreaseStepCount();
             return;
         }
@@ -315,14 +228,14 @@ internal sealed class QuestController
                      (_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
                       _aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
                 {
-                    _pluginLog.Information("Skipping aetheryte teleport");
+                    _logger.LogInformation("Skipping aetheryte teleport");
                     skipTeleport = true;
                 }
             }
 
             if (skipTeleport)
             {
-                _pluginLog.Information("Marking aetheryte shortcut as used");
+                _logger.LogInformation("Marking aetheryte shortcut as used");
                 CurrentQuest = CurrentQuest with
                 {
                     StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
@@ -332,12 +245,12 @@ internal sealed class QuestController
             {
                 if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
                 {
-                    _pluginLog.Error($"Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
+                    _logger.LogError("Aetheryte {Aetheryte} is not unlocked.", step.AetheryteShortcut.Value);
                     _chatGui.Print($"[Questionable] Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
                 }
                 else if (_gameFunctions.TeleportAetheryte(step.AetheryteShortcut.Value))
                 {
-                    _pluginLog.Information("Travelling via aetheryte...");
+                    _logger.LogInformation("Travelling via aetheryte...");
                     CurrentQuest = CurrentQuest with
                     {
                         StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
@@ -345,7 +258,7 @@ internal sealed class QuestController
                 }
                 else
                 {
-                    _pluginLog.Warning("Unable to teleport to aetheryte");
+                    _logger.LogWarning("Unable to teleport to aetheryte");
                     _chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
                 }
 
@@ -355,12 +268,12 @@ internal sealed class QuestController
 
         if (!step.SkipIf.Contains(ESkipCondition.Never))
         {
-            _pluginLog.Information($"Checking skip conditions; {string.Join(",", step.SkipIf)}");
+            _logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", step.SkipIf));
 
             if (step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
                 _gameFunctions.IsFlyingUnlocked(step.TerritoryId))
             {
-                _pluginLog.Information("Skipping step, as flying is unlocked");
+                _logger.LogInformation("Skipping step, as flying is unlocked");
                 IncreaseStepCount();
                 return;
             }
@@ -368,7 +281,7 @@ internal sealed class QuestController
             if (step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
                 !_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
             {
-                _pluginLog.Information("Skipping step, as flying is locked");
+                _logger.LogInformation("Skipping step, as flying is locked");
                 IncreaseStepCount();
                 return;
             }
@@ -380,7 +293,7 @@ internal sealed class QuestController
                 } &&
                 _gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
             {
-                _pluginLog.Information("Skipping step, as aetheryte/aethernet shard is unlocked");
+                _logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
                 IncreaseStepCount();
                 return;
             }
@@ -388,7 +301,7 @@ internal sealed class QuestController
             if (step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
                 _gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
             {
-                _pluginLog.Information("Skipping step, as current is unlocked");
+                _logger.LogInformation("Skipping step, as current is unlocked");
                 IncreaseStepCount();
                 return;
             }
@@ -396,7 +309,7 @@ internal sealed class QuestController
             QuestWork? questWork = _gameFunctions.GetQuestEx(CurrentQuest.Quest.QuestId);
             if (questWork != null && step.MatchesQuestVariables(questWork.Value))
             {
-                _pluginLog.Information("Skipping step, as quest variables match");
+                _logger.LogInformation("Skipping step, as quest variables match");
                 IncreaseStepCount();
                 return;
             }
@@ -418,7 +331,7 @@ internal sealed class QuestController
                 {
                     if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) < 11)
                     {
-                        _pluginLog.Information($"Using lifestream to teleport to {to}");
+                        _logger.LogInformation("Using lifestream to teleport to {Destination}", to);
                         _lifestreamIpc.Teleport(to);
                         CurrentQuest = CurrentQuest with
                         {
@@ -427,7 +340,7 @@ internal sealed class QuestController
                     }
                     else
                     {
-                        _pluginLog.Information("Moving to aethernet shortcut");
+                        _logger.LogInformation("Moving to aethernet shortcut");
                         _movementController.NavigateTo(EMovementType.Quest, (uint)from, _aetheryteData.Locations[from],
                             false, true,
                             AetheryteConverter.IsLargeAetheryte(from) ? 10.9f : 6.9f);
@@ -437,14 +350,16 @@ internal sealed class QuestController
                 }
             }
             else
-                _pluginLog.Warning(
-                    $"Aethernet shortcut not unlocked (from: {step.AethernetShortcut.From}, to: {step.AethernetShortcut.To}), walking manually");
+                _logger.LogWarning(
+                    "Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
+                    step.AethernetShortcut.From, step.AethernetShortcut.To);
         }
 
-        if (step.TargetTerritoryId.HasValue && step.TerritoryId != step.TargetTerritoryId && step.TargetTerritoryId == _clientState.TerritoryType)
+        if (step.TargetTerritoryId.HasValue && step.TerritoryId != step.TargetTerritoryId &&
+            step.TargetTerritoryId == _clientState.TerritoryType)
         {
             // we assume whatever e.g. interaction, walkto etc. we have will trigger the zone transition
-            _pluginLog.Information("Zone transition, skipping rest of step");
+            _logger.LogInformation("Zone transition, skipping rest of step");
             IncreaseStepCount();
             return;
         }
@@ -453,7 +368,7 @@ internal sealed class QuestController
             (_clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <=
             (step.JumpDestination.StopDistance ?? 1f))
         {
-            _pluginLog.Information("We're at the jump destination, skipping movement");
+            _logger.LogInformation("We're at the jump destination, skipping movement");
         }
         else if (step.Position != null)
         {
@@ -468,7 +383,7 @@ internal sealed class QuestController
 
             if (step.Mount == true && !_gameFunctions.HasStatusPreventingSprintOrMount())
             {
-                _pluginLog.Information("Step explicitly wants a mount, trying to mount...");
+                _logger.LogInformation("Step explicitly wants a mount, trying to mount...");
                 if (!_condition[ConditionFlag.Mounted] && !_condition[ConditionFlag.InCombat] &&
                     _territoryData.CanUseMount(_clientState.TerritoryType))
                 {
@@ -478,7 +393,7 @@ internal sealed class QuestController
             }
             else if (step.Mount == false)
             {
-                _pluginLog.Information("Step explicitly wants no mount, trying to unmount...");
+                _logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
                 if (_condition[ConditionFlag.Mounted])
                 {
                     _gameFunctions.Unmount();
@@ -499,7 +414,7 @@ internal sealed class QuestController
                 if (actualDistance > distance)
                 {
                     _movementController.NavigateTo(EMovementType.Quest, step.DataId, step.Position.Value,
-                        fly: step.Fly == true && _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType),
+                        fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
                         sprint: step.Sprint != false,
                         stopDistance: distance);
                     return;
@@ -511,7 +426,7 @@ internal sealed class QuestController
                 if (actualDistance > distance)
                 {
                     _movementController.NavigateTo(EMovementType.Quest, step.DataId, [step.Position.Value],
-                        fly: step.Fly == true && _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType),
+                        fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
                         sprint: step.Sprint != false,
                         stopDistance: distance);
                     return;
@@ -524,12 +439,12 @@ internal sealed class QuestController
             if (gameObject == null ||
                 (gameObject.Position - _clientState.LocalPlayer!.Position).Length() > step.StopDistance)
             {
-                _pluginLog.Warning("Object not found or too far away, no position so we can't move");
+                _logger.LogWarning("Object not found or too far away, no position so we can't move");
                 return;
             }
         }
 
-        _pluginLog.Information($"Running logic for {step.InteractionType}");
+        _logger.LogInformation("Running logic for {InteractionType}", step.InteractionType);
         switch (step.InteractionType)
         {
             case EInteractionType.Interact:
@@ -538,7 +453,7 @@ internal sealed class QuestController
                     GameObject? gameObject = _gameFunctions.FindObjectByDataId(step.DataId.Value);
                     if (gameObject == null)
                     {
-                        _pluginLog.Warning($"No game object with dataId {step.DataId}");
+                        _logger.LogWarning("No game object with dataId {DataId}", step.DataId);
                         return;
                     }
 
@@ -555,7 +470,7 @@ internal sealed class QuestController
                         IncreaseStepCount();
                 }
                 else
-                    _pluginLog.Warning("Not interacting on current step, DataId is null");
+                    _logger.LogWarning("Not interacting on current step, DataId is null");
 
                 break;
 
@@ -584,8 +499,10 @@ internal sealed class QuestController
             case EInteractionType.AttuneAetherCurrent:
                 if (step.DataId != null)
                 {
-                    _pluginLog.Information(
-                        $"{step.AetherCurrentId} → {_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.GetValueOrDefault())}");
+                    _logger.LogInformation(
+                        "{AetherCurrentId} is unlocked = {Unlocked}",
+                        step.AetherCurrentId,
+                        _gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.GetValueOrDefault()));
                     if (step.AetherCurrentId == null ||
                         !_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.Value))
                         _gameFunctions.InteractWith(step.DataId.Value);
@@ -662,7 +579,8 @@ internal sealed class QuestController
 
                 if (step.ChatMessage != null)
                 {
-                    string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest, step.ChatMessage.ExcelSheet,
+                    string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest,
+                        step.ChatMessage.ExcelSheet,
                         step.ChatMessage.Key);
                     if (excelString == null)
                         return;
@@ -722,7 +640,7 @@ internal sealed class QuestController
                 break;
 
             default:
-                _pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
+                _logger.LogWarning("Action '{InteractionType}' is not implemented", step.InteractionType);
                 break;
         }
     }
diff --git a/Questionable/Controller/QuestRegistry.cs b/Questionable/Controller/QuestRegistry.cs
new file mode 100644 (file)
index 0000000..9dccc7d
--- /dev/null
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Text.Json;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using Microsoft.Extensions.Logging;
+using Questionable.Model;
+using Questionable.Model.V1;
+
+namespace Questionable.Controller;
+
+internal sealed class QuestRegistry
+{
+    private readonly DalamudPluginInterface _pluginInterface;
+    private readonly IDataManager _dataManager;
+    private readonly ILogger<QuestRegistry> _logger;
+
+    private readonly Dictionary<ushort, Quest> _quests = new();
+
+    public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager, ILogger<QuestRegistry> logger)
+    {
+        _pluginInterface = pluginInterface;
+        _dataManager = dataManager;
+        _logger = logger;
+    }
+
+    public void Reload()
+    {
+        _quests.Clear();
+
+#if RELEASE
+        _logger.LogInformation("Loading quests from assembly");
+        QuestPaths.AssemblyQuestLoader.LoadQuestsFromEmbeddedResources(LoadQuestFromStream);
+#else
+        DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation?.Directory?.Parent?.Parent;
+        if (solutionDirectory != null)
+        {
+            DirectoryInfo pathProjectDirectory =
+                new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "QuestPaths"));
+            if (pathProjectDirectory.Exists)
+            {
+                LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Shadowbringers")));
+                LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Endwalker")));
+            }
+        }
+#endif
+        LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
+
+        foreach (var (questId, quest) in _quests)
+        {
+            var questData =
+                _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
+            if (questData == null)
+                continue;
+
+            quest.Name = questData.Name.ToString();
+        }
+    }
+
+
+    private void LoadQuestFromStream(string fileName, Stream stream)
+    {
+        _logger.LogTrace("Loading quest from '{FileName}'", fileName);
+        var (questId, name) = ExtractQuestDataFromName(fileName);
+        Quest quest = new Quest
+        {
+            QuestId = questId,
+            Name = name,
+            Data = JsonSerializer.Deserialize<QuestData>(stream)!,
+        };
+        _quests[questId] = quest;
+    }
+
+    private void LoadFromDirectory(DirectoryInfo directory)
+    {
+        if (!directory.Exists)
+        {
+            _logger.LogInformation("Not loading quests from {DirectoryName} (doesn't exist)", directory);
+            return;
+        }
+
+        _logger.LogInformation("Loading quests from {DirectoryName}", directory);
+        foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
+        {
+            try
+            {
+                using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
+                LoadQuestFromStream(fileInfo.Name, stream);
+            }
+            catch (Exception e)
+            {
+                throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
+            }
+        }
+
+        foreach (DirectoryInfo childDirectory in directory.GetDirectories())
+            LoadFromDirectory(childDirectory);
+    }
+
+    private static (ushort QuestId, string Name) ExtractQuestDataFromName(string resourceName)
+    {
+        string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
+        name = name.Substring(name.LastIndexOf('.') + 1);
+
+        string[] parts = name.Split('_', 2);
+        return (ushort.Parse(parts[0], CultureInfo.InvariantCulture), parts[1]);
+    }
+
+    public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId);
+
+    public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest)
+        => _quests.TryGetValue(questId, out quest);
+}
diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs
new file mode 100644 (file)
index 0000000..c0f9d2c
--- /dev/null
@@ -0,0 +1,65 @@
+using System;
+using Dalamud.Game.Command;
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using Questionable.Controller;
+using Questionable.Windows;
+
+namespace Questionable;
+
+internal sealed class DalamudInitializer : IDisposable
+{
+    private readonly DalamudPluginInterface _pluginInterface;
+    private readonly IFramework _framework;
+    private readonly ICommandManager _commandManager;
+    private readonly QuestController _questController;
+    private readonly MovementController _movementController;
+    private readonly NavigationShortcutController _navigationShortcutController;
+    private readonly WindowSystem _windowSystem;
+    private readonly DebugWindow _debugWindow;
+
+    public DalamudInitializer(DalamudPluginInterface pluginInterface, IFramework framework,
+        ICommandManager commandManager, QuestController questController, MovementController movementController,
+        GameUiController gameUiController, NavigationShortcutController navigationShortcutController, WindowSystem windowSystem, DebugWindow debugWindow)
+    {
+        _pluginInterface = pluginInterface;
+        _framework = framework;
+        _commandManager = commandManager;
+        _questController = questController;
+        _movementController = movementController;
+        _navigationShortcutController = navigationShortcutController;
+        _windowSystem = windowSystem;
+        _debugWindow = debugWindow;
+
+        _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
+        _pluginInterface.UiBuilder.OpenMainUi += _debugWindow.Toggle;
+        _framework.Update += FrameworkUpdate;
+        _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)
+        {
+            HelpMessage = "Opens the Questing window"
+        });
+
+        _framework.RunOnTick(gameUiController.HandleCurrentDialogueChoices, TimeSpan.FromMilliseconds(200));
+    }
+
+    private void FrameworkUpdate(IFramework framework)
+    {
+        _questController.Update();
+        _navigationShortcutController.HandleNavigationShortcut();
+        _movementController.Update();
+    }
+
+    private void ProcessCommand(string command, string arguments)
+    {
+        _debugWindow.Toggle();
+    }
+
+    public void Dispose()
+    {
+        _commandManager.RemoveHandler("/qst");
+        _framework.Update -= FrameworkUpdate;
+        _pluginInterface.UiBuilder.OpenMainUi -= _debugWindow.Toggle;
+        _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
+    }
+}
index e07c22bfada0c0a215f8ed1a2d9d90940963d3a3..df23085407dc7d23d6c15ec70afb0ec9da42fdc4 100644 (file)
@@ -22,6 +22,7 @@ using FFXIVClientStructs.FFXIV.Client.System.String;
 using FFXIVClientStructs.FFXIV.Client.UI.Agent;
 using Lumina.Excel.CustomSheets;
 using Lumina.Excel.GeneratedSheets;
+using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Model.V1;
 using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
@@ -51,17 +52,20 @@ internal sealed unsafe class GameFunctions
     private readonly ITargetManager _targetManager;
     private readonly ICondition _condition;
     private readonly IClientState _clientState;
-    private readonly IPluginLog _pluginLog;
+    private readonly QuestRegistry _questRegistry;
+    private readonly ILogger<GameFunctions> _logger;
 
     public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
-        ITargetManager targetManager, ICondition condition, IClientState clientState, IPluginLog pluginLog)
+        ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
+        ILogger<GameFunctions> logger)
     {
         _dataManager = dataManager;
         _objectTable = objectTable;
         _targetManager = targetManager;
         _condition = condition;
         _clientState = clientState;
-        _pluginLog = pluginLog;
+        _questRegistry = questRegistry;
+        _logger = logger;
         _processChatBox =
             Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
         _sanitiseString =
@@ -85,9 +89,6 @@ internal sealed unsafe class GameFunctions
             .AsReadOnly();
     }
 
-    // FIXME
-    public QuestController QuestController { private get; set; } = null!;
-
     public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
     {
         ushort currentQuest;
@@ -108,7 +109,7 @@ internal sealed unsafe class GameFunctions
                         break;
                 }
 
-                if (QuestController.IsKnownQuest(currentQuest))
+                if (_questRegistry.IsKnownQuest(currentQuest))
                     return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
             }
         }
@@ -190,6 +191,8 @@ internal sealed unsafe class GameFunctions
                playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet);
     }
 
+    public bool IsFlyingUnlockedInCurrentZone() => IsFlyingUnlocked(_clientState.TerritoryType);
+
     public bool IsAetherCurrentUnlocked(uint aetherCurrentId)
     {
         var playerState = PlayerState.Instance();
@@ -335,7 +338,7 @@ internal sealed unsafe class GameFunctions
             }
         }
 
-        _pluginLog.Warning($"Could not find GameObject with dataId {dataId}");
+        _logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId);
         return null;
     }
 
@@ -344,7 +347,7 @@ internal sealed unsafe class GameFunctions
         GameObject? gameObject = FindObjectByDataId(dataId);
         if (gameObject != null)
         {
-            _pluginLog.Information($"Setting target with {dataId} to {gameObject.ObjectId}");
+            _logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId);
             _targetManager.Target = gameObject;
 
             TargetSystem.Instance()->InteractWithObject(
@@ -400,7 +403,7 @@ internal sealed unsafe class GameFunctions
 
     public bool HasStatusPreventingSprintOrMount()
     {
-        if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlocked(_clientState.TerritoryType))
+        if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
             return true;
 
         // company chocobo is locked
@@ -428,7 +431,7 @@ internal sealed unsafe class GameFunctions
             {
                 if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
                 {
-                    _pluginLog.Information("Using SDS Fenrir as mount");
+                    _logger.LogInformation("Using SDS Fenrir as mount");
                     ActionManager.Instance()->UseAction(ActionType.Mount, 71);
                 }
             }
@@ -436,7 +439,7 @@ internal sealed unsafe class GameFunctions
             {
                 if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
                 {
-                    _pluginLog.Information("Using mount roulette");
+                    _logger.LogInformation("Using mount roulette");
                     ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
                 }
             }
@@ -449,11 +452,11 @@ internal sealed unsafe class GameFunctions
         {
             if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
             {
-                _pluginLog.Information("Unmounting...");
+                _logger.LogInformation("Unmounting...");
                 ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
             }
             else
-                _pluginLog.Warning("Can't unmount right now?");
+                _logger.LogWarning("Can't unmount right now?");
 
             return true;
         }
@@ -468,11 +471,12 @@ internal sealed unsafe class GameFunctions
             if (UIState.IsInstanceContentUnlocked(contentId))
                 AgentContentsFinder.Instance()->OpenRegularDuty(contentFinderConditionId);
             else
-                _pluginLog.Error(
-                    $"Trying to access a locked duty (cf: {contentFinderConditionId}, content: {contentId})");
+                _logger.LogError(
+                    "Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})",
+                    contentFinderConditionId, contentId);
         }
         else
-            _pluginLog.Error($"Could not find content for content finder condition (cf: {contentFinderConditionId})");
+            _logger.LogError("Could not find content for content finder condition (cf: {ContentFinderId})", contentFinderConditionId);
     }
 
     public string? GetDialogueText(Quest currentQuest, string? excelSheetName, string key)
@@ -482,7 +486,7 @@ internal sealed unsafe class GameFunctions
             var questRow = _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestId + 0x10000);
             if (questRow == null)
             {
-                _pluginLog.Error($"Could not find quest row for {currentQuest.QuestId}");
+                _logger.LogError("Could not find quest row for {QuestId}", currentQuest.QuestId);
                 return null;
             }
 
@@ -492,7 +496,7 @@ internal sealed unsafe class GameFunctions
         var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
         if (excelSheet == null)
         {
-            _pluginLog.Error($"Unknown excel sheet '{excelSheetName}'");
+            _logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName);
             return null;
         }
 
diff --git a/Questionable/GlobalSuppressions.cs b/Questionable/GlobalSuppressions.cs
new file mode 100644 (file)
index 0000000..adfec4f
--- /dev/null
@@ -0,0 +1,10 @@
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global",
+    Justification = "Properties are used for serialization",
+    Scope = "namespaceanddescendants",
+    Target = "Questionable.Model.V1")]
+[assembly: SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global",
+    Justification = "Properties are used for serialization",
+    Scope = "namespaceanddescendants",
+    Target = "Questionable.Model.V1")]
index 40514b5873ed135f3916e1143dd89c159d02e8a9..2fd707a16bbc656e416dcc0f8fee6fcdc879b56e 100644 (file)
@@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
 namespace Questionable.Model.V1;
 
 [JsonConverter(typeof(AethernetShortcutConverter))]
-public sealed class AethernetShortcut
+internal sealed class AethernetShortcut
 {
     public EAetheryteLocation From { get; set; }
     public EAetheryteLocation To { get; set; }
index 71721380eb453db32139aae1e2e069b710741c92..c33bf43ada19bd60c53a0936ff5664ddf001c9eb 100644 (file)
@@ -1,6 +1,9 @@
-namespace Questionable.Model.V1;
+using JetBrains.Annotations;
 
-public sealed class ChatMessage
+namespace Questionable.Model.V1;
+
+[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
+internal sealed class ChatMessage
 {
     public string? ExcelSheet { get; set; }
     public string Key { get; set; } = null!;
index 0857c758b6753309f55749a374894d003a1eac9b..879c3ef8adb4b01093277e3a26950c2032d10355 100644 (file)
@@ -6,7 +6,7 @@ using System.Text.Json.Serialization;
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
+internal sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
 {
     private static readonly Dictionary<EAetheryteLocation, string> EnumToString = new()
     {
index 80b92f7d32d5e2be8e24878ea99cae0d467a4b4f..6d7224588a88443e39e840f91cfb796fa2f71be6 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class AetheryteConverter() : EnumConverter<EAetheryteLocation>(Values)
+internal sealed class AetheryteConverter() : EnumConverter<EAetheryteLocation>(Values)
 {
     private static readonly Dictionary<EAetheryteLocation, string> Values = new()
     {
index 5e35917f4e58868ec3a012d14c75fc95c3308ed7..abb41f69a365669ba671021a90fd967602fe5f60 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
+internal sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
 {
     private static readonly Dictionary<EDialogChoiceType, string> Values = new()
     {
index 7014185cf329f8c9a9c342478dd2c93ae84bc33f..44be7e46638307c7baa1120eb8d0b470874666bc 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
+internal sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
 {
     private static readonly Dictionary<EEmote, string> Values = new()
     {
index 5c5de532e050c24f264390d42f12e6dfd532c9b7..a0ffe8434697786bdcb430a07b893e2d7b7495a2 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class EnemySpawnTypeConverter() : EnumConverter<EEnemySpawnType>(Values)
+internal sealed class EnemySpawnTypeConverter() : EnumConverter<EEnemySpawnType>(Values)
 {
     private static readonly Dictionary<EEnemySpawnType, string> Values = new()
     {
index cf70131be08aa75318b9109f4a1b12a54cf8d0ed..afd37fdecf95e102d086fb460e97cad163da3d6a 100644 (file)
@@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
 
 namespace Questionable.Model.V1.Converter;
 
-public abstract class EnumConverter<T> : JsonConverter<T>
+internal abstract class EnumConverter<T> : JsonConverter<T>
     where T : Enum
 {
     private readonly ReadOnlyDictionary<T, string> _enumToString;
index fcd2228b2b9298e3f7e48e233fcf583eab64adb0..aadd504aa233dbbbd7486c69886ac73f88c3d02c 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>(Values)
+internal sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>(Values)
 {
     private static readonly Dictionary<EInteractionType, string> Values = new()
     {
index dd38ac4f1c0cbddcee774cf02bce377bec2700b9..a65d5044dcaedbadcb669a626a205242893e3baa 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Questionable.Model.V1.Converter;
 
-public sealed class SkipConditionConverter() : EnumConverter<ESkipCondition>(Values)
+internal sealed class SkipConditionConverter() : EnumConverter<ESkipCondition>(Values)
 {
     private static readonly Dictionary<ESkipCondition, string> Values = new()
     {
index e7731e023554211bf535c15483b6e9a5f669998c..575fd885c42b5912a03ae1d8ea681c91a4e77745 100644 (file)
@@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
 
 namespace Questionable.Model.V1.Converter;
 
-public class VectorConverter : JsonConverter<Vector3>
+internal sealed class VectorConverter : JsonConverter<Vector3>
 {
     public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
     {
index 740ad030f196779bce7356e65d7d834ebb670de2..d826f53d52745824434c3ea2bc4a51ecb2fed29e 100644 (file)
@@ -1,14 +1,16 @@
 using System.Text.Json.Serialization;
+using JetBrains.Annotations;
 using Questionable.Model.V1.Converter;
 
 namespace Questionable.Model.V1;
 
-public class DialogueChoice
+[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
+internal sealed class DialogueChoice
 {
     [JsonConverter(typeof(DialogueChoiceTypeConverter))]
     public EDialogChoiceType Type { get; set; }
     public string? ExcelSheet { get; set; }
-    public string? Prompt { get; set; } = null!;
+    public string? Prompt { get; set; }
     public bool Yes { get; set; } = true;
     public string? Answer { get; set; }
 }
index a49a471e97c29e2c6cc0179d5fb6ce4eb4390b13..c6ec79f9254de841e484825f9720f960b3931252 100644 (file)
@@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
 namespace Questionable.Model.V1;
 
 [JsonConverter(typeof(AetheryteConverter))]
-public enum EAetheryteLocation
+internal enum EAetheryteLocation
 {
     None = 0,
 
index f07fa21fd7d78666f47335d847861efff3f9757b..d17a5b7adfc31e138907250fc1f4a202625b9647 100644 (file)
@@ -1,6 +1,6 @@
 namespace Questionable.Model.V1;
 
-public enum EDialogChoiceType
+internal enum EDialogChoiceType
 {
     None,
     YesNo,
index dcdc11b1b4c8921ff2061f46762086aca4286840..238e3ca60503968e2260f878c7b8706fa142fe29 100644 (file)
@@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
 namespace Questionable.Model.V1;
 
 [JsonConverter(typeof(EmoteConverter))]
-public enum EEmote
+internal enum EEmote
 {
     None = 0,
 
index 8465f0119b60de15c90b910d2fc1b78fce767bb4..1c19132df26a1bc7168a5d60b89e1084f696ab4c 100644 (file)
@@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
 namespace Questionable.Model.V1;
 
 [JsonConverter(typeof(EnemySpawnTypeConverter))]
-public enum EEnemySpawnType
+internal enum EEnemySpawnType
 {
     None = 0,
     AfterInteraction,
index f0ca8a32584711474eb550f5631852ac6e92b8ef..20d0c53164ae9052be82014d92013b50dd38ff5b 100644 (file)
@@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
 namespace Questionable.Model.V1;
 
 [JsonConverter(typeof(InteractionTypeConverter))]
-public enum EInteractionType
+internal enum EInteractionType
 {
     Interact,
     WalkTo,
index 4f2639d42196a164e0547a7db179d9bc6903ec8d..f7e173c99d5c2543306dd6c2c25dfe0528473a15 100644 (file)
@@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
 namespace Questionable.Model.V1;
 
 [JsonConverter(typeof(SkipConditionConverter))]
-public enum ESkipCondition
+internal enum ESkipCondition
 {
     None,
     Never,
index e6a709e7f52d0a535ff61a78dbc78dc1cf29b4ed..1827f8793292b09921ab4b1142481ce008f6d77d 100644 (file)
@@ -1,10 +1,12 @@
 using System.Numerics;
 using System.Text.Json.Serialization;
+using JetBrains.Annotations;
 using Questionable.Model.V1.Converter;
 
 namespace Questionable.Model.V1;
 
-public sealed class JumpDestination
+[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
+internal sealed class JumpDestination
 {
     [JsonConverter(typeof(VectorConverter))]
     public Vector3 Position { get; set; }
index 3a1af26089f196ebdb36078a6ffce3f756e38d83..d484481b94663015c728152ef3daee16b9347664 100644 (file)
@@ -1,8 +1,10 @@
 using System.Collections.Generic;
+using JetBrains.Annotations;
 
 namespace Questionable.Model.V1;
 
-public class QuestData
+[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
+internal sealed class QuestData
 {
     public required string Author { get; set; }
     public List<string> Contributors { get; set; } = new();
index 4ea058ed39cf07ff04b6f066ecc958786f0f6119..58393c32d9b3df3ddaf89431e4a6ac1ed795b9d8 100644 (file)
@@ -1,8 +1,10 @@
 using System.Collections.Generic;
+using JetBrains.Annotations;
 
 namespace Questionable.Model.V1;
 
-public class QuestSequence
+[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
+internal sealed class QuestSequence
 {
     public required int Sequence { get; set; }
     public string? Comment { get; set; }
index d16cccd5851f9561bb402bdb07bba5f8cd1ea74e..31013f2cee68a362cad9d8f4d4e9dd5612075623 100644 (file)
@@ -1,12 +1,15 @@
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Numerics;
 using System.Text.Json.Serialization;
 using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
+using JetBrains.Annotations;
 using Questionable.Model.V1.Converter;
 
 namespace Questionable.Model.V1;
 
-public class QuestStep
+[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
+internal sealed class QuestStep
 {
     public EInteractionType InteractionType { get; set; }
 
index e6bf8f84640c938f46ef07e3369768e5177fd5f8..48a10da93ac27cda52d8ad6aa140c40fb1a311e5 100644 (file)
     </PropertyGroup>
 
     <ItemGroup>
+        <PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="3.0.0" />
         <PackageReference Include="DalamudPackager" Version="2.1.12"/>
+        <PackageReference Include="JetBrains.Annotations" Version="2023.3.0" ExcludeAssets="runtime"/>
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
         <PackageReference Include="System.Text.Json" Version="8.0.3"/>
     </ItemGroup>
 
index 8ed248e5598fcd326e9ed3f9e684d8569726df28..1a6ea83d7b80de9580af0b2a6ba59e6f030aa2be 100644 (file)
 using System;
-using System.Linq;
-using System.Numerics;
+using System.Diagnostics.CodeAnalysis;
+using Dalamud.Extensions.MicrosoftLogging;
 using Dalamud.Game;
 using Dalamud.Game.ClientState.Objects;
-using Dalamud.Game.Command;
 using Dalamud.Interface.Windowing;
 using Dalamud.Plugin;
 using Dalamud.Plugin.Services;
-using FFXIVClientStructs.FFXIV.Client.UI;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
 using Questionable.Controller;
 using Questionable.Data;
 using Questionable.External;
-using Questionable.Model;
 using Questionable.Windows;
 
 namespace Questionable;
 
+[SuppressMessage("ReSharper", "UnusedType.Global")]
 public sealed class QuestionablePlugin : IDalamudPlugin
 {
-    private readonly WindowSystem _windowSystem = new(nameof(Questionable));
-
-    private readonly DalamudPluginInterface _pluginInterface;
-    private readonly IClientState _clientState;
-    private readonly IFramework _framework;
-    private readonly IGameGui _gameGui;
-    private readonly ICommandManager _commandManager;
-    private readonly GameFunctions _gameFunctions;
-    private readonly QuestController _questController;
-    private readonly MovementController _movementController;
-    private readonly GameUiController _gameUiController;
-    private readonly Configuration _configuration;
-
-    public QuestionablePlugin(DalamudPluginInterface pluginInterface, IClientState clientState,
-        ITargetManager targetManager, IFramework framework, IGameGui gameGui, IDataManager dataManager,
-        ISigScanner sigScanner, IObjectTable objectTable, IPluginLog pluginLog, ICondition condition, IChatGui chatGui,
-        ICommandManager commandManager, IAddonLifecycle addonLifecycle)
+    private readonly ServiceProvider? _serviceProvider;
+
+    public QuestionablePlugin(DalamudPluginInterface pluginInterface,
+        IClientState clientState,
+        ITargetManager targetManager,
+        IFramework framework,
+        IGameGui gameGui,
+        IDataManager dataManager,
+        ISigScanner sigScanner,
+        IObjectTable objectTable,
+        IPluginLog pluginLog,
+        ICondition condition,
+        IChatGui chatGui,
+        ICommandManager commandManager,
+        IAddonLifecycle addonLifecycle)
     {
         ArgumentNullException.ThrowIfNull(pluginInterface);
-        ArgumentNullException.ThrowIfNull(sigScanner);
-        ArgumentNullException.ThrowIfNull(dataManager);
-        ArgumentNullException.ThrowIfNull(objectTable);
-
-        _pluginInterface = pluginInterface;
-        _clientState = clientState;
-        _framework = framework;
-        _gameGui = gameGui;
-        _commandManager = commandManager;
-        _gameFunctions = new GameFunctions(dataManager, objectTable, sigScanner, targetManager, condition, clientState,
-            pluginLog);
-        _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
-
-        AetheryteData aetheryteData = new AetheryteData(dataManager);
-        NavmeshIpc navmeshIpc = new NavmeshIpc(pluginInterface);
-        LifestreamIpc lifestreamIpc = new LifestreamIpc(pluginInterface, aetheryteData);
-        _movementController =
-            new MovementController(navmeshIpc, clientState, _gameFunctions, condition, pluginLog);
-        _questController = new QuestController(pluginInterface, dataManager, _clientState, _gameFunctions,
-            _movementController, pluginLog, condition, chatGui, framework, aetheryteData, lifestreamIpc);
-        _gameUiController =
-            new GameUiController(addonLifecycle, dataManager, _gameFunctions, _questController, gameGui, pluginLog);
-
-        _windowSystem.AddWindow(new DebugWindow(pluginInterface, _movementController, _questController, _gameFunctions,
-            clientState, framework, targetManager, _gameUiController, _configuration));
-
-        _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
-        _framework.Update += FrameworkUpdate;
-        _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand));
-
-        _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), TimeSpan.FromMilliseconds(200));
-    }
-
-    private void FrameworkUpdate(IFramework framework)
-    {
-        _questController.Update();
 
-        HandleNavigationShortcut();
-        _movementController.Update();
+        ServiceCollection serviceCollection = new();
+        serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
+            .ClearProviders()
+            .AddDalamudLogger(pluginLog));
+        serviceCollection.AddSingleton<IDalamudPlugin>(this);
+        serviceCollection.AddSingleton(pluginInterface);
+        serviceCollection.AddSingleton(clientState);
+        serviceCollection.AddSingleton(targetManager);
+        serviceCollection.AddSingleton(framework);
+        serviceCollection.AddSingleton(gameGui);
+        serviceCollection.AddSingleton(dataManager);
+        serviceCollection.AddSingleton(sigScanner);
+        serviceCollection.AddSingleton(objectTable);
+        serviceCollection.AddSingleton(condition);
+        serviceCollection.AddSingleton(chatGui);
+        serviceCollection.AddSingleton(commandManager);
+        serviceCollection.AddSingleton(addonLifecycle);
+        serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
+        serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
+
+        serviceCollection.AddSingleton<GameFunctions>();
+        serviceCollection.AddSingleton<AetheryteData>();
+        serviceCollection.AddSingleton<TerritoryData>();
+        serviceCollection.AddSingleton<NavmeshIpc>();
+        serviceCollection.AddSingleton<LifestreamIpc>();
+
+        serviceCollection.AddSingleton<MovementController>();
+        serviceCollection.AddSingleton<QuestRegistry>();
+        serviceCollection.AddSingleton<QuestController>();
+        serviceCollection.AddSingleton<GameUiController>();
+        serviceCollection.AddSingleton<NavigationShortcutController>();
+
+        serviceCollection.AddSingleton<DebugWindow>();
+        serviceCollection.AddSingleton<DalamudInitializer>();
+
+        _serviceProvider = serviceCollection.BuildServiceProvider();
+        _serviceProvider.GetRequiredService<QuestRegistry>().Reload();
+        _serviceProvider.GetRequiredService<DebugWindow>();
+        _serviceProvider.GetRequiredService<DalamudInitializer>();
     }
 
-    private void ProcessCommand(string command, string arguments)
-    {
-        _windowSystem.Windows.Single(x => x is DebugWindow).Toggle();
-    }
-
-    private unsafe void HandleNavigationShortcut()
-    {
-        var inputData = UIInputData.Instance();
-        if (inputData == null)
-            return;
-
-        if (inputData->IsGameWindowFocused &&
-            inputData->UIFilteredMouseButtonReleasedFlags.HasFlag(MouseButtonFlags.LBUTTON) &&
-            inputData->GetKeyState(SeVirtualKey.MENU).HasFlag(KeyStateFlags.Down) &&
-            _gameGui.ScreenToWorld(new Vector2(inputData->CursorXPosition, inputData->CursorYPosition),
-                out Vector3 worldPos))
-        {
-            _movementController.NavigateTo(EMovementType.Shortcut, null, worldPos,
-                _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType), true);
-        }
-    }
-
-
     public void Dispose()
     {
-        _commandManager.RemoveHandler("/qst");
-        _framework.Update -= FrameworkUpdate;
-        _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
-
-        _gameUiController.Dispose();
-        _movementController.Dispose();
+        _serviceProvider?.Dispose();
     }
 }
index d1e83b08cb73b8867c6ee0e490f78f777dad70fa..79904f5716ac9007607305d8af768cc495ef3be4 100644 (file)
@@ -18,9 +18,10 @@ using Questionable.Model.V1;
 
 namespace Questionable.Windows;
 
-internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
+internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposable
 {
     private readonly DalamudPluginInterface _pluginInterface;
+    private readonly WindowSystem _windowSystem;
     private readonly MovementController _movementController;
     private readonly QuestController _questController;
     private readonly GameFunctions _gameFunctions;
@@ -30,12 +31,14 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
     private readonly GameUiController _gameUiController;
     private readonly Configuration _configuration;
 
-    public DebugWindow(DalamudPluginInterface pluginInterface, MovementController movementController,
-        QuestController questController, GameFunctions gameFunctions, IClientState clientState, IFramework framework,
-        ITargetManager targetManager, GameUiController gameUiController, Configuration configuration)
+    public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
+        MovementController movementController, QuestController questController, GameFunctions gameFunctions,
+        IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
+        Configuration configuration)
         : base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
     {
         _pluginInterface = pluginInterface;
+        _windowSystem = windowSystem;
         _movementController = movementController;
         _questController = questController;
         _gameFunctions = gameFunctions;
@@ -51,6 +54,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
             MinimumSize = new Vector2(200, 30),
             MaximumSize = default
         };
+
+        _windowSystem.AddWindow(this);
     }
 
     public WindowConfig WindowConfig => _configuration.DebugWindowConfig;
@@ -128,7 +133,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
         ImGui.Separator();
 
         ImGui.Text(
-            $"Current TerritoryId: {_clientState.TerritoryType}, Flying: {(_gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType) ? "Yes" : "No")}");
+            $"Current TerritoryId: {_clientState.TerritoryType}, Flying: {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "Yes" : "No")}");
 
         var q = _gameFunctions.GetCurrentQuest();
         ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}");
@@ -169,7 +174,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
                 if (ImGui.Button("Move to Target"))
                 {
                     _movementController.NavigateTo(EMovementType.DebugWindow, _targetManager.Target.DataId,
-                        _targetManager.Target.Position, _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType),
+                        _targetManager.Target.Position, _gameFunctions.IsFlyingUnlockedInCurrentZone(),
                         true);
                 }
             }
@@ -234,7 +239,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
                             map->FlagMapMarker.TerritoryId != _clientState.TerritoryType);
         if (ImGui.Button("Move to Flag"))
             _gameFunctions.ExecuteCommand(
-                $"/vnav {(_gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType) ? "flyflag" : "moveflag")}");
+                $"/vnav {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}");
         ImGui.EndDisabled();
 
         ImGui.SameLine();
@@ -251,4 +256,9 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
                 TimeSpan.FromMilliseconds(200));
         }
     }
+
+    public void Dispose()
+    {
+        _windowSystem.RemoveWindow(this);
+    }
 }
index 1ff84635321920aa691aa1799e130f809a7768a6..7c5ad37040b6575e84471066982ec8511b15aa23 100644 (file)
@@ -2,12 +2,36 @@
   "version": 1,
   "dependencies": {
     "net8.0-windows7.0": {
+      "Dalamud.Extensions.MicrosoftLogging": {
+        "type": "Direct",
+        "requested": "[3.0.0, )",
+        "resolved": "3.0.0",
+        "contentHash": "jWK3r/cZUXN8H9vHf78gEzeRmMk4YAbCUYzLcTqUAcega8unUiFGwYy+iOjVYJ9urnr9r+hk+vBi1y9wyv+e7Q==",
+        "dependencies": {
+          "Microsoft.Extensions.Logging": "8.0.0"
+        }
+      },
       "DalamudPackager": {
         "type": "Direct",
         "requested": "[2.1.12, )",
         "resolved": "2.1.12",
         "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
       },
+      "JetBrains.Annotations": {
+        "type": "Direct",
+        "requested": "[2023.3.0, )",
+        "resolved": "2023.3.0",
+        "contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA=="
+      },
+      "Microsoft.Extensions.DependencyInjection": {
+        "type": "Direct",
+        "requested": "[8.0.0, )",
+        "resolved": "8.0.0",
+        "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+        }
+      },
       "System.Text.Json": {
         "type": "Direct",
         "requested": "[8.0.3, )",
           "System.Text.Encodings.Web": "8.0.0"
         }
       },
+      "Microsoft.Extensions.DependencyInjection.Abstractions": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
+      },
+      "Microsoft.Extensions.Logging": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection": "8.0.0",
+          "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
+          "Microsoft.Extensions.Options": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.Logging.Abstractions": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.Options": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+          "Microsoft.Extensions.Primitives": "8.0.0"
+        }
+      },
+      "Microsoft.Extensions.Primitives": {
+        "type": "Transitive",
+        "resolved": "8.0.0",
+        "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+      },
       "System.Text.Encodings.Web": {
         "type": "Transitive",
         "resolved": "8.0.0",