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
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
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;
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);
{
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);
}
}
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;
}
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;
}
{
if (dialogueChoice.Answer == null)
{
- _pluginLog.Information("Ignoring entry in DialogueChoices, no answer");
+ _logger.LogInformation("Ignoring entry in DialogueChoices, no answer");
continue;
}
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;
}
if (actualPrompt == null)
return;
- _pluginLog.Verbose($"Prompt: '{actualPrompt}'");
+ _logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
var currentQuest = _questController.CurrentQuest;
if (currentQuest == null)
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;
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;
}
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();
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);
}
{
if (_questController.CurrentQuest?.Quest.QuestId == 4526)
{
- _pluginLog.Information("Closing Unending Codex");
+ _logger.LogInformation("Closing Unending Codex");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
addon->FireCallbackInt(-2);
}
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;
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;
{
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];
if (actualDistance > 100f &&
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4) == 0)
{
- _pluginLog.Information("Triggering Sprint");
+ _logger.LogInformation("Triggering Sprint");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4);
}
}
}
else if (_pathfindTask.IsCompleted)
{
- _pluginLog.Information("Unable to complete pathfinding task");
+ _logger.LogWarning("Unable to complete pathfinding task");
ResetPathfinding();
}
}
{
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));
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);
}
--- /dev/null
+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);
+ }
+ }
+}
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;
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;
}
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()
}
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;
(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;
}
(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;
}
(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;
}
(_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 }
{
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 }
}
else
{
- _pluginLog.Warning("Unable to teleport to aetheryte");
+ _logger.LogWarning("Unable to teleport to aetheryte");
_chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
}
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;
}
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;
}
} &&
_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;
}
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;
}
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;
}
{
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
{
}
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);
}
}
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;
}
(_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)
{
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))
{
}
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();
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;
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;
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:
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;
}
IncreaseStepCount();
}
else
- _pluginLog.Warning("Not interacting on current step, DataId is null");
+ _logger.LogWarning("Not interacting on current step, DataId is null");
break;
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);
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;
break;
default:
- _pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
+ _logger.LogWarning("Action '{InteractionType}' is not implemented", step.InteractionType);
break;
}
}
--- /dev/null
+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);
+}
--- /dev/null
+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;
+ }
+}
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;
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 =
.AsReadOnly();
}
- // FIXME
- public QuestController QuestController { private get; set; } = null!;
-
public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
{
ushort currentQuest;
break;
}
- if (QuestController.IsKnownQuest(currentQuest))
+ if (_questRegistry.IsKnownQuest(currentQuest))
return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
}
}
playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet);
}
+ public bool IsFlyingUnlockedInCurrentZone() => IsFlyingUnlocked(_clientState.TerritoryType);
+
public bool IsAetherCurrentUnlocked(uint aetherCurrentId)
{
var playerState = PlayerState.Instance();
}
}
- _pluginLog.Warning($"Could not find GameObject with dataId {dataId}");
+ _logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId);
return null;
}
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(
public bool HasStatusPreventingSprintOrMount()
{
- if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlocked(_clientState.TerritoryType))
+ if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
return true;
// company chocobo is locked
{
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);
}
}
{
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{
- _pluginLog.Information("Using mount roulette");
+ _logger.LogInformation("Using mount roulette");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
}
}
{
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;
}
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)
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;
}
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;
}
--- /dev/null
+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")]
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; }
-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!;
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()
{
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()
{
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()
{
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()
{
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()
{
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;
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()
{
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()
{
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)
{
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; }
}
namespace Questionable.Model.V1;
[JsonConverter(typeof(AetheryteConverter))]
-public enum EAetheryteLocation
+internal enum EAetheryteLocation
{
None = 0,
namespace Questionable.Model.V1;
-public enum EDialogChoiceType
+internal enum EDialogChoiceType
{
None,
YesNo,
namespace Questionable.Model.V1;
[JsonConverter(typeof(EmoteConverter))]
-public enum EEmote
+internal enum EEmote
{
None = 0,
namespace Questionable.Model.V1;
[JsonConverter(typeof(EnemySpawnTypeConverter))]
-public enum EEnemySpawnType
+internal enum EEnemySpawnType
{
None = 0,
AfterInteraction,
namespace Questionable.Model.V1;
[JsonConverter(typeof(InteractionTypeConverter))]
-public enum EInteractionType
+internal enum EInteractionType
{
Interact,
WalkTo,
namespace Questionable.Model.V1;
[JsonConverter(typeof(SkipConditionConverter))]
-public enum ESkipCondition
+internal enum ESkipCondition
{
None,
Never,
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; }
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();
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; }
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; }
</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>
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();
}
}
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;
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;
MinimumSize = new Vector2(200, 30),
MaximumSize = default
};
+
+ _windowSystem.AddWindow(this);
}
public WindowConfig WindowConfig => _configuration.DebugWindowConfig;
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}");
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);
}
}
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();
TimeSpan.FromMilliseconds(200));
}
}
+
+ public void Dispose()
+ {
+ _windowSystem.RemoveWindow(this);
+ }
}
"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",