mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
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>
620 lines
24 KiB
C++
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);
|
|
}
|