Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make invalid items unusable #7506

Merged
merged 14 commits into from
Nov 15, 2024
2 changes: 2 additions & 0 deletions Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ set(libdevilutionx_SRCS
towners.cpp
track.cpp

common/validation.cpp
kphoenix137 marked this conversation as resolved.
Show resolved Hide resolved

controls/axis_direction.cpp
controls/controller.cpp
controls/controller_buttons.cpp
Expand Down
121 changes: 121 additions & 0 deletions Source/common/validation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @file common/validation.cpp
*
* Implementation of functions for validation of player and item data.
*/

#include "common/validation.h"

#include <cstdint>

#include "items.h"
#include "monstdat.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, uint8_t maxCharacterLevel)
{
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;

// Check all unique monster levels to see if they match the item level
for (const UniqueMonsterData &uniqueMonsterData : UniqueMonstersData) {
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;
kphoenix137 marked this conversation as resolved.
Show resolved Hide resolved
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);
}

} // namespace devilution
17 changes: 17 additions & 0 deletions Source/common/validation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @file common/validation.h
*
* Interface of functions for validation of player and item data.
*/
#pragma once

#include <cstdint>

namespace devilution {

bool IsCreationFlagComboValid(uint16_t iCreateInfo);
bool IsTownItemValid(uint16_t iCreateInfo, uint8_t maxCharacterLevel);
bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff);
bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff);

} // namespace devilution
28 changes: 25 additions & 3 deletions Source/items.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -512,13 +512,35 @@ 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 = true;

if (equipment.IDidx != IDI_GOLD) {
if (!IsCreationFlagComboValid(equipment._iCreateInfo))
isValid = false;
}

if ((equipment._iCreateInfo & CF_TOWN) != 0) {
if (!IsTownItemValid(equipment._iCreateInfo, player.getMaxCharacterLevel()))
isValid = false;
} else if ((equipment._iCreateInfo & CF_USEFUL) == CF_UPER15) {
if (!IsUniqueMonsterItemValid(equipment._iCreateInfo, equipment.dwBuff))
isValid = false;
} else {
if (!IsDungeonItemValid(equipment._iCreateInfo, equipment.dwBuff))
isValid = false;
}

if (currstr < equipment._iMinStr
|| currmag < equipment._iMinMag
|| currdex < equipment._iMinDex)
isValid = false;
kphoenix137 marked this conversation as resolved.
Show resolved Hide resolved

if (isValid)
continue;

changeflag = true;
Expand Down
2 changes: 1 addition & 1 deletion Source/msg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,7 @@ bool IsPItemValid(const TCmdPItem &message, const Player &player)
if (idx != IDI_GOLD)
ValidateField(creationFlags, IsCreationFlagComboValid(creationFlags));
if ((creationFlags & CF_TOWN) != 0)
ValidateField(creationFlags, IsTownItemValid(creationFlags, player));
ValidateField(creationFlags, IsTownItemValid(creationFlags, player.getMaxCharacterLevel()));
else if ((creationFlags & CF_USEFUL) == CF_UPER15)
ValidateFields(creationFlags, dwBuff, IsUniqueMonsterItemValid(creationFlags, dwBuff));
else if ((dwBuff & CF_HELLFIRE) != 0 && AllItemsList[idx].iMiscId == IMISC_BOOK)
Expand Down
104 changes: 2 additions & 102 deletions Source/pack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <cstdint>

#include "common/validation.h"
#include "engine/random.hpp"
#include "init.h"
#include "loadsave.h"
Expand Down Expand Up @@ -75,109 +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 Player &player)
{
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 <= player.getMaxCharacterLevel())
return true;

return level <= maxTownItemLevel;
}

bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff)
{
const uint8_t level = iCreateInfo & CF_LEVEL;

// Check all unique monster levels to see if they match the item level
for (const UniqueMonsterData &uniqueMonsterData : UniqueMonstersData) {
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 RecreateHellfireSpellBook(const Player &player, const TItem &packedItem, Item *item)
{
Item spellBook {};
Expand Down Expand Up @@ -545,7 +445,7 @@ bool UnPackNetItem(const Player &player, const ItemNetPack &packedItem, Item &it
if (idx != IDI_GOLD)
ValidateField(creationFlags, IsCreationFlagComboValid(creationFlags));
if ((creationFlags & CF_TOWN) != 0)
ValidateField(creationFlags, IsTownItemValid(creationFlags, player));
ValidateField(creationFlags, IsTownItemValid(creationFlags, player.getMaxCharacterLevel()));
else if ((creationFlags & CF_USEFUL) == CF_UPER15)
ValidateFields(creationFlags, dwBuff, IsUniqueMonsterItemValid(creationFlags, dwBuff));
else if ((dwBuff & CF_HELLFIRE) != 0 && AllItemsList[idx].iMiscId == IMISC_BOOK)
Expand Down
4 changes: 0 additions & 4 deletions Source/pack.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,6 @@ struct PlayerNetPack {
};
#pragma pack(pop)

bool IsCreationFlagComboValid(uint16_t iCreateInfo);
bool IsTownItemValid(uint16_t iCreateInfo, const Player &player);
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);
Expand Down
17 changes: 17 additions & 0 deletions Source/player.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <algorithm>
#include <array>

#include "common/validation.h"
#include "diablo.h"
#include "engine.h"
#include "engine/actor_position.hpp"
Expand Down Expand Up @@ -400,6 +401,22 @@ struct Player {

bool CanUseItem(const Item &item) const
{
if (item.IDidx != IDI_GOLD) {
if (!IsCreationFlagComboValid(item._iCreateInfo))
return false;
}

if ((item._iCreateInfo & CF_TOWN) != 0) {
if (!IsTownItemValid(item._iCreateInfo, getMaxCharacterLevel()))
return false;
} else if ((item._iCreateInfo & CF_USEFUL) == CF_UPER15) {
if (!IsUniqueMonsterItemValid(item._iCreateInfo, item.dwBuff))
return false;
} else {
if (!IsDungeonItemValid(item._iCreateInfo, item.dwBuff))
return false;
}

return _pStrength >= item._iMinStr
&& _pMagic >= item._iMinMag
&& _pDexterity >= item._iMinDex;
Expand Down
1 change: 1 addition & 0 deletions test/player_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ TEST(Player, CreatePlayer)
ASSERT_TRUE(HaveSpawn() || HaveDiabdat());

LoadPlayerDataFiles();
LoadMonsterData();
LoadItemData();
Players.resize(1);
CreatePlayer(Players[0], HeroClass::Rogue);
Expand Down
1 change: 1 addition & 0 deletions test/writehero_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ TEST(Writehero, pfile_write_hero)

LoadSpellData();
LoadPlayerDataFiles();
LoadMonsterData();
LoadItemData();
_uiheroinfo info {};
info.heroclass = HeroClass::Rogue;
Expand Down
Loading