Created
October 5, 2022 23:00
-
-
Save SeanSullivan86/e912b40bb85f9822b9ef507d91534c88 to your computer and use it in GitHub Desktop.
D2 TreasureClassDropper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "D2Structs.h" | |
#include <WinDef.h> | |
void __fastcall TreasureClassDropper_0055a6d0 | |
(D2GameStrc* pGame, D2UnitStrc* pItemDropper, D2UnitStrc* pItemReceiver, D2TCExShortStrc* pParentTC, | |
uint presetQuality, uint ilvl, BOOL ignoreNoDrop, D2UnitStrc** pOutput, uint* itemCount, uint maxItems) { | |
D2Assert(pParentTC != nullptr, 0xf3a); | |
D2Assert(pOutput == nullptr || maxItems > 0, 0xf44); | |
D2Assert(pGame->bExpansion); // this function has been simplified to remove support for D2Classic | |
uint howManyItemsCreatedSoFar; | |
if (itemCount == nullptr) { | |
itemCount = &howManyItemsCreatedSoFar; | |
} | |
*itemCount = 0; | |
if (pOutput == nullptr) { | |
if (maxItems < 1) { | |
maxItems = 6; | |
} | |
} | |
D2TCStackInfo tcStack[64]; | |
initializeTreasureClassStackEntry(tcStack, 0, pParentTC); | |
int depth = 0; | |
// each iteration makes progress by doing 1 pick, or popping a TC off the stack | |
while (depth >= 0) { | |
D2TCStackInfo tc = tcStack[depth]; | |
if (tc.nPicksRemaining == 0 || tc.pTCData->nProb == 0) { | |
depth--; | |
continue; | |
} | |
tc.nPicksRemaining--; | |
int targetCumulativeFrequency; | |
if (tc.pTCData->nPicks < 0) { | |
// picks < 0 ==> Choose target of each pick deterministically | |
targetCumulativeFrequency = (-tc.pTCData->nPicks) - tc.nPicksRemaining - 1; | |
} else { | |
// picks > 0 ==> Choose random target each time | |
int rand = getRandomPositionWithinTCFrequencySumOrNodrop(tc.pTCData, pGame, pItemReceiver, pItemDropper, ignoreNoDrop); | |
if (rand < 0) { // no-drop | |
continue; | |
} | |
targetCumulativeFrequency = rand; | |
} | |
D2TCItemInfoStrc* tcItem = getTreasureClassItemFromCumulativeFrequency(tc.pTCData, targetCumulativeFrequency); | |
if (tcItem->nFlags & 0x4) { // tcItem is another TC | |
D2TCExShortStrc* newTC = GetTreasureClassShortObjById_00654e00(tcItem->nItemId, 0); | |
depth++; | |
D2Assert(depth < 64, 0xfea); | |
initializeTreasureClassStackEntry(tcStack, depth, newTC); | |
} else { // tcItem is an item (weapon/armor/misc) | |
D2UnitStrc* createdItem = generateAnItemFromATreasureClassItem(pGame, pItemReceiver, pItemDropper, tcItem, | |
&tcStack[depth].qualityInfo, presetQuality, ilvl); | |
if (createdItem != nullptr) { | |
if (pOutput != nullptr) { | |
pOutput[*itemCount] = createdItem; | |
} | |
(*itemCount)++; | |
if (*itemCount >= maxItems) { | |
return; | |
} | |
} | |
} | |
} | |
} | |
void initializeTreasureClassStackEntry(D2TCStackInfo* tcStack, int insertIndex, D2TCExShortStrc* tc) { | |
D2TCStackInfo* tcStackEntry = tcStack + insertIndex; | |
/* Make nPicksRemaining always positive on the TC Stack.If we need to know if it was originally | |
negative (to decide whether or not to do a random roll), check the D2TCExShortStrc.nPicks again. */ | |
int picks = tc->nPicks; | |
if (picks < 0) { | |
picks = -picks; | |
} else if (picks == 0) { | |
picks = 1; | |
} | |
tcStackEntry->pTCData = tc; | |
tcStackEntry->nPicksRemaining = picks; | |
if (insertIndex == 0) { | |
memcpy(&(tcStackEntry->qualityInfo), &(tc->qualityInfo), sizeof(D2TCQualityInfo)); | |
} else { | |
/* Iterate through the 'chance to drop' modifiers for each of the 6 item qualities in the TC struct | |
For each quality, either inherit the value from the parent of the current TC, or take the value | |
from the new TC's config, whichever is better/greater. */ | |
for (int i = 0; i < 6; i++) { | |
((short*)(&tcStackEntry->qualityInfo))[i] = max( | |
((short*)(&(tcStackEntry - 1)->qualityInfo))[i], | |
((short*)(&tc->qualityInfo))[i]); | |
} | |
} | |
} | |
D2UnitStrc* generateAnItemFromATreasureClassItem(D2GameStrc* pGame, D2UnitStrc* pItemReceiver, D2UnitStrc* pItemDropper, | |
D2TCItemInfoStrc* tcItem, D2TCQualityInfo* tcQualityInfo, int presetQuality, int ilvl) { | |
ushort itemId = tcItem->nItemId; | |
if (itemId == 0xffff) { | |
return nullptr; | |
} | |
int setOrUniqueTxtRow = 0, itemQuality; | |
if ((tcItem->nFlags & 1) == 0) { | |
if ((tcItem->nFlags & 2) == 0) { | |
itemQuality = presetQuality; | |
if (presetQuality == 0) { | |
itemQuality = GenerateItemQuality_00558640(itemId, ilvl, pGame, pItemDropper, pItemReceiver, tcQualityInfo); | |
} | |
} else { | |
itemQuality = ITEMQUAL_SET; | |
setOrUniqueTxtRow = tcItem->nTxtRow + 1; | |
} | |
} else { | |
itemQuality = ITEMQUAL_UNIQUE; | |
setOrUniqueTxtRow = tcItem->nTxtRow + 1; | |
} | |
uint itemQualityFlags = 0; | |
if (tcQualityInfo->nSuperior != 0) { | |
int rand = Seed_GetRand0ToN_0045C390(pItemDropper->pSeed, 1024); | |
if (rand < tcQualityInfo->nSuperior) { | |
itemQualityFlags |= 0x4; | |
} | |
} | |
if (tcQualityInfo->nNormal != 0) { | |
int rand = Seed_GetRand0ToN_0045C390(pItemDropper->pSeed, 1024); | |
if (rand < tcQualityInfo->nNormal) { | |
itemQualityFlags |= 0x10; | |
} | |
} | |
D2UnitStrc* createdItem = CreateItem_0055a550(pItemDropper, itemId, pGame, itemQuality, setOrUniqueTxtRow, itemQualityFlags); | |
if (createdItem != nullptr) { | |
if (IsItemUnitBelongingToItemTypePerhapsRecursively_00629bb0(createdItem, 4) && tcItem->nTxtRow != 0) { | |
// for a gold drop, the parameter in the TC Item Name (ie. "gld,mul=2048") is in txtRow field | |
// It's a multiplier expressed in 256ths, so mul=2048 multiplies the gold value by 8 | |
int goldAmount = GetStatValueFromUnit_WIP_00625480(createdItem, 0xe); | |
int adjustedGold = (tcItem->nTxtRow * goldAmount) >> 8; | |
Unit_SetStat_MaybeOnlyForGoldAmount_00530ea0(createdItem, 0xe, adjustedGold); | |
} | |
if (*itemsCreated >= maxItems) { | |
return; | |
} | |
// oops, item doesnt get adjusted for goldfind if it made us hit maxItems ? | |
Item_AdjustGoldDropForPlayerGoldfind_005589a0(player, createdItem); | |
} | |
return createdItem; | |
} | |
int getRandomPositionWithinTCFrequencySumOrNodrop(D2TCExShortStrc* pTreasureClass, | |
D2GameStrc* pGame, D2UnitStrc* pItemReceiver, D2UnitStrc* pItemDropper, BOOL ignoreNoDrop) { | |
int adjustedNodropFreq = getAdjustedNoDropFrequency(pTreasureClass, pGame, pItemReceiver, pItemDropper); | |
int frequencySum = pTreasureClass->nProb + (ignoreNoDrop ? 0 : adjustedNodropFreq); | |
int rand; | |
if (frequencySum < 1) { | |
rand = 0; | |
} else { | |
rand = GetAPlusRandModB_00472280(&pItemDropper->pSeed, 0, frequencySum); | |
} | |
if (ignoreNoDrop) return rand; | |
if (rand >= adjustedNodropFreq) { | |
return rand - adjustedNodropFreq; | |
} | |
return -1; // roll result was no-drop | |
} | |
D2TCItemInfoStrc* getTreasureClassItemFromCumulativeFrequency(D2TCExShortStrc* tc, int targetCumulativeFrequency) { | |
int left = 0, right = tc->nTypes, idx; | |
int selectedIndex = -1; | |
if (tc->nTypes > 0) { | |
do { | |
idx = (right - left) / 2 + left; | |
int partialSum = tc->pInfo[idx].nProb; | |
if (partialSum == targetCumulativeFrequency) { | |
if (left < right) { | |
selectedIndex = idx; | |
} | |
break; | |
} else if (partialSum < targetCumulativeFrequency) { | |
left = idx + 1; | |
idx = right; | |
} else { | |
right = idx; | |
} | |
right = idx; | |
} while (left < idx); | |
} | |
if (selectedIndex == -1) { | |
selectedIndex = (left <= 0) ? 0 : (left - 1); | |
} | |
// original code had a case for looping to the next pick if selectedIndex >= nTypes | |
// TODO: See whether that's possible with their version of binary search | |
return &tc->pInfo[selectedIndex]; | |
} | |
int getAdjustedNoDropFrequency(D2TCExShortStrc* pTreasureClass, D2GameStrc* pGame, D2UnitStrc* pItemReceiver, D2UnitStrc* pItemDropper) { | |
int adjustedPlayerCount = getAdjustedPlayerCountForNodropCalculation(pGame, pItemReceiver, pItemDropper); | |
if (pTreasureClass->nNoDrop == 0) { | |
return 0; | |
} | |
if (adjustedPlayerCount <= 1) { | |
return pTreasureClass->nNoDrop; | |
} | |
double originalNodropRate = ((double)pTreasureClass->nNoDrop) / (double)(pTreasureClass->nNoDrop + pTreasureClass->nProb); | |
// newNodropRate = pow(originalNodropRate, adjustedPlayerCount) | |
// It was implemented in assembly as repeated multiplication | |
double newNodropRate = originalNodropRate; | |
for (int n = 1; n < adjustedPlayerCount; n++) { | |
newNodropRate *= originalNodropRate; | |
} | |
// To avoid division by 0, I suppose. This would only happen if originalNodropRate==1 | |
if ((1.0 - newNodropRate) == 0.0) { | |
return 0; | |
} | |
// we want to choose noDropFreq such that noDropFreq / (noDropFreq + itemFreq) = newNodropRate | |
// nodropFreq = newNodropRate * (nodropFreq + itemFreq) | |
// nodropFreq = newNodropRrate * nodropFreq + newNodropRate * itemFreq | |
// nodropFreq * (1 - newNodropRate) = newNodropRate * itemFreq | |
// nodropFreq = newNodropRate * itemFreq / (1 - newNodropRate) | |
return (int)((newNodropRate * pTreasureClass->nProb) / (1.0 - newNodropRate)); | |
} | |
int getAdjustedPlayerCountForNodropCalculation(D2GameStrc* pGame, D2UnitStrc* pItemReceiver, D2UnitStrc* pItemDropper) { | |
int nearbyPlayerCount = 1; | |
if (pItemReceiver != nullptr) { | |
D2UnitStrc* pPlayer = pItemReceiver; | |
if (pPlayer->dwUnitType != 0) { // if itemReceiver is not a player (for example, mercenary) | |
pPlayer = GetMonsterOwner_0058f0d0(pItemReceiver); | |
} | |
if (pPlayer != nullptr && pPlayer->dwUnitType == 0) { | |
nearbyPlayerCount = Player_GetPartiedPlayerCountInSameDrlgLevel_WIP_005408e0(pGame, pPlayer); | |
if (nearbyPlayerCount < 1) { | |
nearbyPlayerCount = 1; | |
} else if (nearbyPlayerCount > 8) { | |
nearbyPlayerCount = 8; | |
} | |
} | |
} | |
int playerCount = Game_GetMaxOfPlayersNAndLivingPlayerCount_00535790(pGame); | |
// playerCount ~= nearbyPlayers + farPlayers | |
// adjPlayerCount ~= nearbyPlayers + (farPlayers/2) | |
int adjustedPlayerCount = nearbyPlayerCount + (playerCount - nearbyPlayerCount) / 2; | |
if (pItemDropper != nullptr && GetUnitTypeOr6IfNull_0044be50(pItemDropper) == 1) { | |
int monsterPlayerCount = GetStatValueFromUnit_WIP_00625480(pDroppedFromUnit, 100); | |
if (monsterPlayerCount < 1) { | |
monsterPlayerCount = 1; | |
} | |
if (monsterPlayerCount < adjustedPlayerCount) { | |
adjustedPlayerCount = monsterPlayerCount; | |
} | |
} | |
return adjustedPlayerCount; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment