diff --git a/Questionable/.editorconfig b/Questionable/.editorconfig index 1e0e3b2f..140b119d 100644 --- a/Questionable/.editorconfig +++ b/Questionable/.editorconfig @@ -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 diff --git a/Questionable/Controller/GameUiController.cs b/Questionable/Controller/GameUiController.cs index 131814db..eb74e4f2 100644 --- a/Questionable/Controller/GameUiController.cs +++ b/Questionable/Controller/GameUiController.cs @@ -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 _logger; public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions, - QuestController questController, IGameGui gameGui, IPluginLog pluginLog) + QuestController questController, IGameGui gameGui, ILogger 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 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); } diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index 0c58119d..bacea45c 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -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 _logger; private CancellationTokenSource? _cancellationTokenSource; private Task>? _pathfindTask; public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions, - ICondition condition, IPluginLog pluginLog) + ICondition condition, ILogger 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 index 00000000..fcdd8fa4 --- /dev/null +++ b/Questionable/Controller/NavigationShortcutController.cs @@ -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); + } + } +} diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 9c977858..34f7d722 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -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 _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 _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 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()!.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(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 index 00000000..9dccc7d4 --- /dev/null +++ b/Questionable/Controller/QuestRegistry.cs @@ -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 _logger; + + private readonly Dictionary _quests = new(); + + public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager, ILogger 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()!.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(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 index 00000000..c0f9d2cc --- /dev/null +++ b/Questionable/DalamudInitializer.cs @@ -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; + } +} diff --git a/Questionable/GameFunctions.cs b/Questionable/GameFunctions.cs index e07c22bf..df230854 100644 --- a/Questionable/GameFunctions.cs +++ b/Questionable/GameFunctions.cs @@ -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 _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 logger) { _dataManager = dataManager; _objectTable = objectTable; _targetManager = targetManager; _condition = condition; _clientState = clientState; - _pluginLog = pluginLog; + _questRegistry = questRegistry; + _logger = logger; _processChatBox = Marshal.GetDelegateForFunctionPointer(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()!.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(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 index 00000000..adfec4f8 --- /dev/null +++ b/Questionable/GlobalSuppressions.cs @@ -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")] diff --git a/Questionable/Model/V1/AethernetShortcut.cs b/Questionable/Model/V1/AethernetShortcut.cs index 40514b58..2fd707a1 100644 --- a/Questionable/Model/V1/AethernetShortcut.cs +++ b/Questionable/Model/V1/AethernetShortcut.cs @@ -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; } diff --git a/Questionable/Model/V1/ChatMessage.cs b/Questionable/Model/V1/ChatMessage.cs index 71721380..c33bf43a 100644 --- a/Questionable/Model/V1/ChatMessage.cs +++ b/Questionable/Model/V1/ChatMessage.cs @@ -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!; diff --git a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs index 0857c758..879c3ef8 100644 --- a/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs +++ b/Questionable/Model/V1/Converter/AethernetShortcutConverter.cs @@ -6,7 +6,7 @@ using System.Text.Json.Serialization; namespace Questionable.Model.V1.Converter; -public sealed class AethernetShortcutConverter : JsonConverter +internal sealed class AethernetShortcutConverter : JsonConverter { private static readonly Dictionary EnumToString = new() { diff --git a/Questionable/Model/V1/Converter/AetheryteConverter.cs b/Questionable/Model/V1/Converter/AetheryteConverter.cs index 80b92f7d..6d722458 100644 --- a/Questionable/Model/V1/Converter/AetheryteConverter.cs +++ b/Questionable/Model/V1/Converter/AetheryteConverter.cs @@ -2,7 +2,7 @@ namespace Questionable.Model.V1.Converter; -public sealed class AetheryteConverter() : EnumConverter(Values) +internal sealed class AetheryteConverter() : EnumConverter(Values) { private static readonly Dictionary Values = new() { diff --git a/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs b/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs index 5e35917f..abb41f69 100644 --- a/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs +++ b/Questionable/Model/V1/Converter/DialogueChoiceTypeConverter.cs @@ -2,7 +2,7 @@ namespace Questionable.Model.V1.Converter; -public sealed class DialogueChoiceTypeConverter() : EnumConverter(Values) +internal sealed class DialogueChoiceTypeConverter() : EnumConverter(Values) { private static readonly Dictionary Values = new() { diff --git a/Questionable/Model/V1/Converter/EmoteConverter.cs b/Questionable/Model/V1/Converter/EmoteConverter.cs index 7014185c..44be7e46 100644 --- a/Questionable/Model/V1/Converter/EmoteConverter.cs +++ b/Questionable/Model/V1/Converter/EmoteConverter.cs @@ -2,7 +2,7 @@ namespace Questionable.Model.V1.Converter; -public sealed class EmoteConverter() : EnumConverter(Values) +internal sealed class EmoteConverter() : EnumConverter(Values) { private static readonly Dictionary Values = new() { diff --git a/Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs b/Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs index 5c5de532..a0ffe843 100644 --- a/Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs +++ b/Questionable/Model/V1/Converter/EnemySpawnTypeConverter.cs @@ -2,7 +2,7 @@ namespace Questionable.Model.V1.Converter; -public sealed class EnemySpawnTypeConverter() : EnumConverter(Values) +internal sealed class EnemySpawnTypeConverter() : EnumConverter(Values) { private static readonly Dictionary Values = new() { diff --git a/Questionable/Model/V1/Converter/EnumConverter.cs b/Questionable/Model/V1/Converter/EnumConverter.cs index cf70131b..afd37fde 100644 --- a/Questionable/Model/V1/Converter/EnumConverter.cs +++ b/Questionable/Model/V1/Converter/EnumConverter.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; namespace Questionable.Model.V1.Converter; -public abstract class EnumConverter : JsonConverter +internal abstract class EnumConverter : JsonConverter where T : Enum { private readonly ReadOnlyDictionary _enumToString; diff --git a/Questionable/Model/V1/Converter/InteractionTypeConverter.cs b/Questionable/Model/V1/Converter/InteractionTypeConverter.cs index fcd2228b..aadd504a 100644 --- a/Questionable/Model/V1/Converter/InteractionTypeConverter.cs +++ b/Questionable/Model/V1/Converter/InteractionTypeConverter.cs @@ -2,7 +2,7 @@ namespace Questionable.Model.V1.Converter; -public sealed class InteractionTypeConverter() : EnumConverter(Values) +internal sealed class InteractionTypeConverter() : EnumConverter(Values) { private static readonly Dictionary Values = new() { diff --git a/Questionable/Model/V1/Converter/SkipConditionConverter.cs b/Questionable/Model/V1/Converter/SkipConditionConverter.cs index dd38ac4f..a65d5044 100644 --- a/Questionable/Model/V1/Converter/SkipConditionConverter.cs +++ b/Questionable/Model/V1/Converter/SkipConditionConverter.cs @@ -2,7 +2,7 @@ namespace Questionable.Model.V1.Converter; -public sealed class SkipConditionConverter() : EnumConverter(Values) +internal sealed class SkipConditionConverter() : EnumConverter(Values) { private static readonly Dictionary Values = new() { diff --git a/Questionable/Model/V1/Converter/VectorConverter.cs b/Questionable/Model/V1/Converter/VectorConverter.cs index e7731e02..575fd885 100644 --- a/Questionable/Model/V1/Converter/VectorConverter.cs +++ b/Questionable/Model/V1/Converter/VectorConverter.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; namespace Questionable.Model.V1.Converter; -public class VectorConverter : JsonConverter +internal sealed class VectorConverter : JsonConverter { public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/Questionable/Model/V1/DialogueChoice.cs b/Questionable/Model/V1/DialogueChoice.cs index 740ad030..d826f53d 100644 --- a/Questionable/Model/V1/DialogueChoice.cs +++ b/Questionable/Model/V1/DialogueChoice.cs @@ -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; } } diff --git a/Questionable/Model/V1/EAetheryteLocation.cs b/Questionable/Model/V1/EAetheryteLocation.cs index a49a471e..c6ec79f9 100644 --- a/Questionable/Model/V1/EAetheryteLocation.cs +++ b/Questionable/Model/V1/EAetheryteLocation.cs @@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter; namespace Questionable.Model.V1; [JsonConverter(typeof(AetheryteConverter))] -public enum EAetheryteLocation +internal enum EAetheryteLocation { None = 0, diff --git a/Questionable/Model/V1/EDialogChoiceType.cs b/Questionable/Model/V1/EDialogChoiceType.cs index f07fa21f..d17a5b7a 100644 --- a/Questionable/Model/V1/EDialogChoiceType.cs +++ b/Questionable/Model/V1/EDialogChoiceType.cs @@ -1,6 +1,6 @@ namespace Questionable.Model.V1; -public enum EDialogChoiceType +internal enum EDialogChoiceType { None, YesNo, diff --git a/Questionable/Model/V1/EEmote.cs b/Questionable/Model/V1/EEmote.cs index dcdc11b1..238e3ca6 100644 --- a/Questionable/Model/V1/EEmote.cs +++ b/Questionable/Model/V1/EEmote.cs @@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter; namespace Questionable.Model.V1; [JsonConverter(typeof(EmoteConverter))] -public enum EEmote +internal enum EEmote { None = 0, diff --git a/Questionable/Model/V1/EEnemySpawnType.cs b/Questionable/Model/V1/EEnemySpawnType.cs index 8465f011..1c19132d 100644 --- a/Questionable/Model/V1/EEnemySpawnType.cs +++ b/Questionable/Model/V1/EEnemySpawnType.cs @@ -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, diff --git a/Questionable/Model/V1/EInteractionType.cs b/Questionable/Model/V1/EInteractionType.cs index f0ca8a32..20d0c531 100644 --- a/Questionable/Model/V1/EInteractionType.cs +++ b/Questionable/Model/V1/EInteractionType.cs @@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter; namespace Questionable.Model.V1; [JsonConverter(typeof(InteractionTypeConverter))] -public enum EInteractionType +internal enum EInteractionType { Interact, WalkTo, diff --git a/Questionable/Model/V1/ESkipCondition.cs b/Questionable/Model/V1/ESkipCondition.cs index 4f2639d4..f7e173c9 100644 --- a/Questionable/Model/V1/ESkipCondition.cs +++ b/Questionable/Model/V1/ESkipCondition.cs @@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter; namespace Questionable.Model.V1; [JsonConverter(typeof(SkipConditionConverter))] -public enum ESkipCondition +internal enum ESkipCondition { None, Never, diff --git a/Questionable/Model/V1/JumpDestination.cs b/Questionable/Model/V1/JumpDestination.cs index e6a709e7..1827f879 100644 --- a/Questionable/Model/V1/JumpDestination.cs +++ b/Questionable/Model/V1/JumpDestination.cs @@ -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; } diff --git a/Questionable/Model/V1/QuestData.cs b/Questionable/Model/V1/QuestData.cs index 3a1af260..d484481b 100644 --- a/Questionable/Model/V1/QuestData.cs +++ b/Questionable/Model/V1/QuestData.cs @@ -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 Contributors { get; set; } = new(); diff --git a/Questionable/Model/V1/QuestSequence.cs b/Questionable/Model/V1/QuestSequence.cs index 4ea058ed..58393c32 100644 --- a/Questionable/Model/V1/QuestSequence.cs +++ b/Questionable/Model/V1/QuestSequence.cs @@ -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; } diff --git a/Questionable/Model/V1/QuestStep.cs b/Questionable/Model/V1/QuestStep.cs index d16cccd5..31013f2c 100644 --- a/Questionable/Model/V1/QuestStep.cs +++ b/Questionable/Model/V1/QuestStep.cs @@ -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; } diff --git a/Questionable/Questionable.csproj b/Questionable/Questionable.csproj index e6bf8f84..48a10da9 100644 --- a/Questionable/Questionable.csproj +++ b/Questionable/Questionable.csproj @@ -23,7 +23,10 @@ + + + diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 8ed248e5..1a6ea83d 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -1,113 +1,84 @@ 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 ServiceProvider? _serviceProvider; - 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) + 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(); + ServiceCollection serviceCollection = new(); + serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace) + .ClearProviders() + .AddDalamudLogger(pluginLog)); + serviceCollection.AddSingleton(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()); - 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); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); - _windowSystem.AddWindow(new DebugWindow(pluginInterface, _movementController, _questController, _gameFunctions, - clientState, framework, targetManager, _gameUiController, _configuration)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); - _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; - _framework.Update += FrameworkUpdate; - _commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); - _framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), TimeSpan.FromMilliseconds(200)); + _serviceProvider = serviceCollection.BuildServiceProvider(); + _serviceProvider.GetRequiredService().Reload(); + _serviceProvider.GetRequiredService(); + _serviceProvider.GetRequiredService(); } - private void FrameworkUpdate(IFramework framework) - { - _questController.Update(); - - HandleNavigationShortcut(); - _movementController.Update(); - } - - 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(); } } diff --git a/Questionable/Windows/DebugWindow.cs b/Questionable/Windows/DebugWindow.cs index d1e83b08..79904f57 100644 --- a/Questionable/Windows/DebugWindow.cs +++ b/Questionable/Windows/DebugWindow.cs @@ -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); + } } diff --git a/Questionable/packages.lock.json b/Questionable/packages.lock.json index 1ff84635..7c5ad370 100644 --- a/Questionable/packages.lock.json +++ b/Questionable/packages.lock.json @@ -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, )", @@ -17,6 +41,43 @@ "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",