From 5d19bfcb919268b428ec96d87c3769c27e6b0024 Mon Sep 17 00:00:00 2001 From: Antonio Martinez Date: Wed, 21 Aug 2024 02:28:31 -0700 Subject: [PATCH] Dynamic Roulette: mega cleanup, mini fixes --- src/k_hud.cpp | 51 ++-------- src/k_roulette.c | 244 +++++++++++++++++++++++++---------------------- src/k_roulette.h | 5 +- 3 files changed, 142 insertions(+), 158 deletions(-) diff --git a/src/k_hud.cpp b/src/k_hud.cpp index b903e8114..980f2e064 100644 --- a/src/k_hud.cpp +++ b/src/k_hud.cpp @@ -5684,61 +5684,28 @@ static void K_drawDistributionDebugger(void) itemroulette_t rouletteData = {0}; const fixed_t scale = (FRACUNIT >> 1); - const fixed_t space = 24 * scale; const fixed_t pad = 9 * scale; fixed_t x = -pad; - fixed_t y = -pad; - size_t i; if (R_GetViewNumber() != 0) // only for p1 { return; } - K_FillItemRouletteData(stplyr, &rouletteData, false); + K_FillItemRouletteData(stplyr, &rouletteData, false, true); - return; + if (cv_kartdebugdistribution.value <= 1) + return; - for (i = 0; i < rouletteData.itemListLen; i++) - { - const kartitems_t item = static_cast(rouletteData.itemList[i]); - UINT8 amount = 1; + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+10, V_SNAPTOTOP|V_SNAPTORIGHT, va("speed = %u", rouletteData.speed)); - if (y > (BASEVIDHEIGHT << FRACBITS) - space - pad) - { - x += space; - y = -pad; - } + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+22, V_SNAPTOTOP|V_SNAPTORIGHT, va("baseDist = %u", rouletteData.baseDist)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+30, V_SNAPTOTOP|V_SNAPTORIGHT, va("dist = %u", rouletteData.dist)); - V_DrawFixedPatch(x, y, scale, V_SNAPTOTOP, - K_GetSmallStaticCachedItemPatch(item), NULL); - - // Display amount for multi-items - amount = K_ItemResultToAmount(item); - if (amount > 1) - { - V_DrawStringScaled( - x + (18 * scale), - y + (23 * scale), - scale, FRACUNIT, FRACUNIT, - V_SNAPTOTOP, - NULL, HU_FONT, - va("x%d", amount) - ); - } - - y += space; - } - - V_DrawRightAlignedString(320 - (x >> FRACBITS), 10, V_SNAPTOTOP, va("speed = %u", rouletteData.speed)); - - V_DrawRightAlignedString(320 - (x >> FRACBITS), 22, V_SNAPTOTOP, va("baseDist = %u", rouletteData.baseDist)); - V_DrawRightAlignedString(320 - (x >> FRACBITS), 30, V_SNAPTOTOP, va("dist = %u", rouletteData.dist)); - - V_DrawRightAlignedString(320 - (x >> FRACBITS), 42, V_SNAPTOTOP, va("firstDist = %u", rouletteData.firstDist)); - V_DrawRightAlignedString(320 - (x >> FRACBITS), 50, V_SNAPTOTOP, va("secondDist = %u", rouletteData.secondDist)); - V_DrawRightAlignedString(320 - (x >> FRACBITS), 58, V_SNAPTOTOP, va("secondToFirst = %u", rouletteData.secondToFirst)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+42, V_SNAPTOTOP|V_SNAPTORIGHT, va("firstDist = %u", rouletteData.firstDist)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+50, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondDist = %u", rouletteData.secondDist)); + V_DrawRightAlignedThinString(320-(x >> FRACBITS), 100+58, V_SNAPTOTOP|V_SNAPTORIGHT, va("secondToFirst = %u", rouletteData.secondToFirst)); #ifndef ITEM_LIST_SIZE Z_Free(rouletteData.itemList); diff --git a/src/k_roulette.c b/src/k_roulette.c index 2f378ad75..1f1ff3183 100644 --- a/src/k_roulette.c +++ b/src/k_roulette.c @@ -1004,6 +1004,9 @@ static void K_CalculateRouletteSpeed(itemroulette_t *const roulette) roulette->tics = roulette->speed = ROULETTE_SPEED_FASTEST + FixedMul(ROULETTE_SPEED_SLOWEST - ROULETTE_SPEED_FASTEST, total); } +// Honestly, the "power item" class is kind of a vestigial concept, +// but we'll faithfully port it over since it's not hurting anything so far +// (and it's at least ostensibly a Rival balancing mechanism, wheee). static boolean K_IsItemPower(kartitems_t item) { switch (item) @@ -1060,7 +1063,8 @@ static boolean K_IsItemFirstPermitted(kartitems_t item) } } - +// Maybe for later... +#if 0 static boolean K_IsItemSpeed(kartitems_t item) { switch (item) @@ -1075,6 +1079,7 @@ static boolean K_IsItemSpeed(kartitems_t item) return false; } } +#endif static boolean K_IsItemUselessAlone(kartitems_t item) { @@ -1094,7 +1099,7 @@ static boolean K_IsItemUselessAlone(kartitems_t item) } } -// Which items are disallowed for THIS player? +// Which items are disallowed for this player's specific placement? static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player) { if (!(gametyperules & GTR_CIRCUIT)) @@ -1106,14 +1111,15 @@ static boolean K_ShouldPlayerAllowItem(kartitems_t item, const player_t *player) return K_IsItemFirstPermitted(item); else { + // A little inelegant: filter the most chaotic items from courses with early sets and tight layouts. if (K_IsItemPower(item) && (leveltime < ((15*TICRATE) + starttime))) return false; return !K_IsItemFirstOnly(item); } } -// Which items are disallowed for ALL players? -static boolean K_ShouldAllowItem(kartitems_t item, const itemroulette_t *roulette) +// Which items are disallowed because it's the wrong time for them? +static boolean K_TimingPermitsItem(kartitems_t item, const itemroulette_t *roulette) { if (!(gametyperules & GTR_CIRCUIT)) return true; @@ -1159,10 +1165,9 @@ static boolean K_ShouldAllowItem(kartitems_t item, const itemroulette_t *roulett case KITEM_SPB: { - cooldownOnStart = true; - notNearEnd = true; - // TODO forcing, just disable for now - return false; + // In Race, we reintroduce and reenable this item to counter breakaway frontruns. + // No need to roll it if that's not the case. + return false; break; } @@ -1191,26 +1196,16 @@ static boolean K_ShouldAllowItem(kartitems_t item, const itemroulette_t *roulett return false; if (notNearEnd && (roulette != NULL && roulette->baseDist < ENDDIST)) return false; - if (K_DenyShieldOdds(item)) - return false; - - if (roulette && roulette->autoroulette == true) - { - if (K_DenyAutoRouletteOdds(item)) - { - return false; - } - } return true; } /*-------------------------------------------------- - void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox) + void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun) See header file for description. --------------------------------------------------*/ -void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox) +void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun) { UINT32 spawnChance[NUMKARTRESULTS] = {0}; UINT32 totalSpawnChance = 0; @@ -1384,39 +1379,53 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet if (gametyperules & GTR_CIRCUIT) roulette->dist = FixedMul(roulette->preexpdist, max(player->exp, FRACUNIT/2)); + // =============================================================================== // Dynamic Roulette. Oh boy! - - // STAGE 1: Determine what items are permissible - // STAGE 2: Determine the item that's most appropriate for our distance from leader - // STAGE 3: Pick that item, then penalize it - // STAGE 4: Repeat 3 until the reel is full, then cram everything in + // Alright, here's the broad plan: + // 1: Determine what items are permissible + // 2: Determine the permitted item that's most appropriate for our distance from leader + // 3: Pick that item, then penalize it so it's less likely to be repicked + // 4: Repeat 3 until we've picked enough stuff + // 5: Skim any items that are much weaker than the reel's average out of the roulette + // 6: Cram it all in UINT32 targetpower = roulette->dist; // fill roulette with items around this value! - UINT32 powers[NUMKARTRESULTS]; // how strong is each item? + UINT32 powers[NUMKARTRESULTS]; // how strong is each item? think of this as a "target distance" for this item to spawn at UINT32 deltas[NUMKARTRESULTS]; // how different is that strength from target? UINT32 candidates[NUMKARTRESULTS]; // how many of this item should we try to insert? UINT32 dupetolerance[NUMKARTRESULTS]; // how willing are we to select this item after already selecting it? higher values = lower dupe penalty boolean permit[NUMKARTRESULTS]; // is this item allowed? boolean rival = (player->bot && (player->botvars.rival || cv_levelskull.value)); - boolean mothfilter = true; // strip unusually weak items from reel? + boolean filterweakitems = true; // strip unusually weak items from reel? UINT8 reelsize = 15; // How many items to attempt to add in prepass? UINT32 humanscaler = 250 + (roulette->playing * 15); // Scaler that converts "useodds" style distances in odds tables to raw distances. - // Cache which items are permissible + // == ARE THESE ITEMS ALLOWED? + // We have a fuckton of rules about when items are allowed to show up, + // like limiting trap items at the end of the race, limiting strong + // items at the start of the race... Dynamic stuff, not always trivial. + // We're about to do a bunch of work with items, so let's cache them all. for (i = 1; i < NUMKARTRESULTS; i++) { - permit[i] = K_ShouldAllowItem(i, roulette); - - // CONS_Printf("%s permit prepass %d\n", cv_items[i-1].name, permit[i]); - - if (permit[i]) - permit[i] = K_ShouldPlayerAllowItem(i, player); - - // CONS_Printf("%s permit postpass %d\n", cv_items[i-1].name, permit[i]); + if (!K_TimingPermitsItem(i, roulette)) + permit[i] = false; + else if (!K_ShouldPlayerAllowItem(i, player)) + permit[i] = false; + else if (K_GetItemCooldown(i)) + permit[i] = false; + else if (!K_ItemEnabled(i)) + permit[i] = false; + else if (K_DenyShieldOdds(i)) + permit[i] = false; + else if (roulette && roulette->autoroulette == true && K_DenyAutoRouletteOdds(i)) + permit[i] = false; + else + permit[i] = true; } - // temp - i have no fucking clue how pointers work i am so sorry + // == ODDS TIME + // Set up the right item odds for the gametype we're in. for (i = 1; i < NUMKARTRESULTS; i++) { // NOTE: Battle odds are underspecified, we don't invoke roulettes in this mode! @@ -1424,14 +1433,14 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet { powers[i] = humanscaler * K_DynamicItemOddsBattle[i-1][0]; dupetolerance[i] = K_DynamicItemOddsBattle[i-1][1]; - mothfilter = false; + filterweakitems = false; } else if (specialstageinfo.valid == true) { powers[i] = humanscaler * K_DynamicItemOddsSpecial[i-1][0]; dupetolerance[i] = K_DynamicItemOddsSpecial[i-1][1]; - reelsize = 8; - mothfilter = false; + reelsize = 8; // Smaller roulette in Special because there are much fewer standard items. + filterweakitems = false; } else { @@ -1440,29 +1449,34 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet } } - // null stuff that doesn't have odds + // == GTFO WEIRD ITEMS + // If something is set to distance 0 in its odds table, that means the item + // is completely ineligible for the gametype we're in, and should never be selected. for (i = 1; i < NUMKARTRESULTS; i++) { if (powers[i] == 0) { - // CONS_Printf("%s nulled\n", cv_items[i-1].name); permit[i] = false; } } - // Starting deltas + // == REEL CANDIDATE PREP + // Dynamic Roulette works by comparing an item's "ideal" distance to our current distance from 1st. + // It'll pick the most suitable item, do some math, then move on to the next most suitable item. + // Calculate starting deltas and clear out the "candidates" array that stores what we pick. for (i = 1; i < NUMKARTRESULTS; i++) { candidates[i] = 0; deltas[i] = min(targetpower - powers[i], powers[i] - targetpower); - // CONS_Printf("starting delta for %s is %d\n", cv_items[i-1].name, deltas[i]); } + // == LONELINESS DETECTION // A lot of items suck if no players are nearby to interact with them. // Should we bias towards items that get us back to the action? + // This will set the "lonely" flag to be used later. UINT32 lonelinessThreshold = 3*DISTVAR/2; - UINT32 toAttacker = lonelinessThreshold; - UINT32 toDefender = lonelinessThreshold; + UINT32 toAttacker = lonelinessThreshold; // Distance to the player trying to kill us. + UINT32 toDefender = lonelinessThreshold; // Distance to the player we are trying to kill. boolean lonely = false; if ((gametyperules & GTR_CIRCUIT) && specialstageinfo.valid == false) @@ -1483,32 +1497,39 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet if (toAttacker >= lonelinessThreshold && toDefender >= lonelinessThreshold && player->position > 1) lonely = true; - // let's start finding items to list - UINT8 added = 0; - UINT32 totalreelpower = 0; + // == INTRODUCE TRYHARD-EATING PREDATOR + // If the frontrunner's making a major breakaway, "break the rules" + // and insert the SPB into the roulette. This doesn't have to be + // incredibly forceful; there's a truly forced special case above. + fixed_t spb_odds = K_PercentSPBOdds(roulette, player->position); + + if ((gametyperules & GTR_CIRCUIT) + && specialstageinfo.valid == false + && (spb_odds > 0) & (spbplace == -1) + && (roulette->preexpdist >= powers[KITEM_SPB])) // SPECIAL CASE: Check raw distance instead of EXP-influenced target distance. + { + // When reenabling the SPB, we also adjust its delta to ensure that it has good odds of showing up. + // Players who are _seriously_ struggling are more likely to see Invinc or Rockets, since those items + // have a lower target distance, so we nudge the SPB towards them. + permit[KITEM_SPB] = true; + deltas[KITEM_SPB] = Easing_Linear(spb_odds, deltas[KITEM_SPB], 0); + } + + // == ITEM SELECTION + // All the prep work's done: let's pick out a sampler platter of items until we fill the reel. + UINT8 added = 0; // How many items added so far? + UINT32 totalreelpower = 0; // How much total item power in the reel? Used for an average later. for (i = 0; i < reelsize; i++) { UINT32 lowestdelta = INT32_MAX; size_t bestitem = 0; - // CONS_Printf("LOOP %d\n", i); - - // check each kartitem to see which is the best fit, - // based on what's closest to our target power - // (but ignore items that aren't allowed now) + // Each rep, get the legal item with the lowest delta... for (j = 1; j < NUMKARTRESULTS; j++) { - // CONS_Printf("precheck %s, perm %d CD %d\n", cv_items[j-1].name, permit[j], K_GetItemCooldown(j)); - if (!permit[j]) continue; - if (K_GetItemCooldown(j)) - continue; - if (!K_ItemEnabled(j)) - continue; - - // CONS_Printf("checking %s, delta %d\n", cv_items[j-1].name, deltas[j]); if (lowestdelta > deltas[j]) { @@ -1517,117 +1538,115 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet } } - // couldn't find an item? goodbye lol + // Couldn't find any eligible items at all? GTFO. + // (This should never trigger, but you never know with the item switch menu.) if (bestitem == 0) break; - // UINT32 deltapenalty = (DISTVAR*4)^(candidates[bestitem])/dupetolerance[bestitem]; + // Impose a penalty to this item's delta, to bias against selecting it again. + // This is naively slashed by an item's "duplicate tolerance": + // lower tolerance means that an item is less likely to be reselected (it's "rarer"). UINT32 deltapenalty = 4*DISTVAR*(1+candidates[bestitem])/dupetolerance[bestitem]; + // Power items get better odds in frantic, or if you're the rival. + // (For the rival, this is way more likely to matter at lower skills, where they're + // worse at selecting their item—but it always matters in frantic gameplay.) if (K_IsItemPower(bestitem) && rival) deltapenalty = 3 * deltapenalty / 4; if (K_IsItemPower(bestitem) && franticitems) deltapenalty = 3 * deltapenalty / 4; + + // Conversely, if we're lonely, try not to reselect an item that wouldn't be useful to us + // without any players to use it on. if (lonely && K_IsItemUselessAlone(bestitem)) deltapenalty *= 2; + // Draw complex odds debugger. This one breaks down all the calcs in order. if (cv_kartdebugdistribution.value > 1) { UINT16 BASE_X = 18; UINT16 BASE_Y = 5+12*i; INT32 FLAGS = V_SNAPTOTOP|V_SNAPTOLEFT; - // V_DrawThinString(BASE_X + 100, BASE_Y, FLAGS, va("%s", cv_items[lowestindex-1].name)); V_DrawThinString(BASE_X + 35, BASE_Y, FLAGS, va("P%d", powers[bestitem]/humanscaler)); V_DrawThinString(BASE_X + 65, BASE_Y, FLAGS, va("D%d", deltas[bestitem]/humanscaler)); V_DrawThinString(BASE_X + 20, BASE_Y, FLAGS, va("%d", dupetolerance[bestitem])); - //V_DrawThinString(BASE_X + 70, BASE_Y, FLAGS, va("+%d", deltapenalty)); V_DrawFixedPatch(BASE_X*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(bestitem), NULL); UINT8 amount = K_ItemResultToAmount(bestitem); if (amount > 1) - { V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount)); - } } - // otherwise, prep it to be added and give it a duplicaton penalty, - // so that a different item is more likely to be inserted next + // Add the selected item to our list of candidates and update its working delta. candidates[bestitem]++; deltas[bestitem] += deltapenalty; + // Then update our ongoing average of the reel's power. totalreelpower += powers[bestitem]; added++; - - // CONS_Printf("added %s with candidates %d\n", cv_items[lowestindex-1].name, candidates[lowestindex]); } - // Introduce SPB to race if there's a major frontrun breakway - - fixed_t spb_odds = K_PercentSPBOdds(roulette, player->position); - - if ((gametyperules & GTR_CIRCUIT) - && specialstageinfo.valid == false - && (spb_odds > 0) & (spbplace == -1)) + if (added == 0) { - permit[KITEM_SPB] = true; - deltas[KITEM_SPB] = Easing_Linear(spb_odds, 3000, 0); + // Guess we're making circles now. + // Just do something that doesn't crash. + K_AddItemToReel(player, roulette, singleItem); + return; } - UINT8 debugcount = 0; - UINT32 meanreelpower = totalreelpower/max(added, 1); + UINT8 debugcount = 0; // For the "simple" odds debugger. + UINT32 meanreelpower = totalreelpower/max(added, 1); // Average power for the "moth filter". - // set up the list indices used to random-shuffle the ro ulette + // == PREP FOR ADDING TO THE ROULETTE REEL + // Sal's prior work for this is rock-solid. + // This fills the spawnChance array with a rolling count of items, + // so that we can loop upward through it until we hit our random index. for (i = 1; i < NUMKARTRESULTS; i++) { - // filter items vastly too weak for this reel - boolean reject = (powers[i] + DISTVAR < meanreelpower); - - if (!mothfilter) - reject = false; + // If an item is far too week for this reel, reject it. + // This can happen in regions of the odds with a lot of items that + // don't really like to be duplicated. Favor the player; high-rolling + // feels exciting, low-rolling feels punishing! + boolean reject = (filterweakitems) && (powers[i] + DISTVAR < meanreelpower); + // Before we actually apply that rejection, draw the simple odds debugger. + // This one is just to watch the distribution for vibes as you drive around. if (cv_kartdebugdistribution.value && candidates[i]) { UINT16 BASE_X = 280; UINT16 BASE_Y = 5+12*debugcount; INT32 FLAGS = V_SNAPTOTOP|V_SNAPTORIGHT; - V_DrawThinString(BASE_X - 12, 5, FLAGS, va("%d", targetpower/humanscaler)); - V_DrawThinString(BASE_X - 12, 5+12, FLAGS, va("%d", toAttacker)); V_DrawThinString(BASE_X - 12, 5+24, FLAGS, va("%d", toDefender)); if (lonely) V_DrawThinString(BASE_X - 12, 5+36, FLAGS, va(":(")); - for(UINT8 k = 0; k < candidates[i]; k++) V_DrawFixedPatch((BASE_X + 3*k)*FRACUNIT, (BASE_Y-7)*FRACUNIT, (FRACUNIT >> 1), FLAGS, K_GetSmallStaticCachedItemPatch(i), NULL); - UINT8 amount = K_ItemResultToAmount(i); if (amount > 1) - { V_DrawThinString(BASE_X, BASE_Y, FLAGS, va("x%d", amount)); - } - if (reject) V_DrawThinString(BASE_X, BASE_Y, FLAGS|V_60TRANS, va("WEAK")); debugcount++; } - if (!reject) - { - spawnChance[i] = ( - totalSpawnChance += candidates[i] - ); - } + // Okay, apply the rejection now. + if (reject) + candidates[i] = 0; + + // Bump totalSpawnChance, write that rolling counter, and move on. + spawnChance[i] = ( + totalSpawnChance += candidates[i] + ); } - if (totalSpawnChance == 0) - { - // why did this fucking happen LOL - // don't crash - K_AddItemToReel(player, roulette, singleItem); - return; - } + if (dryrun) // We're being called from the debugger on a view conditional! + return; // This is net unsafe if we do things with side effects. GTFO! - // and insert all of our candidates into the roulette in a random order + // == FINALLY ADD THIS SHIT TO THE REEL + // Super simple: generate a random index, + // count up until we hit that index, + // insert that item and decrement everything after. while (totalSpawnChance > 0) { rngRoll = P_RandomKey(PR_ITEM_ROULETTE, totalSpawnChance); @@ -1636,13 +1655,10 @@ void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulet continue; } - // CONS_Printf("adding %s, tsp %d\n", cv_items[i-1].name, totalSpawnChance); - K_AddItemToReel(player, roulette, i); for (; i < NUMKARTRESULTS; i++) { - // Be sure to fix the remaining items' odds too. if (spawnChance[i] > 0) { spawnChance[i]--; @@ -1663,7 +1679,7 @@ void K_StartItemRoulette(player_t *const player, boolean ringbox) itemroulette_t *const roulette = &player->itemRoulette; size_t i; - K_FillItemRouletteData(player, roulette, ringbox); + K_FillItemRouletteData(player, roulette, ringbox, false); if (roulette->autoroulette) roulette->index = P_RandomRange(PR_AUTOROULETTE, 0, roulette->itemListLen - 1); diff --git a/src/k_roulette.h b/src/k_roulette.h index f05354e93..03a4adb22 100644 --- a/src/k_roulette.h +++ b/src/k_roulette.h @@ -98,7 +98,7 @@ INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item); /*-------------------------------------------------- - void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox); + void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun); Fills out the item roulette struct when it is initially created. This function needs to be @@ -110,12 +110,13 @@ INT32 K_KartGetBattleOdds(const player_t *player, UINT8 pos, kartitems_t item); Can be NULL for generic use. roulette - The roulette data struct to fill out. ringbox - Is this roulette fill triggered by a just-respawned Ring Box? + dryrun - Are we calling this from the distribution debugger? Don't call RNG or write roulette data! Return:- N/A --------------------------------------------------*/ -void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox); +void K_FillItemRouletteData(const player_t *player, itemroulette_t *const roulette, boolean ringbox, boolean dryrun); /*--------------------------------------------------