// Consolidated decompiled source — Kadrio-RecipePinner v1.1.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.Linq; 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; // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.ContainerScanner ---- namespace ValheimRecipePinner { public class ContainerScanner { public static List AllContainers = new List(); internal static readonly object ContainerLock = new object(); public Dictionary ContainerCache = new Dictionary(); private static HashSet _processedIDs = new HashSet(); private Vector3 _lastScanPos; private int _lastItemCount = 0; private float _scanTimer = 0f; private const float MovementThresholdSqr = 4f; public void InitializeContainers() { DebugLogger.Log("Initializing container tracking..."); lock (ContainerLock) { if (AllContainers.Count != 0) { return; } Container[] array = Object.FindObjectsByType(FindObjectsSortMode.None); Container[] array2 = array; foreach (Container container in array2) { if (container != null && !AllContainers.Contains(container)) { AllContainers.Add(container); if (container.GetComponent() == null) { ContainerTracker containerTracker = container.gameObject.AddComponent(); containerTracker.MyContainer = container; } } } DebugLogger.Log($"Initialized tracking for {AllContainers.Count} existing containers"); } } public void UpdateScanning() { if (Player.m_localPlayer == null) { return; } _scanTimer += Time.deltaTime; float num = Vector3.SqrMagnitude(Player.m_localPlayer.transform.position - _lastScanPos); bool flag = num > 4f; int num2 = 0; foreach (ItemDrop.ItemData allItem in Player.m_localPlayer.GetInventory().GetAllItems()) { num2 += allItem.m_stack; } bool flag2 = num2 != _lastItemCount; bool flag3 = false; if (InventoryGui.instance != null) { flag3 = ReflectionHelper.GetCurrentContainer(InventoryGui.instance) != null; } float num3 = (flag3 ? 0.5f : RecipePinnerPlugin.ChestScanInterval.Value); if (flag || flag2 || _scanTimer >= num3) { _scanTimer = 0f; _lastItemCount = num2; DebugLogger.Verbose($"Scanning containers - Moved: {flag}, InvChanged: {flag2}, Interval: {_scanTimer >= num3}"); 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; List list; lock (ContainerLock) { list = new List(AllContainers); } _processedIDs.Clear(); int num2 = 0; int num3 = 0; int num4 = 0; foreach (Container item in list) { if (item == null || item.transform == null) { num3++; continue; } int instanceID = item.GetInstanceID(); if (_processedIDs.Contains(instanceID)) { num3++; continue; } _processedIDs.Add(instanceID); float num5 = Vector3.SqrMagnitude(item.transform.position - position); if (num5 > 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)) { return; } lock (ContainerLock) { if (!AllContainers.Contains(__instance)) { AllContainers.Add(__instance); ContainerTracker containerTracker = __instance.gameObject.GetComponent(); if (containerTracker == null) { containerTracker = __instance.gameObject.AddComponent(); } containerTracker.MyContainer = __instance; DebugLogger.Verbose($"New container tracked: {__instance.name} (Total: {AllContainers.Count})"); } } } } } // ---- 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); 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 recipeMgr = RecipePinnerPlugin.Instance.RecipeMgr; List list = new List(); foreach (KeyValuePair pinnedRecipe in recipeMgr.PinnedRecipes) { list.Add($"{pinnedRecipe.Key}:{pinnedRecipe.Value}"); } File.WriteAllLines(savePath, list); DebugLogger.Log($"Saved {list.Count} pinned recipes 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 recipeMgr = RecipePinnerPlugin.Instance.RecipeMgr; if (!File.Exists(savePath)) { DebugLogger.Log("No save file found at: " + savePath); return; } try { recipeMgr.PinnedRecipes.Clear(); string[] array = File.ReadAllLines(savePath); int num = 0; int num2 = 0; string[] array2 = array; foreach (string text in array2) { if (string.IsNullOrWhiteSpace(text)) { continue; } if (text.Contains(":")) { string[] array3 = text.Split(new char[1] { ':' }); if (array3.Length == 2) { string key = array3[0].Trim(); string s = array3[1].Trim(); if (int.TryParse(s, out var result)) { recipeMgr.PinnedRecipes[key] = result; num++; } else { DebugLogger.Warning("Invalid count value in save file: " + text); num2++; } } } else if (!recipeMgr.PinnedRecipes.ContainsKey(text)) { recipeMgr.PinnedRecipes[text] = 1; num++; } } if (recipeMgr.PinnedRecipes.Count > RecipePinnerPlugin.MaximumPins.Value) { int count = recipeMgr.PinnedRecipes.Count; recipeMgr.PinnedRecipes = recipeMgr.PinnedRecipes.Take(RecipePinnerPlugin.MaximumPins.Value).ToDictionary((KeyValuePair k) => k.Key, (KeyValuePair v) => v.Value); DebugLogger.Warning($"Exceeded max pins limit - trimmed from {count} to {recipeMgr.PinnedRecipes.Count}"); } DebugLogger.Log($"Loaded {num} recipes from: {savePath} (Errors: {num2})"); } catch (Exception ex) { DebugLogger.Error("Failed to load pins", ex); } } 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() { return RecipePinnerPlugin.Instance != null && RecipePinnerPlugin.EnableDebugLogging != null && RecipePinnerPlugin.EnableDebugLogging.Value; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.LocalizationManager ---- namespace ValheimRecipePinner { public class LocalizationManager { private RecipePinnerPlugin _plugin; private 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" } }; public LocalizationManager(RecipePinnerPlugin plugin) { _plugin = plugin; DebugLogger.Log("LocalizationManager initialized"); } 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 directoryName = Path.GetDirectoryName(_plugin.Info.Location); string text2 = Path.Combine(directoryName, "RecipePinner_languages", text + ".json"); if (!File.Exists(text2)) { DebugLogger.Log("Language file not found: " + text2 + " - Using default English"); return; } try { string text3 = File.ReadAllText(text2); int num = 0; string[] array = text3.Split(new char[1] { '\n' }); foreach (string text4 in array) { if (!text4.Contains(":")) { continue; } string[] array2 = text4.Split(new char[1] { ':' }, 2); if (array2.Length == 2) { string text5 = array2[0].Trim().Trim(',', '"', ' ', '\t', '\r'); string value = array2[1].Trim().Trim(',', '"', ' ', '\t', '\r'); if (!string.IsNullOrEmpty(text5) && !string.IsNullOrEmpty(value)) { _localizedText[text5] = value; num++; } } } DebugLogger.Log($"Loaded {num} translations from: {text}.json"); } catch (Exception ex) { DebugLogger.Error("Failed to load language file: " + text2, 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 string CachedShadowHeader; public Sprite Icon; public int StackCount; public List Resources = new List(); public bool IsDirty = true; } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.PinnedResData ---- namespace ValheimRecipePinner { public class PinnedResData { public string ItemName; public string CachedName; public string CachedShadowName; public Sprite Icon; public int RequiredAmount; public int LastKnownAmount; public int LastKnownInvAmount; public string CachedAmountString; } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.RecipeManager ---- namespace ValheimRecipePinner { public class RecipeManager { public Dictionary PinnedRecipes = new Dictionary(); public List CachedPins = new List(); private Dictionary _fakeRecipeCache = new Dictionary(); private static readonly Regex CleanNameRegex = new Regex("<.*?>", RegexOptions.Compiled); private static readonly Regex ShadowCleanRegex = new Regex("|", RegexOptions.Compiled); private static readonly Regex AmountSuffixRegex = new Regex("\\s*[xX]?\\s*\\d+$", RegexOptions.Compiled); public void Cleanup() { DebugLogger.Log("RecipeManager cleanup started"); if (_fakeRecipeCache == null) { return; } int count = _fakeRecipeCache.Count; foreach (Recipe value in _fakeRecipeCache.Values) { UnityEngine.Object obj = value; if ((object)obj != null && obj != null) { UnityEngine.Object.Destroy(obj); } } _fakeRecipeCache.Clear(); DebugLogger.Log($"Cleaned up {count} fake recipes"); } public void RefreshRecipeCache() { DebugLogger.Verbose("Refreshing recipe cache..."); CachedPins.Clear(); if (ObjectDB.instance == null) { DebugLogger.Warning("Cannot refresh recipe cache - ObjectDB.instance is null"); return; } int num = 0; int num2 = 0; foreach (KeyValuePair pinnedRecipe in PinnedRecipes) { string key = pinnedRecipe.Key; int value = pinnedRecipe.Value; Recipe recipeByName = GetRecipeByName(key); if (recipeByName != null) { PinnedRecipeData pinnedRecipeData = new PinnedRecipeData { IsDirty = true, RecipeRef = recipeByName, StackCount = value }; if (recipeByName.m_item != null) { pinnedRecipeData.Icon = recipeByName.m_item.m_itemData.GetIcon(); pinnedRecipeData.RawName = recipeByName.m_item.m_itemData.m_shared.m_name; } else { GameObject gameObject = ZNetScene.instance?.GetPrefab(recipeByName.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 = recipeByName.name; } string text = pinnedRecipeData.RawName; if (Localization.instance != null) { text = Localization.instance.Localize(pinnedRecipeData.RawName); } text = text.Replace("\r", "").Replace("\n", ""); if (recipeByName.m_amount > 1) { text += $" (x{recipeByName.m_amount})"; } if (value > 1) { text = $"{value}x {text}"; } pinnedRecipeData.CachedHeader = text; pinnedRecipeData.CachedShadowHeader = text; Piece.Requirement[] resources = recipeByName.m_resources; foreach (Piece.Requirement requirement in resources) { if (requirement != null && !(requirement.m_resItem == null) && requirement.m_amount > 0) { 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 * value, LastKnownAmount = -1, LastKnownInvAmount = -1 }; string text2 = pinnedResData.ItemName; if (Localization.instance != null) { text2 = Localization.instance.Localize(pinnedResData.ItemName); } text2 = (pinnedResData.CachedName = text2.Replace("\r", "").Replace("\n", "")); pinnedResData.CachedShadowName = ShadowCleanRegex.Replace(text2, string.Empty); pinnedRecipeData.Resources.Add(pinnedResData); } } CachedPins.Add(pinnedRecipeData); num++; } else { DebugLogger.Warning("Recipe not found: " + key); num2++; } } DebugLogger.Log($"Recipe cache refreshed: {num} successful, {num2} failed"); if (Player.m_localPlayer != null && RecipePinnerPlugin.Instance != null) { RecipePinnerPlugin.Instance.UIMgr.UpdateUI(isVisible: true); } } public Recipe GetRecipeByName(string name) { if (ObjectDB.instance == null) { return null; } ItemDrop itemDrop = ObjectDB.instance.GetItemPrefab(name)?.GetComponent(); if (itemDrop != null) { Recipe recipe = ObjectDB.instance.GetRecipe(itemDrop.m_itemData); if (recipe != null) { DebugLogger.Verbose("Found standard recipe: " + name); return recipe; } } Recipe recipe2 = ObjectDB.instance.m_recipes.FirstOrDefault((Recipe r) => r.name == name); if (recipe2 != null) { DebugLogger.Verbose("Found recipe in ObjectDB: " + name); return recipe2; } if (_fakeRecipeCache.TryGetValue(name, out var value)) { DebugLogger.Verbose("Found cached fake recipe: " + name); return value; } 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 recipe3 = ScriptableObject.CreateInstance(); recipe3.hideFlags = HideFlags.HideAndDontSave; recipe3.name = name; recipe3.m_item = gameObject.GetComponent(); List list = component.m_resources.ToList(); recipe3.m_resources = new Piece.Requirement[list.Count]; for (int num = 0; num < list.Count; num++) { recipe3.m_resources[num] = list[num]; } _fakeRecipeCache[name] = recipe3; DebugLogger.Verbose("Created fake recipe for piece: " + name); return recipe3; } } DebugLogger.Warning("Recipe not found anywhere: " + name); return null; } public void ValidateAndCleanPins() { if (ObjectDB.instance == null) { DebugLogger.Warning("Cannot validate pins - ObjectDB.instance is null"); return; } DebugLogger.Log("Validating pinned recipes..."); List list = new List(); foreach (string key in PinnedRecipes.Keys) { Recipe recipeByName = GetRecipeByName(key); if (recipeByName == null) { list.Add(key); } } if (list.Count > 0) { foreach (string item in list) { PinnedRecipes.Remove(item); DebugLogger.Warning("Removed invalid recipe: " + item); } if (RecipePinnerPlugin.Instance != null) { RecipePinnerPlugin.Instance.DataMgr.SavePins(); } DebugLogger.Log($"Validation complete: {list.Count} invalid recipes removed"); } else { DebugLogger.Log("All pinned recipes are valid"); } } public void TryPinHoveredRecipe(InventoryGui gui) { DebugLogger.Verbose("Attempting to pin hovered recipe..."); Transform recipeListRoot = ReflectionHelper.GetRecipeListRoot(gui); object availableRecipes = ReflectionHelper.GetAvailableRecipes(gui); IList list = availableRecipes as IList; if (recipeListRoot == null || list == null) { DebugLogger.Verbose("Cannot pin - listRoot or availableRecipes is null"); return; } ScrollRect componentInParent = recipeListRoot.GetComponentInParent(); foreach (Transform item in recipeListRoot) { if (!item.gameObject.activeInHierarchy) { continue; } RectTransform rectTransform = item as RectTransform; if (rectTransform == null || !IsVisibleInScroll(rectTransform, componentInParent) || !InputHelper.IsMouseOverRect(rectTransform)) { 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(); DebugLogger.Verbose("Hovered text: '" + text3 + "'"); foreach (object item2 in list) { Recipe recipeFromObject = GetRecipeFromObject(item2); if (!(recipeFromObject != null)) { continue; } string rawRecipeName = GetRawRecipeName(recipeFromObject); if (!string.IsNullOrEmpty(rawRecipeName)) { string text4 = rawRecipeName; if (Localization.instance != null) { text4 = Localization.instance.Localize(rawRecipeName); } text4 = text4.Replace("\r", "").Replace("\n", ""); if (text4.Equals(text3, StringComparison.OrdinalIgnoreCase) || text4.Equals(text2, StringComparison.OrdinalIgnoreCase)) { DebugLogger.Log("Matched recipe: " + recipeFromObject.name); TogglePin(recipeFromObject.name); return; } } } } } public void TryPinHoveredPiece() { DebugLogger.Verbose("Attempting to pin hovered piece..."); if (!(Hud.instance == null)) { Piece hoveredPiece = ReflectionHelper.GetHoveredPiece(Hud.instance); if (hoveredPiece != null && hoveredPiece.m_resources != null && hoveredPiece.m_resources.Length != 0) { DebugLogger.Log("Pinning piece: " + hoveredPiece.name); TogglePin(hoveredPiece.name); } else { DebugLogger.Verbose("No valid piece to pin (Mouse must be over a recipe icon)"); } } } private void TogglePin(string recipeName) { bool flag = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); LocalizationManager localizationMgr = RecipePinnerPlugin.Instance.LocalizationMgr; if (PinnedRecipes.ContainsKey(recipeName)) { if (flag) { PinnedRecipes[recipeName]--; if (PinnedRecipes[recipeName] <= 0) { PinnedRecipes.Remove(recipeName); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, localizationMgr.GetText("unpinned")); DebugLogger.Log("Unpinned: " + recipeName); } else { string msg = string.Format(localizationMgr.GetText("decreased"), PinnedRecipes[recipeName]); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg); DebugLogger.Log($"Decreased pin count: {recipeName} = {PinnedRecipes[recipeName]}"); } } else { PinnedRecipes[recipeName]++; string msg2 = string.Format(localizationMgr.GetText("added_more"), PinnedRecipes[recipeName]); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, msg2); DebugLogger.Log($"Increased pin count: {recipeName} = {PinnedRecipes[recipeName]}"); } } else { if (flag) { return; } if (PinnedRecipes.Count < RecipePinnerPlugin.MaximumPins.Value) { PinnedRecipes.Add(recipeName, 1); 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(); } private string ExtractTextFromUI(Transform child) { Text componentInChildren = child.GetComponentInChildren(); if (componentInChildren != null) { return componentInChildren.text; } Component[] componentsInChildren = child.GetComponentsInChildren(includeInactive: true); Component[] array = componentsInChildren; foreach (Component component in array) { 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 Recipe GetRecipeFromObject(object data) { if (data == null) { return null; } if (data is Recipe result) { return result; } Type type = data.GetType(); if (type.Name.Contains("KeyValuePair")) { PropertyInfo property = type.GetProperty("Key"); if (property != null && property.GetValue(data, null) is Recipe result2) { return result2; } } FieldInfo field = type.GetField("m_recipe", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(data) as Recipe; } PropertyInfo property2 = type.GetProperty("Recipe"); if (property2 != null) { return property2.GetValue(data, null) as Recipe; } 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); } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.RecipePinnerPlugin ---- namespace ValheimRecipePinner { [BepInPlugin("com.Kadrio.RecipePinner", "Recipe Pinner", "1.1.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 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 HotkeyPin; public static ConfigEntry HotkeyClearAll; public static ConfigEntry HotkeyToggleVisibility; public static ConfigEntry HotkeyPageSwitch; public static ConfigEntry EnableChestScanning; public static ConfigEntry ChestScanRange; public static ConfigEntry ChestScanInterval; public static ConfigEntry UIScale; public static ConfigEntry FontSizeRecipeName; public static ConfigEntry FontSizeMaterials; public static ConfigEntry BackgroundOpacity; public static ConfigEntry ColorHeader; public static ConfigEntry ColorEnoughInInventory; public static ConfigEntry ColorEnoughWithChests; public static ConfigEntry ColorMissing; public static ConfigEntry ColorPaginationActive; public static ConfigEntry PaginationInactiveOpacity; public static ConfigEntry PaginationDotSize; public static ConfigEntry PaginationDotSpacing; 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 LocalizationManager LocalizationMgr; public RecipeManager RecipeMgr; public ContainerScanner ContainerMgr; public UIManager UIMgr; public DataPersistence DataMgr; internal bool _mluiMapListEnabled = false; internal bool _mluiNoMapListEnabled = false; internal bool _mluiInstalled = false; private bool _startupInitialized = false; private string _lastLanguage = ""; private string _currentSessionPlayer = null; private static bool _isUiVisible = true; 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("RecipePinner plugin initializing..."); LocalizationMgr = new LocalizationManager(this); RecipeMgr = new RecipeManager(); ContainerMgr = new ContainerScanner(); UIMgr = new UIManager(); DataMgr = new DataPersistence(); DebugLogger.Log("All managers initialized successfully"); Harmony harmony = new Harmony("com.Kadrio.RecipePinner"); harmony.PatchAll(typeof(RecipePinnerPlugin)); harmony.PatchAll(typeof(ContainerScanner)); DebugLogger.Log("Harmony patches applied successfully"); } private void BindConfigs() { EnableMod = base.Config.Bind("1 - 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(); } }; LanguageOverride = base.Config.Bind("1 - General", "LanguageOverride", "Auto", new ConfigDescription("Force a specific language (e.g., 'German', 'Turkish').", null, new ConfigurationManagerAttributes { Order = 98 })); LanguageOverride.SettingChanged += delegate { LocalizationMgr?.LoadTranslations(); RecipeMgr?.RefreshRecipeCache(); }; LayoutModeConfig = base.Config.Bind("1 - General", "LayoutMode", PinLayoutMode.AutoDetect, new ConfigDescription("Choose layout position.", null, new ConfigurationManagerAttributes { Order = 97 })); LayoutModeConfig.SettingChanged += delegate { UIMgr?.DestroyUI(); }; MaximumPins = base.Config.Bind("1 - General", "MaximumPins", 10, new ConfigDescription("Max pins allowed.", new AcceptableValueRange(1, 20), new ConfigurationManagerAttributes { Order = 96 })); MaximumPins.SettingChanged += delegate { UIMgr?.DestroyUI(); }; PinsPerPage = base.Config.Bind("1 - General", "PinsPerPage", 5, new ConfigDescription("How many pins to show per page.", new AcceptableValueRange(1, 10), new ConfigurationManagerAttributes { Order = 95 })); PinsPerPage.SettingChanged += delegate { UIMgr?.ResetPage(); UIMgr?.DestroyUI(); }; AutoUnpinAfterCrafting = base.Config.Bind("1 - General", "AutoUnpinAfterCrafting", defaultValue: true, new ConfigDescription("Unpin after crafting.", null, new ConfigurationManagerAttributes { Order = 95 })); HotkeyPin = base.Config.Bind("2 - Controls", "HotkeyPin", KeyCode.Mouse2, "Key to pin recipe."); HotkeyClearAll = base.Config.Bind("2 - Controls", "HotkeyClearAll", KeyCode.P, "Key to clear all pins."); HotkeyToggleVisibility = base.Config.Bind("2 - Controls", "HotkeyToggleVisibility", KeyCode.F7, "Key to toggle overlay."); HotkeyPageSwitch = base.Config.Bind("2 - Controls", "HotkeyPageSwitch", KeyCode.LeftAlt, "Key to cycle through pin pages."); EnableChestScanning = base.Config.Bind("3 - Chest Scanner", "EnableChestScanning", defaultValue: false, new ConfigDescription("Count materials in nearby chests.", null, new ConfigurationManagerAttributes { Order = 99 })); EnableChestScanning.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ChestScanRange = base.Config.Bind("3 - Chest Scanner", "ChestScanRange", 20f, new ConfigDescription("Scan radius.", new AcceptableValueRange(5f, 100f), new ConfigurationManagerAttributes { Order = 98 })); ChestScanInterval = base.Config.Bind("3 - Chest Scanner", "ChestScanInterval", 3f, new ConfigDescription("Scan frequency.", new AcceptableValueRange(0.5f, 10f), new ConfigurationManagerAttributes { Order = 97 })); UIScale = base.Config.Bind("4 - Visual Settings", "UIScale", 0.75f, new ConfigDescription("Global UI scale.", new AcceptableValueRange(0.3f, 3f))); UIScale.SettingChanged += delegate { UIMgr?.DestroyUI(); }; BackgroundOpacity = base.Config.Bind("4 - Visual Settings", "BackgroundOpacity", 0.45f, new ConfigDescription("Background opacity.", new AcceptableValueRange(0f, 1f))); FontSizeRecipeName = base.Config.Bind("4 - Visual Settings", "FontSizeRecipeName", 15, "Recipe name font size."); FontSizeRecipeName.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; FontSizeMaterials = base.Config.Bind("4 - Visual Settings", "FontSizeMaterials", 15, "Material font size."); FontSizeMaterials.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorHeader = base.Config.Bind("4 - Visual Settings", "ColorHeader", new Color(1f, 0.717f, 0.368f, 1f), "Recipe title color."); ColorHeader.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorEnoughInInventory = base.Config.Bind("4 - Visual Settings", "ColorEnoughInInventory", new Color(0f, 1f, 0f, 1f), "Color: Enough in inventory."); ColorEnoughInInventory.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorEnoughWithChests = base.Config.Bind("4 - Visual Settings", "ColorEnoughWithChests", new Color(1f, 1f, 0f, 1f), "Color: Enough with chests."); ColorEnoughWithChests.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorMissing = base.Config.Bind("4 - Visual Settings", "ColorMissing", new Color(1f, 0.33f, 0.33f, 1f), "Color: Missing materials."); ColorMissing.SettingChanged += delegate { RecipeMgr?.RefreshRecipeCache(); }; ColorPaginationActive = base.Config.Bind("4 - Visual Settings", "ColorPaginationActive", new Color(1f, 0.717f, 0.368f, 1f), "Active page dot color (Orange)."); ColorPaginationActive.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; PaginationInactiveOpacity = base.Config.Bind("4 - Visual Settings", "PaginationInactiveOpacity", 0.3f, new ConfigDescription("Opacity of inactive page dots (0.0 to 1.0).", new AcceptableValueRange(0.1f, 1f))); PaginationInactiveOpacity.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; PaginationDotSize = base.Config.Bind("4 - Visual Settings", "PaginationDotSize", 10, new ConfigDescription("Size of the pagination squares.", new AcceptableValueRange(5, 20))); PaginationDotSize.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; PaginationDotSpacing = base.Config.Bind("4 - Visual Settings", "PaginationDotSpacing", 8, new ConfigDescription("Space between pagination squares.", new AcceptableValueRange(0, 20))); PaginationDotSpacing.SettingChanged += delegate { UIMgr?.UpdateUI(isVisible: true); }; VerticalListWidth = base.Config.Bind("5 - Layout (Vertical Mode)", "ListWidth", 265f, "List width."); VerticalPinSpacing = base.Config.Bind("5 - Layout (Vertical Mode)", "PinSpacing", 10f, "Spacing between pins."); VerticalPosition = base.Config.Bind("5 - Layout (Vertical Mode)", "Position", new Vector2(-40f, -250f), "Position (X, Y)."); HorizontalColumnWidth = base.Config.Bind("6 - Layout (Horizontal - Map Side)", "ColumnWidth", 250f, "Column width."); HorizontalPinSpacing = base.Config.Bind("6 - Layout (Horizontal - Map Side)", "PinSpacing", 10f, "Spacing between pins."); HorizontalPosition = base.Config.Bind("6 - Layout (Horizontal - Map Side)", "Position", new Vector2(-250f, -40f), "Position (X, Y)."); BottomRightColumnWidth = base.Config.Bind("7 - Layout (Horizontal - Bottom Right)", "ColumnWidth", 250f, "Column width."); BottomRightPinSpacing = base.Config.Bind("7 - Layout (Horizontal - Bottom Right)", "PinSpacing", 10f, "Spacing between pins."); BottomRightPosition = base.Config.Bind("7 - Layout (Horizontal - Bottom Right)", "Position", new Vector2(-40f, 40f), "Position (X, Y)."); EnableDebugLogging = base.Config.Bind("8 - Debug", "EnableDebugLogging", defaultValue: false, "Enable debug logs."); DebugLogger.Log("Configuration loaded successfully"); } private void Start() { DebugLogger.Log("Start() called - Loading translations and initializing containers"); LocalizationMgr.LoadTranslations(); ReadMyLittleUIConfig(); ContainerMgr.InitializeContainers(); DebugLogger.Log("Start() completed successfully"); } private void OnDestroy() { DebugLogger.Log("Plugin destroyed - Cleaning up"); if (Player.m_localPlayer != null) { DataMgr.SavePins(); } RecipeMgr.Cleanup(); } 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-time initialization triggered"); _lastLanguage = Localization.instance.GetSelectedLanguage(); DataMgr.LoadPins(); RecipeMgr.ValidateAndCleanPins(); RecipeMgr.RefreshRecipeCache(); _startupInitialized = true; DebugLogger.Log($"Initialization complete - {RecipeMgr.PinnedRecipes.Count} recipes loaded"); } if (EnableChestScanning.Value && Player.m_localPlayer != null && RecipeMgr.CachedPins.Count > 0) { ContainerMgr.UpdateScanning(); } if (Input.GetKeyDown(HotkeyToggleVisibility.Value) && !InputHelper.IsInputBlocked()) { _isUiVisible = !_isUiVisible; DebugLogger.Log($"UI visibility toggled: {_isUiVisible}"); } if (Player.m_localPlayer != null) { UpdatePlayerSession(); } if (Input.GetKeyDown(HotkeyPin.Value)) { if (InventoryGui.instance != null && InventoryGui.IsVisible()) { RecipeMgr.TryPinHoveredRecipe(InventoryGui.instance); } else if (Hud.instance != null && Player.m_localPlayer != null && Player.m_localPlayer.InPlaceMode()) { RecipeMgr.TryPinHoveredPiece(); } } if (Input.GetKeyDown(HotkeyClearAll.Value)) { if (InputHelper.IsInputBlocked()) { return; } if (RecipeMgr.PinnedRecipes.Count > 0) { int count = RecipeMgr.PinnedRecipes.Count; RecipeMgr.PinnedRecipes.Clear(); RecipeMgr.RefreshRecipeCache(); Player.m_localPlayer?.Message(MessageHud.MessageType.Center, LocalizationMgr.GetText("cleared")); DebugLogger.Log($"Cleared {count} pinned recipes"); } } 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(); } } } if (Input.GetKeyDown(HotkeyPageSwitch.Value) && _isUiVisible && !InputHelper.IsInputBlocked()) { UIMgr?.CyclePage(); } } private void UpdatePlayerSession() { if (Player.m_localPlayer == null || Player.m_localPlayer.IsDead()) { 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.CachedPins.Clear(); UIMgr.DestroyUI(); _currentSessionPlayer = playerName; if (!string.IsNullOrEmpty(playerName)) { DataMgr.LoadPins(); 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; foreach (string text2 in array2) { string text3 = text2.Trim(); if (text3.StartsWith("[") && text3.EndsWith("]")) { text = text3; } else if (text3.StartsWith("Enable")) { bool flag = text3.ToLower().Contains("true"); if (text == "[Status effects - Map - List]") { _mluiMapListEnabled = flag; } else if (text == "[Status effects - Nomap - List]") { _mluiNoMapListEnabled = flag; } } } DebugLogger.Log($"MyLittleUI Config: MapList={_mluiMapListEnabled}, NoMapList={_mluiNoMapListEnabled}"); } catch (Exception ex) { Debug.LogWarning("[RecipePinner] Error reading MyLittleUI config: " + ex.Message); } } [HarmonyPatch(typeof(Game), "SavePlayerProfile")] [HarmonyPostfix] public static void AutoSavePinsHook() { if (Player.m_localPlayer != null && Instance != null) { DebugLogger.Log("Auto-saving pins on profile save"); 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 && Instance.RecipeMgr.PinnedRecipes.ContainsKey(craftRecipe.name)) { Instance.RecipeMgr.PinnedRecipes[craftRecipe.name]--; DebugLogger.Log($"Auto-unpin: {craftRecipe.name}, remaining count: {Instance.RecipeMgr.PinnedRecipes[craftRecipe.name]}"); if (Instance.RecipeMgr.PinnedRecipes[craftRecipe.name] <= 0) { Instance.RecipeMgr.PinnedRecipes.Remove(craftRecipe.name); DebugLogger.Log("Recipe " + craftRecipe.name + " fully unpinned"); } Instance.RecipeMgr.RefreshRecipeCache(); } } [HarmonyPatch(typeof(Player), "ConsumeResources")] [HarmonyPostfix] public static void AutoUnpinBuildHook() { if (Instance == null || !EnableMod.Value || !AutoUnpinAfterCrafting.Value) { return; } Player localPlayer = Player.m_localPlayer; if (localPlayer == null) { return; } PieceTable pieceTable = ReflectionHelper.GetPieceTable(localPlayer); if (pieceTable == null) { return; } Piece selectedPiece = pieceTable.GetSelectedPiece(); if (selectedPiece == null) { return; } string text = selectedPiece.name.Replace("(Clone)", "").Trim(); if (Instance.RecipeMgr.PinnedRecipes.ContainsKey(text)) { Instance.RecipeMgr.PinnedRecipes[text]--; DebugLogger.Log($"Auto-unpin (Build): {text}, remaining count: {Instance.RecipeMgr.PinnedRecipes[text]}"); if (Instance.RecipeMgr.PinnedRecipes[text] <= 0) { Instance.RecipeMgr.PinnedRecipes.Remove(text); DebugLogger.Log("Build recipe " + text + " fully unpinned"); } Instance.RecipeMgr.RefreshRecipeCache(); } } } } // ---- 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 _getHoveredPiece; public static Func CheckContainerAccess; private static FieldInfo _f_buildPieces; public static float currentGuiScaleValue; static ReflectionHelper() { currentGuiScaleValue = 1f; InitializeReflection(); } public static void InitializeReflection() { DebugLogger.Log("Initializing reflection helpers..."); 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(Hud), "m_hoveredPiece"); if (fieldInfo6 != null) { ParameterExpression parameterExpression5 = Expression.Parameter(typeof(Hud), "arg"); _getHoveredPiece = Expression.Lambda>(Expression.Field(parameterExpression5, fieldInfo6), new ParameterExpression[1] { parameterExpression5 }).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 initialization complete: {num} successful, {num2} failed"); if (num2 > 0) { DebugLogger.Warning("Some reflection targets failed - mod may not work correctly!"); } _f_buildPieces = AccessTools.Field(typeof(Player), "m_buildPieces"); if (_f_buildPieces != null) { num++; DebugLogger.Verbose("✓ Player.m_buildPieces"); } else { num2++; DebugLogger.Warning("✗ Player.m_buildPieces not found"); } } catch (Exception ex) { DebugLogger.Error("Critical error during reflection initialization", 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 Piece GetHoveredPiece(Hud hud) { if (_getHoveredPiece == null) { DebugLogger.Verbose("GetHoveredPiece delegate is null"); return null; } return _getHoveredPiece(hud); } public static PieceTable GetPieceTable(Player player) { if (_f_buildPieces == null || player == null) { return null; } return _f_buildPieces.GetValue(player) as PieceTable; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.InputHelper ---- namespace ValheimRecipePinner { public static class InputHelper { public static bool IsInputBlocked() { if (Console.IsVisible()) { return true; } if (Chat.instance != null && Chat.instance.HasFocus()) { return true; } if (TextInput.IsVisible()) { DebugLogger.Verbose("Input blocked: TextInput is visible"); return true; } return false; } public static bool IsMouseOverRect(RectTransform rect) { if (rect == null) { DebugLogger.Verbose("IsMouseOverRect: rect is null"); return false; } bool flag = RectTransformUtility.RectangleContainsScreenPoint(rect, Input.mousePosition); if (flag) { DebugLogger.Verbose("Mouse over rect: " + rect.gameObject.name); } return flag; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.UIBuilder ---- namespace ValheimRecipePinner { public static class UIBuilder { private static Color ValheimOrange = new Color(1f, 0.77f, 0.31f, 1f); private static Color DividerColor = new Color(1f, 1f, 1f, 0.1f); private static Sprite _cachedUiSprite; private static bool _spriteSearchDone = false; private static Sprite GetBackgroundSprite() { if (_cachedUiSprite != null) { return _cachedUiSprite; } if (_spriteSearchDone) { return null; } Sprite[] source = Resources.FindObjectsOfTypeAll(); _cachedUiSprite = source.FirstOrDefault((Sprite x) => x.name == "UISprite") ?? source.FirstOrDefault((Sprite x) => x.name == "Knob"); _spriteSearchDone = true; if (_cachedUiSprite != null) { DebugLogger.Verbose("Found background sprite: " + _cachedUiSprite.name); } else { DebugLogger.Warning("No suitable background sprite found"); } return _cachedUiSprite; } public static PinSlotUI CreatePinSlot(Transform parent, Font font) { GameObject gameObject = new GameObject("PinSlot", typeof(RectTransform)); gameObject.layer = 5; gameObject.transform.SetParent(parent, worldPositionStays: false); PinSlotUI pinSlotUI = gameObject.AddComponent(); pinSlotUI.Rect = gameObject.GetComponent(); Image image = gameObject.AddComponent(); Sprite sprite = (image.sprite = GetBackgroundSprite()); if (sprite != null && sprite.border != Vector4.zero) { image.type = Image.Type.Sliced; } else { image.type = Image.Type.Simple; } float a = ((RecipePinnerPlugin.BackgroundOpacity != null) ? RecipePinnerPlugin.BackgroundOpacity.Value : 0.45f); image.color = new Color(0f, 0f, 0f, a); image.raycastTarget = false; VerticalLayoutGroup verticalLayoutGroup = gameObject.AddComponent(); verticalLayoutGroup.childControlHeight = true; verticalLayoutGroup.childControlWidth = true; verticalLayoutGroup.childForceExpandHeight = false; verticalLayoutGroup.spacing = 5f; verticalLayoutGroup.padding = new RectOffset(8, 8, 8, 8); ContentSizeFitter contentSizeFitter = gameObject.AddComponent(); contentSizeFitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained; contentSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; GameObject gameObject2 = new GameObject("HeaderRow", typeof(RectTransform)); gameObject2.layer = 5; gameObject2.transform.SetParent(gameObject.transform, worldPositionStays: false); HorizontalLayoutGroup horizontalLayoutGroup = gameObject2.AddComponent(); horizontalLayoutGroup.childControlHeight = true; horizontalLayoutGroup.childControlWidth = true; horizontalLayoutGroup.childForceExpandHeight = false; horizontalLayoutGroup.childForceExpandWidth = false; horizontalLayoutGroup.spacing = 8f; LayoutElement layoutElement = gameObject2.AddComponent(); layoutElement.minHeight = 30f; layoutElement.flexibleHeight = 0f; layoutElement.flexibleWidth = 1f; GameObject gameObject3 = new GameObject("Icon", typeof(RectTransform)); gameObject3.layer = 5; gameObject3.transform.SetParent(gameObject2.transform, worldPositionStays: false); Image image2 = gameObject3.AddComponent(); image2.raycastTarget = false; image2.preserveAspect = true; pinSlotUI.IconImage = image2; LayoutElement layoutElement2 = gameObject3.AddComponent(); layoutElement2.minWidth = 28f; layoutElement2.minHeight = 28f; layoutElement2.preferredWidth = 28f; layoutElement2.preferredHeight = 28f; layoutElement2.flexibleWidth = 0f; GameObject gameObject4 = new GameObject("Title", typeof(RectTransform)); gameObject4.layer = 5; gameObject4.transform.SetParent(gameObject2.transform, worldPositionStays: false); Text text = gameObject4.AddComponent(); text.raycastTarget = false; text.font = font; text.fontSize = 18; text.alignment = TextAnchor.MiddleLeft; text.horizontalOverflow = HorizontalWrapMode.Wrap; text.verticalOverflow = VerticalWrapMode.Overflow; text.color = ValheimOrange; pinSlotUI.HeaderText = text; LayoutElement layoutElement3 = gameObject4.AddComponent(); layoutElement3.minHeight = 24f; layoutElement3.flexibleWidth = 1f; GameObject gameObject5 = new GameObject("Divider", typeof(RectTransform)); gameObject5.layer = 5; gameObject5.transform.SetParent(gameObject.transform, worldPositionStays: false); Image image3 = gameObject5.AddComponent(); image3.sprite = GetBackgroundSprite(); if (sprite != null && sprite.border != Vector4.zero) { image3.type = Image.Type.Sliced; } else { image3.type = Image.Type.Simple; } image3.color = DividerColor; image3.raycastTarget = false; LayoutElement layoutElement4 = gameObject5.AddComponent(); layoutElement4.minHeight = 2f; layoutElement4.preferredHeight = 2f; layoutElement4.flexibleWidth = 1f; GameObject gameObject6 = new GameObject("ResourceList", typeof(RectTransform)); gameObject6.layer = 5; gameObject6.transform.SetParent(gameObject.transform, worldPositionStays: false); VerticalLayoutGroup verticalLayoutGroup2 = gameObject6.AddComponent(); verticalLayoutGroup2.childControlHeight = true; verticalLayoutGroup2.childControlWidth = true; verticalLayoutGroup2.childForceExpandHeight = false; verticalLayoutGroup2.spacing = 3f; ContentSizeFitter contentSizeFitter2 = gameObject6.AddComponent(); contentSizeFitter2.verticalFit = ContentSizeFitter.FitMode.PreferredSize; pinSlotUI.ResourceListRoot = gameObject6.transform; DebugLogger.Verbose("Created pin slot UI"); return pinSlotUI; } public static ResourceSlotUI CreateResourceSlot(Transform parent, Font font) { GameObject gameObject = new GameObject("ResSlot", typeof(RectTransform)); gameObject.layer = 5; gameObject.transform.SetParent(parent, worldPositionStays: false); ResourceSlotUI resourceSlotUI = gameObject.AddComponent(); HorizontalLayoutGroup horizontalLayoutGroup = gameObject.AddComponent(); horizontalLayoutGroup.childControlHeight = true; horizontalLayoutGroup.childControlWidth = true; horizontalLayoutGroup.childForceExpandHeight = false; horizontalLayoutGroup.childForceExpandWidth = false; horizontalLayoutGroup.spacing = 6f; LayoutElement layoutElement = gameObject.AddComponent(); layoutElement.minHeight = 22f; layoutElement.flexibleHeight = 0f; GameObject gameObject2 = new GameObject("Icon", typeof(RectTransform)); gameObject2.layer = 5; gameObject2.transform.SetParent(gameObject.transform, worldPositionStays: false); resourceSlotUI.ResIcon = gameObject2.AddComponent(); resourceSlotUI.ResIcon.raycastTarget = false; resourceSlotUI.ResIcon.preserveAspect = true; LayoutElement layoutElement2 = gameObject2.AddComponent(); layoutElement2.minWidth = 20f; layoutElement2.minHeight = 20f; layoutElement2.preferredWidth = 20f; layoutElement2.preferredHeight = 20f; layoutElement2.flexibleWidth = 0f; GameObject gameObject3 = new GameObject("Name", typeof(RectTransform)); gameObject3.layer = 5; gameObject3.transform.SetParent(gameObject.transform, worldPositionStays: false); resourceSlotUI.ResName = gameObject3.AddComponent(); resourceSlotUI.ResName.raycastTarget = false; resourceSlotUI.ResName.font = font; resourceSlotUI.ResName.fontSize = 15; resourceSlotUI.ResName.alignment = TextAnchor.MiddleLeft; resourceSlotUI.ResName.horizontalOverflow = HorizontalWrapMode.Wrap; resourceSlotUI.ResName.color = new Color(0.9f, 0.9f, 0.9f, 1f); LayoutElement layoutElement3 = gameObject3.AddComponent(); layoutElement3.flexibleWidth = 1f; GameObject gameObject4 = new GameObject("Amount", typeof(RectTransform)); gameObject4.layer = 5; gameObject4.transform.SetParent(gameObject.transform, worldPositionStays: false); resourceSlotUI.ResAmount = gameObject4.AddComponent(); resourceSlotUI.ResAmount.raycastTarget = false; resourceSlotUI.ResAmount.font = font; resourceSlotUI.ResAmount.fontSize = 15; resourceSlotUI.ResAmount.alignment = TextAnchor.MiddleRight; LayoutElement layoutElement4 = gameObject4.AddComponent(); layoutElement4.minWidth = 40f; DebugLogger.Verbose("Created resource slot UI"); return resourceSlotUI; } public static GameObject CreatePaginationContainer(Transform parent) { GameObject gameObject = new GameObject("PaginationDots", typeof(RectTransform)); gameObject.layer = 5; gameObject.transform.SetParent(parent, worldPositionStays: false); LayoutElement layoutElement = gameObject.AddComponent(); layoutElement.ignoreLayout = true; HorizontalLayoutGroup horizontalLayoutGroup = gameObject.AddComponent(); horizontalLayoutGroup.childControlHeight = false; horizontalLayoutGroup.childControlWidth = false; horizontalLayoutGroup.childForceExpandHeight = false; horizontalLayoutGroup.childForceExpandWidth = false; horizontalLayoutGroup.spacing = RecipePinnerPlugin.PaginationDotSpacing.Value; horizontalLayoutGroup.childAlignment = TextAnchor.MiddleCenter; ContentSizeFitter contentSizeFitter = gameObject.AddComponent(); contentSizeFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; contentSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; return gameObject; } public static Image CreatePageDot(Transform parent) { GameObject gameObject = new GameObject("PageDot", typeof(RectTransform)); gameObject.layer = 5; gameObject.transform.SetParent(parent, worldPositionStays: false); Image image = gameObject.AddComponent(); image.type = Image.Type.Simple; image.raycastTarget = false; int value = RecipePinnerPlugin.PaginationDotSize.Value; RectTransform component = gameObject.GetComponent(); component.sizeDelta = new Vector2(value, value); component.localRotation = Quaternion.Euler(0f, 0f, 45f); return image; } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.PinSlotUI ---- namespace ValheimRecipePinner { public class PinSlotUI : MonoBehaviour { public RectTransform Rect; public Image IconImage; public Text HeaderText; public Transform ResourceListRoot; public PinnedRecipeData CurrentData; private Coroutine _layoutCoroutine; private Image _cachedBg; private ContentSizeFitter _cachedCsf; public List ResourceSlots = new List(); public Image BgImage => _cachedBg ? _cachedBg : (_cachedBg = GetComponent()); public ContentSizeFitter Csf => _cachedCsf ? _cachedCsf : (_cachedCsf = GetComponent()); public void SetActive(bool active) { base.gameObject.SetActive(active); } public void UpdateData(PinnedRecipeData data, Font font) { IconImage.sprite = data.Icon; HeaderText.text = data.CachedHeader; HeaderText.font = font; HeaderText.fontSize = RecipePinnerPlugin.FontSizeRecipeName.Value; HeaderText.color = RecipePinnerPlugin.ColorHeader.Value; int count = data.Resources.Count; while (ResourceSlots.Count < count) { ResourceSlots.Add(UIBuilder.CreateResourceSlot(ResourceListRoot, font)); } for (int i = 0; i < ResourceSlots.Count; i++) { if (i < count) { ResourceSlots[i].SetActive(active: true); ResourceSlots[i].UpdateResource(data.Resources[i]); } else { ResourceSlots[i].SetActive(active: false); } } if (base.gameObject.activeInHierarchy) { if (_layoutCoroutine != null) { StopCoroutine(_layoutCoroutine); } _layoutCoroutine = StartCoroutine(FixLayout()); } } private void OnDisable() { _layoutCoroutine = null; } private IEnumerator FixLayout() { yield return null; if (ResourceListRoot != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(ResourceListRoot as RectTransform); } if (Rect != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(Rect); } if (base.transform.parent != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(base.transform.parent as RectTransform); } } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.ResourceSlotUI ---- namespace ValheimRecipePinner { public class ResourceSlotUI : MonoBehaviour { public Image ResIcon; public Text ResName; public Text ResAmount; public void SetActive(bool active) { base.gameObject.SetActive(active); } public void UpdateResource(PinnedResData res) { ResIcon.sprite = res.Icon; ResName.text = res.CachedName; ResAmount.text = res.CachedAmountString; if (RecipePinnerPlugin.FontSizeMaterials != null) { int value = RecipePinnerPlugin.FontSizeMaterials.Value; ResName.fontSize = value; ResAmount.fontSize = value; } } } } // ---- plugins/RecipePinner.dll :: ValheimRecipePinner.UIManager ---- namespace ValheimRecipePinner { public class UIManager { private Transform _pinListRoot; private List _pinPool = new List(); private Font _cachedFont; private static Dictionary _reusableInvCounts = new Dictionary(); private int _currentPage = 0; private GameObject _paginationRoot; public void DestroyUI() { DebugLogger.Verbose("Destroying UI..."); if (_pinListRoot != null) { Object.Destroy(_pinListRoot.gameObject); _pinListRoot = null; } if (_pinPool != null) { _pinPool.Clear(); } DebugLogger.Log("UI destroyed successfully"); } public void ResetPage() { _currentPage = 0; } public void CyclePage() { RecipeManager recipeMgr = RecipePinnerPlugin.Instance.RecipeMgr; int count = recipeMgr.CachedPins.Count; int value = RecipePinnerPlugin.PinsPerPage.Value; if (count > value) { int num = Mathf.CeilToInt((float)count / (float)value); _currentPage++; if (_currentPage >= num) { _currentPage = 0; } DebugLogger.Log($"Switched to Page: {_currentPage + 1}/{num}"); UpdateUI(isVisible: true); } } public void UpdateUI(bool isVisible) { if (Player.m_localPlayer == null || Player.m_localPlayer.IsDead()) { return; } Inventory inventory = Player.m_localPlayer.GetInventory(); if (inventory == null) { return; } RecipePinnerPlugin instance = RecipePinnerPlugin.Instance; RecipeManager recipeMgr = instance.RecipeMgr; ContainerScanner containerMgr = instance.ContainerMgr; if (_pinPool.Count < RecipePinnerPlugin.MaximumPins.Value) { DebugLogger.Log($"Pin limit changed (Pool: {_pinPool.Count}, Config: {RecipePinnerPlugin.MaximumPins.Value}). Rebuilding UI..."); DestroyUI(); } if (_pinListRoot == null) { _pinPool.Clear(); CreateCanvasUI(); if (_pinListRoot == null) { return; } foreach (PinnedRecipeData cachedPin in recipeMgr.CachedPins) { cachedPin.IsDirty = true; } } UpdateLayout(); if (_pinListRoot == null) { return; } bool flag = isVisible && !InputHelper.IsInputBlocked() && recipeMgr.CachedPins.Count > 0; if (_pinListRoot.gameObject.activeSelf != flag) { _pinListRoot.gameObject.SetActive(flag); } if (!flag) { return; } _reusableInvCounts.Clear(); foreach (ItemDrop.ItemData allItem in inventory.GetAllItems()) { string name = allItem.m_shared.m_name; if (_reusableInvCounts.ContainsKey(name)) { _reusableInvCounts[name] += allItem.m_stack; } else { _reusableInvCounts[name] = allItem.m_stack; } } int count = recipeMgr.CachedPins.Count; int value = RecipePinnerPlugin.PinsPerPage.Value; int num = _currentPage * value; if (num >= count && _currentPage > 0) { _currentPage--; num = _currentPage * value; } int num2 = Mathf.Min(num + value, count); for (int i = 0; i < _pinPool.Count; i++) { if (!(_pinPool[i] == null)) { int num3 = num + i; if (num3 < num2) { UpdatePinSlot(i, recipeMgr.CachedPins[num3], containerMgr); } else if (_pinPool[i].gameObject.activeSelf) { _pinPool[i].SetActive(active: false); } } } int totalPages = 1; if (count > 0) { totalPages = Mathf.CeilToInt((float)count / (float)value); } UpdatePageDots(totalPages); } private void UpdatePageDots(int totalPages) { if (_paginationRoot == null) { return; } HorizontalLayoutGroup component = _paginationRoot.GetComponent(); if (component != null) { component.spacing = RecipePinnerPlugin.PaginationDotSpacing.Value; } if (totalPages <= 1) { if (_paginationRoot.activeSelf) { _paginationRoot.SetActive(value: false); } return; } if (!_paginationRoot.activeSelf) { _paginationRoot.SetActive(value: true); } foreach (Transform item in _paginationRoot.transform) { Object.Destroy(item.gameObject); } int value = RecipePinnerPlugin.PaginationDotSize.Value; Color value2 = RecipePinnerPlugin.ColorPaginationActive.Value; for (int i = 0; i < totalPages; i++) { Image image = UIBuilder.CreatePageDot(_paginationRoot.transform); if (i == _currentPage) { image.color = value2; image.rectTransform.sizeDelta = new Vector2((float)value * 1.2f, (float)value * 1.2f); continue; } Color color = value2; color.a = RecipePinnerPlugin.PaginationInactiveOpacity.Value; image.color = color; image.rectTransform.sizeDelta = new Vector2(value, value); } } private void UpdateDotsPosition() { if (_paginationRoot == null || _pinListRoot == null) { return; } RectTransform component = _paginationRoot.GetComponent(); bool flag = RecipePinnerPlugin.LayoutModeConfig.Value == RecipePinnerPlugin.PinLayoutMode.ForceBottomRightHorizontal; bool flag2 = Player.m_localPlayer.GetControlledShip() != null; if (RecipePinnerPlugin.Instance.IsHorizontalMode || flag2) { float num = ((flag || flag2) ? RecipePinnerPlugin.BottomRightColumnWidth.Value : RecipePinnerPlugin.HorizontalColumnWidth.Value); float x = 0f - num / 2f; if (flag || flag2) { component.anchorMin = new Vector2(1f, 0f); component.anchorMax = new Vector2(1f, 0f); component.pivot = new Vector2(0.5f, 1f); component.anchoredPosition = new Vector2(x, -15f); } else { component.anchorMin = new Vector2(1f, 1f); component.anchorMax = new Vector2(1f, 1f); component.pivot = new Vector2(0.5f, 0f); component.anchoredPosition = new Vector2(x, 15f); } } else { component.anchorMin = new Vector2(0.5f, 1f); component.anchorMax = new Vector2(0.5f, 1f); component.pivot = new Vector2(0.5f, 0f); component.anchoredPosition = new Vector2(0f, 20f); } } private void UpdatePinSlot(int index, PinnedRecipeData pinData, ContainerScanner containerMgr) { PinSlotUI pinSlotUI = _pinPool[index]; if (pinSlotUI == null || pinSlotUI.gameObject == null) { return; } if (!pinSlotUI.gameObject.activeSelf) { pinSlotUI.SetActive(active: true); } if (pinSlotUI.BgImage != null) { float a = pinSlotUI.BgImage.color.a; if (Mathf.Abs(a - RecipePinnerPlugin.BackgroundOpacity.Value) > 0.01f) { pinSlotUI.BgImage.color = new Color(0f, 0f, 0f, RecipePinnerPlugin.BackgroundOpacity.Value); } } bool flag = RecipePinnerPlugin.LayoutModeConfig.Value == RecipePinnerPlugin.PinLayoutMode.ForceBottomRightHorizontal; bool flag2 = Player.m_localPlayer.GetControlledShip() != null; if (RecipePinnerPlugin.Instance.IsHorizontalMode || flag2) { RectTransform rectTransform = pinSlotUI.Rect ?? pinSlotUI.GetComponent(); float num = ((flag || flag2) ? RecipePinnerPlugin.BottomRightColumnWidth.Value : RecipePinnerPlugin.HorizontalColumnWidth.Value); if (Mathf.Abs(rectTransform.sizeDelta.x - num) > 1f) { rectTransform.sizeDelta = new Vector2(num, rectTransform.sizeDelta.y); } } bool flag3 = pinSlotUI.CurrentData != pinData; pinSlotUI.CurrentData = pinData; foreach (PinnedResData resource in pinData.Resources) { int value = 0; _reusableInvCounts.TryGetValue(resource.ItemName, out value); int num2 = 0; if (RecipePinnerPlugin.EnableChestScanning.Value && containerMgr.ContainerCache.ContainsKey(resource.ItemName)) { num2 = containerMgr.ContainerCache[resource.ItemName]; } int num3 = value + num2; if (num3 != resource.LastKnownAmount || value != resource.LastKnownInvAmount || resource.CachedAmountString == null) { resource.LastKnownAmount = num3; resource.LastKnownInvAmount = value; Color color = ((value >= resource.RequiredAmount) ? RecipePinnerPlugin.ColorEnoughInInventory.Value : ((num3 >= resource.RequiredAmount) ? RecipePinnerPlugin.ColorEnoughWithChests.Value : RecipePinnerPlugin.ColorMissing.Value)); string arg = "#" + ColorUtility.ToHtmlStringRGBA(color); string text = $"{num3}/{resource.RequiredAmount}"; if (resource.CachedAmountString != text) { resource.CachedAmountString = text; pinData.IsDirty = true; } } } if (flag3 || pinData.IsDirty) { pinSlotUI.UpdateData(pinData, _cachedFont); pinData.IsDirty = false; } } private void CreateCanvasUI() { if (_pinListRoot != null) { return; } if (Hud.instance == null || Hud.instance.m_rootObject == null) { DebugLogger.Warning("Cannot create canvas - Hud.instance is null"); return; } if (_cachedFont == null) { _cachedFont = GetGameFont(); } if (_cachedFont == null) { DebugLogger.Error("Cannot create UI - no valid font found"); return; } DebugLogger.Log("Creating canvas UI..."); Transform transform = Hud.instance.m_rootObject.transform; GameObject gameObject = new GameObject("PinListRoot", typeof(RectTransform)); gameObject.layer = 5; gameObject.transform.SetParent(transform, worldPositionStays: false); _pinListRoot = gameObject.transform; _pinListRoot.localScale = Vector3.one * RecipePinnerPlugin.UIScale.Value; RectTransform component = gameObject.GetComponent(); component.anchorMin = new Vector2(1f, 1f); component.anchorMax = new Vector2(1f, 1f); component.pivot = new Vector2(1f, 1f); bool flag = Player.m_localPlayer != null && Player.m_localPlayer.GetControlledShip() != null; bool flag2 = RecipePinnerPlugin.LayoutModeConfig.Value == RecipePinnerPlugin.PinLayoutMode.ForceBottomRightHorizontal; bool flag3 = flag || flag2; bool flag4 = RecipePinnerPlugin.Instance.IsHorizontalMode || flag; DebugLogger.Verbose($"UI Layout - Horizontal: {flag4}, BottomRight: {flag3}, Sailing: {flag}"); if (flag4) { HorizontalLayoutGroup horizontalLayoutGroup = gameObject.AddComponent(); horizontalLayoutGroup.childControlHeight = true; horizontalLayoutGroup.childControlWidth = false; horizontalLayoutGroup.childForceExpandHeight = false; horizontalLayoutGroup.childForceExpandWidth = false; horizontalLayoutGroup.childAlignment = (flag3 ? TextAnchor.LowerRight : TextAnchor.UpperRight); horizontalLayoutGroup.spacing = RecipePinnerPlugin.HorizontalPinSpacing.Value; ContentSizeFitter contentSizeFitter = gameObject.AddComponent(); contentSizeFitter.horizontalFit = ContentSizeFitter.FitMode.PreferredSize; contentSizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; } else { VerticalLayoutGroup verticalLayoutGroup = gameObject.AddComponent(); verticalLayoutGroup.childControlHeight = true; verticalLayoutGroup.childControlWidth = true; verticalLayoutGroup.childForceExpandHeight = false; verticalLayoutGroup.spacing = 8f; ContentSizeFitter contentSizeFitter2 = gameObject.AddComponent(); contentSizeFitter2.verticalFit = ContentSizeFitter.FitMode.PreferredSize; } for (int i = 0; i < RecipePinnerPlugin.MaximumPins.Value; i++) { PinSlotUI pinSlotUI = UIBuilder.CreatePinSlot(_pinListRoot, _cachedFont); if (pinSlotUI != null) { pinSlotUI.SetActive(active: false); _pinPool.Add(pinSlotUI); } } _paginationRoot = UIBuilder.CreatePaginationContainer(_pinListRoot); _paginationRoot = UIBuilder.CreatePaginationContainer(_pinListRoot); _paginationRoot.transform.SetAsLastSibling(); DebugLogger.Log($"Canvas UI created with {_pinPool.Count} pin slots"); } private void UpdateLayout() { if (_pinListRoot == null) { return; } RecipePinnerPlugin instance = RecipePinnerPlugin.Instance; bool flag = RecipePinnerPlugin.LayoutModeConfig.Value == RecipePinnerPlugin.PinLayoutMode.ForceBottomRightHorizontal; bool flag2 = !flag && Player.m_localPlayer.GetControlledShip() != null; bool isBottomRightMode = flag || flag2; bool flag3 = instance.IsHorizontalMode || flag2; HorizontalLayoutGroup component = _pinListRoot.GetComponent(); bool flag4 = component != null; if (flag3 != flag4) { DebugLogger.Log("Layout mode changed - rebuilding UI"); DestroyUI(); return; } if (_pinListRoot.localScale.x != RecipePinnerPlugin.UIScale.Value) { _pinListRoot.localScale = Vector3.one * RecipePinnerPlugin.UIScale.Value; } RectTransform component2 = _pinListRoot.GetComponent(); if (flag3) { UpdateHorizontalLayout(component2, isBottomRightMode); } else { UpdateVerticalLayout(component2); } UpdateDotsPosition(); } private void UpdateHorizontalLayout(RectTransform rootRect, bool isBottomRightMode) { HorizontalLayoutGroup component = _pinListRoot.GetComponent(); if (isBottomRightMode) { if (rootRect.anchorMin != new Vector2(1f, 0f)) { rootRect.anchorMin = new Vector2(1f, 0f); rootRect.anchorMax = new Vector2(1f, 0f); rootRect.pivot = new Vector2(1f, 0f); } if (component != null && component.childAlignment != TextAnchor.LowerRight) { component.childAlignment = TextAnchor.LowerRight; } if (component != null && Mathf.Abs(component.spacing - RecipePinnerPlugin.BottomRightPinSpacing.Value) > 0.01f) { component.spacing = RecipePinnerPlugin.BottomRightPinSpacing.Value; } Vector2 value = RecipePinnerPlugin.BottomRightPosition.Value; if (rootRect.anchoredPosition != value) { rootRect.anchoredPosition = value; } return; } if (rootRect.anchorMin != new Vector2(1f, 1f)) { rootRect.anchorMin = new Vector2(1f, 1f); rootRect.anchorMax = new Vector2(1f, 1f); rootRect.pivot = new Vector2(1f, 1f); } if (component != null && component.childAlignment != TextAnchor.UpperRight) { component.childAlignment = TextAnchor.UpperRight; } if (component != null && Mathf.Abs(component.spacing - RecipePinnerPlugin.HorizontalPinSpacing.Value) > 0.01f) { component.spacing = RecipePinnerPlugin.HorizontalPinSpacing.Value; } Vector2 value2 = RecipePinnerPlugin.HorizontalPosition.Value; if (Game.m_noMap && RecipePinnerPlugin.Instance._mluiInstalled && RecipePinnerPlugin.Instance._mluiNoMapListEnabled) { if (Mathf.Abs(value2.x - -250f) < 1f) { value2.x = -270f; } if (Mathf.Abs(value2.y - -40f) < 1f) { value2.y = -15f; } } if (rootRect.anchoredPosition != value2) { rootRect.anchoredPosition = value2; } } private void UpdateVerticalLayout(RectTransform rootRect) { if (rootRect.anchorMin != new Vector2(1f, 1f)) { rootRect.anchorMin = new Vector2(1f, 1f); rootRect.anchorMax = new Vector2(1f, 1f); rootRect.pivot = new Vector2(1f, 1f); } VerticalLayoutGroup component = _pinListRoot.GetComponent(); if (component != null && Mathf.Abs(component.spacing - RecipePinnerPlugin.VerticalPinSpacing.Value) > 0.01f) { component.spacing = RecipePinnerPlugin.VerticalPinSpacing.Value; } Vector2 value = RecipePinnerPlugin.VerticalPosition.Value; RecipeManager recipeMgr = RecipePinnerPlugin.Instance.RecipeMgr; if (recipeMgr.CachedPins.Count > RecipePinnerPlugin.PinsPerPage.Value) { value.y -= 30f; } if (rootRect.anchoredPosition != value) { rootRect.anchoredPosition = value; } if (Mathf.Abs(rootRect.sizeDelta.x - RecipePinnerPlugin.VerticalListWidth.Value) > 1f) { rootRect.sizeDelta = new Vector2(RecipePinnerPlugin.VerticalListWidth.Value, 0f); } } private Font GetGameFont() { Font[] array = Resources.FindObjectsOfTypeAll(); Font[] array2 = array; foreach (Font font in array2) { if (font != null && font.name == "AveriaSerifLibre-Bold") { DebugLogger.Log("Found game font: AveriaSerifLibre-Bold"); return font; } } Font[] array3 = array; foreach (Font font2 in array3) { if (font2 != null && font2.name == "Arial") { DebugLogger.Log("Using fallback font: Arial"); return font2; } } try { DebugLogger.Log("Creating dynamic font from OS: Arial"); return Font.CreateDynamicFontFromOSFont("Arial", 14); } catch { DebugLogger.Warning("Failed to create dynamic font, using first available font"); return (array.Length != 0) ? array[0] : null; } } } }