From a30f7c0b8ea7c954590967e50abb31cc234186f5 Mon Sep 17 00:00:00 2001 From: Eric Robinson <68359262+kphoenix137@users.noreply.github.com> Date: Tue, 18 Jun 2024 19:20:15 -0400 Subject: [PATCH] Randomize Unique Item Generation (Reverse compatible) (#7060) Co-authored-by: staphen --- Source/items.cpp | 163 ++++++++++++++++++++++++++++++++++++++--------- Source/items.h | 4 +- Source/msg.cpp | 2 +- Source/pack.cpp | 1 + 4 files changed, 137 insertions(+), 33 deletions(-) diff --git a/Source/items.cpp b/Source/items.cpp index 126127d8a12..e7116d6ded8 100644 --- a/Source/items.cpp +++ b/Source/items.cpp @@ -1440,44 +1440,39 @@ _item_indexes RndTypeItems(ItemType itemType, int imid, int lvl) }); } -_unique_items CheckUnique(Item &item, int lvl, int uper, bool recreate) +_unique_items CheckUnique(Item &item, int lvl, int uper, int uidOffset = 0) { std::bitset<128> uok = {}; if (GenerateRnd(100) > uper) return UITEM_INVALID; - int numu = 0; - for (int j = 0, n = static_cast(UniqueItems.size()); j < n; ++j) { + std::vector validUniques; + for (int j = 0; j < static_cast(UniqueItems.size()); ++j) { if (!IsUniqueAvailable(j)) break; - if (UniqueItems[j].UIItemId == AllItemsList[item.IDidx].iItemId - && lvl >= UniqueItems[j].UIMinLvl - && (recreate || !UniqueItemFlags[j] || gbIsMultiplayer)) { - uok[j] = true; - numu++; + if (UniqueItems[j].UIItemId == AllItemsList[item.IDidx].iItemId && lvl >= UniqueItems[j].UIMinLvl) { + validUniques.push_back(j); } } - if (numu == 0) + if (validUniques.empty()) return UITEM_INVALID; DiscardRandomValues(1); - uint8_t itemData = 0; - while (numu > 0) { - if (uok[itemData]) - numu--; - if (numu > 0) - itemData = (itemData + 1) % 128; + + // Check if uidOffset is out of bounds + if (uidOffset >= validUniques.size()) { + return UITEM_INVALID; } - return (_unique_items)itemData; + uint8_t selectedUniqueIndex = validUniques[validUniques.size() - 1 - uidOffset]; + + return static_cast<_unique_items>(selectedUniqueIndex); } void GetUniqueItem(const Player &player, Item &item, _unique_items uid) { - UniqueItemFlags[uid] = true; - const auto &uniqueItemData = UniqueItems[uid]; for (auto power : uniqueItemData.powers) { @@ -1519,7 +1514,7 @@ int GetItemBLevel(int lvl, item_misc_id miscId, bool onlygood, bool uper15) return iblvl; } -void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t iseed, int lvl, int uper, bool onlygood, bool recreate, bool pregen) +void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t iseed, int lvl, int uper, bool onlygood, bool pregen, int uidOffset = 0, bool forceNotUnique = false) { item._iSeed = iseed; SetRndSeed(iseed); @@ -1539,7 +1534,12 @@ void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t if (item._iMiscId != IMISC_UNIQUE) { int iblvl = GetItemBLevel(lvl, item._iMiscId, onlygood, uper == 15); if (iblvl != -1) { - _unique_items uid = CheckUnique(item, iblvl, uper, recreate); + _unique_items uid = UITEM_INVALID; + if (!forceNotUnique) { + uid = CheckUnique(item, iblvl, uper, uidOffset); + } else { + DiscardRandomValues(1); + } if (uid == UITEM_INVALID) { GetItemBonus(player, item, iblvl / 2, iblvl, onlygood, true); } else { @@ -1558,7 +1558,6 @@ void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t GetUniqueItem(player, item, (_unique_items)iseed); // uid is stored in iseed for uniques } } - SetupItem(item); } void SetupBaseItem(Point position, _item_indexes idx, bool onlygood, bool sendmsg, bool delta, bool spawn = false) @@ -1571,7 +1570,9 @@ void SetupBaseItem(Point position, _item_indexes idx, bool onlygood, bool sendms GetSuperItemSpace(position, ii); int curlv = ItemsGetCurrlevel(); - SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * curlv, 1, onlygood, false, delta); + SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * curlv, 1, onlygood, delta); + TryRandomUniqueItem(item, idx, 2 * curlv, 1, onlygood, delta); + SetupItem(item); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); @@ -2246,7 +2247,9 @@ void CreateMagicItem(Point position, int lvl, ItemType itemType, int imid, int i while (true) { item = {}; - SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, false, delta); + SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, delta); + TryRandomUniqueItem(item, idx, 2 * lvl, 1, true, delta); + SetupItem(item); if (item._iCurs == icurs) break; @@ -3283,7 +3286,9 @@ Item *SpawnUnique(_unique_items uid, Point position, std::optional level /* _item_indexes idx = GetItemIndexForDroppableItem(false, [&uniqueItemData](const ItemData &item) { return item.itype == uniqueItemData.itype; }); - SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), curlv * 2, 15, true, false, false); + SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), curlv * 2, 15, true, false); + TryRandomUniqueItem(item, idx, curlv * 2, 15, true, false); + SetupItem(item); } if (sendmsg) @@ -3292,6 +3297,93 @@ Item *SpawnUnique(_unique_items uid, Point position, std::optional level /* return &item; } +void TryRandomUniqueItem(Item &item, _item_indexes idx, int8_t mLevel, int uper, bool onlygood, bool pregen) +{ + // If the item is a non-quest unique, find a random valid uid and force generate items to get an item with that uid. + if ((item._iCreateInfo & CF_UNIQUE) == 0 || item._iMiscId == IMISC_UNIQUE) + return; + + std::vector uids; + + // Gather all potential unique items. uid is the index into UniqueItems. + for (size_t i = 0; i < UniqueItems.size(); ++i) { + const UniqueItem &uniqueItem = UniqueItems[i]; + // Verify the unique item base item matches idx. + bool isMatchingItemId = uniqueItem.UIItemId == AllItemsList[static_cast(idx)].iItemId; + // Verify itemLvl is at least the unique's minimum required level. + // mLevel remains unadjusted when arriving in this function, and there is no call to set iblvl until CheckUnique(), so we adjust here. + bool meetsLevelRequirement = ((uper == 15) ? mLevel + 4 : mLevel) >= uniqueItem.UIMinLvl; + // Verify item hasn't been dropped yet. We set this to true in MP, since uniques previously dropping shouldn't prevent further identical uniques from dropping. + bool uniqueNotDroppedAlready = !UniqueItemFlags[i] || gbIsMultiplayer; + + int uid = static_cast(i); + if (IsUniqueAvailable(uid) && isMatchingItemId && meetsLevelRequirement && uniqueNotDroppedAlready) + uids.emplace_back(uid); + } + + // If we find at least one unique in uids that hasn't been obtained yet, we can proceed getting a random unique. + if (uids.empty()) { + // Set uper to 1 and make the level adjustment so we have better odds of not generating a unique item. + if (uper == 15) + mLevel += 4; + uper = 1; + + Point itemPos = item.position; + + // Force generate a non-unique item. + DiabloGenerator itemGenerator(item._iSeed); + do { + item = {}; // Reset item data + item.position = itemPos; + SetupAllItems(*MyPlayer, item, idx, itemGenerator.advanceRndSeed(), mLevel, uper, onlygood, pregen); + } while (item._iMagical == ITEM_QUALITY_UNIQUE); + + return; + } + + int32_t uidsIdx = std::max(0, GenerateRnd(static_cast(uids.size()))); // Index into uids, used to get a random uid from the uids vector. + int uid = uids[uidsIdx]; // Actual unique id. + + // If the selected unique was already generated, there is no need to fiddle with its parameters. + if (item._iUid == uid) { + UniqueItemFlags[uid] = true; + return; + } + + const UniqueItem &uniqueItem = UniqueItems[uid]; + int targetLvl = 1; // Target level for reverse compatibility, since vanilla always takes the last applicable uid in the list. + + // Set target level. Ideally we use uper 15 to have a 16% chance of generating a unique item. + if (uniqueItem.UIMinLvl - 4 > 0) { // Negative level will underflow. Lvl 0 items may have unintended consequences. + uper = 15; + targetLvl = uniqueItem.UIMinLvl - 4; + } else { + uper = 1; + targetLvl = uniqueItem.UIMinLvl; + } + + // Amount to decrease the final uid by in CheckUnique() to get the desired unique. + const int uidOffset = static_cast(std::count_if(UniqueItems.begin() + uid + 1, UniqueItems.end(), [&uniqueItem](UniqueItem &potentialMatch) { + return uniqueItem.UIItemId == potentialMatch.UIItemId && uniqueItem.UIMinLvl == potentialMatch.UIMinLvl; + })); + + Point itemPos = item.position; + + // Force generate items until we find a uid match. + DiabloGenerator itemGenerator(item._iSeed); + do { + item = {}; // Reset item data + item.position = itemPos; + SetupAllItems(*MyPlayer, item, idx, itemGenerator.advanceRndSeed(), targetLvl, uper, onlygood, pregen, uidOffset); + } while (item._iUid != uid); + + // Set item as obtained to prevent it from being dropped again in SP. + if (!gbIsMultiplayer) { + UniqueItemFlags[uid] = true; + } + item.dwBuff |= (uidOffset << 1) & CF_UIDOFFSET; +} + void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn /*= false*/) { _item_indexes idx; @@ -3321,13 +3413,13 @@ void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn /*= fa // Drop the brain as extra item to ensure that all clients see the brain drop // When executing SpawnItem is not reliable, cause another client can already have the quest state updated before SpawnItem is executed Point posBrain = GetSuperItemLoc(position); - SpawnQuestItem(IDI_BRAIN, posBrain, false, false, true); + SpawnQuestItem(IDI_BRAIN, posBrain, 0, 0, true); } // Normal monster if ((monster.data().treasure & T_NODROP) != 0) return; onlygood = false; - idx = RndItemForMonsterLevel(monster.level(sgGameInitInfo.nDifficulty)); + idx = RndItemForMonsterLevel(static_cast(monster.level(sgGameInitInfo.nDifficulty))); } if (idx == IDI_NONE) @@ -3346,6 +3438,8 @@ void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn /*= fa mLevel -= 15; SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), mLevel, uper, onlygood, false, false); + TryRandomUniqueItem(item, idx, mLevel, uper, onlygood, false); + SetupItem(item); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); @@ -3433,10 +3527,12 @@ void RecreateItem(const Player &player, Item &item, _item_indexes idx, uint16_t uper = 15; bool onlygood = (icreateinfo & CF_ONLYGOOD) != 0; - bool recreate = (icreateinfo & CF_UNIQUE) != 0; + bool forceNotUnique = (icreateinfo & CF_UNIQUE) == 0; bool pregen = (icreateinfo & CF_PREGEN) != 0; + auto uidOffset = static_cast((item.dwBuff & CF_UIDOFFSET) >> 1); - SetupAllItems(player, item, idx, iseed, level, uper, onlygood, recreate, pregen); + SetupAllItems(player, item, idx, iseed, level, uper, onlygood, pregen, uidOffset, forceNotUnique); + SetupItem(item); gbIsHellfire = tmpIsHellfire; } @@ -4561,7 +4657,8 @@ void CreateSpellBook(Point position, SpellID ispell, bool sendmsg, bool delta) while (true) { item = {}; - SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, false, delta); + SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, delta); + SetupItem(item); if (item._iMiscId == IMISC_BOOK && item._iSpell == ispell) break; } @@ -4673,7 +4770,9 @@ std::string DebugSpawnItem(std::string itemName) continue; testItem = {}; - SetupAllItems(*MyPlayer, testItem, idx, AdvanceRndSeed(), monsterLevel, 1, false, false, false); + SetupAllItems(*MyPlayer, testItem, idx, AdvanceRndSeed(), monsterLevel, 1, false, false); + TryRandomUniqueItem(testItem, idx, monsterLevel, 1, false, false); + SetupItem(testItem); std::string tmp = AsciiStrToLower(testItem._iIName); if (tmp.find(itemName) != std::string::npos) @@ -4746,7 +4845,9 @@ std::string DebugSpawnUniqueItem(std::string itemName) for (auto &flag : UniqueItemFlags) flag = true; UniqueItemFlags[uniqueIndex] = false; - SetupAllItems(*MyPlayer, testItem, uniqueBaseIndex, testItem._iMiscId == IMISC_UNIQUE ? uniqueIndex : AdvanceRndSeed(), uniqueItem.UIMinLvl, 1, false, false, false); + SetupAllItems(*MyPlayer, testItem, uniqueBaseIndex, testItem._iMiscId == IMISC_UNIQUE ? uniqueIndex : AdvanceRndSeed(), uniqueItem.UIMinLvl, 1, false, false); + TryRandomUniqueItem(testItem, uniqueBaseIndex, uniqueItem.UIMinLvl, 1, false, false); + SetupItem(testItem); for (auto &flag : UniqueItemFlags) flag = false; diff --git a/Source/items.h b/Source/items.h index f5b49c5083e..c564a5be926 100644 --- a/Source/items.h +++ b/Source/items.h @@ -165,7 +165,8 @@ enum icreateinfo_flag { enum icreateinfo_flag2 { // clang-format off - CF_HELLFIRE = 1, + CF_HELLFIRE = 1 << 0, + CF_UIDOFFSET = ((1 << 4) - 1) << 1, // clang-format on }; @@ -512,6 +513,7 @@ Point GetSuperItemLoc(Point position); void GetItemAttrs(Item &item, _item_indexes itemData, int lvl); void SetupItem(Item &item); Item *SpawnUnique(_unique_items uid, Point position, std::optional level = std::nullopt, bool sendmsg = true, bool exactPosition = false); +void TryRandomUniqueItem(Item &item, _item_indexes idx, int8_t mLevel, int uper, bool onlygood, bool pregen); void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn = false); void CreateRndItem(Point position, bool onlygood, bool sendmsg, bool delta); void CreateRndUseful(Point position, bool sendmsg); diff --git a/Source/msg.cpp b/Source/msg.cpp index f3d90d0095a..954aab2ee07 100644 --- a/Source/msg.cpp +++ b/Source/msg.cpp @@ -2425,6 +2425,7 @@ void PrepareEarForNetwork(const Item &item, TEar &ear) void RecreateItem(const Player &player, const TItem &messageItem, Item &item) { const uint32_t dwBuff = SDL_SwapLE32(messageItem.dwBuff); + item.dwBuff = dwBuff; RecreateItem(player, item, static_cast<_item_indexes>(SDL_SwapLE16(messageItem.wIndx)), SDL_SwapLE16(messageItem.wCI), SDL_SwapLE32(messageItem.dwSeed), SDL_SwapLE16(messageItem.wValue), (dwBuff & CF_HELLFIRE) != 0); @@ -2438,7 +2439,6 @@ void RecreateItem(const Player &player, const TItem &messageItem, Item &item) item._iPLToHit = ClampToHit(item, static_cast(SDL_SwapLE16(messageItem.wToHit))); item._iMaxDam = ClampMaxDam(item, static_cast(SDL_SwapLE16(messageItem.wMaxDam))); } - item.dwBuff = dwBuff; } void ClearLastSentPlayerCmd() diff --git a/Source/pack.cpp b/Source/pack.cpp index a1ef5432215..d1c0f37c2c6 100644 --- a/Source/pack.cpp +++ b/Source/pack.cpp @@ -431,6 +431,7 @@ void UnPackItem(const ItemPack &packedItem, const Player &player, Item &item, bo RecreateEar(item, ic, iseed, ivalue & 0xFF, heroName); } else { item = {}; + item.dwBuff = SDL_SwapLE32(packedItem.dwBuff); RecreateItem(player, item, idx, SDL_SwapLE16(packedItem.iCreateInfo), SDL_SwapLE32(packedItem.iSeed), SDL_SwapLE16(packedItem.wValue), isHellfire); item._iIdentified = (packedItem.bId & 1) != 0; item._iMaxDur = packedItem.bMDur;