From b7e3c5e899cde835fcd0c2f1044e7d488be72e5c Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 12:52:26 -0500 Subject: [PATCH] fix(loot): activity rewards drop from the player at the activity object Live sent quickbuild completion loot (about 190 samples) and the dragon, BONS, spider queen and Frakjaw chest and wishing well rewards with the player who earned them as the DropClientLoot source and owner, use_position true and the spawn position at the object. DLU used the object as the source, so use_position was false. DropActivityLoot now uses the player as the source by default. The growing flowers and the VE mission console have no live samples and keep the object as the source. Co-Authored-By: Claude Opus 5.5 --- dGame/dUtilities/Loot.cpp | 45 ++++++++++++------- dGame/dUtilities/Loot.h | 7 ++- .../02_server/Map/General/GrowingFlower.cpp | 3 +- .../02_server/Map/VE/VeMissionConsole.cpp | 3 +- 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/dGame/dUtilities/Loot.cpp b/dGame/dUtilities/Loot.cpp index a03dbcae7..20549b401 100644 --- a/dGame/dUtilities/Loot.cpp +++ b/dGame/dUtilities/Loot.cpp @@ -335,7 +335,8 @@ void DropRegularLoot(Team& team, GameMessages::DropClientLoot& lootMsg, const bo DistrbuteMsgToTeam(lootMsg, team); } -void DropLoot(Entity* player, const LWOOBJID source, const std::map& rolledItems, uint32_t minCoins, uint32_t maxCoins, const bool noTeamLootOnDeath) { +// source: the DropClientLoot source object; spawnPosition: where the loot spawns +void DropLoot(Entity* player, const LWOOBJID source, const NiPoint3& spawnPosition, const std::map& 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()) { @@ -352,11 +353,6 @@ void DropLoot(Entity* player, const LWOOBJID source, const std::mapGetTeam(playerID); - GameMessages::GetPosition posMsg; - posMsg.target = source; - posMsg.Send(); - - const auto spawnPosition = posMsg.pos; auto* const objectsTable = CDClientManager::GetTable(); for (const auto& [lootLot, info] : rolledItems) { @@ -590,23 +586,36 @@ void Loot::GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti character->SetCoins(character->GetCoins() + coins, eLootSourceType::ACTIVITY, CoinSource::Object(*player)); } -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 +namespace { + NiPoint3 GetDropSpawnPosition(const LWOOBJID object) { + GameMessages::GetPosition posMsg; + posMsg.target = object; + posMsg.Send(); + return posMsg.pos; + } - auto* scriptedActivityComponent = Game::entityManager->GetZoneControlEntity()->GetComponent(); - const bool noTeamLootOnDeath = scriptedActivityComponent ? scriptedActivityComponent->GetNoTeamLootOnDeath() : false; + void RollAndDropLoot(Entity* player, const LWOOBJID source, const NiPoint3& spawnPosition, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) { + player = player->GetOwner(); // if the owner is overwritten, we collect that here - auto* inventoryComponent = player->GetComponent(); + auto* scriptedActivityComponent = Game::entityManager->GetZoneControlEntity()->GetComponent(); + const bool noTeamLootOnDeath = scriptedActivityComponent ? scriptedActivityComponent->GetNoTeamLootOnDeath() : false; - if (!inventoryComponent) - return; + auto* inventoryComponent = player->GetComponent(); - const auto result = ::RollLootMatrix(matrixIndex, LiveEvents::LootChanceMultiplier()); + if (!inventoryComponent) + return; - ::DropLoot(player, source, result, minCoins, maxCoins, noTeamLootOnDeath); + const auto result = ::RollLootMatrix(matrixIndex, LiveEvents::LootChanceMultiplier()); + + ::DropLoot(player, source, spawnPosition, result, minCoins, maxCoins, noTeamLootOnDeath); + } } -void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating) { +void Loot::DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins) { + RollAndDropLoot(player, source, GetDropSpawnPosition(source), matrixIndex, minCoins, maxCoins); +} + +void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating, const bool fromPlayer) { CDActivityRewardsTable* activityRewardsTable = CDClientManager::GetTable(); std::vector activityRewards = activityRewardsTable->Query([activityID](CDActivityRewards entry) { return (entry.objectTemplate == activityID); }); @@ -623,5 +632,7 @@ void Loot::DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t acti const auto [minCoins, maxCoins] = GetActivityCoinRange(*selectedReward); - DropLoot(player, source, selectedReward->LootMatrixIndex, minCoins, maxCoins); + // Live sent activity rewards from the player who earned them, spawning at the activity object + const auto dropSource = fromPlayer ? player->GetOwner()->GetObjectID() : source; + RollAndDropLoot(player, dropSource, GetDropSpawnPosition(source), selectedReward->LootMatrixIndex, minCoins, maxCoins); } diff --git a/dGame/dUtilities/Loot.h b/dGame/dUtilities/Loot.h index b05a7cf4c..f6bb0bd92 100644 --- a/dGame/dUtilities/Loot.h +++ b/dGame/dUtilities/Loot.h @@ -31,7 +31,12 @@ namespace Loot { void GiveActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0); void DropLoot(Entity* player, const LWOOBJID source, uint32_t matrixIndex, uint32_t minCoins, uint32_t maxCoins); void DropItem(Entity& player, GameMessages::DropClientLoot& lootMsg, bool useTeam = false, bool forceFfa = false); - void DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0); + /** + * Drops the loot of an activity's reward for the given rating, spawning at the source object. + * fromPlayer: the DropClientLoot source is the player instead of the object, as live sent quickbuild, chest and + * wishing well rewards (use_position true, spawn position the object's). + */ + void DropActivityLoot(Entity* player, const LWOOBJID source, uint32_t activityID, int32_t rating = 0, bool fromPlayer = true); /** * The coin range {min, max} of an ActivityRewards row, as live rolled it: the reward's ChallengeRating is the diff --git a/dScripts/02_server/Map/General/GrowingFlower.cpp b/dScripts/02_server/Map/General/GrowingFlower.cpp index 2c3929e20..12ccce8f2 100644 --- a/dScripts/02_server/Map/General/GrowingFlower.cpp +++ b/dScripts/02_server/Map/General/GrowingFlower.cpp @@ -16,7 +16,8 @@ void GrowingFlower::OnSkillEventFired(Entity* self, Entity* target, const std::s auto* scriptedActivityComponent = self->GetComponent(); if (scriptedActivityComponent != nullptr) { - Loot::DropActivityLoot(target, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), 0); + // No live samples of this drop: kept sourced from the flower + Loot::DropActivityLoot(target, self->GetObjectID(), scriptedActivityComponent->GetActivityID(), 0, false); } auto* missionComponent = target->GetComponent(); diff --git a/dScripts/02_server/Map/VE/VeMissionConsole.cpp b/dScripts/02_server/Map/VE/VeMissionConsole.cpp index 443c9a570..bf731d232 100644 --- a/dScripts/02_server/Map/VE/VeMissionConsole.cpp +++ b/dScripts/02_server/Map/VE/VeMissionConsole.cpp @@ -7,7 +7,8 @@ #include "eTerminateType.h" void VeMissionConsole::OnUse(Entity* self, Entity* user) { - Loot::DropActivityLoot(user, self->GetObjectID(), 12551); + // No live samples of this drop: kept sourced from the console + Loot::DropActivityLoot(user, self->GetObjectID(), 12551, 0, false); auto* inventoryComponent = user->GetComponent(); if (inventoryComponent != nullptr) {