qstbak/LLib/LLib.Shop/NpcPositionCache.cs
2026-08-19 13:19:57 +10:00

390 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Text;
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 const int MaxPreallocatedEntries = 131072;
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 bool TryLoadFromStream(Stream stream, bool acceptStaleVersion)
{
ArgumentNullException.ThrowIfNull(stream, "stream");
if (_cache != null)
{
return true;
}
lock (_buildLock)
{
if (_cache == null)
{
_cache = ReadCache(stream, acceptStaleVersion, "stream");
}
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;
}
Dictionary<uint, (ushort, Vector3, bool)> dictionary;
try
{
using FileStream stream = File.OpenRead(cacheFilePath);
dictionary = ReadCache(stream, acceptStaleVersion: false, "disk");
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to open NPC position cache file, discarding");
dictionary = null;
}
if (dictionary != null)
{
return dictionary;
}
try
{
File.Delete(cacheFilePath);
}
catch (Exception)
{
}
return null;
}
private Dictionary<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)>? ReadCache(Stream stream, bool acceptStaleVersion, string source)
{
try
{
using BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (binaryReader.ReadUInt32() != 1129336401 || binaryReader.ReadInt32() != 1)
{
_logger.LogDebug("NPC position cache data has an unknown header, discarding");
return null;
}
string text = binaryReader.ReadString();
if (text != _options.GameVersion)
{
if (!acceptStaleVersion)
{
_logger.LogDebug("NPC position cache data is for a different game version, discarding");
return null;
}
_logger.LogInformation("NPC position cache data was built for game version {CachedVersion} instead of {ExpectedVersion}, using it anyway", text, _options.GameVersion);
}
int num = binaryReader.ReadInt32();
if (num < 0)
{
_logger.LogDebug("NPC position cache data has a negative entry count, discarding");
return null;
}
Dictionary<uint, (ushort, Vector3, bool)> dictionary = new Dictionary<uint, (ushort, Vector3, bool)>(Math.Min(num, 131072));
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 {Source} with {Count} entries", source, dictionary.Count);
return dictionary;
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to read NPC position cache data, discarding");
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);
}
}
}