fix(Core/Groups): preserve BoP trade window when mailing roll-won items (#26023)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew
2026-05-29 21:12:45 -03:00
committed by GitHub
parent 46f0d4033a
commit 4f1745483d
2 changed files with 62 additions and 4 deletions
@@ -6209,6 +6209,32 @@ Item* Player::_LoadMailedItem(ObjectGuid const& playerGuid, Player* player, uint
return nullptr;
}
// Rehydrate looters for BoP-tradeable mail items; only the LFG mail path writes this flag, gated on the same config.
if (item->IsBOPTradable() && sWorld->getBoolConfig(CONFIG_SET_BOP_ITEM_TRADEABLE))
{
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ITEM_BOP_TRADE);
stmt->SetData(0, item->GetGUID().GetCounter());
if (PreparedQueryResult result = CharacterDatabase.Query(stmt))
{
AllowedLooterSet looters;
for (std::string_view guidStr : Acore::Tokenize((*result)[0].Get<std::string_view>(), ' ', false))
{
if (Optional<ObjectGuid::LowType> guid = Acore::StringTo<ObjectGuid::LowType>(guidStr))
looters.insert(ObjectGuid::Create<HighGuid::Player>(*guid));
else
LOG_WARN("entities.player.loading", "Player::_LoadMailedItem: invalid item_soulbound_trade_data GUID '{}' for item {}. Skipped.", guidStr, item->GetGUID().ToString());
}
if (looters.size() > 1 && proto->GetMaxStackSize() == 1 && item->IsSoulBound())
item->SetSoulboundTradeable(looters);
else
item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE);
}
else
item->RemoveFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE);
}
if (mail)
{
mail->AddItem(itemGuid, itemEntry);
+36 -4
View File
@@ -63,6 +63,40 @@ Loot* Roll::getLoot()
return getTarget();
}
static void SendRollWonItemViaMail(Player* player, LootItem const* lootItem, uint32 itemId)
{
Item* mailItem = Item::CreateItem(itemId, lootItem->count, player, false, lootItem->randomPropertyId);
if (!mailItem)
return;
AllowedLooterSet looters = lootItem->GetAllowedLooters();
ItemTemplate const* proto = mailItem->GetTemplate();
// Preserve the 2-hour group trade window the item would have had if stored directly.
if (looters.size() > 1 && proto->GetMaxStackSize() == 1 &&
(proto->Bonding == BIND_WHEN_PICKED_UP || proto->Bonding == BIND_QUEST_ITEM) &&
sWorld->getBoolConfig(CONFIG_SET_BOP_ITEM_TRADEABLE))
{
mailItem->SetBinding(true);
mailItem->SetSoulboundTradeable(looters);
mailItem->SetUInt32Value(ITEM_FIELD_CREATE_PLAYED_TIME, player->GetTotalPlayedTime());
std::string lootersStr;
for (ObjectGuid const& guid : looters)
{
if (!lootersStr.empty())
lootersStr += ' ';
lootersStr += std::to_string(guid.GetCounter());
}
CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_ITEM_BOP_TRADE);
stmt->SetData(0, mailItem->GetGUID().GetCounter());
stmt->SetData(1, lootersStr);
CharacterDatabase.Execute(stmt);
}
player->SendItemRetrievalMail(mailItem);
}
Group::Group() : m_leaderName(""), m_groupType(GROUPTYPE_NORMAL),
m_dungeonDifficulty(DUNGEON_DIFFICULTY_NORMAL), m_raidDifficulty(RAID_DIFFICULTY_10MAN_NORMAL),
m_bfGroup(nullptr), m_bgGroup(nullptr), m_lootMethod(FREE_FOR_ALL), m_lootThreshold(ITEM_QUALITY_UNCOMMON),
@@ -1497,8 +1531,7 @@ void Group::CountTheRoll(Rolls::iterator rollI, Map* allowedMap)
roll->getLoot()->NotifyItemRemoved(roll->itemSlot);
roll->getLoot()->unlootedCount--;
player->SendEquipError(msg, nullptr, nullptr, roll->itemid);
if (Item* mailItem = Item::CreateItem(roll->itemid, item->count, player, false, item->randomPropertyId))
player->SendItemRetrievalMail(mailItem);
SendRollWonItemViaMail(player, item, roll->itemid);
}
else
{
@@ -1580,8 +1613,7 @@ void Group::CountTheRoll(Rolls::iterator rollI, Map* allowedMap)
roll->getLoot()->NotifyItemRemoved(roll->itemSlot);
roll->getLoot()->unlootedCount--;
player->SendEquipError(msg, nullptr, nullptr, roll->itemid);
if (Item* mailItem = Item::CreateItem(roll->itemid, item->count, player, false, item->randomPropertyId))
player->SendItemRetrievalMail(mailItem);
SendRollWonItemViaMail(player, item, roll->itemid);
}
else
{