diff --git a/ValheimPlus/GameClasses/InventoryGUI.cs b/ValheimPlus/GameClasses/InventoryGUI.cs index 649b367c..076f72c8 100644 --- a/ValheimPlus/GameClasses/InventoryGUI.cs +++ b/ValheimPlus/GameClasses/InventoryGUI.cs @@ -135,7 +135,7 @@ public static void Prefix(InventoryGui __instance) } } } - + /// /// Inventory HUD setup @@ -215,4 +215,96 @@ private static bool Prefix(Transform elementRoot, Piece.Requirement req, Player return false; } } + + [HarmonyPatch(typeof(InventoryGui), nameof(InventoryGui.DoCrafting))] + public static class InventoryGui_DoCrafting_Transpiler + { + private static MethodInfo method_Player_Inventory_RemoveItem = AccessTools.Method(typeof(Inventory), nameof(Inventory.RemoveItem), new Type[] { typeof(string), typeof(int), typeof(int) }); + private static MethodInfo method_Player_GetFirstRequiredItem = AccessTools.Method(typeof(Player), nameof(Player.GetFirstRequiredItem)); + private static MethodInfo method_UseItemFromIventoryOrChest = AccessTools.Method(typeof(InventoryGui_DoCrafting_Transpiler), nameof(InventoryGui_DoCrafting_Transpiler.UseItemFromIventoryOrChest)); + private static MethodInfo method_GetFirstRequiredItemFromInventoryOrChest = AccessTools.Method(typeof(InventoryGui_DoCrafting_Transpiler), nameof(InventoryGui_DoCrafting_Transpiler.GetFirstRequiredItemFromInventoryOrChest)); + + /// + /// Patches out the code that's called when crafting. + /// This changes the call `player.GetInventory().RemoveItem(itemData.m_shared.m_name, amount2, itemData.m_quality);` + /// to allow crafting recipes with materials comming from containers when they have m_requireOnlyOneIngredient set to True. + /// + [HarmonyTranspiler] + public static IEnumerable Transpile(IEnumerable instructions) + { + if (!Configuration.Current.CraftFromChest.IsEnabled) return instructions; + + List il = instructions.ToList(); + + for (int i = 0; i < il.Count; i++) + { + if (il[i].Calls(method_Player_GetFirstRequiredItem)) + { + il[i] = new CodeInstruction(OpCodes.Call, method_GetFirstRequiredItemFromInventoryOrChest); + il.RemoveRange(i - 6, 2); + break; + } + } + for (int i = 0; i < il.Count; i++) + { + if (il[i].Calls(method_Player_Inventory_RemoveItem)) + { + il[i] = new CodeInstruction(OpCodes.Call, method_UseItemFromIventoryOrChest); + il.RemoveAt(i - 7); // removes calls to Player::GetInventory + + return il.AsEnumerable(); + } + } + + return instructions; + } + + private static ItemDrop.ItemData GetFirstRequiredItemFromInventoryOrChest(Player player, Recipe recipe, int quality, out int quantity) + { + ItemDrop.ItemData found = player.GetFirstRequiredItem(player.GetInventory(), recipe, quality, out quantity); + if (found != null) return found; + + GameObject pos = player.GetCurrentCraftingStation()?.gameObject; + if (!pos || !Configuration.Current.CraftFromChest.checkFromWorkbench) pos = player.gameObject; + + List nearbyChests = InventoryAssistant.GetNearbyChests(pos, Helper.Clamp(Configuration.Current.CraftFromChest.range, 1, 50), !Configuration.Current.CraftFromChest.ignorePrivateAreaCheck); + + foreach (Container chest in nearbyChests) + { + found = player.GetFirstRequiredItem(chest.GetInventory(), recipe, quality, out quantity); + if (found != null) + { + return found; + } + } + + return null; + } + + private static void UseItemFromIventoryOrChest(Player player, string itemName, int quantity, int quality) + { + Inventory playerInventory = player.GetInventory(); + if (playerInventory.CountItems(itemName, quality) >= quantity) + { + playerInventory.RemoveItem(itemName, quantity, quality); + return; + } + + GameObject pos = player.GetCurrentCraftingStation()?.gameObject; + if (!pos || !Configuration.Current.CraftFromChest.checkFromWorkbench) pos = player.gameObject; + + List nearbyChests = InventoryAssistant.GetNearbyChests(pos, Helper.Clamp(Configuration.Current.CraftFromChest.range, 1, 50), !Configuration.Current.CraftFromChest.ignorePrivateAreaCheck); + + int toRemove = quantity; + foreach (Container chest in nearbyChests) + { + Inventory chestInventory = chest.GetInventory(); + if (chestInventory.CountItems(itemName, quality) > 0) + { + toRemove -= InventoryAssistant.RemoveItemFromChest(chest, itemName, toRemove); + if (toRemove == 0) return; + } + } + } + } } diff --git a/ValheimPlus/GameClasses/Player.cs b/ValheimPlus/GameClasses/Player.cs index 4067b64c..a9a51141 100644 --- a/ValheimPlus/GameClasses/Player.cs +++ b/ValheimPlus/GameClasses/Player.cs @@ -117,7 +117,7 @@ private static void Postfix(ref Player __instance, ref Vector3 ___m_moveDir, ref EnvMan env = EnvMan.instance; // only update the time at most once per minute - if (savedEnvMinutes != env.m_totalSeconds/60) + if (savedEnvMinutes != env.m_totalSeconds / 60) { int day = env.GetCurrentDay(); @@ -145,7 +145,7 @@ private static void Postfix(ref Player __instance, ref Vector3 ___m_moveDir, ref timeObj.GetComponent().position = new Vector2(staminaBarRect.position.x, statusEffectBarRect.position.y); timeObj.SetActive(true); - savedEnvMinutes = env.m_totalSeconds/60; + savedEnvMinutes = env.m_totalSeconds / 60; } } } @@ -271,7 +271,7 @@ public static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable + /// Patches out the function Player::GetFirstRequiredItem + /// As the original code is calling Inventory::CountItems using `this` instead of using the inventory parameter + /// we can't use this function to check get the first required item from a Container (chest) inventory. + /// Instead of calling `this.m_inventory.CountItems` we're now calling `inventory.CountItems`. + /// + /// As the original function passes `this.GetInventory()` where `this` is the Player instance as the inventory parameter, + /// there is no change to the way the function would usually work. + /// + [HarmonyTranspiler] + public static IEnumerable Transpile(IEnumerable instructions) + { + List il = instructions.ToList(); + + for (int i = 0; i < il.Count; i++) + { + if (il[i].opcode == OpCodes.Ldarg_0) + { + il[i].opcode = OpCodes.Ldarg_1; + il.RemoveAt(i + 1); + + return il.AsEnumerable(); + } + } + return instructions; + } + } } diff --git a/ValheimPlus/Utility/InventoryAssistant.cs b/ValheimPlus/Utility/InventoryAssistant.cs index ce246d69..e01d3b92 100644 --- a/ValheimPlus/Utility/InventoryAssistant.cs +++ b/ValheimPlus/Utility/InventoryAssistant.cs @@ -18,11 +18,11 @@ public static List GetNearbyChests(GameObject target, float range, bo string[] layerMask = { "piece" }; - if(Configuration.Current.CraftFromChest.allowCraftingFromCarts || Configuration.Current.CraftFromChest.allowCraftingFromShips) + if (Configuration.Current.CraftFromChest.allowCraftingFromCarts || Configuration.Current.CraftFromChest.allowCraftingFromShips) layerMask = new string[] { "piece", "item", "vehicle" }; Collider[] hitColliders = Physics.OverlapSphere(target.transform.position, range, LayerMask.GetMask(layerMask)); - + // Order the found objects to select the nearest first instead of the farthest inventory. IOrderedEnumerable orderedColliders = hitColliders.OrderBy(x => Vector3.Distance(x.gameObject.transform.position, target.transform.position)); @@ -49,7 +49,7 @@ public static List GetNearbyChests(GameObject target, float range, bo if (isShip && !Configuration.Current.CraftFromChest.allowCraftingFromShips) continue; - if(piece.IsPlacedByPlayer() || (isShip && Configuration.Current.CraftFromChest.allowCraftingFromShips)) + if (piece.IsPlacedByPlayer() || (isShip && Configuration.Current.CraftFromChest.allowCraftingFromShips)) validContainers.Add(foundContainer); } } @@ -94,6 +94,19 @@ public static bool ChestContainsItem(Container chest, ItemDrop.ItemData needle) return false; } + public static bool ChestContainsItem(Container chest, string needle) + { + List items = chest.GetInventory().GetAllItems(); + + foreach (ItemDrop.ItemData item in items) + { + if (item.m_shared.m_name == needle) + return true; + } + + return false; + } + /// /// function to get all items in nearby chests by range /// @@ -144,6 +157,17 @@ public static int GetItemAmountInItemList(List itemList, Item return amount; } + public static int GetItemAmountInItemList(List itemList, string needle) + { + int amount = 0; + foreach (ItemDrop.ItemData item in itemList) + { + if (item.m_shared.m_name == needle) amount += item.m_stack; + } + + return amount; + } + // function to remove items in the amount from all nearby chests public static int RemoveItemInAmountFromAllNearbyChests(GameObject target, float range, ItemDrop.ItemData needle, int amount, bool checkWard = true) { @@ -174,6 +198,35 @@ public static int RemoveItemInAmountFromAllNearbyChests(GameObject target, float return itemsRemovedTotal; } + public static int RemoveItemInAmountFromAllNearbyChests(GameObject target, float range, string needle, int amount, bool checkWard = true) + { + List nearbyChests = GetNearbyChests(target, range, checkWard); + + // check if there are enough items nearby + List allItems = GetNearbyChestItemsByContainerList(nearbyChests); + + // get amount of item + int availableAmount = GetItemAmountInItemList(allItems, needle); + + // check if there are enough items + if (amount == 0) + return 0; + + // iterate all chests and remove as many items as possible for the respective chest + int itemsRemovedTotal = 0; + foreach (Container chest in nearbyChests) + { + if (itemsRemovedTotal != amount) + { + int removedItems = RemoveItemFromChest(chest, needle, amount); + itemsRemovedTotal += removedItems; + amount -= removedItems; + } + } + + return itemsRemovedTotal; + } + // function to add a item by name/ItemDrop.ItemData to a specified chest /// @@ -215,6 +268,42 @@ public static int RemoveItemFromChest(Container chest, ItemDrop.ItemData needle, return totalRemoved; } + public static int RemoveItemFromChest(Container chest, string needle, int amount = 1) + { + if (!ChestContainsItem(chest, needle)) + { + return 0; + } + + int totalRemoved = 0; + // find item + List allItems = chest.GetInventory().GetAllItems(); + foreach (ItemDrop.ItemData itemData in allItems) + { + if (itemData.m_shared.m_name == needle) + { + int num = Mathf.Min(itemData.m_stack, amount); + itemData.m_stack -= num; + amount -= num; + totalRemoved += num; + if (amount <= 0) + { + break; + } + } + } + + // We don't want to send chest content through network + if (totalRemoved == 0) return 0; + + allItems.RemoveAll((ItemDrop.ItemData x) => x.m_stack <= 0); + chest.m_inventory.m_inventory = allItems; + + ConveyContainerToNetwork(chest); + + return totalRemoved; + } + /// /// Function to convey the changes of a container to the network ///