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 Lettfeti v0.1.0
plugins/Lettfeti.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Guys Being Vikings")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+dc2b87ab2d62ac35ace2889d6c860967b4b9d765")] [assembly: AssemblyProduct("Lettfeti")] [assembly: AssemblyTitle("Lettfeti")] [assembly: AssemblyVersion("0.1.0.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 GBV.Lettfeti { internal static class Client { private static bool _checked; private static bool _hasUi; internal static bool HasUi { get { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 if (_checked) { return _hasUi; } _checked = true; try { _hasUi = (int)SystemInfo.graphicsDeviceType != 4; } catch (Exception ex) { _hasUi = false; Safety.Failed("Client.HasUi", ex); } return _hasUi; } } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] public static class Commands { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__2_0; internal void <Register>b__2_0(ConsoleEventArgs args) { try { Terminal context = args.Context; if (context != null) { context.AddString(Run(args)); } } catch (Exception ex) { Safety.Failed("lettfeti", ex); Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Lettfeti: that threw. See the log."); } } } } private static bool _registered; [HarmonyPostfix] public static void Postfix() { try { if (!_registered) { _registered = true; Register(); } } catch (Exception ex) { Safety.Failed("Terminal.InitTerminal command registration", ex); } } private static void Register() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown object obj = <>c.<>9__2_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { try { Terminal context = args.Context; if (context != null) { context.AddString(Run(args)); } } catch (Exception ex) { Safety.Failed("lettfeti", ex); Terminal context2 = args.Context; if (context2 != null) { context2.AddString("Lettfeti: that threw. See the log."); } } }; <>c.<>9__2_0 = val; obj = (object)val; } new ConsoleCommand("lettfeti", "lettfeti [on|off|reset] - crafting panel timings, or switch the optimisations on and off to compare.", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } private static string Run(ConsoleEventArgs args) { if (args.Length > 1) { string text = args[1].ToLowerInvariant(); switch (text) { case "off": case "on": { bool flag = text == "on"; if (flag && LettfetiPlugin.Enabled != null && !LettfetiPlugin.Enabled.Value) { return "Lettfeti is switched off in its config file, so nothing was patched at startup and there is nothing to turn on. Set Enabled to true in gbv.valheim.lettfeti.cfg and restart the game."; } Layers.Master = flag; Profile.Reset(); if (!flag) { return "Lettfeti is OFF and the counters are cleared, so the game is doing all of this its own way. The stopwatch is still running - that is the point. Open the inventory a few times, then type lettfeti to see what vanilla costs."; } return "Lettfeti is ON and the counters are cleared. Open the inventory a few times, then type lettfeti."; } case "reset": Profile.Reset(); return "Lettfeti: counters cleared."; default: return "Lettfeti: try 'lettfeti', 'lettfeti on', 'lettfeti off' or 'lettfeti reset'."; } } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Lettfeti 0.1.0 - " + (LettfetiPlugin.Active ? "ON" : "OFF") + (Layers.Master ? "" : " (switched off with 'lettfeti off')")); stringBuilder.AppendLine(); Profile.Report(stringBuilder); if (!LettfetiPlugin.Active) { stringBuilder.AppendLine(); stringBuilder.AppendLine("Those are VANILLA timings - every layer is standing down. 'lettfeti on' to compare."); return stringBuilder.ToString().TrimEnd(); } stringBuilder.AppendLine(); foreach (ILayer item in Layers.Registered) { try { item.Report(stringBuilder); } catch (Exception ex) { Safety.Failed("Commands.Report." + item.Id, ex); } } return stringBuilder.ToString().TrimEnd(); } } internal sealed class CountIndex { private readonly struct Key : IEquatable<Key> { private readonly string _name; private readonly int _quality; private readonly bool _worldLevel; internal Key(string name, int quality, bool worldLevel) { _name = name; _quality = quality; _worldLevel = worldLevel; } public bool Equals(Key other) { if (_quality == other._quality && _worldLevel == other._worldLevel) { return string.Equals(_name, other._name, StringComparison.Ordinal); } return false; } public override bool Equals(object obj) { if (obj is Key other) { return Equals(other); } return false; } public override int GetHashCode() { return (((((_name != null) ? StringComparer.Ordinal.GetHashCode(_name) : 0) * 397) ^ _quality) * 397) ^ (_worldLevel ? 1 : 0); } } private const int AnyQuality = -1; private readonly Dictionary<Key, int> _totals = new Dictionary<Key, int>(); private int _worldLevelFloor; internal int Slots { get; private set; } internal void Reset(int worldLevelFloor) { _totals.Clear(); _worldLevelFloor = worldLevelFloor; Slots = 0; } internal void Add(string name, int quality, int stack, int worldLevel) { Slots++; Fold(name, quality, stack, worldLevel: false); if (worldLevel >= _worldLevelFloor) { Fold(name, quality, stack, worldLevel: true); } } private void Fold(string name, int quality, int stack, bool worldLevel) { if (name != null) { Bump(new Key(name, quality, worldLevel), stack); Bump(new Key(name, -1, worldLevel), stack); } Bump(new Key(null, quality, worldLevel), stack); Bump(new Key(null, -1, worldLevel), stack); } private void Bump(Key key, int stack) { _totals.TryGetValue(key, out var value); _totals[key] = value + stack; } internal int Count(string name, int quality, bool matchWorldLevel) { if (quality < 0) { quality = -1; } _totals.TryGetValue(new Key(name, quality, matchWorldLevel), out var value); return value; } } internal sealed class CountsLayer : ILayer { private static ConfigEntry<bool> _enabled; private static readonly CountIndex Index = new CountIndex(); private static int _scopeFrame = -1; private static Inventory _scopeInventory; public string Id => "counts"; public string Summary => "the inventory is counted once per list rebuild instead of once per requirement per recipe."; public bool Enabled { get { if (LettfetiPlugin.Active && _enabled != null) { return _enabled.Value; } return false; } } public void Bind(ConfigFile config) { _enabled = config.Bind<bool>("Layers", "CacheInventoryCounts", true, "Count what is in your inventory once per crafting list rebuild, instead of re-counting it for every ingredient of every recipe. Makes the biggest difference when you are carrying a lot and know a lot of recipes."); } public string Apply(Harmony harmony) { //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown //IL_00ee: Expected O, but got Unknown //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(InventoryGui), "UpdateRecipeList", (Type[])null, (Type[])null); if (methodInfo == null) { return "InventoryGui.UpdateRecipeList could not be found."; } MethodInfo methodInfo2 = AccessTools.Method(typeof(Inventory), "CountItems", new Type[3] { typeof(string), typeof(int), typeof(bool) }, (Type[])null); if (methodInfo2 == null) { return "Inventory.CountItems(string, int, bool) could not be found."; } MethodInfo methodInfo3 = AccessTools.Method(typeof(Inventory), "Changed", new Type[2] { typeof(bool), typeof(bool) }, (Type[])null); harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(CountsLayer), "OpenScope", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(CountsLayer), "CloseScope", (Type[])null, (Type[])null)), (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(AccessTools.Method(typeof(CountsLayer), "ServeCount", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); if (methodInfo3 != null) { harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(CountsLayer), "InventoryMoved", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { Safety.Warn("Counts.changed", "Inventory.Changed could not be found, so the count cache loses one of its three safety nets. The other two still apply and it is still correct."); } return null; } private static void OpenScope() { try { Close(); if (_enabled == null || !_enabled.Value || !LettfetiPlugin.Active) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } Inventory inventory = ((Humanoid)localPlayer).GetInventory(); if (inventory == null) { return; } List<ItemData> allItems = inventory.GetAllItems(); if (allItems == null) { return; } Index.Reset(Game.m_worldLevel); for (int i = 0; i < allItems.Count; i++) { ItemData val = allItems[i]; if (val != null && val.m_shared != null) { Index.Add(val.m_shared.m_name, val.m_quality, val.m_stack, val.m_worldLevel); } } _scopeInventory = inventory; _scopeFrame = Time.frameCount; Profile.CountBuilds++; Profile.CountSlotsWalked += allItems.Count; } catch (Exception ex) { Close(); Safety.Failed("Counts.OpenScope", ex); } } private static Exception CloseScope() { Close(); return null; } private static void Close() { _scopeFrame = -1; _scopeInventory = null; } private static void InventoryMoved(Inventory __instance) { try { if (_scopeFrame >= 0 && __instance == _scopeInventory) { Safety.Warn("Counts.mutated", "the inventory changed in the middle of a crafting list rebuild, which should not be possible. The count cache has been dropped for safety and the game is counting for itself. Please report this - it is not causing you a problem, but it means another mod is doing something Lettfeti did not expect."); Close(); } } catch (Exception ex) { Close(); Safety.Failed("Counts.InventoryMoved", ex); } } private static bool ServeCount(Inventory __instance, string name, int quality, bool matchWorldLevel, ref int __result) { try { if (_scopeFrame < 0) { return true; } if (_scopeFrame != Time.frameCount) { Close(); return true; } if (__instance != _scopeInventory) { return true; } __result = Index.Count(name, quality, matchWorldLevel); Profile.CountServes++; return false; } catch (Exception ex) { Close(); Safety.Failed("Counts.ServeCount", ex); return true; } } public void Flush() { Close(); } public void Report(StringBuilder sb) { long countBuilds = Profile.CountBuilds; if (countBuilds == 0L && Profile.CountServes == 0L) { sb.AppendLine("Count cache: nothing counted yet."); return; } sb.AppendLine("Count cache: " + Profile.CountServes + " lookups served from " + countBuilds + " inventory walk" + ((countBuilds == 1) ? "" : "s")); if (countBuilds > 0) { sb.AppendLine(" " + Profile.CountSlotsWalked / countBuilds + " slots per walk, so roughly " + Profile.CountServes * (Profile.CountSlotsWalked / Math.Max(1L, countBuilds)) + " string compares avoided"); } } } internal sealed class DiscoveryLayer : ILayer { private static ConfigEntry<bool> _enabled; private static Player _lastPlayer; private static int _lastMaterials = -1; private static int _lastStations = -1; private static int _lastStationLevels = -1; private static int _lastRecipes = -1; private static int _lastTables = -1; private static int _lastPieces = -1; private static object _lastSeason; private static object _lastDb; private static readonly List<PieceTable> Tables = new List<PieceTable>(); public string Id => "discovery"; public string Summary => "the recipe database is only rescanned for new discoveries when something you know has actually changed."; public bool Enabled { get { if (LettfetiPlugin.Active && _enabled != null) { return _enabled.Value; } return false; } } public void Bind(ConfigFile config) { _enabled = config.Bind<bool>("Layers", "SkipRepeatRecipeScans", true, "Only look for newly unlocked recipes when something you know has actually changed, instead of every time anything moves in your inventory. Picking up loot and rearranging your pack get noticeably smoother."); } public string Apply(Harmony harmony) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Expected O, but got Unknown //IL_0066: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(Player), "UpdateKnownRecipesList", (Type[])null, (Type[])null); if (methodInfo == null) { return "Player.UpdateKnownRecipesList could not be found."; } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(DiscoveryLayer), "Skip", (Type[])null, (Type[])null)), new HarmonyMethod(AccessTools.Method(typeof(DiscoveryLayer), "Ran", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return null; } private static bool Skip(Player __instance, HashSet<string> ___m_knownMaterial, Dictionary<string, int> ___m_knownStations) { try { if (_enabled == null || !_enabled.Value || !LettfetiPlugin.Active) { return true; } if (__instance != Player.m_localPlayer) { return true; } if (__instance != _lastPlayer) { return true; } if (!Measure(__instance, ___m_knownMaterial, ___m_knownStations, out var materials, out var stations, out var levels, out var recipes, out var tables, out var pieces, out var season, out var db)) { return true; } if (materials != _lastMaterials) { return true; } if (stations != _lastStations) { return true; } if (levels != _lastStationLevels) { return true; } if (recipes != _lastRecipes) { return true; } if (tables != _lastTables) { return true; } if (pieces != _lastPieces) { return true; } if (season != _lastSeason) { return true; } if (db != _lastDb) { return true; } Profile.DiscoverySkips++; return false; } catch (Exception ex) { Safety.Failed("Discovery.Skip", ex); return true; } } private static void Ran(Player __instance, HashSet<string> ___m_knownMaterial, Dictionary<string, int> ___m_knownStations) { try { if (_enabled != null && _enabled.Value && LettfetiPlugin.Active && __instance == Player.m_localPlayer) { if (!Measure(__instance, ___m_knownMaterial, ___m_knownStations, out var materials, out var stations, out var levels, out var recipes, out var tables, out var pieces, out var season, out var db)) { Forget(); return; } _lastPlayer = __instance; _lastMaterials = materials; _lastStations = stations; _lastStationLevels = levels; _lastRecipes = recipes; _lastTables = tables; _lastPieces = pieces; _lastSeason = season; _lastDb = db; Profile.DiscoveryRuns++; } } catch (Exception ex) { Forget(); Safety.Failed("Discovery.Ran", ex); } } private static bool Measure(Player player, HashSet<string> knownMaterial, Dictionary<string, int> knownStations, out int materials, out int stations, out int levels, out int recipes, out int tables, out int pieces, out object season, out object db) { materials = (stations = (levels = (recipes = (tables = (pieces = -1))))); season = null; db = null; if (knownMaterial == null || knownStations == null) { return false; } ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null || instance.m_recipes == null) { return false; } Inventory inventory = ((Humanoid)player).GetInventory(); if (inventory == null) { return false; } materials = knownMaterial.Count; stations = knownStations.Count; levels = 0; foreach (KeyValuePair<string, int> knownStation in knownStations) { levels += knownStation.Value; } recipes = instance.m_recipes.Count; db = instance; season = player.CurrentSeason; Tables.Clear(); inventory.GetAllPieceTables(Tables); tables = Tables.Count; pieces = 0; for (int i = 0; i < Tables.Count; i++) { PieceTable val = Tables[i]; if ((Object)(object)val != (Object)null && val.m_pieces != null) { pieces += val.m_pieces.Count; } } Tables.Clear(); return true; } private static void Forget() { _lastPlayer = null; _lastMaterials = -1; _lastStations = -1; _lastStationLevels = -1; _lastRecipes = -1; _lastTables = -1; _lastPieces = -1; _lastSeason = null; _lastDb = null; } public void Flush() { Forget(); } public void Report(StringBuilder sb) { long num = Profile.DiscoverySkips + Profile.DiscoveryRuns; if (num == 0L) { sb.AppendLine("Recipe scans: none yet."); return; } sb.AppendLine("Recipe scans: " + Profile.DiscoveryRuns + " run, " + Profile.DiscoverySkips + " skipped as unchanged (" + (100.0 * (double)Profile.DiscoverySkips / (double)num).ToString("0") + "% avoided)"); } } internal interface ILayer { string Id { get; } string Summary { get; } bool Enabled { get; } void Bind(ConfigFile config); string Apply(Harmony harmony); void Flush(); void Report(StringBuilder sb); } internal static class Layers { private static readonly List<ILayer> All = new List<ILayer>(); private static bool _master = true; internal static bool Master { get { return _master; } set { if (_master != value) { _master = value; FlushAll(); } } } internal static IReadOnlyList<ILayer> Registered => All; internal static void ApplyAll(Harmony harmony, ConfigFile config, params ILayer[] layers) { foreach (ILayer layer in layers) { All.Add(layer); try { layer.Bind(config); } catch (Exception ex) { Safety.Failed("Layers.Bind." + layer.Id, ex); continue; } if (!layer.Enabled) { LettfetiPlugin.Log.LogInfo((object)(" " + layer.Id + ": off in the config, so nothing was patched for it.")); continue; } string text; try { text = layer.Apply(harmony); } catch (Exception ex2) { text = "it threw while patching: " + ex2.Message; Safety.Failed("Layers.Apply." + layer.Id, ex2); } if (text == null) { LettfetiPlugin.Log.LogInfo((object)(" " + layer.Id + ": applied. " + layer.Summary)); continue; } Safety.LayerFailed(layer.Id, text); layer.Flush(); } } internal static void FlushAll() { foreach (ILayer item in All) { try { item.Flush(); } catch (Exception ex) { Safety.Failed("Layers.Flush." + item.Id, ex); } } } } [BepInPlugin("gbv.valheim.lettfeti", "Lettfeti", "0.1.0")] public class LettfetiPlugin : BaseUnityPlugin { public const string PluginGuid = "gbv.valheim.lettfeti"; public const string PluginName = "Lettfeti"; public const string PluginVersion = "0.1.0"; internal static ManualLogSource Log; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> VerboseLog; private Harmony _harmony; internal static bool Active { get { if (Layers.Master && Enabled != null) { return Enabled.Value; } return false; } } private void Awake() { //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; if (!Client.HasUi) { Log.LogInfo((object)"Lettfeti 0.1.0: no graphics device, so this is a dedicated server. Every layer here makes the crafting panel cheaper and a server has no panel, so Lettfeti is standing down and patching nothing."); return; } Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Turn every optimisation on or off. Off, the game does its own thing everywhere and Lettfeti is inert. This takes effect immediately, and the console command 'lettfeti off' does the same thing temporarily, which is the easy way to feel the difference without restarting."); VerboseLog = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "VerboseLog", false, "Write what each layer decided to the log. For working out why the panel is still slow, or why a layer stood down."); _harmony = new Harmony("gbv.valheim.lettfeti"); _harmony.PatchAll(typeof(Commands)); string text = Profile.Apply(_harmony); if (text != null) { Safety.LayerFailed("timings", text); } Log.LogInfo((object)"Lettfeti 0.1.0 applying its layers:"); Layers.ApplyAll(_harmony, ((BaseUnityPlugin)this).Config, new PoolLayer(), new CountsLayer(), new DiscoveryLayer(), new TooltipLayer(), new RecipeIndexLayer(), new StationFilterLayer()); Log.LogInfo((object)"Lettfeti 0.1.0 loaded. Type 'lettfeti' in the console for timings."); } private void OnDestroy() { if (Client.HasUi) { try { Layers.FlushAll(); } catch (Exception ex) { Safety.Failed("LettfetiPlugin.OnDestroy", ex); } Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } } internal sealed class PoolLayer : ILayer { private static ConfigEntry<bool> _enabled; private static ConfigEntry<int> _cap; private const int DefaultCap = 2000; private static bool _swapped; private static readonly List<GameObject> Parked = new List<GameObject>(); private static GameObject _prefab; private static string _rentFailure; private static string _returnFailure; public string Id => "rowpool"; public string Summary => "recipe rows are reused instead of destroyed and rebuilt every time the list changes."; public bool Enabled { get { if (LettfetiPlugin.Active && _enabled != null) { return _enabled.Value; } return false; } } internal static int Spare => Parked.Count; private static bool Engaged { get { if (_swapped && _enabled != null && _enabled.Value) { return LettfetiPlugin.Active; } return false; } } private static int Cap { get { if (_cap == null) { return 2000; } if (_cap.Value <= 0) { return 0; } return _cap.Value; } } public void Bind(ConfigFile config) { _enabled = config.Bind<bool>("Layers", "ReuseRecipeRows", true, "Reuse the rows in the crafting list instead of deleting and rebuilding every one of them each time the list changes. This is where most of the speed comes from. Turning it off needs a restart to take effect fully; 'lettfeti off' in the console works straight away."); _cap = config.Bind<int>("Layers", "ReuseRecipeRowsLimit", 2000, "How many spare rows to keep BETWEEN rebuilds. It never limits a list while it is being built, so a list longer than this still reuses every row. It only decides how many to hold on to afterwards, in case the next list is long again. The default comfortably covers a heavily modded recipe list with No Cost on."); } public string Apply(Harmony harmony) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Expected O, but got Unknown //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown //IL_00bf: Expected O, but got Unknown //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(InventoryGui), "AddRecipeToList", (Type[])null, (Type[])null); if (methodInfo == null) { return "InventoryGui.AddRecipeToList could not be found."; } MethodInfo methodInfo2 = AccessTools.Method(typeof(InventoryGui), "UpdateRecipeList", (Type[])null, (Type[])null); if (methodInfo2 == null) { return "InventoryGui.UpdateRecipeList could not be found."; } _rentFailure = null; _returnFailure = null; harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(PoolLayer), "SwapInstantiate", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(PoolLayer), "SwapDestroy", (Type[])null, (Type[])null)), new HarmonyMethod(AccessTools.Method(typeof(PoolLayer), "Trim", (Type[])null, (Type[])null)), (HarmonyMethod)null); if (_rentFailure != null || _returnFailure != null) { _swapped = false; return _rentFailure ?? _returnFailure; } MethodInfo methodInfo3 = AccessTools.Method(typeof(InventoryGui), "OnDestroy", (Type[])null, (Type[])null); if (methodInfo3 != null) { harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(PoolLayer), "GuiGone", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { Safety.Warn("Pool.ondestroy", "InventoryGui.OnDestroy could not be found, so the row pool cannot be dropped when the inventory is torn down. Rows are still checked before reuse, so this is safe - just slightly wasteful after changing worlds."); } _swapped = true; return null; } private static MethodInfo ResolveInstantiate() { MethodInfo[] methods = typeof(Object).GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { if (!(methodInfo.Name != "Instantiate") && methodInfo.IsGenericMethodDefinition && methodInfo.GetGenericArguments().Length == 1) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 2 && parameters[0].ParameterType.IsGenericParameter && !(parameters[1].ParameterType != typeof(Transform))) { return methodInfo.MakeGenericMethod(typeof(GameObject)); } } } return null; } private static IEnumerable<CodeInstruction> SwapInstantiate(IEnumerable<CodeInstruction> instructions) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); MethodInfo methodInfo = ResolveInstantiate(); MethodInfo to = AccessTools.Method(typeof(PoolLayer), "Rent", (Type[])null, (Type[])null); if (!Transpile.SwapCall(list, methodInfo, to, 1, out var why)) { _rentFailure = "In AddRecipeToList, " + why; } return list; } private static IEnumerable<CodeInstruction> SwapDestroy(IEnumerable<CodeInstruction> instructions) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); MethodInfo methodInfo = AccessTools.Method(typeof(Object), "Destroy", new Type[1] { typeof(Object) }, (Type[])null); MethodInfo to = AccessTools.Method(typeof(PoolLayer), "Return", (Type[])null, (Type[])null); if (!Transpile.SwapCall(list, methodInfo, to, 1, out var why)) { _returnFailure = "In UpdateRecipeList, " + why; } return list; } public static GameObject Rent(GameObject prefab, Transform parent) { try { if (!Engaged) { return Object.Instantiate<GameObject>(prefab, parent); } if ((Object)(object)_prefab != (Object)null && (Object)(object)_prefab != (Object)(object)prefab) { Safety.Trace("the row prefab changed, so the pool was thrown away"); DestroyParked(); } _prefab = prefab; while (Parked.Count > 0) { int index = Parked.Count - 1; GameObject val = Parked[index]; Parked.RemoveAt(index); if (!((Object)(object)val == (Object)null)) { if ((Object)(object)val.transform.parent != (Object)(object)parent) { val.transform.SetParent(parent, false); } Button component = val.GetComponent<Button>(); if ((Object)(object)component != (Object)null) { ((UnityEventBase)component.onClick).RemoveAllListeners(); } Profile.PoolRents++; return val; } } Profile.PoolFresh++; return Object.Instantiate<GameObject>(prefab, parent); } catch (Exception ex) { Safety.Failed("Pool.Rent", ex); return Object.Instantiate<GameObject>(prefab, parent); } } public static void Return(Object element) { long timestamp = Stopwatch.GetTimestamp(); try { GameObject val = (GameObject)(object)((element is GameObject) ? element : null); if (!Engaged || (Object)(object)val == (Object)null) { Object.Destroy(element); return; } val.SetActive(false); Parked.Add(val); Profile.PoolReturns++; } catch (Exception ex) { Safety.Failed("Pool.Return", ex); try { Object.Destroy(element); } catch { } } finally { Profile.AddTeardown(Stopwatch.GetTimestamp() - timestamp); } } private static Exception Trim() { try { if (!Engaged) { return null; } int num = Parked.Count - Cap; if (num <= 0) { return null; } for (int num2 = Parked.Count - 1; num2 >= Cap; num2--) { GameObject val = Parked[num2]; Parked.RemoveAt(num2); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } } Profile.PoolEvicted += num; Safety.Trace("trimmed " + num + " spare rows down to the limit of " + Cap); } catch (Exception ex) { Safety.Failed("Pool.Trim", ex); } return null; } private static void GuiGone() { try { Parked.Clear(); _prefab = null; Safety.Trace("InventoryGui went away, so the row pool was dropped"); } catch (Exception ex) { Safety.Failed("Pool.GuiGone", ex); } } private static void DestroyParked() { foreach (GameObject item in Parked) { if ((Object)(object)item != (Object)null) { Object.Destroy((Object)(object)item); } } Parked.Clear(); _prefab = null; } public void Flush() { try { DestroyParked(); } catch (Exception ex) { Safety.Failed("Pool.Flush", ex); } } public void Report(StringBuilder sb) { if (!_swapped) { sb.AppendLine("Row reuse: NOT APPLIED - see the log."); return; } long num = Profile.PoolRents + Profile.PoolFresh; string text = ((num > 0) ? (" (" + (100.0 * (double)Profile.PoolRents / (double)num).ToString("0") + "% reused)") : ""); sb.AppendLine("Row reuse: " + Profile.PoolRents + " reused, " + Profile.PoolFresh + " built new" + text + ", " + Parked.Count + " spare"); if (Profile.PoolEvicted > 0) { sb.AppendLine(" " + Profile.PoolEvicted + " surplus rows trimmed after the list shrank (limit " + Cap + ")"); } } } internal static class Profile { private const int Window = 120; private static readonly double[] SampleMs = new double[120]; private static readonly double[] SampleRowMs = new double[120]; private static readonly double[] SampleTeardownMs = new double[120]; private static readonly double[] SampleSortMs = new double[120]; private static readonly int[] SampleRows = new int[120]; private static readonly int[] SampleFresh = new int[120]; private static readonly int[] SampleSpare = new int[120]; private static long _freshAtStart; private static int _spareAtStart; private static int _count; private static int _next; private static long _rebuilds; private static long _rowTicks; private static long _rowDepth; private static long _teardownTicks; private static long _sortTicks; internal static long PoolRents; internal static long PoolFresh; internal static long PoolReturns; internal static long PoolEvicted; internal static long CountServes; internal static long CountBuilds; internal static long CountSlotsWalked; internal static long DiscoverySkips; internal static long DiscoveryRuns; internal static long TooltipHits; internal static long TooltipMisses; internal static long RecipeIndexServes; internal static long RecipeIndexBuilds; internal static long StationFiltered; private static FieldInfo _availableRecipes; private static readonly double TicksToMs = 1000.0 / (double)Stopwatch.Frequency; internal static bool HasSamples => _count > 0; internal static void AddTeardown(long ticks) { _teardownTicks += ticks; } internal static string Apply(Harmony harmony) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown //IL_009b: Expected O, but got Unknown //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Expected O, but got Unknown //IL_00fc: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(InventoryGui), "UpdateRecipeList", (Type[])null, (Type[])null); if (methodInfo == null) { return "InventoryGui.UpdateRecipeList is not where it used to be, so there are no timings."; } _availableRecipes = AccessTools.Field(typeof(InventoryGui), "m_availableRecipes"); if (_availableRecipes == null) { Safety.Warn("Profile.rows", "InventoryGui.m_availableRecipes is not where it used to be, so timings will be reported without row counts. Everything else still works."); } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(Profile), "Begin", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(Profile), "End", (Type[])null, (Type[])null)), (HarmonyMethod)null); MethodInfo methodInfo2 = AccessTools.Method(typeof(InventoryGui), "AddRecipeToList", (Type[])null, (Type[])null); if (methodInfo2 != null) { harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(AccessTools.Method(typeof(Profile), "RowBegin", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(Profile), "RowEnd", (Type[])null, (Type[])null)), (HarmonyMethod)null); } PatchSort(harmony); return null; } private static void PatchSort(Harmony harmony) { //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Expected O, but got Unknown //IL_00d6: Expected O, but got Unknown try { Type type = AccessTools.Inner(typeof(InventoryGui), "RecipeDataPair"); if (type == null) { Safety.Warn("Profile.sort", "InventoryGui.RecipeDataPair could not be found, so sort time stays lumped in with layout time."); return; } Type type2 = typeof(List<>).MakeGenericType(type); Type type3 = typeof(Comparison<>).MakeGenericType(type); MethodInfo method = type2.GetMethod("Sort", new Type[1] { type3 }); if (method == null) { Safety.Warn("Profile.sort", "List<RecipeDataPair>.Sort(Comparison<T>) could not be found, so sort time stays lumped in with layout time."); } else { harmony.Patch((MethodBase)method, new HarmonyMethod(AccessTools.Method(typeof(Profile), "SortBegin", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(Profile), "SortEnd", (Type[])null, (Type[])null)), (HarmonyMethod)null); } } catch (Exception ex) { Safety.Warn("Profile.sort", "the recipe sort could not be timed separately, so its cost stays lumped in with layout time. Everything else is unaffected. " + ex.Message); } } private static void SortBegin(out long __state) { __state = Stopwatch.GetTimestamp(); } private static Exception SortEnd(long __state) { _sortTicks += Stopwatch.GetTimestamp() - __state; return null; } private static void RowBegin(out long __state) { __state = Stopwatch.GetTimestamp(); _rowDepth++; } private static Exception RowEnd(long __state) { _rowDepth--; if (_rowDepth <= 0) { _rowDepth = 0L; _rowTicks += Stopwatch.GetTimestamp() - __state; } return null; } private static void Begin(out long __state) { _rowTicks = 0L; _rowDepth = 0L; _teardownTicks = 0L; _sortTicks = 0L; _freshAtStart = PoolFresh; _spareAtStart = PoolLayer.Spare; __state = Stopwatch.GetTimestamp(); } private static Exception End(InventoryGui __instance, long __state) { try { double ms = (double)(Stopwatch.GetTimestamp() - __state) * TicksToMs; int rows = -1; if (_availableRecipes != null && _availableRecipes.GetValue(__instance) is ICollection collection) { rows = collection.Count; } Record(ms, rows, (double)_rowTicks * TicksToMs, (double)_teardownTicks * TicksToMs, (double)_sortTicks * TicksToMs); } catch (Exception ex) { Safety.Failed("Profile.End", ex); } return null; } private static void Record(double ms, int rows, double rowMs, double teardownMs, double sortMs) { SampleMs[_next] = ms; SampleRowMs[_next] = rowMs; SampleTeardownMs[_next] = teardownMs; SampleSortMs[_next] = sortMs; SampleRows[_next] = rows; SampleFresh[_next] = (int)(PoolFresh - _freshAtStart); SampleSpare[_next] = _spareAtStart; _next = (_next + 1) % 120; if (_count < 120) { _count++; } _rebuilds++; Safety.Trace("rebuilt " + rows + " rows in " + ms.ToString("0.00") + " ms"); } internal static void Reset() { _count = 0; _next = 0; _rebuilds = 0L; _rowTicks = 0L; _rowDepth = 0L; _teardownTicks = 0L; _sortTicks = 0L; PoolRents = (PoolFresh = (PoolReturns = (PoolEvicted = 0L))); CountServes = (CountBuilds = (CountSlotsWalked = 0L)); DiscoverySkips = (DiscoveryRuns = 0L); TooltipHits = (TooltipMisses = 0L); RecipeIndexServes = (RecipeIndexBuilds = 0L); StationFiltered = 0L; } internal static void Report(StringBuilder sb) { if (_count == 0) { sb.AppendLine("No recipe-list rebuilds recorded yet. Open the inventory."); return; } double num = 0.0; double num2 = 0.0; double num3 = 0.0; double num4 = 0.0; double num5 = 0.0; int num6 = 0; int num7 = 0; for (int i = 0; i < _count; i++) { num += SampleMs[i]; num2 += SampleRowMs[i]; num3 += SampleTeardownMs[i]; num4 += SampleSortMs[i]; if (SampleMs[i] > num5) { num5 = SampleMs[i]; num7 = SampleRows[i]; } if (SampleRows[i] > num6) { num6 = SampleRows[i]; } } int num8 = (_next - 1 + 120) % 120; sb.AppendLine("Recipe list: " + _rebuilds + " rebuilds, last " + _count + " averaged " + (num / (double)_count).ToString("0.00") + " ms"); sb.AppendLine(" last " + SampleMs[num8].ToString("0.00") + " ms over " + SampleRows[num8] + " rows"); sb.AppendLine(" worst " + num5.ToString("0.00") + " ms over " + num7 + " rows (most rows seen: " + num6 + ")"); if (num2 > 0.0) { double num9 = num - num2 - num3 - num4; sb.AppendLine(" where it goes, per rebuild:"); sb.AppendLine(" " + (num3 / (double)_count).ToString("0.00") + " ms clearing the old list"); sb.AppendLine(" " + (num2 / (double)_count).ToString("0.00") + " ms building rows"); sb.AppendLine(" " + (num4 / (double)_count).ToString("0.00") + " ms sorting"); sb.AppendLine(" " + (num9 / (double)_count).ToString("0.00") + " ms laying out and the rest"); if (SampleRows[num8] > 0) { sb.AppendLine(" " + (1000.0 * num2 / (double)_count / (double)Math.Max(1, SampleRows[num8])).ToString("0") + " microseconds per row"); } } int num10 = Math.Min(_count, 10); sb.AppendLine(" last " + num10 + " rebuilds, newest first:"); for (int j = 0; j < num10; j++) { int num11 = (_next - 1 - j + 240) % 120; sb.AppendLine(" " + SampleMs[num11].ToString("0.0").PadLeft(7) + " ms " + SampleRows[num11].ToString().PadLeft(4) + " rows " + SampleFresh[num11].ToString().PadLeft(4) + " built new " + SampleSpare[num11].ToString().PadLeft(4) + " spare at start"); } } } internal sealed class RecipeIndexLayer : ILayer { private static ConfigEntry<bool> _enabled; private static readonly Dictionary<string, Recipe> ByName = new Dictionary<string, Recipe>(StringComparer.Ordinal); private static ObjectDB _builtFrom; private static int _builtAtCount = -1; public string Id => "recipeindex"; public string Summary => "looking a recipe up by item no longer searches the whole recipe database."; public bool Enabled { get { if (LettfetiPlugin.Active && _enabled != null) { return _enabled.Value; } return false; } } public void Bind(ConfigFile config) { _enabled = config.Bind<bool>("Layers", "IndexRecipeLookups", true, "Look recipes up by name instead of searching the whole recipe list every time. Mostly affects the repair button, which searches once per worn item every frame the inventory is open."); } public string Apply(Harmony harmony) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected O, but got Unknown //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(ObjectDB), "GetRecipe", new Type[1] { typeof(ItemData) }, (Type[])null); if (methodInfo == null) { return "ObjectDB.GetRecipe(ItemData) could not be found."; } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(RecipeIndexLayer), "Serve", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); MethodInfo methodInfo2 = AccessTools.Method(typeof(ObjectDB), "Awake", (Type[])null, (Type[])null); if (methodInfo2 != null) { harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(RecipeIndexLayer), "DatabaseChanged", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } MethodInfo methodInfo3 = AccessTools.Method(typeof(ObjectDB), "CopyOtherDB", (Type[])null, (Type[])null); if (methodInfo3 != null) { harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(RecipeIndexLayer), "DatabaseChanged", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } if (methodInfo2 == null && methodInfo3 == null) { Safety.Warn("RecipeIndex.hooks", "neither ObjectDB.Awake nor ObjectDB.CopyOtherDB could be found, so the recipe index relies on its recipe-count check alone. That is still correct, just less direct."); } return null; } private static bool Serve(ObjectDB __instance, ItemData item, ref Recipe __result) { try { if (_enabled == null || !_enabled.Value || !LettfetiPlugin.Active) { return true; } if (item == null || item.m_shared == null) { return true; } if ((Object)(object)__instance == (Object)null || __instance.m_recipes == null) { return true; } if (__instance != _builtFrom || __instance.m_recipes.Count != _builtAtCount) { Build(__instance); } ByName.TryGetValue(item.m_shared.m_name, out var value); __result = value; Profile.RecipeIndexServes++; return false; } catch (Exception ex) { Safety.Failed("RecipeIndex.Serve", ex); Drop(); return true; } } private static void Build(ObjectDB db) { ByName.Clear(); List<Recipe> recipes = db.m_recipes; for (int i = 0; i < recipes.Count; i++) { Recipe val = recipes[i]; if ((Object)(object)val == (Object)null || (Object)(object)val.m_item == (Object)null) { continue; } ItemData itemData = val.m_item.m_itemData; if (itemData != null && itemData.m_shared != null) { string name = itemData.m_shared.m_name; if (name != null && !ByName.ContainsKey(name)) { ByName[name] = val; } } } _builtFrom = db; _builtAtCount = recipes.Count; Profile.RecipeIndexBuilds++; Safety.Trace("indexed " + ByName.Count + " recipes from " + recipes.Count + " entries"); } private static void DatabaseChanged() { Drop(); } private static void Drop() { ByName.Clear(); _builtFrom = null; _builtAtCount = -1; } public void Flush() { Drop(); } public void Report(StringBuilder sb) { if (Profile.RecipeIndexServes == 0L) { sb.AppendLine("Recipe index: no lookups yet."); return; } sb.AppendLine("Recipe index: " + Profile.RecipeIndexServes + " lookups over " + ByName.Count + " recipes, rebuilt " + Profile.RecipeIndexBuilds + " time" + ((Profile.RecipeIndexBuilds == 1) ? "" : "s")); } } internal static class Safety { private static readonly HashSet<string> Reported = new HashSet<string>(StringComparer.Ordinal); public static void Failed(string where, Exception ex) { if (Reported.Add(where)) { ManualLogSource log = LettfetiPlugin.Log; if (log != null) { log.LogError((object)("Lettfeti: " + where + " threw and was ignored, so the game keeps its own behaviour. This is a bug in Lettfeti; please report it. It will not be reported again this session.\n" + ex)); } } } public static void Warn(string where, string message) { if (Reported.Add(where)) { ManualLogSource log = LettfetiPlugin.Log; if (log != null) { log.LogWarning((object)("Lettfeti: " + message)); } } } public static void LayerFailed(string layer, string reason) { ManualLogSource log = LettfetiPlugin.Log; if (log != null) { log.LogError((object)("Lettfeti: the " + layer + " layer could NOT be applied and has been switched off for this session, so the game keeps its own behaviour there. " + reason + " This usually means a Valheim update moved the code Lettfeti was patching; the mod needs rebuilding against the new version.")); } } public static void Trace(string message) { if (LettfetiPlugin.VerboseLog != null && LettfetiPlugin.VerboseLog.Value) { ManualLogSource log = LettfetiPlugin.Log; if (log != null) { log.LogInfo((object)("Lettfeti: " + message)); } } } } internal sealed class TooltipLayer : ILayer { private static ConfigEntry<bool> _enabled; private static ConfigEntry<float> _maxAge; private static bool _swapped; private static string _cached; private static object _keyItem; private static int _keyQuality; private static bool _keyCrafting; private static float _keyWorldLevel; private static int _keyStack; private static bool _keyAppending; private static float _keyHealth; private static float _keyStamina; private static float _keyEitr; private static float _keyEitrRegen; private static float _cachedAt; private static string _failure; public string Id => "tooltip"; public string Summary => "the selected recipe's description is rebuilt when it changes instead of every frame."; public bool Enabled { get { if (LettfetiPlugin.Active && _enabled != null) { return _enabled.Value; } return false; } } private static bool Engaged { get { if (_swapped && _enabled != null && _enabled.Value) { return LettfetiPlugin.Active; } return false; } } private static float MaxAge { get { if (_maxAge == null) { return 1f; } if (!(_maxAge.Value > 0f)) { return 0f; } return _maxAge.Value; } } public void Bind(ConfigFile config) { _enabled = config.Bind<bool>("Layers", "CacheRecipeDescription", true, "Rebuild the selected recipe's description text when something about it changes, instead of rebuilding it from scratch every single frame. Saves a lot of memory churn while the crafting panel is open."); _maxAge = config.Bind<float>("Layers", "CacheRecipeDescriptionSeconds", 1f, "How long a cached description may be reused before it is rebuilt anyway. The cache already tracks everything known to change the text, so this is only a safety net. Set it to 0 to rebuild every frame, which is what the game does."); } public string Apply(Harmony harmony) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(InventoryGui), "UpdateRecipe", (Type[])null, (Type[])null); if (methodInfo == null) { return "InventoryGui.UpdateRecipe could not be found."; } _failure = null; harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(TooltipLayer), "SwapTooltip", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null); if (_failure != null) { _swapped = false; return _failure; } _swapped = true; MethodInfo methodInfo2 = AccessTools.Method(typeof(Inventory), "Changed", new Type[2] { typeof(bool), typeof(bool) }, (Type[])null); if (methodInfo2 != null) { harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(TooltipLayer), "InventoryMoved", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } else { Safety.Warn("Tooltip.changed", "Inventory.Changed could not be found, so a description that depends on your equipment may lag by up to the cache's max age after a gear swap. Lower CacheRecipeDescriptionSeconds if you notice it."); } return null; } private static IEnumerable<CodeInstruction> SwapTooltip(IEnumerable<CodeInstruction> instructions) { List<CodeInstruction> list = new List<CodeInstruction>(instructions); MethodInfo methodInfo = AccessTools.Method(typeof(ItemData), "GetTooltip", new Type[6] { typeof(ItemData), typeof(int), typeof(bool), typeof(float), typeof(int), typeof(bool) }, (Type[])null); MethodInfo to = AccessTools.Method(typeof(TooltipLayer), "Get", (Type[])null, (Type[])null); if (!Transpile.SwapCall(list, methodInfo, to, 1, out var why)) { _failure = "In UpdateRecipe, " + why; } return list; } public static string Get(ItemData item, int qualityLevel, bool crafting, float worldLevel, int stackOverride, bool appending) { try { if (!Engaged) { return ItemData.GetTooltip(item, qualityLevel, crafting, worldLevel, stackOverride, appending); } ReadPlayer(out var health, out var stamina, out var eitr, out var eitrRegen); if (_cached != null && item == _keyItem && qualityLevel == _keyQuality && crafting == _keyCrafting && worldLevel == _keyWorldLevel && stackOverride == _keyStack && appending == _keyAppending && health == _keyHealth && stamina == _keyStamina && eitr == _keyEitr && eitrRegen == _keyEitrRegen && Time.unscaledTime - _cachedAt <= MaxAge) { Profile.TooltipHits++; return _cached; } string result = (_cached = ItemData.GetTooltip(item, qualityLevel, crafting, worldLevel, stackOverride, appending)); _keyItem = item; _keyQuality = qualityLevel; _keyCrafting = crafting; _keyWorldLevel = worldLevel; _keyStack = stackOverride; _keyAppending = appending; _keyHealth = health; _keyStamina = stamina; _keyEitr = eitr; _keyEitrRegen = eitrRegen; _cachedAt = Time.unscaledTime; Profile.TooltipMisses++; return result; } catch (Exception ex) { Safety.Failed("Tooltip.Get", ex); Drop(); return ItemData.GetTooltip(item, qualityLevel, crafting, worldLevel, stackOverride, appending); } } private static void ReadPlayer(out float health, out float stamina, out float eitr, out float eitrRegen) { health = (stamina = (eitr = (eitrRegen = 0f))); Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null)) { health = ((Character)localPlayer).GetMaxHealth(); stamina = ((Character)localPlayer).GetMaxStamina(); eitr = ((Character)localPlayer).GetMaxEitr(); eitrRegen = localPlayer.GetEquipmentEitrRegenModifier(); } } private static void InventoryMoved(Inventory __instance) { try { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && __instance == ((Humanoid)localPlayer).GetInventory()) { Drop(); } } catch (Exception ex) { Drop(); Safety.Failed("Tooltip.InventoryMoved", ex); } } private static void Drop() { _cached = null; _keyItem = null; } public void Flush() { Drop(); } public void Report(StringBuilder sb) { if (!_swapped) { sb.AppendLine("Description cache: NOT APPLIED - see the log."); return; } long num = Profile.TooltipHits + Profile.TooltipMisses; if (num == 0L) { sb.AppendLine("Description cache: nothing drawn yet."); return; } sb.AppendLine("Description cache: " + Profile.TooltipHits + " reused, " + Profile.TooltipMisses + " rebuilt (" + (100.0 * (double)Profile.TooltipHits / (double)num).ToString("0") + "% of frames skipped)"); } } internal static class Transpile { internal static bool SwapCall(List<CodeInstruction> body, MethodInfo from, MethodInfo to, int expect, out string why) { why = null; if (body == null) { why = "there was no method body to patch."; return false; } if (from == null) { why = "the call being replaced could not be resolved."; return false; } if (to == null) { why = "the replacement method could not be resolved."; return false; } if (!to.IsStatic) { why = "the replacement " + to.Name + " is not static."; return false; } int num = 0; for (int i = 0; i < body.Count; i++) { if (Matches(body[i], from)) { num++; } } if (num != expect) { why = "expected " + expect + " call" + ((expect == 1) ? "" : "s") + " to " + Describe(from) + " but found " + num + "."; return false; } for (int j = 0; j < body.Count; j++) { if (Matches(body[j], from)) { body[j].opcode = OpCodes.Call; body[j].operand = to; } } return true; } private static bool Matches(CodeInstruction instruction, MethodInfo target) { if (instruction == null) { return false; } if (instruction.opcode != OpCodes.Call && instruction.opcode != OpCodes.Callvirt) { return false; } return Same(instruction.operand as MethodBase, target); } internal static bool Same(MethodBase candidate, MethodInfo target) { if (candidate == null || target == null) { return false; } if ((object)candidate == target) { return true; } if (candidate.Name != target.Name) { return false; } if (candidate.DeclaringType != target.DeclaringType) { return false; } if (candidate.IsStatic != target.IsStatic) { return false; } ParameterInfo[] parameters = candidate.GetParameters(); ParameterInfo[] parameters2 = target.GetParameters(); if (parameters.Length != parameters2.Length) { return false; } for (int i = 0; i < parameters.Length; i++) { if (parameters[i].ParameterType != parameters2[i].ParameterType) { return false; } } MethodInfo methodInfo = candidate as MethodInfo; if (methodInfo != null && methodInfo.ReturnType != target.ReturnType) { return false; } return true; } private static string Describe(MethodInfo method) { return ((method.DeclaringType != null) ? (method.DeclaringType.Name + ".") : "") + method.Name; } } internal sealed class StationFilterLayer : ILayer { private static ConfigEntry<bool> _enabled; public string Id => "stationfilter"; public string Summary => "with No Cost on, the crafting list is limited to what this station can make."; public bool Enabled { get { if (LettfetiPlugin.Active && _enabled != null) { return _enabled.Value; } return false; } } public void Bind(ConfigFile config) { _enabled = config.Bind<bool>("Tweaks", "NoCostShowsOnlyThisStation", true, "CHANGES WHAT YOU SEE, which is why it lives in its own section.\n\nWith No Cost turned on, Valheim lists every recipe in the game at once, for every workstation. On a modded setup that is over a thousand rows, which is slow to build and no use to scroll. With this on, standing at a station shows what that station makes. Crafting is still free - this only affects the list.\n\nStanding at NO station is left alone, so No Cost still lets you craft anything anywhere out in the field.\n\nIt makes no difference at all unless No Cost is on."); } public string Apply(Harmony harmony) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(typeof(Player), "GetAvailableRecipes", (Type[])null, (Type[])null); if (methodInfo == null) { return "Player.GetAvailableRecipes could not be found."; } harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(StationFilterLayer), "Trim", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return null; } private static void Trim(Player __instance, ref List<Recipe> available) { try { if (_enabled == null || !_enabled.Value || !LettfetiPlugin.Active || available == null || available.Count == 0 || !__instance.NoCostCheat() || (Object)(object)__instance.GetCurrentCraftingStation() == (Object)null) { return; } int num = 0; for (int i = 0; i < available.Count; i++) { Recipe val = available[i]; if (!((Object)(object)val == (Object)null) && __instance.RequiredCraftingStation(val, 1, false)) { available[num++] = val; } } int num2 = available.Count - num; if (num2 > 0) { available.RemoveRange(num, num2); Profile.StationFiltered += num2; } } catch (Exception ex) { Safety.Failed("StationFilter.Trim", ex); } } public void Flush() { } public void Report(StringBuilder sb) { if (Profile.StationFiltered != 0L) { sb.AppendLine("Station filter: " + Profile.StationFiltered + " out-of-station rows dropped while No Cost was on"); } } } }