Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of TrailLedger v0.6.1
plugins/TrailLedger.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
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, "