├── .editorconfig ├── .gitignore ├── BinaryHeap.cs ├── Extensions.cs ├── InstanceDataDumper.cs ├── KMeans.cs ├── PathFinder.cs ├── README.md ├── Radar.MemoryInteraction.cs ├── Radar.Pathfinding.cs ├── Radar.cs ├── Radar.csproj ├── Radar.sln ├── RadarSettings.cs ├── RouteDescription.cs ├── TargetDescription.cs ├── TargetLocations.cs ├── TargetType.cs ├── Vector2d.cs └── targets.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Download this file using PowerShell v3 under Windows with the following comand: 2 | # Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore 3 | # or wget: 4 | # wget --no-check-certificate http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.sln.docstates 10 | 11 | # Build results 12 | [Dd]ebug/ 13 | [Rr]elease/ 14 | x64/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | # build folder is nowadays used for build scripts and should not be ignored 18 | #build/ 19 | 20 | # NuGet Packages 21 | *.nupkg 22 | # The packages folder can be ignored because of Package Restore 23 | **/packages/* 24 | # except build/, which is used as an MSBuild target. 25 | !**/packages/build/ 26 | # Uncomment if necessary however generally it will be regenerated when needed 27 | #!**/packages/repositories.config 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | *_i.c 34 | *_p.c 35 | *.ilk 36 | *.meta 37 | *.obj 38 | *.pch 39 | *.pdb 40 | *.pgc 41 | *.pgd 42 | *.rsp 43 | *.sbr 44 | *.tlb 45 | *.tli 46 | *.tlh 47 | *.tmp 48 | *.tmp_proj 49 | *.log 50 | *.vspscc 51 | *.vssscc 52 | .builds 53 | *.pidb 54 | *.log 55 | *.scc 56 | 57 | # OS generated files # 58 | .DS_Store* 59 | Icon? 60 | 61 | # Visual C++ cache files 62 | ipch/ 63 | *.aps 64 | *.ncb 65 | *.opensdf 66 | *.sdf 67 | *.cachefile 68 | 69 | # Visual Studio profiler 70 | *.psess 71 | *.vsp 72 | *.vspx 73 | 74 | # Guidance Automation Toolkit 75 | *.gpState 76 | 77 | # ReSharper is a .NET coding add-in 78 | _ReSharper*/ 79 | *.[Rr]e[Ss]harper 80 | 81 | # TeamCity is a build add-in 82 | _TeamCity* 83 | 84 | # DotCover is a Code Coverage Tool 85 | *.dotCover 86 | 87 | # NCrunch 88 | *.ncrunch* 89 | .*crunch*.local.xml 90 | 91 | # Installshield output folder 92 | [Ee]xpress/ 93 | 94 | # DocProject is a documentation generator add-in 95 | DocProject/buildhelp/ 96 | DocProject/Help/*.HxT 97 | DocProject/Help/*.HxC 98 | DocProject/Help/*.hhc 99 | DocProject/Help/*.hhk 100 | DocProject/Help/*.hhp 101 | DocProject/Help/Html2 102 | DocProject/Help/html 103 | 104 | # Click-Once directory 105 | publish/ 106 | 107 | # Publish Web Output 108 | *.Publish.xml 109 | 110 | # Windows Azure Build Output 111 | csx 112 | *.build.csdef 113 | 114 | # Windows Store app package directory 115 | AppPackages/ 116 | 117 | # Others 118 | *.Cache 119 | ClientBin/ 120 | [Ss]tyle[Cc]op.* 121 | ~$* 122 | *~ 123 | *.dbmdl 124 | *.[Pp]ublish.xml 125 | *.pfx 126 | *.publishsettings 127 | modulesbin/ 128 | tempbin/ 129 | 130 | # EPiServer Site file (VPP) 131 | AppData/ 132 | 133 | # RIA/Silverlight projects 134 | Generated_Code/ 135 | 136 | # Backup & report files from converting an old project file to a newer 137 | # Visual Studio version. Backup files are not needed, because we have git ;-) 138 | _UpgradeReport_Files/ 139 | Backup*/ 140 | UpgradeLog*.XML 141 | UpgradeLog*.htm 142 | 143 | # vim 144 | *.txt~ 145 | *.swp 146 | *.swo 147 | 148 | # svn 149 | .svn 150 | 151 | # CVS - Source Control 152 | **/CVS/ 153 | 154 | # Remainings from resolvings conflicts in Source Control 155 | *.orig 156 | 157 | # SQL Server files 158 | **/App_Data/*.mdf 159 | **/App_Data/*.ldf 160 | **/App_Data/*.sdf 161 | 162 | 163 | #LightSwitch generated files 164 | GeneratedArtifacts/ 165 | _Pvt_Extensions/ 166 | ModelManifest.xml 167 | 168 | # ========================= 169 | # Windows detritus 170 | # ========================= 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac desktop service store files 183 | .DS_Store 184 | 185 | # SASS Compiler cache 186 | .sass-cache 187 | 188 | # Visual Studio 2014 CTP 189 | **/*.sln.ide 190 | 191 | # Visual Studio temp something 192 | .vs/ 193 | 194 | # dotnet stuff 195 | project.lock.json 196 | 197 | # VS 2015+ 198 | *.vc.vc.opendb 199 | *.vc.db 200 | 201 | # Rider 202 | .idea/ 203 | 204 | # Visual Studio Code 205 | .vscode/ 206 | 207 | # Output folder used by Webpack or other FE stuff 208 | **/node_modules/* 209 | **/wwwroot/* 210 | 211 | # SpecFlow specific 212 | *.feature.cs 213 | *.feature.xlsx.* 214 | *.Specs_*.html 215 | 216 | ##### 217 | # End of core ignore list, below put you custom 'per project' settings (patterns or path) 218 | ##### 219 | Errors.txt 220 | -------------------------------------------------------------------------------- /BinaryHeap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Radar; 4 | 5 | public class BinaryHeap 6 | { 7 | private readonly List> _storage = new List>(); 8 | 9 | private void SieveUp(int startIndex) 10 | { 11 | var index = startIndex; 12 | var nextIndex = (index - 1) / 2; 13 | while (index != nextIndex) 14 | { 15 | if (Compare(index, nextIndex) < 0) 16 | { 17 | Swap(index, nextIndex); 18 | } 19 | else 20 | { 21 | return; 22 | } 23 | 24 | index = nextIndex; 25 | nextIndex = (index - 1) / 2; 26 | } 27 | } 28 | 29 | private void SieveDown(int startIndex) 30 | { 31 | var index = startIndex; 32 | while (index * 2 + 1 < _storage.Count) 33 | { 34 | var child1 = index * 2 + 1; 35 | var child2 = index * 2 + 2; 36 | int nextIndex; 37 | if (child2 < _storage.Count) 38 | { 39 | nextIndex = Compare(index, child1) > 0 40 | ? Compare(index, child2) > 0 41 | ? Compare(child1, child2) > 0 42 | ? child2 43 | : child1 44 | : child1 45 | : Compare(index, child2) > 0 46 | ? child2 47 | : index; 48 | } 49 | else 50 | { 51 | nextIndex = Compare(index, child1) > 0 52 | ? child1 53 | : index; 54 | } 55 | 56 | if (nextIndex == index) 57 | { 58 | return; 59 | } 60 | 61 | Swap(index, nextIndex); 62 | index = nextIndex; 63 | } 64 | } 65 | 66 | private int Compare(int i1, int i2) 67 | { 68 | return Comparer.Default.Compare(_storage[i1].Key, _storage[i2].Key); 69 | } 70 | 71 | private void Swap(int i1, int i2) 72 | { 73 | (_storage[i1], _storage[i2]) = (_storage[i2], _storage[i1]); 74 | } 75 | 76 | public void Add(TKey key, TValue value) 77 | { 78 | _storage.Add(new KeyValuePair(key, value)); 79 | SieveUp(_storage.Count - 1); 80 | } 81 | 82 | public bool TryRemoveTop(out KeyValuePair value) 83 | { 84 | if (_storage.Count == 0) 85 | { 86 | value = default; 87 | return false; 88 | } 89 | 90 | value = _storage[0]; 91 | _storage[0] = _storage[^1]; 92 | _storage.RemoveAt(_storage.Count - 1); 93 | SieveDown(0); 94 | return true; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Numerics; 3 | using System.Text.RegularExpressions; 4 | using ExileCore.PoEMemory.Components; 5 | using GameOffsets.Native; 6 | 7 | namespace Radar; 8 | 9 | public static class Extensions 10 | { 11 | public static Vector3 GridPos(this Render render) 12 | { 13 | return render.PosNum / Radar.GridToWorldMultiplier; 14 | } 15 | 16 | public static Vector2i Truncate(this Vector2 v) 17 | { 18 | return new Vector2i((int)v.X, (int)v.Y); 19 | } 20 | 21 | public static IEnumerable GetEveryNth(this IEnumerable source, int n) 22 | { 23 | var i = 0; 24 | foreach (var item in source) 25 | { 26 | if (i == 0) 27 | { 28 | yield return item; 29 | } 30 | 31 | i++; 32 | i %= n; 33 | } 34 | } 35 | 36 | /// 37 | /// Compares the string against a given pattern. 38 | /// 39 | /// The string. 40 | /// The pattern to match, where "*" means any sequence of characters, and "?" means any single character. 41 | /// true if the string matches the given pattern; otherwise false. 42 | public static bool Like(this string str, string pattern) 43 | { 44 | return ToLikeRegex(pattern).IsMatch(str); 45 | } 46 | 47 | public static Regex ToLikeRegex(this string pattern) 48 | { 49 | return new Regex("^" + 50 | Regex.Escape(pattern) 51 | .Replace(@"\*", ".*") 52 | .Replace(@"\?", ".") 53 | + "$", RegexOptions.IgnoreCase | RegexOptions.Singleline); 54 | } 55 | } -------------------------------------------------------------------------------- /InstanceDataDumper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Radar; 8 | 9 | public class TilePosition 10 | { 11 | public int X { get; set; } 12 | public int Y { get; set; } 13 | public int W { get; set; } 14 | public int H { get; set; } 15 | public List Tiles { get; set; } 16 | } 17 | 18 | public class OptimizedInstanceData 19 | { 20 | public string Name { get; set; } 21 | public int W { get; set; } 22 | public int H { get; set; } 23 | public float[] Heights { get; set; } 24 | public int[] Walk { get; set; } 25 | public int[] Target { get; set; } 26 | public List Tiles { get; set; } 27 | } 28 | 29 | public partial class Radar 30 | { 31 | public void DumpInstanceData(string outputPath) 32 | { 33 | if (_heightData == null || _processedTerrainData == null || _processedTerrainTargetingData == null || _areaDimensions == null) 34 | { 35 | return; 36 | } 37 | 38 | try 39 | { 40 | var dimensions = _areaDimensions.Value; 41 | 42 | // Create flattened arrays 43 | var heights = new float[dimensions.X * dimensions.Y]; 44 | var walk = new int[dimensions.X * dimensions.Y]; 45 | var target = new int[dimensions.X * dimensions.Y]; 46 | 47 | // Fill the arrays 48 | for (var y = 0; y < dimensions.Y && y < _heightData.Length; y++) 49 | { 50 | for (var x = 0; x < dimensions.X && x < _heightData[y].Length; x++) 51 | { 52 | var index = y * dimensions.X + x; 53 | heights[index] = _heightData[y][x]; 54 | walk[index] = _processedTerrainData[y][x]; 55 | target[index] = _processedTerrainTargetingData[y][x]; 56 | } 57 | } 58 | 59 | // Convert to list of TilePositions 60 | var tilePositions = _locationsByPosition.Select(kvp => new TilePosition 61 | { 62 | X = kvp.Key.X, 63 | Y = kvp.Key.Y, 64 | W = TileToGridConversion, 65 | H = TileToGridConversion, 66 | Tiles = kvp.Value 67 | }).ToList(); 68 | 69 | var instanceData = new OptimizedInstanceData 70 | { 71 | Name = GameController.Area.CurrentArea.Area.RawName, 72 | W = dimensions.X, 73 | H = dimensions.Y, 74 | Heights = heights, 75 | Walk = walk, 76 | Target = target, 77 | Tiles = tilePositions 78 | }; 79 | 80 | // Create directory if it doesn't exist 81 | var fullPath = Path.GetFullPath(outputPath); 82 | var directory = Path.GetDirectoryName(fullPath); 83 | 84 | if (!string.IsNullOrEmpty(directory)) 85 | { 86 | Directory.CreateDirectory(directory); 87 | } 88 | 89 | // Serialize and write with indentation for readability 90 | var json = JsonConvert.SerializeObject(instanceData, new JsonSerializerSettings 91 | { 92 | Formatting = Formatting.None 93 | }); 94 | 95 | File.WriteAllText(outputPath, json); 96 | } 97 | catch (Exception ex) 98 | { 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /KMeans.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Radar; 6 | 7 | public static class KMeans 8 | { 9 | public static int[] Cluster(Vector2d[] rawData, int numClusters) 10 | { 11 | if (numClusters >= rawData.Length) 12 | { 13 | return Enumerable.Range(0, rawData.Length).ToArray(); 14 | } 15 | 16 | var clusteringUpdated = true; 17 | var meansUpdated = true; 18 | var clustering = InitClustering(rawData, numClusters); 19 | var means = new Vector2d[numClusters]; 20 | var num = rawData.Length * 10; 21 | var index = 0; 22 | for (; clusteringUpdated && meansUpdated && index < num; ++index) 23 | { 24 | meansUpdated = UpdateMeans(rawData, clustering, means); 25 | clusteringUpdated = UpdateClustering(rawData, clustering, means); 26 | } 27 | 28 | return clustering; 29 | } 30 | 31 | private static int[] InitClustering(Vector2d[] data, int numClusters) 32 | { 33 | var selectedClusters = new HashSet { 0 }; 34 | while (selectedClusters.Count < numClusters) 35 | { 36 | var newPointIndex = data.Select((tuple, index) => (tuple, index)) 37 | .Where(x => !selectedClusters.Contains(x.index)) 38 | .MaxBy(c => selectedClusters.Min(x => Distance(c.tuple, data[x]))); 39 | selectedClusters.Add(newPointIndex.index); 40 | } 41 | 42 | var clusterNumbers = selectedClusters.Select((x, i) => (x, i)).ToDictionary(x => x.x, x => x.i); 43 | var numArray = data.Select(x => clusterNumbers[selectedClusters.MinBy(y => Distance(x, data[y]))]).ToArray(); 44 | return numArray; 45 | } 46 | 47 | private static bool UpdateMeans(Vector2d[] data, int[] clustering, Vector2d[] means) 48 | { 49 | var length = means.Length; 50 | var numArray = new int[length]; 51 | for (var index1 = 0; index1 < data.Length; ++index1) 52 | { 53 | var index2 = clustering[index1]; 54 | ++numArray[index2]; 55 | } 56 | 57 | for (var i = 0; i < length; ++i) 58 | { 59 | if (numArray[i] == 0) 60 | return false; 61 | } 62 | 63 | for (var i = 0; i < means.Length; i++) 64 | { 65 | means[i] = default; 66 | } 67 | 68 | for (var i = 0; i < data.Length; ++i) 69 | { 70 | means[clustering[i]] += data[i]; 71 | } 72 | 73 | for (var i = 0; i < means.Length; ++i) 74 | { 75 | means[i] /= numArray[i]; 76 | } 77 | 78 | return true; 79 | } 80 | 81 | private static bool UpdateClustering(Vector2d[] data, int[] clustering, Vector2d[] means) 82 | { 83 | var length = means.Length; 84 | var didUpdate = false; 85 | var clusteringCopy = new int[clustering.Length]; 86 | Array.Copy(clustering, clusteringCopy, clustering.Length); 87 | var clusterSizes = clusteringCopy.GroupBy(x => x).ToDictionary(x => x.Key, x => x.Count()); 88 | var distances = new double[length]; 89 | for (var index1 = 0; index1 < data.Length; ++index1) 90 | { 91 | for (var index2 = 0; index2 < length; ++index2) 92 | distances[index2] = Distance(data[index1], means[index2]); 93 | var newClusterIndex = distances.Select((distance, index) => (distance, index)).MinBy(x => x.distance).index; 94 | ref var clusterIndex = ref clusteringCopy[index1]; 95 | if (newClusterIndex != clusterIndex) 96 | { 97 | didUpdate = true; 98 | if (clusterSizes[clusterIndex] > 1) 99 | { 100 | clusterSizes[clusterIndex]--; 101 | clusterSizes[newClusterIndex]++; 102 | clusterIndex = newClusterIndex; 103 | } 104 | } 105 | } 106 | 107 | if (!didUpdate) 108 | return false; 109 | var numArray2 = new int[length]; 110 | for (var index3 = 0; index3 < data.Length; ++index3) 111 | { 112 | var index4 = clusteringCopy[index3]; 113 | ++numArray2[index4]; 114 | } 115 | 116 | for (var index = 0; index < length; ++index) 117 | { 118 | if (numArray2[index] == 0) 119 | return false; 120 | } 121 | 122 | Array.Copy(clusteringCopy, clustering, clusteringCopy.Length); 123 | return true; 124 | } 125 | 126 | private static double Distance(Vector2d v1, Vector2d v2) 127 | { 128 | return (v1 - v2).Length; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /PathFinder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using GameOffsets.Native; 7 | 8 | namespace Radar; 9 | 10 | public class PathFinder 11 | { 12 | private readonly bool[][] _grid; 13 | 14 | //target->current->next 15 | private readonly ConcurrentDictionary> ExactDistanceField = new(); 16 | private readonly ConcurrentDictionary DirectionField = new(); 17 | private readonly int _dimension2; 18 | private readonly int _dimension1; 19 | 20 | public PathFinder(int[][] grid, int[] pathableValues) 21 | { 22 | var pv = pathableValues.ToHashSet(); 23 | _grid = grid.Select(x => x.Select(y => pv.Contains(y)).ToArray()).ToArray(); 24 | _dimension1 = _grid.Length; 25 | _dimension2 = _grid[0].Length; 26 | } 27 | 28 | private bool IsTilePathable(Vector2i tile) 29 | { 30 | if (tile.X < 0 || tile.X >= _dimension2) 31 | { 32 | return false; 33 | } 34 | 35 | if (tile.Y < 0 || tile.Y >= _dimension1) 36 | { 37 | return false; 38 | } 39 | 40 | return _grid[tile.Y][tile.X]; 41 | } 42 | 43 | private static readonly List NeighborOffsets = new List 44 | { 45 | new Vector2i(0, 1), 46 | new Vector2i(1, 1), 47 | new Vector2i(1, 0), 48 | new Vector2i(1, -1), 49 | new Vector2i(0, -1), 50 | new Vector2i(-1, -1), 51 | new Vector2i(-1, 0), 52 | new Vector2i(-1, 1), 53 | }; 54 | 55 | private static IEnumerable GetNeighbors(Vector2i tile) 56 | { 57 | return NeighborOffsets.Select(offset => tile + offset); 58 | } 59 | 60 | private static float GetExactDistance(Vector2i tile, Dictionary dict) 61 | { 62 | return dict.GetValueOrDefault(tile, float.PositiveInfinity); 63 | } 64 | 65 | public IEnumerable> RunFirstScan(Vector2i start, Vector2i target) 66 | { 67 | if (DirectionField.ContainsKey(target)) 68 | { 69 | yield break; 70 | } 71 | 72 | if (!ExactDistanceField.TryAdd(target, new Dictionary())) 73 | { 74 | yield break; 75 | } 76 | 77 | var exactDistanceField = ExactDistanceField[target]; 78 | exactDistanceField[target] = 0; 79 | var localBacktrackDictionary = new Dictionary(); 80 | var queue = new BinaryHeap(); 81 | queue.Add(0, target); 82 | 83 | void TryEnqueueTile(Vector2i coord, Vector2i previous, float previousScore) 84 | { 85 | if (!IsTilePathable(coord)) 86 | { 87 | return; 88 | } 89 | 90 | if (localBacktrackDictionary.ContainsKey(coord)) 91 | { 92 | return; 93 | } 94 | 95 | localBacktrackDictionary.Add(coord, previous); 96 | var exactDistance = previousScore + coord.DistanceF(previous); 97 | exactDistanceField.TryAdd(coord, exactDistance); 98 | queue.Add(exactDistance, coord); 99 | } 100 | 101 | var sw = Stopwatch.StartNew(); 102 | 103 | localBacktrackDictionary.Add(target, target); 104 | var reversePath = new List(); 105 | while (queue.TryRemoveTop(out var top)) 106 | { 107 | var current = top.Value; 108 | var currentDistance = top.Key; 109 | if (reversePath.Count == 0 && current.Equals(start)) 110 | { 111 | reversePath.Add(current); 112 | var it = current; 113 | while (it != target && localBacktrackDictionary.TryGetValue(it, out var previous)) 114 | { 115 | reversePath.Add(previous); 116 | it = previous; 117 | } 118 | 119 | yield return reversePath; 120 | } 121 | 122 | foreach (var neighbor in GetNeighbors(current)) 123 | { 124 | TryEnqueueTile(neighbor, current, currentDistance); 125 | } 126 | 127 | if (sw.ElapsedMilliseconds > 100) 128 | { 129 | yield return reversePath; 130 | sw.Restart(); 131 | } 132 | } 133 | 134 | localBacktrackDictionary.Clear(); 135 | 136 | if (_dimension1 * _dimension2 < exactDistanceField.Count * (sizeof(int) * 2 + Unsafe.SizeOf() + Unsafe.SizeOf())) 137 | { 138 | var directionGrid = _grid 139 | .AsParallel().AsOrdered().Select((r, y) => r.Select((_, x) => 140 | { 141 | var coordVec = new Vector2i(x, y); 142 | if (float.IsPositiveInfinity(GetExactDistance(coordVec, exactDistanceField))) 143 | { 144 | return (byte)0; 145 | } 146 | 147 | var neighbors = GetNeighbors(coordVec); 148 | var (closestNeighbor, clndistance) = neighbors.Select(n => (n, distance: GetExactDistance(n, exactDistanceField))).MinBy(p => p.distance); 149 | if (float.IsPositiveInfinity(clndistance)) 150 | { 151 | return (byte)0; 152 | } 153 | 154 | var bestDirection = closestNeighbor - coordVec; 155 | return (byte)(1 + NeighborOffsets.IndexOf(bestDirection)); 156 | }).ToArray()) 157 | .ToArray(); 158 | 159 | DirectionField[target] = directionGrid; 160 | ExactDistanceField.TryRemove(target, out _); 161 | } 162 | } 163 | 164 | public List FindPath(Vector2i start, Vector2i target) 165 | { 166 | if (DirectionField.GetValueOrDefault(target) is { } directionField) 167 | { 168 | if (directionField[start.Y][start.X] == 0) 169 | return null; 170 | var path = new List(); 171 | var current = start; 172 | while (current != target) 173 | { 174 | var directionIndex = directionField[current.Y][current.X]; 175 | if (directionIndex == 0) 176 | { 177 | return null; 178 | } 179 | 180 | var next = NeighborOffsets[directionIndex - 1] + current; 181 | path.Add(next); 182 | current = next; 183 | } 184 | 185 | return path; 186 | } 187 | else 188 | { 189 | var exactDistanceField = ExactDistanceField[target]; 190 | if (float.IsPositiveInfinity(GetExactDistance(start, exactDistanceField))) return null; 191 | var path = new List(); 192 | var current = start; 193 | while (current != target) 194 | { 195 | var next = GetNeighbors(current).MinBy(x => GetExactDistance(x, exactDistanceField)); 196 | path.Add(next); 197 | current = next; 198 | } 199 | 200 | return path; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radar 2 | 3 | If you like it, you can donate via: 4 | 5 | BTC: bc1qke67907s6d5k3cm7lx7m020chyjp9e8ysfwtuz 6 | 7 | ETH: 0x3A37B3f57453555C2ceabb1a2A4f55E0eB969105 8 | -------------------------------------------------------------------------------- /Radar.MemoryInteraction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using ExileCore.Shared.Helpers; 5 | using SixLabors.ImageSharp; 6 | using SixLabors.ImageSharp.PixelFormats; 7 | using SixLabors.ImageSharp.Processing; 8 | using SixLabors.ImageSharp.Processing.Processors.Convolution; 9 | using SixLabors.ImageSharp.Processing.Processors.Transforms; 10 | using Configuration = SixLabors.ImageSharp.Configuration; 11 | using Vector4 = System.Numerics.Vector4; 12 | 13 | namespace Radar; 14 | 15 | public partial class Radar 16 | { 17 | private void GenerateMapTexture() 18 | { 19 | var gridHeightData = _heightData; 20 | var maxX = _areaDimensions.Value.X; 21 | var maxY = _areaDimensions.Value.Y; 22 | var configuration = Configuration.Default.Clone(); 23 | configuration.PreferContiguousImageBuffers = true; 24 | using var image = new Image(configuration, maxX, maxY); 25 | if (Settings.Debug.DrawHeightMap) 26 | { 27 | var minHeight = gridHeightData.Min(x => x.Min()); 28 | var maxHeight = gridHeightData.Max(x => x.Max()); 29 | image.Mutate(configuration, c => c.ProcessPixelRowsAsVector4((row, i) => 30 | { 31 | for (var x = 0; x < row.Length - 1; x += 2) 32 | { 33 | var cellData = gridHeightData[i.Y][x]; 34 | for (var x_s = 0; x_s < 2; ++x_s) 35 | { 36 | row[x + x_s] = new Vector4(0, (cellData - minHeight) / (maxHeight - minHeight), 0, 1); 37 | } 38 | } 39 | })); 40 | } 41 | else 42 | { 43 | if (Settings.Debug.AlternativeEdgeMethod) 44 | { 45 | static float Clamp(float value, float min, float max) => MathF.Min(MathF.Max(value, min), max); 46 | 47 | static Vector4 Lerp(Vector4 a, Vector4 b, float t) => a + t * (b - a); 48 | 49 | using var binaryMap = new Image(configuration, maxX, maxY); 50 | 51 | if (!Settings.Debug.DisableHeightAdjust) 52 | Parallel.For( 53 | 0, maxY, y => 54 | { 55 | for (var x = 0; x < maxX; x++) 56 | { 57 | if (_processedTerrainData[y][x] != 1) 58 | continue; 59 | 60 | var cellData = gridHeightData[y][x]; 61 | var heightOffset = (int)(cellData / GridToWorldMultiplier / 2); 62 | var adjustedX = x - heightOffset; 63 | var adjustedY = y - heightOffset; 64 | 65 | if (adjustedX >= 0 && adjustedX < maxX && adjustedY >= 0 && adjustedY < maxY) 66 | binaryMap[adjustedX, adjustedY] = new L8(255); 67 | } 68 | }); 69 | 70 | else 71 | Parallel.For( 72 | 0, maxY, y => 73 | { 74 | for (var x = 0; x < maxX; x++) 75 | if (_processedTerrainData[y][x] != 1) 76 | binaryMap[x, y] = new L8(255); 77 | }); 78 | 79 | var blurSigma = Settings.Debug.AlternativeEdgeSettings.OutlineBlurSigma.Value; 80 | binaryMap.Mutate(ctx => ctx.GaussianBlur(blurSigma)); 81 | 82 | var eps = Settings.Debug.AlternativeEdgeSettings.OutlineTransitionThreshold.Value; 83 | var aaWidth = Settings.Debug.AlternativeEdgeSettings.OutlineFeatherWidth.Value; 84 | 85 | Parallel.For( 86 | 0, maxY, y => 87 | { 88 | for (var x = 0; x < maxX; x++) 89 | { 90 | var t = binaryMap[x, y].PackedValue / 255f; 91 | 92 | var walkableToEdge = Clamp((t - (eps - aaWidth)) / aaWidth, 0f, 1f); 93 | var edgeToUnwalkable = Clamp((t - (1f - eps)) / aaWidth, 0f, 1f); 94 | 95 | var walkableColor = new Vector4(1f, 1f, 1f, 0.00f); 96 | var outlineTarget = Settings.TerrainColor.Value.ToVector4().ToVector4Num(); 97 | 98 | var color = Lerp(walkableColor, outlineTarget, walkableToEdge); 99 | color = Lerp(color, color with { W = 0f }, edgeToUnwalkable); 100 | 101 | color = new Vector4(Clamp(color.X, 0f, 1f), Clamp(color.Y, 0f, 1f), Clamp(color.Z, 0f, 1f), Clamp(color.W, 0f, 1f)); 102 | 103 | image[x, y] = new Rgba32(color.X, color.Y, color.Z, color.W); 104 | } 105 | }); 106 | } 107 | else 108 | { 109 | var unwalkableMask = Vector4.UnitX + Vector4.UnitW; 110 | var walkableMask = Vector4.UnitY + Vector4.UnitW; 111 | if (Settings.Debug.DisableHeightAdjust) 112 | { 113 | Parallel.For(0, maxY, y => 114 | { 115 | for (var x = 0; x < maxX; x++) 116 | { 117 | var terrainType = _processedTerrainData[y][x]; 118 | image[x, y] = new Rgba32(terrainType is 0 ? unwalkableMask : walkableMask); 119 | } 120 | }); 121 | } 122 | else 123 | { 124 | Parallel.For(0, maxY, y => 125 | { 126 | for (var x = 0; x < maxX; x++) 127 | { 128 | var cellData = gridHeightData[y][x / 2 * 2]; 129 | 130 | //basically, offset x and y by half the offset z would cause when rendering in 3d 131 | var heightOffset = (int)(cellData / GridToWorldMultiplier / 2); 132 | var offsetX = x - heightOffset; 133 | var offsetY = y - heightOffset; 134 | var terrainType = _processedTerrainData[y][x]; 135 | if (offsetX >= 0 && offsetX < maxX && offsetY >= 0 && offsetY < maxY) 136 | { 137 | image[offsetX, offsetY] = new Rgba32(terrainType is 0 ? unwalkableMask : walkableMask); 138 | } 139 | } 140 | }); 141 | } 142 | 143 | if (!Settings.Debug.StandardEdgeSettings.SkipNeighborFill) 144 | { 145 | Parallel.For(0, maxY, y => 146 | { 147 | for (var x = 0; x < maxX; x++) 148 | { 149 | //this fills in the blanks that are left over from the height projection 150 | if (image[x, y].ToVector4() == Vector4.Zero) 151 | { 152 | var countWalkable = 0; 153 | var countUnwalkable = 0; 154 | for (var xO = -1; xO < 2; xO++) 155 | { 156 | for (var yO = -1; yO < 2; yO++) 157 | { 158 | var xx = x + xO; 159 | var yy = y + yO; 160 | if (xx >= 0 && xx < maxX && yy >= 0 && yy < maxY) 161 | { 162 | var nPixel = image[x + xO, y + yO].ToVector4(); 163 | if (nPixel == walkableMask) 164 | countWalkable++; 165 | else if (nPixel == unwalkableMask) 166 | countUnwalkable++; 167 | } 168 | } 169 | } 170 | 171 | image[x, y] = new Rgba32(countWalkable > countUnwalkable ? walkableMask : unwalkableMask); 172 | } 173 | } 174 | }); 175 | } 176 | 177 | if (!Settings.Debug.StandardEdgeSettings.SkipEdgeDetector) 178 | { 179 | var edgeDetector = new EdgeDetectorProcessor(EdgeDetectorKernel.Laplacian5x5, false) 180 | .CreatePixelSpecificProcessor(configuration, image, image.Bounds()); 181 | edgeDetector.Execute(); 182 | } 183 | 184 | if (!Settings.Debug.StandardEdgeSettings.SkipRecoloring) 185 | { 186 | image.Mutate(configuration, c => c.ProcessPixelRowsAsVector4((row, p) => 187 | { 188 | for (var x = 0; x < row.Length - 0; x++) 189 | { 190 | row[x] = row[x] switch 191 | { 192 | { X: 1 } => Settings.TerrainColor.Value.ToImguiVec4(), 193 | { X: 0 } => Vector4.Zero, 194 | var s => s 195 | }; 196 | } 197 | })); 198 | } 199 | } 200 | } 201 | 202 | if (Math.Max(image.Height, image.Width) > Settings.MaximumMapTextureDimension) 203 | { 204 | var (newWidth, newHeight) = (image.Width, image.Height); 205 | if (image.Height > image.Width) 206 | { 207 | newWidth = newWidth * Settings.MaximumMapTextureDimension / newHeight; 208 | newHeight = Settings.MaximumMapTextureDimension; 209 | } 210 | else 211 | { 212 | newHeight = newHeight * Settings.MaximumMapTextureDimension / newWidth; 213 | newWidth = Settings.MaximumMapTextureDimension; 214 | } 215 | 216 | var targetSize = new Size(newWidth, newHeight); 217 | var resizer = new ResizeProcessor(new ResizeOptions { Size = targetSize }, image.Size()) 218 | .CreatePixelSpecificCloningProcessor(configuration, image, image.Bounds()); 219 | resizer.Execute(); 220 | } 221 | 222 | //unfortunately the library doesn't respect our allocation settings above 223 | 224 | using var imageCopy = image.Clone(configuration); 225 | Graphics.LowLevel.AddOrUpdateTexture(TextureName, imageCopy); 226 | } 227 | } -------------------------------------------------------------------------------- /Radar.Pathfinding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using ExileCore.PoEMemory.Components; 10 | using ExileCore.PoEMemory.MemoryObjects; 11 | using ExileCore.Shared.Helpers; 12 | using GameOffsets; 13 | using GameOffsets.Native; 14 | using Newtonsoft.Json; 15 | using SharpDX; 16 | using Vector2 = System.Numerics.Vector2; 17 | 18 | namespace Radar; 19 | 20 | public partial class Radar 21 | { 22 | private Func>, CancellationToken, Task> _addRouteAction; 23 | private Func _getColor; 24 | 25 | private void LoadTargets() 26 | { 27 | var fileText = File.ReadAllText(Path.Combine(DirectoryFullName, "targets.json")); 28 | _targetDescriptions = JsonConvert.DeserializeObject>>(fileText); 29 | } 30 | 31 | private void RestartPathFinding() 32 | { 33 | StopPathFinding(); 34 | StartPathFinding(); 35 | } 36 | 37 | private void StartPathFinding() 38 | { 39 | if (Settings.PathfindingSettings.ShowPathsToTargetsOnMap) 40 | { 41 | var e = RainbowColors.GetEnumerator(); 42 | _getColor = () => 43 | { 44 | if (!e.MoveNext()) 45 | { 46 | e = RainbowColors.GetEnumerator(); 47 | if (!e.MoveNext()) 48 | { 49 | return Color.Green; 50 | } 51 | } 52 | 53 | return e.Current; 54 | }; 55 | var pf = new PathFinder(_processedTerrainData, new[] { 1, 2, 3, 4, 5 }); 56 | _addRouteAction = (point, callback, cancellationToken) => Task.Run(() => FindPath(pf, point, callback, cancellationToken), cancellationToken); 57 | foreach (var (location, target) in _clusteredTargetLocations 58 | .SelectMany(x => x.Value.Locations.Select(loc=>(loc, x.Value.Target))) 59 | .DistinctBy(x => x.loc)) 60 | { 61 | AddRoute(location, target, null); 62 | } 63 | } 64 | } 65 | 66 | private void AddRoute(Vector2 target, TargetDescription targetDescription, Entity entity) 67 | { 68 | var color = _getColor(); 69 | 70 | Color GetWorldColor() => Settings.PathfindingSettings.WorldPathSettings.UseRainbowColorsForPaths 71 | ? color 72 | : Settings.PathfindingSettings.WorldPathSettings.DefaultPathColor; 73 | 74 | Color GetMapColor() => Settings.PathfindingSettings.UseRainbowColorsForMapPaths 75 | ? color 76 | : Settings.PathfindingSettings.DefaultMapPathColor; 77 | 78 | var routes = _routes; 79 | 80 | var cancellationToken = _findPathsCts.Token; 81 | if (targetDescription.TargetType == TargetType.Entity && entity != null) 82 | { 83 | var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 84 | cancellationToken = cts.Token; 85 | 86 | async Task CheckEntity() 87 | { 88 | while (entity.IsValid && entity.GetComponent()?.IsOpened != true) 89 | { 90 | await Task.Delay(100, cancellationToken); 91 | } 92 | 93 | await cts.CancelAsync(); 94 | routes.Remove(target, out _); 95 | } 96 | 97 | _ = CheckEntity(); 98 | } 99 | 100 | AddRoute(target, targetDescription, path => 101 | { 102 | if (path != null) 103 | { 104 | Func customColorFunc = null; 105 | if (targetDescription.Color != null) 106 | { 107 | var color = Color.FromAbgr(uint.Parse(targetDescription.Color, NumberStyles.HexNumber)); 108 | customColorFunc = () => color; 109 | } 110 | 111 | var rd = new RouteDescription 112 | { 113 | Path = path, 114 | MapColor = customColorFunc ?? GetMapColor, 115 | WorldColor = customColorFunc ?? GetWorldColor 116 | }; 117 | routes.AddOrUpdate(target, rd, (_, _) => rd); 118 | } 119 | }, cancellationToken); 120 | } 121 | 122 | private Task AddRoute(Vector2 target, TargetDescription targetDescription, Action> callback, CancellationToken cancellationToken) 123 | { 124 | if (_addRouteAction == null) 125 | { 126 | return Task.FromException(new Exception("Pathfinding wasn't started yet")); 127 | } 128 | 129 | return _addRouteAction(target, callback, cancellationToken); 130 | } 131 | 132 | private void StopPathFinding() 133 | { 134 | _findPathsCts.Cancel(); 135 | _findPathsCts = new CancellationTokenSource(); 136 | _routes = new ConcurrentDictionary(); 137 | _addRouteAction = null; 138 | } 139 | 140 | private async Task WaitUntilPluginEnabled(CancellationToken cancellationToken) 141 | { 142 | while (!Settings.Enable) 143 | { 144 | await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); 145 | } 146 | } 147 | 148 | private async Task FindPath(PathFinder pf, Vector2 point, Action> callback, CancellationToken cancellationToken) 149 | { 150 | var playerPosition = GetPlayerPosition(); 151 | foreach (var elem in pf.RunFirstScan(new Vector2i((int)playerPosition.X, (int)playerPosition.Y), new Vector2i((int)point.X, (int)point.Y))) 152 | { 153 | await WaitUntilPluginEnabled(cancellationToken); 154 | if (cancellationToken.IsCancellationRequested) 155 | { 156 | return; 157 | } 158 | 159 | if (elem.Any()) 160 | { 161 | callback(elem); 162 | } 163 | } 164 | 165 | while (true) 166 | { 167 | await WaitUntilPluginEnabled(cancellationToken); 168 | var newPosition = GetPlayerPosition(); 169 | if (playerPosition == newPosition) 170 | { 171 | await Task.Delay(100, cancellationToken); 172 | continue; 173 | } 174 | 175 | if (cancellationToken.IsCancellationRequested) 176 | { 177 | return; 178 | } 179 | 180 | playerPosition = newPosition; 181 | var path = pf.FindPath(new Vector2i((int)playerPosition.X, (int)playerPosition.Y), new Vector2i((int)point.X, (int)point.Y)); 182 | callback(path); 183 | } 184 | } 185 | 186 | private ConcurrentDictionary> GetTargets() 187 | { 188 | return new ConcurrentDictionary>(GetTileTargets() 189 | .ToLookup(x => x.Key, x => x.Value) 190 | .ToDictionary(x => x.Key, x => x.SelectMany(v => v).ToList())); 191 | } 192 | 193 | //private Dictionary> GetEntityTargets() 194 | //{ 195 | // return GameController.Entities.Where(x => x.HasComponent()).Where(x => _currentZoneTargetEntityPaths.ContainsKey(x.Path)) 196 | // .ToLookup(x => x.Path, x => x.GetComponent().GridPosNum.Truncate()) 197 | // .ToDictionary(x => x.Key, x => x.ToList()); 198 | //} 199 | 200 | private Dictionary> GetTileTargets() 201 | { 202 | var tileData = GameController.Memory.ReadStdVector(_terrainMetadata.TgtArray); 203 | var ret = new ConcurrentDictionary>(); 204 | Parallel.For(0, tileData.Length, tileNumber => 205 | { 206 | var tgtTileStruct = GameController.Memory.Read(tileData[tileNumber].TgtFilePtr); 207 | var key2 = GameController.Memory.Read(tgtTileStruct.TgtDetailPtr) 208 | .name.ToString(GameController.Memory); 209 | var coordinate = new Vector2i( 210 | tileNumber % _terrainMetadata.NumCols * TileToGridConversion, 211 | tileNumber / _terrainMetadata.NumCols * TileToGridConversion); 212 | 213 | if (Settings.PathfindingSettings.IncludeTilePathsAsTargets) 214 | { 215 | var key1 = tgtTileStruct.TgtPath.ToString(GameController.Memory); 216 | if (!string.IsNullOrEmpty(key1)) 217 | { 218 | ret.GetOrAdd(key1, _ => new ConcurrentQueue()).Enqueue(coordinate); 219 | } 220 | } 221 | 222 | if (!string.IsNullOrEmpty(key2)) 223 | { 224 | ret.GetOrAdd(key2, _ => new ConcurrentQueue()).Enqueue(coordinate); 225 | } 226 | }); 227 | return ret.ToDictionary(k => k.Key, k => k.Value.ToList()); 228 | } 229 | 230 | private bool IsDescriptionInArea(string descriptionAreaPattern) 231 | { 232 | return GameController.Area.CurrentArea.Area.RawName.Like(descriptionAreaPattern); 233 | } 234 | 235 | private IEnumerable GetTargetDescriptionsInArea() 236 | { 237 | return _targetDescriptions.Where(x => IsDescriptionInArea(x.Key)).SelectMany(x => x.Value); 238 | } 239 | 240 | private ConcurrentDictionary ClusterTargets() 241 | { 242 | var tileMap = new ConcurrentDictionary(); 243 | Parallel.ForEach(_targetDescriptionsInArea.Values, new ParallelOptions { MaxDegreeOfParallelism = 1 }, target => 244 | { 245 | var locations = ClusterTarget(target); 246 | if (locations != null) 247 | { 248 | tileMap[target.Name] = locations; 249 | } 250 | }); 251 | return tileMap; 252 | } 253 | 254 | private IReadOnlyCollection GetLocationsFromTilePattern(string tilePattern) 255 | { 256 | var regex = tilePattern.ToLikeRegex(); 257 | return _allTargetLocations.Where(x => regex.IsMatch(x.Key)).SelectMany(x => x.Value).ToList(); 258 | } 259 | 260 | private TargetLocations ClusterTarget(TargetDescription target) 261 | { 262 | var expectedCount = target.ExpectedCount; 263 | var targetName = target.Name; 264 | var locations = ClusterTarget(targetName, expectedCount); 265 | if (locations == null) return null; 266 | return new TargetLocations 267 | { 268 | Locations = locations, 269 | Target = target, 270 | }; 271 | } 272 | 273 | private Vector2[] ClusterTarget(string targetName, int expectedCount) 274 | { 275 | var tileList = GetLocationsFromTilePattern(targetName); 276 | if (tileList is not { Count: > 0 }) 277 | { 278 | return null; 279 | } 280 | 281 | var clusterIndexes = KMeans.Cluster(tileList.Select(x => new Vector2d(x.X, x.Y)).ToArray(), expectedCount); 282 | var resultList = new List(); 283 | foreach (var tileGroup in tileList.Zip(clusterIndexes).GroupBy(x => x.Second)) 284 | { 285 | var v = new Vector2(); 286 | var count = 0; 287 | foreach (var (vector, _) in tileGroup) 288 | { 289 | var mult = IsGridWalkable(vector) ? 100 : 1; 290 | v += vector * mult; 291 | count += mult; 292 | } 293 | 294 | v /= count; 295 | var replacement = tileGroup.Select(tile => new Vector2i(tile.First.X, tile.First.Y)) 296 | .Where(IsGridWalkable) 297 | .OrderBy(x => (x - v).LengthSquared()) 298 | .Select(x => (Vector2i?)x) 299 | .FirstOrDefault(); 300 | if (replacement != null) 301 | { 302 | v = replacement.Value; 303 | } 304 | 305 | if (!IsGridWalkable(v.Truncate())) 306 | { 307 | v = GetAllNeighborTiles(v.Truncate()).First(IsGridWalkable); 308 | } 309 | 310 | resultList.Add(v); 311 | } 312 | 313 | var locations = resultList.Distinct().ToArray(); 314 | return locations; 315 | } 316 | 317 | private bool IsGridWalkable(Vector2i tile) 318 | { 319 | return _processedTerrainData[tile.Y][tile.X] is 5 or 4; 320 | } 321 | 322 | private IEnumerable GetAllNeighborTiles(Vector2i start) 323 | { 324 | foreach (var range in Enumerable.Range(1, 100000)) 325 | { 326 | var xStart = Math.Max(0, start.X - range); 327 | var yStart = Math.Max(0, start.Y - range); 328 | var xEnd = Math.Min(_areaDimensions.Value.X, start.X + range); 329 | var yEnd = Math.Min(_areaDimensions.Value.Y, start.Y + range); 330 | for (var x = xStart; x <= xEnd; x++) 331 | { 332 | yield return new Vector2i(x, yStart); 333 | yield return new Vector2i(x, yEnd); 334 | } 335 | 336 | for (var y = yStart + 1; y <= yEnd - 1; y++) 337 | { 338 | yield return new Vector2i(xStart, y); 339 | yield return new Vector2i(xEnd, y); 340 | } 341 | 342 | if (xStart == 0 && yStart == 0 && xEnd == _areaDimensions.Value.X && yEnd == _areaDimensions.Value.Y) 343 | { 344 | break; 345 | } 346 | } 347 | } 348 | } -------------------------------------------------------------------------------- /Radar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.Linq; 6 | using System.Numerics; 7 | using System.Text.RegularExpressions; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using ExileCore; 11 | using ExileCore.PoEMemory.Components; 12 | using ExileCore.PoEMemory.Elements; 13 | using ExileCore.PoEMemory.MemoryObjects; 14 | using ExileCore.Shared; 15 | using ExileCore.Shared.Helpers; 16 | using GameOffsets; 17 | using GameOffsets.Native; 18 | using ImGuiNET; 19 | using Positioned = ExileCore.PoEMemory.Components.Positioned; 20 | 21 | namespace Radar; 22 | 23 | public partial class Radar : BaseSettingsPlugin 24 | { 25 | private const string TextureName = "radar_minimap"; 26 | private const int TileToGridConversion = 23; 27 | private const int TileToWorldConversion = 250; 28 | public const float GridToWorldMultiplier = TileToWorldConversion / (float)TileToGridConversion; 29 | private const double CameraAngle = 38.7 * Math.PI / 180; 30 | private static readonly float CameraAngleCos = (float)Math.Cos(CameraAngle); 31 | private static readonly float CameraAngleSin = (float)Math.Sin(CameraAngle); 32 | private double _mapScale; 33 | 34 | private ConcurrentDictionary> _targetDescriptions = new(); 35 | private Vector2i? _areaDimensions; 36 | private TerrainData _terrainMetadata; 37 | private float[][] _heightData; 38 | private int[][] _processedTerrainData; 39 | private int[][] _processedTerrainTargetingData; 40 | private Dictionary _targetDescriptionsInArea = new(); 41 | private List<(Regex, TargetDescription x)> _currentZoneTargetEntityPaths = new(); 42 | private CancellationTokenSource _findPathsCts = new CancellationTokenSource(); 43 | private ConcurrentDictionary _clusteredTargetLocations = new(); 44 | private ConcurrentDictionary> _allTargetLocations = new(); 45 | private ConcurrentDictionary> _locationsByPosition = new(); 46 | private SharpDX.RectangleF _rect; 47 | private ImDrawListPtr _backGroundWindowPtr; 48 | private ConcurrentDictionary _routes = new(); 49 | 50 | public override bool Initialise() 51 | { 52 | GameController.PluginBridge.SaveMethod("Radar.LookForRoute", 53 | (Vector2 target, Action> callback, CancellationToken cancellationToken) => 54 | AddRoute(target, null, callback, cancellationToken)); 55 | GameController.PluginBridge.SaveMethod("Radar.ClusterTarget", 56 | (string targetName, int expectedCount) => ClusterTarget(targetName, expectedCount)); 57 | 58 | Input.RegisterKey(Settings.ManuallyDumpInstance); 59 | Settings.ManuallyDumpInstance.OnValueChanged += () => { Input.RegisterKey(Settings.ManuallyDumpInstance); }; 60 | return true; 61 | } 62 | 63 | public override void AreaChange(AreaInstance area) 64 | { 65 | StopPathFinding(); 66 | if (GameController.Game.IsInGameState || GameController.Game.IsEscapeState) 67 | { 68 | _targetDescriptionsInArea = GetTargetDescriptionsInArea().DistinctBy(x => x.Name).ToDictionary(x => x.Name); 69 | _currentZoneTargetEntityPaths = _targetDescriptionsInArea.Values.Where(x => x.TargetType == TargetType.Entity).DistinctBy(x => x.Name).Select(x=>(x.Name.ToLikeRegex(), x)).ToList(); 70 | _terrainMetadata = GameController.IngameState.Data.DataStruct.Terrain; 71 | _heightData = GameController.IngameState.Data.RawTerrainHeightData; 72 | _allTargetLocations = GetTargets(); 73 | _locationsByPosition = new ConcurrentDictionary>(_allTargetLocations 74 | .SelectMany(x => x.Value.Select(y => (x.Key, y))) 75 | .ToLookup(x => x.y, x => x.Key) 76 | .ToDictionary(x => x.Key, x => x.ToList())); 77 | _areaDimensions = GameController.IngameState.Data.AreaDimensions; 78 | _processedTerrainData = GameController.IngameState.Data.RawPathfindingData; 79 | _processedTerrainTargetingData = GameController.IngameState.Data.RawTerrainTargetingData; 80 | 81 | if (Settings.AutoDumpInstanceOnAreaChange) 82 | { 83 | DumpInstanceData($@"{DirectoryFullName}\instance_dumps\{GameController.Area.CurrentArea.Area.RawName}_{SanitizeAreaName(GameController.Area.CurrentArea.Area.Name)}.json"); 84 | } 85 | 86 | GenerateMapTexture(); 87 | _clusteredTargetLocations = ClusterTargets(); 88 | StartPathFinding(); 89 | } 90 | } 91 | 92 | private static string SanitizeAreaName(string name) 93 | { 94 | return name.Replace(" ", "_") 95 | .Replace(":", "") 96 | .Replace("/", "") 97 | .Replace("\\", ""); 98 | } 99 | 100 | public override void DrawSettings() 101 | { 102 | Settings.PathfindingSettings.CurrentZoneName.Value = GameController.Area.CurrentArea.Area.RawName; 103 | base.DrawSettings(); 104 | } 105 | 106 | private static readonly List RainbowColors = new List 107 | { 108 | Color.Red, 109 | Color.LightGreen, 110 | Color.White, 111 | Color.Yellow, 112 | Color.LightBlue, 113 | Color.Violet, 114 | Color.Blue, 115 | Color.Orange, 116 | Color.Indigo, 117 | }.Select(x => x.ToSharpDx()).ToList(); 118 | 119 | public override void OnLoad() 120 | { 121 | LoadTargets(); 122 | Settings.Reload.OnPressed = () => 123 | { 124 | Task.Run(() => 125 | { 126 | LoadTargets(); 127 | AreaChange(GameController.Area.CurrentArea); 128 | }); 129 | }; 130 | Settings.MaximumPathCount.OnValueChanged += (_, _) => { Task.Run(RestartPathFinding); }; 131 | Settings.TerrainColor.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 132 | Settings.Debug.DrawHeightMap.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 133 | Settings.Debug.StandardEdgeSettings.SkipEdgeDetector.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 134 | Settings.Debug.StandardEdgeSettings.SkipNeighborFill.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 135 | Settings.Debug.StandardEdgeSettings.SkipRecoloring.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 136 | Settings.Debug.DisableHeightAdjust.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 137 | Settings.MaximumMapTextureDimension.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 138 | Settings.Debug.AlternativeEdgeMethod.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 139 | Settings.Debug.AlternativeEdgeSettings.OutlineBlurSigma.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 140 | Settings.Debug.AlternativeEdgeSettings.OutlineTransitionThreshold.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 141 | Settings.Debug.AlternativeEdgeSettings.OutlineFeatherWidth.OnValueChanged += (_, _) => { GenerateMapTexture(); }; 142 | } 143 | 144 | public override void EntityAdded(Entity entity) 145 | { 146 | var positioned = entity.GetComponent(); 147 | if (positioned != null) 148 | { 149 | var path = entity.Path; 150 | if (_currentZoneTargetEntityPaths.FirstOrDefault(x=>x.Item1.IsMatch(path)).x is {} targetDescription) 151 | { 152 | bool alreadyContains = false; 153 | var truncatedPos = positioned.GridPosNum.Truncate(); 154 | _allTargetLocations.AddOrUpdate(targetDescription.Name, _ => [truncatedPos], 155 | // ReSharper disable once AssignmentInConditionalExpression 156 | (_, l) => (alreadyContains = l.Contains(truncatedPos)) ? l : [..l, truncatedPos]); 157 | _locationsByPosition.AddOrUpdate(truncatedPos, _ => [targetDescription.Name], 158 | (_, l) => l.Contains(targetDescription.Name) ? l : [..l, targetDescription.Name]); 159 | if (!alreadyContains) 160 | { 161 | var oldValue = _clusteredTargetLocations.GetValueOrDefault(targetDescription.Name); 162 | var newValue = _clusteredTargetLocations.AddOrUpdate(targetDescription.Name, 163 | _ => ClusterTarget(_targetDescriptionsInArea[targetDescription.Name]), 164 | (_, _) => ClusterTarget(_targetDescriptionsInArea[targetDescription.Name])); 165 | foreach (var newLocation in newValue.Locations.Except(oldValue?.Locations ?? [])) 166 | { 167 | AddRoute(newLocation, targetDescription, entity); 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | private Vector2 GetPlayerPosition() 175 | { 176 | var player = GameController.Game.IngameState.Data.LocalPlayer; 177 | var playerPositionComponent = player.GetComponent(); 178 | if (playerPositionComponent == null) 179 | return new Vector2(0, 0); 180 | var playerPosition = new Vector2(playerPositionComponent.GridX, playerPositionComponent.GridY); 181 | return playerPosition; 182 | } 183 | 184 | public override void Render() 185 | { 186 | if (Settings.ManuallyDumpInstance.PressedOnce()) 187 | { 188 | DumpInstanceData($@"{DirectoryFullName}\instance_dumps\{GameController.Area.CurrentArea.Area.RawName}_{SanitizeAreaName(GameController.Area.CurrentArea.Area.Name)}.json"); 189 | } 190 | 191 | var ingameUi = GameController.Game.IngameState.IngameUi; 192 | if (!Settings.Debug.IgnoreFullscreenPanels && 193 | ingameUi.FullscreenPanels.Any(x => x.IsVisible)) 194 | { 195 | return; 196 | } 197 | 198 | if (!Settings.Debug.IgnoreLargePanels && 199 | ingameUi.LargePanels.Any(x => x.IsVisible)) 200 | { 201 | return; 202 | } 203 | 204 | _rect = GameController.Window.GetWindowRectangle() with { Location = SharpDX.Vector2.Zero }; 205 | if (!Settings.Debug.DisableDrawRegionLimiting) 206 | { 207 | if (ingameUi.OpenRightPanel.IsVisible) 208 | { 209 | _rect.Right = ingameUi.OpenRightPanel.GetClientRectCache.Left; 210 | } 211 | 212 | if (ingameUi.OpenLeftPanel.IsVisible) 213 | { 214 | _rect.Left = ingameUi.OpenLeftPanel.GetClientRectCache.Right; 215 | } 216 | } 217 | 218 | ImGui.SetNextWindowSize(new Vector2(_rect.Width, _rect.Height)); 219 | ImGui.SetNextWindowPos(new Vector2(_rect.Left, _rect.Top)); 220 | 221 | ImGui.Begin("radar_background", 222 | ImGuiWindowFlags.NoDecoration | 223 | ImGuiWindowFlags.NoInputs | 224 | ImGuiWindowFlags.NoMove | 225 | ImGuiWindowFlags.NoScrollWithMouse | 226 | ImGuiWindowFlags.NoSavedSettings | 227 | ImGuiWindowFlags.NoFocusOnAppearing | 228 | ImGuiWindowFlags.NoBringToFrontOnFocus | 229 | ImGuiWindowFlags.NoBackground); 230 | 231 | _backGroundWindowPtr = ImGui.GetWindowDrawList(); 232 | var map = ingameUi.Map; 233 | var largeMap = map.LargeMap.AsObject(); 234 | if (largeMap.IsVisible) 235 | { 236 | var mapCenter = largeMap.MapCenter + new Vector2(Settings.Debug.MapCenterOffsetX, Settings.Debug.MapCenterOffsetY); 237 | _mapScale = largeMap.MapScale * Settings.CustomScale; 238 | DrawLargeMap(mapCenter); 239 | DrawTargets(mapCenter); 240 | } 241 | 242 | DrawWorldPaths(largeMap); 243 | ImGui.End(); 244 | } 245 | 246 | private void DrawWorldPaths(SubMap largeMap) 247 | { 248 | if (Settings.PathfindingSettings.WorldPathSettings.ShowPathsToTargets && 249 | (!largeMap.IsVisible || !Settings.PathfindingSettings.WorldPathSettings.ShowPathsToTargetsOnlyWithClosedMap)) 250 | { 251 | var player = GameController.Game.IngameState.Data.LocalPlayer; 252 | var playerRender = player?.GetComponent(); 253 | if (playerRender == null) 254 | return; 255 | var initPos = GameController.IngameState.Camera.WorldToScreen(playerRender.PosNum with { Z = playerRender.RenderStruct.Height }); 256 | foreach (var (route, offsetAmount) in _routes.Values 257 | .GroupBy(x => x.Path.Count < 2 ? 0 : (x.Path[1] - x.Path[0]) switch { var diff => Math.Atan2(diff.Y, diff.X) }) 258 | .SelectMany(group => group.Select((route, i) => (route, i - group.Count() / 2.0f + 0.5f)))) 259 | { 260 | var p0 = initPos; 261 | var p0WithOffset = p0; 262 | var i = 0; 263 | foreach (var elem in route.Path) 264 | { 265 | var p1 = GameController.IngameState.Camera.WorldToScreen( 266 | new Vector3(elem.X * GridToWorldMultiplier, elem.Y * GridToWorldMultiplier, _heightData[elem.Y][elem.X])); 267 | var offsetDirection = Settings.PathfindingSettings.WorldPathSettings.OffsetPaths 268 | ? (p1 - p0) switch { var s => new Vector2(s.Y, -s.X) / s.Length() } 269 | : Vector2.Zero; 270 | var finalOffset = offsetDirection * offsetAmount * Settings.PathfindingSettings.WorldPathSettings.PathThickness; 271 | p0 = p1; 272 | p1 += finalOffset; 273 | if (++i % Settings.PathfindingSettings.WorldPathSettings.DrawEveryNthSegment == 0) 274 | { 275 | if (_rect.Contains(p0WithOffset) || _rect.Contains(p1)) 276 | { 277 | Graphics.DrawLine(p0WithOffset, p1, Settings.PathfindingSettings.WorldPathSettings.PathThickness, route.WorldColor()); 278 | } 279 | else 280 | { 281 | break; 282 | } 283 | } 284 | 285 | p0WithOffset = p1; 286 | } 287 | } 288 | } 289 | } 290 | 291 | private void DrawBox(Vector2 p0, Vector2 p1, SharpDX.Color color) 292 | { 293 | _backGroundWindowPtr.AddRectFilled(p0, p1, color.ToImgui()); 294 | } 295 | 296 | private void DrawText(string text, Vector2 pos, SharpDX.Color color) 297 | { 298 | _backGroundWindowPtr.AddText(pos, color.ToImgui(), text); 299 | } 300 | 301 | private Vector2 TranslateGridDeltaToMapDelta(Vector2 delta, float deltaZ) 302 | { 303 | deltaZ /= GridToWorldMultiplier; //z is normally "world" units, translate to grid 304 | return (float)_mapScale * new Vector2((delta.X - delta.Y) * CameraAngleCos, (deltaZ - (delta.X + delta.Y)) * CameraAngleSin); 305 | } 306 | 307 | private void DrawLargeMap(Vector2 mapCenter) 308 | { 309 | if (!Settings.DrawWalkableMap || !Graphics.HasImage(TextureName) || _areaDimensions == null) 310 | return; 311 | var player = GameController.Game.IngameState.Data.LocalPlayer; 312 | var playerRender = player.GetComponent(); 313 | if (playerRender == null) 314 | return; 315 | var rectangleF = new RectangleF(-playerRender.GridPos().X, -playerRender.GridPos().Y, _areaDimensions.Value.X, _areaDimensions.Value.Y); 316 | var playerHeight = -playerRender.RenderStruct.Height; 317 | var p1 = mapCenter + TranslateGridDeltaToMapDelta(new Vector2(rectangleF.Left, rectangleF.Top), playerHeight); 318 | var p2 = mapCenter + TranslateGridDeltaToMapDelta(new Vector2(rectangleF.Right, rectangleF.Top), playerHeight); 319 | var p3 = mapCenter + TranslateGridDeltaToMapDelta(new Vector2(rectangleF.Right, rectangleF.Bottom), playerHeight); 320 | var p4 = mapCenter + TranslateGridDeltaToMapDelta(new Vector2(rectangleF.Left, rectangleF.Bottom), playerHeight); 321 | _backGroundWindowPtr.AddImageQuad(Graphics.GetTextureId(TextureName), p1, p2, p3, p4); 322 | } 323 | 324 | private void DrawTargets(Vector2 mapCenter) 325 | { 326 | var color = Settings.PathfindingSettings.TargetNameColor.Value; 327 | var player = GameController.Game.IngameState.Data.LocalPlayer; 328 | var playerRender = player.GetComponent(); 329 | if (playerRender == null) 330 | return; 331 | var playerPosition = new Vector2(playerRender.GridPos().X, playerRender.GridPos().Y); 332 | var playerHeight = -playerRender.RenderStruct.Height; 333 | var ithElement = 0; 334 | if (Settings.PathfindingSettings.ShowPathsToTargetsOnMap) 335 | { 336 | foreach (var route in _routes.Values) 337 | { 338 | ithElement++; 339 | ithElement %= 5; 340 | foreach (var elem in route.Path.Skip(ithElement).GetEveryNth(5)) 341 | { 342 | var mapDelta = TranslateGridDeltaToMapDelta(new Vector2(elem.X, elem.Y) - playerPosition, playerHeight + _heightData[elem.Y][elem.X]); 343 | DrawBox(mapCenter + mapDelta - new Vector2(2, 2), mapCenter + mapDelta + new Vector2(2, 2), route.MapColor()); 344 | } 345 | } 346 | } 347 | 348 | if (Settings.PathfindingSettings.ShowAllTargets) 349 | { 350 | foreach (var (location, texts) in _locationsByPosition) 351 | { 352 | var regex = string.IsNullOrEmpty(Settings.PathfindingSettings.TargetNameFilter) 353 | ? null 354 | : new Regex(Settings.PathfindingSettings.TargetNameFilter); 355 | 356 | bool TargetFilter(string t) => 357 | (regex?.IsMatch(t) ?? true) && 358 | _allTargetLocations.GetValueOrDefault(t) is { } list && list.Count <= Settings.PathfindingSettings.MaxTargetNameCount; 359 | 360 | var text = string.Join("\n", texts.Distinct().Where(TargetFilter)); 361 | var textOffset = Graphics.MeasureText(text) / 2f; 362 | var mapDelta = TranslateGridDeltaToMapDelta(location - playerPosition, playerHeight + _heightData[location.Y][location.X]); 363 | var mapPos = mapCenter + mapDelta; 364 | if (Settings.PathfindingSettings.EnableTargetNameBackground) 365 | DrawBox(mapPos - textOffset, mapPos + textOffset, Color.Black.ToSharpDx()); 366 | DrawText(text, mapPos - textOffset, color); 367 | } 368 | } 369 | else if (Settings.PathfindingSettings.ShowSelectedTargets) 370 | { 371 | foreach (var (_, description) in _clusteredTargetLocations) 372 | { 373 | foreach (var clusterPosition in description.Locations) 374 | { 375 | float clusterHeight = 0; 376 | if (clusterPosition.X < _heightData[0].Length && clusterPosition.Y < _heightData.Length) 377 | clusterHeight = _heightData[(int)clusterPosition.Y][(int)clusterPosition.X]; 378 | var text = description.DisplayName; 379 | var textOffset = Graphics.MeasureText(text) / 2f; 380 | var mapDelta = TranslateGridDeltaToMapDelta(clusterPosition - playerPosition, playerHeight + clusterHeight); 381 | var mapPos = mapCenter + mapDelta; 382 | if (Settings.PathfindingSettings.EnableTargetNameBackground) 383 | DrawBox(mapPos - textOffset, mapPos + textOffset, Color.Black.ToSharpDx()); 384 | DrawText(text, mapPos - textOffset, color); 385 | } 386 | } 387 | } 388 | } 389 | } -------------------------------------------------------------------------------- /Radar.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0-windows 4 | Library 5 | x64 6 | true 7 | true 8 | true 9 | embedded 10 | $(MSBuildProjectDirectory)=$(MSBuildProjectName) 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | $(exapiPackage)\ExileCore.dll 22 | False 23 | 24 | 25 | $(exapiPackage)\GameOffsets.dll 26 | False 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Radar.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31919.166 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radar", "Radar.csproj", "{58C24999-EA8B-47FC-B789-9CE9926AD701}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {58C24999-EA8B-47FC-B789-9CE9926AD701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {58C24999-EA8B-47FC-B789-9CE9926AD701}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {58C24999-EA8B-47FC-B789-9CE9926AD701}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {58C24999-EA8B-47FC-B789-9CE9926AD701}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {68562BA3-2A00-489C-8CAC-57F6FDBA8807} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /RadarSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Windows.Forms; 3 | using ExileCore.Shared.Attributes; 4 | using ExileCore.Shared.Helpers; 5 | using ExileCore.Shared.Interfaces; 6 | using ExileCore.Shared.Nodes; 7 | using Newtonsoft.Json; 8 | 9 | namespace Radar; 10 | 11 | [Submenu] 12 | public class DebugSettings 13 | { 14 | public ToggleNode DrawHeightMap { get; set; } = new ToggleNode(false); 15 | public ToggleNode DisableHeightAdjust { get; set; } = new ToggleNode(false); 16 | public ToggleNode AlternativeEdgeMethod { get; set; } = new ToggleNode(false); 17 | [ConditionalDisplay(nameof(AlternativeEdgeMethod))] 18 | public AlternativeEdge AlternativeEdgeSettings { get; set; } = new AlternativeEdge(); 19 | [ConditionalDisplay(nameof(AlternativeEdgeMethod), false)] 20 | public CurrentEdge StandardEdgeSettings { get; set; } = new CurrentEdge(); 21 | public ToggleNode DisableDrawRegionLimiting { get; set; } = new ToggleNode(false); 22 | public ToggleNode IgnoreFullscreenPanels { get; set; } = new ToggleNode(false); 23 | public ToggleNode IgnoreLargePanels { get; set; } = new ToggleNode(false); 24 | public RangeNode MapCenterOffsetX { get; set; } = new RangeNode(0, -1000, 1000); 25 | public RangeNode MapCenterOffsetY { get; set; } = new RangeNode(0, -1000, 1000); 26 | } 27 | 28 | [Submenu] 29 | public class WorldPathSettings 30 | { 31 | public ToggleNode ShowPathsToTargets { get; set; } = new ToggleNode(true); 32 | public ToggleNode ShowPathsToTargetsOnlyWithClosedMap { get; set; } = new ToggleNode(true); 33 | public ToggleNode UseRainbowColorsForPaths { get; set; } = new ToggleNode(true); 34 | public ColorNode DefaultPathColor { get; set; } = new ColorNode(Color.Red.ToSharpDx()); 35 | public ToggleNode OffsetPaths { get; set; } = new ToggleNode(true); 36 | public RangeNode PathThickness { get; set; } = new RangeNode(1, 1, 20); 37 | public RangeNode DrawEveryNthSegment { get; set; } = new RangeNode(1, 1, 10); 38 | } 39 | 40 | [Submenu] 41 | public class AlternativeEdge 42 | { 43 | public RangeNode OutlineBlurSigma { get; set; } = new RangeNode(0.438f, 0f, 20f); 44 | public RangeNode OutlineTransitionThreshold { get; set; } = new RangeNode(0.070f, 0f, 1f); 45 | public RangeNode OutlineFeatherWidth { get; set; } = new RangeNode(0.070f, 0f, 1f); 46 | } 47 | 48 | [Submenu] 49 | public class CurrentEdge 50 | { 51 | public ToggleNode SkipNeighborFill { get; set; } = new ToggleNode(false); 52 | public ToggleNode SkipEdgeDetector { get; set; } = new ToggleNode(false); 53 | public ToggleNode SkipRecoloring { get; set; } = new ToggleNode(false); 54 | } 55 | 56 | [Submenu] 57 | public class PathfindingSettings 58 | { 59 | [Menu(null, "For debugging only")] 60 | [JsonIgnore] 61 | public TextNode CurrentZoneName { get; set; } = new TextNode(""); 62 | 63 | [Menu(null, "For debugging only")] 64 | [JsonIgnore] 65 | public TextNode TargetNameFilter { get; set; } = new TextNode(""); 66 | 67 | public ToggleNode ShowPathsToTargetsOnMap { get; set; } = new ToggleNode(true); 68 | public ColorNode DefaultMapPathColor { get; set; } = new ColorNode(Color.Green.ToSharpDx()); 69 | public ToggleNode UseRainbowColorsForMapPaths { get; set; } = new ToggleNode(true); 70 | public ToggleNode ShowAllTargets { get; set; } = new ToggleNode(false); 71 | 72 | [Menu(null, "Do not show targets that occur more than X times per zone")] 73 | [ConditionalDisplay(nameof(ShowAllTargets))] 74 | public RangeNode MaxTargetNameCount { get; set; } = new RangeNode(10, 1, 100); 75 | 76 | public ToggleNode IncludeTilePathsAsTargets { get; set; } = new ToggleNode(true); 77 | public ToggleNode ShowSelectedTargets { get; set; } = new ToggleNode(true); 78 | public ToggleNode EnableTargetNameBackground { get; set; } = new ToggleNode(true); 79 | public ColorNode TargetNameColor { get; set; } = new ColorNode(Color.Violet.ToSharpDx()); 80 | public WorldPathSettings WorldPathSettings { get; set; } = new WorldPathSettings(); 81 | } 82 | 83 | public class RadarSettings : ISettings 84 | { 85 | [JsonIgnore] 86 | public ButtonNode Reload { get; set; } = new ButtonNode(); 87 | public ToggleNode AutoDumpInstanceOnAreaChange { get; set; } = new ToggleNode(false); 88 | public HotkeyNode ManuallyDumpInstance { get; set; } = new HotkeyNode(Keys.None); 89 | public ToggleNode Enable { get; set; } = new ToggleNode(true); 90 | public RangeNode CustomScale { get; set; } = new RangeNode(1, 0.1f, 10); 91 | public ToggleNode DrawWalkableMap { get; set; } = new ToggleNode(true); 92 | public ColorNode TerrainColor { get; set; } = new ColorNode(Color.FromArgb(150, 150, 150, 150).ToSharpDx()); 93 | public RangeNode MaximumMapTextureDimension { get; set; } = new RangeNode(4096, 100, 4096); 94 | public RangeNode MaximumPathCount { get; set; } = new RangeNode(1000, 0, 1000); 95 | public PathfindingSettings PathfindingSettings { get; set; } = new PathfindingSettings(); 96 | public DebugSettings Debug { get; set; } = new DebugSettings(); 97 | } -------------------------------------------------------------------------------- /RouteDescription.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using GameOffsets.Native; 4 | using SharpDX; 5 | 6 | namespace Radar; 7 | 8 | public class RouteDescription 9 | { 10 | public List Path { get; set; } 11 | public Func MapColor { get; set; } 12 | public Func WorldColor { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /TargetDescription.cs: -------------------------------------------------------------------------------- 1 | namespace Radar; 2 | 3 | public record TargetDescription 4 | { 5 | public string Name { get; set; } = ""; 6 | public string DisplayName { get; set; } 7 | public int ExpectedCount { get; set; } = 1; 8 | public TargetType TargetType { get; set; } 9 | public string Color { get; set; } 10 | } -------------------------------------------------------------------------------- /TargetLocations.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Radar; 4 | 5 | public class TargetLocations 6 | { 7 | public string DisplayName => string.IsNullOrWhiteSpace(Target.DisplayName) ? Target.Name : Target.DisplayName; 8 | public Vector2[] Locations { get; set; } 9 | public TargetDescription Target { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /TargetType.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | 4 | namespace Radar; 5 | 6 | [JsonConverter(typeof(StringEnumConverter))] 7 | public enum TargetType 8 | { 9 | Tile, 10 | Entity 11 | } 12 | -------------------------------------------------------------------------------- /Vector2d.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Radar; 4 | 5 | public readonly record struct Vector2d(double X, double Y) 6 | { 7 | public readonly double X = X; 8 | public readonly double Y = Y; 9 | 10 | public double Length => Math.Sqrt(X * X + Y * Y); 11 | 12 | public static Vector2d operator -(Vector2d v1, Vector2d v2) 13 | { 14 | return new Vector2d(v1.X - v2.X, v1.Y - v2.Y); 15 | } 16 | 17 | public static Vector2d operator +(Vector2d v1, Vector2d v2) 18 | { 19 | return new Vector2d(v1.X + v2.X, v1.Y + v2.Y); 20 | } 21 | 22 | public static Vector2d operator /(Vector2d v, double d) 23 | { 24 | return new Vector2d(v.X / d, v.Y / d); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": [ 3 | { 4 | "Name": "waypoint", 5 | "ExpectedCount": 1, 6 | "DisplayName": "Waypoint" 7 | }, 8 | { 9 | "Name": "npc1", 10 | "ExpectedCount": 1, 11 | "DisplayName": "Breaker of Oaths - Warlock [Wild/Purple]" 12 | }, 13 | { 14 | "Name": "npc2", 15 | "ExpectedCount": 1, 16 | "DisplayName": "The Warden of Eaves - Maji [Vivid/Yellow]" 17 | }, 18 | { 19 | "Name": "npc3", 20 | "ExpectedCount": 1, 21 | "DisplayName": "The Primal Huntress - Primalist [Primal/Blue]" 22 | }, 23 | { 24 | "Name": "sticksinmud", 25 | "ExpectedCount": 1, 26 | "DisplayName": "Grisly Trophy [Wild/Purple] - Body Loot" 27 | }, 28 | { 29 | "Name": "boneyard", 30 | "ExpectedCount": 1, 31 | "DisplayName": "Boneyard [Primal/Blue] - 50/50 monsters and loot" 32 | }, 33 | { 34 | "Name": "stonehenge", 35 | "ExpectedCount": 1, 36 | "DisplayName": "Stonehenge [Vivid/Yellow]" 37 | }, 38 | { 39 | "Name": "witchorb", 40 | "ExpectedCount": 1, 41 | "DisplayName": "3rd Maji Quest" 42 | }, 43 | { 44 | "Name": "Metadata/Terrain/Woods/Woods/AzmeriLeague/Features/arenaTransition_01.tdt", 45 | "ExpectedCount": 1, 46 | "DisplayName": "Boss stronghold" 47 | } 48 | ], 49 | "2_6_2": [ 50 | { 51 | "Name": "beachtownnorth", 52 | "ExpectedCount": 1, 53 | "DisplayName": "Lioneye's Watch" 54 | }, 55 | { 56 | "Name": "block", 57 | "ExpectedCount": 1, 58 | "DisplayName": "Area Transitions" 59 | } 60 | ], 61 | "1_1_2": [ 62 | { 63 | "Name": "beachtownnorth", 64 | "ExpectedCount": 1, 65 | "DisplayName": "Lioneye's Watch" 66 | }, 67 | { 68 | "Name": "karui", 69 | "ExpectedCount": 1, 70 | "DisplayName": "Both Transitions" 71 | } 72 | ], 73 | "1_1_2a": [ 74 | { 75 | "Name": "karui", 76 | "ExpectedCount": 1, 77 | "DisplayName": "The Coast" 78 | }, 79 | { 80 | "Name": "medicineboat", 81 | "ExpectedCount": 1, 82 | "DisplayName": "Hailrake" 83 | } 84 | ], 85 | "1_1_3": [ 86 | { 87 | "Name": "watercave", 88 | "ExpectedCount": 1, 89 | "DisplayName": "The Submerged Passage" 90 | }, 91 | { 92 | "Name": "transition", 93 | "ExpectedCount": 1, 94 | "DisplayName": "The Fetid Pool" 95 | }, 96 | { 97 | "Name": "rhoanest", 98 | "ExpectedCount": 3, 99 | "DisplayName": "Rhoa Nest" 100 | } 101 | ], 102 | "1_1_4_1": [ 103 | { 104 | "Name": "entrance", 105 | "ExpectedCount": 1, 106 | "DisplayName": "The Mud Flats" 107 | }, 108 | { 109 | "Name": "towaterydepths", 110 | "ExpectedCount": 1, 111 | "DisplayName": "The Flooded Depths" 112 | }, 113 | { 114 | "Name": "entranceup", 115 | "ExpectedCount": 1, 116 | "DisplayName": "The Ledge" 117 | } 118 | ], 119 | "1_1_5": [ 120 | { 121 | "Name": "caveentrance", 122 | "ExpectedCount": 1, 123 | "DisplayName": "The Submerged Passage" 124 | }, 125 | { 126 | "Name": "passageway", 127 | "ExpectedCount": 1, 128 | "DisplayName": "The Climb" 129 | } 130 | ], 131 | "1_1_6": [ 132 | { 133 | "Name": "prisonback", 134 | "ExpectedCount": 1, 135 | "DisplayName": "The Lower Prison" 136 | }, 137 | { 138 | "Name": "passageway", 139 | "ExpectedCount": 1, 140 | "DisplayName": "The Ledge" 141 | } 142 | ], 143 | "1_1_7_1": [ 144 | { 145 | "Name": "entrance", 146 | "ExpectedCount": 1, 147 | "DisplayName": "The Climb" 148 | }, 149 | { 150 | "Name": "LabyrinthDoor", 151 | "ExpectedCount": 1, 152 | "DisplayName": "Trial of Ascendency" 153 | }, 154 | { 155 | "Name": "entranceup", 156 | "ExpectedCount": 1, 157 | "DisplayName": "The Upper Prison" 158 | } 159 | ], 160 | "1_1_7_2": [ 161 | { 162 | "Name": "entranceup", 163 | "ExpectedCount": 1, 164 | "DisplayName": "The Warden's Quarters" 165 | }, 166 | { 167 | "Name": "bossentrance2", 168 | "ExpectedCount": 1, 169 | "DisplayName": "The Warden's Chambers" 170 | }, 171 | { 172 | "Name": "exitdoor", 173 | "ExpectedCount": 1, 174 | "DisplayName": "Prisoner's Gate" 175 | }, 176 | { 177 | "Name": "entrancedown", 178 | "ExpectedCount": 2, 179 | "DisplayName": "Area Transition" 180 | }, 181 | { 182 | "Name": "Metadata/Terrain/Prison/Hidden/hidden_prison_SecretWall_OpenAnimation_v01_01.tdt", 183 | "ExpectedCount": 1, 184 | "DisplayName": "Flask Strongbox" 185 | } 186 | ], 187 | "1_1_8": [ 188 | { 189 | "Name": "shippassagea", 190 | "ExpectedCount": 1, 191 | "DisplayName": "The Ship Graveyard" 192 | } 193 | ], 194 | "1_1_9": [ 195 | { 196 | "Name": "shipwreckquest", 197 | "ExpectedCount": 1, 198 | "DisplayName": "Captain Fairgraves" 199 | }, 200 | { 201 | "Name": "shipentrance", 202 | "ExpectedCount": 1, 203 | "DisplayName": "The Ship Graveyard Cave" 204 | }, 205 | { 206 | "Name": "shippassageb", 207 | "ExpectedCount": 1, 208 | "DisplayName": "The Prisoner's Gate" 209 | }, 210 | { 211 | "Name": "skeletoncave", 212 | "ExpectedCount": 1, 213 | "DisplayName": "The Cavern Of Wrath" 214 | }, 215 | { 216 | "Name": "skeletoncavecliff", 217 | "ExpectedCount": 1, 218 | "DisplayName": "The Cavern Of Wrath" 219 | } 220 | ], 221 | "1_1_9a": [ 222 | { 223 | "Name": "boatallflame", 224 | "ExpectedCount": 1, 225 | "DisplayName": "Allflame" 226 | }, 227 | { 228 | "Name": "entranceup", 229 | "ExpectedCount": 2, 230 | "DisplayName": "Transition" 231 | } 232 | ], 233 | "1_1_11_1": [ 234 | { 235 | "Name": "entrance", 236 | "ExpectedCount": 1, 237 | "DisplayName": "The Ship Graveyard" 238 | }, 239 | { 240 | "Name": "entranceup", 241 | "ExpectedCount": 1, 242 | "DisplayName": "The Cavern of Anger" 243 | } 244 | ], 245 | "1_1_11_2": [ 246 | { 247 | "Name": "entrancedown", 248 | "ExpectedCount": 1, 249 | "DisplayName": "The Cavern of Wrath" 250 | }, 251 | { 252 | "Name": "entranceup", 253 | "ExpectedCount": 1, 254 | "DisplayName": "Merveil's Lair" 255 | }, 256 | { 257 | "Name": "entrancedownboss", 258 | "ExpectedCount": 1, 259 | "DisplayName": "The Cavern of Anger" 260 | }, 261 | { 262 | "Name": "mervexit", 263 | "ExpectedCount": 1, 264 | "DisplayName": "The Southern Forest" 265 | } 266 | ], 267 | "1_2_1": [ 268 | { 269 | "Name": "foresttowndock", 270 | "ExpectedCount": 1, 271 | "DisplayName": "The Forest Encampment" 272 | } 273 | ], 274 | "1_2_2": [ 275 | { 276 | "Name": "foresttownright", 277 | "ExpectedCount": 1, 278 | "DisplayName": "The Forest Encampment" 279 | }, 280 | { 281 | "Name": "gatetransition", 282 | "ExpectedCount": 1, 283 | "DisplayName": "The Crossroads" 284 | }, 285 | { 286 | "Name": "cave", 287 | "ExpectedCount": 1, 288 | "DisplayName": "The Den" 289 | } 290 | ], 291 | "1_2_2a": [ 292 | { 293 | "Name": "exitup", 294 | "ExpectedCount": 1, 295 | "DisplayName": "Exit" 296 | } 297 | ], 298 | "1_2_3": [ 299 | { 300 | "Name": "bridgetransition", 301 | "ExpectedCount": 1, 302 | "DisplayName": "The Broken Bridge" 303 | }, 304 | { 305 | "Name": "templeentrance", 306 | "ExpectedCount": 1, 307 | "DisplayName": "The Chamber of Sins Level 1" 308 | }, 309 | { 310 | "Name": "gatetransitionReverse", 311 | "ExpectedCount": 2, 312 | "DisplayName": "Transition" 313 | } 314 | ], 315 | "1_2_15": [ 316 | { 317 | "Name": "gatetransition", 318 | "ExpectedCount": 1, 319 | "DisplayName": "The Crossroads" 320 | }, 321 | { 322 | "Name": "entrance", 323 | "ExpectedCount": 1, 324 | "DisplayName": "The Crypt Level 1" 325 | }, 326 | { 327 | "Name": "Metadata/Terrain/Leagues/Sanctum/Foyer/Tiles/reaper_entrance.tdt", 328 | "ExpectedCount": 3, 329 | "DisplayName": "Statue" 330 | } 331 | ], 332 | "1_2_5_1": [ 333 | { 334 | "Name": "exitdown", 335 | "ExpectedCount": 1, 336 | "DisplayName": "The Crypt Level 2" 337 | }, 338 | { 339 | "Name": "exit", 340 | "ExpectedCount": 1, 341 | "DisplayName": "The Fellshrine Ruins" 342 | }, 343 | { 344 | "Name": "LabyrinthDoor", 345 | "ExpectedCount": 1, 346 | "DisplayName": "Trial of Ascendency" 347 | } 348 | ], 349 | "1_2_5_2": [ 350 | { 351 | "Name": "exit", 352 | "ExpectedCount": 1, 353 | "DisplayName": "The Crypt Level 1" 354 | }, 355 | { 356 | "Name": "handaltar", 357 | "ExpectedCount": 1, 358 | "DisplayName": "Altar" 359 | } 360 | ], 361 | "1_2_6_1": [ 362 | { 363 | "Name": "outentrance", 364 | "ExpectedCount": 1, 365 | "DisplayName": "The Crossroads" 366 | }, 367 | { 368 | "Name": "exitdown", 369 | "ExpectedCount": 1, 370 | "DisplayName": "The Chamber of Sins Level 2" 371 | } 372 | ], 373 | "1_2_6_2": [ 374 | { 375 | "Name": "exitup", 376 | "ExpectedCount": 1, 377 | "DisplayName": "Chamber of Sins Level 1" 378 | }, 379 | { 380 | "Name": "narrow_door", 381 | "ExpectedCount": 1, 382 | "DisplayName": "Trial of Ascendency" 383 | }, 384 | { 385 | "Name": "questcart", 386 | "ExpectedCount": 1, 387 | "DisplayName": "Fidelitas" 388 | }, 389 | { 390 | "Name": "pit", 391 | "ExpectedCount": 1, 392 | "DisplayName": "Gems" 393 | } 394 | ], 395 | "1_2_4": [ 396 | { 397 | "Name": "bridgetransition", 398 | "ExpectedCount": 1, 399 | "DisplayName": "The Crossroads" 400 | }, 401 | { 402 | "Name": "brokenbridge", 403 | "ExpectedCount": 1, 404 | "DisplayName": "Kraityn" 405 | } 406 | ], 407 | "1_2_7": [ 408 | { 409 | "Name": "foresttownleft", 410 | "ExpectedCount": 1, 411 | "DisplayName": "The Forest Encampment" 412 | }, 413 | { 414 | "Name": "bridgetransition", 415 | "ExpectedCount": 1, 416 | "DisplayName": "The Wetlands" 417 | }, 418 | { 419 | "Name": "tunnel", 420 | "ExpectedCount": 1, 421 | "DisplayName": "The Western Forest" 422 | } 423 | ], 424 | "1_2_9": [ 425 | { 426 | "Name": "tunnel", 427 | "ExpectedCount": 1, 428 | "DisplayName": "The Riverways" 429 | }, 430 | { 431 | "Name": "spidergroveentrance", 432 | "ExpectedCount": 1, 433 | "DisplayName": "The Weaver's Chambers" 434 | }, 435 | { 436 | "Name": "witcharena", 437 | "ExpectedCount": 1, 438 | "DisplayName": "Alira" 439 | }, 440 | { 441 | "Name": "passage", 442 | "ExpectedCount": 1, 443 | "DisplayName": "Skillpoint" 444 | } 445 | ], 446 | "1_2_10": [ 447 | { 448 | "Name": "exitlit", 449 | "ExpectedCount": 1, 450 | "DisplayName": "The Western Forest" 451 | }, 452 | { 453 | "Name": "exit", 454 | "ExpectedCount": 2, 455 | "DisplayName": "The Weaver's Nest" 456 | } 457 | ], 458 | "1_2_12": [ 459 | { 460 | "Name": "entrance", 461 | "ExpectedCount": 1, 462 | "DisplayName": "The Vaal Ruins" 463 | }, 464 | { 465 | "Name": "bridgetransition", 466 | "ExpectedCount": 1, 467 | "DisplayName": "The Riverways" 468 | }, 469 | { 470 | "Name": "oakgate", 471 | "ExpectedCount": 1, 472 | "DisplayName": "Oak" 473 | } 474 | ], 475 | "1_2_11": [ 476 | { 477 | "Name": "exit", 478 | "ExpectedCount": 2, 479 | "DisplayName": "Area Transition" 480 | }, 481 | { 482 | "Name": "boss", 483 | "ExpectedCount": 1, 484 | "DisplayName": "Ancient Seal" 485 | } 486 | ], 487 | "1_2_8": [ 488 | { 489 | "Name": "incaexit", 490 | "ExpectedCount": 1, 491 | "DisplayName": "The Vaal Ruins" 492 | }, 493 | { 494 | "Name": "waterfallcave", 495 | "ExpectedCount": 1, 496 | "DisplayName": "The Caverns" 497 | }, 498 | { 499 | "Name": "groveentrance", 500 | "ExpectedCount": 1, 501 | "DisplayName": "The Dread Thicket" 502 | } 503 | ], 504 | "1_2_13": [ 505 | { 506 | "Name": "exit", 507 | "ExpectedCount": 1, 508 | "DisplayName": "Exit" 509 | }, 510 | { 511 | "Name": "cave", 512 | "ExpectedCount": 1, 513 | "DisplayName": "Lush Hideout" 514 | } 515 | ], 516 | "1_2_14_2": [ 517 | { 518 | "Name": "exitdown", 519 | "ExpectedCount": 1, 520 | "DisplayName": "The Northern Forest" 521 | }, 522 | { 523 | "Name": "Metadata/Terrain/Dungeon/Features/dungeon_inca_exit_boss_v01_01.tdt", 524 | "ExpectedCount": 2, 525 | "DisplayName": "Transition" 526 | }, 527 | { 528 | "Name": "stairsup", 529 | "ExpectedCount": 1, 530 | "DisplayName": "The Ancient Pyramid" 531 | } 532 | ], 533 | "1_2_14_3": [ 534 | { 535 | "Name": "stairsdown", 536 | "ExpectedCount": 3, 537 | "DisplayName": "stairsdown" 538 | }, 539 | { 540 | "Name": "stairsup", 541 | "ExpectedCount": 3, 542 | "DisplayName": "stairsup" 543 | }, 544 | { 545 | "Name": "emerge", 546 | "ExpectedCount": 1, 547 | "DisplayName": "Vaal" 548 | }, 549 | { 550 | "Name": "exitdoor", 551 | "ExpectedCount": 1, 552 | "DisplayName": "The City of Sarn" 553 | } 554 | ], 555 | "1_3_1": [ 556 | { 557 | "Name": "exit", 558 | "ExpectedCount": 1, 559 | "DisplayName": "The Sarn Encampment" 560 | } 561 | ], 562 | "1_3_2": [ 563 | { 564 | "Name": "slumsentrance", 565 | "ExpectedCount": 1, 566 | "DisplayName": "The Sarn Encampment" 567 | }, 568 | { 569 | "Name": "sewer", 570 | "ExpectedCount": 1, 571 | "DisplayName": "The Sewers" 572 | }, 573 | { 574 | "Name": "prison", 575 | "ExpectedCount": 1, 576 | "DisplayName": "The Crematorium" 577 | } 578 | ], 579 | "1_3_3_1": [ 580 | { 581 | "Name": "entranceup", 582 | "ExpectedCount": 1, 583 | "DisplayName": "The Slums" 584 | }, 585 | { 586 | "Name": "LabyrinthDoor", 587 | "ExpectedCount": 1, 588 | "DisplayName": "Trial of Ascendency" 589 | }, 590 | { 591 | "Name": "questmarker", 592 | "ExpectedCount": 1, 593 | "DisplayName": "Piety" 594 | } 595 | ], 596 | "1_3_10_1": [ 597 | { 598 | "Name": "stash", 599 | "ExpectedCount": 3, 600 | "DisplayName": "Platinum Bust" 601 | }, 602 | { 603 | "Name": "endexit", 604 | "ExpectedCount": 1, 605 | "DisplayName": "The slums" 606 | }, 607 | { 608 | "Name": "sewerentrance", 609 | "ExpectedCount": 1, 610 | "DisplayName": "The Marketplace" 611 | }, 612 | { 613 | "Name": "exit", 614 | "ExpectedCount": 1, 615 | "DisplayName": "The Ebony Barracks" 616 | } 617 | ], 618 | "1_3_5": [ 619 | { 620 | "Name": "churchentrance", 621 | "ExpectedCount": 1, 622 | "DisplayName": "The Catacombs" 623 | }, 624 | { 625 | "Name": "exit", 626 | "ExpectedCount": 1, 627 | "DisplayName": "The Battlefront" 628 | }, 629 | { 630 | "Name": "sewer", 631 | "ExpectedCount": 1, 632 | "DisplayName": "The Sewers" 633 | }, 634 | { 635 | "Name": "decanter", 636 | "ExpectedCount": 1, 637 | "DisplayName": "Decanter Spiritus" 638 | } 639 | ], 640 | "1_3_6": [ 641 | { 642 | "Name": "exit", 643 | "ExpectedCount": 1, 644 | "DisplayName": "The Marketplace" 645 | }, 646 | { 647 | "Name": "LabyrinthDoor", 648 | "ExpectedCount": 1, 649 | "DisplayName": "Trial of Ascendency" 650 | } 651 | ], 652 | "1_3_7": [ 653 | { 654 | "Name": "exit", 655 | "ExpectedCount": 1, 656 | "DisplayName": "The Docks" 657 | }, 658 | { 659 | "Name": "templeentrance", 660 | "ExpectedCount": 1, 661 | "DisplayName": "The Solaris Temple Level 1" 662 | }, 663 | { 664 | "Name": "entrance", 665 | "ExpectedCount": 1, 666 | "DisplayName": "The Marketplace" 667 | }, 668 | { 669 | "Name": "brokenbridge", 670 | "ExpectedCount": 1, 671 | "DisplayName": "Blackguard Chest" 672 | } 673 | ], 674 | "1_3_9": [ 675 | { 676 | "Name": "exit", 677 | "ExpectedCount": 1, 678 | "DisplayName": "The Battlefront" 679 | }, 680 | { 681 | "Name": "questcart", 682 | "ExpectedCount": 1, 683 | "DisplayName": "Supply Container" 684 | } 685 | ], 686 | "1_3_8_1": [ 687 | { 688 | "Name": "templeentrance", 689 | "ExpectedCount": 1, 690 | "DisplayName": "The Battlefront" 691 | }, 692 | { 693 | "Name": "stairsdown", 694 | "ExpectedCount": 1, 695 | "DisplayName": "The Solaris Temple Level 2" 696 | } 697 | ], 698 | "1_3_8_2": [ 699 | { 700 | "Name": "exit", 701 | "ExpectedCount": 1, 702 | "DisplayName": "The Solaris Temple Level 1" 703 | }, 704 | { 705 | "Name": "queenchair", 706 | "ExpectedCount": 1, 707 | "DisplayName": "Lady Dialla" 708 | } 709 | ], 710 | "1_3_13": [ 711 | { 712 | "Name": "sewers", 713 | "ExpectedCount": 1, 714 | "DisplayName": "The Sewers" 715 | }, 716 | { 717 | "Name": "templeentrance", 718 | "ExpectedCount": 1, 719 | "DisplayName": "The Lunaris Temple Level 1" 720 | }, 721 | { 722 | "Name": "largetent", 723 | "ExpectedCount": 1, 724 | "DisplayName": "General Gravicious" 725 | }, 726 | { 727 | "Name": "exit", 728 | "ExpectedCount": 1, 729 | "DisplayName": "The Imperial Gardens" 730 | } 731 | ], 732 | "1_3_14_1": [ 733 | { 734 | "Name": "templeentrance", 735 | "ExpectedCount": 1, 736 | "DisplayName": "The Ebony Barracks" 737 | }, 738 | { 739 | "Name": "stairsdown", 740 | "ExpectedCount": 1, 741 | "DisplayName": "The Lunaris Temple Level 2" 742 | } 743 | ], 744 | "1_3_14_2": [ 745 | { 746 | "Name": "exit", 747 | "ExpectedCount": 1, 748 | "DisplayName": "The Lunaris Temple Level 1" 749 | }, 750 | { 751 | "Name": "roundtopcenter", 752 | "ExpectedCount": 1, 753 | "DisplayName": "Piety" 754 | } 755 | ], 756 | "1_3_15": [ 757 | { 758 | "Name": "exit", 759 | "ExpectedCount": 1, 760 | "DisplayName": "The Ebony Barracks" 761 | }, 762 | { 763 | "Name": "epic", 764 | "ExpectedCount": 1, 765 | "DisplayName": "The Sceptre of God" 766 | }, 767 | { 768 | "Name": "entrance", 769 | "ExpectedCount": 1, 770 | "DisplayName": "The Library" 771 | }, 772 | { 773 | "Name": "3x1_gateway", 774 | "ExpectedCount": 1, 775 | "DisplayName": "Trial of Ascendency" 776 | }, 777 | { 778 | "Name": "fruit", 779 | "ExpectedCount": 1, 780 | "DisplayName": "Plum" 781 | } 782 | ], 783 | "1_3_17_1": [ 784 | { 785 | "Name": "entrance", 786 | "ExpectedCount": 1, 787 | "DisplayName": "The Imperial Gardens" 788 | }, 789 | { 790 | "Name": "siosa", 791 | "ExpectedCount": 1, 792 | "DisplayName": "Siosa" 793 | }, 794 | { 795 | "Name": "exit", 796 | "ExpectedCount": 1, 797 | "DisplayName": "The Archives" 798 | } 799 | ], 800 | "1_3_17_2": [ 801 | { 802 | "Name": "entrancebook", 803 | "ExpectedCount": 1, 804 | "DisplayName": "The Library" 805 | }, 806 | { 807 | "Name": "large", 808 | "ExpectedCount": 4, 809 | "DisplayName": "Book Stand" 810 | } 811 | ], 812 | "1_3_18_1": [ 813 | { 814 | "Name": "epic", 815 | "ExpectedCount": 1, 816 | "DisplayName": "The Imperial Gardens" 817 | }, 818 | { 819 | "Name": "up", 820 | "ExpectedCount": 3, 821 | "DisplayName": "Stairs Up" 822 | }, 823 | { 824 | "Name": "down", 825 | "ExpectedCount": 2, 826 | "DisplayName": "Stairs Down" 827 | } 828 | ], 829 | "1_3_18_2": [ 830 | { 831 | "Name": "down", 832 | "ExpectedCount": 3, 833 | "DisplayName": "Stairs Down" 834 | }, 835 | { 836 | "Name": "up", 837 | "ExpectedCount": 2, 838 | "DisplayName": "Stairs Up" 839 | }, 840 | { 841 | "Name": "exit", 842 | "ExpectedCount": 1, 843 | "DisplayName": "Dominus" 844 | } 845 | ], 846 | "1_4_1": [ 847 | { 848 | "Name": "townconnection", 849 | "ExpectedCount": 1, 850 | "DisplayName": "Highgate" 851 | }, 852 | { 853 | "Name": "entrance", 854 | "ExpectedCount": 1, 855 | "DisplayName": "Waypoint" 856 | }, 857 | { 858 | "Name": "waterwheel", 859 | "ExpectedCount": 1, 860 | "DisplayName": "Unique Boss" 861 | } 862 | ], 863 | "1_4_2": [ 864 | { 865 | "Name": "entrance", 866 | "ExpectedCount": 1, 867 | "DisplayName": "Highgate" 868 | } 869 | ], 870 | "1_4_3_1": [ 871 | { 872 | "Name": "entrance", 873 | "ExpectedCount": 2, 874 | "DisplayName": "entrance" 875 | } 876 | ], 877 | "1_4_3_3": [ 878 | { 879 | "Name": "beast", 880 | "ExpectedCount": 1, 881 | "DisplayName": "The Belly of the Beast Level 1" 882 | }, 883 | { 884 | "Name": "entranceup", 885 | "ExpectedCount": 2, 886 | "DisplayName": "Area Transition" 887 | }, 888 | { 889 | "Name": "entrancedown", 890 | "ExpectedCount": 1, 891 | "DisplayName": "Excavated Hideout" 892 | } 893 | ], 894 | "1_4_3_2": [ 895 | { 896 | "Name": "entrancedown", 897 | "ExpectedCount": 1, 898 | "DisplayName": "The Crystal Veines" 899 | }, 900 | { 901 | "Name": "entranceback", 902 | "ExpectedCount": 1, 903 | "DisplayName": "The Mines Level 1" 904 | } 905 | ], 906 | "1_4_5_1": [ 907 | { 908 | "Name": "entrance", 909 | "ExpectedCount": 1, 910 | "DisplayName": "The Grand Arena" 911 | } 912 | ], 913 | "1_4_5_2": [ 914 | { 915 | "Name": "entrance1", 916 | "ExpectedCount": 1, 917 | "DisplayName": "Daresso's Dream" 918 | }, 919 | { 920 | "Name": "daressoentrance", 921 | "ExpectedCount": 1, 922 | "DisplayName": "Daresso Arena" 923 | }, 924 | { 925 | "Name": "entrance", 926 | "ExpectedCount": 7, 927 | "DisplayName": "Door" 928 | }, 929 | { 930 | "Name": "trapdoor", 931 | "ExpectedCount": 3, 932 | "DisplayName": "Door Back" 933 | }, 934 | { 935 | "Name": "doormerveil", 936 | "ExpectedCount": 3, 937 | "DisplayName": "Door Next" 938 | }, 939 | { 940 | "Name": "gladiatortrapdoor", 941 | "ExpectedCount": 1, 942 | "DisplayName": "Gladiators Door Back" 943 | } 944 | ], 945 | "1_4_4_1": [ 946 | { 947 | "Name": "areatransition", 948 | "ExpectedCount": 1, 949 | "DisplayName": "Kaom's Stronghold" 950 | } 951 | ], 952 | "1_4_4_3": [ 953 | { 954 | "Name": "k3", 955 | "ExpectedCount": 1, 956 | "DisplayName": "Kaom's Dream" 957 | }, 958 | { 959 | "Name": "throne", 960 | "ExpectedCount": 1, 961 | "DisplayName": "Kaom Arena" 962 | } 963 | ], 964 | "1_4_6_1": [ 965 | { 966 | "Name": "entrance", 967 | "ExpectedCount": 2, 968 | "DisplayName": "Area Transition" 969 | } 970 | ], 971 | "1_4_6_2": [ 972 | { 973 | "Name": "entrance", 974 | "ExpectedCount": 2, 975 | "DisplayName": "Area Transition" 976 | }, 977 | { 978 | "Name": "onewaydoor", 979 | "ExpectedCount": 1, 980 | "DisplayName": "Piety Arena" 981 | } 982 | ], 983 | "1_4_6_3": [ 984 | { 985 | "Name": "entrance", 986 | "ExpectedCount": 1, 987 | "DisplayName": "Belly of the Beast Level 2" 988 | }, 989 | { 990 | "Name": "maligaroarena", 991 | "ExpectedCount": 1, 992 | "DisplayName": "Maligaro Arena" 993 | }, 994 | { 995 | "Name": "shavronnearena", 996 | "ExpectedCount": 1, 997 | "DisplayName": "Shavronne Arena" 998 | }, 999 | { 1000 | "Name": "doedrearena", 1001 | "ExpectedCount": 1, 1002 | "DisplayName": "Doedre Arena" 1003 | }, 1004 | { 1005 | "Name": "taster", 1006 | "ExpectedCount": 1, 1007 | "DisplayName": "Malachai Arena" 1008 | } 1009 | ], 1010 | "1_4_7": [ 1011 | { 1012 | "Name": "transition", 1013 | "ExpectedCount": 1, 1014 | "DisplayName": "The Slave Pens" 1015 | }, 1016 | { 1017 | "Name": "pinnacle", 1018 | "ExpectedCount": 1, 1019 | "DisplayName": "The Slave Pens" 1020 | } 1021 | ], 1022 | "1_5_1": [ 1023 | { 1024 | "Name": "town", 1025 | "ExpectedCount": 1, 1026 | "DisplayName": "Overseer's Tower" 1027 | } 1028 | ], 1029 | "1_5_2": [ 1030 | { 1031 | "Name": "town", 1032 | "ExpectedCount": 1, 1033 | "DisplayName": "Overseer's Tower" 1034 | }, 1035 | { 1036 | "Name": "exit", 1037 | "ExpectedCount": 1, 1038 | "DisplayName": "Oriath / Ruined Square" 1039 | }, 1040 | { 1041 | "Name": "ddclosed", 1042 | "ExpectedCount": 1, 1043 | "DisplayName": "Miasmeter" 1044 | } 1045 | ], 1046 | "1_5_3b": [ 1047 | { 1048 | "Name": "04", 1049 | "ExpectedCount": 1, 1050 | "DisplayName": "The Ossuary" 1051 | }, 1052 | { 1053 | "Name": "corner_to_entrance", 1054 | "ExpectedCount": 1, 1055 | "DisplayName": "The Control Blocks" 1056 | }, 1057 | { 1058 | "Name": "02", 1059 | "ExpectedCount": 1, 1060 | "DisplayName": "The Reliquary" 1061 | }, 1062 | { 1063 | "Name": "transition", 1064 | "ExpectedCount": 1, 1065 | "DisplayName": "The Cathedral Rooftop" 1066 | }, 1067 | { 1068 | "Name": "03", 1069 | "ExpectedCount": 1, 1070 | "DisplayName": "The Templar / Torched Courts" 1071 | } 1072 | ], 1073 | "1_5_4b": [ 1074 | { 1075 | "Name": "entrance", 1076 | "ExpectedCount": 1, 1077 | "DisplayName": "Oriath / The Ruined Square" 1078 | }, 1079 | { 1080 | "Name": "innocence_transition", 1081 | "ExpectedCount": 1, 1082 | "DisplayName": "The Chamber of Innocence" 1083 | } 1084 | ], 1085 | "1_5_5": [ 1086 | { 1087 | "Name": "templar_courts_entrance", 1088 | "ExpectedCount": 1, 1089 | "DisplayName": "The Templar / Torched Courts" 1090 | }, 1091 | { 1092 | "Name": "epic_stair", 1093 | "ExpectedCount": 1, 1094 | "DisplayName": "High Templar Avarius Arena" 1095 | } 1096 | ], 1097 | "1_5_6": [ 1098 | { 1099 | "Name": "secretpassage", 1100 | "ExpectedCount": 1, 1101 | "DisplayName": "Quest Item" 1102 | }, 1103 | { 1104 | "Name": "transitiondownbroken", 1105 | "ExpectedCount": 1, 1106 | "DisplayName": "The Ruined Square" 1107 | } 1108 | ], 1109 | "1_5_7": [ 1110 | { 1111 | "Name": "Metadata/Terrain/Theopolis/Reliquary/Feature/Reliquary_Oriath_transition_v01_01.tdt", 1112 | "ExpectedCount": 1, 1113 | "DisplayName": "The Ruined Square" 1114 | } 1115 | ], 1116 | "1_5_8": [ 1117 | { 1118 | "Name": "arena", 1119 | "ExpectedCount": 1, 1120 | "DisplayName": "Kitava Arena" 1121 | }, 1122 | { 1123 | "Name": "chitus", 1124 | "ExpectedCount": 1, 1125 | "DisplayName": "The Ruined Square" 1126 | } 1127 | ], 1128 | "2_6_1": [ 1129 | { 1130 | "Name": "beachtownsouth", 1131 | "ExpectedCount": 1, 1132 | "DisplayName": "Lioneye's Watch" 1133 | }, 1134 | { 1135 | "Name": "zombiebite", 1136 | "ExpectedCount": 1, 1137 | "DisplayName": "Boss" 1138 | } 1139 | ], 1140 | "2_6_3": [ 1141 | { 1142 | "Name": "karui", 1143 | "ExpectedCount": 1, 1144 | "DisplayName": "The Coast" 1145 | }, 1146 | { 1147 | "Name": "medicineboat", 1148 | "ExpectedCount": 1, 1149 | "DisplayName": "Riptide" 1150 | } 1151 | ], 1152 | "2_6_4": [ 1153 | { 1154 | "Name": "karui_reverse", 1155 | "ExpectedCount": 1, 1156 | "DisplayName": "The Coast" 1157 | }, 1158 | { 1159 | "Name": "transition", 1160 | "ExpectedCount": 1, 1161 | "DisplayName": "The Karui Fortress" 1162 | }, 1163 | { 1164 | "Name": "watercave", 1165 | "ExpectedCount": 1, 1166 | "DisplayName": "The Dishonoured Queen" 1167 | } 1168 | ], 1169 | "2_6_5": [ 1170 | { 1171 | "Name": "ridge", 1172 | "ExpectedCount": 1, 1173 | "DisplayName": "The Ridge" 1174 | }, 1175 | { 1176 | "Name": "transition", 1177 | "ExpectedCount": 1, 1178 | "DisplayName": "The Mud Flats" 1179 | }, 1180 | { 1181 | "Name": "tokuhama", 1182 | "ExpectedCount": 1, 1183 | "DisplayName": "Tukohama Arena" 1184 | } 1185 | ], 1186 | "2_6_6": [ 1187 | { 1188 | "Name": "transition", 1189 | "ExpectedCount": 1, 1190 | "DisplayName": "The Karui Fortress" 1191 | }, 1192 | { 1193 | "Name": "prisonback", 1194 | "ExpectedCount": 1, 1195 | "DisplayName": "The Lower Prison" 1196 | } 1197 | ], 1198 | "2_6_7_1": [ 1199 | { 1200 | "Name": "scaffold", 1201 | "ExpectedCount": 1, 1202 | "DisplayName": "The Ridge" 1203 | }, 1204 | { 1205 | "Name": "LabyrinthDoor", 1206 | "ExpectedCount": 1, 1207 | "DisplayName": "Trial of Ascendency" 1208 | }, 1209 | { 1210 | "Name": "shavronne", 1211 | "ExpectedCount": 1, 1212 | "DisplayName": "Shavronne's Tower" 1213 | } 1214 | ], 1215 | "2_6_7_2": [ 1216 | { 1217 | "Name": "shavronne2", 1218 | "ExpectedCount": 1, 1219 | "DisplayName": "The Lower Prison" 1220 | }, 1221 | { 1222 | "Name": "entranceup", 1223 | "ExpectedCount": 3, 1224 | "DisplayName": "Stairs Up" 1225 | }, 1226 | { 1227 | "Name": "entrancedown", 1228 | "ExpectedCount": 3, 1229 | "DisplayName": "Stairs Down" 1230 | }, 1231 | { 1232 | "Name": "ladder_up", 1233 | "ExpectedCount": 1, 1234 | "DisplayName": "Shavronne Arena" 1235 | }, 1236 | { 1237 | "Name": "exitdoor", 1238 | "ExpectedCount": 1, 1239 | "DisplayName": "Prisoner's Gate" 1240 | } 1241 | ], 1242 | "2_6_8": [ 1243 | { 1244 | "Name": "passageblock", 1245 | "ExpectedCount": 1, 1246 | "DisplayName": "The Western Forest" 1247 | }, 1248 | { 1249 | "Name": "transition3", 1250 | "ExpectedCount": 2, 1251 | "DisplayName": "Valley of the Fire Drinker" 1252 | } 1253 | ], 1254 | "2_6_9": [ 1255 | { 1256 | "Name": "passage", 1257 | "ExpectedCount": 1, 1258 | "DisplayName": "Prisoner's Gate" 1259 | }, 1260 | { 1261 | "Name": "tunnel", 1262 | "ExpectedCount": 1, 1263 | "DisplayName": "The Riverways" 1264 | } 1265 | ], 1266 | "2_6_10": [ 1267 | { 1268 | "Name": "bridgetransition", 1269 | "ExpectedCount": 1, 1270 | "DisplayName": "The Wetlands" 1271 | }, 1272 | { 1273 | "Name": "tunnel", 1274 | "ExpectedCount": 1, 1275 | "DisplayName": "The Western Forest" 1276 | }, 1277 | { 1278 | "Name": "transition", 1279 | "ExpectedCount": 1, 1280 | "DisplayName": "The Southern Forest" 1281 | } 1282 | ], 1283 | "2_6_11": [ 1284 | { 1285 | "Name": "roadend", 1286 | "ExpectedCount": 1, 1287 | "DisplayName": "The Riverways" 1288 | }, 1289 | { 1290 | "Name": "caveentrance", 1291 | "ExpectedCount": 2, 1292 | "DisplayName": "The Spawning Ground" 1293 | } 1294 | ], 1295 | "2_6_12": [ 1296 | { 1297 | "Name": "foresttowndock", 1298 | "ExpectedCount": 1, 1299 | "DisplayName": "The Riverways" 1300 | }, 1301 | { 1302 | "Name": "caveentrance", 1303 | "ExpectedCount": 1, 1304 | "DisplayName": "The Cavern of Anger" 1305 | } 1306 | ], 1307 | "2_6_13": [ 1308 | { 1309 | "Name": "mervexit", 1310 | "ExpectedCount": 1, 1311 | "DisplayName": "The Southern Forest" 1312 | }, 1313 | { 1314 | "Name": "entrancedownboss", 1315 | "ExpectedCount": 1, 1316 | "DisplayName": "Passage" 1317 | }, 1318 | { 1319 | "Name": "nessa_corner", 1320 | "ExpectedCount": 1, 1321 | "DisplayName": "Flag Chest" 1322 | }, 1323 | { 1324 | "Name": "coral_hideout", 1325 | "ExpectedCount": 1, 1326 | "DisplayName": "Coral Hideout" 1327 | }, 1328 | { 1329 | "Name": "entranceup", 1330 | "ExpectedCount": 3, 1331 | "DisplayName": "Area Transition (50-50)" 1332 | }, 1333 | { 1334 | "Name": "edge", 1335 | "ExpectedCount": 1, 1336 | "DisplayName": "Near Real Area Transition" 1337 | } 1338 | ], 1339 | "2_6_14": [ 1340 | { 1341 | "Name": "caveentrance", 1342 | "ExpectedCount": 1, 1343 | "DisplayName": "The Cavern of Anger" 1344 | }, 1345 | { 1346 | "Name": "payload", 1347 | "ExpectedCount": 1, 1348 | "DisplayName": "Beacon" 1349 | }, 1350 | { 1351 | "Name": "weylamwalk", 1352 | "ExpectedCount": 1, 1353 | "DisplayName": "Weylam" 1354 | } 1355 | ], 1356 | "2_6_15": [ 1357 | { 1358 | "Name": "transition", 1359 | "ExpectedCount": 1, 1360 | "DisplayName": "The Brine King Arena" 1361 | } 1362 | ], 1363 | "2_7_1": [ 1364 | { 1365 | "Name": "partial", 1366 | "ExpectedCount": 1, 1367 | "DisplayName": "The Bridge Encampment" 1368 | }, 1369 | { 1370 | "Name": "bridgetransition", 1371 | "ExpectedCount": 1, 1372 | "DisplayName": "The Crossroads" 1373 | }, 1374 | { 1375 | "Name": "manorcorner", 1376 | "ExpectedCount": 1, 1377 | "DisplayName": "Dirty Lockbox" 1378 | } 1379 | ], 1380 | "2_7_2": [ 1381 | { 1382 | "Name": "templeentrance", 1383 | "ExpectedCount": 1, 1384 | "DisplayName": "The Chamber of Sins Level 1" 1385 | }, 1386 | { 1387 | "Name": "gatetransitionReverse", 1388 | "ExpectedCount": 1, 1389 | "DisplayName": "The Fellshrine Ruins" 1390 | }, 1391 | { 1392 | "Name": "bridgetransition", 1393 | "ExpectedCount": 1, 1394 | "DisplayName": "The Broken Bridge" 1395 | } 1396 | ], 1397 | "2_7_3": [ 1398 | { 1399 | "Name": "gatetransition", 1400 | "ExpectedCount": 1, 1401 | "DisplayName": "The Crossroads" 1402 | }, 1403 | { 1404 | "Name": "entrance", 1405 | "ExpectedCount": 1, 1406 | "DisplayName": "The Crypt Level 1" 1407 | } 1408 | ], 1409 | "2_7_4": [ 1410 | { 1411 | "Name": "exit", 1412 | "ExpectedCount": 1, 1413 | "DisplayName": "The Fellshrine Ruins" 1414 | }, 1415 | { 1416 | "Name": "LabyrinthDoor", 1417 | "ExpectedCount": 1, 1418 | "DisplayName": "Trial of Ascendency" 1419 | }, 1420 | { 1421 | "Name": "sarcophigusentrance", 1422 | "ExpectedCount": 1, 1423 | "DisplayName": "Stairs" 1424 | }, 1425 | { 1426 | "Name": "entrancebook", 1427 | "ExpectedCount": 1, 1428 | "DisplayName": "Stairs" 1429 | } 1430 | ], 1431 | "2_7_5_1": [ 1432 | { 1433 | "Name": "exitdown", 1434 | "ExpectedCount": 1, 1435 | "DisplayName": "The Chamber of Sins Lever 2" 1436 | }, 1437 | { 1438 | "Name": "outentrance", 1439 | "ExpectedCount": 1, 1440 | "DisplayName": "The Crossroads" 1441 | }, 1442 | { 1443 | "Name": "maporrey4", 1444 | "ExpectedCount": 1, 1445 | "DisplayName": "Map Device" 1446 | } 1447 | ], 1448 | "2_7_5_2": [ 1449 | { 1450 | "Name": "exitup", 1451 | "ExpectedCount": 1, 1452 | "DisplayName": "The Chamber of Sins Level 1" 1453 | }, 1454 | { 1455 | "Name": "narrow_door", 1456 | "ExpectedCount": 1, 1457 | "DisplayName": "Trial of Ascendency" 1458 | }, 1459 | { 1460 | "Name": "maligaro", 1461 | "ExpectedCount": 1, 1462 | "DisplayName": "The Den" 1463 | }, 1464 | { 1465 | "Name": "exitdown", 1466 | "ExpectedCount": 1, 1467 | "DisplayName": "Baleful Hideout" 1468 | } 1469 | ], 1470 | "2_7_6": [ 1471 | { 1472 | "Name": "exitup", 1473 | "ExpectedCount": 1, 1474 | "DisplayName": "The Ashen Fields" 1475 | }, 1476 | { 1477 | "Name": "down", 1478 | "ExpectedCount": 1, 1479 | "DisplayName": "The Chamber of Sins Level 2" 1480 | } 1481 | ], 1482 | "2_7_7": [ 1483 | { 1484 | "Name": "cave", 1485 | "ExpectedCount": 1, 1486 | "DisplayName": "The Den" 1487 | }, 1488 | { 1489 | "Name": "banditcamp", 1490 | "ExpectedCount": 1, 1491 | "DisplayName": "Greust Arena" 1492 | } 1493 | ], 1494 | "2_7_8": [ 1495 | { 1496 | "Name": "groveentrance", 1497 | "ExpectedCount": 1, 1498 | "DisplayName": "The Dread Thicket" 1499 | }, 1500 | { 1501 | "Name": "transition", 1502 | "ExpectedCount": 1, 1503 | "DisplayName": "The Causeway" 1504 | }, 1505 | { 1506 | "Name": "azmeri_shrine", 1507 | "ExpectedCount": 1, 1508 | "DisplayName": "Azmeri Shrine" 1509 | } 1510 | ], 1511 | "2_7_9": [ 1512 | { 1513 | "Name": "exit", 1514 | "ExpectedCount": 1, 1515 | "DisplayName": "The Northern Forest" 1516 | }, 1517 | { 1518 | "Name": "cave", 1519 | "ExpectedCount": 1, 1520 | "DisplayName": "Den of Despair" 1521 | } 1522 | ], 1523 | "2_7_10": [ 1524 | { 1525 | "Name": "transition", 1526 | "ExpectedCount": 1, 1527 | "DisplayName": "The Northern Forest" 1528 | }, 1529 | { 1530 | "Name": "bottom", 1531 | "ExpectedCount": 1, 1532 | "DisplayName": "The Vaal City" 1533 | } 1534 | ], 1535 | "2_7_11": [ 1536 | { 1537 | "Name": "top", 1538 | "ExpectedCount": 1, 1539 | "DisplayName": "The Causeway" 1540 | }, 1541 | { 1542 | "Name": "exit", 1543 | "ExpectedCount": 1, 1544 | "DisplayName": "The Temple of Decay Level 1" 1545 | } 1546 | ], 1547 | "2_7_12_1": [ 1548 | { 1549 | "Name": "entrance", 1550 | "ExpectedCount": 1, 1551 | "DisplayName": "The Vaal City" 1552 | }, 1553 | { 1554 | "Name": "stairsdown", 1555 | "ExpectedCount": 2, 1556 | "DisplayName": "Stairs Down" 1557 | }, 1558 | { 1559 | "Name": "stairsup", 1560 | "ExpectedCount": 1, 1561 | "DisplayName": "Stairs Up" 1562 | } 1563 | ], 1564 | "2_7_12_2": [ 1565 | { 1566 | "Name": "stairsdown", 1567 | "ExpectedCount": 2, 1568 | "DisplayName": "Stairs Down" 1569 | }, 1570 | { 1571 | "Name": "stairsup", 1572 | "ExpectedCount": 3, 1573 | "DisplayName": "Stairs Up" 1574 | }, 1575 | { 1576 | "Name": "exit", 1577 | "ExpectedCount": 2, 1578 | "DisplayName": "Arakaali Arena/Exit" 1579 | } 1580 | ], 1581 | "2_8_1": [ 1582 | { 1583 | "Name": "trapdoor", 1584 | "ExpectedCount": 1, 1585 | "DisplayName": "Stairs" 1586 | }, 1587 | { 1588 | "Name": "transition", 1589 | "ExpectedCount": 1, 1590 | "DisplayName": "The Sarn Encampment" 1591 | }, 1592 | { 1593 | "Name": "transitionwall", 1594 | "ExpectedCount": 2, 1595 | "DisplayName": "Mid-area" 1596 | } 1597 | ], 1598 | "2_8_2_1": [ 1599 | { 1600 | "Name": "transition", 1601 | "ExpectedCount": 1, 1602 | "DisplayName": "The Sarn Encampment" 1603 | }, 1604 | { 1605 | "Name": "endexit", 1606 | "ExpectedCount": 1, 1607 | "DisplayName": "Doedre's Cesspool" 1608 | } 1609 | ], 1610 | "2_8_2_2": [ 1611 | { 1612 | "Name": "endexit", 1613 | "ExpectedCount": 1, 1614 | "DisplayName": "The Toxic Conduits" 1615 | }, 1616 | { 1617 | "Name": "sewer", 1618 | "ExpectedCount": 1, 1619 | "DisplayName": "Loose Grate" 1620 | }, 1621 | { 1622 | "Name": "sewerentrance", 1623 | "ExpectedCount": 1, 1624 | "DisplayName": "The Quay" 1625 | }, 1626 | { 1627 | "Name": "ladderw", 1628 | "ExpectedCount": 1, 1629 | "DisplayName": "The Grand Promenade" 1630 | } 1631 | ], 1632 | "2_8_8": [ 1633 | { 1634 | "Name": "opengrate", 1635 | "ExpectedCount": 1, 1636 | "DisplayName": "Doedre's Cesspool" 1637 | }, 1638 | { 1639 | "Name": "exit", 1640 | "ExpectedCount": 3, 1641 | "DisplayName": "Resurrection Site / The Grain Gate" 1642 | }, 1643 | { 1644 | "Name": "A", 1645 | "ExpectedCount": 1, 1646 | "DisplayName": "Quest item" 1647 | } 1648 | ], 1649 | "2_8_9": [ 1650 | { 1651 | "Name": "transition", 1652 | "ExpectedCount": 1, 1653 | "DisplayName": "The Quay" 1654 | }, 1655 | { 1656 | "Name": "graingate", 1657 | "ExpectedCount": 1, 1658 | "DisplayName": "The Imperial Fields" 1659 | }, 1660 | { 1661 | "Name": "entranceslums", 1662 | "ExpectedCount": 1, 1663 | "DisplayName": "Skillpoint" 1664 | }, 1665 | { 1666 | "Name": "Metadata/Terrain/RuinedCity/AreaTransitions_Part2/slum_sewer_entrance_warehouse.tdt", 1667 | "ExpectedCount": 1, 1668 | "DisplayName": "The Hidden Underbelly" 1669 | } 1670 | ], 1671 | "2_8_10": [ 1672 | { 1673 | "Name": "transition", 1674 | "ExpectedCount": 1, 1675 | "DisplayName": "The Grain Gate" 1676 | }, 1677 | { 1678 | "Name": "templeentrance", 1679 | "ExpectedCount": 1, 1680 | "DisplayName": "The Solaris Temple Level 1" 1681 | } 1682 | ], 1683 | "2_8_12_1": [ 1684 | { 1685 | "Name": "templeentrance", 1686 | "ExpectedCount": 2, 1687 | "DisplayName": "Exit" 1688 | }, 1689 | { 1690 | "Name": "forcedblank", 1691 | "ExpectedCount": 1, 1692 | "DisplayName": "Pressure Plate" 1693 | }, 1694 | { 1695 | "Name": "stairsdown", 1696 | "ExpectedCount": 1, 1697 | "DisplayName": "The Solaris Temple Level 2" 1698 | } 1699 | ], 1700 | "2_8_12_2": [ 1701 | { 1702 | "Name": "exit", 1703 | "ExpectedCount": 1, 1704 | "DisplayName": "The Solaris Temple Level 1" 1705 | }, 1706 | { 1707 | "Name": "queenchair", 1708 | "ExpectedCount": 1, 1709 | "DisplayName": "Solaris Arena" 1710 | } 1711 | ], 1712 | "2_8_11": [ 1713 | { 1714 | "Name": "templeentrance", 1715 | "ExpectedCount": 1, 1716 | "DisplayName": "The Solaris Temple Level 1" 1717 | }, 1718 | { 1719 | "Name": "battlefront_harbor", 1720 | "ExpectedCount": 1, 1721 | "DisplayName": "The Harbour Bridge" 1722 | }, 1723 | { 1724 | "Name": "hidden_tunnel", 1725 | "ExpectedCount": 1, 1726 | "DisplayName": "The Hidden Underbelly" 1727 | } 1728 | ], 1729 | "2_8_13": [ 1730 | { 1731 | "Name": "battlefront_harbor", 1732 | "ExpectedCount": 1, 1733 | "DisplayName": "The Solaris Concourse" 1734 | }, 1735 | { 1736 | "Name": "arena_false", 1737 | "ExpectedCount": 1, 1738 | "DisplayName": "The Sky Shrine" 1739 | }, 1740 | { 1741 | "Name": "SPECIAL", 1742 | "ExpectedCount": 1, 1743 | "DisplayName": "The Lunaris Concourse" 1744 | } 1745 | ], 1746 | "2_8_6": [ 1747 | { 1748 | "Name": "bath_house", 1749 | "ExpectedCount": 1, 1750 | "DisplayName": "The Bath House" 1751 | }, 1752 | { 1753 | "Name": "templeentrance", 1754 | "ExpectedCount": 1, 1755 | "DisplayName": "The Lunaris Temple Level 1" 1756 | }, 1757 | { 1758 | "Name": "broken", 1759 | "ExpectedCount": 1, 1760 | "DisplayName": "The Harbour Bridge" 1761 | } 1762 | ], 1763 | "2_8_5": [ 1764 | { 1765 | "Name": "entry", 1766 | "ExpectedCount": 5, 1767 | "DisplayName": "Exit/Hideout" 1768 | }, 1769 | { 1770 | "Name": "rosette3_waypoint", 1771 | "ExpectedCount": 1, 1772 | "DisplayName": "Waypoint" 1773 | }, 1774 | { 1775 | "Name": "lava", 1776 | "ExpectedCount": 1, 1777 | "DisplayName": "Trial of Ascendency" 1778 | }, 1779 | { 1780 | "Name": "hidden_tunnel", 1781 | "ExpectedCount": 1, 1782 | "DisplayName": "The Hidden Underbelly" 1783 | } 1784 | ], 1785 | "2_8_3": [ 1786 | { 1787 | "Name": "aresenal", 1788 | "ExpectedCount": 1, 1789 | "DisplayName": "The Bath House" 1790 | }, 1791 | { 1792 | "Name": "Metadata/Terrain/RuinedCity/LargeWall/battlefield_arch_v01_03.tdt", 1793 | "ExpectedCount": 2, 1794 | "DisplayName": "Transition" 1795 | }, 1796 | { 1797 | "Name": "ladder", 1798 | "ExpectedCount": 1, 1799 | "DisplayName": "The Toxic Conduits" 1800 | } 1801 | ], 1802 | "2_8_4": [ 1803 | { 1804 | "Name": "entrance", 1805 | "ExpectedCount": 3, 1806 | "DisplayName": "Exit / Arena" 1807 | } 1808 | ], 1809 | "2_8_7_1_": [ 1810 | { 1811 | "Name": "templeentrance", 1812 | "ExpectedCount": 1, 1813 | "DisplayName": "The Lunaris Concourse" 1814 | }, 1815 | { 1816 | "Name": "Metadata/Terrain/Temple/Stairs/templeclean_stairs_convex_v01_01.tdt", 1817 | "ExpectedCount": 1, 1818 | "DisplayName": "Pressure Plate" 1819 | }, 1820 | { 1821 | "Name": "stairsdown", 1822 | "ExpectedCount": 1, 1823 | "DisplayName": "The Lunaris Temple Level 2" 1824 | } 1825 | ], 1826 | "2_8_7_2": [ 1827 | { 1828 | "Name": "exit", 1829 | "ExpectedCount": 1, 1830 | "DisplayName": "The Lunris Temple Level 1" 1831 | }, 1832 | { 1833 | "Name": "roundtopcenter", 1834 | "ExpectedCount": 1, 1835 | "DisplayName": "Lunaris Arena" 1836 | } 1837 | ], 1838 | "2_8_14": [ 1839 | { 1840 | "Name": "sewerentrance", 1841 | "ExpectedCount": 1, 1842 | "DisplayName": "The Grain Gate" 1843 | }, 1844 | { 1845 | "Name": "Metadata/Terrain/RuinedCity/Sewers/Features/sewerwall_end_tunnel_v01_03.tdt", 1846 | "ExpectedCount": 2, 1847 | "DisplayName": "The Solaris Concourse / The Bath House" 1848 | } 1849 | ], 1850 | "2_9_1": [ 1851 | { 1852 | "Name": "entrance", 1853 | "ExpectedCount": 1, 1854 | "DisplayName": "Waypoint" 1855 | }, 1856 | { 1857 | "Name": "townconnection", 1858 | "ExpectedCount": 1, 1859 | "DisplayName": "Highgate" 1860 | } 1861 | ], 1862 | "2_9_2": [ 1863 | { 1864 | "Name": "transition", 1865 | "ExpectedCount": 1, 1866 | "DisplayName": "Highgate" 1867 | }, 1868 | { 1869 | "Name": "topwinch", 1870 | "ExpectedCount": 1, 1871 | "DisplayName": "Supply Host" 1872 | }, 1873 | { 1874 | "Name": "winch", 1875 | "ExpectedCount": 2, 1876 | "DisplayName": "Supply Host" 1877 | } 1878 | ], 1879 | "2_9_3": [ 1880 | { 1881 | "Name": "elevator", 1882 | "ExpectedCount": 1, 1883 | "DisplayName": "The Descent" 1884 | }, 1885 | { 1886 | "Name": "foothills", 1887 | "ExpectedCount": 1, 1888 | "DisplayName": "The Foothills" 1889 | }, 1890 | { 1891 | "Name": "wagons_reverse", 1892 | "ExpectedCount": 1, 1893 | "DisplayName": "The Oasis" 1894 | } 1895 | ], 1896 | "2_9_4": [ 1897 | { 1898 | "Name": "impassable", 1899 | "ExpectedCount": 1, 1900 | "DisplayName": "The Vastiri Desert" 1901 | }, 1902 | { 1903 | "Name": "rockwalltransition", 1904 | "ExpectedCount": 2, 1905 | "DisplayName": "The Sand Pit" 1906 | } 1907 | ], 1908 | "2_9_5": [ 1909 | { 1910 | "Name": "lake", 1911 | "ExpectedCount": 1, 1912 | "DisplayName": "The Boiling Lake" 1913 | }, 1914 | { 1915 | "Name": "desert", 1916 | "ExpectedCount": 1, 1917 | "DisplayName": "The Vastiri Desert" 1918 | }, 1919 | { 1920 | "Name": "tunnel", 1921 | "ExpectedCount": 1, 1922 | "DisplayName": "The Tunnel" 1923 | } 1924 | ], 1925 | "2_9_6": [ 1926 | { 1927 | "Name": "entrance", 1928 | "ExpectedCount": 1, 1929 | "DisplayName": "The Foothills" 1930 | }, 1931 | { 1932 | "Name": "statueground", 1933 | "ExpectedCount": 1, 1934 | "DisplayName": "The Basilisk" 1935 | } 1936 | ], 1937 | "2_9_7": [ 1938 | { 1939 | "Name": "entrance", 1940 | "ExpectedCount": 1, 1941 | "DisplayName": "The Foothills" 1942 | }, 1943 | { 1944 | "Name": "exit", 1945 | "ExpectedCount": 1, 1946 | "DisplayName": "The Quarry" 1947 | }, 1948 | { 1949 | "Name": "doorframe", 1950 | "ExpectedCount": 1, 1951 | "DisplayName": "Trial of Ascendency" 1952 | } 1953 | ], 1954 | "2_9_8": [ 1955 | { 1956 | "Name": "slums_transition", 1957 | "ExpectedCount": 2, 1958 | "DisplayName": "Shrine of the Winds" 1959 | }, 1960 | { 1961 | "Name": "warehouse_rails", 1962 | "ExpectedCount": 1, 1963 | "DisplayName": "The Refinery" 1964 | }, 1965 | { 1966 | "Name": "tunnels", 1967 | "ExpectedCount": 1, 1968 | "DisplayName": "The Tunnel" 1969 | }, 1970 | { 1971 | "Name": "beast_membrane", 1972 | "ExpectedCount": 1, 1973 | "DisplayName": "The Belly of the Beast" 1974 | } 1975 | ], 1976 | "2_9_9": [ 1977 | { 1978 | "Name": "entry_main", 1979 | "ExpectedCount": 1, 1980 | "DisplayName": "The Quarry" 1981 | }, 1982 | { 1983 | "Name": "hidden_tunnel", 1984 | "ExpectedCount": 1, 1985 | "DisplayName": "Refinery Tunnels" 1986 | }, 1987 | { 1988 | "Name": "boss_transition", 1989 | "ExpectedCount": 1, 1990 | "DisplayName": "General Adus Arena" 1991 | } 1992 | ], 1993 | "2_9_10_1": [ 1994 | { 1995 | "Name": "quarry", 1996 | "ExpectedCount": 1, 1997 | "DisplayName": "The Quarry" 1998 | }, 1999 | { 2000 | "Name": "rottingcore", 2001 | "ExpectedCount": 1, 2002 | "DisplayName": "The Rotting Core" 2003 | } 2004 | ], 2005 | "2_9_10_2": [ 2006 | { 2007 | "Name": "entrance", 2008 | "ExpectedCount": 1, 2009 | "DisplayName": "The Belly of the Beast" 2010 | }, 2011 | { 2012 | "Name": "doordown", 2013 | "ExpectedCount": 1, 2014 | "DisplayName": "The Black Core" 2015 | }, 2016 | { 2017 | "Name": "shavronnearena", 2018 | "ExpectedCount": 1, 2019 | "DisplayName": "Shavronne Arena" 2020 | }, 2021 | { 2022 | "Name": "doedrearena", 2023 | "ExpectedCount": 1, 2024 | "DisplayName": "Doedre Arena" 2025 | }, 2026 | { 2027 | "Name": "maligaroarena", 2028 | "ExpectedCount": 1, 2029 | "DisplayName": "Maligaro Arena" 2030 | } 2031 | ], 2032 | "2_10_1": [ 2033 | { 2034 | "Name": "sindock", 2035 | "ExpectedCount": 1, 2036 | "DisplayName": "Oriath Docks" 2037 | }, 2038 | { 2039 | "Name": "arena", 2040 | "ExpectedCount": 1, 2041 | "DisplayName": "Cathedral Apex" 2042 | }, 2043 | { 2044 | "Name": "chitus", 2045 | "ExpectedCount": 1, 2046 | "DisplayName": "The Ravaged Square" 2047 | } 2048 | ], 2049 | "2_10_2": [ 2050 | { 2051 | "Name": "04", 2052 | "ExpectedCount": 1, 2053 | "DisplayName": "The Ossuary" 2054 | }, 2055 | { 2056 | "Name": "slaveden", 2057 | "ExpectedCount": 1, 2058 | "DisplayName": "The Control Blocks" 2059 | }, 2060 | { 2061 | "Name": "03", 2062 | "ExpectedCount": 1, 2063 | "DisplayName": "The Torched Courts" 2064 | }, 2065 | { 2066 | "Name": "02", 2067 | "ExpectedCount": 2, 2068 | "DisplayName": "The Reliquary/VaalSideArea" 2069 | }, 2070 | { 2071 | "Name": "height2", 2072 | "ExpectedCount": 1, 2073 | "DisplayName": "The Cathedral Rooftop" 2074 | } 2075 | ], 2076 | "2_10_9": [ 2077 | { 2078 | "Name": "abyss_transition", 2079 | "ExpectedCount": 2, 2080 | "DisplayName": "The Ravaged Square/Hideout" 2081 | }, 2082 | { 2083 | "Name": "transition", 2084 | "ExpectedCount": 1, 2085 | "DisplayName": "The Ossuary" 2086 | }, 2087 | { 2088 | "Name": "firepit", 2089 | "ExpectedCount": 1, 2090 | "DisplayName": "The Trial of Ascendency/Quest item" 2091 | } 2092 | ], 2093 | "2_10_4": [ 2094 | { 2095 | "Name": "templar_courts_entrance", 2096 | "ExpectedCount": 1, 2097 | "DisplayName": "The Torched Courts" 2098 | }, 2099 | { 2100 | "Name": "epic_stair", 2101 | "ExpectedCount": 1, 2102 | "DisplayName": "Avarius Arena" 2103 | } 2104 | ], 2105 | "2_10_3": [ 2106 | { 2107 | "Name": "innocence_transition", 2108 | "ExpectedCount": 1, 2109 | "DisplayName": "The Desecrated Chambers" 2110 | }, 2111 | { 2112 | "Name": "entrance", 2113 | "ExpectedCount": 1, 2114 | "DisplayName": "The Ravaged Square" 2115 | } 2116 | ], 2117 | "2_10_7": [ 2118 | { 2119 | "Name": "exit", 2120 | "ExpectedCount": 1, 2121 | "DisplayName": "The Ravaged Square" 2122 | }, 2123 | { 2124 | "Name": "transition", 2125 | "ExpectedCount": 2, 2126 | "DisplayName": "Vilenta Arena" 2127 | } 2128 | ], 2129 | "2_10_5": [ 2130 | { 2131 | "Name": "transition", 2132 | "ExpectedCount": 1, 2133 | "DisplayName": "The Feeding Trough" 2134 | }, 2135 | { 2136 | "Name": "reverse_feedingtrough", 2137 | "ExpectedCount": 1, 2138 | "DisplayName": "The Ravaged Square" 2139 | } 2140 | ], 2141 | "2_10_6": [ 2142 | { 2143 | "Name": "transition", 2144 | "ExpectedCount": 1, 2145 | "DisplayName": "The Canals" 2146 | }, 2147 | { 2148 | "Name": "SPECIAL", 2149 | "ExpectedCount": 1, 2150 | "DisplayName": "Kitava Arena" 2151 | } 2152 | ], 2153 | "1_1_1": [ 2154 | { 2155 | "Name": "beachtownsouth", 2156 | "ExpectedCount": 1, 2157 | "DisplayName": "Town" 2158 | } 2159 | ], 2160 | "1_1_4_0": [ 2161 | { 2162 | "Name": "forcedblank", 2163 | "ExpectedCount": 1, 2164 | "DisplayName": "Unique Monster" 2165 | } 2166 | ], 2167 | "1_5_3": [ 2168 | { 2169 | "Name": "slaveden", 2170 | "ExpectedCount": 1, 2171 | "DisplayName": "slaveden" 2172 | }, 2173 | { 2174 | "Name": "03", 2175 | "ExpectedCount": 1, 2176 | "DisplayName": "Templar Courts Entrance" 2177 | } 2178 | ], 2179 | "1_5_4": [ 2180 | { 2181 | "Name": "entrance", 2182 | "ExpectedCount": 1, 2183 | "DisplayName": "Transition" 2184 | }, 2185 | { 2186 | "Name": "innocence_transition", 2187 | "ExpectedCount": 1, 2188 | "DisplayName": "The Chamber Of Innocence" 2189 | } 2190 | ], 2191 | "*_Labyrinth_*": [ 2192 | { 2193 | "Name": "entry", 2194 | "ExpectedCount": 3, 2195 | "DisplayName": "Exit" 2196 | }, 2197 | { 2198 | "Name": "airlock_exit", 2199 | "ExpectedCount": 1, 2200 | "DisplayName": "Back" 2201 | }, 2202 | { 2203 | "Name": "exitup", 2204 | "ExpectedCount": 3, 2205 | "DisplayName": "Exit" 2206 | }, 2207 | { 2208 | "Name": "exit", 2209 | "ExpectedCount": 3, 2210 | "DisplayName": "Exit" 2211 | }, 2212 | { 2213 | "Name": "entranceup", 2214 | "ExpectedCount": 3, 2215 | "DisplayName": "Exit" 2216 | }, 2217 | { 2218 | "Name": "slide", 2219 | "ExpectedCount": 1, 2220 | "DisplayName": "Secret door" 2221 | }, 2222 | { 2223 | "Name": "hidden", 2224 | "ExpectedCount": 1, 2225 | "DisplayName": "Secret door" 2226 | } 2227 | ], 2228 | "Sanctum*": [ 2229 | { 2230 | "Name": "airlock_transition", 2231 | "ExpectedCount": 10, 2232 | "DisplayName": "Transition" 2233 | }, 2234 | { 2235 | "Name": "square_transition", 2236 | "ExpectedCount": 10, 2237 | "DisplayName": "Transition" 2238 | }, 2239 | { 2240 | "Name": "areatransition", 2241 | "ExpectedCount": 1, 2242 | "DisplayName": "Transition" 2243 | }, 2244 | { 2245 | "Name": "transition_down", 2246 | "ExpectedCount": 2, 2247 | "DisplayName": "Transition" 2248 | }, 2249 | { 2250 | "Name": "exitdown", 2251 | "ExpectedCount": 1, 2252 | "DisplayName": "Transition" 2253 | }, 2254 | { 2255 | "Name": "entrance_upper", 2256 | "ExpectedCount": 1, 2257 | "DisplayName": "Transition" 2258 | }, 2259 | { 2260 | "Name": "abyss_transition", 2261 | "ExpectedCount": 1, 2262 | "DisplayName": "Transition" 2263 | }, 2264 | { 2265 | "Name": "transition", 2266 | "ExpectedCount": 3, 2267 | "DisplayName": "Transition" 2268 | } 2269 | ], 2270 | "*Heist*": [ 2271 | { 2272 | "Name": "Metadata/MiscellaneousObjects/Heist/CurioDisplayRoomMarker", 2273 | "ExpectedCount": 1, 2274 | "DisplayName": "Heist target", 2275 | "TargetType": "Entity" 2276 | }, 2277 | { 2278 | "Name": "Metadata/MiscellaneousObjects/AreaTransition", 2279 | "ExpectedCount": 10, 2280 | "DisplayName": "Escape route", 2281 | "TargetType": "Entity" 2282 | } 2283 | ] 2284 | } 2285 | --------------------------------------------------------------------------------