240 lines
8.6 KiB
C#
240 lines
8.6 KiB
C#
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 Microsoft.Extensions.Logging;
|
|
using SmartNav.Model.Navigation;
|
|
|
|
namespace SmartNav.Data;
|
|
|
|
public sealed class LgbZoneBoundarySource : IZoneBoundarySource
|
|
{
|
|
private const uint BoundaryFileMagic = 1112425562u;
|
|
|
|
private const int BoundaryFileFormatVersion = 1;
|
|
|
|
private const string CacheFileName = "zone-boundary-cache.bin";
|
|
|
|
private readonly IDataManager _dataManager;
|
|
|
|
private readonly ILogger<LgbZoneBoundarySource> _logger;
|
|
|
|
private readonly Lazy<IReadOnlyList<ZoneBoundary>> _boundaries;
|
|
|
|
private readonly string? _cacheDirectory;
|
|
|
|
private readonly string? _gameVersion;
|
|
|
|
public LgbZoneBoundarySource(IDataManager dataManager, ILogger<LgbZoneBoundarySource> logger, SmartNavDataOptions? dataOptions = null)
|
|
{
|
|
_dataManager = dataManager;
|
|
_logger = logger;
|
|
_cacheDirectory = dataOptions?.UserOverrideDirectory?.FullName;
|
|
_gameVersion = dataOptions?.GameVersion;
|
|
_boundaries = new Lazy<IReadOnlyList<ZoneBoundary>>(Build, LazyThreadSafetyMode.ExecutionAndPublication);
|
|
}
|
|
|
|
public IReadOnlyList<ZoneBoundary> GetBoundaries()
|
|
{
|
|
return _boundaries.Value;
|
|
}
|
|
|
|
private IReadOnlyList<ZoneBoundary> Build()
|
|
{
|
|
try
|
|
{
|
|
(List<ExitRangeEntry>, Dictionary<(ushort, uint), PopRangeEntry>)? tuple = TryLoadFromDisk();
|
|
if (!tuple.HasValue)
|
|
{
|
|
_logger.LogInformation("No boundary cache found, falling back to in-process LGB scan");
|
|
}
|
|
(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);
|
|
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)" : "");
|
|
return list;
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Zone boundary derivation failed - building graph without boundary edges");
|
|
return Array.Empty<ZoneBoundary>();
|
|
}
|
|
}
|
|
|
|
private (List<ExitRangeEntry> ExitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> PopRangeIndex)? TryLoadFromDisk()
|
|
{
|
|
if (string.IsNullOrEmpty(_cacheDirectory) || string.IsNullOrEmpty(_gameVersion))
|
|
{
|
|
return null;
|
|
}
|
|
string path = Path.Combine(_cacheDirectory, "zone-boundary-cache.bin");
|
|
if (!File.Exists(path))
|
|
{
|
|
return null;
|
|
}
|
|
try
|
|
{
|
|
using BinaryReader binaryReader = new BinaryReader(File.OpenRead(path));
|
|
if (binaryReader.ReadUInt32() != 1112425562 || binaryReader.ReadInt32() != 1)
|
|
{
|
|
_logger.LogDebug("Zone boundary cache has unknown header, discarding");
|
|
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);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to load zone boundary cache, falling back to LGB scan");
|
|
}
|
|
try
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private (List<ExitRangeEntry> ExitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> PopRangeIndex) CollectLgbData()
|
|
{
|
|
List<ExitRangeEntry> list = new List<ExitRangeEntry>();
|
|
Dictionary<(ushort, uint), PopRangeEntry> dictionary = new Dictionary<(ushort, uint), PopRangeEntry>();
|
|
HashSet<uint> hashSet = new HashSet<uint>();
|
|
foreach (Aetheryte item in _dataManager.GetExcelSheet<Aetheryte>())
|
|
{
|
|
hashSet.Add(item.Territory.RowId);
|
|
}
|
|
ExcelSheet<TerritoryType> excelSheet = _dataManager.GetExcelSheet<TerritoryType>();
|
|
HashSet<string> visitedLgbPaths = new HashSet<string>();
|
|
bool cacheFileResources = _dataManager.GameData.Options.CacheFileResources;
|
|
_dataManager.GameData.Options.CacheFileResources = false;
|
|
try
|
|
{
|
|
foreach (TerritoryType item2 in excelSheet)
|
|
{
|
|
if (hashSet.Contains(item2.RowId))
|
|
{
|
|
ScanTerritory(list, dictionary, visitedLgbPaths, item2);
|
|
}
|
|
}
|
|
foreach (TerritoryType item3 in excelSheet)
|
|
{
|
|
ScanTerritory(list, dictionary, visitedLgbPaths, item3);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_dataManager.GameData.Options.CacheFileResources = cacheFileResources;
|
|
}
|
|
return (ExitRanges: list, PopRangeIndex: dictionary);
|
|
}
|
|
|
|
private static bool IsNavigableOverworld(ETerritoryIntendedUse use)
|
|
{
|
|
switch (use)
|
|
{
|
|
case ETerritoryIntendedUse.Town:
|
|
case ETerritoryIntendedUse.Overworld:
|
|
case ETerritoryIntendedUse.OpeningArea:
|
|
case ETerritoryIntendedUse.HousingOutdoor:
|
|
case ETerritoryIntendedUse.Firmament:
|
|
case ETerritoryIntendedUse.SanctumOfTheTwelve:
|
|
case ETerritoryIntendedUse.GoldSaucer:
|
|
case ETerritoryIntendedUse.Eureka:
|
|
case ETerritoryIntendedUse.Bozja:
|
|
case ETerritoryIntendedUse.IslandSanctuary:
|
|
case ETerritoryIntendedUse.CosmicExploration:
|
|
case ETerritoryIntendedUse.OccultCrescent:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void ScanTerritory(List<ExitRangeEntry> exitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> popRangeIndex, HashSet<string> visitedLgbPaths, TerritoryType territory)
|
|
{
|
|
if (territory.RowId == 0 || !IsNavigableOverworld((ETerritoryIntendedUse)territory.TerritoryIntendedUse.RowId))
|
|
{
|
|
return;
|
|
}
|
|
string text = territory.Bg.ExtractText();
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return;
|
|
}
|
|
int num = text.IndexOf("/level/", StringComparison.Ordinal);
|
|
if (num < 0)
|
|
{
|
|
return;
|
|
}
|
|
string text2 = "bg/" + text.Substring(0, num + 1) + "level/planmap.lgb";
|
|
if (!visitedLgbPaths.Add(text2))
|
|
{
|
|
return;
|
|
}
|
|
ushort num2 = (ushort)territory.RowId;
|
|
LgbFile file;
|
|
try
|
|
{
|
|
file = _dataManager.GetFile<LgbFile>(text2);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogTrace(exception, "Failed to load {Path}", text2);
|
|
return;
|
|
}
|
|
if (file == null)
|
|
{
|
|
return;
|
|
}
|
|
LayerCommon.Layer[] layers = file.Layers;
|
|
for (int i = 0; i < layers.Length; i++)
|
|
{
|
|
LayerCommon.InstanceObject[] instanceObjects = layers[i].InstanceObjects;
|
|
for (int j = 0; j < instanceObjects.Length; j++)
|
|
{
|
|
LayerCommon.InstanceObject instanceObject = instanceObjects[j];
|
|
if (instanceObject.AssetType == LayerEntryType.ExitRange)
|
|
{
|
|
LayerCommon.ExitRangeInstanceObject exitRangeInstanceObject = (LayerCommon.ExitRangeInstanceObject)(object)instanceObject.Object;
|
|
exitRanges.Add(new ExitRangeEntry(num2, instanceObject.InstanceId, new Vector3(instanceObject.Transform.Translation.X, instanceObject.Transform.Translation.Y, instanceObject.Transform.Translation.Z), exitRangeInstanceObject.TerritoryType, exitRangeInstanceObject.DestInstanceId, exitRangeInstanceObject.ReturnInstanceId, new Vector3(instanceObject.Transform.Rotation.X, instanceObject.Transform.Rotation.Y, instanceObject.Transform.Rotation.Z), new Vector3(instanceObject.Transform.Scale.X, instanceObject.Transform.Scale.Y, instanceObject.Transform.Scale.Z)));
|
|
}
|
|
else if (instanceObject.AssetType == LayerEntryType.PopRange)
|
|
{
|
|
popRangeIndex[(num2, instanceObject.InstanceId)] = new PopRangeEntry(num2, instanceObject.InstanceId, new Vector3(instanceObject.Transform.Translation.X, instanceObject.Transform.Translation.Y, instanceObject.Transform.Translation.Z));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|