mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
fix(missions): a mission is only turned in when its rewards fit, as the client
LWOMissionComponent::MissionWindowOK (0x00d501f0) turns a mission in only when CanReceiveAllRewards (msgCanReceiveAllRewards 0x00d17e20, sub_00d17750) passes for the reward the player picked: the reward items (the repeat ones once completed, each at least 1) must fit after the items the tasks take (a gather task's target unless it keeps them, one of a delivery task's target) with the first completion's bag rewards added. The fit is LWOInventoryComponent_Common::CheckCanAddItems (0x00d53de0): turned in items leave first (an item of 1 settles the whole count), a unique item fails while one is held outside the buyback, then each item tops up stacks of its LOT (stack size 0 is no limit, LookupItemInfo 0x00c1c190) and takes free slots. MissionDialogueOK now refuses a turn-in that does not pass, which the client never sends. The client's max currency part is not checked: the server writes no cm, so the client has no limit either. InventoryComponent::GetDefaultInventorySize is split out of GetInventory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
#include "EconomyLedger.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
#include "Entity.h"
|
||||
#include "Item.h"
|
||||
@@ -157,6 +160,17 @@ void InventoryComponent::LoadDataFromTemplate() {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t InventoryComponent::GetDefaultInventorySize(const eInventoryType type) {
|
||||
switch (type) {
|
||||
case eInventoryType::ITEMS: return 20u;
|
||||
case eInventoryType::VAULT_MODELS:
|
||||
case eInventoryType::VAULT_ITEMS: return 40u;
|
||||
case eInventoryType::VENDOR_BUYBACK: return 27u;
|
||||
case eInventoryType::DONATION: return 24u;
|
||||
default: return 240u;
|
||||
}
|
||||
}
|
||||
|
||||
Inventory* InventoryComponent::GetInventory(const eInventoryType type) {
|
||||
const auto index = m_Inventories.find(type);
|
||||
|
||||
@@ -165,25 +179,7 @@ Inventory* InventoryComponent::GetInventory(const eInventoryType type) {
|
||||
}
|
||||
|
||||
// Create new empty inventory
|
||||
uint32_t size = 240u;
|
||||
|
||||
switch (type) {
|
||||
case eInventoryType::ITEMS:
|
||||
size = 20u;
|
||||
break;
|
||||
case eInventoryType::VAULT_MODELS:
|
||||
case eInventoryType::VAULT_ITEMS:
|
||||
size = 40u;
|
||||
break;
|
||||
case eInventoryType::VENDOR_BUYBACK:
|
||||
size = 27u;
|
||||
break;
|
||||
case eInventoryType::DONATION:
|
||||
size = 24u;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const auto size = GetDefaultInventorySize(type);
|
||||
|
||||
auto* inventory = new Inventory(type, size, {}, this);
|
||||
|
||||
@@ -646,6 +642,104 @@ bool InventoryComponent::HasSpaceForLoot(const Loot::Return& loot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InventoryComponent::CanAddItems(const std::vector<std::pair<LOT, uint32_t>>& items, const std::vector<std::pair<LOT, uint32_t>>& itemsToTurnIn,
|
||||
const std::map<eInventoryType, uint32_t>& sizeIncrease) const {
|
||||
// A copy of the inventories: per inventory its items in ID order (new ones last, as the client's new IDs)
|
||||
struct SimItem {
|
||||
LOT lot;
|
||||
uint32_t count;
|
||||
uint32_t slot;
|
||||
LWOOBJID subKey;
|
||||
};
|
||||
std::map<eInventoryType, std::vector<SimItem>> inventories;
|
||||
std::map<eInventoryType, uint32_t> sizes;
|
||||
for (const auto& [type, inventory] : m_Inventories) {
|
||||
if (!inventory) continue;
|
||||
auto& simItems = inventories[type];
|
||||
for (const auto& [id, item] : inventory->GetItems()) {
|
||||
if (item) simItems.push_back({ item->GetLot(), item->GetCount(), item->GetSlot(), item->GetSubKey() });
|
||||
}
|
||||
sizes[type] = inventory->GetSize();
|
||||
}
|
||||
for (const auto& [type, increase] : sizeIncrease) sizes[type] += increase;
|
||||
|
||||
// Turned in items leave first; an item of 1 settles the whole count, as the client
|
||||
for (auto [lot, count] : itemsToTurnIn) {
|
||||
for (auto& [type, simItems] : inventories) {
|
||||
for (auto it = simItems.begin(); count != 0 && it != simItems.end();) {
|
||||
if (it->lot != lot) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (it->count < 2) {
|
||||
it = simItems.erase(it);
|
||||
count = 0;
|
||||
continue;
|
||||
}
|
||||
const auto taken = std::min(count, it->count);
|
||||
it->count -= taken;
|
||||
count -= taken;
|
||||
it = it->count == 0 ? simItems.erase(it) : it + 1;
|
||||
}
|
||||
if (count == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
// LWOInventoryComponent_Common::GetInvItemCount over every inventory
|
||||
const auto heldCount = [this](const LOT lot, const std::optional<eInventoryType> only) {
|
||||
uint32_t total = 0;
|
||||
for (const auto& [type, inventory] : m_Inventories) {
|
||||
if (!inventory || (only && type != *only)) continue;
|
||||
for (const auto& [id, item] : inventory->GetItems()) {
|
||||
if (item && item->GetLot() == lot) total += item->GetCount();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
for (const auto& [lot, wanted] : items) {
|
||||
const auto type = Inventory::FindInventoryTypeForLot(lot);
|
||||
// An inventory the server has not made yet is empty (the client refuses one it does not have, but has every
|
||||
// one the server sent)
|
||||
if (!inventories.contains(type)) sizes[type] += GetDefaultInventorySize(type);
|
||||
auto& simItems = inventories[type];
|
||||
|
||||
const auto& info = Inventory::FindItemComponent(lot);
|
||||
// LookupItemInfo (0x00c1c190): a stack size of 0 is no limit
|
||||
const uint32_t stack = info.stackSize == 0 ? std::numeric_limits<uint32_t>::max() : info.stackSize;
|
||||
auto left = wanted;
|
||||
if (info.isUnique) {
|
||||
if (heldCount(lot, std::nullopt) != heldCount(lot, eInventoryType::VENDOR_BUYBACK)) return false; // UniqueItemStillExistsInAnInventory
|
||||
left = 1;
|
||||
}
|
||||
|
||||
for (auto& simItem : simItems) {
|
||||
if (left == 0) break;
|
||||
if (simItem.lot != lot || simItem.subKey != LWOOBJID_EMPTY || simItem.count >= stack) continue;
|
||||
const auto added = std::min(left, stack - simItem.count);
|
||||
simItem.count += added;
|
||||
left -= added;
|
||||
}
|
||||
|
||||
if (left == 0) continue;
|
||||
std::set<uint32_t> takenSlots;
|
||||
for (const auto& simItem : simItems) takenSlots.insert(simItem.slot);
|
||||
const auto size = sizes[type];
|
||||
while (left != 0) {
|
||||
if (takenSlots.size() >= size) return false; // TargetInventoryOutOfRoom
|
||||
uint32_t slot = 0;
|
||||
while (slot < size && takenSlots.contains(slot)) slot++;
|
||||
if (slot >= size) return false; // items sit beyond the size; the client would look for a slot forever
|
||||
takenSlots.insert(slot);
|
||||
const auto added = std::min(left, stack);
|
||||
left -= added;
|
||||
simItems.push_back({ lot, added, slot, LWOOBJID_EMPTY });
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
/**
|
||||
* Cars and rockets built before builds were recorded have no subkey, so the client has no blueprint id to ask for
|
||||
|
||||
@@ -126,6 +126,9 @@ public:
|
||||
*/
|
||||
Inventory* GetInventory(eInventoryType type);
|
||||
|
||||
// The size an inventory starts with when the server makes it
|
||||
static uint32_t GetDefaultInventorySize(eInventoryType type);
|
||||
|
||||
/**
|
||||
* Returns all the inventories this entity has, indexed by type
|
||||
* @return all the inventories this entity has, indexed by type
|
||||
@@ -255,6 +258,18 @@ public:
|
||||
*/
|
||||
bool HasSpaceForLoot(const Loot::Return& loot);
|
||||
|
||||
/**
|
||||
* Whether all of these items would fit, as the client works it out before turning in a mission
|
||||
* (LWOInventoryComponent_Common::CheckCanAddItems 0x00d53de0): the items to turn in are taken out first, the bag
|
||||
* sizes grow by sizeIncrease, a unique item fails while one is held outside the buyback, then each item tops up
|
||||
* stacks of its LOT and takes free slots. Nothing changes.
|
||||
* @param items LOTs and counts to add
|
||||
* @param itemsToTurnIn LOTs and counts taken from any inventory first
|
||||
* @param sizeIncrease extra slots per inventory
|
||||
*/
|
||||
bool CanAddItems(const std::vector<std::pair<LOT, uint32_t>>& items, const std::vector<std::pair<LOT, uint32_t>>& itemsToTurnIn,
|
||||
const std::map<eInventoryType, uint32_t>& sizeIncrease) const;
|
||||
|
||||
/**
|
||||
* Equips an item in the specified slot
|
||||
* @param location the location to store the item (e.g. chest, left hand, etc.)
|
||||
|
||||
@@ -160,6 +160,14 @@ namespace GameMessages {
|
||||
if (iMissionState == eMissionState::AVAILABLE || iMissionState == eMissionState::COMPLETE_AVAILABLE) {
|
||||
missionComponent->AcceptMission(missionID);
|
||||
} else if (iMissionState == eMissionState::READY_TO_COMPLETE || iMissionState == eMissionState::COMPLETE_READY_TO_COMPLETE) {
|
||||
// LWOMissionComponent::MissionWindowOK (0x00d501f0) only turns a mission in when every reward fits
|
||||
// (CanReceiveAllRewards, with the reward the player picked)
|
||||
const auto* const mission = missionComponent->GetMission(missionID);
|
||||
const auto chosen = mission && mission->GetReward() > 0 ? mission->GetReward() : -1;
|
||||
if (!mission || !mission->CanReceiveAllRewards(chosen)) {
|
||||
LOG("Player %llu cannot receive all the rewards of mission %i, not turning it in", player->GetObjectID(), missionID);
|
||||
return;
|
||||
}
|
||||
missionComponent->CompleteMission(missionID);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include "ChatServerLink.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <ctime>
|
||||
@@ -464,6 +467,56 @@ void Mission::Catchup() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Mission::CanReceiveAllRewards(const LOT chosenReward) const {
|
||||
if (IsComplete()) return false;
|
||||
|
||||
auto* const entity = GetAssociate();
|
||||
auto* const inventoryComponent = entity ? entity->GetComponent<InventoryComponent>() : nullptr;
|
||||
if (!inventoryComponent) return false;
|
||||
|
||||
// The repeat rewards once ever completed; a reward item counts at least 1 (LWOMission 0x00fa35d0)
|
||||
const bool repeat = m_Completions > 0;
|
||||
const std::array<std::pair<LOT, int32_t>, 4> rewards = repeat
|
||||
? std::array<std::pair<LOT, int32_t>, 4>{ { { info.reward_item1_repeatable, info.reward_item1_repeat_count }, { info.reward_item2_repeatable, info.reward_item2_repeat_count },
|
||||
{ info.reward_item3_repeatable, info.reward_item3_repeat_count }, { info.reward_item4_repeatable, info.reward_item4_repeat_count } } }
|
||||
: std::array<std::pair<LOT, int32_t>, 4>{ { { info.reward_item1, info.reward_item1_count }, { info.reward_item2, info.reward_item2_count },
|
||||
{ info.reward_item3, info.reward_item3_count }, { info.reward_item4, info.reward_item4_count } } };
|
||||
std::vector<std::pair<LOT, uint32_t>> items;
|
||||
for (const auto& [lot, count] : rewards) {
|
||||
if (lot == LOT_NULL || (chosenReward != -1 && lot != chosenReward)) continue;
|
||||
items.emplace_back(lot, static_cast<uint32_t>(std::max(count, 1)));
|
||||
}
|
||||
|
||||
// The bag rewards come with the first completion only
|
||||
std::map<eInventoryType, uint32_t> sizeIncrease;
|
||||
if (!repeat) {
|
||||
if (info.reward_maxinventory > 0) sizeIncrease[eInventoryType::ITEMS] += info.reward_maxinventory;
|
||||
if (info.reward_maxmodel > 0) sizeIncrease[eInventoryType::MODELS] += info.reward_maxmodel;
|
||||
}
|
||||
|
||||
// The items the tasks take (sub_00fa2f20): a gather task's target unless it keeps them (LWOGatherTask 0x00f919c0),
|
||||
// one of a delivery task's target
|
||||
const bool noGiver = (info.offer_objectID == -1 || info.offer_objectID == 0) && (info.target_objectID == -1 || info.target_objectID == 0);
|
||||
std::vector<std::pair<LOT, uint32_t>> itemsToTurnIn;
|
||||
for (const auto* const task : m_Tasks) {
|
||||
const auto& taskInfo = task->GetClientInfo();
|
||||
const auto target = static_cast<LOT>(taskInfo.target);
|
||||
if (task->GetType() == eMissionTaskType::GATHER) {
|
||||
const auto& parameters = task->GetParameters();
|
||||
int32_t param = parameters.empty() ? 2 : static_cast<int32_t>(parameters[0]);
|
||||
if (noGiver && (param & 2) != 0) param = 1; // an achievement never takes items
|
||||
const bool keep = (param & 1) != 0 && (param & 2) == 0;
|
||||
if (!keep && target != LOT_NULL) itemsToTurnIn.emplace_back(target, static_cast<uint32_t>(taskInfo.targetValue));
|
||||
} else if (task->GetType() == eMissionTaskType::DELIVERY) {
|
||||
if (target != LOT_NULL) itemsToTurnIn.emplace_back(target, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// The client also refuses when the coin reward would pass its max currency (the save's cm, no limit without it);
|
||||
// the server writes no cm, so there is no limit to check
|
||||
return inventoryComponent->CanAddItems(items, itemsToTurnIn, sizeIncrease);
|
||||
}
|
||||
|
||||
void Mission::YieldRewards() {
|
||||
auto* entity = GetAssociate();
|
||||
|
||||
|
||||
@@ -250,6 +250,14 @@ public:
|
||||
* Whether the player has looked at this mission in progress (LWOMissionComponent::msgSetMissionViewed 0x00bec7a0),
|
||||
* saved as v on the mission's <cur> entry
|
||||
*/
|
||||
/**
|
||||
* Whether the player can take every reward of this mission in progress, as the client checks before turning it in
|
||||
* (LWOMissionComponent::msgCanReceiveAllRewards 0x00d17e20, sub_00d17750): the reward items (all, or only the
|
||||
* chosen one) must fit in the inventory after the items the tasks take, with the bag rewards added.
|
||||
* @param chosenReward the reward the player picked, -1 for all
|
||||
*/
|
||||
bool CanReceiveAllRewards(LOT chosenReward = -1) const;
|
||||
|
||||
bool GetViewed() const { return m_Viewed; }
|
||||
void SetViewed(const bool viewed) { m_Viewed = viewed; }
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ set(DCOMPONENTS_TESTS
|
||||
"CollectibleComponentTests.cpp"
|
||||
"MissionOfferTests.cpp"
|
||||
"MissionClientRulesTests.cpp"
|
||||
"MissionRewardSpaceTests.cpp"
|
||||
"ReplicaComponentOrderTests.cpp"
|
||||
"QuickBuildCompleteTests.cpp"
|
||||
"RocketLaunchTests.cpp"
|
||||
|
||||
203
tests/dGameTests/dComponentsTests/MissionRewardSpaceTests.cpp
Normal file
203
tests/dGameTests/dComponentsTests/MissionRewardSpaceTests.cpp
Normal file
@@ -0,0 +1,203 @@
|
||||
// Whether a mission's rewards fit before it is turned in, as the 1.10.64 client checks
|
||||
// (LWOMissionComponent::msgCanReceiveAllRewards 0x00d17e20, LWOInventoryComponent_Common::CheckCanAddItems 0x00d53de0).
|
||||
#include "GameDependencies.h"
|
||||
|
||||
#include "CDClientDatabase.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "CDComponentsRegistryTable.h"
|
||||
#include "CDItemComponentTable.h"
|
||||
#include "CDMissionTasksTable.h"
|
||||
#include "CDMissionsTable.h"
|
||||
#include "Entity.h"
|
||||
#include "GameMessages.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "Item.h"
|
||||
#include "Mission.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "eItemType.h"
|
||||
#include "eMissionTaskType.h"
|
||||
#include "eReplicaComponentType.h"
|
||||
|
||||
#include "tinyxml2.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class MissionRewardSpaceTests : public GameDependenciesTest {
|
||||
protected:
|
||||
static constexpr LOT HAT = 8100; // stack 1
|
||||
static constexpr LOT GEM = 8101; // stack 10
|
||||
static constexpr LOT UNIQUE = 8102; // unique
|
||||
static constexpr LOT DEED = 8103; // stack 0: no limit
|
||||
static constexpr uint32_t PLAIN = 993001; // rewards a hat; the repeat rewards 2 hats
|
||||
static constexpr uint32_t GATHER = 993002; // takes 2 gems, rewards a hat
|
||||
static constexpr uint32_t KEEP = 993003; // gathers 2 gems but keeps them, rewards a hat
|
||||
static constexpr uint32_t BAG = 993004; // rewards a hat and one more slot
|
||||
static constexpr uint32_t GEMS = 993005; // rewards 5 gems
|
||||
static constexpr uint32_t ONE = 993006; // rewards the unique item
|
||||
static constexpr uint32_t CHOICE = 993007; // rewards a hat or 5 gems
|
||||
|
||||
std::unique_ptr<Entity> player;
|
||||
InventoryComponent* inventory{};
|
||||
MissionComponent* missions{};
|
||||
LWOOBJID nextId = 0x9000;
|
||||
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(":memory:"); // MissionComponent counts the achievements
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);");
|
||||
RegisterLot(HAT, 1, false);
|
||||
RegisterLot(GEM, 10, false);
|
||||
RegisterLot(UNIQUE, 1, true);
|
||||
RegisterLot(DEED, 0, false);
|
||||
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
|
||||
|
||||
auto& rows = CDClientManager::GetEntriesMutable<CDMissionsTable>();
|
||||
rows.push_back(Rewards(PLAIN, HAT, 1));
|
||||
rows.back().repeatable = true;
|
||||
rows.back().reward_item1_repeatable = HAT;
|
||||
rows.back().reward_item1_repeat_count = 2;
|
||||
rows.push_back(Rewards(GATHER, HAT, 1));
|
||||
rows.push_back(Rewards(KEEP, HAT, 1));
|
||||
rows.push_back(Rewards(BAG, HAT, 1));
|
||||
rows.back().reward_maxinventory = 1;
|
||||
rows.push_back(Rewards(GEMS, GEM, 5));
|
||||
rows.push_back(Rewards(ONE, UNIQUE, 1));
|
||||
rows.push_back(Rewards(CHOICE, HAT, 1));
|
||||
rows.back().reward_item2 = GEM;
|
||||
rows.back().reward_item2_count = 5;
|
||||
|
||||
auto& tasks = CDClientManager::GetEntriesMutable<CDMissionTasksTable>();
|
||||
tasks.push_back(GatherTask(GATHER, ""));
|
||||
tasks.push_back(GatherTask(KEEP, "1"));
|
||||
|
||||
player = std::make_unique<Entity>(48, info);
|
||||
inventory = player->AddComponent<InventoryComponent>(-1);
|
||||
missions = player->AddComponent<MissionComponent>(-1);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
player.reset();
|
||||
CDClientManager::GetEntriesMutable<CDMissionsTable>().clear();
|
||||
CDClientManager::GetEntriesMutable<CDMissionTasksTable>().clear();
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
static CDMissions Rewards(const uint32_t id, const LOT lot, const int32_t count) {
|
||||
CDMissions row{};
|
||||
row.id = id;
|
||||
row.isMission = true;
|
||||
row.cooldownTime = -1;
|
||||
row.offer_objectID = 1000;
|
||||
row.target_objectID = 1000;
|
||||
for (auto* const item : { &row.reward_item1, &row.reward_item2, &row.reward_item3, &row.reward_item4,
|
||||
&row.reward_item1_repeatable, &row.reward_item2_repeatable, &row.reward_item3_repeatable, &row.reward_item4_repeatable }) *item = LOT_NULL;
|
||||
row.reward_item1 = lot;
|
||||
row.reward_item1_count = count;
|
||||
return row;
|
||||
}
|
||||
|
||||
static CDMissionTasks GatherTask(const uint32_t missionId, const std::string& param) {
|
||||
CDMissionTasks task{};
|
||||
task.id = missionId;
|
||||
task.taskType = static_cast<uint32_t>(eMissionTaskType::GATHER);
|
||||
task.target = GEM;
|
||||
task.targetValue = 2;
|
||||
task.taskParam1 = param;
|
||||
return task;
|
||||
}
|
||||
|
||||
static void RegisterLot(const LOT lot, const uint32_t stack, const bool unique) {
|
||||
const auto componentID = static_cast<uint32_t>(97000 + lot);
|
||||
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
|
||||
registry.insert_or_assign(static_cast<uint64_t>(lot), componentID);
|
||||
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::ITEM) << 32 | static_cast<uint64_t>(lot), componentID);
|
||||
CDItemComponent component{};
|
||||
component.id = componentID;
|
||||
component.itemType = static_cast<uint32_t>(eItemType::HAT);
|
||||
component.stackSize = stack;
|
||||
component.isUnique = unique;
|
||||
CDClientManager::GetEntriesMutable<CDItemComponentTable>().insert_or_assign(componentID, component);
|
||||
}
|
||||
|
||||
void Give(const LOT lot, const uint32_t count, const eInventoryType type = eInventoryType::ITEMS) {
|
||||
auto* const bag = inventory->GetInventory(type);
|
||||
new Item(nextId++, lot, bag, bag->FindEmptySlot(), count, false, {}, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE);
|
||||
}
|
||||
|
||||
// Fills the items bag with hats up to all but `free` slots
|
||||
void Fill(const int32_t free) {
|
||||
auto* const bag = inventory->GetInventory(eInventoryType::ITEMS);
|
||||
while (bag->GetEmptySlots() > free) Give(HAT, 1);
|
||||
}
|
||||
|
||||
// The mission ready to turn in (the first time, or a repeat)
|
||||
const Mission& Ready(const uint32_t id, const bool repeat = false) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto ids = std::to_string(id);
|
||||
const auto done = repeat ? "<m id=\"" + ids + "\" state=\"8\" cct=\"1\" cts=\"1000\"/>" : std::string();
|
||||
const auto xml = "<obj v=\"1\"><mis><done>" + done + "</done><cur><m id=\"" + ids + "\" state=\"" + (repeat ? "12" : "4") + "\"/></cur></mis></obj>";
|
||||
doc.Parse(xml.c_str());
|
||||
missions->LoadFromSaveData(doc);
|
||||
return *missions->GetMission(id);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, RewardNeedsAFreeSlot) {
|
||||
const auto& mission = Ready(PLAIN);
|
||||
Fill(1);
|
||||
EXPECT_TRUE(mission.CanReceiveAllRewards());
|
||||
Fill(0);
|
||||
EXPECT_FALSE(mission.CanReceiveAllRewards());
|
||||
}
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, RepeatRewardsAndTheBagReward) {
|
||||
Fill(1);
|
||||
EXPECT_FALSE(Ready(PLAIN, true).CanReceiveAllRewards()); // 2 hats the second time
|
||||
Fill(0);
|
||||
EXPECT_TRUE(Ready(BAG).CanReceiveAllRewards()); // the reward's own extra slot
|
||||
}
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, TakenItemsMakeRoom) {
|
||||
Fill(1);
|
||||
Give(GEM, 1); // the last slot; taking one gem settles the whole count, as the client
|
||||
EXPECT_TRUE(Ready(GATHER).CanReceiveAllRewards());
|
||||
EXPECT_FALSE(Ready(KEEP).CanReceiveAllRewards());
|
||||
}
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, StacksAreToppedUp) {
|
||||
Fill(1);
|
||||
Give(GEM, 5);
|
||||
EXPECT_TRUE(Ready(GEMS).CanReceiveAllRewards());
|
||||
EXPECT_TRUE(inventory->CanAddItems({ { GEM, 5 } }, {}, {}));
|
||||
EXPECT_FALSE(inventory->CanAddItems({ { GEM, 6 } }, {}, {}));
|
||||
EXPECT_FALSE(inventory->CanAddItems({ { GEM, 5 }, { GEM, 1 } }, {}, {}));
|
||||
EXPECT_FALSE(inventory->CanAddItems({ { DEED, 1000 } }, { { GEM, 4 } }, {})); // the gems keep their slot
|
||||
EXPECT_TRUE(inventory->CanAddItems({ { DEED, 1000 } }, { { GEM, 5 } }, {})); // a stack size of 0 is no limit
|
||||
EXPECT_TRUE(inventory->CanAddItems({ { HAT, 1 } }, {}, { { eInventoryType::ITEMS, 1 } }));
|
||||
}
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, OneUniqueItem) {
|
||||
const auto& mission = Ready(ONE);
|
||||
EXPECT_TRUE(mission.CanReceiveAllRewards());
|
||||
Give(UNIQUE, 1, eInventoryType::VENDOR_BUYBACK);
|
||||
EXPECT_TRUE(mission.CanReceiveAllRewards()); // only sold back
|
||||
Give(UNIQUE, 1, eInventoryType::VAULT_ITEMS);
|
||||
EXPECT_FALSE(mission.CanReceiveAllRewards());
|
||||
}
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, OnlyTheChosenReward) {
|
||||
const auto& mission = Ready(CHOICE);
|
||||
Fill(1);
|
||||
EXPECT_FALSE(mission.CanReceiveAllRewards()); // a hat and the gems need two slots
|
||||
EXPECT_TRUE(mission.CanReceiveAllRewards(HAT));
|
||||
EXPECT_TRUE(mission.CanReceiveAllRewards(GEM));
|
||||
}
|
||||
|
||||
TEST_F(MissionRewardSpaceTests, NotInProgress) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
doc.Parse(R"(<obj v="1"><mis><done><m id="993001" state="8" cct="1" cts="1000"/></done><cur/></mis></obj>)");
|
||||
missions->LoadFromSaveData(doc);
|
||||
EXPECT_FALSE(missions->GetMission(PLAIN)->CanReceiveAllRewards());
|
||||
}
|
||||
Reference in New Issue
Block a user