Use ServiceHost/ILogger

master
Liza 2024-06-08 21:16:57 +02:00
parent fe49d5354f
commit 78357dc288
Signed by: liza
GPG Key ID: 7199F8D727D55F67
35 changed files with 519 additions and 304 deletions

View File

@ -290,7 +290,7 @@ dotnet_diagnostic.CA1806.severity = warning
dotnet_diagnostic.CA1810.severity = warning
# CA1812: Avoid uninstantiated internal classes
dotnet_diagnostic.CA1812.severity = warning
dotnet_diagnostic.CA1812.severity = suggestion
# CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = warning
@ -392,7 +392,7 @@ dotnet_diagnostic.CA1846.severity = warning
dotnet_diagnostic.CA1847.severity = warning
# CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = warning
dotnet_diagnostic.CA1848.severity = suggestion
# CA1849: Call async methods when in an async method
dotnet_diagnostic.CA1849.severity = warning

View File

@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
using Questionable.Model.V1;
using Quest = Questionable.Model.Quest;
@ -21,17 +22,17 @@ internal sealed class GameUiController : IDisposable
private readonly GameFunctions _gameFunctions;
private readonly QuestController _questController;
private readonly IGameGui _gameGui;
private readonly IPluginLog _pluginLog;
private readonly ILogger<GameUiController> _logger;
public GameUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, GameFunctions gameFunctions,
QuestController questController, IGameGui gameGui, IPluginLog pluginLog)
QuestController questController, IGameGui gameGui, ILogger<GameUiController> logger)
{
_addonLifecycle = addonLifecycle;
_dataManager = dataManager;
_gameFunctions = gameFunctions;
_questController = questController;
_gameGui = gameGui;
_pluginLog = pluginLog;
_logger = logger;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
@ -45,26 +46,26 @@ internal sealed class GameUiController : IDisposable
{
if (_gameGui.TryGetAddonByName("SelectString", out AddonSelectString* addonSelectString))
{
_pluginLog.Information("SelectString window is open");
_logger.LogInformation("SelectString window is open");
SelectStringPostSetup(addonSelectString, true);
}
if (_gameGui.TryGetAddonByName("CutSceneSelectString",
out AddonCutSceneSelectString* addonCutSceneSelectString))
{
_pluginLog.Information("CutSceneSelectString window is open");
_logger.LogInformation("CutSceneSelectString window is open");
CutsceneSelectStringPostSetup(addonCutSceneSelectString, true);
}
if (_gameGui.TryGetAddonByName("SelectIconString", out AddonSelectIconString* addonSelectIconString))
{
_pluginLog.Information("SelectIconString window is open");
_logger.LogInformation("SelectIconString window is open");
SelectIconStringPostSetup(addonSelectIconString, true);
}
if (_gameGui.TryGetAddonByName("SelectYesno", out AddonSelectYesno* addonSelectYesno))
{
_pluginLog.Information("SelectYesno window is open");
_logger.LogInformation("SelectYesno window is open");
SelectYesnoPostSetup(addonSelectYesno, true);
}
}
@ -151,7 +152,7 @@ internal sealed class GameUiController : IDisposable
var currentQuest = _questController.CurrentQuest;
if (currentQuest == null)
{
_pluginLog.Information("Ignoring list choice, no active quest");
_logger.LogInformation("Ignoring list choice, no active quest");
return null;
}
@ -167,7 +168,7 @@ internal sealed class GameUiController : IDisposable
var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
if (step == null)
{
_pluginLog.Information("Ignoring list choice, no active step");
_logger.LogInformation("Ignoring list choice, no active step");
return null;
}
@ -178,7 +179,7 @@ internal sealed class GameUiController : IDisposable
{
if (dialogueChoice.Answer == null)
{
_pluginLog.Information("Ignoring entry in DialogueChoices, no answer");
_logger.LogInformation("Ignoring entry in DialogueChoices, no answer");
continue;
}
@ -212,28 +213,31 @@ internal sealed class GameUiController : IDisposable
if (actualPrompt == null && !string.IsNullOrEmpty(excelPrompt))
{
_pluginLog.Information($"Unexpected excelPrompt: {excelPrompt}");
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", excelPrompt);
continue;
}
if (actualPrompt != null && (excelPrompt == null || !GameStringEquals(actualPrompt, excelPrompt)))
{
_pluginLog.Information($"Unexpected excelPrompt: {excelPrompt}, actualPrompt: {actualPrompt}");
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}",
excelPrompt, actualPrompt);
continue;
}
for (int i = 0; i < answers.Count; ++i)
{
_pluginLog.Verbose($"Checking if {answers[i]} == {excelAnswer}");
_logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}",
answers[i], excelAnswer);
if (GameStringEquals(answers[i], excelAnswer))
{
_pluginLog.Information($"Returning {i}: '{answers[i]}' for '{actualPrompt}'");
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'",
i, answers[i], actualPrompt);
return i;
}
}
}
_pluginLog.Information($"No matching answer found for {actualPrompt}.");
_logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
return null;
}
@ -249,7 +253,7 @@ internal sealed class GameUiController : IDisposable
if (actualPrompt == null)
return;
_pluginLog.Verbose($"Prompt: '{actualPrompt}'");
_logger.LogTrace("Prompt: '{Prompt}'", actualPrompt);
var currentQuest = _questController.CurrentQuest;
if (currentQuest == null)
@ -277,7 +281,7 @@ internal sealed class GameUiController : IDisposable
private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
IList<DialogueChoice> dialogueChoices, string actualPrompt, bool checkAllSteps)
{
_pluginLog.Verbose($"DefaultYesNo: Choice count: {dialogueChoices.Count}");
_logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
foreach (var dialogueChoice in dialogueChoices)
{
string? excelPrompt;
@ -325,21 +329,22 @@ internal sealed class GameUiController : IDisposable
bool increaseStepCount = true;
QuestStep? step = sequence.FindStep(currentQuest.Step);
if (step != null)
_pluginLog.Verbose($"Current step: {step.TerritoryId}, {step.TargetTerritoryId}");
_logger.LogTrace("Current step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId,
step.TargetTerritoryId);
if (step == null || step.TargetTerritoryId == null)
{
_pluginLog.Verbose("TravelYesNo: Checking previous step...");
_logger.LogTrace("TravelYesNo: Checking previous step...");
step = sequence.FindStep(currentQuest.Step == 255 ? (sequence.Steps.Count - 1) : (currentQuest.Step - 1));
increaseStepCount = false;
if (step != null)
_pluginLog.Verbose($"Previous step: {step.TerritoryId}, {step.TargetTerritoryId}");
_logger.LogTrace("Previous step: {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, step.TargetTerritoryId);
}
if (step == null || step.TargetTerritoryId == null)
{
_pluginLog.Verbose("TravelYesNo: Not found");
_logger.LogTrace("TravelYesNo: Not found");
return;
}
@ -351,11 +356,11 @@ internal sealed class GameUiController : IDisposable
string? excelPrompt = entry.Question?.ToString();
if (excelPrompt == null || !GameStringEquals(excelPrompt, actualPrompt))
{
_pluginLog.Information($"Ignoring prompt '{excelPrompt}'");
_logger.LogDebug("Ignoring prompt '{Prompt}'", excelPrompt);
continue;
}
_pluginLog.Information($"Using warp {entry.RowId}, {excelPrompt}");
_logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
if (increaseStepCount)
_questController.IncreaseStepCount();
@ -365,7 +370,7 @@ internal sealed class GameUiController : IDisposable
private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
{
_pluginLog.Information("Closing Credits sequence");
_logger.LogInformation("Closing Credits sequence");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
addon->FireCallbackInt(-2);
}
@ -374,7 +379,7 @@ internal sealed class GameUiController : IDisposable
{
if (_questController.CurrentQuest?.Quest.QuestId == 4526)
{
_pluginLog.Information("Closing Unending Codex");
_logger.LogInformation("Closing Unending Codex");
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
addon->FireCallbackInt(-2);
}

View File

@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using Microsoft.Extensions.Logging;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.V1;
@ -26,18 +27,18 @@ internal sealed class MovementController : IDisposable
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
private readonly ICondition _condition;
private readonly IPluginLog _pluginLog;
private readonly ILogger<MovementController> _logger;
private CancellationTokenSource? _cancellationTokenSource;
private Task<List<Vector3>>? _pathfindTask;
public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions,
ICondition condition, IPluginLog pluginLog)
ICondition condition, ILogger<MovementController> logger)
{
_navmeshIpc = navmeshIpc;
_clientState = clientState;
_gameFunctions = gameFunctions;
_condition = condition;
_pluginLog = pluginLog;
_logger = logger;
}
public bool IsNavmeshReady => _navmeshIpc.IsReady;
@ -51,9 +52,8 @@ internal sealed class MovementController : IDisposable
{
if (_pathfindTask.IsCompletedSuccessfully)
{
_pluginLog.Information(
string.Create(CultureInfo.InvariantCulture,
$"Pathfinding complete, route: [{string.Join(" ", _pathfindTask.Result.Select(x => x.ToString()))}]"));
_logger.LogInformation("Pathfinding complete, route: [{Route}]",
string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
var navPoints = _pathfindTask.Result.Skip(1).ToList();
Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
@ -83,7 +83,7 @@ internal sealed class MovementController : IDisposable
if (actualDistance > 100f &&
ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4) == 0)
{
_pluginLog.Information("Triggering Sprint");
_logger.LogInformation("Triggering Sprint");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4);
}
}
@ -94,7 +94,7 @@ internal sealed class MovementController : IDisposable
}
else if (_pathfindTask.IsCompleted)
{
_pluginLog.Information("Unable to complete pathfinding task");
_logger.LogWarning("Unable to complete pathfinding task");
ResetPathfinding();
}
}
@ -170,7 +170,7 @@ internal sealed class MovementController : IDisposable
{
fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance);
_pluginLog.Information($"Pathfinding to {Destination}");
_logger.LogInformation("Pathfinding to {Destination}", Destination);
_cancellationTokenSource = new();
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
@ -183,7 +183,7 @@ internal sealed class MovementController : IDisposable
fly |= _condition[ConditionFlag.Diving];
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance);
_pluginLog.Information($"Moving to {Destination}");
_logger.LogInformation("Moving to {Destination}", Destination);
_navmeshIpc.MoveTo(to, fly);
}

View File

@ -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);
}
}
}

View File

@ -1,15 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Numerics;
using System.Text.Json;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
@ -20,39 +16,34 @@ namespace Questionable.Controller;
internal sealed class QuestController
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDataManager _dataManager;
private readonly IClientState _clientState;
private readonly GameFunctions _gameFunctions;
private readonly MovementController _movementController;
private readonly IPluginLog _pluginLog;
private readonly ILogger<QuestController> _logger;
private readonly ICondition _condition;
private readonly IChatGui _chatGui;
private readonly IFramework _framework;
private readonly AetheryteData _aetheryteData;
private readonly LifestreamIpc _lifestreamIpc;
private readonly TerritoryData _territoryData;
private readonly Dictionary<ushort, Quest> _quests = new();
private readonly QuestRegistry _questRegistry;
public QuestController(DalamudPluginInterface pluginInterface, IDataManager dataManager, IClientState clientState,
GameFunctions gameFunctions, MovementController movementController, IPluginLog pluginLog, ICondition condition,
IChatGui chatGui, IFramework framework, AetheryteData aetheryteData, LifestreamIpc lifestreamIpc)
public QuestController(IClientState clientState, GameFunctions gameFunctions, MovementController movementController,
ILogger<QuestController> logger, ICondition condition, IChatGui chatGui, IFramework framework,
AetheryteData aetheryteData, LifestreamIpc lifestreamIpc, TerritoryData territoryData,
QuestRegistry questRegistry)
{
_pluginInterface = pluginInterface;
_dataManager = dataManager;
_clientState = clientState;
_gameFunctions = gameFunctions;
_movementController = movementController;
_pluginLog = pluginLog;
_logger = logger;
_condition = condition;
_chatGui = chatGui;
_framework = framework;
_aetheryteData = aetheryteData;
_lifestreamIpc = lifestreamIpc;
_territoryData = new TerritoryData(dataManager);
Reload();
_gameFunctions.QuestController = this;
_territoryData = territoryData;
_questRegistry = questRegistry;
}
@ -62,88 +53,10 @@ internal sealed class QuestController
public void Reload()
{
_quests.Clear();
CurrentQuest = null;
DebugState = null;
#if RELEASE
_pluginLog.Information("Loading quests from assembly");
QuestPaths.AssemblyQuestLoader.LoadQuestsFromEmbeddedResources(LoadQuestFromStream);
#else
DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation?.Directory?.Parent?.Parent;
if (solutionDirectory != null)
{
DirectoryInfo pathProjectDirectory =
new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "QuestPaths"));
if (pathProjectDirectory.Exists)
{
LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Shadowbringers")));
LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Endwalker")));
}
}
#endif
LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
foreach (var (questId, quest) in _quests)
{
var questData =
_dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
if (questData == null)
continue;
quest.Name = questData.Name.ToString();
}
}
private void LoadQuestFromStream(string fileName, Stream stream)
{
_pluginLog.Verbose($"Loading quest from '{fileName}'");
var (questId, name) = ExtractQuestDataFromName(fileName);
Quest quest = new Quest
{
QuestId = questId,
Name = name,
Data = JsonSerializer.Deserialize<QuestData>(stream)!,
};
_quests[questId] = quest;
}
public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId);
private void LoadFromDirectory(DirectoryInfo directory)
{
if (!directory.Exists)
{
_pluginLog.Information($"Not loading quests from {directory} (doesn't exist)");
return;
}
_pluginLog.Information($"Loading quests from {directory}");
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
LoadQuestFromStream(fileInfo.Name, stream);
}
catch (Exception e)
{
throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
}
}
foreach (DirectoryInfo childDirectory in directory.GetDirectories())
LoadFromDirectory(childDirectory);
}
private static (ushort QuestId, string Name) ExtractQuestDataFromName(string resourceName)
{
string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
name = name.Substring(name.LastIndexOf('.') + 1);
string[] parts = name.Split('_', 2);
return (ushort.Parse(parts[0], CultureInfo.InvariantCulture), parts[1]);
_questRegistry.Reload();
}
public void Update()
@ -158,7 +71,7 @@ internal sealed class QuestController
}
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
{
if (_quests.TryGetValue(currentQuestId, out var quest))
if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
CurrentQuest = new QuestProgress(quest, currentSequence, 0);
else if (CurrentQuest != null)
CurrentQuest = null;
@ -244,7 +157,7 @@ internal sealed class QuestController
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
{
_pluginLog.Warning("Unable to retrieve next quest step, not increasing step count");
_logger.LogWarning("Unable to retrieve next quest step, not increasing step count");
return;
}
@ -271,7 +184,7 @@ internal sealed class QuestController
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
{
_pluginLog.Warning("Unable to retrieve next quest step, not increasing dialogue choice count");
_logger.LogWarning("Unable to retrieve next quest step, not increasing dialogue choice count");
return;
}
@ -292,13 +205,13 @@ internal sealed class QuestController
(QuestSequence? seq, QuestStep? step) = GetNextStep();
if (CurrentQuest == null || seq == null || step == null)
{
_pluginLog.Warning("Could not retrieve next quest step, not doing anything");
_logger.LogWarning("Could not retrieve next quest step, not doing anything");
return;
}
if (step.Disabled)
{
_pluginLog.Information("Skipping step, as it is disabled");
_logger.LogInformation("Skipping step, as it is disabled");
IncreaseStepCount();
return;
}
@ -315,14 +228,14 @@ internal sealed class QuestController
(_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
{
_pluginLog.Information("Skipping aetheryte teleport");
_logger.LogInformation("Skipping aetheryte teleport");
skipTeleport = true;
}
}
if (skipTeleport)
{
_pluginLog.Information("Marking aetheryte shortcut as used");
_logger.LogInformation("Marking aetheryte shortcut as used");
CurrentQuest = CurrentQuest with
{
StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
@ -332,12 +245,12 @@ internal sealed class QuestController
{
if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
{
_pluginLog.Error($"Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
_logger.LogError("Aetheryte {Aetheryte} is not unlocked.", step.AetheryteShortcut.Value);
_chatGui.Print($"[Questionable] Aetheryte {step.AetheryteShortcut.Value} is not unlocked.");
}
else if (_gameFunctions.TeleportAetheryte(step.AetheryteShortcut.Value))
{
_pluginLog.Information("Travelling via aetheryte...");
_logger.LogInformation("Travelling via aetheryte...");
CurrentQuest = CurrentQuest with
{
StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
@ -345,7 +258,7 @@ internal sealed class QuestController
}
else
{
_pluginLog.Warning("Unable to teleport to aetheryte");
_logger.LogWarning("Unable to teleport to aetheryte");
_chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
}
@ -355,12 +268,12 @@ internal sealed class QuestController
if (!step.SkipIf.Contains(ESkipCondition.Never))
{
_pluginLog.Information($"Checking skip conditions; {string.Join(",", step.SkipIf)}");
_logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", step.SkipIf));
if (step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{
_pluginLog.Information("Skipping step, as flying is unlocked");
_logger.LogInformation("Skipping step, as flying is unlocked");
IncreaseStepCount();
return;
}
@ -368,7 +281,7 @@ internal sealed class QuestController
if (step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
!_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{
_pluginLog.Information("Skipping step, as flying is locked");
_logger.LogInformation("Skipping step, as flying is locked");
IncreaseStepCount();
return;
}
@ -380,7 +293,7 @@ internal sealed class QuestController
} &&
_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
{
_pluginLog.Information("Skipping step, as aetheryte/aethernet shard is unlocked");
_logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
IncreaseStepCount();
return;
}
@ -388,7 +301,7 @@ internal sealed class QuestController
if (step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
_gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
{
_pluginLog.Information("Skipping step, as current is unlocked");
_logger.LogInformation("Skipping step, as current is unlocked");
IncreaseStepCount();
return;
}
@ -396,7 +309,7 @@ internal sealed class QuestController
QuestWork? questWork = _gameFunctions.GetQuestEx(CurrentQuest.Quest.QuestId);
if (questWork != null && step.MatchesQuestVariables(questWork.Value))
{
_pluginLog.Information("Skipping step, as quest variables match");
_logger.LogInformation("Skipping step, as quest variables match");
IncreaseStepCount();
return;
}
@ -418,7 +331,7 @@ internal sealed class QuestController
{
if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) < 11)
{
_pluginLog.Information($"Using lifestream to teleport to {to}");
_logger.LogInformation("Using lifestream to teleport to {Destination}", to);
_lifestreamIpc.Teleport(to);
CurrentQuest = CurrentQuest with
{
@ -427,7 +340,7 @@ internal sealed class QuestController
}
else
{
_pluginLog.Information("Moving to aethernet shortcut");
_logger.LogInformation("Moving to aethernet shortcut");
_movementController.NavigateTo(EMovementType.Quest, (uint)from, _aetheryteData.Locations[from],
false, true,
AetheryteConverter.IsLargeAetheryte(from) ? 10.9f : 6.9f);
@ -437,14 +350,16 @@ internal sealed class QuestController
}
}
else
_pluginLog.Warning(
$"Aethernet shortcut not unlocked (from: {step.AethernetShortcut.From}, to: {step.AethernetShortcut.To}), walking manually");
_logger.LogWarning(
"Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
step.AethernetShortcut.From, step.AethernetShortcut.To);
}
if (step.TargetTerritoryId.HasValue && step.TerritoryId != step.TargetTerritoryId && step.TargetTerritoryId == _clientState.TerritoryType)
if (step.TargetTerritoryId.HasValue && step.TerritoryId != step.TargetTerritoryId &&
step.TargetTerritoryId == _clientState.TerritoryType)
{
// we assume whatever e.g. interaction, walkto etc. we have will trigger the zone transition
_pluginLog.Information("Zone transition, skipping rest of step");
_logger.LogInformation("Zone transition, skipping rest of step");
IncreaseStepCount();
return;
}
@ -453,7 +368,7 @@ internal sealed class QuestController
(_clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <=
(step.JumpDestination.StopDistance ?? 1f))
{
_pluginLog.Information("We're at the jump destination, skipping movement");
_logger.LogInformation("We're at the jump destination, skipping movement");
}
else if (step.Position != null)
{
@ -468,7 +383,7 @@ internal sealed class QuestController
if (step.Mount == true && !_gameFunctions.HasStatusPreventingSprintOrMount())
{
_pluginLog.Information("Step explicitly wants a mount, trying to mount...");
_logger.LogInformation("Step explicitly wants a mount, trying to mount...");
if (!_condition[ConditionFlag.Mounted] && !_condition[ConditionFlag.InCombat] &&
_territoryData.CanUseMount(_clientState.TerritoryType))
{
@ -478,7 +393,7 @@ internal sealed class QuestController
}
else if (step.Mount == false)
{
_pluginLog.Information("Step explicitly wants no mount, trying to unmount...");
_logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
if (_condition[ConditionFlag.Mounted])
{
_gameFunctions.Unmount();
@ -499,7 +414,7 @@ internal sealed class QuestController
if (actualDistance > distance)
{
_movementController.NavigateTo(EMovementType.Quest, step.DataId, step.Position.Value,
fly: step.Fly == true && _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType),
fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
sprint: step.Sprint != false,
stopDistance: distance);
return;
@ -511,7 +426,7 @@ internal sealed class QuestController
if (actualDistance > distance)
{
_movementController.NavigateTo(EMovementType.Quest, step.DataId, [step.Position.Value],
fly: step.Fly == true && _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType),
fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
sprint: step.Sprint != false,
stopDistance: distance);
return;
@ -524,12 +439,12 @@ internal sealed class QuestController
if (gameObject == null ||
(gameObject.Position - _clientState.LocalPlayer!.Position).Length() > step.StopDistance)
{
_pluginLog.Warning("Object not found or too far away, no position so we can't move");
_logger.LogWarning("Object not found or too far away, no position so we can't move");
return;
}
}
_pluginLog.Information($"Running logic for {step.InteractionType}");
_logger.LogInformation("Running logic for {InteractionType}", step.InteractionType);
switch (step.InteractionType)
{
case EInteractionType.Interact:
@ -538,7 +453,7 @@ internal sealed class QuestController
GameObject? gameObject = _gameFunctions.FindObjectByDataId(step.DataId.Value);
if (gameObject == null)
{
_pluginLog.Warning($"No game object with dataId {step.DataId}");
_logger.LogWarning("No game object with dataId {DataId}", step.DataId);
return;
}
@ -555,7 +470,7 @@ internal sealed class QuestController
IncreaseStepCount();
}
else
_pluginLog.Warning("Not interacting on current step, DataId is null");
_logger.LogWarning("Not interacting on current step, DataId is null");
break;
@ -584,8 +499,10 @@ internal sealed class QuestController
case EInteractionType.AttuneAetherCurrent:
if (step.DataId != null)
{
_pluginLog.Information(
$"{step.AetherCurrentId} → {_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.GetValueOrDefault())}");
_logger.LogInformation(
"{AetherCurrentId} is unlocked = {Unlocked}",
step.AetherCurrentId,
_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.GetValueOrDefault()));
if (step.AetherCurrentId == null ||
!_gameFunctions.IsAetherCurrentUnlocked(step.AetherCurrentId.Value))
_gameFunctions.InteractWith(step.DataId.Value);
@ -662,7 +579,8 @@ internal sealed class QuestController
if (step.ChatMessage != null)
{
string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest, step.ChatMessage.ExcelSheet,
string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest,
step.ChatMessage.ExcelSheet,
step.ChatMessage.Key);
if (excelString == null)
return;
@ -722,7 +640,7 @@ internal sealed class QuestController
break;
default:
_pluginLog.Warning($"Action '{step.InteractionType}' is not implemented");
_logger.LogWarning("Action '{InteractionType}' is not implemented", step.InteractionType);
break;
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text.Json;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.V1;
namespace Questionable.Controller;
internal sealed class QuestRegistry
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDataManager _dataManager;
private readonly ILogger<QuestRegistry> _logger;
private readonly Dictionary<ushort, Quest> _quests = new();
public QuestRegistry(DalamudPluginInterface pluginInterface, IDataManager dataManager, ILogger<QuestRegistry> logger)
{
_pluginInterface = pluginInterface;
_dataManager = dataManager;
_logger = logger;
}
public void Reload()
{
_quests.Clear();
#if RELEASE
_logger.LogInformation("Loading quests from assembly");
QuestPaths.AssemblyQuestLoader.LoadQuestsFromEmbeddedResources(LoadQuestFromStream);
#else
DirectoryInfo? solutionDirectory = _pluginInterface.AssemblyLocation?.Directory?.Parent?.Parent;
if (solutionDirectory != null)
{
DirectoryInfo pathProjectDirectory =
new DirectoryInfo(Path.Combine(solutionDirectory.FullName, "QuestPaths"));
if (pathProjectDirectory.Exists)
{
LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Shadowbringers")));
LoadFromDirectory(new DirectoryInfo(Path.Combine(pathProjectDirectory.FullName, "Endwalker")));
}
}
#endif
LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")));
foreach (var (questId, quest) in _quests)
{
var questData =
_dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Quest>()!.GetRow((uint)questId + 0x10000);
if (questData == null)
continue;
quest.Name = questData.Name.ToString();
}
}
private void LoadQuestFromStream(string fileName, Stream stream)
{
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
var (questId, name) = ExtractQuestDataFromName(fileName);
Quest quest = new Quest
{
QuestId = questId,
Name = name,
Data = JsonSerializer.Deserialize<QuestData>(stream)!,
};
_quests[questId] = quest;
}
private void LoadFromDirectory(DirectoryInfo directory)
{
if (!directory.Exists)
{
_logger.LogInformation("Not loading quests from {DirectoryName} (doesn't exist)", directory);
return;
}
_logger.LogInformation("Loading quests from {DirectoryName}", directory);
foreach (FileInfo fileInfo in directory.GetFiles("*.json"))
{
try
{
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
LoadQuestFromStream(fileInfo.Name, stream);
}
catch (Exception e)
{
throw new InvalidDataException($"Unable to load file {fileInfo.FullName}", e);
}
}
foreach (DirectoryInfo childDirectory in directory.GetDirectories())
LoadFromDirectory(childDirectory);
}
private static (ushort QuestId, string Name) ExtractQuestDataFromName(string resourceName)
{
string name = resourceName.Substring(0, resourceName.Length - ".json".Length);
name = name.Substring(name.LastIndexOf('.') + 1);
string[] parts = name.Split('_', 2);
return (ushort.Parse(parts[0], CultureInfo.InvariantCulture), parts[1]);
}
public bool IsKnownQuest(ushort questId) => _quests.ContainsKey(questId);
public bool TryGetQuest(ushort questId, [NotNullWhen(true)] out Quest? quest)
=> _quests.TryGetValue(questId, out quest);
}

View File

@ -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;
}
}

View File

@ -22,6 +22,7 @@ using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.CustomSheets;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller;
using Questionable.Model.V1;
using BattleChara = FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara;
@ -51,17 +52,20 @@ internal sealed unsafe class GameFunctions
private readonly ITargetManager _targetManager;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly QuestRegistry _questRegistry;
private readonly ILogger<GameFunctions> _logger;
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
ITargetManager targetManager, ICondition condition, IClientState clientState, IPluginLog pluginLog)
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
ILogger<GameFunctions> logger)
{
_dataManager = dataManager;
_objectTable = objectTable;
_targetManager = targetManager;
_condition = condition;
_clientState = clientState;
_pluginLog = pluginLog;
_questRegistry = questRegistry;
_logger = logger;
_processChatBox =
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
_sanitiseString =
@ -85,9 +89,6 @@ internal sealed unsafe class GameFunctions
.AsReadOnly();
}
// FIXME
public QuestController QuestController { private get; set; } = null!;
public (ushort CurrentQuest, byte Sequence) GetCurrentQuest()
{
ushort currentQuest;
@ -108,7 +109,7 @@ internal sealed unsafe class GameFunctions
break;
}
if (QuestController.IsKnownQuest(currentQuest))
if (_questRegistry.IsKnownQuest(currentQuest))
return (currentQuest, QuestManager.GetQuestSequence(currentQuest));
}
}
@ -190,6 +191,8 @@ internal sealed unsafe class GameFunctions
playerState->IsAetherCurrentZoneComplete(aetherCurrentCompFlgSet);
}
public bool IsFlyingUnlockedInCurrentZone() => IsFlyingUnlocked(_clientState.TerritoryType);
public bool IsAetherCurrentUnlocked(uint aetherCurrentId)
{
var playerState = PlayerState.Instance();
@ -335,7 +338,7 @@ internal sealed unsafe class GameFunctions
}
}
_pluginLog.Warning($"Could not find GameObject with dataId {dataId}");
_logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId);
return null;
}
@ -344,7 +347,7 @@ internal sealed unsafe class GameFunctions
GameObject? gameObject = FindObjectByDataId(dataId);
if (gameObject != null)
{
_pluginLog.Information($"Setting target with {dataId} to {gameObject.ObjectId}");
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId);
_targetManager.Target = gameObject;
TargetSystem.Instance()->InteractWithObject(
@ -400,7 +403,7 @@ internal sealed unsafe class GameFunctions
public bool HasStatusPreventingSprintOrMount()
{
if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlocked(_clientState.TerritoryType))
if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone())
return true;
// company chocobo is locked
@ -428,7 +431,7 @@ internal sealed unsafe class GameFunctions
{
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
{
_pluginLog.Information("Using SDS Fenrir as mount");
_logger.LogInformation("Using SDS Fenrir as mount");
ActionManager.Instance()->UseAction(ActionType.Mount, 71);
}
}
@ -436,7 +439,7 @@ internal sealed unsafe class GameFunctions
{
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
{
_pluginLog.Information("Using mount roulette");
_logger.LogInformation("Using mount roulette");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
}
}
@ -449,11 +452,11 @@ internal sealed unsafe class GameFunctions
{
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
{
_pluginLog.Information("Unmounting...");
_logger.LogInformation("Unmounting...");
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
}
else
_pluginLog.Warning("Can't unmount right now?");
_logger.LogWarning("Can't unmount right now?");
return true;
}
@ -468,11 +471,12 @@ internal sealed unsafe class GameFunctions
if (UIState.IsInstanceContentUnlocked(contentId))
AgentContentsFinder.Instance()->OpenRegularDuty(contentFinderConditionId);
else
_pluginLog.Error(
$"Trying to access a locked duty (cf: {contentFinderConditionId}, content: {contentId})");
_logger.LogError(
"Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})",
contentFinderConditionId, contentId);
}
else
_pluginLog.Error($"Could not find content for content finder condition (cf: {contentFinderConditionId})");
_logger.LogError("Could not find content for content finder condition (cf: {ContentFinderId})", contentFinderConditionId);
}
public string? GetDialogueText(Quest currentQuest, string? excelSheetName, string key)
@ -482,7 +486,7 @@ internal sealed unsafe class GameFunctions
var questRow = _dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets2.Quest>()!.GetRow((uint)currentQuest.QuestId + 0x10000);
if (questRow == null)
{
_pluginLog.Error($"Could not find quest row for {currentQuest.QuestId}");
_logger.LogError("Could not find quest row for {QuestId}", currentQuest.QuestId);
return null;
}
@ -492,7 +496,7 @@ internal sealed unsafe class GameFunctions
var excelSheet = _dataManager.Excel.GetSheet<QuestDialogueText>(excelSheetName);
if (excelSheet == null)
{
_pluginLog.Error($"Unknown excel sheet '{excelSheetName}'");
_logger.LogError("Unknown excel sheet '{SheetName}'", excelSheetName);
return null;
}

View File

@ -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")]

View File

@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
[JsonConverter(typeof(AethernetShortcutConverter))]
public sealed class AethernetShortcut
internal sealed class AethernetShortcut
{
public EAetheryteLocation From { get; set; }
public EAetheryteLocation To { get; set; }

View File

@ -1,6 +1,9 @@
namespace Questionable.Model.V1;
using JetBrains.Annotations;
public sealed class ChatMessage
namespace Questionable.Model.V1;
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
internal sealed class ChatMessage
{
public string? ExcelSheet { get; set; }
public string Key { get; set; } = null!;

View File

@ -6,7 +6,7 @@ using System.Text.Json.Serialization;
namespace Questionable.Model.V1.Converter;
public sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
internal sealed class AethernetShortcutConverter : JsonConverter<AethernetShortcut>
{
private static readonly Dictionary<EAetheryteLocation, string> EnumToString = new()
{

View File

@ -2,7 +2,7 @@
namespace Questionable.Model.V1.Converter;
public sealed class AetheryteConverter() : EnumConverter<EAetheryteLocation>(Values)
internal sealed class AetheryteConverter() : EnumConverter<EAetheryteLocation>(Values)
{
private static readonly Dictionary<EAetheryteLocation, string> Values = new()
{

View File

@ -2,7 +2,7 @@
namespace Questionable.Model.V1.Converter;
public sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
internal sealed class DialogueChoiceTypeConverter() : EnumConverter<EDialogChoiceType>(Values)
{
private static readonly Dictionary<EDialogChoiceType, string> Values = new()
{

View File

@ -2,7 +2,7 @@
namespace Questionable.Model.V1.Converter;
public sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
internal sealed class EmoteConverter() : EnumConverter<EEmote>(Values)
{
private static readonly Dictionary<EEmote, string> Values = new()
{

View File

@ -2,7 +2,7 @@
namespace Questionable.Model.V1.Converter;
public sealed class EnemySpawnTypeConverter() : EnumConverter<EEnemySpawnType>(Values)
internal sealed class EnemySpawnTypeConverter() : EnumConverter<EEnemySpawnType>(Values)
{
private static readonly Dictionary<EEnemySpawnType, string> Values = new()
{

View File

@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
namespace Questionable.Model.V1.Converter;
public abstract class EnumConverter<T> : JsonConverter<T>
internal abstract class EnumConverter<T> : JsonConverter<T>
where T : Enum
{
private readonly ReadOnlyDictionary<T, string> _enumToString;

View File

@ -2,7 +2,7 @@
namespace Questionable.Model.V1.Converter;
public sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>(Values)
internal sealed class InteractionTypeConverter() : EnumConverter<EInteractionType>(Values)
{
private static readonly Dictionary<EInteractionType, string> Values = new()
{

View File

@ -2,7 +2,7 @@
namespace Questionable.Model.V1.Converter;
public sealed class SkipConditionConverter() : EnumConverter<ESkipCondition>(Values)
internal sealed class SkipConditionConverter() : EnumConverter<ESkipCondition>(Values)
{
private static readonly Dictionary<ESkipCondition, string> Values = new()
{

View File

@ -5,7 +5,7 @@ using System.Text.Json.Serialization;
namespace Questionable.Model.V1.Converter;
public class VectorConverter : JsonConverter<Vector3>
internal sealed class VectorConverter : JsonConverter<Vector3>
{
public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{

View File

@ -1,14 +1,16 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
public class DialogueChoice
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
internal sealed class DialogueChoice
{
[JsonConverter(typeof(DialogueChoiceTypeConverter))]
public EDialogChoiceType Type { get; set; }
public string? ExcelSheet { get; set; }
public string? Prompt { get; set; } = null!;
public string? Prompt { get; set; }
public bool Yes { get; set; } = true;
public string? Answer { get; set; }
}

View File

@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
[JsonConverter(typeof(AetheryteConverter))]
public enum EAetheryteLocation
internal enum EAetheryteLocation
{
None = 0,

View File

@ -1,6 +1,6 @@
namespace Questionable.Model.V1;
public enum EDialogChoiceType
internal enum EDialogChoiceType
{
None,
YesNo,

View File

@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
[JsonConverter(typeof(EmoteConverter))]
public enum EEmote
internal enum EEmote
{
None = 0,

View File

@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
[JsonConverter(typeof(EnemySpawnTypeConverter))]
public enum EEnemySpawnType
internal enum EEnemySpawnType
{
None = 0,
AfterInteraction,

View File

@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
[JsonConverter(typeof(InteractionTypeConverter))]
public enum EInteractionType
internal enum EInteractionType
{
Interact,
WalkTo,

View File

@ -4,7 +4,7 @@ using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
[JsonConverter(typeof(SkipConditionConverter))]
public enum ESkipCondition
internal enum ESkipCondition
{
None,
Never,

View File

@ -1,10 +1,12 @@
using System.Numerics;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
public sealed class JumpDestination
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
internal sealed class JumpDestination
{
[JsonConverter(typeof(VectorConverter))]
public Vector3 Position { get; set; }

View File

@ -1,8 +1,10 @@
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Questionable.Model.V1;
public class QuestData
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
internal sealed class QuestData
{
public required string Author { get; set; }
public List<string> Contributors { get; set; } = new();

View File

@ -1,8 +1,10 @@
using System.Collections.Generic;
using JetBrains.Annotations;
namespace Questionable.Model.V1;
public class QuestSequence
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
internal sealed class QuestSequence
{
public required int Sequence { get; set; }
public string? Comment { get; set; }

View File

@ -1,12 +1,15 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Text.Json.Serialization;
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
using JetBrains.Annotations;
using Questionable.Model.V1.Converter;
namespace Questionable.Model.V1;
public class QuestStep
[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.WithMembers)]
internal sealed class QuestStep
{
public EInteractionType InteractionType { get; set; }

View File

@ -23,7 +23,10 @@
</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>

View File

@ -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<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());
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<GameFunctions>();
serviceCollection.AddSingleton<AetheryteData>();
serviceCollection.AddSingleton<TerritoryData>();
serviceCollection.AddSingleton<NavmeshIpc>();
serviceCollection.AddSingleton<LifestreamIpc>();
_windowSystem.AddWindow(new DebugWindow(pluginInterface, _movementController, _questController, _gameFunctions,
clientState, framework, targetManager, _gameUiController, _configuration));
serviceCollection.AddSingleton<MovementController>();
serviceCollection.AddSingleton<QuestRegistry>();
serviceCollection.AddSingleton<QuestController>();
serviceCollection.AddSingleton<GameUiController>();
serviceCollection.AddSingleton<NavigationShortcutController>();
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_framework.Update += FrameworkUpdate;
_commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand));
serviceCollection.AddSingleton<DebugWindow>();
serviceCollection.AddSingleton<DalamudInitializer>();
_framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(), TimeSpan.FromMilliseconds(200));
_serviceProvider = serviceCollection.BuildServiceProvider();
_serviceProvider.GetRequiredService<QuestRegistry>().Reload();
_serviceProvider.GetRequiredService<DebugWindow>();
_serviceProvider.GetRequiredService<DalamudInitializer>();
}
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();
}
}

View File

@ -18,9 +18,10 @@ using Questionable.Model.V1;
namespace Questionable.Windows;
internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposable
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly WindowSystem _windowSystem;
private readonly MovementController _movementController;
private readonly QuestController _questController;
private readonly GameFunctions _gameFunctions;
@ -30,12 +31,14 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
private readonly GameUiController _gameUiController;
private readonly Configuration _configuration;
public DebugWindow(DalamudPluginInterface pluginInterface, MovementController movementController,
QuestController questController, GameFunctions gameFunctions, IClientState clientState, IFramework framework,
ITargetManager targetManager, GameUiController gameUiController, Configuration configuration)
public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
MovementController movementController, QuestController questController, GameFunctions gameFunctions,
IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
Configuration configuration)
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
{
_pluginInterface = pluginInterface;
_windowSystem = windowSystem;
_movementController = movementController;
_questController = questController;
_gameFunctions = gameFunctions;
@ -51,6 +54,8 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
MinimumSize = new Vector2(200, 30),
MaximumSize = default
};
_windowSystem.AddWindow(this);
}
public WindowConfig WindowConfig => _configuration.DebugWindowConfig;
@ -128,7 +133,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
ImGui.Separator();
ImGui.Text(
$"Current TerritoryId: {_clientState.TerritoryType}, Flying: {(_gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType) ? "Yes" : "No")}");
$"Current TerritoryId: {_clientState.TerritoryType}, Flying: {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "Yes" : "No")}");
var q = _gameFunctions.GetCurrentQuest();
ImGui.Text($"Current Quest: {q.CurrentQuest} → {q.Sequence}");
@ -169,7 +174,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
if (ImGui.Button("Move to Target"))
{
_movementController.NavigateTo(EMovementType.DebugWindow, _targetManager.Target.DataId,
_targetManager.Target.Position, _gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType),
_targetManager.Target.Position, _gameFunctions.IsFlyingUnlockedInCurrentZone(),
true);
}
}
@ -234,7 +239,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
map->FlagMapMarker.TerritoryId != _clientState.TerritoryType);
if (ImGui.Button("Move to Flag"))
_gameFunctions.ExecuteCommand(
$"/vnav {(_gameFunctions.IsFlyingUnlocked(_clientState.TerritoryType) ? "flyflag" : "moveflag")}");
$"/vnav {(_gameFunctions.IsFlyingUnlockedInCurrentZone() ? "flyflag" : "moveflag")}");
ImGui.EndDisabled();
ImGui.SameLine();
@ -251,4 +256,9 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig
TimeSpan.FromMilliseconds(200));
}
}
public void Dispose()
{
_windowSystem.RemoveWindow(this);
}
}

View File

@ -2,12 +2,36 @@
"version": 1,
"dependencies": {
"net8.0-windows7.0": {
"Dalamud.Extensions.MicrosoftLogging": {
"type": "Direct",
"requested": "[3.0.0, )",
"resolved": "3.0.0",
"contentHash": "jWK3r/cZUXN8H9vHf78gEzeRmMk4YAbCUYzLcTqUAcega8unUiFGwYy+iOjVYJ9urnr9r+hk+vBi1y9wyv+e7Q==",
"dependencies": {
"Microsoft.Extensions.Logging": "8.0.0"
}
},
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.12, )",
"resolved": "2.1.12",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
},
"JetBrains.Annotations": {
"type": "Direct",
"requested": "[2023.3.0, )",
"resolved": "2023.3.0",
"contentHash": "PHfnvdBUdGaTVG9bR/GEfxgTwWM0Z97Y6X3710wiljELBISipSfF5okn/vz+C2gfO+ihoEyVPjaJwn8ZalVukA=="
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"System.Text.Json": {
"type": "Direct",
"requested": "[8.0.3, )",
@ -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",