punish v6.8.18.0
This commit is contained in:
commit
060278c1b7
317 changed files with 554155 additions and 0 deletions
492
Questionable/Questionable.Controller/CombatController.cs
Normal file
492
Questionable/Questionable.Controller/CombatController.cs
Normal file
|
@ -0,0 +1,492 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.CombatModules;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Utils;
|
||||
using Questionable.Functions;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class CombatController : IDisposable
|
||||
{
|
||||
private sealed class CurrentFight
|
||||
{
|
||||
public required ICombatModule Module { get; init; }
|
||||
|
||||
public required CombatData Data { get; init; }
|
||||
|
||||
public required DateTime LastDistanceCheck { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CombatData
|
||||
{
|
||||
public required ElementId? ElementId { get; init; }
|
||||
|
||||
public required int Sequence { get; init; }
|
||||
|
||||
public required IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; init; }
|
||||
|
||||
public required EEnemySpawnType SpawnType { get; init; }
|
||||
|
||||
public required List<uint> KillEnemyDataIds { get; init; }
|
||||
|
||||
public required List<ComplexCombatData> ComplexCombatDatas { get; init; }
|
||||
|
||||
public required CombatItemUse? CombatItemUse { get; init; }
|
||||
|
||||
public HashSet<int> CompletedComplexDatas { get; } = new HashSet<int>();
|
||||
}
|
||||
|
||||
public enum EStatus
|
||||
{
|
||||
NotStarted,
|
||||
InCombat,
|
||||
Moving,
|
||||
Complete
|
||||
}
|
||||
|
||||
private const float MaxTargetRange = 55f;
|
||||
|
||||
private const float MaxNameplateRange = 50f;
|
||||
|
||||
private readonly List<ICombatModule> _combatModules;
|
||||
|
||||
private readonly MovementController _movementController;
|
||||
|
||||
private readonly ITargetManager _targetManager;
|
||||
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
private readonly ICondition _condition;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
|
||||
private readonly ILogger<CombatController> _logger;
|
||||
|
||||
private CurrentFight? _currentFight;
|
||||
|
||||
private bool _wasInCombat;
|
||||
|
||||
private ulong? _lastTargetId;
|
||||
|
||||
private List<byte>? _previousQuestVariables;
|
||||
|
||||
public bool IsRunning => _currentFight != null;
|
||||
|
||||
public CombatController(IEnumerable<ICombatModule> combatModules, MovementController movementController, ITargetManager targetManager, IObjectTable objectTable, ICondition condition, IClientState clientState, QuestFunctions questFunctions, ILogger<CombatController> logger)
|
||||
{
|
||||
_combatModules = combatModules.ToList();
|
||||
_movementController = movementController;
|
||||
_targetManager = targetManager;
|
||||
_objectTable = objectTable;
|
||||
_condition = condition;
|
||||
_clientState = clientState;
|
||||
_questFunctions = questFunctions;
|
||||
_logger = logger;
|
||||
_clientState.TerritoryChanged += TerritoryChanged;
|
||||
}
|
||||
|
||||
public bool Start(CombatData combatData)
|
||||
{
|
||||
Stop("Starting combat");
|
||||
ICombatModule combatModule = _combatModules.FirstOrDefault((ICombatModule x) => x.CanHandleFight(combatData));
|
||||
if (combatModule == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (combatModule.Start(combatData))
|
||||
{
|
||||
_currentFight = new CurrentFight
|
||||
{
|
||||
Module = combatModule,
|
||||
Data = combatData,
|
||||
LastDistanceCheck = DateTime.Now
|
||||
};
|
||||
EEnemySpawnType spawnType = combatData.SpawnType;
|
||||
bool wasInCombat = (uint)(spawnType - 8) <= 1u;
|
||||
_wasInCombat = wasInCombat;
|
||||
UpdateLastTargetAndQuestVariables(null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public EStatus Update()
|
||||
{
|
||||
if (_currentFight == null)
|
||||
{
|
||||
return EStatus.Complete;
|
||||
}
|
||||
if (_movementController.IsPathfinding || _movementController.IsPathRunning || _movementController.MovementStartedAt > DateTime.Now.AddSeconds(-1.0))
|
||||
{
|
||||
return EStatus.Moving;
|
||||
}
|
||||
if (_currentFight.Data.SpawnType == EEnemySpawnType.OverworldEnemies)
|
||||
{
|
||||
if (_targetManager.Target != null)
|
||||
{
|
||||
_lastTargetId = _targetManager.Target?.GameObjectId;
|
||||
}
|
||||
else if (_lastTargetId.HasValue)
|
||||
{
|
||||
IGameObject gameObject = _objectTable.FirstOrDefault((IGameObject x) => x.GameObjectId == _lastTargetId);
|
||||
if (gameObject != null)
|
||||
{
|
||||
if (gameObject.IsDead)
|
||||
{
|
||||
ElementId elementId = _currentFight.Data.ElementId;
|
||||
QuestProgressInfo questProgressInfo = ((elementId != null) ? _questFunctions.GetQuestProgressInfo(elementId) : null);
|
||||
if (questProgressInfo != null && questProgressInfo.Sequence == _currentFight.Data.Sequence && QuestWorkUtils.HasCompletionFlags(_currentFight.Data.CompletionQuestVariablesFlags) && QuestWorkUtils.MatchesQuestWork(_currentFight.Data.CompletionQuestVariablesFlags, questProgressInfo))
|
||||
{
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
if (questProgressInfo == null || questProgressInfo.Sequence != _currentFight.Data.Sequence || _previousQuestVariables == null || questProgressInfo.Variables.SequenceEqual(_previousQuestVariables))
|
||||
{
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
UpdateLastTargetAndQuestVariables(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastTargetId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
IGameObject target = _targetManager.Target;
|
||||
if (target != null)
|
||||
{
|
||||
int item = GetKillPriority(target).Priority;
|
||||
IGameObject gameObject2 = FindNextTarget();
|
||||
int num = ((gameObject2 != null) ? GetKillPriority(gameObject2).Priority : 0);
|
||||
if (gameObject2 != null && gameObject2.Equals(target))
|
||||
{
|
||||
if (!IsMovingOrShouldMove(target))
|
||||
{
|
||||
try
|
||||
{
|
||||
_currentFight.Module.Update(target);
|
||||
}
|
||||
catch (TaskException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Combat was interrupted, stopping: {Exception}", ex.Message);
|
||||
SetTarget(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (gameObject2 != null)
|
||||
{
|
||||
if (num > item || item == 0)
|
||||
{
|
||||
SetTarget(gameObject2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetTarget(null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IGameObject gameObject3 = FindNextTarget();
|
||||
if (gameObject3 != null && !gameObject3.IsDead)
|
||||
{
|
||||
SetTarget(gameObject3);
|
||||
}
|
||||
}
|
||||
if (_condition[ConditionFlag.InCombat])
|
||||
{
|
||||
_wasInCombat = true;
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
if (_wasInCombat)
|
||||
{
|
||||
return EStatus.Complete;
|
||||
}
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
|
||||
private unsafe IGameObject? FindNextTarget()
|
||||
{
|
||||
if (_currentFight == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
||||
if (complexCombatDatas.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < complexCombatDatas.Count; i++)
|
||||
{
|
||||
if (_currentFight.Data.CompletedComplexDatas.Contains(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ComplexCombatData complexCombatData = complexCombatDatas[i];
|
||||
if (complexCombatData.RewardItemId.HasValue && complexCombatData.RewardItemCount.HasValue && InventoryManager.Instance()->GetInventoryItemCount(complexCombatData.RewardItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0) >= complexCombatData.RewardItemCount.Value)
|
||||
{
|
||||
_logger.LogInformation("Complex combat condition fulfilled: itemCount({ItemId}) >= {ItemCount}", complexCombatData.RewardItemId, complexCombatData.RewardItemCount);
|
||||
_currentFight.Data.CompletedComplexDatas.Add(i);
|
||||
}
|
||||
else if (QuestWorkUtils.HasCompletionFlags(complexCombatData.CompletionQuestVariablesFlags) && _currentFight.Data.ElementId is QuestId elementId)
|
||||
{
|
||||
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(elementId);
|
||||
if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(complexCombatData.CompletionQuestVariablesFlags, questProgressInfo))
|
||||
{
|
||||
_logger.LogInformation("Complex combat condition fulfilled: QuestWork matches");
|
||||
_currentFight.Data.CompletedComplexDatas.Add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (from x in _objectTable
|
||||
select new
|
||||
{
|
||||
GameObject = x,
|
||||
Priority = GetKillPriority(x).Priority,
|
||||
Distance = Vector3.Distance(x.Position, _clientState.LocalPlayer.Position)
|
||||
} into x
|
||||
where x.Priority > 0
|
||||
orderby x.Priority descending, x.Distance
|
||||
select x.GameObject).FirstOrDefault();
|
||||
}
|
||||
|
||||
public unsafe (int Priority, string Reason) GetKillPriority(IGameObject gameObject)
|
||||
{
|
||||
var (num, text) = GetRawKillPriority(gameObject);
|
||||
if (!num.HasValue)
|
||||
{
|
||||
return (Priority: 0, Reason: text);
|
||||
}
|
||||
if (gameObject is IBattleNpc battleNpc && battleNpc.StatusFlags.HasFlag(StatusFlags.InCombat))
|
||||
{
|
||||
if (gameObject.TargetObjectId == _clientState.LocalPlayer?.GameObjectId)
|
||||
{
|
||||
return (Priority: num.Value + 150, Reason: text + "/Targeted");
|
||||
}
|
||||
Hater hater = UIState.Instance()->Hater;
|
||||
for (int i = 0; i < hater.HaterCount; i++)
|
||||
{
|
||||
if (hater.Haters[i].EntityId == gameObject.GameObjectId)
|
||||
{
|
||||
return (Priority: num.Value + 125, Reason: text + "/Enmity");
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Priority: num.Value, Reason: text);
|
||||
}
|
||||
|
||||
private unsafe (int? Priority, string Reason) GetRawKillPriority(IGameObject gameObject)
|
||||
{
|
||||
if (_currentFight == null)
|
||||
{
|
||||
return (Priority: null, Reason: "Not Fighting");
|
||||
}
|
||||
if (gameObject is IBattleNpc battleNpc)
|
||||
{
|
||||
if (!_currentFight.Module.CanAttack(battleNpc))
|
||||
{
|
||||
return (Priority: null, Reason: "Can't attack");
|
||||
}
|
||||
if (battleNpc.IsDead)
|
||||
{
|
||||
return (Priority: null, Reason: "Dead");
|
||||
}
|
||||
if (!battleNpc.IsTargetable)
|
||||
{
|
||||
return (Priority: null, Reason: "Untargetable");
|
||||
}
|
||||
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
||||
GameObject* address = (GameObject*)gameObject.Address;
|
||||
if (address->FateId != 0 && gameObject.TargetObjectId != _clientState.LocalPlayer?.GameObjectId)
|
||||
{
|
||||
return (Priority: null, Reason: "FATE mob");
|
||||
}
|
||||
Vector3 value = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
|
||||
bool flag = _currentFight.Data.SpawnType != EEnemySpawnType.FinishCombatIfAny && ((_currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies || !(Vector3.Distance(value, battleNpc.Position) >= 50f)) ? true : false);
|
||||
if (complexCombatDatas.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < complexCombatDatas.Count; i++)
|
||||
{
|
||||
if (!_currentFight.Data.CompletedComplexDatas.Contains(i) && (!flag || complexCombatDatas[i].IgnoreQuestMarker || address->NamePlateIconId != 0) && complexCombatDatas[i].DataId == battleNpc.DataId && (!complexCombatDatas[i].NameId.HasValue || complexCombatDatas[i].NameId == battleNpc.NameId))
|
||||
{
|
||||
return (Priority: 100, Reason: "CCD");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if ((!flag || address->NamePlateIconId != 0) && _currentFight.Data.KillEnemyDataIds.Contains(battleNpc.DataId))
|
||||
{
|
||||
return (Priority: 90, Reason: "KED");
|
||||
}
|
||||
Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind battleNpcKind = battleNpc.BattleNpcKind;
|
||||
if ((battleNpcKind == Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind.BattleNpcPart || battleNpcKind == Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind.Enemy) ? true : false)
|
||||
{
|
||||
uint namePlateIconId = address->NamePlateIconId;
|
||||
if ((namePlateIconId == 60093 || namePlateIconId == 60732) ? true : false)
|
||||
{
|
||||
return (Priority: null, Reason: "FATE NPC");
|
||||
}
|
||||
return (Priority: 0, Reason: "Not part of quest");
|
||||
}
|
||||
return (Priority: null, Reason: "Wrong BattleNpcKind");
|
||||
}
|
||||
return (Priority: null, Reason: "Not BattleNpc");
|
||||
}
|
||||
|
||||
private void SetTarget(IGameObject? target)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
if (_targetManager.Target != null)
|
||||
{
|
||||
_logger.LogInformation("Clearing target");
|
||||
_targetManager.Target = null;
|
||||
}
|
||||
}
|
||||
else if (Vector3.Distance(_clientState.LocalPlayer.Position, target.Position) > 55f)
|
||||
{
|
||||
_logger.LogInformation("Moving to target, distance: {Distance:N2}", Vector3.Distance(_clientState.LocalPlayer.Position, target.Position));
|
||||
MoveToTarget(target);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Setting target to {TargetName} ({TargetId:X8})", target.Name.ToString(), target.GameObjectId);
|
||||
_targetManager.Target = target;
|
||||
MoveToTarget(target);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsMovingOrShouldMove(IGameObject gameObject)
|
||||
{
|
||||
if (_movementController.IsPathfinding || _movementController.IsPathRunning)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (DateTime.Now > _currentFight.LastDistanceCheck.AddSeconds(10.0))
|
||||
{
|
||||
MoveToTarget(gameObject);
|
||||
_currentFight.LastDistanceCheck = DateTime.Now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void MoveToTarget(IGameObject gameObject)
|
||||
{
|
||||
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
float num = localPlayer.HitboxRadius + gameObject.HitboxRadius;
|
||||
float num2 = Vector3.Distance(localPlayer.Position, gameObject.Position);
|
||||
byte? b = localPlayer.ClassJob.ValueNullable?.Role;
|
||||
bool flag;
|
||||
if (b.HasValue)
|
||||
{
|
||||
byte valueOrDefault = b.GetValueOrDefault();
|
||||
if ((uint)(valueOrDefault - 3) <= 1u)
|
||||
{
|
||||
flag = true;
|
||||
goto IL_008e;
|
||||
}
|
||||
}
|
||||
flag = false;
|
||||
goto IL_008e;
|
||||
IL_008e:
|
||||
float num3 = (flag ? 20f : 2.9f);
|
||||
bool flag2 = num2 - num >= num3;
|
||||
bool flag3 = IsInLineOfSight(gameObject);
|
||||
if (flag2 || !flag3)
|
||||
{
|
||||
bool flag4 = num2 - num > 5f;
|
||||
if (!flag2 && !flag3)
|
||||
{
|
||||
num3 = Math.Min(num3, num2) / 2f;
|
||||
flag4 = true;
|
||||
}
|
||||
if (!flag4)
|
||||
{
|
||||
_logger.LogInformation("Moving to {TargetName} ({DataId}) to attack", gameObject.Name, gameObject.DataId);
|
||||
MovementController movementController = _movementController;
|
||||
int num4 = 1;
|
||||
List<Vector3> list = new List<Vector3>(num4);
|
||||
CollectionsMarshal.SetCount(list, num4);
|
||||
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
|
||||
int index = 0;
|
||||
span[index] = gameObject.Position;
|
||||
movementController.NavigateTo(EMovementType.Combat, null, list, fly: false, sprint: false, num3 + num - 0.25f, float.MaxValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Moving to {TargetName} ({DataId}) to attack (with navmesh)", gameObject.Name, gameObject.DataId);
|
||||
_movementController.NavigateTo(EMovementType.Combat, null, gameObject.Position, fly: false, sprint: false, num3 + num - 0.25f, float.MaxValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal unsafe bool IsInLineOfSight(IGameObject target)
|
||||
{
|
||||
Vector3 position = _clientState.LocalPlayer.Position;
|
||||
position.Y += 2f;
|
||||
Vector3 position2 = target.Position;
|
||||
position2.Y += 2f;
|
||||
Vector3 value = position2 - position;
|
||||
float maxDistance = value.Length();
|
||||
value = Vector3.Normalize(value);
|
||||
Vector3 vector = new Vector3(position.X, position.Y, position.Z);
|
||||
Vector3 vector2 = new Vector3(value.X, value.Y, value.Z);
|
||||
int* flags = stackalloc int[4] { 16384, 0, 16384, 0 };
|
||||
RaycastHit raycastHit = default(RaycastHit);
|
||||
return !Framework.Instance()->BGCollisionModule->RaycastMaterialFilter(&raycastHit, &vector, &vector2, maxDistance, 1, flags);
|
||||
}
|
||||
|
||||
private void UpdateLastTargetAndQuestVariables(IGameObject? target)
|
||||
{
|
||||
_lastTargetId = target?.GameObjectId;
|
||||
_previousQuestVariables = ((!(_currentFight.Data.ElementId != null)) ? null : _questFunctions.GetQuestProgressInfo(_currentFight.Data.ElementId)?.Variables);
|
||||
}
|
||||
|
||||
public void Stop(string label)
|
||||
{
|
||||
using (_logger.BeginScope(label))
|
||||
{
|
||||
if (_currentFight != null)
|
||||
{
|
||||
_logger.LogInformation("Stopping current fight");
|
||||
_currentFight.Module.Stop();
|
||||
}
|
||||
_currentFight = null;
|
||||
_wasInCombat = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TerritoryChanged(ushort territoryId)
|
||||
{
|
||||
Stop("TerritoryChanged");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_clientState.TerritoryChanged -= TerritoryChanged;
|
||||
Stop("Dispose");
|
||||
}
|
||||
}
|
379
Questionable/Questionable.Controller/CommandHandler.cs
Normal file
379
Questionable/Questionable.Controller/CommandHandler.cs
Normal file
|
@ -0,0 +1,379 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Questionable.Functions;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
using Questionable.Windows;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class CommandHandler : IDisposable
|
||||
{
|
||||
public const string MessageTag = "Questionable";
|
||||
|
||||
public const ushort TagColor = 576;
|
||||
|
||||
private readonly ICommandManager _commandManager;
|
||||
|
||||
private readonly IChatGui _chatGui;
|
||||
|
||||
private readonly QuestController _questController;
|
||||
|
||||
private readonly MovementController _movementController;
|
||||
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
|
||||
private readonly ConfigWindow _configWindow;
|
||||
|
||||
private readonly DebugOverlay _debugOverlay;
|
||||
|
||||
private readonly OneTimeSetupWindow _oneTimeSetupWindow;
|
||||
|
||||
private readonly QuestWindow _questWindow;
|
||||
|
||||
private readonly QuestSelectionWindow _questSelectionWindow;
|
||||
|
||||
private readonly JournalProgressWindow _journalProgressWindow;
|
||||
|
||||
private readonly PriorityWindow _priorityWindow;
|
||||
|
||||
private readonly ITargetManager _targetManager;
|
||||
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
|
||||
private readonly IDataManager _dataManager;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
|
||||
private IReadOnlyList<uint> _previouslyUnlockedUnlockLinks = Array.Empty<uint>();
|
||||
|
||||
public CommandHandler(ICommandManager commandManager, IChatGui chatGui, QuestController questController, MovementController movementController, QuestRegistry questRegistry, ConfigWindow configWindow, DebugOverlay debugOverlay, OneTimeSetupWindow oneTimeSetupWindow, QuestWindow questWindow, QuestSelectionWindow questSelectionWindow, JournalProgressWindow journalProgressWindow, PriorityWindow priorityWindow, ITargetManager targetManager, QuestFunctions questFunctions, GameFunctions gameFunctions, IDataManager dataManager, IClientState clientState, Configuration configuration)
|
||||
{
|
||||
_commandManager = commandManager;
|
||||
_chatGui = chatGui;
|
||||
_questController = questController;
|
||||
_movementController = movementController;
|
||||
_questRegistry = questRegistry;
|
||||
_configWindow = configWindow;
|
||||
_debugOverlay = debugOverlay;
|
||||
_oneTimeSetupWindow = oneTimeSetupWindow;
|
||||
_questWindow = questWindow;
|
||||
_questSelectionWindow = questSelectionWindow;
|
||||
_journalProgressWindow = journalProgressWindow;
|
||||
_priorityWindow = priorityWindow;
|
||||
_targetManager = targetManager;
|
||||
_questFunctions = questFunctions;
|
||||
_gameFunctions = gameFunctions;
|
||||
_dataManager = dataManager;
|
||||
_clientState = clientState;
|
||||
_configuration = configuration;
|
||||
_clientState.Logout += OnLogout;
|
||||
_commandManager.AddHandler("/qst", new CommandInfo(ProcessCommand)
|
||||
{
|
||||
HelpMessage = string.Join(Environment.NewLine + "\t", "Opens the Questing window", "/qst help - displays simplified commands", "/qst help-all - displays all available commands", "/qst config - opens the configuration window", "/qst start - starts doing quests", "/qst stop - stops doing quests")
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessCommand(string command, string arguments)
|
||||
{
|
||||
if (!OpenSetupIfNeeded(arguments))
|
||||
{
|
||||
string[] array = arguments.Split(' ');
|
||||
switch (array[0])
|
||||
{
|
||||
case "h":
|
||||
case "help":
|
||||
_chatGui.Print("Available commands:", "Questionable", 576);
|
||||
_chatGui.Print("/qst - toggles the Questing window", "Questionable", 576);
|
||||
_chatGui.Print("/qst help - displays simplified commands", "Questionable", 576);
|
||||
_chatGui.Print("/qst help-all - displays all available commands", "Questionable", 576);
|
||||
_chatGui.Print("/qst config - opens the configuration window", "Questionable", 576);
|
||||
_chatGui.Print("/qst start - starts doing quests", "Questionable", 576);
|
||||
_chatGui.Print("/qst stop - stops doing quests", "Questionable", 576);
|
||||
_chatGui.Print("/qst reload - reload all quest data", "Questionable", 576);
|
||||
break;
|
||||
case "ha":
|
||||
case "help-all":
|
||||
_chatGui.Print("Available commands:", "Questionable", 576);
|
||||
_chatGui.Print("/qst - toggles the Questing window", "Questionable", 576);
|
||||
_chatGui.Print("/qst help - displays available commands", "Questionable", 576);
|
||||
_chatGui.Print("/qst help-all - displays all available commands", "Questionable", 576);
|
||||
_chatGui.Print("/qst config - opens the configuration window", "Questionable", 576);
|
||||
_chatGui.Print("/qst start - starts doing quests", "Questionable", 576);
|
||||
_chatGui.Print("/qst stop - stops doing quests", "Questionable", 576);
|
||||
_chatGui.Print("/qst reload - reload all quest data", "Questionable", 576);
|
||||
_chatGui.Print("/qst do <questId> - highlights the specified quest in the debug overlay (requires debug overlay to be enabled)", "Questionable", 576);
|
||||
_chatGui.Print("/qst do - clears the highlighted quest in the debug overlay (requires debug overlay to be enabled)", "Questionable", 576);
|
||||
_chatGui.Print("/qst next <questId> - sets the next quest to do (or clears it if no questId is specified)", "Questionable", 576);
|
||||
_chatGui.Print("/qst sim <questId> [sequence] [step] - simulates the specified quest (or clears it if no questId is specified)", "Questionable", 576);
|
||||
_chatGui.Print("/qst which - shows all quests starting with your selected target", "Questionable", 576);
|
||||
_chatGui.Print("/qst zone - shows all quests starting in the current zone (only includes quests with a known quest path, and currently visible unaccepted quests)", "Questionable", 576);
|
||||
_chatGui.Print("/qst journal - toggles the Journal Progress window", "Questionable", 576);
|
||||
_chatGui.Print("/qst priority - toggles the Priority window", "Questionable", 576);
|
||||
_chatGui.Print("/qst mountid - prints information about your current mount", "Questionable", 576);
|
||||
_chatGui.Print("/qst handle-interrupt - makes Questionable handle queued interrupts immediately (useful if you manually start combat)", "Questionable", 576);
|
||||
break;
|
||||
case "c":
|
||||
case "config":
|
||||
_configWindow.ToggleOrUncollapse();
|
||||
break;
|
||||
case "start":
|
||||
_questWindow.IsOpenAndUncollapsed = true;
|
||||
_questController.Start("Start command");
|
||||
break;
|
||||
case "stop":
|
||||
_movementController.Stop();
|
||||
_questController.Stop("Stop command");
|
||||
break;
|
||||
case "reload":
|
||||
_questWindow.Reload();
|
||||
break;
|
||||
case "do":
|
||||
ConfigureDebugOverlay(array.Skip(1).ToArray());
|
||||
break;
|
||||
case "next":
|
||||
SetNextQuest(array.Skip(1).ToArray());
|
||||
break;
|
||||
case "sim":
|
||||
SetSimulatedQuest(array.Skip(1).ToArray());
|
||||
break;
|
||||
case "which":
|
||||
_questSelectionWindow.OpenForTarget(_targetManager.Target);
|
||||
break;
|
||||
case "z":
|
||||
case "zone":
|
||||
_questSelectionWindow.OpenForCurrentZone();
|
||||
break;
|
||||
case "j":
|
||||
case "journal":
|
||||
_journalProgressWindow.ToggleOrUncollapse();
|
||||
break;
|
||||
case "p":
|
||||
case "priority":
|
||||
_priorityWindow.ToggleOrUncollapse();
|
||||
break;
|
||||
case "mountid":
|
||||
PrintMountId();
|
||||
break;
|
||||
case "handle-interrupt":
|
||||
_questController.InterruptQueueWithCombat();
|
||||
break;
|
||||
case "":
|
||||
_questWindow.ToggleOrUncollapse();
|
||||
break;
|
||||
default:
|
||||
_chatGui.PrintError("Unknown subcommand " + array[0], "Questionable", 576);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessDebugCommand(string command, string arguments)
|
||||
{
|
||||
if (OpenSetupIfNeeded(arguments))
|
||||
{
|
||||
return;
|
||||
}
|
||||
switch (arguments.Split(' ')[0])
|
||||
{
|
||||
case "abandon-duty":
|
||||
_gameFunctions.AbandonDuty();
|
||||
break;
|
||||
case "unlock-links":
|
||||
{
|
||||
IReadOnlyList<uint> unlockLinks = _gameFunctions.GetUnlockLinks();
|
||||
if (unlockLinks.Count >= 0)
|
||||
{
|
||||
_chatGui.Print($"Saved {unlockLinks.Count} unlock links to log.", "Questionable", 576);
|
||||
List<uint> list2 = unlockLinks.Except(_previouslyUnlockedUnlockLinks).ToList();
|
||||
if (_previouslyUnlockedUnlockLinks.Count > 0 && list2.Count > 0)
|
||||
{
|
||||
_chatGui.Print("New unlock links: " + string.Join(", ", list2), "Questionable", 576);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.PrintError("Could not query unlock links.", "Questionable", 576);
|
||||
}
|
||||
_previouslyUnlockedUnlockLinks = unlockLinks;
|
||||
break;
|
||||
}
|
||||
case "taxi":
|
||||
{
|
||||
List<string> list3 = new List<string>();
|
||||
ExcelSheet<ChocoboTaxiStand> excelSheet = _dataManager.GetExcelSheet<ChocoboTaxiStand>();
|
||||
UIState* ptr = UIState.Instance();
|
||||
for (byte b2 = 0; b2 < ptr->ChocoboTaxiStandsBitmask.Length * 8; b2++)
|
||||
{
|
||||
if (ptr->IsChocoboTaxiStandUnlocked(b2))
|
||||
{
|
||||
list3.Add($"{excelSheet.GetRow((uint)(b2 + 1179648)).PlaceName} ({b2})");
|
||||
}
|
||||
}
|
||||
_chatGui.Print("Unlocked taxi stands:", "Questionable", 576);
|
||||
{
|
||||
foreach (string item in list3)
|
||||
{
|
||||
_chatGui.Print("- " + item, "Questionable", 576);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
case "festivals":
|
||||
{
|
||||
List<string> list = new List<string>();
|
||||
for (byte b = 0; b < 4; b++)
|
||||
{
|
||||
GameMain.Festival festival = GameMain.Instance()->ActiveFestivals[b];
|
||||
if (festival.Id != 0)
|
||||
{
|
||||
list.Add($"{festival.Id}({festival.Phase})");
|
||||
}
|
||||
}
|
||||
_chatGui.Print("Active festivals: " + string.Join(", ", list), "Questionable", 576);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool OpenSetupIfNeeded(string arguments)
|
||||
{
|
||||
if (!_configuration.IsPluginSetupComplete())
|
||||
{
|
||||
if (string.IsNullOrEmpty(arguments))
|
||||
{
|
||||
_oneTimeSetupWindow.IsOpenAndUncollapsed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.PrintError("Please complete the one-time setup first.", "Questionable", 576);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ConfigureDebugOverlay(string[] arguments)
|
||||
{
|
||||
ElementId elementId;
|
||||
if (!_debugOverlay.DrawConditions())
|
||||
{
|
||||
_chatGui.PrintError("You don't have the debug overlay enabled.", "Questionable", 576);
|
||||
}
|
||||
else if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out elementId) && elementId != null)
|
||||
{
|
||||
if (_questRegistry.TryGetQuest(elementId, out Questionable.Model.Quest quest))
|
||||
{
|
||||
_debugOverlay.HighlightedQuest = quest.Id;
|
||||
_chatGui.Print($"Set highlighted quest to {elementId} ({quest.Info.Name}).", "Questionable", 576);
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.PrintError($"Unknown quest {elementId}.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_debugOverlay.HighlightedQuest = null;
|
||||
_chatGui.Print("Cleared highlighted quest.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetNextQuest(string[] arguments)
|
||||
{
|
||||
if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId elementId) && elementId != null)
|
||||
{
|
||||
Questionable.Model.Quest quest;
|
||||
if (_questFunctions.IsQuestLocked(elementId))
|
||||
{
|
||||
_chatGui.PrintError($"Quest {elementId} is locked.", "Questionable", 576);
|
||||
}
|
||||
else if (_questRegistry.TryGetQuest(elementId, out quest))
|
||||
{
|
||||
_questController.SetNextQuest(quest);
|
||||
_chatGui.Print($"Set next quest to {elementId} ({quest.Info.Name}).", "Questionable", 576);
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.PrintError($"Unknown quest {elementId}.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_questController.SetNextQuest(null);
|
||||
_chatGui.Print("Cleared next quest.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSimulatedQuest(string[] arguments)
|
||||
{
|
||||
if (arguments.Length >= 1 && ElementId.TryFromString(arguments[0], out ElementId elementId) && elementId != null)
|
||||
{
|
||||
if (_questRegistry.TryGetQuest(elementId, out Questionable.Model.Quest quest))
|
||||
{
|
||||
byte sequence = 0;
|
||||
int step = 0;
|
||||
if (arguments.Length >= 2 && byte.TryParse(arguments[1], out var result))
|
||||
{
|
||||
QuestSequence questSequence = quest.FindSequence(result);
|
||||
if (questSequence != null)
|
||||
{
|
||||
sequence = questSequence.Sequence;
|
||||
if (arguments.Length >= 3 && int.TryParse(arguments[2], out var result2) && questSequence.FindStep(result2) != null)
|
||||
{
|
||||
step = result2;
|
||||
}
|
||||
}
|
||||
}
|
||||
_questController.SimulateQuest(quest, sequence, step);
|
||||
_chatGui.Print($"Simulating quest {elementId} ({quest.Info.Name}).", "Questionable", 576);
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.PrintError($"Unknown quest {elementId}.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_questController.SimulateQuest(null, 0, 0);
|
||||
_chatGui.Print("Cleared simulated quest.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintMountId()
|
||||
{
|
||||
ushort? mountId = _gameFunctions.GetMountId();
|
||||
if (mountId.HasValue)
|
||||
{
|
||||
Mount? rowOrDefault = _dataManager.GetExcelSheet<Mount>().GetRowOrDefault(mountId.Value);
|
||||
_chatGui.Print($"Mount ID: {mountId}, Name: {rowOrDefault?.Singular}, Obtainable: {((rowOrDefault?.Order == -1) ? "No" : "Yes")}", "Questionable", 576);
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.Print("You are not mounted.", "Questionable", 576);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLogout(int type, int code)
|
||||
{
|
||||
_previouslyUnlockedUnlockLinks = Array.Empty<uint>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_commandManager.RemoveHandler("/qst");
|
||||
_clientState.Logout -= OnLogout;
|
||||
}
|
||||
}
|
217
Questionable/Questionable.Controller/ContextMenuController.cs
Normal file
217
Questionable/Questionable.Controller/ContextMenuController.cs
Normal file
|
@ -0,0 +1,217 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using LLib.GameData;
|
||||
using LLib.GameUI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
using Questionable.Functions;
|
||||
using Questionable.GameStructs;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Gathering;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class ContextMenuController : IDisposable
|
||||
{
|
||||
private readonly IContextMenu _contextMenu;
|
||||
|
||||
private readonly QuestController _questController;
|
||||
|
||||
private readonly GatheringPointRegistry _gatheringPointRegistry;
|
||||
|
||||
private readonly GatheringData _gatheringData;
|
||||
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
|
||||
private readonly QuestData _questData;
|
||||
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
|
||||
private readonly QuestFunctions _questFunctions;
|
||||
|
||||
private readonly IGameGui _gameGui;
|
||||
|
||||
private readonly IChatGui _chatGui;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly ILogger<ContextMenuController> _logger;
|
||||
|
||||
public ContextMenuController(IContextMenu contextMenu, QuestController questController, GatheringPointRegistry gatheringPointRegistry, GatheringData gatheringData, QuestRegistry questRegistry, QuestData questData, GameFunctions gameFunctions, QuestFunctions questFunctions, IGameGui gameGui, IChatGui chatGui, IClientState clientState, ILogger<ContextMenuController> logger)
|
||||
{
|
||||
_contextMenu = contextMenu;
|
||||
_questController = questController;
|
||||
_gatheringPointRegistry = gatheringPointRegistry;
|
||||
_gatheringData = gatheringData;
|
||||
_questRegistry = questRegistry;
|
||||
_questData = questData;
|
||||
_gameFunctions = gameFunctions;
|
||||
_questFunctions = questFunctions;
|
||||
_gameGui = gameGui;
|
||||
_chatGui = chatGui;
|
||||
_clientState = clientState;
|
||||
_logger = logger;
|
||||
_contextMenu.OnMenuOpened += MenuOpened;
|
||||
}
|
||||
|
||||
private void MenuOpened(IMenuOpenedArgs args)
|
||||
{
|
||||
if (args.AddonName != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
uint num = GetHoveredSatisfactionSupplyItemId();
|
||||
if (num == 0)
|
||||
{
|
||||
_logger.LogTrace("Ignoring context menu, no item hovered");
|
||||
return;
|
||||
}
|
||||
if (num > 1000000)
|
||||
{
|
||||
num -= 1000000;
|
||||
}
|
||||
if (num >= 500000)
|
||||
{
|
||||
num -= 500000;
|
||||
}
|
||||
if (_gatheringData.TryGetCustomDeliveryNpc(num, out var npcId))
|
||||
{
|
||||
AddContextMenuEntry(args, num, npcId, EClassJob.Miner, "Mine");
|
||||
AddContextMenuEntry(args, num, npcId, EClassJob.Botanist, "Harvest");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No custom delivery NPC found for item {ItemId}.", num);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe uint GetHoveredSatisfactionSupplyItemId()
|
||||
{
|
||||
AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance();
|
||||
if (ptr == null || !ptr->IsAgentActive())
|
||||
{
|
||||
return 0u;
|
||||
}
|
||||
if (_gameGui.TryGetAddonByName<AddonSatisfactionSupply>("SatisfactionSupply", out var addonPtr) && LAddon.IsAddonReady(&addonPtr->AtkUnitBase))
|
||||
{
|
||||
int hoveredElementIndex = addonPtr->HoveredElementIndex;
|
||||
if (hoveredElementIndex >= 0 && hoveredElementIndex <= 2)
|
||||
{
|
||||
return ptr->Items[addonPtr->HoveredElementIndex].Id;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
private unsafe void AddContextMenuEntry(IMenuOpenedArgs args, uint itemId, uint npcId, EClassJob classJob, string verb)
|
||||
{
|
||||
EClassJob rowId = (EClassJob)_clientState.LocalPlayer.ClassJob.RowId;
|
||||
bool flag = classJob != rowId;
|
||||
if (flag)
|
||||
{
|
||||
bool flag2 = rowId - 16 <= EClassJob.Gladiator;
|
||||
flag = flag2;
|
||||
}
|
||||
if (flag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!_gatheringPointRegistry.TryGetGatheringPointId(itemId, classJob, out GatheringPointId _))
|
||||
{
|
||||
_logger.LogInformation("No gathering point found for {ClassJob}.", classJob);
|
||||
return;
|
||||
}
|
||||
ushort collectability = _gatheringData.GetRecommendedCollectability(itemId);
|
||||
int quantityToGather = ((collectability > 0) ? 6 : int.MaxValue);
|
||||
if (collectability != 0)
|
||||
{
|
||||
AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance();
|
||||
if (ptr->IsAgentActive())
|
||||
{
|
||||
int maxTurnIns = ((ptr->NpcInfo.SatisfactionRank == 1) ? 3 : 6);
|
||||
quantityToGather = Math.Min(ptr->NpcData.RemainingAllowances, ((AgentSatisfactionSupply2*)ptr)->CalculateTurnInsToNextRank(maxTurnIns));
|
||||
}
|
||||
string text = string.Empty;
|
||||
if (!_questFunctions.IsClassJobUnlocked(classJob))
|
||||
{
|
||||
text = $"{classJob} not unlocked";
|
||||
}
|
||||
else if (quantityToGather == 0)
|
||||
{
|
||||
text = "No allowances";
|
||||
}
|
||||
else if (quantityToGather > _gameFunctions.GetFreeInventorySlots())
|
||||
{
|
||||
text = "Inventory full";
|
||||
}
|
||||
else if (_gameFunctions.IsOccupied())
|
||||
{
|
||||
text = "Can't be used while interacting";
|
||||
}
|
||||
string text2 = verb + " with Questionable";
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
text2 = text2 + " (" + text + ")";
|
||||
}
|
||||
args.AddMenuItem(new MenuItem
|
||||
{
|
||||
Prefix = SeIconChar.Hyadelyn,
|
||||
PrefixColor = 52,
|
||||
Name = text2,
|
||||
OnClicked = delegate
|
||||
{
|
||||
StartGathering(npcId, itemId, quantityToGather, collectability, classJob);
|
||||
},
|
||||
IsEnabled = string.IsNullOrEmpty(text)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void StartGathering(uint npcId, uint itemId, int quantity, ushort collectability, EClassJob classJob)
|
||||
{
|
||||
SatisfactionSupplyInfo satisfactionSupplyInfo = (SatisfactionSupplyInfo)_questData.GetAllByIssuerDataId(npcId).Single((IQuestInfo x) => x is SatisfactionSupplyInfo);
|
||||
if (_questRegistry.TryGetQuest(satisfactionSupplyInfo.QuestId, out Quest quest))
|
||||
{
|
||||
QuestSequence questSequence = quest.FindSequence(0);
|
||||
QuestStep questStep = questSequence.Steps.Single((QuestStep x) => x.InteractionType == EInteractionType.SwitchClass);
|
||||
questStep.TargetClass = classJob switch
|
||||
{
|
||||
EClassJob.Miner => EExtendedClassJob.Miner,
|
||||
EClassJob.Botanist => EExtendedClassJob.Botanist,
|
||||
_ => throw new ArgumentOutOfRangeException("classJob", classJob, null),
|
||||
};
|
||||
QuestStep questStep2 = questSequence.Steps.Single((QuestStep x) => x.InteractionType == EInteractionType.Gather);
|
||||
int num = 1;
|
||||
List<GatheredItem> list = new List<GatheredItem>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<GatheredItem> span = CollectionsMarshal.AsSpan(list);
|
||||
int index = 0;
|
||||
span[index] = new GatheredItem
|
||||
{
|
||||
ItemId = itemId,
|
||||
ItemCount = quantity,
|
||||
Collectability = collectability
|
||||
};
|
||||
questStep2.ItemsToGather = list;
|
||||
_questController.SetGatheringQuest(quest);
|
||||
_questController.StartGatheringQuest("SatisfactionSupply prepare gathering");
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatGui.PrintError($"No associated quest ({satisfactionSupplyInfo.QuestId}).", "Questionable");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_contextMenu.OnMenuOpened -= MenuOpened;
|
||||
}
|
||||
}
|
278
Questionable/Questionable.Controller/GatheringController.cs
Normal file
278
Questionable/Questionable.Controller/GatheringController.cs
Normal file
|
@ -0,0 +1,278 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using LLib;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.Common;
|
||||
using Questionable.Controller.Steps.Gathering;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Controller.Steps.Movement;
|
||||
using Questionable.External;
|
||||
using Questionable.Functions;
|
||||
using Questionable.Model.Gathering;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class GatheringController : MiniTaskController<GatheringController>
|
||||
{
|
||||
internal sealed class CurrentRequest
|
||||
{
|
||||
public required GatheringRequest Data { get; init; }
|
||||
|
||||
public required GatheringRoot Root { get; init; }
|
||||
|
||||
public required List<GatheringNode> Nodes { get; init; }
|
||||
|
||||
public int CurrentIndex { get; set; }
|
||||
}
|
||||
|
||||
public sealed record GatheringRequest(GatheringPointId GatheringPointId, uint ItemId, uint AlternativeItemId, int Quantity, ushort Collectability = 0);
|
||||
|
||||
public enum EStatus
|
||||
{
|
||||
Gathering,
|
||||
Moving,
|
||||
Complete
|
||||
}
|
||||
|
||||
private readonly MovementController _movementController;
|
||||
|
||||
private readonly GatheringPointRegistry _gatheringPointRegistry;
|
||||
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
|
||||
private readonly NavmeshIpc _navmeshIpc;
|
||||
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
private readonly ICondition _condition;
|
||||
|
||||
private readonly ILogger<GatheringController> _logger;
|
||||
|
||||
private readonly Regex _revisitRegex;
|
||||
|
||||
private CurrentRequest? _currentRequest;
|
||||
|
||||
public GatheringController(MovementController movementController, GatheringPointRegistry gatheringPointRegistry, GameFunctions gameFunctions, NavmeshIpc navmeshIpc, IObjectTable objectTable, IChatGui chatGui, ILogger<GatheringController> logger, ICondition condition, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, IPluginLog pluginLog)
|
||||
: base(chatGui, condition, serviceProvider, interruptHandler, dataManager, logger)
|
||||
{
|
||||
_movementController = movementController;
|
||||
_gatheringPointRegistry = gatheringPointRegistry;
|
||||
_gameFunctions = gameFunctions;
|
||||
_navmeshIpc = navmeshIpc;
|
||||
_objectTable = objectTable;
|
||||
_condition = condition;
|
||||
_logger = logger;
|
||||
_revisitRegex = dataManager.GetRegex(5574u, (LogMessage x) => x.Text, pluginLog) ?? throw new InvalidDataException("No regex found for revisit message");
|
||||
}
|
||||
|
||||
public bool Start(GatheringRequest gatheringRequest)
|
||||
{
|
||||
if (!_gatheringPointRegistry.TryGetGatheringPoint(gatheringRequest.GatheringPointId, out GatheringRoot gatheringRoot))
|
||||
{
|
||||
_logger.LogError("Unable to resolve gathering point, no path found for {ItemId} / point {PointId}", gatheringRequest.ItemId, gatheringRequest.GatheringPointId);
|
||||
return false;
|
||||
}
|
||||
_currentRequest = new CurrentRequest
|
||||
{
|
||||
Data = gatheringRequest,
|
||||
Root = gatheringRoot,
|
||||
Nodes = gatheringRoot.Groups.SelectMany((GatheringNodeGroup x) => x.Nodes.OrderBy((GatheringNode y) => y.Locations.Count)).ToList()
|
||||
};
|
||||
if (HasRequestedItems())
|
||||
{
|
||||
_currentRequest = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public EStatus Update()
|
||||
{
|
||||
if (_currentRequest == null)
|
||||
{
|
||||
Stop("No request");
|
||||
return EStatus.Complete;
|
||||
}
|
||||
if (_movementController.IsPathfinding || _movementController.IsPathfinding)
|
||||
{
|
||||
return EStatus.Moving;
|
||||
}
|
||||
if (HasRequestedItems() && !_condition[ConditionFlag.Gathering])
|
||||
{
|
||||
Stop("Has all items");
|
||||
return EStatus.Complete;
|
||||
}
|
||||
if (_taskQueue.AllTasksComplete)
|
||||
{
|
||||
GoToNextNode();
|
||||
}
|
||||
UpdateCurrentTask();
|
||||
return EStatus.Gathering;
|
||||
}
|
||||
|
||||
protected override void OnTaskComplete(ITask task)
|
||||
{
|
||||
GoToNextNode();
|
||||
}
|
||||
|
||||
public override void Stop(string label)
|
||||
{
|
||||
_currentRequest = null;
|
||||
_taskQueue.Reset();
|
||||
}
|
||||
|
||||
private void GoToNextNode()
|
||||
{
|
||||
if (_currentRequest == null || !_taskQueue.AllTasksComplete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
GatheringNode gatheringNode = FindNextTargetableNodeAndUpdateIndex(_currentRequest);
|
||||
if (gatheringNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ushort territoryId = _currentRequest.Root.Steps.Last().TerritoryId;
|
||||
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.MountTask(territoryId, Questionable.Controller.Steps.Common.Mount.EMountIf.Always));
|
||||
bool? fly = gatheringNode.Fly;
|
||||
bool? flyBetweenNodes = _currentRequest.Root.FlyBetweenNodes;
|
||||
bool flag = (fly ?? flyBetweenNodes ?? true) && _gameFunctions.IsFlyingUnlocked(territoryId);
|
||||
if (gatheringNode.Locations.Count > 1)
|
||||
{
|
||||
Vector3 vector = new Vector3
|
||||
{
|
||||
X = gatheringNode.Locations.Sum((GatheringLocation x) => x.Position.X) / (float)gatheringNode.Locations.Count,
|
||||
Y = gatheringNode.Locations.Select((GatheringLocation x) => x.Position.Y).Max() + 5f,
|
||||
Z = gatheringNode.Locations.Sum((GatheringLocation x) => x.Position.Z) / (float)gatheringNode.Locations.Count
|
||||
};
|
||||
Vector3? vector2 = _navmeshIpc.GetPointOnFloor(vector, unlandable: true);
|
||||
if (vector2.HasValue)
|
||||
{
|
||||
Vector3 value = vector2.Value;
|
||||
value.Y = vector2.Value.Y + (flag ? 3f : 0f);
|
||||
vector2 = value;
|
||||
}
|
||||
TaskQueue taskQueue = _taskQueue;
|
||||
Vector3 destination = vector2 ?? vector;
|
||||
float? stopDistance = 50f;
|
||||
bool fly2 = flag;
|
||||
taskQueue.Enqueue(new MoveTask(territoryId, destination, null, stopDistance, null, DisableNavmesh: false, null, fly2, Land: false, IgnoreDistanceToObject: true, RestartNavigation: true, EInteractionType.WalkTo));
|
||||
}
|
||||
_taskQueue.Enqueue(new MoveToLandingLocation.Task(territoryId, flag, gatheringNode));
|
||||
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
|
||||
_taskQueue.Enqueue(new Interact.Task(gatheringNode.DataId, null, EInteractionType.Gather, SkipMarkerCheck: true));
|
||||
QueueGatherNode(gatheringNode);
|
||||
}
|
||||
|
||||
private void QueueGatherNode(GatheringNode currentNode)
|
||||
{
|
||||
bool[] array = new bool[2] { false, true };
|
||||
foreach (bool revisitRequired in array)
|
||||
{
|
||||
_taskQueue.Enqueue(new DoGather.Task(_currentRequest.Data, currentNode, revisitRequired));
|
||||
if (_currentRequest.Data.Collectability > 0)
|
||||
{
|
||||
_taskQueue.Enqueue(new DoGatherCollectable.Task(_currentRequest.Data, currentNode, revisitRequired));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe bool HasRequestedItems()
|
||||
{
|
||||
if (_currentRequest == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
InventoryManager* ptr = InventoryManager.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return ptr->GetInventoryItemCount(_currentRequest.Data.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)_currentRequest.Data.Collectability) >= _currentRequest.Data.Quantity;
|
||||
}
|
||||
|
||||
public bool HasNodeDisappeared(GatheringNode node)
|
||||
{
|
||||
return !_objectTable.Any((IGameObject x) => x.ObjectKind == ObjectKind.GatheringPoint && x.IsTargetable && x.DataId == node.DataId);
|
||||
}
|
||||
|
||||
private GatheringNode? FindNextTargetableNodeAndUpdateIndex(CurrentRequest currentRequest)
|
||||
{
|
||||
for (int i = 0; i < currentRequest.Nodes.Count; i++)
|
||||
{
|
||||
int num = (currentRequest.CurrentIndex + i) % currentRequest.Nodes.Count;
|
||||
GatheringNode currentNode = currentRequest.Nodes[num];
|
||||
List<IGameObject> source = currentNode.Locations.Select((GatheringLocation x) => _objectTable.FirstOrDefault((IGameObject y) => currentNode.DataId == y.DataId && Vector3.Distance(x.Position, y.Position) < 0.1f)).ToList();
|
||||
if (source.Any((IGameObject x) => x == null))
|
||||
{
|
||||
currentRequest.CurrentIndex = (num + 1) % currentRequest.Nodes.Count;
|
||||
return currentNode;
|
||||
}
|
||||
if (source.Any((IGameObject x) => x?.IsTargetable ?? false))
|
||||
{
|
||||
currentRequest.CurrentIndex = (num + 1) % currentRequest.Nodes.Count;
|
||||
return currentNode;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public override IList<string> GetRemainingTaskNames()
|
||||
{
|
||||
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
|
||||
if (task != null)
|
||||
{
|
||||
string text = task.ToString() ?? "?";
|
||||
IList<string> remainingTaskNames = base.GetRemainingTaskNames();
|
||||
int num = 1 + remainingTaskNames.Count;
|
||||
List<string> list = new List<string>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<string> span = CollectionsMarshal.AsSpan(list);
|
||||
int num2 = 0;
|
||||
span[num2] = text;
|
||||
num2++;
|
||||
{
|
||||
foreach (string item in remainingTaskNames)
|
||||
{
|
||||
span[num2] = item;
|
||||
num2++;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
return base.GetRemainingTaskNames();
|
||||
}
|
||||
|
||||
public void OnNormalToast(SeString message)
|
||||
{
|
||||
if (!_revisitRegex.IsMatch(message.TextValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_taskQueue.CurrentTaskExecutor?.CurrentTask is IRevisitAware revisitAware)
|
||||
{
|
||||
revisitAware.OnRevisit();
|
||||
}
|
||||
foreach (ITask remainingTask in _taskQueue.RemainingTasks)
|
||||
{
|
||||
if (remainingTask is IRevisitAware revisitAware2)
|
||||
{
|
||||
revisitAware2.OnRevisit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
182
Questionable/Questionable.Controller/GatheringPointRegistry.cs
Normal file
182
Questionable/Questionable.Controller/GatheringPointRegistry.cs
Normal file
|
@ -0,0 +1,182 @@
|
|||
#define RELEASE
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Dalamud.Plugin;
|
||||
using LLib.GameData;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
using Questionable.GatheringPaths;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Gathering;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class GatheringPointRegistry : IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
|
||||
private readonly QuestRegistry _questRegistry;
|
||||
|
||||
private readonly GatheringData _gatheringData;
|
||||
|
||||
private readonly ILogger<QuestRegistry> _logger;
|
||||
|
||||
private readonly Dictionary<GatheringPointId, GatheringRoot> _gatheringPoints = new Dictionary<GatheringPointId, GatheringRoot>();
|
||||
|
||||
public GatheringPointRegistry(IDalamudPluginInterface pluginInterface, QuestRegistry questRegistry, GatheringData gatheringData, ILogger<QuestRegistry> logger)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_questRegistry = questRegistry;
|
||||
_gatheringData = gatheringData;
|
||||
_logger = logger;
|
||||
_questRegistry.Reloaded += OnReloaded;
|
||||
}
|
||||
|
||||
private void OnReloaded(object? sender, EventArgs e)
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
_gatheringPoints.Clear();
|
||||
LoadGatheringPointsFromAssembly();
|
||||
try
|
||||
{
|
||||
LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "GatheringPoints")));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to load gathering points from user directory (some may have been successfully loaded)");
|
||||
}
|
||||
_logger.LogInformation("Loaded {Count} gathering points in total", _gatheringPoints.Count);
|
||||
}
|
||||
|
||||
[Conditional("RELEASE")]
|
||||
private void LoadGatheringPointsFromAssembly()
|
||||
{
|
||||
_logger.LogInformation("Loading gathering points from assembly");
|
||||
foreach (var (value, value2) in AssemblyGatheringLocationLoader.GetLocations())
|
||||
{
|
||||
_gatheringPoints[new GatheringPointId(value)] = value2;
|
||||
}
|
||||
_logger.LogInformation("Loaded {Count} gathering points from assembly", _gatheringPoints.Count);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void LoadGatheringPointsFromProjectDirectory()
|
||||
{
|
||||
DirectoryInfo directoryInfo = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent;
|
||||
if (directoryInfo == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DirectoryInfo directoryInfo2 = new DirectoryInfo(Path.Combine(directoryInfo.FullName, "GatheringPaths"));
|
||||
if (!directoryInfo2.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (string value in ExpansionData.ExpansionFolders.Values)
|
||||
{
|
||||
LoadFromDirectory(new DirectoryInfo(Path.Combine(directoryInfo2.FullName, value)));
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_gatheringPoints.Clear();
|
||||
_logger.LogError(exception, "Failed to load gathering points from project directory");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadGatheringPointFromStream(string fileName, Stream stream)
|
||||
{
|
||||
GatheringPointId gatheringPointId = ExtractGatheringPointIdFromName(fileName);
|
||||
if (!(gatheringPointId == null))
|
||||
{
|
||||
_gatheringPoints[gatheringPointId] = JsonSerializer.Deserialize<GatheringRoot>(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFromDirectory(DirectoryInfo directory)
|
||||
{
|
||||
if (!directory.Exists)
|
||||
{
|
||||
_logger.LogInformation("Not loading gathering points from {DirectoryName} (doesn't exist)", directory);
|
||||
return;
|
||||
}
|
||||
FileInfo[] files = directory.GetFiles("*.json");
|
||||
foreach (FileInfo fileInfo in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
|
||||
LoadGatheringPointFromStream(fileInfo.Name, stream);
|
||||
}
|
||||
catch (Exception innerException)
|
||||
{
|
||||
throw new InvalidDataException("Unable to load file " + fileInfo.FullName, innerException);
|
||||
}
|
||||
}
|
||||
DirectoryInfo[] directories = directory.GetDirectories();
|
||||
foreach (DirectoryInfo directory2 in directories)
|
||||
{
|
||||
LoadFromDirectory(directory2);
|
||||
}
|
||||
}
|
||||
|
||||
private static GatheringPointId? ExtractGatheringPointIdFromName(string resourceName)
|
||||
{
|
||||
string text = resourceName.Substring(0, resourceName.Length - ".json".Length);
|
||||
text = text.Substring(text.LastIndexOf('.') + 1);
|
||||
if (!text.Contains('_', StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return GatheringPointId.FromString(text.Split('_', 2)[0]);
|
||||
}
|
||||
|
||||
public bool TryGetGatheringPoint(GatheringPointId gatheringPointId, [NotNullWhen(true)] out GatheringRoot? gatheringRoot)
|
||||
{
|
||||
return _gatheringPoints.TryGetValue(gatheringPointId, out gatheringRoot);
|
||||
}
|
||||
|
||||
public bool TryGetGatheringPointId(uint itemId, EClassJob classJobId, [NotNullWhen(true)] out GatheringPointId? gatheringPointId)
|
||||
{
|
||||
switch (classJobId)
|
||||
{
|
||||
case EClassJob.Miner:
|
||||
if (_gatheringData.TryGetMinerGatheringPointByItemId(itemId, out gatheringPointId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
gatheringPointId = (from x in _gatheringPoints
|
||||
where x.Value.ExtraQuestItems.Contains(itemId)
|
||||
select x.Key).FirstOrDefault((GatheringPointId x) => _gatheringData.MinerGatheringPoints.Contains(x));
|
||||
return gatheringPointId != null;
|
||||
case EClassJob.Botanist:
|
||||
if (_gatheringData.TryGetBotanistGatheringPointByItemId(itemId, out gatheringPointId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
gatheringPointId = (from x in _gatheringPoints
|
||||
where x.Value.ExtraQuestItems.Contains(itemId)
|
||||
select x.Key).FirstOrDefault((GatheringPointId x) => _gatheringData.BotanistGatheringPoints.Contains(x));
|
||||
return gatheringPointId != null;
|
||||
default:
|
||||
gatheringPointId = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_questRegistry.Reloaded -= OnReloaded;
|
||||
}
|
||||
}
|
195
Questionable/Questionable.Controller/InterruptHandler.cs
Normal file
195
Questionable/Questionable.Controller/InterruptHandler.cs
Normal file
|
@ -0,0 +1,195 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class InterruptHandler : IDisposable
|
||||
{
|
||||
private unsafe delegate void ProcessActionEffect(uint sourceId, Character* sourceCharacter, Vector3* pos, EffectHeader* effectHeader, EffectEntry* effectArray, ulong* effectTail);
|
||||
|
||||
private static class Signatures
|
||||
{
|
||||
internal const string ActionEffect = "40 ?? 56 57 41 ?? 41 ?? 41 ?? 48 ?? ?? ?? ?? ?? ?? ?? 48";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct EffectEntry
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public EActionEffectType Type;
|
||||
|
||||
[FieldOffset(1)]
|
||||
public byte Param0;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public byte Param1;
|
||||
|
||||
[FieldOffset(3)]
|
||||
public byte Param2;
|
||||
|
||||
[FieldOffset(4)]
|
||||
public byte Mult;
|
||||
|
||||
[FieldOffset(5)]
|
||||
public byte Flags;
|
||||
|
||||
[FieldOffset(6)]
|
||||
public ushort Value;
|
||||
|
||||
public byte AttackType => (byte)(Param1 & 0xF);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Type: {Type}, p0: {Param0:D3}, p1: {Param1:D3}, p2: {Param2:D3} 0x{Param2:X2} '{Convert.ToString(Param2, 2).PadLeft(8, '0')}', mult: {Mult:D3}, flags: {Flags:D3} | {Convert.ToString(Flags, 2).PadLeft(8, '0')}, value: {Value:D6} ATTACK TYPE: {AttackType}";
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct EffectHeader
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public ulong AnimationTargetId;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public uint ActionID;
|
||||
|
||||
[FieldOffset(12)]
|
||||
public uint GlobalEffectCounter;
|
||||
|
||||
[FieldOffset(16)]
|
||||
public float AnimationLockTime;
|
||||
|
||||
[FieldOffset(20)]
|
||||
public uint SomeTargetID;
|
||||
|
||||
[FieldOffset(24)]
|
||||
public ushort SourceSequence;
|
||||
|
||||
[FieldOffset(26)]
|
||||
public ushort Rotation;
|
||||
|
||||
[FieldOffset(28)]
|
||||
public ushort AnimationId;
|
||||
|
||||
[FieldOffset(30)]
|
||||
public byte Variation;
|
||||
|
||||
[FieldOffset(31)]
|
||||
public ActionType ActionType;
|
||||
|
||||
[FieldOffset(33)]
|
||||
public byte TargetCount;
|
||||
}
|
||||
|
||||
private enum EActionEffectType : byte
|
||||
{
|
||||
None = 0,
|
||||
Miss = 1,
|
||||
FullResist = 2,
|
||||
Damage = 3,
|
||||
Heal = 4,
|
||||
BlockedDamage = 5,
|
||||
ParriedDamage = 6,
|
||||
Invulnerable = 7,
|
||||
NoEffectText = 8,
|
||||
Unknown0 = 9,
|
||||
MpLoss = 10,
|
||||
MpGain = 11,
|
||||
TpLoss = 12,
|
||||
TpGain = 13,
|
||||
ApplyStatusEffectTarget = 14,
|
||||
ApplyStatusEffectSource = 15,
|
||||
RecoveredFromStatusEffect = 16,
|
||||
LoseStatusEffectTarget = 17,
|
||||
LoseStatusEffectSource = 18,
|
||||
StatusNoEffect = 20,
|
||||
ThreatPosition = 24,
|
||||
EnmityAmountUp = 25,
|
||||
EnmityAmountDown = 26,
|
||||
StartActionCombo = 27,
|
||||
ComboSucceed = 28,
|
||||
Retaliation = 29,
|
||||
Knockback = 32,
|
||||
Attract1 = 33,
|
||||
Attract2 = 34,
|
||||
Mount = 40,
|
||||
FullResistStatus = 52,
|
||||
FullResistStatus2 = 55,
|
||||
VFX = 59,
|
||||
Gauge = 60,
|
||||
JobGauge = 61,
|
||||
SetModelState = 72,
|
||||
SetHP = 73,
|
||||
PartialInvulnerable = 74,
|
||||
Interrupt = 75
|
||||
}
|
||||
|
||||
private readonly Hook<ProcessActionEffect> _processActionEffectHook;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly TerritoryData _territoryData;
|
||||
|
||||
private readonly ILogger<InterruptHandler> _logger;
|
||||
|
||||
public event EventHandler? Interrupted;
|
||||
|
||||
public unsafe InterruptHandler(IGameInteropProvider gameInteropProvider, IClientState clientState, TerritoryData territoryData, ILogger<InterruptHandler> logger)
|
||||
{
|
||||
_clientState = clientState;
|
||||
_territoryData = territoryData;
|
||||
_logger = logger;
|
||||
_processActionEffectHook = gameInteropProvider.HookFromSignature<ProcessActionEffect>("40 ?? 56 57 41 ?? 41 ?? 41 ?? 48 ?? ?? ?? ?? ?? ?? ?? 48", HandleProcessActionEffect);
|
||||
_processActionEffectHook.Enable();
|
||||
}
|
||||
|
||||
private unsafe void HandleProcessActionEffect(uint sourceId, Character* sourceCharacter, Vector3* pos, EffectHeader* effectHeader, EffectEntry* effectArray, ulong* effectTail)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_territoryData.IsDutyInstance(_clientState.TerritoryType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < effectHeader->TargetCount; i++)
|
||||
{
|
||||
int num = (int)(effectTail[i] & 0xFFFFFFFFu);
|
||||
EffectEntry* ptr = effectArray + 8 * i;
|
||||
bool flag = (uint)num == _clientState.LocalPlayer?.GameObjectId;
|
||||
if (flag)
|
||||
{
|
||||
EActionEffectType type = ptr->Type;
|
||||
bool flag2 = ((type == EActionEffectType.Damage || type - 5 <= EActionEffectType.Miss) ? true : false);
|
||||
flag = flag2;
|
||||
}
|
||||
if (flag)
|
||||
{
|
||||
_logger.LogTrace("Damage action effect on self, from {SourceId} ({EffectType})", sourceId, ptr->Type);
|
||||
this.Interrupted?.Invoke(this, EventArgs.Empty);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Unable to process action effect");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_processActionEffectHook.Original(sourceId, sourceCharacter, pos, effectHeader, effectArray, effectTail);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_processActionEffectHook.Disable();
|
||||
_processActionEffectHook.Dispose();
|
||||
}
|
||||
}
|
257
Questionable/Questionable.Controller/MiniTaskController.cs
Normal file
257
Questionable/Questionable.Controller/MiniTaskController.cs
Normal file
|
@ -0,0 +1,257 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LLib;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.Common;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.Functions;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal abstract class MiniTaskController<T> : IDisposable
|
||||
{
|
||||
protected readonly TaskQueue _taskQueue = new TaskQueue();
|
||||
|
||||
private readonly IChatGui _chatGui;
|
||||
|
||||
private readonly ICondition _condition;
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private readonly InterruptHandler _interruptHandler;
|
||||
|
||||
private readonly ILogger<T> _logger;
|
||||
|
||||
private readonly Regex _actionCanceledText;
|
||||
|
||||
private readonly string _eventCanceledText;
|
||||
|
||||
private readonly string _cantExecuteDueToStatusText;
|
||||
|
||||
protected MiniTaskController(IChatGui chatGui, ICondition condition, IServiceProvider serviceProvider, InterruptHandler interruptHandler, IDataManager dataManager, ILogger<T> logger)
|
||||
{
|
||||
_chatGui = chatGui;
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
_interruptHandler = interruptHandler;
|
||||
_condition = condition;
|
||||
_eventCanceledText = dataManager.GetString(1318u, (LogMessage x) => x.Text);
|
||||
_actionCanceledText = dataManager.GetRegex(1314u, (LogMessage x) => x.Text);
|
||||
_cantExecuteDueToStatusText = dataManager.GetString(7728u, (LogMessage x) => x.Text);
|
||||
_interruptHandler.Interrupted += HandleInterruption;
|
||||
}
|
||||
|
||||
protected virtual void UpdateCurrentTask()
|
||||
{
|
||||
if (_taskQueue.CurrentTaskExecutor == null)
|
||||
{
|
||||
if (!_taskQueue.TryDequeue(out ITask task))
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting task {TaskName}", task.ToString());
|
||||
ITaskExecutor requiredKeyedService = _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(task.GetType());
|
||||
if (requiredKeyedService.Start(task))
|
||||
{
|
||||
_taskQueue.CurrentTaskExecutor = requiredKeyedService;
|
||||
return;
|
||||
}
|
||||
_logger.LogTrace("Task {TaskName} was skipped", task.ToString());
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to start task {TaskName}", task.ToString());
|
||||
_chatGui.PrintError($"Failed to start task '{task}', please check /xllog for details.", "Questionable", 576);
|
||||
Stop("Task failed to start");
|
||||
return;
|
||||
}
|
||||
}
|
||||
ETaskResult eTaskResult;
|
||||
try
|
||||
{
|
||||
if (_taskQueue.CurrentTaskExecutor.WasInterrupted())
|
||||
{
|
||||
InterruptQueueWithCombat();
|
||||
return;
|
||||
}
|
||||
eTaskResult = _taskQueue.CurrentTaskExecutor.Update();
|
||||
}
|
||||
catch (Exception exception2)
|
||||
{
|
||||
_logger.LogError(exception2, "Failed to update task {TaskName}", _taskQueue.CurrentTaskExecutor.CurrentTask.ToString());
|
||||
_chatGui.PrintError($"Failed to update task '{_taskQueue.CurrentTaskExecutor.CurrentTask}', please check /xllog for details.", "Questionable", 576);
|
||||
Stop("Task failed to update");
|
||||
return;
|
||||
}
|
||||
switch (eTaskResult)
|
||||
{
|
||||
case ETaskResult.StillRunning:
|
||||
break;
|
||||
case ETaskResult.SkipRemainingTasksForStep:
|
||||
{
|
||||
_logger.LogInformation("{Task} → {Result}, skipping remaining tasks for step", _taskQueue.CurrentTaskExecutor.CurrentTask, eTaskResult);
|
||||
_taskQueue.CurrentTaskExecutor = null;
|
||||
ITask task3;
|
||||
while (_taskQueue.TryDequeue(out task3))
|
||||
{
|
||||
if ((task3 is ILastTask || task3 is Gather.SkipMarker) ? true : false)
|
||||
{
|
||||
ITaskExecutor requiredKeyedService2 = _serviceProvider.GetRequiredKeyedService<ITaskExecutor>(task3.GetType());
|
||||
requiredKeyedService2.Start(task3);
|
||||
_taskQueue.CurrentTaskExecutor = requiredKeyedService2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ETaskResult.TaskComplete:
|
||||
case ETaskResult.CreateNewTasks:
|
||||
_logger.LogInformation("{Task} → {Result}, remaining tasks: {RemainingTaskCount}", _taskQueue.CurrentTaskExecutor.CurrentTask, eTaskResult, _taskQueue.RemainingTasks.Count());
|
||||
OnTaskComplete(_taskQueue.CurrentTaskExecutor.CurrentTask);
|
||||
if (eTaskResult == ETaskResult.CreateNewTasks && _taskQueue.CurrentTaskExecutor is IExtraTaskCreator extraTaskCreator)
|
||||
{
|
||||
_taskQueue.EnqueueAll(extraTaskCreator.CreateExtraTasks());
|
||||
}
|
||||
_taskQueue.CurrentTaskExecutor = null;
|
||||
break;
|
||||
case ETaskResult.NextStep:
|
||||
{
|
||||
_logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, eTaskResult);
|
||||
ILastTask task2 = (ILastTask)_taskQueue.CurrentTaskExecutor.CurrentTask;
|
||||
_taskQueue.CurrentTaskExecutor = null;
|
||||
OnNextStep(task2);
|
||||
break;
|
||||
}
|
||||
case ETaskResult.End:
|
||||
_logger.LogInformation("{Task} → {Result}", _taskQueue.CurrentTaskExecutor.CurrentTask, eTaskResult);
|
||||
_taskQueue.CurrentTaskExecutor = null;
|
||||
Stop("Task end");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnTaskComplete(ITask task)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnNextStep(ILastTask task)
|
||||
{
|
||||
}
|
||||
|
||||
public abstract void Stop(string label);
|
||||
|
||||
public virtual IList<string> GetRemainingTaskNames()
|
||||
{
|
||||
return _taskQueue.RemainingTasks.Select((ITask x) => x.ToString() ?? "?").ToList();
|
||||
}
|
||||
|
||||
public void InterruptQueueWithCombat()
|
||||
{
|
||||
_logger.LogWarning("Interrupted, attempting to resolve (if in combat)");
|
||||
if (_condition[ConditionFlag.InCombat])
|
||||
{
|
||||
List<ITask> list = new List<ITask>();
|
||||
if (_condition[ConditionFlag.Mounted])
|
||||
{
|
||||
list.Add(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
|
||||
}
|
||||
list.Add(Combat.Factory.CreateTask(null, -1, isLastStep: false, EEnemySpawnType.QuestInterruption, new List<uint>(), new List<QuestWorkValue>(), new List<ComplexCombatData>(), null));
|
||||
list.Add(new WaitAtEnd.WaitDelay());
|
||||
_taskQueue.InterruptWith(list);
|
||||
}
|
||||
else
|
||||
{
|
||||
TaskQueue taskQueue = _taskQueue;
|
||||
int num = 1;
|
||||
List<ITask> list2 = new List<ITask>(num);
|
||||
CollectionsMarshal.SetCount(list2, num);
|
||||
Span<ITask> span = CollectionsMarshal.AsSpan(list2);
|
||||
int index = 0;
|
||||
span[index] = new WaitAtEnd.WaitDelay();
|
||||
taskQueue.InterruptWith(list2);
|
||||
}
|
||||
LogTasksAfterInterruption();
|
||||
}
|
||||
|
||||
private void InterruptWithoutCombat()
|
||||
{
|
||||
if (!(_taskQueue.CurrentTaskExecutor is SinglePlayerDuty.WaitSinglePlayerDutyExecutor))
|
||||
{
|
||||
_logger.LogWarning("Interrupted, attempting to redo previous tasks (not in combat)");
|
||||
TaskQueue taskQueue = _taskQueue;
|
||||
int num = 1;
|
||||
List<ITask> list = new List<ITask>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<ITask> span = CollectionsMarshal.AsSpan(list);
|
||||
int index = 0;
|
||||
span[index] = new WaitAtEnd.WaitDelay();
|
||||
taskQueue.InterruptWith(list);
|
||||
LogTasksAfterInterruption();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogTasksAfterInterruption()
|
||||
{
|
||||
_logger.LogInformation("Remaining tasks after interruption:");
|
||||
foreach (ITask remainingTask in _taskQueue.RemainingTasks)
|
||||
{
|
||||
_logger.LogInformation("- {TaskName}", remainingTask);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnErrorToast(ref SeString message, ref bool isHandled)
|
||||
{
|
||||
if (_taskQueue.CurrentTaskExecutor is IToastAware toastAware && toastAware.OnErrorToast(message))
|
||||
{
|
||||
isHandled = true;
|
||||
}
|
||||
if (isHandled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_actionCanceledText.IsMatch(message.TextValue) && !_condition[ConditionFlag.InFlight])
|
||||
{
|
||||
ITaskExecutor? currentTaskExecutor = _taskQueue.CurrentTaskExecutor;
|
||||
if (currentTaskExecutor != null && currentTaskExecutor.ShouldInterruptOnDamage())
|
||||
{
|
||||
InterruptQueueWithCombat();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (GameFunctions.GameStringEquals(_cantExecuteDueToStatusText, message.TextValue) || GameFunctions.GameStringEquals(_eventCanceledText, message.TextValue))
|
||||
{
|
||||
InterruptWithoutCombat();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleInterruption(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_condition[ConditionFlag.InFlight])
|
||||
{
|
||||
ITaskExecutor? currentTaskExecutor = _taskQueue.CurrentTaskExecutor;
|
||||
if (currentTaskExecutor != null && currentTaskExecutor.ShouldInterruptOnDamage())
|
||||
{
|
||||
InterruptQueueWithCombat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_interruptHandler.Interrupted -= HandleInterruption;
|
||||
}
|
||||
}
|
493
Questionable/Questionable.Controller/MovementController.cs
Normal file
493
Questionable/Questionable.Controller/MovementController.cs
Normal file
|
@ -0,0 +1,493 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Ipc.Exceptions;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Controller.NavigationOverrides;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
using Questionable.Functions;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Common;
|
||||
using Questionable.Model.Common.Converter;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class MovementController : IDisposable
|
||||
{
|
||||
public sealed record DestinationData(EMovementType MovementType, uint? DataId, Vector3 Position, float StopDistance, bool IsFlying, bool CanSprint, float VerticalStopDistance, bool Land, bool UseNavmesh)
|
||||
{
|
||||
public int NavmeshCalculations { get; set; }
|
||||
|
||||
public List<Vector3> PartialRoute { get; } = new List<Vector3>();
|
||||
|
||||
public LastWaypointData? LastWaypoint { get; set; }
|
||||
|
||||
public bool ShouldRecalculateNavmesh()
|
||||
{
|
||||
return NavmeshCalculations < 10;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LastWaypointData(Vector3 Position)
|
||||
{
|
||||
public long UpdatedAt { get; set; }
|
||||
|
||||
public double Distance2DAtLastUpdate { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PathfindingFailedException : Exception
|
||||
{
|
||||
public PathfindingFailedException()
|
||||
{
|
||||
}
|
||||
|
||||
public PathfindingFailedException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public PathfindingFailedException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public const float DefaultVerticalInteractionDistance = 1.95f;
|
||||
|
||||
private readonly NavmeshIpc _navmeshIpc;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
|
||||
private readonly ChatFunctions _chatFunctions;
|
||||
|
||||
private readonly ICondition _condition;
|
||||
|
||||
private readonly MovementOverrideController _movementOverrideController;
|
||||
|
||||
private readonly AetheryteData _aetheryteData;
|
||||
|
||||
private readonly ILogger<MovementController> _logger;
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
private Task<List<Vector3>>? _pathfindTask;
|
||||
|
||||
public bool IsNavmeshReady
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return _navmeshIpc.IsReady;
|
||||
}
|
||||
catch (IpcNotReadyError)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPathRunning
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return _navmeshIpc.IsPathRunning;
|
||||
}
|
||||
catch (IpcNotReadyError)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPathfinding
|
||||
{
|
||||
get
|
||||
{
|
||||
Task<List<Vector3>> pathfindTask = _pathfindTask;
|
||||
if (pathfindTask != null)
|
||||
{
|
||||
return !pathfindTask.IsCompleted;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public DestinationData? Destination { get; set; }
|
||||
|
||||
public DateTime MovementStartedAt { get; private set; } = DateTime.Now;
|
||||
|
||||
public int BuiltNavmeshPercent => _navmeshIpc.GetBuildProgress();
|
||||
|
||||
public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, GameFunctions gameFunctions, ChatFunctions chatFunctions, ICondition condition, MovementOverrideController movementOverrideController, AetheryteData aetheryteData, ILogger<MovementController> logger)
|
||||
{
|
||||
_navmeshIpc = navmeshIpc;
|
||||
_clientState = clientState;
|
||||
_gameFunctions = gameFunctions;
|
||||
_chatFunctions = chatFunctions;
|
||||
_condition = condition;
|
||||
_movementOverrideController = movementOverrideController;
|
||||
_aetheryteData = aetheryteData;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public unsafe void Update()
|
||||
{
|
||||
if (_pathfindTask != null && Destination != null)
|
||||
{
|
||||
if (_pathfindTask.IsCompletedSuccessfully)
|
||||
{
|
||||
_logger.LogInformation("Pathfinding complete, got {Count} points", _pathfindTask.Result.Count);
|
||||
if (_pathfindTask.Result.Count == 0)
|
||||
{
|
||||
ResetPathfinding();
|
||||
throw new PathfindingFailedException();
|
||||
}
|
||||
List<Vector3> list = _pathfindTask.Result.Skip(1).ToList();
|
||||
Vector3 p = _clientState.LocalPlayer?.Position ?? list[0];
|
||||
if (Destination.IsFlying && !_condition[ConditionFlag.InFlight] && _condition[ConditionFlag.Mounted] && (IsOnFlightPath(p) || list.Any(IsOnFlightPath)))
|
||||
{
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
||||
}
|
||||
if (!Destination.IsFlying)
|
||||
{
|
||||
(List<Vector3>, bool) tuple = _movementOverrideController.AdjustPath(list);
|
||||
(list, _) = tuple;
|
||||
if (tuple.Item2 && Destination.ShouldRecalculateNavmesh())
|
||||
{
|
||||
Destination.NavmeshCalculations++;
|
||||
Destination.PartialRoute.AddRange(list);
|
||||
_logger.LogInformation("Running navmesh recalculation with fudged point ({From} to {To})", list.Last(), Destination.Position);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L));
|
||||
_pathfindTask = _navmeshIpc.Pathfind(list.Last(), Destination.Position, Destination.IsFlying, _cancellationTokenSource.Token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
list = Destination.PartialRoute.Concat(list).ToList();
|
||||
_logger.LogInformation("Navigating via route: [{Route}]", string.Join(" → ", _pathfindTask.Result.Select((Vector3 x) => x.ToString("G", CultureInfo.InvariantCulture))));
|
||||
_navmeshIpc.MoveTo(list, Destination.IsFlying);
|
||||
MovementStartedAt = DateTime.Now;
|
||||
ResetPathfinding();
|
||||
}
|
||||
else if (_pathfindTask.IsCompleted)
|
||||
{
|
||||
_logger.LogWarning("Unable to complete pathfinding task");
|
||||
ResetPathfinding();
|
||||
throw new PathfindingFailedException();
|
||||
}
|
||||
}
|
||||
if (!IsPathRunning || !(Destination != null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_gameFunctions.IsLoadingScreenVisible())
|
||||
{
|
||||
_logger.LogInformation("Stopping movement, loading screen visible");
|
||||
Stop();
|
||||
return;
|
||||
}
|
||||
DestinationData destination = Destination;
|
||||
if ((object)destination != null && destination.IsFlying && _condition[ConditionFlag.Swimming])
|
||||
{
|
||||
_logger.LogInformation("Flying but swimming, restarting as non-flying path...");
|
||||
Restart(Destination);
|
||||
return;
|
||||
}
|
||||
destination = Destination;
|
||||
if ((object)destination != null && destination.IsFlying && !_condition[ConditionFlag.Mounted])
|
||||
{
|
||||
_logger.LogInformation("Flying but not mounted, restarting as non-flying path...");
|
||||
Restart(Destination);
|
||||
return;
|
||||
}
|
||||
Vector3 vector = _clientState.LocalPlayer?.Position ?? Vector3.Zero;
|
||||
if (Destination.MovementType == EMovementType.Landing)
|
||||
{
|
||||
if (!_condition[ConditionFlag.InFlight])
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else if ((vector - Destination.Position).Length() < Destination.StopDistance)
|
||||
{
|
||||
if (vector.Y - Destination.Position.Y <= Destination.VerticalStopDistance)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
else if (Destination.DataId.HasValue)
|
||||
{
|
||||
IGameObject gameObject = _gameFunctions.FindObjectByDataId(Destination.DataId.Value);
|
||||
if ((gameObject is ICharacter || gameObject is IEventObj) ? true : false)
|
||||
{
|
||||
if (Math.Abs(vector.Y - gameObject.Position.Y) < 1.95f)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else if (gameObject != null && gameObject.ObjectKind == ObjectKind.Aetheryte)
|
||||
{
|
||||
if (AetheryteConverter.IsLargeAetheryte((EAetheryteLocation)Destination.DataId.Value))
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
else if (Math.Abs(vector.Y - gameObject.Position.Y) < 1.95f)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
List<Vector3> waypoints = _navmeshIpc.GetWaypoints();
|
||||
Vector3? vector2 = _clientState.LocalPlayer?.Position;
|
||||
if (vector2.HasValue && (!Destination.ShouldRecalculateNavmesh() || !RecalculateNavmesh(waypoints, vector2.Value)) && !Destination.IsFlying && !_condition[ConditionFlag.Mounted] && !_gameFunctions.HasStatusPreventingSprint() && Destination.CanSprint)
|
||||
{
|
||||
TriggerSprintIfNeeded(waypoints, vector2.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Restart(DestinationData destination)
|
||||
{
|
||||
Stop();
|
||||
if (destination.UseNavmesh)
|
||||
{
|
||||
NavigateTo(EMovementType.None, destination.DataId, destination.Position, fly: false, sprint: false, destination.StopDistance, destination.VerticalStopDistance);
|
||||
return;
|
||||
}
|
||||
uint? dataId = destination.DataId;
|
||||
int num = 1;
|
||||
List<Vector3> list = new List<Vector3>(num);
|
||||
CollectionsMarshal.SetCount(list, num);
|
||||
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
|
||||
int index = 0;
|
||||
span[index] = destination.Position;
|
||||
NavigateTo(EMovementType.None, dataId, list, fly: false, sprint: false, destination.StopDistance, destination.VerticalStopDistance);
|
||||
}
|
||||
|
||||
private bool IsOnFlightPath(Vector3 p)
|
||||
{
|
||||
Vector3? pointOnFloor = _navmeshIpc.GetPointOnFloor(p, unlandable: true);
|
||||
if (pointOnFloor.HasValue)
|
||||
{
|
||||
return Math.Abs(pointOnFloor.Value.Y - p.Y) > 0.5f;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[MemberNotNull("Destination")]
|
||||
private void PrepareNavigation(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance, float verticalStopDistance, bool land, bool useNavmesh)
|
||||
{
|
||||
ResetPathfinding();
|
||||
if (InputManager.IsAutoRunning())
|
||||
{
|
||||
_logger.LogInformation("Turning off auto-move");
|
||||
_chatFunctions.ExecuteCommand("/automove off");
|
||||
}
|
||||
Destination = new DestinationData(type, dataId, to, stopDistance ?? 2.8f, fly, sprint, verticalStopDistance, land, useNavmesh);
|
||||
MovementStartedAt = DateTime.MaxValue;
|
||||
}
|
||||
|
||||
public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null, float? verticalStopDistance = null, bool land = false)
|
||||
{
|
||||
fly |= _condition[ConditionFlag.Diving];
|
||||
if (fly && land)
|
||||
{
|
||||
Vector3 vector = to;
|
||||
vector.Y = to.Y + 2.6f;
|
||||
to = vector;
|
||||
}
|
||||
PrepareNavigation(type, dataId, to, fly, sprint, stopDistance, verticalStopDistance ?? 1.95f, land, useNavmesh: true);
|
||||
_logger.LogInformation("Pathfinding to {Destination}", Destination);
|
||||
Destination.NavmeshCalculations++;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L));
|
||||
Vector3 vector2 = _clientState.LocalPlayer.Position;
|
||||
if (fly && _aetheryteData.CalculateDistance(vector2, _clientState.TerritoryType, EAetheryteLocation.CoerthasCentralHighlandsCampDragonhead) < 11f)
|
||||
{
|
||||
Vector3 vector = vector2;
|
||||
vector.Y = vector2.Y + 1f;
|
||||
vector2 = vector;
|
||||
_logger.LogInformation("Using modified start position for flying pathfinding: {StartPosition}", vector2.ToString("G", CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (fly)
|
||||
{
|
||||
Vector3 vector = vector2;
|
||||
vector.Y = vector2.Y + 0.2f;
|
||||
vector2 = vector;
|
||||
}
|
||||
_pathfindTask = _navmeshIpc.Pathfind(vector2, to, fly, _cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public void NavigateTo(EMovementType type, uint? dataId, List<Vector3> to, bool fly, bool sprint, float? stopDistance, float? verticalStopDistance = null, bool land = false)
|
||||
{
|
||||
fly |= _condition[ConditionFlag.Diving];
|
||||
if (fly && land && to.Count > 0)
|
||||
{
|
||||
int index = to.Count - 1;
|
||||
Vector3 value = to[to.Count - 1];
|
||||
value.Y = to[to.Count - 1].Y + 2.6f;
|
||||
to[index] = value;
|
||||
}
|
||||
PrepareNavigation(type, dataId, to.Last(), fly, sprint, stopDistance, verticalStopDistance ?? 1.95f, land, useNavmesh: false);
|
||||
_logger.LogInformation("Moving to {Destination}", Destination);
|
||||
_navmeshIpc.MoveTo(to, fly);
|
||||
MovementStartedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
public void ResetPathfinding()
|
||||
{
|
||||
if (_cancellationTokenSource != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
_cancellationTokenSource.Dispose();
|
||||
}
|
||||
_pathfindTask = null;
|
||||
}
|
||||
|
||||
private unsafe bool RecalculateNavmesh(List<Vector3> navPoints, Vector3 start)
|
||||
{
|
||||
if (Destination == null)
|
||||
{
|
||||
throw new InvalidOperationException("Destination is null");
|
||||
}
|
||||
if (DateTime.Now - MovementStartedAt <= TimeSpan.FromSeconds(5L))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3 vector = navPoints.FirstOrDefault();
|
||||
if (vector == default(Vector3))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
float num = Vector2.Distance(new Vector2(start.X, start.Z), new Vector2(vector.X, vector.Z));
|
||||
if (Destination.LastWaypoint == null || (Destination.LastWaypoint.Position - vector).Length() > 0.1f)
|
||||
{
|
||||
Destination.LastWaypoint = new LastWaypointData(vector)
|
||||
{
|
||||
Distance2DAtLastUpdate = num,
|
||||
UpdatedAt = Environment.TickCount64
|
||||
};
|
||||
return false;
|
||||
}
|
||||
if (Environment.TickCount64 - Destination.LastWaypoint.UpdatedAt > 500)
|
||||
{
|
||||
if (Math.Abs((double)num - Destination.LastWaypoint.Distance2DAtLastUpdate) < 0.5)
|
||||
{
|
||||
int navmeshCalculations = Destination.NavmeshCalculations;
|
||||
if (navmeshCalculations % 6 == 1)
|
||||
{
|
||||
_logger.LogWarning("Jumping to try and resolve navmesh problem (n = {Calculations})", navmeshCalculations);
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
||||
Destination.NavmeshCalculations++;
|
||||
Destination.LastWaypoint.UpdatedAt = Environment.TickCount64;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Recalculating navmesh (n = {Calculations})", navmeshCalculations);
|
||||
Restart(Destination);
|
||||
}
|
||||
Destination.NavmeshCalculations = navmeshCalculations + 1;
|
||||
return true;
|
||||
}
|
||||
Destination.LastWaypoint.Distance2DAtLastUpdate = num;
|
||||
Destination.LastWaypoint.UpdatedAt = Environment.TickCount64;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe void TriggerSprintIfNeeded(IEnumerable<Vector3> navPoints, Vector3 start)
|
||||
{
|
||||
float num = 0f;
|
||||
foreach (Vector3 navPoint in navPoints)
|
||||
{
|
||||
num += (start - navPoint).Length();
|
||||
start = navPoint;
|
||||
}
|
||||
float num2 = 100f;
|
||||
bool flag = !_gameFunctions.HasStatus(EStatus.Jog);
|
||||
if (flag)
|
||||
{
|
||||
bool flag2;
|
||||
switch (GameMain.Instance()->CurrentTerritoryIntendedUseId)
|
||||
{
|
||||
case 0:
|
||||
case 7:
|
||||
case 13:
|
||||
case 14:
|
||||
case 15:
|
||||
case 19:
|
||||
case 23:
|
||||
case 29:
|
||||
flag2 = true;
|
||||
break;
|
||||
default:
|
||||
flag2 = false;
|
||||
break;
|
||||
}
|
||||
flag = flag2;
|
||||
}
|
||||
if (flag)
|
||||
{
|
||||
num2 = 30f;
|
||||
}
|
||||
if (num > num2 && ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 4u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
|
||||
{
|
||||
_logger.LogInformation("Triggering Sprint");
|
||||
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 4u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_navmeshIpc.Stop();
|
||||
ResetPathfinding();
|
||||
Destination = null;
|
||||
if (InputManager.IsAutoRunning())
|
||||
{
|
||||
_logger.LogInformation("Turning off auto-move [stop]");
|
||||
_chatFunctions.ExecuteCommand("/automove off");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
1100
Questionable/Questionable.Controller/QuestController.cs
Normal file
1100
Questionable/Questionable.Controller/QuestController.cs
Normal file
File diff suppressed because it is too large
Load diff
290
Questionable/Questionable.Controller/QuestRegistry.cs
Normal file
290
Questionable/Questionable.Controller/QuestRegistry.cs
Normal file
|
@ -0,0 +1,290 @@
|
|||
#define RELEASE
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LLib.GameData;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Questionable.Data;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
using Questionable.QuestPaths;
|
||||
using Questionable.Validation;
|
||||
using Questionable.Validation.Validators;
|
||||
|
||||
namespace Questionable.Controller;
|
||||
|
||||
internal sealed class QuestRegistry
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
|
||||
private readonly QuestData _questData;
|
||||
|
||||
private readonly QuestValidator _questValidator;
|
||||
|
||||
private readonly JsonSchemaValidator _jsonSchemaValidator;
|
||||
|
||||
private readonly ILogger<QuestRegistry> _logger;
|
||||
|
||||
private readonly TerritoryData _territoryData;
|
||||
|
||||
private readonly IChatGui _chatGui;
|
||||
|
||||
private readonly ICallGateProvider<object> _reloadDataIpc;
|
||||
|
||||
private readonly Dictionary<ElementId, Quest> _quests = new Dictionary<ElementId, Quest>();
|
||||
|
||||
private readonly Dictionary<uint, (ElementId QuestId, QuestStep Step)> _contentFinderConditionIds = new Dictionary<uint, (ElementId, QuestStep)>();
|
||||
|
||||
private readonly List<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> _lowPriorityContentFinderConditionQuests = new List<(uint, ElementId, int)>();
|
||||
|
||||
public IEnumerable<Quest> AllQuests => _quests.Values;
|
||||
|
||||
public int Count => _quests.Count<KeyValuePair<ElementId, Quest>>((KeyValuePair<ElementId, Quest> x) => !x.Value.Root.Disabled);
|
||||
|
||||
public int ValidationIssueCount => _questValidator.IssueCount;
|
||||
|
||||
public int ValidationErrorCount => _questValidator.ErrorCount;
|
||||
|
||||
public IReadOnlyList<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> LowPriorityContentFinderConditionQuests => _lowPriorityContentFinderConditionQuests;
|
||||
|
||||
public event EventHandler? Reloaded;
|
||||
|
||||
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator, ILogger<QuestRegistry> logger, TerritoryData territoryData, IChatGui chatGui)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_questData = questData;
|
||||
_questValidator = questValidator;
|
||||
_jsonSchemaValidator = jsonSchemaValidator;
|
||||
_logger = logger;
|
||||
_territoryData = territoryData;
|
||||
_chatGui = chatGui;
|
||||
_reloadDataIpc = _pluginInterface.GetIpcProvider<object>("Questionable.ReloadData");
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
_questValidator.Reset();
|
||||
_quests.Clear();
|
||||
_contentFinderConditionIds.Clear();
|
||||
_lowPriorityContentFinderConditionQuests.Clear();
|
||||
LoadQuestsFromAssembly();
|
||||
try
|
||||
{
|
||||
LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")), Quest.ESource.UserDirectory);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to load all quests from user directory (some may have been successfully loaded)");
|
||||
}
|
||||
LoadCfcIds();
|
||||
ValidateQuests();
|
||||
this.Reloaded?.Invoke(this, EventArgs.Empty);
|
||||
try
|
||||
{
|
||||
_reloadDataIpc.SendMessage();
|
||||
}
|
||||
catch (Exception exception2)
|
||||
{
|
||||
_logger.LogWarning(exception2, "Error during Reload.SendMessage IPC");
|
||||
}
|
||||
_logger.LogInformation("Loaded {Count} quests in total", _quests.Count);
|
||||
}
|
||||
|
||||
[Conditional("RELEASE")]
|
||||
private void LoadQuestsFromAssembly()
|
||||
{
|
||||
_logger.LogInformation("Loading quests from assembly");
|
||||
foreach (var (elementId2, root) in AssemblyQuestLoader.GetQuests())
|
||||
{
|
||||
try
|
||||
{
|
||||
IQuestInfo questInfo = _questData.GetQuestInfo(elementId2);
|
||||
Quest quest = new Quest
|
||||
{
|
||||
Id = elementId2,
|
||||
Root = root,
|
||||
Info = questInfo,
|
||||
Source = Quest.ESource.Assembly
|
||||
};
|
||||
_quests[quest.Id] = quest;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Not loading unknown quest {QuestId} from assembly: {Message}", elementId2, ex.Message);
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("Loaded {Count} quests from assembly", _quests.Count);
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void LoadQuestsFromProjectDirectory()
|
||||
{
|
||||
DirectoryInfo directoryInfo = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent;
|
||||
if (directoryInfo == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DirectoryInfo directoryInfo2 = new DirectoryInfo(Path.Combine(directoryInfo.FullName, "QuestPaths"));
|
||||
if (!directoryInfo2.Exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (string value in ExpansionData.ExpansionFolders.Values)
|
||||
{
|
||||
LoadFromDirectory(new DirectoryInfo(Path.Combine(directoryInfo2.FullName, value)), Quest.ESource.ProjectDirectory, LogLevel.Trace);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_quests.Clear();
|
||||
_chatGui.PrintError("Unable to load quests - " + ex.GetType().Name + ": " + ex.Message, "Questionable", 576);
|
||||
_logger.LogError(ex, "Failed to load quests from project directory");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCfcIds()
|
||||
{
|
||||
foreach (Quest value in _quests.Values)
|
||||
{
|
||||
foreach (QuestSequence item in value.AllSequences())
|
||||
{
|
||||
foreach (QuestStep item2 in item.Steps.Where(delegate(QuestStep x)
|
||||
{
|
||||
EInteractionType interactionType = x.InteractionType;
|
||||
return (uint)(interactionType - 18) <= 1u;
|
||||
}))
|
||||
{
|
||||
if (item2 != null && item2.InteractionType == EInteractionType.Duty)
|
||||
{
|
||||
DutyOptions dutyOptions = item2.DutyOptions;
|
||||
if (dutyOptions != null)
|
||||
{
|
||||
_contentFinderConditionIds[dutyOptions.ContentFinderConditionId] = (value.Id, item2);
|
||||
if (dutyOptions.LowPriority)
|
||||
{
|
||||
_lowPriorityContentFinderConditionQuests.Add((dutyOptions.ContentFinderConditionId, value.Id, item.Sequence));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (item2.InteractionType == EInteractionType.SinglePlayerDuty && _territoryData.TryGetContentFinderConditionForSoloInstance(value.Id, item2.SinglePlayerDutyIndex, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
|
||||
{
|
||||
_contentFinderConditionIds[contentFinderConditionData.ContentFinderConditionId] = (value.Id, item2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateQuests()
|
||||
{
|
||||
_questValidator.Validate(_quests.Values.Where((Quest x) => x.Source != Quest.ESource.Assembly).ToList());
|
||||
}
|
||||
|
||||
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source)
|
||||
{
|
||||
if (source == Quest.ESource.UserDirectory)
|
||||
{
|
||||
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
|
||||
}
|
||||
ElementId elementId = ExtractQuestIdFromName(fileName);
|
||||
if (!(elementId == null))
|
||||
{
|
||||
JsonNode jsonNode = JsonNode.Parse(stream);
|
||||
_jsonSchemaValidator.Enqueue(elementId, jsonNode);
|
||||
QuestRoot root = jsonNode.Deserialize<QuestRoot>();
|
||||
IQuestInfo questInfo = _questData.GetQuestInfo(elementId);
|
||||
Quest quest = new Quest
|
||||
{
|
||||
Id = elementId,
|
||||
Root = root,
|
||||
Info = questInfo,
|
||||
Source = source
|
||||
};
|
||||
_quests[quest.Id] = quest;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFromDirectory(DirectoryInfo directory, Quest.ESource source, LogLevel logLevel = LogLevel.Information)
|
||||
{
|
||||
if (!directory.Exists)
|
||||
{
|
||||
_logger.LogInformation("Not loading quests from {DirectoryName} (doesn't exist)", directory);
|
||||
return;
|
||||
}
|
||||
if (source == Quest.ESource.UserDirectory)
|
||||
{
|
||||
_logger.Log(logLevel, "Loading quests from {DirectoryName}", directory);
|
||||
}
|
||||
FileInfo[] files = directory.GetFiles("*.json");
|
||||
foreach (FileInfo fileInfo in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
|
||||
LoadQuestFromStream(fileInfo.Name, stream, source);
|
||||
}
|
||||
catch (Exception innerException)
|
||||
{
|
||||
throw new InvalidDataException("Unable to load file " + fileInfo.FullName, innerException);
|
||||
}
|
||||
}
|
||||
DirectoryInfo[] directories = directory.GetDirectories();
|
||||
foreach (DirectoryInfo directory2 in directories)
|
||||
{
|
||||
LoadFromDirectory(directory2, source, logLevel);
|
||||
}
|
||||
}
|
||||
|
||||
private static ElementId? ExtractQuestIdFromName(string resourceName)
|
||||
{
|
||||
string text = resourceName.Substring(0, resourceName.Length - ".json".Length);
|
||||
text = text.Substring(text.LastIndexOf('.') + 1);
|
||||
if (!text.Contains('_', StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return ElementId.FromString(text.Split('_', 2)[0]);
|
||||
}
|
||||
|
||||
public bool IsKnownQuest(ElementId questId)
|
||||
{
|
||||
return _quests.ContainsKey(questId);
|
||||
}
|
||||
|
||||
public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest)
|
||||
{
|
||||
return _quests.TryGetValue(questId, out quest);
|
||||
}
|
||||
|
||||
public List<QuestInfo> GetKnownClassJobQuests(EClassJob classJob, bool includeRoleQuests = true)
|
||||
{
|
||||
List<QuestInfo> list = _questData.GetClassJobQuests(classJob, includeRoleQuests).ToList();
|
||||
if (classJob.AsJob() != classJob)
|
||||
{
|
||||
list.AddRange(_questData.GetClassJobQuests(classJob.AsJob(), includeRoleQuests));
|
||||
}
|
||||
return list.Where((QuestInfo x) => IsKnownQuest(x.QuestId)).ToList();
|
||||
}
|
||||
|
||||
public bool TryGetDutyByContentFinderConditionId(uint cfcId, [NotNullWhen(true)] out DutyOptions? dutyOptions)
|
||||
{
|
||||
if (_contentFinderConditionIds.TryGetValue(cfcId, out (ElementId, QuestStep) value))
|
||||
{
|
||||
dutyOptions = value.Item2.DutyOptions;
|
||||
return dutyOptions != null;
|
||||
}
|
||||
dutyOptions = null;
|
||||
return false;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue