• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

Canary Wrong itemtype inside reward container?

endziu2222

Well-Known Member
Joined
Nov 2, 2010
Messages
234
Solutions
1
Reaction score
63
So I have a major problem: sometimes players receive additional broken items like floors or walls along with boss loot, which can also cause crashes. Does anyone know what could possibly trigger this issue or how I can investigate it?

Canary.

I checked loot of each boss for any of the objects found inside the containers and none I can find in monster loot.



View attachment 93920
View attachment 93921

LUA:
-- Unused function
function PushValues(buffer, sep, ...)
    local argv = { ... }
    local argc = #argv
    for k, v in ipairs(argv) do
        table.insert(buffer, v)
        if k < argc and sep then
            table.insert(buffer, sep)
        end
    end
end

function PushSeparated(buffer, sep, ...)
    local argv = { ... }
    local argc = #argv
    for k, v in ipairs(argv) do
        table.insert(buffer, v)
        if k < argc and sep then
            table.insert(buffer, sep)
        end
    end
end

function InsertItems(buffer, info, parent, items)
    local start = info.running
    for _, item in ipairs(items) do
        if item ~= nil then
            if item:getId() == ITEM_REWARD_CONTAINER then
                table.insert(buffer, "(")
                PushSeparated(buffer, ",", info.playerGuid, 0, parent, item:getId(), item:getSubType(), db.escapeString(item:serializeAttributes()))
                table.insert(buffer, "),")
            else
                info.running = info.running + 1
                table.insert(buffer, "(")
                PushSeparated(buffer, ",", info.playerGuid, parent, info.running, item:getId(), item:getSubType(), db.escapeString(item:serializeAttributes()))
                table.insert(buffer, "),")
            end

            if item:isContainer() then
                local size = item:getSize()
                if size > 0 then
                    local subItems = {}
                    for i = 1, size do
                        table.insert(subItems, item:getItem(i - 1))
                    end

                    InsertItems(buffer, info, info.running, subItems)
                end
            end
        end
    end
    return info.running - start
end

function InsertRewardItems(playerGuid, timestamp, itemList)
    local maxSidQueryResult = db.query('select max(`sid`) as max_sid from `player_rewards` where player_id = ' .. playerGuid .. ';')
    local bagSid = (Result.getNumber(maxSidQueryResult, 'max_sid') or 0) + 1;
    local nextSid = bagSid + 1;
    local buffer = { 'INSERT INTO `player_rewards` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES' }
    local info = {
        playerGuid = playerGuid,
        running = nextSid
    }
    local bag = Game.createItem(ITEM_REWARD_CONTAINER)
    bag:setAttribute(ITEM_ATTRIBUTE_DATE, timestamp)
    if itemList then
        for _, p in ipairs(itemList) do
            bag:addItem(p[1], p[2])
        end
    end
    local total = InsertItems(buffer, info, bagSid, { bag })

    if total ~= 0 then
        local insertItemsQuery = table.concat(buffer):sub(1, -2) .. ";";
        db.query(insertItemsQuery)
    end
end

function GetPlayerStats(bossId, playerGuid, autocreate)
    local ret = GlobalBosses[bossId][playerGuid]
    if not ret and autocreate then
        ret = {
            bossId = bossId,
            damageIn = 0, -- damage taken from the boss
            healing = 0, -- healing (other players) done
        }
        GlobalBosses[bossId][playerGuid] = ret
        return ret
    end
    return ret
end

function ResetAndSetTargetList(creature)
    if not creature then
        return
    end

    local bossId = creature:getId()
    local info = GlobalBosses[bossId]
    -- Reset all players' status
    for _, player in pairs(info) do
        player.active = false
    end
    -- Set all players in boss' target list as active in the fight
    local targets = creature:getTargetList()
    for _, target in ipairs(targets) do
        if target:isPlayer() then
            local stats = GetPlayerStats(bossId, target:getGuid(), true)
            stats.playerId = target:getId() -- Update player id
            stats.active = true
        end
    end
end

boss_death
Code:
local bossDeath = CreatureEvent("BossDeath")

function bossDeath.onDeath(creature, corpse, killer, mostDamageKiller, lastHitUnjustified, mostDamageUnjustified)
    -- Deny summons and players
    if not creature or creature:isPlayer() or creature:getMaster() then
        return true
    end

    -- Boss function
    local monsterType = creature:getType()
    -- Make sure it is a boss
    if monsterType and monsterType:isRewardBoss() then
        if not corpse:isContainer() then
            logger.warn("[bossDeath.onDeath] Corpse (id: {}) for reward boss {} is not a container.", corpse:getId(), creature:getName())
        end
        corpse:registerReward()
        local bossId = creature:getId()
        local rewardId = corpse:getAttribute(ITEM_ATTRIBUTE_DATE)

        ResetAndSetTargetList(creature)

        -- Avoid dividing by zero
        local totalDamageOut, totalDamageIn, totalHealing = 0.1, 0.1, 0.1

        local scores = {}
        local info = GlobalBosses[bossId]
        local damageMap = creature:getDamageMap()

        for guid, stats in pairs(info) do
            local player = Player(stats.playerId)
            local part = damageMap[stats.playerId]
            local damageOut, damageIn, healing = (stats.damageOut or 0) + (part and part.total or 0), stats.damageIn or 0, stats.healing or 0

            totalDamageOut = totalDamageOut + damageOut
            totalDamageIn = totalDamageIn + damageIn
            totalHealing = totalHealing + healing

            table.insert(scores, {
                player = player,
                guid = guid,
                damageOut = damageOut,
                damageIn = damageIn,
                healing = healing,
            })
        end

        local participants = 0
        for _, con in ipairs(scores) do
            local score = (con.damageOut / totalDamageOut) + (con.damageIn / totalDamageIn) + (con.healing / totalHealing)
            -- Normalize to 0-1
            con.score = score / 3
            if score ~= 0 then
                participants = participants + 1
            end
        end
        table.sort(scores, function(a, b) return a.score > b.score end)

        local expectedScore = 1 / participants

        for _, con in ipairs(scores) do
            -- Ignoring stamina for now because I heard you get receive rewards even when it's depleted
            if con.score ~= 0 then
                local reward, stamina, player
                if con.player then
                    player = con.player;
                else
                    player = Game.getOfflinePlayer(con.guid)
                end
                reward = player:getReward(rewardId, true)
                stamina = player:getStamina()

                local lootFactor = 1
                -- Tone down the loot a notch if there are many participants
                lootFactor = lootFactor / participants ^ (1 / 3)
                -- Increase the loot multiplicatively by how many times the player surpassed the expected score
                lootFactor = lootFactor * (1 + lootFactor) ^ (con.score / expectedScore)
                -- Bosstiary Loot Bonus
                local rolls = 1
                local isBoostedBoss = creature:getName():lower() == (Game.getBoostedBoss()):lower()
                local bossRaceIds = { player:getSlotBossId(1), player:getSlotBossId(2) }
                local isBoss = table.contains(bossRaceIds, monsterType:bossRaceId()) or isBoostedBoss
                if isBoss and monsterType:bossRaceId() ~= 0 then
                    if monsterType:bossRaceId() == player:getSlotBossId(1) then
                        rolls = rolls + player:getBossBonus(1) / 100
                    elseif monsterType:bossRaceId() == player:getSlotBossId(2) then
                        rolls = rolls + player:getBossBonus(2) / 100
                    else
                        rolls = rolls + configManager.getNumber(configKeys.BOOSTED_BOSS_LOOT_BONUS) / 100
                    end
                end
                -- decide if we get an extra roll
                if math.random(0, 100) < (rolls % 1) * 100 then
                    rolls = math.ceil(rolls)
                else
                    rolls = math.floor(rolls)
                end

                local playerLoot = monsterType:getBossReward(lootFactor, _ == 1, false, {})
                for _ = 2, rolls do
                    playerLoot = monsterType:getBossReward(lootFactor, false, true, playerLoot)
                end
                for id, lootInfo in pairs(playerLoot) do
                    reward:addItem(id, lootInfo.count)
                end

                if con.player then
                    local lootMessage = ("The following items dropped by %s are available in your reward chest: %s"):format(creature:getName(), reward:getContentDescription())
                    if rolls > 1 then
                        lootMessage = lootMessage .. " (boss bonus)"
                    end
                    if stamina > 840 then
                        reward:getContentDescription(lootMessage)
                    end
                    player:sendTextMessage(MESSAGE_LOOT, lootMessage)
                else
                    player:save()
                end
            end
        end
        GlobalBosses[bossId] = nil
    end
    return true
end

bossDeath:register()

C++:
bool IOLoginDataSave::saveRewardItems(Player* player) {
    if (!player) {
        g_logger().warn("[IOLoginData::savePlayer] - Player nullptr: {}", __FUNCTION__);
        return false;
    }

    std::ostringstream query;
    query << "DELETE FROM `player_rewards` WHERE `player_id` = " << player->getGUID();

    if (!Database::getInstance().executeQuery(query.str())) {
        return false;
    }

    std::vector<uint64_t> rewardList;
    player->getRewardList(rewardList);

    ItemRewardList rewardListItems;
    if (!rewardList.empty()) {
        for (const auto &rewardId : rewardList) {
            auto reward = player->getReward(rewardId, false);
            if (!reward->empty() && (getTimeMsNow() - rewardId <= 1000 * 60 * 60 * 24 * 7)) {
                rewardListItems.emplace_back(0, reward);
            }
        }

        DBInsert rewardQuery("INSERT INTO `player_rewards` (`player_id`, `pid`, `sid`, `itemtype`, `count`, `attributes`) VALUES ");
        PropWriteStream propWriteStream;
        if (!saveItems(player, rewardListItems, rewardQuery, propWriteStream)) {
            return false;
        }
    }
    return true;
}

C++:
void IOLoginDataLoad::loadRewardItems(Player* player) {
    if (!player) {
        g_logger().warn("[IOLoginData::loadPlayer] - Player nullptr: {}", __FUNCTION__);
        return;
    }

    ItemsMap rewardItems;
    std::ostringstream query;
    query.str(std::string());
    query << "SELECT `pid`, `sid`, `itemtype`, `count`, `attributes` FROM `player_rewards` WHERE `player_id` = "
          << player->getGUID() << " ORDER BY `pid`, `sid` ASC";
    if (auto result = Database::getInstance().storeQuery(query.str())) {
        loadItems(rewardItems, result, *player);
        bindRewardBag(player, rewardItems);
        insertItemsIntoRewardBag(rewardItems);
    }
}

C++:
void Monster::dropLoot(Container* corpse, Creature*) {
    if (corpse && lootDrop) {
        // Only fiendish drops sliver
        if (ForgeClassifications_t classification = getMonsterForgeClassification();
            // Condition
            classification == ForgeClassifications_t::FORGE_FIENDISH_MONSTER) {
            auto minSlivers = g_configManager().getNumber(FORGE_MIN_SLIVERS);
            auto maxSlivers = g_configManager().getNumber(FORGE_MAX_SLIVERS);

            auto sliverCount = static_cast<uint16_t>(uniform_random(minSlivers, maxSlivers));

            Item* sliver = Item::CreateItem(ITEM_FORGE_SLIVER, sliverCount);
            if (g_game().internalAddItem(corpse, sliver) != RETURNVALUE_NOERROR) {
                corpse->internalAddThing(sliver);
            }
        }
        if (!this->isRewardBoss() && g_configManager().getNumber(RATE_LOOT) > 0) {
            g_callbacks().executeCallback(EventCallback_t::monsterOnDropLoot, &EventCallback::monsterOnDropLoot, this, corpse);
            g_callbacks().executeCallback(EventCallback_t::monsterPostDropLoot, &EventCallback::monsterPostDropLoot, this, corpse);
        }
    }
}
 
Last edited:
Back
Top