muffin v7.5.12

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

View file

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

View file

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

View file

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

View file

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