using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using System.Text.Json; using Dalamud.Plugin.Services; using LLib.GameData; using LLib.Shop; using Lumina.Excel; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using SmartNav.Model.Navigation; namespace SmartNav.Data; public sealed class WarpDataService { private sealed record Snapshot(IReadOnlyList Warps, IReadOnlyDictionary WarpsByRowId, IReadOnlySet SkippedWarpIds, int TotalLuminaWarps, int NavigationRelevantWarps, int WarpsWithDestination, int WarpsWithUnknownSource, int WarpsWithDerivedSource, int WarpsWithDerivedArrival, int SkippedNoDest, int SkippedSourceBlocked, int SkippedDestBlocked, bool DerivedSourcesPending); public sealed record WarpInfo(uint RowId, string? Name, string? Question, ushort SourceTerritoryId, Vector3 SourcePosition, uint? NpcDataId, string? NpcName, ushort DestTerritoryId, Vector3? DestPosition, int GilCost, uint[] RequiredQuestIds, uint[]? AvailableDuringMsqIds = null, ushort RequiredClassLevel = 0, bool DerivedSource = false, bool RequireAllQuests = true, bool DerivedArrival = false); private const uint WarpHandlerType = 2u; private const uint ArrayEventHandlerType = 13u; private static readonly HashSet NonNavigableIntendedUses = new HashSet { ETerritoryIntendedUse.Inn, ETerritoryIntendedUse.Dungeon, ETerritoryIntendedUse.VariantDungeon, ETerritoryIntendedUse.MordionGaol, ETerritoryIntendedUse.BeforeTrialDung, ETerritoryIntendedUse.AllianceRaid, ETerritoryIntendedUse.PreEwOverworldQuestBattle, ETerritoryIntendedUse.Trial, ETerritoryIntendedUse.WaitingRoom, ETerritoryIntendedUse.HousingIndoor, ETerritoryIntendedUse.SoloOverworldInstances, ETerritoryIntendedUse.Raid1, ETerritoryIntendedUse.Raid2, ETerritoryIntendedUse.Frontline, ETerritoryIntendedUse.LordOfVerminion, ETerritoryIntendedUse.ExploratoryMissions, ETerritoryIntendedUse.HallOfTheNovice, ETerritoryIntendedUse.CrystallineConflict, ETerritoryIntendedUse.SoloDuty, ETerritoryIntendedUse.GrandCompanyBarracks, ETerritoryIntendedUse.DeepDungeon, ETerritoryIntendedUse.TreasureMapInstance, ETerritoryIntendedUse.SeasonalInstancedArea, ETerritoryIntendedUse.TripleTriadBattlehall, ETerritoryIntendedUse.CrystallineConflictCustomMatch, ETerritoryIntendedUse.DiademHuntingGrounds, ETerritoryIntendedUse.RivalWings, ETerritoryIntendedUse.LeapOfFaith, ETerritoryIntendedUse.MaskedCarnival, ETerritoryIntendedUse.OceanFishing, ETerritoryIntendedUse.Diadem, ETerritoryIntendedUse.TripleTriadOpenTournament, ETerritoryIntendedUse.TripleTriadInvitationalParlor, ETerritoryIntendedUse.DelubrumReginae, ETerritoryIntendedUse.DelubrumReginaeSavage, ETerritoryIntendedUse.EndwalkerMsqSoloOverworld, ETerritoryIntendedUse.Elysion, ETerritoryIntendedUse.CriterionDungeon, ETerritoryIntendedUse.CriterionDungeonSavage, ETerritoryIntendedUse.Blunderville }; private readonly IDataManager? _dataManager; private readonly SmartNavDataOptions? _dataOptions; private readonly NpcPositionCache? _npcPositionCache; private readonly ILogger? _logger; private readonly object _applyLock = new object(); private volatile Snapshot _current; public IReadOnlyList Warps => _current.Warps; public IReadOnlyDictionary WarpsByRowId => _current.WarpsByRowId; public IReadOnlySet SkippedWarpIds => _current.SkippedWarpIds; public int TotalLuminaWarps => _current.TotalLuminaWarps; public int NavigationRelevantWarps => _current.NavigationRelevantWarps; public int WarpsWithDestination => _current.WarpsWithDestination; public int WarpsWithUnknownSource => _current.WarpsWithUnknownSource; public int WarpsWithDerivedSource => _current.WarpsWithDerivedSource; public int WarpsWithDerivedArrival => _current.WarpsWithDerivedArrival; public int SkippedNoDest => _current.SkippedNoDest; public int SkippedSourceBlocked => _current.SkippedSourceBlocked; public int SkippedDestBlocked => _current.SkippedDestBlocked; public bool DerivedSourcesPending => _current.DerivedSourcesPending; internal WarpDataService(List warps, HashSet? skippedWarpIds = null) { Dictionary dictionary = new Dictionary(); foreach (WarpInfo warp in warps) { dictionary.TryAdd(warp.RowId, warp); } _current = new Snapshot(warps, dictionary, skippedWarpIds ?? new HashSet(), 0, warps.Count, 0, 0, warps.Count((WarpInfo w) => w.DerivedSource), warps.Count((WarpInfo w) => w.DerivedArrival), 0, 0, 0, DerivedSourcesPending: false); } public WarpDataService(IDataManager dataManager, SmartNavDataOptions dataOptions, NpcPositionCache npcPositionCache, ILogger logger) { _dataManager = dataManager; _dataOptions = dataOptions; _npcPositionCache = npcPositionCache; _logger = logger; _current = BuildSnapshot(); } public bool ApplyDerivedSources() { if (_npcPositionCache == null) { return false; } lock (_applyLock) { if (!_current.DerivedSourcesPending || !_npcPositionCache.IsBuilt) { return false; } _current = BuildSnapshot(); return true; } } private Snapshot BuildSnapshot() { IDataManager dataManager = _dataManager; SmartNavDataOptions dataOptions = _dataOptions; NpcPositionCache npcPositionCache = _npcPositionCache; ILogger logger = _logger; bool isBuilt = npcPositionCache.IsBuilt; Func getPlacement = (isBuilt ? new Func(npcPositionCache.TryGetPlacement) : ((Func)((uint _) => ((ushort TerritoryId, Vector3 Position, bool FestivalOnly)?)null))); List list = new List(); HashSet hashSet = new HashSet(); var (readOnlyList, readOnlyDictionary) = AssemblyWarpDestinationLoader.GetOverrides(dataOptions.DevSourceDirectory?.FullName); foreach (uint item4 in readOnlyList) { hashSet.Add(item4); } WarpDestinationEntry value; foreach (KeyValuePair item5 in readOnlyDictionary) { item5.Deconstruct(out var _, out value); WarpDestinationEntry item = value; list.Add(item); } if (dataOptions.UserOverrideDirectory != null) { LoadWarpOverridesFrom(Path.Combine(dataOptions.UserOverrideDirectory.FullName, "warp-destinations.json"), list, hashSet); } Dictionary<(uint, uint), WarpDestinationEntry> dictionary = new Dictionary<(uint, uint), WarpDestinationEntry>(); Dictionary dictionary2 = new Dictionary(); foreach (WarpDestinationEntry item6 in list) { uint? npcDataId = item6.NpcDataId; if (npcDataId.HasValue) { uint valueOrDefault = npcDataId.GetValueOrDefault(); dictionary[(item6.WarpRowId, valueOrDefault)] = item6; } else { dictionary2[item6.WarpRowId] = item6; } } HashSet hashSet2 = new HashSet(); foreach (WarpDestinationEntry item7 in list) { if (item7.SourceTerritoryId > 0) { hashSet2.Add(item7.SourceTerritoryId); } if (item7.DestTerritoryId > 0) { hashSet2.Add(item7.DestTerritoryId); } } Dictionary dictionary3 = (from x in dataManager.GetExcelSheet() where x.RowId != 0 select x).ToDictionary((TerritoryType x) => (ushort)x.RowId, (TerritoryType x) => (ETerritoryIntendedUse)x.TerritoryIntendedUse.RowId); Dictionary> dictionary4 = new Dictionary>(); int nestedArraysSkipped = 0; ExcelSheet arrayEventHandlers = dataManager.GetExcelSheet(); Func> arrayMemberLookup = delegate(uint handlerId) { ArrayEventHandler? rowOrDefault = arrayEventHandlers.GetRowOrDefault(handlerId); return (!rowOrDefault.HasValue) ? null : (from x in rowOrDefault.GetValueOrDefault().Data where x.RowId != 0 select x.RowId).ToList(); }; foreach (ENpcBase item8 in dataManager.GetExcelSheet()) { foreach (RowRef eNpcDatum in item8.ENpcData) { AddWarpFronter(dictionary4, eNpcDatum.RowId, item8.RowId, arrayMemberLookup, ref nestedArraysSkipped); } } foreach (EObj item9 in dataManager.GetExcelSheet()) { AddWarpFronter(dictionary4, item9.Data.RowId, item9.RowId, arrayMemberLookup, ref nestedArraysSkipped); } if (nestedArraysSkipped > 0) { logger.LogWarning("Warp fronter index: {Count} nested ArrayEventHandler refs skipped (hop depth capped at 1)", nestedArraysSkipped); } Dictionary dictionary5 = new Dictionary(); Dictionary dictionary6 = new Dictionary(); Dictionary> dictionary7 = new Dictionary>(); int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; (uint, uint) key2; foreach (Warp item10 in from x in dataManager.GetExcelSheet() where x.RowId != 0 select x) { num++; ushort num5 = (ushort)item10.TerritoryType.RowId; if (num5 == 0) { num2++; continue; } if (dictionary3.TryGetValue(num5, out var value2) && NonNavigableIntendedUses.Contains(value2) && !hashSet2.Contains(num5)) { num4++; continue; } dictionary6[item10.RowId] = item10.PopRange.RowId; ushort num6 = 0; Vector3 sourcePosition = Vector3.Zero; uint? npcDataId2 = null; string npcName = null; if (dictionary2.TryGetValue(item10.RowId, out var value3)) { num6 = value3.SourceTerritoryId; sourcePosition = value3.SourcePosition; npcDataId2 = value3.NpcDataId; } if (num6 == 0) { foreach (KeyValuePair<(uint, uint), WarpDestinationEntry> item11 in dictionary) { item11.Deconstruct(out key2, out value); (uint, uint) tuple2 = key2; WarpDestinationEntry warpDestinationEntry = value; if (tuple2.Item1 == item10.RowId && warpDestinationEntry.SourceTerritoryId > 0) { num6 = warpDestinationEntry.SourceTerritoryId; sourcePosition = warpDestinationEntry.SourcePosition; npcDataId2 = warpDestinationEntry.NpcDataId; break; } } } bool derivedSource = false; if (num6 == 0) { List<(uint, ushort, Vector3)> list2 = ResolveDerivedSources(item10.RowId, dictionary4, getPlacement); if (list2.Count > 0) { (uint, ushort, Vector3) tuple3 = list2[0]; num6 = tuple3.Item2; sourcePosition = tuple3.Item3; npcDataId2 = tuple3.Item1; npcName = ResolveObjectName(tuple3.Item1); derivedSource = true; if (list2.Count > 1) { dictionary7[item10.RowId] = list2; } } } if (num6 > 0 && dictionary3.TryGetValue(num6, out var value4) && NonNavigableIntendedUses.Contains(value4) && !hashSet2.Contains(num6)) { num3++; continue; } int gilCost = 0; ushort requiredClassLevel = 0; List list3 = new List(); bool requireAllQuests = true; WarpCondition? valueNullable = item10.WarpCondition.ValueNullable; if (valueNullable.HasValue) { gilCost = valueNullable.Value.Gil; requiredClassLevel = valueNullable.Value.ClassLevel; if (valueNullable.Value.RequiredQuest1.RowId != 0) { list3.Add(valueNullable.Value.RequiredQuest1.RowId); } if (valueNullable.Value.RequiredQuest2.RowId != 0) { list3.Add(valueNullable.Value.RequiredQuest2.RowId); } if (valueNullable.Value.RequiredQuest3.RowId != 0) { list3.Add(valueNullable.Value.RequiredQuest3.RowId); } if (valueNullable.Value.RequiredQuest4.RowId != 0) { list3.Add(valueNullable.Value.RequiredQuest4.RowId); } requireAllQuests = valueNullable.Value.CompleteParam == 1; } string text = item10.Name.ExtractText(); if (string.IsNullOrEmpty(text)) { text = null; } string text2 = item10.Question.ExtractText(); if (string.IsNullOrEmpty(text2)) { text2 = null; } dictionary5[item10.RowId] = new WarpInfo(item10.RowId, text, text2, num6, sourcePosition, npcDataId2, npcName, num5, null, gilCost, list3.ToArray(), null, requiredClassLevel, derivedSource, requireAllQuests); } List list4 = new List(); Dictionary dictionary8 = new Dictionary(); HashSet<(uint, uint)> hashSet3 = new HashSet<(uint, uint)>(); ExcelSheet levelSheet = dataManager.GetExcelSheet(); Func levelLookup = delegate(uint popId) { Level? rowOrDefault = levelSheet.GetRowOrDefault(popId); return (!rowOrDefault.HasValue) ? (((ushort, Vector3)?)null) : new(ushort, Vector3)?(((ushort)rowOrDefault.Value.Territory.RowId, new Vector3(rowOrDefault.Value.X, rowOrDefault.Value.Y, rowOrDefault.Value.Z))); }; DerivedArrivalFile arrivals = AssemblyDerivedArrivalLoader.GetArrivals(dataOptions.DevSourceDirectory?.FullName); if (!string.IsNullOrEmpty(arrivals.GameVersion) && !string.IsNullOrEmpty(dataOptions.GameVersion) && arrivals.GameVersion != dataOptions.GameVersion) { logger.LogWarning("Derived-arrival data was generated for game version {DataVersion} but the game is {LiveVersion} - rerun SmartNav.Data.ArrivalScanner", arrivals.GameVersion, dataOptions.GameVersion); } uint key3; foreach (KeyValuePair item12 in dictionary5) { item12.Deconstruct(out key3, out var value5); uint num7 = key3; WarpInfo warpInfo = value5; Vector3? destPosition = null; uint[] array = null; foreach (KeyValuePair<(uint, uint), WarpDestinationEntry> item13 in dictionary) { item13.Deconstruct(out key2, out value); (uint, uint) tuple4 = key2; WarpDestinationEntry warpDestinationEntry2 = value; if (tuple4.Item1 != num7) { continue; } if (!destPosition.HasValue && warpDestinationEntry2.DestPosition != Vector3.Zero) { destPosition = warpDestinationEntry2.DestPosition; } if (array == null) { List availableDuringMsqIds = warpDestinationEntry2.AvailableDuringMsqIds; if (availableDuringMsqIds != null && availableDuringMsqIds.Count > 0) { array = warpDestinationEntry2.AvailableDuringMsqIds.ToArray(); } } } if (!destPosition.HasValue && dictionary2.TryGetValue(num7, out var value6) && value6.DestPosition != Vector3.Zero) { destPosition = value6.DestPosition; } if (array == null && dictionary2.TryGetValue(num7, out var value7)) { List availableDuringMsqIds = value7.AvailableDuringMsqIds; if (availableDuringMsqIds != null && availableDuringMsqIds.Count > 0) { array = value7.AvailableDuringMsqIds.ToArray(); } } bool derivedArrival = false; if (!destPosition.HasValue && dictionary6.TryGetValue(num7, out var value8) && value8 != 0) { Vector3? vector = ResolveDerivedArrival(value8, warpInfo.DestTerritoryId, arrivals.Arrivals.GetValueOrDefault(num7), levelLookup); if (vector.HasValue) { destPosition = vector; derivedArrival = true; } } WarpInfo warpInfo2 = warpInfo with { DestPosition = destPosition, AvailableDuringMsqIds = array, DerivedArrival = derivedArrival }; list4.Add(warpInfo2); dictionary8[num7] = warpInfo2; uint? npcDataId = warpInfo.NpcDataId; if (npcDataId.HasValue) { uint valueOrDefault2 = npcDataId.GetValueOrDefault(); hashSet3.Add((num7, valueOrDefault2)); } } foreach (KeyValuePair<(uint, uint), WarpDestinationEntry> item14 in dictionary) { item14.Deconstruct(out key2, out value); (uint, uint) item2 = key2; WarpDestinationEntry warpDestinationEntry3 = value; if (dictionary5.ContainsKey(item2.Item1) && !hashSet3.Contains(item2)) { WarpInfo warpInfo3 = dictionary5[item2.Item1]; WarpInfo warpInfo4 = dictionary8[item2.Item1]; Vector3? destPosition2 = ((warpDestinationEntry3.DestPosition != Vector3.Zero) ? new Vector3?(warpDestinationEntry3.DestPosition) : warpInfo4.DestPosition); uint item3 = item2.Item1; string? name = warpInfo3.Name; string? question = warpInfo3.Question; ushort sourceTerritoryId = warpDestinationEntry3.SourceTerritoryId; Vector3 sourcePosition2 = warpDestinationEntry3.SourcePosition; uint? npcDataId3 = warpDestinationEntry3.NpcDataId; ushort destTerritoryId = warpInfo3.DestTerritoryId; int gilCost2 = warpInfo3.GilCost; uint[] requiredQuestIds = warpInfo3.RequiredQuestIds; List availableDuringMsqIds = warpDestinationEntry3.AvailableDuringMsqIds; list4.Add(new WarpInfo(item3, name, question, sourceTerritoryId, sourcePosition2, npcDataId3, null, destTerritoryId, destPosition2, gilCost2, requiredQuestIds, (availableDuringMsqIds != null && availableDuringMsqIds.Count > 0) ? warpDestinationEntry3.AvailableDuringMsqIds.ToArray() : null, warpInfo3.RequiredClassLevel, DerivedSource: false, warpInfo3.RequireAllQuests, warpDestinationEntry3.DestPosition == Vector3.Zero && warpInfo4.DerivedArrival)); hashSet3.Add(item2); } } foreach (KeyValuePair> item15 in dictionary7) { item15.Deconstruct(out key3, out var value9); uint num8 = key3; List<(uint, ushort, Vector3)> list5 = value9; if (!dictionary8.TryGetValue(num8, out var value10)) { continue; } foreach (var item16 in list5) { if (hashSet3.Add((num8, item16.Item1)) && (!dictionary3.TryGetValue(item16.Item2, out var value11) || !NonNavigableIntendedUses.Contains(value11) || hashSet2.Contains(item16.Item2))) { list4.Add(value10 with { SourceTerritoryId = item16.Item2, SourcePosition = item16.Item3, NpcDataId = item16.Item1, NpcName = ResolveObjectName(item16.Item1), DerivedSource = true }); } } } int num9 = list4.Count((WarpInfo w) => w.DestPosition.HasValue); int num10 = list4.Count((WarpInfo w) => w.SourceTerritoryId == 0); int num11 = list4.Count((WarpInfo w) => w.DerivedSource); int num12 = list4.Count((WarpInfo w) => w.DerivedArrival); logger.LogDebug("WarpDataService: {Total} Lumina warps, {Relevant} navigation-relevant ({UniqueIds} unique IDs), {WithDest} with destination, {DerivedSrc} derived source, {DerivedArr} derived arrival, {UnknownSrc} unknown source{Pending}", num, list4.Count, dictionary8.Count, num9, num11, num12, num10, isBuilt ? "" : " (derived sources pending NPC position cache)"); logger.LogDebug("WarpDataService filtered: {NoDest} no dest territory, {UserSkipped} user-skipped, {SourceBlocked} source territory blocked, {DestBlocked} dest territory blocked", num2, hashSet.Count, num3, num4); return new Snapshot(list4, dictionary8, hashSet, num, list4.Count, num9, num10, num11, num12, num2, num3, num4, !isBuilt); string? ResolveObjectName(uint objectId) { ENpcResident? rowOrDefault = dataManager.GetExcelSheet().GetRowOrDefault(objectId); if (rowOrDefault.HasValue) { string text3 = rowOrDefault.Value.Singular.ExtractText(); if (!string.IsNullOrEmpty(text3)) { return text3; } } EObjName? rowOrDefault2 = dataManager.GetExcelSheet().GetRowOrDefault(objectId); if (rowOrDefault2.HasValue) { string text4 = rowOrDefault2.Value.Singular.ExtractText(); if (!string.IsNullOrEmpty(text4)) { return text4; } } return null; } } internal static void AddWarpFronter(Dictionary> index, uint handlerId, uint fronterObjectId, Func?> arrayMemberLookup, ref int nestedArraysSkipped) { if (handlerId == 0) { return; } switch (handlerId >> 16) { case 2u: AddFronterEntry(index, handlerId, fronterObjectId); break; case 13u: { IEnumerable enumerable = arrayMemberLookup(handlerId); if (enumerable == null) { break; } { foreach (uint item in enumerable) { if (item >> 16 == 2) { AddFronterEntry(index, item, fronterObjectId); } else if (item >> 16 == 13) { nestedArraysSkipped++; } } break; } } } } private static void AddFronterEntry(Dictionary> index, uint warpRowId, uint fronterObjectId) { if (!index.TryGetValue(warpRowId, out List value)) { value = (index[warpRowId] = new List()); } if (!value.Contains(fronterObjectId)) { value.Add(fronterObjectId); } } internal static Vector3? ResolveDerivedArrival(uint popRangeId, ushort destTerritoryId, DerivedArrivalEntry? embedded, Func levelLookup) { if (popRangeId == 0) { return null; } if (embedded != null && embedded.PopRangeId == popRangeId && embedded.TerritoryId == destTerritoryId) { return new Vector3(embedded.X, embedded.Y, embedded.Z); } (ushort, Vector3)? tuple = levelLookup(popRangeId); if (tuple.HasValue) { (ushort, Vector3) valueOrDefault = tuple.GetValueOrDefault(); if (valueOrDefault.Item1 == destTerritoryId) { return valueOrDefault.Item2; } } return null; } internal static List<(uint NpcDataId, ushort TerritoryId, Vector3 Position)> ResolveDerivedSources(uint warpRowId, IReadOnlyDictionary> fronterIndex, Func getPlacement) { List<(uint, ushort, Vector3)> list = new List<(uint, ushort, Vector3)>(); if (!fronterIndex.TryGetValue(warpRowId, out List value)) { return list; } foreach (uint item in value.Order()) { (ushort, Vector3, bool)? tuple = getPlacement(item); if (tuple.HasValue) { (ushort, Vector3, bool) valueOrDefault = tuple.GetValueOrDefault(); if (!valueOrDefault.Item3) { list.Add((item, valueOrDefault.Item1, valueOrDefault.Item2)); } } } return list; } private static void LoadWarpOverridesFrom(string path, List target, HashSet skippedIds) { if (!File.Exists(path)) { return; } WarpDestinationOverrides warpDestinationOverrides = JsonSerializer.Deserialize(File.ReadAllText(path)); if (warpDestinationOverrides == null) { return; } foreach (uint skippedWarpId in warpDestinationOverrides.SkippedWarpIds) { skippedIds.Add(skippedWarpId); } foreach (var (_, item) in warpDestinationOverrides.Destinations) { target.Add(item); } } }