From 0f62a8c388255da1c9eeda0716f123d22e3e0a76 Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:24:07 +0100 Subject: [PATCH 1/7] Fix torn notes not combining when moved into inventory Fixes #8487 Torn notes were only combined when picked up from the ground via CheckQuestItem(). When notes entered the inventory through other paths (e.g. stash transfers, item swaps), the combine logic never ran. Add CheckSpecialInventoryItem() which checks whether all three torn notes are present after an item is placed into InvList, and combines them into the full note if so. Called from: - ChangeInvItem (empty slot path) - ChangeInvItem (swap path) - AutoPlaceItemInInventory (covers stash-to-inventory transfers) --- Source/inv.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++++++-- Source/inv.h | 9 +++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/Source/inv.cpp b/Source/inv.cpp index fc97994c9..a6c4147bb 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -498,7 +498,7 @@ bool ChangeInvItem(Player &player, int slot, Size itemSize) if (prevItemId == 0) { player.InvList[player._pNumInv] = player.HoldItem.pop(); player._pNumInv++; - prevItemId = player._pNumInv; + prevItemId = CheckSpecialInventoryItem(player, player._pNumInv - 1) + 1; } else { const int invIndex = prevItemId - 1; if (player.HoldItem._itype == ItemType::Gold) @@ -512,8 +512,11 @@ bool ChangeInvItem(Player &player, int slot, Size itemSize) if (itemIndex == -prevItemId) itemIndex = 0; } + prevItemId = CheckSpecialInventoryItem(player, invIndex) + 1; } + itemSize = GetInventorySize(player.InvList[prevItemId - 1]); + AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, prevItemId, itemSize, &player == MyPlayer); } @@ -1121,6 +1124,49 @@ int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) } // namespace +int CheckSpecialInventoryItem(Player &player, int invIndex) +{ + if (invIndex < 0 || invIndex >= player._pNumInv) { + return invIndex; + } + + const _item_indexes currentId = player.InvList[invIndex].IDidx; + const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; + + if (IsNoneOf(currentId, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { + return invIndex; + } + + for (const _item_indexes note : notes) { + if (!HasInventoryItemWithId(player, note)) { + return invIndex; + } + } + + player.Say(HeroSpeech::JustWhatIWasLookingFor, 10); + + for (const _item_indexes note : notes) { + if (note != currentId) { + RemoveInventoryItemById(player, note); + } + } + + for (int i = 0; i < player._pNumInv; i++) { + Item ¬eItem = player.InvList[i]; + if (noteItem.IDidx != currentId) { + continue; + } + + noteItem = {}; + GetItemAttrs(noteItem, IDI_FULLNOTE, 16); + SetupItem(noteItem); + noteItem.updateRequiredStatsCacheForPlayer(player); + return i; + } + + return invIndex; +} + void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) { SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); @@ -1400,8 +1446,9 @@ bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetwork if (targetSlot) { player.InvList[player._pNumInv] = item; player._pNumInv++; + const int invIndex = CheckSpecialInventoryItem(player, player._pNumInv - 1); - AddItemToInvGrid(player, *targetSlot, player._pNumInv, itemSize, sendNetworkMessage); + AddItemToInvGrid(player, *targetSlot, invIndex + 1, GetInventorySize(player.InvList[invIndex]), sendNetworkMessage); player.CalcScrolls(); return true; diff --git a/Source/inv.h b/Source/inv.h index 4a1d56c70..737af5858 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -148,6 +148,15 @@ bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool s */ bool CanFitItemInInventory(const Player &player, const Item &item); +/** + * @brief Handles special item behavior after an item has been inserted into InvList. + * @param player The player whose inventory was modified. + * @param invIndex Index of the inserted item in InvList. + * @return The item's current InvList index. This can change if handling the item + * removes other inventory entries and the inventory gets compacted. + */ +int CheckSpecialInventoryItem(Player &player, int invIndex); + /** * @brief Attempts to place the given item in the specified player's inventory. * @param player The player whose inventory will be used. From 99a4f8b5ec837190791549fee70484204167e6e9 Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:17:05 +0100 Subject: [PATCH 2/7] Fix torn note inventory combine semantics Keep Na-Krul note auto-combine limited to player-facing inventory inserts so internal inventory reflow does not trigger partial side effects.\n\nTrack the inserted note through inventory compaction when combining, and add inventory regression tests for duplicate notes and sort behavior. --- Source/inv.cpp | 143 ++++++++++++++++++++++++++-------------------- Source/inv.h | 39 ++++++------- test/inv_test.cpp | 106 ++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 83 deletions(-) diff --git a/Source/inv.cpp b/Source/inv.cpp index a6c4147bb..fbd578ac0 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -140,8 +140,73 @@ namespace { OptionalOwnedClxSpriteList pInvCels; -/** - * @brief Adds an item to a player's InvGrid array +bool IsTornNaKrulNote(_item_indexes id) +{ + return IsAnyOf(id, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3); +} + +bool PlayerHasAllTornNaKrulNotes(const Player &player) +{ + return HasInventoryItemWithId(player, IDI_NOTE1) + && HasInventoryItemWithId(player, IDI_NOTE2) + && HasInventoryItemWithId(player, IDI_NOTE3); +} + +void ConvertItemToFullNote(Item &item) +{ + item = {}; + GetItemAttrs(item, IDI_FULLNOTE, 16); + SetupItem(item); +} + +int TryCombineInsertedNaKrulNote(Player &player, int insertedInvIndex) +{ + if (insertedInvIndex < 0 || insertedInvIndex >= player._pNumInv) { + return insertedInvIndex; + } + + const _item_indexes insertedId = player.InvList[insertedInvIndex].IDidx; + if (!IsTornNaKrulNote(insertedId) || !PlayerHasAllTornNaKrulNotes(player)) { + return insertedInvIndex; + } + + player.Say(HeroSpeech::JustWhatIWasLookingFor, 10); + + std::array removedNoteIndices {}; + size_t removeCount = 0; + for (const _item_indexes note : { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }) { + if (note == insertedId) { + continue; + } + + for (int i = 0; i < player._pNumInv; i++) { + if (player.InvList[i].IDidx == note) { + removedNoteIndices[removeCount++] = i; + break; + } + } + } + + if (removeCount != removedNoteIndices.size()) { + return insertedInvIndex; + } + + std::sort(removedNoteIndices.begin(), removedNoteIndices.end(), std::greater()); + for (const int removedNoteIndex : removedNoteIndices) { + player.RemoveInvItem(removedNoteIndex, false); + if (removedNoteIndex < insertedInvIndex) { + insertedInvIndex--; + } + } + + Item &combinedNote = player.InvList[insertedInvIndex]; + ConvertItemToFullNote(combinedNote); + combinedNote.updateRequiredStatsCacheForPlayer(player); + return insertedInvIndex; +} + +/** + * @brief Adds an item to a player's InvGrid array * @param player The player reference * @param invGridIndex Item's position in InvGrid (this should be the item's topleft grid tile) * @param invListIndex The item's InvList index (it's expected this already has +1 added to it since InvGrid can't store a 0 index) @@ -498,7 +563,7 @@ bool ChangeInvItem(Player &player, int slot, Size itemSize) if (prevItemId == 0) { player.InvList[player._pNumInv] = player.HoldItem.pop(); player._pNumInv++; - prevItemId = CheckSpecialInventoryItem(player, player._pNumInv - 1) + 1; + prevItemId = TryCombineInsertedNaKrulNote(player, player._pNumInv - 1) + 1; } else { const int invIndex = prevItemId - 1; if (player.HoldItem._itype == ItemType::Gold) @@ -512,7 +577,7 @@ bool ChangeInvItem(Player &player, int slot, Size itemSize) if (itemIndex == -prevItemId) itemIndex = 0; } - prevItemId = CheckSpecialInventoryItem(player, invIndex) + 1; + prevItemId = TryCombineInsertedNaKrulNote(player, invIndex) + 1; } itemSize = GetInventorySize(player.InvList[prevItemId - 1]); @@ -960,31 +1025,25 @@ void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool void TryCombineNaKrulNotes(Player &player, Item ¬eItem) { - const int idx = noteItem.IDidx; - const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; - - if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { + const _item_indexes idx = noteItem.IDidx; + if (!IsTornNaKrulNote(idx)) { return; } - for (const _item_indexes note : notes) { - if (idx != note && !HasInventoryItemWithId(player, note)) { - return; // the player doesn't have all notes - } + if (!PlayerHasAllTornNaKrulNotes(player)) { + return; // the player doesn't have all notes } MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - for (const _item_indexes note : notes) { + for (const _item_indexes note : { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }) { if (idx != note) { RemoveInventoryItemById(player, note); } } const Point position = noteItem.position; // copy the position to restore it after re-initialising the item - noteItem = {}; - GetItemAttrs(noteItem, IDI_FULLNOTE, 16); - SetupItem(noteItem); + ConvertItemToFullNote(noteItem); noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table } @@ -1124,49 +1183,6 @@ int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) } // namespace -int CheckSpecialInventoryItem(Player &player, int invIndex) -{ - if (invIndex < 0 || invIndex >= player._pNumInv) { - return invIndex; - } - - const _item_indexes currentId = player.InvList[invIndex].IDidx; - const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; - - if (IsNoneOf(currentId, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { - return invIndex; - } - - for (const _item_indexes note : notes) { - if (!HasInventoryItemWithId(player, note)) { - return invIndex; - } - } - - player.Say(HeroSpeech::JustWhatIWasLookingFor, 10); - - for (const _item_indexes note : notes) { - if (note != currentId) { - RemoveInventoryItemById(player, note); - } - } - - for (int i = 0; i < player._pNumInv; i++) { - Item ¬eItem = player.InvList[i]; - if (noteItem.IDidx != currentId) { - continue; - } - - noteItem = {}; - GetItemAttrs(noteItem, IDI_FULLNOTE, 16); - SetupItem(noteItem); - noteItem.updateRequiredStatsCacheForPlayer(player); - return i; - } - - return invIndex; -} - void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) { SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); @@ -1438,7 +1454,7 @@ bool CanFitItemInInventory(const Player &player, const Item &item) return static_cast(FindSlotForItem(player, GetInventorySize(item))); } -bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage) +bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage, InventoryInsertSemantics semantics) { const Size itemSize = GetInventorySize(item); std::optional targetSlot = FindSlotForItem(player, itemSize); @@ -1446,7 +1462,10 @@ bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetwork if (targetSlot) { player.InvList[player._pNumInv] = item; player._pNumInv++; - const int invIndex = CheckSpecialInventoryItem(player, player._pNumInv - 1); + int invIndex = player._pNumInv - 1; + if (semantics == InventoryInsertSemantics::PlayerAction) { + invIndex = TryCombineInsertedNaKrulNote(player, invIndex); + } AddItemToInvGrid(player, *targetSlot, invIndex + 1, GetInventorySize(player.InvList[invIndex]), sendNetworkMessage); player.CalcScrolls(); @@ -1506,7 +1525,7 @@ void ReorganizeInventory(Player &player) bool reorganizationFailed = false; for (const int index : sortedIndices) { const Item &item = tempStorage[index]; - if (!AutoPlaceItemInInventory(player, item, false)) { + if (!AutoPlaceItemInInventory(player, item, false, InventoryInsertSemantics::InternalRebuild)) { reorganizationFailed = true; break; } diff --git a/Source/inv.h b/Source/inv.h index 737af5858..a4bad9fc6 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -74,14 +74,19 @@ enum inv_xy_slot : uint8_t { }; enum item_color : uint8_t { - // clang-format off - ICOL_YELLOW = PAL16_YELLOW + 5, - ICOL_WHITE = PAL16_GRAY + 5, - ICOL_BLUE = PAL16_BLUE + 5, + // clang-format off + ICOL_YELLOW = PAL16_YELLOW + 5, + ICOL_WHITE = PAL16_GRAY + 5, + ICOL_BLUE = PAL16_BLUE + 5, ICOL_RED = PAL16_RED + 5, // clang-format on }; +enum class InventoryInsertSemantics { + PlayerAction, + InternalRebuild, +}; + extern bool invflag; extern const Rectangle InvRect[NUM_XY_SLOTS]; @@ -148,24 +153,16 @@ bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool s */ bool CanFitItemInInventory(const Player &player, const Item &item); -/** - * @brief Handles special item behavior after an item has been inserted into InvList. - * @param player The player whose inventory was modified. - * @param invIndex Index of the inserted item in InvList. - * @return The item's current InvList index. This can change if handling the item - * removes other inventory entries and the inventory gets compacted. - */ -int CheckSpecialInventoryItem(Player &player, int invIndex); - -/** - * @brief Attempts to place the given item in the specified player's inventory. - * @param player The player whose inventory will be used. - * @param item The item to be placed. - * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. - * Should only be set if a local player is placing an item in a play session (not when creating a new game) - * @return 'True' if the item was placed on the player's inventory and 'False' otherwise. +/** + * @brief Attempts to place the given item in the specified player's inventory. + * @param player The player whose inventory will be used. + * @param item The item to be placed. + * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. + * Should only be set if a local player is placing an item in a play session (not when creating a new game) + * @param semantics Distinguishes player-facing item insertion from internal inventory rebuilds. + * @return 'True' if the item was placed on the player's inventory and 'False' otherwise. */ -bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false); +bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false, InventoryInsertSemantics semantics = InventoryInsertSemantics::PlayerAction); /** * @brief Checks whether the given item can be placed on the specified player's belt. Returns 'True' when the item can be placed diff --git a/test/inv_test.cpp b/test/inv_test.cpp index c32119c4e..4cd5dc735 100644 --- a/test/inv_test.cpp +++ b/test/inv_test.cpp @@ -1,3 +1,5 @@ +#include + #include #include "cursor.h" @@ -64,6 +66,39 @@ void clear_inventory() MyPlayer->_pNumInv = 0; } +void place_inventory_item(int invIndex, int gridIndex, _item_indexes itemId) +{ + Item &item = MyPlayer->InvList[invIndex]; + InitializeItem(item, itemId); + item.updateRequiredStatsCacheForPlayer(*MyPlayer); + MyPlayer->InvGrid[gridIndex] = invIndex + 1; + MyPlayer->_pNumInv = std::max(MyPlayer->_pNumInv, invIndex + 1); +} + +int count_inventory_items_with_id(_item_indexes itemId) +{ + int count = 0; + for (int i = 0; i < MyPlayer->_pNumInv; i++) { + if (MyPlayer->InvList[i].IDidx == itemId) { + count++; + } + } + + return count; +} + +int count_positive_inv_grid_slots() +{ + int count = 0; + for (const int8_t cell : MyPlayer->InvGrid) { + if (cell > 0) { + count++; + } + } + + return count; +} + // Test that the scroll is used in the inventory in correct conditions TEST_F(InvTest, UseScroll_from_inventory) { @@ -385,5 +420,76 @@ TEST_F(InvTest, ItemSizeLastDiabloItem) EXPECT_EQ(GetInventorySize(testItem), Size(2, 3)); } +TEST_F(InvTest, AutoPlaceItemInInventoryCombinesInsertedNaKrulNote) +{ + if (!gbIsHellfire) return; + SNetInitializeProvider(SELCONN_LOOPBACK, nullptr); + + clear_inventory(); + place_inventory_item(0, 0, IDI_NOTE2); + place_inventory_item(1, 1, IDI_NOTE3); + + Item insertedNote {}; + InitializeItem(insertedNote, IDI_NOTE1); + insertedNote.updateRequiredStatsCacheForPlayer(*MyPlayer); + + ASSERT_TRUE(AutoPlaceItemInInventory(*MyPlayer, insertedNote)); + EXPECT_EQ(MyPlayer->_pNumInv, 1); + EXPECT_EQ(count_inventory_items_with_id(IDI_FULLNOTE), 1); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE1), 0); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE2), 0); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE3), 0); + EXPECT_EQ(MyPlayer->InvGrid[0], 0); + EXPECT_EQ(MyPlayer->InvGrid[1], 0); + EXPECT_EQ(MyPlayer->InvGrid[2], 1); +} + +TEST_F(InvTest, AutoPlaceItemInInventoryCombinesInsertedDuplicateNaKrulNote) +{ + if (!gbIsHellfire) return; + SNetInitializeProvider(SELCONN_LOOPBACK, nullptr); + + clear_inventory(); + place_inventory_item(0, 0, IDI_NOTE1); + place_inventory_item(1, 1, IDI_NOTE2); + place_inventory_item(2, 2, IDI_NOTE3); + + Item insertedNote {}; + InitializeItem(insertedNote, IDI_NOTE1); + insertedNote.updateRequiredStatsCacheForPlayer(*MyPlayer); + + ASSERT_TRUE(AutoPlaceItemInInventory(*MyPlayer, insertedNote)); + EXPECT_EQ(MyPlayer->_pNumInv, 2); + EXPECT_EQ(count_inventory_items_with_id(IDI_FULLNOTE), 1); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE1), 1); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE2), 0); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE3), 0); + EXPECT_EQ(count_positive_inv_grid_slots(), 2); + EXPECT_EQ(MyPlayer->InvGrid[0], 1); + EXPECT_EQ(MyPlayer->InvGrid[3], 2); + EXPECT_EQ(MyPlayer->InvList[0].IDidx, IDI_NOTE1); + EXPECT_EQ(MyPlayer->InvList[1].IDidx, IDI_FULLNOTE); +} + +TEST_F(InvTest, ReorganizeInventoryDoesNotCombineNaKrulNotes) +{ + if (!gbIsHellfire) return; + SNetInitializeProvider(SELCONN_LOOPBACK, nullptr); + + clear_inventory(); + place_inventory_item(0, 0, IDI_NOTE1); + place_inventory_item(1, 1, IDI_NOTE2); + place_inventory_item(2, 2, IDI_NOTE3); + + ReorganizeInventory(*MyPlayer); + + EXPECT_EQ(MyPlayer->_pNumInv, 3); + EXPECT_EQ(count_inventory_items_with_id(IDI_FULLNOTE), 0); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE1), 1); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE2), 1); + EXPECT_EQ(count_inventory_items_with_id(IDI_NOTE3), 1); + EXPECT_EQ(count_positive_inv_grid_slots(), 3); +} + } // namespace } // namespace devilution From 686d172cca5a1e6fb6928ceb342c6f8c8ae77f0f Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:24:28 +0100 Subject: [PATCH 3/7] fixing clang-format-off --- Source/inv.h | 776 +++++++++++++++++++++++++-------------------------- 1 file changed, 388 insertions(+), 388 deletions(-) diff --git a/Source/inv.h b/Source/inv.h index a4bad9fc6..95a63d570 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -1,158 +1,158 @@ -/** - * @file inv.h - * - * Interface of player inventory. - */ -#pragma once - -#include - -#include "engine/palette.h" -#include "engine/point.hpp" -#include "inv_iterators.hpp" -#include "items.h" -#include "player.h" -#include "utils/algorithm/container.hpp" - -namespace devilution { - -#define INV_SLOT_SIZE_PX 28 -#define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2) -constexpr Size InventorySizeInSlots { 10, 4 }; -#define INV_ROW_SLOT_SIZE InventorySizeInSlots.width -constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX }; - -enum inv_item : int8_t { - // clang-format off - INVITEM_HEAD = 0, - INVITEM_RING_LEFT = 1, - INVITEM_RING_RIGHT = 2, - INVITEM_AMULET = 3, - INVITEM_HAND_LEFT = 4, - INVITEM_HAND_RIGHT = 5, - INVITEM_CHEST = 6, - INVITEM_INV_FIRST = 7, - INVITEM_INV_LAST = 46, - INVITEM_BELT_FIRST = 47, - INVITEM_BELT_LAST = 54, - // clang-format on -}; - -/** - * identifiers for each of the inventory squares - * @see InvRect - */ -enum inv_xy_slot : uint8_t { - // clang-format off - SLOTXY_HEAD = 0, - SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD, - SLOTXY_RING_LEFT = 1, - SLOTXY_RING_RIGHT = 2, - SLOTXY_AMULET = 3, - SLOTXY_HAND_LEFT = 4, - SLOTXY_HAND_RIGHT = 5, - SLOTXY_CHEST = 6, - SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST, - - // regular inventory - SLOTXY_INV_FIRST = 7, - SLOTXY_INV_ROW1_FIRST = SLOTXY_INV_FIRST, - SLOTXY_INV_ROW1_LAST = 16, - SLOTXY_INV_ROW2_FIRST = 17, - SLOTXY_INV_ROW2_LAST = 26, - SLOTXY_INV_ROW3_FIRST = 27, - SLOTXY_INV_ROW3_LAST = 36, - SLOTXY_INV_ROW4_FIRST = 37, - SLOTXY_INV_ROW4_LAST = 46, - SLOTXY_INV_LAST = SLOTXY_INV_ROW4_LAST, - - // belt items - SLOTXY_BELT_FIRST = 47, - SLOTXY_BELT_LAST = 54, - NUM_XY_SLOTS = 55 - // clang-format on -}; - -enum item_color : uint8_t { +/** + * @file inv.h + * + * Interface of player inventory. + */ +#pragma once + +#include + +#include "engine/palette.h" +#include "engine/point.hpp" +#include "inv_iterators.hpp" +#include "items.h" +#include "player.h" +#include "utils/algorithm/container.hpp" + +namespace devilution { + +#define INV_SLOT_SIZE_PX 28 +#define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2) +constexpr Size InventorySizeInSlots { 10, 4 }; +#define INV_ROW_SLOT_SIZE InventorySizeInSlots.width +constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX }; + +enum inv_item : int8_t { + // clang-format off + INVITEM_HEAD = 0, + INVITEM_RING_LEFT = 1, + INVITEM_RING_RIGHT = 2, + INVITEM_AMULET = 3, + INVITEM_HAND_LEFT = 4, + INVITEM_HAND_RIGHT = 5, + INVITEM_CHEST = 6, + INVITEM_INV_FIRST = 7, + INVITEM_INV_LAST = 46, + INVITEM_BELT_FIRST = 47, + INVITEM_BELT_LAST = 54, + // clang-format on +}; + +/** + * identifiers for each of the inventory squares + * @see InvRect + */ +enum inv_xy_slot : uint8_t { + // clang-format off + SLOTXY_HEAD = 0, + SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD, + SLOTXY_RING_LEFT = 1, + SLOTXY_RING_RIGHT = 2, + SLOTXY_AMULET = 3, + SLOTXY_HAND_LEFT = 4, + SLOTXY_HAND_RIGHT = 5, + SLOTXY_CHEST = 6, + SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST, + + // regular inventory + SLOTXY_INV_FIRST = 7, + SLOTXY_INV_ROW1_FIRST = SLOTXY_INV_FIRST, + SLOTXY_INV_ROW1_LAST = 16, + SLOTXY_INV_ROW2_FIRST = 17, + SLOTXY_INV_ROW2_LAST = 26, + SLOTXY_INV_ROW3_FIRST = 27, + SLOTXY_INV_ROW3_LAST = 36, + SLOTXY_INV_ROW4_FIRST = 37, + SLOTXY_INV_ROW4_LAST = 46, + SLOTXY_INV_LAST = SLOTXY_INV_ROW4_LAST, + + // belt items + SLOTXY_BELT_FIRST = 47, + SLOTXY_BELT_LAST = 54, + NUM_XY_SLOTS = 55 + // clang-format on +}; + +enum item_color : uint8_t { // clang-format off ICOL_YELLOW = PAL16_YELLOW + 5, ICOL_WHITE = PAL16_GRAY + 5, ICOL_BLUE = PAL16_BLUE + 5, - ICOL_RED = PAL16_RED + 5, - // clang-format on -}; - -enum class InventoryInsertSemantics { - PlayerAction, - InternalRebuild, -}; - -extern bool invflag; -extern const Rectangle InvRect[NUM_XY_SLOTS]; - -void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality); -/** - * @brief Checks whether the given item can be placed on the belt. Takes item size as well as characteristics into account. Items - * that cannot be placed on the belt have to be placed in the inventory instead. - * @param item The item to be checked. - * @return 'True' in case the item can be placed on the belt and 'False' otherwise. - */ -bool CanBePlacedOnBelt(const Player &player, const Item &item); - -/** - * @brief Function type which performs an operation on the given item. - */ -using ItemFunc = void (*)(Item &); - -void CloseInventory(); -void CloseStash(); -void FreeInvGFX(); -void InitInv(); - -/** - * @brief Render the inventory panel to the given buffer. - */ -void DrawInv(const Surface &out); - -void DrawInvBelt(const Surface &out); - -/** - * @brief Removes equipment from the specified location on the player's body. - * @param player The player from which equipment will be removed. - * @param bodyLocation The location from which equipment will be removed. - * @param hiPri Priority of the network message to sync player equipment. - */ -void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri); - -/** - * @brief Checks whether or not auto-equipping behavior is enabled for the given player and item. - * @param player The player to check. - * @param item The item to check. - * @return 'True' if auto-equipping behavior is enabled for the player and item and 'False' otherwise. - */ -bool AutoEquipEnabled(const Player &player, const Item &item); - -/** - * @brief Automatically attempts to equip the specified item in the most appropriate location in the player's body. - * @note On success, this will broadcast an equipment_change event to let other players know about the equipment change. - * @param player The player whose inventory will be checked for compatibility with the item. - * @param item The item to equip. - * @param persistItem Indicates whether or not the item should be persisted in the player's body. Pass 'False' to check - * whether the player can equip the item but you don't want the item to actually be equipped. 'True' by default. - * @param sendNetworkMessage Set to true if you want an equip sound and network message to be generated if the equipment - * changes. Should only be set if a local player is equipping an item in a play session (not when creating a new game) - * @return 'True' if the item was equipped and 'False' otherwise. - */ -bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool sendNetworkMessage = false); - -/** - * @brief Checks whether the given item can be placed on the specified player's inventory. - * @param player The player whose inventory will be checked. - * @param item The item to be checked. - * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. - */ -bool CanFitItemInInventory(const Player &player, const Item &item); - + ICOL_RED = PAL16_RED + 5, + // clang-format on +}; + +enum class InventoryInsertSemantics { + PlayerAction, + InternalRebuild, +}; + +extern bool invflag; +extern const Rectangle InvRect[NUM_XY_SLOTS]; + +void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality); +/** + * @brief Checks whether the given item can be placed on the belt. Takes item size as well as characteristics into account. Items + * that cannot be placed on the belt have to be placed in the inventory instead. + * @param item The item to be checked. + * @return 'True' in case the item can be placed on the belt and 'False' otherwise. + */ +bool CanBePlacedOnBelt(const Player &player, const Item &item); + +/** + * @brief Function type which performs an operation on the given item. + */ +using ItemFunc = void (*)(Item &); + +void CloseInventory(); +void CloseStash(); +void FreeInvGFX(); +void InitInv(); + +/** + * @brief Render the inventory panel to the given buffer. + */ +void DrawInv(const Surface &out); + +void DrawInvBelt(const Surface &out); + +/** + * @brief Removes equipment from the specified location on the player's body. + * @param player The player from which equipment will be removed. + * @param bodyLocation The location from which equipment will be removed. + * @param hiPri Priority of the network message to sync player equipment. + */ +void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri); + +/** + * @brief Checks whether or not auto-equipping behavior is enabled for the given player and item. + * @param player The player to check. + * @param item The item to check. + * @return 'True' if auto-equipping behavior is enabled for the player and item and 'False' otherwise. + */ +bool AutoEquipEnabled(const Player &player, const Item &item); + +/** + * @brief Automatically attempts to equip the specified item in the most appropriate location in the player's body. + * @note On success, this will broadcast an equipment_change event to let other players know about the equipment change. + * @param player The player whose inventory will be checked for compatibility with the item. + * @param item The item to equip. + * @param persistItem Indicates whether or not the item should be persisted in the player's body. Pass 'False' to check + * whether the player can equip the item but you don't want the item to actually be equipped. 'True' by default. + * @param sendNetworkMessage Set to true if you want an equip sound and network message to be generated if the equipment + * changes. Should only be set if a local player is equipping an item in a play session (not when creating a new game) + * @return 'True' if the item was equipped and 'False' otherwise. + */ +bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool sendNetworkMessage = false); + +/** + * @brief Checks whether the given item can be placed on the specified player's inventory. + * @param player The player whose inventory will be checked. + * @param item The item to be checked. + * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. + */ +bool CanFitItemInInventory(const Player &player, const Item &item); + /** * @brief Attempts to place the given item in the specified player's inventory. * @param player The player whose inventory will be used. @@ -161,240 +161,240 @@ bool CanFitItemInInventory(const Player &player, const Item &item); * Should only be set if a local player is placing an item in a play session (not when creating a new game) * @param semantics Distinguishes player-facing item insertion from internal inventory rebuilds. * @return 'True' if the item was placed on the player's inventory and 'False' otherwise. - */ -bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false, InventoryInsertSemantics semantics = InventoryInsertSemantics::PlayerAction); - -/** - * @brief Checks whether the given item can be placed on the specified player's belt. Returns 'True' when the item can be placed - * on belt slots and the player has at least one empty slot in his belt. - * If 'persistItem' is 'True', the item is also placed in the belt. - * @param player The player on whose belt will be checked. - * @param item The item to be checked. - * @param persistItem Pass 'True' to actually place the item in the belt. The default is 'False'. - * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. - * Should only be set if a local player is placing an item in a play session (not when creating a new game) - * @return 'True' in case the item can be placed on the player's belt and 'False' otherwise. - */ -bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem = false, bool sendNetworkMessage = false); - -/** - * @brief Sort player inventory. - */ -void ReorganizeInventory(Player &player); - -/** - * @brief Calculate the maximum additional gold that may fit in the user's inventory - */ -int RoomForGold(); - -/** - * @return The leftover amount that didn't fit, if any - */ -int AddGoldToInventory(Player &player, int value); -bool GoldAutoPlace(Player &player, Item &goldStack); -void CheckInvSwap(Player &player, inv_body_loc bLoc); -void inv_update_rem_item(Player &player, inv_body_loc iv); -void CheckInvSwap(Player &player, const Item &item, int invGridIndex); -void CheckInvRemove(Player &player, int invGridIndex); -void TransferItemToStash(Player &player, int location); -void CheckInvItem(bool isShiftHeld = false, bool isCtrlHeld = false); - -/** - * Check for interactions with belt - */ -void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld); -void InvGetItem(Player &player, int ii); - -/** - * @brief Returns the first free space that can take an item preferencing tiles in front of the current position - * - * The search starts with the adjacent tile in the desired direction and alternates sides until it ends up checking the - * opposite tile, before finally checking the origin tile - * - * @param origin center tile of the search space - * @param facing direction of the adjacent tile to check first - * @return the first valid point or an empty optional - */ -std::optional FindAdjacentPositionForItem(Point origin, Direction facing); -void AutoGetItem(Player &player, Item *itemPointer, int ii); - -/** - * @brief Searches for a dropped item with the same type/createInfo/seed - * @param iseed The value used to initialise the RNG when generating the item - * @param idx The overarching type of the target item - * @param ci Flags used to describe the specific subtype of the target item - * @return An index into ActiveItems or -1 if no matching item was found - */ -int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t ci); -void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci); - -/** - * @brief Checks if the tile has room for an item - * @param position tile coordinates - * @return True if the space is free of obstructions, false if blocked - */ -bool CanPut(Point position); - -int ClampDurability(const Item &item, int durability); -int16_t ClampToHit(const Item &item, int16_t toHit); -uint8_t ClampMaxDam(const Item &item, uint8_t maxDam); -int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam); -int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname); -int8_t CheckInvHLight(); -bool CanUseScroll(Player &player, SpellID spell); -void ConsumeStaffCharge(Player &player); -bool CanUseStaff(Player &player, SpellID spellId); -Item &GetInventoryItem(Player &player, int location); -bool UseInvItem(int cii); -void DoTelekinesis(); -int CalculateGold(Player &player); - -/** - * @brief Gets the size, in inventory cells, of the given item. - * @param item The item whose size is to be determined. - * @return The size, in inventory cells, of the item. - */ -Size GetInventorySize(const Item &item); - -/** - * @brief Checks whether the player has an inventory item matching the predicate. - */ -template -bool HasInventoryItem(const Player &player, Predicate &&predicate) -{ - const InventoryPlayerItemsRange items { player }; - return c_find_if(items, std::forward(predicate)) != items.end(); -} - -/** - * @brief Checks whether the player has a belt item matching the predicate. - */ -template -bool HasBeltItem(const Player &player, Predicate &&predicate) -{ - const BeltPlayerItemsRange items { player }; - return c_find_if(items, std::forward(predicate)) != items.end(); -} - -/** - * @brief Checks whether the player has an inventory or a belt item matching the predicate. - */ -template -bool HasInventoryOrBeltItem(const Player &player, Predicate &&predicate) -{ - return HasInventoryItem(player, predicate) || HasBeltItem(player, predicate); -} - -/** - * @brief Checks whether the player has an inventory item with the given ID (IDidx). - */ -inline bool HasInventoryItemWithId(const Player &player, _item_indexes id) -{ - return HasInventoryItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Checks whether the player has a belt item with the given ID (IDidx). - */ -inline bool HasBeltItemWithId(const Player &player, _item_indexes id) -{ - return HasBeltItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Checks whether the player has an inventory or a belt item with the given ID (IDidx). - */ -inline bool HasInventoryOrBeltItemWithId(const Player &player, _item_indexes id) -{ - return HasInventoryItemWithId(player, id) || HasBeltItemWithId(player, id); -} - -/** - * @brief Removes the first inventory item matching the predicate. - * - * @return Whether an item was found and removed. - */ -template -bool RemoveInventoryItem(Player &player, Predicate &&predicate) -{ - const InventoryPlayerItemsRange items { player }; - const auto it = c_find_if(items, std::forward(predicate)); - if (it == items.end()) - return false; - player.RemoveInvItem(static_cast(it.index())); - return true; -} - -/** - * @brief Removes the first belt item matching the predicate. - * - * @return Whether an item was found and removed. - */ -template -bool RemoveBeltItem(Player &player, Predicate &&predicate) -{ - const BeltPlayerItemsRange items { player }; - const auto it = c_find_if(items, std::forward(predicate)); - if (it == items.end()) - return false; - player.RemoveSpdBarItem(static_cast(it.index())); - return true; -} - -/** - * @brief Removes the first inventory or belt item matching the predicate. - * - * @return Whether an item was found and removed. - */ -template -bool RemoveInventoryOrBeltItem(Player &player, Predicate &&predicate) -{ - return RemoveInventoryItem(player, predicate) || RemoveBeltItem(player, predicate); -} - -/** - * @brief Removes the first inventory item with the given id (IDidx). - * - * @return Whether an item was found and removed. - */ -inline bool RemoveInventoryItemById(Player &player, _item_indexes id) -{ - return RemoveInventoryItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Removes the first belt item with the given id (IDidx). - * - * @return Whether an item was found and removed. - */ -inline bool RemoveBeltItemById(Player &player, _item_indexes id) -{ - return RemoveBeltItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Removes the first inventory or belt item with the given id (IDidx). - * - * @return Whether an item was found and removed. - */ -inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id) -{ - return RemoveInventoryItemById(player, id) || RemoveBeltItemById(player, id); -} - -/** - * @brief Removes the first inventory or belt scroll with the player's current spell. - */ -void ConsumeScroll(Player &player); - -/* data */ - -} // namespace devilution + */ +bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false, InventoryInsertSemantics semantics = InventoryInsertSemantics::PlayerAction); + +/** + * @brief Checks whether the given item can be placed on the specified player's belt. Returns 'True' when the item can be placed + * on belt slots and the player has at least one empty slot in his belt. + * If 'persistItem' is 'True', the item is also placed in the belt. + * @param player The player on whose belt will be checked. + * @param item The item to be checked. + * @param persistItem Pass 'True' to actually place the item in the belt. The default is 'False'. + * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. + * Should only be set if a local player is placing an item in a play session (not when creating a new game) + * @return 'True' in case the item can be placed on the player's belt and 'False' otherwise. + */ +bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem = false, bool sendNetworkMessage = false); + +/** + * @brief Sort player inventory. + */ +void ReorganizeInventory(Player &player); + +/** + * @brief Calculate the maximum additional gold that may fit in the user's inventory + */ +int RoomForGold(); + +/** + * @return The leftover amount that didn't fit, if any + */ +int AddGoldToInventory(Player &player, int value); +bool GoldAutoPlace(Player &player, Item &goldStack); +void CheckInvSwap(Player &player, inv_body_loc bLoc); +void inv_update_rem_item(Player &player, inv_body_loc iv); +void CheckInvSwap(Player &player, const Item &item, int invGridIndex); +void CheckInvRemove(Player &player, int invGridIndex); +void TransferItemToStash(Player &player, int location); +void CheckInvItem(bool isShiftHeld = false, bool isCtrlHeld = false); + +/** + * Check for interactions with belt + */ +void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld); +void InvGetItem(Player &player, int ii); + +/** + * @brief Returns the first free space that can take an item preferencing tiles in front of the current position + * + * The search starts with the adjacent tile in the desired direction and alternates sides until it ends up checking the + * opposite tile, before finally checking the origin tile + * + * @param origin center tile of the search space + * @param facing direction of the adjacent tile to check first + * @return the first valid point or an empty optional + */ +std::optional FindAdjacentPositionForItem(Point origin, Direction facing); +void AutoGetItem(Player &player, Item *itemPointer, int ii); + +/** + * @brief Searches for a dropped item with the same type/createInfo/seed + * @param iseed The value used to initialise the RNG when generating the item + * @param idx The overarching type of the target item + * @param ci Flags used to describe the specific subtype of the target item + * @return An index into ActiveItems or -1 if no matching item was found + */ +int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t ci); +void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci); + +/** + * @brief Checks if the tile has room for an item + * @param position tile coordinates + * @return True if the space is free of obstructions, false if blocked + */ +bool CanPut(Point position); + +int ClampDurability(const Item &item, int durability); +int16_t ClampToHit(const Item &item, int16_t toHit); +uint8_t ClampMaxDam(const Item &item, uint8_t maxDam); +int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam); +int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname); +int8_t CheckInvHLight(); +bool CanUseScroll(Player &player, SpellID spell); +void ConsumeStaffCharge(Player &player); +bool CanUseStaff(Player &player, SpellID spellId); +Item &GetInventoryItem(Player &player, int location); +bool UseInvItem(int cii); +void DoTelekinesis(); +int CalculateGold(Player &player); + +/** + * @brief Gets the size, in inventory cells, of the given item. + * @param item The item whose size is to be determined. + * @return The size, in inventory cells, of the item. + */ +Size GetInventorySize(const Item &item); + +/** + * @brief Checks whether the player has an inventory item matching the predicate. + */ +template +bool HasInventoryItem(const Player &player, Predicate &&predicate) +{ + const InventoryPlayerItemsRange items { player }; + return c_find_if(items, std::forward(predicate)) != items.end(); +} + +/** + * @brief Checks whether the player has a belt item matching the predicate. + */ +template +bool HasBeltItem(const Player &player, Predicate &&predicate) +{ + const BeltPlayerItemsRange items { player }; + return c_find_if(items, std::forward(predicate)) != items.end(); +} + +/** + * @brief Checks whether the player has an inventory or a belt item matching the predicate. + */ +template +bool HasInventoryOrBeltItem(const Player &player, Predicate &&predicate) +{ + return HasInventoryItem(player, predicate) || HasBeltItem(player, predicate); +} + +/** + * @brief Checks whether the player has an inventory item with the given ID (IDidx). + */ +inline bool HasInventoryItemWithId(const Player &player, _item_indexes id) +{ + return HasInventoryItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Checks whether the player has a belt item with the given ID (IDidx). + */ +inline bool HasBeltItemWithId(const Player &player, _item_indexes id) +{ + return HasBeltItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Checks whether the player has an inventory or a belt item with the given ID (IDidx). + */ +inline bool HasInventoryOrBeltItemWithId(const Player &player, _item_indexes id) +{ + return HasInventoryItemWithId(player, id) || HasBeltItemWithId(player, id); +} + +/** + * @brief Removes the first inventory item matching the predicate. + * + * @return Whether an item was found and removed. + */ +template +bool RemoveInventoryItem(Player &player, Predicate &&predicate) +{ + const InventoryPlayerItemsRange items { player }; + const auto it = c_find_if(items, std::forward(predicate)); + if (it == items.end()) + return false; + player.RemoveInvItem(static_cast(it.index())); + return true; +} + +/** + * @brief Removes the first belt item matching the predicate. + * + * @return Whether an item was found and removed. + */ +template +bool RemoveBeltItem(Player &player, Predicate &&predicate) +{ + const BeltPlayerItemsRange items { player }; + const auto it = c_find_if(items, std::forward(predicate)); + if (it == items.end()) + return false; + player.RemoveSpdBarItem(static_cast(it.index())); + return true; +} + +/** + * @brief Removes the first inventory or belt item matching the predicate. + * + * @return Whether an item was found and removed. + */ +template +bool RemoveInventoryOrBeltItem(Player &player, Predicate &&predicate) +{ + return RemoveInventoryItem(player, predicate) || RemoveBeltItem(player, predicate); +} + +/** + * @brief Removes the first inventory item with the given id (IDidx). + * + * @return Whether an item was found and removed. + */ +inline bool RemoveInventoryItemById(Player &player, _item_indexes id) +{ + return RemoveInventoryItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Removes the first belt item with the given id (IDidx). + * + * @return Whether an item was found and removed. + */ +inline bool RemoveBeltItemById(Player &player, _item_indexes id) +{ + return RemoveBeltItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Removes the first inventory or belt item with the given id (IDidx). + * + * @return Whether an item was found and removed. + */ +inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id) +{ + return RemoveInventoryItemById(player, id) || RemoveBeltItemById(player, id); +} + +/** + * @brief Removes the first inventory or belt scroll with the player's current spell. + */ +void ConsumeScroll(Player &player); + +/* data */ + +} // namespace devilution From 099e2f551d9dc57cd53da1dc9360d8617ad5c4cd Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:25:47 +0100 Subject: [PATCH 4/7] Revert "fixing clang-format-off" This reverts commit 686d172cca5a1e6fb6928ceb342c6f8c8ae77f0f. --- Source/inv.h | 776 +++++++++++++++++++++++++-------------------------- 1 file changed, 388 insertions(+), 388 deletions(-) diff --git a/Source/inv.h b/Source/inv.h index 95a63d570..a4bad9fc6 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -1,158 +1,158 @@ -/** - * @file inv.h - * - * Interface of player inventory. - */ -#pragma once - -#include - -#include "engine/palette.h" -#include "engine/point.hpp" -#include "inv_iterators.hpp" -#include "items.h" -#include "player.h" -#include "utils/algorithm/container.hpp" - -namespace devilution { - -#define INV_SLOT_SIZE_PX 28 -#define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2) -constexpr Size InventorySizeInSlots { 10, 4 }; -#define INV_ROW_SLOT_SIZE InventorySizeInSlots.width -constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX }; - -enum inv_item : int8_t { - // clang-format off - INVITEM_HEAD = 0, - INVITEM_RING_LEFT = 1, - INVITEM_RING_RIGHT = 2, - INVITEM_AMULET = 3, - INVITEM_HAND_LEFT = 4, - INVITEM_HAND_RIGHT = 5, - INVITEM_CHEST = 6, - INVITEM_INV_FIRST = 7, - INVITEM_INV_LAST = 46, - INVITEM_BELT_FIRST = 47, - INVITEM_BELT_LAST = 54, - // clang-format on -}; - -/** - * identifiers for each of the inventory squares - * @see InvRect - */ -enum inv_xy_slot : uint8_t { - // clang-format off - SLOTXY_HEAD = 0, - SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD, - SLOTXY_RING_LEFT = 1, - SLOTXY_RING_RIGHT = 2, - SLOTXY_AMULET = 3, - SLOTXY_HAND_LEFT = 4, - SLOTXY_HAND_RIGHT = 5, - SLOTXY_CHEST = 6, - SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST, - - // regular inventory - SLOTXY_INV_FIRST = 7, - SLOTXY_INV_ROW1_FIRST = SLOTXY_INV_FIRST, - SLOTXY_INV_ROW1_LAST = 16, - SLOTXY_INV_ROW2_FIRST = 17, - SLOTXY_INV_ROW2_LAST = 26, - SLOTXY_INV_ROW3_FIRST = 27, - SLOTXY_INV_ROW3_LAST = 36, - SLOTXY_INV_ROW4_FIRST = 37, - SLOTXY_INV_ROW4_LAST = 46, - SLOTXY_INV_LAST = SLOTXY_INV_ROW4_LAST, - - // belt items - SLOTXY_BELT_FIRST = 47, - SLOTXY_BELT_LAST = 54, - NUM_XY_SLOTS = 55 - // clang-format on -}; - -enum item_color : uint8_t { +/** + * @file inv.h + * + * Interface of player inventory. + */ +#pragma once + +#include + +#include "engine/palette.h" +#include "engine/point.hpp" +#include "inv_iterators.hpp" +#include "items.h" +#include "player.h" +#include "utils/algorithm/container.hpp" + +namespace devilution { + +#define INV_SLOT_SIZE_PX 28 +#define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2) +constexpr Size InventorySizeInSlots { 10, 4 }; +#define INV_ROW_SLOT_SIZE InventorySizeInSlots.width +constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX }; + +enum inv_item : int8_t { + // clang-format off + INVITEM_HEAD = 0, + INVITEM_RING_LEFT = 1, + INVITEM_RING_RIGHT = 2, + INVITEM_AMULET = 3, + INVITEM_HAND_LEFT = 4, + INVITEM_HAND_RIGHT = 5, + INVITEM_CHEST = 6, + INVITEM_INV_FIRST = 7, + INVITEM_INV_LAST = 46, + INVITEM_BELT_FIRST = 47, + INVITEM_BELT_LAST = 54, + // clang-format on +}; + +/** + * identifiers for each of the inventory squares + * @see InvRect + */ +enum inv_xy_slot : uint8_t { + // clang-format off + SLOTXY_HEAD = 0, + SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD, + SLOTXY_RING_LEFT = 1, + SLOTXY_RING_RIGHT = 2, + SLOTXY_AMULET = 3, + SLOTXY_HAND_LEFT = 4, + SLOTXY_HAND_RIGHT = 5, + SLOTXY_CHEST = 6, + SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST, + + // regular inventory + SLOTXY_INV_FIRST = 7, + SLOTXY_INV_ROW1_FIRST = SLOTXY_INV_FIRST, + SLOTXY_INV_ROW1_LAST = 16, + SLOTXY_INV_ROW2_FIRST = 17, + SLOTXY_INV_ROW2_LAST = 26, + SLOTXY_INV_ROW3_FIRST = 27, + SLOTXY_INV_ROW3_LAST = 36, + SLOTXY_INV_ROW4_FIRST = 37, + SLOTXY_INV_ROW4_LAST = 46, + SLOTXY_INV_LAST = SLOTXY_INV_ROW4_LAST, + + // belt items + SLOTXY_BELT_FIRST = 47, + SLOTXY_BELT_LAST = 54, + NUM_XY_SLOTS = 55 + // clang-format on +}; + +enum item_color : uint8_t { // clang-format off ICOL_YELLOW = PAL16_YELLOW + 5, ICOL_WHITE = PAL16_GRAY + 5, ICOL_BLUE = PAL16_BLUE + 5, - ICOL_RED = PAL16_RED + 5, - // clang-format on -}; - -enum class InventoryInsertSemantics { - PlayerAction, - InternalRebuild, -}; - -extern bool invflag; -extern const Rectangle InvRect[NUM_XY_SLOTS]; - -void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality); -/** - * @brief Checks whether the given item can be placed on the belt. Takes item size as well as characteristics into account. Items - * that cannot be placed on the belt have to be placed in the inventory instead. - * @param item The item to be checked. - * @return 'True' in case the item can be placed on the belt and 'False' otherwise. - */ -bool CanBePlacedOnBelt(const Player &player, const Item &item); - -/** - * @brief Function type which performs an operation on the given item. - */ -using ItemFunc = void (*)(Item &); - -void CloseInventory(); -void CloseStash(); -void FreeInvGFX(); -void InitInv(); - -/** - * @brief Render the inventory panel to the given buffer. - */ -void DrawInv(const Surface &out); - -void DrawInvBelt(const Surface &out); - -/** - * @brief Removes equipment from the specified location on the player's body. - * @param player The player from which equipment will be removed. - * @param bodyLocation The location from which equipment will be removed. - * @param hiPri Priority of the network message to sync player equipment. - */ -void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri); - -/** - * @brief Checks whether or not auto-equipping behavior is enabled for the given player and item. - * @param player The player to check. - * @param item The item to check. - * @return 'True' if auto-equipping behavior is enabled for the player and item and 'False' otherwise. - */ -bool AutoEquipEnabled(const Player &player, const Item &item); - -/** - * @brief Automatically attempts to equip the specified item in the most appropriate location in the player's body. - * @note On success, this will broadcast an equipment_change event to let other players know about the equipment change. - * @param player The player whose inventory will be checked for compatibility with the item. - * @param item The item to equip. - * @param persistItem Indicates whether or not the item should be persisted in the player's body. Pass 'False' to check - * whether the player can equip the item but you don't want the item to actually be equipped. 'True' by default. - * @param sendNetworkMessage Set to true if you want an equip sound and network message to be generated if the equipment - * changes. Should only be set if a local player is equipping an item in a play session (not when creating a new game) - * @return 'True' if the item was equipped and 'False' otherwise. - */ -bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool sendNetworkMessage = false); - -/** - * @brief Checks whether the given item can be placed on the specified player's inventory. - * @param player The player whose inventory will be checked. - * @param item The item to be checked. - * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. - */ -bool CanFitItemInInventory(const Player &player, const Item &item); - + ICOL_RED = PAL16_RED + 5, + // clang-format on +}; + +enum class InventoryInsertSemantics { + PlayerAction, + InternalRebuild, +}; + +extern bool invflag; +extern const Rectangle InvRect[NUM_XY_SLOTS]; + +void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality); +/** + * @brief Checks whether the given item can be placed on the belt. Takes item size as well as characteristics into account. Items + * that cannot be placed on the belt have to be placed in the inventory instead. + * @param item The item to be checked. + * @return 'True' in case the item can be placed on the belt and 'False' otherwise. + */ +bool CanBePlacedOnBelt(const Player &player, const Item &item); + +/** + * @brief Function type which performs an operation on the given item. + */ +using ItemFunc = void (*)(Item &); + +void CloseInventory(); +void CloseStash(); +void FreeInvGFX(); +void InitInv(); + +/** + * @brief Render the inventory panel to the given buffer. + */ +void DrawInv(const Surface &out); + +void DrawInvBelt(const Surface &out); + +/** + * @brief Removes equipment from the specified location on the player's body. + * @param player The player from which equipment will be removed. + * @param bodyLocation The location from which equipment will be removed. + * @param hiPri Priority of the network message to sync player equipment. + */ +void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri); + +/** + * @brief Checks whether or not auto-equipping behavior is enabled for the given player and item. + * @param player The player to check. + * @param item The item to check. + * @return 'True' if auto-equipping behavior is enabled for the player and item and 'False' otherwise. + */ +bool AutoEquipEnabled(const Player &player, const Item &item); + +/** + * @brief Automatically attempts to equip the specified item in the most appropriate location in the player's body. + * @note On success, this will broadcast an equipment_change event to let other players know about the equipment change. + * @param player The player whose inventory will be checked for compatibility with the item. + * @param item The item to equip. + * @param persistItem Indicates whether or not the item should be persisted in the player's body. Pass 'False' to check + * whether the player can equip the item but you don't want the item to actually be equipped. 'True' by default. + * @param sendNetworkMessage Set to true if you want an equip sound and network message to be generated if the equipment + * changes. Should only be set if a local player is equipping an item in a play session (not when creating a new game) + * @return 'True' if the item was equipped and 'False' otherwise. + */ +bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool sendNetworkMessage = false); + +/** + * @brief Checks whether the given item can be placed on the specified player's inventory. + * @param player The player whose inventory will be checked. + * @param item The item to be checked. + * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. + */ +bool CanFitItemInInventory(const Player &player, const Item &item); + /** * @brief Attempts to place the given item in the specified player's inventory. * @param player The player whose inventory will be used. @@ -161,240 +161,240 @@ bool CanFitItemInInventory(const Player &player, const Item &item); * Should only be set if a local player is placing an item in a play session (not when creating a new game) * @param semantics Distinguishes player-facing item insertion from internal inventory rebuilds. * @return 'True' if the item was placed on the player's inventory and 'False' otherwise. - */ -bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false, InventoryInsertSemantics semantics = InventoryInsertSemantics::PlayerAction); - -/** - * @brief Checks whether the given item can be placed on the specified player's belt. Returns 'True' when the item can be placed - * on belt slots and the player has at least one empty slot in his belt. - * If 'persistItem' is 'True', the item is also placed in the belt. - * @param player The player on whose belt will be checked. - * @param item The item to be checked. - * @param persistItem Pass 'True' to actually place the item in the belt. The default is 'False'. - * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. - * Should only be set if a local player is placing an item in a play session (not when creating a new game) - * @return 'True' in case the item can be placed on the player's belt and 'False' otherwise. - */ -bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem = false, bool sendNetworkMessage = false); - -/** - * @brief Sort player inventory. - */ -void ReorganizeInventory(Player &player); - -/** - * @brief Calculate the maximum additional gold that may fit in the user's inventory - */ -int RoomForGold(); - -/** - * @return The leftover amount that didn't fit, if any - */ -int AddGoldToInventory(Player &player, int value); -bool GoldAutoPlace(Player &player, Item &goldStack); -void CheckInvSwap(Player &player, inv_body_loc bLoc); -void inv_update_rem_item(Player &player, inv_body_loc iv); -void CheckInvSwap(Player &player, const Item &item, int invGridIndex); -void CheckInvRemove(Player &player, int invGridIndex); -void TransferItemToStash(Player &player, int location); -void CheckInvItem(bool isShiftHeld = false, bool isCtrlHeld = false); - -/** - * Check for interactions with belt - */ -void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld); -void InvGetItem(Player &player, int ii); - -/** - * @brief Returns the first free space that can take an item preferencing tiles in front of the current position - * - * The search starts with the adjacent tile in the desired direction and alternates sides until it ends up checking the - * opposite tile, before finally checking the origin tile - * - * @param origin center tile of the search space - * @param facing direction of the adjacent tile to check first - * @return the first valid point or an empty optional - */ -std::optional FindAdjacentPositionForItem(Point origin, Direction facing); -void AutoGetItem(Player &player, Item *itemPointer, int ii); - -/** - * @brief Searches for a dropped item with the same type/createInfo/seed - * @param iseed The value used to initialise the RNG when generating the item - * @param idx The overarching type of the target item - * @param ci Flags used to describe the specific subtype of the target item - * @return An index into ActiveItems or -1 if no matching item was found - */ -int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t ci); -void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci); - -/** - * @brief Checks if the tile has room for an item - * @param position tile coordinates - * @return True if the space is free of obstructions, false if blocked - */ -bool CanPut(Point position); - -int ClampDurability(const Item &item, int durability); -int16_t ClampToHit(const Item &item, int16_t toHit); -uint8_t ClampMaxDam(const Item &item, uint8_t maxDam); -int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam); -int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname); -int8_t CheckInvHLight(); -bool CanUseScroll(Player &player, SpellID spell); -void ConsumeStaffCharge(Player &player); -bool CanUseStaff(Player &player, SpellID spellId); -Item &GetInventoryItem(Player &player, int location); -bool UseInvItem(int cii); -void DoTelekinesis(); -int CalculateGold(Player &player); - -/** - * @brief Gets the size, in inventory cells, of the given item. - * @param item The item whose size is to be determined. - * @return The size, in inventory cells, of the item. - */ -Size GetInventorySize(const Item &item); - -/** - * @brief Checks whether the player has an inventory item matching the predicate. - */ -template -bool HasInventoryItem(const Player &player, Predicate &&predicate) -{ - const InventoryPlayerItemsRange items { player }; - return c_find_if(items, std::forward(predicate)) != items.end(); -} - -/** - * @brief Checks whether the player has a belt item matching the predicate. - */ -template -bool HasBeltItem(const Player &player, Predicate &&predicate) -{ - const BeltPlayerItemsRange items { player }; - return c_find_if(items, std::forward(predicate)) != items.end(); -} - -/** - * @brief Checks whether the player has an inventory or a belt item matching the predicate. - */ -template -bool HasInventoryOrBeltItem(const Player &player, Predicate &&predicate) -{ - return HasInventoryItem(player, predicate) || HasBeltItem(player, predicate); -} - -/** - * @brief Checks whether the player has an inventory item with the given ID (IDidx). - */ -inline bool HasInventoryItemWithId(const Player &player, _item_indexes id) -{ - return HasInventoryItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Checks whether the player has a belt item with the given ID (IDidx). - */ -inline bool HasBeltItemWithId(const Player &player, _item_indexes id) -{ - return HasBeltItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Checks whether the player has an inventory or a belt item with the given ID (IDidx). - */ -inline bool HasInventoryOrBeltItemWithId(const Player &player, _item_indexes id) -{ - return HasInventoryItemWithId(player, id) || HasBeltItemWithId(player, id); -} - -/** - * @brief Removes the first inventory item matching the predicate. - * - * @return Whether an item was found and removed. - */ -template -bool RemoveInventoryItem(Player &player, Predicate &&predicate) -{ - const InventoryPlayerItemsRange items { player }; - const auto it = c_find_if(items, std::forward(predicate)); - if (it == items.end()) - return false; - player.RemoveInvItem(static_cast(it.index())); - return true; -} - -/** - * @brief Removes the first belt item matching the predicate. - * - * @return Whether an item was found and removed. - */ -template -bool RemoveBeltItem(Player &player, Predicate &&predicate) -{ - const BeltPlayerItemsRange items { player }; - const auto it = c_find_if(items, std::forward(predicate)); - if (it == items.end()) - return false; - player.RemoveSpdBarItem(static_cast(it.index())); - return true; -} - -/** - * @brief Removes the first inventory or belt item matching the predicate. - * - * @return Whether an item was found and removed. - */ -template -bool RemoveInventoryOrBeltItem(Player &player, Predicate &&predicate) -{ - return RemoveInventoryItem(player, predicate) || RemoveBeltItem(player, predicate); -} - -/** - * @brief Removes the first inventory item with the given id (IDidx). - * - * @return Whether an item was found and removed. - */ -inline bool RemoveInventoryItemById(Player &player, _item_indexes id) -{ - return RemoveInventoryItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Removes the first belt item with the given id (IDidx). - * - * @return Whether an item was found and removed. - */ -inline bool RemoveBeltItemById(Player &player, _item_indexes id) -{ - return RemoveBeltItem(player, [id](const Item &item) { - return item.IDidx == id; - }); -} - -/** - * @brief Removes the first inventory or belt item with the given id (IDidx). - * - * @return Whether an item was found and removed. - */ -inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id) -{ - return RemoveInventoryItemById(player, id) || RemoveBeltItemById(player, id); -} - -/** - * @brief Removes the first inventory or belt scroll with the player's current spell. - */ -void ConsumeScroll(Player &player); - -/* data */ - -} // namespace devilution + */ +bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false, InventoryInsertSemantics semantics = InventoryInsertSemantics::PlayerAction); + +/** + * @brief Checks whether the given item can be placed on the specified player's belt. Returns 'True' when the item can be placed + * on belt slots and the player has at least one empty slot in his belt. + * If 'persistItem' is 'True', the item is also placed in the belt. + * @param player The player on whose belt will be checked. + * @param item The item to be checked. + * @param persistItem Pass 'True' to actually place the item in the belt. The default is 'False'. + * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. + * Should only be set if a local player is placing an item in a play session (not when creating a new game) + * @return 'True' in case the item can be placed on the player's belt and 'False' otherwise. + */ +bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem = false, bool sendNetworkMessage = false); + +/** + * @brief Sort player inventory. + */ +void ReorganizeInventory(Player &player); + +/** + * @brief Calculate the maximum additional gold that may fit in the user's inventory + */ +int RoomForGold(); + +/** + * @return The leftover amount that didn't fit, if any + */ +int AddGoldToInventory(Player &player, int value); +bool GoldAutoPlace(Player &player, Item &goldStack); +void CheckInvSwap(Player &player, inv_body_loc bLoc); +void inv_update_rem_item(Player &player, inv_body_loc iv); +void CheckInvSwap(Player &player, const Item &item, int invGridIndex); +void CheckInvRemove(Player &player, int invGridIndex); +void TransferItemToStash(Player &player, int location); +void CheckInvItem(bool isShiftHeld = false, bool isCtrlHeld = false); + +/** + * Check for interactions with belt + */ +void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld); +void InvGetItem(Player &player, int ii); + +/** + * @brief Returns the first free space that can take an item preferencing tiles in front of the current position + * + * The search starts with the adjacent tile in the desired direction and alternates sides until it ends up checking the + * opposite tile, before finally checking the origin tile + * + * @param origin center tile of the search space + * @param facing direction of the adjacent tile to check first + * @return the first valid point or an empty optional + */ +std::optional FindAdjacentPositionForItem(Point origin, Direction facing); +void AutoGetItem(Player &player, Item *itemPointer, int ii); + +/** + * @brief Searches for a dropped item with the same type/createInfo/seed + * @param iseed The value used to initialise the RNG when generating the item + * @param idx The overarching type of the target item + * @param ci Flags used to describe the specific subtype of the target item + * @return An index into ActiveItems or -1 if no matching item was found + */ +int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t ci); +void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci); + +/** + * @brief Checks if the tile has room for an item + * @param position tile coordinates + * @return True if the space is free of obstructions, false if blocked + */ +bool CanPut(Point position); + +int ClampDurability(const Item &item, int durability); +int16_t ClampToHit(const Item &item, int16_t toHit); +uint8_t ClampMaxDam(const Item &item, uint8_t maxDam); +int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam); +int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname); +int8_t CheckInvHLight(); +bool CanUseScroll(Player &player, SpellID spell); +void ConsumeStaffCharge(Player &player); +bool CanUseStaff(Player &player, SpellID spellId); +Item &GetInventoryItem(Player &player, int location); +bool UseInvItem(int cii); +void DoTelekinesis(); +int CalculateGold(Player &player); + +/** + * @brief Gets the size, in inventory cells, of the given item. + * @param item The item whose size is to be determined. + * @return The size, in inventory cells, of the item. + */ +Size GetInventorySize(const Item &item); + +/** + * @brief Checks whether the player has an inventory item matching the predicate. + */ +template +bool HasInventoryItem(const Player &player, Predicate &&predicate) +{ + const InventoryPlayerItemsRange items { player }; + return c_find_if(items, std::forward(predicate)) != items.end(); +} + +/** + * @brief Checks whether the player has a belt item matching the predicate. + */ +template +bool HasBeltItem(const Player &player, Predicate &&predicate) +{ + const BeltPlayerItemsRange items { player }; + return c_find_if(items, std::forward(predicate)) != items.end(); +} + +/** + * @brief Checks whether the player has an inventory or a belt item matching the predicate. + */ +template +bool HasInventoryOrBeltItem(const Player &player, Predicate &&predicate) +{ + return HasInventoryItem(player, predicate) || HasBeltItem(player, predicate); +} + +/** + * @brief Checks whether the player has an inventory item with the given ID (IDidx). + */ +inline bool HasInventoryItemWithId(const Player &player, _item_indexes id) +{ + return HasInventoryItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Checks whether the player has a belt item with the given ID (IDidx). + */ +inline bool HasBeltItemWithId(const Player &player, _item_indexes id) +{ + return HasBeltItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Checks whether the player has an inventory or a belt item with the given ID (IDidx). + */ +inline bool HasInventoryOrBeltItemWithId(const Player &player, _item_indexes id) +{ + return HasInventoryItemWithId(player, id) || HasBeltItemWithId(player, id); +} + +/** + * @brief Removes the first inventory item matching the predicate. + * + * @return Whether an item was found and removed. + */ +template +bool RemoveInventoryItem(Player &player, Predicate &&predicate) +{ + const InventoryPlayerItemsRange items { player }; + const auto it = c_find_if(items, std::forward(predicate)); + if (it == items.end()) + return false; + player.RemoveInvItem(static_cast(it.index())); + return true; +} + +/** + * @brief Removes the first belt item matching the predicate. + * + * @return Whether an item was found and removed. + */ +template +bool RemoveBeltItem(Player &player, Predicate &&predicate) +{ + const BeltPlayerItemsRange items { player }; + const auto it = c_find_if(items, std::forward(predicate)); + if (it == items.end()) + return false; + player.RemoveSpdBarItem(static_cast(it.index())); + return true; +} + +/** + * @brief Removes the first inventory or belt item matching the predicate. + * + * @return Whether an item was found and removed. + */ +template +bool RemoveInventoryOrBeltItem(Player &player, Predicate &&predicate) +{ + return RemoveInventoryItem(player, predicate) || RemoveBeltItem(player, predicate); +} + +/** + * @brief Removes the first inventory item with the given id (IDidx). + * + * @return Whether an item was found and removed. + */ +inline bool RemoveInventoryItemById(Player &player, _item_indexes id) +{ + return RemoveInventoryItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Removes the first belt item with the given id (IDidx). + * + * @return Whether an item was found and removed. + */ +inline bool RemoveBeltItemById(Player &player, _item_indexes id) +{ + return RemoveBeltItem(player, [id](const Item &item) { + return item.IDidx == id; + }); +} + +/** + * @brief Removes the first inventory or belt item with the given id (IDidx). + * + * @return Whether an item was found and removed. + */ +inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id) +{ + return RemoveInventoryItemById(player, id) || RemoveBeltItemById(player, id); +} + +/** + * @brief Removes the first inventory or belt scroll with the player's current spell. + */ +void ConsumeScroll(Player &player); + +/* data */ + +} // namespace devilution From b2ccedfb7d4faebc22c32ae35b623233879da18a Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Thu, 12 Mar 2026 07:50:03 +0100 Subject: [PATCH 5/7] Refactor Na-Krul note combine selection Extract the shared preserved-note mapping into GetOtherNaKrulNotes. Keep the ground pickup and inserted-inventory entry points separate because they still have different item lifetime and inventory compaction semantics. --- Source/inv.cpp | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/Source/inv.cpp b/Source/inv.cpp index fbd578ac0..2c0fa5f53 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -159,6 +159,22 @@ void ConvertItemToFullNote(Item &item) SetupItem(item); } +std::array<_item_indexes, 2> GetOtherNaKrulNotes(_item_indexes preservedNoteId) +{ + assert(IsTornNaKrulNote(preservedNoteId)); + + switch (preservedNoteId) { + case IDI_NOTE1: + return { IDI_NOTE2, IDI_NOTE3 }; + case IDI_NOTE2: + return { IDI_NOTE1, IDI_NOTE3 }; + case IDI_NOTE3: + return { IDI_NOTE1, IDI_NOTE2 }; + default: + app_fatal("Unexpected Na-Krul note id"); + } +} + int TryCombineInsertedNaKrulNote(Player &player, int insertedInvIndex) { if (insertedInvIndex < 0 || insertedInvIndex >= player._pNumInv) { @@ -174,11 +190,7 @@ int TryCombineInsertedNaKrulNote(Player &player, int insertedInvIndex) std::array removedNoteIndices {}; size_t removeCount = 0; - for (const _item_indexes note : { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }) { - if (note == insertedId) { - continue; - } - + for (const _item_indexes note : GetOtherNaKrulNotes(insertedId)) { for (int i = 0; i < player._pNumInv; i++) { if (player.InvList[i].IDidx == note) { removedNoteIndices[removeCount++] = i; @@ -1025,21 +1037,15 @@ void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool void TryCombineNaKrulNotes(Player &player, Item ¬eItem) { - const _item_indexes idx = noteItem.IDidx; - if (!IsTornNaKrulNote(idx)) { - return; - } - - if (!PlayerHasAllTornNaKrulNotes(player)) { + const _item_indexes noteId = noteItem.IDidx; + if (!IsTornNaKrulNote(noteId) || !PlayerHasAllTornNaKrulNotes(player)) { return; // the player doesn't have all notes } MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - for (const _item_indexes note : { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }) { - if (idx != note) { - RemoveInventoryItemById(player, note); - } + for (const _item_indexes note : GetOtherNaKrulNotes(noteId)) { + RemoveInventoryItemById(player, note); } const Point position = noteItem.position; // copy the position to restore it after re-initialising the item From 2fb27f9fea2d65b66e668c710e8ed59982451f42 Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:04:29 +0100 Subject: [PATCH 6/7] Rename inventory Na-Krul combine path Rename the inventory-insert combine helper to clarify that it runs after an item is inserted into InvList. This keeps the separate flow-specific entry point but makes its purpose clearer during review. --- Source/inv.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/inv.cpp b/Source/inv.cpp index 2c0fa5f53..0d654973b 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -175,7 +175,7 @@ std::array<_item_indexes, 2> GetOtherNaKrulNotes(_item_indexes preservedNoteId) } } -int TryCombineInsertedNaKrulNote(Player &player, int insertedInvIndex) +int TryCombineNaKrulNoteAfterInventoryInsert(Player &player, int insertedInvIndex) { if (insertedInvIndex < 0 || insertedInvIndex >= player._pNumInv) { return insertedInvIndex; @@ -575,7 +575,7 @@ bool ChangeInvItem(Player &player, int slot, Size itemSize) if (prevItemId == 0) { player.InvList[player._pNumInv] = player.HoldItem.pop(); player._pNumInv++; - prevItemId = TryCombineInsertedNaKrulNote(player, player._pNumInv - 1) + 1; + prevItemId = TryCombineNaKrulNoteAfterInventoryInsert(player, player._pNumInv - 1) + 1; } else { const int invIndex = prevItemId - 1; if (player.HoldItem._itype == ItemType::Gold) @@ -589,7 +589,7 @@ bool ChangeInvItem(Player &player, int slot, Size itemSize) if (itemIndex == -prevItemId) itemIndex = 0; } - prevItemId = TryCombineInsertedNaKrulNote(player, invIndex) + 1; + prevItemId = TryCombineNaKrulNoteAfterInventoryInsert(player, invIndex) + 1; } itemSize = GetInventorySize(player.InvList[prevItemId - 1]); @@ -1470,7 +1470,7 @@ bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetwork player._pNumInv++; int invIndex = player._pNumInv - 1; if (semantics == InventoryInsertSemantics::PlayerAction) { - invIndex = TryCombineInsertedNaKrulNote(player, invIndex); + invIndex = TryCombineNaKrulNoteAfterInventoryInsert(player, invIndex); } AddItemToInvGrid(player, *targetSlot, invIndex + 1, GetInventorySize(player.InvList[invIndex]), sendNetworkMessage); From 6f772a361a452bda1f505f5a18a6a4a22499a192 Mon Sep 17 00:00:00 2001 From: morfidon <57798071+morfidon@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:31:56 +0100 Subject: [PATCH 7/7] Unify Na-Krul note helper names Rename the local Na-Krul note helpers to a more consistent naming scheme. This keeps behavior unchanged while making the shared predicates and transformations easier to follow in review. --- Source/inv.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/inv.cpp b/Source/inv.cpp index 0d654973b..e2cddb5ad 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -145,21 +145,21 @@ bool IsTornNaKrulNote(_item_indexes id) return IsAnyOf(id, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3); } -bool PlayerHasAllTornNaKrulNotes(const Player &player) +bool HasAllTornNaKrulNotes(const Player &player) { return HasInventoryItemWithId(player, IDI_NOTE1) && HasInventoryItemWithId(player, IDI_NOTE2) && HasInventoryItemWithId(player, IDI_NOTE3); } -void ConvertItemToFullNote(Item &item) +void ConvertToFullNaKrulNote(Item &item) { item = {}; GetItemAttrs(item, IDI_FULLNOTE, 16); SetupItem(item); } -std::array<_item_indexes, 2> GetOtherNaKrulNotes(_item_indexes preservedNoteId) +std::array<_item_indexes, 2> GetOtherTornNaKrulNotes(_item_indexes preservedNoteId) { assert(IsTornNaKrulNote(preservedNoteId)); @@ -182,7 +182,7 @@ int TryCombineNaKrulNoteAfterInventoryInsert(Player &player, int insertedInvInde } const _item_indexes insertedId = player.InvList[insertedInvIndex].IDidx; - if (!IsTornNaKrulNote(insertedId) || !PlayerHasAllTornNaKrulNotes(player)) { + if (!IsTornNaKrulNote(insertedId) || !HasAllTornNaKrulNotes(player)) { return insertedInvIndex; } @@ -190,7 +190,7 @@ int TryCombineNaKrulNoteAfterInventoryInsert(Player &player, int insertedInvInde std::array removedNoteIndices {}; size_t removeCount = 0; - for (const _item_indexes note : GetOtherNaKrulNotes(insertedId)) { + for (const _item_indexes note : GetOtherTornNaKrulNotes(insertedId)) { for (int i = 0; i < player._pNumInv; i++) { if (player.InvList[i].IDidx == note) { removedNoteIndices[removeCount++] = i; @@ -212,7 +212,7 @@ int TryCombineNaKrulNoteAfterInventoryInsert(Player &player, int insertedInvInde } Item &combinedNote = player.InvList[insertedInvIndex]; - ConvertItemToFullNote(combinedNote); + ConvertToFullNaKrulNote(combinedNote); combinedNote.updateRequiredStatsCacheForPlayer(player); return insertedInvIndex; } @@ -1038,18 +1038,18 @@ void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool void TryCombineNaKrulNotes(Player &player, Item ¬eItem) { const _item_indexes noteId = noteItem.IDidx; - if (!IsTornNaKrulNote(noteId) || !PlayerHasAllTornNaKrulNotes(player)) { + if (!IsTornNaKrulNote(noteId) || !HasAllTornNaKrulNotes(player)) { return; // the player doesn't have all notes } MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - for (const _item_indexes note : GetOtherNaKrulNotes(noteId)) { + for (const _item_indexes note : GetOtherTornNaKrulNotes(noteId)) { RemoveInventoryItemById(player, note); } const Point position = noteItem.position; // copy the position to restore it after re-initialising the item - ConvertItemToFullNote(noteItem); + ConvertToFullNaKrulNote(noteItem); noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table }