muffin v7.5.13

This commit is contained in:
alydev 2026-08-22 10:25:22 +10:00
parent 911ed68baa
commit acf3e3cb18
69 changed files with 2731 additions and 1425 deletions

View file

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

View file

@ -4,9 +4,53 @@ using SmartNav.Model;
namespace SmartNav;
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))
{

View file

@ -225,6 +225,15 @@ public sealed class NavGraphView(CostCalculator costCalculator, ITerritoryInfo t
}
}
private void AddWithinZoneEdge(NavGraph queryGraph, NavNode from, NavNode to, uint territoryId, PlayerNavState playerState)
{
float num = costCalculator.EstimateWithinZoneTravelTime(from.Position, to.Position, territoryId, playerState);
if (float.IsFinite(num))
{
queryGraph.AddEdge(new NavEdge(from.Id, to.Id, NavEdgeType.WithinZone, num, 0));
}
}
private void AddWithinZoneEdgesFrom(NavGraph queryGraph, NavNode origin, PlayerNavState playerState)
{
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);
}
}
}

View file

@ -12,6 +12,12 @@ public abstract record NavInstruction
public sealed record Move(ushort Territory, Vector3 Position, bool Fly, bool LandAtTarget, float? StopDistance, uint? DataId, bool? Mount, bool DisableNavmesh, bool AllowZoneTransition, string? TargetNodeId, string? ApproachNodeId, bool IsFinal) : NavInstruction();
public sealed record 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;

View file

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

View file

@ -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();

View file

@ -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++;

View file

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

View file

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

View file

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

View file

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