341 lines
9.6 KiB
C#
341 lines
9.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Numerics;
|
|
using System.Threading;
|
|
using Dalamud.Plugin.Services;
|
|
using Lumina.Data.Files;
|
|
using Lumina.Data.Parsing.Layer;
|
|
using Lumina.Excel;
|
|
using Lumina.Excel.Sheets;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LLib.Shop;
|
|
|
|
public sealed class NpcPositionCache
|
|
{
|
|
private static readonly string[] LgbFileNames = new string[4] { "planevent", "planmap", "planner", "planlive" };
|
|
|
|
private const uint FileMagic = 1129336401u;
|
|
|
|
private const int FileFormatVersion = 1;
|
|
|
|
private const string FileName = "npc-position-cache.bin";
|
|
|
|
private readonly IDataManager _dataManager;
|
|
|
|
private readonly ILogger<NpcPositionCache> _logger;
|
|
|
|
private readonly NpcPositionCacheOptions _options;
|
|
|
|
private readonly object _buildLock = new object();
|
|
|
|
private volatile Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)>? _cache;
|
|
|
|
public bool IsBuilt => _cache != null;
|
|
|
|
private string? CacheFilePath
|
|
{
|
|
get
|
|
{
|
|
if (string.IsNullOrEmpty(_options.CacheDirectory) || string.IsNullOrEmpty(_options.GameVersion))
|
|
{
|
|
return null;
|
|
}
|
|
return Path.Combine(_options.CacheDirectory, "npc-position-cache.bin");
|
|
}
|
|
}
|
|
|
|
public NpcPositionCache(IDataManager dataManager, ILogger<NpcPositionCache> logger, NpcPositionCacheOptions? options = null)
|
|
{
|
|
_dataManager = dataManager;
|
|
_logger = logger;
|
|
_options = options ?? new NpcPositionCacheOptions();
|
|
}
|
|
|
|
public (ushort TerritoryId, Vector3 Position)? TryGetPosition(uint npcId)
|
|
{
|
|
Dictionary<uint, (ushort, Vector3, bool)> cache = _cache;
|
|
if (cache == null || !cache.TryGetValue(npcId, out var value))
|
|
{
|
|
return null;
|
|
}
|
|
return (value.Item1, value.Item2);
|
|
}
|
|
|
|
public (ushort TerritoryId, Vector3 Position, bool FestivalOnly)? TryGetPlacement(uint npcId)
|
|
{
|
|
Dictionary<uint, (ushort, Vector3, bool)> cache = _cache;
|
|
if (cache == null || !cache.TryGetValue(npcId, out var value))
|
|
{
|
|
return null;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public bool TryLoadFromDisk()
|
|
{
|
|
if (_cache != null)
|
|
{
|
|
return true;
|
|
}
|
|
lock (_buildLock)
|
|
{
|
|
if (_cache == null)
|
|
{
|
|
_cache = LoadFromDisk();
|
|
}
|
|
return _cache != null;
|
|
}
|
|
}
|
|
|
|
public void WarmUp(CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
EnsureBuilt(cancellationToken);
|
|
}
|
|
|
|
public (ushort TerritoryId, Vector3 Position)? GetPosition(uint npcId)
|
|
{
|
|
EnsureBuilt();
|
|
if (!_cache.TryGetValue(npcId, out (ushort, Vector3, bool) value))
|
|
{
|
|
return null;
|
|
}
|
|
return (value.Item1, value.Item2);
|
|
}
|
|
|
|
public (ushort TerritoryId, Vector3 Position, bool FestivalOnly)? GetPlacement(uint npcId)
|
|
{
|
|
EnsureBuilt();
|
|
if (!_cache.TryGetValue(npcId, out (ushort, Vector3, bool) value))
|
|
{
|
|
return null;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public IReadOnlyCollection<uint> GetAllNpcIds()
|
|
{
|
|
EnsureBuilt();
|
|
return _cache.Keys;
|
|
}
|
|
|
|
private void EnsureBuilt(CancellationToken cancellationToken = default(CancellationToken))
|
|
{
|
|
if (_cache != null)
|
|
{
|
|
return;
|
|
}
|
|
lock (_buildLock)
|
|
{
|
|
if (_cache == null)
|
|
{
|
|
Dictionary<uint, (ushort, Vector3, bool)> dictionary = LoadFromDisk();
|
|
if (dictionary == null)
|
|
{
|
|
dictionary = BuildNpcPositionCache(cancellationToken);
|
|
SaveToDisk(dictionary);
|
|
_logger.LogDebug("NPC position cache built with {Count} entries", dictionary.Count);
|
|
}
|
|
_cache = dictionary;
|
|
}
|
|
}
|
|
}
|
|
|
|
private Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)>? LoadFromDisk()
|
|
{
|
|
string cacheFilePath = CacheFilePath;
|
|
if (cacheFilePath == null || !File.Exists(cacheFilePath))
|
|
{
|
|
return null;
|
|
}
|
|
try
|
|
{
|
|
using BinaryReader binaryReader = new BinaryReader(File.OpenRead(cacheFilePath));
|
|
if (binaryReader.ReadUInt32() != 1129336401 || binaryReader.ReadInt32() != 1)
|
|
{
|
|
_logger.LogDebug("NPC position cache file has an unknown header, discarding");
|
|
}
|
|
else
|
|
{
|
|
if (!(binaryReader.ReadString() != _options.GameVersion))
|
|
{
|
|
int num = binaryReader.ReadInt32();
|
|
Dictionary<uint, (ushort, Vector3, bool)> dictionary = new Dictionary<uint, (ushort, Vector3, bool)>(num);
|
|
for (int i = 0; i < num; i++)
|
|
{
|
|
uint key = binaryReader.ReadUInt32();
|
|
ushort item = binaryReader.ReadUInt16();
|
|
Vector3 item2 = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
|
|
bool item3 = binaryReader.ReadBoolean();
|
|
dictionary[key] = (item, item2, item3);
|
|
}
|
|
_logger.LogDebug("NPC position cache loaded from disk with {Count} entries", dictionary.Count);
|
|
return dictionary;
|
|
}
|
|
_logger.LogDebug("NPC position cache file is for a different game version, discarding");
|
|
}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to load NPC position cache file, discarding");
|
|
}
|
|
try
|
|
{
|
|
File.Delete(cacheFilePath);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void SaveToDisk(Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)> cache)
|
|
{
|
|
string cacheFilePath = CacheFilePath;
|
|
if (cacheFilePath == null)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
Directory.CreateDirectory(_options.CacheDirectory);
|
|
string text = cacheFilePath + ".tmp";
|
|
using (BinaryWriter binaryWriter = new BinaryWriter(File.Create(text)))
|
|
{
|
|
binaryWriter.Write(1129336401u);
|
|
binaryWriter.Write(1);
|
|
binaryWriter.Write(_options.GameVersion);
|
|
binaryWriter.Write(cache.Count);
|
|
foreach (var (value, tuple2) in cache)
|
|
{
|
|
binaryWriter.Write(value);
|
|
binaryWriter.Write(tuple2.Item1);
|
|
binaryWriter.Write(tuple2.Item2.X);
|
|
binaryWriter.Write(tuple2.Item2.Y);
|
|
binaryWriter.Write(tuple2.Item2.Z);
|
|
binaryWriter.Write(tuple2.Item3);
|
|
}
|
|
}
|
|
File.Move(text, cacheFilePath, overwrite: true);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to save NPC position cache file");
|
|
}
|
|
}
|
|
|
|
private Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)> BuildNpcPositionCache(CancellationToken cancellationToken)
|
|
{
|
|
Dictionary<uint, (ushort, Vector3, bool)> dictionary = new Dictionary<uint, (ushort, Vector3, bool)>();
|
|
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))
|
|
{
|
|
ScanLgbFiles(dictionary, visitedLgbPaths, item2, cancellationToken);
|
|
}
|
|
}
|
|
foreach (TerritoryType item3 in excelSheet)
|
|
{
|
|
ScanLgbFiles(dictionary, visitedLgbPaths, item3, cancellationToken);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_dataManager.GameData.Options.CacheFileResources = cacheFileResources;
|
|
}
|
|
ExcelSheet<ENpcBase> excelSheet2 = _dataManager.GetExcelSheet<ENpcBase>();
|
|
ExcelSheet<EObj> excelSheet3 = _dataManager.GetExcelSheet<EObj>();
|
|
foreach (Level item4 in _dataManager.GetExcelSheet<Level>())
|
|
{
|
|
if (item4.RowId != 0 && item4.Object.RowId != 0 && item4.Territory.IsValid && (excelSheet2.HasRow(item4.Object.RowId) || excelSheet3.HasRow(item4.Object.RowId)))
|
|
{
|
|
dictionary.TryAdd(item4.Object.RowId, ((ushort)item4.Territory.RowId, new Vector3(item4.X, item4.Y, item4.Z), false));
|
|
}
|
|
}
|
|
return dictionary;
|
|
}
|
|
|
|
private void ScanLgbFiles(Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)> cache, HashSet<string> visitedLgbPaths, TerritoryType territory, CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
string text = territory.Bg.ExtractText();
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return;
|
|
}
|
|
int num = text.IndexOf("/level/", StringComparison.Ordinal);
|
|
if (num < 0)
|
|
{
|
|
return;
|
|
}
|
|
int num2 = 0;
|
|
string[] lgbFileNames = LgbFileNames;
|
|
foreach (string text2 in lgbFileNames)
|
|
{
|
|
string text3 = "bg/" + text.Substring(0, num + 1) + "level/" + text2 + ".lgb";
|
|
if (!visitedLgbPaths.Add(text3))
|
|
{
|
|
continue;
|
|
}
|
|
LgbFile file;
|
|
try
|
|
{
|
|
file = _dataManager.GetFile<LgbFile>(text3);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogTrace(exception, "Failed to load LGB file {Path}", text3);
|
|
continue;
|
|
}
|
|
if (file == null)
|
|
{
|
|
continue;
|
|
}
|
|
num2++;
|
|
LayerCommon.Layer[] layers = file.Layers;
|
|
for (int j = 0; j < layers.Length; j++)
|
|
{
|
|
LayerCommon.Layer layer = layers[j];
|
|
bool flag = layer.FestivalID != 0 || layer.IsTemporary != 0;
|
|
LayerCommon.InstanceObject[] instanceObjects = layer.InstanceObjects;
|
|
for (int k = 0; k < instanceObjects.Length; k++)
|
|
{
|
|
LayerCommon.InstanceObject instanceObject = instanceObjects[k];
|
|
uint baseId;
|
|
if (instanceObject.AssetType == LayerEntryType.EventNPC)
|
|
{
|
|
baseId = ((LayerCommon.ENPCInstanceObject)instanceObject.Object).ParentData.ParentData.BaseId;
|
|
}
|
|
else
|
|
{
|
|
if (instanceObject.AssetType != LayerEntryType.EventObject)
|
|
{
|
|
continue;
|
|
}
|
|
baseId = ((LayerCommon.EventInstanceObject)instanceObject.Object).ParentData.BaseId;
|
|
}
|
|
if (baseId != 0 && (!cache.TryGetValue(baseId, out (ushort, Vector3, bool) value) || !(!value.Item3 || flag)))
|
|
{
|
|
cache[baseId] = ((ushort)territory.RowId, new Vector3(instanceObject.Transform.Translation.X, instanceObject.Transform.Translation.Y, instanceObject.Transform.Translation.Z), flag);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (num2 > 0 && _options.ScanThrottle > TimeSpan.Zero)
|
|
{
|
|
Thread.Sleep(_options.ScanThrottle);
|
|
}
|
|
}
|
|
}
|