qstbak/SmartNav/SmartNav.Data/WarpDataService.cs
2026-08-17 20:29:32 +10:00

611 lines
22 KiB
C#

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<WarpInfo> Warps, IReadOnlyDictionary<uint, WarpInfo> WarpsByRowId, IReadOnlySet<uint> 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<ETerritoryIntendedUse> NonNavigableIntendedUses = new HashSet<ETerritoryIntendedUse>
{
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<WarpDataService>? _logger;
private readonly object _applyLock = new object();
private volatile Snapshot _current;
public IReadOnlyList<WarpInfo> Warps => _current.Warps;
public IReadOnlyDictionary<uint, WarpInfo> WarpsByRowId => _current.WarpsByRowId;
public IReadOnlySet<uint> 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<WarpInfo> warps, HashSet<uint>? skippedWarpIds = null)
{
Dictionary<uint, WarpInfo> dictionary = new Dictionary<uint, WarpInfo>();
foreach (WarpInfo warp in warps)
{
dictionary.TryAdd(warp.RowId, warp);
}
_current = new Snapshot(warps, dictionary, skippedWarpIds ?? new HashSet<uint>(), 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<WarpDataService> 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<WarpDataService> logger = _logger;
bool isBuilt = npcPositionCache.IsBuilt;
Func<uint, (ushort, Vector3, bool)?> getPlacement = (isBuilt ? new Func<uint, (ushort, Vector3, bool)?>(npcPositionCache.TryGetPlacement) : ((Func<uint, (ushort, Vector3, bool)?>)((uint _) => ((ushort TerritoryId, Vector3 Position, bool FestivalOnly)?)null)));
List<WarpDestinationEntry> list = new List<WarpDestinationEntry>();
HashSet<uint> hashSet = new HashSet<uint>();
var (readOnlyList, readOnlyDictionary) = AssemblyWarpDestinationLoader.GetOverrides(dataOptions.DevSourceDirectory?.FullName);
foreach (uint item4 in readOnlyList)
{
hashSet.Add(item4);
}
WarpDestinationEntry value;
foreach (KeyValuePair<string, WarpDestinationEntry> 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<uint, WarpDestinationEntry> dictionary2 = new Dictionary<uint, WarpDestinationEntry>();
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<ushort> hashSet2 = new HashSet<ushort>();
foreach (WarpDestinationEntry item7 in list)
{
if (item7.SourceTerritoryId > 0)
{
hashSet2.Add(item7.SourceTerritoryId);
}
if (item7.DestTerritoryId > 0)
{
hashSet2.Add(item7.DestTerritoryId);
}
}
Dictionary<ushort, ETerritoryIntendedUse> dictionary3 = (from x in dataManager.GetExcelSheet<TerritoryType>()
where x.RowId != 0
select x).ToDictionary((TerritoryType x) => (ushort)x.RowId, (TerritoryType x) => (ETerritoryIntendedUse)x.TerritoryIntendedUse.RowId);
Dictionary<uint, List<uint>> dictionary4 = new Dictionary<uint, List<uint>>();
int nestedArraysSkipped = 0;
ExcelSheet<ArrayEventHandler> arrayEventHandlers = dataManager.GetExcelSheet<ArrayEventHandler>();
Func<uint, IEnumerable<uint>> 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<ENpcBase>())
{
foreach (RowRef eNpcDatum in item8.ENpcData)
{
AddWarpFronter(dictionary4, eNpcDatum.RowId, item8.RowId, arrayMemberLookup, ref nestedArraysSkipped);
}
}
foreach (EObj item9 in dataManager.GetExcelSheet<EObj>())
{
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<uint, WarpInfo> dictionary5 = new Dictionary<uint, WarpInfo>();
Dictionary<uint, uint> dictionary6 = new Dictionary<uint, uint>();
Dictionary<uint, List<(uint, ushort, Vector3)>> dictionary7 = new Dictionary<uint, List<(uint, ushort, Vector3)>>();
int num = 0;
int num2 = 0;
int num3 = 0;
int num4 = 0;
(uint, uint) key2;
foreach (Warp item10 in from x in dataManager.GetExcelSheet<Warp>()
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<uint> list3 = new List<uint>();
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<WarpInfo> list4 = new List<WarpInfo>();
Dictionary<uint, WarpInfo> dictionary8 = new Dictionary<uint, WarpInfo>();
HashSet<(uint, uint)> hashSet3 = new HashSet<(uint, uint)>();
ExcelSheet<Level> levelSheet = dataManager.GetExcelSheet<Level>();
Func<uint, (ushort, Vector3)?> 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<uint, WarpInfo> 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<uint> 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<uint> 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<uint> 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<uint, List<(uint, ushort, Vector3)>> 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<ENpcResident>().GetRowOrDefault(objectId);
if (rowOrDefault.HasValue)
{
string text3 = rowOrDefault.Value.Singular.ExtractText();
if (!string.IsNullOrEmpty(text3))
{
return text3;
}
}
EObjName? rowOrDefault2 = dataManager.GetExcelSheet<EObjName>().GetRowOrDefault(objectId);
if (rowOrDefault2.HasValue)
{
string text4 = rowOrDefault2.Value.Singular.ExtractText();
if (!string.IsNullOrEmpty(text4))
{
return text4;
}
}
return null;
}
}
internal static void AddWarpFronter(Dictionary<uint, List<uint>> index, uint handlerId, uint fronterObjectId, Func<uint, IEnumerable<uint>?> arrayMemberLookup, ref int nestedArraysSkipped)
{
if (handlerId == 0)
{
return;
}
switch (handlerId >> 16)
{
case 2u:
AddFronterEntry(index, handlerId, fronterObjectId);
break;
case 13u:
{
IEnumerable<uint> 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<uint, List<uint>> index, uint warpRowId, uint fronterObjectId)
{
if (!index.TryGetValue(warpRowId, out List<uint> value))
{
value = (index[warpRowId] = new List<uint>());
}
if (!value.Contains(fronterObjectId))
{
value.Add(fronterObjectId);
}
}
internal static Vector3? ResolveDerivedArrival(uint popRangeId, ushort destTerritoryId, DerivedArrivalEntry? embedded, Func<uint, (ushort TerritoryId, Vector3 Position)?> 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<uint, List<uint>> fronterIndex, Func<uint, (ushort TerritoryId, Vector3 Position, bool FestivalOnly)?> getPlacement)
{
List<(uint, ushort, Vector3)> list = new List<(uint, ushort, Vector3)>();
if (!fronterIndex.TryGetValue(warpRowId, out List<uint> 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<WarpDestinationEntry> target, HashSet<uint> skippedIds)
{
if (!File.Exists(path))
{
return;
}
WarpDestinationOverrides warpDestinationOverrides = JsonSerializer.Deserialize<WarpDestinationOverrides>(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);
}
}
}