muffin v7.5.12

This commit is contained in:
alydev 2026-08-19 13:19:57 +10:00
parent 3102923ce2
commit 911ed68baa
65 changed files with 2356 additions and 1539 deletions

View file

@ -4,18 +4,18 @@ internal static class PathBundleSecret
{ {
private static readonly byte[] A = new byte[32] private static readonly byte[] A = new byte[32]
{ {
184, 146, 158, 4, 12, 188, 14, 129, 154, 207, 48, 212, 251, 149, 233, 68, 48, 175, 219, 236,
124, 249, 11, 86, 207, 158, 174, 9, 114, 165, 217, 100, 130, 200, 255, 152, 233, 112, 177, 94,
243, 163, 70, 242, 33, 231, 45, 140, 119, 185, 93, 198, 133, 46, 110, 244, 154, 244, 247, 13,
162, 26 145, 158
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
150, 86, 230, 115, 69, 121, 30, 79, 176, 162, 79, 60, 140, 255, 167, 84, 206, 132, 105, 228,
216, 139, 248, 83, 68, 111, 173, 223, 203, 79, 71, 209, 251, 6, 250, 62, 38, 86, 249, 66,
42, 143, 208, 219, 192, 216, 102, 249, 57, 147, 102, 234, 109, 58, 57, 229, 131, 131, 42, 66,
95, 21 172, 97
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -4,18 +4,18 @@ internal static class PathBundleSecret
{ {
private static readonly byte[] A = new byte[32] private static readonly byte[] A = new byte[32]
{ {
145, 72, 249, 36, 189, 185, 130, 238, 193, 10, 83, 53, 162, 177, 116, 210, 154, 147, 32, 246,
202, 232, 4, 205, 253, 37, 116, 120, 105, 80, 109, 49, 21, 10, 185, 40, 3, 223, 158, 234,
155, 225, 237, 122, 124, 183, 185, 180, 222, 128, 82, 124, 250, 209, 185, 174, 77, 93, 58, 15,
163, 58 16, 246
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
38, 241, 153, 134, 236, 129, 153, 181, 53, 177, 11, 0, 215, 132, 153, 74, 202, 77, 15, 86,
92, 29, 47, 248, 180, 1, 80, 8, 68, 19, 243, 43, 187, 26, 168, 53, 42, 2, 79, 60,
82, 214, 85, 67, 101, 195, 46, 138, 196, 69, 154, 215, 216, 52, 147, 70, 38, 49, 232, 46,
206, 117 225, 130
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
using System.Text;
using System.Threading; using System.Threading;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Data.Files; using Lumina.Data.Files;
@ -22,6 +23,8 @@ public sealed class NpcPositionCache
private const string FileName = "npc-position-cache.bin"; private const string FileName = "npc-position-cache.bin";
private const int MaxPreallocatedEntries = 131072;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly ILogger<NpcPositionCache> _logger; private readonly ILogger<NpcPositionCache> _logger;
@ -89,6 +92,23 @@ public sealed class NpcPositionCache
} }
} }
public bool TryLoadFromStream(Stream stream, bool acceptStaleVersion)
{
ArgumentNullException.ThrowIfNull(stream, "stream");
if (_cache != null)
{
return true;
}
lock (_buildLock)
{
if (_cache == null)
{
_cache = ReadCache(stream, acceptStaleVersion, "stream");
}
return _cache != null;
}
}
public void WarmUp(CancellationToken cancellationToken = default(CancellationToken)) public void WarmUp(CancellationToken cancellationToken = default(CancellationToken))
{ {
EnsureBuilt(cancellationToken); EnsureBuilt(cancellationToken);
@ -149,36 +169,20 @@ public sealed class NpcPositionCache
{ {
return null; return null;
} }
Dictionary<uint, (ushort, Vector3, bool)> dictionary;
try try
{ {
using BinaryReader binaryReader = new BinaryReader(File.OpenRead(cacheFilePath)); using FileStream stream = File.OpenRead(cacheFilePath);
if (binaryReader.ReadUInt32() != 1129336401 || binaryReader.ReadInt32() != 1) dictionary = ReadCache(stream, acceptStaleVersion: false, "disk");
{
_logger.LogDebug("NPC position cache file has an unknown header, discarding");
}
else
{
if (!(binaryReader.ReadString() != _options.GameVersion))
{
int num = binaryReader.ReadInt32();
Dictionary<uint, (ushort, Vector3, bool)> dictionary = new Dictionary<uint, (ushort, Vector3, bool)>(num);
for (int i = 0; i < num; i++)
{
uint key = binaryReader.ReadUInt32();
ushort item = binaryReader.ReadUInt16();
Vector3 item2 = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
bool item3 = binaryReader.ReadBoolean();
dictionary[key] = (item, item2, item3);
}
_logger.LogDebug("NPC position cache loaded from disk with {Count} entries", dictionary.Count);
return dictionary;
}
_logger.LogDebug("NPC position cache file is for a different game version, discarding");
}
} }
catch (Exception exception) catch (Exception exception)
{ {
_logger.LogWarning(exception, "Failed to load NPC position cache file, discarding"); _logger.LogWarning(exception, "Failed to open NPC position cache file, discarding");
dictionary = null;
}
if (dictionary != null)
{
return dictionary;
} }
try try
{ {
@ -190,6 +194,51 @@ public sealed class NpcPositionCache
return null; return null;
} }
private Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)>? ReadCache(Stream stream, bool acceptStaleVersion, string source)
{
try
{
using BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (binaryReader.ReadUInt32() != 1129336401 || binaryReader.ReadInt32() != 1)
{
_logger.LogDebug("NPC position cache data has an unknown header, discarding");
return null;
}
string text = binaryReader.ReadString();
if (text != _options.GameVersion)
{
if (!acceptStaleVersion)
{
_logger.LogDebug("NPC position cache data is for a different game version, discarding");
return null;
}
_logger.LogInformation("NPC position cache data was built for game version {CachedVersion} instead of {ExpectedVersion}, using it anyway", text, _options.GameVersion);
}
int num = binaryReader.ReadInt32();
if (num < 0)
{
_logger.LogDebug("NPC position cache data has a negative entry count, discarding");
return null;
}
Dictionary<uint, (ushort, Vector3, bool)> dictionary = new Dictionary<uint, (ushort, Vector3, bool)>(Math.Min(num, 131072));
for (int i = 0; i < num; i++)
{
uint key = binaryReader.ReadUInt32();
ushort item = binaryReader.ReadUInt16();
Vector3 item2 = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
bool item3 = binaryReader.ReadBoolean();
dictionary[key] = (item, item2, item3);
}
_logger.LogDebug("NPC position cache loaded from {Source} with {Count} entries", source, dictionary.Count);
return dictionary;
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to read NPC position cache data, discarding");
return null;
}
}
private void SaveToDisk(Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)> cache) private void SaveToDisk(Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)> cache)
{ {
string cacheFilePath = CacheFilePath; string cacheFilePath = CacheFilePath;

View file

@ -4,18 +4,18 @@ internal static class PathBundleSecret
{ {
private static readonly byte[] A = new byte[32] private static readonly byte[] A = new byte[32]
{ {
14, 205, 205, 220, 252, 13, 221, 232, 100, 189, 73, 4, 92, 189, 214, 0, 53, 235, 105, 164,
37, 224, 91, 77, 175, 255, 30, 195, 23, 136, 216, 243, 244, 200, 198, 26, 227, 227, 153, 121,
234, 230, 125, 209, 122, 165, 20, 174, 100, 226, 241, 147, 166, 171, 63, 63, 121, 152, 52, 115,
27, 222 89, 201
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
241, 56, 43, 159, 122, 127, 111, 25, 223, 150, 243, 132, 180, 1, 83, 89, 202, 66, 33, 20,
24, 59, 99, 24, 253, 141, 217, 107, 154, 16, 242, 184, 99, 15, 215, 2, 4, 125, 221, 134,
247, 125, 175, 254, 169, 41, 27, 2, 18, 101, 186, 51, 132, 151, 141, 220, 230, 57, 252, 77,
46, 175 103, 57
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -548,6 +548,10 @@ public sealed class AethernetShardConverter : EnumConverter<EAetheryteLocation>
EAetheryteLocation.TuliyollalXakTuralSkygate, EAetheryteLocation.TuliyollalXakTuralSkygate,
"[Tuliyollal] Xak Tural Skygate (Shaaloani)" "[Tuliyollal] Xak Tural Skygate (Shaaloani)"
}, },
{
EAetheryteLocation.TuliyollalPhantomVillage,
"[Tuliyollal] Phantom Village"
},
{ {
EAetheryteLocation.SolutionNine, EAetheryteLocation.SolutionNine,
"[Solution Nine] Aetheryte Plaza" "[Solution Nine] Aetheryte Plaza"

View file

@ -11,6 +11,8 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
{ {
private const byte ModeOff = 0; private const byte ModeOff = 0;
private const byte ModeManual = 3;
private const byte ModeHenched = 4; private const byte ModeHenched = 4;
private readonly ILogger<RotationSolverRebornModule> _logger; private readonly ILogger<RotationSolverRebornModule> _logger;
@ -21,15 +23,12 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
private readonly ICallGateSubscriber<byte, object> _changeOperationMode; private readonly ICallGateSubscriber<byte, object> _changeOperationMode;
private readonly ICallGateSubscriber<string, object, object> _setConfig;
public RotationSolverRebornModule(ILogger<RotationSolverRebornModule> logger, IDalamudPluginInterface pluginInterface, Configuration configuration) public RotationSolverRebornModule(ILogger<RotationSolverRebornModule> logger, IDalamudPluginInterface pluginInterface, Configuration configuration)
{ {
_logger = logger; _logger = logger;
_configuration = configuration; _configuration = configuration;
_test = pluginInterface.GetIpcSubscriber<string, object>("RotationSolverReborn.Test"); _test = pluginInterface.GetIpcSubscriber<string, object>("RotationSolverReborn.Test");
_changeOperationMode = pluginInterface.GetIpcSubscriber<byte, object>("RotationSolverReborn.ChangeOperatingMode"); _changeOperationMode = pluginInterface.GetIpcSubscriber<byte, object>("RotationSolverReborn.ChangeOperatingMode");
_setConfig = pluginInterface.GetIpcSubscriber<string, object, object>("RotationSolverReborn.SetConfig");
} }
public bool IsAvailable() public bool IsAvailable()
@ -55,13 +54,20 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
} }
public bool Start(CombatController.CombatData combatData) public bool Start(CombatController.CombatData combatData)
{
return Start(3);
}
public bool StartForDuty()
{
return Start(4);
}
private bool Start(byte mode)
{ {
try try
{ {
_changeOperationMode.InvokeAction(4); _changeOperationMode.InvokeAction(mode);
SetConfig("AutoOffAfterCombat", false);
SetConfig("HealPartyMembers", true);
SetConfig("HostileType", 1);
return true; return true;
} }
catch (IpcError exception) catch (IpcError exception)
@ -101,16 +107,4 @@ internal sealed class RotationSolverRebornModule : ICombatModule, IDisposable
public void Dispose() public void Dispose()
{ {
} }
private void SetConfig(string key, object value)
{
try
{
_setConfig.InvokeAction(key, value);
}
catch (IpcError exception)
{
_logger.LogDebug(exception, "RSR SetConfig {Key} failed", key);
}
}
} }

View file

@ -47,6 +47,10 @@ internal sealed class DeliveryNpcApproachExecutor(NavmeshIpc navmeshIpc, Movemen
{ {
return true; return true;
} }
if (Vector3.Distance(localPlayer.Position, _npcPosition) <= _interactionDistance + 0.5f)
{
return false;
}
float num = MathF.Atan2(localPlayer.Position.X - _npcPosition.X, localPlayer.Position.Z - _npcPosition.Z); float num = MathF.Atan2(localPlayer.Position.X - _npcPosition.X, localPlayer.Position.Z - _npcPosition.Z);
float num2 = MathF.Max(1f, _interactionDistance - 0.5f); float num2 = MathF.Max(1f, _interactionDistance - 0.5f);
for (int i = 0; i < 24; i++) for (int i = 0; i < 24; i++)

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib.GameData; using LLib.GameData;
using LLib.Inventory; using LLib.Inventory;
using LLib.Shop; using LLib.Shop;
@ -73,27 +74,39 @@ internal sealed class DeliveryPlannerService
return null; return null;
} }
uint supplyIndex = (uint)row.SatisfactionNpcParams[npcState.Rank].SupplyIndex; uint supplyIndex = (uint)row.SatisfactionNpcParams[npcState.Rank].SupplyIndex;
SubrowExcelSheet<SatisfactionSupply> subrowExcelSheet = _dataManager.GetSubrowExcelSheet<SatisfactionSupply>();
if (TryGetLiveSupplyRow(npcIndex, slot, supplyIndex, out var supplyRow, out var hasLiveRequest))
{
_logger.LogDebug("Using live requested item {ItemId} for NPC {NpcIndex} slot {Slot}", supplyRow.Item.RowId, npcIndex, slot);
}
else
{
if (hasLiveRequest)
{
return null;
}
uint num = _supplyHelper.CalculateRequestedItems(supplyIndex, supplySeed)[(int)slot]; uint num = _supplyHelper.CalculateRequestedItems(supplyIndex, supplySeed)[(int)slot];
if (num == uint.MaxValue) if (num == uint.MaxValue)
{ {
_logger.LogDebug("No item predicted for NPC {NpcIndex} slot {Slot}", npcIndex, slot); _logger.LogDebug("No item predicted for NPC {NpcIndex} slot {Slot}", npcIndex, slot);
return null; return null;
} }
if (!_dataManager.GetSubrowExcelSheet<SatisfactionSupply>().TryGetSubrow(supplyIndex, (ushort)num, out var subrow)) if (!subrowExcelSheet.TryGetSubrow(supplyIndex, (ushort)num, out supplyRow))
{ {
_logger.LogWarning("SatisfactionSupply subrow ({SupplyIndex}, {SubrowIndex}) not found", supplyIndex, num); _logger.LogWarning("SatisfactionSupply subrow ({SupplyIndex}, {SubrowIndex}) not found", supplyIndex, num);
return null; return null;
} }
uint rowId = subrow.Item.RowId; }
uint rowId = supplyRow.Item.RowId;
ushort num2 = _configuration.CustomDeliveries.CollectabilityTier switch ushort num2 = _configuration.CustomDeliveries.CollectabilityTier switch
{ {
Configuration.CustomDeliveryConfiguration.ECollectabilityTier.Low => subrow.CollectabilityLow, Configuration.CustomDeliveryConfiguration.ECollectabilityTier.Low => supplyRow.CollectabilityLow,
Configuration.CustomDeliveryConfiguration.ECollectabilityTier.Mid => subrow.CollectabilityMid, Configuration.CustomDeliveryConfiguration.ECollectabilityTier.Mid => supplyRow.CollectabilityMid,
_ => subrow.CollectabilityHigh, _ => supplyRow.CollectabilityHigh,
}; };
if (rowId == 0) if (rowId == 0)
{ {
_logger.LogWarning("SatisfactionSupply subrow ({SupplyIndex}, {SubrowIndex}) has no item", supplyIndex, num); _logger.LogWarning("SatisfactionSupply row {SupplyIndex} has no item for slot {Slot}", supplyIndex, slot);
return null; return null;
} }
int num3 = row.DeliveriesPerWeek - npcState.UsedDeliveries; int num3 = row.DeliveriesPerWeek - npcState.UsedDeliveries;
@ -107,7 +120,7 @@ internal sealed class DeliveryPlannerService
if (satisfactionRequired > 0) if (satisfactionRequired > 0)
{ {
Configuration.CustomDeliveryConfiguration.ECollectabilityTier collectabilityTier = _configuration.CustomDeliveries.CollectabilityTier; Configuration.CustomDeliveryConfiguration.ECollectabilityTier collectabilityTier = _configuration.CustomDeliveries.CollectabilityTier;
int num5 = _rewardCalculator.CalculateSatisfactionPerDelivery(subrow.Reward.RowId, subrow.IsBonus, collectabilityTier); int num5 = _rewardCalculator.CalculateSatisfactionPerDelivery(supplyRow.Reward.RowId, supplyRow.IsBonus, collectabilityTier);
if (num5 > 0) if (num5 > 0)
{ {
int num6 = (satisfactionRequired - npcState.SatisfactionCurrent + num5 - 1) / num5; int num6 = (satisfactionRequired - npcState.SatisfactionCurrent + num5 - 1) / num5;
@ -131,8 +144,8 @@ internal sealed class DeliveryPlannerService
} }
if (_configuration.CustomDeliveries.ScripOvercapMode != Configuration.CustomDeliveryConfiguration.EScripOvercapMode.Ignore) if (_configuration.CustomDeliveries.ScripOvercapMode != Configuration.CustomDeliveryConfiguration.EScripOvercapMode.Ignore)
{ {
uint rowId2 = subrow.Reward.RowId; uint rowId2 = supplyRow.Reward.RowId;
bool isBonus = subrow.IsBonus; bool isBonus = supplyRow.IsBonus;
int num8 = _rewardCalculator.MaxDeliveriesBeforeOvercap(rowId2, isBonus, _configuration.CustomDeliveries.CollectabilityTier); int num8 = _rewardCalculator.MaxDeliveriesBeforeOvercap(rowId2, isBonus, _configuration.CustomDeliveries.CollectabilityTier);
if (num8 <= 0) if (num8 <= 0)
{ {
@ -201,6 +214,31 @@ internal sealed class DeliveryPlannerService
return result; return result;
} }
private unsafe bool TryGetLiveSupplyRow(int npcIndex, EDeliverySlot slot, uint supplyIndex, out SatisfactionSupply supplyRow, out bool hasLiveRequest)
{
hasLiveRequest = false;
AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance();
if (ptr != null && ptr->IsAgentActive() && ptr->NpcInfo.Valid && ptr->NpcInfo.Initialized && ptr->NpcInfo.Id == (uint)(npcIndex + 1))
{
uint id = ptr->Items[(int)slot].Id;
if (id != 0)
{
hasLiveRequest = true;
foreach (SatisfactionSupply item in _dataManager.GetSubrowExcelSheet<SatisfactionSupply>().Flatten())
{
if (item.RowId == supplyIndex && item.Item.RowId == id)
{
supplyRow = item;
return true;
}
}
_logger.LogWarning("Live requested item {ItemId} was not found in SatisfactionSupply row {SupplyIndex}", id, supplyIndex);
}
}
supplyRow = default(SatisfactionSupply);
return false;
}
public bool IsNpcUnlocked(int npcIndex) public bool IsNpcUnlocked(int npcIndex)
{ {
if (!_dataManager.GetExcelSheet<SatisfactionNpc>().TryGetRow((uint)(npcIndex + 1), out var row)) if (!_dataManager.GetExcelSheet<SatisfactionNpc>().TryGetRow((uint)(npcIndex + 1), out var row))

View file

@ -1,6 +1,8 @@
using System; using System;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LLib; using LLib;
using LLib.Shop; using LLib.Shop;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -89,6 +91,10 @@ internal sealed class SatisfactionSupplyTurnInExecutor(GameFunctions gameFunctio
case EPhase.SelectSlot: case EPhase.SelectSlot:
if (_throttle.TryReset(0.5)) if (_throttle.TryReset(0.5))
{ {
if (!PlanStillMatchesRequest())
{
return ETaskResult.RetryStep;
}
if (SatisfactionSupplyActions.IsNpcTradeReady()) if (SatisfactionSupplyActions.IsNpcTradeReady())
{ {
logger.LogDebug("NpcTrade agent ready"); logger.LogDebug("NpcTrade agent ready");
@ -106,11 +112,19 @@ internal sealed class SatisfactionSupplyTurnInExecutor(GameFunctions gameFunctio
} }
break; break;
case EPhase.ConfirmTrade: case EPhase.ConfirmTrade:
if (_throttle.TryReset(0.5) && SatisfactionSupplyActions.TryConfirmTrade()) if (_throttle.TryReset(0.5))
{
if (!HasRemainingItems())
{
logger.LogWarning("Planned item {ItemId} is no longer available for turn-in", base.Task.ItemId);
return ETaskResult.RetryStep;
}
if (SatisfactionSupplyActions.TryConfirmTrade())
{ {
logger.LogDebug("Confirmed trade for slot {Slot}", base.Task.Slot); logger.LogDebug("Confirmed trade for slot {Slot}", base.Task.Slot);
SetPhase(EPhase.WaitForCutsceneStart); SetPhase(EPhase.WaitForCutsceneStart);
} }
}
break; break;
case EPhase.WaitForCutsceneStart: case EPhase.WaitForCutsceneStart:
if (HasDeliveryRegistered()) if (HasDeliveryRegistered())
@ -192,6 +206,32 @@ internal sealed class SatisfactionSupplyTurnInExecutor(GameFunctions gameFunctio
return SatisfactionSupplyActions.GetNpcState(base.Task.NpcIndex).UsedDeliveries > _lastUsedDeliveries; return SatisfactionSupplyActions.GetNpcState(base.Task.NpcIndex).UsedDeliveries > _lastUsedDeliveries;
} }
private unsafe bool PlanStillMatchesRequest()
{
AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance();
if (ptr == null || !ptr->IsAgentActive() || !ptr->NpcInfo.Valid || !ptr->NpcInfo.Initialized)
{
return true;
}
uint id = ptr->Items[(int)base.Task.Slot].Id;
if (ptr->NpcInfo.Id == (uint)(base.Task.NpcIndex + 1) && id == base.Task.ItemId && HasRemainingItems())
{
return true;
}
logger.LogWarning("Turn-in plan no longer matches live request: NPC {NpcIndex}, slot {Slot}, planned item {PlannedItemId}, live item {LiveItemId}", base.Task.NpcIndex, base.Task.Slot, base.Task.ItemId, id);
return false;
}
private unsafe bool HasRemainingItems()
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr != null)
{
return ptr->GetInventoryItemCount(base.Task.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)base.Task.Collectability) >= _remaining;
}
return false;
}
public override bool ShouldInterruptOnDamage() public override bool ShouldInterruptOnDamage()
{ {
return false; return false;

View file

@ -2,7 +2,7 @@ using Questionable.Controller.Steps;
namespace Questionable.Controller.CustomDelivery; namespace Questionable.Controller.CustomDelivery;
internal sealed record SatisfactionSupplyTurnInTask(int NpcIndex, EDeliverySlot Slot, int DeliveryCount, uint NpcDataId) : ITask internal sealed record SatisfactionSupplyTurnInTask(int NpcIndex, EDeliverySlot Slot, int DeliveryCount, uint NpcDataId, uint ItemId, ushort Collectability) : ITask
{ {
public override string ToString() public override string ToString()
{ {

View file

@ -97,7 +97,7 @@ internal sealed class InteractionUiController : IDisposable
{ {
get get
{ {
if (!_questController.IsRunning && !_fateController.IsRunning && !_seasonalDutyController.IsRunning && !_attunementController.IsRunning) if (!SmartNavWarpRowId.HasValue && !SmartNavDestTerritoryId.HasValue && SmartNavTaxiDestPlaceName == null && AethernetDestinationName == null && !_questController.IsRunning && !_fateController.IsRunning && !_seasonalDutyController.IsRunning && !_attunementController.IsRunning)
{ {
return _territoryData.IsQuestBattleInstance(_clientState.TerritoryType); return _territoryData.IsQuestBattleInstance(_clientState.TerritoryType);
} }

View file

@ -106,9 +106,19 @@ internal static class DoGather
else else
{ {
List<SlotInfo> list = ReadSlots(addonPtr2); List<SlotInfo> list = ReadSlots(addonPtr2);
if (list.Count == 0)
{
return ETaskResult.StillRunning;
}
if (base.Task.Request.Collectability > 0) if (base.Task.Request.Collectability > 0)
{ {
SlotInfo slotInfo = list.Single((SlotInfo x) => x.ItemId == base.Task.Request.ItemId); SlotInfo slotInfo = list.FirstOrDefault((SlotInfo x) => x.ItemId == base.Task.Request.ItemId);
if (!(slotInfo != null))
{
logger.LogDebug("Requested collectable {ItemId} is not available at this node", base.Task.Request.ItemId);
addonPtr2->FireCallbackInt(-1);
return ETaskResult.TaskComplete;
}
addonPtr2->FireCallbackInt(slotInfo.Index); addonPtr2->FireCallbackInt(slotInfo.Index);
} }
else else

View file

@ -86,6 +86,12 @@ internal static class Action
return Environment.TickCount64 - _questFlagWaitStarted > (long)(timeoutSeconds * 1000f); return Environment.TickCount64 - _questFlagWaitStarted > (long)(timeoutSeconds * 1000f);
} }
public override void ResetTimeout()
{
base.ResetTimeout();
_questFlagWaitStarted = Environment.TickCount64;
}
protected override bool Start() protected override bool Start()
{ {
if (base.Task.DataId.HasValue) if (base.Task.DataId.HasValue)
@ -283,6 +289,12 @@ internal static class Action
return Environment.TickCount64 - _startedAt > (long)(timeoutSeconds * 1000f); return Environment.TickCount64 - _startedAt > (long)(timeoutSeconds * 1000f);
} }
public override void ResetTimeout()
{
base.ResetTimeout();
_startedAt = Environment.TickCount64;
}
protected override bool Start() protected override bool Start()
{ {
if (gameFunctions.HasStatus(base.Task.Status)) if (gameFunctions.HasStatus(base.Task.Status))

View file

@ -209,6 +209,8 @@ internal static class Duty
{ {
private long _startMs; private long _startMs;
private bool _runAccepted;
protected unsafe override bool Start() protected unsafe override bool Start()
{ {
if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
@ -240,6 +242,7 @@ internal static class Duty
} }
autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode); autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode);
_startMs = Environment.TickCount64; _startMs = Environment.TickCount64;
_runAccepted = false;
return true; return true;
} }
@ -255,8 +258,15 @@ internal static class Duty
} }
if (!autoDutyIpc.IsStopped()) if (!autoDutyIpc.IsStopped())
{ {
_runAccepted = true;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (!_runAccepted)
{
logger.LogError("AutoDuty did not start duty {CfcId} (territory {TerritoryId}) - Run was rejected, check the AutoDuty log [{Installation}]", base.Task.ContentFinderConditionId, contentFinderConditionData.TerritoryId, autoDutyIpc.DescribeInstallation());
chatGui.PrintError("AutoDuty did not start the duty (rejected the run), check the AutoDuty log.", "Questionable", 576);
return ETaskResult.End;
}
if (Environment.TickCount64 - _startMs < 10000) if (Environment.TickCount64 - _startMs < 10000)
{ {
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
@ -277,7 +287,7 @@ internal static class Duty
} }
} }
internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : ITask internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : IDutyTask, ITask
{ {
public override string ToString() public override string ToString()
{ {
@ -433,17 +443,7 @@ internal static class Duty
{ {
protected override bool Start() protected override bool Start()
{ {
CombatController.CombatData combatData = new CombatController.CombatData if (rsrModule.StartForDuty())
{
ElementId = null,
Sequence = 0,
CompletionQuestVariablesFlags = new List<QuestWorkValue>(),
SpawnType = EEnemySpawnType.None,
KillEnemyDataIds = new List<uint>(),
ComplexCombatDatas = new List<ComplexCombatData>(),
CombatItemUse = null
};
if (rsrModule.Start(combatData))
{ {
logger.LogDebug("Enabled RSR Henched mode for AutoDuty run"); logger.LogDebug("Enabled RSR Henched mode for AutoDuty run");
return true; return true;

View file

@ -92,6 +92,10 @@ internal static class EquipRecommended
private bool _smartApplied; private bool _smartApplied;
private bool _useDirectEquip;
private uint _smartClassJobId;
private int _directEquipCursor; private int _directEquipCursor;
private bool _directEquipStarted; private bool _directEquipStarted;
@ -150,8 +154,9 @@ internal static class EquipRecommended
EClassJob job = (EClassJob)rowId; EClassJob job = (EClassJob)rowId;
StatPriority priority = StatPriority.ForJob(job); StatPriority priority = StatPriority.ForJob(job);
byte playerLevel = GetPlayerLevel(job); byte playerLevel = GetPlayerLevel(job);
RaptureGearsetModule.GearsetEntry* entry = (RaptureGearsetModule.GearsetEntry*)Unsafe.AsPointer(in ptr->Entries[currentGearsetIndex]); _smartClassJobId = rowId;
if (!TryComputeUpgradesAndMoves(job, priority, playerLevel, entry, out _smartUpgrades, out _smartMoves)) _useDirectEquip = configuration.General.PreserveGearset || !CurrentGearsetMatchesEquippedItems();
if (!TryComputeUpgradesAndMoves(job, priority, playerLevel, out _smartUpgrades, out _smartMoves))
{ {
_smartUpgrades = null; _smartUpgrades = null;
_smartMoves = null; _smartMoves = null;
@ -196,7 +201,11 @@ internal static class EquipRecommended
} }
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (configuration.General.PreserveGearset) if (!_useDirectEquip && !CurrentGearsetMatchesEquippedItems())
{
_useDirectEquip = true;
}
if (_useDirectEquip)
{ {
return DirectEquipPhase(); return DirectEquipPhase();
} }
@ -403,10 +412,20 @@ internal static class EquipRecommended
return true; return true;
} }
private unsafe bool TryComputeUpgradesAndMoves(EClassJob job, StatPriority priority, byte playerLevel, RaptureGearsetModule.GearsetEntry* entry, out List<(int Slot, BestItemRef Item)> upgrades, out List<PendingMove> moves) private unsafe bool TryComputeUpgradesAndMoves(EClassJob job, StatPriority priority, byte playerLevel, out List<(int Slot, BestItemRef Item)> upgrades, out List<PendingMove> moves)
{ {
upgrades = new List<(int, BestItemRef)>(); upgrades = new List<(int, BestItemRef)>();
moves = new List<PendingMove>(); moves = new List<PendingMove>();
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return false;
}
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
return false;
}
BestItemRef? bestItemRef = null; BestItemRef? bestItemRef = null;
bool flag = job.IsCrafter() || job.IsGatherer(); bool flag = job.IsCrafter() || job.IsGatherer();
Dictionary<InventoryType, int> cursors = new Dictionary<InventoryType, int>(); Dictionary<InventoryType, int> cursors = new Dictionary<InventoryType, int>();
@ -434,7 +453,8 @@ internal static class EquipRecommended
{ {
bestItemRef = bestItemRef2; bestItemRef = bestItemRef2;
} }
if (((ItemHandle)entry->GetItem((RaptureGearsetModule.GearsetItemIndex)i).ItemId).Id == ((ItemHandle)bestItemRef2.Value.ItemId).Id) InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (((inventorySlot != null) ? ItemHandle.FromInventorySlot(inventorySlot).Id : 0) == ((ItemHandle)bestItemRef2.Value.ItemId).Id)
{ {
continue; continue;
} }
@ -457,6 +477,48 @@ internal static class EquipRecommended
return true; return true;
} }
private unsafe bool CurrentGearsetMatchesEquippedItems()
{
RaptureGearsetModule* ptr = RaptureGearsetModule.Instance();
if (ptr == null)
{
return false;
}
int currentGearsetIndex = ptr->CurrentGearsetIndex;
if (currentGearsetIndex < 0 || !ptr->IsValidGearset(currentGearsetIndex))
{
return false;
}
RaptureGearsetModule.GearsetEntry* ptr2 = (RaptureGearsetModule.GearsetEntry*)Unsafe.AsPointer(in ptr->Entries[currentGearsetIndex]);
if (ptr2->ClassJob != _smartClassJobId)
{
return false;
}
InventoryManager* ptr3 = InventoryManager.Instance();
if (ptr3 == null)
{
return false;
}
InventoryContainer* inventoryContainer = ptr3->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
return false;
}
for (int i = 0; i <= 13; i++)
{
if (i != 5)
{
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
uint num = ((inventorySlot != null) ? ItemHandle.FromInventorySlot(inventorySlot).Id : 0u);
if (((ItemHandle)ptr2->GetItem((RaptureGearsetModule.GearsetItemIndex)i).ItemId).Id != num)
{
return false;
}
}
}
return true;
}
private static bool IsBagContainer(InventoryType type) private static bool IsBagContainer(InventoryType type)
{ {
if (type <= InventoryType.Inventory4) if (type <= InventoryType.Inventory4)

View file

@ -66,6 +66,12 @@ internal static class MeldMateria
return Environment.TickCount64 - _stateChangedAt > (long)(timeoutSeconds * 1000f); return Environment.TickCount64 - _stateChangedAt > (long)(timeoutSeconds * 1000f);
} }
public override void ResetTimeout()
{
base.ResetTimeout();
_stateChangedAt = Environment.TickCount64;
}
private void SetState(EMeldState state) private void SetState(EMeldState state)
{ {
_state = state; _state = state;

View file

@ -193,7 +193,7 @@ internal static class SinglePlayerDuty
} }
} }
internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : ITask internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : IDutyTask, ITask
{ {
public override string ToString() public override string ToString()
{ {
@ -302,7 +302,7 @@ internal static class SinglePlayerDuty
} }
} }
internal sealed record CheckDutyOutcome(ElementId QuestId, byte SequenceBeforeEntering, IReadOnlyList<byte>? VariablesBeforeEntering, IList<QuestWorkValue?> CompletionQuestVariablesFlags, IReadOnlyList<ITask> DutyTasks) : ITask internal sealed record CheckDutyOutcome(ElementId QuestId, byte SequenceBeforeEntering, IReadOnlyList<byte>? VariablesBeforeEntering, IList<QuestWorkValue?> CompletionQuestVariablesFlags, IReadOnlyList<ITask> DutyTasks) : IDutyTask, ITask
{ {
public override string ToString() public override string ToString()
{ {

View file

@ -32,6 +32,7 @@ internal static class AethernetRide
private enum EAethernetPhase private enum EAethernetPhase
{ {
None, None,
WaitingForPlayer,
Mounting, Mounting,
Moving, Moving,
Unmounting, Unmounting,
@ -72,6 +73,12 @@ internal static class AethernetRide
return Environment.TickCount64 - _phaseStartedAt > (long)(timeoutSeconds * 1000f); return Environment.TickCount64 - _phaseStartedAt > (long)(timeoutSeconds * 1000f);
} }
public override void ResetTimeout()
{
base.ResetTimeout();
_phaseStartedAt = Environment.TickCount64;
}
protected override bool Start() protected override bool Start()
{ {
SetPhase(EAethernetPhase.None); SetPhase(EAethernetPhase.None);
@ -79,22 +86,37 @@ internal static class AethernetRide
aethernetTeleportService.Reset(); aethernetTeleportService.Reset();
if (aetheryteFunctions.IsAetheryteUnlocked(base.Task.From) && aetheryteFunctions.IsAetheryteUnlocked(base.Task.To)) if (aetheryteFunctions.IsAetheryteUnlocked(base.Task.From) && aetheryteFunctions.IsAetheryteUnlocked(base.Task.To))
{ {
uint territoryType = clientState.TerritoryType;
IPlayerCharacter localPlayer = objectTable.LocalPlayer; IPlayerCharacter localPlayer = objectTable.LocalPlayer;
if (localPlayer == null) if (localPlayer == null)
{
SetPhase(EAethernetPhase.WaitingForPlayer);
return true;
}
Vector3 position = localPlayer.Position;
if (HasArrived(position))
{ {
return false; return false;
} }
Vector3 playerPosition = localPlayer.Position; StartFromCurrentPosition(position);
if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.To)) return true;
}
if (clientState.TerritoryType == aetheryteData.TerritoryIds[base.Task.To])
{ {
logger.LogWarning("Aethernet ride not unlocked (from: {FromAetheryte}, to: {ToAetheryte}), skipping as we are already in the destination territory", base.Task.From, base.Task.To);
return false;
}
throw new TaskException($"Aethernet ride not unlocked (from: {base.Task.From}, to: {base.Task.To})");
}
private void StartFromCurrentPosition(Vector3 playerPosition)
{
uint territoryType = clientState.TerritoryType;
float num = (base.Task.From.IsFirmamentAetheryte() ? 11f : (AetheryteConverter.IsLargeAetheryte(base.Task.From) ? 11f : 4f)); float num = (base.Task.From.IsFirmamentAetheryte() ? 11f : (AetheryteConverter.IsLargeAetheryte(base.Task.From) ? 11f : 4f));
if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < num) if (aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) < num)
{ {
BeginTeleport(); BeginTeleport();
return true;
} }
if (base.Task.From == EAetheryteLocation.SolutionNine) else if (base.Task.From == EAetheryteLocation.SolutionNine)
{ {
logger.LogDebug("Moving to S9 aetheryte"); logger.LogDebug("Moving to S9 aetheryte");
int num2 = 4; int num2 = 4;
@ -108,27 +130,16 @@ internal static class AethernetRide
Vector3 to = list.MinBy((Vector3 x) => Vector3.Distance(playerPosition, x)); Vector3 to = list.MinBy((Vector3 x) => Vector3.Distance(playerPosition, x));
SetPhase(EAethernetPhase.Moving); SetPhase(EAethernetPhase.Moving);
movementController.NavigateTo(EMovementType.Quest, (uint)base.Task.From, to, fly: false, sprint: true, 0.25f); 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() && gameFunctions.Mount()) else if (territoryData.CanUseMount(territoryType) && aetheryteData.CalculateDistance(playerPosition, territoryType, base.Task.From) > 30f && !gameFunctions.HasStatusPreventingMount() && gameFunctions.Mount())
{ {
SetPhase(EAethernetPhase.Mounting); SetPhase(EAethernetPhase.Mounting);
_continueAt = Environment.TickCount64 + 500; _continueAt = Environment.TickCount64 + 500;
return true;
}
StartMoving();
return true;
}
} }
else else
{ {
if (clientState.TerritoryType != aetheryteData.TerritoryIds[base.Task.To]) StartMoving();
{
throw new TaskException($"Aethernet ride not unlocked (from: {base.Task.From}, to: {base.Task.To})");
} }
logger.LogWarning("Aethernet ride 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 StartMoving() private void StartMoving()
@ -178,6 +189,20 @@ internal static class AethernetRide
} }
switch (_phase) switch (_phase)
{ {
case EAethernetPhase.WaitingForPlayer:
{
Vector3? vector2 = objectTable.LocalPlayer?.Position;
if (!vector2.HasValue)
{
return ETaskResult.StillRunning;
}
if (HasArrived(vector2.Value))
{
return ETaskResult.TaskComplete;
}
StartFromCurrentPosition(vector2.Value);
return ETaskResult.StillRunning;
}
case EAethernetPhase.Mounting: case EAethernetPhase.Mounting:
if (condition[ConditionFlag.Mounted]) if (condition[ConditionFlag.Mounted])
{ {
@ -228,21 +253,7 @@ internal static class AethernetRide
{ {
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (aetheryteData.IsAirshipLanding(base.Task.To)) if (!HasArrived(vector.Value))
{
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.StillRunning;
} }
@ -254,6 +265,15 @@ internal static class AethernetRide
} }
} }
private bool HasArrived(Vector3 position)
{
if (!aetheryteData.IsAirshipLanding(base.Task.To))
{
return aetheryteData.CalculateDistance(position, clientState.TerritoryType, base.Task.To) <= 20f;
}
return aetheryteData.CalculateAirshipLandingDistance(position, clientState.TerritoryType, base.Task.To) <= 5f;
}
public override bool ShouldInterruptOnDamage() public override bool ShouldInterruptOnDamage()
{ {
return true; return true;

View file

@ -16,33 +16,85 @@ namespace Questionable.Controller.Steps.Shared;
internal static class RedeemRewardItems internal static class RedeemRewardItems
{ {
internal sealed class Factory(QuestData questData, Configuration configuration) : ITaskFactory internal sealed class Factory(QuestData questData, Configuration configuration, ILogger<Factory> logger) : ITaskFactory
{ {
public unsafe IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
if (step.InteractionType != EInteractionType.AcceptQuest) if (step.InteractionType != EInteractionType.AcceptQuest)
{ {
return Array.Empty<ITask>(); return Array.Empty<ITask>();
} }
List<ITask> list = new List<ITask>(); return CreateTasks(questData, configuration, logger);
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return list;
}
bool hasFreeInventorySlot = InventoryHelper.HasFreeInventorySlot();
foreach (ItemReward redeemableItem in questData.RedeemableItems)
{
if (ptr->GetInventoryItemCount(redeemableItem.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) != 0 && !redeemableItem.IsUnlocked() && PassesRedemptionFilters(redeemableItem.Type, redeemableItem.IsUntradable, configuration.General, hasFreeInventorySlot))
{
list.Add(new Task(redeemableItem));
} }
} }
if (list.Count > 0)
internal sealed class CompletionFactory : ITaskFactory
{ {
list.Insert(0, new Mount.UnmountTask()); public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.CompleteQuest)
{
return Array.Empty<ITask>();
} }
return list; return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new ScanAfterQuestCompletion(step.TurnInQuestId ?? quest.Id));
}
}
internal sealed record ScanAfterQuestCompletion(ElementId ElementId) : ITask
{
public override string ToString()
{
return $"ScanRewardsAfterQuestCompletion({ElementId})";
}
}
internal sealed class ScanAfterQuestCompletionExecutor(QuestFunctions questFunctions, QuestData questData, Configuration configuration, ILogger<ScanAfterQuestCompletionExecutor> logger) : TaskExecutor<ScanAfterQuestCompletion>(), IExtraTaskCreator, ITaskExecutor
{
private const long InventorySettleDelayMs = 1000L;
private long? _scanAt;
private List<ITask> _tasks = new List<ITask>();
protected override bool Start()
{
_scanAt = null;
_tasks = new List<ITask>();
return true;
}
public override ETaskResult Update()
{
if (!questFunctions.IsQuestComplete(base.Task.ElementId))
{
return ETaskResult.StillRunning;
}
long tickCount = Environment.TickCount64;
if (!_scanAt.HasValue)
{
_scanAt = tickCount + 1000;
return ETaskResult.StillRunning;
}
if (tickCount < _scanAt)
{
return ETaskResult.StillRunning;
}
_tasks = CreateTasks(questData, configuration, logger);
if (_tasks.Count <= 0)
{
return ETaskResult.TaskComplete;
}
return ETaskResult.CreateNewTasks;
}
public IEnumerable<ITask> CreateExtraTasks()
{
return _tasks;
}
public override bool ShouldInterruptOnDamage()
{
return false;
} }
} }
@ -182,6 +234,36 @@ internal static class RedeemRewardItems
} }
} }
private unsafe static List<ITask> CreateTasks(QuestData questData, Configuration configuration, ILogger logger)
{
List<ITask> list = new List<ITask>();
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{
return list;
}
bool flag = InventoryHelper.HasFreeInventorySlot();
foreach (ItemReward redeemableItem in questData.RedeemableItems)
{
if (ptr->GetInventoryItemCount(redeemableItem.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) != 0 && !redeemableItem.IsUnlocked())
{
if (!PassesRedemptionFilters(redeemableItem.Type, redeemableItem.IsUntradable, configuration.General, flag))
{
logger.LogDebug("Skipping quest reward {ItemName}: type={RewardType}, untradable={IsUntradable}, freeInventorySlot={HasFreeInventorySlot}", redeemableItem.Name, redeemableItem.Type, redeemableItem.IsUntradable, flag);
}
else
{
list.Add(new Task(redeemableItem));
}
}
}
if (list.Count > 0)
{
list.Insert(0, new Mount.UnmountTask());
}
return list;
}
internal static bool PassesRedemptionFilters(EItemRewardType type, bool isUntradable, Configuration.GeneralConfiguration general, bool hasFreeInventorySlot) internal static bool PassesRedemptionFilters(EItemRewardType type, bool isUntradable, Configuration.GeneralConfiguration general, bool hasFreeInventorySlot)
{ {
if (general.DisabledRewardTypes.Contains(type)) if (general.DisabledRewardTypes.Contains(type))

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib.GameData;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Utils; using Questionable.Controller.Utils;
using Questionable.Data; using Questionable.Data;
@ -61,19 +62,19 @@ internal static class WaitAtEnd
{ {
break; break;
} }
goto IL_01bf; goto IL_01ad;
case EInteractionType.SinglePlayerDuty: case EInteractionType.SinglePlayerDuty:
if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions)) if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions))
{ {
break; break;
} }
goto IL_01bf; goto IL_01ad;
case EInteractionType.Fish: case EInteractionType.Fish:
if (autoHookIpc.IsAvailable()) if (!autoHookIpc.IsAvailable())
{ {
break; return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new EndAutomation());
} }
goto IL_01bf; break;
case EInteractionType.WalkTo: case EInteractionType.WalkTo:
case EInteractionType.Jump: case EInteractionType.Jump:
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(Next(quest, sequence)); return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(Next(quest, sequence));
@ -91,13 +92,13 @@ internal static class WaitAtEnd
{ {
break; break;
} }
goto IL_02a6; goto IL_02ca;
case EInteractionType.UseItem: case EInteractionType.UseItem:
if (!step.TargetTerritoryId.HasValue) if (!step.TargetTerritoryId.HasValue)
{ {
break; break;
} }
goto IL_02a6; goto IL_02ca;
case EInteractionType.AcceptQuest: case EInteractionType.AcceptQuest:
{ {
WaitQuestAccepted waitQuestAccepted = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id); WaitQuestAccepted waitQuestAccepted = new WaitQuestAccepted(step.PickUpQuestId ?? quest.Id);
@ -128,9 +129,14 @@ internal static class WaitAtEnd
} }
return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { waitQuestCompleted, waitDelay2 }); return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[2] { waitQuestCompleted, waitDelay2 });
} }
IL_01bf: IL_01ad:
return new global::_003C_003Ez__ReadOnlySingleElementList<ITask>(new EndAutomation()); return new global::_003C_003Ez__ReadOnlyArray<ITask>(new ITask[3]
IL_02a6: {
new WaitManualDuty(),
new WaitDelay(),
Next(quest, sequence)
});
IL_02ca:
if (step.TerritoryId != step.TargetTerritoryId) if (step.TerritoryId != step.TargetTerritoryId)
{ {
task2 = new WaitCondition.Task(() => clientState.TerritoryType == step.TargetTerritoryId, "Wait(tp to territory: " + territoryData.GetNameAndId(step.TargetTerritoryId.Value) + ")"); task2 = new WaitCondition.Task(() => clientState.TerritoryType == step.TargetTerritoryId, "Wait(tp to territory: " + territoryData.GetNameAndId(step.TargetTerritoryId.Value) + ")");
@ -369,6 +375,58 @@ internal static class WaitAtEnd
} }
} }
internal sealed record WaitManualDuty : IDutyTask, ITask
{
public override string ToString()
{
return "Wait(manual duty)";
}
}
internal sealed class WaitManualDutyExecutor(IClientState clientState, ICondition condition, TerritoryData territoryData) : TaskExecutor<WaitManualDuty>(), IDebugStateProvider, ITaskExecutor
{
private bool _enteredDuty;
protected override bool Start()
{
_enteredDuty = false;
return true;
}
public override ETaskResult Update()
{
uint territoryType = clientState.TerritoryType;
bool flag = territoryData.IsDutyInstance(territoryType) && !territoryData.IsFieldOperation(territoryType);
if (!_enteredDuty)
{
if (flag)
{
_enteredDuty = true;
}
return ETaskResult.StillRunning;
}
if (flag || ConditionHelper.IsBetweenAreas(condition))
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
public string? GetDebugState()
{
if (!_enteredDuty)
{
return "Waiting for you to enter the duty";
}
return "Waiting for the duty to finish";
}
}
internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask, ITask internal sealed record NextStep(ElementId ElementId, int Sequence) : ILastTask, ITask
{ {
public override string ToString() public override string ToString()

View file

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

View file

@ -16,6 +16,8 @@ internal interface ITaskExecutor
bool WasInterrupted(); bool WasInterrupted();
void ResetTimeout();
bool HasTimedOut(float timeoutSeconds); bool HasTimedOut(float timeoutSeconds);
ETaskResult Update(); ETaskResult Update();

View file

@ -32,6 +32,11 @@ internal abstract class TaskExecutor<T> : ITaskExecutor where T : class, ITask
return Environment.TickCount64 - _lastProgressAt > (long)(timeoutSeconds * 1000f); return Environment.TickCount64 - _lastProgressAt > (long)(timeoutSeconds * 1000f);
} }
public virtual void ResetTimeout()
{
ResetProgressTimer();
}
protected void ResetProgressTimer() protected void ResetProgressTimer()
{ {
_lastProgressAt = Environment.TickCount64; _lastProgressAt = Environment.TickCount64;

View file

@ -786,19 +786,19 @@ internal sealed class CustomDeliveryController : MiniTaskController<CustomDelive
if (!_npcPositions.TryGetValue(npcIndex, out (ushort, Vector3) value)) if (!_npcPositions.TryGetValue(npcIndex, out (ushort, Vector3) value))
{ {
_logger.LogWarning("No position found for delivery NPC index {NpcIndex}, skipping navigation", npcIndex); _logger.LogWarning("No position found for delivery NPC index {NpcIndex}, skipping navigation", npcIndex);
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
return; return;
} }
uint valueOrDefault = _npcDataIds.GetValueOrDefault(npcIndex); uint valueOrDefault = _npcDataIds.GetValueOrDefault(npcIndex);
_routeEnqueuer.SetDestination(value.Item1, value.Item2);
if (_clientState.TerritoryType != value.Item1 && !_routeEnqueuer.TryEnqueueToTerritory(_taskQueue, value.Item1, value.Item2)) if (_clientState.TerritoryType != value.Item1 && !_routeEnqueuer.TryEnqueueToTerritory(_taskQueue, value.Item1, value.Item2))
{ {
_logger.LogWarning("Could not route to delivery NPC territory {TerritoryId}; falling back to normal routing", value.Item1); _logger.LogWarning("Could not route to delivery NPC territory {TerritoryId}; falling back to normal routing", value.Item1);
EnqueueNavigateTo(value.Item1, value.Item2); EnqueueNavigateTo(value.Item1, value.Item2);
} }
else _taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
{
_taskQueue.Enqueue(new DeliveryNpcApproachTask(value.Item1, value.Item2, valueOrDefault)); _taskQueue.Enqueue(new DeliveryNpcApproachTask(value.Item1, value.Item2, valueOrDefault));
} }
}
private void EnqueueNavigateTo(ushort territoryId, Vector3 position) private void EnqueueNavigateTo(ushort territoryId, Vector3 position)
{ {
@ -808,12 +808,11 @@ internal sealed class CustomDeliveryController : MiniTaskController<CustomDelive
private void EnqueueTurnInSequence(DeliveryPlan plan) private void EnqueueTurnInSequence(DeliveryPlan plan)
{ {
EnqueueNavigateToNpc(plan.NpcIndex); EnqueueNavigateToNpc(plan.NpcIndex);
_taskQueue.Enqueue(new Questionable.Controller.Steps.Common.Mount.UnmountTask());
if (!_npcDataIds.ContainsKey(plan.NpcIndex)) if (!_npcDataIds.ContainsKey(plan.NpcIndex))
{ {
_logger.LogWarning("No DataId for delivery NPC index {NpcIndex}, turn-in may fail", plan.NpcIndex); _logger.LogWarning("No DataId for delivery NPC index {NpcIndex}, turn-in may fail", plan.NpcIndex);
} }
_taskQueue.Enqueue(new SatisfactionSupplyTurnInTask(plan.NpcIndex, plan.Slot, plan.DeliveryCount, _npcDataIds.GetValueOrDefault(plan.NpcIndex))); _taskQueue.Enqueue(new SatisfactionSupplyTurnInTask(plan.NpcIndex, plan.Slot, plan.DeliveryCount, _npcDataIds.GetValueOrDefault(plan.NpcIndex), plan.ItemId, plan.Collectability));
_taskQueue.Enqueue(new WaitAtEnd.WaitDelay()); _taskQueue.Enqueue(new WaitAtEnd.WaitDelay());
} }

View file

@ -7,6 +7,7 @@ using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LLib; using LLib;
using LLib.GameData;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -106,7 +107,11 @@ internal abstract class MiniTaskController<T> : IDisposable
InterruptQueueWithCombat(); InterruptQueueWithCombat();
return; return;
} }
if (currentTaskExecutor.HasTimedOut(_configuration.Advanced.InteractionTimeoutSeconds)) if (ConditionHelper.IsInCutscene(_condition))
{
currentTaskExecutor.ResetTimeout();
}
else if (currentTaskExecutor.HasTimedOut(_configuration.Advanced.InteractionTimeoutSeconds))
{ {
_logger.LogWarning("Task {TaskName} timed out after {Timeout}s", currentTask, _configuration.Advanced.InteractionTimeoutSeconds); _logger.LogWarning("Task {TaskName} timed out after {Timeout}s", currentTask, _configuration.Advanced.InteractionTimeoutSeconds);
if (_condition[ConditionFlag.InCombat]) if (_condition[ConditionFlag.InCombat])

View file

@ -61,6 +61,11 @@ internal sealed class QuestController : MiniTaskController<QuestController>
} }
} }
internal void ResetInitialQuestWork(QuestProgressInfo? questWork)
{
InitialQuestWork = questWork;
}
public void SetSequence(byte sequence, int step = 0) public void SetSequence(byte sequence, int step = 0)
{ {
Sequence = sequence; Sequence = sequence;
@ -180,6 +185,8 @@ internal sealed class QuestController : MiniTaskController<QuestController>
private QuestProgress? _pendingQuest; private QuestProgress? _pendingQuest;
private (ElementId QuestId, QuestProgressInfo QuestWork)? _initialQuestWorkPreservedForReload;
private EAutomationType _automationType; private EAutomationType _automationType;
private bool _commandAfterStopFired; private bool _commandAfterStopFired;
@ -210,6 +217,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
private bool _resumeAfterSideTasks; private bool _resumeAfterSideTasks;
private bool _resumeAfterDutyExit;
private bool _resumeAfterOccupiedQuestTransition;
private bool _runningDebugTasks; private bool _runningDebugTasks;
private const char ClipboardSeparator = ';'; private const char ClipboardSeparator = ';';
@ -360,6 +371,13 @@ internal sealed class QuestController : MiniTaskController<QuestController>
Dictionary<GatheringPointId, GatheringRoot> gatheringPoints = _gatheringPointRegistry.Build(); Dictionary<GatheringPointId, GatheringRoot> gatheringPoints = _gatheringPointRegistry.Build();
_framework.RunOnFrameworkThread(delegate _framework.RunOnFrameworkThread(delegate
{ {
QuestProgress questProgress = CurrentQuestDetails?.Progress;
QuestProgressInfo questProgressInfo = questProgress?.InitialQuestWork;
if (questProgress != null && questProgressInfo == null)
{
questProgressInfo = _questFunctions.GetQuestProgressInfo(questProgress.Quest.Id);
}
_initialQuestWorkPreservedForReload = ((questProgress != null && questProgressInfo != null) ? new(ElementId, QuestProgressInfo)?((questProgress.Quest.Id, questProgressInfo)) : (((ElementId, QuestProgressInfo)?)null));
ResetInternalState(); ResetInternalState();
_gatheringPointRegistry.Publish(gatheringPoints); _gatheringPointRegistry.Publish(gatheringPoints);
_questRegistry.Publish(questSnapshot); _questRegistry.Publish(questSnapshot);
@ -373,6 +391,8 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_pendingQuest = null; _pendingQuest = null;
_simulatedQuest = null; _simulatedQuest = null;
_safeAnimationEnd = 0L; _safeAnimationEnd = 0L;
_resumeAfterDutyExit = false;
_resumeAfterOccupiedQuestTransition = false;
DebugState = null; DebugState = null;
} }
@ -501,6 +521,28 @@ internal sealed class QuestController : MiniTaskController<QuestController>
DebugState = "Not logged in"; DebugState = "Not logged in";
return; return;
} }
if (_resumeAfterDutyExit)
{
if (ConditionHelper.IsBetweenAreas(_condition) || IsInsideDutyInstance())
{
DebugState = "Waiting for duty exit";
return;
}
_resumeAfterDutyExit = false;
CheckNextTasks("Resume after duty exit");
return;
}
if (_resumeAfterOccupiedQuestTransition)
{
if (_gameFunctions.IsOccupied())
{
DebugState = "Waiting for quest transition to finish";
return;
}
_resumeAfterOccupiedQuestTransition = false;
CheckNextTasks("Resume after occupied quest transition");
return;
}
if (_runningDebugTasks) if (_runningDebugTasks)
{ {
if (!_taskQueue.AllTasksComplete) if (!_taskQueue.AllTasksComplete)
@ -638,9 +680,33 @@ internal sealed class QuestController : MiniTaskController<QuestController>
} }
else if (questProgress.Sequence != b) else if (questProgress.Sequence != b)
{ {
if (IsDutyChainActive())
{
DebugState = "Waiting for duty to finish";
return;
}
questProgress.SetSequence(b); questProgress.SetSequence(b);
QuestProgressInfo questProgressInfo = _questFunctions.GetQuestProgressInfo(questProgress.Quest.Id);
questProgress.ResetInitialQuestWork(questProgressInfo);
_logger.LogDebug("Captured initial quest variables for {QuestId} sequence {Sequence}: {QuestWork}", questProgress.Quest.Id, b, questProgressInfo);
bool flag = _gameFunctions.IsOccupied();
if (flag)
{
EAutomationType automationType = AutomationType;
bool flag2 = (uint)(automationType - 1) <= 2u;
flag = flag2;
}
if (flag)
{
ClearTasksInternal();
_resumeAfterOccupiedQuestTransition = true;
DebugState = "Waiting for quest transition to finish";
}
else
{
CheckNextTasks($"New sequence {questProgress == _startedQuest}"); CheckNextTasks($"New sequence {questProgress == _startedQuest}");
} }
}
else if (questProgress.Step == 255) else if (questProgress.Step == 255)
{ {
DebugState = $"Waiting for sequence update (current: {questProgress.Sequence})"; DebugState = $"Waiting for sequence update (current: {questProgress.Sequence})";
@ -770,6 +836,25 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return (null, 0); return (null, 0);
} }
private bool IsInsideDutyInstance()
{
uint territoryType = _clientState.TerritoryType;
if (_territoryData.IsDutyInstance(territoryType))
{
return !_territoryData.IsFieldOperation(territoryType);
}
return false;
}
private bool IsDutyChainActive()
{
if (!(_taskQueue.CurrentTaskExecutor?.CurrentTask is IDutyTask))
{
return _taskQueue.RemainingTasks.Any((ITask x) => x is IDutyTask);
}
return true;
}
private bool IsLevelingModeActive() private bool IsLevelingModeActive()
{ {
ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask; ITask task = _taskQueue.CurrentTaskExecutor?.CurrentTask;
@ -890,6 +975,8 @@ internal sealed class QuestController : MiniTaskController<QuestController>
_deathCount = 0; _deathCount = 0;
_deathRecoveryPending = false; _deathRecoveryPending = false;
_resumeAfterSideTasks = false; _resumeAfterSideTasks = false;
_resumeAfterDutyExit = false;
_resumeAfterOccupiedQuestTransition = false;
SessionConditions.Clear(); SessionConditions.Clear();
_conditionsMetAtStart.Clear(); _conditionsMetAtStart.Clear();
} }
@ -1179,12 +1266,29 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{ {
return null; return null;
} }
questProgress.CaptureInitialQuestWork(_questFunctions.GetQuestProgressInfo(questId));
return questProgress.InitialQuestWork; return questProgress.InitialQuestWork;
} }
private QuestProgress CreateQuestProgress(Quest quest, byte sequence = 0, int step = 0) private QuestProgress CreateQuestProgress(Quest quest, byte sequence = 0, int step = 0)
{ {
return new QuestProgress(quest, sequence, step, _questFunctions.GetQuestProgressInfo(quest.Id)); (ElementId, QuestProgressInfo)? initialQuestWorkPreservedForReload = _initialQuestWorkPreservedForReload;
QuestProgressInfo questProgressInfo;
if (initialQuestWorkPreservedForReload.HasValue)
{
(ElementId, QuestProgressInfo) valueOrDefault = initialQuestWorkPreservedForReload.GetValueOrDefault();
if (valueOrDefault.Item1.Equals(quest.Id))
{
questProgressInfo = valueOrDefault.Item2;
_initialQuestWorkPreservedForReload = null;
_logger.LogDebug("Restored initial quest variables for {QuestId} after data reload: {QuestWork}", quest.Id, questProgressInfo);
goto IL_0075;
}
}
questProgressInfo = _questFunctions.GetQuestProgressInfo(quest.Id);
goto IL_0075;
IL_0075:
return new QuestProgress(quest, sequence, step, questProgressInfo);
} }
private void CaptureInitialQuestWork(QuestProgress? progress, ElementId questId) private void CaptureInitialQuestWork(QuestProgress? progress, ElementId questId)
@ -1257,8 +1361,9 @@ internal sealed class QuestController : MiniTaskController<QuestController>
return; return;
} }
} }
if (questStep != null && questStep.TerritoryId != _clientState.TerritoryType && (_territoryData.IsDutyInstance(_clientState.TerritoryType) || _territoryData.IsQuestBattleInstance(_clientState.TerritoryType))) if (questStep != null && questStep.TerritoryId != _clientState.TerritoryType && IsInsideDutyInstance())
{ {
_resumeAfterDutyExit = true;
_logger.LogDebug("Deferring next step: step territory {StepTerritory} != current instance territory {CurrentTerritory}, waiting for duty exit", questStep.TerritoryId, _clientState.TerritoryType); _logger.LogDebug("Deferring next step: step territory {StepTerritory} != current instance territory {CurrentTerritory}, waiting for duty exit", questStep.TerritoryId, _clientState.TerritoryType);
DebugState = "Waiting for duty exit"; DebugState = "Waiting for duty exit";
return; return;

View file

@ -84,7 +84,7 @@ internal sealed class QuestPriorityResolver
{ {
return QuestResolution.Slot(EQuestResolutionType.Simulated, slots.Simulated, "Simulated quest"); return QuestResolution.Slot(EQuestResolutionType.Simulated, slots.Simulated, "Simulated quest");
} }
if (slots.Next != null) if (slots.Next != null && !_questFunctions.IsQuestBlacklisted(slots.Next.Quest.Id))
{ {
return QuestResolution.Slot(EQuestResolutionType.NextQuest, slots.Next, $"Next quest {slots.Next.Quest.Id}"); return QuestResolution.Slot(EQuestResolutionType.NextQuest, slots.Next, $"Next quest {slots.Next.Quest.Id}");
} }
@ -93,7 +93,7 @@ internal sealed class QuestPriorityResolver
private QuestResolution ResolveFromGameState(List<Quest> manualPriorityQuests, QuestController.EAutomationType automationType) private QuestResolution ResolveFromGameState(List<Quest> manualPriorityQuests, QuestController.EAutomationType automationType)
{ {
bool allowNewMsq = automationType != QuestController.EAutomationType.SingleQuestB; bool flag = automationType != QuestController.EAutomationType.SingleQuestB;
QuestReference resolvedMsqQuest = GetResolvedMsqQuest(); QuestReference resolvedMsqQuest = GetResolvedMsqQuest();
int num = _objectTable.LocalPlayer?.Level ?? 0; int num = _objectTable.LocalPlayer?.Level ?? 0;
EClassJob valueOrDefault = ((EClassJob?)_objectTable.LocalPlayer?.ClassJob.RowId).GetValueOrDefault(); EClassJob valueOrDefault = ((EClassJob?)_objectTable.LocalPlayer?.ClassJob.RowId).GetValueOrDefault();
@ -103,6 +103,16 @@ internal sealed class QuestPriorityResolver
QuestResolution valueOrDefault2 = questResolution.GetValueOrDefault(); QuestResolution valueOrDefault2 = questResolution.GetValueOrDefault();
return ApplyQuestRedirects(valueOrDefault2); return ApplyQuestRedirects(valueOrDefault2);
} }
uint territoryType = _clientState.TerritoryType;
bool flag2 = territoryType - 181 <= 2;
if (flag2 && flag && _configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Manual)
{
ElementId currentQuest = resolvedMsqQuest.CurrentQuest;
if ((object)currentQuest != null && currentQuest.Value > 0 && !_questFunctions.IsQuestAccepted(currentQuest))
{
return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, currentQuest, resolvedMsqQuest.Sequence, resolvedMsqQuest.State, $"Starting city opening MSQ {currentQuest}");
}
}
questResolution = TryResolveInProgressClassQuest(valueOrDefault, resolvedMsqQuest.State); questResolution = TryResolveInProgressClassQuest(valueOrDefault, resolvedMsqQuest.State);
if (questResolution.HasValue) if (questResolution.HasValue)
{ {
@ -130,7 +140,7 @@ internal sealed class QuestPriorityResolver
_lastLoggedPriorityClassQuest = null; _lastLoggedPriorityClassQuest = null;
_loggedNoClassQuestsAvailable = false; _loggedNoClassQuestsAvailable = false;
_loggedAdventurerClass = false; _loggedAdventurerClass = false;
questResolution = TryResolveMsqImmediate(resolvedMsqQuest, allowNewMsq); questResolution = TryResolveMsqImmediate(resolvedMsqQuest, flag);
if (questResolution.HasValue) if (questResolution.HasValue)
{ {
return questResolution.GetValueOrDefault(); return questResolution.GetValueOrDefault();
@ -147,7 +157,7 @@ internal sealed class QuestPriorityResolver
QuestResolution valueOrDefault4 = questResolution.GetValueOrDefault(); QuestResolution valueOrDefault4 = questResolution.GetValueOrDefault();
return ApplyQuestRedirects(valueOrDefault4); return ApplyQuestRedirects(valueOrDefault4);
} }
questResolution = TryResolveMsqFallback(resolvedMsqQuest, allowNewMsq); questResolution = TryResolveMsqFallback(resolvedMsqQuest, flag);
if (questResolution.HasValue) if (questResolution.HasValue)
{ {
return questResolution.GetValueOrDefault(); return questResolution.GetValueOrDefault();
@ -259,8 +269,14 @@ internal sealed class QuestPriorityResolver
{ {
return null; return null;
} }
(ElementId, byte) tuple = (from x in manualPriorityQuests (ElementId, byte) tuple = (from x in manualPriorityQuests.Where(delegate(Quest x)
where _questFunctions.IsQuestAccepted(x.Id) ? ((!_configuration.Advanced.AutoPrioritizeAlliedSocietyRankUp || !(x.Id is QuestId questId) || !_questFunctions.WouldOvercapAlliedSocietyReputation(questId)) ? true : false) : _questFunctions.IsReadyToAcceptQuest(x.Id) {
if (_questFunctions.IsQuestBlacklisted(x.Id))
{
return false;
}
return _questFunctions.IsQuestAccepted(x.Id) ? ((!_configuration.Advanced.AutoPrioritizeAlliedSocietyRankUp || !(x.Id is QuestId questId) || !_questFunctions.WouldOvercapAlliedSocietyReputation(questId)) ? true : false) : _questFunctions.IsReadyToAcceptQuest(x.Id);
})
select (QuestId: x.Id, Sequence: _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault(); select (QuestId: x.Id, Sequence: _questFunctions.GetQuestProgressInfo(x.Id)?.Sequence ?? 0)).FirstOrDefault();
if ((object)tuple.Item1 != null) if ((object)tuple.Item1 != null)
{ {
@ -271,12 +287,12 @@ internal sealed class QuestPriorityResolver
private QuestResolution? TryResolveInProgressClassQuest(EClassJob currentClassJob, MainScenarioQuestState msqState) private QuestResolution? TryResolveInProgressClassQuest(EClassJob currentClassJob, MainScenarioQuestState msqState)
{ {
if (currentClassJob == EClassJob.Adventurer) if (_configuration.Advanced.SkipClassJobQuests || currentClassJob == EClassJob.Adventurer)
{ {
return null; return null;
} }
QuestInfo questInfo = (from x in _questData.GetClassJobQuests(currentClassJob) QuestInfo questInfo = (from x in _questData.GetClassJobQuests(currentClassJob)
where (x.Level <= 5 || !_configuration.Advanced.SkipClassJobQuests) && _questFunctions.IsQuestAccepted(x.QuestId) && !_questFunctions.IsQuestComplete(x.QuestId) where !_questFunctions.IsQuestBlacklisted(x.QuestId) && _questFunctions.IsQuestAccepted(x.QuestId) && !_questFunctions.IsQuestComplete(x.QuestId)
orderby x.Level orderby x.Level
select x).FirstOrDefault(); select x).FirstOrDefault();
if (questInfo == null) if (questInfo == null)
@ -422,11 +438,15 @@ internal sealed class QuestPriorityResolver
{ {
case 1: case 1:
{ {
ElementId elementId = new QuestId(ptr->NormalQuests[trackingWork.Index].QuestId); QuestWork questWork = ptr->NormalQuests[trackingWork.Index];
if (!questWork.IsHidden)
{
ElementId elementId = new QuestId(questWork.QuestId);
if (_questRegistry.IsKnownQuest(elementId) && !_questFunctions.IsQuestBlacklisted(elementId)) if (_questRegistry.IsKnownQuest(elementId) && !_questFunctions.IsQuestBlacklisted(elementId))
{ {
list.Add((elementId, QuestManager.GetQuestSequence(elementId.Value))); list.Add((elementId, QuestManager.GetQuestSequence(elementId.Value)));
} }
}
break; break;
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -198,6 +198,18 @@ internal sealed class TerritoryData : ITerritoryInfo
return false; return false;
} }
public bool IsFieldOperation(uint territoryId)
{
uint value;
bool flag = _dutyTerritories.TryGetValue(territoryId, out value);
if (flag)
{
bool flag2 = ((value == 26 || value == 29 || value == 38) ? true : false);
flag = flag2;
}
return flag;
}
public string? GetInstanceName(uint instanceId) public string? GetInstanceName(uint instanceId)
{ {
return _instanceNames.GetValueOrDefault(instanceId); return _instanceNames.GetValueOrDefault(instanceId);

View file

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Exceptions;
@ -17,6 +19,8 @@ internal sealed class AutoDutyIpc
UnsyncRegular UnsyncRegular
} }
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly TerritoryData _territoryData; private readonly TerritoryData _territoryData;
@ -45,6 +49,7 @@ internal sealed class AutoDutyIpc
public AutoDutyIpc(IDalamudPluginInterface pluginInterface, Configuration configuration, TerritoryData territoryData, ILogger<AutoDutyIpc> logger) public AutoDutyIpc(IDalamudPluginInterface pluginInterface, Configuration configuration, TerritoryData territoryData, ILogger<AutoDutyIpc> logger)
{ {
_pluginInterface = pluginInterface;
_configuration = configuration; _configuration = configuration;
_territoryData = territoryData; _territoryData = territoryData;
_logger = logger; _logger = logger;
@ -210,6 +215,41 @@ internal sealed class AutoDutyIpc
} }
} }
public string DescribeInstallation()
{
List<string> list = (from p in _pluginInterface.InstalledPlugins
where p.InternalName.Contains("AutoDuty", StringComparison.OrdinalIgnoreCase) || p.Name.Contains("AutoDuty", StringComparison.OrdinalIgnoreCase)
select $"{p.InternalName} {p.Version} from '{p.Manifest.InstalledFromUrl}' (loaded: {p.IsLoaded}, dev: {p.IsDev})").ToList();
string value = ((list.Count > 0) ? string.Join("; ", list) : "no AutoDuty plugin installed");
string value2 = "IPC Run: " + (_run.HasAction ? "registered" : "missing") + ", IsStopped: " + (_isStopped.HasFunction ? "registered" : "missing");
string value3 = $"GetConfig probe: {ProbeConfig("Questionable.Probe")}, dutyModeEnum: {ProbeConfig("dutyModeEnum")}, LoopTimes: {ProbeConfig("LoopTimes")}, IsStopped: {ProbeIsStopped()}";
return $"{value}; {value2}; {value3}";
}
private string ProbeConfig(string key)
{
try
{
return "'" + _getConfig.InvokeFunc(key) + "'";
}
catch (IpcError ipcError)
{
return $"<{ipcError.GetType().Name}: {ipcError.Message}>";
}
}
private string ProbeIsStopped()
{
try
{
return _isStopped.InvokeFunc().ToString();
}
catch (IpcError ipcError)
{
return $"<{ipcError.GetType().Name}: {ipcError.Message}>";
}
}
public bool IsStopped() public bool IsStopped()
{ {
try try

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Questionable.Controller.CustomDelivery;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Movement; using Questionable.Controller.Steps.Movement;
@ -40,28 +41,32 @@ internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartN
} }
} }
List<ITask> list = ExtractNonMovementTasks(taskQueue); List<ITask> list = ExtractNonMovementTasks(taskQueue);
if (taskQueue.CurrentTaskExecutor?.CurrentTask is DeliveryNpcApproachTask item)
{
list.Insert(0, item);
}
ReRouteDecision reRouteDecision = reRoutePolicy.HandleMovementFailure(failedTargetNodeId, failedApproachNodeId, list.Count > 0); ReRouteDecision reRouteDecision = reRoutePolicy.HandleMovementFailure(failedTargetNodeId, failedApproachNodeId, list.Count > 0);
if (!(reRouteDecision is ReRouteDecision.SkipMovement)) if (!(reRouteDecision is ReRouteDecision.SkipMovement))
{ {
if (reRouteDecision is ReRouteDecision.Replan replan) if (reRouteDecision is ReRouteDecision.Replan replan)
{ {
taskQueue.Reset(); taskQueue.Reset();
foreach (ITask item in taskMapper.MapInstructions(replan.Instructions)) foreach (ITask item2 in taskMapper.MapInstructions(replan.Instructions))
{
taskQueue.Enqueue(item);
}
foreach (ITask item2 in list)
{ {
taskQueue.Enqueue(item2); taskQueue.Enqueue(item2);
} }
foreach (ITask item3 in list)
{
taskQueue.Enqueue(item3);
}
return true; return true;
} }
return false; return false;
} }
taskQueue.Reset(); taskQueue.Reset();
foreach (ITask item3 in list) foreach (ITask item4 in list)
{ {
taskQueue.Enqueue(item3); taskQueue.Enqueue(item4);
} }
logger.LogInformation("Re-route: skipping movement with {TaskCount} preserved tasks", list.Count); logger.LogInformation("Re-route: skipping movement with {TaskCount} preserved tasks", list.Count);
return true; return true;

View file

@ -63,7 +63,7 @@ internal sealed class SmartNavRouteEnqueuer(NavRouter navRouter, PlayerNavStateB
{ {
taskQueue.Enqueue(item); taskQueue.Enqueue(item);
} }
reRouteService.ClearDestination(); reRouteService.SetDestination(territoryId, position);
return true; return true;
} }
@ -156,4 +156,9 @@ internal sealed class SmartNavRouteEnqueuer(NavRouter navRouter, PlayerNavStateB
{ {
reRouteService.ClearDestination(); reRouteService.ClearDestination();
} }
public void SetDestination(uint territoryId, Vector3 position)
{
reRouteService.SetDestination(territoryId, position);
}
} }

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
@ -18,6 +19,8 @@ namespace Questionable.Windows.ConfigComponents;
internal sealed class BlacklistConfigComponent : ConfigComponent internal sealed class BlacklistConfigComponent : ConfigComponent
{ {
private const string ClipboardPrefix = "qst:blacklist:";
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly QuestSelector _questSelector; private readonly QuestSelector _questSelector;
@ -152,14 +155,62 @@ internal sealed class BlacklistConfigComponent : ConfigComponent
} }
} }
_questSelector.DrawSelection(); _questSelector.DrawSelection();
if (blacklistedQuests.Count > 0 && UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear all")) DrawClipboardButtons(blacklistedQuests);
if (blacklistedQuests.Count > 0)
{
ImGui.SameLine();
if (UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear all"))
{ {
base.Configuration.General.BlacklistedQuests.Clear(); base.Configuration.General.BlacklistedQuests.Clear();
Save(); Save();
} }
}
UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList); UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList);
} }
private void DrawClipboardButtons(HashSet<ElementId> blacklistedQuests)
{
using (ImRaii.Disabled(blacklistedQuests.Count == 0))
{
if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Copy, "Copy"))
{
string s = string.Join(";", from x in blacklistedQuests
orderby x.ToString()
select x.ToString());
ImGui.SetClipboardText("qst:blacklist:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(s)));
}
}
ImGui.SameLine();
string text = ThrottledClipboard.GetText();
using (ImRaii.Disabled(!text.StartsWith("qst:blacklist:", StringComparison.InvariantCulture)))
{
if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Paste, "Paste"))
{
ImportBlacklistedQuests(text);
}
}
}
private void ImportBlacklistedQuests(string clipboardText)
{
try
{
string s = clipboardText.Substring("qst:blacklist:".Length);
int skippedCount;
List<ElementId> list = ElementId.FromStrings(Encoding.UTF8.GetString(Convert.FromBase64String(s)).Split(";", StringSplitOptions.RemoveEmptyEntries), out skippedCount);
if (list.Count != 0)
{
base.Configuration.General.BlacklistedQuests.Clear();
base.Configuration.General.BlacklistedQuests.UnionWith(list);
Save();
}
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Failed to import blacklisted quests from clipboard");
}
}
private void DrawCurrentlyAcceptedQuests() private void DrawCurrentlyAcceptedQuests()
{ {
List<Quest> currentlyAcceptedQuests = GetCurrentlyAcceptedQuests(); List<Quest> currentlyAcceptedQuests = GetCurrentlyAcceptedQuests();

View file

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
@ -15,12 +14,9 @@ internal sealed class DebugConfigComponent : ConfigComponent
{ {
private readonly Dictionary<string, bool> _featurePausingOpenState = new Dictionary<string, bool>(); private readonly Dictionary<string, bool> _featurePausingOpenState = new Dictionary<string, bool>();
private readonly string? _cacheDirectory;
public DebugConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration) public DebugConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration)
: base(pluginInterface, configuration) : base(pluginInterface, configuration)
{ {
_cacheDirectory = pluginInterface.ConfigDirectory.FullName;
} }
public override void DrawTab() public override void DrawTab()
@ -61,45 +57,8 @@ internal sealed class DebugConfigComponent : ConfigComponent
} }
UiThemeUtils.EndCard(item4, item5, item6); UiThemeUtils.EndCard(item4, item5, item6);
UiThemeUtils.SectionSpacing(); UiThemeUtils.SectionSpacing();
UiThemeUtils.SectionHeader("Data Cache");
var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard();
UiThemeUtils.WrappedText("NPC position and zone boundary caches are rebuilt automatically after game patches. Use this button to force a rebuild on next load.");
bool flag = _cacheDirectory != null && (File.Exists(Path.Combine(_cacheDirectory, "npc-position-cache.bin")) || File.Exists(Path.Combine(_cacheDirectory, "zone-boundary-cache.bin")));
long num = 0L;
if (_cacheDirectory != null)
{
num += FileSize(Path.Combine(_cacheDirectory, "npc-position-cache.bin"));
num += FileSize(Path.Combine(_cacheDirectory, "zone-boundary-cache.bin"));
}
using (ImRaii.Disabled(!flag))
{
if (ImGui.Button("Clear LGB Caches") && _cacheDirectory != null)
{
TryDelete(Path.Combine(_cacheDirectory, "npc-position-cache.bin"));
TryDelete(Path.Combine(_cacheDirectory, "zone-boundary-cache.bin"));
}
if (flag && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImU8String tooltip = new ImU8String(20, 1);
tooltip.AppendLiteral("Current cache size: ");
tooltip.AppendFormatted(FormatBytes(num));
ImGui.SetTooltip(tooltip);
}
}
if (!flag)
{
ImGui.SameLine();
ImGui.TextDisabled("(no cache files present)");
}
else
{
ImGui.SameLine();
ImGui.TextDisabled("Takes effect on next plugin load.");
}
UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList);
UiThemeUtils.SectionSpacing();
UiThemeUtils.SectionHeader("Danger Zone"); UiThemeUtils.SectionHeader("Danger Zone");
var (contentStartPos2, availableWidth2, drawList2) = UiThemeUtils.BeginCard(); var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard();
UiThemeUtils.WrappedTextColored(UiThemeUtils.StatusLocked, "Enabling any option below may cause unexpected behavior. Use at your own risk."); UiThemeUtils.WrappedTextColored(UiThemeUtils.StatusLocked, "Enabling any option below may cause unexpected behavior. Use at your own risk.");
bool value4 = base.Configuration.Advanced.DisablePartyWatchdog; bool value4 = base.Configuration.Advanced.DisablePartyWatchdog;
if (UiThemeUtils.WrappedCheckbox("Disable Party Watchdog", ref value4, "The Party Watchdog stops Questionable when entering certain zones with other party members, or when entering unsupported content. Disabling this allows Questionable to continue working while in a party, but may cause unexpected behavior in group content.")) if (UiThemeUtils.WrappedCheckbox("Disable Party Watchdog", ref value4, "The Party Watchdog stops Questionable when entering certain zones with other party members, or when entering unsupported content. Disabling this allows Questionable to continue working while in a party, but may cause unexpected behavior in group content."))
@ -109,7 +68,7 @@ internal sealed class DebugConfigComponent : ConfigComponent
} }
DrawFeaturePausingSection("Pandora's Box Feature Pausing", PandorasBoxIpc.ConflictingFeatures, "Auto Active Time Maneuver", "Only applies when Auto-Solve QTE is enabled.", base.Configuration.Advanced.PandoraFeatureExclusions); DrawFeaturePausingSection("Pandora's Box Feature Pausing", PandorasBoxIpc.ConflictingFeatures, "Auto Active Time Maneuver", "Only applies when Auto-Solve QTE is enabled.", base.Configuration.Advanced.PandoraFeatureExclusions);
DrawFeaturePausingSection("Bundle of Tweaks Feature Pausing", AutomatonIpc.ConflictingTweaks, "AutoSnipeQuests", "Only applies when AutoSnipe is enabled.", base.Configuration.Advanced.AutomatonTweakExclusions); DrawFeaturePausingSection("Bundle of Tweaks Feature Pausing", AutomatonIpc.ConflictingTweaks, "AutoSnipeQuests", "Only applies when AutoSnipe is enabled.", base.Configuration.Advanced.AutomatonTweakExclusions);
UiThemeUtils.EndCard(contentStartPos2, availableWidth2, drawList2); UiThemeUtils.EndCard(contentStartPos, availableWidth, drawList);
} }
private void DrawFeaturePausingSection(string header, ImmutableHashSet<string> features, string conditionalFeature, string conditionalHelpText, HashSet<string> exclusions) private void DrawFeaturePausingSection(string header, ImmutableHashSet<string> features, string conditionalFeature, string conditionalHelpText, HashSet<string> exclusions)
@ -164,51 +123,4 @@ internal sealed class DebugConfigComponent : ConfigComponent
} }
} }
} }
private static void TryDelete(string path)
{
try
{
File.Delete(path);
}
catch
{
}
}
private static long FileSize(string path)
{
try
{
FileInfo fileInfo = new FileInfo(path);
long result;
if (fileInfo != null && fileInfo.Exists)
{
long length = fileInfo.Length;
result = length;
}
else
{
result = 0L;
}
return result;
}
catch
{
return 0L;
}
}
private static string FormatBytes(long bytes)
{
if (bytes < 1048576)
{
if (bytes >= 1024)
{
return $"{(double)bytes / 1024.0:F1} KB";
}
return $"{bytes} B";
}
return $"{(double)bytes / 1048576.0:F1} MB";
}
} }

View file

@ -296,7 +296,7 @@ internal sealed class GeneralConfigComponent : ConfigComponent
{ {
UiThemeUtils.SectionHeader("Reward redemption"); UiThemeUtils.SectionHeader("Reward redemption");
var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard(); var (contentStartPos, availableWidth, drawList) = UiThemeUtils.BeginCard();
ImGui.TextWrapped("Quest-reward items in your inventory are used automatically when you accept a quest. Choose which types to redeem."); ImGui.TextWrapped("Quest-reward items in your inventory are used automatically after quest completion and when you accept a quest. Choose which types to redeem.");
ImGui.Spacing(); ImGui.Spacing();
UiThemeUtils.GridCheckbox[] array = new UiThemeUtils.GridCheckbox[RewardTypeOptions.Length]; UiThemeUtils.GridCheckbox[] array = new UiThemeUtils.GridCheckbox[RewardTypeOptions.Length];
for (int i = 0; i < RewardTypeOptions.Length; i++) for (int i = 0; i < RewardTypeOptions.Length; i++)

View file

@ -2,12 +2,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Questionable.Controller; using Questionable.Controller;
using Questionable.Controller.Conditions; using Questionable.Controller.Conditions;
using Questionable.Data; using Questionable.Data;
@ -21,6 +24,8 @@ namespace Questionable.Windows.ConfigComponents;
internal sealed class StopConditionComponent : ConfigComponent internal sealed class StopConditionComponent : ConfigComponent
{ {
private const string ClipboardPrefix = "qst:stop:";
private static readonly string[] ConditionModeNames = new string[2] { "Pause", "Stop" }; private static readonly string[] ConditionModeNames = new string[2] { "Pause", "Stop" };
private static readonly string[] ConditionTypeNames = new string[7] { "Quest Complete", "Quest Accept", "Level", "Global Sequence", "Inventory Full", "Before Duty", "Gil Threshold" }; private static readonly string[] ConditionTypeNames = new string[7] { "Quest Complete", "Quest Accept", "Level", "Global Sequence", "Inventory Full", "Before Duty", "Gil Threshold" };
@ -59,6 +64,8 @@ internal sealed class StopConditionComponent : ConfigComponent
private long _acceptedQuestsRefreshAtMs; private long _acceptedQuestsRefreshAtMs;
private StopCondition? _draggedCondition;
public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestData questData, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IObjectTable objectTable, QuestController questController, Configuration configuration, ILogger<StopConditionComponent> logger) public StopConditionComponent(IDalamudPluginInterface pluginInterface, QuestSelector questSelector, QuestFunctions questFunctions, QuestRegistry questRegistry, QuestData questData, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IObjectTable objectTable, QuestController questController, Configuration configuration, ILogger<StopConditionComponent> logger)
: base(pluginInterface, configuration) : base(pluginInterface, configuration)
{ {
@ -131,31 +138,120 @@ internal sealed class StopConditionComponent : ConfigComponent
private void DrawConditionsList() private void DrawConditionsList()
{ {
List<StopCondition> conditions = base.Configuration.Stop.Conditions; List<StopCondition> conditions = base.Configuration.Stop.Conditions;
DrawClipboardButtons(conditions);
if (conditions.Count == 0) if (conditions.Count == 0)
{ {
ImGui.TextDisabled("No conditions configured."); ImGui.TextDisabled("No conditions configured.");
return; return;
} }
ImGui.SameLine();
if (UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear all")) if (UiThemeUtils.DestructiveButton(FontAwesomeIcon.Trash, "Clear all"))
{ {
conditions.Clear(); conditions.Clear();
Save(); Save();
} }
int? indexToRemove = null; int? indexToRemove = null;
StopCondition stopCondition = null;
int index = 0;
float x = ImGui.GetContentRegionAvail().X;
List<(Vector2, Vector2)> list = new List<(Vector2, Vector2)>();
for (int i = 0; i < conditions.Count; i++) for (int i = 0; i < conditions.Count; i++)
{ {
Vector2 item = ImGui.GetCursorScreenPos() + new Vector2(0f, (0f - ImGui.GetStyle().ItemSpacing.Y) / 2f);
using (ImRaii.PushId(i)) using (ImRaii.PushId(i))
{ {
StopCondition condition = conditions[i]; StopCondition stopCondition2 = conditions[i];
DrawConditionRow(condition, i, ref indexToRemove); if (conditions.Count > 1)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.Bars);
if (_draggedCondition == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedCondition = stopCondition2;
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Drag to reorder");
}
ImGui.SameLine();
}
DrawConditionRow(stopCondition2, i, ref indexToRemove);
Vector2 item2 = new Vector2(item.X + x, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2f);
list.Add((item, item2));
}
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
_draggedCondition = null;
}
else if (_draggedCondition != null)
{
int num = conditions.IndexOf(_draggedCondition);
if (num >= 0)
{
var (pMin, pMax) = list[num];
ImGui.GetWindowDrawList().AddRect(pMin, pMax, ImGui.ColorConvertFloat4ToU32(UiThemeUtils.CardBorderHighColor), 3f, ImDrawFlags.RoundCornersAll);
int num2 = list.FindIndex(((Vector2 TopLeft, Vector2 BottomRight) tuple2) => ImGui.IsMouseHoveringRect(tuple2.TopLeft, tuple2.BottomRight, clip: true));
if (num2 >= 0 && num != num2)
{
stopCondition = _draggedCondition;
index = num2;
}
} }
} }
if (indexToRemove.HasValue) if (indexToRemove.HasValue)
{ {
int valueOrDefault = indexToRemove.GetValueOrDefault(); int valueOrDefault = indexToRemove.GetValueOrDefault();
conditions.RemoveAt(valueOrDefault); conditions.RemoveAt(valueOrDefault);
_draggedCondition = null;
Save(); Save();
} }
else if (stopCondition != null)
{
conditions.Remove(stopCondition);
conditions.Insert(index, stopCondition);
Save();
}
}
private void DrawClipboardButtons(List<StopCondition> conditions)
{
using (ImRaii.Disabled(conditions.Count == 0))
{
if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Copy, "Copy"))
{
string s = JsonConvert.SerializeObject(conditions);
ImGui.SetClipboardText("qst:stop:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(s)));
}
}
ImGui.SameLine();
string text = ThrottledClipboard.GetText();
using (ImRaii.Disabled(!text.StartsWith("qst:stop:", StringComparison.InvariantCulture)))
{
if (UiThemeUtils.IconTextButton(FontAwesomeIcon.Paste, "Paste"))
{
ImportConditions(text);
}
}
}
private void ImportConditions(string clipboardText)
{
try
{
string s = clipboardText.Substring("qst:stop:".Length);
List<StopCondition> list = JsonConvert.DeserializeObject<List<StopCondition>>(Encoding.UTF8.GetString(Convert.FromBase64String(s))) ?? new List<StopCondition>();
list.RemoveAll((StopCondition x) => x?.IsSession ?? true);
if (list.Count != 0)
{
base.Configuration.Stop.Conditions.Clear();
base.Configuration.Stop.Conditions.AddRange(list);
Save();
}
}
catch (Exception exception)
{
_logger.LogDebug(exception, "Failed to import stop conditions from clipboard");
}
} }
private void DrawConditionRow(StopCondition condition, int index, ref int? indexToRemove) private void DrawConditionRow(StopCondition condition, int index, ref int? indexToRemove)

View file

@ -125,6 +125,10 @@ internal sealed class AttunementJournalComponent
private readonly QuestData _questData; private readonly QuestData _questData;
private readonly QuestRegistry _questRegistry;
private readonly QuestJournalUtils _questJournalUtils;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
@ -219,11 +223,7 @@ internal sealed class AttunementJournalComponent
private static readonly int CountPadWidth = 9999.ToString(CultureInfo.CurrentCulture).Length; private static readonly int CountPadWidth = 9999.ToString(CultureInfo.CurrentCulture).Length;
public Action<string>? SelectTabAction { get; set; } public AttunementJournalComponent(AetheryteData aetheryteData, AetherCurrentData aetherCurrentData, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, TerritoryData territoryData, NavRouter navRouter, PlayerNavStateBuilder playerNavStateBuilder, RouteInstructionBuilder instructionBuilder, SmartNavTaskMapper taskMapper, MovementController movementController, AttunementController attunementController, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, QuestData questData, QuestRegistry questRegistry, QuestJournalUtils questJournalUtils, IClientState clientState, IObjectTable objectTable, IDataManager dataManager, IDalamudPluginInterface pluginInterface, ILogger<AttunementJournalComponent> logger)
public QuestChainComponent? QuestChainComponent { get; set; }
public AttunementJournalComponent(AetheryteData aetheryteData, AetherCurrentData aetherCurrentData, AetheryteFunctions aetheryteFunctions, GameFunctions gameFunctions, TerritoryData territoryData, NavRouter navRouter, PlayerNavStateBuilder playerNavStateBuilder, RouteInstructionBuilder instructionBuilder, SmartNavTaskMapper taskMapper, MovementController movementController, AttunementController attunementController, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, QuestData questData, IClientState clientState, IObjectTable objectTable, IDataManager dataManager, IDalamudPluginInterface pluginInterface, ILogger<AttunementJournalComponent> logger)
{ {
_aetheryteData = aetheryteData; _aetheryteData = aetheryteData;
_aetherCurrentData = aetherCurrentData; _aetherCurrentData = aetherCurrentData;
@ -243,6 +243,8 @@ internal sealed class AttunementJournalComponent
_uiUtils = uiUtils; _uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent; _questTooltipComponent = questTooltipComponent;
_questData = questData; _questData = questData;
_questRegistry = questRegistry;
_questJournalUtils = questJournalUtils;
_clientState = clientState; _clientState = clientState;
_objectTable = objectTable; _objectTable = objectTable;
_dataManager = dataManager; _dataManager = dataManager;
@ -727,27 +729,14 @@ internal sealed class AttunementJournalComponent
label.AppendLiteral("##"); label.AppendLiteral("##");
label.AppendFormatted(row.Key); label.AppendFormatted(row.Key);
ImGui.Selectable(label); ImGui.Selectable(label);
if (current.QuestId != 0 && ImGui.IsItemHovered() && _questData.TryGetQuestInfo(QuestId.FromRowId(current.QuestId), out IQuestInfo questInfo)) if (current.QuestId != 0 && _questData.TryGetQuestInfo(QuestId.FromRowId(current.QuestId), out IQuestInfo questInfo))
{
if (ImGui.IsItemHovered())
{ {
_questTooltipComponent.Draw(questInfo); _questTooltipComponent.Draw(questInfo);
} }
string text = $"##CurrentQuest_{current.AetherCurrentId}"; _questRegistry.TryGetQuest(questInfo.QuestId, out Questionable.Model.Quest quest);
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) _questJournalUtils.ShowContextMenu(questInfo, quest, "AttunementJournalComponent");
{
ImGui.OpenPopup(text);
}
ImRaii.PopupDisposable popupDisposable = ImRaii.Popup(text);
try
{
if (popupDisposable.Success && ImGui.MenuItem("View Quest") && current.QuestId != 0)
{
QuestChainComponent?.SelectQuest(QuestId.FromRowId(current.QuestId));
SelectTabAction?.Invoke("Quest Chain");
}
}
finally
{
popupDisposable.Dispose();
} }
} }
else else
@ -759,13 +748,13 @@ internal sealed class AttunementJournalComponent
if (!valueOrDefault) if (!valueOrDefault)
{ {
AetherCurrentPosition overworldPosition = _aetherCurrentData.GetOverworldPosition(current.AetherCurrentId); AetherCurrentPosition overworldPosition = _aetherCurrentData.GetOverworldPosition(current.AetherCurrentId);
string text2 = $"##AttuneCurrent_{current.AetherCurrentId}"; string text = $"##AttuneCurrent_{current.AetherCurrentId}";
if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{ {
ImGui.OpenPopup(text2); ImGui.OpenPopup(text);
} }
using ImRaii.PopupDisposable popupDisposable2 = ImRaii.Popup(text2); using ImRaii.PopupDisposable popupDisposable = ImRaii.Popup(text);
if ((bool)popupDisposable2) if ((bool)popupDisposable)
{ {
bool flag = overworldPosition != null && !IsAnyControllerRunning(); bool flag = overworldPosition != null && !IsAnyControllerRunning();
using (ImRaii.Disabled(!flag)) using (ImRaii.Disabled(!flag))

View file

@ -43,6 +43,8 @@ internal sealed class QuestJournalUtils
private long _nextAvailableCountRefreshMs; private long _nextAvailableCountRefreshMs;
public Action? OpenJournal { get; set; }
public QuestJournalUtils(QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestFunctions questFunctions, QuestData questData, QuestRegistry questRegistry, QuestChainComponent questChainComponent, IChatGui chatGui, ILogger<QuestJournalUtils> logger) public QuestJournalUtils(QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestFunctions questFunctions, QuestData questData, QuestRegistry questRegistry, QuestChainComponent questChainComponent, IChatGui chatGui, ILogger<QuestJournalUtils> logger)
{ {
_questController = questController; _questController = questController;
@ -89,12 +91,7 @@ internal sealed class QuestJournalUtils
{ {
if (ImGui.MenuItem("Start as next quest")) if (ImGui.MenuItem("Start as next quest"))
{ {
_fateController.Stop("Quest journal start"); StartQuestAsNext(quest, label);
_seasonalDutyController.Stop("Quest journal start");
_customDeliveryController.Stop("Quest journal start");
_attunementController.Stop("Quest journal start");
_questController.SetNextQuest(quest);
_questController.Start(label);
} }
} }
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
@ -138,12 +135,24 @@ internal sealed class QuestJournalUtils
} }
if (label != "QuestChainComponent" && ImGui.MenuItem("View in Quest Chain")) if (label != "QuestChainComponent" && ImGui.MenuItem("View in Quest Chain"))
{ {
OpenJournal?.Invoke();
_questChainComponent.SelectQuest(questInfo.QuestId); _questChainComponent.SelectQuest(questInfo.QuestId);
_questChainComponent.SelectTabAction?.Invoke("Quest Chain"); _questChainComponent.SelectTabAction?.Invoke("Quest Chain");
} }
return true; return true;
} }
public void StartQuestAsNext(Quest quest, string label)
{
string label2 = label + " start";
_fateController.Stop(label2);
_seasonalDutyController.Stop(label2);
_customDeliveryController.Stop(label2);
_attunementController.Stop(label2);
_questController.SetNextQuest(quest);
_questController.Start(label);
}
public unsafe List<ElementId> GetIncompletePrerequisiteQuests(IQuestInfo questInfo) public unsafe List<ElementId> GetIncompletePrerequisiteQuests(IQuestInfo questInfo)
{ {
List<ElementId> list = new List<ElementId>(); List<ElementId> list = new List<ElementId>();

View file

@ -15,6 +15,7 @@ using Questionable.Data;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Questionable.Windows.JournalComponents;
namespace Questionable.Windows.QuestComponents; namespace Questionable.Windows.QuestComponents;
@ -42,6 +43,8 @@ internal sealed class EventInfoComponent
private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestTooltipComponent _questTooltipComponent;
private readonly QuestJournalUtils _questJournalUtils;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
@ -71,7 +74,7 @@ internal sealed class EventInfoComponent
} }
} }
public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestTooltipComponent questTooltipComponent, Configuration configuration, IDataManager dataManager, JournalData journalData, ILogger<EventInfoComponent> logger) public EventInfoComponent(QuestData questData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestTooltipComponent questTooltipComponent, QuestJournalUtils questJournalUtils, Configuration configuration, IDataManager dataManager, JournalData journalData, ILogger<EventInfoComponent> logger)
{ {
_questData = questData; _questData = questData;
_questRegistry = questRegistry; _questRegistry = questRegistry;
@ -83,6 +86,7 @@ internal sealed class EventInfoComponent
_customDeliveryController = customDeliveryController; _customDeliveryController = customDeliveryController;
_attunementController = attunementController; _attunementController = attunementController;
_questTooltipComponent = questTooltipComponent; _questTooltipComponent = questTooltipComponent;
_questJournalUtils = questJournalUtils;
_configuration = configuration; _configuration = configuration;
_dataManager = dataManager; _dataManager = dataManager;
_journalData = journalData; _journalData = journalData;
@ -194,12 +198,7 @@ internal sealed class EventInfoComponent
{ {
if (ImGuiComponents.IconButton(FontAwesomeIcon.Play)) if (ImGuiComponents.IconButton(FontAwesomeIcon.Play))
{ {
_fateController.Stop("Seasonal event start"); _questJournalUtils.StartQuestAsNext(quest, "SeasonalEventSelection");
_seasonalDutyController.Stop("Seasonal event start");
_customDeliveryController.Stop("Seasonal event start");
_attunementController.Stop("Seasonal event start");
_questController.SetNextQuest(quest);
_questController.Start("SeasonalEventSelection");
} }
} }
bool num = ImGui.IsItemHovered(); bool num = ImGui.IsItemHovered();

View file

@ -94,7 +94,13 @@ internal sealed class SavedPresetsComponent
{ {
DrawSaveSection(); DrawSaveSection();
UiThemeUtils.SectionSpacing(); UiThemeUtils.SectionSpacing();
using (ImRaii.ChildDisposable childDisposable = ImRaii.Child("SavedPresetsList", new Vector2(-1f, 300f), border: true, ImGuiWindowFlags.AlwaysVerticalScrollbar))
{
if ((bool)childDisposable)
{
DrawSavedPresets(); DrawSavedPresets();
}
}
UiThemeUtils.SectionSpacing(); UiThemeUtils.SectionSpacing();
DrawBottomButtons(); DrawBottomButtons();
} }

View file

@ -91,13 +91,15 @@ internal sealed class JournalProgressWindow : ThemedWindow, IDisposable
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = configuration; _configuration = configuration;
_pages = new Action[9] { _questJournalComponent.DrawQuests, _dutyJournalComponent.DrawDuties, _attunementJournalComponent.DrawAttunement, _gatheringJournalComponent.DrawGatheringItems, _questRewardComponent.DrawItemRewards, _questMapComponent.DrawQuestMap, _questChainComponent.DrawQuestChain, _alliedSocietyJournalComponent.DrawAlliedSocietyQuests, _customDeliveryJournalComponent.DrawCustomDeliveries }; _pages = new Action[9] { _questJournalComponent.DrawQuests, _dutyJournalComponent.DrawDuties, _attunementJournalComponent.DrawAttunement, _gatheringJournalComponent.DrawGatheringItems, _questRewardComponent.DrawItemRewards, _questMapComponent.DrawQuestMap, _questChainComponent.DrawQuestChain, _alliedSocietyJournalComponent.DrawAlliedSocietyQuests, _customDeliveryJournalComponent.DrawCustomDeliveries };
questJournalUtils.OpenJournal = delegate
{
base.IsOpen = true;
};
_questChainComponent.SelectTabAction = SelectTab; _questChainComponent.SelectTabAction = SelectTab;
_questChainComponent.QuestJournalUtils = questJournalUtils; _questChainComponent.QuestJournalUtils = questJournalUtils;
_questMapComponent.SelectTabAction = SelectTab; _questMapComponent.SelectTabAction = SelectTab;
_questMapComponent.QuestChainComponent = _questChainComponent; _questMapComponent.QuestChainComponent = _questChainComponent;
_questMapComponent.QuestJournalUtils = questJournalUtils; _questMapComponent.QuestJournalUtils = questJournalUtils;
_attunementJournalComponent.SelectTabAction = SelectTab;
_attunementJournalComponent.QuestChainComponent = _questChainComponent;
_clientState.Login += _questJournalComponent.RefreshCounts; _clientState.Login += _questJournalComponent.RefreshCounts;
_clientState.Logout += _dutyJournalComponent.ClearCounts; _clientState.Logout += _dutyJournalComponent.ClearCounts;
_clientState.Login += _gatheringJournalComponent.RefreshCounts; _clientState.Login += _gatheringJournalComponent.RefreshCounts;

View file

@ -18,6 +18,7 @@ using Questionable.Data;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using Questionable.Windows.JournalComponents;
using Questionable.Windows.QuestComponents; using Questionable.Windows.QuestComponents;
namespace Questionable.Windows; namespace Questionable.Windows;
@ -56,6 +57,8 @@ internal sealed class QuestSelectionWindow : ThemedWindow
private readonly QuestTooltipComponent _questTooltipComponent; private readonly QuestTooltipComponent _questTooltipComponent;
private readonly QuestJournalUtils _questJournalUtils;
private List<IQuestInfo> _quests = new List<IQuestInfo>(); private List<IQuestInfo> _quests = new List<IQuestInfo>();
private List<IQuestInfo> _offeredQuests = new List<IQuestInfo>(); private List<IQuestInfo> _offeredQuests = new List<IQuestInfo>();
@ -64,7 +67,7 @@ internal sealed class QuestSelectionWindow : ThemedWindow
private string _searchText = string.Empty; private string _searchText = string.Empty;
public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, QuestFunctions questFunctions, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface, TerritoryData territoryData, IClientState clientState, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent) public QuestSelectionWindow(QuestData questData, IGameGui gameGui, IChatGui chatGui, QuestFunctions questFunctions, QuestController questController, FateController fateController, SeasonalDutyController seasonalDutyController, CustomDeliveryController customDeliveryController, AttunementController attunementController, QuestRegistry questRegistry, IDalamudPluginInterface pluginInterface, TerritoryData territoryData, IClientState clientState, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, QuestJournalUtils questJournalUtils)
: base("Quest Selection###QuestionableQuestSelection") : base("Quest Selection###QuestionableQuestSelection")
{ {
_questData = questData; _questData = questData;
@ -82,6 +85,7 @@ internal sealed class QuestSelectionWindow : ThemedWindow
_clientState = clientState; _clientState = clientState;
_uiUtils = uiUtils; _uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent; _questTooltipComponent = questTooltipComponent;
_questJournalUtils = questJournalUtils;
base.Size = new Vector2(500f, 200f); base.Size = new Vector2(500f, 200f);
base.SizeCondition = ImGuiCond.Once; base.SizeCondition = ImGuiCond.Once;
base.SizeConstraints = new WindowSizeConstraints base.SizeConstraints = new WindowSizeConstraints
@ -214,6 +218,7 @@ internal sealed class QuestSelectionWindow : ThemedWindow
} }
} }
UiThemeUtils.RowLabel(item.Name, 0f); UiThemeUtils.RowLabel(item.Name, 0f);
_questJournalUtils.ShowContextMenu(item, quest, "QuestSelectionWindow");
} }
if (!ImGui.TableNextColumn()) if (!ImGui.TableNextColumn())
{ {
@ -266,16 +271,11 @@ internal sealed class QuestSelectionWindow : ThemedWindow
} }
if (flag2) if (flag2)
{ {
_fateController.Stop("Quest selection start");
_seasonalDutyController.Stop("Quest selection start");
_customDeliveryController.Stop("Quest selection start");
_attunementController.Stop("Quest selection start");
_questController.SetNextQuest(quest);
if (!_questController.ManualPriorityQuests.Contains(quest)) if (!_questController.ManualPriorityQuests.Contains(quest))
{ {
_questController.ManualPriorityQuests.Insert(0, quest); _questController.ManualPriorityQuests.Insert(0, quest);
} }
_questController.Start("QuestSelectionWindow"); _questJournalUtils.StartQuestAsNext(quest, "QuestSelectionWindow");
} }
ImGui.SameLine(); ImGui.SameLine();
bool num2 = UiThemeUtils.IconButton(FontAwesomeIcon.AngleDoubleRight, ImGui.GetFrameHeight()); bool num2 = UiThemeUtils.IconButton(FontAwesomeIcon.AngleDoubleRight, ImGui.GetFrameHeight());

View file

@ -61,6 +61,9 @@
<Reference Include="InteropGenerator.Runtime"> <Reference Include="InteropGenerator.Runtime">
<HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\15.0.3.2\InteropGenerator.Runtime.dll</HintPath> <HintPath>C:\Users\Aly\AppData\Roaming\XIVLauncher\addon\Hooks\15.0.3.2\InteropGenerator.Runtime.dll</HintPath>
</Reference> </Reference>
<Reference Include="SmartNav.Data">
<HintPath>..\..\SmartNav.Data.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Logging"> <Reference Include="Microsoft.Extensions.Logging">
<HintPath>..\..\Microsoft.Extensions.Logging.dll</HintPath> <HintPath>..\..\Microsoft.Extensions.Logging.dll</HintPath>
</Reference> </Reference>

View file

@ -180,6 +180,7 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
_pluginInterface.SavePluginConfig(configuration); _pluginInterface.SavePluginConfig(configuration);
} }
serviceCollection.AddSingleton(configuration); serviceCollection.AddSingleton(configuration);
MigrateLegacyConfigDirectoryFiles();
AddBasicFunctionsAndData(serviceCollection); AddBasicFunctionsAndData(serviceCollection);
AddTaskFactories(serviceCollection); AddTaskFactories(serviceCollection);
AddControllers(serviceCollection); AddControllers(serviceCollection);
@ -218,6 +219,59 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
} }
} }
private void MigrateLegacyConfigDirectoryFiles()
{
string[] obj = new string[5] { "npc-position-cache.bin", "zone-boundary-cache.bin", "lgb-worker-manifest.json", "npc-position-cache.bin.tmp", "zone-boundary-cache.bin.tmp" };
bool flag = false;
string[] array = obj;
foreach (string path in array)
{
try
{
FileInfo fileInfo = new FileInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, path));
if (fileInfo.Exists)
{
fileInfo.Delete();
flag = true;
}
}
catch (Exception)
{
}
}
if (flag)
{
_pluginLog.Debug("Deleted legacy LGB worker cache files from the config directory");
}
string text = Path.Combine(_pluginInterface.ConfigDirectory.FullName, "nav-overrides");
array = new string[2] { "warp-destinations.json", "teleport-tickets.json" };
foreach (string path2 in array)
{
try
{
FileInfo fileInfo2 = new FileInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, path2));
if (fileInfo2.Exists)
{
string text2 = Path.Combine(text, path2);
if (File.Exists(text2))
{
_pluginLog.Warning($"Not moving legacy override file {fileInfo2.FullName}: {text2} already exists");
}
else
{
string fullName = fileInfo2.FullName;
Directory.CreateDirectory(text);
fileInfo2.MoveTo(text2);
_pluginLog.Debug("Moved legacy override file " + fullName + " to " + text2);
}
}
}
catch (Exception)
{
}
}
}
private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection) private static void AddBasicFunctionsAndData(ServiceCollection serviceCollection)
{ {
serviceCollection.AddSingleton<AetheryteFunctions>(); serviceCollection.AddSingleton<AetheryteFunctions>();
@ -264,13 +318,14 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
IDalamudPluginInterface requiredService = sp.GetRequiredService<IDalamudPluginInterface>(); IDalamudPluginInterface requiredService = sp.GetRequiredService<IDalamudPluginInterface>();
DirectoryInfo devSourceDirectory = null; DirectoryInfo devSourceDirectory = null;
sp.GetRequiredService<IDataManager>().GameData.Repositories.TryGetValue("ffxiv", out Repository value); sp.GetRequiredService<IDataManager>().GameData.Repositories.TryGetValue("ffxiv", out Repository value);
return new SmartNavDataOptions(requiredService.ConfigDirectory, devSourceDirectory, value?.Version); return new SmartNavDataOptions(new DirectoryInfo(Path.Combine(requiredService.ConfigDirectory.FullName, "nav-overrides")), devSourceDirectory, value?.Version);
}); });
serviceCollection.AddSingleton<WarpDataService>(); serviceCollection.AddSingleton<WarpDataService>();
serviceCollection.AddSingleton<TaxiStandDataService>(); serviceCollection.AddSingleton<TaxiStandDataService>();
serviceCollection.AddSingleton<ZoneSubRegionService>(); serviceCollection.AddSingleton<ZoneSubRegionService>();
serviceCollection.AddSingleton<TeleportTicketService>(); serviceCollection.AddSingleton<TeleportTicketService>();
((IServiceCollection)serviceCollection).AddSingleton((Func<IServiceProvider, IZoneBoundarySource>)((IServiceProvider sp) => new LgbZoneBoundarySource(sp.GetRequiredService<IDataManager>(), sp.GetRequiredService<ILogger<LgbZoneBoundarySource>>(), sp.GetRequiredService<SmartNavDataOptions>()))); serviceCollection.AddSingleton((IServiceProvider sp) => new LgbZoneBoundarySource(sp.GetRequiredService<ILogger<LgbZoneBoundarySource>>(), sp.GetRequiredService<SmartNavDataOptions>()));
((IServiceCollection)serviceCollection).AddSingleton((Func<IServiceProvider, IZoneBoundarySource>)((IServiceProvider sp) => sp.GetRequiredService<LgbZoneBoundarySource>()));
((IServiceCollection)serviceCollection).AddSingleton((Func<IServiceProvider, ITerritoryInfo>)((IServiceProvider sp) => sp.GetRequiredService<TerritoryData>())); ((IServiceCollection)serviceCollection).AddSingleton((Func<IServiceProvider, ITerritoryInfo>)((IServiceProvider sp) => sp.GetRequiredService<TerritoryData>()));
serviceCollection.AddSingleton<IPlayerContext, QuestionablePlayerContext>(); serviceCollection.AddSingleton<IPlayerContext, QuestionablePlayerContext>();
serviceCollection.AddSingleton<CostCalculator>(); serviceCollection.AddSingleton<CostCalculator>();
@ -382,9 +437,11 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
serviceCollection.AddTaskExecutor<SinglePlayerDuty.CheckDutyOutcome, SinglePlayerDuty.CheckDutyOutcomeExecutor>(); serviceCollection.AddTaskExecutor<SinglePlayerDuty.CheckDutyOutcome, SinglePlayerDuty.CheckDutyOutcomeExecutor>();
serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>(); serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>();
serviceCollection.AddTaskExecutor<WaitNavmesh.Task, WaitNavmesh.Executor>(); serviceCollection.AddTaskExecutor<WaitNavmesh.Task, WaitNavmesh.Executor>();
serviceCollection.AddTaskFactoryAndExecutor<RedeemRewardItems.ScanAfterQuestCompletion, RedeemRewardItems.CompletionFactory, RedeemRewardItems.ScanAfterQuestCompletionExecutor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>(); serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>(); serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitNextStepOrSequence, WaitAtEnd.WaitNextStepOrSequenceExecutor>(); serviceCollection.AddTaskExecutor<WaitAtEnd.WaitNextStepOrSequence, WaitAtEnd.WaitNextStepOrSequenceExecutor>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitManualDuty, WaitAtEnd.WaitManualDutyExecutor>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitForCompletionFlags, WaitAtEnd.WaitForCompletionFlagsExecutor>(); serviceCollection.AddTaskExecutor<WaitAtEnd.WaitForCompletionFlags, WaitAtEnd.WaitForCompletionFlagsExecutor>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitObjectAtPosition, WaitAtEnd.WaitObjectAtPositionExecutor>(); serviceCollection.AddTaskExecutor<WaitAtEnd.WaitObjectAtPosition, WaitAtEnd.WaitObjectAtPositionExecutor>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitQuestAccepted, WaitAtEnd.WaitQuestAcceptedExecutor>(); serviceCollection.AddTaskExecutor<WaitAtEnd.WaitQuestAccepted, WaitAtEnd.WaitQuestAcceptedExecutor>();
@ -413,14 +470,10 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
sp.GetRequiredService<IDataManager>().GameData.Repositories.TryGetValue("ffxiv", out Repository value); sp.GetRequiredService<IDataManager>().GameData.Repositories.TryGetValue("ffxiv", out Repository value);
return new NpcPositionCacheOptions return new NpcPositionCacheOptions
{ {
CacheDirectory = sp.GetRequiredService<IDalamudPluginInterface>().ConfigDirectory.FullName, GameVersion = value?.Version
GameVersion = value?.Version,
ScanThrottle = TimeSpan.FromMilliseconds(10L)
}; };
}); });
serviceCollection.AddSingleton<NpcPositionCache>(); serviceCollection.AddSingleton<NpcPositionCache>();
serviceCollection.AddSingleton<LgbWorkerSupervisor>();
serviceCollection.AddSingleton<NpcPositionCacheWarmer>();
serviceCollection.AddSingleton<VendorResolver>(); serviceCollection.AddSingleton<VendorResolver>();
serviceCollection.AddSingleton<VendorResolverService>(); serviceCollection.AddSingleton<VendorResolverService>();
serviceCollection.AddSingleton<DeliveryPlannerService>(); serviceCollection.AddSingleton<DeliveryPlannerService>();
@ -539,7 +592,27 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
{ {
ILogger<QuestionablePlugin> requiredService = serviceProvider.GetRequiredService<ILogger<QuestionablePlugin>>(); ILogger<QuestionablePlugin> requiredService = serviceProvider.GetRequiredService<ILogger<QuestionablePlugin>>();
Stopwatch stopwatch = Stopwatch.StartNew(); Stopwatch stopwatch = Stopwatch.StartNew();
serviceProvider.GetRequiredService<NpcPositionCache>().TryLoadFromDisk(); try
{
Stopwatch stopwatch2 = Stopwatch.StartNew();
bool flag;
using (Stream stream = AssemblyLgbCacheLoader.OpenNpcPositionCache())
{
flag = serviceProvider.GetRequiredService<NpcPositionCache>().TryLoadFromStream(stream, acceptStaleVersion: true);
}
if (flag)
{
requiredService.LogInformation("NPC position cache hydrated in {Duration}ms", (long)stopwatch2.Elapsed.TotalMilliseconds);
}
else
{
requiredService.LogError("Embedded NPC position cache was rejected (bad magic, version or truncated) - derived warp sources, mender and vendor resolution are unavailable this session");
}
}
catch (Exception exception)
{
requiredService.LogError(exception, "Unable to hydrate the embedded NPC position cache");
}
InlineArray12<Task> buffer = default(InlineArray12<Task>); InlineArray12<Task> buffer = default(InlineArray12<Task>);
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<InlineArray12<Task>, Task>(ref buffer, 0) = Task.Run(() => serviceProvider.GetRequiredService<QuestData>()); global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<InlineArray12<Task>, Task>(ref buffer, 0) = Task.Run(() => serviceProvider.GetRequiredService<QuestData>());
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<InlineArray12<Task>, Task>(ref buffer, 1) = Task.Run(() => serviceProvider.GetRequiredService<TerritoryData>()); global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<InlineArray12<Task>, Task>(ref buffer, 1) = Task.Run(() => serviceProvider.GetRequiredService<TerritoryData>());
@ -578,7 +651,6 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
serviceProvider.GetRequiredService<ChocoboNameHandler>(); serviceProvider.GetRequiredService<ChocoboNameHandler>();
serviceProvider.GetRequiredService<DalamudInitializer>(); serviceProvider.GetRequiredService<DalamudInitializer>();
serviceProvider.GetRequiredService<TextAdvanceIpc>(); serviceProvider.GetRequiredService<TextAdvanceIpc>();
serviceProvider.GetRequiredService<NpcPositionCacheWarmer>();
ChangelogWindow requiredService2 = serviceProvider.GetRequiredService<ChangelogWindow>(); ChangelogWindow requiredService2 = serviceProvider.GetRequiredService<ChangelogWindow>();
Configuration requiredService3 = serviceProvider.GetRequiredService<Configuration>(); Configuration requiredService3 = serviceProvider.GetRequiredService<Configuration>();
if (requiredService3.IsPluginSetupComplete() && requiredService3.General.ShowChangelogOnUpdate) if (requiredService3.IsPluginSetupComplete() && requiredService3.General.ShowChangelogOnUpdate)

View file

@ -4,18 +4,18 @@ internal static class PathBundleSecret
{ {
private static readonly byte[] A = new byte[32] private static readonly byte[] A = new byte[32]
{ {
251, 250, 177, 24, 254, 78, 188, 97, 60, 65, 192, 237, 68, 154, 123, 161, 164, 81, 179, 101,
105, 80, 183, 91, 229, 181, 77, 98, 171, 136, 208, 145, 185, 118, 184, 66, 42, 173, 202, 110,
116, 248, 217, 243, 101, 83, 97, 216, 103, 115, 201, 113, 73, 242, 250, 58, 135, 189, 143, 92,
176, 184 151, 77
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
10, 142, 23, 61, 218, 98, 70, 165, 207, 244, 239, 73, 204, 3, 150, 201, 87, 43, 137, 223,
222, 75, 120, 153, 93, 56, 21, 57, 47, 72, 14, 253, 125, 35, 109, 186, 32, 12, 130, 132,
65, 245, 253, 11, 142, 198, 115, 115, 202, 36, 201, 118, 255, 168, 253, 104, 107, 98, 213, 28,
6, 70 83, 69
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -18,11 +18,15 @@
<None Remove="SmartNav.Data.chocobo-taxi-stands.bin" /> <None Remove="SmartNav.Data.chocobo-taxi-stands.bin" />
<None Remove="SmartNav.Data.zone-sub-regions.bin" /> <None Remove="SmartNav.Data.zone-sub-regions.bin" />
<None Remove="SmartNav.Data.derived-arrivals.bin" /> <None Remove="SmartNav.Data.derived-arrivals.bin" />
<None Remove="SmartNav.Data.npc-position-cache.bin" />
<None Remove="SmartNav.Data.zone-boundary-cache.bin" />
<EmbeddedResource Include="SmartNav.Data.warp-destinations.bin" LogicalName="SmartNav.Data.warp-destinations.bin" /> <EmbeddedResource Include="SmartNav.Data.warp-destinations.bin" LogicalName="SmartNav.Data.warp-destinations.bin" />
<EmbeddedResource Include="SmartNav.Data.teleport-tickets.bin" LogicalName="SmartNav.Data.teleport-tickets.bin" /> <EmbeddedResource Include="SmartNav.Data.teleport-tickets.bin" LogicalName="SmartNav.Data.teleport-tickets.bin" />
<EmbeddedResource Include="SmartNav.Data.chocobo-taxi-stands.bin" LogicalName="SmartNav.Data.chocobo-taxi-stands.bin" /> <EmbeddedResource Include="SmartNav.Data.chocobo-taxi-stands.bin" LogicalName="SmartNav.Data.chocobo-taxi-stands.bin" />
<EmbeddedResource Include="SmartNav.Data.zone-sub-regions.bin" LogicalName="SmartNav.Data.zone-sub-regions.bin" /> <EmbeddedResource Include="SmartNav.Data.zone-sub-regions.bin" LogicalName="SmartNav.Data.zone-sub-regions.bin" />
<EmbeddedResource Include="SmartNav.Data.derived-arrivals.bin" LogicalName="SmartNav.Data.derived-arrivals.bin" /> <EmbeddedResource Include="SmartNav.Data.derived-arrivals.bin" LogicalName="SmartNav.Data.derived-arrivals.bin" />
<EmbeddedResource Include="SmartNav.Data.npc-position-cache.bin" LogicalName="SmartNav.Data.npc-position-cache.bin" />
<EmbeddedResource Include="SmartNav.Data.zone-boundary-cache.bin" LogicalName="SmartNav.Data.zone-boundary-cache.bin" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="SmartNav.Model"> <Reference Include="SmartNav.Model">

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,16 @@
using System.IO;
namespace SmartNav.Data;
public static class AssemblyLgbCacheLoader
{
public static Stream OpenNpcPositionCache()
{
return SmartNavResources.OpenSealed("npc-position-cache.bin");
}
public static Stream OpenZoneBoundaryCache()
{
return SmartNavResources.OpenSealed("zone-boundary-cache.bin");
}
}

View file

@ -4,18 +4,18 @@ internal static class DataBundleSecret
{ {
private static readonly byte[] A = new byte[32] private static readonly byte[] A = new byte[32]
{ {
249, 130, 120, 19, 233, 226, 108, 14, 107, 82, 13, 38, 21, 255, 71, 40, 19, 213, 79, 201,
130, 220, 135, 231, 132, 145, 238, 79, 114, 87, 20, 24, 146, 227, 159, 156, 142, 23, 138, 144,
123, 65, 202, 110, 156, 178, 236, 146, 151, 20, 119, 132, 179, 149, 190, 134, 23, 209, 204, 192,
187, 222 234, 63
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
4, 22, 39, 195, 114, 48, 170, 103, 209, 88, 252, 191, 81, 239, 79, 239, 196, 247, 66, 179,
134, 221, 112, 108, 144, 135, 150, 118, 49, 201, 73, 171, 175, 76, 172, 241, 89, 23, 110, 4,
74, 13, 92, 179, 155, 25, 114, 34, 102, 151, 105, 193, 2, 137, 162, 77, 122, 136, 209, 181,
79, 34 229, 197
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -32,13 +32,32 @@ internal static class SmartNavResources
private static T? DeserializeResource<T>(string resourceName) private static T? DeserializeResource<T>(string resourceName)
{ {
string name = Path.ChangeExtension(resourceName, ".bin"); byte[] array = Unseal(Path.ChangeExtension(resourceName, ".bin"));
using Stream stream = Assembly.GetManifestResourceStream(name); if (array == null)
if (stream == null)
{ {
return default(T); return default(T);
} }
using MemoryStream memoryStream = new MemoryStream(); ReadOnlySpan<byte> readOnlySpan = array;
if (readOnlySpan.StartsWith("\ufeff"u8))
{
readOnlySpan = readOnlySpan.Slice(3);
}
return JsonSerializer.Deserialize<T>(readOnlySpan);
}
public static Stream OpenSealed(string fileName)
{
return new MemoryStream(Unseal("SmartNav.Data." + fileName) ?? throw new InvalidOperationException("embedded resource '" + fileName + "' is missing"), writable: false);
}
private static byte[]? Unseal(string sealedName)
{
using Stream stream = Assembly.GetManifestResourceStream(sealedName);
if (stream == null)
{
return null;
}
using MemoryStream memoryStream = new MemoryStream((int)stream.Length);
stream.CopyTo(memoryStream); stream.CopyTo(memoryStream);
byte[] array = memoryStream.ToArray(); byte[] array = memoryStream.ToArray();
byte[] array2 = new byte[array.Length - 28]; byte[] array2 = new byte[array.Length - 28];
@ -52,7 +71,9 @@ internal static class SmartNavResources
{ {
CryptographicOperations.ZeroMemory(array3); CryptographicOperations.ZeroMemory(array3);
} }
using DeflateStream utf8Json = new DeflateStream(new MemoryStream(array2), CompressionMode.Decompress); using DeflateStream deflateStream = new DeflateStream(new MemoryStream(array2), CompressionMode.Decompress);
return JsonSerializer.Deserialize<T>(utf8Json); using MemoryStream memoryStream2 = new MemoryStream(array2.Length * 4);
deflateStream.CopyTo(memoryStream2);
return memoryStream2.ToArray();
} }
} }

View file

@ -217,6 +217,7 @@ public enum EAetheryteLocation
TuliyollalIhuykatumu = 227, TuliyollalIhuykatumu = 227,
TuliyollalDirigibleLandingYakTel = 228, TuliyollalDirigibleLandingYakTel = 228,
TuliyollalXakTuralSkygate = 229, TuliyollalXakTuralSkygate = 229,
TuliyollalPhantomVillage = 239,
SolutionNine = 217, SolutionNine = 217,
SolutionNineInformationCenter = 230, SolutionNineInformationCenter = 230,
SolutionNineTrueVue = 231, SolutionNineTrueVue = 231,

View file

@ -308,6 +308,10 @@ public sealed class AetheryteData
EAetheryteLocation.IshgardGatesOfJudgement, EAetheryteLocation.IshgardGatesOfJudgement,
new Vector3(-160.8786f, 304.1538f, -322.6239f) new Vector3(-160.8786f, 304.1538f, -322.6239f)
}, },
{
EAetheryteLocation.IshgardFirmament,
new Vector3(9.92315f, -15.2f, 173.5059f)
},
{ {
EAetheryteLocation.Idyllshire, EAetheryteLocation.Idyllshire,
new Vector3(71.94617f, 211.26111f, -18.905945f) new Vector3(71.94617f, 211.26111f, -18.905945f)
@ -824,6 +828,10 @@ public sealed class AetheryteData
EAetheryteLocation.TuliyollalXakTuralSkygate, EAetheryteLocation.TuliyollalXakTuralSkygate,
new Vector3(284.959f, 15.999984f, 771.9063f) new Vector3(284.959f, 15.999984f, 771.9063f)
}, },
{
EAetheryteLocation.TuliyollalPhantomVillage,
new Vector3(43.3027f, 0.0199971f, -1.07516f)
},
{ {
EAetheryteLocation.SolutionNine, EAetheryteLocation.SolutionNine,
new Vector3(-0.015319824f, 8.987488f, -0.015319824f) new Vector3(-0.015319824f, 8.987488f, -0.015319824f)

View file

@ -2,13 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
using System.Threading; using System.Text;
using Dalamud.Plugin.Services;
using LLib.GameData;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Layer;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SmartNav.Model.Navigation; using SmartNav.Model.Navigation;
@ -16,51 +10,61 @@ namespace SmartNav.Data;
public sealed class LgbZoneBoundarySource : IZoneBoundarySource public sealed class LgbZoneBoundarySource : IZoneBoundarySource
{ {
private sealed record LgbData(List<ExitRangeEntry> ExitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> PopRangeIndex);
private const uint BoundaryFileMagic = 1112425562u; private const uint BoundaryFileMagic = 1112425562u;
private const int BoundaryFileFormatVersion = 1; private const int BoundaryFileFormatVersion = 1;
private const string CacheFileName = "zone-boundary-cache.bin"; private const string CacheFileName = "zone-boundary-cache.bin";
private readonly IDataManager _dataManager; private const int MaxPreallocatedEntries = 131072;
private readonly ILogger<LgbZoneBoundarySource> _logger; private readonly ILogger<LgbZoneBoundarySource> _logger;
private readonly Lazy<IReadOnlyList<ZoneBoundary>> _boundaries; private readonly string? _overrideDirectory;
private readonly string? _cacheDirectory;
private readonly string? _gameVersion; private readonly string? _gameVersion;
public LgbZoneBoundarySource(IDataManager dataManager, ILogger<LgbZoneBoundarySource> logger, SmartNavDataOptions? dataOptions = null) private readonly object _buildLock = new object();
private IReadOnlyList<ZoneBoundary>? _boundaries;
public LgbZoneBoundarySource(ILogger<LgbZoneBoundarySource> logger, SmartNavDataOptions? dataOptions = null)
{ {
_dataManager = dataManager;
_logger = logger; _logger = logger;
_cacheDirectory = dataOptions?.UserOverrideDirectory?.FullName; _overrideDirectory = dataOptions?.UserOverrideDirectory?.FullName;
_gameVersion = dataOptions?.GameVersion; _gameVersion = dataOptions?.GameVersion;
_boundaries = new Lazy<IReadOnlyList<ZoneBoundary>>(Build, LazyThreadSafetyMode.ExecutionAndPublication);
} }
public IReadOnlyList<ZoneBoundary> GetBoundaries() public IReadOnlyList<ZoneBoundary> GetBoundaries()
{ {
return _boundaries.Value; lock (_buildLock)
{
return _boundaries ?? (_boundaries = Build());
}
}
public void Invalidate()
{
lock (_buildLock)
{
_boundaries = null;
}
} }
private IReadOnlyList<ZoneBoundary> Build() private IReadOnlyList<ZoneBoundary> Build()
{ {
try try
{ {
(List<ExitRangeEntry>, Dictionary<(ushort, uint), PopRangeEntry>)? tuple = TryLoadFromDisk(); var (lgbData, text) = LoadRawData();
if (!tuple.HasValue) if (lgbData == null)
{ {
_logger.LogInformation("No boundary cache found, falling back to in-process LGB scan"); return Array.Empty<ZoneBoundary>();
} }
(List<ExitRangeEntry>, Dictionary<(ushort, uint), PopRangeEntry>) obj = tuple ?? CollectLgbData(); List<ZoneBoundary> list = ZoneBoundaryDerivation.Derive(lgbData.ExitRanges, lgbData.PopRangeIndex, _logger);
List<ExitRangeEntry> item = obj.Item1;
Dictionary<(ushort, uint), PopRangeEntry> item2 = obj.Item2;
List<ZoneBoundary> list = ZoneBoundaryDerivation.Derive(item, item2, _logger);
ZoneBoundaryDerivation.ApplyOverrides(list, ZoneBoundaryOverrides.FlyingPairs, ZoneBoundaryOverrides.PositionOverrides, _logger); ZoneBoundaryDerivation.ApplyOverrides(list, ZoneBoundaryOverrides.FlyingPairs, ZoneBoundaryOverrides.PositionOverrides, _logger);
_logger.LogDebug("Derived {Count} zone boundaries from {ExitCount} ExitRanges{Source}", list.Count, item.Count, tuple.HasValue ? " (from cache)" : ""); _logger.LogDebug("Derived {Count} zone boundaries from {ExitCount} ExitRanges ({Source})", list.Count, lgbData.ExitRanges.Count, text);
return list; return list;
} }
catch (Exception exception) catch (Exception exception)
@ -70,38 +74,87 @@ public sealed class LgbZoneBoundarySource : IZoneBoundarySource
} }
} }
private (List<ExitRangeEntry> ExitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> PopRangeIndex)? TryLoadFromDisk() private (LgbData? Data, string Source) LoadRawData()
{ {
if (string.IsNullOrEmpty(_cacheDirectory) || string.IsNullOrEmpty(_gameVersion)) LgbData lgbData = TryLoadOverrideFile("override file");
if (lgbData != null)
{
return (Data: lgbData, Source: "override file");
}
return (Data: TryLoadEmbedded("embedded resource"), Source: "embedded resource");
}
private LgbData? TryLoadOverrideFile(string source)
{
if (string.IsNullOrEmpty(_overrideDirectory))
{ {
return null; return null;
} }
string path = Path.Combine(_cacheDirectory, "zone-boundary-cache.bin"); string text = Path.Combine(_overrideDirectory, "zone-boundary-cache.bin");
if (!File.Exists(path)) if (!File.Exists(text))
{ {
return null; return null;
} }
try try
{ {
using BinaryReader binaryReader = new BinaryReader(File.OpenRead(path)); using Stream stream = File.OpenRead(text);
LgbData lgbData = ReadCache(stream, source);
if (lgbData.ExitRanges.Count == 0 && lgbData.PopRangeIndex.Count == 0)
{
_logger.LogWarning("Zone boundary {Source} {Path} holds no exits and no pops, ignoring it and using the embedded data", source, text);
return null;
}
return lgbData;
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to read zone boundary {Source}, falling back to the embedded data", source);
return null;
}
}
private LgbData? TryLoadEmbedded(string source)
{
try
{
using Stream stream = AssemblyLgbCacheLoader.OpenZoneBoundaryCache();
return ReadCache(stream, source);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to read zone boundary {Source} - building graph without boundary edges", source);
return null;
}
}
private LgbData ReadCache(Stream stream, string source)
{
using BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (binaryReader.ReadUInt32() != 1112425562 || binaryReader.ReadInt32() != 1) if (binaryReader.ReadUInt32() != 1112425562 || binaryReader.ReadInt32() != 1)
{ {
_logger.LogDebug("Zone boundary cache has unknown header, discarding"); throw new InvalidDataException("zone boundary " + source + " has an unknown header");
return null;
} }
if (binaryReader.ReadString() != _gameVersion) string text = binaryReader.ReadString();
if (!string.IsNullOrEmpty(_gameVersion) && text != _gameVersion)
{ {
_logger.LogDebug("Zone boundary cache is for a different game version, discarding"); _logger.LogInformation("Zone boundary {Source} was built for game version {FileVersion}, running {GameVersion} - using it anyway", source, text, _gameVersion);
return null;
} }
int num = binaryReader.ReadInt32(); int num = binaryReader.ReadInt32();
List<ExitRangeEntry> list = new List<ExitRangeEntry>(num); if (num < 0)
{
throw new InvalidDataException($"zone boundary {source} has a negative exit count ({num})");
}
List<ExitRangeEntry> list = new List<ExitRangeEntry>(Math.Min(num, 131072));
for (int i = 0; i < num; i++) for (int i = 0; i < num; i++)
{ {
list.Add(new ExitRangeEntry(binaryReader.ReadUInt16(), binaryReader.ReadUInt32(), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()), binaryReader.ReadUInt16(), binaryReader.ReadUInt32(), binaryReader.ReadUInt32(), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()))); list.Add(new ExitRangeEntry(binaryReader.ReadUInt16(), binaryReader.ReadUInt32(), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()), binaryReader.ReadUInt16(), binaryReader.ReadUInt32(), binaryReader.ReadUInt32(), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle())));
} }
int num2 = binaryReader.ReadInt32(); int num2 = binaryReader.ReadInt32();
Dictionary<(ushort, uint), PopRangeEntry> dictionary = new Dictionary<(ushort, uint), PopRangeEntry>(num2); if (num2 < 0)
{
throw new InvalidDataException($"zone boundary {source} has a negative pop count ({num2})");
}
Dictionary<(ushort, uint), PopRangeEntry> dictionary = new Dictionary<(ushort, uint), PopRangeEntry>(Math.Min(num2, 131072));
for (int j = 0; j < num2; j++) for (int j = 0; j < num2; j++)
{ {
ushort num3 = binaryReader.ReadUInt16(); ushort num3 = binaryReader.ReadUInt16();
@ -109,132 +162,7 @@ public sealed class LgbZoneBoundarySource : IZoneBoundarySource
Vector3 position = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()); Vector3 position = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
dictionary[(num3, num4)] = new PopRangeEntry(num3, num4, position); dictionary[(num3, num4)] = new PopRangeEntry(num3, num4, position);
} }
_logger.LogDebug("Zone boundary cache loaded from disk ({ExitCount} exits, {PopCount} pops)", list.Count, dictionary.Count); _logger.LogDebug("Zone boundary data loaded from {Source} ({ExitCount} exits, {PopCount} pops)", source, list.Count, dictionary.Count);
return (list, dictionary); return new LgbData(list, dictionary);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to load zone boundary cache, falling back to LGB scan");
}
try
{
File.Delete(path);
}
catch
{
}
return null;
}
private (List<ExitRangeEntry> ExitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> PopRangeIndex) CollectLgbData()
{
List<ExitRangeEntry> list = new List<ExitRangeEntry>();
Dictionary<(ushort, uint), PopRangeEntry> dictionary = new Dictionary<(ushort, uint), PopRangeEntry>();
HashSet<uint> hashSet = new HashSet<uint>();
foreach (Aetheryte item in _dataManager.GetExcelSheet<Aetheryte>())
{
hashSet.Add(item.Territory.RowId);
}
ExcelSheet<TerritoryType> excelSheet = _dataManager.GetExcelSheet<TerritoryType>();
HashSet<string> visitedLgbPaths = new HashSet<string>();
bool cacheFileResources = _dataManager.GameData.Options.CacheFileResources;
_dataManager.GameData.Options.CacheFileResources = false;
try
{
foreach (TerritoryType item2 in excelSheet)
{
if (hashSet.Contains(item2.RowId))
{
ScanTerritory(list, dictionary, visitedLgbPaths, item2);
}
}
foreach (TerritoryType item3 in excelSheet)
{
ScanTerritory(list, dictionary, visitedLgbPaths, item3);
}
}
finally
{
_dataManager.GameData.Options.CacheFileResources = cacheFileResources;
}
return (ExitRanges: list, PopRangeIndex: dictionary);
}
private static bool IsNavigableOverworld(ETerritoryIntendedUse use)
{
switch (use)
{
case ETerritoryIntendedUse.Town:
case ETerritoryIntendedUse.Overworld:
case ETerritoryIntendedUse.OpeningArea:
case ETerritoryIntendedUse.HousingOutdoor:
case ETerritoryIntendedUse.Firmament:
case ETerritoryIntendedUse.SanctumOfTheTwelve:
case ETerritoryIntendedUse.GoldSaucer:
case ETerritoryIntendedUse.Eureka:
case ETerritoryIntendedUse.Bozja:
case ETerritoryIntendedUse.IslandSanctuary:
case ETerritoryIntendedUse.CosmicExploration:
case ETerritoryIntendedUse.OccultCrescent:
return true;
default:
return false;
}
}
private void ScanTerritory(List<ExitRangeEntry> exitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> popRangeIndex, HashSet<string> visitedLgbPaths, TerritoryType territory)
{
if (territory.RowId == 0 || !IsNavigableOverworld((ETerritoryIntendedUse)territory.TerritoryIntendedUse.RowId))
{
return;
}
string text = territory.Bg.ExtractText();
if (string.IsNullOrEmpty(text))
{
return;
}
int num = text.IndexOf("/level/", StringComparison.Ordinal);
if (num < 0)
{
return;
}
string text2 = "bg/" + text.Substring(0, num + 1) + "level/planmap.lgb";
if (!visitedLgbPaths.Add(text2))
{
return;
}
ushort num2 = (ushort)territory.RowId;
LgbFile file;
try
{
file = _dataManager.GetFile<LgbFile>(text2);
}
catch (Exception exception)
{
_logger.LogTrace(exception, "Failed to load {Path}", text2);
return;
}
if (file == null)
{
return;
}
LayerCommon.Layer[] layers = file.Layers;
for (int i = 0; i < layers.Length; i++)
{
LayerCommon.InstanceObject[] instanceObjects = layers[i].InstanceObjects;
for (int j = 0; j < instanceObjects.Length; j++)
{
LayerCommon.InstanceObject instanceObject = instanceObjects[j];
if (instanceObject.AssetType == LayerEntryType.ExitRange)
{
LayerCommon.ExitRangeInstanceObject exitRangeInstanceObject = (LayerCommon.ExitRangeInstanceObject)(object)instanceObject.Object;
exitRanges.Add(new ExitRangeEntry(num2, instanceObject.InstanceId, new Vector3(instanceObject.Transform.Translation.X, instanceObject.Transform.Translation.Y, instanceObject.Transform.Translation.Z), exitRangeInstanceObject.TerritoryType, exitRangeInstanceObject.DestInstanceId, exitRangeInstanceObject.ReturnInstanceId, new Vector3(instanceObject.Transform.Rotation.X, instanceObject.Transform.Rotation.Y, instanceObject.Transform.Rotation.Z), new Vector3(instanceObject.Transform.Scale.X, instanceObject.Transform.Scale.Y, instanceObject.Transform.Scale.Z)));
}
else if (instanceObject.AssetType == LayerEntryType.PopRange)
{
popRangeIndex[(num2, instanceObject.InstanceId)] = new PopRangeEntry(num2, instanceObject.InstanceId, new Vector3(instanceObject.Transform.Translation.X, instanceObject.Transform.Translation.Y, instanceObject.Transform.Translation.Z));
}
}
}
} }
} }

View file

@ -133,6 +133,32 @@ public sealed class NavGraphBuilder
} }
} }
} }
NavNode navNode = null;
foreach (NavNode value5 in graph.Nodes.Values)
{
if (GetAethernetParticipant(value5) == EAetheryteLocation.IshgardFirmament)
{
navNode = value5;
break;
}
}
if (navNode != null)
{
foreach (NavNode value6 in graph.Nodes.Values)
{
EAetheryteLocation? aethernetParticipant2 = GetAethernetParticipant(value6);
if (!(value6.Id == navNode.Id) && aethernetParticipant2.HasValue)
{
EAetheryteLocation valueOrDefault3 = aethernetParticipant2.GetValueOrDefault();
if (valueOrDefault3.IsFirmamentAetheryte())
{
graph.AddEdge(new NavEdge(navNode.Id, value6.Id, NavEdgeType.AethernetHop, 0f, 0));
graph.AddEdge(new NavEdge(value6.Id, navNode.Id, NavEdgeType.AethernetHop, 0f, 0));
num += 2;
}
}
}
}
_logger.LogDebug("Added {Count} aethernet edges across {GroupCount} groups", num, dictionary.Count); _logger.LogDebug("Added {Count} aethernet edges across {GroupCount} groups", num, dictionary.Count);
} }

View file

@ -103,7 +103,7 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
if ((object)trigger != null && TryComputeApproach(trigger, seg.From.Position, previous?.From.Position, out var approach)) if ((object)trigger != null && TryComputeApproach(trigger, seg.From.Position, previous?.From.Position, out var approach))
{ {
instructions.Add(new NavInstruction.Move((ushort)territoryId3, approach, Fly: true, LandAtTarget: true, 3f, null, null, DisableNavmesh: false, AllowZoneTransition: true, seg.From.Id, null, IsFinal: false)); instructions.Add(new NavInstruction.Move((ushort)territoryId3, approach, Fly: true, LandAtTarget: true, 3f, null, null, DisableNavmesh: false, AllowZoneTransition: true, seg.From.Id, null, IsFinal: false));
instructions.Add(new NavInstruction.Move((ushort)territoryId3, seg.From.Position, Fly: false, LandAtTarget: false, 0f, null, null, DisableNavmesh: false, AllowZoneTransition: true, seg.From.Id, null, IsFinal: false)); instructions.Add(new NavInstruction.Move((ushort)territoryId3, seg.From.Position, Fly: false, LandAtTarget: false, 0f, null, null, DisableNavmesh: true, AllowZoneTransition: true, seg.From.Id, null, IsFinal: false));
goto IL_04b4; goto IL_04b4;
} }
} }