Skip to content

Commit

Permalink
Make invalid items unusable (diasurgical#7506)
Browse files Browse the repository at this point in the history
  • Loading branch information
kphoenix137 authored and StephenCWills committed Dec 1, 2024
1 parent f92ef8e commit e7d61f2
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 153 deletions.
2 changes: 2 additions & 0 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 16 additions & 10 deletions Source/items.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions Source/items.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
157 changes: 157 additions & 0 deletions Source/items/validation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @file items/validation.cpp
*
* Implementation of functions for validation of player and item data.
*/

#include "items/validation.h"

#include <cstdint>

#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 = gbIsHellfire ? MaxBoyValueHf : MaxBoyValue;
if ((item._iCreateInfo & CF_BOY) != 0 && item._iIvalue > boyPriceLimit)
return false;

const uint16_t smithOrWitch = CF_SMITH | CF_SMITHPREMIUM | 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<uint8_t>(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<int16_t>(NUM_MTYPES); i++) {
const auto &monsterData = MonstersData[i];
auto monsterLevel = static_cast<uint8_t>(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 (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
23 changes: 23 additions & 0 deletions Source/items/validation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @file items/validation.h
*
* Interface of functions for validation of player and item data.
*/
#pragma once

#include <cstdint>

// 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
24 changes: 1 addition & 23 deletions Source/loadsave.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ void LoadPlayer(LoadHelper &file, Player &player)
sgGameInitInfo.nDifficulty = static_cast<_difficulty>(file.NextLE<uint32_t>());
player.pDamAcFlags = static_cast<ItemSpecialEffectHf>(file.NextLE<uint32_t>());
file.Skip(20); // Available bytes
CalcPlrItemVals(player, false);
CalcPlrInv(player, false);

player.executedSpell = player.queuedSpell; // Ensures backwards compatibility

Expand Down Expand Up @@ -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;
Expand All @@ -1001,10 +983,6 @@ void LoadMatchingItems(LoadHelper &file, const Player &player, const int n, Item
unpackedItem._iMaxCharges = clamp<int>(heroItem._iMaxCharges, 0, unpackedItem._iMaxCharges);
unpackedItem._iCharges = clamp<int>(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
Expand Down
Loading

0 comments on commit e7d61f2

Please sign in to comment.