// Consolidated decompiled source — Kadrio-RecipePinner v1.3.0 // Generated by Hexium's decompiled-source browser. Best-effort concatenation of every type in this // version's manifest — decompiler output isn't guaranteed to compile as-is. using System.Collections.Generic; using HarmonyLib; using UnityEngine; using System; using System.IO; using System.Text; using BepInEx; using System.Collections; using System.Reflection; using System.Text.RegularExpressions; using UnityEngine.UI; using BepInEx.Bootstrap; using BepInEx.Configuration; using System.Linq.Expressions; using UnityEngine.EventSystems; // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.ContainerScanner ---- namespace ValheimRecipePinner { public class ContainerScanner { public static List AllContainers = new List(); private static readonly HashSet _containerSet = new HashSet(); internal static readonly object ContainerLock = new object(); public Dictionary ContainerCache = new Dictionary(); private static readonly HashSet _processedIDs = new HashSet(); private readonly List _snapshotBuffer = new List(); private Vector3 _lastScanPos; private int _lastItemCount; private float _scanTimer; private float _moveScanCooldown; private const float MovementThresholdSqr = 4f; private const float MinMoveScanCooldown = 1f; private static volatile bool _isInitializing = false; public void InitializeContainers() { if (!RecipePinnerPlugin.EnableChestScanning.Value) { DebugLogger.Verbose("InitializeContainers skipped — chest scanning disabled"); return; } if (_isInitializing) { DebugLogger.Verbose("InitializeContainers skipped — already initializing"); return; } _isInitializing = true; try { DebugLogger.Verbose("Init containers"); lock (ContainerLock) { if (AllContainers.Count > 0) { DebugLogger.Verbose($"InitializeContainers: list already populated ({AllContainers.Count}), skipping scan"); return; } Container[] array = Object.FindObjectsByType(FindObjectsSortMode.None); foreach (Container container in array) { if (container != null && _containerSet.Add(container)) { AllContainers.Add(container); if (container.GetComponent() == null) { container.gameObject.AddComponent().MyContainer = container; } } } DebugLogger.Verbose($"Tracking {AllContainers.Count} containers"); } } finally { _isInitializing = false; } } public static void ClearAll() { lock (ContainerLock) { AllContainers.Clear(); _containerSet.Clear(); DebugLogger.Verbose("ContainerScanner: all container references cleared"); } } public void UpdateScanning() { if (Player.m_localPlayer == null) { return; } _scanTimer += Time.deltaTime; _moveScanCooldown += Time.deltaTime; bool flag = Vector3.SqrMagnitude(Player.m_localPlayer.transform.position - _lastScanPos) > 4f && _moveScanCooldown >= 1f; float num = ((InventoryGui.instance != null && ReflectionHelper.GetCurrentContainer(InventoryGui.instance) != null) ? 0.5f : RecipePinnerPlugin.ChestScanInterval.Value); bool flag2 = _scanTimer >= num; if (!flag && !flag2) { return; } int num2 = 0; foreach (ItemDrop.ItemData allItem in Player.m_localPlayer.GetInventory().GetAllItems()) { num2 += allItem.m_stack; } bool flag3 = num2 != _lastItemCount; _lastItemCount = num2; DebugLogger.Verbose($"Scanning containers - Moved: {flag}, InvChanged: {flag3}, Interval: {flag2}"); _scanTimer = 0f; if (flag) { _moveScanCooldown = 0f; } UpdateContainerCache(); } private void UpdateContainerCache() { ContainerCache.Clear(); if (Player.m_localPlayer == null) { DebugLogger.Verbose("Cannot scan - player is null"); return; } Vector3 position = Player.m_localPlayer.transform.position; float value = RecipePinnerPlugin.ChestScanRange.Value; float num = value * value; _snapshotBuffer.Clear(); lock (ContainerLock) { _snapshotBuffer.AddRange(AllContainers); } _processedIDs.Clear(); int num2 = 0; int num3 = 0; int num4 = 0; foreach (Container item in _snapshotBuffer) { if (item == null || item.transform == null) { num3++; continue; } int instanceID = item.GetInstanceID(); if (!_processedIDs.Add(instanceID)) { num3++; continue; } if (Vector3.SqrMagnitude(item.transform.position - position) > num) { num3++; continue; } bool flag = true; if (ReflectionHelper.CheckContainerAccess != null) { flag = ReflectionHelper.CheckContainerAccess(item, Player.m_localPlayer.GetPlayerID()); } if (!flag) { num4++; continue; } Inventory inventory = item.GetInventory(); if (inventory == null) { continue; } foreach (ItemDrop.ItemData allItem in inventory.GetAllItems()) { string name = allItem.m_shared.m_name; if (ContainerCache.TryGetValue(name, out var value2)) { ContainerCache[name] = value2 + allItem.m_stack; } else { ContainerCache[name] = allItem.m_stack; } } num2++; } _lastScanPos = position; DebugLogger.Verbose($"Container scan complete - Scanned: {num2}, Skipped: {num3}, AccessDenied: {num4}, UniqueItems: {ContainerCache.Count}"); } [HarmonyPatch(typeof(Container), "Awake")] [HarmonyPostfix] public static void TrackContainerAwake(Container __instance) { if (__instance == null || !RecipePinnerPlugin.EnableChestScanning.Value) { return; } lock (ContainerLock) { if (_containerSet.Add(__instance)) { AllContainers.Add(__instance); (__instance.gameObject.GetComponent() ?? __instance.gameObject.AddComponent()).MyContainer = __instance; DebugLogger.Verbose($"New container tracked: {__instance.name} (Total: {AllContainers.Count})"); } } } public static void RemoveFromSet(Container c) { _containerSet.Remove(c); } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.ContainerTracker ---- namespace ValheimRecipePinner { public class ContainerTracker : MonoBehaviour { public Container MyContainer; private void OnDestroy() { if (ContainerScanner.AllContainers != null && MyContainer != null) { lock (ContainerScanner.ContainerLock) { ContainerScanner.AllContainers.Remove(MyContainer); ContainerScanner.RemoveFromSet(MyContainer); DebugLogger.Verbose($"Container removed: {MyContainer.name} (Remaining: {ContainerScanner.AllContainers.Count})"); } } } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.DataPersistence ---- namespace ValheimRecipePinner { public class DataPersistence { public void SavePins() { try { string savePath = GetSavePath(); if (string.IsNullOrEmpty(savePath)) { DebugLogger.Warning("Cannot save - save path is invalid"); return; } RecipeManager recipeManager = RecipePinnerPlugin.Instance?.RecipeMgr; if (recipeManager == null) { DebugLogger.Warning("Cannot save - RecipeMgr is null"); return; } List list = new List(); HashSet hashSet = new HashSet(); foreach (string item in recipeManager.PinnedRecipeOrder) { int value3; if (!hashSet.Add(item)) { DebugLogger.Warning("Skipping duplicate pin order entry while saving: " + item); } else if (item.StartsWith("GROUP:")) { string text = item.Substring(6); if (!recipeManager.PinGroups.TryGetValue(text, out var value)) { continue; } List list2 = new List(); foreach (string memberRecipeKey in value.MemberRecipeKeys) { int value2; int num = ((!value.MemberCounts.TryGetValue(memberRecipeKey, out value2)) ? 1 : value2); list2.Add($"{EscapeSaveValue(memberRecipeKey)}:{num}"); } string text2 = string.Join(",", list2); list.Add("GROUP:" + EscapeSaveValue(text) + "|" + text2); DebugLogger.Verbose($"Saved group: {text} with {value.MemberRecipeKeys.Count} members"); } else if (recipeManager.PinnedRecipes.TryGetValue(item, out value3)) { list.Add($"{EscapeSaveValue(item)}:{value3}"); } } WriteAllLinesAtomically(savePath, list); int count = recipeManager.PinGroups.Count; DebugLogger.Log($"Saved {list.Count} entries ({list.Count - count} pins, {count} groups) to: {savePath}"); } catch (Exception ex) { DebugLogger.Error("Failed to save pins", ex); } } public void LoadPins() { string savePath = GetSavePath(); if (string.IsNullOrEmpty(savePath)) { DebugLogger.Warning("Cannot load - save path is invalid"); return; } RecipeManager recipeManager = RecipePinnerPlugin.Instance?.RecipeMgr; if (recipeManager == null) { DebugLogger.Warning("Cannot load - RecipeMgr is null"); return; } if (!File.Exists(savePath)) { DebugLogger.Log("No save file found at: " + savePath); return; } try { string[] array = File.ReadAllLines(savePath); Dictionary dictionary = new Dictionary(); Dictionary dictionary2 = new Dictionary(); List list = new List(); HashSet hashSet = new HashSet(); int num = 0; int num2 = 0; int num3 = 0; string[] array2 = array; foreach (string text in array2) { if (string.IsNullOrWhiteSpace(text)) { continue; } if (text.StartsWith("GROUP:")) { string text2 = text.Substring(6); int num4 = FindGroupSeparator(text2); if (num4 > 0 && num4 < text2.Length - 1) { string text3 = UnescapeSaveValue(text2.Substring(0, num4).Trim()); string[] array3 = text2.Substring(num4 + 1).Trim().Split(new char[1] { ',' }); if (!string.IsNullOrEmpty(text3) && array3.Length >= 2) { PinGroupData pinGroupData = new PinGroupData { GroupName = text3 }; string[] array4 = array3; for (int j = 0; j < array4.Length; j++) { string text4 = array4[j].Trim(); if (string.IsNullOrEmpty(text4)) { continue; } int num5 = text4.LastIndexOf(':'); if (num5 > 0 && num5 < text4.Length - 1) { string text5 = UnescapeSaveValue(text4.Substring(0, num5)); int result = 1; int.TryParse(text4.Substring(num5 + 1), out result); if (result < 1) { result = 1; } pinGroupData.MemberRecipeKeys.Add(text5); pinGroupData.MemberCounts[text5] = result; } else { string text6 = UnescapeSaveValue(text4); pinGroupData.MemberRecipeKeys.Add(text6); pinGroupData.MemberCounts[text6] = 1; } } if (pinGroupData.MemberRecipeKeys.Count >= 2) { string item = "GROUP:" + text3; if (!hashSet.Add(item)) { DebugLogger.Warning("Duplicate group entry in save file, keeping first order position and latest data: " + text3); num3++; } else { list.Add(item); } dictionary2[text3] = pinGroupData; num2++; DebugLogger.Verbose($"Loaded group: {text3} with {pinGroupData.MemberRecipeKeys.Count} members"); } else { DebugLogger.Warning("Group '" + text3 + "' has less than 2 members, skipping"); num3++; } } else { DebugLogger.Warning("Invalid group format: " + text); num3++; } } else { DebugLogger.Warning("Invalid group line (missing pipe): " + text); num3++; } continue; } int num6 = text.LastIndexOf(':'); if (num6 > 0 && num6 < text.Length - 1) { string text7 = UnescapeSaveValue(text.Substring(0, num6).Trim()); if (int.TryParse(text.Substring(num6 + 1).Trim(), out var result2)) { if (!hashSet.Add(text7)) { DebugLogger.Warning("Duplicate pin entry in save file, keeping first order position and latest count: " + text7); num3++; } else { list.Add(text7); } dictionary[text7] = result2; num++; } else { DebugLogger.Warning("Invalid count value in save file: " + text); num3++; } } else { string text8 = UnescapeSaveValue(text.Trim()); if (!hashSet.Add(text8)) { DebugLogger.Warning("Duplicate legacy pin entry in save file, keeping first order position and latest count: " + text8); num3++; } else { list.Add(text8); } dictionary[text8] = 1; num++; } } recipeManager.PinnedRecipes.Clear(); recipeManager.PinGroups.Clear(); recipeManager.PinnedRecipeOrder.Clear(); foreach (KeyValuePair item2 in dictionary) { recipeManager.PinnedRecipes[item2.Key] = item2.Value; } foreach (KeyValuePair item3 in dictionary2) { recipeManager.PinGroups[item3.Key] = item3.Value; } recipeManager.PinnedRecipeOrder.AddRange(list); int effectivePinCount = recipeManager.GetEffectivePinCount(); if (effectivePinCount > RecipePinnerPlugin.MaximumPins.Value) { int num7 = recipeManager.TrimToMaximumPins(RecipePinnerPlugin.MaximumPins.Value); DebugLogger.Warning($"Loaded save exceeded max effective pins ({effectivePinCount} > {RecipePinnerPlugin.MaximumPins.Value}) - trimmed {num7} effective pin(s)"); } DebugLogger.Log($"Loaded {num} pins and {num2} groups from: {savePath} (Errors: {num3})"); } catch (Exception ex) { DebugLogger.Error("Failed to load pins", ex); } } private void WriteAllLinesAtomically(string savePath, List lines) { string directoryName = Path.GetDirectoryName(savePath); if (string.IsNullOrEmpty(directoryName)) { throw new IOException("Invalid save directory for path: " + savePath); } string fileName = Path.GetFileName(savePath); string text = Path.Combine(directoryName, $"{fileName}.{Guid.NewGuid():N}.tmp"); string text2 = savePath + ".bak"; try { File.WriteAllLines(text, lines); if (File.Exists(savePath)) { if (File.Exists(text2)) { File.Delete(text2); } File.Replace(text, savePath, text2, ignoreMetadataErrors: true); } else { File.Move(text, savePath); } } catch { try { if (File.Exists(text)) { File.Delete(text); } } catch (Exception ex) { DebugLogger.Warning("Failed to delete temp save file '" + text + "': " + ex.Message); } throw; } } private static int FindGroupSeparator(string groupContent) { int num = groupContent.LastIndexOf('|'); if (num >= 0) { return num; } return -1; } private static string EscapeSaveValue(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } return value.Replace("%", "%25").Replace("|", "%7C").Replace(",", "%2C") .Replace("\r", "%0D") .Replace("\n", "%0A"); } private static string UnescapeSaveValue(string value) { if (string.IsNullOrEmpty(value) || value.IndexOf('%') < 0) { return value; } StringBuilder stringBuilder = new StringBuilder(value.Length); for (int i = 0; i < value.Length; i++) { if (value[i] == '%' && i + 2 < value.Length && IsHexDigit(value[i + 1]) && IsHexDigit(value[i + 2])) { string value2 = value.Substring(i + 1, 2); stringBuilder.Append((char)Convert.ToInt32(value2, 16)); i += 2; } else { stringBuilder.Append(value[i]); } } return stringBuilder.ToString(); } private static bool IsHexDigit(char c) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { if (c >= 'A') { return c <= 'F'; } return false; } return true; } private string GetSavePath() { if (Player.m_localPlayer == null) { DebugLogger.Verbose("Cannot get save path - local player is null"); return null; } string playerName = Player.m_localPlayer.GetPlayerName(); if (string.IsNullOrWhiteSpace(playerName)) { DebugLogger.Warning("Cannot get save path - player name is empty"); return null; } string text = Path.Combine(Paths.ConfigPath, "RecipePinner_Data"); if (!Directory.Exists(text)) { try { Directory.CreateDirectory(text); DebugLogger.Log("Created save directory: " + text); } catch (Exception ex) { DebugLogger.Error("Failed to create save directory: " + text, ex); return null; } } string text2 = playerName; char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { text2 = text2.Replace(oldChar, '_'); } string text3 = Path.Combine(text, text2 + ".txt"); DebugLogger.Verbose("Save path: " + text3); return text3; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.DebugLogger ---- namespace ValheimRecipePinner { public static class DebugLogger { private const string Prefix = "[RecipePinner]"; public static void Log(string message) { if (IsDebugEnabled()) { Debug.Log("[RecipePinner] " + message); } } public static void Warning(string message) { Debug.LogWarning("[RecipePinner] " + message); } public static void Error(string message) { Debug.LogError("[RecipePinner] " + message); } public static void Error(string message, Exception ex) { Debug.LogError("[RecipePinner] " + message + "\nException: " + ex.Message + "\nStackTrace: " + ex.StackTrace); } public static void Verbose(string message) { if (IsDebugEnabled()) { Debug.Log("[RecipePinner] [VERBOSE] " + message); } } private static bool IsDebugEnabled() { if (RecipePinnerPlugin.Instance != null && RecipePinnerPlugin.EnableDebugLogging != null) { return RecipePinnerPlugin.EnableDebugLogging.Value; } return false; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.LocalizationManager ---- namespace ValheimRecipePinner { public class LocalizationManager { private readonly RecipePinnerPlugin _plugin; private readonly Dictionary _localizedText = new Dictionary(); private static readonly Dictionary _defaultEnglish = new Dictionary { { "pinned", "Recipe Pinned!" }, { "unpinned", "Pin Removed" }, { "list_full", "List Full!" }, { "added_more", "Added More: {0}x" }, { "decreased", "Decreased: {0}x" }, { "cleared", "Pinned Recipes Cleared" }, { "clear_confirm_hotkey", "Press again to clear all pins" }, { "max_level", "Max Level Reached" }, { "no_upgrade_cost", "No upgrade cost found" }, { "gathering_title", "GATHERING LIST" }, { "gathering_opened", "Gathering List Opened" }, { "gathering_closed", "Gathering List Closed" }, { "gathering_empty", "No Recipes Pinned" }, { "gathering_hint", "Open/Close: {0}" }, { "mypins_title", "MY PINS" }, { "mypins_button", "Pins" }, { "mypins_empty", "No Recipes Pinned" }, { "group_button", "Group" }, { "group_confirm", "Confirm" }, { "group_cancel", "Cancel" }, { "group_name_prompt", "Enter group name:" }, { "group_created", "Group Created: {0}" }, { "group_disbanded", "Group Disbanded: {0}" }, { "group_select_hint", "Select pins to group" }, { "group_min_select", "Select at least 2 pins" }, { "group_need_more", "At least 2 pins needed to create a group" }, { "group_create_failed", "Group could not be created" }, { "group_name_exists", "Group '{0}' already exists" }, { "disband_button", "Disband" }, { "confirm_delete_group", "Delete group \"{0}\" and all member pins?" }, { "confirm_delete_pin", "Delete \"{0}\"?" }, { "confirm_remove_member", "Remove \"{0}\" from group \"{1}\"?" }, { "confirm_button", "Confirm" }, { "cancel_button", "Cancel" }, { "confirm_disband_group", "Disband group \"{0}\"? Members will become individual pins." }, { "clear_button", "Clear" }, { "clear_confirm_msg", "Remove all pins?" }, { "clear_no_pins", "No pins to clear" }, { "group_no_pins", "Not enough pins to group" }, { "close_button", "Close" }, { "controls_title", "CONTROLS" }, { "controls_config_note", "Controls can be changed in\nthe config file." }, { "controls_config_note_single", "Controls can be changed in the config file." }, { "howto_header", "HOW TO USE" }, { "howto_pin", "Hover over a recipe in the crafting menu and press [{0}] to pin it." }, { "howto_unpin", "Hold [{0}] and press [{1}] to unpin a recipe." }, { "howto_toggle_hud", "Press [{0}] to show or hide the pinned recipe overlay." }, { "howto_gathering", "Press [{0}] to open or close the gathering list." }, { "howto_next_page", "Press [{0}] to cycle through HUD pages." }, { "howto_clear_all", "Press [{0}] to remove all pinned recipes." }, { "keybindings_header", "KEY BINDINGS" }, { "ctrl_pin", "Pin Recipe" }, { "ctrl_unpin", "Unpin (hold + Pin Recipe key)" }, { "ctrl_toggle_hud", "Toggle HUD Visibility" }, { "ctrl_gathering", "Toggle Gathering List" }, { "ctrl_next_page", "Next HUD Page" }, { "ctrl_clear_all", "Clear All Pins" } }; public LocalizationManager(RecipePinnerPlugin plugin) { _plugin = plugin; DebugLogger.Log("LocalizationManager init"); } public void LoadTranslations() { _localizedText.Clear(); string text = RecipePinnerPlugin.LanguageOverride?.Value?.Trim(); if (string.IsNullOrEmpty(text) || text.ToLower() == "auto") { text = ((Localization.instance == null) ? "English" : Localization.instance.GetSelectedLanguage()); DebugLogger.Log("Auto-detected language: " + text); } else { DebugLogger.Log("Using forced language: " + text); } string text2 = text; char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { text2 = text2.Replace(oldChar, '_'); } string text3 = Path.Combine(Path.GetDirectoryName(_plugin.Info.Location), "RecipePinner_languages", text2 + ".json"); if (!File.Exists(text3)) { DebugLogger.Log("Language file not found: " + text3 + " - Using default English"); return; } try { string text4 = File.ReadAllText(text3); int num = 0; string[] array = text4.Split(new string[3] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text5 = array[i].Trim(); if (string.IsNullOrEmpty(text5) || text5 == "{" || text5 == "}" || !text5.Contains(":")) { continue; } string[] array2 = text5.Split(new char[1] { ':' }, 2); if (array2.Length == 2) { string text6 = array2[0].Trim(',', '"', ' ', '\t', '\r'); string text7 = array2[1].Trim(',', '"', ' ', '\t', '\r'); text7 = text7.Replace("\\\"", "\"").Replace("\\n", "\n").Replace("\\t", "\t") .Replace("\\\\", "\\"); if (!string.IsNullOrEmpty(text6) && !string.IsNullOrEmpty(text7)) { _localizedText[text6] = text7; num++; } } } DebugLogger.Log($"Loaded {num} translations from: {text}.json"); } catch (Exception ex) { DebugLogger.Error("Failed to load language file: " + text3, ex); } } public string GetText(string key) { if (_localizedText.TryGetValue(key, out var value)) { DebugLogger.Verbose("Translation found for '" + key + "': " + value); return value; } if (_defaultEnglish.TryGetValue(key, out var value2)) { DebugLogger.Verbose("Using default English for '" + key + "': " + value2); return value2; } DebugLogger.Warning("No translation found for key: " + key); return key; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.PinnedRecipeData ---- namespace ValheimRecipePinner { public class PinnedRecipeData { public Recipe RecipeRef; public string RawName; public string CachedHeader; public Sprite Icon; public int StackCount; public List Resources = new List(); public bool IsDirty = true; public bool IsGroup; public PinGroupData GroupRef; } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.PinnedResData ---- namespace ValheimRecipePinner { public class PinnedResData { public string ItemName; public string CachedName; public Sprite Icon; public int RequiredAmount; public int LastKnownAmount; public int LastKnownInvAmount; public string CachedAmountString; } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.PinGroupData ---- namespace ValheimRecipePinner { public class PinGroupData { public string GroupName; public List MemberRecipeKeys = new List(); public Dictionary MemberCounts = new Dictionary(); public List MemberPins = new List(); public List MergedResources = new List(); public List MemberIcons = new List(); public bool IsDirty = true; } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.RecipeManager ---- namespace ValheimRecipePinner { public class RecipeManager { public Dictionary PinnedRecipes = new Dictionary(); public List PinnedRecipeOrder = new List(); public List CachedPins = new List(); public Dictionary PinGroups = new Dictionary(); private readonly Dictionary _fakeRecipeCache = new Dictionary(); private static readonly Regex CleanNameRegex = new Regex("<.*?>", RegexOptions.Compiled); private static readonly Regex AmountSuffixRegex = new Regex("\\s*[xX]?\\s*\\d+$", RegexOptions.Compiled); private static readonly Regex UpgradeStarRegex = new Regex("\\s*★(\\d+)$", RegexOptions.Compiled); private static readonly Dictionary _cachedRecipeFields = new Dictionary(); private static readonly Dictionary _cachedRecipeProps = new Dictionary(); private static readonly Dictionary _cachedItemFields = new Dictionary(); private static readonly Dictionary _cachedItemProps = new Dictionary(); private static readonly Dictionary _cachedElementProps = new Dictionary(); private static readonly Dictionary _cachedElementFields = new Dictionary(); private static readonly HashSet _elementLookupFailed = new HashSet(); public void Cleanup() { DebugLogger.Log("RecipeManager cleanup"); int count = _fakeRecipeCache.Count; foreach (Recipe value in _fakeRecipeCache.Values) { if (value != null) { UnityEngine.Object.Destroy(value); } } _fakeRecipeCache.Clear(); DebugLogger.Log($"Cleaned {count} fake recipes"); PinGroups.Clear(); _cachedRecipeFields.Clear(); _cachedRecipeProps.Clear(); _cachedItemFields.Clear(); _cachedItemProps.Clear(); _cachedElementProps.Clear(); _cachedElementFields.Clear(); _elementLookupFailed.Clear(); } public void RefreshRecipeCache() { DebugLogger.Verbose("Refreshing cache"); CachedPins.Clear(); if (ObjectDB.instance == null) { DebugLogger.Warning("ObjectDB null, can't refresh"); return; } int num = 0; int num2 = 0; Dictionary dictionary = new Dictionary(); foreach (PinGroupData value9 in PinGroups.Values) { foreach (string memberRecipeKey in value9.MemberRecipeKeys) { int value; int num3 = ((!value9.MemberCounts.TryGetValue(memberRecipeKey, out value)) ? 1 : value); if (dictionary.ContainsKey(memberRecipeKey)) { dictionary[memberRecipeKey] += num3; } else { dictionary[memberRecipeKey] = num3; } } } Dictionary dictionary2 = new Dictionary(); int num4 = 0; foreach (KeyValuePair pinGroup in PinGroups) { PinGroupData value2 = pinGroup.Value; value2.MemberPins.Clear(); value2.MergedResources.Clear(); value2.MemberIcons.Clear(); value2.IsDirty = true; Dictionary dictionary3 = new Dictionary(); foreach (string memberRecipeKey2 in value2.MemberRecipeKeys) { int value3; int count = ((!value2.MemberCounts.TryGetValue(memberRecipeKey2, out value3)) ? 1 : value3); Recipe recipeByName = GetRecipeByName(memberRecipeKey2); if (recipeByName == null) { DebugLogger.Warning("Group '" + value2.GroupName + "' member not found: " + memberRecipeKey2); continue; } PinnedRecipeData pinnedRecipeData = BuildPinnedRecipeData(recipeByName, memberRecipeKey2, count); if (pinnedRecipeData == null) { continue; } value2.MemberPins.Add(pinnedRecipeData); if (pinnedRecipeData.Icon != null && value2.MemberIcons.Count < 4) { value2.MemberIcons.Add(pinnedRecipeData.Icon); } foreach (PinnedResData resource in pinnedRecipeData.Resources) { if (dictionary3.TryGetValue(resource.ItemName, out var value4)) { value4.RequiredAmount += resource.RequiredAmount; continue; } dictionary3[resource.ItemName] = new PinnedResData { ItemName = resource.ItemName, CachedName = resource.CachedName, Icon = resource.Icon, RequiredAmount = resource.RequiredAmount, LastKnownAmount = -1, LastKnownInvAmount = -1 }; } } foreach (PinnedResData value10 in dictionary3.Values) { value2.MergedResources.Add(value10); } PinnedRecipeData value5 = new PinnedRecipeData { IsDirty = true, RecipeRef = null, RawName = value2.GroupName, CachedHeader = value2.GroupName, Icon = ((value2.MemberIcons.Count > 0) ? value2.MemberIcons[0] : null), StackCount = 1, Resources = value2.MergedResources, IsGroup = true, GroupRef = value2 }; dictionary2[pinGroup.Key] = value5; num4++; DebugLogger.Verbose($"Group pin built: {value2.GroupName} ({value2.MemberPins.Count} members, {value2.MergedResources.Count} resources)"); } foreach (string item in GetDisplayPinOrder()) { if (item.StartsWith("GROUP:")) { string key = item.Substring(6); if (dictionary2.TryGetValue(key, out var value6)) { CachedPins.Add(value6); } } else { if (!PinnedRecipes.TryGetValue(item, out var value7)) { continue; } if (dictionary.TryGetValue(item, out var value8)) { int num5 = value7 - value8; if (num5 <= 0) { DebugLogger.Verbose($"Skipping grouped recipe (no excess): {item} (claims={value8})"); continue; } value7 = num5; DebugLogger.Verbose($"Grouped recipe excess for overlay: {item} x{num5} (claims={value8})"); } Recipe recipeByName2 = GetRecipeByName(item); if (recipeByName2 != null) { PinnedRecipeData pinnedRecipeData2 = BuildPinnedRecipeData(recipeByName2, item, value7); if (pinnedRecipeData2 != null) { CachedPins.Add(pinnedRecipeData2); num++; } else { num2++; } } else { DebugLogger.Warning("Recipe not found: " + item); num2++; } } } DebugLogger.Log($"Cache refreshed: {num} pins, {num4} groups, {num2} failed"); if (Player.m_localPlayer != null && RecipePinnerPlugin.Instance != null) { RecipePinnerPlugin.Instance.UIMgr.UpdateUI(RecipePinnerPlugin.IsUiVisible); RecipePinnerPlugin.Instance.UIMgr.RefreshMyPinsList(); } } public List GetDisplayPinOrder() { List list = new List(); List list2 = new List(); HashSet hashSet = new HashSet(); Dictionary dictionary = new Dictionary(); foreach (string item in PinnedRecipeOrder) { if (!item.StartsWith("GROUP:")) { continue; } string text = item.Substring(6); if (!PinGroups.TryGetValue(text, out var value)) { continue; } foreach (string memberRecipeKey in value.MemberRecipeKeys) { dictionary[memberRecipeKey] = text; } } foreach (string item2 in PinnedRecipeOrder) { if (item2.StartsWith("GROUP:")) { string text2 = item2.Substring(6); if (PinGroups.ContainsKey(text2)) { list.Add(item2); AppendDeferredExcessForGroup(list, list2, hashSet, dictionary, text2); } } else { if (!PinnedRecipes.TryGetValue(item2, out var value2)) { continue; } int groupClaimCount = GetGroupClaimCount(item2); if (groupClaimCount <= 0) { list.Add(item2); } else { if (value2 <= groupClaimCount) { continue; } if (dictionary.ContainsKey(item2)) { if (hashSet.Add(item2)) { list2.Add(item2); } } else { list.Add(item2); } } } } foreach (string item3 in list2) { list.Add(item3); } return list; } private static void AppendDeferredExcessForGroup(List displayOrder, List deferredExcess, HashSet deferredSet, Dictionary lastClaimingGroup, string groupName) { int num = 0; while (num < deferredExcess.Count) { string text = deferredExcess[num]; if (lastClaimingGroup.TryGetValue(text, out var value) && value == groupName) { displayOrder.Add(text); deferredSet.Remove(text); deferredExcess.RemoveAt(num); } else { num++; } } } public Recipe GetRecipeByName(string name) { if (ObjectDB.instance == null) { return null; } if (_fakeRecipeCache.TryGetValue(name, out var value)) { DebugLogger.Verbose("Found cached fake recipe: " + name); return value; } Match match = UpgradeStarRegex.Match(name); if (match.Success) { string name2 = name.Substring(0, match.Index).Trim(); if (!int.TryParse(match.Groups[1].Value, out var result)) { DebugLogger.Warning("Invalid upgrade level in recipe key: " + name); return null; } Recipe recipeByName = GetRecipeByName(name2); if (recipeByName != null) { if (!IsValidUpgradeTarget(recipeByName, result, name)) { return null; } Recipe recipe = CreateFakeUpgradeRecipe(recipeByName, result, name); if (recipe != null) { return recipe; } } } ItemDrop itemDrop = ObjectDB.instance.GetItemPrefab(name)?.GetComponent(); if (itemDrop != null) { Recipe recipe2 = ObjectDB.instance.GetRecipe(itemDrop.m_itemData); if (recipe2 != null) { DebugLogger.Verbose("Found standard recipe: " + name); return recipe2; } } Recipe recipe3 = null; foreach (Recipe recipe5 in ObjectDB.instance.m_recipes) { if (recipe5.name == name) { recipe3 = recipe5; break; } } if (recipe3 != null) { DebugLogger.Verbose("Found recipe in ObjectDB: " + name); return recipe3; } GameObject gameObject = ZNetScene.instance?.GetPrefab(name); if (gameObject != null) { Piece component = gameObject.GetComponent(); if (component != null && component.m_resources != null && component.m_resources.Length != 0) { Recipe recipe4 = ScriptableObject.CreateInstance(); recipe4.hideFlags = HideFlags.HideAndDontSave; recipe4.name = name; recipe4.m_item = gameObject.GetComponent(); recipe4.m_resources = (Piece.Requirement[])component.m_resources.Clone(); _fakeRecipeCache[name] = recipe4; DebugLogger.Verbose("Created fake recipe for piece: " + name); return recipe4; } } DebugLogger.Warning("Recipe not found anywhere: " + name); return null; } private Recipe CreateFakeUpgradeRecipe(Recipe baseRecipe, int targetLevel, string customName) { if (baseRecipe == null) { return null; } if (!IsValidUpgradeTarget(baseRecipe, targetLevel, customName)) { return null; } Recipe recipe = ScriptableObject.CreateInstance(); recipe.hideFlags = HideFlags.HideAndDontSave; recipe.name = customName; recipe.m_item = baseRecipe.m_item; recipe.m_amount = 1; int num = Mathf.Max(1, targetLevel - 1); List list = new List(); Piece.Requirement[] resources = baseRecipe.m_resources; foreach (Piece.Requirement requirement in resources) { if (requirement.m_amountPerLevel > 0) { Piece.Requirement item = new Piece.Requirement { m_resItem = requirement.m_resItem, m_amount = requirement.m_amountPerLevel * num, m_amountPerLevel = 0, m_recover = requirement.m_recover }; list.Add(item); } } if (list.Count == 0) { UnityEngine.Object.Destroy(recipe); return null; } recipe.m_resources = list.ToArray(); _fakeRecipeCache[customName] = recipe; DebugLogger.Verbose("Created fake upgrade recipe: " + customName); return recipe; } private bool IsValidUpgradeTarget(Recipe baseRecipe, int targetLevel, string customName) { if (targetLevel < 2) { DebugLogger.Warning("Invalid upgrade level for '" + customName + "': target level must be at least 2"); return false; } ItemDrop.ItemData.SharedData sharedData = (baseRecipe.m_item?.m_itemData)?.m_shared; if (sharedData == null) { DebugLogger.Warning("Cannot validate upgrade level for '" + customName + "' - item data is missing"); return false; } int maxQuality = sharedData.m_maxQuality; if (maxQuality < 2 || targetLevel > maxQuality) { DebugLogger.Warning($"Invalid upgrade level for '{customName}': target={targetLevel}, max={maxQuality}"); return false; } return true; } public void ValidateAndCleanPins() { if (ObjectDB.instance == null) { DebugLogger.Warning("Cannot validate pins - ObjectDB.instance is null"); return; } DebugLogger.Log("Validating pins"); List list = new List(); foreach (string key in PinnedRecipes.Keys) { if (GetRecipeByName(key) == null) { list.Add(key); } } if (list.Count > 0) { foreach (string item in list) { PinnedRecipes.Remove(item); PinnedRecipeOrder.Remove(item); DebugLogger.Warning("Removed invalid recipe: " + item); } DebugLogger.Log($"Removed {list.Count} invalid pins"); } else { DebugLogger.Log("All individual pins valid"); } int num = CleanInvalidGroupMembers(); if (list.Count > 0 || num > 0) { RecipePinnerPlugin.Instance?.DataMgr.SavePins(); } } private int CleanInvalidGroupMembers() { int num = 0; List list = new List(); foreach (KeyValuePair pinGroup in PinGroups) { string key = pinGroup.Key; PinGroupData value = pinGroup.Value; List list2 = new List(); foreach (string memberRecipeKey in value.MemberRecipeKeys) { if (GetRecipeByName(memberRecipeKey) == null) { value.MemberCounts.Remove(memberRecipeKey); num++; DebugLogger.Warning("Removed invalid group member: " + memberRecipeKey + " from group '" + key + "'"); } else { list2.Add(memberRecipeKey); } } if (list2.Count != value.MemberRecipeKeys.Count) { value.MemberRecipeKeys.Clear(); value.MemberRecipeKeys.AddRange(list2); } List list3 = new List(); foreach (string key2 in value.MemberCounts.Keys) { if (!value.MemberRecipeKeys.Contains(key2)) { list3.Add(key2); } } foreach (string item in list3) { value.MemberCounts.Remove(item); } if (value.MemberRecipeKeys.Count < 2) { list.Add(key); } } foreach (string item2 in list) { PinGroups.Remove(item2); PinnedRecipeOrder.Remove("GROUP:" + item2); DebugLogger.Warning("Removed group '" + item2 + "' because it has less than 2 valid members"); } if (num > 0 || list.Count > 0) { DebugLogger.Log($"Removed {num} invalid group member(s) and {list.Count} invalid group(s)"); } return num + list.Count; } public void TryPinHoveredRecipe(InventoryGui gui) { Transform recipeListRoot = ReflectionHelper.GetRecipeListRoot(gui); if (!(ReflectionHelper.GetAvailableRecipes(gui) is IList list) || recipeListRoot == null) { DebugLogger.Verbose("Cannot pin - listRoot or availableRecipes is null"); return; } ScrollRect componentInParent = recipeListRoot.GetComponentInParent(); bool flag = !gui.m_tabUpgrade.interactable; foreach (Transform item in recipeListRoot) { if (!item.gameObject.activeInHierarchy) { continue; } RectTransform rectTransform = item as RectTransform; if (rectTransform == null || !IsVisibleInScroll(rectTransform, componentInParent) || !InputHelper.IsMouseOverRect(rectTransform, logHit: false)) { continue; } string text = ExtractTextFromUI(item); if (string.IsNullOrEmpty(text)) { continue; } string text2 = CleanNameRegex.Replace(text, string.Empty).Trim(); text2 = text2.Replace("\r", "").Replace("\n", ""); string text3 = AmountSuffixRegex.Replace(text2, "").Trim(); int num = -1; for (int i = 0; i < list.Count; i++) { GameObject interfaceElementFromObject = GetInterfaceElementFromObject(list[i]); if (!(interfaceElementFromObject == null) && (interfaceElementFromObject == item.gameObject || interfaceElementFromObject.transform.IsChildOf(item))) { num = i; break; } } int num2 = -1; foreach (object item2 in list) { num2++; if (num >= 0 && num2 != num) { continue; } Recipe recipeFromObject = GetRecipeFromObject(item2); if (!(recipeFromObject != null)) { continue; } bool flag2 = num >= 0; if (!flag2) { string rawRecipeName = GetRawRecipeName(recipeFromObject); if (string.IsNullOrEmpty(rawRecipeName)) { continue; } string text4 = rawRecipeName; if (Localization.instance != null) { text4 = Localization.instance.Localize(rawRecipeName); } text4 = text4.Replace("\r", "").Replace("\n", ""); flag2 = text4.Equals(text3, StringComparison.OrdinalIgnoreCase) || text4.Equals(text2, StringComparison.OrdinalIgnoreCase); } if (!flag2) { continue; } if (flag) { ItemDrop.ItemData itemData = GetItemDataFromObject(item2) ?? ReflectionHelper.GetCraftUpgradeItem(gui); if (itemData != null) { int quality = itemData.m_quality; int num3 = quality + 1; int maxQuality = itemData.m_shared.m_maxQuality; if (quality >= maxQuality) { string text5 = RecipePinnerPlugin.Instance.LocalizationMgr.GetText("max_level"); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, text5); return; } string name = recipeFromObject.m_item.name; string text6 = $"{name} ★{num3}"; if (!IsUnpinHotkeyHeld() || PinnedRecipes.ContainsKey(text6)) { DebugLogger.Verbose("Attempting to pin hovered recipe..."); DebugLogger.Verbose($"Hovered: '{text3}' (UpgradeTab: {flag})"); DebugLogger.Log("Attempting to pin upgrade: " + text6 + " (Base: " + name + ")"); if (GetRecipeByName(text6) != null) { TogglePin(text6); return; } string text7 = RecipePinnerPlugin.Instance.LocalizationMgr.GetText("no_upgrade_cost"); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, text7); } } else { DebugLogger.Warning("Matched name but could not get ItemData for upgrade."); } } else if (!IsUnpinHotkeyHeld() || PinnedRecipes.ContainsKey(recipeFromObject.name)) { DebugLogger.Verbose("Attempting to pin hovered recipe..."); DebugLogger.Verbose($"Hovered: '{text3}' (UpgradeTab: {flag})"); DebugLogger.Log("Matched recipe: " + recipeFromObject.name); TogglePin(recipeFromObject.name); } return; } } } public void TryPinHoveredPiece() { if (!(Hud.instance == null)) { Piece hoveredPiece = ReflectionHelper.GetHoveredPiece(Hud.instance); if (hoveredPiece != null && hoveredPiece.m_resources != null && hoveredPiece.m_resources.Length != 0 && (!IsUnpinHotkeyHeld() || PinnedRecipes.ContainsKey(hoveredPiece.name))) { DebugLogger.Verbose("Attempting to pin hovered piece..."); DebugLogger.Log("Pinning piece: " + hoveredPiece.name); TogglePin(hoveredPiece.name); } } } private bool IsUnpinHotkeyHeld() { KeyCode keyCode = RecipePinnerPlugin.HotkeyUnpin?.Value ?? KeyCode.LeftShift; if (keyCode != KeyCode.None) { return Input.GetKey(keyCode); } return false; } private void TogglePin(string recipeName) { bool flag = IsUnpinHotkeyHeld(); LocalizationManager localizationMgr = RecipePinnerPlugin.Instance.LocalizationMgr; if (PinnedRecipes.TryGetValue(recipeName, out var value)) { if (flag) { int groupClaimCount = GetGroupClaimCount(recipeName); int num = groupClaimCount; value--; if (value < num) { if (groupClaimCount > 0) { value = groupClaimCount; PinnedRecipes[recipeName] = value; string groupContainingRecipe = GetGroupContainingRecipe(recipeName); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, "Cannot remove: in group \"" + groupContainingRecipe + "\""); DebugLogger.Log($"Hotkey unpin blocked: {recipeName} min={groupClaimCount}"); } else { PinnedRecipes.Remove(recipeName); PinnedRecipeOrder.Remove(recipeName); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("unpinned")); DebugLogger.Log("Unpinned: " + recipeName); } } else if (value == 0) { PinnedRecipes.Remove(recipeName); PinnedRecipeOrder.Remove(recipeName); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("unpinned")); DebugLogger.Log("Unpinned: " + recipeName); } else { PinnedRecipes[recipeName] = value; int num2 = value - groupClaimCount; if (num2 > 0) { string msg = string.Format(localizationMgr.GetText("decreased"), num2); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg); } else { Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("unpinned")); } DebugLogger.Log($"Decreased pin count: {recipeName} = {value}"); } } else { value++; PinnedRecipes[recipeName] = value; int groupClaimCount2 = GetGroupClaimCount(recipeName); if (groupClaimCount2 > 0) { int num3 = value - groupClaimCount2; if (num3 == 1) { if (!PinnedRecipeOrder.Contains(recipeName)) { PinnedRecipeOrder.Add(recipeName); } Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("pinned")); } else { string msg2 = string.Format(localizationMgr.GetText("added_more"), num3); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg2); } } else { string msg3 = string.Format(localizationMgr.GetText("added_more"), value); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg3); } DebugLogger.Log($"Increased pin count: {recipeName} = {value}"); } } else { if (flag) { return; } if (GetEffectivePinCount() < RecipePinnerPlugin.MaximumPins.Value) { PinnedRecipes.Add(recipeName, 1); if (!PinnedRecipeOrder.Contains(recipeName)) { PinnedRecipeOrder.Add(recipeName); } Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("pinned")); DebugLogger.Log("Pinned new recipe: " + recipeName); } else { Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("list_full")); DebugLogger.Warning($"Cannot pin {recipeName} - max pins reached ({RecipePinnerPlugin.MaximumPins.Value})"); } } RefreshRecipeCache(); RecipePinnerPlugin.Instance?.DataMgr.SavePins(); } private Recipe GetRecipeFromObject(object data) { if (data == null) { return null; } if (data is Recipe result) { return result; } Type type = data.GetType(); if (_cachedRecipeFields.TryGetValue(type, out var value)) { return value.GetValue(data) as Recipe; } if (_cachedRecipeProps.TryGetValue(type, out var value2)) { return value2.GetValue(data, null) as Recipe; } PropertyInfo property = type.GetProperty("Key"); if (property != null && property.GetValue(data, null) is Recipe result2) { _cachedRecipeProps[type] = property; return result2; } FieldInfo field = type.GetField("m_recipe", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && field.GetValue(data) is Recipe result3) { _cachedRecipeFields[type] = field; return result3; } FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(Recipe)) { _cachedRecipeFields[type] = fieldInfo; return fieldInfo.GetValue(data) as Recipe; } } PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.PropertyType == typeof(Recipe) && propertyInfo.CanRead) { _cachedRecipeProps[type] = propertyInfo; return propertyInfo.GetValue(data, null) as Recipe; } } return null; } private ItemDrop.ItemData GetItemDataFromObject(object data) { if (data == null) { return null; } Type type = data.GetType(); if (_cachedItemFields.TryGetValue(type, out var value)) { return value.GetValue(data) as ItemDrop.ItemData; } if (_cachedItemProps.TryGetValue(type, out var value2)) { return value2.GetValue(data, null) as ItemDrop.ItemData; } PropertyInfo property = type.GetProperty("Value"); if (property != null && property.GetValue(data, null) is ItemDrop.ItemData result) { _cachedItemProps[type] = property; return result; } PropertyInfo property2 = type.GetProperty("Item2"); if (property2 != null && property2.GetValue(data, null) is ItemDrop.ItemData result2) { _cachedItemProps[type] = property2; return result2; } FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(ItemDrop.ItemData)) { _cachedItemFields[type] = fieldInfo; return fieldInfo.GetValue(data) as ItemDrop.ItemData; } } PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.PropertyType == typeof(ItemDrop.ItemData) && propertyInfo.CanRead) { _cachedItemProps[type] = propertyInfo; return propertyInfo.GetValue(data, null) as ItemDrop.ItemData; } } return null; } private GameObject GetInterfaceElementFromObject(object data) { if (data == null) { return null; } Type type = data.GetType(); if (_elementLookupFailed.Contains(type)) { return null; } if (_cachedElementProps.TryGetValue(type, out var value)) { return value.GetValue(data, null) as GameObject; } if (_cachedElementFields.TryGetValue(type, out var value2)) { return value2.GetValue(data) as GameObject; } PropertyInfo property = type.GetProperty("InterfaceElement", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.PropertyType == typeof(GameObject) && property.CanRead) { _cachedElementProps[type] = property; return property.GetValue(data, null) as GameObject; } PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.PropertyType == typeof(GameObject) && propertyInfo.CanRead) { _cachedElementProps[type] = propertyInfo; return propertyInfo.GetValue(data, null) as GameObject; } } FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(GameObject)) { _cachedElementFields[type] = fieldInfo; return fieldInfo.GetValue(data) as GameObject; } } _elementLookupFailed.Add(type); DebugLogger.Warning("GetInterfaceElementFromObject: no GameObject member on '" + type.Name + "' - falling back to name matching"); return null; } private string ExtractTextFromUI(Transform child) { Text componentInChildren = child.GetComponentInChildren(); if (componentInChildren != null) { return componentInChildren.text; } Component[] componentsInChildren = child.GetComponentsInChildren(includeInactive: true); foreach (Component component in componentsInChildren) { if (!component.GetType().Name.Contains("TextMeshPro") && !component.GetType().Name.Contains("TMP_Text")) { continue; } PropertyInfo property = component.GetType().GetProperty("text"); if (property != null) { string text = property.GetValue(component, null) as string; if (!string.IsNullOrEmpty(text)) { return text; } } } return null; } private string GetRawRecipeName(Recipe r) { if (r.m_item != null && r.m_item.m_itemData != null) { return r.m_item.m_itemData.m_shared.m_name; } GameObject gameObject = ZNetScene.instance?.GetPrefab(r.name); if (gameObject != null) { ItemDrop component = gameObject.GetComponent(); if (component != null) { return component.m_itemData.m_shared.m_name; } Piece component2 = gameObject.GetComponent(); if (component2 != null) { return component2.m_name; } } return null; } private bool IsVisibleInScroll(RectTransform item, ScrollRect scrollRect) { if (item == null || !item.gameObject.activeInHierarchy) { return false; } if (scrollRect == null || scrollRect.viewport == null) { return true; } Vector3[] array = new Vector3[4]; scrollRect.viewport.GetWorldCorners(array); Rect rect = new Rect(array[0].x, array[0].y, array[2].x - array[0].x, array[2].y - array[0].y); Vector3[] array2 = new Vector3[4]; item.GetWorldCorners(array2); Vector3 point = (array2[0] + array2[2]) / 2f; return rect.Contains(point); } private PinnedRecipeData BuildPinnedRecipeData(Recipe r, string recipeName, int count) { if (r == null) { return null; } PinnedRecipeData pinnedRecipeData = new PinnedRecipeData { IsDirty = true, RecipeRef = r, StackCount = count }; if (r.m_item != null && r.m_item.m_itemData != null) { pinnedRecipeData.Icon = r.m_item.m_itemData.GetIcon(); pinnedRecipeData.RawName = r.m_item.m_itemData.m_shared?.m_name; } else if (r.m_item != null) { DebugLogger.Warning("BuildPinnedRecipeData: recipe '" + recipeName + "' has item without itemData, using fallback name"); } else { GameObject gameObject = ZNetScene.instance?.GetPrefab(r.name); if (gameObject != null) { Piece component = gameObject.GetComponent(); if (component != null) { pinnedRecipeData.Icon = component.m_icon; pinnedRecipeData.RawName = component.m_name; } } } if (string.IsNullOrEmpty(pinnedRecipeData.RawName)) { pinnedRecipeData.RawName = r.name; } string text = pinnedRecipeData.RawName; if (Localization.instance != null) { Match match = UpgradeStarRegex.Match(recipeName); text = ((!match.Success) ? Localization.instance.Localize(pinnedRecipeData.RawName) : (Localization.instance.Localize(pinnedRecipeData.RawName) + match.Value)); } text = text.Replace("\r", "").Replace("\n", ""); if (r.m_amount > 1) { text += $" (x{r.m_amount})"; } if (count > 1) { text = $"{count}x {text}"; } pinnedRecipeData.CachedHeader = text; if (r.m_resources == null) { DebugLogger.Warning("BuildPinnedRecipeData: recipe '" + recipeName + "' has null resources, skipping"); return null; } Piece.Requirement[] resources = r.m_resources; foreach (Piece.Requirement requirement in resources) { if (requirement == null || requirement.m_amount <= 0) { continue; } if (requirement.m_resItem == null || requirement.m_resItem.m_itemData == null) { DebugLogger.Warning("BuildPinnedRecipeData: skipping invalid resource in '" + recipeName + "'"); continue; } PinnedResData pinnedResData = new PinnedResData { ItemName = requirement.m_resItem.m_itemData.m_shared?.m_name, Icon = requirement.m_resItem.m_itemData.GetIcon(), RequiredAmount = requirement.m_amount * count, LastKnownAmount = -1, LastKnownInvAmount = -1 }; if (string.IsNullOrEmpty(pinnedResData.ItemName)) { DebugLogger.Warning("BuildPinnedRecipeData: skipping resource with empty item name in '" + recipeName + "'"); continue; } string text2 = pinnedResData.ItemName; if (Localization.instance != null) { text2 = Localization.instance.Localize(pinnedResData.ItemName); } text2 = text2.Replace("\r", "").Replace("\n", ""); pinnedResData.CachedName = text2; pinnedRecipeData.Resources.Add(pinnedResData); } return pinnedRecipeData; } public bool CreateGroup(string groupName, List selectedKeys) { if (string.IsNullOrWhiteSpace(groupName)) { DebugLogger.Warning("CreateGroup: group name is empty"); return false; } if (selectedKeys == null || selectedKeys.Count < 2) { DebugLogger.Warning($"CreateGroup: need at least 2 pins, got {selectedKeys?.Count ?? 0}"); return false; } if (PinGroups.ContainsKey(groupName)) { DebugLogger.Warning("CreateGroup: group '" + groupName + "' already exists"); return false; } PinGroupData pinGroupData = new PinGroupData { GroupName = groupName }; foreach (string selectedKey in selectedKeys) { if (PinnedRecipes.TryGetValue(selectedKey, out var value)) { int groupClaimCount = GetGroupClaimCount(selectedKey); int num = value - groupClaimCount; if (num <= 0) { DebugLogger.Warning($"CreateGroup: recipe key '{selectedKey}' has no ungrouped excess to claim (total={value}, claims={groupClaimCount}), skipping"); continue; } pinGroupData.MemberRecipeKeys.Add(selectedKey); pinGroupData.MemberCounts[selectedKey] = num; DebugLogger.Verbose($"CreateGroup: added member '{selectedKey}' to group '{groupName}' (claim={num}, total={value}, previousClaims={groupClaimCount})"); } else { DebugLogger.Warning("CreateGroup: recipe key '" + selectedKey + "' not found in PinnedRecipes, skipping"); } } if (pinGroupData.MemberRecipeKeys.Count < 2) { DebugLogger.Warning($"CreateGroup: only {pinGroupData.MemberRecipeKeys.Count} valid members, need at least 2"); return false; } PinGroups[groupName] = pinGroupData; string item = "GROUP:" + groupName; PinnedRecipeOrder.Add(item); DebugLogger.Log($"Group created: '{groupName}' with {pinGroupData.MemberRecipeKeys.Count} members (added to end)"); RefreshRecipeCache(); RecipePinnerPlugin.Instance?.DataMgr.SavePins(); return true; } public bool DisbandGroup(string groupName) { if (!PinGroups.TryGetValue(groupName, out var value)) { DebugLogger.Warning("DisbandGroup: group '" + groupName + "' not found"); return false; } PinGroups.Remove(groupName); PinnedRecipeOrder.Remove("GROUP:" + groupName); DebugLogger.Log($"Group disbanded: '{groupName}' ({value.MemberRecipeKeys.Count} members restored)"); RefreshRecipeCache(); RecipePinnerPlugin.Instance?.DataMgr.SavePins(); return true; } public void DecrementGroupMemberCounts(string recipeKey) { string text = null; foreach (string item in PinnedRecipeOrder) { if (item.StartsWith("GROUP:")) { string text2 = item.Substring(6); if (PinGroups.TryGetValue(text2, out var value) && value.MemberCounts.ContainsKey(recipeKey)) { text = text2; } } } if (text == null) { return; } string text3 = text; if (!PinGroups.TryGetValue(text3, out var value2)) { return; } int num = value2.MemberCounts[recipeKey]; int num2 = num - 1; if (num2 <= 0) { value2.MemberCounts.Remove(recipeKey); value2.MemberRecipeKeys.Remove(recipeKey); DebugLogger.Log($"AutoUnpin: '{recipeKey}' claim reached 0, removed from group '{text3}' ({value2.MemberRecipeKeys.Count} remaining)"); if (value2.MemberRecipeKeys.Count < 2) { DebugLogger.Log($"AutoUnpin: Group '{text3}' auto-disbanded ({value2.MemberRecipeKeys.Count} members)"); PinGroups.Remove(text3); PinnedRecipeOrder.Remove("GROUP:" + text3); } } else { value2.MemberCounts[recipeKey] = num2; DebugLogger.Log($"AutoUnpin: '{recipeKey}' claim decremented in group '{text3}': {num}->{num2}"); } } public void RemoveMemberFromGroup(string groupName, string recipeKey) { if (!PinGroups.TryGetValue(groupName, out var value)) { DebugLogger.Warning("RemoveMemberFromGroup: group '" + groupName + "' not found"); return; } if (!value.MemberRecipeKeys.Remove(recipeKey)) { DebugLogger.Warning("RemoveMemberFromGroup: '" + recipeKey + "' not in group '" + groupName + "'"); return; } int value2; int num = ((!value.MemberCounts.TryGetValue(recipeKey, out value2)) ? 1 : value2); value.MemberCounts.Remove(recipeKey); DebugLogger.Log($"Removed '{recipeKey}' from group '{groupName}' (claim={num}, {value.MemberRecipeKeys.Count} remaining)"); if (PinnedRecipes.TryGetValue(recipeKey, out var value3)) { int num2 = value3 - num; if (num2 <= 0) { PinnedRecipes.Remove(recipeKey); PinnedRecipeOrder.Remove(recipeKey); } else { PinnedRecipes[recipeKey] = num2; } } if (value.MemberRecipeKeys.Count < 2) { DebugLogger.Log($"Group '{groupName}' has {value.MemberRecipeKeys.Count} member(s), auto-disbanding"); PinGroups.Remove(groupName); PinnedRecipeOrder.Remove("GROUP:" + groupName); } RefreshRecipeCache(); RecipePinnerPlugin.Instance?.DataMgr.SavePins(); if (GetEffectivePinCount() < 2) { RecipePinnerPlugin.Instance?.UIMgr.CloseGatheringList(); } } public void RemovePinFromMyPinsPanel(string key) { if (PinGroups.TryGetValue(key, out var value)) { foreach (string memberRecipeKey in value.MemberRecipeKeys) { if (PinnedRecipes.TryGetValue(memberRecipeKey, out var value2)) { int value3; int num = ((!value.MemberCounts.TryGetValue(memberRecipeKey, out value3)) ? 1 : value3); int num2 = value2 - num; if (num2 <= 0) { PinnedRecipes.Remove(memberRecipeKey); PinnedRecipeOrder.Remove(memberRecipeKey); DebugLogger.Verbose("Removed group member pin entirely: " + memberRecipeKey); } else { PinnedRecipes[memberRecipeKey] = num2; DebugLogger.Verbose($"Group member pin kept as individual: {memberRecipeKey} x{num2}"); } } } PinGroups.Remove(key); PinnedRecipeOrder.Remove("GROUP:" + key); DebugLogger.Log("Removed group: " + key + " (member excess pins preserved)"); } else { int groupClaimCount = GetGroupClaimCount(key); if (groupClaimCount > 0) { if (PinnedRecipes.TryGetValue(key, out var value4) && value4 > groupClaimCount) { PinnedRecipes[key] = groupClaimCount; DebugLogger.Log($"Removed individual excess for grouped pin: {key} (kept {groupClaimCount} for groups)"); } else { DebugLogger.Log($"No individual excess to remove for: {key} (claims={groupClaimCount})"); } } else { PinnedRecipes.Remove(key); PinnedRecipeOrder.Remove(key); DebugLogger.Log("Removed pin: " + key); } } RefreshRecipeCache(); RecipePinnerPlugin.Instance?.DataMgr.SavePins(); if (GetEffectivePinCount() < 2) { RecipePinnerPlugin.Instance?.UIMgr.CloseGatheringList(); } } public void AdjustPinCount(string key, int delta, bool showMessage = true) { if (!PinnedRecipes.TryGetValue(key, out var value)) { DebugLogger.Warning("AdjustPinCount: recipe '" + key + "' not found"); return; } int groupClaimCount = GetGroupClaimCount(key); int num = ((groupClaimCount <= 0) ? 1 : (groupClaimCount + 1)); value += delta; DebugLogger.Log($"AdjustPinCount: {key} -> {value} (min={num}, claims={groupClaimCount})"); if (value < num) { value = num; DebugLogger.Log($"AdjustPinCount: clamped to minimum {num} for '{key}'"); return; } PinnedRecipes[key] = value; if (showMessage) { LocalizationManager localizationManager = RecipePinnerPlugin.Instance?.LocalizationMgr; if (localizationManager != null) { int num2 = value - groupClaimCount; if (delta > 0) { string msg = string.Format(localizationManager.GetText("added_more"), num2); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg); } else { string msg2 = string.Format(localizationManager.GetText("decreased"), num2); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg2); } } } RefreshRecipeCache(); RecipePinnerPlugin.Instance?.DataMgr.SavePins(); } public string GetGroupContainingRecipe(string recipeKey) { foreach (KeyValuePair pinGroup in PinGroups) { if (pinGroup.Value.MemberRecipeKeys.Contains(recipeKey)) { return pinGroup.Key; } } return null; } public int GetGroupClaimCount(string recipeKey) { int num = 0; foreach (PinGroupData value2 in PinGroups.Values) { if (value2.MemberCounts.TryGetValue(recipeKey, out var value)) { num += value; } else if (value2.MemberRecipeKeys.Contains(recipeKey)) { num++; } } return num; } public int TrimToMaximumPins(int maxEffectivePins) { int num = 0; while (GetEffectivePinCount() > maxEffectivePins) { bool flag = false; for (int num2 = PinnedRecipeOrder.Count - 1; num2 >= 0; num2--) { string text = PinnedRecipeOrder[num2]; if (text.StartsWith("GROUP:")) { string key = text.Substring(6); PinnedRecipeOrder.RemoveAt(num2); if (PinGroups.TryGetValue(key, out var value)) { foreach (string memberRecipeKey in value.MemberRecipeKeys) { if (PinnedRecipes.TryGetValue(memberRecipeKey, out var value2)) { int value3; int num3 = ((!value.MemberCounts.TryGetValue(memberRecipeKey, out value3)) ? 1 : value3); int num4 = value2 - num3; if (num4 > 0) { PinnedRecipes[memberRecipeKey] = num4; continue; } PinnedRecipes.Remove(memberRecipeKey); PinnedRecipeOrder.Remove(memberRecipeKey); } } PinGroups.Remove(key); } num++; flag = true; break; } if (PinnedRecipes.TryGetValue(text, out var value4)) { int groupClaimCount = GetGroupClaimCount(text); if (value4 > groupClaimCount) { if (groupClaimCount > 0) { PinnedRecipes[text] = groupClaimCount; } else { PinnedRecipes.Remove(text); PinnedRecipeOrder.RemoveAt(num2); } num++; flag = true; break; } } } if (!flag) { break; } } return num; } public int GetEffectivePinCount() { int num = PinGroups.Count; foreach (KeyValuePair pinnedRecipe in PinnedRecipes) { int groupClaimCount = GetGroupClaimCount(pinnedRecipe.Key); if (pinnedRecipe.Value > groupClaimCount) { num++; } } return num; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.RecipePinnerPlugin ---- namespace ValheimRecipePinner { [BepInPlugin("com.Kadrio.RecipePinner", "Recipe Pinner", "1.3.0")] public class RecipePinnerPlugin : BaseUnityPlugin { public enum PinLayoutMode { AutoDetect, ForceVertical, ForceHorizontal, ForceBottomRightHorizontal } public class ConfigurationManagerAttributes { public bool? ShowRangeAsPercent; public Action CustomDrawer; public bool? Browsable; public string Category; public object DefaultValue; public bool? HideDefaultButton; public bool? HideSettingName; public string Description; public string DispName; public int? Order; public bool? ReadOnly; public bool? IsAdvanced; public Func ObjToStr; public Func StrToObj; } public static RecipePinnerPlugin Instance; public LocalizationManager LocalizationMgr; public RecipeManager RecipeMgr; public ContainerScanner ContainerMgr; public UIManager UIMgr; public DataPersistence DataMgr; internal bool _mluiMapListEnabled; internal bool _mluiNoMapListEnabled; internal bool _mluiInstalled; private bool _startupInitialized; private string _lastLanguage = ""; private string _currentSessionPlayer; private const float ClearAllConfirmWindow = 2f; private float _clearAllArmedUntil; private static bool _isUiVisible = true; public static ConfigEntry EnableMod; public static ConfigEntry LanguageOverride; public static ConfigEntry LayoutModeConfig; public static ConfigEntry MaximumPins; public static ConfigEntry PinsPerPage; public static ConfigEntry AutoUnpinAfterCrafting; public static ConfigEntry AutoUnpinAfterBuilding; public static ConfigEntry HotkeyPin; public static ConfigEntry HotkeyUnpin; public static ConfigEntry HotkeyClearAll; public static ConfigEntry HotkeyToggleVisibility; public static ConfigEntry HotkeyPageSwitch; public static ConfigEntry HotkeyGatheringList; public static ConfigEntry EnableChestScanning; public static ConfigEntry ChestScanRange; public static ConfigEntry ChestScanInterval; public static ConfigEntry UIScale; public static ConfigEntry BackgroundOpacity; public static ConfigEntry FontSizeRecipeName; public static ConfigEntry FontSizeMaterials; public static ConfigEntry HudRecipeIconSize; public static ConfigEntry HudMaterialIconSize; public static ConfigEntry HudGroupIconSize; public static ConfigEntry EnableCraftReadiness; public static ConfigEntry ColorHeader; public static ConfigEntry ColorEnoughInInventory; public static ConfigEntry ColorEnoughWithChests; public static ConfigEntry ColorMissing; public static ConfigEntry ColorCraftReady; public static ConfigEntry ColorCraftNotReady; public static ConfigEntry ColorPaginationActive; public static ConfigEntry PaginationInactiveOpacity; public static ConfigEntry PaginationDotSize; public static ConfigEntry PaginationDotSpacing; public static ConfigEntry EnableGatheringList; public static ConfigEntry AutoOpenGatheringList; public static ConfigEntry GatheringListColumns; public static ConfigEntry GatheringListFontSizeTitle; public static ConfigEntry GatheringListFontSizeMaterials; public static ConfigEntry ContainerGatheringListPosition; public static ConfigEntry GroupCompactThreshold; public static ConfigEntry GroupCompactMaxRows; public static ConfigEntry GroupIconFontSize; public static ConfigEntry MyPinsPanelWidth; public static ConfigEntry MyPinsPanelHeight; public static ConfigEntry MyPinsPanelPosition; public static ConfigEntry ButtonTextColor; public static ConfigEntry MyPinsButtonPosition; public static ConfigEntry MyPinsButtonSize; public static ConfigEntry VerticalListWidth; public static ConfigEntry VerticalPinSpacing; public static ConfigEntry VerticalPosition; public static ConfigEntry HorizontalColumnWidth; public static ConfigEntry HorizontalPinSpacing; public static ConfigEntry HorizontalPosition; public static ConfigEntry BottomRightColumnWidth; public static ConfigEntry BottomRightPinSpacing; public static ConfigEntry BottomRightPosition; public static ConfigEntry EnableDebugLogging; public static bool IsUiVisible => _isUiVisible; internal bool IsPinDataLoaded => _startupInitialized; public bool IsHorizontalMode { get { if (LayoutModeConfig.Value == PinLayoutMode.ForceBottomRightHorizontal) { return true; } if (LayoutModeConfig.Value == PinLayoutMode.ForceHorizontal) { return true; } if (LayoutModeConfig.Value == PinLayoutMode.ForceVertical) { return false; } if (!_mluiInstalled) { return false; } if (Game.m_noMap) { return _mluiNoMapListEnabled; } return _mluiMapListEnabled; } } private void Awake() { Instance = this; BindConfigs(); DebugLogger.Log("Plugin init"); LocalizationMgr = new LocalizationManager(this); RecipeMgr = new RecipeManager(); ContainerMgr = new ContainerScanner(); UIMgr = new UIManager(); DataMgr = new DataPersistence(); DebugLogger.Log("Managers ready"); Harmony harmony = new Harmony("com.Kadrio.RecipePinner"); harmony.PatchAll(typeof(RecipePinnerPlugin)); harmony.PatchAll(typeof(ContainerScanner)); DebugLogger.Log("Patches applied"); } private void Start() { DebugLogger.Log("Start()"); LocalizationMgr.LoadTranslations(); ReadMyLittleUIConfig(); ContainerMgr.InitializeContainers(); DebugLogger.Log("Start done"); } private void OnDestroy() { DebugLogger.Log("OnDestroy"); Player localPlayer = Player.m_localPlayer; if (localPlayer != null && !string.IsNullOrEmpty(localPlayer.GetPlayerName())) { if (EnableMod == null || !EnableMod.Value) { DebugLogger.Verbose("OnDestroy save skipped - mod is disabled"); } else if (!_startupInitialized) { DebugLogger.Verbose("OnDestroy save skipped - pin data not loaded yet"); } else { DataMgr.SavePins(); } } RecipeMgr.Cleanup(); } private void OnApplicationFocus(bool hasFocus) { if (hasFocus) { UIMgr?.ResetMyPinsInputState("application focus restored"); } } private void Update() { if (!EnableMod.Value) { return; } ReflectionHelper.UpdateGuiScale(); if (!_startupInitialized && Player.m_localPlayer != null && ObjectDB.instance != null && ObjectDB.instance.m_recipes.Count > 0) { DebugLogger.Log("First init"); _lastLanguage = Localization.instance.GetSelectedLanguage(); DataMgr.LoadPins(); RecipeMgr.ValidateAndCleanPins(); RecipeMgr.RefreshRecipeCache(); _startupInitialized = true; DebugLogger.Log($"Init done - {RecipeMgr.PinnedRecipes.Count} pins loaded"); } if (EnableChestScanning.Value && Player.m_localPlayer != null && RecipeMgr.CachedPins.Count > 0) { ContainerMgr.UpdateScanning(); } bool flag = (Input.GetKeyDown(HotkeyToggleVisibility.Value) || Input.GetKeyDown(HotkeyPin.Value) || Input.GetKeyDown(HotkeyClearAll.Value) || Input.GetKeyDown(HotkeyPageSwitch.Value) || Input.GetKeyDown(HotkeyGatheringList.Value)) && AreRecipePinnerHotkeysBlocked(); if (Input.GetKeyDown(HotkeyToggleVisibility.Value) && !flag) { _isUiVisible = !_isUiVisible; DebugLogger.Log($"UI visibility toggled: {_isUiVisible}"); } if (Player.m_localPlayer != null) { UpdatePlayerSession(); } bool flag2 = Player.m_localPlayer != null && Player.m_localPlayer.InPlaceMode(); if (Input.GetKeyDown(HotkeyPin.Value) && !flag) { if (InventoryGui.instance != null && InventoryGui.IsVisible()) { RecipeMgr.TryPinHoveredRecipe(InventoryGui.instance); } else if (flag2) { RecipeMgr.TryPinHoveredPiece(); } } if (Input.GetKeyDown(HotkeyClearAll.Value) && !flag && (RecipeMgr.PinnedRecipes.Count > 0 || RecipeMgr.PinGroups.Count > 0)) { if (Time.unscaledTime <= _clearAllArmedUntil) { _clearAllArmedUntil = 0f; int count = RecipeMgr.PinnedRecipes.Count; int count2 = RecipeMgr.PinGroups.Count; RecipeMgr.PinnedRecipes.Clear(); RecipeMgr.PinnedRecipeOrder.Clear(); RecipeMgr.PinGroups.Clear(); _isUiVisible = true; RecipeMgr.RefreshRecipeCache(); UIMgr.CloseGatheringList(); UIMgr.RefreshMyPinsList(); DataMgr.SavePins(); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, LocalizationMgr.GetText("cleared")); DebugLogger.Log($"Cleared {count} pinned recipes and {count2} groups"); } else { _clearAllArmedUntil = Time.unscaledTime + 2f; Player.m_localPlayer?.Message(MessageHud.MessageType.Center, LocalizationMgr.GetText("clear_confirm_hotkey")); DebugLogger.Log("Clear-all armed - press again to confirm"); } } if (Localization.instance != null) { string selectedLanguage = Localization.instance.GetSelectedLanguage(); if (_lastLanguage != selectedLanguage) { DebugLogger.Log("Language changed from " + _lastLanguage + " to " + selectedLanguage); _lastLanguage = selectedLanguage; LocalizationMgr.LoadTranslations(); if (ObjectDB.instance != null) { RecipeMgr.RefreshRecipeCache(); } UIMgr?.DestroyUI(); UIMgr?.DestroyMyPinsUI(); } } if (Input.GetKeyDown(HotkeyPageSwitch.Value) && _isUiVisible && !flag) { UIMgr?.CyclePage(); } if (Input.GetKeyDown(HotkeyGatheringList.Value) && !flag && EnableGatheringList.Value) { UIMgr?.ToggleGatheringList(); } if (Player.m_localPlayer != null) { UIMgr?.UpdateMyPinsInventoryState(); } } private bool AreRecipePinnerHotkeysBlocked() { if (InputHelper.IsInputBlocked()) { return true; } if (UIMgr != null && UIMgr.IsMyPinsPanelOpen) { return true; } if (ControlsInfoPanel.IsOpen) { return true; } InventoryGui instance = InventoryGui.instance; if (instance != null && InventoryGui.IsVisible() && ReflectionHelper.IsBlockingInventoryPanelOpen(instance)) { return true; } return false; } private void UpdatePlayerSession() { if (Player.m_localPlayer == null || Player.m_localPlayer.IsDead()) { UIMgr?.UpdateUI(isVisible: false); return; } string playerName = Player.m_localPlayer.GetPlayerName(); if (string.IsNullOrEmpty(playerName)) { return; } if (_currentSessionPlayer != playerName) { DebugLogger.Log("Player session changed from '" + _currentSessionPlayer + "' to '" + playerName + "'"); RecipeMgr.PinnedRecipes.Clear(); RecipeMgr.PinnedRecipeOrder.Clear(); RecipeMgr.CachedPins.Clear(); RecipeMgr.PinGroups.Clear(); UIMgr.DestroyUI(); UIMgr.DestroyMyPinsUI(); _currentSessionPlayer = playerName; DataMgr.LoadPins(); if (ObjectDB.instance != null && ObjectDB.instance.m_recipes.Count > 0) { RecipeMgr.ValidateAndCleanPins(); } RecipeMgr.RefreshRecipeCache(); } UIMgr.UpdateUI(_isUiVisible); } private void ReadMyLittleUIConfig() { if (!Chainloader.PluginInfos.ContainsKey("shudnal.MyLittleUI")) { _mluiInstalled = false; DebugLogger.Log("MyLittleUI not detected"); return; } _mluiInstalled = true; _mluiMapListEnabled = true; _mluiNoMapListEnabled = true; string path = Path.Combine(Paths.ConfigPath, "shudnal.MyLittleUI.cfg"); if (!File.Exists(path)) { DebugLogger.Log("MyLittleUI installed but config not found"); return; } try { string[] array = File.ReadAllLines(path); string text = ""; string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string text2 = array2[i].Trim(); bool value; if (text2.StartsWith("[") && text2.EndsWith("]")) { text = text2; } else if (TryReadBoolConfigValue(text2, "Enable", out value)) { if (text == "[Status effects - Map - List]") { _mluiMapListEnabled = value; } else if (text == "[Status effects - Nomap - List]") { _mluiNoMapListEnabled = value; } } } DebugLogger.Log($"MyLittleUI Config: MapList={_mluiMapListEnabled}, NoMapList={_mluiNoMapListEnabled}"); } catch (Exception ex) { DebugLogger.Error("Error reading MyLittleUI config", ex); } } private static bool TryReadBoolConfigValue(string line, string key, out bool value) { value = false; int num = line.IndexOf('='); if (num <= 0) { return false; } if (!string.Equals(line.Substring(0, num).Trim(), key, StringComparison.OrdinalIgnoreCase)) { return false; } string text = line.Substring(num + 1); int num2 = text.IndexOf('#'); if (num2 >= 0) { text = text.Substring(0, num2); } return bool.TryParse(text.Trim(), out value); } [HarmonyPatch(typeof(Game), "SavePlayerProfile")] [HarmonyPostfix] public static void AutoSavePinsHook() { if (!(Player.m_localPlayer == null) && !(Instance == null)) { if (EnableMod == null || !EnableMod.Value) { DebugLogger.Verbose("Auto-save skipped - mod is disabled"); return; } if (!Instance.IsPinDataLoaded) { DebugLogger.Verbose("Auto-save skipped - pin data not loaded yet"); return; } DebugLogger.Log("Auto-saving pins"); Instance.DataMgr.SavePins(); } } [HarmonyPatch(typeof(InventoryGui), "DoCrafting")] [HarmonyPostfix] public static void AutoUnpinHook(InventoryGui __instance) { if (!EnableMod.Value || !AutoUnpinAfterCrafting.Value || Instance == null) { return; } Recipe craftRecipe = ReflectionHelper.GetCraftRecipe(__instance); if (!(craftRecipe != null)) { return; } string text = null; if (!__instance.m_tabUpgrade.interactable) { ItemDrop.ItemData craftUpgradeItem = ReflectionHelper.GetCraftUpgradeItem(__instance); if (craftUpgradeItem != null) { string arg = craftRecipe.m_item.name; int quality = craftUpgradeItem.m_quality; int num = quality + 1; text = $"{arg} ★{num}"; DebugLogger.Log($"Upgrade crafted: Unpinning target {text} (Base Level: {quality})"); } } else { text = craftRecipe.name; } if (text != null && Instance.RecipeMgr.PinnedRecipes.TryGetValue(text, out var value)) { bool num2 = value > Instance.RecipeMgr.GetGroupClaimCount(text); value--; DebugLogger.Log($"Auto-unpin: {text}, remaining count: {value}"); if (num2) { DebugLogger.Verbose("Auto-unpin: consumed ungrouped copy of '" + text + "', group claims untouched"); } else { Instance.RecipeMgr.DecrementGroupMemberCounts(text); } if (value <= 0) { Instance.RecipeMgr.PinnedRecipes.Remove(text); Instance.RecipeMgr.PinnedRecipeOrder.Remove(text); DebugLogger.Log("Recipe " + text + " fully unpinned"); } else { Instance.RecipeMgr.PinnedRecipes[text] = value; } Instance.RecipeMgr.RefreshRecipeCache(); Instance.DataMgr.SavePins(); if (Instance.RecipeMgr.GetEffectivePinCount() < 2) { Instance.UIMgr.CloseGatheringList(); } } else if (text != null) { string text2 = string.Join(", ", Instance.RecipeMgr.PinnedRecipes.Keys); DebugLogger.Verbose("Auto-unpin: '" + text + "' not found in PinnedRecipes. Current keys: [" + text2 + "]"); } } [HarmonyPatch(typeof(Player), "PlacePiece")] [HarmonyPostfix] public static void AutoUnpinBuildHook(Piece piece) { DebugLogger.Verbose("AutoUnpinBuildHook fired (PlacePiece postfix)"); if (Instance == null || !EnableMod.Value || !AutoUnpinAfterBuilding.Value) { DebugLogger.Verbose($"AutoUnpinBuildHook early exit: Instance={Instance != null}, EnableMod={EnableMod?.Value}, AutoUnpin={AutoUnpinAfterBuilding?.Value}"); return; } if (piece == null) { DebugLogger.Verbose("AutoUnpinBuildHook: piece is null"); return; } string text = piece.name.Replace("(Clone)", "").Trim(); if (!Instance.RecipeMgr.PinnedRecipes.TryGetValue(text, out var value)) { DebugLogger.Verbose("AutoUnpinBuildHook: '" + text + "' not pinned"); return; } bool num = value > Instance.RecipeMgr.GetGroupClaimCount(text); value--; DebugLogger.Log($"Auto-unpin (Build): {text}, remaining count: {value}"); if (num) { DebugLogger.Verbose("Auto-unpin (Build): consumed ungrouped copy of '" + text + "', group claims untouched"); } else { Instance.RecipeMgr.DecrementGroupMemberCounts(text); } if (value <= 0) { Instance.RecipeMgr.PinnedRecipes.Remove(text); Instance.RecipeMgr.PinnedRecipeOrder.Remove(text); DebugLogger.Log("Build recipe " + text + " fully unpinned"); } else { Instance.RecipeMgr.PinnedRecipes[text] = value; } Instance.RecipeMgr.RefreshRecipeCache(); Instance.DataMgr.SavePins(); if (Instance.RecipeMgr.GetEffectivePinCount() < 2) { Instance.UIMgr.CloseGatheringList(); } } [HarmonyPatch(typeof(Player), "TakeInput")] [HarmonyPrefix] public static bool Player_TakeInput_BlockDuringDialog() { if (GroupNameDialog.IsDialogOpen || ConfirmDialog.IsDialogOpen) { return false; } if (Instance?.UIMgr != null && Instance.UIMgr.IsMyPinsPanelOpen) { return false; } return true; } [HarmonyPatch(typeof(InventoryGui), "Hide")] [HarmonyPrefix] public static bool InventoryGui_Hide_BlockDuringDialog() { if (GroupNameDialog.IsDialogOpen) { DebugLogger.Verbose("InventoryGui.Hide blocked - GroupNameDialog is open"); return false; } if (ConfirmDialog.IsDialogOpen) { DebugLogger.Verbose("InventoryGui.Hide blocked - ConfirmDialog is open"); return false; } if (ControlsInfoPanel.IsOpen) { ControlsInfoPanel.Instance?.Hide(); DebugLogger.Log("InventoryGui.Hide intercepted (ESC) - closing ControlsInfoPanel only"); return false; } if (Instance?.UIMgr != null && Instance.UIMgr.IsMyPinsPanelOpen) { bool keyDown = Input.GetKeyDown(KeyCode.Escape); Instance.UIMgr.ToggleMyPinsPanel(); if (keyDown) { DebugLogger.Log("InventoryGui.Hide intercepted (ESC) - closing My Pins panel only"); return false; } DebugLogger.Log("InventoryGui.Hide intercepted (Tab) - closing My Pins panel + inventory"); return true; } return true; } private void BindConfigs() { EnableMod = base.Config.Bind("01 - General", "EnableMod", defaultValue: true, new ConfigDescription("Enable or disable the mod completely.", null, new ConfigurationManagerAttributes { Order = 99 })); EnableMod.SettingChanged += delegate { if (!EnableMod.Value) { UIMgr?.DestroyUI(); UIMgr?.DestroyMyPinsUI(); } }; LanguageOverride = base.Config.Bind("01 - General", "LanguageOverride", "Auto", new ConfigDescription("Force a specific language (e.g., 'German', 'Turkish'). 'Auto' uses the game language.", null, new ConfigurationManagerAttributes { Order = 98 })); LanguageOverride.SettingChanged += delegate { LocalizationMgr?.LoadTranslations(); RecipeMgr?.RefreshRecipeCache(); UIMgr?.DestroyUI(); UIMgr?.DestroyMyPinsUI(); }; LayoutModeConfig = base.Config.Bind("01 - General", "LayoutMode", PinLayoutMode.AutoDetect, new ConfigDescription("HUD pin layout position.", null, new ConfigurationManagerAttributes { Order = 97 })); LayoutModeConfig.SettingChanged += delegate { ReadMyLittleUIConfig(); UIMgr?.DestroyUI(); }; MaximumPins = base.Config.Bind("01 - General", "MaximumPins", 10, new ConfigDescription("Maximum number of pins allowed at once.", new AcceptableValueRange(1, 20), new ConfigurationManagerAttributes { Order = 96 })); MaximumPins.SettingChanged += delegate { int num = RecipeMgr?.TrimToMaximumPins(MaximumPins.Value) ?? 0; if (num > 0) { DebugLogger.Log($"MaximumPins reduced, trimmed {num} effective pin(s)"); RecipeMgr?.RefreshRecipeCache(); } UIMgr?.ResetPage(); UIMgr?.DestroyUI(); if (num > 0) { DataMgr?.SavePins(); } }; PinsPerPage = base.Config.Bind("01 - General", "PinsPerPage", 5, new ConfigDescription("How many pins to show per HUD page.", new AcceptableValueRange(1, 10), new ConfigurationManagerAttributes { Order = 95 })); PinsPerPage.SettingChanged += delegate { UIMgr?.ResetPage(); UIMgr?.DestroyUI(); }; AutoUnpinAfterCrafting = base.Config.Bind("01 - General", "AutoUnpinAfterCrafting", defaultValue: true, new ConfigDescription("Automatically unpin a recipe after it is crafted.", null, new ConfigurationManagerAttributes { Order = 94 })); AutoUnpinAfterBuilding = base.Config.Bind("01 - General", "AutoUnpinAfterBuilding", defaultValue: true, new ConfigDescription("Automatically unpin a recipe after placing a building piece.", null, new ConfigurationManagerAttributes { Order = 93 })); HotkeyPin = base.Config.Bind("02 - Controls", "HotkeyPin", KeyCode.Mouse2, new ConfigDescription("Hotkey to pin the currently viewed recipe.", null, new ConfigurationManagerAttributes { Order = 99 })); HotkeyUnpin = base.Config.Bind("02 - Controls", "HotkeyUnpin", KeyCode.LeftShift, new ConfigDescription("Hold this key + press the Pin hotkey over a recipe or build piece to decrease/remove that pin.", null, new ConfigurationManagerAttributes { Order = 98 })); HotkeyToggleVisibility = base.Config.Bind("02 - Controls", "HotkeyToggleVisibility", KeyCode.F7, new ConfigDescription("Hotkey to show/hide the HUD pin overlay.", null, new ConfigurationManagerAttributes { Order = 97 })); HotkeyGatheringList = base.Config.Bind("02 - Controls", "HotkeyGatheringList", KeyCode.F8, new ConfigDescription("Hotkey to toggle the Gathering List panel.", null, new ConfigurationManagerAttributes { Order = 96 })); HotkeyPageSwitch = base.Config.Bind("02 - Controls", "HotkeyPageSwitch", KeyCode.LeftAlt, new ConfigDescription("Press this key to cycle through HUD pages.", null, new ConfigurationManagerAttributes { Order = 95 })); HotkeyClearAll = base.Config.Bind("02 - Controls", "HotkeyClearAll", KeyCode.P, new ConfigDescription("Hotkey to clear all pinned recipes.", null, new ConfigurationManagerAttributes { Order = 94 })); EnableChestScanning = base.Config.Bind("03 - Chest Scanner", "EnableChestScanning", defaultValue: false, new ConfigDescription("Count materials found in nearby chests towards requirements.", null, new ConfigurationManagerAttributes { Order = 99 })); EnableChestScanning.SettingChanged += delegate { if (EnableChestScanning.Value) { ContainerScanner.ClearAll(); ContainerMgr?.InitializeContainers(); } else { ContainerScanner.ClearAll(); } RecipeMgr?.RefreshRecipeCache(); }; ChestScanRange = base.Config.Bind("03 - Chest Scanner", "ChestScanRange", 20f, new ConfigDescription("Radius (meters) in which chests are scanned.", new AcceptableValueRange(5f, 100f), new ConfigurationManagerAttributes { Order = 98 })); ChestScanInterval = base.Config.Bind("03 - Chest Scanner", "ChestScanInterval", 3f, new ConfigDescription("How often (seconds) chests are re-scanned.", new AcceptableValueRange(0.5f, 10f), new ConfigurationManagerAttributes { Order = 97 })); UIScale = base.Config.Bind("04 - HUD Appearance", "UIScale", 0.75f, new ConfigDescription("Global UI scale multiplier.", new AcceptableValueRange(0.3f, 3f), new ConfigurationManagerAttributes { Order = 99 })); UIScale.SettingChanged += delegate { UIMgr?.DestroyUI(); }; BackgroundOpacity = base.Config.Bind("04 - HUD Appearance", "BackgroundOpacity", 0.5f, new ConfigDescription("Background panel opacity (0 = transparent, 1 = opaque).", new AcceptableValueRange(0f, 1f), new ConfigurationManagerAttributes { Order = 98 })); FontSizeRecipeName = base.Config.Bind("04 - HUD Appearance", "FontSizeRecipeName", 16, new ConfigDescription("Font size for recipe/group name in HUD pins.", new AcceptableValueRange(8, 40), new ConfigurationManagerAttributes { Order = 97 })); FontSizeRecipeName.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; FontSizeMaterials = base.Config.Bind("04 - HUD Appearance", "FontSizeMaterials", 15, new ConfigDescription("Font size for material names and amounts in HUD pins.", new AcceptableValueRange(8, 40), new ConfigurationManagerAttributes { Order = 96 })); FontSizeMaterials.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; HudRecipeIconSize = base.Config.Bind("04 - HUD Appearance", "HudRecipeIconSize", 28, new ConfigDescription("Size (px) of the recipe icon in the HUD pin header.", new AcceptableValueRange(12, 64), new ConfigurationManagerAttributes { Order = 95 })); HudRecipeIconSize.SettingChanged += delegate { UIMgr?.DestroyUI(); }; HudMaterialIconSize = base.Config.Bind("04 - HUD Appearance", "HudMaterialIconSize", 20, new ConfigDescription("Size (px) of the material icons in the HUD pin resource list.", new AcceptableValueRange(10, 48), new ConfigurationManagerAttributes { Order = 94 })); HudMaterialIconSize.SettingChanged += delegate { UIMgr?.DestroyUI(); }; HudGroupIconSize = base.Config.Bind("04 - HUD Appearance", "HudGroupIconSize", 28, new ConfigDescription("Size (px) of the group icon (stacked cards) in the HUD pin header.", new AcceptableValueRange(12, 64), new ConfigurationManagerAttributes { Order = 93 })); HudGroupIconSize.SettingChanged += delegate { UIMgr?.DestroyUI(); }; EnableCraftReadiness = base.Config.Bind("04 - HUD Appearance", "EnableCraftReadiness", defaultValue: true, new ConfigDescription("Show a colored accent bar indicating whether a recipe can be crafted.", null, new ConfigurationManagerAttributes { Order = 92 })); EnableCraftReadiness.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorHeader = base.Config.Bind("05 - Colors", "ColorHeader", new Color(1f, 0.808f, 0f, 1f), new ConfigDescription("Recipe/group name text color.", null, new ConfigurationManagerAttributes { Order = 99 })); ColorHeader.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); UIMgr?.DestroyUI(); }; ColorEnoughInInventory = base.Config.Bind("05 - Colors", "ColorEnoughInInventory", new Color(0f, 1f, 0f, 1f), new ConfigDescription("Material amount color when you have enough in your inventory.", null, new ConfigurationManagerAttributes { Order = 98 })); ColorEnoughInInventory.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorEnoughWithChests = base.Config.Bind("05 - Colors", "ColorEnoughWithChests", new Color(1f, 1f, 0f, 1f), new ConfigDescription("Material amount color when enough only if chests are counted.", null, new ConfigurationManagerAttributes { Order = 97 })); ColorEnoughWithChests.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorMissing = base.Config.Bind("05 - Colors", "ColorMissing", new Color(1f, 0.33f, 0.33f, 1f), new ConfigDescription("Material amount color when materials are missing.", null, new ConfigurationManagerAttributes { Order = 96 })); ColorMissing.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorCraftReady = base.Config.Bind("05 - Colors", "ColorCraftReady", new Color(0.2f, 0.9f, 0.3f, 0.85f), new ConfigDescription("Accent bar color when all materials are available.", null, new ConfigurationManagerAttributes { Order = 95 })); ColorCraftReady.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorCraftNotReady = base.Config.Bind("05 - Colors", "ColorCraftNotReady", new Color(0.9f, 0.25f, 0.25f, 0.5f), new ConfigDescription("Accent bar color when materials are missing.", null, new ConfigurationManagerAttributes { Order = 94 })); ColorCraftNotReady.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorPaginationActive = base.Config.Bind("06 - Pagination", "ColorPaginationActive", new Color(1f, 0.717f, 0.368f, 1f), new ConfigDescription("Active page indicator dot color.", null, new ConfigurationManagerAttributes { Order = 99 })); ColorPaginationActive.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; PaginationInactiveOpacity = base.Config.Bind("06 - Pagination", "PaginationInactiveOpacity", 0.3f, new ConfigDescription("Opacity of inactive page indicator dots.", new AcceptableValueRange(0.1f, 1f), new ConfigurationManagerAttributes { Order = 98 })); PaginationInactiveOpacity.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; PaginationDotSize = base.Config.Bind("06 - Pagination", "PaginationDotSize", 10, new ConfigDescription("Size of the pagination dot squares.", new AcceptableValueRange(5, 20), new ConfigurationManagerAttributes { Order = 97 })); PaginationDotSize.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; PaginationDotSpacing = base.Config.Bind("06 - Pagination", "PaginationDotSpacing", 8, new ConfigDescription("Space between pagination squares.", new AcceptableValueRange(0, 20), new ConfigurationManagerAttributes { Order = 96 })); PaginationDotSpacing.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; EnableGatheringList = base.Config.Bind("07 - Gathering List", "EnableGatheringList", defaultValue: true, new ConfigDescription("Enable the Gathering List (aggregated material overview).", null, new ConfigurationManagerAttributes { Order = 99 })); EnableGatheringList.SettingChanged += delegate { UIMgr?.DestroyUI(); }; AutoOpenGatheringList = base.Config.Bind("07 - Gathering List", "AutoOpenGatheringList", defaultValue: true, new ConfigDescription("Automatically open the Gathering List when 2+ recipes are pinned.", null, new ConfigurationManagerAttributes { Order = 98 })); GatheringListColumns = base.Config.Bind("07 - Gathering List", "GatheringListColumns", 4, new ConfigDescription("Number of columns in the Gathering List grid (horizontal modes only). Panel width adjusts proportionally.", new AcceptableValueRange(1, 10), new ConfigurationManagerAttributes { Order = 97 })); GatheringListColumns.SettingChanged += delegate { UIMgr?.DestroyUI(); }; GatheringListFontSizeTitle = base.Config.Bind("07 - Gathering List", "GatheringListFontSizeTitle", 20, new ConfigDescription("Font size for the Gathering List title.", new AcceptableValueRange(8, 40), new ConfigurationManagerAttributes { Order = 96 })); GatheringListFontSizeTitle.SettingChanged += delegate { UIMgr?.DestroyUI(); }; GatheringListFontSizeMaterials = base.Config.Bind("07 - Gathering List", "GatheringListFontSizeMaterials", 15, new ConfigDescription("Font size for material amounts in the Gathering List.", new AcceptableValueRange(8, 40), new ConfigurationManagerAttributes { Order = 95 })); GatheringListFontSizeMaterials.SettingChanged += delegate { UIMgr?.DestroyUI(); }; ContainerGatheringListPosition = base.Config.Bind("07 - Gathering List", "ContainerGatheringListPosition", new Vector2(-400f, 320f), new ConfigDescription("Gathering List position offset (X, Y) when a container (chest/inventory) is open.", null, new ConfigurationManagerAttributes { Order = 94 })); GroupCompactThreshold = base.Config.Bind("08 - Groups", "GroupCompactThreshold", 4, new ConfigDescription("Number of unique materials above which a group pin switches to compact (grid) layout. Default: 4 (triggers at 5+).", new AcceptableValueRange(1, 10), new ConfigurationManagerAttributes { Order = 99 })); GroupCompactThreshold.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; GroupCompactMaxRows = base.Config.Bind("08 - Groups", "GroupCompactMaxRows", 3, new ConfigDescription("Number of grid rows a compact group pin shows before collapsing the rest into a \"+N\" cell. Default: 3.", new AcceptableValueRange(1, 10), new ConfigurationManagerAttributes { Order = 97 })); GroupCompactMaxRows.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; GroupIconFontSize = base.Config.Bind("08 - Groups", "GroupIconFontSize", 16, new ConfigDescription("Font size of the member count number displayed on the group icon.", new AcceptableValueRange(8, 32), new ConfigurationManagerAttributes { Order = 98 })); GroupIconFontSize.SettingChanged += delegate { UIMgr?.DestroyUI(); }; MyPinsPanelWidth = base.Config.Bind("09 - My Pins Panel", "PanelWidth", 375f, new ConfigDescription("Width of the My Pins panel.", new AcceptableValueRange(200f, 600f), new ConfigurationManagerAttributes { Order = 99 })); MyPinsPanelWidth.SettingChanged += delegate { UIMgr?.DestroyMyPinsUI(); }; MyPinsPanelHeight = base.Config.Bind("09 - My Pins Panel", "PanelHeight", 480f, new ConfigDescription("Height of the My Pins panel.", new AcceptableValueRange(200f, 800f), new ConfigurationManagerAttributes { Order = 98 })); MyPinsPanelHeight.SettingChanged += delegate { UIMgr?.DestroyMyPinsUI(); }; MyPinsPanelPosition = base.Config.Bind("09 - My Pins Panel", "PanelPosition", Vector2.zero, new ConfigDescription("Position offset (X, Y) of the My Pins panel from the screen center. (0, 0) = perfectly centered.", null, new ConfigurationManagerAttributes { Order = 97 })); MyPinsPanelPosition.SettingChanged += delegate { UIMgr?.DestroyMyPinsUI(); }; ButtonTextColor = base.Config.Bind("09 - My Pins Panel", "ButtonIconColor", new Color(1f, 0.631f, 0.239f, 1f), new ConfigDescription("Tint color of the pin icon on the My Pins button (default: #ffa13d).", null, new ConfigurationManagerAttributes { Order = 96 })); ButtonTextColor.SettingChanged += delegate { UIMgr?.DestroyMyPinsUI(); }; MyPinsButtonPosition = base.Config.Bind("09 - My Pins Panel", "MyPinsButtonPosition", new Vector2(-500f, 570f), new ConfigDescription("Position offset (X, Y) of the My Pins button from the inventory.", null, new ConfigurationManagerAttributes { Order = 95 })); MyPinsButtonPosition.SettingChanged += delegate { UIMgr?.DestroyMyPinsUI(); }; MyPinsButtonSize = base.Config.Bind("09 - My Pins Panel", "MyPinsButtonSize", 40, new ConfigDescription("Size of the My Pins icon button in pixels.", new AcceptableValueRange(20, 200), new ConfigurationManagerAttributes { Order = 94 })); MyPinsButtonSize.SettingChanged += delegate { UIMgr?.DestroyMyPinsUI(); }; VerticalListWidth = base.Config.Bind("10 - Layout (Vertical Mode)", "ListWidth", 265f, new ConfigDescription("Width of the pin list panel.", null, new ConfigurationManagerAttributes { Order = 99 })); VerticalPinSpacing = base.Config.Bind("10 - Layout (Vertical Mode)", "PinSpacing", 10f, new ConfigDescription("Vertical spacing between pin cards.", null, new ConfigurationManagerAttributes { Order = 98 })); VerticalPosition = base.Config.Bind("10 - Layout (Vertical Mode)", "Position", new Vector2(-40f, -250f), new ConfigDescription("Anchor position offset (X, Y).", null, new ConfigurationManagerAttributes { Order = 97 })); HorizontalColumnWidth = base.Config.Bind("11 - Layout (Horizontal - Map Side)", "ColumnWidth", 265f, new ConfigDescription("Width of each pin column.", null, new ConfigurationManagerAttributes { Order = 99 })); HorizontalPinSpacing = base.Config.Bind("11 - Layout (Horizontal - Map Side)", "PinSpacing", 10f, new ConfigDescription("Spacing between pin cards in the column.", null, new ConfigurationManagerAttributes { Order = 98 })); HorizontalPosition = base.Config.Bind("11 - Layout (Horizontal - Map Side)", "Position", new Vector2(-250f, -40f), new ConfigDescription("Anchor position offset (X, Y).", null, new ConfigurationManagerAttributes { Order = 97 })); BottomRightColumnWidth = base.Config.Bind("12 - Layout (Horizontal - Bottom Right)", "ColumnWidth", 265f, new ConfigDescription("Width of each pin column.", null, new ConfigurationManagerAttributes { Order = 99 })); BottomRightPinSpacing = base.Config.Bind("12 - Layout (Horizontal - Bottom Right)", "PinSpacing", 10f, new ConfigDescription("Spacing between pin cards in the column.", null, new ConfigurationManagerAttributes { Order = 98 })); BottomRightPosition = base.Config.Bind("12 - Layout (Horizontal - Bottom Right)", "Position", new Vector2(-40f, 40f), new ConfigDescription("Anchor position offset (X, Y).", null, new ConfigurationManagerAttributes { Order = 97 })); EnableDebugLogging = base.Config.Bind("13 - Debug", "EnableDebugLogging", defaultValue: false, new ConfigDescription("Enable verbose debug logging to the BepInEx console.", null, new ConfigurationManagerAttributes { Order = 99 })); DebugLogger.Log("Config loaded"); } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.ReflectionHelper ---- namespace ValheimRecipePinner { public static class ReflectionHelper { private static Func _getGuiScale; private static Func _getRecipeListRoot; private static Func _getAvailableRecipes; private static Func _getCurrentContainer; private static Func _getCraftRecipe; private static Func _getCraftUpgradeItem; private static Func _getHoveredPiece; private static FieldInfo[] _blockingInventoryPanelFields; public static Func CheckContainerAccess; public static float currentGuiScaleValue; static ReflectionHelper() { currentGuiScaleValue = 1f; InitializeReflection(); } public static void InitializeReflection() { DebugLogger.Log("Reflection init"); int num = 0; int num2 = 0; try { FieldInfo fieldInfo = AccessTools.Field(typeof(GuiScaler), "m_largeGuiScale"); if (fieldInfo != null && fieldInfo.IsStatic) { _getGuiScale = Expression.Lambda>(Expression.Field(null, fieldInfo), Array.Empty()).Compile(); num++; DebugLogger.Verbose("✓ GuiScaler.m_largeGuiScale"); } else { num2++; DebugLogger.Warning("✗ GuiScaler.m_largeGuiScale not found"); } FieldInfo fieldInfo2 = AccessTools.Field(typeof(InventoryGui), "m_recipeListRoot"); if (fieldInfo2 != null) { ParameterExpression parameterExpression = Expression.Parameter(typeof(InventoryGui), "arg"); _getRecipeListRoot = Expression.Lambda>(Expression.Field(parameterExpression, fieldInfo2), new ParameterExpression[1] { parameterExpression }).Compile(); num++; DebugLogger.Verbose("✓ InventoryGui.m_recipeListRoot"); } else { num2++; DebugLogger.Warning("✗ InventoryGui.m_recipeListRoot not found"); } FieldInfo fieldInfo3 = AccessTools.Field(typeof(InventoryGui), "m_availableRecipes"); if (fieldInfo3 != null) { ParameterExpression parameterExpression2 = Expression.Parameter(typeof(InventoryGui), "arg"); _getAvailableRecipes = Expression.Lambda>(Expression.Field(parameterExpression2, fieldInfo3), new ParameterExpression[1] { parameterExpression2 }).Compile(); num++; DebugLogger.Verbose("✓ InventoryGui.m_availableRecipes"); } else { num2++; DebugLogger.Warning("✗ InventoryGui.m_availableRecipes not found"); } FieldInfo fieldInfo4 = AccessTools.Field(typeof(InventoryGui), "m_currentContainer"); if (fieldInfo4 != null) { ParameterExpression parameterExpression3 = Expression.Parameter(typeof(InventoryGui), "arg"); _getCurrentContainer = Expression.Lambda>(Expression.Field(parameterExpression3, fieldInfo4), new ParameterExpression[1] { parameterExpression3 }).Compile(); num++; DebugLogger.Verbose("✓ InventoryGui.m_currentContainer"); } else { num2++; DebugLogger.Warning("✗ InventoryGui.m_currentContainer not found"); } FieldInfo fieldInfo5 = AccessTools.Field(typeof(InventoryGui), "m_craftRecipe"); if (fieldInfo5 != null) { ParameterExpression parameterExpression4 = Expression.Parameter(typeof(InventoryGui), "arg"); _getCraftRecipe = Expression.Lambda>(Expression.Field(parameterExpression4, fieldInfo5), new ParameterExpression[1] { parameterExpression4 }).Compile(); num++; DebugLogger.Verbose("✓ InventoryGui.m_craftRecipe"); } else { num2++; DebugLogger.Warning("✗ InventoryGui.m_craftRecipe not found"); } FieldInfo fieldInfo6 = AccessTools.Field(typeof(InventoryGui), "m_craftUpgradeItem"); if (fieldInfo6 != null) { ParameterExpression parameterExpression5 = Expression.Parameter(typeof(InventoryGui), "arg"); _getCraftUpgradeItem = Expression.Lambda>(Expression.Field(parameterExpression5, fieldInfo6), new ParameterExpression[1] { parameterExpression5 }).Compile(); num++; DebugLogger.Verbose("✓ InventoryGui.m_craftUpgradeItem"); } else { num2++; DebugLogger.Warning("✗ InventoryGui.m_craftUpgradeItem not found"); } FieldInfo fieldInfo7 = AccessTools.Field(typeof(Hud), "m_hoveredPiece"); if (fieldInfo7 != null) { ParameterExpression parameterExpression6 = Expression.Parameter(typeof(Hud), "arg"); _getHoveredPiece = Expression.Lambda>(Expression.Field(parameterExpression6, fieldInfo7), new ParameterExpression[1] { parameterExpression6 }).Compile(); num++; DebugLogger.Verbose("✓ Hud.m_hoveredPiece"); } else { num2++; DebugLogger.Warning("✗ Hud.m_hoveredPiece not found"); } MethodInfo methodInfo = AccessTools.Method(typeof(Container), "CheckAccess", new Type[1] { typeof(long) }); if (methodInfo != null) { CheckContainerAccess = AccessTools.MethodDelegate>(methodInfo); num++; DebugLogger.Verbose("✓ Container.CheckAccess"); } else { num2++; DebugLogger.Warning("✗ Container.CheckAccess not found"); } DebugLogger.Log($"Reflection done: {num} ok, {num2} failed"); if (num2 > 0) { DebugLogger.Warning("Some reflection targets failed"); } } catch (Exception ex) { DebugLogger.Error("Reflection init failed", ex); } } public static void UpdateGuiScale() { if (_getGuiScale != null) { currentGuiScaleValue = _getGuiScale(); } else { currentGuiScaleValue = 1f; } } public static Transform GetRecipeListRoot(InventoryGui gui) { if (_getRecipeListRoot == null) { DebugLogger.Warning("GetRecipeListRoot delegate is null"); return null; } return _getRecipeListRoot(gui); } public static object GetAvailableRecipes(InventoryGui gui) { if (_getAvailableRecipes == null) { DebugLogger.Warning("GetAvailableRecipes delegate is null"); return null; } return _getAvailableRecipes(gui); } public static Container GetCurrentContainer(InventoryGui gui) { if (_getCurrentContainer == null) { DebugLogger.Verbose("GetCurrentContainer delegate is null"); return null; } return _getCurrentContainer(gui); } public static Recipe GetCraftRecipe(InventoryGui gui) { if (_getCraftRecipe == null) { DebugLogger.Warning("GetCraftRecipe delegate is null"); return null; } return _getCraftRecipe(gui); } public static ItemDrop.ItemData GetCraftUpgradeItem(InventoryGui gui) { return _getCraftUpgradeItem?.Invoke(gui); } public static Piece GetHoveredPiece(Hud hud) { if (_getHoveredPiece == null) { DebugLogger.Verbose("GetHoveredPiece delegate is null"); return null; } return _getHoveredPiece(hud); } public static bool IsBlockingInventoryPanelOpen(InventoryGui gui) { if (gui == null) { return false; } FieldInfo[] blockingInventoryPanelFields = GetBlockingInventoryPanelFields(); for (int i = 0; i < blockingInventoryPanelFields.Length; i++) { if (IsActiveUnityObject(blockingInventoryPanelFields[i].GetValue(gui))) { return true; } } return false; } private static FieldInfo[] GetBlockingInventoryPanelFields() { if (_blockingInventoryPanelFields != null) { return _blockingInventoryPanelFields; } List list = new List(); FieldInfo[] fields = typeof(InventoryGui).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { string text = fieldInfo.Name.ToLowerInvariant(); if ((text.Contains("troph") || text.Contains("skill") || text.Contains("compendium") || text.Contains("texts")) && (typeof(GameObject).IsAssignableFrom(fieldInfo.FieldType) || typeof(Component).IsAssignableFrom(fieldInfo.FieldType))) { list.Add(fieldInfo); } } _blockingInventoryPanelFields = list.ToArray(); return _blockingInventoryPanelFields; } private static bool IsActiveUnityObject(object value) { if (value is GameObject gameObject) { return gameObject.activeInHierarchy; } if (value is Component component) { if (component.gameObject != null) { return component.gameObject.activeInHierarchy; } return false; } return false; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.InputHelper ---- namespace ValheimRecipePinner { public static class InputHelper { public static bool IsInputBlocked() { if (GroupNameDialog.IsDialogOpen) { return true; } if (ConfirmDialog.IsDialogOpen) { return true; } if (Console.IsVisible()) { return true; } if (Chat.instance != null && Chat.instance.HasFocus()) { return true; } if (TextInput.IsVisible()) { return true; } return false; } public static bool IsMouseOverRect(RectTransform rect, bool logHit = true) { if (rect == null) { DebugLogger.Verbose("IsMouseOverRect: rect is null"); return false; } bool num = RectTransformUtility.RectangleContainsScreenPoint(rect, Input.mousePosition); if (num && logHit) { DebugLogger.Verbose("Mouse over rect: " + rect.gameObject.name); } return num; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.GatheringItemUI ---- namespace ValheimRecipePinner { public class GatheringItemUI : MonoBehaviour { public Image Icon; public Text AmountText; public void SetActive(bool active) { base.gameObject.SetActive(active); } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.GatheringListUI ---- namespace ValheimRecipePinner { public class GatheringListUI : MonoBehaviour { public RectTransform PanelRect; public Image BgImage; public Text TitleText; public Transform ItemListRoot; public Text HintText; public List ItemSlots = new List(); private Coroutine _layoutCoroutine; public void SetActive(bool active) { base.gameObject.SetActive(active); } public void RefreshLayout() { if (base.gameObject.activeInHierarchy) { if (_layoutCoroutine != null) { StopCoroutine(_layoutCoroutine); } _layoutCoroutine = StartCoroutine(FixLayout()); } } private void OnDisable() { _layoutCoroutine = null; } private IEnumerator FixLayout() { yield return null; if (ItemListRoot != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(ItemListRoot as RectTransform); } if (PanelRect != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(PanelRect); } } public void ApplyColumns(int configCols) { if (ItemListRoot == null) { return; } GridLayoutGroup component = ItemListRoot.GetComponent(); if (!(component == null)) { VerticalLayoutGroup component2 = GetComponent(); float num = ((component2 != null) ? ((float)(component2.padding.left + component2.padding.right)) : 22f); float x = component.cellSize.x; float x2 = component.spacing.x; if (configCols > 0) { component.constraintCount = configCols; float x3 = (float)configCols * (x + x2) - x2 + num; PanelRect.sizeDelta = new Vector2(x3, PanelRect.sizeDelta.y); } else { float num2 = PanelRect.sizeDelta.x - num; int constraintCount = Mathf.Max(1, Mathf.FloorToInt((num2 + x2) / (x + x2))); component.constraintCount = constraintCount; } } } public static float CalculateWidthForColumns(int cols) { float num = 58f; float num2 = 4f; float num3 = 22f; return (float)cols * (num + num2) - num2 + num3; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.GatheringItemData ---- namespace ValheimRecipePinner { public class GatheringItemData { public string ItemName; public string DisplayName; public Sprite Icon; public int TotalRequired; public int TotalHave; public bool IsComplete; } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.UIBuilder ---- namespace ValheimRecipePinner { public static class UIBuilder { private static Color DividerColor = new Color(1f, 1f, 1f, 0.1f); private static Sprite _cachedUiSprite; private static bool _spriteSearchDone = false; private static Font _cachedNorseFont; private static Sprite _cachedPinIcon; private static bool _pinIconLoadAttempted = false; private static bool _vanillaBtnCached = false; private static Sprite _vanillaBtnSprite; private static Material _vanillaBtnMaterial; private static ColorBlock _vanillaBtnColors; private static Font _vanillaBtnFont; private static int _vanillaBtnFontSize; private static FontStyle _vanillaBtnFontStyle; private static Color _vanillaBtnTextColor; private static bool _vanillaBtnHasOutline; private static Color _vanillaBtnOutlineColor; private static Vector2 _vanillaBtnOutlineDistance; private static Selectable.Transition _vanillaBtnTransition; private static SpriteState _vanillaBtnSpriteState; private static AnimationTriggers _vanillaBtnAnimTriggers; private static GameObject _cachedButtonSfxPrefab; private static Color ValheimOrange => RecipePinnerPlugin.ButtonTextColor?.Value ?? new Color(1f, 0.631f, 0.239f, 1f); private static Sprite LoadPinIconSprite() { if (_pinIconLoadAttempted) { return _cachedPinIcon; } _pinIconLoadAttempted = true; try { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("RecipePinner.pinIcon.png"); if (stream == null) { DebugLogger.Warning("pin.png embedded resource not found"); return null; } byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { stream.CopyTo(memoryStream); array = memoryStream.ToArray(); } Texture2D texture2D = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false); texture2D.filterMode = FilterMode.Bilinear; bool flag = false; MethodInfo method = typeof(Texture2D).GetMethod("LoadImage", BindingFlags.Instance | BindingFlags.Public, null, new Type[1] { typeof(byte[]) }, null); if (method != null) { flag = (bool)(method.Invoke(texture2D, new object[1] { array }) ?? ((object)false)); } else { Type type = Type.GetType("UnityEngine.ImageConversion, UnityEngine.ImageConversionModule"); if (type != null) { MethodInfo method2 = type.GetMethod("LoadImage", new Type[2] { typeof(Texture2D), typeof(byte[]) }); if (method2 != null) { flag = (bool)(method2.Invoke(null, new object[2] { texture2D, array }) ?? ((object)false)); } } } if (!flag) { DebugLogger.Warning("pin.png: LoadImage failed (reflection)"); return null; } _cachedPinIcon = Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f), 100f); DebugLogger.Log($"pin.png loaded: {texture2D.width}x{texture2D.height}"); return _cachedPinIcon; } catch (Exception ex) { DebugLogger.Warning("pin.png load error: " + ex.Message); return null; } } private static bool TryGetTrophiesPanelBackground(out Sprite sprite, out Material material) { sprite = null; material = null; if (InventoryGui.instance == null || InventoryGui.instance.m_trophiesPanel == null) { DebugLogger.Warning("TryGetTrophiesPanelBackground: InventoryGui or m_trophiesPanel is null"); return false; } GameObject trophiesPanel = InventoryGui.instance.m_trophiesPanel; Image component = trophiesPanel.GetComponent(); if (component != null && component.sprite != null) { sprite = component.sprite; material = component.material; DebugLogger.Verbose("TryGetTrophiesPanelBackground: Found on root - sprite=" + sprite.name + ", material=" + ((material != null) ? material.name : "null")); return true; } Transform transform = trophiesPanel.transform.Find("background") ?? trophiesPanel.transform.Find("Background") ?? trophiesPanel.transform.Find("bg"); if (transform != null) { Image component2 = transform.GetComponent(); if (component2 != null && component2.sprite != null) { sprite = component2.sprite; material = component2.material; DebugLogger.Verbose("TryGetTrophiesPanelBackground: Found on child '" + transform.name + "' - sprite=" + sprite.name); return true; } } Image[] componentsInChildren = trophiesPanel.GetComponentsInChildren(includeInactive: true); foreach (Image image in componentsInChildren) { if (image != null && image.sprite != null && image.gameObject != trophiesPanel) { sprite = image.sprite; material = image.material; DebugLogger.Verbose("TryGetTrophiesPanelBackground: Found on child '" + image.gameObject.name + "' via iteration"); return true; } } DebugLogger.Warning("TryGetTrophiesPanelBackground: No suitable background found"); return false; } private static bool TryGetVanillaInputFieldStyle(out Sprite sprite, out Material material, out Color color, out Selectable source) { sprite = null; material = null; color = Color.white; source = null; TextInput instance = TextInput.instance; if (instance == null || (UnityEngine.Object)(object)instance.m_inputField == null) { DebugLogger.Verbose("TryGetVanillaInputFieldStyle: TextInput or m_inputField is null"); return false; } Image image = ((Selectable)(object)instance.m_inputField).targetGraphic as Image; if (image == null) { image = ((Component)(object)instance.m_inputField).GetComponent(); } if (image == null || image.sprite == null) { DebugLogger.Verbose("TryGetVanillaInputFieldStyle: no background Image with a sprite"); return false; } sprite = image.sprite; material = image.material; color = image.color; source = (Selectable)(object)instance.m_inputField; DebugLogger.Verbose("TryGetVanillaInputFieldStyle: sprite=" + sprite.name + ", material=" + ((material != null) ? material.name : "null")); return true; } private static Sprite GetBackgroundSprite() { if (_cachedUiSprite != null) { return _cachedUiSprite; } if (_spriteSearchDone) { return null; } Sprite[] array = Resources.FindObjectsOfTypeAll(); Sprite sprite = null; Sprite[] array2 = array; foreach (Sprite sprite2 in array2) { if (!(sprite2 == null)) { if (sprite2.name == "UISprite") { _cachedUiSprite = sprite2; break; } if (sprite == null && sprite2.name == "Knob") { sprite = sprite2; } } } if (_cachedUiSprite == null) { _cachedUiSprite = sprite; } _spriteSearchDone = true; if (_cachedUiSprite != null) { DebugLogger.Verbose("Found background sprite: " + _cachedUiSprite.name); } else { DebugLogger.Warning("No suitable background sprite found"); } return _cachedUiSprite; } public static Sprite GetUISpritePublic() { return GetBackgroundSprite(); } public static void PlayButtonSFX() { if (_cachedButtonSfxPrefab == null) { InventoryGui instance = InventoryGui.instance; if (instance != null) { EffectList setActiveGroupEffects = instance.m_setActiveGroupEffects; if (setActiveGroupEffects != null && setActiveGroupEffects.m_effectPrefabs?.Length > 0) { _cachedButtonSfxPrefab = instance.m_setActiveGroupEffects.m_effectPrefabs[0].m_prefab; if (_cachedButtonSfxPrefab != null) { DebugLogger.Log("Cached button SFX prefab: " + _cachedButtonSfxPrefab.name); } } } } if (_cachedButtonSfxPrefab != null) { Player localPlayer = Player.m_localPlayer; Vector3 position = ((localPlayer != null) ? localPlayer.transform.position : Vector3.zero); UnityEngine.Object.Instantiate(_cachedButtonSfxPrefab, position, Quaternion.identity); } } private static void CacheVanillaButtonStyle() { if (_vanillaBtnCached) { return; } _vanillaBtnCached = true; if (InventoryGui.instance == null) { return; } FieldInfo fieldInfo = AccessTools.Field(typeof(InventoryGui), "m_craftButton"); if (fieldInfo == null) { DebugLogger.Warning("Vanilla button style: m_craftButton field not found"); return; } Button button = fieldInfo.GetValue(InventoryGui.instance) as Button; if (button == null) { DebugLogger.Warning("Vanilla button style: m_craftButton value is null"); return; } Image component = button.GetComponent(); if (component != null) { _vanillaBtnSprite = component.sprite; _vanillaBtnMaterial = component.material; } _vanillaBtnColors = button.colors; Text componentInChildren = button.GetComponentInChildren(); if (componentInChildren != null) { _vanillaBtnFont = componentInChildren.font; _vanillaBtnFontSize = componentInChildren.fontSize; _vanillaBtnFontStyle = componentInChildren.fontStyle; _vanillaBtnTextColor = componentInChildren.color; } Outline componentInChildren2 = button.GetComponentInChildren(); if (componentInChildren2 != null) { _vanillaBtnHasOutline = true; _vanillaBtnOutlineColor = componentInChildren2.effectColor; _vanillaBtnOutlineDistance = componentInChildren2.effectDistance; } _vanillaBtnTransition = button.transition; _vanillaBtnSpriteState = button.spriteState; _vanillaBtnAnimTriggers = button.animationTriggers; DebugLogger.Log($"Vanilla button style cached (sprite={_vanillaBtnSprite?.name}, font={_vanillaBtnFont?.name}, transition={_vanillaBtnTransition})"); } public static Button CreateVanillaButton(Transform parent, string label, float minWidth = -1f, float minHeight = 35f) { CacheVanillaButtonStyle(); GameObject gameObject = new GameObject("Btn_" + label, typeof(RectTransform)) { layer = 5 }; gameObject.transform.SetParent(parent, worldPositionStays: false); Image image = gameObject.AddComponent(); if (_vanillaBtnSprite != null) { image.sprite = _vanillaBtnSprite; image.material = _vanillaBtnMaterial; image.type = ((_vanillaBtnSprite.border != Vector4.zero) ? Image.Type.Sliced : Image.Type.Simple); image.color = Color.white; } else { Sprite sprite = (image.sprite = GetBackgroundSprite()); if (sprite != null && sprite.border != Vector4.zero) { image.type = Image.Type.Sliced; } image.color = new Color(0.2f, 0.2f, 0.2f, 0.9f); } image.raycastTarget = true; Button button = gameObject.AddComponent