punish v6.8.18.0

This commit is contained in:
alydev 2025-10-09 07:47:19 +10:00
commit cfb4dea47e
316 changed files with 554088 additions and 0 deletions

View file

@ -0,0 +1,145 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CompilerGenerated]
internal sealed class _003C_003Ez__ReadOnlyArray<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T>
{
int ICollection.Count => _items.Length;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get
{
return _items[index];
}
set
{
throw new NotSupportedException();
}
}
bool IList.IsFixedSize => true;
bool IList.IsReadOnly => true;
int IReadOnlyCollection<T>.Count => _items.Length;
T IReadOnlyList<T>.this[int index] => _items[index];
int ICollection<T>.Count => _items.Length;
bool ICollection<T>.IsReadOnly => true;
T IList<T>.this[int index]
{
get
{
return _items[index];
}
set
{
throw new NotSupportedException();
}
}
public _003C_003Ez__ReadOnlyArray(T[] items)
{
_items = items;
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_items).GetEnumerator();
}
void ICollection.CopyTo(Array array, int index)
{
((ICollection)_items).CopyTo(array, index);
}
int IList.Add(object? value)
{
throw new NotSupportedException();
}
void IList.Clear()
{
throw new NotSupportedException();
}
bool IList.Contains(object? value)
{
return ((IList)_items).Contains(value);
}
int IList.IndexOf(object? value)
{
return ((IList)_items).IndexOf(value);
}
void IList.Insert(int index, object? value)
{
throw new NotSupportedException();
}
void IList.Remove(object? value)
{
throw new NotSupportedException();
}
void IList.RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)_items).GetEnumerator();
}
void ICollection<T>.Add(T item)
{
throw new NotSupportedException();
}
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<T>.Contains(T item)
{
return ((ICollection<T>)_items).Contains(item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
((ICollection<T>)_items).CopyTo(array, arrayIndex);
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
int IList<T>.IndexOf(T item)
{
return ((IList<T>)_items).IndexOf(item);
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
}

View file

@ -0,0 +1,145 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CompilerGenerated]
internal sealed class _003C_003Ez__ReadOnlyList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T>
{
int ICollection.Count => _items.Count;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get
{
return _items[index];
}
set
{
throw new NotSupportedException();
}
}
bool IList.IsFixedSize => true;
bool IList.IsReadOnly => true;
int IReadOnlyCollection<T>.Count => _items.Count;
T IReadOnlyList<T>.this[int index] => _items[index];
int ICollection<T>.Count => _items.Count;
bool ICollection<T>.IsReadOnly => true;
T IList<T>.this[int index]
{
get
{
return _items[index];
}
set
{
throw new NotSupportedException();
}
}
public _003C_003Ez__ReadOnlyList(List<T> items)
{
_items = items;
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_items).GetEnumerator();
}
void ICollection.CopyTo(Array array, int index)
{
((ICollection)_items).CopyTo(array, index);
}
int IList.Add(object? value)
{
throw new NotSupportedException();
}
void IList.Clear()
{
throw new NotSupportedException();
}
bool IList.Contains(object? value)
{
return ((IList)_items).Contains(value);
}
int IList.IndexOf(object? value)
{
return ((IList)_items).IndexOf(value);
}
void IList.Insert(int index, object? value)
{
throw new NotSupportedException();
}
void IList.Remove(object? value)
{
throw new NotSupportedException();
}
void IList.RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)_items).GetEnumerator();
}
void ICollection<T>.Add(T item)
{
throw new NotSupportedException();
}
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<T>.Contains(T item)
{
return _items.Contains(item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
_items.CopyTo(array, arrayIndex);
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
int IList<T>.IndexOf(T item)
{
return _items.IndexOf(item);
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
}

View file

@ -0,0 +1,201 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CompilerGenerated]
internal sealed class _003C_003Ez__ReadOnlySingleElementList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T>
{
private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T>
{
object IEnumerator.Current => _item;
T IEnumerator<T>.Current => _item;
public Enumerator(T item)
{
_item = item;
}
bool IEnumerator.MoveNext()
{
if (!_moveNextCalled)
{
return _moveNextCalled = true;
}
return false;
}
void IEnumerator.Reset()
{
_moveNextCalled = false;
}
void IDisposable.Dispose()
{
}
}
int ICollection.Count => 1;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => this;
object? IList.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
set
{
throw new NotSupportedException();
}
}
bool IList.IsFixedSize => true;
bool IList.IsReadOnly => true;
int IReadOnlyCollection<T>.Count => 1;
T IReadOnlyList<T>.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
}
int ICollection<T>.Count => 1;
bool ICollection<T>.IsReadOnly => true;
T IList<T>.this[int index]
{
get
{
if (index != 0)
{
throw new IndexOutOfRangeException();
}
return _item;
}
set
{
throw new NotSupportedException();
}
}
public _003C_003Ez__ReadOnlySingleElementList(T item)
{
_item = item;
}
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(_item);
}
void ICollection.CopyTo(Array array, int index)
{
array.SetValue(_item, index);
}
int IList.Add(object? value)
{
throw new NotSupportedException();
}
void IList.Clear()
{
throw new NotSupportedException();
}
bool IList.Contains(object? value)
{
return EqualityComparer<T>.Default.Equals(_item, (T)value);
}
int IList.IndexOf(object? value)
{
if (!EqualityComparer<T>.Default.Equals(_item, (T)value))
{
return -1;
}
return 0;
}
void IList.Insert(int index, object? value)
{
throw new NotSupportedException();
}
void IList.Remove(object? value)
{
throw new NotSupportedException();
}
void IList.RemoveAt(int index)
{
throw new NotSupportedException();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return new Enumerator(_item);
}
void ICollection<T>.Add(T item)
{
throw new NotSupportedException();
}
void ICollection<T>.Clear()
{
throw new NotSupportedException();
}
bool ICollection<T>.Contains(T item)
{
return EqualityComparer<T>.Default.Equals(_item, item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
array[arrayIndex] = _item;
}
bool ICollection<T>.Remove(T item)
{
throw new NotSupportedException();
}
int IList<T>.IndexOf(T item)
{
if (!EqualityComparer<T>.Default.Equals(_item, item))
{
return -1;
}
return 0;
}
void IList<T>.Insert(int index, T item)
{
throw new NotSupportedException();
}
void IList<T>.RemoveAt(int index)
{
throw new NotSupportedException();
}
}

View file

@ -0,0 +1,293 @@
{
"Name": "Questionable",
"Modules": {
"BossMod.Autorotation.xan.DNC": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.MCH": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.MNK": [
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.PCT": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
},
{
"Track": "Motifs",
"Option": "Downtime"
}
],
"BossMod.Autorotation.xan.PLD": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SAM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SGE": [
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.VPR": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.NIN": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.GNB": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SMN": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.DRK": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.RPR": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.WHM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.AST": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.BRD": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SCH": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.BLM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.RDM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.DRG": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.VeynWAR": [
{
"Track": "AOE",
"Option": "AutoFinishCombo"
}
],
"BossMod.Autorotation.MiscAI.NormalMovement": [
{
"Track": "Destination",
"Option": "Pathfind"
}
]
}
}

View file

@ -0,0 +1,308 @@
{
"Name": "Questionable - Quest Battles",
"Modules": {
"BossMod.Autorotation.MiscAI.AutoFarm": [],
"BossMod.Autorotation.MiscAI.AutoPull": [
{
"Track": "Automatically attack hunt marks once they are below 95% HP",
"Option": "Disabled"
},
{
"Track": "Automatically attack deep dungeon bosses when solo",
"Option": "Disabled"
},
{
"Track": "Automatically attack all targets if the Epic Echo status is present (i.e. when unsynced)",
"Option": "Disabled"
}
],
"BossMod.Autorotation.xan.DNC": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.MCH": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.MNK": [
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.PCT": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
},
{
"Track": "Motifs",
"Option": "Downtime"
}
],
"BossMod.Autorotation.xan.PLD": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SAM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SGE": [
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.VPR": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.NIN": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.GNB": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SMN": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.DRK": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.RPR": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.WHM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.AST": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.BRD": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.SCH": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.BLM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.RDM": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.xan.DRG": [
{
"Track": "Buffs",
"Option": "Auto"
},
{
"Track": "AOE",
"Option": "AOE"
},
{
"Track": "Targeting",
"Option": "Manual"
}
],
"BossMod.Autorotation.VeynWAR": [
{
"Track": "AOE",
"Option": "AutoFinishCombo"
}
],
"BossMod.Autorotation.MiscAI.NormalMovement": [
{
"Track": "Destination",
"Option": "Pathfind"
}
]
}
}

View file

@ -0,0 +1,74 @@
using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Ipc.Exceptions;
using Microsoft.Extensions.Logging;
using Questionable.External;
namespace Questionable.Controller.CombatModules;
internal sealed class BossModModule : ICombatModule, IDisposable
{
private readonly ILogger<BossModModule> _logger;
private readonly BossModIpc _bossModIpc;
private readonly Configuration _configuration;
public BossModModule(ILogger<BossModModule> logger, BossModIpc bossModIpc, Configuration configuration)
{
_logger = logger;
_bossModIpc = bossModIpc;
_configuration = configuration;
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
if (_configuration.General.CombatModule != Configuration.ECombatModule.BossMod)
{
return false;
}
return _bossModIpc.IsSupported();
}
public bool Start(CombatController.CombatData combatData)
{
try
{
_bossModIpc.SetPreset(BossModIpc.EPreset.Overworld);
return true;
}
catch (IpcError exception)
{
_logger.LogWarning(exception, "Could not start combat");
return false;
}
}
public bool Stop()
{
try
{
_bossModIpc.ClearPreset();
return true;
}
catch (IpcError exception)
{
_logger.LogWarning(exception, "Could not turn off combat");
return false;
}
}
public void Update(IGameObject gameObject)
{
}
public bool CanAttack(IBattleNpc target)
{
return true;
}
public void Dispose()
{
Stop();
}
}

View file

@ -0,0 +1,16 @@
using Dalamud.Game.ClientState.Objects.Types;
namespace Questionable.Controller.CombatModules;
internal interface ICombatModule
{
bool CanHandleFight(CombatController.CombatData combatData);
bool Start(CombatController.CombatData combatData);
bool Stop();
void Update(IGameObject nextTarget);
bool CanAttack(IBattleNpc target);
}

View file

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Model.Questing;
namespace Questionable.Controller.CombatModules;
internal sealed class ItemUseModule : ICombatModule
{
private readonly IServiceProvider _serviceProvider;
private readonly ICondition _condition;
private readonly ILogger<ItemUseModule> _logger;
private ICombatModule? _delegate;
private CombatController.CombatData? _combatData;
private bool _isDoingRotation;
private DateTime _continueAt;
public ItemUseModule(IServiceProvider serviceProvider, ICondition condition, ILogger<ItemUseModule> logger)
{
_serviceProvider = serviceProvider;
_condition = condition;
_logger = logger;
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
if (combatData.CombatItemUse == null)
{
return false;
}
_delegate = (from x in _serviceProvider.GetRequiredService<IEnumerable<ICombatModule>>()
where !(x is ItemUseModule)
select x).FirstOrDefault((ICombatModule x) => x.CanHandleFight(combatData));
_logger.LogInformation("ItemUse delegate: {Delegate}", _delegate?.GetType().Name);
return _delegate != null;
}
public bool Start(CombatController.CombatData combatData)
{
if (_delegate.Start(combatData))
{
_combatData = combatData;
_isDoingRotation = true;
_continueAt = DateTime.Now;
return true;
}
return false;
}
public bool Stop()
{
if (_isDoingRotation)
{
_delegate.Stop();
_isDoingRotation = false;
_combatData = null;
_delegate = null;
_continueAt = DateTime.Now;
}
return true;
}
public unsafe void Update(IGameObject nextTarget)
{
if (_delegate == null || _continueAt > DateTime.Now)
{
return;
}
if (_combatData?.CombatItemUse == null)
{
_delegate.Update(nextTarget);
}
else if (_combatData.KillEnemyDataIds.Contains(nextTarget.DataId) || _combatData.ComplexCombatDatas.Any((ComplexCombatData x) => x.DataId == nextTarget.DataId && (!x.NameId.HasValue || (nextTarget is ICharacter character && x.NameId == character.NameId))))
{
if (_isDoingRotation)
{
if (InventoryManager.Instance()->GetInventoryItemCount(_combatData.CombatItemUse.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) == 0)
{
_isDoingRotation = false;
_delegate.Stop();
}
else if (ShouldUseItem(nextTarget))
{
_isDoingRotation = false;
_delegate.Stop();
_logger.LogInformation("Using item {ItemId}", _combatData.CombatItemUse.ItemId);
AgentInventoryContext.Instance()->UseItem(_combatData.CombatItemUse.ItemId, InventoryType.Invalid, 0u, 0);
_continueAt = DateTime.Now.AddSeconds(2.0);
}
else
{
_delegate.Update(nextTarget);
}
}
else if (_condition[ConditionFlag.Casting])
{
DateTime dateTime = DateTime.Now.AddSeconds(0.5);
if (dateTime > _continueAt)
{
_continueAt = dateTime;
}
}
else
{
_isDoingRotation = true;
_delegate.Start(_combatData);
}
}
else if (_isDoingRotation)
{
_delegate.Update(nextTarget);
}
}
private unsafe bool ShouldUseItem(IGameObject gameObject)
{
if (_combatData?.CombatItemUse == null)
{
return false;
}
if (gameObject is IBattleChara)
{
BattleChara* address = (BattleChara*)gameObject.Address;
if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.Incapacitated)
{
return (address->ActorControlFlags & 0x40) != 0;
}
if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.HealthPercent)
{
return 100f * (float)address->Health / (float)address->MaxHealth < (float)_combatData.CombatItemUse.Value;
}
if (_combatData.CombatItemUse.Condition == ECombatItemUseCondition.MissingStatus)
{
return !address->StatusManager.HasStatus((uint)_combatData.CombatItemUse.Value);
}
}
return false;
}
public bool CanAttack(IBattleNpc target)
{
return _delegate.CanAttack(target);
}
}

View file

@ -0,0 +1,60 @@
using Dalamud.Game.ClientState.Objects.Types;
using Questionable.Functions;
using Questionable.Model.Questing;
namespace Questionable.Controller.CombatModules;
internal sealed class Mount128Module : ICombatModule
{
public const ushort MountId = 128;
private readonly EAction[] _actions = new EAction[2]
{
EAction.MagitekThunder,
EAction.MagitekPulse
};
private readonly GameFunctions _gameFunctions;
public Mount128Module(GameFunctions gameFunctions)
{
_gameFunctions = gameFunctions;
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
return _gameFunctions.GetMountId() == 128;
}
public bool Start(CombatController.CombatData combatData)
{
return true;
}
public bool Stop()
{
return true;
}
public void Update(IGameObject gameObject)
{
EAction[] actions = _actions;
foreach (EAction action in actions)
{
if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false))
{
break;
}
}
}
public bool CanAttack(IBattleNpc target)
{
uint dataId = target.DataId;
if (dataId - 7504 <= 1 || dataId == 14107)
{
return true;
}
return false;
}
}

View file

@ -0,0 +1,51 @@
using Dalamud.Game.ClientState.Objects.Types;
using Questionable.Functions;
using Questionable.Model.Questing;
namespace Questionable.Controller.CombatModules;
internal sealed class Mount147Module : ICombatModule
{
public const ushort MountId = 147;
private readonly EAction[] _actions = new EAction[1] { EAction.Trample };
private readonly GameFunctions _gameFunctions;
public Mount147Module(GameFunctions gameFunctions)
{
_gameFunctions = gameFunctions;
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
return _gameFunctions.GetMountId() == 147;
}
public bool Start(CombatController.CombatData combatData)
{
return true;
}
public bool Stop()
{
return true;
}
public void Update(IGameObject gameObject)
{
EAction[] actions = _actions;
foreach (EAction action in actions)
{
if (_gameFunctions.UseAction(gameObject, action, checkCanUse: false))
{
break;
}
}
}
public bool CanAttack(IBattleNpc target)
{
return target.DataId == 8593;
}
}

View file

@ -0,0 +1,97 @@
using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.CombatModules;
internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
{
private enum StateCommandType : byte
{
Off,
Auto,
Manual
}
private readonly ILogger<RotationSolverRebornModule> _logger;
private readonly Configuration _configuration;
private readonly ICallGateSubscriber<string, object> _test;
private readonly ICallGateSubscriber<StateCommandType, object> _changeOperationMode;
public RotationSolverRebornModule(ILogger<RotationSolverRebornModule> logger, IDalamudPluginInterface pluginInterface, Configuration configuration)
{
_logger = logger;
_configuration = configuration;
_test = pluginInterface.GetIpcSubscriber<string, object>("RotationSolverReborn.Test");
_changeOperationMode = pluginInterface.GetIpcSubscriber<StateCommandType, object>("RotationSolverReborn.ChangeOperatingMode");
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
if (_configuration.General.CombatModule != Configuration.ECombatModule.RotationSolverReborn)
{
return false;
}
try
{
_test.InvokeAction("Validate RSR is callable from Questionable");
return true;
}
catch (IpcError)
{
return false;
}
}
public bool Start(CombatController.CombatData combatData)
{
try
{
_changeOperationMode.InvokeAction(StateCommandType.Manual);
return true;
}
catch (IpcError exception)
{
_logger.LogWarning(exception, "Could not start combat");
return false;
}
}
public bool Stop()
{
if (!_changeOperationMode.HasAction)
{
return true;
}
try
{
_changeOperationMode.InvokeAction(StateCommandType.Off);
return true;
}
catch (IpcError exception)
{
_logger.LogWarning(exception, "Could not turn off combat");
return false;
}
}
public void Update(IGameObject gameObject)
{
}
public bool CanAttack(IBattleNpc target)
{
return true;
}
public void Dispose()
{
Stop();
}
}

View file

@ -0,0 +1,187 @@
using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
namespace Questionable.Controller.CombatModules;
internal sealed class WrathComboModule : ICombatModule, IDisposable
{
public enum ESetResult
{
Okay = 0,
OkayWorking = 1,
IpcDisabled = 10,
InvalidLease = 11,
BlacklistedLease = 12,
Duplicate = 13,
PlayerNotAvailable = 14,
InvalidConfiguration = 15,
InvalidValue = 16
}
public enum AutoRotationConfigOption
{
InCombatOnly,
DPSRotationMode,
HealerRotationMode,
FATEPriority,
QuestPriority,
SingleTargetHPP,
AoETargetHPP,
SingleTargetRegenHPP,
ManageKardia,
AutoRez,
AutoRezDPSJobs,
AutoCleanse,
IncludeNPCs,
OnlyAttackInCombat,
OrbwalkerIntegration,
AutoRezOutOfParty,
DPSAoETargets,
SingleTargetExcogHPP
}
public enum HealerRotationMode
{
Manual,
Highest_Current,
Lowest_Current
}
private const string CallbackPrefix = "Questionable$Wrath";
private readonly ILogger<WrathComboModule> _logger;
private readonly Configuration _configuration;
private readonly ICallGateSubscriber<object> _test;
private readonly ICallGateSubscriber<string, string, string, Guid?> _registerForLeaseWithCallback;
private readonly ICallGateSubscriber<Guid, object> _releaseControl;
private readonly ICallGateSubscriber<Guid, bool, ESetResult> _setAutoRotationState;
private readonly ICallGateSubscriber<Guid, object, object, ESetResult> _setAutoRotationConfigState;
private readonly ICallGateSubscriber<Guid, ESetResult> _setCurrentJobAutoRotationReady;
private readonly ICallGateProvider<int, string, object> _callback;
private Guid? _lease;
public WrathComboModule(ILogger<WrathComboModule> logger, Configuration configuration, IDalamudPluginInterface pluginInterface)
{
_logger = logger;
_configuration = configuration;
_test = pluginInterface.GetIpcSubscriber<object>("WrathCombo.Test");
_registerForLeaseWithCallback = pluginInterface.GetIpcSubscriber<string, string, string, Guid?>("WrathCombo.RegisterForLeaseWithCallback");
_releaseControl = pluginInterface.GetIpcSubscriber<Guid, object>("WrathCombo.ReleaseControl");
_setAutoRotationState = pluginInterface.GetIpcSubscriber<Guid, bool, ESetResult>("WrathCombo.SetAutoRotationState");
_setAutoRotationConfigState = pluginInterface.GetIpcSubscriber<Guid, object, object, ESetResult>("WrathCombo.SetAutoRotationConfigState");
_setCurrentJobAutoRotationReady = pluginInterface.GetIpcSubscriber<Guid, ESetResult>("WrathCombo.SetCurrentJobAutoRotationReady");
_callback = pluginInterface.GetIpcProvider<int, string, object>("Questionable$Wrath.WrathComboCallback");
_callback.RegisterAction(Callback);
}
public bool CanHandleFight(CombatController.CombatData combatData)
{
if (_configuration.General.CombatModule != Configuration.ECombatModule.WrathCombo)
{
return false;
}
try
{
_test.InvokeAction();
return true;
}
catch (IpcError)
{
return false;
}
}
public bool Start(CombatController.CombatData combatData)
{
try
{
_lease = _registerForLeaseWithCallback.InvokeFunc("Questionable", "Questionable", "Questionable$Wrath");
if (_lease.HasValue)
{
_logger.LogDebug("Wrath combo lease: {Lease}", _lease.Value);
if (!_setAutoRotationState.InvokeFunc(_lease.Value, arg2: true).IsSuccess())
{
_logger.LogError("Unable to set autorotation state");
Stop();
return false;
}
if (!_setCurrentJobAutoRotationReady.InvokeFunc(_lease.Value).IsSuccess())
{
_logger.LogError("Unable to set current job for autorotation");
Stop();
return false;
}
ESetResult eSetResult = _setAutoRotationConfigState.InvokeFunc(_lease.Value, AutoRotationConfigOption.HealerRotationMode, HealerRotationMode.Lowest_Current);
if (!eSetResult.IsSuccess())
{
_logger.LogError("Unable to configure healing priority for autorotation: {Result}", eSetResult);
}
return true;
}
_logger.LogError("Wrath combo did not return a lease");
return false;
}
catch (IpcError exception)
{
_logger.LogError(exception, "Unable to use wrath combo for combat");
return false;
}
}
public bool Stop()
{
try
{
if (_lease.HasValue)
{
_releaseControl.InvokeAction(_lease.Value);
_lease = null;
}
return true;
}
catch (IpcError exception)
{
_logger.LogWarning(exception, "Could not turn off wrath combo");
return false;
}
}
public void Update(IGameObject nextTarget)
{
if (!_lease.HasValue)
{
throw new TaskException("Wrath Combo Lease is cancelled");
}
}
public bool CanAttack(IBattleNpc target)
{
return true;
}
private void Callback(int reason, string additionalInfo)
{
_logger.LogWarning("WrathCombo callback: {Reason} ({Info})", reason, additionalInfo);
_lease = null;
}
public void Dispose()
{
Stop();
_callback.UnregisterAction();
}
}

View file

@ -0,0 +1,13 @@
namespace Questionable.Controller.CombatModules;
internal static class WrathResultExtensions
{
public static bool IsSuccess(this WrathComboModule.ESetResult result)
{
if ((uint)result <= 1u)
{
return true;
}
return false;
}
}

View file

@ -0,0 +1,155 @@
using System;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.GameUi;
internal sealed class CraftworksSupplyController : IDisposable
{
private readonly QuestController _questController;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly ILogger<CraftworksSupplyController> _logger;
private bool ShouldHandleUiInteractions => _questController.IsRunning;
public CraftworksSupplyController(QuestController questController, IAddonLifecycle addonLifecycle, IGameGui gameGui, IFramework framework, ILogger<CraftworksSupplyController> logger)
{
_questController = questController;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_framework = framework;
_logger = logger;
_addonLifecycle.RegisterListener(AddonEvent.PostReceiveEvent, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "BankaCraftworksSupply", BankaCraftworksSupplyPostUpdate);
}
private unsafe void BankaCraftworksSupplyPostUpdate(AddonEvent type, AddonArgs args)
{
if (ShouldHandleUiInteractions)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
InteractWithBankaCraftworksSupply(address);
}
}
private unsafe void InteractWithBankaCraftworksSupply()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("BankaCraftworksSupply", out var addonPtr))
{
InteractWithBankaCraftworksSupply(addonPtr);
}
}
private unsafe void InteractWithBankaCraftworksSupply(AtkUnitBase* addon)
{
AtkValue* atkValues = addon->AtkValues;
uint uInt = atkValues[7].UInt;
uint num = 6 - uInt;
for (int i = 0; i < num; i++)
{
if (atkValues[31 + i].UInt == 0)
{
_logger.LogInformation("Selecting an item for slot {Slot}", i);
AtkValue* values = stackalloc AtkValue[2]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 2
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = i
}
};
addon->FireCallback(2u, values);
return;
}
}
if (atkValues[31].UInt != 0)
{
_logger.LogInformation("Confirming turn-in");
addon->FireCallbackInt(0);
}
}
private unsafe void ContextIconMenuPostReceiveEvent(AddonEvent type, AddonArgs args)
{
if (!ShouldHandleUiInteractions)
{
return;
}
AddonContextIconMenu* address = (AddonContextIconMenu*)args.Addon.Address;
if (!address->IsVisible)
{
return;
}
ushort contextMenuParentId = address->ContextMenuParentId;
if (contextMenuParentId == 0)
{
return;
}
AtkUnitBase* addonById = AtkStage.Instance()->RaptureAtkUnitManager->GetAddonById(contextMenuParentId);
if (addonById->NameString == "BankaCraftworksSupply")
{
_logger.LogInformation("Picking item for {AddonName}", addonById->NameString);
AtkValue* values = stackalloc AtkValue[5]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 0
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 0
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt,
UInt = 20802u
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt,
UInt = 0u
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Undefined,
Int = 0
}
};
address->FireCallback(5u, values);
address->Close(fireCallback: true);
if (addonById->NameString == "BankaCraftworksSupply")
{
_framework.RunOnTick((Action)InteractWithBankaCraftworksSupply, TimeSpan.FromMilliseconds(50L, 0L), 0, default(CancellationToken));
}
}
else
{
_logger.LogTrace("Ignoring contextmenu event for {AddonName}", addonById->NameString);
}
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "BankaCraftworksSupply", BankaCraftworksSupplyPostUpdate);
_addonLifecycle.UnregisterListener(AddonEvent.PostReceiveEvent, "ContextIconMenu", ContextIconMenuPostReceiveEvent);
}
}

View file

@ -0,0 +1,52 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.GameUi;
internal sealed class CreditsController : IDisposable
{
private readonly IAddonLifecycle _addonLifecycle;
private readonly ILogger<CreditsController> _logger;
public CreditsController(IAddonLifecycle addonLifecycle, ILogger<CreditsController> logger)
{
_addonLifecycle = addonLifecycle;
_logger = logger;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
}
private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args)
{
_logger.LogInformation("Closing Credits sequence");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(-2);
}
private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args)
{
_logger.LogInformation("Closing Credits sequence");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(-2);
}
private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args)
{
_logger.LogInformation("Closing CreditPlayer");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->Close(fireCallback: true);
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditPlayer", CreditPlayerPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Credit", CreditPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CreditScroll", CreditScrollPostSetup);
}
}

View file

@ -0,0 +1,172 @@
using System;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.GameUi;
internal sealed class HelpUiController : IDisposable
{
private readonly QuestController _questController;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly ILogger<HelpUiController> _logger;
public HelpUiController(QuestController questController, IAddonLifecycle addonLifecycle, IGameGui gameGui, IFramework framework, ILogger<HelpUiController> logger)
{
_questController = questController;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_framework = framework;
_logger = logger;
_questController.AutomationTypeChanged += CloseHelpWindowsWhenStartingQuests;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "JobHudNotice", JobHudNoticePostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Guide", GuidePostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "EventTutorial", EventTutorialPostSetup);
}
private unsafe void CloseHelpWindowsWhenStartingQuests(object sender, QuestController.EAutomationType e)
{
if (e != QuestController.EAutomationType.Manual)
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("Guide", out var addonPtr))
{
_logger.LogInformation("Guide window is open");
GuidePostSetup(addonPtr);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("EventTutorial", out var addonPtr2))
{
_logger.LogInformation("EventTutorial window is open");
EventTutorialPostSetup(addonPtr2);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("ContentsTutorial", out var addonPtr3))
{
_logger.LogInformation("ContentsTutorial window is open");
ContentsTutorialPostSetup(addonPtr3);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("JobHudNotice", out var addonPtr4))
{
_logger.LogInformation("JobHudNotice window is open");
JobHudNoticePostSetup(addonPtr4);
}
}
}
private unsafe void UnendingCodexPostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.StartedQuest?.Quest.Id.Value == 4526)
{
_logger.LogInformation("Closing Unending Codex");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(-2);
}
}
private unsafe void ContentsTutorialPostSetup(AddonEvent type, AddonArgs args)
{
bool flag;
switch (_questController.StartedQuest?.Quest.Id.Value)
{
case 245:
case 3872:
case 5253:
flag = true;
break;
default:
flag = false;
break;
}
if (flag)
{
ContentsTutorialPostSetup((AtkUnitBase*)args.Addon.Address);
}
}
private unsafe void ContentsTutorialPostSetup(AtkUnitBase* addon)
{
_logger.LogInformation("Closing ContentsTutorial");
addon->FireCallbackInt(13);
}
private unsafe void MultipleHelpWindowPostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.StartedQuest?.Quest.Id.Value == 245)
{
_logger.LogInformation("Closing MultipleHelpWindow");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(-2);
address->FireCallbackInt(-1);
}
}
private unsafe void JobHudNoticePostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.IsRunning || _questController.AutomationType != QuestController.EAutomationType.Manual)
{
JobHudNoticePostSetup((AtkUnitBase*)args.Addon.Address);
}
}
private unsafe void JobHudNoticePostSetup(AtkUnitBase* addon)
{
_logger.LogInformation("Clicking the JobHudNotice window to open the relevant Guide page");
addon->FireCallbackInt(0);
}
private unsafe void GuidePostSetup(AddonEvent type, AddonArgs args)
{
if (_questController.IsRunning || _questController.AutomationType != QuestController.EAutomationType.Manual)
{
GuidePostSetup((AtkUnitBase*)args.Addon.Address);
}
}
private unsafe void GuidePostSetup(AtkUnitBase* addon)
{
_logger.LogInformation("Closing Guide window");
addon->FireCallbackInt(-1);
}
private unsafe void EventTutorialPostSetup(AddonEvent type, AddonArgs args)
{
if (!_questController.IsRunning && _questController.AutomationType == QuestController.EAutomationType.Manual)
{
return;
}
_framework.RunOnTick(delegate
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("EventTutorial", out var addonPtr))
{
EventTutorialPostSetup(addonPtr);
}
});
}
private unsafe void EventTutorialPostSetup(AtkUnitBase* addon)
{
_logger.LogInformation("Closing EventTutorial window");
addon->FireCallbackInt(-1);
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "EventTutorial", EventTutorialPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Guide", GuidePostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "JobHudNotice", JobHudNoticePostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "MultipleHelpWindow", MultipleHelpWindowPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "ContentsTutorial", ContentsTutorialPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "AkatsukiNote", UnendingCodexPostSetup);
_questController.AutomationTypeChanged -= CloseHelpWindowsWhenStartingQuests;
}
}

View file

@ -0,0 +1,957 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib;
using LLib.GameData;
using LLib.GameUI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.GameUi;
internal sealed class InteractionUiController : IDisposable
{
private sealed record DialogueChoiceInfo(Questionable.Model.Quest? Quest, DialogueChoice DialogueChoice);
private readonly IAddonLifecycle _addonLifecycle;
private readonly IDataManager _dataManager;
private readonly QuestFunctions _questFunctions;
private readonly AetheryteFunctions _aetheryteFunctions;
private readonly ExcelFunctions _excelFunctions;
private readonly QuestController _questController;
private readonly GatheringPointRegistry _gatheringPointRegistry;
private readonly QuestRegistry _questRegistry;
private readonly QuestData _questData;
private readonly TerritoryData _territoryData;
private readonly IGameGui _gameGui;
private readonly ITargetManager _targetManager;
private readonly IClientState _clientState;
private readonly ShopController _shopController;
private readonly BossModIpc _bossModIpc;
private readonly Configuration _configuration;
private readonly ILogger<InteractionUiController> _logger;
private readonly Regex _returnRegex;
private readonly Regex _purchaseItemRegex;
private bool _isInitialCheck;
private bool ShouldHandleUiInteractions
{
get
{
if (!_isInitialCheck && !_questController.IsRunning)
{
return _territoryData.IsQuestBattleInstance(_clientState.TerritoryType);
}
return true;
}
}
public unsafe InteractionUiController(IAddonLifecycle addonLifecycle, IDataManager dataManager, QuestFunctions questFunctions, AetheryteFunctions aetheryteFunctions, ExcelFunctions excelFunctions, QuestController questController, GatheringPointRegistry gatheringPointRegistry, QuestRegistry questRegistry, QuestData questData, TerritoryData territoryData, IGameGui gameGui, ITargetManager targetManager, IPluginLog pluginLog, IClientState clientState, ShopController shopController, BossModIpc bossModIpc, Configuration configuration, ILogger<InteractionUiController> logger)
{
_addonLifecycle = addonLifecycle;
_dataManager = dataManager;
_questFunctions = questFunctions;
_aetheryteFunctions = aetheryteFunctions;
_excelFunctions = excelFunctions;
_questController = questController;
_gatheringPointRegistry = gatheringPointRegistry;
_questRegistry = questRegistry;
_questData = questData;
_territoryData = territoryData;
_gameGui = gameGui;
_targetManager = targetManager;
_clientState = clientState;
_shopController = shopController;
_bossModIpc = bossModIpc;
_configuration = configuration;
_logger = logger;
_returnRegex = _dataManager.GetExcelSheet<Addon>().GetRow(196u).GetRegex((Addon addon) => addon.Text, pluginLog);
_purchaseItemRegex = _dataManager.GetRegex(3406u, (Addon addon) => addon.Text, pluginLog);
_questController.AutomationTypeChanged += HandleCurrentDialogueChoices;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
if (_gameGui.TryGetAddonByName<AtkUnitBase>("RhythmAction", out var addonPtr))
{
addonPtr->Close(fireCallback: true);
}
}
private void HandleCurrentDialogueChoices(object sender, QuestController.EAutomationType automationType)
{
if (automationType != QuestController.EAutomationType.Manual)
{
HandleCurrentDialogueChoices();
}
}
internal unsafe void HandleCurrentDialogueChoices()
{
try
{
_isInitialCheck = true;
if (_gameGui.TryGetAddonByName<AddonSelectString>("SelectString", out var addonPtr))
{
_logger.LogInformation("SelectString window is open");
SelectStringPostSetup(addonPtr, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AddonCutSceneSelectString>("CutSceneSelectString", out var addonPtr2))
{
_logger.LogInformation("CutSceneSelectString window is open");
CutsceneSelectStringPostSetup(addonPtr2, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AddonSelectIconString>("SelectIconString", out var addonPtr3))
{
_logger.LogInformation("SelectIconString window is open");
SelectIconStringPostSetup(addonPtr3, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AddonSelectYesno>("SelectYesno", out var addonPtr4))
{
_logger.LogInformation("SelectYesno window is open");
SelectYesnoPostSetup(addonPtr4, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("DifficultySelectYesNo", out var addonPtr5))
{
_logger.LogInformation("DifficultySelectYesNo window is open");
DifficultySelectYesNoPostSetup(addonPtr5, checkAllSteps: true);
}
if (_gameGui.TryGetAddonByName<AtkUnitBase>("PointMenu", out var addonPtr6))
{
_logger.LogInformation("PointMenu is open");
PointMenuPostSetup(addonPtr6);
}
}
finally
{
_isInitialCheck = false;
}
}
private unsafe void SelectStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectString* address = (AddonSelectString*)args.Addon.Address;
SelectStringPostSetup(address, checkAllSteps: false);
}
private unsafe void SelectStringPostSetup(AddonSelectString* addonSelectString, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = addonSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
if (text == null)
{
return;
}
List<string> list = new List<string>();
for (ushort num = 7; num < addonSelectString->AtkUnitBase.AtkValuesCount; num++)
{
if (addonSelectString->AtkUnitBase.AtkValues[(int)num].Type == FFXIVClientStructs.FFXIV.Component.GUI.ValueType.String)
{
list.Add(addonSelectString->AtkUnitBase.AtkValues[(int)num].ReadAtkString());
}
}
int? num2 = HandleListChoice(text, list, checkAllSteps) ?? HandleInstanceListChoice(text);
if (num2.HasValue)
{
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", num2, text);
addonSelectString->AtkUnitBase.FireCallbackInt(num2.Value);
}
}
private unsafe void CutsceneSelectStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonCutSceneSelectString* address = (AddonCutSceneSelectString*)args.Addon.Address;
CutsceneSelectStringPostSetup(address, checkAllSteps: false);
}
private unsafe void CutsceneSelectStringPostSetup(AddonCutSceneSelectString* addonCutSceneSelectString, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = addonCutSceneSelectString->AtkUnitBase.AtkValues[2].ReadAtkString();
if (text != null)
{
List<string> list = new List<string>();
for (int i = 5; i < addonCutSceneSelectString->AtkUnitBase.AtkValuesCount; i++)
{
list.Add(addonCutSceneSelectString->AtkUnitBase.AtkValues[i].ReadAtkString());
}
int? num = HandleListChoice(text, list, checkAllSteps);
if (num.HasValue)
{
addonCutSceneSelectString->AtkUnitBase.FireCallbackInt(num.Value);
}
}
}
private unsafe void SelectIconStringPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectIconString* address = (AddonSelectIconString*)args.Addon.Address;
SelectIconStringPostSetup(address, checkAllSteps: false);
}
private unsafe void SelectIconStringPostSetup(AddonSelectIconString* addonSelectIconString, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = addonSelectIconString->AtkUnitBase.AtkValues[3].ReadAtkString();
if (string.IsNullOrEmpty(text))
{
text = null;
}
List<string> choices = GetChoices(addonSelectIconString);
int? num = HandleListChoice(text, choices, checkAllSteps);
if (num.HasValue)
{
_logger.LogInformation("Using choice {Choice} for list prompt '{Prompt}'", num, text);
addonSelectIconString->AtkUnitBase.FireCallbackInt(num.Value);
return;
}
string text2 = (*addonSelectIconString->AtkValues).ReadAtkString();
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest != null && (text == null || text2 != null))
{
_logger.LogInformation("Checking if current quest {Name} is on the list", startedQuest.Quest.Info.Name);
if (CheckQuestSelection(addonSelectIconString, startedQuest.Quest, choices))
{
return;
}
QuestStep questStep = startedQuest.Quest.FindSequence(startedQuest.Sequence)?.FindStep(startedQuest.Step);
if (questStep != null && questStep.InteractionType == EInteractionType.AcceptQuest && (object)questStep.PickUpQuestId != null && _questRegistry.TryGetQuest(questStep.PickUpQuestId, out Questionable.Model.Quest quest))
{
_logger.LogInformation("Checking if current picked-up {Name} is on the list", quest.Info.Name);
if (CheckQuestSelection(addonSelectIconString, quest, choices))
{
return;
}
}
}
QuestController.QuestProgress nextQuest = _questController.NextQuest;
if (nextQuest != null && (text == null || text2 != null))
{
_logger.LogInformation("Checking if next quest {Name} is on the list", nextQuest.Quest.Info.Name);
CheckQuestSelection(addonSelectIconString, nextQuest.Quest, choices);
}
}
private unsafe bool CheckQuestSelection(AddonSelectIconString* addonSelectIconString, Questionable.Model.Quest quest, List<string?> answers)
{
string questName = quest.Info.Name;
int num = answers.FindIndex((string? x) => GameFunctions.GameStringEquals(questName, x));
if (num >= 0)
{
_logger.LogInformation("Selecting quest {QuestName}", questName);
addonSelectIconString->AtkUnitBase.FireCallbackInt(num);
return true;
}
return false;
}
public unsafe static List<string?> GetChoices(AddonSelectIconString* addonSelectIconString)
{
List<string> list = new List<string>();
for (ushort num = 0; num < addonSelectIconString->AtkUnitBase.AtkValues[5].Int; num++)
{
list.Add(addonSelectIconString->AtkValues[num * 3 + 7].ReadAtkString());
}
return list;
}
private unsafe int? HandleListChoice(string? actualPrompt, List<string?> answers, bool checkAllSteps)
{
List<DialogueChoiceInfo> list = new List<DialogueChoiceInfo>();
QuestController.QuestProgress questProgress = _questController.SimulatedQuest ?? _questController.GatheringQuest ?? _questController.StartedQuest;
if (questProgress != null)
{
Questionable.Model.Quest quest = questProgress.Quest;
bool flag = false;
List<EAetheryteLocation> source = new List<EAetheryteLocation>();
if (checkAllSteps)
{
QuestSequence? questSequence = quest.FindSequence(questProgress.Sequence);
IEnumerable<DialogueChoice> enumerable = questSequence?.Steps.SelectMany((QuestStep x) => x.DialogueChoices);
if (enumerable != null)
{
list.AddRange(enumerable.Select((DialogueChoice x) => new DialogueChoiceInfo(quest, x)));
}
flag = questSequence?.Steps.Any((QuestStep x) => x.InteractionType == EInteractionType.UnlockTaxiStand) ?? false;
source = (from x in questSequence?.Steps
where x != null && x.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte && x.Aetheryte.HasValue
select x.Aetheryte.Value).ToList() ?? new List<EAetheryteLocation>();
}
else
{
QuestStep questStep = null;
if (_territoryData.IsQuestBattleInstance(_clientState.TerritoryType))
{
questStep = quest.FindSequence(questProgress.Sequence)?.Steps.FirstOrDefault((QuestStep x) => x.InteractionType == EInteractionType.SinglePlayerDuty);
}
if (questStep == null)
{
questStep = quest.FindSequence(questProgress.Sequence)?.FindStep(questProgress.Step);
}
if (questStep == null)
{
_logger.LogDebug("Ignoring current quest dialogue choices, no active step");
}
else
{
list.AddRange(questStep.DialogueChoices.Select((DialogueChoice x) => new DialogueChoiceInfo(quest, x)));
if (questStep.PurchaseMenu != null)
{
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = questStep.PurchaseMenu.ExcelSheet,
Prompt = null,
Answer = questStep.PurchaseMenu.Key
}));
}
if (questStep != null && questStep.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte)
{
EAetheryteLocation? aetheryte = questStep.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault = aetheryte.GetValueOrDefault();
int num = 1;
List<EAetheryteLocation> list2 = new List<EAetheryteLocation>(num);
CollectionsMarshal.SetCount(list2, num);
Span<EAetheryteLocation> span = CollectionsMarshal.AsSpan(list2);
int index = 0;
span[index] = valueOrDefault;
source = list2;
}
}
flag = questStep.InteractionType == EInteractionType.UnlockTaxiStand;
}
}
if (flag)
{
_logger.LogInformation("Adding chocobo taxi stand unlock dialogue choices");
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = "transport/ChocoboTaxiStand",
Prompt = ExcelRef.FromKey("TEXT_CHOCOBOTAXISTAND_00000_Q1_000_1"),
Answer = ExcelRef.FromKey("TEXT_CHOCOBOTAXISTAND_00000_A1_000_3")
}));
}
if (source.Any((EAetheryteLocation x) => _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(x) == AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable))
{
_logger.LogInformation("Adding security token aetheryte unlock dialogue choice");
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = "transport/Aetheryte",
Prompt = ExcelRef.FromKey("TEXT_AETHERYTE_MAINMENU_TITLE"),
PromptIsRegularExpression = true,
Answer = ExcelRef.FromKey("TEXT_AETHERYTE_REGISTER_TOKEN_FAVORITE"),
AnswerIsRegularExpression = true
}));
}
else if (source.Any((EAetheryteLocation x) => _aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(x) == AetheryteRegistrationResult.FavoredDestinationAvailable))
{
_logger.LogInformation("Adding favored aetheryte unlock dialogue choice");
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = "transport/Aetheryte",
Prompt = ExcelRef.FromKey("TEXT_AETHERYTE_MAINMENU_TITLE"),
PromptIsRegularExpression = true,
Answer = ExcelRef.FromKey("TEXT_AETHERYTE_REGISTER_FAVORITE"),
AnswerIsRegularExpression = true
}));
}
ushort? num2 = FindTargetTerritoryFromQuestStep(questProgress);
if (num2.HasValue)
{
foreach (string answer in answers)
{
if (answer != null && TryFindWarp(num2.Value, answer, out uint? warpId, out string warpText))
{
_logger.LogInformation("Adding warp {Id}, {Prompt}", warpId, warpText);
list.Add(new DialogueChoiceInfo(quest, new DialogueChoice
{
Type = EDialogChoiceType.List,
ExcelSheet = null,
Prompt = null,
Answer = ExcelRef.FromSheetValue(warpText)
}));
}
}
}
}
else
{
_logger.LogDebug("Ignoring current quest dialogue choices, no active quest");
}
IGameObject target = _targetManager.Target;
if (target != null)
{
foreach (IQuestInfo item in from x in _questData.GetAllByIssuerDataId(target.DataId)
where x.QuestId is QuestId
select x)
{
if (!_questFunctions.IsReadyToAcceptQuest(item.QuestId) || !_questRegistry.TryGetQuest(item.QuestId, out Questionable.Model.Quest knownQuest))
{
continue;
}
List<DialogueChoice> list3 = knownQuest.FindSequence(0)?.Steps.SelectMany((QuestStep x) => x.DialogueChoices).ToList();
if (list3 != null && list3.Count > 0)
{
_logger.LogInformation("Adding {Count} dialogue choices from not accepted quest {QuestName}", list3.Count, item.Name);
list.AddRange(list3.Select((DialogueChoice x) => new DialogueChoiceInfo(knownQuest, x)));
}
}
}
if (list.Count == 0)
{
_logger.LogDebug("No dialogue choices to check");
return null;
}
foreach (var (quest3, dialogueChoice2) in list)
{
if (dialogueChoice2.Type != EDialogChoiceType.List)
{
continue;
}
if (dialogueChoice2.SpecialCondition == "NoDutyActions")
{
try
{
ContentDirector* contentDirector = EventFramework.Instance()->GetContentDirector();
if (contentDirector != null && contentDirector->DutyActionManager.ActionsPresent)
{
_logger.LogInformation("NoDutyActions: actions present, skipping dialogue choice");
continue;
}
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to check for duty actions");
continue;
}
}
if (dialogueChoice2.Answer == null)
{
_logger.LogDebug("Ignoring entry in DialogueChoices, no answer");
continue;
}
if (dialogueChoice2.DataId.HasValue && dialogueChoice2.DataId != _targetManager.Target?.DataId)
{
_logger.LogDebug("Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}", dialogueChoice2.DataId, _targetManager.Target?.DataId);
continue;
}
StringOrRegex stringOrRegex = ResolveReference(quest3, dialogueChoice2.ExcelSheet, dialogueChoice2.Prompt, dialogueChoice2.PromptIsRegularExpression);
StringOrRegex stringOrRegex2 = ResolveReference(quest3, dialogueChoice2.ExcelSheet, dialogueChoice2.Answer, dialogueChoice2.AnswerIsRegularExpression);
if (actualPrompt == null && stringOrRegex != null)
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}", stringOrRegex);
continue;
}
if (actualPrompt != null && (stringOrRegex == null || !IsMatch(actualPrompt, stringOrRegex)))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", stringOrRegex, actualPrompt);
continue;
}
for (int num3 = 0; num3 < answers.Count; num3++)
{
_logger.LogTrace("Checking if {ActualAnswer} == {ExpectedAnswer}", answers[num3], stringOrRegex2);
if (!IsMatch(answers[num3], stringOrRegex2))
{
continue;
}
_logger.LogInformation("Returning {Index}: '{Answer}' for '{Prompt}'", num3, answers[num3], actualPrompt);
if (quest3?.Id is SatisfactionSupplyNpcId)
{
if (_questController.GatheringQuest == null || _questController.GatheringQuest.Sequence == byte.MaxValue)
{
return null;
}
_questController.GatheringQuest.SetSequence(1);
_questController.StartGatheringQuest("SatisfactionSupply turn in");
}
return num3;
}
}
_logger.LogInformation("No matching answer found for {Prompt}.", actualPrompt);
return null;
}
private static bool IsMatch(string? actualAnswer, StringOrRegex? expectedAnswer)
{
if (actualAnswer == null && expectedAnswer == null)
{
return true;
}
if (actualAnswer == null || expectedAnswer == null)
{
return false;
}
return expectedAnswer.IsMatch(actualAnswer);
}
private int? HandleInstanceListChoice(string? actualPrompt)
{
string b = _excelFunctions.GetDialogueTextByRowId("Addon", 2090u, isRegex: false).GetString();
if (GameFunctions.GameStringEquals(actualPrompt, b))
{
_logger.LogInformation("Selecting no prefered instance as answer for '{Prompt}'", actualPrompt);
return 0;
}
return null;
}
private unsafe void SelectYesnoPostSetup(AddonEvent type, AddonArgs args)
{
AddonSelectYesno* address = (AddonSelectYesno*)args.Addon.Address;
SelectYesnoPostSetup(address, checkAllSteps: false);
}
private unsafe void SelectYesnoPostSetup(AddonSelectYesno* addonSelectYesno, bool checkAllSteps)
{
if (!ShouldHandleUiInteractions)
{
return;
}
string text = (*addonSelectYesno->AtkUnitBase.AtkValues).ReadAtkString();
if (text == null)
{
return;
}
_logger.LogTrace("Prompt: '{Prompt}'", text);
if (_shopController.IsAwaitingYesNo && _purchaseItemRegex.IsMatch(text))
{
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
_shopController.IsAwaitingYesNo = false;
return;
}
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest != null && CheckQuestYesNo(addonSelectYesno, startedQuest, text, checkAllSteps))
{
return;
}
QuestController.QuestProgress simulatedQuest = _questController.SimulatedQuest;
if (simulatedQuest == null || !HandleTravelYesNo(addonSelectYesno, simulatedQuest, text))
{
QuestController.QuestProgress nextQuest = _questController.NextQuest;
if (nextQuest != null)
{
CheckQuestYesNo(addonSelectYesno, nextQuest, text, checkAllSteps);
}
}
}
private unsafe bool CheckQuestYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest, string actualPrompt, bool checkAllSteps)
{
Questionable.Model.Quest quest = currentQuest.Quest;
if (checkAllSteps)
{
QuestSequence questSequence = quest.FindSequence(currentQuest.Sequence);
if (questSequence != null && questSequence.Steps.Any((QuestStep step) => HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt)))
{
return true;
}
}
else
{
QuestStep questStep = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
if (questStep != null && HandleDefaultYesNo(addonSelectYesno, quest, questStep, questStep.DialogueChoices, actualPrompt))
{
return true;
}
}
if (HandleTravelYesNo(addonSelectYesno, currentQuest, actualPrompt))
{
return true;
}
return false;
}
private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Questionable.Model.Quest quest, QuestStep? step, List<DialogueChoice> dialogueChoices, string actualPrompt)
{
if (step != null && step.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte)
{
EAetheryteLocation? aetheryte = step.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault = aetheryte.GetValueOrDefault();
Span<DialogueChoice> span2;
Span<DialogueChoice> span;
switch (_aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(valueOrDefault))
{
case AetheryteRegistrationResult.SecurityTokenFreeDestinationAvailable:
{
List<DialogueChoice> list = dialogueChoices;
int num2 = 1 + list.Count;
List<DialogueChoice> list3 = new List<DialogueChoice>(num2);
CollectionsMarshal.SetCount(list3, num2);
span2 = CollectionsMarshal.AsSpan(list3);
int num = 0;
span = CollectionsMarshal.AsSpan(list);
span.CopyTo(span2.Slice(num, span.Length));
num += span.Length;
span2[num] = new DialogueChoice
{
Type = EDialogChoiceType.YesNo,
ExcelSheet = "Addon",
Prompt = ExcelRef.FromRowId(102334u),
Yes = true
};
dialogueChoices = list3;
break;
}
case AetheryteRegistrationResult.FavoredDestinationAvailable:
{
List<DialogueChoice> list = dialogueChoices;
int num = 1 + list.Count;
List<DialogueChoice> list2 = new List<DialogueChoice>(num);
CollectionsMarshal.SetCount(list2, num);
span = CollectionsMarshal.AsSpan(list2);
int num2 = 0;
span2 = CollectionsMarshal.AsSpan(list);
span2.CopyTo(span.Slice(num2, span2.Length));
num2 += span2.Length;
span[num2] = new DialogueChoice
{
Type = EDialogChoiceType.YesNo,
ExcelSheet = "Addon",
Prompt = ExcelRef.FromRowId(102306u),
Yes = true
};
dialogueChoices = list2;
break;
}
}
}
}
_logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
foreach (DialogueChoice dialogueChoice in dialogueChoices)
{
if (dialogueChoice.Type != EDialogChoiceType.YesNo)
{
continue;
}
if (dialogueChoice.DataId.HasValue && dialogueChoice.DataId != _targetManager.Target?.DataId)
{
_logger.LogDebug("Skipping entry in DialogueChoice expecting target dataId {ExpectedDataId}, actual target is {ActualTargetId}", dialogueChoice.DataId, _targetManager.Target?.DataId);
continue;
}
StringOrRegex stringOrRegex = ResolveReference(quest, dialogueChoice.ExcelSheet, dialogueChoice.Prompt, dialogueChoice.PromptIsRegularExpression);
if (stringOrRegex == null || !IsMatch(actualPrompt, stringOrRegex))
{
_logger.LogInformation("Unexpected excelPrompt: {ExcelPrompt}, actualPrompt: {ActualPrompt}", stringOrRegex, actualPrompt);
continue;
}
_logger.LogInformation("Returning {YesNo} for '{Prompt}'", dialogueChoice.Yes ? "Yes" : "No", actualPrompt);
addonSelectYesno->AtkUnitBase.FireCallbackInt((!dialogueChoice.Yes) ? 1 : 0);
return true;
}
if (CheckSinglePlayerDutyYesNo(quest.Id, step))
{
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
return false;
}
private bool CheckSinglePlayerDutyYesNo(ElementId questId, QuestStep? step)
{
if (step != null && step.InteractionType == EInteractionType.SinglePlayerDuty && _bossModIpc.IsConfiguredToRunSoloInstance(questId, step.SinglePlayerDutyOptions))
{
_logger.LogInformation("SinglePlayerDutyYesNo: probably Single Player Duty");
return true;
}
return false;
}
private unsafe bool HandleTravelYesNo(AddonSelectYesno* addonSelectYesno, QuestController.QuestProgress currentQuest, string actualPrompt)
{
_logger.LogInformation("TravelYesNo");
if (_aetheryteFunctions.ReturnRequestedAt >= DateTime.Now.AddSeconds(-2.0) && _returnRegex.IsMatch(actualPrompt))
{
_logger.LogInformation("Automatically confirming return...");
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
if (_questController.IsRunning && _gameGui.TryGetAddonByName<AtkUnitBase>("HousingSelectBlock", out var _))
{
_logger.LogInformation("Automatically confirming ward selection");
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
ushort? num = FindTargetTerritoryFromQuestStep(currentQuest);
if (num.HasValue && TryFindWarp(num.Value, actualPrompt, out uint? warpId, out string warpText))
{
_logger.LogInformation("Using warp {Id}, {Prompt}", warpId, warpText);
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
return false;
}
private unsafe void DifficultySelectYesNoPostSetup(AddonEvent type, AddonArgs args)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
DifficultySelectYesNoPostSetup(address, checkAllSteps: false);
}
private unsafe void DifficultySelectYesNoPostSetup(AtkUnitBase* addonDifficultySelectYesNo, bool checkAllSteps)
{
if (!_questController.IsRunning)
{
return;
}
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest == null)
{
return;
}
Questionable.Model.Quest quest = startedQuest.Quest;
bool flag;
if (checkAllSteps)
{
flag = quest.FindSequence(startedQuest.Sequence)?.Steps.Any((QuestStep step) => CheckSinglePlayerDutyYesNo(quest.Id, step)) ?? false;
}
else
{
QuestStep questStep = quest.FindSequence(startedQuest.Sequence)?.FindStep(startedQuest.Step);
flag = questStep != null && CheckSinglePlayerDutyYesNo(quest.Id, questStep);
}
if (flag)
{
_logger.LogInformation("Confirming difficulty ({Difficulty}) for quest battle", _configuration.SinglePlayerDuties.RetryDifficulty);
AtkValue* values = stackalloc AtkValue[2]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 0
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = _configuration.SinglePlayerDuties.RetryDifficulty
}
};
addonDifficultySelectYesNo->FireCallback(2u, values);
}
}
private ushort? FindTargetTerritoryFromQuestStep(QuestController.QuestProgress currentQuest)
{
QuestSequence questSequence = currentQuest.Quest.FindSequence(currentQuest.Sequence);
if (questSequence == null)
{
return null;
}
QuestStep questStep = questSequence.FindStep(currentQuest.Step);
if (questStep != null)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep (current): {CurrentTerritory}, {TargetTerritory}", questStep.TerritoryId, questStep.TargetTerritoryId);
}
if (questStep != null && (questStep.TerritoryId != _clientState.TerritoryType || !questStep.TargetTerritoryId.HasValue) && questStep.InteractionType == EInteractionType.Gather && _gatheringPointRegistry.TryGetGatheringPointId(questStep.ItemsToGather[0].ItemId, ((EClassJob?)_clientState.LocalPlayer?.ClassJob.RowId).GetValueOrDefault(), out GatheringPointId gatheringPointId) && _gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot gatheringRoot))
{
foreach (QuestStep step in gatheringRoot.Steps)
{
if (step.TerritoryId == _clientState.TerritoryType && step.TargetTerritoryId.HasValue)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep (gathering): {CurrentTerritory}, {TargetTerritory}", step.TerritoryId, step.TargetTerritoryId);
return step.TargetTerritoryId;
}
}
}
if (questStep == null || !questStep.TargetTerritoryId.HasValue)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep: Checking previous step...");
questStep = questSequence.FindStep((currentQuest.Step == 255) ? (questSequence.Steps.Count - 1) : (currentQuest.Step - 1));
if (questStep != null)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep (previous): {CurrentTerritory}, {TargetTerritory}", questStep.TerritoryId, questStep.TargetTerritoryId);
}
}
if (questStep == null || !questStep.TargetTerritoryId.HasValue)
{
_logger.LogTrace("FindTargetTerritoryFromQuestStep: Not found");
return null;
}
_logger.LogDebug("Target territory for quest step: {TargetTerritory}", questStep.TargetTerritoryId);
return questStep.TargetTerritoryId;
}
private bool TryFindWarp(ushort targetTerritoryId, string actualPrompt, [NotNullWhen(true)] out uint? warpId, [NotNullWhen(true)] out string? warpText)
{
foreach (Warp item in from x in _dataManager.GetExcelSheet<Warp>()
where x.RowId != 0 && x.TerritoryType.RowId == targetTerritoryId
select x)
{
string text = item.Name.WithCertainMacroCodeReplacements();
string text2 = item.Question.WithCertainMacroCodeReplacements();
if (!string.IsNullOrEmpty(text2) && GameFunctions.GameStringEquals(text2, actualPrompt))
{
warpId = item.RowId;
warpText = text2;
return true;
}
if (!string.IsNullOrEmpty(text) && GameFunctions.GameStringEquals(text, actualPrompt))
{
warpId = item.RowId;
warpText = text;
return true;
}
_logger.LogDebug("Ignoring prompt '{Prompt}'", text2);
}
warpId = null;
warpText = null;
return false;
}
private unsafe void PointMenuPostSetup(AddonEvent type, AddonArgs args)
{
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
PointMenuPostSetup(address);
}
private unsafe void PointMenuPostSetup(AtkUnitBase* addonPointMenu)
{
if (!ShouldHandleUiInteractions)
{
return;
}
QuestController.QuestProgress startedQuest = _questController.StartedQuest;
if (startedQuest == null)
{
_logger.LogInformation("Ignoring point menu, no active quest");
return;
}
QuestSequence questSequence = startedQuest.Quest.FindSequence(startedQuest.Sequence);
if (questSequence == null)
{
return;
}
QuestStep questStep = questSequence.FindStep(startedQuest.Step);
if (questStep == null)
{
return;
}
if (questStep.PointMenuChoices.Count == 0)
{
_logger.LogWarning("No point menu choices");
return;
}
int pointMenuCounter = startedQuest.StepProgress.PointMenuCounter;
if (pointMenuCounter >= questStep.PointMenuChoices.Count)
{
_logger.LogWarning("No remaining point menu choices");
return;
}
uint num = questStep.PointMenuChoices[pointMenuCounter];
_logger.LogInformation("Handling point menu, picking choice {Choice} (index = {Index})", num, pointMenuCounter);
AtkValue* values = stackalloc AtkValue[2]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 13
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt,
UInt = num
}
};
addonPointMenu->FireCallback(2u, values);
startedQuest.IncreasePointMenuCounter();
}
private unsafe void HousingSelectBlockPostSetup(AddonEvent type, AddonArgs args)
{
if (ShouldHandleUiInteractions)
{
_logger.LogInformation("Confirming selected housing ward");
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
address->FireCallbackInt(0);
}
}
private StringOrRegex? ResolveReference(Questionable.Model.Quest? quest, string? excelSheet, ExcelRef? excelRef, bool isRegExp)
{
if (excelRef == null)
{
return null;
}
if (excelRef.Type == ExcelRef.EType.Key)
{
return _excelFunctions.GetDialogueText(quest, excelSheet, excelRef.AsKey(), isRegExp);
}
if (excelRef.Type == ExcelRef.EType.RowId)
{
return _excelFunctions.GetDialogueTextByRowId(excelSheet, excelRef.AsRowId(), isRegExp);
}
if (excelRef.Type == ExcelRef.EType.RawString)
{
return new StringOrRegex(excelRef.AsRawString());
}
return null;
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "HousingSelectBlock", HousingSelectBlockPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "PointMenu", PointMenuPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "DifficultySelectYesNo", DifficultySelectYesNoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectYesno", SelectYesnoPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectIconString", SelectIconStringPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "CutSceneSelectString", CutsceneSelectStringPostSetup);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "SelectString", SelectStringPostSetup);
_questController.AutomationTypeChanged -= HandleCurrentDialogueChoices;
}
}

View file

@ -0,0 +1,177 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using LLib.Shop;
using LLib.Shop.Model;
using Microsoft.Extensions.Logging;
using Questionable.Model.Questing;
namespace Questionable.Controller.GameUi;
internal sealed class ShopController : IDisposable, IShopWindow
{
private readonly QuestController _questController;
private readonly IGameGui _gameGui;
private readonly IFramework _framework;
private readonly RegularShopBase _shop;
private readonly ILogger<ShopController> _logger;
public bool IsEnabled => _questController.IsRunning;
public bool IsOpen { get; set; }
public bool IsAutoBuyEnabled => _shop.AutoBuyEnabled;
public bool IsAwaitingYesNo
{
get
{
return _shop.IsAwaitingYesNo;
}
set
{
_shop.IsAwaitingYesNo = value;
}
}
public Vector2? Position { get; set; }
public ShopController(QuestController questController, IGameGui gameGui, IAddonLifecycle addonLifecycle, IFramework framework, ILogger<ShopController> logger, IPluginLog pluginLog)
{
_questController = questController;
_gameGui = gameGui;
_framework = framework;
_shop = new RegularShopBase(this, "Shop", pluginLog, gameGui, addonLifecycle);
_logger = logger;
_framework.Update += FrameworkUpdate;
}
public void Dispose()
{
_framework.Update -= FrameworkUpdate;
_shop.Dispose();
}
private void FrameworkUpdate(IFramework framework)
{
if (!IsOpen || _shop.ItemForSale == null)
{
return;
}
if (_shop.PurchaseState != null)
{
_shop.HandleNextPurchaseStep();
return;
}
QuestStep questStep = FindCurrentStep();
if (questStep != null && questStep.InteractionType == EInteractionType.PurchaseItem)
{
int num = Math.Max(0, questStep.ItemCount.GetValueOrDefault() - (int)_shop.ItemForSale.OwnedItems);
if (Math.Min(_shop.GetMaxItemsToPurchase(), num) > 0)
{
_logger.LogDebug("Auto-buying {MissingItems} {ItemName}", num, _shop.ItemForSale.ItemName);
_shop.StartAutoPurchase(num);
_shop.HandleNextPurchaseStep();
}
else
{
_shop.CancelAutoPurchase();
}
}
}
public int GetCurrencyCount()
{
return _shop.GetItemCount(1u);
}
private QuestStep? FindCurrentStep()
{
QuestController.QuestProgress currentQuest = _questController.CurrentQuest;
return (currentQuest?.Quest.FindSequence(currentQuest.Sequence))?.FindStep(currentQuest?.Step ?? 0);
}
public unsafe void UpdateShopStock(AtkUnitBase* addon)
{
QuestStep currentStep = FindCurrentStep();
if (currentStep == null || currentStep.InteractionType != EInteractionType.PurchaseItem)
{
_shop.ItemForSale = null;
return;
}
if (addon->AtkValuesCount != 625)
{
_logger.LogError("Unexpected amount of atkvalues for Shop addon ({AtkValueCount})", addon->AtkValuesCount);
_shop.ItemForSale = null;
return;
}
AtkValue* atkValues = addon->AtkValues;
if (atkValues->UInt != 0)
{
_shop.ItemForSale = null;
return;
}
uint uInt = atkValues[2].UInt;
if (uInt == 0)
{
_shop.ItemForSale = null;
return;
}
_shop.ItemForSale = (from i in Enumerable.Range(0, (int)uInt)
select new ItemForSale
{
Position = i,
ItemName = atkValues[14 + i].ReadAtkString(),
Price = atkValues[75 + i].UInt,
OwnedItems = atkValues[136 + i].UInt,
ItemId = atkValues[441 + i].UInt
}).FirstOrDefault((ItemForSale x) => x.ItemId == currentStep.ItemId);
}
public unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow)
{
AtkValue* values = stackalloc AtkValue[4]
{
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = 0
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = _shop.ItemForSale.Position
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
Int = buyNow
},
new AtkValue
{
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Undefined,
Int = 0
}
};
addonShop->FireCallback(4u, values);
}
public void SaveExternalPluginState()
{
}
public unsafe void RestoreExternalPluginState()
{
if (_gameGui.TryGetAddonByName<AtkUnitBase>("Shop", out var addonPtr))
{
addonPtr->FireCallbackInt(-1);
}
}
}

View file

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace Questionable.Controller.NavigationOverrides;
public sealed record AlternateLocation(Vector3 Point, bool RecalculateNavmesh)
{
public override string ToString()
{
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
DefaultInterpolatedStringHandler handler = new DefaultInterpolatedStringHandler(6, 4, invariantCulture);
handler.AppendFormatted("Point");
handler.AppendLiteral(": ");
handler.AppendFormatted(Point, "G");
handler.AppendLiteral(", ");
handler.AppendFormatted("RecalculateNavmesh");
handler.AppendLiteral(": ");
handler.AppendFormatted(RecalculateNavmesh);
return string.Create(invariantCulture, ref handler);
}
}

View file

@ -0,0 +1,16 @@
using System.Numerics;
namespace Questionable.Controller.NavigationOverrides;
internal sealed record BlacklistedArea(ushort TerritoryId, Vector3 Center, float MinDistance, float MaxDistance, bool RecalculateNavmesh = false) : IBlacklistedLocation
{
public AlternateLocation? AdjustPoint(Vector3 point)
{
float num = (point - Center).Length();
if (num < MinDistance || num > MaxDistance)
{
return null;
}
return new AlternateLocation(Center + Vector3.Normalize(point - Center) * MaxDistance, RecalculateNavmesh);
}
}

View file

@ -0,0 +1,15 @@
using System.Numerics;
namespace Questionable.Controller.NavigationOverrides;
public sealed record BlacklistedPoint(ushort TerritoryId, Vector3 Original, Vector3 Replacement, float CheckDistance = 0.05f, bool RecalculateNavmesh = false) : IBlacklistedLocation
{
public AlternateLocation? AdjustPoint(Vector3 point)
{
if ((point - Original).Length() > CheckDistance)
{
return null;
}
return new AlternateLocation(Replacement, RecalculateNavmesh);
}
}

View file

@ -0,0 +1,10 @@
using System.Numerics;
namespace Questionable.Controller.NavigationOverrides;
internal interface IBlacklistedLocation
{
ushort TerritoryId { get; }
AlternateLocation? AdjustPoint(Vector3 point);
}

View file

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.NavigationOverrides;
internal sealed class MovementOverrideController
{
private static readonly List<IBlacklistedLocation> BlacklistedLocations;
private readonly IClientState _clientState;
private readonly ILogger<MovementOverrideController> _logger;
public MovementOverrideController(IClientState clientState, ILogger<MovementOverrideController> logger)
{
_clientState = clientState;
_logger = logger;
}
public (List<Vector3>, bool) AdjustPath(List<Vector3> navPoints)
{
foreach (IBlacklistedLocation blacklistedLocation in BlacklistedLocations)
{
if (_clientState.TerritoryType != blacklistedLocation.TerritoryId)
{
continue;
}
for (int i = 0; i < navPoints.Count; i++)
{
AlternateLocation alternateLocation = blacklistedLocation.AdjustPoint(navPoints[i]);
if (alternateLocation != null)
{
_logger.LogInformation("Fudging navmesh point from {Original} to {Replacement} in blacklisted area", navPoints[i].ToString("G", CultureInfo.InvariantCulture), alternateLocation);
navPoints[i] = alternateLocation.Point;
if (alternateLocation.RecalculateNavmesh)
{
return (navPoints.Take(i + 1).ToList(), true);
}
}
}
}
return (navPoints, false);
}
static MovementOverrideController()
{
int num = 31;
List<IBlacklistedLocation> list = new List<IBlacklistedLocation>(num);
CollectionsMarshal.SetCount(list, num);
Span<IBlacklistedLocation> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = new BlacklistedArea(1191, new Vector3(-223.0412f, 31.937134f, -584.03906f), 5f, 7.75f);
num2++;
span[num2] = new BlacklistedPoint(128, new Vector3(2f, 40.25f, 36.5f), new Vector3(0.25f, 40.25f, 36.5f));
num2++;
span[num2] = new BlacklistedPoint(132, new Vector3(29f, -8f, 120.5f), new Vector3(28.265165f, -8.000001f, 120.149734f));
num2++;
span[num2] = new BlacklistedPoint(132, new Vector3(28.25f, -8f, 125f), new Vector3(27.372725f, -8.200001f, 125.55859f));
num2++;
span[num2] = new BlacklistedPoint(132, new Vector3(32.25f, -8f, 126.5f), new Vector3(32.022232f, -8.200011f, 126.86095f));
num2++;
span[num2] = new BlacklistedPoint(205, new Vector3(26.75f, 0.5f, 20.75f), new Vector3(27.179117f, 0.26728272f, 19.714373f));
num2++;
span[num2] = new BlacklistedPoint(130, new Vector3(59.5f, 4.25f, -118f), new Vector3(60.551353f, 4f, -119.76446f));
num2++;
span[num2] = new BlacklistedPoint(145, new Vector3(-139.75f, -32.25f, 75.25f), new Vector3(-139.57748f, -33.785175f, 77.87906f));
num2++;
span[num2] = new BlacklistedPoint(146, new Vector3(-201.75f, 10.5f, -265.5f), new Vector3(-203.75235f, 10.130764f, -265.15314f));
num2++;
span[num2] = new BlacklistedArea(135, new Vector3(156.11499f, 15.518433f, 673.21277f), 0.5f, 5f);
num2++;
span[num2] = new BlacklistedPoint(139, new Vector3(366f, -2.5f, 95.5f), new Vector3(362.65973f, -3.4f, 96.6896f), 2f);
num2++;
span[num2] = new BlacklistedPoint(155, new Vector3(-478.75f, 149.25f, -305.75f), new Vector3(-476.1802f, 149.06573f, -304.7811f));
num2++;
span[num2] = new BlacklistedPoint(351, new Vector3(3.25f, 0.75f, 8.5f), new Vector3(4f, 0f, 9.5f));
num2++;
span[num2] = new BlacklistedPoint(418, new Vector3(-136.75f, 2.75f, 9f), new Vector3(-138.66408f, 2.0333426f, 8.860787f), 1f);
num2++;
span[num2] = new BlacklistedPoint(401, new Vector3(-14.75f, -136.75f, 515.75f), new Vector3(-17.631899f, -137.39148f, 512.6676f), 2f);
num2++;
span[num2] = new BlacklistedPoint(397, new Vector3(-93.75f, 87.75f, -715.5f), new Vector3(-87.78183f, 87.188995f, -713.3343f), 2f);
num2++;
span[num2] = new BlacklistedPoint(400, new Vector3(384f, -74f, 648.75f), new Vector3(386.0543f, -72.409454f, 652.0184f), 3f);
num2++;
span[num2] = new BlacklistedPoint(399, new Vector3(-514.4851f, 149.63762f, -480.58087f), new Vector3(-528.78656f, 151.17374f, -473.07077f), 5f, RecalculateNavmesh: true);
num2++;
span[num2] = new BlacklistedPoint(399, new Vector3(-534.5f, 153f, -476.75f), new Vector3(-528.78656f, 151.17374f, -473.07077f), 5f, RecalculateNavmesh: true);
num2++;
span[num2] = new BlacklistedPoint(478, new Vector3(14.5f, 215.25f, -101.5f), new Vector3(18.133032f, 215.44998f, -107.83075f), 5f);
num2++;
span[num2] = new BlacklistedPoint(478, new Vector3(11f, 215.5f, -104.5f), new Vector3(18.133032f, 215.44998f, -107.83075f), 5f);
num2++;
span[num2] = new BlacklistedPoint(1189, new Vector3(574f, -142.25f, 504.25f), new Vector3(574.44183f, -142.12766f, 507.60065f));
num2++;
span[num2] = new BlacklistedPoint(814, new Vector3(-324f, 348.75f, -181.75f), new Vector3(-322.75076f, 347.0529f, -177.69328f), 3f);
num2++;
span[num2] = new BlacklistedPoint(956, new Vector3(6.25f, -27.75f, -41.5f), new Vector3(5.0831127f, -28.213453f, -42.239136f));
num2++;
span[num2] = new BlacklistedPoint(1189, new Vector3(-115.75f, -213.75f, 336.5f), new Vector3(-112.40265f, -215.01514f, 339.0067f), 2f);
num2++;
span[num2] = new BlacklistedPoint(1190, new Vector3(-292.29004f, 18.598045f, -133.83907f), new Vector3(-288.20895f, 18.652182f, -132.67445f), 4f);
num2++;
span[num2] = new BlacklistedPoint(1191, new Vector3(-108f, 29.25f, -350.75f), new Vector3(-107.56289f, 29.008266f, -348.80087f));
num2++;
span[num2] = new BlacklistedPoint(1191, new Vector3(-105.75f, 29.75f, -351f), new Vector3(-105.335304f, 29.017048f, -348.85077f));
num2++;
span[num2] = new BlacklistedPoint(1186, new Vector3(284.25f, 50.75f, 171.25f), new Vector3(284.25f, 50.75f, 166.25f));
num2++;
span[num2] = new BlacklistedPoint(1186, new Vector3(283.75f, 50.75f, 167.25f), new Vector3(284.25f, 50.75f, 166.25f));
num2++;
span[num2] = new BlacklistedPoint(1186, new Vector3(287.75f, 51.25f, 172f), new Vector3(288.875f, 50.75f, 166.25f));
BlacklistedLocations = list;
}
}

View file

@ -0,0 +1,43 @@
using System;
namespace Questionable.Controller.Steps.Common;
internal abstract class AbstractDelayedTaskExecutor<T> : TaskExecutor<T> where T : class, ITask
{
private DateTime _continueAt;
protected TimeSpan Delay { get; set; }
protected AbstractDelayedTaskExecutor()
: this(TimeSpan.FromSeconds(5L))
{
}
protected AbstractDelayedTaskExecutor(TimeSpan delay)
{
Delay = delay;
}
protected sealed override bool Start()
{
bool result = StartInternal();
_continueAt = DateTime.Now.Add(Delay);
return result;
}
protected abstract bool StartInternal();
public override ETaskResult Update()
{
if (_continueAt >= DateTime.Now)
{
return ETaskResult.StillRunning;
}
return UpdateInternal();
}
protected virtual ETaskResult UpdateInternal()
{
return ETaskResult.TaskComplete;
}
}

View file

@ -0,0 +1,244 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
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;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Common;
internal static class Mount
{
internal sealed record MountTask : ITask
{
public ushort TerritoryId { get; init; }
public EMountIf MountIf { get; init; }
public FFXIVClientStructs.FFXIV.Common.Math.Vector3? Position { get; }
public MountTask(ushort TerritoryId, EMountIf MountIf, FFXIVClientStructs.FFXIV.Common.Math.Vector3? Position = null)
{
this.TerritoryId = TerritoryId;
this.MountIf = MountIf;
this.Position = ((MountIf == EMountIf.AwayFromPosition) ? new FFXIVClientStructs.FFXIV.Common.Math.Vector3?(Position ?? throw new ArgumentNullException("Position")) : ((FFXIVClientStructs.FFXIV.Common.Math.Vector3?)null));
base._002Ector();
}
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return "Mount";
}
[CompilerGenerated]
public void Deconstruct(out ushort TerritoryId, out EMountIf MountIf, out FFXIVClientStructs.FFXIV.Common.Math.Vector3? Position)
{
TerritoryId = this.TerritoryId;
MountIf = this.MountIf;
Position = this.Position;
}
}
internal sealed class MountEvaluator(GameFunctions gameFunctions, ICondition condition, TerritoryData territoryData, IClientState clientState, ILogger<MountEvaluator> logger)
{
public unsafe MountResult EvaluateMountState(MountTask task, bool dryRun, ref DateTime retryAt)
{
if (condition[ConditionFlag.Mounted])
{
return MountResult.DontMount;
}
LogLevel logLevel = (dryRun ? LogLevel.None : LogLevel.Information);
if (!territoryData.CanUseMount(task.TerritoryId))
{
logger.Log(logLevel, "Can't use mount in current territory {Id}", task.TerritoryId);
return MountResult.DontMount;
}
if (gameFunctions.HasStatusPreventingMount())
{
logger.Log(logLevel, "Can't mount due to status preventing sprint or mount");
return MountResult.DontMount;
}
if (task.MountIf == EMountIf.AwayFromPosition)
{
float num = System.Numerics.Vector3.Distance((FFXIVClientStructs.FFXIV.Common.Math.Vector3)(clientState.LocalPlayer?.Position ?? ((System.Numerics.Vector3)FFXIVClientStructs.FFXIV.Common.Math.Vector3.Zero)), task.Position.GetValueOrDefault());
if (task.TerritoryId == clientState.TerritoryType && num < 30f && !Conditions.Instance()->Diving)
{
logger.Log(logLevel, "Not using mount, as we're close to the target");
return MountResult.DontMount;
}
logger.Log(logLevel, "Want to use mount if away from destination ({Distance} yalms), trying (in territory {Id})...", num, task.TerritoryId);
}
else
{
logger.Log(logLevel, "Want to use mount, trying (in territory {Id})...", task.TerritoryId);
}
if (!condition[ConditionFlag.InCombat])
{
if (dryRun)
{
retryAt = DateTime.Now.AddSeconds(0.5);
}
return MountResult.Mount;
}
return MountResult.WhenOutOfCombat;
}
}
internal sealed class MountExecutor(GameFunctions gameFunctions, ICondition condition, MountEvaluator mountEvaluator, ILogger<MountExecutor> logger) : TaskExecutor<MountTask>()
{
private bool _mountTriggered;
private DateTime _retryAt = DateTime.MinValue;
protected override bool Start()
{
_mountTriggered = false;
return mountEvaluator.EvaluateMountState(base.Task, dryRun: false, ref _retryAt) == MountResult.Mount;
}
public override ETaskResult Update()
{
if (_mountTriggered && !condition[ConditionFlag.Mounted] && DateTime.Now > _retryAt)
{
logger.LogInformation("Not mounted, retrying...");
_mountTriggered = false;
_retryAt = DateTime.MaxValue;
}
if (!_mountTriggered)
{
if (gameFunctions.HasStatusPreventingMount())
{
logger.LogInformation("Can't mount due to status preventing sprint or mount");
return ETaskResult.TaskComplete;
}
base.ProgressContext = InteractionProgressContext.FromActionUse(() => _mountTriggered = gameFunctions.Mount());
_retryAt = DateTime.Now.AddSeconds(5.0);
return ETaskResult.StillRunning;
}
if (!condition[ConditionFlag.Mounted])
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal enum MountResult
{
DontMount,
Mount,
WhenOutOfCombat
}
internal sealed record UnmountTask : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return "Unmount";
}
}
internal sealed class UnmountExecutor(ICondition condition, ILogger<UnmountTask> logger, GameFunctions gameFunctions, IClientState clientState) : TaskExecutor<UnmountTask>()
{
private bool _unmountTriggered;
private DateTime _continueAt = DateTime.MinValue;
protected override bool Start()
{
if (!condition[ConditionFlag.Mounted])
{
return false;
}
logger.LogInformation("Step explicitly wants no mount, trying to unmount...");
if (condition[ConditionFlag.InFlight])
{
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return true;
}
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return true;
}
public override ETaskResult Update()
{
if (_continueAt >= DateTime.Now)
{
return ETaskResult.StillRunning;
}
if (IsUnmounting())
{
return ETaskResult.StillRunning;
}
if (!_unmountTriggered)
{
if (condition[ConditionFlag.InFlight])
{
gameFunctions.Unmount();
}
else
{
_unmountTriggered = gameFunctions.Unmount();
}
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
if (condition[ConditionFlag.Mounted] && condition[ConditionFlag.InCombat])
{
_unmountTriggered = gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
if (!condition[ConditionFlag.Mounted])
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
private unsafe bool IsUnmounting()
{
IPlayerCharacter localPlayer = clientState.LocalPlayer;
if (localPlayer != null)
{
BattleChara* address = (BattleChara*)localPlayer.Address;
return (address->Mount.Flags & 1) == 1;
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
public enum EMountIf
{
Always,
AwayFromPosition
}
}

View file

@ -0,0 +1,86 @@
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Common;
internal static class NextQuest
{
internal sealed class Factory(QuestFunctions questFunctions) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.CompleteQuest)
{
return null;
}
if (step.NextQuestId == null)
{
return null;
}
if (step.NextQuestId == quest.Id)
{
return null;
}
if (questFunctions.GetPriorityQuests(onlyClassAndRoleQuests: true).Contains(step.NextQuestId))
{
return null;
}
return new SetQuestTask(step.NextQuestId, quest.Id);
}
}
internal sealed record SetQuestTask(ElementId NextQuestId, ElementId CurrentQuestId) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"SetNextQuest({NextQuestId})";
}
}
internal sealed class NextQuestExecutor(QuestRegistry questRegistry, QuestController questController, QuestFunctions questFunctions, ILogger<NextQuestExecutor> logger) : TaskExecutor<SetQuestTask>()
{
protected override bool Start()
{
QuestController.EAutomationType automationType = questController.AutomationType;
Quest quest;
if ((uint)(automationType - 3) <= 1u)
{
logger.LogInformation("Won't set next quest to {QuestId}, automation type is CurrentQuestOnly", base.Task.NextQuestId);
questController.SetNextQuest(null);
}
else if (questFunctions.IsQuestLocked(base.Task.NextQuestId, base.Task.CurrentQuestId))
{
logger.LogInformation("Can't set next quest to {QuestId}, quest is locked", base.Task.NextQuestId);
questController.SetNextQuest(null);
}
else if (questRegistry.TryGetQuest(base.Task.NextQuestId, out quest))
{
logger.LogInformation("Setting next quest to {QuestId}: '{QuestName}'", base.Task.NextQuestId, quest.Info.Name);
questController.SetNextQuest(quest);
}
else
{
logger.LogInformation("Next quest with id {QuestId} not found", base.Task.NextQuestId);
questController.SetNextQuest(null);
}
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,159 @@
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Common;
internal static class SendNotification
{
internal sealed class Factory(AutomatonIpc automatonIpc, AutoDutyIpc autoDutyIpc, BossModIpc bossModIpc, TerritoryData territoryData) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
switch (step.InteractionType)
{
case EInteractionType.Snipe:
if (!automatonIpc.IsAutoSnipeEnabled)
{
return new Task(step.InteractionType, step.Comment);
}
break;
case EInteractionType.Duty:
if (!autoDutyIpc.IsConfiguredToRunContent(step.DutyOptions))
{
EInteractionType interactionType = step.InteractionType;
uint? num = step.DutyOptions?.ContentFinderConditionId;
object comment;
if (num.HasValue)
{
uint valueOrDefault = num.GetValueOrDefault();
comment = territoryData.GetContentFinderCondition(valueOrDefault)?.Name;
}
else
{
comment = step.Comment;
}
return new Task(interactionType, (string?)comment);
}
break;
case EInteractionType.SinglePlayerDuty:
if (!bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{
return new Task(step.InteractionType, quest.Info.Name);
}
break;
}
return null;
}
}
internal sealed record Task(EInteractionType InteractionType, string? Comment) : ITask
{
public override string ToString()
{
return "SendNotification";
}
}
internal sealed class Executor(NotificationMasterIpc notificationMasterIpc, IChatGui chatGui, Configuration configuration) : TaskExecutor<Task>()
{
protected override bool Start()
{
if (!configuration.Notifications.Enabled)
{
return false;
}
string text;
switch (base.Task.InteractionType)
{
case EInteractionType.Duty:
text = "Duty";
break;
case EInteractionType.SinglePlayerDuty:
text = "Single player duty";
break;
case EInteractionType.WaitForManualProgress:
case EInteractionType.Snipe:
case EInteractionType.Instruction:
text = "Manual interaction required";
break;
default:
text = $"{base.Task.InteractionType}";
break;
}
string text2 = text;
if (!string.IsNullOrEmpty(base.Task.Comment))
{
text2 = text2 + " - " + base.Task.Comment;
}
if (configuration.Notifications.ChatType != XivChatType.None)
{
XivChatEntry xivChatEntry;
switch (configuration.Notifications.ChatType)
{
case XivChatType.Say:
case XivChatType.Shout:
case XivChatType.TellOutgoing:
case XivChatType.TellIncoming:
case XivChatType.Party:
case XivChatType.Alliance:
case XivChatType.Ls1:
case XivChatType.Ls2:
case XivChatType.Ls3:
case XivChatType.Ls4:
case XivChatType.Ls5:
case XivChatType.Ls6:
case XivChatType.Ls7:
case XivChatType.Ls8:
case XivChatType.FreeCompany:
case XivChatType.NoviceNetwork:
case XivChatType.Yell:
case XivChatType.CrossParty:
case XivChatType.PvPTeam:
case XivChatType.CrossLinkShell1:
case XivChatType.NPCDialogue:
case XivChatType.NPCDialogueAnnouncements:
case XivChatType.CrossLinkShell2:
case XivChatType.CrossLinkShell3:
case XivChatType.CrossLinkShell4:
case XivChatType.CrossLinkShell5:
case XivChatType.CrossLinkShell6:
case XivChatType.CrossLinkShell7:
case XivChatType.CrossLinkShell8:
xivChatEntry = new XivChatEntry
{
Message = text2,
Type = configuration.Notifications.ChatType,
Name = new SeStringBuilder().AddUiForeground("Questionable", 576).Build()
};
break;
default:
xivChatEntry = new XivChatEntry
{
Message = new SeStringBuilder().AddUiForeground("[Questionable] ", 576).Append(text2).Build(),
Type = configuration.Notifications.ChatType
};
break;
}
XivChatEntry chat = xivChatEntry;
chatGui.Print(chat);
}
notificationMasterIpc.Notify(text2);
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,42 @@
using System;
namespace Questionable.Controller.Steps.Common;
internal static class WaitCondition
{
internal sealed record Task(Func<bool> Predicate, string Description) : ITask
{
public override string ToString()
{
return Description;
}
}
internal sealed class WaitConditionExecutor : TaskExecutor<Task>
{
private DateTime _continueAt = DateTime.MaxValue;
protected override bool Start()
{
return !base.Task.Predicate();
}
public override ETaskResult Update()
{
if (_continueAt == DateTime.MaxValue && base.Task.Predicate())
{
_continueAt = DateTime.Now.AddSeconds(0.5);
}
if (!(DateTime.Now >= _continueAt))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,43 @@
namespace Questionable.Controller.Steps.Common;
internal sealed class WaitNavmesh
{
internal sealed record Task : ITask
{
public override string ToString()
{
return "Wait(navmesh)";
}
}
internal sealed class Executor(MovementController movementController) : TaskExecutor<Task>(), IDebugStateProvider, ITaskExecutor
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (!movementController.IsNavmeshReady)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
public string? GetDebugState()
{
if (!movementController.IsNavmeshReady)
{
return $"Navmesh: {movementController.BuiltNavmeshPercent}%";
}
return null;
}
}
}

View file

@ -0,0 +1,293 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering;
internal static class DoGather
{
internal sealed record Task(GatheringController.GatheringRequest Request, GatheringNode Node, bool RevisitRequired) : ITask, IRevisitAware
{
public bool RevisitTriggered { get; private set; }
public void OnRevisit()
{
RevisitTriggered = true;
}
public override string ToString()
{
return "DoGather" + (RevisitRequired ? " if revist" : "");
}
}
internal sealed class GatherExecutor(GatheringController gatheringController, GameFunctions gameFunctions, IGameGui gameGui, IClientState clientState, ICondition condition, ILogger<GatherExecutor> logger) : TaskExecutor<Task>()
{
private bool _wasGathering;
private bool _usedLuck;
private SlotInfo? _slotToGather;
private Queue<EAction>? _actionQueue;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
Task task = base.Task;
if ((object)task != null && task.RevisitRequired && !task.RevisitTriggered)
{
logger.LogInformation("No revisit");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasNodeDisappeared(base.Task.Node))
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gameFunctions.GetFreeInventorySlots() == 0)
{
throw new TaskException("Inventory full");
}
if (condition[ConditionFlag.Gathering])
{
if (gameGui.TryGetAddonByName<AtkUnitBase>("GatheringMasterpiece", out var _))
{
return ETaskResult.TaskComplete;
}
_wasGathering = true;
if (gameGui.TryGetAddonByName<AddonGathering>("Gathering", out var addonPtr2))
{
if (gatheringController.HasRequestedItems())
{
addonPtr2->FireCallbackInt(-1);
}
else
{
List<SlotInfo> list = ReadSlots(addonPtr2);
if (base.Task.Request.Collectability > 0)
{
SlotInfo slotInfo = list.Single((SlotInfo x) => x.ItemId == base.Task.Request.ItemId);
addonPtr2->FireCallbackInt(slotInfo.Index);
}
else
{
NodeCondition nodeCondition = new NodeCondition(addonPtr2->AtkValues[110].UInt, addonPtr2->AtkValues[111].UInt);
if (_actionQueue != null && _actionQueue.TryPeek(out var result))
{
if (gameFunctions.UseAction(result))
{
logger.LogInformation("Used action {Action} on node", result);
_actionQueue.Dequeue();
}
return ETaskResult.StillRunning;
}
_actionQueue = GetNextActions(nodeCondition, list);
if (_actionQueue == null)
{
logger.LogInformation("Skipping the rest of gathering...");
addonPtr2->FireCallbackInt(-1);
return ETaskResult.TaskComplete;
}
if (_actionQueue.Count == 0)
{
SlotInfo slotInfo2 = _slotToGather ?? list.SingleOrDefault((SlotInfo x) => x.ItemId == base.Task.Request.ItemId) ?? list.MinBy((SlotInfo x) => x.ItemId);
switch (slotInfo2?.ItemId)
{
case 2u:
case 3u:
case 4u:
case 5u:
case 6u:
case 7u:
case 8u:
case 9u:
case 10u:
case 11u:
case 12u:
case 13u:
case 14u:
case 15u:
case 16u:
case 17u:
case 18u:
case 19u:
if (InventoryManager.Instance()->GetInventoryItemCount(slotInfo2.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) == 9999)
{
slotInfo2 = null;
}
break;
}
if (slotInfo2 != null)
{
addonPtr2->FireCallbackInt(slotInfo2.Index);
}
else
{
addonPtr2->FireCallbackInt(-1);
}
}
}
}
}
}
if (!_wasGathering || condition[ConditionFlag.Gathering])
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
private unsafe List<SlotInfo> ReadSlots(AddonGathering* addonGathering)
{
List<SlotInfo> list = new List<SlotInfo>();
for (int i = 0; i < 8; i++)
{
uint num = addonGathering->ItemIds[i];
if (num != 0)
{
AtkComponentCheckBox* value = addonGathering->GatheredItemComponentCheckbox[i].Value;
if (!int.TryParse(value->UldManager.SearchNodeById(10u)->GetAsAtkTextNode()->NodeText.ToString(), out var result))
{
result = 0;
}
if (!int.TryParse(value->UldManager.SearchNodeById(16u)->GetAsAtkTextNode()->NodeText.ToString(), out var result2))
{
result2 = 0;
}
AtkTextNode* asAtkTextNode = value->UldManager.SearchNodeById(31u)->GetAsAtkComponentNode()->Component->UldManager.SearchNodeById(7u)->GetAsAtkTextNode();
if (!asAtkTextNode->IsVisible() || !int.TryParse(asAtkTextNode->NodeText.ToString(), out var result3))
{
result3 = 1;
}
SlotInfo item = new SlotInfo(i, num, result, result2, result3);
list.Add(item);
}
}
logger.LogTrace("Slots: {Slots}", string.Join(", ", list));
return list;
}
private Queue<EAction>? GetNextActions(NodeCondition nodeCondition, List<SlotInfo> slots)
{
if (_slotToGather != null && slots.All((SlotInfo x) => x.Index != _slotToGather.Index))
{
_slotToGather = null;
}
Queue<EAction> queue = new Queue<EAction>();
if (!gameFunctions.HasStatus(EStatus.GatheringRateUp))
{
if (base.Task.Request.AlternativeItemId != 0)
{
SlotInfo slotInfo = slots.Single((SlotInfo x) => x.ItemId == base.Task.Request.AlternativeItemId);
if (slotInfo.GatheringChance == 100)
{
_slotToGather = slotInfo;
return queue;
}
if (slotInfo.GatheringChance > 0)
{
if (slotInfo.GatheringChance >= 95 && CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
_slotToGather = slotInfo;
queue.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return queue;
}
if (slotInfo.GatheringChance >= 85 && CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{
_slotToGather = slotInfo;
queue.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return queue;
}
if (slotInfo.GatheringChance >= 50 && CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{
_slotToGather = slotInfo;
queue.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return queue;
}
}
}
SlotInfo slotInfo2 = slots.SingleOrDefault((SlotInfo x) => x.ItemId == base.Task.Request.ItemId);
if (slotInfo2 == null)
{
if (!_usedLuck && nodeCondition.CurrentIntegrity == nodeCondition.MaxIntegrity && CanUseAction(EAction.LuckOfTheMountaineer, EAction.LuckOfThePioneer))
{
_usedLuck = true;
queue.Enqueue(PickAction(EAction.LuckOfTheMountaineer, EAction.LuckOfThePioneer));
return queue;
}
if (_usedLuck)
{
if (nodeCondition.CurrentIntegrity != nodeCondition.MaxIntegrity)
{
return null;
}
_slotToGather = slots.MinBy((SlotInfo x) => x.ItemId);
return queue;
}
}
slotInfo2 = slots.SingleOrDefault((SlotInfo x) => x.ItemId == base.Task.Request.ItemId);
if ((object)slotInfo2 != null)
{
int gatheringChance = slotInfo2.GatheringChance;
if (gatheringChance > 0 && gatheringChance < 100)
{
if (slotInfo2.GatheringChance >= 95 && CanUseAction(EAction.SharpVision1, EAction.FieldMastery1))
{
queue.Enqueue(PickAction(EAction.SharpVision1, EAction.FieldMastery1));
return queue;
}
if (slotInfo2.GatheringChance >= 85 && CanUseAction(EAction.SharpVision2, EAction.FieldMastery2))
{
queue.Enqueue(PickAction(EAction.SharpVision2, EAction.FieldMastery2));
return queue;
}
if (slotInfo2.GatheringChance >= 50 && CanUseAction(EAction.SharpVision3, EAction.FieldMastery3))
{
queue.Enqueue(PickAction(EAction.SharpVision3, EAction.FieldMastery3));
return queue;
}
}
}
}
return queue;
}
private EAction PickAction(EAction minerAction, EAction botanistAction)
{
if (clientState.LocalPlayer?.ClassJob.RowId == 16)
{
return minerAction;
}
return botanistAction;
}
private unsafe bool CanUseAction(EAction minerAction, EAction botanistAction)
{
EAction actionId = PickAction(minerAction, botanistAction);
return ActionManager.Instance()->GetActionStatus(ActionType.Action, (uint)actionId, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
private sealed record SlotInfo(int Index, uint ItemId, int GatheringChance, int BoonChance, int Quantity);
private sealed record NodeCondition(uint CurrentIntegrity, uint MaxIntegrity);
}

View file

@ -0,0 +1,199 @@
using System.Collections.Generic;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering;
internal static class DoGatherCollectable
{
internal sealed record Task(GatheringController.GatheringRequest Request, GatheringNode Node, bool RevisitRequired) : ITask, IRevisitAware
{
public bool RevisitTriggered { get; private set; }
public void OnRevisit()
{
RevisitTriggered = true;
}
public override string ToString()
{
return $"DoGatherCollectable({SeIconChar.Collectible.ToIconString()}/{Request.Collectability}){(RevisitRequired ? " if revist" : "")}";
}
}
internal sealed class GatherCollectableExecutor(GatheringController gatheringController, GameFunctions gameFunctions, IClientState clientState, IGameGui gameGui, ILogger<GatherCollectableExecutor> logger) : TaskExecutor<Task>()
{
private Queue<EAction>? _actionQueue;
private bool? _expectedScrutiny;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (base.Task.RevisitRequired && !base.Task.RevisitTriggered)
{
logger.LogInformation("No revisit");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasNodeDisappeared(base.Task.Node))
{
logger.LogInformation("Node disappeared");
return ETaskResult.TaskComplete;
}
if (gatheringController.HasRequestedItems())
{
if (gameGui.TryGetAddonByName<AtkUnitBase>("GatheringMasterpiece", out var addonPtr))
{
addonPtr->FireCallbackInt(1);
return ETaskResult.StillRunning;
}
if (gameGui.TryGetAddonByName<AtkUnitBase>("Gathering", out addonPtr))
{
addonPtr->FireCallbackInt(-1);
return ETaskResult.TaskComplete;
}
}
if (gameFunctions.GetFreeInventorySlots() == 0)
{
throw new TaskException("Inventory full");
}
NodeCondition nodeCondition = GetNodeCondition();
if (nodeCondition == null)
{
return ETaskResult.TaskComplete;
}
if (_expectedScrutiny.HasValue)
{
if (nodeCondition.ScrutinyActive != _expectedScrutiny)
{
return ETaskResult.StillRunning;
}
_expectedScrutiny = null;
return ETaskResult.StillRunning;
}
if (_actionQueue != null && _actionQueue.TryPeek(out var result))
{
if (gameFunctions.UseAction(result))
{
bool? expectedScrutiny;
switch (result)
{
case EAction.ScrutinyMiner:
case EAction.ScrutinyBotanist:
expectedScrutiny = true;
break;
case EAction.ScourMiner:
case EAction.MeticulousMiner:
case EAction.ScourBotanist:
case EAction.MeticulousBotanist:
expectedScrutiny = false;
break;
default:
expectedScrutiny = null;
break;
}
_expectedScrutiny = expectedScrutiny;
logger.LogInformation("Used action {Action} on node", result);
_actionQueue.Dequeue();
}
return ETaskResult.StillRunning;
}
if (nodeCondition.CollectabilityToGoal(base.Task.Request.Collectability) != 0)
{
_actionQueue = GetNextActions(nodeCondition);
if (_actionQueue != null)
{
foreach (EAction item in _actionQueue)
{
logger.LogInformation("Next Actions {Action}", item);
}
return ETaskResult.StillRunning;
}
}
_actionQueue = new Queue<EAction>();
_actionQueue.Enqueue(PickAction(EAction.CollectMiner, EAction.CollectBotanist));
return ETaskResult.StillRunning;
}
private unsafe NodeCondition? GetNodeCondition()
{
if (gameGui.TryGetAddonByName<AtkUnitBase>("GatheringMasterpiece", out var addonPtr))
{
AtkValue* atkValues = addonPtr->AtkValues;
return new NodeCondition(atkValues[13].UInt, atkValues[14].UInt, atkValues[62].UInt, atkValues[63].UInt, atkValues[54].Bool, atkValues[48].UInt, atkValues[51].UInt);
}
return null;
}
private Queue<EAction> GetNextActions(NodeCondition nodeCondition)
{
uint currentGp = clientState.LocalPlayer.CurrentGp;
logger.LogTrace("Getting next actions (with {GP} GP, {MeticulousCollectability}~ meticulous, {ScourCollectability}~ scour)", currentGp, nodeCondition.CollectabilityFromMeticulous, nodeCondition.CollectabilityFromScour);
Queue<EAction> queue = new Queue<EAction>();
uint num = nodeCondition.CollectabilityToGoal(base.Task.Request.Collectability);
if (num <= nodeCondition.CollectabilityFromMeticulous)
{
logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ meticulous", num, nodeCondition.CollectabilityFromMeticulous);
queue.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
return queue;
}
if (num <= nodeCondition.CollectabilityFromScour)
{
logger.LogTrace("Can get all needed {NeededCollectability} from {Collectability}~ scour", num, nodeCondition.CollectabilityFromScour);
queue.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
return queue;
}
if (!nodeCondition.ScrutinyActive && currentGp >= 200)
{
logger.LogTrace("Still missing {NeededCollectability} collectability, scrutiny inactive", num);
queue.Enqueue(PickAction(EAction.ScrutinyMiner, EAction.ScrutinyBotanist));
return queue;
}
if (nodeCondition.ScrutinyActive)
{
logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ meticulous", num, nodeCondition.CollectabilityFromMeticulous);
queue.Enqueue(PickAction(EAction.MeticulousMiner, EAction.MeticulousBotanist));
return queue;
}
logger.LogTrace("Scrutiny active, need {NeededCollectability} and we expect {Collectability}~ scour", num, nodeCondition.CollectabilityFromScour);
queue.Enqueue(PickAction(EAction.ScourMiner, EAction.ScourBotanist));
return queue;
}
private EAction PickAction(EAction minerAction, EAction botanistAction)
{
if (clientState.LocalPlayer?.ClassJob.RowId == 16)
{
return minerAction;
}
return botanistAction;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
private sealed record NodeCondition(uint CurrentCollectability, uint MaxCollectability, uint CurrentIntegrity, uint MaxIntegrity, bool ScrutinyActive, uint CollectabilityFromScour, uint CollectabilityFromMeticulous)
{
public uint CollectabilityToGoal(uint goal)
{
if (goal >= CurrentCollectability)
{
return goal - CurrentCollectability;
}
return (CurrentCollectability == 0) ? 1u : 0u;
}
}
}

View file

@ -0,0 +1,70 @@
using System.Globalization;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Movement;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering;
internal static class MoveToLandingLocation
{
internal sealed record Task(ushort TerritoryId, bool FlyBetweenNodes, GatheringNode GatheringNode) : ITask
{
public override string ToString()
{
return $"Land/{FlyBetweenNodes}";
}
}
internal sealed class MoveToLandingLocationExecutor(MoveExecutor moveExecutor, GameFunctions gameFunctions, IObjectTable objectTable, ILogger<MoveToLandingLocationExecutor> logger) : TaskExecutor<Task>(), IToastAware, ITaskExecutor
{
private ITask _moveTask;
protected override bool Start()
{
GatheringLocation location = base.Task.GatheringNode.Locations.First();
if (base.Task.GatheringNode.Locations.Count > 1)
{
IGameObject gameObject = objectTable.SingleOrDefault((IGameObject x) => x.ObjectKind == ObjectKind.GatheringPoint && x.DataId == base.Task.GatheringNode.DataId && x.IsTargetable);
if (gameObject == null)
{
return false;
}
location = base.Task.GatheringNode.Locations.Single((GatheringLocation x) => Vector3.Distance(x.Position, gameObject.Position) < 0.1f);
}
var (vector, num, num2) = GatheringMath.CalculateLandingLocation(location);
logger.LogInformation("Preliminary landing location: {Location}, with degrees = {Degrees}, range = {Range}", vector.ToString("G", CultureInfo.InvariantCulture), num, num2);
bool flag = base.Task.FlyBetweenNodes && gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId);
ushort territoryId = base.Task.TerritoryId;
Vector3 destination = vector;
float? stopDistance = 0.25f;
uint? dataId = base.Task.GatheringNode.DataId;
bool fly = flag;
_moveTask = new MoveTask(territoryId, destination, null, stopDistance, dataId, DisableNavmesh: false, null, fly, Land: false, IgnoreDistanceToObject: true, RestartNavigation: true, EInteractionType.Gather);
return moveExecutor.Start(_moveTask);
}
public override ETaskResult Update()
{
return moveExecutor.Update();
}
public bool OnErrorToast(SeString message)
{
return moveExecutor.OnErrorToast(message);
}
public override bool ShouldInterruptOnDamage()
{
return moveExecutor.ShouldInterruptOnDamage();
}
}
}

View file

@ -0,0 +1,108 @@
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Gathering;
internal static class TurnInDelivery
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (!(quest.Id is SatisfactionSupplyNpcId) || sequence.Sequence != 1)
{
return null;
}
return new Task();
}
}
internal sealed record Task : ITask
{
public override string ToString()
{
return "WeeklyDeliveryTurnIn";
}
}
internal sealed class SatisfactionSupplyTurnIn(ILogger<SatisfactionSupplyTurnIn> logger) : TaskExecutor<Task>()
{
private ushort? _remainingAllowances;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance();
if (ptr == null || !ptr->IsAgentActive())
{
if (_remainingAllowances.HasValue)
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
uint addonId = ptr->GetAddonId();
if (addonId == 0)
{
if (_remainingAllowances.HasValue)
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
AtkUnitBase* addonById = LAddon.GetAddonById(addonId);
if (addonById == null || !LAddon.IsAddonReady(addonById))
{
return ETaskResult.StillRunning;
}
ushort remainingAllowances = ptr->NpcData.RemainingAllowances;
if (remainingAllowances == 0)
{
logger.LogInformation("No remaining weekly allowances");
addonById->FireCallbackInt(0);
return ETaskResult.TaskComplete;
}
if (InventoryManager.Instance()->GetInventoryItemCount(ptr->Items[1].Id, isHq: false, checkEquipped: true, checkArmory: true, (short)ptr->Items[1].Collectability1) == 0)
{
logger.LogInformation("Inventory has no {ItemId}", ptr->Items[1].Id);
addonById->FireCallbackInt(0);
return ETaskResult.TaskComplete;
}
if (_remainingAllowances == remainingAllowances)
{
return ETaskResult.StillRunning;
}
logger.LogInformation("Attempting turn-in (remaining allowances: {RemainingAllowances})", remainingAllowances);
_remainingAllowances = remainingAllowances;
AtkValue* values = stackalloc AtkValue[2]
{
new AtkValue
{
Type = ValueType.Int,
Int = 1
},
new AtkValue
{
Type = ValueType.Int,
Int = 1
}
};
addonById->FireCallback(2u, values);
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,277 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Action
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Action)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.Action, "step.Action");
ITask task = OnObject(step.DataId, quest, step.Action.Value, step.CompletionQuestVariablesFlags);
if (step.Action.Value.RequiresMount())
{
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(task);
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2]
{
new Mount.UnmountTask(),
task
});
}
public static ITask OnObject(uint? dataId, Quest quest, EAction action, List<QuestWorkValue?>? completionQuestVariablesFlags)
{
if ((uint)(action - 2265) <= 2u)
{
ArgumentNullException.ThrowIfNull(dataId, "dataId");
return new UseMudraOnObject(dataId.Value, action);
}
return new UseOnObject(dataId, quest, action, completionQuestVariablesFlags);
}
}
internal sealed record UseOnObject(uint? DataId, Quest? Quest, EAction Action, List<QuestWorkValue?>? CompletionQuestVariablesFlags) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"Action({Action})";
}
}
internal sealed class UseOnObjectExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ILogger<UseOnObject> logger) : TaskExecutor<UseOnObject>()
{
private bool _usedAction;
private DateTime _continueAt = DateTime.MinValue;
protected override bool Start()
{
if (base.Task.DataId.HasValue)
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId.Value);
if (gameObject == null)
{
logger.LogWarning("No game object with dataId {DataId}", base.Task.DataId);
return false;
}
if (gameObject.IsTargetable)
{
if (base.Task.Action == EAction.Diagnosis && gameFunctions.HasStatus(EStatus.Eukrasia) && GameFunctions.RemoveStatus(EStatus.Eukrasia))
{
_continueAt = DateTime.Now.AddSeconds(2.0);
return true;
}
_usedAction = gameFunctions.UseAction(gameObject, base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
return true;
}
_usedAction = gameFunctions.UseAction(base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
public override ETaskResult Update()
{
if (DateTime.Now <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (!_usedAction)
{
if (base.Task.DataId.HasValue)
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId.Value);
if (gameObject == null || !gameObject.IsTargetable)
{
return ETaskResult.StillRunning;
}
_usedAction = gameFunctions.UseAction(gameObject, base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
}
else
{
_usedAction = gameFunctions.UseAction(base.Task.Action);
_continueAt = DateTime.Now.AddSeconds(0.5);
}
return ETaskResult.StillRunning;
}
if (base.Task.Quest != null && base.Task.CompletionQuestVariablesFlags != null && QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariablesFlags))
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(base.Task.Quest.Id);
if (questProgressInfo == null || !QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record UseMudraOnObject(uint DataId, EAction Action) : ITask
{
public override string ToString()
{
return $"Mudra({Action})";
}
}
internal sealed class UseMudraOnObjectExecutor(GameFunctions gameFunctions, ILogger<UseMudraOnObject> logger) : TaskExecutor<UseMudraOnObject>()
{
private static readonly ReadOnlyDictionary<EAction, Dictionary<EAction, EAction>> Combos = new Dictionary<EAction, Dictionary<EAction, EAction>>
{
{
EAction.FumaShuriken,
new Dictionary<EAction, EAction> {
{
EAction.Ninjutsu,
EAction.Ten
} }
},
{
EAction.Raiton,
new Dictionary<EAction, EAction>
{
{
EAction.Ninjutsu,
EAction.Ten
},
{
EAction.FumaShuriken,
EAction.Chi
}
}
},
{
EAction.Katon,
new Dictionary<EAction, EAction>
{
{
EAction.Ninjutsu,
EAction.Chi
},
{
EAction.FumaShuriken,
EAction.Ten
}
}
}
}.AsReadOnly();
private DateTime _continueAt = DateTime.MinValue;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
EAction adjustedActionId = (EAction)ActionManager.Instance()->GetAdjustedActionId(2260u);
if (adjustedActionId == EAction.RabbitMedium)
{
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null || !gameObject.IsTargetable)
{
return ETaskResult.StillRunning;
}
if (adjustedActionId == base.Task.Action)
{
_continueAt = DateTime.Now.AddSeconds(0.25);
if (!gameFunctions.UseAction(gameObject, base.Task.Action))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
if (Combos.TryGetValue(base.Task.Action, out Dictionary<EAction, EAction> value))
{
if (value.TryGetValue(adjustedActionId, out var value2))
{
_continueAt = DateTime.Now.AddSeconds(0.25);
gameFunctions.UseAction(value2);
return ETaskResult.StillRunning;
}
_continueAt = DateTime.Now.AddSeconds(0.25);
return ETaskResult.StillRunning;
}
logger.LogError("Unable to find relevant combo for {Action}", base.Task.Action);
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record TriggerStatusIfMissing(EStatus Status, EAction Action) : ITask
{
public override string ToString()
{
return $"TriggerStatus({Status})";
}
}
internal sealed class TriggerStatusIfMissingExecutor(GameFunctions gameFunctions) : TaskExecutor<TriggerStatusIfMissing>()
{
protected override bool Start()
{
if (gameFunctions.HasStatus(base.Task.Status))
{
return false;
}
gameFunctions.UseAction(base.Task.Action);
return true;
}
public override ETaskResult Update()
{
if (!gameFunctions.HasStatus(base.Task.Status))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class AetherCurrent
{
internal sealed class Factory(AetherCurrentData aetherCurrentData, IChatGui chatGui) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AttuneAetherCurrent)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
ArgumentNullException.ThrowIfNull(step.AetherCurrentId, "step.AetherCurrentId");
if (!aetherCurrentData.IsValidAetherCurrent(step.TerritoryId, step.AetherCurrentId.Value))
{
chatGui.PrintError($"Aether current with id {step.AetherCurrentId} is referencing an invalid aether current, will skip attunement", "Questionable", 576);
return null;
}
return new Attune(step.DataId.Value, step.AetherCurrentId.Value);
}
}
internal sealed record Attune(uint DataId, uint AetherCurrentId) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"AttuneAetherCurrent({AetherCurrentId})";
}
}
internal sealed class DoAttune(GameFunctions gameFunctions, ILogger<DoAttune> logger) : TaskExecutor<Attune>()
{
protected override bool Start()
{
if (!gameFunctions.IsAetherCurrentUnlocked(base.Task.AetherCurrentId))
{
logger.LogInformation("Attuning to aether current {AetherCurrentId} / {DataId}", base.Task.AetherCurrentId, base.Task.DataId);
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith(base.Task.DataId, ObjectKind.EventObj));
return true;
}
logger.LogInformation("Already attuned to aether current {AetherCurrentId} / {DataId}", base.Task.AetherCurrentId, base.Task.DataId);
return false;
}
public override ETaskResult Update()
{
if (!gameFunctions.IsAetherCurrentUnlocked(base.Task.AetherCurrentId))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,67 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class AethernetShard
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AttuneAethernetShard)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.AethernetShard, "step.AethernetShard");
return new Attune(step.AethernetShard.Value);
}
}
internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"AttuneAethernetShard({AetheryteLocation})";
}
}
internal sealed class DoAttune(AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, ILogger<DoAttune> logger) : TaskExecutor<Attune>()
{
protected override bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
logger.LogInformation("Attuning to aethernet shard {AethernetShard}", base.Task.AetheryteLocation);
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith((uint)base.Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
logger.LogInformation("Already attuned to aethernet shard {AethernetShard}", base.Task.AetheryteLocation);
return false;
}
public override ETaskResult Update()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,67 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Aetheryte
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AttuneAetheryte)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.Aetheryte, "step.Aetheryte");
return new Attune(step.Aetheryte.Value);
}
}
internal sealed record Attune(EAetheryteLocation AetheryteLocation) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"AttuneAetheryte({AetheryteLocation})";
}
}
internal sealed class DoAttune(AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, ILogger<DoAttune> logger) : TaskExecutor<Attune>()
{
protected override bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
logger.LogInformation("Attuning to aetheryte {Aetheryte}", base.Task.AetheryteLocation);
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith((uint)base.Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
logger.LogInformation("Already attuned to aetheryte {Aetheryte}", base.Task.AetheryteLocation);
return false;
}
public override ETaskResult Update()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,70 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Microsoft.Extensions.Logging;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class AetheryteFreeOrFavored
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.RegisterFreeOrFavoredAetheryte)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.Aetheryte, "step.Aetheryte");
return new Register(step.Aetheryte.Value);
}
}
internal sealed record Register(EAetheryteLocation AetheryteLocation) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"RegisterFreeOrFavoredAetheryte({AetheryteLocation})";
}
}
internal sealed class DoRegister(AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, ILogger<DoRegister> logger) : TaskExecutor<Register>()
{
protected override bool Start()
{
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.AetheryteLocation))
{
throw new TaskException($"Aetheryte {base.Task.AetheryteLocation} is not attuned");
}
if (aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(base.Task.AetheryteLocation) == AetheryteRegistrationResult.NotPossible)
{
logger.LogInformation("Could not register aetheryte {AetheryteLocation} as free or favored", base.Task.AetheryteLocation);
return false;
}
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => gameFunctions.InteractWith((uint)base.Task.AetheryteLocation, ObjectKind.Aetheryte));
return true;
}
public override ETaskResult Update()
{
if (aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(base.Task.AetheryteLocation) != AetheryteRegistrationResult.NotPossible)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Combat
{
internal sealed class Factory(GameFunctions gameFunctions) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Combat)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.EnemySpawnType, "step.EnemySpawnType");
if (gameFunctions.GetMountId() != 128 && gameFunctions.GetMountId() != 147)
{
yield return new Mount.UnmountTask();
}
if (step.CombatDelaySecondsAtStart.HasValue)
{
yield return new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(step.CombatDelaySecondsAtStart.Value));
}
switch (step.EnemySpawnType)
{
case EEnemySpawnType.AfterInteraction:
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
yield return new Interact.Task(step.DataId.Value, quest, EInteractionType.None, SkipMarkerCheck: true);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AfterItemUse:
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
if (step.GroundTarget == true)
{
if (step.DataId.HasValue)
{
yield return new UseItem.UseOnGround(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
else
{
ArgumentNullException.ThrowIfNull(step.Position, "step.Position");
yield return new UseItem.UseOnPosition(quest.Id, step.Position.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
}
else if (step.DataId.HasValue)
{
yield return new UseItem.UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
else
{
yield return new UseItem.UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags, StartingCombat: true);
}
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AfterAction:
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
ArgumentNullException.ThrowIfNull(step.Action, "step.Action");
if (!step.Action.Value.RequiresMount())
{
yield return new Mount.UnmountTask();
}
yield return new Action.UseOnObject(step.DataId.Value, null, step.Action.Value, null);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AfterEmote:
ArgumentNullException.ThrowIfNull(step.Emote, "step.Emote");
yield return new Mount.UnmountTask();
if (step.DataId.HasValue)
{
yield return new Emote.UseOnObject(step.Emote.Value, step.DataId.Value);
}
else
{
yield return new Emote.UseOnSelf(step.Emote.Value);
}
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.AutoOnEnterArea:
if (!step.CombatDelaySecondsAtStart.HasValue)
{
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(1L));
}
yield return CreateTask(quest, sequence, step);
break;
case EEnemySpawnType.OverworldEnemies:
case EEnemySpawnType.FateEnemies:
case EEnemySpawnType.FinishCombatIfAny:
yield return CreateTask(quest, sequence, step);
break;
default:
throw new ArgumentOutOfRangeException("step", $"Unknown spawn type {step.EnemySpawnType}");
}
}
private static Task CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
ArgumentNullException.ThrowIfNull(step.EnemySpawnType, "step.EnemySpawnType");
bool isLastStep = sequence.Steps.Last() == step;
return CreateTask(quest.Id, sequence.Sequence, isLastStep, step.EnemySpawnType.Value, step.KillEnemyDataIds, step.CompletionQuestVariablesFlags, step.ComplexCombatData, step.CombatItemUse);
}
internal static Task CreateTask(ElementId? elementId, int sequence, bool isLastStep, EEnemySpawnType enemySpawnType, IList<uint> killEnemyDataIds, IList<QuestWorkValue?> completionQuestVariablesFlags, IList<ComplexCombatData> complexCombatData, CombatItemUse? combatItemUse)
{
return new Task(new CombatController.CombatData
{
ElementId = elementId,
Sequence = sequence,
CompletionQuestVariablesFlags = completionQuestVariablesFlags,
SpawnType = enemySpawnType,
KillEnemyDataIds = killEnemyDataIds.ToList(),
ComplexCombatDatas = complexCombatData.ToList(),
CombatItemUse = combatItemUse
}, completionQuestVariablesFlags, isLastStep);
}
}
internal sealed record Task(CombatController.CombatData CombatData, IList<QuestWorkValue?> CompletionQuestVariableFlags, bool IsLastStep) : ITask
{
public override string ToString()
{
if (CombatData.SpawnType == EEnemySpawnType.FinishCombatIfAny)
{
return "HandleCombat(wait: not in combat, optional)";
}
if (QuestWorkUtils.HasCompletionFlags(CompletionQuestVariableFlags))
{
return "HandleCombat(wait: QW flags)";
}
if (IsLastStep)
{
return "HandleCombat(wait: next sequence)";
}
return "HandleCombat(wait: not in combat)";
}
}
internal sealed class HandleCombat(CombatController combatController, QuestFunctions questFunctions) : TaskExecutor<Task>()
{
private CombatController.EStatus _status;
protected override bool Start()
{
return combatController.Start(base.Task.CombatData);
}
public override ETaskResult Update()
{
_status = combatController.Update();
if (_status != CombatController.EStatus.Complete)
{
return ETaskResult.StillRunning;
}
if (QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariableFlags) && base.Task.CombatData.ElementId is QuestId elementId)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(elementId);
if (questProgressInfo == null)
{
return ETaskResult.StillRunning;
}
if (QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariableFlags, questProgressInfo))
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
if (base.Task.IsLastStep)
{
return ETaskResult.StillRunning;
}
combatController.Stop("Combat task complete");
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Dive
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Dive)
{
return null;
}
return new Task();
}
}
internal sealed class Task : ITask
{
public override string ToString()
{
return "Dive";
}
}
internal sealed class DoDive(ICondition condition, ILogger<DoDive> logger) : AbstractDelayedTaskExecutor<Task>(TimeSpan.FromSeconds(5L))
{
private readonly Queue<(uint Type, nint Key)> _keysToPress = new Queue<(uint, nint)>();
private int _attempts;
protected override bool StartInternal()
{
if (condition[ConditionFlag.Diving])
{
return false;
}
if (condition[ConditionFlag.Mounted] || condition[ConditionFlag.Swimming])
{
Descend();
return true;
}
throw new TaskException("You aren't swimming, so we can't dive.");
}
public unsafe override ETaskResult Update()
{
if (_keysToPress.TryDequeue(out (uint, nint) result))
{
if (result.Item1 == 0)
{
return ETaskResult.StillRunning;
}
logger.LogDebug("{Action} key {KeyCode:X2}", (result.Item1 == 256) ? "Pressing" : "Releasing", result.Item2);
NativeMethods.SendMessage((nint)Device.Instance()->hWnd, result.Item1, result.Item2, IntPtr.Zero);
return ETaskResult.StillRunning;
}
return base.Update();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
protected override ETaskResult UpdateInternal()
{
if (condition[ConditionFlag.Diving])
{
return ETaskResult.TaskComplete;
}
if (_attempts >= 3)
{
throw new TaskException("Please dive manually.");
}
Descend();
_attempts++;
return ETaskResult.StillRunning;
}
private unsafe void Descend()
{
UIInputData.Keybind keybind = default(UIInputData.Keybind);
Utf8String* name = Utf8String.FromString("MOVE_DESCENT");
UIInputData.Instance()->GetKeybindByName(name, (Keybind*)(&keybind));
logger.LogInformation("Dive keybind: {Key1} + {Modifier1}, {Key2} + {Modifier2}", keybind.Key, keybind.Modifier, keybind.AltKey, keybind.AltModifier);
int num = 2;
List<List<nint>> list = new List<List<nint>>(num);
CollectionsMarshal.SetCount(list, num);
Span<List<nint>> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = GetKeysToPress(keybind.Key, keybind.Modifier);
num2++;
span[num2] = GetKeysToPress(keybind.AltKey, keybind.AltModifier);
List<nint> list2 = (from x in list
where x != null
select (x)).MinBy((List<nint> x) => x.Count);
if (list2 == null || list2.Count == 0)
{
throw new TaskException("No useable keybind found for diving");
}
foreach (nint item in list2)
{
_keysToPress.Enqueue((256u, item));
_keysToPress.Enqueue((0u, 0));
_keysToPress.Enqueue((0u, 0));
}
for (int num3 = 0; num3 < 5; num3++)
{
_keysToPress.Enqueue((0u, 0));
}
list2.Reverse();
foreach (nint item2 in list2)
{
_keysToPress.Enqueue((257u, item2));
}
}
}
private static class NativeMethods
{
public const uint WM_KEYUP = 257u;
public const uint WM_KEYDOWN = 256u;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam);
}
private static List<nint>? GetKeysToPress(SeVirtualKey key, ModifierFlag modifier)
{
List<nint> list = new List<nint>();
if (modifier.HasFlag(ModifierFlag.Ctrl))
{
list.Add(17);
}
if (modifier.HasFlag(ModifierFlag.Shift))
{
list.Add(16);
}
if (modifier.HasFlag(ModifierFlag.Alt))
{
list.Add(18);
}
nint num = (nint)key;
if (num == 0)
{
return null;
}
list.Add(num);
return list;
}
}

View file

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib.Gear;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Duty
{
internal sealed class Factory(AutoDutyIpc autoDutyIpc) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Duty)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.DutyOptions, "step.DutyOptions");
uint contentFinderConditionId;
int dutyMode;
if (autoDutyIpc.IsConfiguredToRunContent(step.DutyOptions))
{
contentFinderConditionId = step.DutyOptions.ContentFinderConditionId;
ElementId id = quest.Id;
if (id is QuestId)
{
ushort value = id.Value;
if (value >= 357 && value <= 360)
{
dutyMode = 2;
goto IL_00b2;
}
}
dutyMode = 1;
goto IL_00b2;
}
if (!step.DutyOptions.LowPriority)
{
yield return new OpenDutyFinderTask(step.DutyOptions.ContentFinderConditionId);
}
yield break;
IL_00b2:
yield return new StartAutoDutyTask(contentFinderConditionId, (AutoDutyIpc.DutyMode)dutyMode);
yield return new WaitAutoDutyTask(step.DutyOptions.ContentFinderConditionId);
if (!QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{
yield return new WaitAtEnd.WaitNextStepOrSequence();
}
}
}
internal sealed record StartAutoDutyTask(uint ContentFinderConditionId, AutoDutyIpc.DutyMode DutyMode) : ITask
{
public override string ToString()
{
return $"StartAutoDuty({ContentFinderConditionId}, {DutyMode})";
}
}
internal sealed class StartAutoDutyExecutor(GearStatsCalculator gearStatsCalculator, AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState, IChatGui chatGui, SendNotification.Executor sendNotificationExecutor) : TaskExecutor<StartAutoDutyTask>(), IStoppableTaskExecutor, ITaskExecutor
{
protected unsafe override bool Start()
{
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
{
throw new TaskException("Failed to get territory ID for content finder condition");
}
InventoryManager* intPtr = InventoryManager.Instance();
if (intPtr == null)
{
throw new TaskException("Inventory unavailable");
}
InventoryContainer* inventoryContainer = intPtr->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
throw new TaskException("Equipped items unavailable");
}
short num = gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer);
if (contentFinderConditionData.RequiredItemLevel > num)
{
string text = $"Could not use AutoDuty to queue for {contentFinderConditionData.Name}, required item level: {contentFinderConditionData.RequiredItemLevel}, current item level: {num}.";
if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Duty, text)))
{
chatGui.PrintError(text, "Questionable", 576);
}
return false;
}
autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode);
return true;
}
public override ETaskResult Update()
{
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
{
throw new TaskException("Failed to get territory ID for content finder condition");
}
if (clientState.TerritoryType != contentFinderConditionData.TerritoryId)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public void StopNow()
{
autoDutyIpc.Stop();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"Wait(AutoDuty, left instance {ContentFinderConditionId})";
}
}
internal sealed class WaitAutoDutyExecutor(AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState) : TaskExecutor<WaitAutoDutyTask>(), IStoppableTaskExecutor, ITaskExecutor
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
{
throw new TaskException("Failed to get territory ID for content finder condition");
}
if (clientState.TerritoryType == contentFinderConditionData.TerritoryId || !autoDutyIpc.IsStopped())
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public void StopNow()
{
autoDutyIpc.Stop();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"OpenDutyFinder({ContentFinderConditionId})";
}
}
internal sealed class OpenDutyFinderExecutor(GameFunctions gameFunctions, ICondition condition) : TaskExecutor<OpenDutyFinderTask>()
{
protected override bool Start()
{
if (condition[ConditionFlag.InDutyQueue])
{
return false;
}
gameFunctions.OpenDutyFinder(base.Task.ContentFinderConditionId);
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Emote
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 28) <= 1u) ? true : false)
{
if (!step.Emote.HasValue)
{
return Array.Empty<ITask>();
}
}
else if (step.InteractionType != EInteractionType.Emote)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.Emote, "step.Emote");
Mount.UnmountTask unmountTask = new Mount.UnmountTask();
if (step.DataId.HasValue)
{
UseOnObject useOnObject = new UseOnObject(step.Emote.Value, step.DataId.Value);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnObject });
}
UseOnSelf useOnSelf = new UseOnSelf(step.Emote.Value);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnSelf });
}
}
internal sealed record UseOnObject(EEmote Emote, uint DataId) : ITask
{
public override string ToString()
{
return $"Emote({Emote} on {DataId})";
}
}
internal sealed class UseOnObjectExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnObject>()
{
protected override bool StartInternal()
{
chatFunctions.UseEmote(base.Task.DataId, base.Task.Emote);
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record UseOnSelf(EEmote Emote) : ITask
{
public override string ToString()
{
return $"Emote({Emote})";
}
}
internal sealed class UseOnSelfExecutor(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<UseOnSelf>()
{
protected override bool StartInternal()
{
chatFunctions.UseEmote(base.Task.Emote);
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,239 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
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.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class EquipItem
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Questionable.Model.Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.EquipItem)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
return new Task(step.ItemId.Value);
}
}
internal sealed record Task(uint ItemId) : ITask
{
public override string ToString()
{
return $"Equip({ItemId})";
}
}
internal sealed class DoEquip(IDataManager dataManager, ILogger<DoEquip> logger) : TaskExecutor<Task>(), IToastAware, ITaskExecutor
{
private const int MaxAttempts = 3;
private static readonly IReadOnlyList<InventoryType> SourceInventoryTypes = new global::_003C_003Ez__ReadOnlyArray<InventoryType>(new InventoryType[16]
{
InventoryType.ArmoryMainHand,
InventoryType.ArmoryOffHand,
InventoryType.ArmoryHead,
InventoryType.ArmoryBody,
InventoryType.ArmoryHands,
InventoryType.ArmoryLegs,
InventoryType.ArmoryFeets,
InventoryType.ArmoryEar,
InventoryType.ArmoryNeck,
InventoryType.ArmoryWrist,
InventoryType.ArmoryRings,
InventoryType.ArmorySoulCrystal,
InventoryType.Inventory1,
InventoryType.Inventory2,
InventoryType.Inventory3,
InventoryType.Inventory4
});
private int _attempts;
private Item? _item;
private List<ushort> _targetSlots;
private DateTime _continueAt = DateTime.MaxValue;
protected override bool Start()
{
_item = dataManager.GetExcelSheet<Item>().GetRowOrDefault(base.Task.ItemId) ?? throw new ArgumentOutOfRangeException("ItemId");
_targetSlots = GetEquipSlot(_item) ?? throw new InvalidOperationException("Not a piece of equipment");
Equip();
_continueAt = DateTime.Now.AddSeconds(1.0);
return true;
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return ETaskResult.StillRunning;
}
foreach (ushort targetSlot in _targetSlots)
{
InventoryItem* inventorySlot = ptr->GetInventorySlot(InventoryType.EquippedItems, targetSlot);
if (inventorySlot != null && inventorySlot->ItemId == base.Task.ItemId)
{
return ETaskResult.TaskComplete;
}
}
Equip();
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
private unsafe void Equip()
{
_attempts++;
if (_attempts > 3)
{
throw new TaskException("Unable to equip gear.");
}
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
{
return;
}
InventoryContainer* inventoryContainer = inventoryManager->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
return;
}
foreach (ushort targetSlot in _targetSlots)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(targetSlot);
if (inventorySlot != null && inventorySlot->ItemId == base.Task.ItemId)
{
logger.LogInformation("Already equipped {Item}, skipping step", _item?.Name.ToString());
return;
}
}
foreach (InventoryType sourceInventoryType in SourceInventoryTypes)
{
InventoryContainer* inventoryContainer2 = inventoryManager->GetInventoryContainer(sourceInventoryType);
if (inventoryContainer2 == null || (inventoryManager->GetItemCountInContainer(base.Task.ItemId, sourceInventoryType, isHq: true, 0) == 0 && inventoryManager->GetItemCountInContainer(base.Task.ItemId, sourceInventoryType, isHq: false, 0) == 0))
{
continue;
}
for (ushort num = 0; num < inventoryContainer2->Size; num++)
{
InventoryItem* inventorySlot2 = inventoryContainer2->GetInventorySlot(num);
if (inventorySlot2 != null && inventorySlot2->ItemId == base.Task.ItemId)
{
ushort num2 = _targetSlots.Where(delegate(ushort x)
{
InventoryItem* inventorySlot3 = inventoryManager->GetInventorySlot(InventoryType.EquippedItems, x);
return inventorySlot3 == null || inventorySlot3->ItemId == 0;
}).Concat(_targetSlots).First();
logger.LogInformation("Equipping item from {SourceInventory}, {SourceSlot} to {TargetInventory}, {TargetSlot}", sourceInventoryType, num, InventoryType.EquippedItems, num2);
int num3 = inventoryManager->MoveItemSlot(sourceInventoryType, num, InventoryType.EquippedItems, num2, a6: true);
logger.LogInformation("MoveItemSlot result: {Result}", num3);
return;
}
}
}
throw new TaskException($"Could not equip item {base.Task.ItemId}.");
}
private static List<ushort>? GetEquipSlot(Item? item)
{
if (!item.HasValue)
{
return new List<ushort>();
}
Span<ushort> span;
switch (item.Value.EquipSlotCategory.RowId)
{
case 1u:
case 2u:
case 3u:
case 4u:
case 5u:
case 6u:
case 7u:
case 8u:
case 9u:
case 10u:
case 11u:
{
int index = 1;
List<ushort> list4 = new List<ushort>(index);
CollectionsMarshal.SetCount(list4, index);
span = CollectionsMarshal.AsSpan(list4);
int num = 0;
span[num] = (ushort)(item.Value.EquipSlotCategory.RowId - 1);
return list4;
}
case 12u:
{
int num = 2;
List<ushort> list3 = new List<ushort>(num);
CollectionsMarshal.SetCount(list3, num);
span = CollectionsMarshal.AsSpan(list3);
int index = 0;
span[index] = 11;
index++;
span[index] = 12;
return list3;
}
case 13u:
{
int index = 1;
List<ushort> list2 = new List<ushort>(index);
CollectionsMarshal.SetCount(list2, index);
span = CollectionsMarshal.AsSpan(list2);
int num = 0;
span[num] = 0;
return list2;
}
case 17u:
{
int num = 1;
List<ushort> list = new List<ushort>(num);
CollectionsMarshal.SetCount(list, num);
span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = 13;
return list;
}
default:
return null;
}
}
public bool OnErrorToast(SeString message)
{
string b = dataManager.GetString(709u, (LogMessage x) => x.Text);
if (GameFunctions.GameStringEquals(message.TextValue, b))
{
_attempts = 3;
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,124 @@
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.Interop;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class EquipRecommended
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.EquipRecommended)
{
return null;
}
return new EquipTask();
}
}
internal sealed class BeforeDutyOrInstance : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Duty && step.InteractionType != EInteractionType.SinglePlayerDuty && step.InteractionType != EInteractionType.Combat)
{
return null;
}
return new EquipTask();
}
}
internal sealed class EquipTask : ITask
{
public override string ToString()
{
return "EquipRecommended";
}
}
internal sealed class DoEquipRecommended(IClientState clientState, IChatGui chatGui, ICondition condition) : TaskExecutor<EquipTask>()
{
private bool _checkedOrTriggeredEquipmentUpdate;
private DateTime _continueAt = DateTime.MinValue;
protected unsafe override bool Start()
{
if (condition[ConditionFlag.InCombat])
{
return false;
}
RecommendEquipModule.Instance()->SetupForClassJob((byte)clientState.LocalPlayer.ClassJob.RowId);
return true;
}
public unsafe override ETaskResult Update()
{
RecommendEquipModule* ptr = RecommendEquipModule.Instance();
if (ptr->IsUpdating)
{
return ETaskResult.StillRunning;
}
if (!_checkedOrTriggeredEquipmentUpdate)
{
if (!IsAllRecommendeGearEquipped())
{
chatGui.Print("Equipping recommended gear.", "Questionable", 576);
ptr->EquipRecommendedGear();
_continueAt = DateTime.Now.AddSeconds(1.0);
}
_checkedOrTriggeredEquipmentUpdate = true;
return ETaskResult.StillRunning;
}
if (!(DateTime.Now >= _continueAt))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
private unsafe bool IsAllRecommendeGearEquipped()
{
RecommendEquipModule* intPtr = RecommendEquipModule.Instance();
InventoryContainer* inventoryContainer = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
bool result = true;
Span<Pointer<InventoryItem>> recommendedItems = intPtr->RecommendedItems;
for (int i = 0; i < recommendedItems.Length; i++)
{
Pointer<InventoryItem> pointer = recommendedItems[i];
InventoryItem* value = pointer.Value;
if (value == null || value->ItemId == 0)
{
continue;
}
bool flag = false;
for (int j = 0; j < inventoryContainer->Size; j++)
{
InventoryItem inventoryItem = inventoryContainer->Items[j];
if (inventoryItem.ItemId != 0 && inventoryItem.ItemId == value->ItemId)
{
flag = true;
break;
}
}
if (!flag)
{
result = false;
}
}
return result;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Enums;
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 Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Interact
{
internal sealed class Factory(AutomatonIpc automatonIpc, Configuration configuration) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 28) <= 1u) ? true : false)
{
if ((step.InteractionType == EInteractionType.CompleteQuest && configuration.Advanced.PreventQuestCompletion) || step.Emote.HasValue || step.ChatMessage != null || step.ItemId.HasValue || !step.DataId.HasValue)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.PurchaseItem)
{
if (!step.DataId.HasValue)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.Snipe)
{
if (!automatonIpc.IsAutoSnipeEnabled)
{
yield break;
}
}
else if (step.InteractionType == EInteractionType.UnlockTaxiStand)
{
if (!step.TaxiStandId.HasValue)
{
yield break;
}
}
else if (step.InteractionType != EInteractionType.Interact)
{
yield break;
}
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0)
{
yield return new WaitAtEnd.WaitDelay();
}
uint value = step.DataId.Value;
EInteractionType interactionType2 = step.InteractionType;
int skipMarkerCheck;
if (!step.TargetTerritoryId.HasValue && !(quest.Id is SatisfactionSupplyNpcId))
{
SkipConditions skipConditions = step.SkipConditions;
if (skipConditions != null)
{
SkipStepConditions stepIf = skipConditions.StepIf;
if (stepIf != null && stepIf.Never)
{
goto IL_0247;
}
}
if (step.InteractionType != EInteractionType.PurchaseItem)
{
skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0);
goto IL_0248;
}
}
goto IL_0247;
IL_0247:
skipMarkerCheck = 1;
goto IL_0248;
IL_0248:
yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, step.PickUpItemId, step.TaxiStandId, step.SkipConditions?.StepIf, step.CompletionQuestVariablesFlags);
}
}
internal sealed record Task : ITask
{
public uint DataId { get; init; }
public Quest? Quest { get; init; }
public EInteractionType InteractionType { get; init; }
public bool SkipMarkerCheck { get; init; }
public uint? PickUpItemId { get; init; }
public byte? TaxiStandId { get; init; }
public SkipStepConditions? SkipConditions { get; init; }
public List<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
public bool HasCompletionQuestVariablesFlags { get; }
public Task(uint DataId, Quest? Quest, EInteractionType InteractionType, bool SkipMarkerCheck = false, uint? PickUpItemId = null, byte? TaxiStandId = null, SkipStepConditions? SkipConditions = null, List<QuestWorkValue?>? CompletionQuestVariablesFlags = null)
{
this.DataId = DataId;
this.Quest = Quest;
this.InteractionType = InteractionType;
this.SkipMarkerCheck = SkipMarkerCheck;
this.PickUpItemId = PickUpItemId;
this.TaxiStandId = TaxiStandId;
this.SkipConditions = SkipConditions;
this.CompletionQuestVariablesFlags = CompletionQuestVariablesFlags ?? new List<QuestWorkValue>();
HasCompletionQuestVariablesFlags = Quest != null && CompletionQuestVariablesFlags != null && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags);
base._002Ector();
}
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"Interact{(HasCompletionQuestVariablesFlags ? "*" : "")}({DataId})";
}
[CompilerGenerated]
public void Deconstruct(out uint DataId, out Quest? Quest, out EInteractionType InteractionType, out bool SkipMarkerCheck, out uint? PickUpItemId, out byte? TaxiStandId, out SkipStepConditions? SkipConditions, out List<QuestWorkValue?>? CompletionQuestVariablesFlags)
{
DataId = this.DataId;
Quest = this.Quest;
InteractionType = this.InteractionType;
SkipMarkerCheck = this.SkipMarkerCheck;
PickUpItemId = this.PickUpItemId;
TaxiStandId = this.TaxiStandId;
SkipConditions = this.SkipConditions;
CompletionQuestVariablesFlags = this.CompletionQuestVariablesFlags;
}
}
internal sealed class DoInteract(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<DoInteract> logger) : TaskExecutor<Task>(), IConditionChangeAware, ITaskExecutor
{
private enum EInteractionState
{
None,
InteractionTriggered,
InteractionConfirmed
}
private bool _needsUnmount;
private EInteractionState _interactionState;
private DateTime _continueAt = DateTime.MinValue;
private bool delayedFinalCheck;
public Quest? Quest => base.Task.Quest;
public EInteractionType InteractionType { get; set; }
protected override bool Start()
{
InteractionType = base.Task.InteractionType;
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null)
{
logger.LogWarning("No game object with dataId {DataId}", base.Task.DataId);
return false;
}
if (!gameObject.IsTargetable)
{
SkipStepConditions skipConditions = base.Task.SkipConditions;
if (skipConditions != null && !skipConditions.Never && skipConditions.NotTargetable)
{
logger.LogInformation("Not interacting with {DataId} because it is not targetable (but skippable)", base.Task.DataId);
return false;
}
}
if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] && gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint)
{
logger.LogInformation("Preparing interaction for {DataId} by unmounting", base.Task.DataId);
_needsUnmount = true;
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return true;
}
if (gameObject.IsTargetable && HasAnyMarker(gameObject))
{
TriggerInteraction(gameObject);
return true;
}
return true;
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (_needsUnmount)
{
if (condition[ConditionFlag.Mounted])
{
gameFunctions.Unmount();
_continueAt = DateTime.Now.AddSeconds(1.0);
return ETaskResult.StillRunning;
}
_needsUnmount = false;
}
uint? pickUpItemId = base.Task.PickUpItemId;
if (pickUpItemId.HasValue)
{
uint valueOrDefault = pickUpItemId.GetValueOrDefault();
if (InventoryManager.Instance()->GetInventoryItemCount(valueOrDefault, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0)
{
return ETaskResult.TaskComplete;
}
}
else
{
byte? taxiStandId = base.Task.TaxiStandId;
if (taxiStandId.HasValue)
{
byte valueOrDefault2 = taxiStandId.GetValueOrDefault();
if (UIState.Instance()->IsChocoboTaxiStandUnlocked(valueOrDefault2))
{
return ETaskResult.TaskComplete;
}
}
else
{
if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering])
{
return ETaskResult.TaskComplete;
}
if (Quest != null && base.Task.HasCompletionQuestVariablesFlags)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(Quest.Id);
if (questProgressInfo == null || !QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
if (base.ProgressContext != null)
{
if (base.ProgressContext.WasInterrupted())
{
return ETaskResult.StillRunning;
}
if (base.ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed)
{
if (delayedFinalCheck)
{
return ETaskResult.TaskComplete;
}
_continueAt = DateTime.Now.AddSeconds(0.2);
delayedFinalCheck = true;
return ETaskResult.StillRunning;
}
}
}
}
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject))
{
return ETaskResult.StillRunning;
}
TriggerInteraction(gameObject);
return ETaskResult.StillRunning;
}
private void TriggerInteraction(IGameObject gameObject)
{
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(delegate
{
if (gameFunctions.InteractWith(gameObject))
{
_interactionState = EInteractionState.InteractionTriggered;
}
else
{
_interactionState = EInteractionState.None;
}
return _interactionState != EInteractionState.None;
});
_continueAt = DateTime.Now.AddSeconds(0.5);
}
private unsafe bool HasAnyMarker(IGameObject gameObject)
{
if (base.Task.SkipMarkerCheck || gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.EventNpc)
{
return true;
}
GameObject* address = (GameObject*)gameObject.Address;
return address->NamePlateIconId != 0;
}
public void OnConditionChange(ConditionFlag flag, bool value)
{
if (base.ProgressContext == null || (!base.ProgressContext.WasInterrupted() && !base.ProgressContext.WasSuccessful()))
{
logger.LogDebug("Condition change: {Flag} = {Value}", flag, value);
bool flag2 = _interactionState == EInteractionState.InteractionTriggered;
if (flag2)
{
bool flag3 = (uint)(flag - 31) <= 1u;
flag2 = flag3;
}
if (flag2 && value)
{
logger.LogInformation("Interaction was most likely triggered");
_interactionState = EInteractionState.InteractionConfirmed;
}
}
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Jump
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Jump)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.JumpDestination, "step.JumpDestination");
if (step.JumpDestination.Type == EJumpType.SingleJump)
{
return new SingleJumpTask(step.DataId, step.JumpDestination, step.Comment);
}
return new RepeatedJumpTask(step.DataId, step.JumpDestination, step.Comment);
}
}
internal interface IJumpTask : ITask
{
uint? DataId { get; }
JumpDestination JumpDestination { get; }
string? Comment { get; }
}
internal sealed record SingleJumpTask(uint? DataId, JumpDestination JumpDestination, string? Comment) : IJumpTask, ITask
{
public override string ToString()
{
return "Jump(" + Comment + ")";
}
}
internal abstract class JumpBase<T>(MovementController movementController, IClientState clientState, IFramework framework) : TaskExecutor<T>() where T : class, IJumpTask
{
protected unsafe override bool Start()
{
float num = base.Task.JumpDestination.CalculateStopDistance();
if ((_003CclientState_003EP.LocalPlayer.Position - base.Task.JumpDestination.Position).Length() <= num)
{
return false;
}
MovementController movementController = _003CmovementController_003EP;
uint? dataId = base.Task.DataId;
int num2 = 1;
List<Vector3> list = new List<Vector3>(num2);
CollectionsMarshal.SetCount(list, num2);
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
int index = 0;
span[index] = base.Task.JumpDestination.Position;
movementController.NavigateTo(EMovementType.Quest, dataId, list, fly: false, sprint: false, base.Task.JumpDestination.StopDistance ?? num);
_003Cframework_003EP.RunOnTick(delegate
{
ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
}, TimeSpan.FromSeconds(base.Task.JumpDestination.DelaySeconds ?? 0.5f));
return true;
}
public override ETaskResult Update()
{
if (_003CmovementController_003EP.IsPathfinding || _003CmovementController_003EP.IsPathRunning)
{
return ETaskResult.StillRunning;
}
DateTime movementStartedAt = _003CmovementController_003EP.MovementStartedAt;
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(1.0) >= DateTime.Now)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed class DoSingleJump : JumpBase<SingleJumpTask>
{
public DoSingleJump(MovementController movementController, IClientState clientState, IFramework framework)
: base(movementController, clientState, framework)
{
}
}
internal sealed record RepeatedJumpTask(uint? DataId, JumpDestination JumpDestination, string? Comment) : IJumpTask, ITask
{
public override string ToString()
{
return "RepeatedJump(" + Comment + ")";
}
}
internal sealed class DoRepeatedJumps : JumpBase<RepeatedJumpTask>
{
private readonly IClientState _clientState;
private DateTime _continueAt;
private int _attempts;
public DoRepeatedJumps(MovementController movementController, IClientState clientState, IFramework framework, ICondition condition, ILogger<DoRepeatedJumps> logger)
{
_003Ccondition_003EP = condition;
_003Clogger_003EP = logger;
_clientState = clientState;
_continueAt = DateTime.MinValue;
base._002Ector(movementController, clientState, framework);
}
protected override bool Start()
{
_continueAt = DateTime.Now + TimeSpan.FromSeconds(2f * (base.Task.JumpDestination.DelaySeconds ?? 0.5f));
return base.Start();
}
public unsafe override ETaskResult Update()
{
if (DateTime.Now < _continueAt || _003Ccondition_003EP[ConditionFlag.Jumping])
{
return ETaskResult.StillRunning;
}
float num = base.Task.JumpDestination.CalculateStopDistance();
if ((_clientState.LocalPlayer.Position - base.Task.JumpDestination.Position).Length() <= num || _clientState.LocalPlayer.Position.Y >= base.Task.JumpDestination.Position.Y - 0.5f)
{
return ETaskResult.TaskComplete;
}
_003Clogger_003EP.LogTrace("Y-Heights for jumps: player={A}, target={B}", _clientState.LocalPlayer.Position.Y, base.Task.JumpDestination.Position.Y - 0.5f);
if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null))
{
_attempts++;
}
if (_attempts >= 50)
{
throw new TaskException("Tried to jump too many times, didn't reach the target");
}
_continueAt = DateTime.Now + TimeSpan.FromSeconds(base.Task.JumpDestination.DelaySeconds ?? 0.5f);
return ETaskResult.StillRunning;
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class PurchaseItem
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.PurchaseItem)
{
return null;
}
throw new NotImplementedException();
}
}
internal sealed class PurchaseRequest
{
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class Say
{
internal sealed class Factory(ExcelFunctions excelFunctions) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((uint)(interactionType - 28) <= 1u)
{
if (step.ChatMessage == null)
{
return Array.Empty<ITask>();
}
}
else if (step.InteractionType != EInteractionType.Say)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.ChatMessage, "step.ChatMessage");
string? text = excelFunctions.GetDialogueText(quest, step.ChatMessage.ExcelSheet, step.ChatMessage.Key, isRegex: false).GetString();
ArgumentNullException.ThrowIfNull(text, "excelString");
Mount.UnmountTask unmountTask = new Mount.UnmountTask();
Task task = new Task(text);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, task });
}
}
internal sealed record Task(string ChatMessage) : ITask
{
public override string ToString()
{
return "Say(" + ChatMessage + ")";
}
}
internal sealed class UseChat(ChatFunctions chatFunctions) : AbstractDelayedTaskExecutor<Task>()
{
protected override bool StartInternal()
{
chatFunctions.ExecuteCommand("/say " + base.Task.ChatMessage);
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,268 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class SinglePlayerDuty
{
internal static class SpecialTerritories
{
public const ushort Lahabrea = 1052;
public const ushort ItsProbablyATrap = 665;
public const ushort Naadam = 688;
}
internal sealed class Factory(BossModIpc bossModIpc, TerritoryData territoryData, ICondition condition, IClientState clientState) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SinglePlayerDuty || !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{
yield break;
}
if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out TerritoryData.ContentFinderConditionData cfcData))
{
throw new TaskException("Failed to get content finder condition for solo instance");
}
yield return new Mount.UnmountTask();
yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(2L));
yield return new EnableAi(cfcData.TerritoryId == 688);
if (cfcData.TerritoryId == 1052)
{
yield return new SetTarget(14643u);
yield return new WaitCondition.Task(() => condition[ConditionFlag.Unconscious] || clientState.TerritoryType != 1052, "Wait(death)");
yield return new DisableAi();
yield return new WaitCondition.Task(() => !condition[ConditionFlag.Unconscious] || clientState.TerritoryType != 1052, "Wait(resurrection)");
yield return new EnableAi();
}
else if (cfcData.TerritoryId == 665)
{
yield return new WaitCondition.Task(() => DutyActionsAvailable() || clientState.TerritoryType != 665, "Wait(Phase 2)");
yield return new EnableAi(Passive: true);
}
else if (cfcData.TerritoryId == 688)
{
yield return new WaitCondition.Task(delegate
{
if (clientState.TerritoryType != 688)
{
return true;
}
Vector3 vector = clientState.LocalPlayer?.Position ?? default(Vector3);
return (new Vector3(352.01f, -1.45f, 288.59f) - vector).Length() < 10f;
}, "Wait(moving to Ovoo)");
yield return new Mount.UnmountTask();
yield return new EnableAi();
}
yield return new WaitSinglePlayerDuty(cfcData.ContentFinderConditionId);
yield return new DisableAi();
yield return new WaitAtEnd.WaitNextStepOrSequence();
}
private unsafe bool DutyActionsAvailable()
{
ContentDirector* contentDirector = EventFramework.Instance()->GetContentDirector();
if (contentDirector != null)
{
return contentDirector->DutyActionManager.ActionsPresent;
}
return false;
}
}
internal sealed record StartSinglePlayerDuty(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"Wait(BossMod, entered instance {ContentFinderConditionId})";
}
}
internal sealed class StartSinglePlayerDutyExecutor(ICondition condition) : TaskExecutor<StartSinglePlayerDuty>()
{
private DateTime _enteredAt = DateTime.MinValue;
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (GameMain.Instance()->CurrentContentFinderConditionId != base.Task.ContentFinderConditionId)
{
return ETaskResult.StillRunning;
}
if (!condition[ConditionFlag.BoundByDuty])
{
return ETaskResult.StillRunning;
}
if (_enteredAt == DateTime.MinValue)
{
_enteredAt = DateTime.Now;
}
if (!(DateTime.Now - _enteredAt >= TimeSpan.FromSeconds(2L)))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record EnableAi(bool Passive = false) : ITask
{
public override string ToString()
{
return "BossMod.EnableAi(" + (Passive ? "Passive" : "AutoPull") + ")";
}
}
internal sealed class EnableAiExecutor(BossModIpc bossModIpc) : TaskExecutor<EnableAi>()
{
protected override bool Start()
{
bossModIpc.EnableAi(base.Task.Passive);
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : ITask
{
public override string ToString()
{
return $"Wait(BossMod, left instance {ContentFinderConditionId})";
}
}
internal sealed class WaitSinglePlayerDutyExecutor(BossModIpc bossModIpc, MovementController movementController) : TaskExecutor<WaitSinglePlayerDuty>(), IStoppableTaskExecutor, ITaskExecutor, IDebugStateProvider
{
protected override bool Start()
{
return true;
}
public unsafe override ETaskResult Update()
{
if (GameMain.Instance()->CurrentContentFinderConditionId == base.Task.ContentFinderConditionId)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public void StopNow()
{
bossModIpc.DisableAi();
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
public string? GetDebugState()
{
if (!movementController.IsNavmeshReady)
{
return $"Navmesh: {movementController.BuiltNavmeshPercent}%";
}
return null;
}
}
internal sealed record DisableAi : ITask
{
public override string ToString()
{
return "BossMod.DisableAi";
}
}
internal sealed class DisableAiExecutor(BossModIpc bossModIpc) : TaskExecutor<DisableAi>()
{
protected override bool Start()
{
bossModIpc.DisableAi();
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record SetTarget(uint DataId) : ITask
{
public override string ToString()
{
return $"SetTarget({DataId})";
}
}
internal sealed class SetTargetExecutor(ITargetManager targetManager, IObjectTable objectTable) : TaskExecutor<SetTarget>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (targetManager.Target?.DataId == base.Task.DataId)
{
return ETaskResult.TaskComplete;
}
IGameObject gameObject = objectTable.FirstOrDefault((IGameObject x) => x.DataId == base.Task.DataId);
if (gameObject == null)
{
return ETaskResult.StillRunning;
}
targetManager.Target = gameObject;
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,62 @@
using System;
using Questionable.Controller.Steps.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class StatusOff
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.StatusOff)
{
return null;
}
ArgumentNullException.ThrowIfNull(step.Status, "step.Status");
return new Task(step.Status.Value);
}
}
internal sealed record Task(EStatus Status) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"StatusOff({Status})";
}
}
internal sealed class DoStatusOff(GameFunctions gameFunctions) : AbstractDelayedTaskExecutor<Task>()
{
protected override bool StartInternal()
{
if (gameFunctions.HasStatus(base.Task.Status))
{
return GameFunctions.RemoveStatus(base.Task.Status);
}
return false;
}
public override ETaskResult Update()
{
if (!gameFunctions.HasStatus(base.Task.Status))
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Movement;
using Questionable.Controller.Steps.Shared;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class UseItem
{
internal sealed class Factory(IClientState clientState, TerritoryData territoryData, ILogger<Factory> logger) : ITaskFactory
{
public unsafe IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
EInteractionType interactionType = step.InteractionType;
if ((interactionType == EInteractionType.SinglePlayerDuty || interactionType == EInteractionType.CompleteQuest) ? true : false)
{
if (!step.ItemId.HasValue)
{
return Array.Empty<ITask>();
}
}
else if (step.InteractionType != EInteractionType.UseItem)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
if (step.ItemId == 30362)
{
if (InventoryManager.Instance()->GetInventoryItemCount(step.ItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0) == 0)
{
return CreateVesperBayFallbackTask();
}
UseOnSelf useOnSelf = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
int num = sequence.Steps.IndexOf(step);
Vector3? position = (sequence.Steps.Skip(num + 1).FirstOrDefault() ?? step).Position;
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[4]
{
useOnSelf,
new WaitCondition.Task(() => clientState.TerritoryType == 140, "Wait(territory: " + territoryData.GetNameAndId(140) + ")"),
new Mount.MountTask(140, position.HasValue ? Mount.EMountIf.AwayFromPosition : Mount.EMountIf.Always, position),
new MoveTask(140, new Vector3(-408.92343f, 23.167036f, -351.16223f), null, 0.25f, null, DisableNavmesh: true, false, Fly: false, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.WalkTo)
});
}
Mount.UnmountTask unmountTask = new Mount.UnmountTask();
if (step.GroundTarget == true)
{
ITask task;
if (step.DataId.HasValue)
{
task = new UseOnGround(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
}
else
{
ArgumentNullException.ThrowIfNull(step.Position, "step.Position");
task = new UseOnPosition(quest.Id, step.Position.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
unmountTask,
new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5)),
task
});
}
if (step.DataId.HasValue)
{
UseOnObject useOnObject = new UseOnObject(quest.Id, step.DataId.Value, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnObject });
}
UseOnSelf useOnSelf2 = new UseOnSelf(quest.Id, step.ItemId.Value, step.CompletionQuestVariablesFlags);
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { unmountTask, useOnSelf2 });
}
private IEnumerable<ITask> CreateVesperBayFallbackTask()
{
logger.LogWarning("No vesper bay aetheryte tickets in inventory, navigating via ferry in Limsa instead");
uint npcId = 1003540u;
ushort territoryId = 129;
Vector3 destination = new Vector3(-360.9217f, 8f, 38.92566f);
yield return new AetheryteShortcut.Task(null, null, EAetheryteLocation.Limsa, territoryId);
yield return new Questionable.Controller.Steps.Shared.AethernetShortcut.Task(EAetheryteLocation.Limsa, EAetheryteLocation.LimsaArcanist);
yield return new WaitAtEnd.WaitDelay();
uint? dataId = npcId;
bool? sprint = false;
yield return new MoveTask(territoryId, destination, null, null, dataId, DisableNavmesh: false, sprint, Fly: false, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.WalkTo);
yield return new Interact.Task(npcId, null, EInteractionType.None, SkipMarkerCheck: true);
}
}
internal interface IUseItemBase : ITask
{
ElementId? QuestId { get; }
uint ItemId { get; }
IList<QuestWorkValue?> CompletionQuestVariablesFlags { get; }
bool StartingCombat { get; }
}
internal abstract class UseItemExecutorBase<T>(QuestFunctions questFunctions, ICondition condition, ILogger logger) : TaskExecutor<T>() where T : class, IUseItemBase
{
private bool _usedItem;
private DateTime _continueAt;
private int _itemCount;
private ElementId? QuestId => base.Task.QuestId;
protected uint ItemId => base.Task.ItemId;
private IList<QuestWorkValue?> CompletionQuestVariablesFlags => base.Task.CompletionQuestVariablesFlags;
private bool StartingCombat => base.Task.StartingCombat;
protected abstract bool UseItem();
protected unsafe override bool Start()
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
throw new TaskException("No InventoryManager");
}
_itemCount = ptr->GetInventoryItemCount(ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0);
if (_itemCount == 0)
{
throw new TaskException($"Don't have any {ItemId} in inventory (checks NQ only)");
}
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay());
return true;
}
public unsafe override ETaskResult Update()
{
if (QuestId is QuestId elementId && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags))
{
QuestProgressInfo questProgressInfo = _003CquestFunctions_003EP.GetQuestProgressInfo(elementId);
if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(CompletionQuestVariablesFlags, questProgressInfo))
{
return ETaskResult.TaskComplete;
}
}
if (DateTime.Now <= _continueAt)
{
return ETaskResult.StillRunning;
}
if (StartingCombat && _003Ccondition_003EP[ConditionFlag.InCombat])
{
return ETaskResult.TaskComplete;
}
if (ItemId == 30362 && _usedItem)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
_003Clogger_003EP.LogWarning("InventoryManager is not available");
return ETaskResult.StillRunning;
}
if (ptr->GetInventoryItemCount(ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) == _itemCount)
{
_003Clogger_003EP.LogInformation("Attempted to use vesper bay aetheryte ticket, but it didn't consume an item - reattempting next frame");
_usedItem = false;
return ETaskResult.StillRunning;
}
}
if (!_usedItem)
{
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => _usedItem = UseItem());
_continueAt = DateTime.Now.Add(GetRetryDelay());
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
private TimeSpan GetRetryDelay()
{
if (ItemId == 30362)
{
return TimeSpan.FromSeconds(11L);
}
return TimeSpan.FromSeconds(5L);
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record UseOnGround(ElementId? QuestId, uint DataId, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId} on ground at {DataId})";
}
}
internal sealed class UseOnGroundExecutor : UseItemExecutorBase<UseOnGround>
{
public UseOnGroundExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnGroundExecutor> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItemOnGround(base.Task.DataId, base.ItemId);
}
}
internal sealed record UseOnPosition(ElementId? QuestId, Vector3 Position, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId} on ground at {Position.ToString("G", CultureInfo.InvariantCulture)})";
}
}
internal sealed class UseOnPositionExecutor : UseItemExecutorBase<UseOnPosition>
{
public UseOnPositionExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnPosition> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItemOnPosition(base.Task.Position, base.ItemId);
}
}
internal sealed record UseOnObject(ElementId? QuestId, uint DataId, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId} on {DataId})";
}
}
internal sealed class UseOnObjectExecutor : UseItemExecutorBase<UseOnObject>
{
public UseOnObjectExecutor(QuestFunctions questFunctions, GameFunctions gameFunctions, ICondition condition, ILogger<UseOnObject> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItem(base.Task.DataId, base.ItemId);
}
}
internal sealed record UseOnSelf(ElementId? QuestId, uint ItemId, IList<QuestWorkValue?> CompletionQuestVariablesFlags, bool StartingCombat = false) : IUseItemBase, ITask
{
public override string ToString()
{
return $"UseItem({ItemId})";
}
}
internal sealed class UseOnSelfExecutor : UseItemExecutorBase<UseOnSelf>
{
public UseOnSelfExecutor(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger<UseOnSelf> logger)
{
_003CgameFunctions_003EP = gameFunctions;
base._002Ector(questFunctions, condition, (ILogger)logger);
}
protected override bool UseItem()
{
return _003CgameFunctions_003EP.UseItem(base.ItemId);
}
}
}

View file

@ -0,0 +1,61 @@
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.Steps.Movement;
internal sealed class LandExecutor(IClientState clientState, ICondition condition, ILogger<LandExecutor> logger) : TaskExecutor<LandTask>()
{
private bool _landing;
private DateTime _continueAt;
protected override bool Start()
{
if (!condition[ConditionFlag.InFlight])
{
logger.LogInformation("Not flying, not attempting to land");
return false;
}
_landing = AttemptLanding();
_continueAt = DateTime.Now.AddSeconds(0.25);
return true;
}
public override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
if (condition[ConditionFlag.InFlight])
{
if (!_landing)
{
_landing = AttemptLanding();
_continueAt = DateTime.Now.AddSeconds(0.25);
}
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
private unsafe bool AttemptLanding()
{
Character* ptr = (Character*)(clientState.LocalPlayer?.Address ?? 0);
if (ptr != null && ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0)
{
logger.LogInformation("Attempting to land");
return ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}

View file

@ -0,0 +1,14 @@
namespace Questionable.Controller.Steps.Movement;
internal sealed class LandTask : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return "Land";
}
}

View file

@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
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.Common;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Movement;
internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskExecutor
{
private readonly string _cannotExecuteAtThisTime;
private readonly MovementController _movementController;
private readonly GameFunctions _gameFunctions;
private readonly ILogger<MoveExecutor> _logger;
private readonly IClientState _clientState;
private readonly ICondition _condition;
private readonly Questionable.Controller.Steps.Common.Mount.MountEvaluator _mountEvaluator;
private readonly IServiceProvider _serviceProvider;
private System.Action? _startAction;
private Vector3 _destination;
private bool _canRestart;
private (Questionable.Controller.Steps.Common.Mount.MountExecutor Executor, Questionable.Controller.Steps.Common.Mount.MountTask Task)? _mountBeforeMovement;
private (Questionable.Controller.Steps.Common.Mount.UnmountExecutor Executor, Questionable.Controller.Steps.Common.Mount.UnmountTask Task)? _unmountBeforeMovement;
private (Questionable.Controller.Steps.Common.Mount.MountExecutor Executor, Questionable.Controller.Steps.Common.Mount.MountTask Task)? _mountDuringMovement;
public MoveExecutor(MovementController movementController, GameFunctions gameFunctions, ILogger<MoveExecutor> logger, IClientState clientState, ICondition condition, IDataManager dataManager, Questionable.Controller.Steps.Common.Mount.MountEvaluator mountEvaluator, IServiceProvider serviceProvider)
{
_movementController = movementController;
_gameFunctions = gameFunctions;
_logger = logger;
_clientState = clientState;
_condition = condition;
_serviceProvider = serviceProvider;
_mountEvaluator = mountEvaluator;
_cannotExecuteAtThisTime = dataManager.GetString(579u, (LogMessage x) => x.Text);
}
private void PrepareMovementIfNeeded()
{
if (!_gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId))
{
base.Task = base.Task with
{
Fly = false,
Land = false
};
}
if (!base.Task.DisableNavmesh)
{
_startAction = delegate
{
_movementController.NavigateTo(EMovementType.Quest, base.Task.DataId, _destination, base.Task.Fly, base.Task.Sprint ?? (!_mountDuringMovement.HasValue), base.Task.StopDistance, base.Task.IgnoreDistanceToObject ? new float?(float.MaxValue) : ((float?)null), base.Task.Land);
};
return;
}
_startAction = delegate
{
MovementController movementController = _movementController;
uint? dataId = base.Task.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;
movementController.NavigateTo(EMovementType.Quest, dataId, list, base.Task.Fly, base.Task.Sprint ?? (!_mountDuringMovement.HasValue), base.Task.StopDistance, base.Task.IgnoreDistanceToObject ? new float?(float.MaxValue) : ((float?)null), base.Task.Land);
};
}
protected override bool Start()
{
_canRestart = base.Task.RestartNavigation;
_destination = base.Task.Destination;
float num = base.Task.StopDistance ?? 3f;
Vector3? vector = _clientState.LocalPlayer?.Position;
float num2 = ((!vector.HasValue) ? float.MaxValue : Vector3.Distance(vector.Value, _destination));
if (num2 > num)
{
PrepareMovementIfNeeded();
}
if (base.Task.Mount == true)
{
Questionable.Controller.Steps.Common.Mount.MountTask mountTask = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, Questionable.Controller.Steps.Common.Mount.EMountIf.Always);
_mountBeforeMovement = (_serviceProvider.GetRequiredService<Questionable.Controller.Steps.Common.Mount.MountExecutor>(), mountTask);
if (!_mountBeforeMovement.Value.Executor.Start(mountTask))
{
_mountBeforeMovement = null;
}
}
else if (base.Task.Mount == false)
{
Questionable.Controller.Steps.Common.Mount.UnmountTask unmountTask = new Questionable.Controller.Steps.Common.Mount.UnmountTask();
_unmountBeforeMovement = (_serviceProvider.GetRequiredService<Questionable.Controller.Steps.Common.Mount.UnmountExecutor>(), unmountTask);
if (!_unmountBeforeMovement.Value.Executor.Start(unmountTask))
{
_unmountBeforeMovement = null;
}
}
else if (!base.Task.DisableNavmesh)
{
Questionable.Controller.Steps.Common.Mount.EMountIf mountIf = ((!(num2 > num) || !base.Task.Fly || !_gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId)) ? Questionable.Controller.Steps.Common.Mount.EMountIf.AwayFromPosition : Questionable.Controller.Steps.Common.Mount.EMountIf.Always);
Questionable.Controller.Steps.Common.Mount.MountTask mountTask2 = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, mountIf, _destination);
DateTime retryAt = DateTime.Now;
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? tuple;
if (_mountEvaluator.EvaluateMountState(mountTask2, dryRun: true, ref retryAt) != Questionable.Controller.Steps.Common.Mount.MountResult.DontMount)
{
tuple = (_serviceProvider.GetRequiredService<Questionable.Controller.Steps.Common.Mount.MountExecutor>(), mountTask2);
tuple.Value.Item1.Start(mountTask2);
}
else
{
tuple = null;
}
if (base.Task.Fly)
{
_mountBeforeMovement = tuple;
}
else
{
_mountDuringMovement = tuple;
}
}
if (!_mountBeforeMovement.HasValue && !_unmountBeforeMovement.HasValue && _startAction != null)
{
_startAction();
}
return true;
}
public override ETaskResult Update()
{
ETaskResult? eTaskResult = UpdateMountState();
if (eTaskResult.HasValue)
{
return eTaskResult.GetValueOrDefault();
}
if (_startAction == null)
{
return ETaskResult.TaskComplete;
}
if (_movementController.IsPathfinding || _movementController.IsPathRunning)
{
return ETaskResult.StillRunning;
}
DateTime movementStartedAt = _movementController.MovementStartedAt;
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2.0) >= DateTime.Now)
{
return ETaskResult.StillRunning;
}
if (_canRestart && Vector3.Distance(_clientState.LocalPlayer.Position, _destination) > (base.Task.StopDistance ?? 3f) + 5f)
{
_canRestart = false;
if (_clientState.TerritoryType == base.Task.TerritoryId)
{
_logger.LogInformation("Looks like movement was interrupted, re-attempting to move");
_startAction();
return ETaskResult.StillRunning;
}
_logger.LogInformation("Looks like movement was interrupted, do nothing since we're in a different territory now");
}
return ETaskResult.TaskComplete;
}
private ETaskResult? UpdateMountState()
{
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement;
if (mountBeforeMovement.HasValue)
{
Questionable.Controller.Steps.Common.Mount.MountExecutor item = mountBeforeMovement.GetValueOrDefault().Item1;
if (item != null)
{
if (item.Update() == ETaskResult.TaskComplete)
{
_logger.LogInformation("MountBeforeMovement complete");
_mountBeforeMovement = null;
_startAction?.Invoke();
return null;
}
return ETaskResult.StillRunning;
}
}
(Questionable.Controller.Steps.Common.Mount.UnmountExecutor, Questionable.Controller.Steps.Common.Mount.UnmountTask)? unmountBeforeMovement = _unmountBeforeMovement;
if (unmountBeforeMovement.HasValue)
{
Questionable.Controller.Steps.Common.Mount.UnmountExecutor item2 = unmountBeforeMovement.GetValueOrDefault().Item1;
if (item2 != null)
{
if (item2.Update() == ETaskResult.TaskComplete)
{
_logger.LogInformation("UnmountBeforeMovement complete");
_unmountBeforeMovement = null;
_startAction?.Invoke();
return null;
}
return ETaskResult.StillRunning;
}
}
mountBeforeMovement = _mountDuringMovement;
if (mountBeforeMovement.HasValue)
{
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask) valueOrDefault = mountBeforeMovement.GetValueOrDefault();
var (mountExecutor, _) = valueOrDefault;
if (mountExecutor != null)
{
Questionable.Controller.Steps.Common.Mount.MountTask item3 = valueOrDefault.Item2;
if ((object)item3 != null)
{
if (mountExecutor.Update() == ETaskResult.TaskComplete)
{
_logger.LogInformation("MountDuringMovement complete (mounted)");
_mountDuringMovement = null;
return null;
}
DateTime retryAt = DateTime.Now;
if (_mountEvaluator.EvaluateMountState(item3, dryRun: true, ref retryAt) == Questionable.Controller.Steps.Common.Mount.MountResult.DontMount)
{
_logger.LogInformation("MountDuringMovement implicitly complete (shouldn't mount anymore)");
_mountDuringMovement = null;
return null;
}
return null;
}
}
}
return null;
}
public override bool WasInterrupted()
{
DateTime retryAt = DateTime.Now;
if (base.Task.Fly && _condition[ConditionFlag.InCombat] && !_condition[ConditionFlag.Mounted])
{
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement;
if (mountBeforeMovement.HasValue)
{
Questionable.Controller.Steps.Common.Mount.MountTask item = mountBeforeMovement.GetValueOrDefault().Item2;
if ((object)item != null && _mountEvaluator.EvaluateMountState(item, dryRun: true, ref retryAt) == Questionable.Controller.Steps.Common.Mount.MountResult.WhenOutOfCombat)
{
return true;
}
}
}
return base.WasInterrupted();
}
public override bool ShouldInterruptOnDamage()
{
if (!_mountBeforeMovement.HasValue)
{
return ShouldResolveCombatBeforeNextInteraction();
}
return true;
}
private bool ShouldResolveCombatBeforeNextInteraction()
{
return base.Task.InteractionType == EInteractionType.Jump;
}
public bool OnErrorToast(SeString message)
{
if (GameFunctions.GameStringEquals(_cannotExecuteAtThisTime, message.TextValue))
{
return true;
}
return false;
}
}

View file

@ -0,0 +1,23 @@
using System.Globalization;
using System.Numerics;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Movement;
internal sealed record MoveTask(ushort TerritoryId, Vector3 Destination, bool? Mount = null, float? StopDistance = null, uint? DataId = null, bool DisableNavmesh = false, bool? Sprint = null, bool Fly = false, bool Land = false, bool IgnoreDistanceToObject = false, bool RestartNavigation = true, EInteractionType InteractionType = EInteractionType.None) : ITask
{
public MoveTask(QuestStep step, Vector3 destination)
: this(step.TerritoryId, destination, step.Mount, step.CalculateActualStopDistance(), step.DataId, step.DisableNavmesh, step.Sprint, step.Fly == true, step.Land == true, step.IgnoreDistanceToObject == true, step.RestartNavigationIfCancelled != false, step.InteractionType)
{
}
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return "MoveTo(" + Destination.ToString("G", CultureInfo.InvariantCulture) + ")";
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Movement;
internal static class MoveTo
{
internal sealed class Factory(IClientState clientState, AetheryteData aetheryteData, TerritoryData territoryData, ILogger<Factory> logger) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.Position.HasValue)
{
return CreateMoveTasks(step, step.Position.Value);
}
if (step != null && step.DataId.HasValue && step.StopDistance.HasValue)
{
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new WaitForNearDataId(step.DataId.Value, step.StopDistance.Value));
}
EAetheryteLocation valueOrDefault = default(EAetheryteLocation);
bool flag;
if (step != null)
{
EInteractionType interactionType = step.InteractionType;
if ((uint)(interactionType - 4) <= 1u)
{
EAetheryteLocation? aetheryte = step.Aetheryte;
if (aetheryte.HasValue)
{
valueOrDefault = aetheryte.GetValueOrDefault();
flag = true;
goto IL_00a3;
}
}
}
flag = false;
goto IL_00a3;
IL_00a3:
if (flag)
{
return CreateMoveTasks(step, aetheryteData.Locations[valueOrDefault]);
}
if (step != null && step.InteractionType == EInteractionType.AttuneAethernetShard)
{
EAetheryteLocation? aetheryte = step.AethernetShard;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault2 = aetheryte.GetValueOrDefault();
return CreateMoveTasks(step, aetheryteData.Locations[valueOrDefault2]);
}
}
return Array.Empty<ITask>();
}
private IEnumerable<ITask> CreateMoveTasks(QuestStep step, Vector3 destination)
{
if (step.InteractionType == EInteractionType.Jump && step.JumpDestination != null && (clientState.LocalPlayer.Position - step.JumpDestination.Position).Length() <= (step.JumpDestination.StopDistance ?? 1f))
{
logger.LogInformation("We're at the jump destination, skipping movement");
yield break;
}
yield return new WaitCondition.Task(() => clientState.TerritoryType == step.TerritoryId, "Wait(territory: " + territoryData.GetNameAndId(step.TerritoryId) + ")");
if (!step.DisableNavmesh)
{
yield return new WaitNavmesh.Task();
}
yield return new MoveTask(step, destination);
if (step != null)
{
bool? fly = step.Fly;
if (fly.HasValue && fly == true && (step.Land ?? false))
{
yield return new LandTask();
}
}
}
}
}

View file

@ -0,0 +1,3 @@
namespace Questionable.Controller.Steps.Movement;
internal sealed record NoOpTask : ITask;

View file

@ -0,0 +1,19 @@
namespace Questionable.Controller.Steps.Movement;
internal sealed class NoOpTaskExecutor : TaskExecutor<NoOpTask>
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}

View file

@ -0,0 +1,9 @@
namespace Questionable.Controller.Steps.Movement;
internal sealed record WaitForNearDataId(uint DataId, float StopDistance) : ITask
{
public bool ShouldRedoOnInterrupt()
{
return true;
}
}

View file

@ -0,0 +1,28 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Questionable.Functions;
namespace Questionable.Controller.Steps.Movement;
internal sealed class WaitForNearDataIdExecutor(GameFunctions gameFunctions, IClientState clientState) : TaskExecutor<WaitForNearDataId>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId);
if (gameObject == null || (gameObject.Position - clientState.LocalPlayer.Position).Length() > base.Task.StopDistance)
{
throw new TaskException("Object not found or too far away, no position so we can't move");
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}

View file

@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
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.Steps.Shared;
internal static class AethernetShortcut
{
internal sealed class Factory(AetheryteData aetheryteData, TerritoryData territoryData, IClientState clientState) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.AethernetShortcut == null)
{
yield break;
}
yield return new WaitNavmesh.Task();
yield return new Task(step.AethernetShortcut.From, step.AethernetShortcut.To, step.SkipConditions?.AethernetShortcutIf ?? new SkipAetheryteCondition());
if (AetheryteShortcut.MoveAwayFromAetheryteExecutor.AppliesTo(step.AethernetShortcut.To))
{
yield return new WaitCondition.Task(() => clientState.TerritoryType == aetheryteData.TerritoryIds[step.AethernetShortcut.To], "Wait(territory: " + territoryData.GetNameAndId(aetheryteData.TerritoryIds[step.AethernetShortcut.To]) + ")");
yield return new AetheryteShortcut.MoveAwayFromAetheryte(step.AethernetShortcut.To);
}
}
}
internal sealed record Task(EAetheryteLocation From, EAetheryteLocation To, SkipAetheryteCondition SkipConditions) : ISkippableTask, ITask
{
public Task(EAetheryteLocation from, EAetheryteLocation to)
: this(from, to, new SkipAetheryteCondition())
{
}
public override string ToString()
{
return $"UseAethernet({From} -> {To})";
}
}
internal sealed class UseAethernetShortcut(ILogger<UseAethernetShortcut> logger, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, QuestFunctions questFunctions, IClientState clientState, AetheryteData aetheryteData, TerritoryData territoryData, LifestreamIpc lifestreamIpc, MovementController movementController, ICondition condition) : TaskExecutor<Task>()
{
private bool _moving;
private bool _teleported;
private bool _triedMounting;
private DateTime _continueAt = DateTime.MinValue;
protected override bool Start()
{
if (!base.Task.SkipConditions.Never)
{
if (base.Task.SkipConditions.InSameTerritory && clientState.TerritoryType == aetheryteData.TerritoryIds[base.Task.To])
{
logger.LogInformation("Skipping aethernet shortcut because the target is in the same territory");
return false;
}
if (base.Task.SkipConditions.InTerritory.Contains(clientState.TerritoryType))
{
logger.LogInformation("Skipping aethernet shortcut because the target is in the specified territory");
return false;
}
if (base.Task.SkipConditions.QuestsCompleted.Count > 0 && base.Task.SkipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
{
logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are complete");
return true;
}
if (base.Task.SkipConditions.QuestsAccepted.Count > 0 && base.Task.SkipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{
logger.LogInformation("Skipping aethernet shortcut, all prequisite quests are accepted");
return true;
}
if (base.Task.SkipConditions.AetheryteLocked.HasValue && !aetheryteFunctions.IsAetheryteUnlocked(base.Task.SkipConditions.AetheryteLocked.Value))
{
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is locked");
return false;
}
if (base.Task.SkipConditions.AetheryteUnlocked.HasValue && aetheryteFunctions.IsAetheryteUnlocked(base.Task.SkipConditions.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping aethernet shortcut because the target aetheryte is unlocked");
return false;
}
}
if (aetheryteFunctions.IsAetheryteUnlocked(base.Task.From) && aetheryteFunctions.IsAetheryteUnlocked(base.Task.To))
{
ushort territoryType = clientState.TerritoryType;
Vector3 playerPosition = clientState.LocalPlayer.Position;
if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.To))
{
if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < (base.Task.From.IsFirmamentAetheryte() ? 11f : 4f))
{
DoTeleport();
return true;
}
if (base.Task.From == EAetheryteLocation.SolutionNine)
{
logger.LogInformation("Moving to S9 aetheryte");
int num = 4;
List<Vector3> list = new List<Vector3>(num);
CollectionsMarshal.SetCount(list, num);
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = new Vector3(0f, 8.442986f, 9f);
num2++;
span[num2] = new Vector3(9f, 8.442986f, 0f);
num2++;
span[num2] = new Vector3(-9f, 8.442986f, 0f);
num2++;
span[num2] = new Vector3(0f, 8.442986f, -9f);
Vector3 to = list.MinBy((Vector3 x) => Vector3.Distance(playerPosition, x));
_moving = true;
movementController.NavigateTo(EMovementType.Quest, (uint)base.Task.From, to, fly: false, sprint: true, 0.25f);
return true;
}
if (territoryData.CanUseMount(territoryType) && aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) > 30f && !gameFunctions.HasStatusPreventingMount())
{
_triedMounting = gameFunctions.Mount();
if (_triedMounting)
{
_continueAt = DateTime.Now.AddSeconds(0.5);
return true;
}
}
MoveTo();
return true;
}
}
else
{
if (clientState.TerritoryType != aetheryteData.TerritoryIds[base.Task.To])
{
throw new TaskException($"Aethernet shortcut not unlocked (from: {base.Task.From}, to: {base.Task.To})");
}
logger.LogWarning("Aethernet shortcut not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), skipping as we are already in the destination territory", base.Task.From, base.Task.To);
}
return false;
}
private void MoveTo()
{
logger.LogInformation("Moving to aethernet shortcut");
_moving = true;
EAetheryteLocation eAetheryteLocation = base.Task.From;
float num = (base.Task.From.IsFirmamentAetheryte() ? 4.4f : ((eAetheryteLocation != EAetheryteLocation.UldahChamberOfRule) ? ((!AetheryteConverter.IsLargeAetheryte(base.Task.From)) ? 6.9f : 10.9f) : 5f));
float value = num;
bool flag = aetheryteData.IsGoldSaucerAetheryte(base.Task.From) && !AetheryteConverter.IsLargeAetheryte(base.Task.From);
movementController.NavigateTo(EMovementType.Quest, (uint)base.Task.From, aetheryteData.Locations[base.Task.From], fly: false, sprint: true, value, flag ? new float?(5f) : ((float?)null));
}
private void DoTeleport()
{
logger.LogInformation("Using lifestream to teleport to {Destination}", base.Task.To);
lifestreamIpc.Teleport(base.Task.To);
_teleported = true;
}
public override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
if (_triedMounting)
{
if (condition[ConditionFlag.Mounted])
{
_triedMounting = false;
MoveTo();
return ETaskResult.StillRunning;
}
return ETaskResult.StillRunning;
}
if (_moving)
{
DateTime movementStartedAt = movementController.MovementStartedAt;
if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2.0) >= DateTime.Now)
{
return ETaskResult.StillRunning;
}
if (!movementController.IsPathfinding && !movementController.IsPathRunning)
{
_moving = false;
}
return ETaskResult.StillRunning;
}
if (!_teleported)
{
DoTeleport();
return ETaskResult.StillRunning;
}
Vector3? vector = clientState.LocalPlayer?.Position;
if (!vector.HasValue)
{
return ETaskResult.StillRunning;
}
if (aetheryteData.IsAirshipLanding(base.Task.To))
{
if (aetheryteData.CalculateAirshipLandingDistance(vector.Value, clientState.TerritoryType, base.Task.To) > 5f)
{
return ETaskResult.StillRunning;
}
}
else if (aetheryteData.IsCityAetheryte(base.Task.To) || aetheryteData.IsGoldSaucerAetheryte(base.Task.To))
{
if (aetheryteData.CalculateDistance(vector.Value, clientState.TerritoryType, base.Task.To) > 20f)
{
return ETaskResult.StillRunning;
}
}
else if (clientState.TerritoryType != aetheryteData.TerritoryIds[base.Task.To])
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Movement;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class AetheryteShortcut
{
internal sealed class Factory(AetheryteData aetheryteData, TerritoryData territoryData, IClientState clientState) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (!step.AetheryteShortcut.HasValue)
{
yield break;
}
yield return new Task(step, quest.Id, step.AetheryteShortcut.Value, aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]);
yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(0.5));
if (MoveAwayFromAetheryteExecutor.AppliesTo(step.AetheryteShortcut.Value) && step.AethernetShortcut?.From != step.AetheryteShortcut.Value)
{
yield return new WaitCondition.Task(() => clientState.TerritoryType == aetheryteData.TerritoryIds[step.AetheryteShortcut.Value], "Wait(territory: " + territoryData.GetNameAndId(aetheryteData.TerritoryIds[step.AetheryteShortcut.Value]) + ")");
yield return new MoveAwayFromAetheryte(step.AetheryteShortcut.Value);
}
}
}
internal sealed record Task(QuestStep? Step, ElementId? ElementId, EAetheryteLocation TargetAetheryte, ushort ExpectedTerritoryId) : ISkippableTask, ITask
{
public override string ToString()
{
return $"UseAetheryte({TargetAetheryte})";
}
}
internal sealed class UseAetheryteShortcut(ILogger<UseAetheryteShortcut> logger, AetheryteFunctions aetheryteFunctions, QuestFunctions questFunctions, IClientState clientState, IChatGui chatGui, ICondition condition, AetheryteData aetheryteData, ExtraConditionUtils extraConditionUtils) : TaskExecutor<Task>()
{
private bool _teleported;
private DateTime _continueAt;
protected override bool Start()
{
return !ShouldSkipTeleport();
}
public override ETaskResult Update()
{
if (DateTime.Now < _continueAt)
{
return ETaskResult.StillRunning;
}
if (!_teleported)
{
_teleported = DoTeleport();
return ETaskResult.StillRunning;
}
if (clientState.TerritoryType == base.Task.ExpectedTerritoryId)
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
private bool ShouldSkipTeleport()
{
ushort territoryType = clientState.TerritoryType;
if (base.Task.Step != null)
{
SkipAetheryteCondition skipAetheryteCondition = base.Task.Step.SkipConditions?.AetheryteShortcutIf ?? new SkipAetheryteCondition();
if (skipAetheryteCondition != null && !skipAetheryteCondition.Never)
{
if (skipAetheryteCondition.InTerritory.Contains(territoryType))
{
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (InTerritory)");
return true;
}
if (skipAetheryteCondition.QuestsCompleted.Count > 0 && skipAetheryteCondition.QuestsCompleted.All(questFunctions.IsQuestComplete))
{
logger.LogInformation("Skipping aetheryte, all prequisite quests are complete");
return true;
}
if (skipAetheryteCondition.QuestsAccepted.Count > 0 && skipAetheryteCondition.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{
logger.LogInformation("Skipping aetheryte, all prequisite quests are accepted");
return true;
}
if (skipAetheryteCondition.AetheryteLocked.HasValue && !aetheryteFunctions.IsAetheryteUnlocked(skipAetheryteCondition.AetheryteLocked.Value))
{
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteLocked)");
return true;
}
if (skipAetheryteCondition.AetheryteUnlocked.HasValue && aetheryteFunctions.IsAetheryteUnlocked(skipAetheryteCondition.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (AetheryteUnlocked)");
return true;
}
if (base.Task.ElementId != null)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(base.Task.ElementId);
if (skipAetheryteCondition.RequiredQuestVariablesNotMet && questProgressInfo != null && !QuestWorkUtils.MatchesRequiredQuestWorkConfig(base.Task.Step.RequiredQuestVariables, questProgressInfo, logger))
{
logger.LogInformation("Skipping aetheryte teleport, as required variables do not match");
return true;
}
}
NearPositionCondition nearPosition = skipAetheryteCondition.NearPosition;
if (nearPosition != null && clientState.TerritoryType == nearPosition.TerritoryId && Vector3.Distance(nearPosition.Position, clientState.LocalPlayer.Position) <= nearPosition.MaximumDistance)
{
logger.LogInformation("Skipping aetheryte shortcut, as we're near the position");
return true;
}
NearPositionCondition notNearPosition = skipAetheryteCondition.NotNearPosition;
if (notNearPosition != null && clientState.TerritoryType == notNearPosition.TerritoryId && notNearPosition.MaximumDistance <= Vector3.Distance(notNearPosition.Position, clientState.LocalPlayer.Position))
{
logger.LogInformation("Skipping aetheryte shortcut, as we're not near the position");
return true;
}
if (skipAetheryteCondition.ExtraCondition.HasValue && skipAetheryteCondition.ExtraCondition != EExtraSkipCondition.None && extraConditionUtils.MatchesExtraCondition(skipAetheryteCondition.ExtraCondition.Value))
{
logger.LogInformation("Skipping step, extra condition {} matches", skipAetheryteCondition.ExtraCondition);
return true;
}
}
if (base.Task.ExpectedTerritoryId == territoryType && !skipAetheryteCondition.Never)
{
if (skipAetheryteCondition != null && skipAetheryteCondition.InSameTerritory)
{
logger.LogInformation("Skipping aetheryte teleport due to SkipCondition (InSameTerritory)");
return true;
}
Vector3 position = clientState.LocalPlayer.Position;
if (base.Task.Step.Position.HasValue && (position - base.Task.Step.Position.Value).Length() < base.Task.Step.CalculateActualStopDistance())
{
logger.LogInformation("Skipping aetheryte teleport, we're near the target");
return true;
}
if (aetheryteData.CalculateDistance(position, territoryType, base.Task.TargetAetheryte) < 20f || (base.Task.Step.AethernetShortcut != null && (aetheryteData.CalculateDistance(position, territoryType, base.Task.Step.AethernetShortcut.From) < 20f || aetheryteData.CalculateDistance(position, territoryType, base.Task.Step.AethernetShortcut.To) < 20f)))
{
logger.LogInformation("Skipping aetheryte teleport");
return true;
}
}
}
return false;
}
private bool DoTeleport()
{
if (!aetheryteFunctions.CanTeleport(base.Task.TargetAetheryte))
{
if (!aetheryteFunctions.IsTeleportUnlocked())
{
throw new TaskException("Teleport is not unlocked, attune to any aetheryte first.");
}
_continueAt = DateTime.Now.AddSeconds(1.0);
logger.LogTrace("Waiting for teleport cooldown...");
return false;
}
_continueAt = DateTime.Now.AddSeconds(8.0);
if (!aetheryteFunctions.IsAetheryteUnlocked(base.Task.TargetAetheryte))
{
chatGui.PrintError($"Aetheryte {base.Task.TargetAetheryte} is not unlocked.", "Questionable", 576);
throw new TaskException("Aetheryte is not unlocked");
}
base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(() => aetheryteFunctions.TeleportAetheryte(base.Task.TargetAetheryte));
if (base.ProgressContext != null)
{
logger.LogInformation("Travelling via aetheryte...");
return true;
}
chatGui.Print("Unable to teleport to aetheryte.", "Questionable", 576);
throw new TaskException("Unable to teleport to aetheryte");
}
public override bool WasInterrupted()
{
if (!condition[ConditionFlag.InCombat])
{
return base.WasInterrupted();
}
return true;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
internal sealed record MoveAwayFromAetheryte(EAetheryteLocation TargetAetheryte) : ITask
{
public override string ToString()
{
return $"MoveAway({TargetAetheryte})";
}
}
internal sealed class MoveAwayFromAetheryteExecutor(MoveExecutor moveExecutor, AetheryteData aetheryteData, IClientState clientState) : TaskExecutor<MoveAwayFromAetheryte>()
{
private static readonly Dictionary<EAetheryteLocation, List<Vector3>> AetherytesToMoveFrom;
public static bool AppliesTo(EAetheryteLocation location)
{
return AetherytesToMoveFrom.ContainsKey(location);
}
protected override bool Start()
{
Vector3 playerPosition = clientState.LocalPlayer.Position;
if (aetheryteData.CalculateDistance(playerPosition, clientState.TerritoryType, base.Task.TargetAetheryte) >= 20f)
{
return false;
}
Vector3 destination = AetherytesToMoveFrom[base.Task.TargetAetheryte].MinBy((Vector3 x) => Vector3.Distance(x, playerPosition));
MoveTask task = new MoveTask(aetheryteData.TerritoryIds[base.Task.TargetAetheryte], destination, false, 0.25f, null, DisableNavmesh: true, null, Fly: false, Land: false, IgnoreDistanceToObject: false, RestartNavigation: false);
return moveExecutor.Start(task);
}
public override ETaskResult Update()
{
return moveExecutor.Update();
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
static MoveAwayFromAetheryteExecutor()
{
Dictionary<EAetheryteLocation, List<Vector3>> dictionary = new Dictionary<EAetheryteLocation, List<Vector3>>();
int num = 4;
List<Vector3> list = new List<Vector3>(num);
CollectionsMarshal.SetCount(list, num);
Span<Vector3> span = CollectionsMarshal.AsSpan(list);
int num2 = 0;
span[num2] = new Vector3(0f, 8.8f, 15.5f);
num2++;
span[num2] = new Vector3(0f, 8.8f, -15.5f);
num2++;
span[num2] = new Vector3(15.5f, 8.8f, 0f);
num2++;
span[num2] = new Vector3(-15.5f, 8.8f, 0f);
dictionary.Add(EAetheryteLocation.SolutionNine, list);
AetherytesToMoveFrom = dictionary;
}
}
}

View file

@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameData;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class Craft
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Questionable.Model.Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Craft)
{
return Array.Empty<ITask>();
}
ArgumentNullException.ThrowIfNull(step.ItemId, "step.ItemId");
ArgumentNullException.ThrowIfNull(step.ItemCount, "step.ItemCount");
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2]
{
new Questionable.Controller.Steps.Common.Mount.UnmountTask(),
new CraftTask(step.ItemId.Value, step.ItemCount.Value)
});
}
}
internal sealed record CraftTask(uint ItemId, int ItemCount) : ITask
{
public override string ToString()
{
return $"Craft {ItemCount}x {ItemId} (with Artisan)";
}
}
internal sealed class DoCraft(IDataManager dataManager, IClientState clientState, ArtisanIpc artisanIpc, ILogger<DoCraft> logger) : TaskExecutor<CraftTask>()
{
protected override bool Start()
{
if (HasRequestedItems())
{
logger.LogInformation("Already own {ItemCount}x {ItemId}", base.Task.ItemCount, base.Task.ItemId);
return false;
}
RecipeLookup? rowOrDefault = dataManager.GetExcelSheet<RecipeLookup>().GetRowOrDefault(base.Task.ItemId);
if (!rowOrDefault.HasValue)
{
throw new TaskException($"Item {base.Task.ItemId} is not craftable");
}
uint num = (EClassJob)clientState.LocalPlayer.ClassJob.RowId switch
{
EClassJob.Carpenter => rowOrDefault.Value.CRP.RowId,
EClassJob.Blacksmith => rowOrDefault.Value.BSM.RowId,
EClassJob.Armorer => rowOrDefault.Value.ARM.RowId,
EClassJob.Goldsmith => rowOrDefault.Value.GSM.RowId,
EClassJob.Leatherworker => rowOrDefault.Value.LTW.RowId,
EClassJob.Weaver => rowOrDefault.Value.WVR.RowId,
EClassJob.Alchemist => rowOrDefault.Value.ALC.RowId,
EClassJob.Culinarian => rowOrDefault.Value.CUL.RowId,
_ => 0u,
};
if (num == 0)
{
num = new uint[8]
{
rowOrDefault.Value.CRP.RowId,
rowOrDefault.Value.BSM.RowId,
rowOrDefault.Value.ARM.RowId,
rowOrDefault.Value.GSM.RowId,
rowOrDefault.Value.LTW.RowId,
rowOrDefault.Value.WVR.RowId,
rowOrDefault.Value.ALC.RowId,
rowOrDefault.Value.WVR.RowId
}.FirstOrDefault((uint x) => x != 0);
}
if (num == 0)
{
throw new TaskException($"Unable to determine recipe for item {base.Task.ItemId}");
}
int num2 = base.Task.ItemCount - GetOwnedItemCount();
logger.LogInformation("Starting craft for item {ItemId} with recipe {RecipeId} for {RemainingItemCount} items", base.Task.ItemId, num, num2);
if (!artisanIpc.CraftItem((ushort)num, num2))
{
throw new TaskException($"Failed to start Artisan craft for recipe {num}");
}
return true;
}
public unsafe override ETaskResult Update()
{
if (HasRequestedItems() && !artisanIpc.IsCrafting())
{
AgentRecipeNote* ptr = AgentRecipeNote.Instance();
if (ptr != null && ptr->IsAgentActive())
{
uint addonId = ptr->GetAddonId();
if (addonId == 0)
{
return ETaskResult.StillRunning;
}
AtkUnitBase* addonById = AtkStage.Instance()->RaptureAtkUnitManager->GetAddonById((ushort)addonId);
if (addonById != null)
{
logger.LogInformation("Closing crafting window");
addonById->FireCallbackInt(-1);
return ETaskResult.TaskComplete;
}
}
}
return ETaskResult.StillRunning;
}
private bool HasRequestedItems()
{
return GetOwnedItemCount() >= base.Task.ItemCount;
}
private unsafe int GetOwnedItemCount()
{
InventoryManager* ptr = InventoryManager.Instance();
return ptr->GetInventoryItemCount(base.Task.ItemId, isHq: false, checkEquipped: false, checkArmory: true, 0) + ptr->GetInventoryItemCount(base.Task.ItemId, isHq: true, checkEquipped: false, checkArmory: true, 0);
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using System.Numerics;
using Dalamud.Plugin.Services;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal sealed class ExtraConditionUtils
{
private readonly IClientState _clientState;
public ExtraConditionUtils(IClientState clientState)
{
_clientState = clientState;
}
public bool MatchesExtraCondition(EExtraSkipCondition skipCondition)
{
Vector3? vector = _clientState.LocalPlayer?.Position;
if (vector.HasValue && _clientState.TerritoryType != 0)
{
return MatchesExtraCondition(skipCondition, vector.Value, _clientState.TerritoryType);
}
return false;
}
public static bool MatchesExtraCondition(EExtraSkipCondition skipCondition, Vector3 position, ushort territoryType)
{
return skipCondition switch
{
EExtraSkipCondition.WakingSandsMainArea => territoryType == 212 && position.X < 24f,
EExtraSkipCondition.WakingSandsSolar => territoryType == 212 && position.X >= 24f,
EExtraSkipCondition.RisingStonesSolar => territoryType == 351 && position.Z <= -28f,
EExtraSkipCondition.RoguesGuild => territoryType == 129 && position.Y <= -115f,
EExtraSkipCondition.NotRoguesGuild => territoryType == 129 && position.Y > -115f,
EExtraSkipCondition.DockStorehouse => territoryType == 137 && position.Y <= -20f,
_ => throw new ArgumentOutOfRangeException("skipCondition", skipCondition, null),
};
}
}

View file

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib.GameData;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Gathering;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class Gather
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.Gather)
{
yield break;
}
foreach (GatheredItem item in step.ItemsToGather)
{
yield return new DelayedGatheringTask(item, quest, sequence.Sequence);
}
}
}
internal sealed record DelayedGatheringTask(GatheredItem GatheredItem, Quest Quest, byte Sequence) : ITask
{
public override string ToString()
{
return $"Gathering(pending for {GatheredItem.ItemId})";
}
}
internal sealed class DelayedGatheringExecutor(GatheringPointRegistry gatheringPointRegistry, TerritoryData territoryData, IClientState clientState, IServiceProvider serviceProvider, ILogger<DelayedGatheringExecutor> logger) : TaskExecutor<DelayedGatheringTask>(), IExtraTaskCreator, ITaskExecutor
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
return ETaskResult.CreateNewTasks;
}
public IEnumerable<ITask> CreateExtraTasks()
{
EClassJob rowId = (EClassJob)clientState.LocalPlayer.ClassJob.RowId;
if (!gatheringPointRegistry.TryGetGatheringPointId(base.Task.GatheredItem.ItemId, rowId, out GatheringPointId gatheringPointId))
{
throw new TaskException($"No gathering point found for item {base.Task.GatheredItem.ItemId}");
}
if (!gatheringPointRegistry.TryGetGatheringPoint(gatheringPointId, out GatheringRoot gatheringRoot))
{
throw new TaskException($"No path found for gathering point {gatheringPointId}");
}
if (HasRequiredItems(base.Task.GatheredItem))
{
yield break;
}
switch (rowId)
{
case EClassJob.Miner:
yield return new Questionable.Controller.Steps.Interactions.Action.TriggerStatusIfMissing(EStatus.Prospect, EAction.Prospect);
break;
case EClassJob.Botanist:
yield return new Questionable.Controller.Steps.Interactions.Action.TriggerStatusIfMissing(EStatus.Triangulate, EAction.Triangulate);
break;
}
using (logger.BeginScope("Gathering(inner)"))
{
QuestSequence gatheringSequence = new QuestSequence
{
Sequence = 0,
Steps = gatheringRoot.Steps
};
foreach (QuestStep step in gatheringSequence.Steps)
{
foreach (ITask item in serviceProvider.GetRequiredService<TaskCreator>().CreateTasks(base.Task.Quest, base.Task.Sequence, gatheringSequence, step))
{
if (item is WaitAtEnd.NextStep)
{
yield return new SkipMarker();
}
else
{
yield return item;
}
}
}
}
ushort territoryId = gatheringRoot.Steps.Last().TerritoryId;
yield return new WaitCondition.Task(() => clientState.TerritoryType == territoryId, "Wait(territory: " + territoryData.GetNameAndId(territoryId) + ")");
yield return new WaitNavmesh.Task();
yield return new GatheringTask(gatheringPointId, base.Task.GatheredItem);
yield return new WaitAtEnd.WaitDelay();
}
private unsafe bool HasRequiredItems(GatheredItem itemToGather)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr != null)
{
return ptr->GetInventoryItemCount(itemToGather.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)itemToGather.Collectability) >= itemToGather.ItemCount;
}
return false;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record GatheringTask(GatheringPointId gatheringPointId, GatheredItem gatheredItem) : ITask
{
public override string ToString()
{
if (gatheredItem.Collectability != 0)
{
return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId} {SeIconChar.Collectible.ToIconString()} {gatheredItem.Collectability})";
}
return $"Gather({gatheredItem.ItemCount}x {gatheredItem.ItemId})";
}
}
internal sealed class StartGathering(GatheringController gatheringController) : TaskExecutor<GatheringTask>(), IToastAware, ITaskExecutor
{
protected override bool Start()
{
return gatheringController.Start(new GatheringController.GatheringRequest(base.Task.gatheringPointId, base.Task.gatheredItem.ItemId, base.Task.gatheredItem.AlternativeItemId, base.Task.gatheredItem.ItemCount, base.Task.gatheredItem.Collectability));
}
public override ETaskResult Update()
{
if (gatheringController.Update() == GatheringController.EStatus.Complete)
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
public bool OnErrorToast(SeString message)
{
bool isHandled = false;
gatheringController.OnErrorToast(ref message, ref isHandled);
return isHandled;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed class SkipMarker : ITask
{
public override string ToString()
{
return "Gather/SkipMarker";
}
}
internal sealed class DoSkip : TaskExecutor<SkipMarker>
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class RedeemRewardItems
{
internal sealed class Factory(QuestData questData) : ITaskFactory
{
public unsafe IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.AcceptQuest)
{
return Array.Empty<ITask>();
}
List<ITask> list = new List<ITask>();
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return list;
}
foreach (ItemReward redeemableItem in questData.RedeemableItems)
{
if (ptr->GetInventoryItemCount(redeemableItem.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0 && !redeemableItem.IsUnlocked())
{
list.Add(new Task(redeemableItem));
}
}
return list;
}
}
internal sealed record Task(ItemReward ItemReward) : ITask
{
public override string ToString()
{
return "TryRedeem(" + ItemReward.Name + ")";
}
}
internal sealed class Executor(GameFunctions gameFunctions, ICondition condition) : TaskExecutor<Task>()
{
private static readonly TimeSpan MinimumCastTime = TimeSpan.FromSeconds(4L);
private DateTime _continueAt;
protected override bool Start()
{
if (condition[ConditionFlag.Mounted])
{
return false;
}
TimeSpan timeSpan = base.Task.ItemReward.CastTime;
if (timeSpan < MinimumCastTime)
{
timeSpan = MinimumCastTime;
}
_continueAt = DateTime.Now.Add(timeSpan).AddSeconds(3.0);
return gameFunctions.UseItem(base.Task.ItemReward.ItemId);
}
public override ETaskResult Update()
{
if (condition[ConditionFlag.Casting])
{
return ETaskResult.StillRunning;
}
if (!(DateTime.Now <= _continueAt))
{
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return true;
}
}
}

View file

@ -0,0 +1,457 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
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 LLib.GameData;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class SkipCondition
{
internal sealed class Factory(Configuration configuration) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
SkipStepConditions skipStepConditions = step.SkipConditions?.StepIf;
if (skipStepConditions != null && skipStepConditions.Never)
{
return null;
}
if ((skipStepConditions == null || !skipStepConditions.HasSkipConditions()) && !QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags) && step.RequiredQuestVariables.Count == 0 && !step.TaxiStandId.HasValue && step.PickUpQuestId == null && step.NextQuestId == null && step.RequiredCurrentJob.Count == 0 && step.RequiredQuestAcceptedJob.Count == 0 && (step.InteractionType != EInteractionType.AttuneAetherCurrent || !configuration.Advanced.SkipAetherCurrents))
{
return null;
}
return new SkipTask(step, skipStepConditions ?? new SkipStepConditions(), quest.Id);
}
}
internal sealed record SkipTask(QuestStep Step, SkipStepConditions SkipConditions, ElementId ElementId) : ITask
{
public override string ToString()
{
return "CheckSkip";
}
}
internal sealed class CheckSkip(ILogger<CheckSkip> logger, Configuration configuration, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, QuestFunctions questFunctions, IClientState clientState, ICondition condition, ExtraConditionUtils extraConditionUtils, ClassJobUtils classJobUtils) : TaskExecutor<SkipTask>()
{
protected override bool Start()
{
SkipStepConditions skipConditions = base.Task.SkipConditions;
QuestStep step = base.Task.Step;
ElementId elementId = base.Task.ElementId;
logger.LogInformation("Checking skip conditions; {ConfiguredConditions}", string.Join(",", skipConditions));
if (CheckFlyingCondition(step, skipConditions))
{
return true;
}
if (CheckUnlockedMountCondition(skipConditions))
{
return true;
}
if (CheckDivingCondition(skipConditions))
{
return true;
}
if (CheckTerritoryCondition(skipConditions))
{
return true;
}
if (CheckQuestConditions(skipConditions))
{
return true;
}
if (CheckTargetableCondition(step, skipConditions))
{
return true;
}
if (CheckNameplateCondition(step, skipConditions))
{
return true;
}
if (CheckItemCondition(step, skipConditions))
{
return true;
}
if (CheckAetheryteCondition(step, skipConditions))
{
return true;
}
if (CheckAetherCurrentCondition(step))
{
return true;
}
if (CheckQuestWorkConditions(elementId, step))
{
return true;
}
if (CheckJobCondition(elementId, step))
{
return true;
}
if (CheckPositionCondition(skipConditions))
{
return true;
}
if (skipConditions.ExtraCondition.HasValue && skipConditions.ExtraCondition != EExtraSkipCondition.None && extraConditionUtils.MatchesExtraCondition(skipConditions.ExtraCondition.Value))
{
logger.LogInformation("Skipping step, extra condition {} matches", skipConditions.ExtraCondition);
return true;
}
if (CheckPickUpTurnInQuestIds(step))
{
return true;
}
if (CheckTaxiStandUnlocked(step))
{
return true;
}
return false;
}
private bool CheckFlyingCondition(QuestStep step, SkipStepConditions skipConditions)
{
if (skipConditions.Flying == ELockedSkipCondition.Unlocked && gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{
logger.LogInformation("Skipping step, as flying is unlocked");
return true;
}
if (skipConditions.Flying == ELockedSkipCondition.Locked && !gameFunctions.IsFlyingUnlocked(step.TerritoryId))
{
logger.LogInformation("Skipping step, as flying is locked");
return true;
}
return false;
}
private unsafe bool CheckUnlockedMountCondition(SkipStepConditions skipConditions)
{
if (skipConditions.Chocobo == ELockedSkipCondition.Unlocked && PlayerState.Instance()->IsMountUnlocked(1u))
{
logger.LogInformation("Skipping step, as chocobo is unlocked");
return true;
}
return false;
}
private bool CheckTerritoryCondition(SkipStepConditions skipConditions)
{
if (skipConditions.InTerritory.Count > 0 && skipConditions.InTerritory.Contains(clientState.TerritoryType))
{
logger.LogInformation("Skipping step, as in a skip.InTerritory");
return true;
}
if (skipConditions.NotInTerritory.Count > 0 && !skipConditions.NotInTerritory.Contains(clientState.TerritoryType))
{
logger.LogInformation("Skipping step, as not in a skip.NotInTerritory");
return true;
}
return false;
}
private bool CheckDivingCondition(SkipStepConditions skipConditions)
{
if (skipConditions.Diving == true && condition[ConditionFlag.Diving])
{
logger.LogInformation("Skipping step, as you're currently diving underwater");
return true;
}
if (skipConditions.Diving == false && !condition[ConditionFlag.Diving])
{
logger.LogInformation("Skipping step, as you're not currently diving underwater");
return true;
}
return false;
}
private bool CheckQuestConditions(SkipStepConditions skipConditions)
{
if (skipConditions.QuestsCompleted.Count > 0 && skipConditions.QuestsCompleted.All(questFunctions.IsQuestComplete))
{
logger.LogInformation("Skipping step, all prequisite quests are complete");
return true;
}
if (skipConditions.QuestsAccepted.Count > 0 && skipConditions.QuestsAccepted.All(questFunctions.IsQuestAccepted))
{
logger.LogInformation("Skipping step, all prequisite quests are accepted");
return true;
}
return false;
}
private bool CheckTargetableCondition(QuestStep step, SkipStepConditions skipConditions)
{
if (skipConditions.NotTargetable && step != null && step.DataId.HasValue)
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(step.DataId.Value);
if (gameObject == null)
{
if ((step.Position.GetValueOrDefault() - clientState.LocalPlayer.Position).Length() < 100f)
{
logger.LogInformation("Skipping step, object is not nearby (but we are)");
return true;
}
}
else if (!gameObject.IsTargetable)
{
logger.LogInformation("Skipping step, object is not targetable");
return true;
}
}
return false;
}
private unsafe bool CheckNameplateCondition(QuestStep step, SkipStepConditions skipConditions)
{
if (skipConditions.NotNamePlateIconId.Count > 0 && step != null && step.DataId.HasValue)
{
IGameObject gameObject = gameFunctions.FindObjectByDataId(step.DataId.Value);
if (gameObject != null)
{
GameObject* address = (GameObject*)gameObject.Address;
if (!skipConditions.NotNamePlateIconId.Contains(address->NamePlateIconId))
{
logger.LogInformation("Skipping step, object has icon id {IconId}", address->NamePlateIconId);
return true;
}
}
}
return false;
}
private unsafe bool CheckItemCondition(QuestStep step, SkipStepConditions skipConditions)
{
SkipItemConditions item = skipConditions.Item;
if (item != null && item.NotInInventory && step != null && step.ItemId.HasValue)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr->GetInventoryItemCount(step.ItemId.Value, isHq: false, checkEquipped: true, checkArmory: true, 0) == 0 && ptr->GetInventoryItemCount(step.ItemId.Value, isHq: true, checkEquipped: true, checkArmory: true, 0) == 0)
{
logger.LogInformation("Skipping step, no item with itemId {ItemId} in inventory", step.ItemId.Value);
return true;
}
}
return false;
}
private bool CheckAetheryteCondition(QuestStep step, SkipStepConditions skipConditions)
{
if (step != null)
{
EAetheryteLocation? aetheryte = step.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault = aetheryte.GetValueOrDefault();
if (step.InteractionType == EInteractionType.AttuneAetheryte && aetheryteFunctions.IsAetheryteUnlocked(valueOrDefault))
{
logger.LogInformation("Skipping step, as aetheryte is unlocked");
return true;
}
}
}
if (step != null)
{
EAetheryteLocation? aetheryte = step.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault2 = aetheryte.GetValueOrDefault();
if (step.InteractionType == EInteractionType.AttuneAethernetShard && aetheryteFunctions.IsAetheryteUnlocked(valueOrDefault2))
{
logger.LogInformation("Skipping step, as aethernet shard is unlocked");
return true;
}
}
}
if (step != null)
{
EAetheryteLocation? aetheryte = step.Aetheryte;
if (aetheryte.HasValue)
{
EAetheryteLocation valueOrDefault3 = aetheryte.GetValueOrDefault();
if (step.InteractionType == EInteractionType.RegisterFreeOrFavoredAetheryte && aetheryteFunctions.CanRegisterFreeOrFavoriteAetheryte(valueOrDefault3) == AetheryteRegistrationResult.NotPossible)
{
logger.LogInformation("Skipping step, already registered all possible free or favored aetherytes");
return true;
}
}
}
if (skipConditions.AetheryteLocked.HasValue && !aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteLocked.Value))
{
logger.LogInformation("Skipping step, as aetheryte is locked");
return true;
}
if (skipConditions.AetheryteUnlocked.HasValue && aetheryteFunctions.IsAetheryteUnlocked(skipConditions.AetheryteUnlocked.Value))
{
logger.LogInformation("Skipping step, as aetheryte is unlocked");
return true;
}
return false;
}
private bool CheckAetherCurrentCondition(QuestStep step)
{
if (step != null && step.DataId.HasValue && step.InteractionType == EInteractionType.AttuneAetherCurrent && gameFunctions.IsAetherCurrentUnlocked(step.DataId.Value))
{
logger.LogInformation("Skipping step, as current is unlocked");
return true;
}
if (step != null && step.InteractionType == EInteractionType.AttuneAetherCurrent && configuration.Advanced.SkipAetherCurrents)
{
logger.LogInformation("Skipping step, as aether currents should be skipped");
return true;
}
return false;
}
private bool CheckQuestWorkConditions(ElementId elementId, QuestStep step)
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(elementId);
if (questProgressInfo != null)
{
if (QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags) && QuestWorkUtils.MatchesQuestWork(step.CompletionQuestVariablesFlags, questProgressInfo))
{
logger.LogInformation("Skipping step, as quest variables match (step is complete)");
return true;
}
if (step != null)
{
SkipConditions skipConditions = step.SkipConditions;
if (skipConditions != null)
{
SkipStepConditions stepIf = skipConditions.StepIf;
if (stepIf != null && QuestWorkUtils.MatchesQuestWork(stepIf.CompletionQuestVariablesFlags, questProgressInfo))
{
logger.LogInformation("Skipping step, as quest variables match (step can be skipped)");
return true;
}
}
}
if (step != null)
{
List<List<QuestWorkValue>> requiredQuestVariables = step.RequiredQuestVariables;
if (requiredQuestVariables != null && !QuestWorkUtils.MatchesRequiredQuestWorkConfig(requiredQuestVariables, questProgressInfo, logger))
{
logger.LogInformation("Skipping step, as required variables do not match");
return true;
}
}
if (step != null)
{
List<EExtendedClassJob> requiredQuestAcceptedJob = step.RequiredQuestAcceptedJob;
if (requiredQuestAcceptedJob != null && requiredQuestAcceptedJob.Count > 0)
{
List<EClassJob> list = step.RequiredQuestAcceptedJob.SelectMany((EExtendedClassJob x) => classJobUtils.AsIndividualJobs(x, elementId)).ToList();
EClassJob classJob = questProgressInfo.ClassJob;
logger.LogInformation("Checking quest job {QuestJob} against {ExpectedJobs}", classJob, string.Join(",", list));
if (classJob != EClassJob.Adventurer && !list.Contains(classJob))
{
logger.LogInformation("Skipping step, as quest was accepted on a different job");
return true;
}
}
}
}
return false;
}
private bool CheckJobCondition(ElementId elementId, QuestStep step)
{
if (step != null)
{
List<EExtendedClassJob> requiredCurrentJob = step.RequiredCurrentJob;
if (requiredCurrentJob != null && requiredCurrentJob.Count > 0)
{
List<EClassJob> list = step.RequiredCurrentJob.SelectMany((EExtendedClassJob x) => classJobUtils.AsIndividualJobs(x, elementId)).ToList();
EClassJob rowId = (EClassJob)clientState.LocalPlayer.ClassJob.RowId;
logger.LogInformation("Checking current job {CurrentJob} against {ExpectedJobs}", rowId, string.Join(",", list));
if (!list.Contains(rowId))
{
logger.LogInformation("Skipping step, as step requires a different job");
return true;
}
}
}
return false;
}
private bool CheckPositionCondition(SkipStepConditions skipConditions)
{
NearPositionCondition nearPosition = skipConditions.NearPosition;
if (nearPosition != null && clientState.TerritoryType == nearPosition.TerritoryId && Vector3.Distance(nearPosition.Position, clientState.LocalPlayer.Position) <= nearPosition.MaximumDistance)
{
logger.LogInformation("Skipping step, as we're near the position");
return true;
}
NearPositionCondition notNearPosition = skipConditions.NotNearPosition;
if (notNearPosition != null && clientState.TerritoryType == notNearPosition.TerritoryId && notNearPosition.MaximumDistance <= Vector3.Distance(notNearPosition.Position, clientState.LocalPlayer.Position))
{
logger.LogInformation("Skipping step, as we're not near the position");
return true;
}
return false;
}
private bool CheckPickUpTurnInQuestIds(QuestStep step)
{
if (step.PickUpQuestId != null && questFunctions.IsQuestAcceptedOrComplete(step.PickUpQuestId))
{
logger.LogInformation("Skipping step, as we have already picked up the relevant quest");
return true;
}
if (step.TurnInQuestId != null && questFunctions.IsQuestComplete(step.TurnInQuestId))
{
logger.LogInformation("Skipping step, as we have already completed the relevant quest");
return true;
}
if (step.PickUpQuestId != null && configuration.Advanced.SkipAetherCurrents && QuestData.AetherCurrentQuests.Contains(step.PickUpQuestId))
{
logger.LogInformation("Skipping step, as aether current quests should be skipped");
return true;
}
if (step.PickUpQuestId != null && configuration.Advanced.SkipARealmRebornHardModePrimals && QuestData.HardModePrimals.Contains(step.PickUpQuestId))
{
logger.LogInformation("Skipping step, as hard mode primal quests should be skipped");
return true;
}
return false;
}
private unsafe bool CheckTaxiStandUnlocked(QuestStep step)
{
UIState* ptr = UIState.Instance();
byte? taxiStandId = step.TaxiStandId;
if (taxiStandId.HasValue)
{
byte valueOrDefault = taxiStandId.GetValueOrDefault();
if (ptr->IsChocoboTaxiStandUnlocked(valueOrDefault))
{
logger.LogInformation("Skipping step, as taxi stand {TaxiStandId} is unlocked", valueOrDefault);
return true;
}
}
return false;
}
public override ETaskResult Update()
{
return ETaskResult.SkipRemainingTasksForStep;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class StepDisabled
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (!step.Disabled)
{
return null;
}
return new SkipRemainingTasks();
}
}
internal sealed class SkipRemainingTasks : ITask
{
public override string ToString()
{
return "StepDisabled";
}
}
internal sealed class SkipDisabledStepsExecutor(ILogger<SkipRemainingTasks> logger) : TaskExecutor<SkipRemainingTasks>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
logger.LogInformation("Skipping step, as it is disabled");
return ETaskResult.SkipRemainingTasksForStep;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,68 @@
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using LLib.GameData;
using Questionable.Controller.Steps.Common;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class SwitchClassJob
{
internal sealed class Factory(ClassJobUtils classJobUtils) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SwitchClass)
{
return null;
}
return new Task(classJobUtils.AsIndividualJobs(step.TargetClass, quest.Id).Single());
}
}
internal sealed record Task(EClassJob ClassJob) : ITask
{
public override string ToString()
{
return $"SwitchJob({ClassJob})";
}
}
internal sealed class SwitchClassJobExecutor(IClientState clientState) : AbstractDelayedTaskExecutor<Task>()
{
protected unsafe override bool StartInternal()
{
if (clientState.LocalPlayer.ClassJob.RowId == (uint)base.Task.ClassJob)
{
return false;
}
RaptureGearsetModule* ptr = RaptureGearsetModule.Instance();
if (ptr != null)
{
for (int i = 0; i < 100; i++)
{
RaptureGearsetModule.GearsetEntry* gearset = ptr->GetGearset(i);
if (gearset->ClassJob == (byte)base.Task.ClassJob)
{
ptr->EquipGearset(gearset->Id, 0);
return true;
}
}
}
throw new TaskException($"No gearset found for {base.Task.ClassJob}");
}
protected override ETaskResult UpdateInternal()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,405 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils;
using Questionable.Data;
using Questionable.External;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class WaitAtEnd
{
internal sealed class Factory(IClientState clientState, ICondition condition, TerritoryData territoryData, AutoDutyIpc autoDutyIpc, BossModIpc bossModIpc) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.CompletionQuestVariablesFlags.Count == 6 && QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags))
{
WaitForCompletionFlags waitForCompletionFlags = new WaitForCompletionFlags((QuestId)quest.Id, step);
WaitDelay waitDelay = new WaitDelay();
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
waitForCompletionFlags,
waitDelay,
Next(quest, sequence)
});
}
ITask task;
switch (step.InteractionType)
{
case EInteractionType.Combat:
{
if (step.EnemySpawnType == EEnemySpawnType.FinishCombatIfAny)
{
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(Next(quest, sequence));
}
WaitCondition.Task task2 = new WaitCondition.Task(() => !condition[ConditionFlag.InCombat], "Wait(not in combat)");
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[4]
{
new WaitDelay(),
task2,
new WaitDelay(),
Next(quest, sequence)
});
}
case EInteractionType.WaitForManualProgress:
case EInteractionType.Snipe:
case EInteractionType.Instruction:
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new WaitNextStepOrSequence());
case EInteractionType.Duty:
if (autoDutyIpc.IsConfiguredToRunContent(step.DutyOptions))
{
break;
}
goto IL_019d;
case EInteractionType.SinglePlayerDuty:
if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{
break;
}
goto IL_019d;
case EInteractionType.WalkTo:
case EInteractionType.Jump:
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(Next(quest, sequence));
case EInteractionType.WaitForObjectAtPosition:
ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId");
ArgumentNullException.ThrowIfNull(step.Position, "step.Position");
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
new WaitObjectAtPosition(step.DataId.Value, step.Position.Value, step.NpcWaitDistance ?? 0.5f),
new WaitDelay(),
Next(quest, sequence)
});
case EInteractionType.Interact:
if (!step.TargetTerritoryId.HasValue)
{
break;
}
goto IL_0284;
case EInteractionType.UseItem:
if (!step.TargetTerritoryId.HasValue)
{
break;
}
goto IL_0284;
case EInteractionType.AcceptQuest:
{
WaitQuestAccepted waitQuestAccepted = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id);
WaitDelay waitDelay3 = new WaitDelay();
if (step.PickUpQuestId != null)
{
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
waitQuestAccepted,
waitDelay3,
Next(quest, sequence)
});
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { waitQuestAccepted, waitDelay3 });
}
case EInteractionType.CompleteQuest:
{
WaitQuestCompleted waitQuestCompleted = new WaitQuestCompleted(step.TurnInQuestId ?? quest.Id);
WaitDelay waitDelay2 = new WaitDelay();
if (step.TurnInQuestId != null)
{
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
waitQuestCompleted,
waitDelay2,
Next(quest, sequence)
});
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { waitQuestCompleted, waitDelay2 });
}
IL_019d:
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new EndAutomation());
IL_0284:
if (step.TerritoryId != step.TargetTerritoryId)
{
task = new WaitCondition.Task(() => clientState.TerritoryType == step.TargetTerritoryId, "Wait(tp to territory: " + territoryData.GetNameAndId(step.TargetTerritoryId.Value) + ")");
}
else
{
Vector3 lastPosition = step.Position ?? clientState.LocalPlayer?.Position ?? Vector3.Zero;
task = new WaitCondition.Task(delegate
{
Vector3? vector = clientState.LocalPlayer?.Position;
return vector.HasValue && (lastPosition - vector.Value).Length() > 2f;
}, "Wait(tp away from " + lastPosition.ToString("G", CultureInfo.InvariantCulture) + ")");
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
{
task,
new WaitDelay(),
Next(quest, sequence)
});
}
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2]
{
new WaitDelay(),
Next(quest, sequence)
});
}
private static NextStep Next(Quest quest, QuestSequence sequence)
{
return new NextStep(quest.Id, sequence.Sequence);
}
}
internal sealed record WaitDelay(TimeSpan Delay) : ITask
{
public WaitDelay()
: this(TimeSpan.FromSeconds(1L))
{
}
public bool ShouldRedoOnInterrupt()
{
return true;
}
public override string ToString()
{
return $"Wait(seconds: {Delay.TotalSeconds})";
}
}
internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
{
protected override bool StartInternal()
{
base.Delay = base.Task.Delay;
return true;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed class WaitNextStepOrSequence : ITask
{
public override string ToString()
{
return "Wait(next step or sequence)";
}
}
internal sealed class WaitNextStepOrSequenceExecutor : TaskExecutor<WaitNextStepOrSequence>
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
return ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitForCompletionFlags(QuestId Quest, QuestStep Step) : ITask
{
public override string ToString()
{
return "Wait(QW: " + string.Join(", ", Step.CompletionQuestVariablesFlags.Select((QuestWorkValue x) => x?.ToString() ?? "-")) + ")";
}
}
internal sealed class WaitForCompletionFlagsExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitForCompletionFlags>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(base.Task.Quest);
if (questProgressInfo == null || !QuestWorkUtils.MatchesQuestWork(base.Task.Step.CompletionQuestVariablesFlags, questProgressInfo))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitObjectAtPosition(uint DataId, Vector3 Destination, float Distance) : ITask
{
public override string ToString()
{
return $"WaitObj({DataId} at {Destination.ToString("G", CultureInfo.InvariantCulture)} < {Distance})";
}
}
internal sealed class WaitObjectAtPositionExecutor(GameFunctions gameFunctions) : TaskExecutor<WaitObjectAtPosition>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (!gameFunctions.IsObjectAtPosition(base.Task.DataId, base.Task.Destination, base.Task.Distance))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitQuestAccepted(ElementId ElementId) : ITask
{
public override string ToString()
{
return $"WaitQuestAccepted({ElementId})";
}
}
internal sealed class WaitQuestAcceptedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestAccepted>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (!questFunctions.IsQuestAccepted(base.Task.ElementId))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record WaitQuestCompleted(ElementId ElementId) : ITask
{
public override string ToString()
{
return $"WaitQuestComplete({ElementId})";
}
}
internal sealed class WaitQuestCompletedExecutor(QuestFunctions questFunctions) : TaskExecutor<WaitQuestCompleted>()
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
if (!questFunctions.IsQuestComplete(base.Task.ElementId))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask, ITask
{
public override string ToString()
{
return "NextStep";
}
}
internal sealed class NextStepExecutor : TaskExecutor<NextStep>
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
return ETaskResult.NextStep;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
internal sealed class EndAutomation : ILastTask, ITask
{
public ElementId ElementId
{
get
{
throw new InvalidOperationException();
}
}
public int Sequence
{
get
{
throw new InvalidOperationException();
}
}
public override string ToString()
{
return "EndAutomation";
}
}
internal sealed class EndAutomationExecutor : TaskExecutor<EndAutomation>
{
protected override bool Start()
{
return true;
}
public override ETaskResult Update()
{
return ETaskResult.End;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,43 @@
using System;
using Questionable.Controller.Steps.Common;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Shared;
internal static class WaitAtStart
{
internal sealed class Factory : SimpleTaskFactory
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (!step.DelaySecondsAtStart.HasValue)
{
return null;
}
return new WaitDelay(TimeSpan.FromSeconds(step.DelaySecondsAtStart.Value));
}
}
internal sealed record WaitDelay(TimeSpan Delay) : ITask
{
public override string ToString()
{
return $"Wait[S](seconds: {Delay.TotalSeconds})";
}
}
internal sealed class WaitDelayExecutor : AbstractDelayedTaskExecutor<WaitDelay>
{
protected override bool StartInternal()
{
base.Delay = base.Task.Delay;
return true;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,11 @@
namespace Questionable.Controller.Steps;
internal enum ETaskResult
{
StillRunning,
TaskComplete,
SkipRemainingTasksForStep,
CreateNewTasks,
NextStep,
End
}

View file

@ -0,0 +1,8 @@
using Dalamud.Game.ClientState.Conditions;
namespace Questionable.Controller.Steps;
internal interface IConditionChangeAware : ITaskExecutor
{
void OnConditionChange(ConditionFlag flag, bool value);
}

View file

@ -0,0 +1,6 @@
namespace Questionable.Controller.Steps;
internal interface IDebugStateProvider : ITaskExecutor
{
string? GetDebugState();
}

View file

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Questionable.Controller.Steps;
internal interface IExtraTaskCreator : ITaskExecutor
{
IEnumerable<ITask> CreateExtraTasks();
}

View file

@ -0,0 +1,10 @@
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps;
internal interface ILastTask : ITask
{
ElementId ElementId { get; }
int Sequence { get; }
}

View file

@ -0,0 +1,6 @@
namespace Questionable.Controller.Steps;
internal interface IRevisitAware
{
void OnRevisit();
}

View file

@ -0,0 +1,5 @@
namespace Questionable.Controller.Steps;
internal interface ISkippableTask : ITask
{
}

View file

@ -0,0 +1,6 @@
namespace Questionable.Controller.Steps;
internal interface IStoppableTaskExecutor : ITaskExecutor
{
void StopNow();
}

View file

@ -0,0 +1,9 @@
namespace Questionable.Controller.Steps;
internal interface ITask
{
bool ShouldRedoOnInterrupt()
{
return false;
}
}

View file

@ -0,0 +1,20 @@
using System;
namespace Questionable.Controller.Steps;
internal interface ITaskExecutor
{
ITask CurrentTask { get; }
InteractionProgressContext? ProgressContext { get; }
Type GetTaskType();
bool Start(ITask task);
bool ShouldInterruptOnDamage();
bool WasInterrupted();
ETaskResult Update();
}

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps;
internal interface ITaskFactory
{
IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step);
}

View file

@ -0,0 +1,8 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Questionable.Controller.Steps;
internal interface IToastAware : ITaskExecutor
{
bool OnErrorToast(SeString message);
}

View file

@ -0,0 +1,112 @@
using System;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace Questionable.Controller.Steps;
internal sealed class InteractionProgressContext
{
private bool _firstUpdateDone;
public bool CheckSequence { get; private set; }
public int CurrentSequence { get; private set; }
private InteractionProgressContext(bool checkSequence, int currentSequence)
{
CheckSequence = checkSequence;
CurrentSequence = currentSequence;
}
public unsafe static InteractionProgressContext Create(bool checkSequence)
{
if (!checkSequence)
{
ActionManager.Instance()->CastTimeElapsed = ActionManager.Instance()->CastTimeTotal;
}
return new InteractionProgressContext(checkSequence, ActionManager.Instance()->LastUsedActionSequence);
}
private unsafe static (bool, InteractionProgressContext?) FromActionUseInternal(Func<bool> func)
{
int lastUsedActionSequence = ActionManager.Instance()->LastUsedActionSequence;
if (!func())
{
return (false, null);
}
int lastUsedActionSequence2 = ActionManager.Instance()->LastUsedActionSequence;
if (lastUsedActionSequence == lastUsedActionSequence2)
{
return (true, null);
}
return (true, Create(checkSequence: true));
}
public static InteractionProgressContext? FromActionUse(Func<bool> func)
{
return FromActionUseInternal(func).Item2;
}
public static InteractionProgressContext? FromActionUseOrDefault(Func<bool> func)
{
(bool, InteractionProgressContext) tuple = FromActionUseInternal(func);
if (!tuple.Item1)
{
return null;
}
return tuple.Item2 ?? Create(checkSequence: false);
}
public unsafe void Update()
{
if (!_firstUpdateDone)
{
int lastUsedActionSequence = ActionManager.Instance()->LastUsedActionSequence;
if (!CheckSequence && lastUsedActionSequence > CurrentSequence)
{
CheckSequence = true;
CurrentSequence = lastUsedActionSequence;
}
_firstUpdateDone = true;
}
}
public unsafe bool WasSuccessful()
{
if (CheckSequence && (CurrentSequence != ActionManager.Instance()->LastUsedActionSequence || CurrentSequence != ActionManager.Instance()->LastHandledActionSequence))
{
return false;
}
if (ActionManager.Instance()->CastTimeElapsed > 0f)
{
return Math.Abs(ActionManager.Instance()->CastTimeElapsed - ActionManager.Instance()->CastTimeTotal) < 0.001f;
}
return false;
}
public unsafe bool WasInterrupted()
{
if (CheckSequence && CurrentSequence == ActionManager.Instance()->LastHandledActionSequence && CurrentSequence == ActionManager.Instance()->LastUsedActionSequence)
{
return false;
}
if (ActionManager.Instance()->CastTimeElapsed == 0f)
{
return ActionManager.Instance()->CastTimeTotal > 0f;
}
return false;
}
public override string ToString()
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(12, 3);
defaultInterpolatedStringHandler.AppendLiteral("IPCtx(");
defaultInterpolatedStringHandler.AppendFormatted(CheckSequence ? ((object)CurrentSequence) : "-");
defaultInterpolatedStringHandler.AppendLiteral(" - ");
defaultInterpolatedStringHandler.AppendFormatted(WasSuccessful());
defaultInterpolatedStringHandler.AppendLiteral(", ");
defaultInterpolatedStringHandler.AppendFormatted(WasInterrupted());
defaultInterpolatedStringHandler.AppendLiteral(")");
return defaultInterpolatedStringHandler.ToStringAndClear();
}
}

View file

@ -0,0 +1,124 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib.GameUI;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
using Questionable.Model.Common;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps;
internal static class QuestCleanUp
{
internal sealed class CheckAlliedSocietyMount(GameFunctions gameFunctions, AetheryteData aetheryteData, AlliedSocietyData alliedSocietyData, ILogger<CheckAlliedSocietyMount> logger) : SimpleTaskFactory()
{
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
{
if (sequence.Sequence == 0)
{
return null;
}
ushort? mountId = gameFunctions.GetMountId();
if (mountId.HasValue)
{
ushort valueOrDefault = mountId.GetValueOrDefault();
if (alliedSocietyData.Mounts.TryGetValue(valueOrDefault, out AlliedSocietyMountConfiguration mountConfiguration))
{
logger.LogInformation("We are on a known allied society mount with id = {MountId}", valueOrDefault);
EAetheryteLocation eAetheryteLocation = step.AetheryteShortcut ?? mountConfiguration.ClosestAetheryte;
AetheryteShortcut.Task result = new AetheryteShortcut.Task(null, quest.Id, eAetheryteLocation, aetheryteData.TerritoryIds[eAetheryteLocation]);
if (sequence.Sequence == byte.MaxValue)
{
logger.LogInformation("Mount can't be used to finish quest, teleporting to {Aetheryte}", mountConfiguration.ClosestAetheryte);
return result;
}
if (!quest.AllSteps().Any<(QuestSequence, int, QuestStep)>(delegate((QuestSequence Sequence, int StepId, QuestStep Step) x)
{
EAction? action = x.Step.Action;
if (action.HasValue)
{
EAction valueOrDefault2 = action.GetValueOrDefault();
if (valueOrDefault2.RequiresMount())
{
return true;
}
}
return x.Step.InteractionType == EInteractionType.Combat && x.Step.KillEnemyDataIds.Contains(8593u);
}))
{
logger.LogInformation("Quest doesn't use any mount actions, teleporting to {Aetheryte}", mountConfiguration.ClosestAetheryte);
return result;
}
if (!(from x in quest.AllSequences()
where x.Sequence > 0 && x.Sequence < sequence.Sequence
select x).SelectMany((QuestSequence x) => x.Steps).ToList().Any((QuestStep x) => x.DataId.HasValue && mountConfiguration.IssuerDataIds.Contains(x.DataId.Value)))
{
logger.LogInformation("Haven't talked to mount NPC for this allied society quest; {Aetheryte}", mountConfiguration.ClosestAetheryte);
return result;
}
}
}
return null;
}
}
internal sealed class CloseGatheringAddonFactory(IGameGui gameGui) : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (IsAddonOpen("GatheringMasterpiece"))
{
yield return new CloseGatheringAddonTask("GatheringMasterpiece");
}
if (IsAddonOpen("Gathering"))
{
yield return new CloseGatheringAddonTask("Gathering");
}
}
private unsafe bool IsAddonOpen(string name)
{
if (gameGui.TryGetAddonByName<AtkUnitBase>(name, out var addonPtr))
{
return addonPtr->IsVisible;
}
return false;
}
}
internal sealed record CloseGatheringAddonTask(string AddonName) : ITask
{
public override string ToString()
{
return "CloseAddon(" + AddonName + ")";
}
}
internal sealed class DoCloseAddon(IGameGui gameGui) : TaskExecutor<CloseGatheringAddonTask>()
{
protected unsafe override bool Start()
{
if (gameGui.TryGetAddonByName<AtkUnitBase>(base.Task.AddonName, out var addonPtr))
{
addonPtr->FireCallbackInt(-1);
return true;
}
return false;
}
public override ETaskResult Update()
{
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps;
internal abstract class SimpleTaskFactory : ITaskFactory
{
public abstract ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step);
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
ITask task = CreateTask(quest, sequence, step);
if (task != null)
{
yield return task;
}
}
}

View file

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps;
internal sealed class TaskCreator
{
private readonly IServiceProvider _serviceProvider;
private readonly TerritoryData _territoryData;
private readonly IClientState _clientState;
private readonly IChatGui _chatGui;
private readonly ILogger<TaskCreator> _logger;
public TaskCreator(IServiceProvider serviceProvider, TerritoryData territoryData, IClientState clientState, IChatGui chatGui, ILogger<TaskCreator> logger)
{
_serviceProvider = serviceProvider;
_territoryData = territoryData;
_clientState = clientState;
_chatGui = chatGui;
_logger = logger;
}
public IReadOnlyList<ITask> CreateTasks(Quest quest, byte sequenceNumber, QuestSequence? sequence, QuestStep? step)
{
List<ITask> list2;
if (sequence == null)
{
_chatGui.PrintError($"Path for quest '{quest.Info.Name}' ({quest.Id}) does not contain sequence {sequenceNumber}, please report this: https://github.com/PunishXIV/Questionable/discussions/20", "Questionable", 576);
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.WaitNextStepOrSequence();
list2 = list;
}
else if (step == null)
{
int index = 1;
List<ITask> list3 = new List<ITask>(index);
CollectionsMarshal.SetCount(list3, index);
Span<ITask> span = CollectionsMarshal.AsSpan(list3);
int num = 0;
span[num] = new WaitAtEnd.WaitNextStepOrSequence();
list2 = list3;
}
else
{
using IServiceScope serviceScope = _serviceProvider.CreateScope();
list2 = serviceScope.ServiceProvider.GetRequiredService<IEnumerable<ITaskFactory>>().SelectMany(delegate(ITaskFactory x)
{
List<ITask> list4 = x.CreateAllTasks(quest, sequence, step).ToList();
if (list4.Count > 0 && _logger.IsEnabled(LogLevel.Trace))
{
string text = x.GetType().FullName ?? x.GetType().Name;
if (text.Contains('.', StringComparison.Ordinal))
{
string text2 = text;
int num3 = text.LastIndexOf('.') + 1;
text = text2.Substring(num3, text2.Length - num3);
}
_logger.LogTrace("Factory {FactoryName} created Task {TaskNames}", text, string.Join(", ", list4.Select((ITask y) => y.ToString())));
}
return list4;
}).ToList();
SinglePlayerDuty.StartSinglePlayerDuty startSinglePlayerDuty = list2.Where((ITask y) => y is SinglePlayerDuty.StartSinglePlayerDuty).Cast<SinglePlayerDuty.StartSinglePlayerDuty>().FirstOrDefault();
if (startSinglePlayerDuty != null && _territoryData.TryGetContentFinderCondition(startSinglePlayerDuty.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData) && _clientState.TerritoryType == contentFinderConditionData.TerritoryId)
{
int num2 = list2.IndexOf(startSinglePlayerDuty);
_logger.LogWarning("Skipping {SkippedTaskCount} out of {TotalCount} tasks, questionable was started while in single player duty", num2 + 1, list2.Count);
list2.RemoveRange(0, num2 + 1);
_logger.LogInformation("Next actual task: {NextTask}, total tasks left: {RemainingTaskCount}", list2.FirstOrDefault(), list2.Count);
}
}
if (list2.Count == 0)
{
_logger.LogInformation("Nothing to execute for step?");
}
else
{
_logger.LogInformation("Tasks for {QuestId}, {Sequence}, {Step}: {Tasks}", quest.Id, sequenceNumber, (step == null) ? ((int?)null) : sequence?.Steps.IndexOf(step), string.Join(", ", list2.Select((ITask x) => x.ToString())));
}
return list2;
}
}

View file

@ -0,0 +1,20 @@
using System;
namespace Questionable.Controller.Steps;
public class TaskException : Exception
{
public TaskException()
{
}
public TaskException(string message)
: base(message)
{
}
public TaskException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View file

@ -0,0 +1,44 @@
using System;
namespace Questionable.Controller.Steps;
internal abstract class TaskExecutor<T> : ITaskExecutor where T : class, ITask
{
protected T Task { get; set; }
public InteractionProgressContext? ProgressContext { get; set; }
ITask ITaskExecutor.CurrentTask => Task;
public virtual bool WasInterrupted()
{
InteractionProgressContext progressContext = ProgressContext;
if (progressContext != null)
{
progressContext.Update();
return progressContext.WasInterrupted();
}
return false;
}
public Type GetTaskType()
{
return typeof(T);
}
protected abstract bool Start();
public bool Start(ITask task)
{
if (task is T task2)
{
Task = task2;
return Start();
}
throw new TaskException($"Unable to cast {task.GetType()} to {typeof(T)}");
}
public abstract ETaskResult Update();
public abstract bool ShouldInterruptOnDamage();
}

View file

@ -0,0 +1,78 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Questionable.Controller.Steps;
internal sealed class TaskQueue
{
private readonly List<ITask> _completedTasks = new List<ITask>();
private readonly List<ITask> _tasks = new List<ITask>();
public ITaskExecutor? CurrentTaskExecutor { get; set; }
public IEnumerable<ITask> RemainingTasks => _tasks;
public bool AllTasksComplete
{
get
{
if (CurrentTaskExecutor == null)
{
return _tasks.Count == 0;
}
return false;
}
}
public void Enqueue(ITask task)
{
_tasks.Add(task);
}
public void EnqueueAll(IEnumerable<ITask> tasks)
{
_tasks.InsertRange(0, tasks);
}
public bool TryDequeue([NotNullWhen(true)] out ITask? task)
{
task = _tasks.FirstOrDefault();
if (task == null)
{
return false;
}
if (task.ShouldRedoOnInterrupt())
{
_completedTasks.Add(task);
}
_tasks.RemoveAt(0);
return true;
}
public bool TryPeek([NotNullWhen(true)] out ITask? task)
{
task = _tasks.FirstOrDefault();
return task != null;
}
public void Reset()
{
_tasks.Clear();
_completedTasks.Clear();
CurrentTaskExecutor = null;
}
public void InterruptWith(List<ITask> interruptionTasks)
{
List<ITask> list = new List<ITask>();
list.AddRange(interruptionTasks);
list.AddRange(_completedTasks.Where((ITask x) => x != CurrentTaskExecutor?.CurrentTask).ToList());
list.Add(CurrentTaskExecutor?.CurrentTask);
list.AddRange(_tasks);
List<ITask> source = list;
Reset();
_tasks.AddRange(source.Where((ITask x) => x != null).Cast<ITask>());
}
}

View file

@ -0,0 +1,134 @@
using System;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using LLib.GameData;
using Microsoft.Extensions.Logging;
namespace Questionable.Controller.Utils;
internal sealed class PartyWatchDog : IDisposable
{
private readonly QuestController _questController;
private readonly IClientState _clientState;
private readonly IChatGui _chatGui;
private readonly ILogger<PartyWatchDog> _logger;
private ushort? _uncheckedTeritoryId;
public PartyWatchDog(QuestController questController, IClientState clientState, IChatGui chatGui, ILogger<PartyWatchDog> logger)
{
_questController = questController;
_clientState = clientState;
_chatGui = chatGui;
_logger = logger;
_clientState.TerritoryChanged += TerritoryChanged;
}
private unsafe void TerritoryChanged(ushort newTerritoryId)
{
switch ((ETerritoryIntendedUse)GameMain.Instance()->CurrentTerritoryIntendedUseId)
{
case ETerritoryIntendedUse.Gaol:
case ETerritoryIntendedUse.Frontline:
case ETerritoryIntendedUse.LordOfVerminion:
case ETerritoryIntendedUse.Diadem:
case ETerritoryIntendedUse.CrystallineConflict:
case ETerritoryIntendedUse.DeepDungeon:
case ETerritoryIntendedUse.TreasureMapDuty:
case ETerritoryIntendedUse.Battlehall:
case ETerritoryIntendedUse.CrystallineConflict2:
case ETerritoryIntendedUse.Diadem2:
case ETerritoryIntendedUse.RivalWings:
case ETerritoryIntendedUse.Eureka:
case ETerritoryIntendedUse.LeapOfFaith:
case ETerritoryIntendedUse.OceanFishing:
case ETerritoryIntendedUse.Diadem3:
case ETerritoryIntendedUse.Bozja:
case ETerritoryIntendedUse.Battlehall2:
case ETerritoryIntendedUse.Battlehall3:
case ETerritoryIntendedUse.LargeScaleRaid:
case ETerritoryIntendedUse.LargeScaleSavageRaid:
case ETerritoryIntendedUse.Blunderville:
StopIfRunning($"Unsupported Area entered ({newTerritoryId})");
break;
case ETerritoryIntendedUse.Dungeon:
case ETerritoryIntendedUse.VariantDungeon:
case ETerritoryIntendedUse.AllianceRaid:
case ETerritoryIntendedUse.Trial:
case ETerritoryIntendedUse.Raid:
case ETerritoryIntendedUse.Raid2:
case ETerritoryIntendedUse.SeasonalEvent:
case ETerritoryIntendedUse.SeasonalEvent2:
case ETerritoryIntendedUse.CriterionDuty:
case ETerritoryIntendedUse.CriterionSavageDuty:
_uncheckedTeritoryId = newTerritoryId;
_logger.LogInformation("Will check territory {TerritoryId} after loading", newTerritoryId);
break;
case ETerritoryIntendedUse.StartingArea:
case ETerritoryIntendedUse.QuestArea:
case ETerritoryIntendedUse.QuestBattle:
case (ETerritoryIntendedUse)11:
case ETerritoryIntendedUse.QuestArea2:
case ETerritoryIntendedUse.ResidentialArea:
case ETerritoryIntendedUse.HousingInstances:
case ETerritoryIntendedUse.QuestArea3:
case (ETerritoryIntendedUse)19:
case ETerritoryIntendedUse.ChocoboSquare:
case ETerritoryIntendedUse.RestorationEvent:
case ETerritoryIntendedUse.Sanctum:
case ETerritoryIntendedUse.GoldSaucer:
case (ETerritoryIntendedUse)24:
case ETerritoryIntendedUse.HallOfTheNovice:
case ETerritoryIntendedUse.QuestBattle2:
case ETerritoryIntendedUse.Barracks:
case ETerritoryIntendedUse.SeasonalEventDuty:
case (ETerritoryIntendedUse)36:
case ETerritoryIntendedUse.Unknown1:
case (ETerritoryIntendedUse)42:
case ETerritoryIntendedUse.MaskedCarnivale:
case ETerritoryIntendedUse.IslandSanctuary:
case ETerritoryIntendedUse.QuestArea4:
case (ETerritoryIntendedUse)55:
case ETerritoryIntendedUse.TribalInstance:
break;
}
}
public unsafe void Update()
{
if (_uncheckedTeritoryId != _clientState.TerritoryType || GameMain.Instance()->TerritoryLoadState != 2)
{
return;
}
GroupManager* ptr = GroupManager.Instance();
if (ptr != null)
{
byte memberCount = ptr->MainGroup.MemberCount;
bool isAlliance = ptr->MainGroup.IsAlliance;
_logger.LogDebug("Territory {TerritoryId} with {MemberCount} members, alliance: {IsInAlliance}", _uncheckedTeritoryId, memberCount, isAlliance);
if (memberCount > 1 || isAlliance)
{
StopIfRunning("Other party members present");
}
_uncheckedTeritoryId = null;
}
}
private void StopIfRunning(string reason)
{
if (_questController.IsRunning || _questController.AutomationType != QuestController.EAutomationType.Manual)
{
_chatGui.PrintError("Stopping Questionable: " + reason + ". If you believe this to be correct, please restart Questionable manually.", "Questionable", 576);
_questController.Stop(reason);
}
}
public void Dispose()
{
_clientState.TerritoryChanged -= TerritoryChanged;
}
}

View file

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Utils;
internal static class QuestWorkUtils
{
public static bool HasCompletionFlags(IList<QuestWorkValue?> completionQuestVariablesFlags)
{
if (completionQuestVariablesFlags.Count == 6)
{
return completionQuestVariablesFlags.Any((QuestWorkValue x) => x != null && (x.High != 0 || x.Low != 0));
}
return false;
}
public static bool MatchesQuestWork(IList<QuestWorkValue?> completionQuestVariablesFlags, QuestProgressInfo questProgressInfo)
{
if (!HasCompletionFlags(completionQuestVariablesFlags) || questProgressInfo.Variables.Count != 6)
{
return false;
}
for (int i = 0; i < questProgressInfo.Variables.Count; i++)
{
QuestWorkValue questWorkValue = completionQuestVariablesFlags[i];
if (questWorkValue == null)
{
continue;
}
EQuestWorkMode mode = questWorkValue.Mode;
byte b = (byte)(questProgressInfo.Variables[i] >> 4);
byte b2 = (byte)(questProgressInfo.Variables[i] & 0xF);
byte? high = questWorkValue.High;
byte? low = questWorkValue.Low;
byte valueOrDefault = high.GetValueOrDefault();
byte valueOrDefault2 = low.GetValueOrDefault();
switch (mode)
{
case EQuestWorkMode.Exact:
if (high.HasValue && b != valueOrDefault)
{
return false;
}
if (low.HasValue && b2 != valueOrDefault2)
{
return false;
}
break;
case EQuestWorkMode.Bitwise:
if (high.HasValue && (b & high) != valueOrDefault)
{
return false;
}
if (low.HasValue && (b2 & low) != valueOrDefault2)
{
return false;
}
break;
default:
throw new InvalidOperationException($"Unknown qw mode {mode}");
}
}
return true;
}
public static bool MatchesRequiredQuestWorkConfig(List<List<QuestWorkValue>?> requiredQuestVariables, QuestProgressInfo questWork, ILogger logger)
{
if (requiredQuestVariables.Count != 6 || requiredQuestVariables.All((List<QuestWorkValue> x) => x == null || x.Count == 0))
{
logger.LogDebug("No RequiredQW defined");
return true;
}
for (int num = 0; num < 6; num++)
{
if (requiredQuestVariables[num] == null)
{
logger.LogDebug("No RequiredQW {Index} defined", num);
continue;
}
byte b = (byte)(questWork.Variables[num] >> 4);
byte b2 = (byte)(questWork.Variables[num] & 0xF);
foreach (QuestWorkValue item in requiredQuestVariables[num])
{
logger.LogDebug("H: {ExpectedHigh} - {ActualHigh}, L: {ExpectedLow} - {ActualLow}", item.High, b, item.Low, b2);
if ((!item.High.HasValue || item.High == b) && (!item.Low.HasValue || item.Low == b2))
{
return true;
}
}
}
logger.LogInformation("Should execute step");
return false;
}
}

View 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");
}
}

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

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

View 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();
}
}
}
}

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

View 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();
}
}

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

View 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();
}
}

Some files were not shown because too many files have changed in this diff Show more