Decompiled source of TrailLedger v0.6.1

plugins/TrailLedger.dll

Decompiled a week ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using Jotunn.Entities;
using Jotunn.Managers;
using Jotunn.Utils;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using Splatform;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("TrailLedger")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.6.1.0")]
[assembly: AssemblyInformationalVersion("0.6.1")]
[assembly: AssemblyProduct("TrailLedger")]
[assembly: AssemblyTitle("TrailLedger")]
[assembly: AssemblyVersion("0.6.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace TrailLedger
{
	public enum Availability
	{
		Unknown,
		Available,
		Regrowing
	}
	public static class TrackingRules
	{
		public static float ClampRadius(float value, float min, float max)
		{
			if (!float.IsNaN(value))
			{
				return Math.Max(min, Math.Min(max, value));
			}
			return min;
		}

		public static bool CanDiscover(string type, string category, bool interior, bool trackInterior, bool wishbone)
		{
			if (type != "silver" || wishbone)
			{
				return category == "Dungeons" || !interior || trackInterior;
			}
			return false;
		}

		public static int Stars(int level)
		{
			return Math.Max(0, level - 1);
		}

		public static bool RadarEligible(bool alive, bool explored, float distanceSquared, float radius, int level, int minimumStars)
		{
			if (alive && explored && distanceSquared <= radius * radius)
			{
				return Stars(level) >= minimumStars;
			}
			return false;
		}
	}
	public static class Renewal
	{
		public static double Remaining(Record r)
		{
			return Math.Max(0.0, r.RespawnSeconds - r.ActiveElapsed);
		}

		public static void Observe(Record r, bool picked, double duration, bool harvest)
		{
			r.LastObserved = ((!picked) ? Availability.Available : Availability.Regrowing);
			if (!picked)
			{
				r.Availability = Availability.Available;
				r.TimerKnown = false;
				r.ActiveElapsed = 0.0;
				return;
			}
			bool flag = duration > 0.0 && !double.IsNaN(duration) && !double.IsInfinity(duration);
			if (harvest && flag)
			{
				r.RespawnSeconds = duration;
				r.ActiveElapsed = 0.0;
				r.TimerKnown = true;
			}
			else if (r.Availability == Availability.Available || (r.TimerKnown && Remaining(r) <= 0.0))
			{
				r.TimerKnown = false;
			}
			r.Availability = Availability.Regrowing;
		}

		public static bool Advance(IEnumerable<Record> records, double seconds, bool active)
		{
			if (!active || seconds <= 0.0 || double.IsNaN(seconds) || double.IsInfinity(seconds))
			{
				return false;
			}
			bool result = false;
			foreach (Record item in records.Where((Record r) => r.TimerKnown && r.Availability == Availability.Regrowing))
			{
				item.ActiveElapsed = Math.Min(item.RespawnSeconds, item.ActiveElapsed + seconds);
				result = true;
				if (Remaining(item) <= 0.0)
				{
					item.Availability = Availability.Unknown;
				}
			}
			return result;
		}

		public static string Details(IReadOnlyList<Record> records)
		{
			int num = records.Count((Record r) => r.Availability == Availability.Available);
			Record record = records.Where((Record r) => r.Availability == Availability.Regrowing && r.TimerKnown).OrderBy(Remaining).FirstOrDefault();
			string text = num + "/" + records.Count + " confirmed available";
			if (record != null)
			{
				TimeSpan timeSpan = TimeSpan.FromSeconds(Remaining(record));
				text = text + "\nRegrowing · " + (int)timeSpan.TotalMinutes + "m " + timeSpan.Seconds + "s known active time remaining";
			}
			else if (num < records.Count)
			{
				text += "\nAvailability unconfirmed / observed empty; timer unknown";
			}
			return text;
		}
	}
	public sealed class CreatureCatalogueCache
	{
		public int Schema = 1;

		public int CatalogueVersion = 1;

		public string GameVersion = "";

		public string ModFingerprint = "";

		public Dictionary<string, CreatureType> Dynamic = new Dictionary<string, CreatureType>();

		public bool IsStale(string game, string mods)
		{
			if (!(GameVersion != game) && !(ModFingerprint != mods))
			{
				return CatalogueVersion != 1;
			}
			return true;
		}
	}
	public static class CatalogueModel
	{
		public static readonly string[] Biomes = new string[9] { "Meadows", "Black Forest", "Swamp", "Mountain", "Plains", "Mistlands", "Ashlands", "Deep North", "Ocean" };

		public static readonly Dictionary<string, string> Progression = new Dictionary<string, string>
		{
			{ "Black Forest", "defeated_eikthyr" },
			{ "Swamp", "defeated_gdking" },
			{ "Mountain", "defeated_bonemass" },
			{ "Plains", "defeated_dragon" },
			{ "Mistlands", "defeated_goblinking" },
			{ "Ashlands", "defeated_queen" },
			{ "Deep North", "defeated_fader" }
		};

		public static string BiomeName(string name)
		{
			return name switch
			{
				"DeepNorth" => "Deep North", 
				"AshLands" => "Ashlands", 
				"BlackForest" => "Black Forest", 
				_ => name, 
			};
		}

		public static bool Unlock(HashSet<string> unlocked, string entered, Func<string, bool> key)
		{
			bool flag = unlocked.Add("Meadows");
			if (!string.IsNullOrEmpty(entered) && entered != "None")
			{
				flag |= unlocked.Add(BiomeName(entered));
			}
			foreach (KeyValuePair<string, string> item in Progression)
			{
				if (key(item.Value))
				{
					flag |= unlocked.Add(item.Key);
				}
			}
			return flag;
		}

		public static void Validate(CreatureCatalogueCache cache)
		{
			if (cache == null || cache.Schema != 1 || cache.Dynamic == null)
			{
				throw new InvalidDataException("Invalid creature cache schema");
			}
			foreach (KeyValuePair<string, CreatureType> item in cache.Dynamic)
			{
				if (item.Value == null || item.Key != item.Value.Id || string.IsNullOrWhiteSpace(item.Key) || string.IsNullOrWhiteSpace(item.Value.Name) || item.Value.Biomes == null || item.Value.Biomes.Length != 1 || string.IsNullOrWhiteSpace(item.Value.Biomes[0]) || item.Value.Aliases == null || item.Value.Aliases.Any(string.IsNullOrWhiteSpace))
				{
					throw new InvalidDataException("Invalid dynamic creature entry");
				}
			}
		}

		public static string SettingsId(VanillaCreature entry, Filters f)
		{
			if (f.CreatureMinimumStars.ContainsKey(entry.Id) || f.CreatureVisible.ContainsKey(entry.Id))
			{
				return entry.Id;
			}
			return entry.Aliases.FirstOrDefault((string a) => f.CreatureMinimumStars.ContainsKey(a)) ?? entry.Aliases.FirstOrDefault((string a) => f.CreatureVisible.ContainsKey(a)) ?? entry.Id;
		}

		public static Dictionary<string, CreatureType> Compose(Filters f, CreatureCatalogueCache cache, Func<string, bool> available)
		{
			Dictionary<string, CreatureType> creatureTypes = f.CreatureTypes;
			Dictionary<string, CreatureType> dictionary = new Dictionary<string, CreatureType>(StringComparer.Ordinal);
			VanillaCreature[] all = VanillaCatalogue.All;
			foreach (VanillaCreature vanillaCreature in all)
			{
				if (vanillaCreature.Aliases.Any(available))
				{
					string text = SettingsId(vanillaCreature, f);
					CreatureType value;
					int maxSpawnLevel = (creatureTypes.TryGetValue(text, out value) ? value.MaxSpawnLevel : vanillaCreature.MaxSpawnLevel);
					dictionary[text] = new CreatureType
					{
						Id = text,
						CanonicalId = vanillaCreature.Id,
						Name = vanillaCreature.Name,
						Family = "Enemies",
						Biomes = new string[1] { vanillaCreature.Biome },
						Aliases = vanillaCreature.Aliases,
						Special = vanillaCreature.Special,
						Boss = vanillaCreature.Boss,
						Neutral = vanillaCreature.Neutral,
						Tameable = vanillaCreature.Tameable,
						MaxSpawnLevel = maxSpawnLevel
					};
				}
			}
			foreach (CreatureType value2 in cache.Dynamic.Values)
			{
				if (!VanillaCatalogue.KnownVanilla.Contains(value2.Id) && !VanillaCatalogue.ByPrefab.ContainsKey(value2.Id))
				{
					dictionary[value2.Id] = value2;
				}
			}
			return dictionary;
		}

		public static IEnumerable<CreatureType> Target(CreatureCatalogueCache cache, string biome)
		{
			return cache.Dynamic.Values.Where((CreatureType t) => biome == null || t.Biomes.Contains(biome)).ToArray();
		}

		public static void ReplaceTarget(CreatureCatalogueCache cache, string biome, IEnumerable<CreatureType> entries)
		{
			CreatureType[] array = Target(cache, biome).ToArray();
			foreach (CreatureType creatureType in array)
			{
				cache.Dynamic.Remove(creatureType.Id);
			}
			foreach (CreatureType entry in entries)
			{
				if (!VanillaCatalogue.KnownVanilla.Contains(entry.Id) && !VanillaCatalogue.ByPrefab.ContainsKey(entry.Id) && (biome == null || entry.Biomes.Contains(biome)))
				{
					cache.Dynamic[entry.Id] = entry;
				}
			}
		}
	}
	public sealed class Definition
	{
		public string Id;

		public string Name;

		public string Category;

		public string Behavior;

		public string[] Prefabs;

		public Definition(string id, string name, string category, string behavior, params string[] prefabs)
		{
			Id = id;
			Name = name;
			Category = category;
			Behavior = behavior;
			Prefabs = prefabs;
		}
	}
	public static class Registry
	{
		public static readonly Definition[] All = new Definition[21]
		{
			new Definition("copper", "Copper", "Ores", "Depletable", "rock4_copper"),
			new Definition("tin", "Tin", "Ores", "Depletable", "MineRock_Tin"),
			new Definition("silver", "Silver", "Ores", "Depletable", "silvervein"),
			new Definition("flametal", "Flametal", "Ores", "Depletable", "LeviathanLava", "MineRock_Meteorite"),
			new Definition("raspberries", "Raspberries", "Foraging", "Renewable", "RaspberryBush"),
			new Definition("blueberries", "Blueberries", "Foraging", "Renewable", "BlueberryBush"),
			new Definition("cloudberries", "Cloudberries", "Foraging", "Renewable", "CloudberryBush"),
			new Definition("mushrooms", "Mushrooms", "Foraging", "Renewable", "Pickable_Mushroom"),
			new Definition("yellow_mushrooms", "Yellow mushrooms", "Foraging", "Renewable", "Pickable_Mushroom_yellow"),
			new Definition("thistle", "Thistle", "Foraging", "Renewable", "Pickable_Thistle"),
			new Definition("jotun", "Jotun puffs", "Foraging", "Renewable", "Pickable_Mushroom_JotunPuffs"),
			new Definition("magecaps", "Magecaps", "Foraging", "Renewable", "Pickable_Mushroom_Magecap"),
			new Definition("carrot", "Carrot seeds", "Seeds", "Depletable", "Pickable_SeedCarrot"),
			new Definition("turnip", "Turnip seeds", "Seeds", "Depletable", "Pickable_SeedTurnip"),
			new Definition("burial", "Burial Chambers", "Dungeons", "Permanent", "Crypt2", "Crypt3", "Crypt4"),
			new Definition("crypt", "Sunken Crypts", "Dungeons", "Permanent", "SunkenCrypt4"),
			new Definition("caves", "Frost Caves", "Dungeons", "Permanent", "MountainCave02"),
			new Definition("mines", "Infested Mines", "Dungeons", "Permanent", "Mistlands_DvergrTownEntrance1", "Mistlands_DvergrTownEntrance2"),
			new Definition("leviathan", "Leviathans", "Other", "Special", "Leviathan"),
			new Definition("surtling", "Surtling spawners", "Other", "Special", "FireHole"),
			new Definition("greydwarf", "Greydwarf nests", "Other", "Special", "Spawner_GreydwarfNest")
		};

		public static readonly Dictionary<string, Definition> Prefabs = All.SelectMany((Definition d) => d.Prefabs.Select((string p) => new { p, d })).ToDictionary(x => x.p, x => x.d, StringComparer.Ordinal);

		public static readonly string[] Categories = new string[5] { "Ores", "Foraging", "Seeds", "Dungeons", "Other" };
	}
	public sealed class Record
	{
		public bool Completed;

		public Availability Availability;

		public Availability LastObserved;

		public double RespawnSeconds;

		public double ActiveElapsed;

		public bool TimerKnown;

		public string Id = Guid.NewGuid().ToString("N");

		public string Type;

		public string Category;

		public string Behavior;

		public string Prefab;

		public string State = "Discovered";

		public float X;

		public float Y;

		public float Z;

		public DateTime DiscoveredUtc = DateTime.UtcNow;

		public Dictionary<string, string> Metadata = new Dictionary<string, string>();
	}
	public sealed class Database
	{
		public HashSet<string> UnlockedCreatureBiomes = new HashSet<string> { "Meadows" };

		public int Schema = 2;

		public long World;

		public List<Record> Records = new List<Record>();
	}
	public sealed class Ledger
	{
		private readonly Dictionary<string, List<Record>> cells = new Dictionary<string, List<Record>>();

		public Database Data { get; private set; }

		public Ledger(Database data)
		{
			Data = data;
			foreach (Record record in data.Records)
			{
				Index(record);
			}
		}

		private string Cell(string type, int x, int z)
		{
			return type + ":" + x + ":" + z;
		}

		private int Grid(float n)
		{
			return (int)Math.Floor(n / 2f);
		}

		private void Index(Record r)
		{
			string key = Cell(r.Type, Grid(r.X), Grid(r.Z));
			if (!cells.TryGetValue(key, out var value))
			{
				value = (cells[key] = new List<Record>());
			}
			value.Add(r);
		}

		public bool Discover(Definition d, float x, float y, float z, string prefab)
		{
			if (Find(d.Id, x, y, z) != null)
			{
				return false;
			}
			Record record = new Record
			{
				Type = d.Id,
				Category = d.Category,
				Behavior = d.Behavior,
				X = x,
				Y = y,
				Z = z,
				Prefab = prefab
			};
			Data.Records.Add(record);
			Index(record);
			return true;
		}

		public Record Find(string type, float x, float y, float z)
		{
			for (int i = -1; i <= 1; i++)
			{
				for (int j = -1; j <= 1; j++)
				{
					if (!cells.TryGetValue(Cell(type, Grid(x) + i, Grid(z) + j), out var value))
					{
						continue;
					}
					foreach (Record item in value)
					{
						if ((item.X - x) * (item.X - x) + (item.Y - y) * (item.Y - y) + (item.Z - z) * (item.Z - z) <= 1f)
						{
							return item;
						}
					}
				}
			}
			return null;
		}
	}
	public enum PinLabelMode
	{
		BothMaps,
		FullMapOnly,
		Hidden
	}
	public static class PinLabelRules
	{
		public static bool Show(PinLabelMode mode, bool large)
		{
			if (mode != PinLabelMode.BothMaps)
			{
				return mode == PinLabelMode.FullMapOnly && large;
			}
			return true;
		}

		public static PinLabelMode Next(PinLabelMode mode)
		{
			return mode switch
			{
				PinLabelMode.FullMapOnly => PinLabelMode.Hidden, 
				PinLabelMode.BothMaps => PinLabelMode.FullMapOnly, 
				_ => PinLabelMode.BothMaps, 
			};
		}

		public static string Title(PinLabelMode mode)
		{
			return mode switch
			{
				PinLabelMode.Hidden => "Hidden", 
				PinLabelMode.FullMapOnly => "Full Map Only", 
				_ => "Both Maps", 
			};
		}
	}
	public sealed class Filters
	{
		public PinLabelMode PinLabels;

		public Dictionary<string, int> CreatureMinimumStars = new Dictionary<string, int>();

		public Dictionary<string, bool> ExpandedBiomes = new Dictionary<string, bool>();

		public Dictionary<string, CreatureType> CreatureTypes = new Dictionary<string, CreatureType>();

		public Dictionary<string, bool> CreatureVisible = new Dictionary<string, bool>();

		public bool NewCreatureTypesVisible = true;

		public bool ShowCompleted = true;

		public bool ShowUncompleted = true;

		public bool TrackInteriors;

		public bool ShowCreatures;

		public bool Animals = true;

		public bool Enemies = true;

		public bool Tamed = true;

		public bool Bosses = true;

		public int MinimumStars;

		public int Schema = 1;

		public Dictionary<string, bool> Visible = new Dictionary<string, bool>();

		public int CreatureStars(string id)
		{
			if (!CreatureMinimumStars.TryGetValue(id, out var value))
			{
				return 0;
			}
			return value;
		}

		public void CreatureBiome(string biome, bool value)
		{
			foreach (CreatureType item in CreatureTypes.Values.Where((CreatureType t) => t.Biomes.Contains(biome)))
			{
				CreatureVisible[item.Id] = value;
			}
		}

		public bool CreatureTypeVisible(string id)
		{
			if (!CreatureVisible.TryGetValue(id, out var value))
			{
				return NewCreatureTypesVisible;
			}
			return value;
		}

		public bool RegisterCreature(CreatureType type)
		{
			if (CreatureTypes.ContainsKey(type.Id))
			{
				return false;
			}
			CreatureTypes.Add(type.Id, type);
			return true;
		}

		public void AllCreatureTypes(bool value)
		{
			NewCreatureTypesVisible = value;
			foreach (string key in CreatureTypes.Keys)
			{
				CreatureVisible[key] = value;
			}
		}

		public void CreatureFamily(string family, bool value)
		{
			foreach (CreatureType item in CreatureTypes.Values.Where((CreatureType t) => t.Family == family))
			{
				CreatureVisible[item.Id] = value;
			}
		}

		public bool ShouldSerializeMinimumStars()
		{
			return Schema < 2;
		}

		public bool ShouldSerializeAnimals()
		{
			return Schema < 2;
		}

		public bool ShouldSerializeBosses()
		{
			return Schema < 2;
		}

		public bool DungeonVisible(Record r)
		{
			if (!(r.Category != "Dungeons"))
			{
				if (!r.Completed)
				{
					return ShowUncompleted;
				}
				return ShowCompleted;
			}
			return true;
		}

		public bool IsVisible(string id)
		{
			bool value;
			return !Visible.TryGetValue(id, out value) || value;
		}

		public void Category(string category, bool value)
		{
			foreach (Definition item in Registry.All.Where((Definition d) => d.Category == category))
			{
				Visible[item.Id] = value;
			}
		}

		public void All(bool value)
		{
			Definition[] all = Registry.All;
			foreach (Definition definition in all)
			{
				Visible[definition.Id] = value;
			}
		}

		public void Preset(string category)
		{
			All(value: false);
			Category(category, value: true);
		}
	}
	public static class Storage
	{
		public static Filters LoadFilters(string path, Action<string> log)
		{
			Filters filters = Load(path, () => new Filters
			{
				Schema = 2
			}, Validate, log);
			if (filters.Schema == 1)
			{
				if (File.Exists(path))
				{
					File.Copy(path, path + ".pre-0.5-" + DateTime.UtcNow.ToString("yyyyMMddHHmmssfff") + ".bak");
				}
				foreach (CreatureType value in filters.CreatureTypes.Values)
				{
					if (!filters.CreatureVisible.ContainsKey(value.Id))
					{
						filters.CreatureVisible[value.Id] = filters.CreatureTypeVisible(value.Id) && ((value.Family == "Animals") ? filters.Animals : ((value.Family == "Bosses") ? filters.Bosses : filters.Enemies));
					}
					value.Boss = value.Family == "Bosses";
					value.Family = "Enemies";
				}
				filters.MinimumStars = 0;
				filters.Schema = 2;
				Save(path, filters);
				log("Filters migrated to schema 2; original backed up. Per-creature minimum stars start at Any.");
			}
			return filters;
		}

		public static Database LoadWorld(string root, long world, Action<string> log)
		{
			string text = WorldPath(root, world);
			Database database = Load(text, () => new Database
			{
				World = world
			}, delegate(Database d)
			{
				Validate(d, world);
			}, log);
			if (database.Schema == 1)
			{
				if (File.Exists(text) && !File.Exists(text + ".pre-0.4.bak"))
				{
					File.Copy(text, text + ".pre-0.4.bak");
				}
				database.Schema = 2;
				Save(text, database);
				log("Migrated schema 1 to 2; original preserved as .pre-0.4.bak");
			}
			return database;
		}

		public static string WorldPath(string root, long world)
		{
			return Path.Combine(root, "world-" + world.ToString(CultureInfo.InvariantCulture) + ".json");
		}

		public static void Save(string path, object data)
		{
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			string text = path + ".tmp";
			using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None))
			{
				using StreamWriter streamWriter = new StreamWriter(fileStream);
				streamWriter.Write(JsonConvert.SerializeObject(data, (Formatting)1));
				streamWriter.Flush();
				fileStream.Flush(flushToDisk: true);
			}
			if (File.Exists(path))
			{
				File.Replace(text, path, path + ".bak");
			}
			else
			{
				File.Move(text, path);
			}
		}

		public static T Load<T>(string path, Func<T> fresh, Action<T> validate, Action<string> log)
		{
			if (!File.Exists(path))
			{
				return fresh();
			}
			try
			{
				T val = JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
				if (val == null)
				{
					throw new InvalidDataException("Null data");
				}
				validate(val);
				return val;
			}
			catch (Exception ex)
			{
				string text = path + ".invalid-" + DateTime.UtcNow.ToString("yyyyMMddHHmmssfff") + "-" + Guid.NewGuid().ToString("N");
				File.Copy(path, text);
				log("Invalid data preserved at " + text + ": " + ex.Message);
				if (File.Exists(path + ".bak"))
				{
					try
					{
						T val2 = JsonConvert.DeserializeObject<T>(File.ReadAllText(path + ".bak"));
						validate(val2);
						log("Recovered last good backup");
						return val2;
					}
					catch (Exception)
					{
					}
				}
				return fresh();
			}
		}

		public static void Validate(Database d, long world)
		{
			if (d == null || (d.Schema != 1 && d.Schema != 2) || d.World != world || d.Records == null || d.UnlockedCreatureBiomes == null)
			{
				throw new InvalidDataException("Schema/world mismatch");
			}
			HashSet<string> hashSet = new HashSet<string>();
			foreach (Record record in d.Records)
			{
				if (record == null || string.IsNullOrEmpty(record.Id) || !hashSet.Add(record.Id) || string.IsNullOrEmpty(record.Type) || float.IsNaN(record.X) || float.IsInfinity(record.X) || float.IsNaN(record.Y) || float.IsInfinity(record.Y) || float.IsNaN(record.Z) || float.IsInfinity(record.Z))
				{
					throw new InvalidDataException("Invalid record");
				}
			}
			foreach (Record record2 in d.Records)
			{
				if (!Enum.IsDefined(typeof(Availability), record2.Availability) || !Enum.IsDefined(typeof(Availability), record2.LastObserved) || double.IsNaN(record2.RespawnSeconds) || double.IsInfinity(record2.RespawnSeconds) || record2.RespawnSeconds < 0.0 || double.IsNaN(record2.ActiveElapsed) || double.IsInfinity(record2.ActiveElapsed) || record2.ActiveElapsed < 0.0 || record2.ActiveElapsed > record2.RespawnSeconds)
				{
					throw new InvalidDataException("Invalid availability state");
				}
			}
		}

		public static void Validate(Filters f)
		{
			if (f == null || (f.Schema != 1 && f.Schema != 2) || f.Visible == null || f.CreatureTypes == null || f.CreatureVisible == null || f.CreatureMinimumStars == null || f.ExpandedBiomes == null)
			{
				throw new InvalidDataException("Invalid filter schema");
			}
			if (f.CreatureMinimumStars.Values.Any((int v) => v < 0))
			{
				throw new InvalidDataException("Negative star threshold");
			}
			foreach (KeyValuePair<string, CreatureType> creatureType in f.CreatureTypes)
			{
				if (creatureType.Value == null || creatureType.Value.Id != creatureType.Key || string.IsNullOrWhiteSpace(creatureType.Value.Name) || (!(creatureType.Value.Family == "Animals") && !(creatureType.Value.Family == "Enemies") && !(creatureType.Value.Family == "Bosses")))
				{
					throw new InvalidDataException("Invalid creature type metadata");
				}
			}
		}
	}
	[BepInPlugin("au.astra.trailledger", "TrailLedger", "0.6.1")]
	[BepInDependency("com.jotunn.jotunn", "2.30.0")]
	[NetworkCompatibility(/*Could not decode attribute arguments.*/)]
	public sealed class Plugin : BaseUnityPlugin
	{
		private sealed class Candidate
		{
			public Transform Transform;

			public Definition Definition;

			public string Prefab;

			public bool Recorded;

			public Record Record;

			public Pickable Pickable;
		}

		private sealed class TrailCommand : ConsoleCommand
		{
			private readonly Plugin plugin;

			public override string Name => "trailledger";

			public override string Help => "stats";

			public TrailCommand(Plugin plugin)
			{
				this.plugin = plugin;
			}

			public override void Run(string[] args, Terminal context)
			{
				try
				{
					ReadOnlySpan<string> span = args;
					object obj = global::<PrivateImplementationDetails>.A7F86D8FB88353423EFADCD3682785B5CF60DA981CF9EA7A9F6848C0065B92C8_B11;
					if (obj == null)
					{
						obj = new string[1] { "stats" };
						global::<PrivateImplementationDetails>.A7F86D8FB88353423EFADCD3682785B5CF60DA981CF9EA7A9F6848C0065B92C8_B11 = (string[])obj;
					}
					string text = (span.SequenceEqual(new ReadOnlySpan<string>((string[]?)obj)) ? plugin.Metrics() : ((ConsoleCommand)this).Help);
					string[] array = text.Split('\n');
					foreach (string text2 in array)
					{
						if (context != null)
						{
							context.AddString(text2);
						}
					}
					((BaseUnityPlugin)plugin).Logger.LogInfo((object)text);
				}
				catch (Exception ex)
				{
					if (context != null)
					{
						context.AddString("TrailLedger command failed: " + ex.Message);
					}
					((BaseUnityPlugin)plugin).Logger.LogError((object)ex);
				}
			}
		}

		private ZNetScene catalogueScene;

		private Coroutine catalogueJob;

		private CreatureCatalogueCache catalogueCache;

		private readonly Dictionary<string, CreatureType> catalogueAliases = new Dictionary<string, CreatureType>(StringComparer.Ordinal);

		private string cataloguePath;

		private string catalogueStatus = "";

		private string modFingerprint;

		private bool catalogueDirty;

		private float nextUnlockCheck;

		private readonly Dictionary<Character, CreatureType> liveTypes = new Dictionary<Character, CreatureType>();

		private int creatureTypeRevision;

		private float dungeonDiagnosticsUntil;

		private float lastOwnedSelectionAt = float.NegativeInfinity;

		private Vector2 lastOwnedPointer;

		private static readonly MethodInfo closestSavedPin = AccessTools.Method(typeof(Minimap), "GetClosestPinToCursor", (Type[])null, (Type[])null);

		private long blockedMapWheels;

		private long passedMapWheels;

		public const string Guid = "au.astra.trailledger";

		private static Plugin instance;

		private Harmony harmony;

		private ConfigEntry<bool> discover;

		private ConfigEntry<bool> uiEnabled;

		private ConfigEntry<bool> debug;

		private ConfigEntry<bool> checkCreatureRows;

		private ConfigEntry<float> radius;

		private ConfigEntry<float> eligibilityInterval;

		private ConfigEntry<float> fallbackInterval;

		private ConfigEntry<int> pinLimit;

		private string root;

		private string filterPath;

		private Filters filters;

		private Ledger ledger;

		private bool dirty;

		private bool filtersDirty;

		private bool markerDirty;

		private float nextCheck;

		private float nextFallback;

		private float nextSave;

		private float nextMarkers;

		private float nextMetrics;

		private long world;

		private long callbacks;

		private long ignored;

		private long recognized;

		private long newRecords;

		private long fallbacks;

		private ExplorationGate exploration;

		private readonly CandidateIndex<Candidate> candidates = new CandidateIndex<Candidate>();

		private readonly HashSet<string> seen = new HashSet<string>();

		private OwnedMarkers<PinData> markers;

		private readonly DisplayCache display = new DisplayCache();

		private GroupingConfig grouping;

		private bool discoveryPaused;

		private bool hasRenderPosition;

		private Vector3 renderPosition;

		private Minimap map;

		private IconProvider icons;

		private MapPanel panel;

		private bool uiFailed;

		private ConfigEntry<float> radarRadius;

		private RadiusPreview preview;

		private DisplayGroup selectedGroup;

		private readonly Dictionary<Character, PinData> radar = new Dictionary<Character, PinData>();

		private float nextRadar;

		private float nextVisual;

		private bool lastInterior;

		private bool lastWishbone;

		private double activePending;

		private double activeTotal;

		private long suppressed;

		private void InitializeCatalogue()
		{
			cataloguePath = Path.Combine(root, "creature-catalog.json");
			using (StartupTimings.Measure("modFingerprint"))
			{
				using SHA256 sHA = SHA256.Create();
				modFingerprint = BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(string.Join(";", (from p in Chainloader.PluginInfos.Values
					where p.Metadata.GUID != "au.astra.trailledger"
					select p.Metadata.GUID + "=" + p.Metadata.Version).OrderBy<string, string>((string s) => s, StringComparer.Ordinal))))).Replace("-", "");
			}
			using (StartupTimings.Measure("cacheLoad"))
			{
				catalogueCache = Storage.Load(cataloguePath, () => new CreatureCatalogueCache
				{
					GameVersion = Version.GetVersionString(false),
					ModFingerprint = modFingerprint
				}, CatalogueModel.Validate, delegate(string s)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)s);
					catalogueDirty = true;
				});
			}
			catalogueDirty |= !File.Exists(cataloguePath);
			if (catalogueCache.IsStale(Version.GetVersionString(false), modFingerprint))
			{
				catalogueStatus = "Creature catalogue may be out of date. Rebuild available in Settings.";
			}
			else
			{
				catalogueStatus = "Curated vanilla catalogue + cached modded types. No startup scan.";
			}
		}

		private void RefreshCatalogue()
		{
			if (!Object.op_Implicit((Object)(object)ZNetScene.instance))
			{
				return;
			}
			if ((Object)(object)catalogueScene != (Object)(object)ZNetScene.instance)
			{
				catalogueScene = ZNetScene.instance;
				using (StartupTimings.Measure("catalogue"))
				{
					foreach (CreatureType value in filters.CreatureTypes.Values)
					{
						if (!VanillaCatalogue.KnownVanilla.Contains(value.Id) && !VanillaCatalogue.ByPrefab.ContainsKey(value.Id) && !catalogueCache.Dynamic.ContainsKey(value.Id))
						{
							value.Biomes = new string[1] { (value.Biomes != null && value.Biomes.Length == 1) ? CatalogueModel.BiomeName(value.Biomes[0]) : "Other / Modded" };
							value.CanonicalId = value.Id;
							value.Aliases = new string[1] { value.Id };
							catalogueCache.Dynamic[value.Id] = value;
							catalogueDirty = true;
						}
					}
					ApplyCatalogue();
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)("TrailLedger startup timings (cumulative): " + StartupTimings.Summary() + "; globalScan=0; locationDungeonScan=0; types=" + filters.CreatureTypes.Count));
			}
			if (Time.unscaledTime >= nextUnlockCheck)
			{
				nextUnlockCheck = Time.unscaledTime + 1f;
				UpdateCatalogueUnlocks();
			}
		}

		private void ApplyCatalogue()
		{
			using (StartupTimings.Measure("prefabLookupAndAliases"))
			{
				filters.CreatureTypes = CatalogueModel.Compose(filters, catalogueCache, (string id) => Object.op_Implicit((Object)(object)catalogueScene) && Object.op_Implicit((Object)(object)catalogueScene.GetPrefab(id)));
			}
			catalogueAliases.Clear();
			liveTypes.Clear();
			foreach (CreatureType value in filters.CreatureTypes.Values)
			{
				catalogueAliases[value.Id] = value;
				string[] array = value.Aliases ?? Array.Empty<string>();
				foreach (string key in array)
				{
					catalogueAliases[key] = value;
				}
			}
			creatureTypeRevision++;
			filtersDirty = true;
			panel?.RefreshCreatureTypes(creatureTypeRevision);
		}

		private string CurrentCreatureBiome()
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)Player.m_localPlayer))
			{
				return null;
			}
			return CatalogueModel.BiomeName(((object)Player.m_localPlayer.GetCurrentBiome()/*cast due to .constrained prefix*/).ToString());
		}

		private bool CreatureBiomeUnlocked(string biome)
		{
			if (!(biome == "Other / Modded"))
			{
				if (ledger != null)
				{
					return ledger.Data.UnlockedCreatureBiomes.Contains(biome);
				}
				return false;
			}
			return true;
		}

		private void UpdateCatalogueUnlocks()
		{
			Player localPlayer = Player.m_localPlayer;
			if (Object.op_Implicit((Object)(object)localPlayer) && ledger != null && Object.op_Implicit((Object)(object)ZNet.instance) && ZNet.instance.GetWorldUID() == world)
			{
				string entered = ((!((Character)localPlayer).InIntro() && !((Character)localPlayer).InInterior() && !((Character)localPlayer).IsDead()) ? CurrentCreatureBiome() : null);
				if (CatalogueModel.Unlock(ledger.Data.UnlockedCreatureBiomes, entered, (string key) => Object.op_Implicit((Object)(object)ZoneSystem.instance) && ZoneSystem.instance.GetGlobalKey(key)))
				{
					dirty = true;
					creatureTypeRevision++;
					panel?.RefreshCreatureTypes(creatureTypeRevision);
				}
			}
		}

		private CreatureType DynamicDefinition(GameObject prefab, string biome)
		{
			//IL_007c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Invalid comparison between Unknown and I4
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Invalid comparison between Unknown and I4
			if (!Object.op_Implicit((Object)(object)prefab) || VanillaCatalogue.KnownVanilla.Contains(((Object)prefab).name) || VanillaCatalogue.ByPrefab.ContainsKey(((Object)prefab).name))
			{
				return null;
			}
			Character component = prefab.GetComponent<Character>();
			if (!Object.op_Implicit((Object)(object)component) || component.IsPlayer() || !Object.op_Implicit((Object)(object)prefab.GetComponent<BaseAI>()) || !Object.op_Implicit((Object)(object)prefab.GetComponent<ZNetView>()) || Object.op_Implicit((Object)(object)prefab.GetComponent<Trader>()) || string.IsNullOrWhiteSpace(component.m_name))
			{
				return null;
			}
			Faction faction = component.GetFaction();
			if ((int)faction == 12 || (int)faction == 0 || (int)faction == 11)
			{
				return null;
			}
			string text = DiscoveryRules.Normalize(((Object)prefab).name);
			string text2 = Localization.instance.Localize(component.m_name);
			if (string.IsNullOrWhiteSpace(text2) || text2.StartsWith("$", StringComparison.Ordinal))
			{
				text2 = text;
			}
			CreatureType value;
			int maxSpawnLevel = ((!filters.CreatureTypes.TryGetValue(text, out value)) ? 1 : value.MaxSpawnLevel);
			CreatureType creatureType = new CreatureType();
			creatureType.Id = text;
			creatureType.CanonicalId = text;
			creatureType.Aliases = new string[1] { text };
			creatureType.Name = text2;
			creatureType.Family = "Enemies";
			creatureType.Boss = component.IsBoss();
			creatureType.Biomes = new string[1] { biome ?? "Other / Modded" };
			creatureType.MaxSpawnLevel = maxSpawnLevel;
			return creatureType;
		}

		private CreatureType EncounterDynamic(GameObject prefab, string biome)
		{
			CreatureType creatureType = DynamicDefinition(prefab, biome);
			if (creatureType == null)
			{
				return null;
			}
			if (catalogueCache.Dynamic.TryGetValue(creatureType.Id, out var value))
			{
				creatureType.Biomes = value.Biomes;
				creatureType.MaxSpawnLevel = value.MaxSpawnLevel;
			}
			catalogueCache.Dynamic[creatureType.Id] = creatureType;
			filters.CreatureTypes[creatureType.Id] = creatureType;
			catalogueAliases[creatureType.Id] = creatureType;
			catalogueDirty = true;
			filtersDirty = true;
			creatureTypeRevision++;
			return creatureType;
		}

		private void SaveCatalogue()
		{
			if (catalogueDirty && catalogueCache != null)
			{
				using (StartupTimings.Measure("cacheSave"))
				{
					Storage.Save(cataloguePath, catalogueCache);
				}
				catalogueDirty = false;
			}
		}

		private void RebuildCreatureCatalogue(string target)
		{
			if (catalogueJob != null)
			{
				catalogueStatus = "A catalogue rebuild is already running.";
				return;
			}
			if (target == "@current")
			{
				target = CurrentCreatureBiome();
				if (string.IsNullOrEmpty(target) || target == "None")
				{
					return;
				}
			}
			if (Object.op_Implicit((Object)(object)catalogueScene) && world != 0L)
			{
				catalogueJob = ((MonoBehaviour)this).StartCoroutine(RebuildDynamicCatalogue(target, world));
			}
		}

		private IEnumerator RebuildDynamicCatalogue(string target, long expectedWorld)
		{
			Stopwatch elapsed = Stopwatch.StartNew();
			double inspectMs = 0.0;
			int inspected = 0;
			catalogueStatus = "Rebuilding " + (target ?? "entire") + " creature catalogue…";
			yield return null;
			Dictionary<string, CreatureType> updated = new Dictionary<string, CreatureType>(StringComparer.Ordinal);
			string[] array = ((target == null) ? (from p in catalogueScene.m_prefabs
				where Object.op_Implicit((Object)(object)p)
				select ((Object)p).name).ToArray() : (from t in CatalogueModel.Target(catalogueCache, target)
				select t.CanonicalId ?? t.Id).ToArray());
			if (target != null && target == CurrentCreatureBiome())
			{
				array = array.Concat(from id in Character.GetAllCharacters().Where(delegate(Character c)
					{
						//IL_000e: Unknown result type (might be due to invalid IL or missing references)
						//IL_001d: Unknown result type (might be due to invalid IL or missing references)
						//IL_0022: Unknown result type (might be due to invalid IL or missing references)
						//IL_0027: Unknown result type (might be due to invalid IL or missing references)
						if (Object.op_Implicit((Object)(object)c))
						{
							Vector3 val = ((Component)c).transform.position - ((Component)Player.m_localPlayer).transform.position;
							return ((Vector3)(ref val)).sqrMagnitude <= radarRadius.Value * radarRadius.Value;
						}
						return false;
					}).Select(delegate(Character c)
					{
						ZNetView component = ((Component)c).GetComponent<ZNetView>();
						GameObject val = ((Object.op_Implicit((Object)(object)component) && component.IsValid()) ? catalogueScene.GetPrefab(component.GetZDO().GetPrefab()) : null);
						return (!Object.op_Implicit((Object)(object)val)) ? null : ((Object)val).name;
					})
					where id != null
					select id).Distinct().ToArray();
			}
			Stopwatch budget = Stopwatch.StartNew();
			string[] array2 = array;
			foreach (string text in array2)
			{
				if (world != expectedWorld || !Object.op_Implicit((Object)(object)catalogueScene))
				{
					yield break;
				}
				Stopwatch stopwatch = Stopwatch.StartNew();
				try
				{
					CreatureType value;
					string biome = target ?? (catalogueCache.Dynamic.TryGetValue(text, out value) ? value.Biomes[0] : "Other / Modded");
					CreatureType creatureType = DynamicDefinition(catalogueScene.GetPrefab(text), biome);
					if (creatureType != null)
					{
						updated[creatureType.Id] = creatureType;
					}
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Dynamic catalogue entry skipped: " + text + ": " + ex.Message));
				}
				inspectMs += stopwatch.Elapsed.TotalMilliseconds;
				inspected++;
				if (budget.ElapsedMilliseconds >= 3)
				{
					yield return null;
					budget.Restart();
				}
			}
			CatalogueModel.ReplaceTarget(catalogueCache, target, updated.Values);
			if (target == null)
			{
				catalogueCache.GameVersion = Version.GetVersionString(false);
				catalogueCache.ModFingerprint = modFingerprint;
				catalogueCache.CatalogueVersion = 1;
			}
			catalogueDirty = true;
			try
			{
				SaveCatalogue();
				ApplyCatalogue();
				catalogueStatus = "Rebuilt " + (target ?? "entire") + " catalogue. " + updated.Count + " dynamic types." + (catalogueCache.IsStale(Version.GetVersionString(false), modFingerprint) ? " Other categories may still be out of date." : "");
			}
			catch (Exception ex2)
			{
				catalogueStatus = "Rebuild save failed; pending cache retained: " + ex2.Message;
				((BaseUnityPlugin)this).Logger.LogError((object)catalogueStatus);
			}
			elapsed.Stop();
			((BaseUnityPlugin)this).Logger.LogInfo((object)("TrailLedger catalogue rebuild: target=" + (target ?? "all") + "; inspected=" + inspected + "; inspection=" + inspectMs.ToString("F1") + "ms; elapsed=" + elapsed.Elapsed.TotalMilliseconds.ToString("F1") + "ms; " + StartupTimings.Summary()));
			catalogueJob = null;
			panel?.RefreshCreatureTypes(++creatureTypeRevision);
		}

		private string CreatureFamily(Character c)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Invalid comparison between Unknown and I4
			if ((int)c.GetFaction() != 12)
			{
				return "Enemies";
			}
			return null;
		}

		private CreatureType CreatureTypeFor(Character c)
		{
			if (liveTypes.TryGetValue(c, out var value))
			{
				return value;
			}
			ZNetView component = ((Component)c).GetComponent<ZNetView>();
			if (!Object.op_Implicit((Object)(object)component) || !component.IsValid() || string.IsNullOrWhiteSpace(c.m_name) || !Object.op_Implicit((Object)(object)((Component)c).GetComponent<BaseAI>()))
			{
				return null;
			}
			GameObject val = (Object.op_Implicit((Object)(object)ZNetScene.instance) ? ZNetScene.instance.GetPrefab(component.GetZDO().GetPrefab()) : null);
			string text = DiscoveryRules.Normalize(Object.op_Implicit((Object)(object)val) ? ((Object)val).name : ((Object)((Component)c).gameObject).name);
			if (string.IsNullOrWhiteSpace(text))
			{
				return null;
			}
			if (VanillaCatalogue.KnownVanilla.Contains(text) && !VanillaCatalogue.ByPrefab.ContainsKey(text))
			{
				return null;
			}
			if (!catalogueAliases.TryGetValue(text, out var value2))
			{
				value2 = EncounterDynamic(val, CurrentCreatureBiome());
			}
			if (value2 == null)
			{
				return null;
			}
			liveTypes[c] = value2;
			return value2;
		}

		private void UpdateRadar(Player player)
		{
			//IL_007f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ba: Unknown result type (might be due to invalid IL or missing references)
			//IL_022f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0230: Unknown result type (might be due to invalid IL or missing references)
			//IL_018e: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01af: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b7: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)map))
			{
				ClearRadar();
				return;
			}
			HashSet<Character> wanted = new HashSet<Character>();
			HashSet<Character> represented = new HashSet<Character>();
			foreach (Character allCharacter in Character.GetAllCharacters())
			{
				if (!Object.op_Implicit((Object)(object)allCharacter) || allCharacter.IsPlayer() || !((Component)allCharacter).gameObject.activeInHierarchy || allCharacter.IsDead())
				{
					continue;
				}
				Vector3 position = ((Component)allCharacter).transform.position;
				if (allCharacter.InInterior() != ((Character)player).InInterior())
				{
					continue;
				}
				bool explored = exploration.Contains(map, position);
				Vector3 val = position - ((Component)player).transform.position;
				if (!TrackingRules.RadarEligible(alive: true, explored, ((Vector3)(ref val)).sqrMagnitude, radarRadius.Value, allCharacter.GetLevel(), 0))
				{
					continue;
				}
				represented.Add(allCharacter);
				CreatureType creatureType = CreatureTypeFor(allCharacter);
				if (creatureType == null)
				{
					continue;
				}
				string text = CreatureRules.Category(allCharacter.IsTamed(), allCharacter.IsBoss(), creatureType.Family);
				if (CreatureRules.Visible(filters, creatureType.Id, text, allCharacter.GetLevel()))
				{
					wanted.Add(allCharacter);
					int num = TrackingRules.Stars(allCharacter.GetLevel());
					string text2 = ((num <= 8) ? new string('★', num) : ("★×" + num));
					if (!radar.TryGetValue(allCharacter, out var value))
					{
						value = map.AddPin(position, (PinType)8, creatureType.Name + " " + text2, false, false, 0L, default(PlatformUserID));
						radar.Add(allCharacter, value);
						value.m_icon = icons.Creature(allCharacter, creatureType, map.m_selectedIcon3.sprite);
						Trace("Radar register " + creatureType.Id + "; category=" + text + "; stars=" + num);
					}
					value.m_pos = position;
					value.m_name = creatureType.Name + " " + text2;
					PinNameData namePinData = value.m_NamePinData;
					if (Object.op_Implicit((Object)(object)((namePinData != null) ? namePinData.PinNameText : null)))
					{
						value.m_NamePinData.PinNameText.text = value.m_name;
					}
					Color? status = text switch
					{
						"Tamed" => Color.cyan, 
						"Bosses" => new Color(1f, 0.75f, 0.15f), 
						"Enemies" => new Color(1f, 0.35f, 0.3f), 
						_ => null, 
					};
					MarkerVisual.Apply(value, -1f, text2, status, completed: false);
				}
			}
			Character[] array = radar.Keys.Where((Character c) => !wanted.Contains(c)).ToArray();
			foreach (Character key in array)
			{
				map.RemovePin(radar[key]);
				radar.Remove(key);
				Trace("Radar unregister; active count=" + radar.Count);
			}
			array = liveTypes.Keys.Where((Character c) => !represented.Contains(c)).ToArray();
			foreach (Character key2 in array)
			{
				liveTypes.Remove(key2);
			}
			panel?.RefreshCreatureTypes(creatureTypeRevision);
		}

		private void ClearRadar()
		{
			if (Object.op_Implicit((Object)(object)map))
			{
				foreach (PinData value in radar.Values)
				{
					map.RemovePin(value);
				}
			}
			radar.Clear();
			liveTypes.Clear();
		}

		private void DungeonTrace(string message)
		{
			if (debug.Value || Time.unscaledTime < dungeonDiagnosticsUntil)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Dungeon interaction: " + message));
			}
		}

		private void BeginDungeonDiagnostics()
		{
			dungeonDiagnosticsUntil = Time.unscaledTime + 120f;
			DungeonTrace("diagnostics enabled for 120s; click an existing TL dungeon or select it from the dungeon list");
		}

		private static bool MapLeftClick(Minimap __instance)
		{
			if (!Object.op_Implicit((Object)(object)instance) || !((Behaviour)instance).enabled)
			{
				return true;
			}
			try
			{
				return !instance.SelectAtMapPointer(__instance);
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)instance).Logger.LogWarning((object)("TL map selection failed; vanilla click retained: " + ex));
				return true;
			}
		}

		private static bool MapDoubleClick(Minimap __instance)
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Unknown result type (might be due to invalid IL or missing references)
			if (Object.op_Implicit((Object)(object)instance) && (Object)(object)__instance == (Object)(object)instance.map && Time.unscaledTime - instance.lastOwnedSelectionAt < 0.4f)
			{
				Vector2 val = Vector2.op_Implicit(ZInput.pointerPosition) - instance.lastOwnedPointer;
				if (((Vector2)(ref val)).sqrMagnitude <= 484f)
				{
					return false;
				}
			}
			return MapLeftClick(__instance);
		}

		private static Vector2 ScreenCenter(PinData pin)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			RectTransform uiElement = pin.m_uiElement;
			Canvas componentInParent = ((Component)uiElement).GetComponentInParent<Canvas>();
			Camera obj = ((Object.op_Implicit((Object)(object)componentInParent) && (int)componentInParent.renderMode != 0) ? componentInParent.worldCamera : null);
			Rect rect = uiElement.rect;
			return RectTransformUtility.WorldToScreenPoint(obj, ((Transform)uiElement).TransformPoint(Vector2.op_Implicit(((Rect)(ref rect)).center)));
		}

		private unsafe bool SelectAtMapPointer(Minimap target)
		{
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0043: Unknown result type (might be due to invalid IL or missing references)
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_011c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0122: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ec: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0183: Unknown result type (might be due to invalid IL or missing references)
			//IL_0189: Unknown result type (might be due to invalid IL or missing references)
			//IL_018e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0193: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			if (!uiEnabled.Value || (Object)(object)target != (Object)(object)map || !map.m_largeRoot.activeInHierarchy || markers == null)
			{
				return false;
			}
			Vector2 pointer = Vector2.op_Implicit(ZInput.pointerPosition);
			if (panel != null && panel.Contains(pointer))
			{
				return false;
			}
			Tuple<DisplayGroup, PinData>[] array = markers.Entries.Where((Tuple<DisplayGroup, PinData> e) => Object.op_Implicit((Object)(object)e.Item2.m_uiElement) && ((Component)e.Item2.m_uiElement).gameObject.activeInHierarchy).ToArray();
			Tuple<DisplayGroup, PinData> tuple = PinSelection.Nearest(array, delegate(Tuple<DisplayGroup, PinData> e)
			{
				//IL_0006: Unknown result type (might be due to invalid IL or missing references)
				//IL_000c: Unknown result type (might be due to invalid IL or missing references)
				//IL_0011: Unknown result type (might be due to invalid IL or missing references)
				//IL_0016: Unknown result type (might be due to invalid IL or missing references)
				Vector2 val3 = ScreenCenter(e.Item2) - pointer;
				return ((Vector2)(ref val3)).sqrMagnitude;
			}, 22.0);
			string[] obj = new string[6] { "map click at ", null, null, null, null, null };
			Vector2 val = pointer;
			obj[1] = ((object)(*(Vector2*)(&val))/*cast due to .constrained prefix*/).ToString();
			obj[2] = "; visible owned pins=";
			obj[3] = array.Length.ToString();
			obj[4] = "; candidate=";
			obj[5] = ((tuple == null) ? "none within 22px" : (tuple.Item1.Key + " distance=" + Vector2.Distance(ScreenCenter(tuple.Item2), pointer).ToString("F1") + "px"));
			DungeonTrace(string.Concat(obj));
			if (tuple == null)
			{
				return false;
			}
			object? obj2 = closestSavedPin.Invoke(map, null);
			PinData val2 = (PinData)((obj2 is PinData) ? obj2 : null);
			if (val2 != null && Object.op_Implicit((Object)(object)val2.m_uiElement))
			{
				val = ScreenCenter(val2) - pointer;
				float sqrMagnitude = ((Vector2)(ref val)).sqrMagnitude;
				val = ScreenCenter(tuple.Item2) - pointer;
				if (sqrMagnitude <= ((Vector2)(ref val)).sqrMagnitude)
				{
					DungeonTrace("closer saved pin: retain vanilla interaction");
					return false;
				}
			}
			bool num = SelectGroup(tuple.Item1, focus: false, "native map click");
			if (num)
			{
				lastOwnedSelectionAt = Time.unscaledTime;
				lastOwnedPointer = pointer;
			}
			return num;
		}

		private bool SelectGroup(DisplayGroup group, bool focus, string source)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			if (ledger == null || group == null || !EnsurePanel())
			{
				return false;
			}
			selectedGroup = group;
			if (focus)
			{
				map.ShowPointOnMap(new Vector3(group.X, group.Y, group.Z));
			}
			panel.SetVisible(visible: true);
			panel.ShowDetails();
			DungeonTrace("details panel opened via " + source + "; record=" + group.Records[0].Id + "; completed=" + group.Records[0].Completed);
			return true;
		}

		private void SelectDungeon(string id)
		{
			Record record = ledger?.Data.Records.FirstOrDefault((Record x) => x.Id == id && x.Category == "Dungeons");
			if (record == null)
			{
				throw new InvalidOperationException("Dungeon is no longer in the current world.");
			}
			SelectGroup(new DisplayGroup(new Record[1] { record }), focus: true, "dungeon list");
		}

		private static IEnumerable<CodeInstruction> MapWheelTranspiler(IEnumerable<CodeInstruction> instructions)
		{
			int replaced = 0;
			foreach (CodeInstruction instruction in instructions)
			{
				if (CodeInstructionExtensions.Calls(instruction, AccessTools.Method(typeof(ZInput), "GetMouseScrollWheel", (Type[])null, (Type[])null)))
				{
					instruction.opcode = OpCodes.Call;
					instruction.operand = AccessTools.Method(typeof(Plugin), "MapWheel", (Type[])null, (Type[])null);
					replaced++;
				}
				yield return instruction;
			}
			if (replaced != 1)
			{
				throw new InvalidOperationException("Expected one map wheel read, found " + replaced);
			}
		}

		private static float MapWheel()
		{
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			float mouseScrollWheel = ZInput.GetMouseScrollWheel();
			bool flag = Object.op_Implicit((Object)(object)instance) && instance.panel != null && instance.panel.Contains(Vector2.op_Implicit(ZInput.pointerPosition));
			if (Object.op_Implicit((Object)(object)instance) && mouseScrollWheel != 0f)
			{
				if (flag)
				{
					instance.blockedMapWheels++;
				}
				else
				{
					instance.passedMapWheels++;
				}
				instance.Trace("Map wheel: " + (flag ? "blocked over TrailLedger" : "passed outside TrailLedger") + "; delta=" + mouseScrollWheel);
			}
			return SidebarRules.MapWheel(mouseScrollWheel, flag);
		}

		private void Awake()
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0098: Expected O, but got Unknown
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d6: Expected O, but got Unknown
			//IL_0128: Unknown result type (might be due to invalid IL or missing references)
			//IL_0132: Expected O, but got Unknown
			//IL_0246: Unknown result type (might be due to invalid IL or missing references)
			//IL_0250: Expected O, but got Unknown
			discover = ((BaseUnityPlugin)this).Config.Bind<bool>("Discovery", "Enabled", true, "Record supported loaded resources independently of visibility filters.");
			radius = ((BaseUnityPlugin)this).Config.Bind<float>("Discovery", "Radius", 20f, new ConfigDescription("3D encounter radius; local exploration is also required.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(5f, 50f), Array.Empty<object>()));
			eligibilityInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Discovery", "EligibilitySeconds", 0.15f, new ConfigDescription("Recheck registered nearby objects while moving or map exploration catches up. Replaces 0.1 ScanSeconds.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 1f), Array.Empty<object>()));
			fallbackInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Discovery", "FallbackSeconds", 30f, new ConfigDescription("Safety reconciliation for missed lifecycle events; not the primary discovery path.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 120f), Array.Empty<object>()));
			uiEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Map", "UIEnabled", true, "Show the full-map filter panel.");
			pinLimit = ((BaseUnityPlugin)this).Config.Bind<int>("Map", "MaxRenderedPins", 1500, new ConfigDescription("Nearest eligible group/individual markers displayed; all records remain stored.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(100, 10000), Array.Empty<object>()));
			pinLimit.SettingChanged += RenderSettingsChanged;
			debug = ((BaseUnityPlugin)this).Config.Bind<bool>("Logging", "Verbose", false, "Print aggregate callback, fallback, marker and icon metrics every 30 seconds.");
			checkCreatureRows = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "CheckCreatureRowsOnce", false, "On next panel creation, validate real Unity row hierarchies and injected failure cleanup. No clicks or filter changes. Automatically resets; live interaction checks are still required.");
			root = Path.Combine(Paths.ConfigPath, "TrailLedger");
			filterPath = Path.Combine(root, "filters.json");
			try
			{
				using (StartupTimings.Measure("filterLoadMigration"))
				{
					filters = Storage.LoadFilters(filterPath, delegate(string s)
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)s);
					});
				}
				InitializeCatalogue();
				grouping = new GroupingConfig(((BaseUnityPlugin)this).Config, delegate
				{
					markerDirty = true;
				});
				exploration = new ExplorationGate();
				icons = new IconProvider(Trace);
				InitializeTracking();
				instance = this;
				harmony = new Harmony("au.astra.trailledger");
				InstallHooks();
				CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new TrailCommand(this));
				((BaseUnityPlugin)this).Logger.LogInfo((object)("TrailLedger 0.6.1 loaded; Valheim " + Version.GetVersionString(false) + "; Jotunn 2.30.0; schema 2; filters loaded; display grouping enabled=" + grouping.Enabled));
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("TrailLedger initialization failed safely: " + ex));
				Harmony obj = harmony;
				if (obj != null)
				{
					obj.UnpatchSelf();
				}
				instance = null;
				((Behaviour)this).enabled = false;
			}
		}

		private void InstallHooks()
		{
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Expected O, but got Unknown
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Expected O, but got Unknown
			//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Expected O, but got Unknown
			//IL_00ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_0102: Unknown result type (might be due to invalid IL or missing references)
			//IL_010f: Expected O, but got Unknown
			//IL_010f: Expected O, but got Unknown
			//IL_022d: Unknown result type (might be due to invalid IL or missing references)
			//IL_023a: Expected O, but got Unknown
			harmony.Patch((MethodBase)AccessTools.Method(typeof(Minimap), "UpdateMap", (Type[])null, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "MapWheelTranspiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null);
			harmony.Patch((MethodBase)AccessTools.Method(typeof(Minimap), "OnMapLeftClick", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "MapLeftClick", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			harmony.Patch((MethodBase)AccessTools.Method(typeof(Minimap), "OnMapDblClick", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "MapDoubleClick", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Map interaction hooks: native left click and double click -> owned pin selection.");
			harmony.Patch((MethodBase)AccessTools.Method(typeof(Pickable), "RPC_SetPicked", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforePicked", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterPicked", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			Tuple<Type, string>[] array = new Tuple<Type, string>[8]
			{
				Tuple.Create(typeof(Pickable), "Awake"),
				Tuple.Create(typeof(MineRock), "Start"),
				Tuple.Create(typeof(MineRock5), "Awake"),
				Tuple.Create(typeof(Destructible), "Start"),
				Tuple.Create(typeof(Location), "Awake"),
				Tuple.Create(typeof(Leviathan), "Awake"),
				Tuple.Create(typeof(SpawnArea), "Awake"),
				Tuple.Create(typeof(CreatureSpawner), "Awake")
			};
			foreach (Tuple<Type, string> tuple in array)
			{
				try
				{
					MethodInfo method = tuple.Item1.GetMethod(tuple.Item2, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
					if (method == null)
					{
						throw new MissingMethodException(tuple.Item1.Name, tuple.Item2);
					}
					harmony.Patch((MethodBase)method, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "Loaded", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Discovery hook: " + tuple.Item1.Name + "." + tuple.Item2));
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Lifecycle hook unavailable; fallback remains active: " + tuple.Item1.Name + ": " + ex.Message));
				}
			}
		}

		private static void Loaded(Component __instance)
		{
			if (!Object.op_Implicit((Object)(object)instance) || !((Behaviour)instance).enabled || !Object.op_Implicit((Object)(object)__instance))
			{
				return;
			}
			try
			{
				instance.callbacks++;
				if (!instance.Register(__instance.transform))
				{
					instance.ignored++;
					return;
				}
				instance.recognized++;
				instance.nextCheck = 0f;
			}
			catch (Exception ex)
			{
				if (instance.debug.Value)
				{
					((BaseUnityPlugin)instance).Logger.LogWarning((object)("Object registration skipped: " + ex.Message));
				}
			}
		}

		private bool Register(Transform transform, CandidateIndex<Candidate> target = null)
		{
			target = target ?? candidates;
			Transform val = transform;
			while ((Object)(object)val != (Object)null)
			{
				Definition definition = DiscoveryRules.Resolve(((Object)val).name);
				if (definition != null)
				{
					int instanceID = ((Object)val).GetInstanceID();
					if (!target.TryGet(instanceID, out var value) || (Object)(object)value.Transform != (Object)(object)val)
					{
						target.Register(instanceID, new Candidate
						{
							Transform = val,
							Definition = definition,
							Prefab = DiscoveryRules.Normalize(((Object)val).name),
							Pickable = ((Component)val).GetComponent<Pickable>()
						});
					}
					return true;
				}
				val = val.parent;
			}
			return false;
		}

		private void Update()
		{
			//IL_02fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0300: Unknown result type (might be due to invalid IL or missing references)
			//IL_0350: Unknown result type (might be due to invalid IL or missing references)
			//IL_0355: Unknown result type (might be due to invalid IL or missing references)
			//IL_0357: Unknown result type (might be due to invalid IL or missing references)
			//IL_0359: Unknown result type (might be due to invalid IL or missing references)
			//IL_035b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0360: Unknown result type (might be due to invalid IL or missing references)
			//IL_0396: Unknown result type (might be due to invalid IL or missing references)
			//IL_03cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_045e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0465: Unknown result type (might be due to invalid IL or missing references)
			//IL_046c: Unknown result type (might be due to invalid IL or missing references)
			TickActiveTime();
			if (Time.unscaledTime < nextCheck)
			{
				return;
			}
			nextCheck = Time.unscaledTime + eligibilityInterval.Value;
			try
			{
				Player localPlayer = Player.m_localPlayer;
				long num = ((Object.op_Implicit((Object)(object)localPlayer) && Object.op_Implicit((Object)(object)ZNet.instance)) ? ZNet.instance.GetWorldUID() : 0);
				if (num != world)
				{
					ClearPins();
					ClearRadar();
					catalogueScene = null;
					if (catalogueJob != null)
					{
						((MonoBehaviour)this).StopCoroutine(catalogueJob);
						catalogueJob = null;
					}
					preview?.Stop();
					panel?.Dispose();
					panel = null;
					uiFailed = false;
					if (!Save())
					{
						return;
					}
					ledger = null;
					world = 0L;
					seen.Clear();
					candidates.Clear();
					discoveryPaused = false;
					display.Reload(Array.Empty<Record>());
					if (num != 0L)
					{
						Database database;
						using (StartupTimings.Measure("worldData"))
						{
							database = Storage.LoadWorld(root, num, delegate(string s)
							{
								((BaseUnityPlugin)this).Logger.LogWarning((object)s);
							});
						}
						ledger = new Ledger(database);
						world = num;
						nextFallback = 0f;
						markerDirty = true;
						display.Reload(database.Records);
						((BaseUnityPlugin)this).Logger.LogInfo((object)("World UID " + world + ": " + database.Records.Count + " discoveries loaded; schema " + database.Schema));
					}
				}
				if (world == 0L || ledger == null)
				{
					return;
				}
				if ((Object)(object)map != (Object)(object)Minimap.instance)
				{
					ClearPins();
					ClearRadar();
					map = Minimap.instance;
					Minimap targetMap = map;
					markers = (Object.op_Implicit((Object)(object)map) ? new OwnedMarkers<PinData>((DisplayGroup g) => CreateMarker(targetMap, g), delegate(PinData p)
					{
						if (Object.op_Implicit((Object)(object)targetMap))
						{
							targetMap.RemovePin(p);
						}
					}) : null);
					panel?.Dispose();
					panel = null;
					uiFailed = false;
					markerDirty = true;
				}
				RefreshCatalogue();
				UpdateTracking(localPlayer);
				if (!discoveryPaused && Object.op_Implicit((Object)(object)map))
				{
					if (Time.unscaledTime >= nextFallback)
					{
						using (StartupTimings.Measure("discoveryFallback"))
						{
							Refresh();
						}
						nextFallback = Time.unscaledTime + fallbackInterval.Value;
					}
					Vector3 position = ((Component)localPlayer).transform.position;
					foreach (Candidate value in candidates.Values)
					{
						if (!Object.op_Implicit((Object)(object)value.Transform) || !((Component)value.Transform).gameObject.activeInHierarchy)
						{
							continue;
						}
						Vector3 position2 = value.Transform.position;
						Vector3 val = position2 - position;
						float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude;
						if (sqrMagnitude > radius.Value * radius.Value || !DiscoveryRules.Eligible(active: true, exploration.Contains(map, position2), sqrMagnitude, radius.Value))
						{
							continue;
						}
						if (value.Record == null)
						{
							value.Record = FindRecord(value.Definition.Id, position2);
						}
						if (value.Record != null)
						{
							Observe(value, harvest: false);
						}
						else
						{
							if (!discover.Value)
							{
								continue;
							}
							if (!TrackingRules.CanDiscover(value.Definition.Id, value.Definition.Category, ((Character)localPlayer).InInterior(), filters.TrackInteriors, HasWishbone(localPlayer)))
							{
								suppressed++;
								continue;
							}
							value.Recorded = true;
							if (ledger.Discover(value.Definition, position2.x, position2.y, position2.z, value.Prefab))
							{
								newRecords++;
								dirty = true;
								markerDirty = true;
								display.Add(ledger.Data.Records[ledger.Data.Records.Count - 1]);
								value.Record = ledger.Data.Records[ledger.Data.Records.Count - 1];
								Observe(value, harvest: false);
								if (seen.Add(value.Definition.Id))
								{
									((BaseUnityPlugin)this).Logger.LogInfo((object)("Encountered " + value.Definition.Name));
								}
							}
						}
					}
				}
				if (markerDirty || Time.unscaledTime >= nextMarkers)
				{
					Reconcile();
					nextMarkers = Time.unscaledTime + 2f;
					markerDirty = false;
				}
				if (Time.unscaledTime >= nextSave)
				{
					Save();
					nextSave = Time.unscaledTime + 15f;
				}
				if (debug.Value && Time.unscaledTime >= nextMetrics)
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)Metrics());
					nextMetrics = Time.unscaledTime + 30f;
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("TrailLedger update failed safely: " + ex));
				nextCheck = Time.unscaledTime + 5f;
			}
		}

		private void Refresh()
		{
			CandidateIndex<Candidate> candidateIndex = new CandidateIndex<Candidate>();
			ZNetView[] array = Object.FindObjectsByType<ZNetView>((FindObjectsSortMode)0);
			foreach (ZNetView val in array)
			{
				Register(((Component)val).transform, candidateIndex);
			}
			Location[] array2 = Object.FindObjectsByType<Location>((FindObjectsSortMode)0);
			foreach (Location val2 in array2)
			{
				Register(((Component)val2).transform, candidateIndex);
			}
			candidates.Reconcile(candidateIndex.Entries);
			fallbacks++;
		}

		private void LateUpdate()
		{
			RefreshPinLabels();
			if (!Object.op_Implicit((Object)(object)map) || world == 0L || !uiEnabled.Value)
			{
				panel?.SetVisible(visible: false);
				return;
			}
			if (map.m_largeRoot.activeInHierarchy)
			{
				EnsurePanel();
			}
			panel?.SetVisible(map.m_largeRoot.activeInHierarchy);
			if (!map.m_largeRoot.activeInHierarchy)
			{
				preview?.Stop();
			}
		}

		private bool EnsurePanel()
		{
			if (!Object.op_Implicit((Object)(object)map) || world == 0L || !uiEnabled.Value)
			{
				return false;
			}
			if (panel == null && !uiFailed && map.m_largeRoot.activeInHierarchy)
			{
				try
				{
					using (StartupTimings.Measure("uiAndJotunn"))
					{
						panel = new MapPanel(map, filters, () => ledger.Data.Records, icons, Changed, () => grouping.Enabled, SetGrouping, () => world, ExecuteMaintenance, () => discoveryPaused, ResumeDiscovery, () => radius.Value, delegate(float v)
						{
							SetRadius(creature: false, v);
						}, () => radarRadius.Value, delegate(float v)
						{
							SetRadius(creature: true, v);
						}, delegate(bool v)
						{
							Preview(v);
						}, () => selectedGroup, ToggleDungeon, delegate
						{
							preview.Stop();
						}, SelectDungeon, BeginDungeonDiagnostics, CreatureBiomeUnlocked, RebuildCreatureCatalogue, () => catalogueStatus, CurrentCreatureBiome);
					}
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Jotunn map filter UI initialized. TrailLedger startup timings (cumulative): " + StartupTimings.Summary()));
					if (checkCreatureRows.Value)
					{
						checkCreatureRows.Value = false;
						try
						{
							panel.CheckCreatureRows();
						}
						catch (Exception ex)
						{
							((BaseUnityPlugin)this).Logger.LogError((object)("Creature row validation failed: " + ex));
						}
					}
				}
				catch (Exception ex2)
				{
					uiFailed = true;
					panel?.Dispose();
					panel = null;
					((BaseUnityPlugin)this).Logger.LogError((object)("Map UI unavailable; vanilla map remains usable: " + ex2));
				}
			}
			return panel != null;
		}

		private void Reconcile()
		{
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_0125: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)map) || markers == null || ledger == null || !Object.op_Implicit((Object)(object)Player.m_localPlayer))
			{
				return;
			}
			Vector3 position = ((Component)Player.m_localPlayer).transform.position;
			if (!markerDirty && hasRenderPosition)
			{
				Vector3 val = position - renderPosition;
				if (((Vector3)(ref val)).sqrMagnitude < 100f)
				{
					return;
				}
			}
			List<DisplayGroup> list = (from g in display.Visible(filters, (string type) => (!Registry.All.Any((Definition d) => d.Id == type && d.Category == "Dungeons")) ? grouping.Resolve(type) : new GroupRule(enabled: false, 0f))
				where Registry.All.Any((Definition d) => d.Id == g.Type) && g.Records.All(filters.DungeonVisible)
				select g).ToList();
			IEnumerable<DisplayGroup> desired = list;
			if (list.Count > pinLimit.Value)
			{
				desired = list.OrderBy((DisplayGroup g) => (g.X - position.x) * (g.X - position.x) + (g.Z - position.z) * (g.Z - position.z)).ThenBy<DisplayGroup, string>((DisplayGroup g) => g.Key, StringComparer.Ordinal).Take(pinLimit.Value);
			}
			markers.Apply(desired);
			renderPosition = position;
			hasRenderPosition = true;
		}

		private PinData CreateMarker(Minimap target, DisplayGroup group)
		{
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			Definition definition = Registry.All.First((Definition d) => d.Id == group.Type);
			string text = "TL · " + definition.Name + ((group.Count > 1) ? (" ×" + group.Count) : "");
			PinData val = target.AddPin(new Vector3(group.X, group.Y, group.Z), (PinType)8, text, false, false, 0L, default(PlatformUserID));
			try
			{
				val.m_icon = icons.Get(definition, target.m_selectedIcon3.sprite);
				return val;
			}
			catch
			{
				target.RemovePin(val);
				throw;
			}
		}

		private bool Save()
		{
			try
			{
				if (dirty && ledger != null)
				{
					Storage.Save(Storage.WorldPath(root, ledger.Data.World), ledger.Data);
					dirty = false;
					((BaseUnityPlugin)this).Logger.LogInfo((object)("Discovery saved: " + ledger.Data.Records.Count + " entries; world " + ledger.Data.World));
				}
				if (filtersDirty)
				{
					Storage.Save(filterPath, filters);
					filtersDirty = false;
				}
				SaveCatalogue();
				return true;
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Persistence save failed; pending data retained: " + ex));
				return false;
			}
		}

		private void Changed()
		{
			filtersDirty = true;
			markerDirty = true;
			Save();
			Reconcile();
			RefreshPinLabels();
		}

		private void RefreshPinLabels()
		{
			if (!Object.op_Implicit((Object)(object)map) || filters == null)
			{
				return;
			}
			bool show = PinLabelRules.Show(filters.PinLabels, map.m_largeRoot.activeInHierarchy);
			if (markers != null)
			{
				foreach (Tuple<DisplayGroup, PinData> entry in markers.Entries)
				{
					MarkerVisual.ApplyLabels(entry.Item2, show, entry.Item1.Count);
				}
			}
			foreach (PinData value in radar.Values)
			{
				MarkerVisual.ApplyLabels(value, show, 1);
			}
		}

		private void SetGrouping(bool value)
		{
			grouping.SetEnabled(value);
			markerDirty = true;
			Reconcile();
		}

		private void RenderSettingsChanged(object sender, EventArgs args)
		{
			markerDirty = true;
		}

		private void ClearPins()
		{
			markers?.Clear();
			selectedGroup = null;
			hasRenderPosition = false;
		}

		private string Metrics()
		{
			return $"Lifecycle callbacks={callbacks}, irrelevant={ignored}, recognized={recognized}, new records={newRecords}, fallback passes={fallbacks} ({fallbackInterval.Value}s), candidates={candidates.Count}, markers={markers?.Count ?? 0}, icon cache={icons.Count}, creature icon types={icons.CreatureCount}, trophy icons={icons.TrophyCount}, group recalculations={display.Recalculations}, discovery paused={discoveryPaused}, dirty={dirty}, interior/Wishbone suppressed checks={suppressed}, radar active={radar.Count}, catalogue={filters.CreatureTypes.Count}, map wheel blocked={blockedMapWheels}, passed={passedMapWheels}, UI={panel?.Diagnostics()}, timings={StartupTimings.Summary()}, active timer seconds this session={activeTotal:F1}";
		}

		private string ExecuteMaintenance(MaintenanceAction action, long expectedWorld)
		{
			if (!Object.op_Implicit((Object)(object)Player.m_localPlayer) || !Object.op_Implicit((Object)(object)map) || (Object)(object)map != (Object)(object)Minimap.instance || ledger == null)
			{
				throw new InvalidOperationException("Current map unavailable.");
			}
			WorldMaintenance.RequireWorld(expectedWorld, world, Object.op_Implicit((Object)(object)ZNet.instance) ? ZNet.instance.GetWorldUID() : 0);
			switch (action)
			{
			case MaintenanceAction.Rebuild:
			{
				Database database2 = WorldMaintenance.Reload(root, ledger.Data, dirty);
				ledger = new Ledger(database2);
				dirty = false;
				ClearPins();
				candidates.Clear();
				nextFallback = 0f;
				display.Reload(database2.Records);
				markerDirty = true;
				Reconcile();
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Rebuilt owned TL markers from " + database2.Records.Count + " stored discoveries; world " + world));
				return "Rebuilt TL markers. " + database2.Records.Count + " discoveries remain stored.";
			}
			case MaintenanceAction.ClearWorld:
			{
				string backup;
				Database database = WorldMaintenance.Clear(root, ledger.Data, dirty, out backup);
				ledger = new Ledger(database);
				dirty = false;
				discoveryPaused = true;
				ClearPins();
				display.Reload(database.Records);
				candidates.Clear();
				nextFallback = 0f;
				markerDirty = true;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Explicitly cleared TrailLedger world " + world + "; backup: " + backup));
				return "This world's TL discoveries were cleared. Discovery is paused until Resume or the next world/session. Backup: " + backup;
			}
			default:
				throw new InvalidOperationException("No maintenance action selected.");
			}
		}

		private string ResumeDiscovery()
		{
			discoveryPaused = false;
			nextFallback = 0f;
			if (!discover.Value)
			{
				return "Pause cleared. Discovery.Enabled is still disabled in configuration.";
			}
			return "Automatic discovery resumed.";
		}

		private void OnApplicationQuit()
		{
			Save();
		}

		private void OnDestroy()
		{
			Save();
			Harmony obj = harmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			instance = null;
			ClearPins();
			ClearRadar();
			preview?.Dispose();
			panel?.Dispose();
			icons?.Dispose();
			grouping?.Dispose();
			MarkerVisual.Release();
			if (pinLimit != null)
			{
				pinLimit.SettingChanged -= RenderSettingsChanged;
			}
		}

		private void InitializeTracking()
		{
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Expected O, but got Unknown
			radarRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Creatures", "Radius", 80f, new ConfigDescription("Loaded, explored nearby creatures only.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 150f), Array.Empty<object>()));
			preview = new RadiusPreview(delegate
			{
				Trace("Radius preview stopped");
			});
			radius.SettingChanged += delegate
			{
				nextCheck = 0f;
				Trace("Discovery radius=" + radius.Value);
			};
		}

		private void Trace(string message)
		{
			if (debug.Value)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)message);
			}
		}

		private static bool HasWishbone(Player player)
		{
			return ((Character)player).GetSEMan().GetStatusEffects().Any((StatusEffect s) => s is SE_Finder);
		}

		private void SetRadius(bool creature, float value)
		{
			(creature ? radarRadius : radius).Value = TrackingRules.ClampRadius(value, creature ? 10 : 5, creature ? 150 : 50);
			((BaseUnityPlugin)this).Config.Save();
			Preview(creature);
		}

		private void Preview(bool creature)
		{
			preview.Show(creature ? radarRadius.Value : radius.Value);
			Trace("Radius preview start: " + (creature ? "creatures" : "discovery"));
		}

		private void TickActiveTime()
		{
			if (ledger != null && world != 0L && Object.op_Implicit((Object)(object)Player.m_localPlayer) && Object.op_Implicit((Object)(object)ZNet.instance) && ZNet.instance.GetWorldUID() == world && !Game.IsPaused())
			{
				activePending += Math.Min(Time.deltaTime, 0.25f);
				if (activePending >= 1.0)
				{
					dirty |= Renewal.Advance(ledger.Data.Records, activePending, active: true);
					activeTotal += activePending;
					activePending = 0.0;
				}
			}
			else
			{
				activePending = 0.0;
			}
		}

		private Record FindRecord(string type, Vector3 point)
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			return ledger?.Find(type, point.x, point.y, point.z);
		}

		private void Observe(Candidate c, bool harvest)
		{
			if (c.Record != null && Object.op_Implicit((Object)(object)c.Pickable) && !(c.Definition.Behavior != "Renewable"))
			{
				Record record = c.Record;
				Availability availability = record.Availability;
				bool timerKnown = record.TimerKnown;
				double activeElapsed = record.ActiveElapsed;
				Renewal.Observe(record, c.Pickable.GetPicked(), (double)c.Pickable.m_respawnTimeMinutes * 60.0, harvest);
				if (availability != record.Availability || timerKnown != record.TimerKnown || activeElapsed != record.ActiveElapsed)
				{
					dirty = true;
					Trace("Availability " + record.Type + ": " + record.Availability.ToString() + "; harvest=" + harvest + "; duration=" + record.RespawnSeconds);
				}
			}
		}

		private static void BeforePicked(Pickable __instance, out bool __state)
		{
			__state = __instance.GetPicked();
		}

		private static void AfterPicked(Pickable __instance, bool __state)
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_008e: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)instance) || !Object.op_Implicit((Object)(object)Player.m_localPlayer) || instance.ledger == null)
			{
				return;
			}
			try
			{
				Vector3 position = ((Component)__instance).transform.position;
				Vector3 val = position - ((Component)Player.m_localPlayer).transform.position;
				if (((Vector3)(ref val)).sqrMagnitude > instance.radius.Value * instance.radius.Value)
				{
					return;
				}
				Definition definition = DiscoveryRules.Resolve(((Object)__instance).name);
				if (definition != null)
				{
					Record record = instance.FindRecord(definition.Id, position);
					if (record != null)
					{
						instance.Observe(new Candidate
						{
							Definition = definition,
							Record = record,
							Pickable = __instance
						}, !__state && __instance.GetPicked());
					}
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)instance).Logger.LogWarning((object)("Harvest observation skipped: " + ex.Message));
			}
		}

		private void ToggleDungeon()
		{
			if (selectedGroup != null)
			{
				Record record = ledger?.Data.Records.FirstOrDefault((Record x) => x.Id == selectedGroup.Records[0].Id);
				if (record != null && !(record.Category != "Dungeons"))
				{
					DungeonTrace("completion toggle requested; record=" + record.Id);
					record.Completed = !record.Completed;
					dirty = true;
					markerDirty = true;
					DungeonTrace("database record updated; completed=" + record.Completed);
					bool flag = Save();
					Reconcile();
					RefreshPersistentVisuals();
					nextVisual = 0f;
					DungeonTrace("marker visuals refreshed; saved=" + flag + "; show completed=" + filters.ShowCompleted + "; show uncompleted=" + filters.ShowUncompleted);
				}
			}
		}

		private void UpdateTracking(Player player)
		{
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			bool flag = ((Character)player).InInterior();
			bool flag2 = HasWishbone(player);
			if (flag != lastInterior)
			{
				Trace("Interior state=" + flag + "; resource tracking=" + filters.TrackInteriors);
				lastInterior = flag;
			}
			if (flag2 != lastWishbone)
			{
				Trace("Wishbone Silver eligibility=" + flag2);
				lastWishbone = flag2;
			}
			preview.Update(((Component)player).transform.position);
			if (Time.unscaledTime >= nextRadar)
			{
				UpdateRadar(player);
				nextRadar = Time.unscaledTime + 0.25f;
			}
			if (Time.unscaledTime >= nextVisual)
			{
				nextVisual = Time.unscaledTime + 1f;
				RefreshPersistentVisuals();
				panel?.RefreshDetails();
			}
		}

		private void RefreshPersistentVisuals()
		{
			if (markers == null)
			{
				return;
			}
			foreach (Tuple<DisplayGroup, PinData> entry in markers.Entries)
			{
				DisplayGroup group = entry.Item1;
				PinData item = entry.Item2;
				bool flag = group.Records.Any((Record r) => r.Behavior == "Renewable");
				Record record = group.Records.Where((Record r) => r.TimerKnown && r.Availability == Availability.Regrowing).OrderBy(Renewal.Remaining).FirstOrDefault();
				item.m_checked = false;
				item.m_name = "TL · " + Registry.All.First((Definition d) => d.Id == group.Type).Name + ((group.Count > 1) ? (" ×" + group.Count) : "") + ((flag && group.Count > 1) ? (" · " + group.Records.Count((Record r) => r.Availability == Availability.Available) + "/" + group.Count + " available") : "") + (group.Records[0].Completed ? " ✓ Completed" : "");
				PinNameData namePinData = item.m_NamePinData;
				if (Object.op_Implicit((Object)(object)((namePinData != null) ? namePinData.PinNameText : null)))
				{
					item.m_NamePinData.PinNameText.text = item.m_name;
				}
				MarkerVisual.Apply(item, (record == null) ? (-1f) : ((float)(Renewal.Remaining(record) / record.RespawnSeconds)), "", null, group.Records[0].Completed);
			}
		}
	}
	public sealed class CreatureType
	{
		public string Id;

		public string Name;

		public string Family;

		public string[] Biomes = Array.Empty<string>();

		public bool Boss;

		public int MaxSpawnLevel = 1;

		public string CanonicalId;

		public string[] Aliases = Array.Empty<string>();

		public bool Special;

		public bool Neutral;

		public bool Tameable;
	}
	public static class CreatureRules
	{
		public static string[] MergeBiomes(IEnumerable<string> existing, IEnumerable<string> incoming)
		{
			string[] array = (from s in existing.Concat(incoming)
				where !string.IsNullOrWhiteSpace(s)
				select s).Distinct().OrderBy<string, string>((string s) => s, StringComparer.Ordinal).ToArray();
			if (array.Length == 0)
			{
				return new string[1] { "Other / Modded" };
			}
			if (array.Length > 1)
			{
				array = array.Where((string s) => s != "Other / Modded").ToArray();
			}
			if (array.Length > 1)
			{
				array = array.Where((string s) => s != "Dungeons / Other").ToArray();
			}
			return array;
		}

		public static string Category(bool tamed, bool boss, string family)
		{
			if (!tamed)
			{
				return "Enemies";
			}
			return "Tamed";
		}

		public static bool Visible(Filters filters, string type, string category, int level)
		{
			if (filters.ShowCreatures && filters.CreatureTypeVisible(type) && CategoryVisible(filters, category))
			{
				return TrackingRules.Stars(level) >= filters.CreatureStars(type);
			}
			return false;
		}

		public static bool CategoryVisible(Filters f, string category)
		{
			if (!(category == "Tamed"))
			{
				return f.Enemies;
			}
			return f.Tamed;
		}
	}
	public sealed class DropIcon<T> where T : class
	{
		public readonly bool Trophy;

		public readonly T Sprite;

		public DropIcon(bool trophy, T sprite)
		{
			Trophy = trophy;
			Sprite = sprite;
		}
	}
	public sealed class CreatureIconCache<T> where T : class
	{
		private readonly Dictionary<string, T> trophies = new Dictionary<string, T>(StringComparer.Ordinal);

		public int Count => trophies.Count;

		public int ResolvedCount => trophies.Values.Count((T v) => v != null);

		public T Get(string type, Func<IEnumerable<DropIcon<T>>> drops, Func<T> fallback, Action<Exception> failure = null)
		{
			if (!trophies.TryGetValue(type, out var value))
			{
				try
				{
					value = (from d in drops()
						where d?.Trophy ?? false
						select d.Sprite).FirstOrDefault((T s) => s != null);
				}
				catch (Exception obj)
				{
					failure?.Invoke(obj);
					value = null;
				}
				trophies.Add(type, value);
			}
			return value ?? fallback();
		}

		public void Clear()
		{
			trophies.Clear();
		}
	}
	public static class PinSelection
	{
		public static T Nearest<T>(IEnumerable<T> candidates, Func<T, double> pixelDistanceSquared, double tolerance) where T : class
		{
			T result = null;
			double num = tolerance * tolerance;
			foreach (T candidate in candidates)
			{
				double num2 = pixelDistanceSquared(candidate);
				if (num2 >= 0.0 && num2 <= num)
				{
					result = candidate;
					num = num2;
				}
			}
			return result;
		}
	}
	internal sealed class ExplorationGate
	{
		private delegate void PixelConverter(Minimap map, Vector3 point, out int x, out int y);

		private readonly PixelConverter toPixel;

		private readonly FieldRef<Minimap, BitArray> personal;

		public ExplorationGate()
		{
			MethodInfo method = typeof(Minimap).GetMethod("WorldToPixel", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[3]
			{
				typeof(Vector3),
				typeof(int).MakeByRefType(),
				typeof(int).MakeByRefType()
			}, null);
			if (method == null)
			{
				throw new MissingMethodException("Minimap.WorldToPixel");
			}
			toPixel = (PixelConverter)Delegate.CreateDelegate(typeof(PixelConverter), method);
			personal = AccessTools.FieldRefAccess<Minimap, BitArray>("m_explored");
		}

		public bool Contains(Minimap map, Vector3 point)
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			if (!Object.op_Implicit((Object)(object)map))
			{
				return false;
			}
			toPixel(map, point, out var x, out var y);
			BitArray bitArray = personal.Invoke(map);
			if (bitArray == null || x < 0 || y < 0 || x >= map.m_textureSize || y >= map.m_textureSize)
			{
				return false;
			}
			long num = (long)y * (long)map.m_textureSize + x;
			if (num < bitArray.Count)
			{
				return bitArray[(int)num];
			}
			return false;
		}
	}
	public enum GroupingOverride
	{
		Inherit,
		Enabled,
		Disabled
	}
	public static class GroupingPolicy
	{
		public static GroupRule Default(string category)
		{
			return category switch
			{
				"Foraging" => new GroupRule(enabled: true, 12f), 
				"Seeds" => new GroupRule(enabled: true, 8f), 
				"Ores" => new GroupRule(enabled: true, 5f), 
				_ => new GroupRule(enabled: false, 10f), 
			};
		}

		public static GroupRule Effective(bool global, GroupRule category, GroupingOverride mode, float radius)
		{
			return new GroupRule((byte)((global ? 1u : 0u) & (uint)(mode switch
			{
				GroupingOverride.Inherit => category.Enabled ? 1 : 0, 
				GroupingOverride.Enabled => 1, 
				_ => 0, 
			})) != 0, (radius > 0f) ? radius : category.Radius);
		}
	}
	public readonly struct GroupRule : IEquatable<GroupRule>
	{
		public readonly bool Enabled;

		public readonly float Radius;

		public GroupRule(bool enabled, float radius)
		{
			if (float.IsNaN(radius) || float.IsInfinity(radius) || radius < 0f)
			{
				throw new ArgumentOutOfRangeException("radius");
			}
			Enabled = enabled && radius > 0f;
			Radius = radius;
		}

		public bool Equals(GroupRule other)
		{
			if (Enabled == other.Enabled)
			{
				return Radius == other.Radius;
			}
			return false;
		}
	}
	public sealed class DisplayGroup
	{
		public string Key { get; }

		public string Type { get; }

		public float X { get; }

		public float Y { get; }

		public float Z { get; }

		public int Count => Members.Count;

		public IReadOnlyList<string> Members { get; }

		public IReadOnlyList<Record> Records { get; }

		internal DisplayGroup(IList<Record> records)
		{
			Type = records[0].Type;
			Records = Array.AsReadOnly(records.ToArray());
			string[] array = records.Select((Record r) => r.Id).OrderBy<string, string>((string id) => id, StringComparer.Ordinal).ToArray();
			Key = Type + ":" + array[0];
			Members = Array.AsReadOnly(array);
			X = (float)(((IEnumerable<Record>)records).Sum((Func<Record, double>)((Record r) => r.X)) / (double)records.Count);
			Y = (float)(((IEnumerable<Record>)records).Sum((Func<Record, double>)((Record r) => r.Y)) / (double)records.Count);
			Z = (float)(((IEnumerable<Record>)records).Sum((Func<Record, double>)((Record r) => r.Z)) / (double)records.Count);
		}

		public bool SameAppearance(DisplayGroup other)
		{
			if (other != null && Type == other.Type && Count == other.Count && X == other.X && Y == other.Y)
			{
				return Z == other.Z;
			}
			return false;
		}
	}
	public static class SpatialGrouping
	{
		public static List<DisplayGroup> Build(IEnumerable<Record> source, GroupRule rule)
		{
			List<DisplayGroup> list = new List<DisplayGroup>();
			foreach (IGrouping<string, Record> item in (from r in source
				group r by r.Type).OrderBy<IGrouping<string, Record>, string>((IGrouping<string, Record> g) => g.Key, StringComparer.Ordinal))
			{
				Record[] array = (from r in item
					orderby r.X, r.Y, r.Z
					select r).ThenBy<Record, string>((Record r) => r.Id, StringComparer.Ordinal).ToArray();
				if (!rule.Enabled)
				{
					list.AddRange(array.Select((Record r) => new DisplayGroup(new Record[1] { r })));
					continue;
				}
				int[] parent = Enumerable.Range(0, array.Length).ToArray();
				Dictionary<(long, long, long), List<int>> dictionary = new Dictionary<(long, long, long), List<int>>();
				double num = (double)rule.Radius * (double)rule.Radius;
				for (int num2 = 0; num2 < array.Length; num2++)
				{
					Record record = array[num2];
					(long, long, long) key = (Cell(record.X), Cell(record.Y), Cell(record.Z));
					for (int num3 = -1; num3 <= 1; num3++)
					{
						for (int num4 = -1; num4 <= 1; num4++)
						{
							for (int num5 = -1; num5 <= 1; num5++)
							{
								if (!dictionary.TryGetValue((key.Item1 + num3, key.Item2 + num4, key.Item3 + num5), out var value))
								{
									continue;
								}
								foreach (int item2 in value)
								{
									if (Find(num2) != Find(item2))
									{
										double num6 = (double)record.X - (double)array[item2].X;
										double num7 = (double)record.Y - (double)array[item2].Y;
										double num8 = (double)record.Z - (double)array[item2].Z;
										if (num6 * num6 + num7 * num7 + num8 * num8 <= num)
										{
											Union(num2, item2);
										}
									}
								}
							}
						}
					}
					if (!dictionary.TryGetValue(key, out var value2))
					{
						value2 = (dictionary[key] = new List<int>());
					}
					value2.Add(num2);
				}
				Dictionary<int, List<Record>> dictionary2 = new Dictionary<int, List<Record>>();
				for (int num9 = 0; num9 < array.Length; num9++)
				{
					int key2 = Find(num9);
					if (!dictionary2.TryGetValue(key2, out var value3))
					{
						value3 = (dictionary2[key2] = new List<Record>());
					}
					value3.Add(array[num9]);
				}
				list.AddRange(dictionary2.Values.Select((List<Record> component) => new DisplayGroup(component)));
				int Find(int i)
				{
					while (parent[i] != i)
					{
						parent[i] = parent[parent[i]];
						i = parent[i];
					}
					return i;
				}
				void Union(int a, int b)
				{
					a = Find(a);
					b = Find(b);
					if (a != b)
					{
						parent[Math.Max(a, b)] = Math.Min(a, b);
					}
				}
			}
			return list.OrderBy<DisplayGroup, string>((DisplayGroup g) => g.Key, StringComparer.Ordinal).ToList();
			long Cell(float n)
			{
				return checked((long)Math.Floor((double)n / (double)rule.Radius));
			}
		}
	}
	public sealed class DisplayCache
	{
		private readonly Dictionary<string, List<Record>> records = new Dictionary<string, List<Record>>();

		private readonly Dictionary<string, List<DisplayGroup>> groups = new Dictionary<string, List<DisplayGroup>>();

		private readonly Dictionary<string, GroupRule> rules = new Dictionary<string, GroupRule>();

		public int Recalculations { get; private set; }

		public void Reload(IEnumerable<Record> source)
		{
			records.Clear();
			groups.Clear();
			rules.Clear();
			foreach (Record item in source)
			{
				Add(item);
			}
		}

		public void Add(Record record)
		{
			if (!records.TryGetValue(record.Type, out var value))
			{
				value = (records[record.Type] = new List<Record>());
			}
			value.Add(record);
			groups.Remove(record.Type);
		}

		public List<DisplayGroup> Visible(Filters filters, Func<string, GroupRule> resolve)
		{
			List<DisplayGroup> list = new List<DisplayGroup>();
			foreach (KeyValuePair<string, List<Record>> item in records.OrderBy<KeyValuePair<string, List<Record>>, string>((KeyValuePair<string, List<Record>> p) => p.Key, StringComparer.Ordinal))
			{
				if (filters.IsVisible(item.Key))
				{
					GroupRule groupRule = resolve(item.Key);
					if (!groups.TryGetValue(item.Key, out var value) || !rules[item.Key].Equals(groupRule))
					{
						value = SpatialGrouping.Build(item.Value, groupRule);
						groups[item.Key] = value;
						rules[item.Key] = groupRule;
						Recalculations++;
					}
					list.AddRange(value);
				}
			}
			return list;
		}
	}
	public sealed class OwnedMarkers<T> where T : class
	{
		private readonly Dictionary<string, Tuple<DisplayGroup, T>> rendered = new Dictionary<string, Tuple<DisplayGroup, T>>();

		private readonly Func<DisplayGroup, T> create;

		private readonly Action<T> remove;

		public int Count => rendered.Count;

		public IEnumerable<Tuple<DisplayGroup, T>> Entries => rendered.Values;

		public OwnedMarkers(Func<DisplayGroup, T> create, Action<T> remove)
		{
			this.create = create;
			this.remove = remove;
		}

		public void Apply(IEnumerable<DisplayGroup> desired)
		{
			Dictionary<string, DisplayGroup> dictionary = desired.ToDictionary<DisplayGroup, string>((DisplayGroup g) => g.Key, StringComparer.Ordinal);
			string[] array = rendered.Keys.ToArray();
			foreach (string key in array)
			{
				if (!dictionary.TryGetValue(key, out var value) || !rendered[key].Item1.SameAppearance(value))
				{
					remove(rendered[key].Item2);
					rendered.Remove(key);
				}
			}
			foreach (DisplayGroup value2 in dictionary.Values)
			{
				if (!rendered.ContainsKey(value2.Key))
				{
					rendered[value2.Key] = Tuple.Create(value2, create(value2));
				}
			}
		}

		public void Clear()
		{
			string[] array = rendered.Keys.ToArray();
			foreach (string key in array)
			{
				remove(rendered[key].Item2);
				rendered.Remove(key);
			}
		}
	}
	internal sealed class GroupingConfig
	{
		private readonly ConfigFile file;

		private readonly ConfigEntry<bool> global;

		private readonly Dictionary<string, ConfigEntry<bool>> categories = new Dictionary<string, ConfigEntry<bool>>();

		private readonly Dictionary<string, ConfigEntry<float>> radii = new Dictionary<string, ConfigEntry<float>>();

		private readonly Dictionary<string, ConfigEntry<GroupingOverride>> overrides = new Dictionary<string, ConfigEntry<GroupingOverride>>();

		private readonly Dictionary<string, ConfigEntry<float>> overrideRadii = new Dictionary<string, ConfigEntry<float>>();

		private readonly Action changed;

		public bool Enabled => global.Value;

		public GroupingConfig(ConfigFile file, Action changed)
		{
			//IL_00e3: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: Expected O, but got Unknown
			//IL_0189: Unknown result type (might be due to invalid IL or missing references)
			//IL_0193: Expected O, but got Unknown
			this.file = file;
			this.changed = changed;
			global = file.Bind<bool>("Grouping", "GroupNearbyResources", true, "