muffin v7.5.13
This commit is contained in:
parent
911ed68baa
commit
acf3e3cb18
69 changed files with 2731 additions and 1425 deletions
Binary file not shown.
|
|
@ -4,18 +4,18 @@ internal static class PathBundleSecret
|
|||
{
|
||||
private static readonly byte[] A = new byte[32]
|
||||
{
|
||||
48, 212, 251, 149, 233, 68, 48, 175, 219, 236,
|
||||
217, 100, 130, 200, 255, 152, 233, 112, 177, 94,
|
||||
93, 198, 133, 46, 110, 244, 154, 244, 247, 13,
|
||||
145, 158
|
||||
183, 148, 243, 175, 81, 166, 60, 18, 24, 50,
|
||||
233, 21, 123, 58, 154, 174, 171, 214, 240, 163,
|
||||
19, 222, 77, 126, 21, 146, 142, 237, 152, 128,
|
||||
16, 0
|
||||
};
|
||||
|
||||
private static readonly byte[] B = new byte[32]
|
||||
{
|
||||
79, 60, 140, 255, 167, 84, 206, 132, 105, 228,
|
||||
71, 209, 251, 6, 250, 62, 38, 86, 249, 66,
|
||||
102, 234, 109, 58, 57, 229, 131, 131, 42, 66,
|
||||
172, 97
|
||||
167, 25, 83, 195, 35, 102, 138, 74, 178, 77,
|
||||
83, 219, 116, 213, 10, 227, 200, 219, 186, 181,
|
||||
158, 22, 1, 102, 179, 218, 15, 30, 54, 210,
|
||||
214, 232
|
||||
};
|
||||
|
||||
internal static byte[] Key()
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -4,18 +4,18 @@ internal static class PathBundleSecret
|
|||
{
|
||||
private static readonly byte[] A = new byte[32]
|
||||
{
|
||||
83, 53, 162, 177, 116, 210, 154, 147, 32, 246,
|
||||
109, 49, 21, 10, 185, 40, 3, 223, 158, 234,
|
||||
82, 124, 250, 209, 185, 174, 77, 93, 58, 15,
|
||||
16, 246
|
||||
219, 178, 133, 86, 37, 102, 10, 221, 178, 245,
|
||||
197, 51, 203, 134, 231, 122, 11, 25, 197, 240,
|
||||
44, 220, 234, 91, 41, 93, 186, 183, 17, 72,
|
||||
109, 31
|
||||
};
|
||||
|
||||
private static readonly byte[] B = new byte[32]
|
||||
{
|
||||
11, 0, 215, 132, 153, 74, 202, 77, 15, 86,
|
||||
243, 43, 187, 26, 168, 53, 42, 2, 79, 60,
|
||||
154, 215, 216, 52, 147, 70, 38, 49, 232, 46,
|
||||
225, 130
|
||||
107, 156, 123, 202, 221, 77, 66, 210, 234, 31,
|
||||
174, 55, 4, 171, 94, 88, 13, 248, 215, 191,
|
||||
115, 73, 208, 117, 230, 149, 32, 243, 105, 23,
|
||||
154, 250
|
||||
};
|
||||
|
||||
internal static byte[] Key()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
using System;
|
||||
using System.Reflection;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Windowing;
|
||||
|
||||
namespace LLib.ImGui;
|
||||
|
||||
[Obfuscation(Feature = "-rename", Exclude = false, ApplyToMembers = true)]
|
||||
public abstract class LWindow : Window
|
||||
{
|
||||
private bool _initializedConfig;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
using System.Reflection;
|
||||
|
||||
namespace LLib.ImGui;
|
||||
|
||||
[Obfuscation(Feature = "-rename", Exclude = false, ApplyToMembers = true)]
|
||||
public class WindowConfig
|
||||
{
|
||||
public bool IsPinned { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -120,33 +119,7 @@ public sealed class DalamudLoggerProvider : ILoggerProvider, IDisposable, ISuppo
|
|||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
string name = _categoryNameFormatter(categoryName);
|
||||
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;
|
||||
return new DalamudLogger(_categoryNameFormatter(categoryName), this, _pluginLog);
|
||||
}
|
||||
|
||||
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -185,34 +185,58 @@
|
|||
"description": "If true, will go to the position in a straight line instead of using pathfinding",
|
||||
"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": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
"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": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"description": "GENERAL MOVEMENT: Controls Sprint independently of mount and flight behavior."
|
||||
},
|
||||
"ItemId": {
|
||||
"type": [
|
||||
|
|
|
|||
|
|
@ -4,18 +4,18 @@ internal static class PathBundleSecret
|
|||
{
|
||||
private static readonly byte[] A = new byte[32]
|
||||
{
|
||||
73, 4, 92, 189, 214, 0, 53, 235, 105, 164,
|
||||
216, 243, 244, 200, 198, 26, 227, 227, 153, 121,
|
||||
241, 147, 166, 171, 63, 63, 121, 152, 52, 115,
|
||||
89, 201
|
||||
153, 30, 158, 74, 159, 14, 199, 185, 40, 166,
|
||||
85, 6, 37, 167, 5, 38, 221, 72, 99, 65,
|
||||
160, 20, 49, 186, 80, 230, 97, 96, 19, 189,
|
||||
194, 228
|
||||
};
|
||||
|
||||
private static readonly byte[] B = new byte[32]
|
||||
{
|
||||
243, 132, 180, 1, 83, 89, 202, 66, 33, 20,
|
||||
242, 184, 99, 15, 215, 2, 4, 125, 221, 134,
|
||||
186, 51, 132, 151, 141, 220, 230, 57, 252, 77,
|
||||
103, 57
|
||||
30, 173, 221, 145, 116, 199, 45, 52, 72, 186,
|
||||
186, 181, 129, 66, 39, 24, 195, 19, 249, 56,
|
||||
14, 14, 134, 179, 100, 23, 222, 226, 221, 184,
|
||||
242, 90
|
||||
};
|
||||
|
||||
internal static byte[] Key()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -45,6 +45,10 @@ public sealed class QuestStep
|
|||
|
||||
public bool DisableNavmesh { get; set; }
|
||||
|
||||
public ETravelMode TravelMode { get; set; }
|
||||
|
||||
public EArrivalMode ArrivalMode { get; set; }
|
||||
|
||||
public bool? Mount { get; set; }
|
||||
|
||||
public bool MountRequired { get; set; }
|
||||
|
|
|
|||
|
|
@ -56,19 +56,19 @@ internal sealed class BossModModule : ICombatModule, IDisposable
|
|||
if ((uint)(valueOrDefault - 3) <= 1u)
|
||||
{
|
||||
flag = true;
|
||||
goto IL_00bd;
|
||||
goto IL_00be;
|
||||
}
|
||||
}
|
||||
flag = false;
|
||||
goto IL_00bd;
|
||||
goto IL_00be;
|
||||
}
|
||||
goto IL_012e;
|
||||
IL_00bd:
|
||||
goto IL_012f;
|
||||
IL_00be:
|
||||
bool flag2 = flag;
|
||||
_logger.LogDebug("BossModModule.Start: isRanged={IsRanged}, job role={Role}", flag2, localPlayer.ClassJob.ValueNullable?.Role);
|
||||
_bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, flag2);
|
||||
goto IL_012e;
|
||||
IL_012e:
|
||||
goto IL_012f;
|
||||
IL_012f:
|
||||
return true;
|
||||
}
|
||||
catch (IpcError exception)
|
||||
|
|
|
|||
|
|
@ -29,12 +29,28 @@ internal sealed class ItemUseModule : ICombatModule
|
|||
|
||||
private long _continueAt;
|
||||
|
||||
private bool _isWaitingToUseItem;
|
||||
|
||||
private bool _itemUsePending;
|
||||
|
||||
private long _itemUsePendingUntil;
|
||||
|
||||
private int _itemCountBeforeUse;
|
||||
|
||||
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)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
|
|
@ -62,6 +78,7 @@ internal sealed class ItemUseModule : ICombatModule
|
|||
_combatData = combatData;
|
||||
_isDoingRotation = true;
|
||||
_continueAt = Environment.TickCount64;
|
||||
_isWaitingToUseItem = false;
|
||||
_itemUsePending = false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -73,11 +90,13 @@ internal sealed class ItemUseModule : ICombatModule
|
|||
if (_isDoingRotation)
|
||||
{
|
||||
_delegate.Stop();
|
||||
_isDoingRotation = false;
|
||||
_combatData = null;
|
||||
_delegate = null;
|
||||
_continueAt = Environment.TickCount64;
|
||||
}
|
||||
_isDoingRotation = false;
|
||||
_isWaitingToUseItem = false;
|
||||
_itemUsePending = false;
|
||||
_combatData = null;
|
||||
_delegate = null;
|
||||
_continueAt = Environment.TickCount64;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -93,43 +112,78 @@ internal sealed class ItemUseModule : ICombatModule
|
|||
}
|
||||
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);
|
||||
if (_itemUsePending)
|
||||
{
|
||||
int inventoryItemCount = InventoryManager.Instance()->GetInventoryItemCount(_combatData.CombatItemUse.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0);
|
||||
if (_itemUsePending)
|
||||
if (inventoryItemCount < _itemCountBeforeUse)
|
||||
{
|
||||
if (Environment.TickCount64 < _itemUsePendingUntil)
|
||||
{
|
||||
_logger.LogDebug("Item use pending; ignoring temporary inventory count={Count}", inventoryItemCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
_itemUsePending = false;
|
||||
}
|
||||
_itemUsePending = false;
|
||||
return;
|
||||
}
|
||||
if (!_itemUsePending && inventoryItemCount == 0)
|
||||
if (_condition[ConditionFlag.Casting])
|
||||
{
|
||||
_itemUsePendingUntil = Environment.TickCount64 + 1000;
|
||||
}
|
||||
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);
|
||||
}
|
||||
else if (ShouldUseItem(nextTarget))
|
||||
{
|
||||
_logger.LogDebug("Attempting to use item {ItemId}", _combatData.CombatItemUse.ItemId);
|
||||
_itemUsePending = true;
|
||||
_itemUsePendingUntil = Environment.TickCount64 + 3000;
|
||||
AgentInventoryContext.Instance()->UseItem(_combatData.CombatItemUse.ItemId, InventoryType.Invalid, 0u, 0);
|
||||
_continueAt = Environment.TickCount64 + 2000;
|
||||
_logger.LogDebug("Pausing rotation before using item {ItemId}", _combatData.CombatItemUse.ItemId);
|
||||
_isDoingRotation = false;
|
||||
_isWaitingToUseItem = true;
|
||||
_delegate.Stop();
|
||||
_continueAt = Environment.TickCount64 + 3000;
|
||||
_itemUsePendingUntil = Environment.TickCount64 + 4000;
|
||||
}
|
||||
else
|
||||
{
|
||||
_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])
|
||||
{
|
||||
long num = Environment.TickCount64 + 500;
|
||||
if (num > _continueAt)
|
||||
long num2 = Environment.TickCount64 + 500;
|
||||
if (num2 > _continueAt)
|
||||
{
|
||||
_continueAt = num;
|
||||
_continueAt = num2;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ internal static class Dive
|
|||
|
||||
internal sealed class Task : ITask
|
||||
{
|
||||
public bool SmartNavRouted { get; init; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Dive";
|
||||
|
|
@ -51,6 +53,10 @@ internal static class Dive
|
|||
{
|
||||
return false;
|
||||
}
|
||||
if (base.Task.SmartNavRouted && !condition[ConditionFlag.Swimming])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (condition[ConditionFlag.Mounted] || condition[ConditionFlag.Swimming])
|
||||
{
|
||||
Descend();
|
||||
|
|
|
|||
|
|
@ -80,14 +80,14 @@ internal static class Duty
|
|||
|
||||
private EDutyMode GetEffectiveEDutyMode(uint cfcId, EDutyMode? registryDutyMode)
|
||||
{
|
||||
if (registryDutyMode.HasValue)
|
||||
{
|
||||
return registryDutyMode.Value;
|
||||
}
|
||||
if (configuration.Duties.DutyModeOverrides.TryGetValue(cfcId, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
if (registryDutyMode.HasValue)
|
||||
{
|
||||
return registryDutyMode.Value;
|
||||
}
|
||||
EDutyMode defaultDutyMode = configuration.Duties.DefaultDutyMode;
|
||||
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()
|
||||
{
|
||||
|
|
@ -392,7 +392,7 @@ internal static class Duty
|
|||
{
|
||||
protected override bool Start()
|
||||
{
|
||||
bossModIpc.EnableAi(BossModIpc.EPreset.Active);
|
||||
bossModIpc.EnableAi(BossModIpc.EPreset.Active, base.Task.ActivateAi);
|
||||
IPlayerCharacter localPlayer = objectTable.LocalPlayer;
|
||||
bool flag;
|
||||
if (localPlayer != null)
|
||||
|
|
@ -404,18 +404,18 @@ internal static class Duty
|
|||
if ((uint)(valueOrDefault - 3) <= 1u)
|
||||
{
|
||||
flag = true;
|
||||
goto IL_0077;
|
||||
goto IL_0082;
|
||||
}
|
||||
}
|
||||
flag = false;
|
||||
goto IL_0077;
|
||||
goto IL_0082;
|
||||
}
|
||||
goto IL_0087;
|
||||
IL_0077:
|
||||
goto IL_0092;
|
||||
IL_0082:
|
||||
bool isRanged = flag;
|
||||
bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, isRanged);
|
||||
goto IL_0087;
|
||||
IL_0087:
|
||||
goto IL_0092;
|
||||
IL_0092:
|
||||
logger.LogDebug("Enabled BossMod Active preset for AutoDuty run");
|
||||
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()
|
||||
{
|
||||
|
|
@ -475,7 +475,7 @@ internal static class Duty
|
|||
{
|
||||
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)");
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ internal static class EquipRecommended
|
|||
|
||||
private bool _checkedOrTriggeredEquipmentUpdate;
|
||||
|
||||
private List<uint>? _gameRecommendedItemIds;
|
||||
|
||||
private long _timeoutAt;
|
||||
|
||||
private List<(int Slot, BestItemRef Item)>? _smartUpgrades;
|
||||
|
|
@ -177,8 +179,7 @@ internal static class EquipRecommended
|
|||
{
|
||||
if (!TryStartMove(pendingMove))
|
||||
{
|
||||
chatGui.Print($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} rejected; skipping equip step.", "Questionable", 576);
|
||||
return ETaskResult.TaskComplete;
|
||||
throw EquipFailure($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} was rejected");
|
||||
}
|
||||
_currentMoveStarted = true;
|
||||
_timeoutAt = Environment.TickCount64 + 2000;
|
||||
|
|
@ -196,8 +197,7 @@ internal static class EquipRecommended
|
|||
}
|
||||
if (Environment.TickCount64 >= _timeoutAt)
|
||||
{
|
||||
chatGui.Print($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} timed out; skipping equip step.", "Questionable", 576);
|
||||
return ETaskResult.TaskComplete;
|
||||
throw EquipFailure($"Equip move {_smartMoveCursor + 1}/{smartMoves.Count} timed out");
|
||||
}
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
|
@ -227,11 +227,11 @@ internal static class EquipRecommended
|
|||
{
|
||||
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()
|
||||
|
|
@ -247,20 +247,18 @@ internal static class EquipRecommended
|
|||
{
|
||||
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];
|
||||
if (!_directEquipStarted)
|
||||
{
|
||||
if (!TryStartDirectEquip(item, num))
|
||||
{
|
||||
chatGui.Print($"Direct equip for slot {num} rejected, skipping.", "Questionable", 576);
|
||||
_directEquipCursor++;
|
||||
return ETaskResult.StillRunning;
|
||||
throw EquipFailure($"Direct equip for slot {num} was rejected");
|
||||
}
|
||||
_directEquipStarted = true;
|
||||
_timeoutAt = Environment.TickCount64 + 2000;
|
||||
|
|
@ -274,8 +272,7 @@ internal static class EquipRecommended
|
|||
}
|
||||
if (Environment.TickCount64 >= _timeoutAt)
|
||||
{
|
||||
_directEquipCursor++;
|
||||
_directEquipStarted = false;
|
||||
throw EquipFailure($"Direct equip for slot {num} timed out");
|
||||
}
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
|
@ -366,7 +363,8 @@ internal static class EquipRecommended
|
|||
}
|
||||
if (!_checkedOrTriggeredEquipmentUpdate)
|
||||
{
|
||||
if (IsAllRecommendedGearEquipped())
|
||||
_gameRecommendedItemIds = SnapshotRecommendedItemIds(ptr);
|
||||
if (AreAllItemsEquipped(_gameRecommendedItemIds))
|
||||
{
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
|
|
@ -376,15 +374,15 @@ internal static class EquipRecommended
|
|||
_checkedOrTriggeredEquipmentUpdate = true;
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
if (IsAllRecommendedGearEquipped())
|
||||
if (_gameRecommendedItemIds != null && AreAllItemsEquipped(_gameRecommendedItemIds))
|
||||
{
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
if (Environment.TickCount64 < _timeoutAt)
|
||||
if (Environment.TickCount64 >= _timeoutAt)
|
||||
{
|
||||
return ETaskResult.StillRunning;
|
||||
throw EquipFailure("EquipRecommendedGear did not finish before the timeout");
|
||||
}
|
||||
return ETaskResult.TaskComplete;
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
|
||||
private unsafe bool AreUpgradesApplied(IReadOnlyList<(int Slot, BestItemRef Item)> upgrades)
|
||||
|
|
@ -621,19 +619,70 @@ internal static class EquipRecommended
|
|||
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++)
|
||||
{
|
||||
Pointer<InventoryItem> pointer = recommendedItems[i];
|
||||
InventoryItem* value = pointer.Value;
|
||||
if (value != null && value->ItemId != 0 && !InventoryHelper.IsItemEquipped(value->ItemId))
|
||||
if (value != null && value->ItemId != 0)
|
||||
{
|
||||
return false;
|
||||
list.Add(((ItemHandle)value->ItemId).Id);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return list;
|
||||
}
|
||||
|
||||
private unsafe static bool AreAllItemsEquipped(IReadOnlyList<uint> expectedItemIds)
|
||||
{
|
||||
InventoryManager* ptr = InventoryManager.Instance();
|
||||
if (ptr == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems);
|
||||
if (inventoryContainer == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
|
|
@ -16,12 +17,13 @@ using Questionable.Controller.Utils;
|
|||
using Questionable.Functions;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
using SmartNav.Data;
|
||||
|
||||
namespace Questionable.Controller.Steps.Interactions;
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -63,6 +65,25 @@ internal static class Interact
|
|||
{
|
||||
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;
|
||||
EInteractionType interactionType2 = step.InteractionType;
|
||||
int skipMarkerCheck;
|
||||
|
|
@ -74,20 +95,17 @@ internal static class Interact
|
|||
SkipStepConditions stepIf = skipConditions.StepIf;
|
||||
if (stepIf != null && stepIf.Never)
|
||||
{
|
||||
goto IL_024a;
|
||||
goto IL_03d7;
|
||||
}
|
||||
}
|
||||
if (step.InteractionType != EInteractionType.PurchaseItem)
|
||||
{
|
||||
skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0);
|
||||
goto IL_024b;
|
||||
goto IL_03d8;
|
||||
}
|
||||
}
|
||||
goto IL_024a;
|
||||
IL_024a:
|
||||
skipMarkerCheck = 1;
|
||||
goto IL_024b;
|
||||
IL_024b:
|
||||
goto IL_03d7;
|
||||
IL_03d8:
|
||||
uint? pickUpItemId = step.PickUpItemId ?? step.GCPurchase?.ItemId;
|
||||
byte? taxiStandId = step.TaxiStandId;
|
||||
SkipStepConditions? skipConditions2 = step.SkipConditions?.StepIf;
|
||||
|
|
@ -95,6 +113,10 @@ internal static class Interact
|
|||
uint? purchaseItemId = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemId : ((uint?)null));
|
||||
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 break;
|
||||
IL_03d7:
|
||||
skipMarkerCheck = 1;
|
||||
goto IL_03d8;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ internal static class SinglePlayerDuty
|
|||
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)
|
||||
{
|
||||
|
|
@ -54,7 +54,12 @@ internal static class SinglePlayerDuty
|
|||
byte seqBefore = questProgressInfo?.Sequence ?? 0;
|
||||
IReadOnlyList<byte> varsBefore = questProgressInfo?.Variables.ToArray();
|
||||
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())
|
||||
{
|
||||
dutyTasks.Add(new Duty.EnableWrathForDutyTask());
|
||||
|
|
@ -65,14 +70,12 @@ internal static class SinglePlayerDuty
|
|||
}
|
||||
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())
|
||||
{
|
||||
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());
|
||||
if (contentFinderConditionData.TerritoryId == 1052)
|
||||
{
|
||||
|
|
@ -132,8 +135,6 @@ internal static class SinglePlayerDuty
|
|||
|
||||
internal sealed class StartSinglePlayerDutyExecutor(ICondition condition) : TaskExecutor<StartSinglePlayerDuty>()
|
||||
{
|
||||
private long _enteredAtMs;
|
||||
|
||||
protected override bool Start()
|
||||
{
|
||||
return true;
|
||||
|
|
@ -149,14 +150,6 @@ internal static class SinglePlayerDuty
|
|||
{
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
if (_enteredAtMs == 0L)
|
||||
{
|
||||
_enteredAtMs = Environment.TickCount64;
|
||||
}
|
||||
if (Environment.TickCount64 - _enteredAtMs < 2000)
|
||||
{
|
||||
return ETaskResult.StillRunning;
|
||||
}
|
||||
return ETaskResult.TaskComplete;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
float num3 = ((!vector.HasValue) ? float.MaxValue : Vector3.Distance(vector.Value, _destination));
|
||||
if (num3 > num2)
|
||||
float num2 = ((!vector.HasValue) ? float.MaxValue : Vector3.Distance(vector.Value, _destination));
|
||||
if (!vector.HasValue || !IsWithinInteractionRange(vector.Value, _destination, stopDistance, GetVerticalStopDistance()))
|
||||
{
|
||||
PrepareMovementIfNeeded();
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
|
|||
else if (!base.Task.DisableNavmesh)
|
||||
{
|
||||
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
|
||||
{
|
||||
|
|
@ -184,8 +184,8 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
|
|||
Land = true
|
||||
};
|
||||
}
|
||||
float num4 = (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);
|
||||
float num3 = (base.Task.Fly ? _configuration.Navigation.MountFlyDistance : _configuration.Navigation.MountGroundDistance);
|
||||
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);
|
||||
long retryAtMs = 0L;
|
||||
(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;
|
||||
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])
|
||||
{
|
||||
|
|
@ -255,20 +255,25 @@ internal sealed class MoveExecutor : TaskExecutor<MoveTask>, IToastAware, ITaskE
|
|||
}
|
||||
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;
|
||||
}
|
||||
Vector3 position = localPlayer.Position;
|
||||
position.Y = localPlayer.Position.Y - 100f;
|
||||
Vector3 vector = position;
|
||||
Vector3 destination = _destination;
|
||||
destination.Y = localPlayer.Position.Y;
|
||||
Vector3 vector = destination;
|
||||
destination = _destination;
|
||||
destination.Y = _destination.Y - 100f;
|
||||
Vector3 vector2 = destination;
|
||||
_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;
|
||||
uint? dataId = base.Task.DataId;
|
||||
int num2 = 1;
|
||||
int num2 = 2;
|
||||
List<Vector3> list = new List<Vector3>(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);
|
||||
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");
|
||||
MovementController movementController2 = _movementController;
|
||||
uint? dataId2 = base.Task.DataId;
|
||||
Vector3 destination = _destination;
|
||||
Vector3 destination2 = _destination;
|
||||
float? stopDistance = base.Task.StopDistance;
|
||||
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;
|
||||
}
|
||||
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);
|
||||
if (gameObject != null)
|
||||
{
|
||||
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;
|
||||
_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);
|
||||
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)
|
||||
{
|
||||
float num4 = 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);
|
||||
throw new MovementController.PathfindingFailedException($"SmartNav movement ended {num4:F1}y from target (stop distance {num:F1}y)");
|
||||
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", num5, num);
|
||||
throw new MovementController.PathfindingFailedException($"SmartNav movement ended {num5:F1}y from target (stop distance {num:F1}y)");
|
||||
}
|
||||
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()
|
||||
{
|
||||
(Questionable.Controller.Steps.Common.Mount.MountExecutor, Questionable.Controller.Steps.Common.Mount.MountTask)? mountBeforeMovement = _mountBeforeMovement;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
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()
|
||||
{
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ internal static class MoveTo
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ internal static class SmartNavStep
|
|||
{
|
||||
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);
|
||||
if (routeResult.Segments.Count == 0)
|
||||
{
|
||||
|
|
@ -37,10 +41,10 @@ internal static class SmartNavStep
|
|||
}
|
||||
HandledRouting = true;
|
||||
logger.LogDebug("SmartNav routing to {Territory} with {Count} segments", territoryData.GetNameAndId(step.TerritoryId), routeResult.Segments.Count);
|
||||
reRouteService.SetDestination(step.TerritoryId, vector.Value);
|
||||
foreach (ITask item in taskMapper.MapInstructions(instructionBuilder.Build(routeResult, step.TerritoryId, step.DisableNavmesh), step))
|
||||
reRouteService.SetDestination(step.TerritoryId, vector.Value, step.TravelMode);
|
||||
foreach (ITask item3 in taskMapper.MapInstructions(instructionBuilder.Build(routeResult, step.TerritoryId, step.DisableNavmesh), step, step.TravelMode))
|
||||
{
|
||||
yield return item;
|
||||
yield return item3;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -261,6 +261,10 @@ internal sealed class CombatController : IDisposable
|
|||
SetTarget(gameObject3);
|
||||
}
|
||||
}
|
||||
if (_currentFight.Module is ItemUseModule { IsItemUseInProgress: not false })
|
||||
{
|
||||
return EStatus.InCombat;
|
||||
}
|
||||
if (_condition[ConditionFlag.InCombat])
|
||||
{
|
||||
_wasInCombat = true;
|
||||
|
|
@ -362,43 +366,39 @@ internal sealed class CombatController : IDisposable
|
|||
int num = 0;
|
||||
float num2 = float.MaxValue;
|
||||
bool flag5 = _logger.IsEnabled(LogLevel.Debug);
|
||||
foreach (IGameObject item in _objectTable)
|
||||
foreach (IGameObject item2 in _objectTable)
|
||||
{
|
||||
var (num3, text) = GetKillPriority(item, localPlayer);
|
||||
if (num3 <= 0)
|
||||
int item = GetKillPriority(item2, localPlayer).Priority;
|
||||
if (item <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (_losIgnoreList.TryGetValue(item.GameObjectId, out var value2))
|
||||
if (_losIgnoreList.TryGetValue(item2.GameObjectId, out var value2))
|
||||
{
|
||||
if (Environment.TickCount64 < value2)
|
||||
{
|
||||
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;
|
||||
}
|
||||
_losIgnoreList.Remove(item.GameObjectId);
|
||||
_losIgnoreList.Remove(item2.GameObjectId);
|
||||
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);
|
||||
if (flag5)
|
||||
float num3 = Vector3.Distance(item2.Position, value);
|
||||
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);
|
||||
}
|
||||
if (num3 > num || (num3 == num && num4 < num2))
|
||||
{
|
||||
gameObject = item;
|
||||
num = num3;
|
||||
num2 = num4;
|
||||
gameObject = item2;
|
||||
num = item;
|
||||
num2 = num3;
|
||||
}
|
||||
}
|
||||
ulong? num5 = gameObject?.GameObjectId;
|
||||
if (num5 != _lastTargetId)
|
||||
ulong? num4 = gameObject?.GameObjectId;
|
||||
if (num4 != _lastTargetId)
|
||||
{
|
||||
if (gameObject != null)
|
||||
{
|
||||
|
|
@ -408,7 +408,7 @@ internal sealed class CombatController : IDisposable
|
|||
{
|
||||
_logger.LogDebug("No valid target found");
|
||||
}
|
||||
_lastTargetId = num5;
|
||||
_lastTargetId = num4;
|
||||
}
|
||||
if (gameObject != null && num <= 15 && !_condition[ConditionFlag.InCombat])
|
||||
{
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ internal sealed class MovementController : IDisposable
|
|||
}
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1538,6 +1538,10 @@ internal sealed class QuestController : MiniTaskController<QuestController>
|
|||
{
|
||||
return false;
|
||||
}
|
||||
if (_startedQuest != null && QuestFunctions.IsStartingCityOpeningQuest(_startedQuest.Quest.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ElementId elementId = (from x in _questFunctions.GetNextPriorityQuestsThatCanBeAccepted()
|
||||
where x.IsAvailable
|
||||
select x.QuestId).FirstOrDefault();
|
||||
|
|
|
|||
|
|
@ -103,15 +103,10 @@ internal sealed class QuestPriorityResolver
|
|||
QuestResolution valueOrDefault2 = questResolution.GetValueOrDefault();
|
||||
return ApplyQuestRedirects(valueOrDefault2);
|
||||
}
|
||||
uint territoryType = _clientState.TerritoryType;
|
||||
bool flag2 = territoryType - 181 <= 2;
|
||||
if (flag2 && flag && _configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Manual)
|
||||
QuestId activeStartingCityOpeningQuest = _questFunctions.GetActiveStartingCityOpeningQuest();
|
||||
if ((object)activeStartingCityOpeningQuest != null && (_questFunctions.IsQuestAccepted(activeStartingCityOpeningQuest) || (flag && _configuration.General.MsqPriority != Configuration.EMsqPriorityMode.Manual)))
|
||||
{
|
||||
ElementId currentQuest = resolvedMsqQuest.CurrentQuest;
|
||||
if ((object)currentQuest != null && currentQuest.Value > 0 && !_questFunctions.IsQuestAccepted(currentQuest))
|
||||
{
|
||||
return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, currentQuest, resolvedMsqQuest.Sequence, resolvedMsqQuest.State, $"Starting city opening MSQ {currentQuest}");
|
||||
}
|
||||
return QuestResolution.ForQuest(EQuestResolutionType.MsqImmediate, activeStartingCityOpeningQuest, _questFunctions.GetStartingCityOpeningSequence(activeStartingCityOpeningQuest), resolvedMsqQuest.State, $"Starting city opening MSQ {activeStartingCityOpeningQuest}");
|
||||
}
|
||||
questResolution = TryResolveInProgressClassQuest(valueOrDefault, resolvedMsqQuest.State);
|
||||
if (questResolution.HasValue)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -391,7 +391,7 @@ internal sealed class BossModIpc : IDisposable
|
|||
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))
|
||||
{
|
||||
|
|
@ -411,6 +411,10 @@ internal sealed class BossModIpc : IDisposable
|
|||
_logger.LogWarning("Unable to set BossMod preset {Preset}: {Message}", preset, ipcError.Message);
|
||||
return false;
|
||||
}
|
||||
if (!activateAi)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
string aiCommand = GetAiCommand();
|
||||
if (aiCommand != null)
|
||||
{
|
||||
|
|
@ -424,7 +428,7 @@ internal sealed class BossModIpc : IDisposable
|
|||
|
||||
public void EnableQuestBattleAi()
|
||||
{
|
||||
EnableAi(EPreset.Active);
|
||||
EnableAi(EPreset.Active, _configuration.SinglePlayerDuties.EnableBossModAi);
|
||||
ConfigureSoloDutyZone();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,22 @@ namespace Questionable.Functions;
|
|||
|
||||
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>
|
||||
{
|
||||
{
|
||||
|
|
@ -95,6 +111,56 @@ internal sealed class QuestFunctions
|
|||
|
||||
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)
|
||||
{
|
||||
_questRegistry = questRegistry;
|
||||
|
|
|
|||
|
|
@ -5,21 +5,28 @@ using Microsoft.Extensions.Logging;
|
|||
using Questionable.Controller.CustomDelivery;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.Common;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Controller.Steps.Movement;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.Model.Questing;
|
||||
using SmartNav;
|
||||
|
||||
namespace Questionable.Navigation;
|
||||
|
||||
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()
|
||||
{
|
||||
_travelMode = ETravelMode.Auto;
|
||||
reRoutePolicy.ClearDestination();
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +58,7 @@ internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartN
|
|||
if (reRouteDecision is ReRouteDecision.Replan replan)
|
||||
{
|
||||
taskQueue.Reset();
|
||||
foreach (ITask item2 in taskMapper.MapInstructions(replan.Instructions))
|
||||
foreach (ITask item2 in taskMapper.MapInstructions(replan.Instructions, null, _travelMode))
|
||||
{
|
||||
taskQueue.Enqueue(item2);
|
||||
}
|
||||
|
|
@ -79,10 +86,33 @@ internal sealed class SmartNavReRouteService(ReRoutePolicy reRoutePolicy, SmartN
|
|||
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,27 @@ internal sealed class SmartNavRouteEnqueuer(NavRouter navRouter, PlayerNavStateB
|
|||
{
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -120,6 +140,18 @@ internal sealed class SmartNavRouteEnqueuer(NavRouter navRouter, PlayerNavStateB
|
|||
{
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Numerics;
|
|||
using Dalamud.Plugin.Services;
|
||||
using Questionable.Controller.Steps;
|
||||
using Questionable.Controller.Steps.Common;
|
||||
using Questionable.Controller.Steps.Interactions;
|
||||
using Questionable.Controller.Steps.Movement;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.Data;
|
||||
|
|
@ -15,13 +16,22 @@ namespace Questionable.Navigation;
|
|||
|
||||
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++)
|
||||
{
|
||||
foreach (ITask item in Map(instructions[i], NextTravelInstruction(instructions, i), step))
|
||||
{
|
||||
yield return item;
|
||||
if (item is MoveTask task && !(instructions[i] is NavInstruction.Swim))
|
||||
{
|
||||
yield return ApplyTravelMode(task, travelMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,66 +58,131 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
|
|||
{
|
||||
if (!(instruction is NavInstruction.Move move))
|
||||
{
|
||||
if (!(instruction is NavInstruction.Land))
|
||||
if (!(instruction is NavInstruction.Swim swim))
|
||||
{
|
||||
if (!(instruction is NavInstruction.Unmount))
|
||||
if (!(instruction is NavInstruction.DiveEntry dive))
|
||||
{
|
||||
NavInstruction.WaitForTerritory waitForTerritory = instruction as NavInstruction.WaitForTerritory;
|
||||
if ((object)waitForTerritory == null)
|
||||
if (!(instruction is NavInstruction.Surface surface))
|
||||
{
|
||||
if (!(instruction is NavInstruction.WaitForNavmesh))
|
||||
if (!(instruction is NavInstruction.Land))
|
||||
{
|
||||
if (!(instruction is NavInstruction.InteractWarp interactWarp))
|
||||
if (!(instruction is NavInstruction.Unmount))
|
||||
{
|
||||
if (!(instruction is NavInstruction.UseTeleportTicket useTeleportTicket))
|
||||
NavInstruction.WaitForTerritory waitForTerritory = instruction as NavInstruction.WaitForTerritory;
|
||||
if ((object)waitForTerritory == null)
|
||||
{
|
||||
if (!(instruction is NavInstruction.SubRegionTransport subRegionTransport))
|
||||
if (!(instruction is NavInstruction.WaitForNavmesh))
|
||||
{
|
||||
throw new InvalidOperationException($"Unmapped NavInstruction: {instruction}");
|
||||
if (!(instruction is NavInstruction.InteractWarp interactWarp))
|
||||
{
|
||||
if (!(instruction is NavInstruction.UseTeleportTicket useTeleportTicket))
|
||||
{
|
||||
if (!(instruction is NavInstruction.SubRegionTransport subRegionTransport))
|
||||
{
|
||||
throw new InvalidOperationException($"Unmapped NavInstruction: {instruction}");
|
||||
}
|
||||
yield return new SubRegionTransport.Task(subRegionTransport.From, subRegionTransport.To, subRegionTransport.Territory);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new TicketTeleport.Task(useTeleportTicket.ItemId, useTeleportTicket.Territory);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WarpInteract.Task(interactWarp.NpcDataId, interactWarp.WarpRowId, interactWarp.FromTerritory, interactWarp.ToTerritory);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitNavmesh.Task();
|
||||
}
|
||||
yield return new SubRegionTransport.Task(subRegionTransport.From, subRegionTransport.To, subRegionTransport.Territory);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new TicketTeleport.Task(useTeleportTicket.ItemId, useTeleportTicket.Territory);
|
||||
yield return new WaitCondition.Task(() => waitForTerritory.Acceptable.Contains(clientState.TerritoryType), waitForTerritory.Description);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WarpInteract.Task(interactWarp.NpcDataId, interactWarp.WarpRowId, interactWarp.FromTerritory, interactWarp.ToTerritory);
|
||||
yield return new Mount.UnmountTask();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitNavmesh.Task();
|
||||
yield return new LandTask();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new WaitCondition.Task(() => waitForTerritory.Acceptable.Contains(clientState.TerritoryType), waitForTerritory.Description);
|
||||
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 Mount.UnmountTask();
|
||||
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
|
||||
{
|
||||
yield return new LandTask();
|
||||
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)
|
||||
{
|
||||
Vector3 destination = step.Position ?? move.Position;
|
||||
yield return new MoveTask(step, destination)with
|
||||
Vector3 destination4 = step.Position ?? move.Position;
|
||||
MoveTask moveTask = new MoveTask(step, destination4)with
|
||||
{
|
||||
Fly = (move.Fly || step.Fly == true),
|
||||
Land = (move.Fly || step.Land == true),
|
||||
SmartNavRouted = true,
|
||||
RouteTargetNodeId = move.TargetNodeId,
|
||||
RouteApproachNodeId = move.ApproachNodeId
|
||||
Fly = (move.Fly || step.Fly == true)
|
||||
};
|
||||
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)
|
||||
{
|
||||
bool? fly = step.Fly;
|
||||
|
|
@ -116,22 +191,30 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
|
|||
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
|
||||
{
|
||||
NavInstruction.Move move2 = move;
|
||||
ushort territory = move2.Territory;
|
||||
Vector3 position = move2.Position;
|
||||
bool? mount = move2.Mount;
|
||||
float? stopDistance = move2.StopDistance;
|
||||
ushort territory4 = move2.Territory;
|
||||
Vector3 position3 = move2.Position;
|
||||
bool? mount2 = move2.Mount;
|
||||
float? stopDistance2 = move2.StopDistance;
|
||||
uint? dataId = move2.DataId;
|
||||
bool disableNavmesh = move2.DisableNavmesh;
|
||||
bool fly2 = move2.Fly;
|
||||
bool landAtTarget = move2.LandAtTarget;
|
||||
bool flag = move2.Fly;
|
||||
bool flag2 = move2.LandAtTarget;
|
||||
bool allowZoneTransition = move2.AllowZoneTransition;
|
||||
string targetNodeId = move2.TargetNodeId;
|
||||
string approachNodeId = 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);
|
||||
string approachNodeId = move2.TargetNodeId;
|
||||
string targetNodeId = move2.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
|
||||
|
|
@ -147,4 +230,50 @@ internal sealed class SmartNavTaskMapper(IClientState clientState, TerritoryData
|
|||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -317,6 +317,13 @@ internal sealed class SinglePlayerDutyConfigComponent : ConfigComponent
|
|||
}
|
||||
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();
|
||||
int currentItem = (int)base.Configuration.SinglePlayerDuties.RetryDifficulty;
|
||||
ImGui.SetNextItemWidth(MathF.Min(ImGui.GetContentRegionAvail().X * 0.4f, 260f));
|
||||
|
|
|
|||
|
|
@ -338,30 +338,27 @@ internal sealed class QuestSequenceComponent
|
|||
ImGui.Spacing();
|
||||
try
|
||||
{
|
||||
Lumina.Excel.Sheets.Quest? quest = _dataManager.GetExcelSheet<Lumina.Excel.Sheets.Quest>()?.GetRowOrDefault((uint)(questId.Value + 65536));
|
||||
if (!quest.HasValue)
|
||||
if (!(_dataManager.GetExcelSheet<Lumina.Excel.Sheets.Quest>()?.GetRowOrDefault((uint)(questId.Value + 65536))).HasValue)
|
||||
{
|
||||
DrawErrorState("Quest Not Found in Game Data", "Unable to load quest information from Lumina.");
|
||||
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()
|
||||
orderby result
|
||||
select result).ToList();
|
||||
if (list.Count == 0)
|
||||
List<int> completionSequences = GameQuestSequences.GetCompletionSequences(_dataManager, questId);
|
||||
if (completionSequences.Count == 0)
|
||||
{
|
||||
DrawErrorState("No Sequence Data Available", "This quest may not have traditional sequences.");
|
||||
return;
|
||||
}
|
||||
int num8 = list.Max();
|
||||
int num8 = completionSequences.Where((int num13) => num13 != 255).DefaultIfEmpty(0).Max();
|
||||
int num9 = num8 + 1;
|
||||
Questionable.Model.Quest quest2;
|
||||
bool flag = _questRegistry.TryGetQuest(questElementId, out quest2);
|
||||
Questionable.Model.Quest quest;
|
||||
bool flag = _questRegistry.TryGetQuest(questElementId, out quest);
|
||||
HashSet<int> hashSet = new HashSet<int>();
|
||||
Dictionary<int, (int, string)> dictionary = new Dictionary<int, (int, string)>();
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Game.Text;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
|
@ -14,7 +13,6 @@ using Questionable.Model.Questing;
|
|||
|
||||
namespace Questionable;
|
||||
|
||||
[Obfuscation(Feature = "-rename", Exclude = false, ApplyToMembers = true)]
|
||||
internal sealed class Configuration : IPluginConfiguration
|
||||
{
|
||||
internal sealed class GeneralConfiguration
|
||||
|
|
@ -160,6 +158,8 @@ internal sealed class Configuration : IPluginConfiguration
|
|||
{
|
||||
public bool RunSoloInstancesWithBossMod { get; set; }
|
||||
|
||||
public bool EnableBossModAi { get; set; } = true;
|
||||
|
||||
public ERetryDifficulty RetryDifficulty { get; set; }
|
||||
|
||||
public int MaxRetries { get; set; }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -53,7 +52,6 @@ using SmartNav.Data;
|
|||
|
||||
namespace Questionable;
|
||||
|
||||
[Obfuscation(Feature = "-ctrl flow", Exclude = false, ApplyToMembers = true)]
|
||||
public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
|
||||
{
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
|
@ -326,6 +324,8 @@ public sealed class QuestionablePlugin : IAsyncDalamudPlugin, IAsyncDisposable
|
|||
serviceCollection.AddSingleton<TeleportTicketService>();
|
||||
serviceCollection.AddSingleton((IServiceProvider sp) => new LgbZoneBoundarySource(sp.GetRequiredService<ILogger<LgbZoneBoundarySource>>(), sp.GetRequiredService<SmartNavDataOptions>()));
|
||||
((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>()));
|
||||
serviceCollection.AddSingleton<IPlayerContext, QuestionablePlayerContext>();
|
||||
serviceCollection.AddSingleton<CostCalculator>();
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -4,18 +4,18 @@ internal static class PathBundleSecret
|
|||
{
|
||||
private static readonly byte[] A = new byte[32]
|
||||
{
|
||||
192, 237, 68, 154, 123, 161, 164, 81, 179, 101,
|
||||
208, 145, 185, 118, 184, 66, 42, 173, 202, 110,
|
||||
201, 113, 73, 242, 250, 58, 135, 189, 143, 92,
|
||||
151, 77
|
||||
14, 238, 168, 186, 100, 48, 138, 129, 114, 53,
|
||||
75, 217, 99, 224, 198, 248, 103, 129, 59, 33,
|
||||
149, 44, 226, 149, 187, 253, 141, 40, 151, 199,
|
||||
127, 185
|
||||
};
|
||||
|
||||
private static readonly byte[] B = new byte[32]
|
||||
{
|
||||
239, 73, 204, 3, 150, 201, 87, 43, 137, 223,
|
||||
14, 253, 125, 35, 109, 186, 32, 12, 130, 132,
|
||||
201, 118, 255, 168, 253, 104, 107, 98, 213, 28,
|
||||
83, 69
|
||||
87, 34, 127, 49, 238, 14, 221, 143, 224, 157,
|
||||
207, 147, 247, 122, 30, 185, 79, 187, 117, 57,
|
||||
242, 252, 100, 251, 176, 253, 30, 79, 135, 124,
|
||||
73, 92
|
||||
};
|
||||
|
||||
internal static byte[] Key()
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -20,6 +20,7 @@
|
|||
<None Remove="SmartNav.Data.derived-arrivals.bin" />
|
||||
<None Remove="SmartNav.Data.npc-position-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.teleport-tickets.bin" LogicalName="SmartNav.Data.teleport-tickets.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.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.water-cache.bin" LogicalName="SmartNav.Data.water-cache.bin" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="SmartNav.Model">
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
SmartNav.Data/SmartNav.Data.water-cache.bin
Normal file
BIN
SmartNav.Data/SmartNav.Data.water-cache.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
29
SmartNav.Data/SmartNav.Data/AssemblyWaterMapLoader.cs
Normal file
29
SmartNav.Data/SmartNav.Data/AssemblyWaterMapLoader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,18 +4,18 @@ internal static class DataBundleSecret
|
|||
{
|
||||
private static readonly byte[] A = new byte[32]
|
||||
{
|
||||
13, 38, 21, 255, 71, 40, 19, 213, 79, 201,
|
||||
20, 24, 146, 227, 159, 156, 142, 23, 138, 144,
|
||||
119, 132, 179, 149, 190, 134, 23, 209, 204, 192,
|
||||
234, 63
|
||||
71, 215, 20, 29, 12, 252, 253, 194, 59, 69,
|
||||
18, 55, 34, 247, 239, 216, 228, 201, 157, 67,
|
||||
207, 24, 144, 24, 62, 38, 55, 197, 249, 132,
|
||||
62, 40
|
||||
};
|
||||
|
||||
private static readonly byte[] B = new byte[32]
|
||||
{
|
||||
252, 191, 81, 239, 79, 239, 196, 247, 66, 179,
|
||||
73, 171, 175, 76, 172, 241, 89, 23, 110, 4,
|
||||
105, 193, 2, 137, 162, 77, 122, 136, 209, 181,
|
||||
229, 197
|
||||
63, 81, 210, 107, 86, 21, 96, 76, 152, 72,
|
||||
178, 235, 191, 217, 62, 127, 34, 130, 14, 224,
|
||||
171, 239, 111, 52, 2, 7, 1, 25, 82, 57,
|
||||
94, 195
|
||||
};
|
||||
|
||||
internal static byte[] Key()
|
||||
|
|
|
|||
|
|
@ -45,6 +45,24 @@ internal static class SmartNavResources
|
|||
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)
|
||||
{
|
||||
return new MemoryStream(Unseal("SmartNav.Data." + fileName) ?? throw new InvalidOperationException("embedded resource '" + fileName + "' is missing"), writable: false);
|
||||
|
|
|
|||
188
SmartNav.Model/SmartNav.Model.Navigation/TerritoryWaterMap.cs
Normal file
188
SmartNav.Model/SmartNav.Model.Navigation/TerritoryWaterMap.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
122
SmartNav.Model/SmartNav.Model.Navigation/WaterCacheFormat.cs
Normal file
122
SmartNav.Model/SmartNav.Model.Navigation/WaterCacheFormat.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace SmartNav.Model.Navigation;
|
||||
|
||||
public enum WaterCellClass : byte
|
||||
{
|
||||
None,
|
||||
SwimOnly,
|
||||
Divable
|
||||
}
|
||||
|
|
@ -56,9 +56,25 @@ public static class MovementData
|
|||
|
||||
public const float DefaultMountGroundDistance = 20f;
|
||||
|
||||
public const float MinMountTimeSavedSeconds = 2f;
|
||||
|
||||
public const float DefaultMountFlyDistance = 15f;
|
||||
|
||||
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
|
||||
|
|
@ -74,5 +90,9 @@ public static class MovementData
|
|||
public const float SprintDurationInCombat = 10f;
|
||||
|
||||
public const float SprintCooldown = 60f;
|
||||
|
||||
public const float DiveEntryDuration = 4f;
|
||||
|
||||
public const float SurfaceExitDuration = 1f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
46
SmartNav/SmartNav.Data/WaterMapService.cs
Normal file
46
SmartNav/SmartNav.Data/WaterMapService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,53 @@ using SmartNav.Model;
|
|||
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
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);
|
||||
if (!(node == null) && AreNodesConnectedWithinZone(origin, node, origin.TerritoryId))
|
||||
{
|
||||
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(origin.Position, node.Position, origin.TerritoryId, playerState);
|
||||
queryGraph.AddEdge(new NavEdge(origin.Id, item, NavEdgeType.WithinZone, timeCostSeconds, 0));
|
||||
AddWithinZoneEdge(queryGraph, origin, node, origin.TerritoryId, playerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -250,8 +258,7 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
|
|||
NavNode node = queryGraph.GetNode(item);
|
||||
if (!(node == null) && AreNodesConnectedWithinZone(node, destination, destination.TerritoryId))
|
||||
{
|
||||
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, destination.Position, destination.TerritoryId, playerState);
|
||||
queryGraph.AddEdge(new NavEdge(item, destination.Id, NavEdgeType.WithinZone, timeCostSeconds, 0));
|
||||
AddWithinZoneEdge(queryGraph, node, destination, destination.TerritoryId, playerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -348,10 +355,8 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
|
|||
NavNode node = queryGraph.GetNode(item);
|
||||
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);
|
||||
float timeCostSeconds2 = costCalculator.EstimateWithinZoneTravelTime(zoneBoundaryNode.Position, node.Position, 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));
|
||||
AddWithinZoneEdge(queryGraph, node, zoneBoundaryNode, zoneBoundaryNode.TerritoryId, playerState);
|
||||
AddWithinZoneEdge(queryGraph, zoneBoundaryNode, node, zoneBoundaryNode.TerritoryId, playerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -373,10 +378,8 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
|
|||
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))
|
||||
{
|
||||
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, warpNode.Position, warpNode.TerritoryId, playerState);
|
||||
float timeCostSeconds2 = costCalculator.EstimateWithinZoneTravelTime(warpNode.Position, node.Position, 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));
|
||||
AddWithinZoneEdge(queryGraph, node, warpNode, warpNode.TerritoryId, playerState);
|
||||
AddWithinZoneEdge(queryGraph, warpNode, node, warpNode.TerritoryId, playerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -403,10 +406,8 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
|
|||
bool flag = ((node is NavNode.ZoneBoundaryNode || node is NavNode.WarpNode) ? true : false);
|
||||
if (!flag && AreNodesConnectedWithinZone(subRegionConnectorNode, node, subRegionConnectorNode.TerritoryId))
|
||||
{
|
||||
float timeCostSeconds = costCalculator.EstimateWithinZoneTravelTime(node.Position, subRegionConnectorNode.Position, subRegionConnectorNode.TerritoryId, playerState);
|
||||
float timeCostSeconds2 = costCalculator.EstimateWithinZoneTravelTime(subRegionConnectorNode.Position, node.Position, 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));
|
||||
AddWithinZoneEdge(queryGraph, node, subRegionConnectorNode, subRegionConnectorNode.TerritoryId, playerState);
|
||||
AddWithinZoneEdge(queryGraph, subRegionConnectorNode, node, subRegionConnectorNode.TerritoryId, playerState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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 Unmount : NavInstruction;
|
||||
|
|
|
|||
|
|
@ -93,4 +93,42 @@ public sealed class PlayerNavState
|
|||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ public class PlayerNavStateBuilder(IClientState clientState, IObjectTable object
|
|||
return hashSet;
|
||||
}
|
||||
|
||||
private unsafe Dictionary<uint, int> BuildMountSpeedLevels()
|
||||
private Dictionary<uint, int> BuildMountSpeedLevels()
|
||||
{
|
||||
Dictionary<uint, int> dictionary = new Dictionary<uint, int>();
|
||||
ExcelSheet<TerritoryType> excelSheet = dataManager.GetExcelSheet<TerritoryType>();
|
||||
|
|
@ -125,30 +125,51 @@ public class PlayerNavStateBuilder(IClientState clientState, IObjectTable object
|
|||
{
|
||||
return dictionary;
|
||||
}
|
||||
UIState* ptr = UIState.Instance();
|
||||
foreach (TerritoryType item in excelSheet)
|
||||
{
|
||||
if (item.RowId != 0 && item.MountSpeed.RowId != 0)
|
||||
{
|
||||
MountSpeed value = item.MountSpeed.Value;
|
||||
int num = 0;
|
||||
if (value.Quest.RowId != 0 && ptr != null && ptr->IsUnlockLinkUnlockedOrQuestCompleted(value.Quest.RowId, 0))
|
||||
int unlockedMountSpeedLevel = GetUnlockedMountSpeedLevel(item.MountSpeed.Value);
|
||||
if (unlockedMountSpeedLevel > 0)
|
||||
{
|
||||
num++;
|
||||
}
|
||||
if (value.Unknown0 != 0 && ptr != null && ptr->IsUnlockLinkUnlockedOrQuestCompleted(value.Unknown0, 0))
|
||||
{
|
||||
num++;
|
||||
}
|
||||
if (num > 0)
|
||||
{
|
||||
dictionary[item.RowId] = num;
|
||||
dictionary[item.RowId] = unlockedMountSpeedLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
InventoryManager* ptr = InventoryManager.Instance();
|
||||
|
|
|
|||
|
|
@ -13,17 +13,23 @@ public sealed class ReRoutePolicy(NavRouter navRouter, PlayerNavStateBuilder pla
|
|||
|
||||
private bool _hasDestination;
|
||||
|
||||
private bool _allowMount = true;
|
||||
|
||||
private bool _allowFlight = true;
|
||||
|
||||
private int _reRouteCount;
|
||||
|
||||
private readonly HashSet<string> _penalizedTargets = new HashSet<string>();
|
||||
|
||||
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;
|
||||
_destinationPosition = position;
|
||||
_allowMount = allowMount;
|
||||
_allowFlight = allowFlight;
|
||||
_hasDestination = true;
|
||||
_reRouteCount = 0;
|
||||
_penalizedTargets.Clear();
|
||||
|
|
@ -68,6 +74,7 @@ public sealed class ReRoutePolicy(NavRouter navRouter, PlayerNavStateBuilder pla
|
|||
logger.LogWarning("Cannot build PlayerNavState for re-routing");
|
||||
return new ReRouteDecision.GiveUp();
|
||||
}
|
||||
playerNavState = playerNavState.WithTravelCapabilities(_allowMount, _allowFlight);
|
||||
if (playerNavState.CurrentTerritoryId != _destinationTerritoryId && hasPreservedTasks)
|
||||
{
|
||||
_reRouteCount++;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -7,7 +8,7 @@ using SmartNav.Model.Navigation;
|
|||
|
||||
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;
|
||||
|
||||
|
|
@ -17,7 +18,25 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
|
|||
for (int i = 0; i < route.Segments.Count; 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);
|
||||
}
|
||||
|
|
@ -52,6 +71,39 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
|
|||
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)
|
||||
{
|
||||
uint territoryId4;
|
||||
|
|
@ -79,15 +131,21 @@ public sealed class RouteInstructionBuilder(AetheryteData aetheryteData, ITerrit
|
|||
{
|
||||
uint territoryId = seg.From.TerritoryId;
|
||||
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) + ")"));
|
||||
if (!disableNavmesh)
|
||||
{
|
||||
instructions.Add(new NavInstruction.WaitForNavmesh());
|
||||
}
|
||||
uint? num = ((!(seg.To is NavNode.WarpNode warpNode)) ? ((uint?)null) : warpNode.NpcDataId);
|
||||
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));
|
||||
if (waterPlan == null)
|
||||
{
|
||||
instructions.Add(BuildWithinZoneMove(seg, territoryId, flag));
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitWaterLegs(instructions, waterPlan, seg, territoryId, flag);
|
||||
}
|
||||
break;
|
||||
}
|
||||
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, 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));
|
||||
goto IL_04b4;
|
||||
goto IL_04bb;
|
||||
}
|
||||
case NavEdgeType.Warp:
|
||||
{
|
||||
uint territoryId5 = seg.From.TerritoryId;
|
||||
uint territoryId6 = seg.To.TerritoryId;
|
||||
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;
|
||||
}
|
||||
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);
|
||||
break;
|
||||
}
|
||||
IL_04b4:
|
||||
IL_04bb:
|
||||
instructions.Add(new NavInstruction.WaitForTerritory(new global::_003C_003Ez__ReadOnlySingleElementList<uint>(territoryId4), "Wait(territory: " + territoryInfo.GetNameAndId(territoryId4) + ")"));
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
5
SmartNav/SmartNav/WaterPlan.cs
Normal file
5
SmartNav/SmartNav/WaterPlan.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace SmartNav;
|
||||
|
||||
public sealed record WaterPlan(WaterPlanKind Kind, Vector3? SurfacePoint, Vector3? EntryPoint, float? GoalSurfaceY);
|
||||
9
SmartNav/SmartNav/WaterPlanKind.cs
Normal file
9
SmartNav/SmartNav/WaterPlanKind.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace SmartNav;
|
||||
|
||||
public enum WaterPlanKind
|
||||
{
|
||||
Submerged,
|
||||
DiveIn,
|
||||
SurfaceOut,
|
||||
SurfaceOutDiveIn
|
||||
}
|
||||
66
SmartNav/SmartNav/WaterRoutePlanner.cs
Normal file
66
SmartNav/SmartNav/WaterRoutePlanner.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue