muffin v6.12
This commit is contained in:
parent
e786325cda
commit
0950798597
64 changed files with 40100 additions and 58121 deletions
|
@ -33,6 +33,8 @@ internal sealed class CombatController : IDisposable
|
|||
public required CombatData Data { get; init; }
|
||||
|
||||
public required DateTime LastDistanceCheck { get; set; }
|
||||
|
||||
public bool HasAttemptedFateSync { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CombatData
|
||||
|
@ -70,6 +72,8 @@ internal sealed class CombatController : IDisposable
|
|||
|
||||
private readonly MovementController _movementController;
|
||||
|
||||
private readonly GameFunctions _gameFunctions;
|
||||
|
||||
private readonly ITargetManager _targetManager;
|
||||
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
@ -92,10 +96,11 @@ internal sealed class CombatController : IDisposable
|
|||
|
||||
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)
|
||||
public CombatController(IEnumerable<ICombatModule> combatModules, MovementController movementController, GameFunctions gameFunctions, ITargetManager targetManager, IObjectTable objectTable, ICondition condition, IClientState clientState, QuestFunctions questFunctions, ILogger<CombatController> logger)
|
||||
{
|
||||
_combatModules = combatModules.ToList();
|
||||
_movementController = movementController;
|
||||
_gameFunctions = gameFunctions;
|
||||
_targetManager = targetManager;
|
||||
_objectTable = objectTable;
|
||||
_condition = condition;
|
||||
|
@ -257,7 +262,7 @@ internal sealed class CombatController : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
return (from x in _objectTable
|
||||
IGameObject gameObject = (from x in _objectTable
|
||||
select new
|
||||
{
|
||||
GameObject = x,
|
||||
|
@ -267,6 +272,17 @@ internal sealed class CombatController : IDisposable
|
|||
where x.Priority > 0
|
||||
orderby x.Priority descending, x.Distance
|
||||
select x.GameObject).FirstOrDefault();
|
||||
if (gameObject != null && _currentFight.Data.SpawnType == EEnemySpawnType.FateEnemies && !_currentFight.HasAttemptedFateSync)
|
||||
{
|
||||
ushort currentFateId = _gameFunctions.GetCurrentFateId();
|
||||
if (currentFateId != 0)
|
||||
{
|
||||
_logger.LogInformation("Checking FATE sync for FATE {FateId}", currentFateId);
|
||||
_gameFunctions.SyncToFate(currentFateId);
|
||||
_currentFight.HasAttemptedFateSync = true;
|
||||
}
|
||||
}
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
public unsafe (int Priority, string Reason) GetKillPriority(IGameObject gameObject)
|
||||
|
@ -316,12 +332,12 @@ internal sealed class CombatController : IDisposable
|
|||
}
|
||||
List<ComplexCombatData> complexCombatDatas = _currentFight.Data.ComplexCombatDatas;
|
||||
GameObject* address = (GameObject*)gameObject.Address;
|
||||
if (address->FateId != 0 && gameObject.TargetObjectId != _clientState.LocalPlayer?.GameObjectId)
|
||||
if (address->FateId != 0 && _currentFight.Data.SpawnType != EEnemySpawnType.FateEnemies && 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);
|
||||
bool flag = _currentFight.Data.SpawnType != EEnemySpawnType.FinishCombatIfAny && (_currentFight.Data.SpawnType != EEnemySpawnType.OverworldEnemies || !(Vector3.Distance(value, battleNpc.Position) >= 50f)) && _currentFight.Data.SpawnType != EEnemySpawnType.FateEnemies;
|
||||
if (complexCombatDatas.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < complexCombatDatas.Count; i++)
|
||||
|
|
|
@ -5,6 +5,7 @@ using System.Linq;
|
|||
using System.Numerics;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.Gui.Toast;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
@ -289,11 +290,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
_safeAnimationEnd = DateTime.Now.AddSeconds(1f + num);
|
||||
}
|
||||
}
|
||||
UpdateCurrentQuest();
|
||||
if (AutomationType == EAutomationType.Manual && !IsRunning && !IsQuestWindowOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UpdateCurrentQuest();
|
||||
if (!_clientState.IsLoggedIn)
|
||||
{
|
||||
StopAllDueToConditionFailed("Logged out");
|
||||
|
@ -320,6 +321,17 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (_configuration.Stop.Enabled && _configuration.Stop.SequenceToStopAfter && CurrentQuest != null)
|
||||
{
|
||||
int sequence = CurrentQuest.Sequence;
|
||||
if (sequence >= _configuration.Stop.TargetSequence && IsRunning)
|
||||
{
|
||||
_logger.LogInformation("Reached quest sequence stop condition (sequence: {CurrentSequence}, target: {TargetSequence})", sequence, _configuration.Stop.TargetSequence);
|
||||
_chatGui.Print($"Quest sequence {sequence} reached target sequence {_configuration.Stop.TargetSequence}.", "Questionable", 576);
|
||||
Stop($"Sequence stop condition reached [{sequence}]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
bool flag = AutomationType == EAutomationType.Automatic && (_taskQueue.AllTasksComplete || _taskQueue.CurrentTaskExecutor?.CurrentTask is WaitAtEnd.WaitQuestAccepted);
|
||||
bool flag2;
|
||||
if (flag)
|
||||
|
@ -331,14 +343,14 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
if (step == 0 || step == 255)
|
||||
{
|
||||
flag2 = true;
|
||||
goto IL_02de;
|
||||
goto IL_0422;
|
||||
}
|
||||
}
|
||||
flag2 = false;
|
||||
goto IL_02de;
|
||||
goto IL_0422;
|
||||
}
|
||||
goto IL_02e2;
|
||||
IL_02e2:
|
||||
goto IL_0426;
|
||||
IL_0426:
|
||||
if (flag && DateTime.Now >= CurrentQuest.StepProgress.StartedAt.AddSeconds(15.0))
|
||||
{
|
||||
lock (_progressLock)
|
||||
|
@ -354,23 +366,32 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
UpdateCurrentTask();
|
||||
}
|
||||
return;
|
||||
IL_02de:
|
||||
IL_0422:
|
||||
flag = flag2;
|
||||
goto IL_02e2;
|
||||
goto IL_0426;
|
||||
}
|
||||
|
||||
private void CheckAutoRefreshCondition()
|
||||
{
|
||||
if (!_configuration.General.AutoStepRefreshEnabled || AutomationType != EAutomationType.Automatic || !IsRunning || CurrentQuest == null || !_clientState.IsLoggedIn || _clientState.LocalPlayer == null || DateTime.Now < _lastAutoRefresh.AddSeconds(5.0))
|
||||
if (!ShouldCheckAutoRefresh() || DateTime.Now < _lastAutoRefresh.AddSeconds(5.0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_condition[ConditionFlag.InCombat] || _condition[ConditionFlag.Unconscious] || _condition[ConditionFlag.BoundByDuty] || _condition[ConditionFlag.InDeepDungeon] || _condition[ConditionFlag.WatchingCutscene] || _condition[ConditionFlag.WatchingCutscene78] || _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || _gameFunctions.IsOccupied() || _movementController.IsPathfinding || _movementController.IsPathRunning || !_movementController.IsNavmeshReady || _taskQueue.CurrentTaskExecutor?.CurrentTask.GetType().Namespace == typeof(WaitAtEnd).Namespace || DateTime.Now < _safeAnimationEnd)
|
||||
if (ShouldPreventAutoRefresh())
|
||||
{
|
||||
_lastProgressUpdate = DateTime.Now;
|
||||
return;
|
||||
}
|
||||
Vector3 position = _clientState.LocalPlayer.Position;
|
||||
IPlayerCharacter localPlayer = _clientState.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3 position = localPlayer.Position;
|
||||
if (CurrentQuest == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ElementId id = CurrentQuest.Quest.Id;
|
||||
byte sequence = CurrentQuest.Sequence;
|
||||
int step = CurrentQuest.Step;
|
||||
|
@ -395,6 +416,79 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
}
|
||||
}
|
||||
|
||||
private bool ShouldCheckAutoRefresh()
|
||||
{
|
||||
if (_configuration.General.AutoStepRefreshEnabled && AutomationType == EAutomationType.Automatic && IsRunning && CurrentQuest != null && _clientState.IsLoggedIn)
|
||||
{
|
||||
return _clientState.LocalPlayer != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ShouldPreventAutoRefresh()
|
||||
{
|
||||
if (HasWaitingTasks())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (HasManualInterventionStep())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (HasSystemConditionsPreventingRefresh())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (HasConfigurationConditionsPreventingRefresh())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool HasWaitingTasks()
|
||||
{
|
||||
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
|
||||
if (task is WaitAtEnd.WaitObjectAtPosition || task is WaitAtEnd.WaitForCompletionFlags)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool HasManualInterventionStep()
|
||||
{
|
||||
switch (GetNextStep().Step?.InteractionType)
|
||||
{
|
||||
case EInteractionType.WaitForManualProgress:
|
||||
case EInteractionType.Duty:
|
||||
case EInteractionType.SinglePlayerDuty:
|
||||
case EInteractionType.Snipe:
|
||||
case EInteractionType.Instruction:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasSystemConditionsPreventingRefresh()
|
||||
{
|
||||
if (_movementController.IsNavmeshReady && !_condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Unconscious] && !_condition[ConditionFlag.BoundByDuty] && !_condition[ConditionFlag.InDeepDungeon] && !_condition[ConditionFlag.WatchingCutscene] && !_condition[ConditionFlag.WatchingCutscene78] && !_condition[ConditionFlag.BetweenAreas] && !_condition[ConditionFlag.BetweenAreas51] && !_gameFunctions.IsOccupied() && !_movementController.IsPathfinding && !_movementController.IsPathRunning)
|
||||
{
|
||||
return DateTime.Now < _safeAnimationEnd;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HasConfigurationConditionsPreventingRefresh()
|
||||
{
|
||||
if (_configuration.Advanced.PreventQuestCompletion)
|
||||
{
|
||||
return CurrentQuest?.Sequence == byte.MaxValue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateCurrentQuest()
|
||||
{
|
||||
lock (_progressLock)
|
||||
|
@ -417,6 +511,20 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
_pendingQuest = null;
|
||||
CheckNextTasks("Pending quest accepted");
|
||||
}
|
||||
if (_startedQuest != null && !_questFunctions.IsQuestAccepted(_startedQuest.Quest.Id))
|
||||
{
|
||||
if (_startedQuest.Quest.Info.IsRepeatable)
|
||||
{
|
||||
_logger.LogInformation("Repeatable quest {QuestId} is no longer accepted, clearing started quest", _startedQuest.Quest.Id);
|
||||
}
|
||||
else if (!_questFunctions.IsQuestComplete(_startedQuest.Quest.Id))
|
||||
{
|
||||
_logger.LogInformation("Quest {QuestId} was abandoned, clearing started quest", _startedQuest.Quest.Id);
|
||||
_startedQuest = null;
|
||||
Stop("Quest abandoned");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_simulatedQuest == null && _nextQuest != null && !((!_nextQuest.Quest.Info.IsRepeatable) ? (!_questFunctions.IsQuestAcceptedOrComplete(_nextQuest.Quest.Id)) : (!_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id))))
|
||||
{
|
||||
_logger.LogInformation("Next quest {QuestId} accepted or completed", _nextQuest.Quest.Id);
|
||||
|
@ -425,21 +533,37 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
_startedQuest = _nextQuest;
|
||||
AutomationType = EAutomationType.SingleQuestB;
|
||||
}
|
||||
else if (_questFunctions.IsQuestAccepted(_nextQuest.Quest.Id))
|
||||
{
|
||||
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(_nextQuest.Quest.Id);
|
||||
if (questProgressInfo != null)
|
||||
{
|
||||
_startedQuest = new QuestProgress(_nextQuest.Quest, questProgressInfo.Sequence);
|
||||
_logger.LogInformation("Moving accepted next quest to started quest (sequence: {Sequence})", questProgressInfo.Sequence);
|
||||
_nextQuest = null;
|
||||
CheckNextTasks("Next quest already accepted");
|
||||
return;
|
||||
}
|
||||
_logger.LogWarning("Could not get quest progress info for accepted quest {QuestId}", _nextQuest.Quest.Id);
|
||||
}
|
||||
_logger.LogDebug("Started: {StartedQuest}", _startedQuest?.Quest.Id);
|
||||
_nextQuest = null;
|
||||
}
|
||||
byte b;
|
||||
QuestProgress questProgress;
|
||||
ElementId CurrentQuest;
|
||||
byte Sequence;
|
||||
MainScenarioQuestState State;
|
||||
if (_simulatedQuest != null)
|
||||
{
|
||||
b = _simulatedQuest.Sequence;
|
||||
questProgress = _simulatedQuest;
|
||||
}
|
||||
else if (_nextQuest != null && _questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id))
|
||||
else if (_nextQuest != null)
|
||||
{
|
||||
questProgress = _nextQuest;
|
||||
b = _nextQuest.Sequence;
|
||||
if (_nextQuest.Step == 0 && _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic)
|
||||
if (_questFunctions.IsReadyToAcceptQuest(_nextQuest.Quest.Id) && _nextQuest.Step == 0 && _taskQueue.AllTasksComplete && AutomationType == EAutomationType.Automatic)
|
||||
{
|
||||
ExecuteNextStep();
|
||||
}
|
||||
|
@ -453,24 +577,71 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
ExecuteNextStep();
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (_startedQuest != null)
|
||||
{
|
||||
_questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out ElementId CurrentQuest, out byte Sequence, out MainScenarioQuestState State);
|
||||
ElementId elementId = CurrentQuest;
|
||||
b = Sequence;
|
||||
MainScenarioQuestState mainScenarioQuestState = State;
|
||||
(ElementId, byte)? tuple = (from x in ManualPriorityQuests
|
||||
where _questFunctions.IsReadyToAcceptQuest(x.Id) || _questFunctions.IsQuestAccepted(x.Id)
|
||||
select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault();
|
||||
if (tuple.HasValue)
|
||||
questProgress = _startedQuest;
|
||||
b = _startedQuest.Sequence;
|
||||
QuestProgressInfo questProgressInfo2 = _questFunctions.GetQuestProgressInfo(_startedQuest.Quest.Id);
|
||||
if (questProgressInfo2 != null && questProgressInfo2.Sequence != b)
|
||||
{
|
||||
(ElementId, byte) valueOrDefault = tuple.GetValueOrDefault();
|
||||
if ((object)valueOrDefault.Item1 != null)
|
||||
_logger.LogInformation("Updating started quest sequence from {OldSequence} to {NewSequence}", b, questProgressInfo2.Sequence);
|
||||
b = questProgressInfo2.Sequence;
|
||||
}
|
||||
if (AutomationType == EAutomationType.Manual || !IsRunning)
|
||||
{
|
||||
_questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out CurrentQuest, out Sequence, out State);
|
||||
ElementId elementId = CurrentQuest;
|
||||
byte sequence = Sequence;
|
||||
(ElementId, byte)? tuple = (from x in ManualPriorityQuests
|
||||
where _questFunctions.IsReadyToAcceptQuest(x.Id) || _questFunctions.IsQuestAccepted(x.Id)
|
||||
select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault();
|
||||
if (tuple.HasValue)
|
||||
{
|
||||
(elementId, b) = valueOrDefault;
|
||||
(ElementId, byte) valueOrDefault = tuple.GetValueOrDefault();
|
||||
if ((object)valueOrDefault.Item1 != null)
|
||||
{
|
||||
(elementId, sequence) = valueOrDefault;
|
||||
}
|
||||
}
|
||||
if (elementId != null && elementId.Value != 0 && _startedQuest.Quest.Id != elementId)
|
||||
{
|
||||
_logger.LogInformation("Game current quest changed from {OldQuest} to {NewQuest}, updating started quest", _startedQuest.Quest.Id, elementId);
|
||||
if (_questRegistry.TryGetQuest(elementId, out Quest quest))
|
||||
{
|
||||
_logger.LogInformation("Switching to new quest: {QuestName}", quest.Info.Name);
|
||||
_startedQuest = new QuestProgress(quest, sequence);
|
||||
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest.Info.Level)
|
||||
{
|
||||
_logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest.Info.Level);
|
||||
Stop("Quest level too high");
|
||||
}
|
||||
questProgress = _startedQuest;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("New quest {QuestId} not found in registry", elementId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (elementId == null || elementId.Value == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
_questFunctions.GetCurrentQuest(AutomationType != EAutomationType.SingleQuestB).Deconstruct(out CurrentQuest, out Sequence, out State);
|
||||
ElementId elementId2 = CurrentQuest;
|
||||
b = Sequence;
|
||||
MainScenarioQuestState mainScenarioQuestState = State;
|
||||
(ElementId, byte)? tuple3 = (from x in ManualPriorityQuests
|
||||
where _questFunctions.IsReadyToAcceptQuest(x.Id) || _questFunctions.IsQuestAccepted(x.Id)
|
||||
select (Id: x.Id, _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault();
|
||||
if (tuple3.HasValue)
|
||||
{
|
||||
(ElementId, byte) valueOrDefault2 = tuple3.GetValueOrDefault();
|
||||
if ((object)valueOrDefault2.Item1 != null)
|
||||
{
|
||||
(elementId2, b) = valueOrDefault2;
|
||||
}
|
||||
}
|
||||
if (elementId2 == null || elementId2.Value == 0)
|
||||
{
|
||||
if (_startedQuest != null)
|
||||
{
|
||||
|
@ -491,9 +662,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
}
|
||||
else
|
||||
{
|
||||
if (_startedQuest == null || _startedQuest.Quest.Id != elementId)
|
||||
if (_startedQuest == null || _startedQuest.Quest.Id != elementId2)
|
||||
{
|
||||
Quest quest;
|
||||
Quest quest2;
|
||||
if (_configuration.Stop.Enabled && _startedQuest != null && _configuration.Stop.QuestsToStopAfter.Contains(_startedQuest.Quest.Id) && _questFunctions.IsQuestComplete(_startedQuest.Quest.Id))
|
||||
{
|
||||
ElementId id = _startedQuest.Quest.Id;
|
||||
|
@ -502,13 +673,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
_startedQuest = null;
|
||||
Stop($"Stopping point [{id}] reached");
|
||||
}
|
||||
else if (_questRegistry.TryGetQuest(elementId, out quest))
|
||||
else if (_questRegistry.TryGetQuest(elementId2, out quest2))
|
||||
{
|
||||
_logger.LogInformation("New quest: {QuestName}", quest.Info.Name);
|
||||
_startedQuest = new QuestProgress(quest, b);
|
||||
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest.Info.Level)
|
||||
_logger.LogInformation("New quest: {QuestName}", quest2.Info.Name);
|
||||
_startedQuest = new QuestProgress(quest2, b);
|
||||
if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.Level < quest2.Info.Level)
|
||||
{
|
||||
_logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest.Info.Level);
|
||||
_logger.LogInformation("Stopping automation, player level ({PlayerLevel}) < quest level ({QuestLevel}", _clientState.LocalPlayer.Level, quest2.Info.Level);
|
||||
Stop("Quest level too high");
|
||||
return;
|
||||
}
|
||||
|
@ -533,7 +704,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
if (questProgress == null)
|
||||
{
|
||||
DebugState = "No quest active";
|
||||
Stop("No quest active");
|
||||
if (!IsRunning)
|
||||
{
|
||||
Stop("No quest active");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questProgress.Quest))
|
||||
|
@ -739,6 +913,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
}
|
||||
}
|
||||
|
||||
public void SetStartedQuest(Quest quest, byte sequence = 0)
|
||||
{
|
||||
_logger.LogInformation("Setting started quest: {QuestId}", quest.Id);
|
||||
_startedQuest = new QuestProgress(quest, sequence);
|
||||
_nextQuest = null;
|
||||
}
|
||||
|
||||
public void SetGatheringQuest(Quest? quest)
|
||||
{
|
||||
_logger.LogInformation("GatheringQuest: {QuestId}", quest?.Id);
|
||||
|
|
|
@ -3,10 +3,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
@ -45,6 +47,8 @@ internal sealed class QuestRegistry
|
|||
|
||||
private readonly List<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> _lowPriorityContentFinderConditionQuests = new List<(uint, ElementId, int)>();
|
||||
|
||||
private readonly Dictionary<ElementId, string> _questFolderNames = new Dictionary<ElementId, string>();
|
||||
|
||||
public IEnumerable<Quest> AllQuests => _quests.Values;
|
||||
|
||||
public int Count => _quests.Count<KeyValuePair<ElementId, Quest>>((KeyValuePair<ElementId, Quest> x) => !x.Value.Root.Disabled);
|
||||
|
@ -75,6 +79,7 @@ internal sealed class QuestRegistry
|
|||
_quests.Clear();
|
||||
_contentFinderConditionIds.Clear();
|
||||
_lowPriorityContentFinderConditionQuests.Clear();
|
||||
_questFolderNames.Clear();
|
||||
LoadQuestsFromAssembly();
|
||||
try
|
||||
{
|
||||
|
@ -102,19 +107,69 @@ internal sealed class QuestRegistry
|
|||
private void LoadQuestsFromAssembly()
|
||||
{
|
||||
_logger.LogInformation("Loading quests from assembly");
|
||||
foreach (var (elementId2, root) in AssemblyQuestLoader.GetQuests())
|
||||
foreach (var (elementId2, questRoot2) in AssemblyQuestLoader.GetQuests())
|
||||
{
|
||||
try
|
||||
{
|
||||
IQuestInfo questInfo = _questData.GetQuestInfo(elementId2);
|
||||
bool? flag = null;
|
||||
DateTime? dateTime = null;
|
||||
bool flag2 = false;
|
||||
bool flag3 = false;
|
||||
try
|
||||
{
|
||||
flag = questRoot2.IsSeasonalQuest;
|
||||
flag2 = flag.HasValue;
|
||||
if (questRoot2.SeasonalQuestExpiry.HasValue)
|
||||
{
|
||||
dateTime = DateTime.SpecifyKind(questRoot2.SeasonalQuestExpiry.Value, DateTimeKind.Utc);
|
||||
flag3 = true;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Failed to read seasonal fields from embedded QuestRoot for {QuestId}", elementId2);
|
||||
}
|
||||
if (_questData.TryGetQuestInfo(elementId2, out IQuestInfo questInfo))
|
||||
{
|
||||
goto IL_01c8;
|
||||
}
|
||||
if (elementId2 is UnlockLinkId unlockLinkId)
|
||||
{
|
||||
string text = unlockLinkId.ToString();
|
||||
if (text.Length > 1 && text.StartsWith('U'))
|
||||
{
|
||||
string text2 = text.Substring(1);
|
||||
string text3 = ((text2 == "568") ? "Patch 7.3 Fantasia" : ((!(text2 == "506")) ? ("U" + text2) : "Patch 7.2 Fantasia"));
|
||||
text = text3;
|
||||
}
|
||||
else
|
||||
{
|
||||
text = $"Unlock Link {unlockLinkId.Value}";
|
||||
}
|
||||
questInfo = new UnlockLinkQuestInfo(unlockLinkId, text, 0u, dateTime);
|
||||
_logger.LogDebug("Created UnlockLinkQuestInfo for {QuestId} from assembly", elementId2);
|
||||
_questData.AddOrReplaceQuestInfo(questInfo);
|
||||
goto IL_01c8;
|
||||
}
|
||||
_logger.LogWarning("Not loading unknown quest {QuestId} from assembly: Quest not found in quest data", elementId2);
|
||||
goto end_IL_003d;
|
||||
IL_01c8:
|
||||
if (flag2 || flag3)
|
||||
{
|
||||
bool flag4 = flag ?? questInfo.IsSeasonalQuest;
|
||||
_questData.ApplySeasonalOverride(elementId2, flag4, dateTime);
|
||||
_logger.LogDebug("Applied seasonal override for quest {QuestId} from assembly: IsSeasonal={IsSeasonal}, Expiry={Expiry}", elementId2, flag4, dateTime?.ToString("o") ?? "(null)");
|
||||
}
|
||||
IQuestInfo questInfo2 = _questData.GetQuestInfo(elementId2);
|
||||
Quest quest = new Quest
|
||||
{
|
||||
Id = elementId2,
|
||||
Root = root,
|
||||
Info = questInfo,
|
||||
Root = questRoot2,
|
||||
Info = questInfo2,
|
||||
Source = Quest.ESource.Assembly
|
||||
};
|
||||
_quests[quest.Id] = quest;
|
||||
end_IL_003d:;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -191,27 +246,151 @@ internal sealed class QuestRegistry
|
|||
_questValidator.Validate(_quests.Values.Where((Quest x) => x.Source != Quest.ESource.Assembly).ToList());
|
||||
}
|
||||
|
||||
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source)
|
||||
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source, string directoryName)
|
||||
{
|
||||
if (source == Quest.ESource.UserDirectory)
|
||||
{
|
||||
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
|
||||
}
|
||||
ElementId elementId = ExtractQuestIdFromName(fileName);
|
||||
if (!(elementId == null))
|
||||
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
|
||||
return;
|
||||
}
|
||||
JsonNode jsonNode;
|
||||
try
|
||||
{
|
||||
jsonNode = JsonNode.Parse(stream);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
ValidationIssue issue = new ValidationIssue
|
||||
{
|
||||
Id = elementId,
|
||||
Root = root,
|
||||
Info = questInfo,
|
||||
Source = source
|
||||
ElementId = elementId,
|
||||
Sequence = null,
|
||||
Step = null,
|
||||
Type = EIssueType.InvalidJsonSyntax,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = $"JSON parsing error in file '{fileName}': {ex.Message}\n\nThis usually indicates a syntax error such as:\n\ufffd Missing comma between properties\n\ufffd Unclosed quotes or brackets\n\ufffd Invalid escape sequences\n\ufffd Trailing commas where not allowed\n\nPlease check the JSON syntax around the indicated position."
|
||||
};
|
||||
_quests[quest.Id] = quest;
|
||||
_questValidator.AddValidationIssue(issue);
|
||||
return;
|
||||
}
|
||||
_jsonSchemaValidator.Enqueue(elementId, jsonNode);
|
||||
bool? flag = null;
|
||||
DateTime? dateTime = null;
|
||||
bool flag2 = false;
|
||||
bool flag3 = false;
|
||||
if (jsonNode is JsonObject jsonObject)
|
||||
{
|
||||
if (jsonObject.TryGetPropertyValue("IsSeasonalQuest", out JsonNode jsonNode2) && jsonNode2 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
flag = jsonNode2.GetValue<bool>();
|
||||
flag2 = true;
|
||||
_logger.LogDebug("Quest {QuestId}: parsed IsSeasonalQuest override = {IsSeasonal}", elementId, flag);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "Quest {QuestId}: failed to parse IsSeasonalQuest from JSON", elementId);
|
||||
}
|
||||
}
|
||||
if (jsonObject.TryGetPropertyValue("SeasonalQuestExpiry", out JsonNode jsonNode3) && jsonNode3 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string value = jsonNode3.GetValue<string>();
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
dateTime = ((!DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) ? new DateTime?(DateTime.Parse(value, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal)) : new DateTime?(DateTime.SpecifyKind(result, DateTimeKind.Utc)));
|
||||
flag3 = true;
|
||||
_logger.LogDebug("Quest {QuestId}: parsed SeasonalQuestExpiry override = {Expiry}", elementId, dateTime);
|
||||
}
|
||||
}
|
||||
catch (Exception exception2)
|
||||
{
|
||||
_logger.LogWarning(exception2, "Quest {QuestId}: failed to parse SeasonalQuestExpiry from JSON", elementId);
|
||||
}
|
||||
}
|
||||
}
|
||||
QuestRoot root = jsonNode.Deserialize<QuestRoot>();
|
||||
if (!_questData.TryGetQuestInfo(elementId, out IQuestInfo questInfo))
|
||||
{
|
||||
if (!(elementId is UnlockLinkId unlockLinkId))
|
||||
{
|
||||
_logger.LogWarning("Not loading unknown quest {QuestId} from project file {FileName}", elementId, fileName);
|
||||
return;
|
||||
}
|
||||
string name;
|
||||
try
|
||||
{
|
||||
string text = fileName.Substring(0, fileName.Length - ".json".Length);
|
||||
int num = text.IndexOf('_', StringComparison.Ordinal);
|
||||
string text2;
|
||||
if (num < 0 || num + 1 >= text.Length)
|
||||
{
|
||||
text2 = text;
|
||||
}
|
||||
else
|
||||
{
|
||||
string text3 = text;
|
||||
int num2 = num + 1;
|
||||
text2 = text3.Substring(num2, text3.Length - num2);
|
||||
}
|
||||
name = text2;
|
||||
}
|
||||
catch
|
||||
{
|
||||
name = fileName.Substring(0, fileName.Length - ".json".Length);
|
||||
}
|
||||
name = NormalizeDerivedName(name);
|
||||
uint issuerDataId = 0u;
|
||||
string patch = null;
|
||||
if (jsonNode is JsonObject jsonObject2)
|
||||
{
|
||||
if (jsonObject2.TryGetPropertyValue("DataId", out JsonNode jsonNode4) && jsonNode4 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
issuerDataId = jsonNode4.GetValue<uint>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
issuerDataId = 0u;
|
||||
}
|
||||
}
|
||||
if (jsonObject2.TryGetPropertyValue("Patch", out JsonNode jsonNode5) && jsonNode5 != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
patch = jsonNode5.GetValue<string>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
patch = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
questInfo = new UnlockLinkQuestInfo(unlockLinkId, name, issuerDataId, dateTime, patch);
|
||||
_logger.LogDebug("Created UnlockLinkQuestInfo for {QuestId} from project file '{FileName}'", elementId, fileName);
|
||||
_questData.AddOrReplaceQuestInfo(questInfo);
|
||||
}
|
||||
if ((flag2 || flag3) && _questData.TryGetQuestInfo(elementId, out IQuestInfo questInfo2))
|
||||
{
|
||||
_questData.ApplySeasonalOverride(elementId, flag ?? questInfo2.IsSeasonalQuest, dateTime);
|
||||
}
|
||||
Quest quest = new Quest
|
||||
{
|
||||
Id = elementId,
|
||||
Root = root,
|
||||
Info = questInfo,
|
||||
Source = source
|
||||
};
|
||||
_quests[quest.Id] = quest;
|
||||
if (!string.IsNullOrEmpty(directoryName))
|
||||
{
|
||||
_questFolderNames[elementId] = directoryName;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,7 +411,7 @@ internal sealed class QuestRegistry
|
|||
try
|
||||
{
|
||||
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
|
||||
LoadQuestFromStream(fileInfo.Name, stream, source);
|
||||
LoadQuestFromStream(fileInfo.Name, stream, source, directory.Name);
|
||||
}
|
||||
catch (Exception innerException)
|
||||
{
|
||||
|
@ -287,4 +466,26 @@ internal sealed class QuestRegistry
|
|||
dutyOptions = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerable<ElementId> GetAllQuestIds()
|
||||
{
|
||||
return _quests.Keys;
|
||||
}
|
||||
|
||||
public bool TryGetQuestFolderName(ElementId questId, [NotNullWhen(true)] out string? folderName)
|
||||
{
|
||||
return _questFolderNames.TryGetValue(questId, out folderName);
|
||||
}
|
||||
|
||||
private static string NormalizeDerivedName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return name ?? string.Empty;
|
||||
}
|
||||
name = name.Replace("_", " ", StringComparison.OrdinalIgnoreCase);
|
||||
name = Regex.Replace(name, "\\s+", " ");
|
||||
name = Regex.Replace(name, "\\b(Patch)\\s+(\\d+)\\s+(\\d+)\\b", "$1 $2.$3", RegexOptions.IgnoreCase);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue