Files
DarkflameServer/dGame/dUtilities/Loot.cpp
Aaron Kimbrell b2a1e9e0b8 refactor: remaining game messages as structs, switch removed
Every game message still written or read by hand is now a NetGameMsg with
Serialize and Deserialize, in per-domain files:
- MovementMessages: teleport, platforms (resync and its request), orient to
  angle, node rotation lock, gravity scale, jetpack mode, control scheme,
  respawn checkpoint, rails (set/start, ready, cancel, arrived), mount
  inventory ID, dismount complete, possession ack, ghost reference override
  and position, camera cycling (eCameraTargetCyclingMode moves here).
- ZoneMessages: player loaded (the old PLAYER_LOADED case), ready for
  updates, player ready, restore to post load stats, server done loading,
  invalid zone transfer list, zone summary display/dismissed, level
  processing complete, object world state, and the localized announcement
  WorldMigration wrote by hand.
- PlayerMessages: chat mode, GM level, LEGO score, currency, reputation,
  GM invis, pickup currency, zone and player statistics, chat commands, bug
  reports, verify ack.
- ObjectMessages: fire event client/server side, notify client
  (zone) object, notify object, script network vars, failed preconditions,
  terminate interaction, set name, request use, request server object info.
- QuickBuildMessages: notify state, enable, cancel.
- ActivityMessages gains match response/update/request, leaderboard request
  and data, shooting gallery score/rotation/fire, activity state change.
- MissionMessages gains MissionDialogueCancelled (a no-op, as before).
The wire structs left in GameMessages.h move to their domains (tooltip and
emote to Effects, loot and item use to Inventory, model build to Building,
behavior sound to Property, skill sets to Skill) and gain the missing
direction. The dismount logic moves to PossessorComponent::OnDismountComplete.

Every inbound message is registered in the GameMessageHandler map; the
switch is gone, and GameMessages.cpp only holds the GameMsg/NetGameMsg base
code. Call sites build the structs (NotifyClientObject, TerminateInteraction,
Teleport, PlatformResync, FireEventClientSide, NotifyObject and
NotifyClientZoneObject get convenience constructors like PlayFXEffect).
Dead senders are dropped: SendSetShootingGalleryParams (no callers, field
order was a guess), SendTeamPickupItem (the struct already existed),
SendRequestActivitySummaryLeaderboardData (the struct covers it).

Verified with RemainingMessagesTests: every old Send* function is frozen
verbatim in Legacy/RemainingMessagesLegacy.h and compared byte for byte
(same bits, destination and broadcast flag) over grids of inputs; every old
Handle* read sequence is frozen as a Read* oracle and compared with the
struct's Deserialize; round trips, truncation and a golden packet.
PlayerLoaded (0x00dc36f0), SetGMLevel (0x00dd6230), MissionDialogueCancelled
(0x00d9cc10) and LocalizedAnnouncementServerToSingleClient (0x00f23c50)
were checked against the client. Behaviour notes: an inbound message that
fails to deserialize is dropped, so ParseChatMessage over MAX_MESSAGE_LENGTH
is dropped instead of truncated, and PLAYER_LOADED / READY_FOR_UPDATES /
MISSION_DIALOGUE_CANCELLED now read their (unused) client fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:56 -05:00

620 lines
24 KiB
C++

#include "Loot.h"
#include "EconomyLedger.h"
#include "LiveEvents.h"
#include <algorithm>
#include <unordered_set>
#include "CDComponentsRegistryTable.h"
#include "CDItemComponentTable.h"
#include "CDLootMatrixTable.h"
#include "CDLootTableTable.h"
#include "CDRarityTableTable.h"
#include "CDActivityRewardsTable.h"
#include "CDCurrencyTableTable.h"
#include "Character.h"
#include "Entity.h"
#include "GameMessages.h"
#include "GeneralUtils.h"
#include "InventoryComponent.h"
#include "MissionComponent.h"
#include "eMissionState.h"
#include "eReplicaComponentType.h"
#include "TeamManager.h"
#include "CDObjectsTable.h"
#include "ObjectIDManager.h"
#include "CDActivitiesTable.h"
#include "ScriptedActivityComponent.h"
#include "InventoryMessages.h"
namespace {
std::unordered_set<uint32_t> CachedMatrices;
constexpr float g_MAX_DROP_RADIUS = 700.0f;
}
struct LootDropInfo {
CDLootTable table{};
uint32_t count{ 0 };
};
// chanceMultiplier: a live event's loot bonus (1: none)
std::map<LOT, LootDropInfo> RollLootMatrix(uint32_t matrixIndex, float chanceMultiplier = 1.0f) {
CDComponentsRegistryTable* componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
CDLootMatrixTable* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
CDLootTableTable* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
CDRarityTableTable* rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
std::map<LOT, LootDropInfo> drops;
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
for (const auto& entry : matrix) {
if (GeneralUtils::GenerateRandomNumber<float>(0, 1) < LiveOpsRules::ScaleChance(entry.percent, chanceMultiplier)) {
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
uint32_t dropCount = GeneralUtils::GenerateRandomNumber<uint32_t>(entry.minToDrop, entry.maxToDrop);
for (uint32_t i = 0; i < dropCount; ++i) {
uint32_t maxRarity = 1;
float rarityRoll = GeneralUtils::GenerateRandomNumber<float>(0, 1);
for (const auto& rarity : rarityTable) {
if (rarity.randmax >= rarityRoll) {
maxRarity = rarity.rarity;
} else {
break;
}
}
bool rarityFound = false;
std::vector<CDLootTable> possibleDrops;
for (const auto& loot : lootTable) {
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
if (rarity == maxRarity) {
possibleDrops.push_back(loot);
rarityFound = true;
} else if (rarity < maxRarity && !rarityFound) {
possibleDrops.push_back(loot);
maxRarity = rarity;
}
}
if (!possibleDrops.empty()) {
const auto& drop = possibleDrops[GeneralUtils::GenerateRandomNumber<uint32_t>(0, possibleDrops.size() - 1)];
auto& info = drops[drop.itemid];
if (info.count == 0) info.table = drop;
info.count++;
}
}
}
}
return drops;
}
// Generates a 'random' final position for the loot drop based on its input spawn position.
void CalcFinalDropPos(GameMessages::DropClientLoot& lootMsg) {
if (lootMsg.spawnPos != NiPoint3Constant::ZERO) {
lootMsg.bUsePosition = true;
//Calculate where the loot will go:
uint16_t degree = GeneralUtils::GenerateRandomNumber<uint16_t>(0, 360);
double rad = degree * 3.14 / 180;
double sin_v = sin(rad) * 4.2;
double cos_v = cos(rad) * 4.2;
const auto [x, y, z] = lootMsg.spawnPos;
lootMsg.finalPosition = NiPoint3(static_cast<float>(x + sin_v), y, static_cast<float>(z + cos_v));
}
}
// Visually drop the loot to all team members, though only the lootMsg.ownerID can pick it up
void DistrbuteMsgToTeam(const GameMessages::DropClientLoot& lootMsg, const Team& team) {
for (const auto memberClient : team.members) {
const auto* const memberEntity = Game::entityManager->GetEntity(memberClient);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
}
// The following 8 functions are all ever so slightly different such that combining them
// would make the logic harder to follow. Please read the comments!
// Given a faction token proxy LOT to drop, drop 1 token for each player on a team, or the provided player.
// token drops are always given to every player on the team.
void DropFactionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
const auto playerID = player.GetObjectID();
GameMessages::GetFactionTokenType factionTokenType{};
factionTokenType.target = playerID;
// If we're not in a faction, this message will return false
if (factionTokenType.Send()) {
lootMsg.item = factionTokenType.tokenType;
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop on the player
GameMessages::DeliverLocally(lootMsg);
// Visually drop it for the player
lootMsg.Send(player.GetSystemAddress());
}
}
// Drops 1 token for each player on a team
// token drops are always given to every player on the team.
void DropFactionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::IsDead isDeadMsg{};
// Skip dead players
if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue;
GameMessages::GetFactionTokenType factionTokenType{};
factionTokenType.target = member;
// If we're not in a faction, this message will return false
if (factionTokenType.Send()) {
lootMsg.item = factionTokenType.tokenType;
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop on this team member
GameMessages::DeliverLocally(lootMsg);
// Show the rewards on all connected members of the team. Only the loot owner will be able to pick the tokens up.
DistrbuteMsgToTeam(lootMsg, team);
}
}
}
// Drop the power up with no owner
// Power ups can be picked up by anyone on a team, however unlike actual loot items,
// if multiple clients say they picked one up, we let them pick it up.
void DropPowerupLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
const auto playerID = player.GetObjectID();
CalcFinalDropPos(lootMsg);
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.ownerID = playerID;
lootMsg.target = playerID;
// Register the drop on the player
GameMessages::DeliverLocally(lootMsg);
// Visually drop it for the player
lootMsg.Send(player.GetSystemAddress());
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::POWERUP_DROPS, lootMsg.item, lootMsg.spawnPos, 1, &player);
}
// Drop the power up with no owner
// Power ups can be picked up by anyone on a team, however unlike actual loot items,
// if multiple clients say they picked one up, we let them pick it up.
void DropPowerupLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.ownerID = LWOOBJID_EMPTY; // By setting ownerID to empty, any client that gets this DropClientLoot message can pick up the item.
CalcFinalDropPos(lootMsg);
// One drop, however many teammates see it
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::POWERUP_DROPS, lootMsg.item, lootMsg.spawnPos, 1);
// We want to drop the powerups as the same ID and the same position to all members of the team
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::IsDead isDeadMsg{};
// Skip dead players
if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue;
lootMsg.target = member;
// By sending this message with the same ID to all players on the team, all players on the team are allowed to pick it up.
GameMessages::DeliverLocally(lootMsg);
// No need to send to all members in a loop since that will happen by using the outer loop above and also since there is no owner
// sending to all will do nothing.
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
}
// Drops a mission item for a player
// If the player does not need this item, it will not be dropped.
void DropMissionLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
GameMessages::MissionNeedsLot needMsg{};
needMsg.item = lootMsg.item;
const auto playerID = player.GetObjectID();
needMsg.target = playerID;
// Will return false if the item is not required
if (needMsg.Send()) {
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop with the player
GameMessages::DeliverLocally(lootMsg);
// Visually drop the loot to be picked up
lootMsg.Send(player.GetSystemAddress());
}
}
// Check if the item needs to be dropped for anyone on the team
// Only players who need the item will have it dropped
void DropMissionLoot(const Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
GameMessages::MissionNeedsLot needMsg{};
needMsg.item = lootMsg.item;
for (const auto member : team.members) {
GameMessages::GetPosition memberPosMsg{};
memberPosMsg.target = member;
memberPosMsg.Send();
if (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS) continue;
GameMessages::IsDead isDeadMsg{};
// Skip dead players
if (noTeamLootOnDeath && isDeadMsg.Send(member) && isDeadMsg.bDead) continue;
needMsg.target = member;
// Will return false if the item is not required
if (needMsg.Send()) {
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
// Register the drop with the player
GameMessages::DeliverLocally(lootMsg);
DistrbuteMsgToTeam(lootMsg, team);
}
}
}
// Drop a regular piece of loot.
// Most items will go through this.
// A player will always get a drop that goes through this function
void DropRegularLoot(Entity& player, GameMessages::DropClientLoot& lootMsg) {
const auto playerID = player.GetObjectID();
CalcFinalDropPos(lootMsg);
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
lootMsg.target = playerID;
lootMsg.ownerID = playerID;
// Register the drop with the player
GameMessages::DeliverLocally(lootMsg);
// Visually drop the loot to be picked up
lootMsg.Send(player.GetSystemAddress());
}
// Drop a regular piece of loot.
// Most items will go through this.
// Finds the next loot owner on the team the is in range of the kill and gives them this reward.
void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg, const bool noTeamLootOnDeath = false) {
auto earningPlayer = LWOOBJID_EMPTY;
lootMsg.lootID = ObjectIDManager::GenerateObjectID();
CalcFinalDropPos(lootMsg);
GameMessages::GetPosition memberPosMsg{};
GameMessages::IsDead isDeadMsg{};
// Find the next loot owner. Eventually this will run into the `player` passed into this function, since those will
// have the same ID, this loop will only ever run at most 4 times.
do {
earningPlayer = team.GetNextLootOwner();
memberPosMsg.target = earningPlayer;
memberPosMsg.Send();
if (noTeamLootOnDeath) {
isDeadMsg.target = earningPlayer;
// Skip dead players
isDeadMsg.Send();
}
} while (isDeadMsg.bDead || (NiPoint3::Distance(memberPosMsg.pos, lootMsg.spawnPos) > g_MAX_DROP_RADIUS));
if (team.lootOption == 0 /* Shared loot */) {
lootMsg.target = earningPlayer;
lootMsg.ownerID = earningPlayer;
GameMessages::DeliverLocally(lootMsg);
} else /* Free for all loot */ {
lootMsg.ownerID = LWOOBJID_EMPTY;
// By sending the loot with NO owner and to ALL members of the team,
// its a first come, first serve with who picks the item up.
for (const auto ffaMember : team.members) {
lootMsg.target = ffaMember;
GameMessages::DeliverLocally(lootMsg);
}
}
DistrbuteMsgToTeam(lootMsg, team);
}
void DropLoot(Entity* player, const LWOOBJID source, const std::map<LOT, LootDropInfo>& rolledItems, uint32_t minCoins, uint32_t maxCoins, const bool noTeamLootOnDeath) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
const auto playerID = player->GetObjectID();
if (!player->IsPlayer()) {
LOG("Trying to drop loot for non-player %llu:%i", playerID, player->GetLOT());
return;
}
// TODO should be scene based instead of radius based
// drop loot to either single player or team
// powerups never have an owner when dropped
// for every player on the team in a radius of 700 (arbitrary value, not lore)
// if shared loot, drop everything but tokens to the next team member that gets loot,
// then tokens to everyone (1 token drop in a 3 person team means everyone gets a token)
// if Free for all, drop everything with NO owner, except tokens which follow the same logic as above
auto* team = TeamManager::Instance()->GetTeam(playerID);
GameMessages::GetPosition posMsg;
posMsg.target = source;
posMsg.Send();
const auto spawnPosition = posMsg.pos;
auto* const objectsTable = CDClientManager::GetTable<CDObjectsTable>();
for (const auto& [lootLot, info] : rolledItems) {
if (info.count > 0) EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::ITEM_DROPS, lootLot, spawnPosition, info.count, player);
}
constexpr LOT TOKEN_PROXY = 13763;
// Go through the drops 1 at a time to drop them
for (auto it = rolledItems.begin(); it != rolledItems.end(); it++) {
auto& [lootLot, info] = *it;
for (int i = 0; i < info.count; i++) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = lootLot;
lootMsg.count = 1;
lootMsg.currency = 0;
const CDObjects& object = objectsTable->GetByID(lootLot);
if (lootLot == TOKEN_PROXY) {
team ? DropFactionLoot(*team, lootMsg, noTeamLootOnDeath) : DropFactionLoot(*player, lootMsg);
} else if (info.table.MissionDrop) {
team ? DropMissionLoot(*team, lootMsg, noTeamLootOnDeath) : DropMissionLoot(*player, lootMsg);
} else if (object.type == "Powerup") {
team ? DropPowerupLoot(*team, lootMsg, noTeamLootOnDeath) : DropPowerupLoot(*player, lootMsg);
} else {
team ? DropRegularLoot(*team, lootMsg, noTeamLootOnDeath) : DropRegularLoot(*player, lootMsg);
}
}
}
// Filter out dead player if we need to
std::vector<LWOOBJID> lootEarners;
if (team) {
if (noTeamLootOnDeath) {
for (const auto member : team->members) {
GameMessages::IsDead isDeadMsg{};
isDeadMsg.target = member;
if (isDeadMsg.Send() && !isDeadMsg.bDead) {
lootEarners.push_back(member);
}
}
} else {
lootEarners = team->members;
}
} else {
lootEarners.push_back(playerID);
}
// Coin roll is divided up between the members, rounded up, then dropped for each player
const uint32_t coinRoll = LiveEvents::ScaleCoins(static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins)));
// Just in case its empty don't allow divide by 0
const auto droppedCoins = lootEarners.empty() ? coinRoll : static_cast<uint32_t>(std::ceil(static_cast<float>(coinRoll) / lootEarners.size()));
if (droppedCoins > 0 && !lootEarners.empty()) {
EconomyLedger::RecordMapEvent(IEconomyLedger::eMapEvent::COIN_DROPS, 0, spawnPosition, static_cast<int64_t>(droppedCoins) * lootEarners.size(), player);
}
// Drops coins for each alive member of a team (or just a player)
for (auto member : lootEarners) {
GameMessages::DropClientLoot lootMsg{};
lootMsg.target = member;
lootMsg.ownerID = member;
lootMsg.currency = droppedCoins;
lootMsg.spawnPos = spawnPosition;
lootMsg.sourceID = source;
lootMsg.item = LOT_NULL;
CalcFinalDropPos(lootMsg);
GameMessages::DeliverLocally(lootMsg);
const auto* const memberEntity = Game::entityManager->GetEntity(member);
if (memberEntity) lootMsg.Send(memberEntity->GetSystemAddress());
}
}
void Loot::DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam, bool forceFfa) {
auto* const team = useTeam ? TeamManager::Instance()->GetTeam(player.GetObjectID()) : nullptr;
char oldTeamLoot{};
if (team && forceFfa) {
oldTeamLoot = team->lootOption;
team->lootOption = 1;
}
auto* const objectsTable = CDClientManager::GetTable<CDObjectsTable>();
const CDObjects& object = objectsTable->GetByID(lootMsg.item);
constexpr LOT TOKEN_PROXY = 13763;
if (lootMsg.item == TOKEN_PROXY) {
team ? DropFactionLoot(*team, lootMsg) : DropFactionLoot(player, lootMsg);
} else if (object.type == "Powerup") {
team ? DropPowerupLoot(*team, lootMsg) : DropPowerupLoot(player, lootMsg);
} else {
team ? DropRegularLoot(*team, lootMsg) : DropRegularLoot(player, lootMsg);
}
if (team) team->lootOption = oldTeamLoot;
}
void Loot::CacheMatrix(uint32_t matrixIndex) {
if (CachedMatrices.contains(matrixIndex)) return;
CachedMatrices.insert(matrixIndex);
CDComponentsRegistryTable* componentsRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
CDLootMatrixTable* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
CDLootTableTable* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
CDRarityTableTable* rarityTableTable = CDClientManager::GetTable<CDRarityTableTable>();
const auto& matrix = lootMatrixTable->GetMatrix(matrixIndex);
for (const auto& entry : matrix) {
const auto& lootTable = lootTableTable->GetTable(entry.LootTableIndex);
const auto& rarityTable = rarityTableTable->GetRarityTable(entry.RarityTableIndex);
for (const auto& loot : lootTable) {
uint32_t itemComponentId = componentsRegistryTable->GetByIDAndType(loot.itemid, eReplicaComponentType::ITEM);
uint32_t rarity = itemComponentTable->GetItemComponentByID(itemComponentId).rarity;
}
}
}
Loot::Return Loot::RollLootMatrix(Entity* player, uint32_t matrixIndex) {
auto* const missionComponent = player ? player->GetComponent<MissionComponent>() : nullptr;
Loot::Return toReturn;
const auto drops = ::RollLootMatrix(matrixIndex);
// if no mission component, just convert the map and skip checking if its a mission drop
if (!missionComponent) {
for (const auto& [lot, info] : drops) toReturn[lot] = info.count;
} else {
for (const auto& [lot, info] : drops) {
const auto& itemInfo = info.table;
// filter out uneeded mission items
if (itemInfo.MissionDrop && !missionComponent->RequiresItem(itemInfo.itemid))
continue;
LOT itemLot = lot;
// convert faction token proxy
if (itemLot == 13763) {
if (missionComponent->GetMissionState(545) == eMissionState::COMPLETE)
itemLot = 8318; // "Assembly Token"
else if (missionComponent->GetMissionState(556) == eMissionState::COMPLETE)
itemLot = 8321; // "Venture League Token"
else if (missionComponent->GetMissionState(567) == eMissionState::COMPLETE)
itemLot = 8319; // "Sentinels Token"
else if (missionComponent->GetMissionState(578) == eMissionState::COMPLETE)
itemLot = 8320; // "Paradox Token"
}
if (itemLot == 13763) {
continue;
} // check if we aren't in faction
toReturn[itemLot] = info.count;
}
}
if (player) {
for (const auto& [lot, count] : toReturn) {
LOG("Player %llu has rolled %i of item %i from loot matrix %i", player->GetObjectID(), count, lot, matrixIndex);
}
}
return toReturn;
}
void Loot::GiveLoot(Entity* player, uint32_t matrixIndex, eLootSourceType lootSourceType) {
player = player->GetOwner(); // If the owner is overwritten, we collect that here
const auto result = RollLootMatrix(player, matrixIndex);
GiveLoot(player, result, lootSourceType);
}
void Loot::GiveLoot(Entity* player, const Loot::Return& result, eLootSourceType lootSourceType) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
if (!inventoryComponent)
return;
for (const auto& pair : result) {
inventoryComponent->AddItem(pair.first, pair.second, lootSourceType);
}
}
void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) {
CDActivityRewardsTable* activityRewardsTable = CDClientManager::GetTable<CDActivityRewardsTable>();
std::vector<CDActivityRewards> activityRewards = activityRewardsTable->Query([activityID](CDActivityRewards entry) { return (entry.objectTemplate == activityID); });
const CDActivityRewards* selectedReward = nullptr;
for (const auto& activityReward : activityRewards) {
if (activityReward.activityRating <= rating && (selectedReward == nullptr || activityReward.activityRating > selectedReward->activityRating)) {
selectedReward = &activityReward;
}
}
if (!selectedReward)
return;
uint32_t minCoins = 0;
uint32_t maxCoins = 0;
CDCurrencyTableTable* currencyTableTable = CDClientManager::GetTable<CDCurrencyTableTable>();
std::vector<CDCurrencyTable> currencyTable = currencyTableTable->Query([selectedReward](CDCurrencyTable entry) { return (entry.currencyIndex == selectedReward->CurrencyIndex && entry.npcminlevel == 1); });
if (currencyTable.size() > 0) {
minCoins = currencyTable[0].minvalue;
maxCoins = currencyTable[0].maxvalue;
}
GiveLoot(player, selectedReward->LootMatrixIndex, eLootSourceType::ACTIVITY);
uint32_t coins = static_cast<uint32_t>(minCoins + GeneralUtils::GenerateRandomNumber<float>(0, 1) * (maxCoins - minCoins));
auto* character = player->GetCharacter();
character->SetCoins(character->GetCoins() + coins, eLootSourceType::ACTIVITY);
}
void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) {
player = player->GetOwner(); // if the owner is overwritten, we collect that here
auto* scriptedActivityComponent = Game::entityManager->GetZoneControlEntity()->GetComponent<ScriptedActivityComponent>();
const bool noTeamLootOnDeath = scriptedActivityComponent ? scriptedActivityComponent->GetNoTeamLootOnDeath() : false;
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
if (!inventoryComponent)
return;
const auto result = ::RollLootMatrix(matrixIndex, LiveEvents::LootChanceMultiplier());
::DropLoot(player, source, result, minCoins, maxCoins, noTeamLootOnDeath);
}
void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) {
CDActivityRewardsTable* activityRewardsTable = CDClientManager::GetTable<CDActivityRewardsTable>();
std::vector<CDActivityRewards> activityRewards = activityRewardsTable->Query([activityID](CDActivityRewards entry) { return (entry.objectTemplate == activityID); });
const CDActivityRewards* selectedReward = nullptr;
for (const auto& activityReward : activityRewards) {
if (activityReward.activityRating <= rating && (selectedReward == nullptr || activityReward.activityRating > selectedReward->activityRating)) {
selectedReward = &activityReward;
}
}
if (selectedReward == nullptr) {
return;
}
uint32_t minCoins = 0;
uint32_t maxCoins = 0;
CDCurrencyTableTable* currencyTableTable = CDClientManager::GetTable<CDCurrencyTableTable>();
std::vector<CDCurrencyTable> currencyTable = currencyTableTable->Query([selectedReward](CDCurrencyTable entry) { return (entry.currencyIndex == selectedReward->CurrencyIndex && entry.npcminlevel == 1); });
if (currencyTable.size() > 0) {
minCoins = currencyTable[0].minvalue;
maxCoins = currencyTable[0].maxvalue;
}
DropLoot(player, source, selectedReward->LootMatrixIndex, minCoins, maxCoins);
}