diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index d9bc5b8e666..ca375d0c98f 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -117,6 +117,8 @@ set(libdevilutionx_SRCS engine/render/scrollrt.cpp engine/render/text_render.cpp + items/validation.cpp + levels/crypt.cpp levels/drlg_l1.cpp levels/drlg_l2.cpp diff --git a/Source/items.cpp b/Source/items.cpp index db68ad250c4..e19351805d7 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -514,13 +514,19 @@ void CalcSelfItems(Player &player) const int currdex = std::max(0, da + player._pBaseDex); changeflag = false; + // Iterate over equipped items and remove stat bonuses if they are not valid for (Item &equipment : EquippedPlayerItemsRange(player)) { if (!equipment._iStatFlag) continue; - if (currstr >= equipment._iMinStr - && currmag >= equipment._iMinMag - && currdex >= equipment._iMinDex) + bool isValid = IsItemValid(equipment); + + if (currstr < equipment._iMinStr + || currmag < equipment._iMinMag + || currdex < equipment._iMinDex) + isValid = false; + + if (isValid) continue; changeflag = true; @@ -2005,7 +2011,7 @@ void SpawnOnePremium(Item &premiumItem, int plvl, const Player &player) GetItemBonus(player, premiumItem, plvl / 2, plvl, true, !gbIsHellfire); if (!gbIsHellfire) { - if (premiumItem._iIvalue <= 140000) { + if (premiumItem._iIvalue <= MaxVendorValue) { break; } } else { @@ -2042,7 +2048,7 @@ void SpawnOnePremium(Item &premiumItem, int plvl, const Player &player) break; } itemValue = itemValue * 4 / 5; // avoids forced int > float > int conversion - if (premiumItem._iIvalue <= 200000 + if (premiumItem._iIvalue <= MaxVendorValueHf && premiumItem._iMinStr <= strength && premiumItem._iMinMag <= magic && premiumItem._iMinDex <= dexterity @@ -4216,10 +4222,10 @@ void SpawnSmith(int lvl) { constexpr int PinnedItemCount = 0; - int maxValue = 140000; + int maxValue = MaxVendorValue; int maxItems = 20; if (gbIsHellfire) { - maxValue = 200000; + maxValue = MaxVendorValueHf; maxItems = 25; } @@ -4288,7 +4294,7 @@ void SpawnWitch(int lvl) const int pinnedBookCount = gbIsHellfire ? GenerateRnd(MaxPinnedBookCount) : 0; const int reservedItems = gbIsHellfire ? 10 : 17; const int itemCount = GenerateRnd(WITCH_ITEMS - reservedItems) + 10; - const int maxValue = gbIsHellfire ? 200000 : 140000; + const int maxValue = gbIsHellfire ? MaxVendorValueHf : MaxVendorValue; for (int i = 0; i < WITCH_ITEMS; i++) { Item &item = witchitem[i]; @@ -4373,7 +4379,7 @@ void SpawnBoy(int lvl) GetItemBonus(*MyPlayer, boyitem, lvl, 2 * lvl, true, true); if (!gbIsHellfire) { - if (boyitem._iIvalue > 90000) { + if (boyitem._iIvalue > MaxBoyValue) { keepgoing = true; // prevent breaking the do/while loop too early by failing hellfire's condition in while continue; } @@ -4448,7 +4454,7 @@ void SpawnBoy(int lvl) } } while (keepgoing || (( - boyitem._iIvalue > 200000 + boyitem._iIvalue > MaxBoyValueHf || boyitem._iMinStr > strength || boyitem._iMinMag > magic || boyitem._iMinDex > dexterity diff --git a/Source/items.h b/Source/items.h index 750e9e1d44a..6a76d82a8f3 100644 --- a/Source/items.h +++ b/Source/items.h @@ -28,6 +28,11 @@ namespace devilution { // Item indestructible durability #define DUR_INDESTRUCTIBLE 255 +constexpr int MaxVendorValue = 140000; +constexpr int MaxVendorValueHf = 200000; +constexpr int MaxBoyValue = 90000; +constexpr int MaxBoyValueHf = 200000; + enum item_quality : uint8_t { ITEM_QUALITY_NORMAL, ITEM_QUALITY_MAGIC, diff --git a/Source/items/validation.cpp b/Source/items/validation.cpp new file mode 100644 index 00000000000..91f1cd1abbd --- /dev/null +++ b/Source/items/validation.cpp @@ -0,0 +1,164 @@ +/** + * @file items/validation.cpp + * + * Implementation of functions for validation of player and item data. + */ + +#include "items/validation.h" + +#include + +#include "items.h" +#include "monstdat.h" +#include "player.h" + +namespace devilution { + +namespace { + +bool hasMultipleFlags(uint16_t flags) +{ + return (flags & (flags - 1)) > 0; +} + +} // namespace + +bool IsCreationFlagComboValid(uint16_t iCreateInfo) +{ + iCreateInfo = iCreateInfo & ~CF_LEVEL; + const bool isTownItem = (iCreateInfo & CF_TOWN) != 0; + const bool isPregenItem = (iCreateInfo & CF_PREGEN) != 0; + const bool isUsefulItem = (iCreateInfo & CF_USEFUL) == CF_USEFUL; + + if (isPregenItem) { + // Pregen flags are discarded when an item is picked up, therefore impossible to have in the inventory + return false; + } + if (isUsefulItem && (iCreateInfo & ~CF_USEFUL) != 0) + return false; + if (isTownItem && hasMultipleFlags(iCreateInfo)) { + // Items from town can only have 1 towner flag + return false; + } + return true; +} + +bool IsTownItemValid(uint16_t iCreateInfo) +{ + const uint8_t level = iCreateInfo & CF_LEVEL; + const bool isBoyItem = (iCreateInfo & CF_BOY) != 0; + const uint8_t maxTownItemLevel = 30; + + // Wirt items in multiplayer are equal to the level of the player, therefore they cannot exceed the max character level + if (isBoyItem && level <= MaxCharacterLevel) + return true; + + return level <= maxTownItemLevel; +} + +bool IsShopPriceValid(const Item &item) +{ + const int boyPriceLimit = MaxBoyValue; + if (!gbIsHellfire && (item._iCreateInfo & CF_BOY) != 0 && item._iIvalue > boyPriceLimit) + return false; + + const int premiumPriceLimit = MaxVendorValue; + if (!gbIsHellfire && (item._iCreateInfo & CF_SMITHPREMIUM) != 0 && item._iIvalue > premiumPriceLimit) + return false; + + const uint16_t smithOrWitch = CF_SMITH | CF_WITCH; + const int smithAndWitchPriceLimit = gbIsHellfire ? MaxVendorValueHf : MaxVendorValue; + if ((item._iCreateInfo & smithOrWitch) != 0 && item._iIvalue > smithAndWitchPriceLimit) + return false; + + return true; +} + +bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff) +{ + const uint8_t level = iCreateInfo & CF_LEVEL; + const bool isHellfireItem = (dwBuff & CF_HELLFIRE) != 0; + + // Check all unique monster levels to see if they match the item level + for (int i = 0; UniqueMonstersData[i].mName != nullptr; i++) { + const auto &uniqueMonsterData = UniqueMonstersData[i]; + const auto &uniqueMonsterLevel = static_cast(MonstersData[uniqueMonsterData.mtype].level); + + if (IsAnyOf(uniqueMonsterData.mtype, MT_DEFILER, MT_NAKRUL, MT_HORKDMN)) { + // These monsters don't use their mlvl for item generation + continue; + } + + if (level == uniqueMonsterLevel) { + // If the ilvl matches the mlvl, we confirm the item is legitimate + return true; + } + } + + return false; +} + +bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff) +{ + const uint8_t level = iCreateInfo & CF_LEVEL; + const bool isHellfireItem = (dwBuff & CF_HELLFIRE) != 0; + + // Check all monster levels to see if they match the item level + for (int16_t i = 0; i < static_cast(NUM_MTYPES); i++) { + const auto &monsterData = MonstersData[i]; + auto monsterLevel = static_cast(monsterData.level); + + if (i != MT_DIABLO && monsterData.availability == MonsterAvailability::Never) { + // Skip monsters that are unable to appear in the game + continue; + } + + if (i == MT_DIABLO && !isHellfireItem) { + // Adjust The Dark Lord's mlvl if the item isn't a Hellfire item to match the Diablo mlvl + monsterLevel -= 15; + } + + if (level == monsterLevel) { + // If the ilvl matches the mlvl, we confirm the item is legitimate + return true; + } + } + + if (isHellfireItem) { + uint8_t hellfireMaxDungeonLevel = 24; + + // Hellfire adjusts the currlevel minus 7 in dungeon levels 20-24 for generating items + hellfireMaxDungeonLevel -= 7; + return level <= (hellfireMaxDungeonLevel * 2); + } + + uint8_t diabloMaxDungeonLevel = 16; + + // Diablo doesn't have containers that drop items in dungeon level 16, therefore we decrement by 1 + diabloMaxDungeonLevel--; + return level <= (diabloMaxDungeonLevel * 2); +} + +bool IsItemValid(const Item &item) +{ + if (!gbIsMultiplayer) + return true; + + if (item.IDidx != IDI_GOLD && !IsCreationFlagComboValid(item._iCreateInfo)) + return false; + + if ((item._iCreateInfo & CF_TOWN) != 0) { + if (!IsTownItemValid(item._iCreateInfo) || !IsShopPriceValid(item)) + return false; + } else if ((item._iCreateInfo & CF_USEFUL) == CF_UPER15) { + if (!IsUniqueMonsterItemValid(item._iCreateInfo, item.dwBuff)) + return false; + } + + if (!IsDungeonItemValid(item._iCreateInfo, item.dwBuff)) + return false; + + return true; +} + +} // namespace devilution diff --git a/Source/items/validation.h b/Source/items/validation.h new file mode 100644 index 00000000000..884ecf3b87e --- /dev/null +++ b/Source/items/validation.h @@ -0,0 +1,23 @@ +/** + * @file items/validation.h + * + * Interface of functions for validation of player and item data. + */ +#pragma once + +#include + +// Forward declared structs to avoid circular dependencies +struct Item; +struct Player; + +namespace devilution { + +bool IsCreationFlagComboValid(uint16_t iCreateInfo); +bool IsTownItemValid(uint16_t iCreateInfo); +bool IsShopPriceValid(const Item &item); +bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff); +bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff); +bool IsItemValid(const Item &item); + +} // namespace devilution diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 6d7216ee5d4..75ee449dd21 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -566,7 +566,7 @@ void LoadPlayer(LoadHelper &file, Player &player) sgGameInitInfo.nDifficulty = static_cast<_difficulty>(file.NextLE()); player.pDamAcFlags = static_cast(file.NextLE()); file.Skip(20); // Available bytes - CalcPlrItemVals(player, false); + CalcPlrInv(player, false); player.executedSpell = player.queuedSpell; // Ensures backwards compatibility @@ -958,24 +958,6 @@ bool LevelFileExists(SaveWriter &archive) return archive.HasFile(szName); } -bool IsShopPriceValid(const Item &item) -{ - const int boyPriceLimit = 90000; - if (!gbIsHellfire && (item._iCreateInfo & CF_BOY) != 0 && item._iIvalue > boyPriceLimit) - return false; - - const int premiumPriceLimit = 140000; - if (!gbIsHellfire && (item._iCreateInfo & CF_SMITHPREMIUM) != 0 && item._iIvalue > premiumPriceLimit) - return false; - - const uint16_t smithOrWitch = CF_SMITH | CF_WITCH; - const int smithAndWitchPriceLimit = gbIsHellfire ? 200000 : 140000; - if ((item._iCreateInfo & smithOrWitch) != 0 && item._iIvalue > smithAndWitchPriceLimit) - return false; - - return true; -} - void LoadMatchingItems(LoadHelper &file, const Player &player, const int n, Item *pItem) { Item heroItem; @@ -1001,10 +983,6 @@ void LoadMatchingItems(LoadHelper &file, const Player &player, const int n, Item unpackedItem._iMaxCharges = clamp(heroItem._iMaxCharges, 0, unpackedItem._iMaxCharges); unpackedItem._iCharges = clamp(heroItem._iCharges, 0, unpackedItem._iMaxCharges); } - if (!IsShopPriceValid(unpackedItem)) { - unpackedItem.clear(); - continue; - } if (gbIsHellfire) { unpackedItem._iPLToHit = ClampToHit(unpackedItem, heroItem._iPLToHit); // Oil of Accuracy unpackedItem._iMaxDam = ClampMaxDam(unpackedItem, heroItem._iMaxDam); // Oil of Sharpness diff --git a/Source/pack.cpp b/Source/pack.cpp index 3d2f2465c75..fc82b02d5b0 100644 --- a/Source/pack.cpp +++ b/Source/pack.cpp @@ -9,6 +9,7 @@ #include "engine/random.hpp" #include "init.h" +#include "items/validation.h" #include "loadsave.h" #include "playerdat.hpp" #include "plrmsg.h" @@ -75,111 +76,8 @@ void VerifyGoldSeeds(Player &player) } } -bool hasMultipleFlags(uint16_t flags) -{ - return (flags & (flags - 1)) > 0; -} - } // namespace -bool IsCreationFlagComboValid(uint16_t iCreateInfo) -{ - iCreateInfo = iCreateInfo & ~CF_LEVEL; - const bool isTownItem = (iCreateInfo & CF_TOWN) != 0; - const bool isPregenItem = (iCreateInfo & CF_PREGEN) != 0; - const bool isUsefulItem = (iCreateInfo & CF_USEFUL) == CF_USEFUL; - - if (isPregenItem) { - // Pregen flags are discarded when an item is picked up, therefore impossible to have in the inventory - return false; - } - if (isUsefulItem && (iCreateInfo & ~CF_USEFUL) != 0) - return false; - if (isTownItem && hasMultipleFlags(iCreateInfo)) { - // Items from town can only have 1 towner flag - return false; - } - return true; -} - -bool IsTownItemValid(uint16_t iCreateInfo) -{ - const uint8_t level = iCreateInfo & CF_LEVEL; - const bool isBoyItem = (iCreateInfo & CF_BOY) != 0; - const uint8_t maxTownItemLevel = 30; - - // Wirt items in multiplayer are equal to the level of the player, therefore they cannot exceed the max character level - if (isBoyItem && level <= MaxCharacterLevel) - return true; - - return level <= maxTownItemLevel; -} - -bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff) -{ - const uint8_t level = iCreateInfo & CF_LEVEL; - const bool isHellfireItem = (dwBuff & CF_HELLFIRE) != 0; - - // Check all unique monster levels to see if they match the item level - for (int i = 0; UniqueMonstersData[i].mName != nullptr; i++) { - const auto &uniqueMonsterData = UniqueMonstersData[i]; - const auto &uniqueMonsterLevel = static_cast(MonstersData[uniqueMonsterData.mtype].level); - - if (IsAnyOf(uniqueMonsterData.mtype, MT_DEFILER, MT_NAKRUL, MT_HORKDMN)) { - // These monsters don't use their mlvl for item generation - continue; - } - - if (level == uniqueMonsterLevel) { - // If the ilvl matches the mlvl, we confirm the item is legitimate - return true; - } - } - - return false; -} - -bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff) -{ - const uint8_t level = iCreateInfo & CF_LEVEL; - const bool isHellfireItem = (dwBuff & CF_HELLFIRE) != 0; - - // Check all monster levels to see if they match the item level - for (int16_t i = 0; i < static_cast(NUM_MTYPES); i++) { - const auto &monsterData = MonstersData[i]; - auto monsterLevel = static_cast(monsterData.level); - - if (i != MT_DIABLO && monsterData.availability == MonsterAvailability::Never) { - // Skip monsters that are unable to appear in the game - continue; - } - - if (i == MT_DIABLO && !isHellfireItem) { - // Adjust The Dark Lord's mlvl if the item isn't a Hellfire item to match the Diablo mlvl - monsterLevel -= 15; - } - - if (level == monsterLevel) { - // If the ilvl matches the mlvl, we confirm the item is legitimate - return true; - } - } - - if (isHellfireItem) { - uint8_t hellfireMaxDungeonLevel = 24; - - // Hellfire adjusts the currlevel minus 7 in dungeon levels 20-24 for generating items - hellfireMaxDungeonLevel -= 7; - return level <= (hellfireMaxDungeonLevel * 2); - } - - uint8_t diabloMaxDungeonLevel = 16; - - // Diablo doesn't have containers that drop items in dungeon level 16, therefore we decrement by 1 - diabloMaxDungeonLevel--; - return level <= (diabloMaxDungeonLevel * 2); -} - bool RecreateHellfireSpellBook(const Player &player, const TItem &packedItem, Item *item) { Item spellBook {}; diff --git a/Source/pack.h b/Source/pack.h index eef50d6d21e..9b2b8e65ba0 100644 --- a/Source/pack.h +++ b/Source/pack.h @@ -142,10 +142,6 @@ struct PlayerNetPack { }; #pragma pack(pop) -bool IsCreationFlagComboValid(uint16_t iCreateInfo); -bool IsTownItemValid(uint16_t iCreateInfo); -bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff); -bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff); bool RecreateHellfireSpellBook(const Player &player, const TItem &packedItem, Item *item = nullptr); void PackPlayer(PlayerPack &pPack, const Player &player); void UnPackPlayer(const PlayerPack &pPack, Player &player); diff --git a/Source/player.cpp b/Source/player.cpp index fa258f3036a..be53b771af0 100644 --- a/Source/player.cpp +++ b/Source/player.cpp @@ -1619,6 +1619,16 @@ void Player::CalcScrolls() EnsureValidReadiedSpell(*this); } +bool Player::CanUseItem(const Item &item) const +{ + if (!IsItemValid(item)) + return false; + + return _pStrength >= item._iMinStr + && _pMagic >= item._iMinMag + && _pDexterity >= item._iMinDex; +} + void Player::RemoveInvItem(int iv, bool calcScrolls) { if (this == MyPlayer) { diff --git a/Source/player.h b/Source/player.h index a89263c1e8f..801670a7e1c 100644 --- a/Source/player.h +++ b/Source/player.h @@ -20,6 +20,7 @@ #include "engine/point.hpp" #include "interfac.h" #include "items.h" +#include "items/validation.h" #include "levels/gendung.h" #include "multi.h" #include "spelldat.h" @@ -379,12 +380,7 @@ struct Player { void CalcScrolls(); - bool CanUseItem(const Item &item) const - { - return _pStrength >= item._iMinStr - && _pMagic >= item._iMinMag - && _pDexterity >= item._iMinDex; - } + bool CanUseItem(const Item &item) const; /** * @brief Remove an item from player inventory diff --git a/Source/stores.cpp b/Source/stores.cpp index e1114ccfa21..5ed01eee4d8 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -1758,11 +1758,11 @@ void BoyEnter() StartStore(TalkID::Gossip); } -void BoyBuyItem(Item &item) +void BoyBuyItem(Item &item, int itemPrice) { - TakePlrsMoney(item._iIvalue); + TakePlrsMoney(itemPrice); StoreAutoPlace(item, true); - boyitem.clear(); + item.clear(); stextshold = TalkID::Boy; CalcPlrInv(*MyPlayer, true); stextlhold = 12; @@ -1886,7 +1886,7 @@ void ConfirmEnter(Item &item) WitchRechargeItem(item._iIvalue); break; case TalkID::BoyBuy: - BoyBuyItem(item); + BoyBuyItem(boyitem, item._iIvalue); break; case TalkID::HealerBuy: HealerBuyItem(item); diff --git a/test/missiles_test.cpp b/test/missiles_test.cpp index e4ef2ac35fb..8892c358daf 100644 --- a/test/missiles_test.cpp +++ b/test/missiles_test.cpp @@ -42,7 +42,7 @@ TEST(Missiles, RotateBlockedMissileArrow) MyPlayer = &Players[MyPlayerId]; *MyPlayer = {}; - Player &player = Players[0]; + devilution::Player &player = Players[0]; // missile can be a copy or a reference, there's no nullptr check and the functions that use it don't expect the instance to be part of a global structure so it doesn't really matter for this use. Missile missile = *AddMissile({ 0, 0 }, { 0, 0 }, Direction::South, MissileID::Arrow, TARGET_MONSTERS, player.getId(), 0, 0); diff --git a/test/player_test.cpp b/test/player_test.cpp index a92baa7291d..c379c4a767d 100644 --- a/test/player_test.cpp +++ b/test/player_test.cpp @@ -10,7 +10,7 @@ extern bool TestPlayerDoGotHit(Player &player); int RunBlockTest(int frames, ItemSpecialEffect flags) { - Player &player = Players[0]; + devilution::Player &player = Players[0]; player._pHFrames = frames; player._pIFlags = flags; @@ -84,7 +84,7 @@ TEST(Player, PM_DoGotHit) } } -static void AssertPlayer(Player &player) +static void AssertPlayer(devilution::Player &player) { ASSERT_EQ(CountU8(player._pSplLvl, 64), 0); ASSERT_EQ(Count8(player.InvGrid, InventoryGridCells), 1); diff --git a/test/player_test.h b/test/player_test.h index 955d8b25b59..903d42d4965 100644 --- a/test/player_test.h +++ b/test/player_test.h @@ -10,9 +10,9 @@ using namespace devilution; -static int CountItems(Item *items, int n) +static int CountItems(devilution::Item *items, int n) { - return std::count_if(items, items + n, [](Item x) { return !x.isEmpty(); }); + return std::count_if(items, items + n, [](devilution::Item x) { return !x.isEmpty(); }); } static int Count8(int8_t *ints, int n) diff --git a/test/stores_test.cpp b/test/stores_test.cpp index b321cf87ecd..bc718291f20 100644 --- a/test/stores_test.cpp +++ b/test/stores_test.cpp @@ -8,7 +8,7 @@ namespace { TEST(Stores, AddStoreHoldRepair_magic) { - Item *item; + devilution::Item *item; item = &storehold[0]; @@ -41,7 +41,7 @@ TEST(Stores, AddStoreHoldRepair_magic) TEST(Stores, AddStoreHoldRepair_normal) { - Item *item; + devilution::Item *item; item = &storehold[0];