muffin v7.5.13

This commit is contained in:
alydev 2026-08-22 10:25:22 +10:00
parent 911ed68baa
commit acf3e3cb18
69 changed files with 2731 additions and 1425 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]
{ {
48, 212, 251, 149, 233, 68, 48, 175, 219, 236, 183, 148, 243, 175, 81, 166, 60, 18, 24, 50,
217, 100, 130, 200, 255, 152, 233, 112, 177, 94, 233, 21, 123, 58, 154, 174, 171, 214, 240, 163,
93, 198, 133, 46, 110, 244, 154, 244, 247, 13, 19, 222, 77, 126, 21, 146, 142, 237, 152, 128,
145, 158 16, 0
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
79, 60, 140, 255, 167, 84, 206, 132, 105, 228, 167, 25, 83, 195, 35, 102, 138, 74, 178, 77,
71, 209, 251, 6, 250, 62, 38, 86, 249, 66, 83, 219, 116, 213, 10, 227, 200, 219, 186, 181,
102, 234, 109, 58, 57, 229, 131, 131, 42, 66, 158, 22, 1, 102, 179, 218, 15, 30, 54, 210,
172, 97 214, 232
}; };
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]
{ {
83, 53, 162, 177, 116, 210, 154, 147, 32, 246, 219, 178, 133, 86, 37, 102, 10, 221, 178, 245,
109, 49, 21, 10, 185, 40, 3, 223, 158, 234, 197, 51, 203, 134, 231, 122, 11, 25, 197, 240,
82, 124, 250, 209, 185, 174, 77, 93, 58, 15, 44, 220, 234, 91, 41, 93, 186, 183, 17, 72,
16, 246 109, 31
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
11, 0, 215, 132, 153, 74, 202, 77, 15, 86, 107, 156, 123, 202, 221, 77, 66, 210, 234, 31,
243, 43, 187, 26, 168, 53, 42, 2, 79, 60, 174, 55, 4, 171, 94, 88, 13, 248, 215, 191,
154, 215, 216, 52, 147, 70, 38, 49, 232, 46, 115, 73, 208, 117, 230, 149, 32, 243, 105, 23,
225, 130 154, 250
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -1,11 +1,9 @@
using System; using System;
using System.Reflection;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
namespace LLib.ImGui; namespace LLib.ImGui;
[Obfuscation(Feature = "-rename", Exclude = false, ApplyToMembers = true)]
public abstract class LWindow : Window public abstract class LWindow : Window
{ {
private bool _initializedConfig; private bool _initializedConfig;

View file

@ -1,8 +1,5 @@
using System.Reflection;
namespace LLib.ImGui; namespace LLib.ImGui;
[Obfuscation(Feature = "-rename", Exclude = false, ApplyToMembers = true)]
public class WindowConfig public class WindowConfig
{ {
public bool IsPinned { get; set; } public bool IsPinned { get; set; }

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Text; using System.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -120,33 +119,7 @@ public sealed class DalamudLoggerProvider : ILoggerProvider, IDisposable, ISuppo
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
{ {
string name = _categoryNameFormatter(categoryName); return new DalamudLogger(_categoryNameFormatter(categoryName), this, _pluginLog);
if (LooksObfuscated(name))
{
name = null;
}
return new DalamudLogger(name, this, _pluginLog);
}
private static bool LooksObfuscated(string? name)
{
if (string.IsNullOrEmpty(name))
{
return false;
}
foreach (char c in name)
{
if (char.IsControl(c))
{
return true;
}
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (((uint)(unicodeCategory - 15) <= 2u || unicodeCategory == UnicodeCategory.OtherNotAssigned) ? true : false)
{
return true;
}
}
return false;
} }
public void SetScopeProvider(IExternalScopeProvider scopeProvider) public void SetScopeProvider(IExternalScopeProvider scopeProvider)

View file

@ -185,34 +185,58 @@
"description": "If true, will go to the position in a straight line instead of using pathfinding", "description": "If true, will go to the position in a straight line instead of using pathfinding",
"type": "boolean" "type": "boolean"
}, },
"TravelMode": {
"type": "string",
"enum": [
"Auto",
"OnFoot",
"GroundMount",
"Flying"
],
"description": "ROUTE PREFERENCE: How to travel for the full route. Auto lets navigation choose; OnFoot forbids mounting and flying; GroundMount forces a mount but forbids flying; Flying uses flight where it is unlocked. Quest requirements take priority."
},
"ArrivalMode": {
"type": "string",
"enum": [
"Auto",
"StayMounted",
"Land",
"Dismount"
],
"description": "ARRIVAL PREFERENCE: What to do after reaching the destination. This does not change how the route is traveled. Quest requirements take priority."
},
"Mount": { "Mount": {
"type": [ "type": [
"boolean", "boolean",
"null" "null"
], ],
"description": "Travel hint: If true, will mount regardless of distance to position. If false, will unmount. Ignored by SmartNav auto-decide when not set. For quest-mechanical mount requirements, use MountRequired/DismountRequired instead." "description": "LEGACY COMPATIBILITY: Old per-movement mount hint retained for existing quests. Use TravelMode and ArrivalMode for new paths.",
"deprecated": true
}, },
"MountRequired": { "MountRequired": {
"type": "boolean", "type": "boolean",
"description": "Quest mechanic: the quest requires the player to be mounted (e.g. mounted escort, chocobo race). Takes priority over Mount and auto-decide." "description": "QUEST REQUIREMENT: Use only when the quest mechanic itself requires being mounted, such as a mounted escort or race. Overrides TravelMode and ArrivalMode."
}, },
"DismountRequired": { "DismountRequired": {
"type": "boolean", "type": "boolean",
"description": "Quest mechanic: the quest requires the player to be on foot (e.g. interaction that fails while mounted). Takes priority over Mount and auto-decide." "description": "QUEST REQUIREMENT: Use only when the quest mechanic itself fails while mounted. Overrides TravelMode and ArrivalMode."
}, },
"Fly": { "Fly": {
"type": "boolean", "type": "boolean",
"description": "If true and flying is unlocked in a zone, will use a flight path" "description": "LEGACY COMPATIBILITY: Old flight hint retained for existing quests. Use TravelMode for new paths.",
"deprecated": true
}, },
"Land": { "Land": {
"type": "boolean", "type": "boolean",
"description": "If true and flying, will attempt to land on the ground" "description": "LEGACY COMPATIBILITY: Old arrival hint retained for existing quests. Use ArrivalMode for new paths.",
"deprecated": true
}, },
"Sprint": { "Sprint": {
"type": [ "type": [
"boolean", "boolean",
"null" "null"
] ],
"description": "GENERAL MOVEMENT: Controls Sprint independently of mount and flight behavior."
}, },
"ItemId": { "ItemId": {
"type": [ "type": [

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]
{ {
73, 4, 92, 189, 214, 0, 53, 235, 105, 164, 153, 30, 158, 74, 159, 14, 199, 185, 40, 166,
216, 243, 244, 200, 198, 26, 227, 227, 153, 121, 85, 6, 37, 167, 5, 38, 221, 72, 99, 65,
241, 147, 166, 171, 63, 63, 121, 152, 52, 115, 160, 20, 49, 186, 80, 230, 97, 96, 19, 189,
89, 201 194, 228
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
243, 132, 180, 1, 83, 89, 202, 66, 33, 20, 30, 173, 221, 145, 116, 199, 45, 52, 72, 186,
242, 184, 99, 15, 215, 2, 4, 125, 221, 134, 186, 181, 129, 66, 39, 24, 195, 19, 249, 56,
186, 51, 132, 151, 141, 220, 230, 57, 252, 77, 14, 14, 134, 179, 100, 23, 222, 226, 221, 184,
103, 57 242, 90
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using SmartNav.Model.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class ArrivalModeConverter : EnumConverter<EArrivalMode>
{
private static readonly Dictionary<EArrivalMode, string> Values = new Dictionary<EArrivalMode, string>
{
{
EArrivalMode.Auto,
"Auto"
},
{
EArrivalMode.StayMounted,
"StayMounted"
},
{
EArrivalMode.Land,
"Land"
},
{
EArrivalMode.Dismount,
"Dismount"
}
};
public ArrivalModeConverter()
: base((IReadOnlyDictionary<EArrivalMode, string>)Values)
{
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using SmartNav.Model.Converter;
namespace Questionable.Model.Questing.Converter;
public sealed class TravelModeConverter : EnumConverter<ETravelMode>
{
private static readonly Dictionary<ETravelMode, string> Values = new Dictionary<ETravelMode, string>
{
{
ETravelMode.Auto,
"Auto"
},
{
ETravelMode.OnFoot,
"OnFoot"
},
{
ETravelMode.GroundMount,
"GroundMount"
},
{
ETravelMode.Flying,
"Flying"
}
};
public TravelModeConverter()
: base((IReadOnlyDictionary<ETravelMode, string>)Values)
{
}
}

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(ArrivalModeConverter))]
public enum EArrivalMode
{
Auto,
StayMounted,
Land,
Dismount
}

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
using Questionable.Model.Questing.Converter;
namespace Questionable.Model.Questing;
[JsonConverter(typeof(TravelModeConverter))]
public enum ETravelMode
{
Auto,
OnFoot,
GroundMount,
Flying
}

View file

@ -45,6 +45,10 @@ public sealed class QuestStep
public bool DisableNavmesh { get; set; } public bool DisableNavmesh { get; set; }
public ETravelMode TravelMode { get; set; }
public EArrivalMode ArrivalMode { get; set; }
public bool? Mount { get; set; } public bool? Mount { get; set; }
public bool MountRequired { get; set; } public bool MountRequired { get; set; }

View file

@ -56,19 +56,19 @@ internal sealed class BossModModule : ICombatModule, IDisposable
if ((uint)(valueOrDefault - 3) <= 1u) if ((uint)(valueOrDefault - 3) <= 1u)
{ {
flag = true; flag = true;
goto IL_00bd; goto IL_00be;
} }
} }
flag = false; flag = false;
goto IL_00bd; goto IL_00be;
} }
goto IL_012e; goto IL_012f;
IL_00bd: IL_00be:
bool flag2 = flag; bool flag2 = flag;
_logger.LogDebug("BossModModule.Start: isRanged={IsRanged}, job role={Role}", flag2, localPlayer.ClassJob.ValueNullable?.Role); _logger.LogDebug("BossModModule.Start: isRanged={IsRanged}, job role={Role}", flag2, localPlayer.ClassJob.ValueNullable?.Role);
_bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, flag2); _bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, flag2);
goto IL_012e; goto IL_012f;
IL_012e: IL_012f:
return true; return true;
} }
catch (IpcError exception) catch (IpcError exception)

View file

@ -29,12 +29,28 @@ internal sealed class ItemUseModule : ICombatModule
private long _continueAt; private long _continueAt;
private bool _isWaitingToUseItem;
private bool _itemUsePending; private bool _itemUsePending;
private long _itemUsePendingUntil; private long _itemUsePendingUntil;
private int _itemCountBeforeUse;
public ICombatModule? Delegate => _delegate; public ICombatModule? Delegate => _delegate;
public bool IsItemUseInProgress
{
get
{
if (_isWaitingToUseItem || _itemUsePending)
{
return Environment.TickCount64 < _itemUsePendingUntil;
}
return false;
}
}
public ItemUseModule(IServiceProvider serviceProvider, ICondition condition, ILogger<ItemUseModule> logger) public ItemUseModule(IServiceProvider serviceProvider, ICondition condition, ILogger<ItemUseModule> logger)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
@ -62,6 +78,7 @@ internal sealed class ItemUseModule : ICombatModule
_combatData = combatData; _combatData = combatData;
_isDoingRotation = true; _isDoingRotation = true;
_continueAt = Environment.TickCount64; _continueAt = Environment.TickCount64;
_isWaitingToUseItem = false;
_itemUsePending = false; _itemUsePending = false;
return true; return true;
} }
@ -73,11 +90,13 @@ internal sealed class ItemUseModule : ICombatModule
if (_isDoingRotation) if (_isDoingRotation)
{ {
_delegate.Stop(); _delegate.Stop();
}
_isDoingRotation = false; _isDoingRotation = false;
_isWaitingToUseItem = false;
_itemUsePending = false;
_combatData = null; _combatData = null;
_delegate = null; _delegate = null;
_continueAt = Environment.TickCount64; _continueAt = Environment.TickCount64;
}
return true; return true;
} }
@ -92,44 +111,79 @@ internal sealed class ItemUseModule : ICombatModule
_delegate.Update(nextTarget); _delegate.Update(nextTarget);
} }
else if (_combatData.KillEnemyDataIds.Contains(nextTarget.BaseId) || _combatData.ComplexCombatDatas.Any((ComplexCombatData x) => x.DataId == nextTarget.BaseId && (!x.NameId.HasValue || (nextTarget is ICharacter character && x.NameId == character.NameId)))) else if (_combatData.KillEnemyDataIds.Contains(nextTarget.BaseId) || _combatData.ComplexCombatDatas.Any((ComplexCombatData x) => x.DataId == nextTarget.BaseId && (!x.NameId.HasValue || (nextTarget is ICharacter character && x.NameId == character.NameId))))
{
if (_isDoingRotation)
{ {
int inventoryItemCount = InventoryManager.Instance()->GetInventoryItemCount(_combatData.CombatItemUse.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0); int inventoryItemCount = InventoryManager.Instance()->GetInventoryItemCount(_combatData.CombatItemUse.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0);
if (_itemUsePending) if (_itemUsePending)
{ {
if (Environment.TickCount64 < _itemUsePendingUntil) if (inventoryItemCount < _itemCountBeforeUse)
{
_logger.LogDebug("Item use pending; ignoring temporary inventory count={Count}", inventoryItemCount);
}
else
{ {
_itemUsePending = false; _itemUsePending = false;
return;
} }
if (_condition[ConditionFlag.Casting])
{
_itemUsePendingUntil = Environment.TickCount64 + 1000;
} }
if (!_itemUsePending && inventoryItemCount == 0) if (Environment.TickCount64 < _itemUsePendingUntil)
{
_logger.LogDebug("Item use pending; inventory count={Count}", inventoryItemCount);
return;
}
_logger.LogDebug("Item use was not observed; retrying");
_itemUsePending = false;
_isWaitingToUseItem = true;
_itemUsePendingUntil = Environment.TickCount64 + 4000;
_continueAt = Environment.TickCount64 + 1000;
}
else if (_isDoingRotation)
{
if (inventoryItemCount == 0)
{ {
_delegate.Update(nextTarget); _delegate.Update(nextTarget);
} }
else if (ShouldUseItem(nextTarget)) else if (ShouldUseItem(nextTarget))
{ {
_logger.LogDebug("Attempting to use item {ItemId}", _combatData.CombatItemUse.ItemId); _logger.LogDebug("Pausing rotation before using item {ItemId}", _combatData.CombatItemUse.ItemId);
_itemUsePending = true; _isDoingRotation = false;
_itemUsePendingUntil = Environment.TickCount64 + 3000; _isWaitingToUseItem = true;
AgentInventoryContext.Instance()->UseItem(_combatData.CombatItemUse.ItemId, InventoryType.Invalid, 0u, 0); _delegate.Stop();
_continueAt = Environment.TickCount64 + 2000; _continueAt = Environment.TickCount64 + 3000;
_itemUsePendingUntil = Environment.TickCount64 + 4000;
} }
else else
{ {
_delegate.Update(nextTarget); _delegate.Update(nextTarget);
} }
} }
else if (_isWaitingToUseItem)
{
if (_condition[ConditionFlag.Casting])
{
_continueAt = Environment.TickCount64 + 500;
_itemUsePendingUntil = Environment.TickCount64 + 4000;
return;
}
if (!ShouldUseItem(nextTarget) || inventoryItemCount == 0)
{
_isWaitingToUseItem = false;
return;
}
_logger.LogDebug("Attempting to use item {ItemId}", _combatData.CombatItemUse.ItemId);
long num = AgentInventoryContext.Instance()->UseItem(_combatData.CombatItemUse.ItemId, InventoryType.Invalid, 0u, 0);
_logger.LogDebug("UseItem result: {Result}", num);
bool itemUsePending = (((ulong)num <= 1uL) ? true : false);
_itemUsePending = itemUsePending;
_isWaitingToUseItem = !_itemUsePending;
_itemCountBeforeUse = inventoryItemCount;
_itemUsePendingUntil = Environment.TickCount64 + 3000;
_continueAt = Environment.TickCount64 + (_itemUsePending ? 2000 : 500);
}
else if (_condition[ConditionFlag.Casting]) else if (_condition[ConditionFlag.Casting])
{ {
long num = Environment.TickCount64 + 500; long num2 = Environment.TickCount64 + 500;
if (num > _continueAt) if (num2 > _continueAt)
{ {
_continueAt = num; _continueAt = num2;
} }
} }
else else

View file

@ -31,6 +31,8 @@ internal static class Dive
internal sealed class Task : ITask internal sealed class Task : ITask
{ {
public bool SmartNavRouted { get; init; }
public override string ToString() public override string ToString()
{ {
return "Dive"; return "Dive";
@ -51,6 +53,10 @@ internal static class Dive
{ {
return false; return false;
} }
if (base.Task.SmartNavRouted && !condition[ConditionFlag.Swimming])
{
return false;
}
if (condition[ConditionFlag.Mounted] || condition[ConditionFlag.Swimming]) if (condition[ConditionFlag.Mounted] || condition[ConditionFlag.Swimming])
{ {
Descend(); Descend();

View file

@ -80,14 +80,14 @@ internal static class Duty
private EDutyMode GetEffectiveEDutyMode(uint cfcId, EDutyMode? registryDutyMode) private EDutyMode GetEffectiveEDutyMode(uint cfcId, EDutyMode? registryDutyMode)
{ {
if (registryDutyMode.HasValue)
{
return registryDutyMode.Value;
}
if (configuration.Duties.DutyModeOverrides.TryGetValue(cfcId, out var value)) if (configuration.Duties.DutyModeOverrides.TryGetValue(cfcId, out var value))
{ {
return value; return value;
} }
if (registryDutyMode.HasValue)
{
return registryDutyMode.Value;
}
EDutyMode defaultDutyMode = configuration.Duties.DefaultDutyMode; EDutyMode defaultDutyMode = configuration.Duties.DefaultDutyMode;
if (defaultDutyMode == EDutyMode.Support && configuration.Duties.AutoUnsyncOverleveled && IsSafelyOverleveled(cfcId)) if (defaultDutyMode == EDutyMode.Support && configuration.Duties.AutoUnsyncOverleveled && IsSafelyOverleveled(cfcId))
{ {
@ -380,7 +380,7 @@ internal static class Duty
} }
} }
internal sealed record EnableBossModForDutyTask : ITask internal sealed record EnableBossModForDutyTask(bool ActivateAi = true) : ITask
{ {
public override string ToString() public override string ToString()
{ {
@ -392,7 +392,7 @@ internal static class Duty
{ {
protected override bool Start() protected override bool Start()
{ {
bossModIpc.EnableAi(BossModIpc.EPreset.Active); bossModIpc.EnableAi(BossModIpc.EPreset.Active, base.Task.ActivateAi);
IPlayerCharacter localPlayer = objectTable.LocalPlayer; IPlayerCharacter localPlayer = objectTable.LocalPlayer;
bool flag; bool flag;
if (localPlayer != null) if (localPlayer != null)
@ -404,18 +404,18 @@ internal static class Duty
if ((uint)(valueOrDefault - 3) <= 1u) if ((uint)(valueOrDefault - 3) <= 1u)
{ {
flag = true; flag = true;
goto IL_0077; goto IL_0082;
} }
} }
flag = false; flag = false;
goto IL_0077; goto IL_0082;
} }
goto IL_0087; goto IL_0092;
IL_0077: IL_0082:
bool isRanged = flag; bool isRanged = flag;
bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, isRanged); bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, isRanged);
goto IL_0087; goto IL_0092;
IL_0087: IL_0092:
logger.LogDebug("Enabled BossMod Active preset for AutoDuty run"); logger.LogDebug("Enabled BossMod Active preset for AutoDuty run");
return true; return true;
} }
@ -463,7 +463,7 @@ internal static class Duty
} }
} }
internal sealed record EnableBossModPassiveForDutyTask : ITask internal sealed record EnableBossModPassiveForDutyTask(bool ActivateAi = true) : ITask
{ {
public override string ToString() public override string ToString()
{ {
@ -475,7 +475,7 @@ internal static class Duty
{ {
protected override bool Start() protected override bool Start()
{ {
bossModIpc.EnableAi(BossModIpc.EPreset.Passive); bossModIpc.EnableAi(BossModIpc.EPreset.Passive, base.Task.ActivateAi);
logger.LogDebug("Enabled BossMod Passive preset for AutoDuty run (hazard avoidance)"); logger.LogDebug("Enabled BossMod Passive preset for AutoDuty run (hazard avoidance)");
return true; return true;
} }

View file

@ -80,6 +80,8 @@ internal static class EquipRecommended
private bool _checkedOrTriggeredEquipmentUpdate; private bool _checkedOrTriggeredEquipmentUpdate;
private List<uint>? _gameRecommendedItemIds;
private long _timeoutAt; private long _timeoutAt;
private List<(int Slot, BestItemRef Item)>? _smartUpgrades; private List<(int Slot, BestItemRef Item)>? _smartUpgrades;
@ -177,8 +179,7 @@ internal static class EquipRecommended
{ {
if (!TryStartMove(pendingMove)) if (!TryStartMove(pendingMove))
{ {
chatGui.Print($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} rejected; skipping equip step.", "Questionable", 576); throw EquipFailure($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} was rejected");
return ETaskResult.TaskComplete;
} }
_currentMoveStarted = true; _currentMoveStarted = true;
_timeoutAt = Environment.TickCount64 + 2000; _timeoutAt = Environment.TickCount64 + 2000;
@ -196,8 +197,7 @@ internal static class EquipRecommended
} }
if (Environment.TickCount64 >= _timeoutAt) if (Environment.TickCount64 >= _timeoutAt)
{ {
chatGui.Print($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} timed out; skipping equip step.", "Questionable", 576); throw EquipFailure($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} timed out");
return ETaskResult.TaskComplete;
} }
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -227,11 +227,11 @@ internal static class EquipRecommended
{ {
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
if (Environment.TickCount64 < _timeoutAt) if (Environment.TickCount64 >= _timeoutAt)
{ {
return ETaskResult.StillRunning; throw EquipFailure("Recommended gear was not equipped before the timeout");
} }
return ETaskResult.TaskComplete; return ETaskResult.StillRunning;
} }
private ETaskResult DirectEquipPhase() private ETaskResult DirectEquipPhase()
@ -247,20 +247,18 @@ internal static class EquipRecommended
{ {
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
if (Environment.TickCount64 < _timeoutAt) if (Environment.TickCount64 >= _timeoutAt)
{ {
return ETaskResult.StillRunning; throw EquipFailure("Recommended gear was not equipped before the timeout");
} }
return ETaskResult.TaskComplete; return ETaskResult.StillRunning;
} }
var (num, item) = smartUpgrades[_directEquipCursor]; var (num, item) = smartUpgrades[_directEquipCursor];
if (!_directEquipStarted) if (!_directEquipStarted)
{ {
if (!TryStartDirectEquip(item, num)) if (!TryStartDirectEquip(item, num))
{ {
chatGui.Print($"Direct equip for slot {num} rejected, skipping.", "Questionable", 576); throw EquipFailure($"Direct equip for slot {num} was rejected");
_directEquipCursor++;
return ETaskResult.StillRunning;
} }
_directEquipStarted = true; _directEquipStarted = true;
_timeoutAt = Environment.TickCount64 + 2000; _timeoutAt = Environment.TickCount64 + 2000;
@ -274,8 +272,7 @@ internal static class EquipRecommended
} }
if (Environment.TickCount64 >= _timeoutAt) if (Environment.TickCount64 >= _timeoutAt)
{ {
_directEquipCursor++; throw EquipFailure($"Direct equip for slot {num} timed out");
_directEquipStarted = false;
} }
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -366,7 +363,8 @@ internal static class EquipRecommended
} }
if (!_checkedOrTriggeredEquipmentUpdate) if (!_checkedOrTriggeredEquipmentUpdate)
{ {
if (IsAllRecommendedGearEquipped()) _gameRecommendedItemIds = SnapshotRecommendedItemIds(ptr);
if (AreAllItemsEquipped(_gameRecommendedItemIds))
{ {
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
@ -376,16 +374,16 @@ internal static class EquipRecommended
_checkedOrTriggeredEquipmentUpdate = true; _checkedOrTriggeredEquipmentUpdate = true;
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (IsAllRecommendedGearEquipped()) if (_gameRecommendedItemIds != null && AreAllItemsEquipped(_gameRecommendedItemIds))
{ {
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
if (Environment.TickCount64 < _timeoutAt) if (Environment.TickCount64 >= _timeoutAt)
{ {
throw EquipFailure("EquipRecommendedGear did not finish before the timeout");
}
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
return ETaskResult.TaskComplete;
}
private unsafe bool AreUpgradesApplied(IReadOnlyList<(int Slot, BestItemRef Item)> upgrades) private unsafe bool AreUpgradesApplied(IReadOnlyList<(int Slot, BestItemRef Item)> upgrades)
{ {
@ -621,19 +619,70 @@ internal static class EquipRecommended
return (byte)classJobLevel; return (byte)classJobLevel;
} }
private unsafe bool IsAllRecommendedGearEquipped() private unsafe static List<uint> SnapshotRecommendedItemIds(RecommendEquipModule* recommendedEquipModule)
{ {
Span<Pointer<InventoryItem>> recommendedItems = RecommendEquipModule.Instance()->RecommendedItems; List<uint> list = new List<uint>();
Span<Pointer<InventoryItem>> recommendedItems = recommendedEquipModule->RecommendedItems;
for (int i = 0; i < recommendedItems.Length; i++) for (int i = 0; i < recommendedItems.Length; i++)
{ {
Pointer<InventoryItem> pointer = recommendedItems[i]; Pointer<InventoryItem> pointer = recommendedItems[i];
InventoryItem* value = pointer.Value; InventoryItem* value = pointer.Value;
if (value != null && value->ItemId != 0 && !InventoryHelper.IsItemEquipped(value->ItemId)) if (value != null && value->ItemId != 0)
{
list.Add(((ItemHandle)value->ItemId).Id);
}
}
return list;
}
private unsafe static bool AreAllItemsEquipped(IReadOnlyList<uint> expectedItemIds)
{
InventoryManager* ptr = InventoryManager.Instance();
if (ptr == null)
{ {
return false; return false;
} }
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems);
if (inventoryContainer == null)
{
return false;
} }
return true; Dictionary<uint, int> dictionary = new Dictionary<uint, int>();
foreach (uint expectedItemId in expectedItemIds)
{
dictionary[expectedItemId] = dictionary.GetValueOrDefault(expectedItemId) + 1;
}
for (int i = 0; i < inventoryContainer->Size; i++)
{
if (dictionary.Count <= 0)
{
break;
}
InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i);
if (inventorySlot == null || inventorySlot->ItemId == 0)
{
continue;
}
uint id = ItemHandle.FromInventorySlot(inventorySlot).Id;
if (dictionary.TryGetValue(id, out var value))
{
if (value == 1)
{
dictionary.Remove(id);
}
else
{
dictionary[id] = value - 1;
}
}
}
return dictionary.Count == 0;
}
private TaskException EquipFailure(string message)
{
chatGui.PrintError(message + "; stopping automation.", "Questionable", 576);
return new TaskException(message);
} }
public override bool ShouldInterruptOnDamage() public override bool ShouldInterruptOnDamage()

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
@ -16,12 +17,13 @@ using Questionable.Controller.Utils;
using Questionable.Functions; using Questionable.Functions;
using Questionable.Model; using Questionable.Model;
using Questionable.Model.Questing; using Questionable.Model.Questing;
using SmartNav.Data;
namespace Questionable.Controller.Steps.Interactions; namespace Questionable.Controller.Steps.Interactions;
internal static class Interact internal static class Interact
{ {
internal sealed class Factory(Configuration configuration) : ITaskFactory internal sealed class Factory(Configuration configuration, WarpDataService warpDataService) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -63,6 +65,25 @@ internal static class Interact
{ {
yield return new WaitAtEnd.WaitDelay(); yield return new WaitAtEnd.WaitDelay();
} }
ushort? targetTerritoryId = step.TargetTerritoryId;
List<uint> list;
if (targetTerritoryId.HasValue)
{
ushort targetTerritoryId2 = targetTerritoryId.GetValueOrDefault();
list = (from x in warpDataService.Warps
where x.NpcDataId == step.DataId && x.SourceTerritoryId == step.TerritoryId && x.DestTerritoryId == targetTerritoryId2
select x.RowId).Distinct().Take(2).ToList();
}
else
{
list = new List<uint>();
}
List<uint> list2 = list;
if (list2.Count == 1)
{
yield return new WarpInteract.Task(step.DataId.Value, list2[0], step.TerritoryId, step.TargetTerritoryId.Value);
yield break;
}
uint value = step.DataId.Value; uint value = step.DataId.Value;
EInteractionType interactionType2 = step.InteractionType; EInteractionType interactionType2 = step.InteractionType;
int skipMarkerCheck; int skipMarkerCheck;
@ -74,20 +95,17 @@ internal static class Interact
SkipStepConditions stepIf = skipConditions.StepIf; SkipStepConditions stepIf = skipConditions.StepIf;
if (stepIf != null && stepIf.Never) if (stepIf != null && stepIf.Never)
{ {
goto IL_024a; goto IL_03d7;
} }
} }
if (step.InteractionType != EInteractionType.PurchaseItem) if (step.InteractionType != EInteractionType.PurchaseItem)
{ {
skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0); skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0);
goto IL_024b; goto IL_03d8;
} }
} }
goto IL_024a; goto IL_03d7;
IL_024a: IL_03d8:
skipMarkerCheck = 1;
goto IL_024b;
IL_024b:
uint? pickUpItemId = step.PickUpItemId ?? step.GCPurchase?.ItemId; uint? pickUpItemId = step.PickUpItemId ?? step.GCPurchase?.ItemId;
byte? taxiStandId = step.TaxiStandId; byte? taxiStandId = step.TaxiStandId;
SkipStepConditions? skipConditions2 = step.SkipConditions?.StepIf; SkipStepConditions? skipConditions2 = step.SkipConditions?.StepIf;
@ -95,6 +113,10 @@ internal static class Interact
uint? purchaseItemId = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemId : ((uint?)null)); uint? purchaseItemId = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemId : ((uint?)null));
int purchaseItemCount = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemCount.GetValueOrDefault() : 0); int purchaseItemCount = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemCount.GetValueOrDefault() : 0);
yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, pickUpItemId, taxiStandId, skipConditions2, completionQuestVariablesFlags, null, purchaseItemId, purchaseItemCount); yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, pickUpItemId, taxiStandId, skipConditions2, completionQuestVariablesFlags, null, purchaseItemId, purchaseItemCount);
yield break;
IL_03d7:
skipMarkerCheck = 1;
goto IL_03d8;
} }
} }

View file

@ -33,7 +33,7 @@ internal static class SinglePlayerDuty
public const ushort Naadam = 688; public const ushort Naadam = 688;
} }
internal sealed class Factory(BossModIpc bossModIpc, WrathComboModule wrathComboModule, RotationSolverRebornModule rsrModule, Configuration configuration, TerritoryData territoryData, QuestFunctions questFunctions, DutyRetryState dutyRetryState, ICondition condition, IClientState clientState, IObjectTable objectTable) : ITaskFactory internal sealed class Factory(BossModIpc bossModIpc, WrathComboModule wrathComboModule, RotationSolverRebornModule rsrModule, Configuration configuration, TerritoryData territoryData, QuestFunctions questFunctions, GameFunctions gameFunctions, DutyRetryState dutyRetryState, ICondition condition, IClientState clientState, IObjectTable objectTable) : ITaskFactory
{ {
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{ {
@ -54,7 +54,12 @@ internal static class SinglePlayerDuty
byte seqBefore = questProgressInfo?.Sequence ?? 0; byte seqBefore = questProgressInfo?.Sequence ?? 0;
IReadOnlyList<byte> varsBefore = questProgressInfo?.Variables.ToArray(); IReadOnlyList<byte> varsBefore = questProgressInfo?.Variables.ToArray();
dutyRetryState.Reset(); dutyRetryState.Reset();
List<ITask> dutyTasks = new List<ITask>(); List<ITask> dutyTasks = new List<ITask>
{
new StartSinglePlayerDuty(contentFinderConditionData.ContentFinderConditionId),
new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(10L)),
new WaitCondition.Task(() => !gameFunctions.IsOccupied(), "Wait(solo duty ready)")
};
if (wrathComboModule.IsAvailable()) if (wrathComboModule.IsAvailable())
{ {
dutyTasks.Add(new Duty.EnableWrathForDutyTask()); dutyTasks.Add(new Duty.EnableWrathForDutyTask());
@ -65,14 +70,12 @@ internal static class SinglePlayerDuty
} }
if (configuration.General.CombatModule == Configuration.ECombatModule.BossMod && bossModIpc.IsSupported()) if (configuration.General.CombatModule == Configuration.ECombatModule.BossMod && bossModIpc.IsSupported())
{ {
dutyTasks.Add(new Duty.EnableBossModForDutyTask()); dutyTasks.Add(new Duty.EnableBossModForDutyTask(configuration.SinglePlayerDuties.EnableBossModAi));
} }
else if (bossModIpc.IsSupported()) else if (bossModIpc.IsSupported())
{ {
dutyTasks.Add(new Duty.EnableBossModPassiveForDutyTask()); dutyTasks.Add(new Duty.EnableBossModPassiveForDutyTask(configuration.SinglePlayerDuties.EnableBossModAi));
} }
dutyTasks.Add(new StartSinglePlayerDuty(contentFinderConditionData.ContentFinderConditionId));
dutyTasks.Add(new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(2L)));
dutyTasks.Add(new EnableAi()); dutyTasks.Add(new EnableAi());
if (contentFinderConditionData.TerritoryId == 1052) if (contentFinderConditionData.TerritoryId == 1052)
{ {
@ -132,8 +135,6 @@ internal static class SinglePlayerDuty
internal sealed class StartSinglePlayerDutyExecutor(ICondition condition) : TaskExecutor<StartSinglePlayerDuty>() internal sealed class StartSinglePlayerDutyExecutor(ICondition condition) : TaskExecutor<StartSinglePlayerDuty>()
{ {
private long _enteredAtMs;
protected override bool Start() protected override bool Start()
{ {
return true; return true;
@ -149,14 +150,6 @@ internal static class SinglePlayerDuty
{ {
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (_enteredAtMs == 0L)
{
_enteredAtMs = Environment.TickCount64;
}
if (Environment.TickCount64 - _enteredAtMs < 2000)
{
return ETaskResult.StillRunning;
}
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }

View file

@ -130,10 +130,10 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
}; };
} }
} }
float num2 = base.Task.StopDistance ?? 3f; float stopDistance = base.Task.StopDistance ?? 3f;
Vector3? vector = _objectTable.LocalPlayer?.Position; Vector3? vector = _objectTable.LocalPlayer?.Position;
float num3 = ((!vector.HasValue) ? float.MaxValue : Vector3.Distance(vector.Value, _destination)); float num2 = ((!vector.HasValue) ? float.MaxValue : Vector3.Distance(vector.Value, _destination));
if (num3 > num2) if (!vector.HasValue || !IsWithinInteractionRange(vector.Value, _destination, stopDistance, GetVerticalStopDistance()))
{ {
PrepareMovementIfNeeded(); PrepareMovementIfNeeded();
} }
@ -176,7 +176,7 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
else if (!base.Task.DisableNavmesh) else if (!base.Task.DisableNavmesh)
{ {
bool flag = _gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId); bool flag = _gameFunctions.IsFlyingUnlocked(base.Task.TerritoryId);
if (!base.Task.SmartNavRouted && !base.Task.Fly && flag && num3 > _configuration.Navigation.MountFlyDistance) if (!base.Task.SmartNavRouted && !base.Task.Fly && flag && num2 > _configuration.Navigation.MountFlyDistance)
{ {
base.Task = base.Task with base.Task = base.Task with
{ {
@ -184,8 +184,8 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
Land = true Land = true
}; };
} }
float num4 = (base.Task.Fly ? _configuration.Navigation.MountFlyDistance : _configuration.Navigation.MountGroundDistance); float num3 = (base.Task.Fly ? _configuration.Navigation.MountFlyDistance : _configuration.Navigation.MountGroundDistance);
Questionable.Controller.Steps.Common.Mount.EMountIf mountIf = ((!(num3 > num4)) ? Questionable.Controller.Steps.Common.Mount.EMountIf.AwayFromPosition : Questionable.Controller.Steps.Common.Mount.EMountIf.Always); Questionable.Controller.Steps.Common.Mount.EMountIf mountIf = ((!(num2 > num3)) ? Questionable.Controller.Steps.Common.Mount.EMountIf.AwayFromPosition : Questionable.Controller.Steps.Common.Mount.EMountIf.Always);
Questionable.Controller.Steps.Common.Mount.MountTask mountTask3 = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, mountIf, _destination); Questionable.Controller.Steps.Common.Mount.MountTask mountTask3 = new Questionable.Controller.Steps.Common.Mount.MountTask(base.Task.TerritoryId, mountIf, _destination);
long retryAtMs = 0L; long retryAtMs = 0L;
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? tuple; (Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? tuple;
@ -244,7 +244,7 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
} }
IPlayerCharacter localPlayer = _objectTable.LocalPlayer; IPlayerCharacter localPlayer = _objectTable.LocalPlayer;
float num = base.Task.StopDistance ?? 3f; float num = base.Task.StopDistance ?? 3f;
if (base.Task.Fly && base.Task.Land && _canRestart && localPlayer != null && _clientState.TerritoryType == base.Task.TerritoryId && Vector3.Distance(localPlayer.Position, _destination) > num) if (base.Task.Fly && base.Task.Land && _canRestart && localPlayer != null && _clientState.TerritoryType == base.Task.TerritoryId && (_condition[ConditionFlag.InFlight] || Vector3.Distance(localPlayer.Position, _destination) > num))
{ {
if (_condition[ConditionFlag.InFlight]) if (_condition[ConditionFlag.InFlight])
{ {
@ -255,20 +255,25 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
} }
if (!_movementController.IsNavmeshReady) if (!_movementController.IsNavmeshReady)
{ {
_logger.LogDebug("Airborne outside stop distance, waiting for navmesh to descend"); _logger.LogDebug("Still airborne after fly-and-land movement, waiting for navmesh to descend");
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
Vector3 position = localPlayer.Position; Vector3 destination = _destination;
position.Y = localPlayer.Position.Y - 100f; destination.Y = localPlayer.Position.Y;
Vector3 vector = position; Vector3 vector = destination;
destination = _destination;
destination.Y = _destination.Y - 100f;
Vector3 vector2 = destination;
_descendAttempted = true; _descendAttempted = true;
_logger.LogDebug("Outside stop distance but still airborne, descending to {Target}", vector.ToString("G", CultureInfo.InvariantCulture)); _logger.LogDebug("Still airborne, moving above {LandingTarget} and descending to {DescendTarget}", _destination.ToString("G", CultureInfo.InvariantCulture), vector2.ToString("G", CultureInfo.InvariantCulture));
MovementController movementController = _movementController; MovementController movementController = _movementController;
uint? dataId = base.Task.DataId; uint? dataId = base.Task.DataId;
int num2 = 1; int num2 = 2;
List<Vector3> list = new List<Vector3>(num2); List<Vector3> list = new List<Vector3>(num2);
CollectionsMarshal.SetCount(list, num2); CollectionsMarshal.SetCount(list, num2);
CollectionsMarshal.AsSpan(list)[0] = vector; Span<Vector3> span = CollectionsMarshal.AsSpan(list);
span[0] = vector;
span[1] = vector2;
movementController.NavigateTo(EMovementType.Landing, dataId, list, fly: true, sprint: false, null); movementController.NavigateTo(EMovementType.Landing, dataId, list, fly: true, sprint: false, null);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -276,22 +281,29 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
_logger.LogDebug("Landed outside stop distance, closing gap on foot"); _logger.LogDebug("Landed outside stop distance, closing gap on foot");
MovementController movementController2 = _movementController; MovementController movementController2 = _movementController;
uint? dataId2 = base.Task.DataId; uint? dataId2 = base.Task.DataId;
Vector3 destination = _destination; Vector3 destination2 = _destination;
float? stopDistance = base.Task.StopDistance; float? stopDistance = base.Task.StopDistance;
bool smartNavRouted = base.Task.SmartNavRouted; bool smartNavRouted = base.Task.SmartNavRouted;
movementController2.NavigateTo(EMovementType.Quest, dataId2, destination, fly: false, sprint: false, stopDistance, null, land: false, smartNavRouted); movementController2.NavigateTo(EMovementType.Quest, dataId2, destination2, fly: false, sprint: false, stopDistance, null, land: false, smartNavRouted);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
if (base.Task.DataId.HasValue && !_adjustedToObject && localPlayer != null && !_condition[ConditionFlag.InFlight] && _clientState.TerritoryType == base.Task.TerritoryId) if (base.Task.DataId.HasValue && localPlayer != null && !_condition[ConditionFlag.InFlight] && _clientState.TerritoryType == base.Task.TerritoryId)
{ {
IGameObject gameObject = _gameFunctions.FindObjectByDataId(base.Task.DataId.Value, null, warnIfMissing: false); IGameObject gameObject = _gameFunctions.FindObjectByDataId(base.Task.DataId.Value, null, warnIfMissing: false);
if (gameObject != null) if (gameObject != null)
{ {
float num3 = Vector3.Distance(localPlayer.Position, gameObject.Position); float num3 = Vector3.Distance(localPlayer.Position, gameObject.Position);
if (num3 > num) float num4 = Math.Abs(localPlayer.Position.Y - gameObject.Position.Y);
if (!IsWithinInteractionRange(localPlayer.Position, gameObject.Position, num, GetVerticalStopDistance()))
{ {
if (_adjustedToObject)
{
_logger.LogWarning("Movement to actual object position ended out of interaction range ({Distance:F1}y away, vertical difference {VerticalDistance:F1}y), treating as unreachable", num3, num4);
throw new MovementController.PathfindingFailedException($"Movement to object ended {num3:F1}y away with a {num4:F1}y vertical difference");
}
_adjustedToObject = true; _adjustedToObject = true;
_logger.LogDebug("Adjusting to actual object position ({Distance:F1}y away)", num3); _canRestart = false;
_logger.LogDebug("Adjusting to actual object position ({Distance:F1}y away, vertical difference {VerticalDistance:F1}y)", num3, num4);
_movementController.NavigateTo(EMovementType.Quest, base.Task.DataId, gameObject.Position, fly: false, sprint: false, base.Task.StopDistance); _movementController.NavigateTo(EMovementType.Quest, base.Task.DataId, gameObject.Position, fly: false, sprint: false, base.Task.StopDistance);
return ETaskResult.StillRunning; return ETaskResult.StillRunning;
} }
@ -316,13 +328,31 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
} }
if (base.Task.SmartNavRouted && !_adjustedToObject && localPlayer != null && _clientState.TerritoryType == base.Task.TerritoryId && Vector3.Distance(localPlayer.Position, _destination) > num + 5f) if (base.Task.SmartNavRouted && !_adjustedToObject && localPlayer != null && _clientState.TerritoryType == base.Task.TerritoryId && Vector3.Distance(localPlayer.Position, _destination) > num + 5f)
{ {
float num4 = Vector3.Distance(localPlayer.Position, _destination); float num5 = Vector3.Distance(localPlayer.Position, _destination);
_logger.LogWarning("SmartNav movement ended {Distance:F1}y from target (stop distance {StopDistance:F1}y), treating as unreachable", num4, num); _logger.LogWarning("SmartNav movement ended {Distance:F1}y from target (stop distance {StopDistance:F1}y), treating as unreachable", num5, num);
throw new MovementController.PathfindingFailedException($"SmartNav movement ended {num4:F1}y from target (stop distance {num:F1}y)"); throw new MovementController.PathfindingFailedException($"SmartNav movement ended {num5:F1}y from target (stop distance {num:F1}y)");
} }
return ETaskResult.TaskComplete; return ETaskResult.TaskComplete;
} }
private float GetVerticalStopDistance()
{
if (!base.Task.IgnoreDistanceToObject)
{
return 1.95f;
}
return float.MaxValue;
}
internal static bool IsWithinInteractionRange(Vector3 playerPosition, Vector3 targetPosition, float stopDistance, float verticalStopDistance)
{
if (Vector3.Distance(playerPosition, targetPosition) <= stopDistance)
{
return Math.Abs(playerPosition.Y - targetPosition.Y) <= verticalStopDistance;
}
return false;
}
private ETaskResult? UpdateMountState() private ETaskResult? UpdateMountState()
{ {
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement; (Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement;

View file

@ -7,10 +7,64 @@ namespace Questionable.Controller.Steps.Movement;
internal sealed record MoveTask(ushort TerritoryId, Vector3 Destination, bool? Mount = null, bool MountRequired = false, bool DismountRequired = false, float? StopDistance = null, uint? DataId = null, bool DisableNavmesh = false, bool? Sprint = null, bool Fly = false, bool Land = false, bool IgnoreDistanceToObject = false, bool RestartNavigation = true, EInteractionType InteractionType = EInteractionType.None, bool SmartNavRouted = false, bool AllowZoneTransition = false, string? RouteTargetNodeId = null, string? RouteApproachNodeId = null) : ITask internal sealed record MoveTask(ushort TerritoryId, Vector3 Destination, bool? Mount = null, bool MountRequired = false, bool DismountRequired = false, float? StopDistance = null, uint? DataId = null, bool DisableNavmesh = false, bool? Sprint = null, bool Fly = false, bool Land = false, bool IgnoreDistanceToObject = false, bool RestartNavigation = true, EInteractionType InteractionType = EInteractionType.None, bool SmartNavRouted = false, bool AllowZoneTransition = false, string? RouteTargetNodeId = null, string? RouteApproachNodeId = null) : ITask
{ {
public MoveTask(QuestStep step, Vector3 destination) public MoveTask(QuestStep step, Vector3 destination)
: this(step.TerritoryId, destination, step.Mount, step.MountRequired, step.DismountRequired, step.CalculateActualStopDistance(), step.DataId, step.DisableNavmesh, step.Sprint, step.Fly == true, step.Land == true, step.IgnoreDistanceToObject == true, step.RestartNavigationIfCancelled != false, step.InteractionType) : this(step.TerritoryId, destination, ResolveMount(step), step.MountRequired, step.DismountRequired, step.CalculateActualStopDistance(), step.DataId, step.DisableNavmesh, step.Sprint, ResolveFly(step), ResolveLand(step), step.IgnoreDistanceToObject == true, step.RestartNavigationIfCancelled != false, step.InteractionType)
{ {
} }
private static bool? ResolveMount(QuestStep step)
{
if (step.MountRequired)
{
return true;
}
if (step.DismountRequired)
{
return false;
}
switch (step.TravelMode)
{
case ETravelMode.OnFoot:
return false;
case ETravelMode.GroundMount:
case ETravelMode.Flying:
return true;
default:
return step.Mount;
}
}
private static bool ResolveFly(QuestStep step)
{
if (step.DismountRequired)
{
return false;
}
switch (step.TravelMode)
{
case ETravelMode.OnFoot:
case ETravelMode.GroundMount:
return false;
case ETravelMode.Flying:
return true;
default:
return step.Fly == true;
}
}
private static bool ResolveLand(QuestStep step)
{
ETravelMode travelMode = step.TravelMode;
bool flag = (uint)(travelMode - 1) <= 1u;
bool flag2 = !flag;
if (flag2)
{
EArrivalMode arrivalMode = step.ArrivalMode;
bool flag3 = (uint)(arrivalMode - 2) <= 1u;
flag2 = flag3 || step.Land == true;
}
return flag2;
}
public bool ShouldRedoOnInterrupt() public bool ShouldRedoOnInterrupt()
{ {
return true; return true;

View file

@ -109,6 +109,14 @@ internal static class MoveTo
yield return new LandTask(); yield return new LandTask();
} }
} }
if (step.ArrivalMode == EArrivalMode.StayMounted && !step.DismountRequired)
{
yield return new Mount.MountTask(step.TerritoryId, Mount.EMountIf.Always);
}
else if (step.ArrivalMode == EArrivalMode.Dismount && !step.MountRequired)
{
yield return new Mount.UnmountTask();
}
} }
} }
} }

View file

@ -30,6 +30,10 @@ internal static class SmartNavStep
{ {
yield break; yield break;
} }
(bool AllowMount, bool AllowFlight) capabilities = SmartNavReRouteService.GetCapabilities(step.TravelMode);
bool item = capabilities.AllowMount;
bool item2 = capabilities.AllowFlight;
playerNavState = playerNavState.WithTravelCapabilities(item, item2);
RouteResult routeResult = navRouter.FindRoute(playerNavState, step.TerritoryId, vector.Value); RouteResult routeResult = navRouter.FindRoute(playerNavState, step.TerritoryId, vector.Value);
if (routeResult.Segments.Count == 0) if (routeResult.Segments.Count == 0)
{ {
@ -37,10 +41,10 @@ internal static class SmartNavStep
} }
HandledRouting = true; HandledRouting = true;
logger.LogDebug("SmartNav routing to {Territory} with {Count} segments", territoryData.GetNameAndId(step.TerritoryId), routeResult.Segments.Count); logger.LogDebug("SmartNav routing to {Territory} with {Count} segments", territoryData.GetNameAndId(step.TerritoryId), routeResult.Segments.Count);
reRouteService.SetDestination(step.TerritoryId, vector.Value); reRouteService.SetDestination(step.TerritoryId, vector.Value, step.TravelMode);
foreach (ITask item in taskMapper.MapInstructions(instructionBuilder.Build(routeResult, step.TerritoryId, step.DisableNavmesh), step)) foreach (ITask item3 in taskMapper.MapInstructions(instructionBuilder.Build(routeResult, step.TerritoryId, step.DisableNavmesh), step, step.TravelMode))
{ {
yield return item; yield return item3;
} }
} }

View file

@ -261,6 +261,10 @@ internal sealed class CombatController : IDisposable
SetTarget(gameObject3); SetTarget(gameObject3);
} }
} }
if (_currentFight.Module is ItemUseModule { IsItemUseInProgress: not false })
{
return EStatus.InCombat;
}
if (_condition[ConditionFlag.InCombat]) if (_condition[ConditionFlag.InCombat])
{ {
_wasInCombat = true; _wasInCombat = true;
@ -362,43 +366,39 @@ internal sealed class CombatController : IDisposable
int num = 0; int num = 0;
float num2 = float.MaxValue; float num2 = float.MaxValue;
bool flag5 = _logger.IsEnabled(LogLevel.Debug); bool flag5 = _logger.IsEnabled(LogLevel.Debug);
foreach (IGameObject item in _objectTable) foreach (IGameObject item2 in _objectTable)
{ {
var (num3, text) = GetKillPriority(item, localPlayer); int item = GetKillPriority(item2, localPlayer).Priority;
if (num3 <= 0) if (item <= 0)
{ {
continue; continue;
} }
if (_losIgnoreList.TryGetValue(item.GameObjectId, out var value2)) if (_losIgnoreList.TryGetValue(item2.GameObjectId, out var value2))
{ {
if (Environment.TickCount64 < value2) if (Environment.TickCount64 < value2)
{ {
if (flag5) if (flag5)
{ {
_logger.LogDebug("Target {Name} ({Id:X8}) skipped - LoS ignore list ({Remaining}ms left)", item.Name, item.GameObjectId, value2 - Environment.TickCount64); _logger.LogDebug("Target {Name} ({Id:X8}) skipped - LoS ignore list ({Remaining}ms left)", item2.Name, item2.GameObjectId, value2 - Environment.TickCount64);
} }
continue; continue;
} }
_losIgnoreList.Remove(item.GameObjectId); _losIgnoreList.Remove(item2.GameObjectId);
if (flag5) if (flag5)
{ {
_logger.LogDebug("Target {Name} ({Id:X8}) - LoS ignore expired", item.Name, item.GameObjectId); _logger.LogDebug("Target {Name} ({Id:X8}) - LoS ignore expired", item2.Name, item2.GameObjectId);
} }
} }
float num4 = Vector3.Distance(item.Position, value); float num3 = Vector3.Distance(item2.Position, value);
if (flag5) if (item > num || (item == num && num3 < num2))
{ {
_logger.LogDebug("Target candidate: {Name} ({Id:X8}) BaseId={BaseId} Priority={Priority} ({Reason}) Distance={Distance:N1}", item.Name, item.GameObjectId, item.BaseId, num3, text, num4); gameObject = item2;
} num = item;
if (num3 > num || (num3 == num && num4 < num2)) num2 = num3;
{
gameObject = item;
num = num3;
num2 = num4;
} }
} }
ulong? num5 = gameObject?.GameObjectId; ulong? num4 = gameObject?.GameObjectId;
if (num5 != _lastTargetId) if (num4 != _lastTargetId)
{ {
if (gameObject != null) if (gameObject != null)
{ {
@ -408,7 +408,7 @@ internal sealed class CombatController : IDisposable
{ {
_logger.LogDebug("No valid target found"); _logger.LogDebug("No valid target found");
} }
_lastTargetId = num5; _lastTargetId = num4;
} }
if (gameObject != null && num <= 15 && !_condition[ConditionFlag.InCombat]) if (gameObject != null && num <= 15 && !_condition[ConditionFlag.InCombat])
{ {

View file

@ -416,7 +416,7 @@ internal sealed class MovementController : IDisposable
} }
else if ((vector4 - Destination.Position).Length() < Destination.StopDistance) else if ((vector4 - Destination.Position).Length() < Destination.StopDistance)
{ {
if (vector4.Y - Destination.Position.Y <= Destination.VerticalStopDistance) if (Math.Abs(vector4.Y - Destination.Position.Y) <= Destination.VerticalStopDistance)
{ {
Stop(); Stop();
} }

View file

@ -1538,6 +1538,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
{ {
return false; return false;
} }
if (_startedQuest != null && QuestFunctions.IsStartingCityOpeningQuest(_startedQuest.Quest.Id))
{
return false;
}
ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted() ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted()
where x.IsAvailable where x.IsAvailable
select x.QuestId).FirstOrDefault(); select x.QuestId).FirstOrDefault();

View file

@ -103,15 +103,10 @@ internal sealed class QuestPriorityResolver
QuestResolution valueOrDefault2 = questResolution.GetValueOrDefault(); QuestResolution valueOrDefault2 = questResolution.GetValueOrDefault();
return ApplyQuestRedirects(valueOrDefault2); return ApplyQuestRedirects(valueOrDefault2);
} }
uint territoryType = _clientState.TerritoryType; QuestId activeStartingCityOpeningQuest = _questFunctions.GetActiveStartingCityOpeningQuest();
bool flag2 = territoryType - 181 <= 2; if ((object)activeStartingCityOpeningQuest != null && (_questFunctions.IsQuestAccepted(activeStartingCityOpeningQuest) || (flag && _configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Manual)))
if (flag2 && flag && _configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Manual)
{ {
ElementId currentQuest = resolvedMsqQuest.CurrentQuest; return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, activeStartingCityOpeningQuest, _questFunctions.GetStartingCityOpeningSequence(activeStartingCityOpeningQuest), resolvedMsqQuest.State, $"Starting city opening MSQ {activeStartingCityOpeningQuest}");
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)

File diff suppressed because it is too large Load diff

View file

@ -391,7 +391,7 @@ internal sealed class BossModIpc : IDisposable
AddTransientStrategy(preset, "BossMod.Autorotation.MiscAI.StayCloseToTarget", "Range", isRanged ? "15" : "3"); AddTransientStrategy(preset, "BossMod.Autorotation.MiscAI.StayCloseToTarget", "Range", isRanged ? "15" : "3");
} }
public bool EnableAi(EPreset preset) public bool EnableAi(EPreset preset, bool activateAi = true)
{ {
if (!CreateShellPreset(preset)) if (!CreateShellPreset(preset))
{ {
@ -411,6 +411,10 @@ internal sealed class BossModIpc : IDisposable
_logger.LogWarning("Unable to set BossMod preset {Preset}: {Message}", preset, ipcError.Message); _logger.LogWarning("Unable to set BossMod preset {Preset}: {Message}", preset, ipcError.Message);
return false; return false;
} }
if (!activateAi)
{
return true;
}
string aiCommand = GetAiCommand(); string aiCommand = GetAiCommand();
if (aiCommand != null) if (aiCommand != null)
{ {
@ -424,7 +428,7 @@ internal sealed class BossModIpc : IDisposable
public void EnableQuestBattleAi() public void EnableQuestBattleAi()
{ {
EnableAi(EPreset.Active); EnableAi(EPreset.Active, _configuration.SinglePlayerDuties.EnableBossModAi);
ConfigureSoloDutyZone(); ConfigureSoloDutyZone();
} }

View file

@ -23,6 +23,22 @@ namespace Questionable.Functions;
internal sealed class QuestFunctions internal sealed class QuestFunctions
{ {
private static readonly Dictionary<byte, QuestId> ComingToStartingCityQuests = new Dictionary<byte, QuestId>
{
{
1,
new QuestId(107)
},
{
2,
new QuestId(39)
},
{
3,
new QuestId(594)
}
};
private static readonly Dictionary<ushort, EClassJob> CloseToHomeQuests = new Dictionary<ushort, EClassJob> private static readonly Dictionary<ushort, EClassJob> CloseToHomeQuests = new Dictionary<ushort, EClassJob>
{ {
{ {
@ -95,6 +111,56 @@ internal sealed class QuestFunctions
private static readonly HashSet<ushort> RemovedQuestIds = new HashSet<ushort> { 487, 1428, 1429 }; private static readonly HashSet<ushort> RemovedQuestIds = new HashSet<ushort> { 487, 1428, 1429 };
public static bool IsStartingCityOpeningQuest(ElementId elementId)
{
if (elementId is QuestId questId)
{
if (!ComingToStartingCityQuests.ContainsValue(questId))
{
return CloseToHomeQuests.ContainsKey(questId.Value);
}
return true;
}
return false;
}
public QuestId? GetActiveStartingCityOpeningQuest()
{
QuestId questId = ComingToStartingCityQuests.Values.Concat(CloseToHomeQuests.Keys.Select((ushort x) => new QuestId(x))).Where(IsQuestAccepted).Cast<QuestId>()
.FirstOrDefault();
if (questId != null)
{
return questId;
}
EClassJob startingClass = ((EClassJob?)_objectTable.LocalPlayer?.ClassJob.RowId).GetValueOrDefault();
ushort num = (from x in CloseToHomeQuests
where x.Value == startingClass
select x.Key).FirstOrDefault();
if (num != 0)
{
QuestId questId2 = new QuestId(num);
if (IsReadyToAcceptQuest(questId2, ignoreLevel: true))
{
return questId2;
}
}
if (ComingToStartingCityQuests.TryGetValue(PlayerStateHelper.GetStartTown(), out QuestId value) && IsReadyToAcceptQuest(value, ignoreLevel: true))
{
return value;
}
return null;
}
public byte GetStartingCityOpeningSequence(QuestId questId)
{
byte questSequence = QuestManager.GetQuestSequence(questId.Value);
if (ComingToStartingCityQuests.ContainsValue(questId) && IsQuestAccepted(questId) && questSequence == 0)
{
return byte.MaxValue;
}
return questSequence;
}
public QuestFunctions(QuestRegistry questRegistry, QuestData questData, JournalData journalData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, Configuration configuration, IDataManager dataManager, IObjectTable objectTable, IGameGui gameGui, ILogger<QuestFunctions> logger) public QuestFunctions(QuestRegistry questRegistry, QuestData questData, JournalData journalData, AetheryteFunctions aetheryteFunctions, AlliedSocietyQuestFunctions alliedSocietyQuestFunctions, AlliedSocietyData alliedSocietyData, Configuration configuration, IDataManager dataManager, IObjectTable objectTable, IGameGui gameGui, ILogger<QuestFunctions> logger)
{ {
_questRegistry = questRegistry; _questRegistry = questRegistry;

View file

@ -5,21 +5,28 @@ using Microsoft.Extensions.Logging;
using Questionable.Controller.CustomDelivery; 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.Interactions;
using Questionable.Controller.Steps.Movement; using Questionable.Controller.Steps.Movement;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Model.Questing;
using SmartNav; using SmartNav;
namespace Questionable.Navigation; namespace Questionable.Navigation;
internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartNavTaskMapper taskMapper, Configuration configuration, ILogger<SmartNavReRouteService> logger) internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartNavTaskMapper taskMapper, Configuration configuration, ILogger<SmartNavReRouteService> logger)
{ {
public void SetDestination(uint territoryId, Vector3 position) private ETravelMode _travelMode;
public void SetDestination(uint territoryId, Vector3 position, ETravelMode travelMode = ETravelMode.Auto)
{ {
reRoutePolicy.SetDestination(territoryId, position); _travelMode = travelMode;
var (allowMount, allowFlight) = GetCapabilities(travelMode);
reRoutePolicy.SetDestination(territoryId, position, allowMount, allowFlight);
} }
public void ClearDestination() public void ClearDestination()
{ {
_travelMode = ETravelMode.Auto;
reRoutePolicy.ClearDestination(); reRoutePolicy.ClearDestination();
} }
@ -51,7 +58,7 @@ internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartN
if (reRouteDecision is ReRouteDecision.Replan replan) if (reRouteDecision is ReRouteDecision.Replan replan)
{ {
taskQueue.Reset(); taskQueue.Reset();
foreach (ITask item2 in taskMapper.MapInstructions(replan.Instructions)) foreach (ITask item2 in taskMapper.MapInstructions(replan.Instructions, null, _travelMode))
{ {
taskQueue.Enqueue(item2); taskQueue.Enqueue(item2);
} }
@ -79,10 +86,33 @@ internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartN
private static bool IsMovementTask(ITask task) private static bool IsMovementTask(ITask task)
{ {
if (task is MoveTask || task is LandTask || task is Mount.MountTask || task is AetheryteTeleport.Task || task is AethernetRide.Task || task is WaitNavmesh.Task || task is WaitCondition.Task || task is WarpInteract.Task || task is TaxiInteract.Task || task is SubRegionTransport.Task || task is TicketTeleport.Task) if (!(task is MoveTask))
{ {
return true; if (task is Dive.Task task2)
{
if (task2.SmartNavRouted)
{
goto IL_006c;
}
}
else if (task is LandTask || task is Mount.MountTask || task is AetheryteTeleport.Task || task is AethernetRide.Task || task is WaitNavmesh.Task || task is WaitCondition.Task || task is WarpInteract.Task || task is TaxiInteract.Task || task is SubRegionTransport.Task || task is TicketTeleport.Task)
{
goto IL_006c;
} }
return false; return false;
} }
goto IL_006c;
IL_006c:
return true;
}
internal static (bool AllowMount, bool AllowFlight) GetCapabilities(ETravelMode travelMode)
{
return travelMode switch
{
ETravelMode.OnFoot => (AllowMount: false, AllowFlight: false),
ETravelMode.GroundMount => (AllowMount: true, AllowFlight: false),
_ => (AllowMount: true, AllowFlight: true),
};
}
} }

View file

@ -53,7 +53,27 @@ internal sealed class SmartNavRouteEnqueuer(NavRouter navRouter, PlayerNavStateB
{ {
return false; return false;
} }
List<NavInstruction> list = instructionBuilder.Build(routeResult, territoryId).TakeWhile((NavInstruction x) => !(x is NavInstruction.Move move) || !move.IsFinal).ToList(); List<NavInstruction> list = instructionBuilder.Build(routeResult, territoryId).TakeWhile(delegate(NavInstruction x)
{
if (x is NavInstruction.Move move)
{
if (move.IsFinal)
{
goto IL_0026;
}
}
else if (x is NavInstruction.Swim { IsFinal: not false })
{
goto IL_0026;
}
bool flag = false;
goto IL_002c;
IL_0026:
flag = true;
goto IL_002c;
IL_002c:
return !flag;
}).ToList();
if (list.Count == 0) if (list.Count == 0)
{ {
return false; return false;
@ -120,6 +140,18 @@ internal sealed class SmartNavRouteEnqueuer(NavRouter navRouter, PlayerNavStateB
{ {
return false; return false;
} }
if (instruction is NavInstruction.Swim)
{
return false;
}
if (instruction is NavInstruction.DiveEntry)
{
return false;
}
if (instruction is NavInstruction.Surface)
{
return false;
}
if (instruction is NavInstruction.AethernetHop) if (instruction is NavInstruction.AethernetHop)
{ {
return false; return false;

View file

@ -5,6 +5,7 @@ using System.Numerics;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Questionable.Controller.Steps; using Questionable.Controller.Steps;
using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Common;
using Questionable.Controller.Steps.Interactions;
using Questionable.Controller.Steps.Movement; using Questionable.Controller.Steps.Movement;
using Questionable.Controller.Steps.Shared; using Questionable.Controller.Steps.Shared;
using Questionable.Data; using Questionable.Data;
@ -15,16 +16,25 @@ namespace Questionable.Navigation;
internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData territoryData) internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData territoryData)
{ {
public IEnumerable<ITask> MapInstructions(IReadOnlyList<NavInstruction> instructions, QuestStep? step = null) private const float SurfaceExitRise = 3f;
public IEnumerable<ITask> MapInstructions(IReadOnlyList<NavInstruction> instructions, QuestStep? step = null, ETravelMode travelMode = ETravelMode.Auto)
{ {
for (int i = 0; i < instructions.Count; i++) for (int i = 0; i < instructions.Count; i++)
{ {
foreach (ITask item in Map(instructions[i], NextTravelInstruction(instructions, i), step)) foreach (ITask item in Map(instructions[i], NextTravelInstruction(instructions, i), step))
{
if (item is MoveTask task && !(instructions[i] is NavInstruction.Swim))
{
yield return ApplyTravelMode(task, travelMode);
}
else
{ {
yield return item; yield return item;
} }
} }
} }
}
private static NavInstruction? NextTravelInstruction(IReadOnlyList<NavInstruction> instructions, int i) private static NavInstruction? NextTravelInstruction(IReadOnlyList<NavInstruction> instructions, int i)
{ {
@ -47,6 +57,12 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
if (!(instruction is NavInstruction.AethernetHop aethernetHop)) if (!(instruction is NavInstruction.AethernetHop aethernetHop))
{ {
if (!(instruction is NavInstruction.Move move)) if (!(instruction is NavInstruction.Move move))
{
if (!(instruction is NavInstruction.Swim swim))
{
if (!(instruction is NavInstruction.DiveEntry dive))
{
if (!(instruction is NavInstruction.Surface surface))
{ {
if (!(instruction is NavInstruction.Land)) if (!(instruction is NavInstruction.Land))
{ {
@ -97,17 +113,76 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
yield return new LandTask(); yield return new LandTask();
} }
} }
else
{
ushort territory = surface.Territory;
Vector3 position = surface.Position;
position.Y = surface.Position.Y + 3f;
Vector3 destination = position;
bool? mount = true;
string targetNodeId = surface.TargetNodeId;
string approachNodeId = surface.ApproachNodeId;
yield return new MoveTask(territory, destination, mount, MountRequired: false, DismountRequired: false, null, null, DisableNavmesh: true, null, Fly: true, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.None, SmartNavRouted: true, AllowZoneTransition: false, targetNodeId, approachNodeId);
}
}
else
{
yield return new Dive.Task
{
SmartNavRouted = true
};
ushort territory2 = dive.Territory;
Vector3 position = dive.Position;
position.Y = dive.Position.Y - 25f;
Vector3 destination2 = position;
string approachNodeId = dive.TargetNodeId;
string targetNodeId = dive.ApproachNodeId;
yield return new MoveTask(territory2, destination2, null, MountRequired: false, DismountRequired: false, null, null, DisableNavmesh: true, null, Fly: true, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.None, SmartNavRouted: true, AllowZoneTransition: false, approachNodeId, targetNodeId);
}
}
else if (swim.IsFinal && step != null)
{
Vector3 destination3 = step.Position ?? swim.Position;
yield return new MoveTask(step, destination3)with
{
Fly = true,
Land = false,
SmartNavRouted = true,
RouteTargetNodeId = swim.TargetNodeId,
RouteApproachNodeId = swim.ApproachNodeId
};
}
else
{
NavInstruction.Swim swim2 = swim;
ushort territory3 = swim2.Territory;
Vector3 position2 = swim2.Position;
float? stopDistance = swim2.StopDistance;
string targetNodeId = swim2.TargetNodeId;
string approachNodeId = swim2.ApproachNodeId;
yield return new MoveTask(territory3, position2, null, MountRequired: false, DismountRequired: false, stopDistance, null, DisableNavmesh: false, null, Fly: true, Land: false, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.None, SmartNavRouted: true, AllowZoneTransition: false, targetNodeId, approachNodeId);
}
}
else if (move.IsFinal && step != null) else if (move.IsFinal && step != null)
{ {
Vector3 destination = step.Position ?? move.Position; Vector3 destination4 = step.Position ?? move.Position;
yield return new MoveTask(step, destination)with MoveTask moveTask = new MoveTask(step, destination4)with
{ {
Fly = (move.Fly || step.Fly == true), Fly = (move.Fly || step.Fly == true)
Land = (move.Fly || step.Land == true),
SmartNavRouted = true,
RouteTargetNodeId = move.TargetNodeId,
RouteApproachNodeId = move.ApproachNodeId
}; };
MoveTask moveTask2 = moveTask;
bool flag = move.Fly || step.Land == true;
if (!flag)
{
EArrivalMode arrivalMode = step.ArrivalMode;
bool flag2 = (uint)(arrivalMode - 2) <= 1u;
flag = flag2;
}
moveTask2.Land = flag;
moveTask.SmartNavRouted = true;
moveTask.RouteTargetNodeId = move.TargetNodeId;
moveTask.RouteApproachNodeId = move.ApproachNodeId;
yield return moveTask;
if (step != null) if (step != null)
{ {
bool? fly = step.Fly; bool? fly = step.Fly;
@ -116,22 +191,30 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
yield return new LandTask(); yield return new LandTask();
} }
} }
if (step.ArrivalMode == EArrivalMode.StayMounted && !step.DismountRequired)
{
yield return new Mount.MountTask(step.TerritoryId, Mount.EMountIf.Always);
}
else if (step.ArrivalMode == EArrivalMode.Dismount && !step.MountRequired)
{
yield return new Mount.UnmountTask();
}
} }
else else
{ {
NavInstruction.Move move2 = move; NavInstruction.Move move2 = move;
ushort territory = move2.Territory; ushort territory4 = move2.Territory;
Vector3 position = move2.Position; Vector3 position3 = move2.Position;
bool? mount = move2.Mount; bool? mount2 = move2.Mount;
float? stopDistance = move2.StopDistance; float? stopDistance2 = move2.StopDistance;
uint? dataId = move2.DataId; uint? dataId = move2.DataId;
bool disableNavmesh = move2.DisableNavmesh; bool disableNavmesh = move2.DisableNavmesh;
bool fly2 = move2.Fly; bool flag = move2.Fly;
bool landAtTarget = move2.LandAtTarget; bool flag2 = move2.LandAtTarget;
bool allowZoneTransition = move2.AllowZoneTransition; bool allowZoneTransition = move2.AllowZoneTransition;
string targetNodeId = move2.TargetNodeId; string approachNodeId = move2.TargetNodeId;
string approachNodeId = move2.ApproachNodeId; string targetNodeId = move2.ApproachNodeId;
yield return new MoveTask(territory, position, mount, MountRequired: false, DismountRequired: false, stopDistance, dataId, disableNavmesh, null, fly2, landAtTarget, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.None, SmartNavRouted: true, allowZoneTransition, targetNodeId, approachNodeId); yield return new MoveTask(territory4, position3, mount2, MountRequired: false, DismountRequired: false, stopDistance2, dataId, disableNavmesh, null, flag, flag2, IgnoreDistanceToObject: false, RestartNavigation: true, EInteractionType.None, SmartNavRouted: true, allowZoneTransition, approachNodeId, targetNodeId);
} }
} }
else else
@ -147,4 +230,50 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
yield return new AetheryteTeleport.MoveAwayFromAetheryte(teleport.Target); yield return new AetheryteTeleport.MoveAwayFromAetheryte(teleport.Target);
} }
} }
private static MoveTask ApplyTravelMode(MoveTask task, ETravelMode travelMode)
{
if (travelMode == ETravelMode.Auto)
{
return task;
}
if (task.MountRequired)
{
return task with
{
Mount = true,
Fly = (travelMode == ETravelMode.Flying && task.Fly),
Land = (travelMode == ETravelMode.Flying && task.Land)
};
}
if (task.DismountRequired)
{
return task with
{
Mount = false,
Fly = false,
Land = false
};
}
return travelMode switch
{
ETravelMode.OnFoot => task with
{
Mount = false,
Fly = false,
Land = false
},
ETravelMode.GroundMount => task with
{
Mount = true,
Fly = false,
Land = false
},
ETravelMode.Flying => task with
{
Mount = true
},
_ => task,
};
}
} }

View file

@ -317,6 +317,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
} }
using (ImRaii.Disabled(!value)) using (ImRaii.Disabled(!value))
{ {
ImGui.Spacing();
bool value2 = base.Configuration.SinglePlayerDuties.EnableBossModAi;
if (UiThemeUtils.WrappedCheckbox("Activate BossMod AI during quest battles", ref value2, "When disabled, Questionable still loads its BossMod preset for combat, but does not issue the /vbmai on or /bmrai on command for autonomous movement and targeting."))
{
base.Configuration.SinglePlayerDuties.EnableBossModAi = value2;
Save();
}
ImGui.Spacing(); ImGui.Spacing();
int currentItem = (int)base.Configuration.SinglePlayerDuties.RetryDifficulty; int currentItem = (int)base.Configuration.SinglePlayerDuties.RetryDifficulty;
ImGui.SetNextItemWidth(MathF.Min(ImGui.GetContentRegionAvail().X * 0.4f, 260f)); ImGui.SetNextItemWidth(MathF.Min(ImGui.GetContentRegionAvail().X * 0.4f, 260f));

View file

@ -338,30 +338,27 @@ internal sealed class QuestSequenceComponent
ImGui.Spacing(); ImGui.Spacing();
try try
{ {
Lumina.Excel.Sheets.Quest? quest = _dataManager.GetExcelSheet<Lumina.Excel.Sheets.Quest>()?.GetRowOrDefault((uint)(questId.Value + 65536)); if (!(_dataManager.GetExcelSheet<Lumina.Excel.Sheets.Quest>()?.GetRowOrDefault((uint)(questId.Value + 65536))).HasValue)
if (!quest.HasValue)
{ {
DrawErrorState("Quest Not Found in Game Data", "Unable to load quest information from Lumina."); DrawErrorState("Quest Not Found in Game Data", "Unable to load quest information from Lumina.");
return; return;
} }
List<int> list = (from result in quest.Value.TodoParams.Where((Lumina.Excel.Sheets.Quest.TodoParamsStruct todoParamsStruct) => todoParamsStruct.ToDoCompleteSeq > 0 && todoParamsStruct.ToDoCompleteSeq < byte.MaxValue).Select((Func<Lumina.Excel.Sheets.Quest.TodoParamsStruct, int>)((Lumina.Excel.Sheets.Quest.TodoParamsStruct todoParamsStruct) => todoParamsStruct.ToDoCompleteSeq)).Distinct() List<int> completionSequences = GameQuestSequences.GetCompletionSequences(_dataManager, questId);
orderby result if (completionSequences.Count == 0)
select result).ToList();
if (list.Count == 0)
{ {
DrawErrorState("No Sequence Data Available", "This quest may not have traditional sequences."); DrawErrorState("No Sequence Data Available", "This quest may not have traditional sequences.");
return; return;
} }
int num8 = list.Max(); int num8 = completionSequences.Where((int num13) => num13 != 255).DefaultIfEmpty(0).Max();
int num9 = num8 + 1; int num9 = num8 + 1;
Questionable.Model.Quest quest2; Questionable.Model.Quest quest;
bool flag = _questRegistry.TryGetQuest(questElementId, out quest2); bool flag = _questRegistry.TryGetQuest(questElementId, out quest);
HashSet<int> hashSet = new HashSet<int>(); HashSet<int> hashSet = new HashSet<int>();
Dictionary<int, (int, string)> dictionary = new Dictionary<int, (int, string)>(); Dictionary<int, (int, string)> dictionary = new Dictionary<int, (int, string)>();
bool flag2 = false; bool flag2 = false;
if (flag && quest2 != null && quest2.Root.QuestSequence.Count > 0) if (flag && quest != null && quest.Root.QuestSequence.Count > 0)
{ {
foreach (QuestSequence item in quest2.Root.QuestSequence) foreach (QuestSequence item in quest.Root.QuestSequence)
{ {
if (item.Sequence == byte.MaxValue) if (item.Sequence == byte.MaxValue)
{ {

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -14,7 +13,6 @@ using Questionable.Model.Questing;
namespace Questionable; namespace Questionable;
[Obfuscation(Feature = "-rename", Exclude = false, ApplyToMembers = true)]
internal sealed class Configuration : IPluginConfiguration internal sealed class Configuration : IPluginConfiguration
{ {
internal sealed class GeneralConfiguration internal sealed class GeneralConfiguration
@ -160,6 +158,8 @@ internal sealed class Configuration : IPluginConfiguration
{ {
public bool RunSoloInstancesWithBossMod { get; set; } public bool RunSoloInstancesWithBossMod { get; set; }
public bool EnableBossModAi { get; set; } = true;
public ERetryDifficulty RetryDifficulty { get; set; } public ERetryDifficulty RetryDifficulty { get; set; }
public int MaxRetries { get; set; } public int MaxRetries { get; set; }

View file

@ -2,7 +2,6 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -53,7 +52,6 @@ using SmartNav.Data;
namespace Questionable; namespace Questionable;
[Obfuscation(Feature = "-ctrl flow", Exclude = false, ApplyToMembers = true)]
public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
{ {
private ServiceProvider? _serviceProvider; private ServiceProvider? _serviceProvider;
@ -326,6 +324,8 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
serviceCollection.AddSingleton<TeleportTicketService>(); serviceCollection.AddSingleton<TeleportTicketService>();
serviceCollection.AddSingleton((IServiceProvider sp) => new LgbZoneBoundarySource(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, IZoneBoundarySource>)((IServiceProvider sp) => sp.GetRequiredService<LgbZoneBoundarySource>()));
serviceCollection.AddSingleton<WaterMapService>();
serviceCollection.AddSingleton<WaterRoutePlanner>();
((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>();

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]
{ {
192, 237, 68, 154, 123, 161, 164, 81, 179, 101, 14, 238, 168, 186, 100, 48, 138, 129, 114, 53,
208, 145, 185, 118, 184, 66, 42, 173, 202, 110, 75, 217, 99, 224, 198, 248, 103, 129, 59, 33,
201, 113, 73, 242, 250, 58, 135, 189, 143, 92, 149, 44, 226, 149, 187, 253, 141, 40, 151, 199,
151, 77 127, 185
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
239, 73, 204, 3, 150, 201, 87, 43, 137, 223, 87, 34, 127, 49, 238, 14, 221, 143, 224, 157,
14, 253, 125, 35, 109, 186, 32, 12, 130, 132, 207, 147, 247, 122, 30, 185, 79, 187, 117, 57,
201, 118, 255, 168, 253, 104, 107, 98, 213, 28, 242, 252, 100, 251, 176, 253, 30, 79, 135, 124,
83, 69 73, 92
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -20,6 +20,7 @@
<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.npc-position-cache.bin" />
<None Remove="SmartNav.Data.zone-boundary-cache.bin" /> <None Remove="SmartNav.Data.zone-boundary-cache.bin" />
<None Remove="SmartNav.Data.water-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" />
@ -27,6 +28,7 @@
<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.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" /> <EmbeddedResource Include="SmartNav.Data.zone-boundary-cache.bin" LogicalName="SmartNav.Data.zone-boundary-cache.bin" />
<EmbeddedResource Include="SmartNav.Data.water-cache.bin" LogicalName="SmartNav.Data.water-cache.bin" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="SmartNav.Model"> <Reference Include="SmartNav.Model">

Binary file not shown.

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.IO;
using SmartNav.Model.Navigation;
namespace SmartNav.Data;
public static class AssemblyWaterMapLoader
{
public static (string? GameVersion, IReadOnlyDictionary<ushort, TerritoryWaterMap> Maps) GetWaterMaps(string? overrideDirectory = null)
{
using Stream stream = SmartNavResources.TryOpen("water-cache.bin", overrideDirectory);
if (stream == null)
{
return (GameVersion: null, Maps: new Dictionary<ushort, TerritoryWaterMap>());
}
(string GameVersion, List<TerritoryWaterMap> Maps) tuple = WaterCacheFormat.Read(stream);
string item = tuple.GameVersion;
List<TerritoryWaterMap> item2 = tuple.Maps;
Dictionary<ushort, TerritoryWaterMap> dictionary = new Dictionary<ushort, TerritoryWaterMap>(item2.Count);
foreach (TerritoryWaterMap item3 in item2)
{
if (!dictionary.TryAdd(item3.TerritoryId, item3))
{
throw new InvalidDataException($"water cache lists territory {item3.TerritoryId} twice");
}
}
return (GameVersion: item, Maps: dictionary);
}
}

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]
{ {
13, 38, 21, 255, 71, 40, 19, 213, 79, 201, 71, 215, 20, 29, 12, 252, 253, 194, 59, 69,
20, 24, 146, 227, 159, 156, 142, 23, 138, 144, 18, 55, 34, 247, 239, 216, 228, 201, 157, 67,
119, 132, 179, 149, 190, 134, 23, 209, 204, 192, 207, 24, 144, 24, 62, 38, 55, 197, 249, 132,
234, 63 62, 40
}; };
private static readonly byte[] B = new byte[32] private static readonly byte[] B = new byte[32]
{ {
252, 191, 81, 239, 79, 239, 196, 247, 66, 179, 63, 81, 210, 107, 86, 21, 96, 76, 152, 72,
73, 171, 175, 76, 172, 241, 89, 23, 110, 4, 178, 235, 191, 217, 62, 127, 34, 130, 14, 224,
105, 193, 2, 137, 162, 77, 122, 136, 209, 181, 171, 239, 111, 52, 2, 7, 1, 25, 82, 57,
229, 197 94, 195
}; };
internal static byte[] Key() internal static byte[] Key()

View file

@ -45,6 +45,24 @@ internal static class SmartNavResources
return JsonSerializer.Deserialize<T>(readOnlySpan); return JsonSerializer.Deserialize<T>(readOnlySpan);
} }
public static Stream? TryOpen(string fileName, string? overrideDirectory)
{
if (overrideDirectory != null)
{
string path = Path.Combine(overrideDirectory, fileName);
if (File.Exists(path))
{
return File.OpenRead(path);
}
}
byte[] array = Unseal("SmartNav.Data." + fileName);
if (array != null)
{
return new MemoryStream(array, writable: false);
}
return null;
}
public static Stream OpenSealed(string fileName) public static Stream OpenSealed(string fileName)
{ {
return new MemoryStream(Unseal("SmartNav.Data." + fileName) ?? throw new InvalidOperationException("embedded resource '" + fileName + "' is missing"), writable: false); return new MemoryStream(Unseal("SmartNav.Data." + fileName) ?? throw new InvalidOperationException("embedded resource '" + fileName + "' is missing"), writable: false);

View file

@ -0,0 +1,188 @@
using System;
using System.Numerics;
namespace SmartNav.Model.Navigation;
public sealed class TerritoryWaterMap
{
public const int GridSize = 256;
public const int CellCount = 65536;
public const float CellSize = 8f;
public const float WorldMin = -1024f;
public const float WorldMax = 1024f;
private const float SurfaceYScale = 32f;
private const float SurfaceYBias = 1024f;
private readonly byte[] _classes;
private readonly ushort[] _surfaceY;
public ushort TerritoryId { get; }
public ReadOnlySpan<byte> Classes => _classes;
public ReadOnlySpan<ushort> SurfaceYRaw => _surfaceY;
public bool HasWater => ((ReadOnlySpan<byte>)_classes.AsSpan()).ContainsAnyExcept((byte)0);
public TerritoryWaterMap(ushort territoryId)
: this(territoryId, new byte[65536], new ushort[65536])
{
}
public TerritoryWaterMap(ushort territoryId, byte[] classes, ushort[] surfaceY)
{
ArgumentNullException.ThrowIfNull(classes, "classes");
ArgumentNullException.ThrowIfNull(surfaceY, "surfaceY");
if (classes.Length != 65536)
{
throw new ArgumentException($"expected {65536} class entries, got {classes.Length}", "classes");
}
if (surfaceY.Length != 65536)
{
throw new ArgumentException($"expected {65536} surface Y entries, got {surfaceY.Length}", "surfaceY");
}
TerritoryId = territoryId;
_classes = classes;
_surfaceY = surfaceY;
}
public static ushort EncodeSurfaceY(float y)
{
if (float.IsNaN(y))
{
throw new ArgumentException("surface Y is NaN", "y");
}
float num = MathF.Round((y + 1024f) * 32f);
if (num <= 0f)
{
return 0;
}
if (num >= 65535f)
{
return ushort.MaxValue;
}
return (ushort)num;
}
public static float DecodeSurfaceY(ushort raw)
{
return (float)(int)raw / 32f - 1024f;
}
public static float CellCenterCoordinate(int colOrRow)
{
return -1024f + ((float)colOrRow + 0.5f) * 8f;
}
public static bool TryGetCell(float x, float z, out int col, out int row)
{
col = (int)MathF.Floor((x - -1024f) / 8f);
row = (int)MathF.Floor((z - -1024f) / 8f);
if (col < 0 || col >= 256 || row < 0 || row >= 256)
{
col = -1;
row = -1;
return false;
}
return true;
}
public void SetCell(int col, int row, WaterCellClass cellClass, float surfaceY)
{
int num = IndexOf(col, row);
_classes[num] = (byte)cellClass;
_surfaceY[num] = EncodeSurfaceY(surfaceY);
}
public WaterCellClass GetCell(int col, int row)
{
return (WaterCellClass)_classes[IndexOf(col, row)];
}
public WaterCellClass ClassifyAt(float x, float z)
{
if (!TryGetCell(x, z, out var col, out var row))
{
return WaterCellClass.None;
}
return (WaterCellClass)_classes[row * 256 + col];
}
public bool TryGetSurfaceY(float x, float z, out float surfaceY)
{
surfaceY = 0f;
if (!TryGetCell(x, z, out var col, out var row))
{
return false;
}
int num = row * 256 + col;
if (_classes[num] == 0)
{
return false;
}
surfaceY = DecodeSurfaceY(_surfaceY[num]);
return true;
}
public bool TryFindNearestDivable(float x, float z, float maxRadiusYalms, out Vector3 entryCellCenter)
{
entryCellCenter = default(Vector3);
if (float.IsNaN(maxRadiusYalms) || maxRadiusYalms < 0f || !TryGetCell(x, z, out var col, out var row))
{
return false;
}
float num = MathF.Ceiling(maxRadiusYalms / 8f) + 1f;
int num2 = ((num >= 256f) ? 256 : ((int)num));
float num3 = float.MaxValue;
int num4 = -1;
int num5 = -1;
for (int i = 0; i <= num2 && (num4 < 0 || !(((float)i - 0.5f) * 8f > num3)); i++)
{
int num6 = col - i;
int num7 = col + i;
int num8 = row - i;
int num9 = row + i;
for (int j = Math.Max(num8, 0); j <= Math.Min(num9, 255); j++)
{
int num10 = ((j == num8 || j == num9) ? 1 : Math.Max(num7 - num6, 1));
for (int k = num6; k <= num7; k += num10)
{
if (k >= 0 && k < 256 && _classes[j * 256 + k] == 2)
{
float num11 = CellCenterCoordinate(k) - x;
float num12 = CellCenterCoordinate(j) - z;
float num13 = MathF.Sqrt(num11 * num11 + num12 * num12);
if (!(num13 > maxRadiusYalms) && !(num13 >= num3))
{
num3 = num13;
num4 = k;
num5 = j;
}
}
}
}
}
if (num4 < 0)
{
return false;
}
entryCellCenter = new Vector3(CellCenterCoordinate(num4), DecodeSurfaceY(_surfaceY[num5 * 256 + num4]), CellCenterCoordinate(num5));
return true;
}
private static int IndexOf(int col, int row)
{
ArgumentOutOfRangeException.ThrowIfNegative(col, "col");
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(col, 256, "col");
ArgumentOutOfRangeException.ThrowIfNegative(row, "row");
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(row, 256, "row");
return row * 256 + col;
}
}

View file

@ -0,0 +1,122 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace SmartNav.Model.Navigation;
public static class WaterCacheFormat
{
public const uint FileMagic = 1129469015u;
public const int FileFormatVersion = 1;
public const string FileName = "water-cache.bin";
private const int MaxTerritoryCount = 65536;
private const int ClassBytes = 65536;
private const int SurfaceBytes = 131072;
private const int PayloadBytes = 196608;
public static void Write(Stream stream, string gameVersion, IReadOnlyList<TerritoryWaterMap> maps)
{
ArgumentNullException.ThrowIfNull(stream, "stream");
ArgumentNullException.ThrowIfNull(gameVersion, "gameVersion");
ArgumentNullException.ThrowIfNull(maps, "maps");
List<TerritoryWaterMap> list = new List<TerritoryWaterMap>(maps.Count);
foreach (TerritoryWaterMap map in maps)
{
if (map.HasWater)
{
list.Add(map);
}
}
list.Sort((TerritoryWaterMap left, TerritoryWaterMap right) => left.TerritoryId.CompareTo(right.TerritoryId));
using BinaryWriter binaryWriter = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
binaryWriter.Write(1129469015u);
binaryWriter.Write(1);
binaryWriter.Write(gameVersion);
binaryWriter.Write(list.Count);
foreach (TerritoryWaterMap item in list)
{
byte[] array = new byte[196608];
item.Classes.CopyTo(array.AsSpan(0, 65536));
ReadOnlySpan<ushort> surfaceYRaw = item.SurfaceYRaw;
for (int num = 0; num < surfaceYRaw.Length; num++)
{
BinaryPrimitives.WriteUInt16LittleEndian(array.AsSpan(65536 + num * 2), surfaceYRaw[num]);
}
using MemoryStream memoryStream = new MemoryStream();
using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true))
{
deflateStream.Write(array);
}
binaryWriter.Write(item.TerritoryId);
binaryWriter.Write((int)memoryStream.Length);
binaryWriter.Write(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}
public static (string GameVersion, List<TerritoryWaterMap> Maps) Read(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream, "stream");
using BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
uint num = binaryReader.ReadUInt32();
if (num != 1129469015)
{
throw new InvalidDataException($"water cache has magic 0x{num:X8}, expected 0x{1129469015:X8}");
}
int num2 = binaryReader.ReadInt32();
if (num2 != 1)
{
throw new InvalidDataException($"water cache has format version {num2}, expected {1}");
}
string item = binaryReader.ReadString();
int num3 = binaryReader.ReadInt32();
if (num3 < 0 || num3 > 65536)
{
throw new InvalidDataException($"water cache declares {num3} territories");
}
List<TerritoryWaterMap> list = new List<TerritoryWaterMap>(num3);
for (int i = 0; i < num3; i++)
{
ushort num4 = binaryReader.ReadUInt16();
int num5 = binaryReader.ReadInt32();
if (num5 < 0)
{
throw new InvalidDataException($"territory {num4} declares {num5} compressed bytes");
}
byte[] array = binaryReader.ReadBytes(num5);
if (array.Length != num5)
{
throw new InvalidDataException($"territory {num4} is truncated: {array.Length} of {num5} bytes");
}
byte[] array2 = new byte[196608];
using (DeflateStream deflateStream = new DeflateStream(new MemoryStream(array, writable: false), CompressionMode.Decompress))
{
deflateStream.ReadExactly(array2);
}
byte[] subArray = array2[..65536];
byte[] array3 = subArray;
foreach (byte b in array3)
{
if (b > 2)
{
throw new InvalidDataException($"territory {num4} has cell class {b}, expected 0..{2}");
}
}
ushort[] array4 = new ushort[65536];
for (int k = 0; k < array4.Length; k++)
{
array4[k] = BinaryPrimitives.ReadUInt16LittleEndian(array2.AsSpan(65536 + k * 2));
}
list.Add(new TerritoryWaterMap(num4, subArray, array4));
}
return (GameVersion: item, Maps: list);
}
}

View file

@ -0,0 +1,8 @@
namespace SmartNav.Model.Navigation;
public enum WaterCellClass : byte
{
None,
SwimOnly,
Divable
}

View file

@ -56,9 +56,25 @@ public static class MovementData
public const float DefaultMountGroundDistance = 20f; public const float DefaultMountGroundDistance = 20f;
public const float MinMountTimeSavedSeconds = 2f;
public const float DefaultMountFlyDistance = 15f; public const float DefaultMountFlyDistance = 15f;
public const float DefaultStopDistance = 3f; public const float DefaultStopDistance = 3f;
public static float MountGroundDistanceFor(int mountSpeedLevel)
{
return 2f / (1f / 6f - 1f / Speeds.GetMountGroundSpeed(mountSpeedLevel));
}
}
public static class Water
{
public const float EjectDepth = 20.6f;
public const float EntryDepth = 25f;
public const float SwimDepth = 0.6f;
} }
public static class Timings public static class Timings
@ -74,5 +90,9 @@ public static class MovementData
public const float SprintDurationInCombat = 10f; public const float SprintDurationInCombat = 10f;
public const float SprintCooldown = 60f; public const float SprintCooldown = 60f;
public const float DiveEntryDuration = 4f;
public const float SurfaceExitDuration = 1f;
} }
} }

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.Extensions.Logging;
using SmartNav.Model.Navigation;
namespace SmartNav.Data;
public sealed class WaterMapService
{
private readonly Lazy<IReadOnlyDictionary<ushort, TerritoryWaterMap>> _maps;
public WaterMapService(SmartNavDataOptions dataOptions, ILogger<WaterMapService> logger)
{
_maps = new Lazy<IReadOnlyDictionary<ushort, TerritoryWaterMap>>(delegate
{
try
{
var (text, readOnlyDictionary) = AssemblyWaterMapLoader.GetWaterMaps(dataOptions.DevSourceDirectory?.FullName);
logger.LogInformation("WaterMapService: {Count} territories with water maps loaded (game version {GameVersion})", readOnlyDictionary.Count, text ?? "<none>");
return readOnlyDictionary;
}
catch (Exception exception)
{
logger.LogError(exception, "Water cache could not be loaded - routing proceeds without water data");
return new Dictionary<ushort, TerritoryWaterMap>();
}
}, LazyThreadSafetyMode.ExecutionAndPublication);
}
internal WaterMapService(IReadOnlyDictionary<ushort, TerritoryWaterMap> maps)
{
_maps = new Lazy<IReadOnlyDictionary<ushort, TerritoryWaterMap>>(maps);
}
public bool TryGetMap(uint territoryId, [NotNullWhen(true)] out TerritoryWaterMap? map)
{
if (territoryId > 65535)
{
map = null;
return false;
}
return _maps.Value.TryGetValue((ushort)territoryId, out map);
}
}

View file

@ -4,9 +4,53 @@ using SmartNav.Model;
namespace SmartNav; namespace SmartNav;
public sealed class CostCalculator(ITerritoryInfo territoryInfo) public sealed class CostCalculator(ITerritoryInfo territoryInfo, WaterRoutePlanner waterPlanner)
{ {
public float EstimateWithinZoneTravelTime(Vector3 from, Vector3 to, uint territoryId, PlayerNavState playerState) public float EstimateWithinZoneTravelTime(Vector3 from, Vector3 to, uint territoryId, PlayerNavState playerState)
{
WaterPlan waterPlan = waterPlanner.Plan(territoryId, from, to);
if (waterPlan == null)
{
return EstimateDry(from, to, territoryId, playerState);
}
bool flag = playerState.MountUnlocked && playerState.IsFlyingUnlocked(territoryId);
switch (waterPlan.Kind)
{
case WaterPlanKind.Submerged:
return Vector3.Distance(from, to) / 6f;
case WaterPlanKind.SurfaceOut:
return AscentTime(from, waterPlan.SurfacePoint.Value) + EstimateDry(waterPlan.SurfacePoint.Value, to, territoryId, playerState);
case WaterPlanKind.DiveIn:
if (!flag)
{
return float.PositiveInfinity;
}
return EstimateDry(from, waterPlan.EntryPoint.Value, territoryId, playerState) + DescentTime(waterPlan.EntryPoint.Value, to);
case WaterPlanKind.SurfaceOutDiveIn:
if (!flag)
{
return float.PositiveInfinity;
}
return AscentTime(from, waterPlan.SurfacePoint.Value) + EstimateDry(waterPlan.SurfacePoint.Value, waterPlan.EntryPoint.Value, territoryId, playerState) + DescentTime(waterPlan.EntryPoint.Value, to);
default:
throw new ArgumentOutOfRangeException("plan", waterPlan.Kind, "unknown water plan kind");
}
}
private static float AscentTime(Vector3 from, Vector3 surfacePoint)
{
return MathF.Max(0f, surfacePoint.Y - from.Y - 20.6f) / 6f + 1f;
}
private static float DescentTime(Vector3 entryPoint, Vector3 to)
{
Vector3 vector = entryPoint;
vector.Y = entryPoint.Y - 25f;
Vector3 value = vector;
return 4f + Vector3.Distance(value, to) / 6f;
}
private float EstimateDry(Vector3 from, Vector3 to, uint territoryId, PlayerNavState playerState)
{ {
if (!territoryInfo.CanUseMount(territoryId)) if (!territoryInfo.CanUseMount(territoryId))
{ {

View file

@ -225,6 +225,15 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
} }
} }
private void AddWithinZoneEdge(NavGraph queryGraph, NavNode from, NavNode to, uint territoryId, PlayerNavState playerState)
{
float num = costCalculator.EstimateWithinZoneTravelTime(from.Position, to.Position, territoryId, playerState);
if (float.IsFinite(num))
{
queryGraph.AddEdge(new NavEdge(from.Id, to.Id, NavEdgeType.WithinZone, num, 0));
}
}
private void AddWithinZoneEdgesFrom(NavGraph queryGraph, NavNode origin, PlayerNavState playerState) private void AddWithinZoneEdgesFrom(NavGraph queryGraph, NavNode origin, PlayerNavState playerState)
{ {
foreach (string item in queryGraph.GetNodesInTerritory(origin.TerritoryId)) foreach (string item in queryGraph.GetNodesInTerritory(origin.TerritoryId))
@ -234,8 +243,7 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
NavNode node = queryGraph.GetNode(item); NavNode node = queryGraph.GetNode(item);
if (!(node == null) && AreNodesConnectedWithinZone(origin, node, origin.TerritoryId)) if (!(node == null) && AreNodesConnectedWithinZone(origin, node, origin.TerritoryId))
{ {
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(origin.Position, node.Position, origin.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, origin, node, origin.TerritoryId, playerState);
queryGraph.AddEdge(new NavEdge(origin.Id, item, NavEdgeType.WithinZone, timeCostSeconds, 0));
} }
} }
} }
@ -250,8 +258,7 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
NavNode node = queryGraph.GetNode(item); NavNode node = queryGraph.GetNode(item);
if (!(node == null) && AreNodesConnectedWithinZone(node, destination, destination.TerritoryId)) if (!(node == null) && AreNodesConnectedWithinZone(node, destination, destination.TerritoryId))
{ {
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, destination.Position, destination.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, node, destination, destination.TerritoryId, playerState);
queryGraph.AddEdge(new NavEdge(item, destination.Id, NavEdgeType.WithinZone, timeCostSeconds, 0));
} }
} }
} }
@ -348,10 +355,8 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
NavNode node = queryGraph.GetNode(item); NavNode node = queryGraph.GetNode(item);
if (!(node == null) && (!(node is NavNode.ZoneBoundaryNode) || string.CompareOrdinal(zoneBoundaryNode.Id, item) < 0) && AreNodesConnectedWithinZone(zoneBoundaryNode, node, zoneBoundaryNode.TerritoryId)) if (!(node == null) && (!(node is NavNode.ZoneBoundaryNode) || string.CompareOrdinal(zoneBoundaryNode.Id, item) < 0) && AreNodesConnectedWithinZone(zoneBoundaryNode, node, zoneBoundaryNode.TerritoryId))
{ {
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, zoneBoundaryNode.Position, zoneBoundaryNode.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, node, zoneBoundaryNode, zoneBoundaryNode.TerritoryId, playerState);
float timeCostSeconds2 = costCalculator.EstimateWithinZoneTravelTime(zoneBoundaryNode.Position, node.Position, zoneBoundaryNode.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, zoneBoundaryNode, node, zoneBoundaryNode.TerritoryId, playerState);
queryGraph.AddEdge(new NavEdge(item, zoneBoundaryNode.Id, NavEdgeType.WithinZone, timeCostSeconds, 0));
queryGraph.AddEdge(new NavEdge(zoneBoundaryNode.Id, item, NavEdgeType.WithinZone, timeCostSeconds2, 0));
} }
} }
} }
@ -373,10 +378,8 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
NavNode node = queryGraph.GetNode(item); NavNode node = queryGraph.GetNode(item);
if (!(node == null) && (!(node is NavNode.WarpNode) || string.CompareOrdinal(warpNode.Id, item) < 0) && !(node is NavNode.ZoneBoundaryNode) && AreNodesConnectedWithinZone(warpNode, node, warpNode.TerritoryId)) if (!(node == null) && (!(node is NavNode.WarpNode) || string.CompareOrdinal(warpNode.Id, item) < 0) && !(node is NavNode.ZoneBoundaryNode) && AreNodesConnectedWithinZone(warpNode, node, warpNode.TerritoryId))
{ {
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, warpNode.Position, warpNode.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, node, warpNode, warpNode.TerritoryId, playerState);
float timeCostSeconds2 = costCalculator.EstimateWithinZoneTravelTime(warpNode.Position, node.Position, warpNode.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, warpNode, node, warpNode.TerritoryId, playerState);
queryGraph.AddEdge(new NavEdge(item, warpNode.Id, NavEdgeType.WithinZone, timeCostSeconds, 0));
queryGraph.AddEdge(new NavEdge(warpNode.Id, item, NavEdgeType.WithinZone, timeCostSeconds2, 0));
} }
} }
} }
@ -403,10 +406,8 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
bool flag = ((node is NavNode.ZoneBoundaryNode || node is NavNode.WarpNode) ? true : false); bool flag = ((node is NavNode.ZoneBoundaryNode || node is NavNode.WarpNode) ? true : false);
if (!flag && AreNodesConnectedWithinZone(subRegionConnectorNode, node, subRegionConnectorNode.TerritoryId)) if (!flag && AreNodesConnectedWithinZone(subRegionConnectorNode, node, subRegionConnectorNode.TerritoryId))
{ {
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, subRegionConnectorNode.Position, subRegionConnectorNode.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, node, subRegionConnectorNode, subRegionConnectorNode.TerritoryId, playerState);
float timeCostSeconds2 = costCalculator.EstimateWithinZoneTravelTime(subRegionConnectorNode.Position, node.Position, subRegionConnectorNode.TerritoryId, playerState); AddWithinZoneEdge(queryGraph, subRegionConnectorNode, node, subRegionConnectorNode.TerritoryId, playerState);
queryGraph.AddEdge(new NavEdge(item, subRegionConnectorNode.Id, NavEdgeType.WithinZone, timeCostSeconds, 0));
queryGraph.AddEdge(new NavEdge(subRegionConnectorNode.Id, item, NavEdgeType.WithinZone, timeCostSeconds2, 0));
} }
} }
} }

View file

@ -12,6 +12,12 @@ public abstract record NavInstruction
public sealed record Move(ushort Territory, Vector3 Position, bool Fly, bool LandAtTarget, float? StopDistance, uint? DataId, bool? Mount, bool DisableNavmesh, bool AllowZoneTransition, string? TargetNodeId, string? ApproachNodeId, bool IsFinal) : NavInstruction(); public sealed record Move(ushort Territory, Vector3 Position, bool Fly, bool LandAtTarget, float? StopDistance, uint? DataId, bool? Mount, bool DisableNavmesh, bool AllowZoneTransition, string? TargetNodeId, string? ApproachNodeId, bool IsFinal) : NavInstruction();
public sealed record DiveEntry(ushort Territory, Vector3 Position, string? TargetNodeId, string? ApproachNodeId) : NavInstruction();
public sealed record Swim(ushort Territory, Vector3 Position, float SurfaceY, float? StopDistance, string? TargetNodeId, string? ApproachNodeId, bool IsFinal) : NavInstruction();
public sealed record Surface(ushort Territory, Vector3 Position, string? TargetNodeId, string? ApproachNodeId) : NavInstruction();
public sealed record Land : NavInstruction; public sealed record Land : NavInstruction;
public sealed record Unmount : NavInstruction; public sealed record Unmount : NavInstruction;

View file

@ -93,4 +93,42 @@ public sealed class PlayerNavState
{ {
return QuestSequenceChecker(questRowId); return QuestSequenceChecker(questRowId);
} }
public PlayerNavState WithTravelCapabilities(bool allowMount, bool allowFlight)
{
PlayerNavState obj = new PlayerNavState
{
CurrentTerritoryId = CurrentTerritoryId,
CurrentPosition = CurrentPosition,
Gil = Gil,
GilReserve = GilReserve,
UnlockedAetherytes = UnlockedAetherytes,
HomeAetheryte = HomeAetheryte,
FreeAetheryte = FreeAetheryte,
TeleportCosts = TeleportCosts
};
IReadOnlySet<uint> flyingUnlockedTerritories;
if (!(allowMount && allowFlight))
{
IReadOnlySet<uint> readOnlySet = new HashSet<uint>();
flyingUnlockedTerritories = readOnlySet;
}
else
{
flyingUnlockedTerritories = FlyingUnlockedTerritories;
}
obj.FlyingUnlockedTerritories = flyingUnlockedTerritories;
obj.MountSpeedLevels = MountSpeedLevels;
obj.TeleportUnlocked = TeleportUnlocked;
obj.ReturnAvailable = ReturnAvailable;
obj.MountUnlocked = allowMount && MountUnlocked;
obj.MaxExpansion = MaxExpansion;
obj.CurrentLevel = CurrentLevel;
obj.CurrentMsqQuestId = CurrentMsqQuestId;
obj.PreferFreeTravel = PreferFreeTravel;
obj.QuestCompleteChecker = QuestCompleteChecker;
obj.QuestSequenceChecker = QuestSequenceChecker;
obj.AvailableTeleportTickets = AvailableTeleportTickets;
return obj;
}
} }

View file

@ -117,7 +117,7 @@ public class PlayerNavStateBuilder(IClientState clientState, IObjectTable object
return hashSet; return hashSet;
} }
private unsafe Dictionary<uint, int> BuildMountSpeedLevels() private Dictionary<uint, int> BuildMountSpeedLevels()
{ {
Dictionary<uint, int> dictionary = new Dictionary<uint, int>(); Dictionary<uint, int> dictionary = new Dictionary<uint, int>();
ExcelSheet<TerritoryType> excelSheet = dataManager.GetExcelSheet<TerritoryType>(); ExcelSheet<TerritoryType> excelSheet = dataManager.GetExcelSheet<TerritoryType>();
@ -125,30 +125,51 @@ public class PlayerNavStateBuilder(IClientState clientState, IObjectTable object
{ {
return dictionary; return dictionary;
} }
UIState* ptr = UIState.Instance();
foreach (TerritoryType item in excelSheet) foreach (TerritoryType item in excelSheet)
{ {
if (item.RowId != 0 && item.MountSpeed.RowId != 0) if (item.RowId != 0 && item.MountSpeed.RowId != 0)
{ {
MountSpeed value = item.MountSpeed.Value; int unlockedMountSpeedLevel = GetUnlockedMountSpeedLevel(item.MountSpeed.Value);
int num = 0; if (unlockedMountSpeedLevel > 0)
if (value.Quest.RowId != 0 && ptr != null && ptr->IsUnlockLinkUnlockedOrQuestCompleted(value.Quest.RowId, 0))
{ {
num++; dictionary[item.RowId] = unlockedMountSpeedLevel;
}
if (value.Unknown0 != 0 && ptr != null && ptr->IsUnlockLinkUnlockedOrQuestCompleted(value.Unknown0, 0))
{
num++;
}
if (num > 0)
{
dictionary[item.RowId] = num;
} }
} }
} }
return dictionary; return dictionary;
} }
public (int Level, int MaxLevel)? GetTerritoryMountSpeed(uint territoryId)
{
TerritoryType? territoryType = dataManager.GetExcelSheet<TerritoryType>()?.GetRowOrDefault(territoryId);
if (!territoryType.HasValue || territoryType.Value.MountSpeed.RowId == 0)
{
return null;
}
MountSpeed value = territoryType.Value.MountSpeed.Value;
int item = ((value.Quest.RowId != 0) ? 1 : 0) + ((value.Unknown0 != 0) ? 1 : 0);
return (GetUnlockedMountSpeedLevel(value), item);
}
private unsafe static int GetUnlockedMountSpeedLevel(MountSpeed mountSpeed)
{
UIState* ptr = UIState.Instance();
if (ptr == null)
{
return 0;
}
int num = 0;
if (mountSpeed.Quest.RowId != 0 && ptr->IsUnlockLinkUnlockedOrQuestCompleted(mountSpeed.Quest.RowId, 0))
{
num++;
}
if (mountSpeed.Unknown0 != 0 && ptr->IsUnlockLinkUnlockedOrQuestCompleted(mountSpeed.Unknown0, 0))
{
num++;
}
return num;
}
private unsafe static int GetCurrentGil() private unsafe static int GetCurrentGil()
{ {
InventoryManager* ptr = InventoryManager.Instance(); InventoryManager* ptr = InventoryManager.Instance();

View file

@ -13,17 +13,23 @@ public sealed class ReRoutePolicy(NavRouter navRouter, PlayerNavStateBuilder pla
private bool _hasDestination; private bool _hasDestination;
private bool _allowMount = true;
private bool _allowFlight = true;
private int _reRouteCount; private int _reRouteCount;
private readonly HashSet<string> _penalizedTargets = new HashSet<string>(); private readonly HashSet<string> _penalizedTargets = new HashSet<string>();
private const int MaxReRoutes = 3; private const int MaxReRoutes = 3;
public void SetDestination(uint territoryId, Vector3 position) public void SetDestination(uint territoryId, Vector3 position, bool allowMount = true, bool allowFlight = true)
{ {
bool num = _hasDestination && _destinationTerritoryId == territoryId && Vector3.DistanceSquared(_destinationPosition, position) < 1f; bool num = _hasDestination && _destinationTerritoryId == territoryId && Vector3.DistanceSquared(_destinationPosition, position) < 1f && _allowMount == allowMount && _allowFlight == allowFlight;
_destinationTerritoryId = territoryId; _destinationTerritoryId = territoryId;
_destinationPosition = position; _destinationPosition = position;
_allowMount = allowMount;
_allowFlight = allowFlight;
_hasDestination = true; _hasDestination = true;
_reRouteCount = 0; _reRouteCount = 0;
_penalizedTargets.Clear(); _penalizedTargets.Clear();
@ -68,6 +74,7 @@ public sealed class ReRoutePolicy(NavRouter navRouter, PlayerNavStateBuilder pla
logger.LogWarning("Cannot build PlayerNavState for re-routing"); logger.LogWarning("Cannot build PlayerNavState for re-routing");
return new ReRouteDecision.GiveUp(); return new ReRouteDecision.GiveUp();
} }
playerNavState = playerNavState.WithTravelCapabilities(_allowMount, _allowFlight);
if (playerNavState.CurrentTerritoryId != _destinationTerritoryId && hasPreservedTasks) if (playerNavState.CurrentTerritoryId != _destinationTerritoryId && hasPreservedTasks)
{ {
_reRouteCount++; _reRouteCount++;

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -7,7 +8,7 @@ using SmartNav.Model.Navigation;
namespace SmartNav; namespace SmartNav;
public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerritoryInfo territoryInfo, ILogger<RouteInstructionBuilder> logger) public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerritoryInfo territoryInfo, WaterRoutePlanner waterPlanner, ILogger<RouteInstructionBuilder> logger)
{ {
private const float BoundaryApproachMargin = 5f; private const float BoundaryApproachMargin = 5f;
@ -17,7 +18,25 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
for (int i = 0; i < route.Segments.Count; i++) for (int i = 0; i < route.Segments.Count; i++)
{ {
RouteSegment routeSegment = route.Segments[i]; RouteSegment routeSegment = route.Segments[i];
if (routeSegment.EdgeType != NavEdgeType.WithinZone || i + 1 >= route.Segments.Count || route.Segments[i + 1].EdgeType != NavEdgeType.ZoneBoundary) if (routeSegment.EdgeType == NavEdgeType.WithinZone && i + 1 < route.Segments.Count && route.Segments[i + 1].EdgeType == NavEdgeType.ZoneBoundary)
{
WaterPlan waterPlan = waterPlanner.Plan(routeSegment.From.TerritoryId, routeSegment.From.Position, routeSegment.To.Position);
if ((object)waterPlan != null && waterPlan.Kind == WaterPlanKind.SurfaceOut)
{
uint territoryId = routeSegment.From.TerritoryId;
list.Add(new NavInstruction.WaitForTerritory(new global::_003C_003Ez__ReadOnlySingleElementList<uint>(territoryId), "Wait(territory: " + territoryInfo.GetNameAndId(territoryId) + ")"));
if (!disableNavmesh)
{
list.Add(new NavInstruction.WaitForNavmesh());
}
list.Add(new NavInstruction.Surface((ushort)territoryId, waterPlan.SurfacePoint.Value, routeSegment.To.Id, routeSegment.From.Id));
}
else if (waterPlan != null)
{
logger.LogWarning("SmartNav: water plan {Kind} on a within-zone segment collapsed into a boundary crossing in {Territory}; only the boundary move is emitted", waterPlan.Kind, territoryInfo.GetNameAndId(routeSegment.From.TerritoryId));
}
}
else
{ {
BuildSegment(list, routeSegment, (i > 0) ? route.Segments[i - 1] : null, i, route.Segments.Count, destinationTerritory, disableNavmesh); BuildSegment(list, routeSegment, (i > 0) ? route.Segments[i - 1] : null, i, route.Segments.Count, destinationTerritory, disableNavmesh);
} }
@ -52,6 +71,39 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
return true; return true;
} }
private static NavInstruction.Move BuildWithinZoneMove(RouteSegment seg, uint territory, bool isFinalSegment)
{
uint? num = ((!(seg.To is NavNode.WarpNode warpNode)) ? ((uint?)null) : warpNode.NpcDataId);
uint? dataId = num;
return new NavInstruction.Move((ushort)territory, seg.To.Position, seg.Fly, seg.Fly, isFinalSegment ? ((float?)null) : new float?(3f), dataId, null, DisableNavmesh: false, AllowZoneTransition: false, seg.To.Id, seg.From.Id, isFinalSegment);
}
private static void EmitWaterLegs(List<NavInstruction> instructions, WaterPlan plan, RouteSegment seg, uint territory, bool isFinalSegment)
{
WaterPlanKind kind = plan.Kind;
if ((uint)kind > 3u)
{
throw new ArgumentOutOfRangeException("plan", plan.Kind, "unknown water plan kind");
}
kind = plan.Kind;
if ((uint)(kind - 2) <= 1u)
{
instructions.Add(new NavInstruction.Surface((ushort)territory, plan.SurfacePoint.Value, seg.To.Id, seg.From.Id));
}
if (plan.Kind == WaterPlanKind.SurfaceOut)
{
instructions.Add(BuildWithinZoneMove(seg, territory, isFinalSegment));
return;
}
kind = plan.Kind;
if ((kind == WaterPlanKind.DiveIn || kind == WaterPlanKind.SurfaceOutDiveIn) ? true : false)
{
instructions.Add(new NavInstruction.Move((ushort)territory, plan.EntryPoint.Value, Fly: true, LandAtTarget: false, 3f, null, null, DisableNavmesh: false, AllowZoneTransition: false, seg.To.Id, seg.From.Id, IsFinal: false));
instructions.Add(new NavInstruction.DiveEntry((ushort)territory, plan.EntryPoint.Value, seg.To.Id, seg.From.Id));
}
instructions.Add(new NavInstruction.Swim((ushort)territory, seg.To.Position, plan.GoalSurfaceY.Value, isFinalSegment ? ((float?)null) : new float?(3f), seg.To.Id, seg.From.Id, isFinalSegment));
}
private void BuildSegment(List<NavInstruction> instructions, RouteSegment seg, RouteSegment? previous, int index, int segmentCount, uint destinationTerritory, bool disableNavmesh) private void BuildSegment(List<NavInstruction> instructions, RouteSegment seg, RouteSegment? previous, int index, int segmentCount, uint destinationTerritory, bool disableNavmesh)
{ {
uint territoryId4; uint territoryId4;
@ -79,15 +131,21 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
{ {
uint territoryId = seg.From.TerritoryId; uint territoryId = seg.From.TerritoryId;
bool flag = index == segmentCount - 1 && seg.To.TerritoryId == destinationTerritory; bool flag = index == segmentCount - 1 && seg.To.TerritoryId == destinationTerritory;
logger.LogTrace("SmartNav: Move within {Territory}{Final}", territoryInfo.GetNameAndId(territoryId), flag ? " (final)" : ""); WaterPlan waterPlan = waterPlanner.Plan(territoryId, seg.From.Position, seg.To.Position);
logger.LogTrace("SmartNav: Move within {Territory}{Final}{Water}", territoryInfo.GetNameAndId(territoryId), flag ? " (final)" : "", (waterPlan == null) ? "" : $" (water: {waterPlan.Kind})");
instructions.Add(new NavInstruction.WaitForTerritory(new global::_003C_003Ez__ReadOnlySingleElementList<uint>(territoryId), "Wait(territory: " + territoryInfo.GetNameAndId(territoryId) + ")")); instructions.Add(new NavInstruction.WaitForTerritory(new global::_003C_003Ez__ReadOnlySingleElementList<uint>(territoryId), "Wait(territory: " + territoryInfo.GetNameAndId(territoryId) + ")"));
if (!disableNavmesh) if (!disableNavmesh)
{ {
instructions.Add(new NavInstruction.WaitForNavmesh()); instructions.Add(new NavInstruction.WaitForNavmesh());
} }
uint? num = ((!(seg.To is NavNode.WarpNode warpNode)) ? ((uint?)null) : warpNode.NpcDataId); if (waterPlan == null)
uint? dataId = num; {
instructions.Add(new NavInstruction.Move((ushort)territoryId, seg.To.Position, seg.Fly, seg.Fly, flag ? ((float?)null) : new float?(3f), dataId, null, DisableNavmesh: false, AllowZoneTransition: false, seg.To.Id, seg.From.Id, flag)); instructions.Add(BuildWithinZoneMove(seg, territoryId, flag));
}
else
{
EmitWaterLegs(instructions, waterPlan, seg, territoryId, flag);
}
break; break;
} }
case NavEdgeType.ZoneBoundary: case NavEdgeType.ZoneBoundary:
@ -104,20 +162,20 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
{ {
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: true, 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_04bb;
} }
} }
instructions.Add(new NavInstruction.Move((ushort)territoryId3, seg.From.Position, seg.Fly, seg.Fly, 0f, null, null, DisableNavmesh: false, AllowZoneTransition: true, seg.From.Id, null, IsFinal: false)); instructions.Add(new NavInstruction.Move((ushort)territoryId3, seg.From.Position, seg.Fly, seg.Fly, 0f, null, null, DisableNavmesh: false, AllowZoneTransition: true, seg.From.Id, null, IsFinal: false));
goto IL_04b4; goto IL_04bb;
} }
case NavEdgeType.Warp: case NavEdgeType.Warp:
{ {
uint territoryId5 = seg.From.TerritoryId; uint territoryId5 = seg.From.TerritoryId;
uint territoryId6 = seg.To.TerritoryId; uint territoryId6 = seg.To.TerritoryId;
logger.LogTrace("SmartNav: Warp from {From} to {To}", territoryInfo.GetNameAndId(territoryId5), territoryInfo.GetNameAndId(territoryId6)); logger.LogTrace("SmartNav: Warp from {From} to {To}", territoryInfo.GetNameAndId(territoryId5), territoryInfo.GetNameAndId(territoryId6));
if (seg.From is NavNode.WarpNode { NpcDataId: { } npcDataId } warpNode2) if (seg.From is NavNode.WarpNode { NpcDataId: { } npcDataId } warpNode)
{ {
instructions.Add(new NavInstruction.InteractWarp(npcDataId, warpNode2.WarpRowId, (ushort)territoryId5, (ushort)territoryId6)); instructions.Add(new NavInstruction.InteractWarp(npcDataId, warpNode.WarpRowId, (ushort)territoryId5, (ushort)territoryId6));
break; break;
} }
logger.LogWarning("SmartNav: warp segment {From} -> {To} has no interactable source NPC; segment dropped", territoryInfo.GetNameAndId(territoryId5), territoryInfo.GetNameAndId(territoryId6)); logger.LogWarning("SmartNav: warp segment {From} -> {To} has no interactable source NPC; segment dropped", territoryInfo.GetNameAndId(territoryId5), territoryInfo.GetNameAndId(territoryId6));
@ -156,7 +214,7 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
logger.LogWarning("SmartNav: unhandled edge type {EdgeType}; segment dropped", seg.EdgeType); logger.LogWarning("SmartNav: unhandled edge type {EdgeType}; segment dropped", seg.EdgeType);
break; break;
} }
IL_04b4: IL_04bb:
instructions.Add(new NavInstruction.WaitForTerritory(new global::_003C_003Ez__ReadOnlySingleElementList<uint>(territoryId4), "Wait(territory: " + territoryInfo.GetNameAndId(territoryId4) + ")")); instructions.Add(new NavInstruction.WaitForTerritory(new global::_003C_003Ez__ReadOnlySingleElementList<uint>(territoryId4), "Wait(territory: " + territoryInfo.GetNameAndId(territoryId4) + ")"));
break; break;
} }

View file

@ -0,0 +1,5 @@
using System.Numerics;
namespace SmartNav;
public sealed record WaterPlan(WaterPlanKind Kind, Vector3? SurfacePoint, Vector3? EntryPoint, float? GoalSurfaceY);

View file

@ -0,0 +1,9 @@
namespace SmartNav;
public enum WaterPlanKind
{
Submerged,
DiveIn,
SurfaceOut,
SurfaceOutDiveIn
}

View file

@ -0,0 +1,66 @@
using System;
using System.Numerics;
using SmartNav.Data;
using SmartNav.Model.Navigation;
namespace SmartNav;
public sealed class WaterRoutePlanner(WaterMapService waterMaps)
{
private const float SameBodySurfaceTolerance = 0.5f;
public bool IsSubmerged(uint territoryId, Vector3 point, out float surfaceY)
{
surfaceY = 0f;
if (!waterMaps.TryGetMap(territoryId, out TerritoryWaterMap map))
{
return false;
}
return IsSubmerged(map, point, out surfaceY);
}
public WaterPlan? Plan(uint territoryId, Vector3 from, Vector3 to)
{
if (!waterMaps.TryGetMap(territoryId, out TerritoryWaterMap map))
{
return null;
}
float surfaceY;
bool flag = IsSubmerged(map, from, out surfaceY);
float surfaceY2;
bool flag2 = IsSubmerged(map, to, out surfaceY2);
if (!flag && !flag2)
{
return null;
}
Vector3? surfacePoint = (flag ? new Vector3?(new Vector3(from.X, surfaceY, from.Z)) : ((Vector3?)null));
Vector3 value = new Vector3(to.X, surfaceY2, to.Z);
if (!flag)
{
return new WaterPlan(WaterPlanKind.DiveIn, null, value, surfaceY2);
}
if (!flag2)
{
return new WaterPlan(WaterPlanKind.SurfaceOut, surfacePoint, null, null);
}
if (MathF.Abs(surfaceY - surfaceY2) <= 0.5f)
{
return new WaterPlan(WaterPlanKind.Submerged, null, null, surfaceY2);
}
return new WaterPlan(WaterPlanKind.SurfaceOutDiveIn, surfacePoint, value, surfaceY2);
}
private static bool IsSubmerged(TerritoryWaterMap map, Vector3 point, out float surfaceY)
{
surfaceY = 0f;
if (map.ClassifyAt(point.X, point.Z) != WaterCellClass.Divable)
{
return false;
}
if (map.TryGetSurfaceY(point.X, point.Z, out surfaceY))
{
return point.Y <= surfaceY - 20.6f;
}
return false;
}
}