forked from liza/Questionable
Rewrite logic, all quest steps can be executed automatically now
This commit is contained in:
parent
78357dc288
commit
81e849abc3
@ -34,6 +34,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sequence": 2,
|
||||
"Steps": [
|
||||
{
|
||||
"DataId": 2012185,
|
||||
"Position": {
|
||||
"X": -5.416992,
|
||||
"Y": -49.05786,
|
||||
"Z": -269.24548
|
||||
},
|
||||
"TerritoryId": 959,
|
||||
"InteractionType": "WaitForManualProgress",
|
||||
"Comment": "Follow Urianger"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Sequence": 3,
|
||||
"Steps": [
|
||||
@ -45,7 +61,7 @@
|
||||
"Z": -269.24548
|
||||
},
|
||||
"TerritoryId": 959,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"InteractionType": "WaitForManualProgress",
|
||||
"Comment": "Follow Urianger"
|
||||
}
|
||||
]
|
||||
|
@ -362,8 +362,8 @@ internal sealed class GameUiController : IDisposable
|
||||
|
||||
_logger.LogInformation("Using warp {Id}, {Prompt}", entry.RowId, excelPrompt);
|
||||
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
|
||||
if (increaseStepCount)
|
||||
_questController.IncreaseStepCount();
|
||||
//if (increaseStepCount)
|
||||
//_questController.IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ internal sealed class MovementController : IDisposable
|
||||
public bool IsPathRunning => _navmeshIpc.IsPathRunning;
|
||||
public bool IsPathfinding => _pathfindTask is { IsCompleted: false };
|
||||
public DestinationData? Destination { get; private set; }
|
||||
public DateTime MovementStartedAt { get; private set; } = DateTime.MaxValue;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
@ -53,7 +54,14 @@ internal sealed class MovementController : IDisposable
|
||||
if (_pathfindTask.IsCompletedSuccessfully)
|
||||
{
|
||||
_logger.LogInformation("Pathfinding complete, route: [{Route}]",
|
||||
string.Join(" → ", _pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
|
||||
string.Join(" → ",
|
||||
_pathfindTask.Result.Select(x => x.ToString("G", CultureInfo.InvariantCulture))));
|
||||
|
||||
if (_pathfindTask.Result.Count == 0)
|
||||
{
|
||||
ResetPathfinding();
|
||||
throw new PathfindingFailedException();
|
||||
}
|
||||
|
||||
var navPoints = _pathfindTask.Result.Skip(1).ToList();
|
||||
Vector3 start = _clientState.LocalPlayer?.Position ?? navPoints[0];
|
||||
@ -90,12 +98,15 @@ internal sealed class MovementController : IDisposable
|
||||
}
|
||||
|
||||
_navmeshIpc.MoveTo(navPoints, Destination.IsFlying);
|
||||
MovementStartedAt = DateTime.Now;
|
||||
|
||||
ResetPathfinding();
|
||||
}
|
||||
else if (_pathfindTask.IsCompleted)
|
||||
{
|
||||
_logger.LogWarning("Unable to complete pathfinding task");
|
||||
ResetPathfinding();
|
||||
throw new PathfindingFailedException();
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +167,8 @@ internal sealed class MovementController : IDisposable
|
||||
return pointOnFloor != null && Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f;
|
||||
}
|
||||
|
||||
private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance)
|
||||
private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
|
||||
float? stopDistance)
|
||||
{
|
||||
ResetPathfinding();
|
||||
|
||||
@ -164,9 +176,11 @@ internal sealed class MovementController : IDisposable
|
||||
_gameFunctions.ExecuteCommand("/automove off");
|
||||
|
||||
Destination = new DestinationData(dataId, to, stopDistance ?? (DefaultStopDistance - 0.2f), fly, sprint);
|
||||
MovementStartedAt = DateTime.MaxValue;
|
||||
}
|
||||
|
||||
public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null)
|
||||
public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint,
|
||||
float? stopDistance = null)
|
||||
{
|
||||
fly |= _condition[ConditionFlag.Diving];
|
||||
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance);
|
||||
@ -178,13 +192,15 @@ internal sealed class MovementController : IDisposable
|
||||
_navmeshIpc.Pathfind(_clientState.LocalPlayer!.Position, to, fly, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint, float? stopDistance)
|
||||
public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint,
|
||||
float? stopDistance)
|
||||
{
|
||||
fly |= _condition[ConditionFlag.Diving];
|
||||
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance);
|
||||
|
||||
_logger.LogInformation("Moving to {Destination}", Destination);
|
||||
_navmeshIpc.MoveTo(to, fly);
|
||||
MovementStartedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
public void ResetPathfinding()
|
||||
@ -219,5 +235,27 @@ internal sealed class MovementController : IDisposable
|
||||
Stop();
|
||||
}
|
||||
|
||||
public sealed record DestinationData(uint? DataId, Vector3 Position, float StopDistance, bool IsFlying, bool CanSprint);
|
||||
public sealed record DestinationData(
|
||||
uint? DataId,
|
||||
Vector3 Position,
|
||||
float StopDistance,
|
||||
bool IsFlying,
|
||||
bool CanSprint);
|
||||
|
||||
public sealed class PathfindingFailedException : Exception
|
||||
{
|
||||
public PathfindingFailedException()
|
||||
{
|
||||
}
|
||||
|
||||
public PathfindingFailedException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public PathfindingFailedException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
using Questionable.Model;
|
||||
@ -20,30 +26,30 @@ internal sealed class QuestController
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
private readonly MovementController _movementController;
|
||||
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 QuestRegistry _questRegistry;
|
||||
private readonly IKeyState _keyState;
|
||||
private readonly IReadOnlyList<ITaskFactory> _taskFactories;
|
||||
|
||||
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)
|
||||
private readonly Queue<ITask> _taskQueue = new();
|
||||
private ITask? _currentTask;
|
||||
private bool _automatic;
|
||||
|
||||
public QuestController(
|
||||
IClientState clientState,
|
||||
GameFunctions gameFunctions,
|
||||
MovementController movementController,
|
||||
ILogger<QuestController> logger,
|
||||
QuestRegistry questRegistry,
|
||||
IKeyState keyState,
|
||||
IEnumerable<ITaskFactory> taskFactories)
|
||||
{
|
||||
_clientState = clientState;
|
||||
_gameFunctions = gameFunctions;
|
||||
_movementController = movementController;
|
||||
_logger = logger;
|
||||
_condition = condition;
|
||||
_chatGui = chatGui;
|
||||
_framework = framework;
|
||||
_aetheryteData = aetheryteData;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
_territoryData = territoryData;
|
||||
_questRegistry = questRegistry;
|
||||
_keyState = keyState;
|
||||
_taskFactories = taskFactories.ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
|
||||
@ -60,6 +66,22 @@ internal sealed class QuestController
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
UpdateCurrentQuest();
|
||||
|
||||
if (_keyState[VirtualKey.ESCAPE])
|
||||
{
|
||||
Stop();
|
||||
_movementController.Stop();
|
||||
}
|
||||
|
||||
if (CurrentQuest != null && CurrentQuest.Quest.Data.TerritoryBlacklist.Contains(_clientState.TerritoryType))
|
||||
return;
|
||||
|
||||
UpdateCurrentTask();
|
||||
}
|
||||
|
||||
private void UpdateCurrentQuest()
|
||||
{
|
||||
DebugState = null;
|
||||
|
||||
@ -67,28 +89,38 @@ internal sealed class QuestController
|
||||
if (currentQuestId == 0)
|
||||
{
|
||||
if (CurrentQuest != null)
|
||||
{
|
||||
_logger.LogInformation("No current quest, resetting data");
|
||||
CurrentQuest = null;
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else if (CurrentQuest == null || CurrentQuest.Quest.QuestId != currentQuestId)
|
||||
{
|
||||
if (_questRegistry.TryGetQuest(currentQuestId, out var quest))
|
||||
{
|
||||
_logger.LogInformation("New quest: {QuestName}", quest.Name);
|
||||
CurrentQuest = new QuestProgress(quest, currentSequence, 0);
|
||||
}
|
||||
else if (CurrentQuest != null)
|
||||
{
|
||||
_logger.LogInformation("No active quest anymore? Not sure what happened...");
|
||||
CurrentQuest = null;
|
||||
}
|
||||
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentQuest == null)
|
||||
{
|
||||
DebugState = "No quest active";
|
||||
Comment = null;
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
|
||||
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
|
||||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
|
||||
_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
|
||||
_condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57])
|
||||
if (_gameFunctions.IsOccupied())
|
||||
{
|
||||
DebugState = "Occupied";
|
||||
return;
|
||||
@ -106,14 +138,22 @@ internal sealed class QuestController
|
||||
}
|
||||
|
||||
if (CurrentQuest.Sequence != currentSequence)
|
||||
{
|
||||
CurrentQuest = CurrentQuest with { Sequence = currentSequence, Step = 0 };
|
||||
|
||||
bool automatic = _automatic;
|
||||
Stop();
|
||||
if (automatic)
|
||||
ExecuteNextStep(true);
|
||||
}
|
||||
|
||||
var q = CurrentQuest.Quest;
|
||||
var sequence = q.FindSequence(CurrentQuest.Sequence);
|
||||
if (sequence == null)
|
||||
{
|
||||
DebugState = "Sequence not found";
|
||||
Comment = null;
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -121,6 +161,7 @@ internal sealed class QuestController
|
||||
{
|
||||
DebugState = "Step completed";
|
||||
Comment = null;
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -128,6 +169,7 @@ internal sealed class QuestController
|
||||
{
|
||||
DebugState = "Step not found";
|
||||
Comment = null;
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -152,7 +194,7 @@ internal sealed class QuestController
|
||||
return (seq, seq.Steps[CurrentQuest.Step]);
|
||||
}
|
||||
|
||||
public void IncreaseStepCount()
|
||||
public void IncreaseStepCount(bool shouldContinue = false)
|
||||
{
|
||||
(QuestSequence? seq, QuestStep? step) = GetNextStep();
|
||||
if (CurrentQuest == null || seq == null || step == null)
|
||||
@ -161,6 +203,7 @@ internal sealed class QuestController
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Increasing step count from {CurrentValue}", CurrentQuest.Step);
|
||||
if (CurrentQuest.Step + 1 < seq.Steps.Count)
|
||||
{
|
||||
CurrentQuest = CurrentQuest with
|
||||
@ -177,6 +220,10 @@ internal sealed class QuestController
|
||||
StepProgress = new()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (shouldContinue && _automatic)
|
||||
ExecuteNextStep(true);
|
||||
}
|
||||
|
||||
public void IncreaseDialogueChoicesSelected()
|
||||
@ -196,12 +243,114 @@ internal sealed class QuestController
|
||||
}
|
||||
};
|
||||
|
||||
/* TODO Is this required?
|
||||
if (CurrentQuest.StepProgress.DialogueChoicesSelected >= step.DialogueChoices.Count)
|
||||
IncreaseStepCount();
|
||||
*/
|
||||
}
|
||||
|
||||
public unsafe void ExecuteNextStep()
|
||||
public void Stop()
|
||||
{
|
||||
_currentTask = null;
|
||||
|
||||
if (_taskQueue.Count > 0)
|
||||
_taskQueue.Clear();
|
||||
|
||||
// reset task queue
|
||||
_automatic = false;
|
||||
}
|
||||
|
||||
private void UpdateCurrentTask()
|
||||
{
|
||||
if (_gameFunctions.IsOccupied())
|
||||
return;
|
||||
|
||||
if (_currentTask == null)
|
||||
{
|
||||
if (_taskQueue.TryDequeue(out ITask? upcomingTask))
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting task {TaskName}", upcomingTask.ToString());
|
||||
if (upcomingTask.Start())
|
||||
{
|
||||
_currentTask = upcomingTask;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogTrace("Task {TaskName} was skipped", upcomingTask.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to start task {TaskName}", upcomingTask.ToString());
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
ETaskResult result;
|
||||
try
|
||||
{
|
||||
result = _currentTask.Update();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Failed to update task {TaskName}", _currentTask.ToString());
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case ETaskResult.StillRunning:
|
||||
return;
|
||||
|
||||
case ETaskResult.SkipRemainingTasksForStep:
|
||||
_logger.LogInformation("Result: {Result}, skipping remaining tasks for step", result);
|
||||
_currentTask = null;
|
||||
|
||||
while (_taskQueue.TryDequeue(out ITask? nextTask))
|
||||
{
|
||||
if (nextTask is ILastTask)
|
||||
{
|
||||
_currentTask = nextTask;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
case ETaskResult.TaskComplete:
|
||||
_logger.LogInformation("Result: {Result}, remaining tasks: {RemainingTaskCount}", result,
|
||||
_taskQueue.Count);
|
||||
_currentTask = null;
|
||||
|
||||
// handled in next update
|
||||
return;
|
||||
|
||||
case ETaskResult.NextStep:
|
||||
_logger.LogInformation("Result: {Result}", result);
|
||||
IncreaseStepCount(true);
|
||||
return;
|
||||
|
||||
case ETaskResult.End:
|
||||
_logger.LogInformation("Result: {Result}", result);
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void ExecuteNextStep(bool automatic)
|
||||
{
|
||||
Stop();
|
||||
_automatic = automatic;
|
||||
|
||||
(QuestSequence? seq, QuestStep? step) = GetNextStep();
|
||||
if (CurrentQuest == null || seq == null || step == null)
|
||||
{
|
||||
@ -209,440 +358,40 @@ internal sealed class QuestController
|
||||
return;
|
||||
}
|
||||
|
||||
if (step.Disabled)
|
||||
var newTasks = _taskFactories
|
||||
.SelectMany(x =>
|
||||
{
|
||||
IList<ITask> tasks = x.CreateAllTasks(CurrentQuest.Quest, seq, step).ToList();
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
string factoryName = x.GetType().FullName ?? x.GetType().Name;
|
||||
if (factoryName.Contains('.', StringComparison.Ordinal))
|
||||
factoryName = factoryName[(factoryName.LastIndexOf('.') + 1)..];
|
||||
|
||||
_logger.LogTrace("Factory {FactoryName} created Task {TaskNames}",
|
||||
factoryName, string.Join(", ", tasks.Select(y => y.ToString())));
|
||||
}
|
||||
|
||||
return tasks;
|
||||
})
|
||||
.ToList();
|
||||
if (newTasks.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Skipping step, as it is disabled");
|
||||
IncreaseStepCount();
|
||||
_logger.LogInformation("Nothing to execute for step?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CurrentQuest.StepProgress.AetheryteShortcutUsed && step.AetheryteShortcut != null)
|
||||
{
|
||||
bool skipTeleport = false;
|
||||
ushort territoryType = _clientState.TerritoryType;
|
||||
if (step.TerritoryId == territoryType)
|
||||
{
|
||||
Vector3 pos = _clientState.LocalPlayer!.Position;
|
||||
if (_aetheryteData.CalculateDistance(pos, territoryType, step.AetheryteShortcut.Value) < 11 ||
|
||||
(step.AethernetShortcut != null &&
|
||||
(_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.From) < 20 ||
|
||||
_aetheryteData.CalculateDistance(pos, territoryType, step.AethernetShortcut.To) < 20)))
|
||||
{
|
||||
_logger.LogInformation("Skipping aetheryte teleport");
|
||||
skipTeleport = true;
|
||||
}
|
||||
}
|
||||
foreach (var task in newTasks)
|
||||
_taskQueue.Enqueue(task);
|
||||
}
|
||||
|
||||
if (skipTeleport)
|
||||
{
|
||||
_logger.LogInformation("Marking aetheryte shortcut as used");
|
||||
CurrentQuest = CurrentQuest with
|
||||
{
|
||||
StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_gameFunctions.IsAetheryteUnlocked(step.AetheryteShortcut.Value))
|
||||
{
|
||||
_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))
|
||||
{
|
||||
_logger.LogInformation("Travelling via aetheryte...");
|
||||
CurrentQuest = CurrentQuest with
|
||||
{
|
||||
StepProgress = CurrentQuest.StepProgress with { AetheryteShortcutUsed = true }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Unable to teleport to aetheryte");
|
||||
_chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
|
||||
}
|
||||
public IList<string> GetRemainingTaskNames() =>
|
||||
_taskQueue.Select(x => x.ToString() ?? "?").ToList();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!step.SkipIf.Contains(ESkipCondition.Never))
|
||||
{
|
||||
_logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", step.SkipIf));
|
||||
|
||||
if (step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
|
||||
_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
|
||||
{
|
||||
_logger.LogInformation("Skipping step, as flying is unlocked");
|
||||
IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
|
||||
!_gameFunctions.IsFlyingUnlocked(step.TerritoryId))
|
||||
{
|
||||
_logger.LogInformation("Skipping step, as flying is locked");
|
||||
IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step is
|
||||
{
|
||||
DataId: not null,
|
||||
InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
|
||||
} &&
|
||||
_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
|
||||
{
|
||||
_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))
|
||||
{
|
||||
_logger.LogInformation("Skipping step, as current is unlocked");
|
||||
IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
|
||||
QuestWork? questWork = _gameFunctions.GetQuestEx(CurrentQuest.Quest.QuestId);
|
||||
if (questWork != null && step.MatchesQuestVariables(questWork.Value))
|
||||
{
|
||||
_logger.LogInformation("Skipping step, as quest variables match");
|
||||
IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CurrentQuest.StepProgress.AethernetShortcutUsed && step.AethernetShortcut != null)
|
||||
{
|
||||
if (_gameFunctions.IsAetheryteUnlocked(step.AethernetShortcut.From) &&
|
||||
_gameFunctions.IsAetheryteUnlocked(step.AethernetShortcut.To))
|
||||
{
|
||||
EAetheryteLocation from = step.AethernetShortcut.From;
|
||||
EAetheryteLocation to = step.AethernetShortcut.To;
|
||||
ushort territoryType = _clientState.TerritoryType;
|
||||
Vector3 playerPosition = _clientState.LocalPlayer!.Position;
|
||||
|
||||
// closer to the source
|
||||
if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) <
|
||||
_aetheryteData.CalculateDistance(playerPosition, territoryType, to))
|
||||
{
|
||||
if (_aetheryteData.CalculateDistance(playerPosition, territoryType, from) < 11)
|
||||
{
|
||||
_logger.LogInformation("Using lifestream to teleport to {Destination}", to);
|
||||
_lifestreamIpc.Teleport(to);
|
||||
CurrentQuest = CurrentQuest with
|
||||
{
|
||||
StepProgress = CurrentQuest.StepProgress with { AethernetShortcutUsed = true }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Moving to aethernet shortcut");
|
||||
_movementController.NavigateTo(EMovementType.Quest, (uint)from, _aetheryteData.Locations[from],
|
||||
false, true,
|
||||
AetheryteConverter.IsLargeAetheryte(from) ? 10.9f : 6.9f);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
_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)
|
||||
{
|
||||
// we assume whatever e.g. interaction, walkto etc. we have will trigger the zone transition
|
||||
_logger.LogInformation("Zone transition, skipping rest of step");
|
||||
IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null &&
|
||||
(_clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <=
|
||||
(step.JumpDestination.StopDistance ?? 1f))
|
||||
{
|
||||
_logger.LogInformation("We're at the jump destination, skipping movement");
|
||||
}
|
||||
else if (step.Position != null)
|
||||
{
|
||||
float distance;
|
||||
if (step.InteractionType == EInteractionType.WalkTo)
|
||||
distance = step.StopDistance ?? 0.25f;
|
||||
else
|
||||
distance = step.StopDistance ?? MovementController.DefaultStopDistance;
|
||||
|
||||
var position = _clientState.LocalPlayer?.Position ?? new Vector3();
|
||||
float actualDistance = (position - step.Position.Value).Length();
|
||||
|
||||
if (step.Mount == true && !_gameFunctions.HasStatusPreventingSprintOrMount())
|
||||
{
|
||||
_logger.LogInformation("Step explicitly wants a mount, trying to mount...");
|
||||
if (!_condition[ConditionFlag.Mounted] && !_condition[ConditionFlag.InCombat] &&
|
||||
_territoryData.CanUseMount(_clientState.TerritoryType))
|
||||
{
|
||||
_gameFunctions.Mount();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (step.Mount == false)
|
||||
{
|
||||
_logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
|
||||
if (_condition[ConditionFlag.Mounted])
|
||||
{
|
||||
_gameFunctions.Unmount();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!step.DisableNavmesh)
|
||||
{
|
||||
if (step.Mount != false && actualDistance > 30f && !_condition[ConditionFlag.Mounted] &&
|
||||
!_condition[ConditionFlag.InCombat] && _territoryData.CanUseMount(_clientState.TerritoryType) &&
|
||||
!_gameFunctions.HasStatusPreventingSprintOrMount())
|
||||
{
|
||||
_gameFunctions.Mount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (actualDistance > distance)
|
||||
{
|
||||
_movementController.NavigateTo(EMovementType.Quest, step.DataId, step.Position.Value,
|
||||
fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
|
||||
sprint: step.Sprint != false,
|
||||
stopDistance: distance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// navmesh won't move close enough
|
||||
if (actualDistance > distance)
|
||||
{
|
||||
_movementController.NavigateTo(EMovementType.Quest, step.DataId, [step.Position.Value],
|
||||
fly: step.Fly == true && _gameFunctions.IsFlyingUnlockedInCurrentZone(),
|
||||
sprint: step.Sprint != false,
|
||||
stopDistance: distance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (step.DataId != null && step.StopDistance != null)
|
||||
{
|
||||
GameObject? gameObject = _gameFunctions.FindObjectByDataId(step.DataId.Value);
|
||||
if (gameObject == null ||
|
||||
(gameObject.Position - _clientState.LocalPlayer!.Position).Length() > step.StopDistance)
|
||||
{
|
||||
_logger.LogWarning("Object not found or too far away, no position so we can't move");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Running logic for {InteractionType}", step.InteractionType);
|
||||
switch (step.InteractionType)
|
||||
{
|
||||
case EInteractionType.Interact:
|
||||
if (step.DataId != null)
|
||||
{
|
||||
GameObject? gameObject = _gameFunctions.FindObjectByDataId(step.DataId.Value);
|
||||
if (gameObject == null)
|
||||
{
|
||||
_logger.LogWarning("No game object with dataId {DataId}", step.DataId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gameObject.IsTargetable && _condition[ConditionFlag.Mounted])
|
||||
{
|
||||
_gameFunctions.Unmount();
|
||||
return;
|
||||
}
|
||||
|
||||
_gameFunctions.InteractWith(step.DataId.Value);
|
||||
|
||||
// if we have any dialogue, that is handled in GameUiController
|
||||
if (step.DialogueChoices.Count == 0)
|
||||
IncreaseStepCount();
|
||||
}
|
||||
else
|
||||
_logger.LogWarning("Not interacting on current step, DataId is null");
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.AttuneAethernetShard:
|
||||
if (step.DataId != null)
|
||||
{
|
||||
if (!_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
|
||||
_gameFunctions.InteractWith(step.DataId.Value);
|
||||
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.AttuneAetheryte:
|
||||
if (step.DataId != null)
|
||||
{
|
||||
if (!_gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)step.DataId.Value))
|
||||
_gameFunctions.InteractWith(step.DataId.Value);
|
||||
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.AttuneAetherCurrent:
|
||||
if (step.DataId != null)
|
||||
{
|
||||
_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);
|
||||
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.WalkTo:
|
||||
IncreaseStepCount();
|
||||
break;
|
||||
|
||||
case EInteractionType.UseItem:
|
||||
if (_gameFunctions.Unmount())
|
||||
return;
|
||||
|
||||
if (step is { DataId: not null, ItemId: not null, GroundTarget: true })
|
||||
{
|
||||
_gameFunctions.UseItemOnGround(step.DataId.Value, step.ItemId.Value);
|
||||
IncreaseStepCount();
|
||||
}
|
||||
else if (step is { DataId: not null, ItemId: not null })
|
||||
{
|
||||
_gameFunctions.UseItem(step.DataId.Value, step.ItemId.Value);
|
||||
IncreaseStepCount();
|
||||
}
|
||||
else if (step.ItemId != null)
|
||||
{
|
||||
_gameFunctions.UseItem(step.ItemId.Value);
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.Combat:
|
||||
if (_gameFunctions.Unmount())
|
||||
return;
|
||||
|
||||
if (step.EnemySpawnType != null)
|
||||
{
|
||||
if (step is { DataId: not null, EnemySpawnType: EEnemySpawnType.AfterInteraction })
|
||||
_gameFunctions.InteractWith(step.DataId.Value);
|
||||
|
||||
if (step is { DataId: not null, ItemId: not null, EnemySpawnType: EEnemySpawnType.AfterItemUse })
|
||||
_gameFunctions.UseItem(step.DataId.Value, step.ItemId.Value);
|
||||
|
||||
// next sequence should trigger automatically
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.Emote:
|
||||
if (step is { DataId: not null, Emote: not null })
|
||||
{
|
||||
_gameFunctions.UseEmote(step.DataId.Value, step.Emote.Value);
|
||||
IncreaseStepCount();
|
||||
}
|
||||
else if (step.Emote != null)
|
||||
{
|
||||
_gameFunctions.UseEmote(step.Emote.Value);
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.Say:
|
||||
if (_condition[ConditionFlag.Mounted])
|
||||
{
|
||||
_gameFunctions.Unmount();
|
||||
return;
|
||||
}
|
||||
|
||||
if (step.ChatMessage != null)
|
||||
{
|
||||
string? excelString = _gameFunctions.GetDialogueText(CurrentQuest.Quest,
|
||||
step.ChatMessage.ExcelSheet,
|
||||
step.ChatMessage.Key);
|
||||
if (excelString == null)
|
||||
return;
|
||||
|
||||
_gameFunctions.ExecuteCommand($"/say {excelString}");
|
||||
IncreaseStepCount();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.WaitForObjectAtPosition:
|
||||
if (step is { DataId: not null, Position: not null } &&
|
||||
!_gameFunctions.IsObjectAtPosition(step.DataId.Value, step.Position.Value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IncreaseStepCount();
|
||||
break;
|
||||
|
||||
case EInteractionType.WaitForManualProgress:
|
||||
// something needs to be done manually, the next sequence will be picked up automatically
|
||||
break;
|
||||
|
||||
case EInteractionType.Duty:
|
||||
if (step.ContentFinderConditionId != null)
|
||||
_gameFunctions.OpenDutyFinder(step.ContentFinderConditionId.Value);
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.SinglePlayerDuty:
|
||||
// TODO: Disable YesAlready, interact with NPC to open dialog, restore YesAlready
|
||||
// TODO: also implement check for territory blacklist
|
||||
break;
|
||||
|
||||
case EInteractionType.Jump:
|
||||
if (step.JumpDestination != null && !_condition[ConditionFlag.Jumping])
|
||||
{
|
||||
float stopDistance = step.JumpDestination.StopDistance ?? 1f;
|
||||
if ((_clientState.LocalPlayer!.Position - step.JumpDestination.Position).Length() <= stopDistance)
|
||||
IncreaseStepCount();
|
||||
else
|
||||
{
|
||||
_movementController.NavigateTo(EMovementType.Quest, step.DataId,
|
||||
[step.JumpDestination.Position], false, false,
|
||||
step.JumpDestination.StopDistance ?? stopDistance);
|
||||
_framework.RunOnTick(() => ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2),
|
||||
TimeSpan.FromSeconds(step.JumpDestination.DelaySeconds ?? 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EInteractionType.ShouldBeAJump:
|
||||
case EInteractionType.Instruction:
|
||||
// Need to manually forward
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.LogWarning("Action '{InteractionType}' is not implemented", step.InteractionType);
|
||||
break;
|
||||
}
|
||||
public string ToStatString()
|
||||
{
|
||||
return _currentTask == null ? $"- (+{_taskQueue.Count})" : $"{_currentTask} (+{_taskQueue.Count})";
|
||||
}
|
||||
|
||||
public sealed record QuestProgress(
|
||||
@ -657,8 +406,7 @@ internal sealed class QuestController
|
||||
}
|
||||
}
|
||||
|
||||
// TODO is this still required?
|
||||
public sealed record StepProgress(
|
||||
bool AetheryteShortcutUsed = false,
|
||||
bool AethernetShortcutUsed = false,
|
||||
int DialogueChoicesSelected = 0);
|
||||
}
|
||||
|
120
Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs
Normal file
120
Questionable/Controller/Steps/BaseFactory/AethernetShortcut.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
using Questionable.Model.V1.Converter;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class AethernetShortcut
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.AethernetShortcut == null)
|
||||
return null;
|
||||
|
||||
return serviceProvider.GetRequiredService<UseAethernetShortcut>()
|
||||
.With(step.AethernetShortcut.From, step.AethernetShortcut.To);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UseAethernetShortcut(
|
||||
ILogger<UseAethernetShortcut> logger,
|
||||
GameFunctions gameFunctions,
|
||||
IClientState clientState,
|
||||
AetheryteData aetheryteData,
|
||||
LifestreamIpc lifestreamIpc,
|
||||
MovementController movementController) : ITask
|
||||
{
|
||||
private bool _moving;
|
||||
private bool _teleported;
|
||||
|
||||
public EAetheryteLocation From { get; set; }
|
||||
public EAetheryteLocation To { get; set; }
|
||||
|
||||
public ITask With(EAetheryteLocation from, EAetheryteLocation to)
|
||||
{
|
||||
From = from;
|
||||
To = to;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (gameFunctions.IsAetheryteUnlocked(From) &&
|
||||
gameFunctions.IsAetheryteUnlocked(To))
|
||||
{
|
||||
ushort territoryType = clientState.TerritoryType;
|
||||
Vector3 playerPosition = clientState.LocalPlayer!.Position;
|
||||
|
||||
// closer to the source
|
||||
if (aetheryteData.CalculateDistance(playerPosition, territoryType, From) <
|
||||
aetheryteData.CalculateDistance(playerPosition, territoryType, To))
|
||||
{
|
||||
if (aetheryteData.CalculateDistance(playerPosition, territoryType, From) < 11)
|
||||
{
|
||||
logger.LogInformation("Using lifestream to teleport to {Destination}", To);
|
||||
lifestreamIpc.Teleport(To);
|
||||
|
||||
_teleported = true;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Moving to aethernet shortcut");
|
||||
_moving = true;
|
||||
movementController.NavigateTo(EMovementType.Quest, (uint)From, aetheryteData.Locations[From],
|
||||
false, true,
|
||||
AetheryteConverter.IsLargeAetheryte(From) ? 10.9f : 6.9f);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
logger.LogWarning(
|
||||
"Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), walking manually",
|
||||
From, To);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (_moving)
|
||||
{
|
||||
var movementStartedAt = movementController.MovementStartedAt;
|
||||
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
if (!movementController.IsPathfinding && !movementController.IsPathRunning)
|
||||
_moving = false;
|
||||
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
if (!_teleported)
|
||||
{
|
||||
logger.LogInformation("Using lifestream to teleport to {Destination}", To);
|
||||
lifestreamIpc.Teleport(To);
|
||||
|
||||
_teleported = true;
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
if (aetheryteData.CalculateDistance(clientState.LocalPlayer?.Position ?? Vector3.Zero,
|
||||
clientState.TerritoryType, To) > 11)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
|
||||
public override string ToString() => $"UseAethernet({From} -> {To})";
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Data;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class AetheryteShortcut
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider, GameFunctions gameFunctions) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.AetheryteShortcut == null)
|
||||
return [];
|
||||
|
||||
var task = serviceProvider.GetRequiredService<UseAetheryteShortcut>()
|
||||
.With(step, step.AetheryteShortcut.Value);
|
||||
return [new WaitConditionTask(gameFunctions.CanTeleport, "CanTeleport"), task];
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
internal sealed class UseAetheryteShortcut(
|
||||
ILogger<UseAetheryteShortcut> logger,
|
||||
GameFunctions gameFunctions,
|
||||
IClientState clientState,
|
||||
IChatGui chatGui,
|
||||
AetheryteData aetheryteData) : ITask
|
||||
{
|
||||
private DateTime _continueAt;
|
||||
|
||||
public QuestStep Step { get; set; } = null!;
|
||||
public EAetheryteLocation TargetAetheryte { get; set; }
|
||||
|
||||
public ITask With(QuestStep step, EAetheryteLocation targetAetheryte)
|
||||
{
|
||||
Step = step;
|
||||
TargetAetheryte = targetAetheryte;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
_continueAt = DateTime.Now.AddSeconds(8);
|
||||
ushort territoryType = clientState.TerritoryType;
|
||||
if (Step.TerritoryId == territoryType)
|
||||
{
|
||||
Vector3 pos = clientState.LocalPlayer!.Position;
|
||||
if (aetheryteData.CalculateDistance(pos, territoryType, TargetAetheryte) < 11 ||
|
||||
(Step.AethernetShortcut != null &&
|
||||
(aetheryteData.CalculateDistance(pos, territoryType, Step.AethernetShortcut.From) < 20 ||
|
||||
aetheryteData.CalculateDistance(pos, territoryType, Step.AethernetShortcut.To) < 20)))
|
||||
{
|
||||
logger.LogInformation("Skipping aetheryte teleport");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!gameFunctions.IsAetheryteUnlocked(TargetAetheryte))
|
||||
{
|
||||
chatGui.Print($"[Questionable] Aetheryte {TargetAetheryte} is not unlocked.");
|
||||
throw new TaskException("Aetheryte is not unlocked");
|
||||
}
|
||||
else if (gameFunctions.TeleportAetheryte(TargetAetheryte))
|
||||
{
|
||||
logger.LogInformation("Travelling via aetheryte...");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
chatGui.Print("[Questionable] Unable to teleport to aetheryte.");
|
||||
throw new TaskException("Unable to teleport to aetheryte");
|
||||
}
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
|
||||
if (DateTime.Now >= _continueAt && clientState.TerritoryType == Step.TerritoryId)
|
||||
return ETaskResult.TaskComplete;
|
||||
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
public override string ToString() => $"UseAetheryte({TargetAetheryte})";
|
||||
}
|
||||
}
|
168
Questionable/Controller/Steps/BaseFactory/Move.cs
Normal file
168
Questionable/Controller/Steps/BaseFactory/Move.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class Move
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.Position != null)
|
||||
{
|
||||
var builder = serviceProvider.GetRequiredService<MoveBuilder>();
|
||||
builder.Step = step;
|
||||
builder.Destination = step.Position.Value;
|
||||
return builder.Build();
|
||||
}
|
||||
else if (step is { DataId: not null, StopDistance: not null })
|
||||
{
|
||||
var task = serviceProvider.GetRequiredService<ExpectToBeNearDataId>();
|
||||
task.DataId = step.DataId.Value;
|
||||
task.StopDistance = step.StopDistance.Value;
|
||||
return [task];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
internal sealed class MoveBuilder(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<MoveBuilder> logger,
|
||||
GameFunctions gameFunctions,
|
||||
IClientState clientState,
|
||||
MovementController movementController)
|
||||
{
|
||||
public QuestStep Step { get; set; } = null!;
|
||||
public Vector3 Destination { get; set; }
|
||||
|
||||
public IEnumerable<ITask> Build()
|
||||
{
|
||||
if (Step.InteractionType == EInteractionType.Jump && Step.JumpDestination != null &&
|
||||
(clientState.LocalPlayer!.Position - Step.JumpDestination.Position).Length() <=
|
||||
(Step.JumpDestination.StopDistance ?? 1f))
|
||||
{
|
||||
logger.LogInformation("We're at the jump destination, skipping movement");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new WaitConditionTask(() => clientState.TerritoryType == Step.TerritoryId, $"Wait(territory: {Step.TerritoryId}");
|
||||
yield return new WaitConditionTask(() => movementController.IsNavmeshReady, "Wait(navmesh ready)");
|
||||
|
||||
float distance;
|
||||
if (Step.InteractionType == EInteractionType.WalkTo)
|
||||
distance = Step.StopDistance ?? 0.25f;
|
||||
else
|
||||
distance = Step.StopDistance ?? MovementController.DefaultStopDistance;
|
||||
|
||||
var position = clientState.LocalPlayer?.Position ?? new Vector3();
|
||||
float actualDistance = (position - Destination).Length();
|
||||
|
||||
if (Step.Mount == true)
|
||||
yield return serviceProvider.GetRequiredService<MountTask>().With(Step.TerritoryId);
|
||||
else if (Step.Mount == false)
|
||||
yield return serviceProvider.GetRequiredService<UnmountTask>();
|
||||
|
||||
if (!Step.DisableNavmesh)
|
||||
{
|
||||
if (Step.Mount == null && actualDistance > 30f)
|
||||
yield return serviceProvider.GetRequiredService<MountTask>().With(Step.TerritoryId);
|
||||
|
||||
if (actualDistance > distance)
|
||||
{
|
||||
yield return serviceProvider.GetRequiredService<MoveInternal>()
|
||||
.With(Destination, m =>
|
||||
{
|
||||
m.NavigateTo(EMovementType.Quest, Step.DataId, Destination,
|
||||
fly: Step.Fly == true && gameFunctions.IsFlyingUnlockedInCurrentZone(),
|
||||
sprint: Step.Sprint != false,
|
||||
stopDistance: distance);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// navmesh won't move close enough
|
||||
if (actualDistance > distance)
|
||||
{
|
||||
yield return serviceProvider.GetRequiredService<MoveInternal>()
|
||||
.With(Destination, m =>
|
||||
{
|
||||
m.NavigateTo(EMovementType.Quest, Step.DataId, [Destination],
|
||||
fly: Step.Fly == true && gameFunctions.IsFlyingUnlockedInCurrentZone(),
|
||||
sprint: Step.Sprint != false,
|
||||
stopDistance: distance);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MoveInternal(MovementController movementController, ILogger<MoveInternal> logger) : ITask
|
||||
{
|
||||
public Action<MovementController> StartAction { get; set; } = null!;
|
||||
public Vector3 Destination { get; set; }
|
||||
|
||||
public ITask With(Vector3 destination, Action<MovementController> startAction)
|
||||
{
|
||||
Destination = destination;
|
||||
StartAction = startAction;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
logger.LogInformation("Moving to {Destination}", Destination.ToString("G", CultureInfo.InvariantCulture));
|
||||
StartAction(movementController);
|
||||
return true;
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (movementController.IsPathfinding || movementController.IsPathRunning)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
DateTime movementStartedAt = movementController.MovementStartedAt;
|
||||
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
|
||||
public override string ToString() => $"MoveTo({Destination.ToString("G", CultureInfo.InvariantCulture)})";
|
||||
}
|
||||
|
||||
internal sealed class ExpectToBeNearDataId(GameFunctions gameFunctions, IClientState clientState) : ITask
|
||||
{
|
||||
public uint DataId { get; set; }
|
||||
public float StopDistance { get; set; }
|
||||
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
|
||||
if (gameObject == null ||
|
||||
(gameObject.Position - clientState.LocalPlayer!.Position).Length() > StopDistance)
|
||||
{
|
||||
throw new TaskException("Object not found or too far away, no position so we can't move");
|
||||
}
|
||||
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
}
|
||||
}
|
92
Questionable/Controller/Steps/BaseFactory/SkipCondition.cs
Normal file
92
Questionable/Controller/Steps/BaseFactory/SkipCondition.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class SkipCondition
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.SkipIf.Count == 0)
|
||||
return null;
|
||||
|
||||
if (step.SkipIf.Contains(ESkipCondition.Never))
|
||||
return null;
|
||||
|
||||
return serviceProvider.GetRequiredService<CheckTask>()
|
||||
.With(step, quest.QuestId);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CheckTask(
|
||||
ILogger<CheckTask> logger,
|
||||
GameFunctions gameFunctions) : ITask
|
||||
{
|
||||
public QuestStep Step { get; set; } = null!;
|
||||
public ushort QuestId { get; set; }
|
||||
|
||||
public ITask With(QuestStep step, ushort questId)
|
||||
{
|
||||
Step = step;
|
||||
QuestId = questId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", Step.SkipIf));
|
||||
|
||||
if (Step.SkipIf.Contains(ESkipCondition.FlyingUnlocked) &&
|
||||
gameFunctions.IsFlyingUnlocked(Step.TerritoryId))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as flying is unlocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Step.SkipIf.Contains(ESkipCondition.FlyingLocked) &&
|
||||
!gameFunctions.IsFlyingUnlocked(Step.TerritoryId))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as flying is locked");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Step is
|
||||
{
|
||||
DataId: not null,
|
||||
InteractionType: EInteractionType.AttuneAetheryte or EInteractionType.AttuneAethernetShard
|
||||
} &&
|
||||
gameFunctions.IsAetheryteUnlocked((EAetheryteLocation)Step.DataId.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as aetheryte/aethernet shard is unlocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Step is { DataId: not null, InteractionType: EInteractionType.AttuneAetherCurrent } &&
|
||||
gameFunctions.IsAetherCurrentUnlocked(Step.DataId.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as current is unlocked");
|
||||
return true;
|
||||
}
|
||||
|
||||
QuestWork? questWork = gameFunctions.GetQuestEx(QuestId);
|
||||
if (questWork != null && Step.MatchesQuestVariables(questWork.Value))
|
||||
{
|
||||
logger.LogInformation("Skipping step, as quest variables match");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ETaskResult Update() => ETaskResult.SkipRemainingTasksForStep;
|
||||
|
||||
public override string ToString() => $"CheckSkip({string.Join(", ", Step.SkipIf)})";
|
||||
}
|
||||
}
|
34
Questionable/Controller/Steps/BaseFactory/StepDisabled.cs
Normal file
34
Questionable/Controller/Steps/BaseFactory/StepDisabled.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class StepDisabled
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (!step.Disabled)
|
||||
return null;
|
||||
|
||||
return serviceProvider.GetRequiredService<Task>();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Task(ILogger<Task> logger) : ITask
|
||||
{
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
logger.LogInformation("Skipping step, as it is disabled");
|
||||
return ETaskResult.SkipRemainingTasksForStep;
|
||||
}
|
||||
|
||||
public override string ToString() => "StepDisabled";
|
||||
}
|
||||
}
|
134
Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs
Normal file
134
Questionable/Controller/Steps/BaseFactory/WaitAtEnd.cs
Normal file
@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class WaitAtEnd
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.CompletionQuestVariablesFlags.Count == 6)
|
||||
{
|
||||
var task = serviceProvider.GetRequiredService<WaitForCompletionFlags>()
|
||||
.With(quest, step);
|
||||
var delay = serviceProvider.GetRequiredService<WaitDelay>();
|
||||
return [task, delay, new NextStep()];
|
||||
}
|
||||
|
||||
switch (step.InteractionType)
|
||||
{
|
||||
case EInteractionType.Combat:
|
||||
case EInteractionType.WaitForManualProgress:
|
||||
case EInteractionType.ShouldBeAJump:
|
||||
case EInteractionType.Instruction:
|
||||
return [serviceProvider.GetRequiredService<WaitNextStepOrSequence>()];
|
||||
|
||||
case EInteractionType.Duty:
|
||||
case EInteractionType.SinglePlayerDuty:
|
||||
return [new EndAutomation()];
|
||||
|
||||
case EInteractionType.WalkTo:
|
||||
case EInteractionType.Jump:
|
||||
// no need to wait if we're just moving around
|
||||
return [new NextStep()];
|
||||
|
||||
case EInteractionType.WaitForObjectAtPosition:
|
||||
return [serviceProvider.GetRequiredService<WaitObjectAtPosition>(), new NextStep()];
|
||||
|
||||
default:
|
||||
return [serviceProvider.GetRequiredService<WaitDelay>(), new NextStep()];
|
||||
}
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
internal sealed class WaitDelay() : AbstractDelayedTask(TimeSpan.FromSeconds(1))
|
||||
{
|
||||
protected override bool StartInternal() => true;
|
||||
|
||||
public override string ToString() => $"Wait(seconds: {Delay.TotalSeconds})";
|
||||
}
|
||||
|
||||
internal sealed class WaitNextStepOrSequence : ITask
|
||||
{
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update() => ETaskResult.StillRunning;
|
||||
|
||||
public override string ToString() => "Wait(next step or sequence)";
|
||||
}
|
||||
|
||||
internal sealed class WaitForCompletionFlags(GameFunctions gameFunctions) : ITask
|
||||
{
|
||||
public Quest Quest { get; set; } = null!;
|
||||
public QuestStep Step { get; set; } = null!;
|
||||
public IList<short?> Flags { get; set; } = null!;
|
||||
|
||||
public ITask With(Quest quest, QuestStep step)
|
||||
{
|
||||
Quest = quest;
|
||||
Step = step;
|
||||
Flags = step.CompletionQuestVariablesFlags;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
QuestWork? questWork = gameFunctions.GetQuestEx(Quest.QuestId);
|
||||
return questWork != null && Step.MatchesQuestVariables(questWork.Value)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
public override string ToString() =>
|
||||
$"WaitCF({string.Join(", ", Flags.Select(x => x?.ToString(CultureInfo.InvariantCulture) ?? "-"))})";
|
||||
}
|
||||
|
||||
internal sealed class WaitObjectAtPosition(GameFunctions gameFunctions) : ITask
|
||||
{
|
||||
public uint DataId { get; set; }
|
||||
public Vector3 Destination { get; set; }
|
||||
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update() =>
|
||||
gameFunctions.IsObjectAtPosition(DataId, Destination)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
|
||||
public override string ToString() =>
|
||||
$"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)})";
|
||||
}
|
||||
|
||||
internal sealed class NextStep : ILastTask
|
||||
{
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update() => ETaskResult.NextStep;
|
||||
|
||||
public override string ToString() => "Next Step";
|
||||
}
|
||||
|
||||
internal sealed class EndAutomation : ILastTask
|
||||
{
|
||||
public bool Start() => true;
|
||||
|
||||
public ETaskResult Update() => ETaskResult.End;
|
||||
|
||||
public override string ToString() => "End automation";
|
||||
}
|
||||
}
|
36
Questionable/Controller/Steps/BaseFactory/ZoneChange.cs
Normal file
36
Questionable/Controller/Steps/BaseFactory/ZoneChange.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseFactory;
|
||||
|
||||
internal static class ZoneChange
|
||||
{
|
||||
internal sealed class Factory : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WaitForZone : ITask
|
||||
{
|
||||
/* TODO: Unsure when this would evne be needed again, this should probably be moved to AFTER walkTo/interacting
|
||||
|
||||
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
|
||||
_logger.LogInformation("Zone transition, skipping rest of step");
|
||||
IncreaseStepCount();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
public bool Start() => throw new NotImplementedException();
|
||||
|
||||
public ETaskResult Update() => throw new NotImplementedException();
|
||||
|
||||
public override string ToString() => "WaitForZone";
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseTasks;
|
||||
|
||||
internal abstract class AbstractDelayedTask : ITask
|
||||
{
|
||||
protected readonly TimeSpan Delay;
|
||||
private DateTime _continueAt;
|
||||
|
||||
protected AbstractDelayedTask(TimeSpan delay)
|
||||
{
|
||||
Delay = delay;
|
||||
}
|
||||
|
||||
protected AbstractDelayedTask()
|
||||
: this(TimeSpan.FromSeconds(5))
|
||||
{
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
_continueAt = DateTime.Now.Add(Delay);
|
||||
return StartInternal();
|
||||
}
|
||||
|
||||
protected abstract bool StartInternal();
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (_continueAt >= DateTime.Now)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
return UpdateInternal();
|
||||
}
|
||||
|
||||
protected virtual ETaskResult UpdateInternal() => ETaskResult.TaskComplete;
|
||||
}
|
61
Questionable/Controller/Steps/BaseTasks/MountTask.cs
Normal file
61
Questionable/Controller/Steps/BaseTasks/MountTask.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseTasks;
|
||||
|
||||
internal sealed class MountTask(
|
||||
GameFunctions gameFunctions,
|
||||
ICondition condition,
|
||||
TerritoryData territoryData,
|
||||
ILogger<MountTask> logger) : ITask
|
||||
{
|
||||
private ushort _territoryId;
|
||||
private bool _mountTriggered;
|
||||
|
||||
public ITask With(ushort territoryId)
|
||||
{
|
||||
_territoryId = territoryId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (condition[ConditionFlag.Mounted])
|
||||
return false;
|
||||
|
||||
if (!territoryData.CanUseMount(_territoryId))
|
||||
{
|
||||
logger.LogInformation("Can't use mount in current territory {Id}", _territoryId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gameFunctions.HasStatusPreventingSprintOrMount())
|
||||
return false;
|
||||
|
||||
logger.LogInformation("Step wants a mount, trying to mount in territory {Id}...", _territoryId);
|
||||
if (!condition[ConditionFlag.InCombat])
|
||||
{
|
||||
_mountTriggered = gameFunctions.Mount();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (!_mountTriggered)
|
||||
{
|
||||
_mountTriggered = gameFunctions.Mount();
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
return condition[ConditionFlag.Mounted]
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
public override string ToString() => "Mount";
|
||||
}
|
36
Questionable/Controller/Steps/BaseTasks/UnmountTask.cs
Normal file
36
Questionable/Controller/Steps/BaseTasks/UnmountTask.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseTasks;
|
||||
|
||||
internal sealed class UnmountTask(ICondition condition, ILogger<UnmountTask> logger, GameFunctions gameFunctions)
|
||||
: ITask
|
||||
{
|
||||
private bool _unmountTriggered;
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!condition[ConditionFlag.Mounted])
|
||||
return false;
|
||||
|
||||
logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
|
||||
_unmountTriggered = gameFunctions.Unmount();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (!_unmountTriggered)
|
||||
{
|
||||
_unmountTriggered = gameFunctions.Unmount();
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
return condition[ConditionFlag.Mounted]
|
||||
? ETaskResult.StillRunning
|
||||
: ETaskResult.TaskComplete;
|
||||
}
|
||||
|
||||
public override string ToString() => "Unmount";
|
||||
}
|
12
Questionable/Controller/Steps/BaseTasks/WaitConditionTask.cs
Normal file
12
Questionable/Controller/Steps/BaseTasks/WaitConditionTask.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Questionable.Controller.Steps.BaseTasks;
|
||||
|
||||
internal sealed class WaitConditionTask(Func<bool> predicate, string description) : ITask
|
||||
{
|
||||
public bool Start() => predicate();
|
||||
|
||||
public ETaskResult Update() => predicate() ? ETaskResult.TaskComplete : ETaskResult.StillRunning;
|
||||
|
||||
public override string ToString() => description;
|
||||
}
|
16
Questionable/Controller/Steps/ETaskResult.cs
Normal file
16
Questionable/Controller/Steps/ETaskResult.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Questionable.Controller.Steps;
|
||||
|
||||
internal enum ETaskResult
|
||||
{
|
||||
StillRunning,
|
||||
|
||||
TaskComplete,
|
||||
|
||||
/// <summary>
|
||||
/// This step is complete, regardless of what any other following tasks would do.
|
||||
/// </summary>
|
||||
SkipRemainingTasksForStep,
|
||||
|
||||
NextStep,
|
||||
End,
|
||||
}
|
6
Questionable/Controller/Steps/ILastTask.cs
Normal file
6
Questionable/Controller/Steps/ILastTask.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Questionable.Controller.Steps;
|
||||
|
||||
internal interface ILastTask : ITask
|
||||
{
|
||||
|
||||
}
|
11
Questionable/Controller/Steps/ITask.cs
Normal file
11
Questionable/Controller/Steps/ITask.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Questionable.Controller.Steps;
|
||||
|
||||
internal interface ITask
|
||||
{
|
||||
bool Start();
|
||||
|
||||
ETaskResult Update();
|
||||
}
|
17
Questionable/Controller/Steps/ITaskFactory.cs
Normal file
17
Questionable/Controller/Steps/ITaskFactory.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps;
|
||||
|
||||
internal interface ITaskFactory
|
||||
{
|
||||
ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step);
|
||||
|
||||
IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
var task = CreateTask(quest, sequence, step);
|
||||
if (task != null)
|
||||
yield return task;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class AetherCurrent
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.AttuneAetherCurrent)
|
||||
return null;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
ArgumentNullException.ThrowIfNull(step.AetherCurrentId);
|
||||
|
||||
return serviceProvider.GetRequiredService<DoAttune>()
|
||||
.With(step.DataId.Value, step.AetherCurrentId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
|
||||
{
|
||||
public uint DataId { get; set; }
|
||||
public uint AetherCurrentId { get; set; }
|
||||
|
||||
public ITask With(uint dataId, uint aetherCurrentId)
|
||||
{
|
||||
DataId = dataId;
|
||||
AetherCurrentId = aetherCurrentId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!gameFunctions.IsAetherCurrentUnlocked(AetherCurrentId))
|
||||
{
|
||||
logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", AetherCurrentId,
|
||||
DataId);
|
||||
gameFunctions.InteractWith(DataId);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", AetherCurrentId, DataId);
|
||||
return false;
|
||||
}
|
||||
|
||||
public ETaskResult Update() =>
|
||||
gameFunctions.IsAetherCurrentUnlocked(AetherCurrentId)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
|
||||
public override string ToString() => $"AttuneAetherCurrent({AetherCurrentId})";
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class AethernetShard
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.AttuneAethernetShard)
|
||||
return null;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
|
||||
return serviceProvider.GetRequiredService<DoAttune>()
|
||||
.With((EAetheryteLocation)step.DataId);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
|
||||
{
|
||||
public EAetheryteLocation AetheryteLocation { get; set; }
|
||||
|
||||
public ITask? With(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
AetheryteLocation = aetheryteLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
|
||||
{
|
||||
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", AetheryteLocation);
|
||||
gameFunctions.InteractWith((uint)AetheryteLocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", AetheryteLocation);
|
||||
return false;
|
||||
}
|
||||
|
||||
public ETaskResult Update() =>
|
||||
gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
|
||||
public override string ToString() => $"AttuneAethernetShard({AetheryteLocation})";
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Aetheryte
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.AttuneAetheryte)
|
||||
return null;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
|
||||
return serviceProvider.GetRequiredService<DoAttune>()
|
||||
.With((EAetheryteLocation)step.DataId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : ITask
|
||||
{
|
||||
public EAetheryteLocation AetheryteLocation { get; set; }
|
||||
|
||||
public ITask With(EAetheryteLocation aetheryteLocation)
|
||||
{
|
||||
AetheryteLocation = aetheryteLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!gameFunctions.IsAetheryteUnlocked(AetheryteLocation))
|
||||
{
|
||||
logger.LogInformation("Attuning to aetheryte {Aetheryte}", AetheryteLocation);
|
||||
gameFunctions.InteractWith((uint)AetheryteLocation);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.LogInformation("Already attuned to aetheryte {Aetheryte}", AetheryteLocation);
|
||||
return false;
|
||||
}
|
||||
|
||||
public ETaskResult Update() =>
|
||||
gameFunctions.IsAetheryteUnlocked(AetheryteLocation)
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
|
||||
public override string ToString() => $"AttuneAetheryte({AetheryteLocation})";
|
||||
}
|
||||
}
|
47
Questionable/Controller/Steps/InteractionFactory/Combat.cs
Normal file
47
Questionable/Controller/Steps/InteractionFactory/Combat.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Combat
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.Combat)
|
||||
return [];
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.EnemySpawnType);
|
||||
|
||||
var unmount = serviceProvider.GetRequiredService<UnmountTask>();
|
||||
if (step.EnemySpawnType == EEnemySpawnType.AfterInteraction)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
|
||||
var task = serviceProvider.GetRequiredService<Interact.DoInteract>()
|
||||
.With(step.DataId.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
else if (step.EnemySpawnType == EEnemySpawnType.AfterItemUse)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
ArgumentNullException.ThrowIfNull(step.ItemId);
|
||||
|
||||
var task = serviceProvider.GetRequiredService<UseItem.UseOnObject>()
|
||||
.With(step.DataId.Value, step.ItemId.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
else
|
||||
// automatically triggered when entering area, i.e. only unmount
|
||||
return [unmount];
|
||||
}
|
||||
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
}
|
44
Questionable/Controller/Steps/InteractionFactory/Duty.cs
Normal file
44
Questionable/Controller/Steps/InteractionFactory/Duty.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Duty
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.Duty)
|
||||
return null;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
|
||||
|
||||
return serviceProvider.GetRequiredService<OpenDutyFinder>()
|
||||
.With(step.ContentFinderConditionId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class OpenDutyFinder(GameFunctions gameFunctions) : ITask
|
||||
{
|
||||
public uint ContentFinderConditionId { get; set; }
|
||||
|
||||
public ITask With(uint contentFinderConditionId)
|
||||
{
|
||||
ContentFinderConditionId = contentFinderConditionId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
gameFunctions.OpenDutyFinder(ContentFinderConditionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public ETaskResult Update() => ETaskResult.TaskComplete;
|
||||
|
||||
public override string ToString() => $"OpenDutyFinder({ContentFinderConditionId})";
|
||||
}
|
||||
}
|
77
Questionable/Controller/Steps/InteractionFactory/Emote.cs
Normal file
77
Questionable/Controller/Steps/InteractionFactory/Emote.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Emote
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.Emote)
|
||||
return [];
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.Emote);
|
||||
|
||||
var unmount = serviceProvider.GetRequiredService<UnmountTask>();
|
||||
if (step.DataId != null)
|
||||
{
|
||||
var task = serviceProvider.GetRequiredService<UseOnObject>().With(step.Emote.Value, step.DataId.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = serviceProvider.GetRequiredService<Use>().With(step.Emote.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask
|
||||
{
|
||||
public EEmote Emote { get; set; }
|
||||
public uint DataId { get; set; }
|
||||
|
||||
public ITask With(EEmote emote, uint dataId)
|
||||
{
|
||||
Emote = emote;
|
||||
DataId = dataId;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override bool StartInternal()
|
||||
{
|
||||
gameFunctions.UseEmote(DataId, Emote);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Emote({Emote} on {DataId})";
|
||||
}
|
||||
|
||||
internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask
|
||||
{
|
||||
public EEmote Emote { get; set; }
|
||||
|
||||
public ITask With(EEmote emote)
|
||||
{
|
||||
Emote = emote;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override bool StartInternal()
|
||||
{
|
||||
gameFunctions.UseEmote(Emote);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Emote({Emote})";
|
||||
}
|
||||
}
|
99
Questionable/Controller/Steps/InteractionFactory/Interact.cs
Normal file
99
Questionable/Controller/Steps/InteractionFactory/Interact.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Interact
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.Interact)
|
||||
return null;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
|
||||
return serviceProvider.GetRequiredService<DoInteract>().With(step.DataId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoInteract(GameFunctions gameFunctions, ICondition condition, ILogger<DoInteract> logger)
|
||||
: ITask
|
||||
{
|
||||
private bool _interacted;
|
||||
private DateTime _continueAt = DateTime.MinValue;
|
||||
|
||||
private uint DataId { get; set; }
|
||||
|
||||
public ITask With(uint dataId)
|
||||
{
|
||||
DataId = dataId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
|
||||
if (gameObject == null)
|
||||
{
|
||||
logger.LogWarning("No game object with dataId {DataId}", DataId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// this is only relevant for followers on quests
|
||||
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted])
|
||||
{
|
||||
gameFunctions.Unmount();
|
||||
_continueAt = DateTime.Now.AddSeconds(0.5);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gameObject.IsTargetable && HasAnyMarker(gameObject))
|
||||
{
|
||||
_interacted = gameFunctions.InteractWith(DataId);
|
||||
_continueAt = DateTime.Now.AddSeconds(0.5);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (DateTime.Now <= _continueAt)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
if (!_interacted)
|
||||
{
|
||||
GameObject? gameObject = gameFunctions.FindObjectByDataId(DataId);
|
||||
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
_interacted = gameFunctions.InteractWith(DataId);
|
||||
_continueAt = DateTime.Now.AddSeconds(0.5);
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
|
||||
private unsafe bool HasAnyMarker(GameObject gameObject)
|
||||
{
|
||||
if (gameObject.ObjectKind != ObjectKind.EventNpc)
|
||||
return true;
|
||||
|
||||
var gameObjectStruct = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address;
|
||||
return gameObjectStruct->NamePlateIconId != 0;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Interact({DataId})";
|
||||
}
|
||||
}
|
77
Questionable/Controller/Steps/InteractionFactory/Jump.cs
Normal file
77
Questionable/Controller/Steps/InteractionFactory/Jump.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Jump
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.Jump)
|
||||
return null;
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.JumpDestination);
|
||||
|
||||
return serviceProvider.GetRequiredService<DoJump>()
|
||||
.With(step.DataId, step.JumpDestination, step.Comment);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class DoJump(
|
||||
MovementController movementController,
|
||||
IClientState clientState,
|
||||
IFramework framework) : ITask
|
||||
{
|
||||
public uint? DataId { get; set; }
|
||||
public JumpDestination JumpDestination { get; set; } = null!;
|
||||
public string? Comment { get; set; }
|
||||
|
||||
public ITask With(uint? dataId, JumpDestination jumpDestination, string? comment)
|
||||
{
|
||||
DataId = dataId;
|
||||
JumpDestination = jumpDestination;
|
||||
Comment = comment ?? string.Empty;
|
||||
return this;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
float stopDistance = JumpDestination.StopDistance ?? 1f;
|
||||
if ((clientState.LocalPlayer!.Position - JumpDestination.Position).Length() <= stopDistance)
|
||||
return false;
|
||||
|
||||
movementController.NavigateTo(EMovementType.Quest, DataId, [JumpDestination.Position], false, false,
|
||||
JumpDestination.StopDistance ?? stopDistance);
|
||||
framework.RunOnTick(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2);
|
||||
}
|
||||
},
|
||||
TimeSpan.FromSeconds(JumpDestination.DelaySeconds ?? 0.5f));
|
||||
return true;
|
||||
}
|
||||
|
||||
public ETaskResult Update()
|
||||
{
|
||||
if (movementController.IsPathfinding || movementController.IsPathRunning)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
DateTime movementStartedAt = movementController.MovementStartedAt;
|
||||
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2) >= DateTime.Now)
|
||||
return ETaskResult.StillRunning;
|
||||
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Jump({Comment})";
|
||||
}
|
||||
}
|
52
Questionable/Controller/Steps/InteractionFactory/Say.cs
Normal file
52
Questionable/Controller/Steps/InteractionFactory/Say.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class Say
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider, GameFunctions gameFunctions) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.Emote)
|
||||
return [];
|
||||
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.ChatMessage);
|
||||
|
||||
string? excelString = gameFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key);
|
||||
ArgumentNullException.ThrowIfNull(excelString);
|
||||
|
||||
var unmount = serviceProvider.GetRequiredService<UnmountTask>();
|
||||
var task = serviceProvider.GetRequiredService<UseChat>().With(excelString);
|
||||
return [unmount, task];
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
internal sealed class UseChat(GameFunctions gameFunctions) : AbstractDelayedTask
|
||||
{
|
||||
public string ChatMessage { get; set; } = null!;
|
||||
|
||||
public ITask With(string chatMessage)
|
||||
{
|
||||
ChatMessage = chatMessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override bool StartInternal()
|
||||
{
|
||||
gameFunctions.ExecuteCommand($"/say {ChatMessage}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Say({ChatMessage})";
|
||||
}
|
||||
}
|
109
Questionable/Controller/Steps/InteractionFactory/UseItem.cs
Normal file
109
Questionable/Controller/Steps/InteractionFactory/UseItem.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
|
||||
namespace Questionable.Controller.Steps.InteractionFactory;
|
||||
|
||||
internal static class UseItem
|
||||
{
|
||||
internal sealed class Factory(IServiceProvider serviceProvider) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.UseItem)
|
||||
return [];
|
||||
|
||||
ArgumentNullException.ThrowIfNull(step.ItemId);
|
||||
|
||||
var unmount = serviceProvider.GetRequiredService<UnmountTask>();
|
||||
if (step.GroundTarget == true)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(step.DataId);
|
||||
|
||||
var task = serviceProvider.GetRequiredService<UseOnGround>()
|
||||
.With(step.DataId.Value, step.ItemId.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
else if (step.DataId != null)
|
||||
{
|
||||
var task = serviceProvider.GetRequiredService<UseOnObject>()
|
||||
.With(step.DataId.Value, step.ItemId.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
else
|
||||
{
|
||||
var task = serviceProvider.GetRequiredService<Use>()
|
||||
.With(step.ItemId.Value);
|
||||
return [unmount, task];
|
||||
}
|
||||
}
|
||||
|
||||
public ITask CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
=> throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
|
||||
internal sealed class UseOnGround(GameFunctions gameFunctions) : AbstractDelayedTask
|
||||
{
|
||||
public uint DataId { get; set; }
|
||||
public uint ItemId { get; set; }
|
||||
|
||||
public ITask With(uint dataId, uint itemId)
|
||||
{
|
||||
DataId = dataId;
|
||||
ItemId = itemId;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override bool StartInternal()
|
||||
{
|
||||
gameFunctions.UseItemOnGround(DataId, ItemId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"UseItem({ItemId} on ground at {DataId})";
|
||||
}
|
||||
|
||||
internal sealed class UseOnObject(GameFunctions gameFunctions) : AbstractDelayedTask
|
||||
{
|
||||
public uint DataId { get; set; }
|
||||
public uint ItemId { get; set; }
|
||||
|
||||
public ITask With(uint dataId, uint itemId)
|
||||
{
|
||||
DataId = dataId;
|
||||
ItemId = itemId;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override bool StartInternal()
|
||||
{
|
||||
gameFunctions.UseItem(DataId, ItemId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"UseItem({ItemId} on {DataId})";
|
||||
}
|
||||
|
||||
internal sealed class Use(GameFunctions gameFunctions) : AbstractDelayedTask
|
||||
{
|
||||
public uint ItemId { get; set; }
|
||||
|
||||
public ITask With(uint itemId)
|
||||
{
|
||||
ItemId = itemId;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected override bool StartInternal()
|
||||
{
|
||||
gameFunctions.UseItem(ItemId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string ToString() => $"UseItem({ItemId})";
|
||||
}
|
||||
}
|
20
Questionable/Controller/Steps/TaskException.cs
Normal file
20
Questionable/Controller/Steps/TaskException.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Questionable.Controller.Steps;
|
||||
|
||||
public class TaskException : Exception
|
||||
{
|
||||
public TaskException()
|
||||
{
|
||||
}
|
||||
|
||||
public TaskException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public TaskException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
@ -47,7 +47,15 @@ internal sealed class DalamudInitializer : IDisposable
|
||||
{
|
||||
_questController.Update();
|
||||
_navigationShortcutController.HandleNavigationShortcut();
|
||||
_movementController.Update();
|
||||
|
||||
try
|
||||
{
|
||||
_movementController.Update();
|
||||
}
|
||||
catch (MovementController.PathfindingFailedException)
|
||||
{
|
||||
_questController.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessCommand(string command, string arguments)
|
||||
|
@ -20,6 +20,8 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LLib.GameUI;
|
||||
using Lumina.Excel.CustomSheets;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -53,11 +55,12 @@ internal sealed unsafe class GameFunctions
|
||||
private readonly ICondition _condition;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly ILogger<GameFunctions> _logger;
|
||||
|
||||
public GameFunctions(IDataManager dataManager, IObjectTable objectTable, ISigScanner sigScanner,
|
||||
ITargetManager targetManager, ICondition condition, IClientState clientState, QuestRegistry questRegistry,
|
||||
ILogger<GameFunctions> logger)
|
||||
IGameGui gameGui, ILogger<GameFunctions> logger)
|
||||
{
|
||||
_dataManager = dataManager;
|
||||
_objectTable = objectTable;
|
||||
@ -65,6 +68,7 @@ internal sealed unsafe class GameFunctions
|
||||
_condition = condition;
|
||||
_clientState = clientState;
|
||||
_questRegistry = questRegistry;
|
||||
_gameGui = gameGui;
|
||||
_logger = logger;
|
||||
_processChatBox =
|
||||
Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(sigScanner.ScanText(Signatures.SendChat));
|
||||
@ -166,6 +170,8 @@ internal sealed unsafe class GameFunctions
|
||||
public bool IsAetheryteUnlocked(EAetheryteLocation aetheryteLocation)
|
||||
=> IsAetheryteUnlocked((uint)aetheryteLocation, out _);
|
||||
|
||||
public bool CanTeleport() => ActionManager.Instance()->GetActionStatus(ActionType.Action, 5) == 0;
|
||||
|
||||
public bool TeleportAetheryte(uint aetheryteId)
|
||||
{
|
||||
var status = ActionManager.Instance()->GetActionStatus(ActionType.Action, 5);
|
||||
@ -342,7 +348,7 @@ internal sealed unsafe class GameFunctions
|
||||
return null;
|
||||
}
|
||||
|
||||
public void InteractWith(uint dataId)
|
||||
public bool InteractWith(uint dataId)
|
||||
{
|
||||
GameObject? gameObject = FindObjectByDataId(dataId);
|
||||
if (gameObject != null)
|
||||
@ -350,9 +356,12 @@ internal sealed unsafe class GameFunctions
|
||||
_logger.LogInformation("Setting target with {DataId} to {ObjectId}", dataId, gameObject.ObjectId);
|
||||
_targetManager.Target = gameObject;
|
||||
|
||||
TargetSystem.Instance()->InteractWithObject(
|
||||
ulong result = TargetSystem.Instance()->InteractWithObject(
|
||||
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address, false);
|
||||
return result != 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void UseItem(uint itemId)
|
||||
@ -422,46 +431,46 @@ internal sealed unsafe class GameFunctions
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Mount()
|
||||
public bool Mount()
|
||||
{
|
||||
if (!_condition[ConditionFlag.Mounted])
|
||||
if (_condition[ConditionFlag.Mounted])
|
||||
return true;
|
||||
|
||||
var playerState = PlayerState.Instance();
|
||||
if (playerState != null && playerState->IsMountUnlocked(71))
|
||||
{
|
||||
var playerState = PlayerState.Instance();
|
||||
if (playerState != null && playerState->IsMountUnlocked(71))
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
|
||||
{
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, 71) == 0)
|
||||
{
|
||||
_logger.LogInformation("Using SDS Fenrir as mount");
|
||||
ActionManager.Instance()->UseAction(ActionType.Mount, 71);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
|
||||
{
|
||||
_logger.LogInformation("Using mount roulette");
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
|
||||
}
|
||||
_logger.LogInformation("Using SDS Fenrir as mount");
|
||||
return ActionManager.Instance()->UseAction(ActionType.Mount, 71);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9) == 0)
|
||||
{
|
||||
_logger.LogInformation("Using mount roulette");
|
||||
return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Unmount()
|
||||
{
|
||||
if (_condition[ConditionFlag.Mounted])
|
||||
if (!_condition[ConditionFlag.Mounted])
|
||||
return false;
|
||||
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
|
||||
{
|
||||
if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23) == 0)
|
||||
{
|
||||
_logger.LogInformation("Unmounting...");
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
|
||||
}
|
||||
else
|
||||
_logger.LogWarning("Can't unmount right now?");
|
||||
|
||||
return true;
|
||||
_logger.LogInformation("Unmounting...");
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23);
|
||||
}
|
||||
else
|
||||
_logger.LogWarning("Can't unmount right now?");
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OpenDutyFinder(uint contentFinderConditionId)
|
||||
@ -508,4 +517,19 @@ internal sealed unsafe class GameFunctions
|
||||
var questRow = _dataManager.GetExcelSheet<ContentTalk>()!.GetRow(rowId);
|
||||
return questRow?.Text?.ToString();
|
||||
}
|
||||
|
||||
public bool IsOccupied()
|
||||
{
|
||||
if (_gameGui.TryGetAddonByName("FadeMiddle", out AtkUnitBase* fade) &&
|
||||
LAddon.IsAddonReady(fade) &&
|
||||
fade->IsVisible)
|
||||
return true;
|
||||
|
||||
return _condition[ConditionFlag.Occupied] || _condition[ConditionFlag.Occupied30] ||
|
||||
_condition[ConditionFlag.Occupied33] || _condition[ConditionFlag.Occupied38] ||
|
||||
_condition[ConditionFlag.Occupied39] || _condition[ConditionFlag.OccupiedInEvent] ||
|
||||
_condition[ConditionFlag.OccupiedInQuestEvent] || _condition[ConditionFlag.OccupiedInCutSceneEvent] ||
|
||||
_condition[ConditionFlag.Casting] || _condition[ConditionFlag.Unknown57] ||
|
||||
_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
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")]
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Version>0.5</Version>
|
||||
<Version>0.6</Version>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
@ -23,7 +23,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="3.0.0" />
|
||||
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1" />
|
||||
<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" />
|
||||
@ -59,6 +59,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LLib\LLib.csproj" />
|
||||
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj" Condition="'$(Configuration)' == 'Release'" />
|
||||
<ProjectReference Include="..\QuestPaths\QuestPaths.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -7,8 +7,13 @@ using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.BaseFactory;
|
||||
using Questionable.Controller.Steps.BaseTasks;
|
||||
using Questionable.Controller.Steps.InteractionFactory;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
using Questionable.Windows;
|
||||
@ -32,14 +37,15 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
ICondition condition,
|
||||
IChatGui chatGui,
|
||||
ICommandManager commandManager,
|
||||
IAddonLifecycle addonLifecycle)
|
||||
IAddonLifecycle addonLifecycle,
|
||||
IKeyState keyState)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pluginInterface);
|
||||
|
||||
ServiceCollection serviceCollection = new();
|
||||
serviceCollection.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Trace)
|
||||
.ClearProviders()
|
||||
.AddDalamudLogger(pluginLog));
|
||||
.AddDalamudLogger(pluginLog, t => t[(t.LastIndexOf('.') + 1)..]));
|
||||
serviceCollection.AddSingleton<IDalamudPlugin>(this);
|
||||
serviceCollection.AddSingleton(pluginInterface);
|
||||
serviceCollection.AddSingleton(clientState);
|
||||
@ -53,6 +59,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceCollection.AddSingleton(chatGui);
|
||||
serviceCollection.AddSingleton(commandManager);
|
||||
serviceCollection.AddSingleton(addonLifecycle);
|
||||
serviceCollection.AddSingleton(keyState);
|
||||
serviceCollection.AddSingleton(new WindowSystem(nameof(Questionable)));
|
||||
serviceCollection.AddSingleton((Configuration?)pluginInterface.GetPluginConfig() ?? new Configuration());
|
||||
|
||||
@ -62,6 +69,39 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceCollection.AddSingleton<NavmeshIpc>();
|
||||
serviceCollection.AddSingleton<LifestreamIpc>();
|
||||
|
||||
// individual tasks
|
||||
serviceCollection.AddTransient<MountTask>();
|
||||
serviceCollection.AddTransient<UnmountTask>();
|
||||
|
||||
// tasks with factories
|
||||
serviceCollection.AddTaskWithFactory<StepDisabled.Factory, StepDisabled.Task>();
|
||||
serviceCollection.AddTaskWithFactory<AetheryteShortcut.Factory, AetheryteShortcut.UseAetheryteShortcut>();
|
||||
serviceCollection.AddTaskWithFactory<SkipCondition.Factory, SkipCondition.CheckTask>();
|
||||
serviceCollection.AddTaskWithFactory<AethernetShortcut.Factory, AethernetShortcut.UseAethernetShortcut>();
|
||||
serviceCollection.AddTaskWithFactory<Move.Factory, Move.MoveInternal, Move.ExpectToBeNearDataId>();
|
||||
serviceCollection.AddTransient<Move.MoveBuilder>();
|
||||
|
||||
serviceCollection.AddTaskWithFactory<AetherCurrent.Factory, AetherCurrent.DoAttune>();
|
||||
serviceCollection.AddTaskWithFactory<AethernetShard.Factory, AethernetShard.DoAttune>();
|
||||
serviceCollection.AddTaskWithFactory<Aetheryte.Factory, Aetheryte.DoAttune>();
|
||||
serviceCollection.AddSingleton<ITaskFactory, Combat.Factory>();
|
||||
serviceCollection.AddTaskWithFactory<Duty.Factory, Duty.OpenDutyFinder>();
|
||||
serviceCollection.AddTaskWithFactory<Emote.Factory, Emote.UseOnObject, Emote.Use>();
|
||||
serviceCollection.AddTaskWithFactory<Interact.Factory, Interact.DoInteract>();
|
||||
serviceCollection.AddTaskWithFactory<Jump.Factory, Jump.DoJump>();
|
||||
serviceCollection.AddTaskWithFactory<Say.Factory, Say.UseChat>();
|
||||
serviceCollection.AddTaskWithFactory<UseItem.Factory, UseItem.UseOnGround, UseItem.UseOnObject, UseItem.Use>();
|
||||
|
||||
// TODO sort this in properly
|
||||
serviceCollection.AddTaskWithFactory<ZoneChange.Factory, ZoneChange.WaitForZone>();
|
||||
|
||||
serviceCollection
|
||||
.AddTaskWithFactory<WaitAtEnd.Factory,
|
||||
WaitAtEnd.WaitDelay,
|
||||
WaitAtEnd.WaitNextStepOrSequence,
|
||||
WaitAtEnd.WaitForCompletionFlags,
|
||||
WaitAtEnd.WaitObjectAtPosition>();
|
||||
|
||||
serviceCollection.AddSingleton<MovementController>();
|
||||
serviceCollection.AddSingleton<QuestRegistry>();
|
||||
serviceCollection.AddSingleton<QuestController>();
|
||||
|
84
Questionable/ServiceCollectionExtensions.cs
Normal file
84
Questionable/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Questionable.Controller.Steps;
|
||||
|
||||
namespace Questionable;
|
||||
|
||||
internal static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddTaskWithFactory<
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TFactory,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask>(
|
||||
this IServiceCollection serviceCollection)
|
||||
where TFactory : class, ITaskFactory
|
||||
where TTask : class, ITask
|
||||
{
|
||||
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
|
||||
serviceCollection.AddTransient<TTask>();
|
||||
}
|
||||
|
||||
public static void AddTaskWithFactory<
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TFactory,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask1,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask2>(
|
||||
this IServiceCollection serviceCollection)
|
||||
where TFactory : class, ITaskFactory
|
||||
where TTask1 : class, ITask
|
||||
where TTask2 : class, ITask
|
||||
{
|
||||
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
|
||||
serviceCollection.AddTransient<TTask1>();
|
||||
serviceCollection.AddTransient<TTask2>();
|
||||
}
|
||||
|
||||
public static void AddTaskWithFactory<
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TFactory,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask1,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask2,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask3>(
|
||||
this IServiceCollection serviceCollection)
|
||||
where TFactory : class, ITaskFactory
|
||||
where TTask1 : class, ITask
|
||||
where TTask2 : class, ITask
|
||||
where TTask3 : class, ITask
|
||||
{
|
||||
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
|
||||
serviceCollection.AddTransient<TTask1>();
|
||||
serviceCollection.AddTransient<TTask2>();
|
||||
serviceCollection.AddTransient<TTask3>();
|
||||
}
|
||||
|
||||
public static void AddTaskWithFactory<
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TFactory,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask1,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask2,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask3,
|
||||
[MeansImplicitUse(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
|
||||
TTask4>(
|
||||
this IServiceCollection serviceCollection)
|
||||
where TFactory : class, ITaskFactory
|
||||
where TTask1 : class, ITask
|
||||
where TTask2 : class, ITask
|
||||
where TTask3 : class, ITask
|
||||
where TTask4 : class, ITask
|
||||
{
|
||||
serviceCollection.AddSingleton<ITaskFactory, TFactory>();
|
||||
serviceCollection.AddTransient<TTask1>();
|
||||
serviceCollection.AddTransient<TTask2>();
|
||||
serviceCollection.AddTransient<TTask3>();
|
||||
serviceCollection.AddTransient<TTask4>();
|
||||
}
|
||||
}
|
@ -9,9 +9,11 @@ using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using ImGuiNET;
|
||||
using LLib.ImGui;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.V1;
|
||||
@ -30,11 +32,12 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
private readonly ITargetManager _targetManager;
|
||||
private readonly GameUiController _gameUiController;
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ILogger<DebugWindow> _logger;
|
||||
|
||||
public DebugWindow(DalamudPluginInterface pluginInterface, WindowSystem windowSystem,
|
||||
MovementController movementController, QuestController questController, GameFunctions gameFunctions,
|
||||
IClientState clientState, IFramework framework, ITargetManager targetManager, GameUiController gameUiController,
|
||||
Configuration configuration)
|
||||
Configuration configuration, ILogger<DebugWindow> logger)
|
||||
: base("Questionable", ImGuiWindowFlags.AlwaysAutoResize)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
@ -47,6 +50,7 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
_targetManager = targetManager;
|
||||
_gameUiController = gameUiController;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
|
||||
IsOpen = true;
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
@ -109,23 +113,36 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
ImGui.EndDisabled();
|
||||
ImGui.TextUnformatted(_questController.Comment ?? "--");
|
||||
|
||||
var nextStep = _questController.GetNextStep();
|
||||
ImGui.BeginDisabled(nextStep.Step == null);
|
||||
ImGui.Text(string.Create(CultureInfo.InvariantCulture,
|
||||
$"{nextStep.Step?.InteractionType} @ {nextStep.Step?.Position}"));
|
||||
//var nextStep = _questController.GetNextStep();
|
||||
//ImGui.BeginDisabled(nextStep.Step == null);
|
||||
ImGui.Text(_questController.ToStatString());
|
||||
//ImGui.EndDisabled();
|
||||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
|
||||
{
|
||||
_questController.ExecuteNextStep();
|
||||
_questController.ExecuteNextStep(true);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
|
||||
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.StepForward, "Step"))
|
||||
{
|
||||
_questController.IncreaseStepCount();
|
||||
_questController.ExecuteNextStep(false);
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop))
|
||||
{
|
||||
_movementController.Stop();
|
||||
_questController.Stop();
|
||||
}
|
||||
|
||||
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowCircleRight, "Skip"))
|
||||
{
|
||||
_questController.Stop();
|
||||
_questController.IncreaseStepCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
ImGui.Text("No active quest");
|
||||
@ -165,8 +182,10 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
ImGui.Separator();
|
||||
ImGui.Text(string.Create(CultureInfo.InvariantCulture,
|
||||
$"Target: {_targetManager.Target.Name} ({_targetManager.Target.ObjectKind}; {_targetManager.Target.DataId})"));
|
||||
|
||||
GameObject* gameObject = (GameObject*)_targetManager.Target.Address;
|
||||
ImGui.Text(string.Create(CultureInfo.InvariantCulture,
|
||||
$"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}, Y: {_targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y:F2}"));
|
||||
$"Distance: {(_targetManager.Target.Position - _clientState.LocalPlayer.Position).Length():F2}, Y: {_targetManager.Target.Position.Y - _clientState.LocalPlayer.Position.Y:F2} | QM: {gameObject->NamePlateIconId}"));
|
||||
|
||||
ImGui.BeginDisabled(!_movementController.IsNavmeshReady);
|
||||
if (!_movementController.IsPathfinding)
|
||||
@ -189,8 +208,9 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Interact"))
|
||||
{
|
||||
TargetSystem.Instance()->InteractWithObject(
|
||||
ulong result = TargetSystem.Instance()->InteractWithObject(
|
||||
(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)_targetManager.Target.Address, false);
|
||||
_logger.LogInformation("XXXXX Interaction Result: {Result}", result);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
@ -246,7 +266,11 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
|
||||
ImGui.BeginDisabled(!_movementController.IsPathRunning);
|
||||
if (ImGui.Button("Stop Nav"))
|
||||
{
|
||||
_movementController.Stop();
|
||||
_questController.Stop();
|
||||
}
|
||||
|
||||
ImGui.EndDisabled();
|
||||
|
||||
if (ImGui.Button("Reload Data"))
|
||||
@ -255,6 +279,16 @@ internal sealed class DebugWindow : LWindow, IPersistableWindowConfig, IDisposab
|
||||
_framework.RunOnTick(() => _gameUiController.HandleCurrentDialogueChoices(),
|
||||
TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
var remainingTasks = _questController.GetRemainingTaskNames();
|
||||
if (remainingTasks.Count > 0)
|
||||
{
|
||||
ImGui.Separator();
|
||||
ImGui.BeginDisabled();
|
||||
foreach (var task in remainingTasks)
|
||||
ImGui.TextUnformatted(task);
|
||||
ImGui.EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -4,9 +4,9 @@
|
||||
"net8.0-windows7.0": {
|
||||
"Dalamud.Extensions.MicrosoftLogging": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.0.0, )",
|
||||
"resolved": "3.0.0",
|
||||
"contentHash": "jWK3r/cZUXN8H9vHf78gEzeRmMk4YAbCUYzLcTqUAcega8unUiFGwYy+iOjVYJ9urnr9r+hk+vBi1y9wyv+e7Q==",
|
||||
"requested": "[4.0.1, )",
|
||||
"resolved": "4.0.1",
|
||||
"contentHash": "fMEL2ajtF/30SBBku7vMyG0yye5eHN/A9fgT//1CEjUth/Wz2CYco5Ehye21T8KN1IuAPwoqJuu49rB71j+8ug==",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.Logging": "8.0.0"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user