diff --git a/src/ItemManager.cpp b/src/ItemManager.cpp index 37beac3..1c1e36b 100644 --- a/src/ItemManager.cpp +++ b/src/ItemManager.cpp @@ -11,6 +11,13 @@ std::map, Item> ItemManager::ItemData; std::map> ItemManager::VendorTables; std::map ItemManager::CrocPotTable; +std::map> ItemManager::RarityRatios; +std::map ItemManager::Crates; +/// pair Itemset, Rarity -> vector of pointers (map iterators) to records in ItemData +std::map, std::vector, Item>::iterator>> ItemManager::CrateItems; + +// buffer for error messages +char buffer[255]; void ItemManager::init() { REGISTER_SHARD_PACKET(P_CL2FE_REQ_ITEM_MOVE, itemMoveHandler); @@ -801,12 +808,12 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) { if (data->size != sizeof(sP_CL2FE_REQ_ITEM_CHEST_OPEN)) return; // ignore the malformed packet - sP_CL2FE_REQ_ITEM_CHEST_OPEN *pkt = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf; - Player *plr = PlayerManager::getPlayer(sock); + sP_CL2FE_REQ_ITEM_CHEST_OPEN *chest = (sP_CL2FE_REQ_ITEM_CHEST_OPEN *)data->buf; + Player *player = PlayerManager::getPlayer(sock); - if (plr == nullptr) + if (player == nullptr) return; - + // item giving packet const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); assert(resplen < CN_PACKET_BUFFER_SIZE - 8); @@ -820,21 +827,23 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) { memset(respbuf, 0, resplen); // maintain stats - reward->m_iCandy = plr->money; - reward->m_iFusionMatter = plr->fusionmatter; + reward->m_iCandy = player->money; + reward->m_iFusionMatter = player->fusionmatter; reward->iFatigue = 100; // prevents warning message reward->iFatigue_Level = 1; reward->iItemCnt = 1; // remember to update resplen if you change this - // item reward - item->sItem.iType = 0; - item->sItem.iID = 96; - item->sItem.iOpt = 1; - item->iSlotNum = pkt->iSlotNum; - item->eIL = pkt->eIL; + // item reward + if (chest->ChestItem.iType != 9) + std::cout << "[WARN] Player tried to open a crate with incorrect iType ?!" << std::endl; + else + item->sItem = openCrate(chest->ChestItem.iID, player->PCStyle.iGender); + + item->iSlotNum = chest->iSlotNum; + item->eIL = chest->eIL; // update player - plr->Inven[pkt->iSlotNum] = item->sItem; + player->Inven[chest->iSlotNum] = item->sItem; // transmit item sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen); @@ -842,12 +851,119 @@ void ItemManager::chestOpenHandler(CNSocket *sock, CNPacketData *data) { // chest opening acknowledgement packet INITSTRUCT(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, resp); - resp.iSlotNum = pkt->iSlotNum; + resp.iSlotNum = chest->iSlotNum; std::cout << "opening chest..." << std::endl; sock->sendPacket((void*)&resp, P_FE2CL_REP_ITEM_CHEST_OPEN_SUCC, sizeof(sP_FE2CL_REP_ITEM_CHEST_OPEN_SUCC)); } +sItemBase ItemManager::openCrate(int crateId, int playerGender) { + sItemBase reward = {}; + try { + Crate crate = getCrate(crateId); + int itemSetId = getItemSetId(crate, crateId); + int rarity = getRarity(crate, itemSetId); + reward = getCrateItem(itemSetId, rarity, playerGender); + } + catch (const std::exception& err) { + std::cerr << "[WARN] An error has occured while trying to open a crate. Error description: \n" << err.what() << std::endl; + // if we failed to open a crate, at least give the player a gumball (suggested by Jade) + reward.iType = 7; + reward.iID = 119 + (rand() % 3); + reward.iOpt = 1; + } + + return reward; +} + +Crate ItemManager::getCrate(int crateId) { + if (Crates.find(crateId) == Crates.end()) + throwError(sprintf(buffer, "Crate %d was not found!", crateId)); + return Crates[crateId]; +} + +int ItemManager::getItemSetId(Crate crate, int crateId) { + int itemSetsCount = crate.itemSets.size(); + if (itemSetsCount == 0) + throwError(sprintf(buffer, "Crate %d has no item sets assigned?!", crateId)); + + // if crate points to multiple itemSets, choose a random one + int itemSetIndex = rand() % itemSetsCount; + return crate.itemSets[itemSetIndex]; +} + +int ItemManager::getRarity(Crate crate , int itemSetId) { + // find rarity ratio + if (RarityRatios.find(crate.rarityRatioId) == RarityRatios.end()) + throwError(sprintf(buffer, "Rarity Ratio %d not found!", crate.rarityRatioId)); + + std::vector rarityRatio = RarityRatios[crate.rarityRatioId]; + + /* + * First we have to check if specified item set contains items with all specified rarities, + * and if not eliminate them from the draw + * it is simpler to do here than to fix individually in the file + */ + + // remember that rarities start from 1 ! + for (int i = 0; i < rarityRatio.size(); i++){ + if (CrateItems.find(std::make_pair(itemSetId, i+1)) == CrateItems.end()) + rarityRatio[i] = 0; + } + + int total = 0; + for (int value : rarityRatio) + total += value; + + // if we didn't find any items, throw exception + if (total == 0) + throwError(sprintf(buffer, "Item Set %d has no items assigned?!", itemSetId)); + + // now return a random rarity number + int randomNum = rand() % total; + int rarity = 0; + int sum = 0; + do { + sum += rarityRatio[rarity]; + rarity++; + } while (sum <= randomNum); + + return rarity; +} + +sItemBase ItemManager::getCrateItem(int itemSetId, int rarity, int playerGender) { + std::pair key = std::make_pair(itemSetId, rarity); + + if (CrateItems.find(key) == CrateItems.end()) + throwError(sprintf(buffer, "Item Set ID %d Rarity %d items have not been found", itemSetId, rarity)); + + // only take into account items that have correct gender + std::vector, Item>::iterator> items; + for (auto crateitem : CrateItems[key]) + { + int gender = crateitem->second.gender; + // if gender is incorrect, exclude item + if (gender != 0 && gender != playerGender) + continue; + items.push_back(crateitem); + } + + if (items.size() == 0) + throwError(sprintf(buffer, "Gender inequality! Set ID %d Rarity %d contains only %s items?!", itemSetId, rarity, playerGender==2 ? "boys" : "girls")); + auto item = items[rand() % items.size()]; + sItemBase result = {}; + result.iID = item->first.first; + result.iType = item->first.second; + result.iOpt = 1; + + return result; +} + +// argument is here only so we can call sprintf in brackets +void ItemManager::throwError(int ignore) { + throw buffer; +} + // TODO: use this in cleaned up ItemManager int ItemManager::findFreeSlot(Player *plr) { int i; diff --git a/src/ItemManager.hpp b/src/ItemManager.hpp index 819075e..9d9a8cd 100644 --- a/src/ItemManager.hpp +++ b/src/ItemManager.hpp @@ -5,7 +5,7 @@ struct Item { bool tradeable, sellable; - int buyPrice, sellPrice, stackSize, level, rarity, pointDamage, groupDamage, defense; // TODO: implement more as needed + int buyPrice, sellPrice, stackSize, level, rarity, pointDamage, groupDamage, defense, gender; // TODO: implement more as needed }; struct VendorListing { int sort, type, iID; @@ -14,6 +14,10 @@ struct CrocPotEntry { int multStats, multLooks; float base, rd0, rd1, rd2, rd3; }; +struct Crate { + int rarityRatioId; + std::vector itemSets; +}; namespace ItemManager { enum class SlotType { @@ -25,6 +29,11 @@ namespace ItemManager { extern std::map, Item> ItemData; // -> data extern std::map> VendorTables; extern std::map CrocPotTable; // level gap -> entry + extern std::map> RarityRatios; + extern std::map Crates; + // pair -> vector of pointers (map iterators) to records in ItemData (it looks a lot scarier than it is) + extern std::map, + std::vector, Item>::iterator>> CrateItems; void init(); @@ -46,6 +55,14 @@ namespace ItemManager { void itemTradeChatHandler(CNSocket* sock, CNPacketData* data); void chestOpenHandler(CNSocket* sock, CNPacketData* data); + // crate opening logic with all helper functions + sItemBase openCrate(int crateId, int playerGender); + Crate getCrate(int crateId); + int getItemSetId(Crate crate, int crateId); + int getRarity(Crate crate, int itemSetId); + sItemBase getCrateItem(int itemSetId, int rarity, int playerGender); + void throwError(int ignore); + int findFreeSlot(Player *plr); Item* getItemData(int32_t id, int32_t type); void checkItemExpire(CNSocket* sock, Player* player); diff --git a/src/MobManager.cpp b/src/MobManager.cpp index d2a33f1..af54a4c 100644 --- a/src/MobManager.cpp +++ b/src/MobManager.cpp @@ -11,6 +11,8 @@ #include std::map MobManager::Mobs; +std::map MobManager::MobDropChances; +std::map MobManager::MobDrops; std::queue MobManager::RemovalQueue; bool MobManager::simulateMobs; @@ -143,7 +145,7 @@ void MobManager::npcAttackPc(Mob *mob, time_t currTime) { } } -void MobManager::giveReward(CNSocket *sock) { +void MobManager::giveReward(CNSocket *sock, Mob* mob) { Player *plr = PlayerManager::getPlayer(sock); if (plr == nullptr) @@ -160,9 +162,31 @@ void MobManager::giveReward(CNSocket *sock) { // don't forget to zero the buffer! memset(respbuf, 0, resplen); - // NOTE: these will need to be scaled according to the player/mob level difference - plr->money += (int)MissionManager::AvatarGrowth[plr->level]["m_iMobFM"]; // this one's innacurate, but close enough for now - MissionManager::updateFusionMatter(sock, MissionManager::AvatarGrowth[plr->level]["m_iMobFM"]); + // sanity check + if (MobDrops.find(mob->dropType) == MobDrops.end()) { + std::cout << "[WARN] Drop Type " << mob->dropType << " was not found" << std::endl; + return; + } + // find correct mob drop + MobDrop drop = MobDrops[mob->dropType]; + + plr->money += drop.taros; + // formula for scaling FM with player/mob level difference + // TODO: adjust this better + int levelDifference = plr->level - mob->level; + int fm = drop.fm; + if (levelDifference > 0) + fm = levelDifference < 10 ? fm - (levelDifference * fm / 10) : 0; + + MissionManager::updateFusionMatter(sock, fm); + + // give boosts 1 in 3 times + if (drop.boosts > 0) { + if (rand() % 3 == 0) + plr->batteryN += drop.boosts; + if (rand() % 3 == 0) + plr->batteryW += drop.boosts; + } // simple rewards reward->m_iCandy = plr->money; @@ -173,20 +197,29 @@ void MobManager::giveReward(CNSocket *sock) { reward->iFatigue_Level = 1; reward->iItemCnt = 1; // remember to update resplen if you change this -#if 0 + int slot = ItemManager::findFreeSlot(plr); - if (slot == -1) { -#else - int slot = -1; - if (true) { -#endif + + bool awardDrop = false; + MobDropChance chance; + // sanity check + if (MobDropChances.find(drop.dropChanceType) == MobDropChances.end()) + std::cout << "[WARN] Unknown Drop Chance Type: " << drop.dropChanceType << std::endl; + else { + chance = MobDropChances[drop.dropChanceType]; + awardDrop = (rand() % 1000 < chance.dropChance); + } + + + // no drop + if (slot == -1 || !awardDrop) { + // no room for an item, but you still get FM and taros reward->iItemCnt = 0; sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, sizeof(sP_FE2CL_REP_REWARD_ITEM)); } else { // item reward - item->sItem.iType = 9; - item->sItem.iID = 1; + item->sItem = getReward(&drop, &chance); item->iSlotNum = slot; item->eIL = 1; // Inventory Location. 1 means player inventory. @@ -194,8 +227,91 @@ void MobManager::giveReward(CNSocket *sock) { plr->Inven[slot] = item->sItem; sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen); + } + // event crates + if (settings::EVENTMODE != 0) + giveEventReward(sock, plr); +} + +sItemBase MobManager::getReward(MobDrop* drop, MobDropChance* chance) { + sItemBase reward = {}; + reward.iType = 9; + reward.iOpt = 1; + + int total = 0; + for (int ratio : chance->cratesRatio) + total += ratio; + + // randomizing a crate + int randomNum = rand() % total; + int i = 0; + int sum = 0; + do { + reward.iID = drop->crateIDs[i]; + sum += chance->cratesRatio[i]; + i++; + } + while (sum<=randomNum); + return reward; +} + +void MobManager::giveEventReward(CNSocket* sock, Player* player) { + + // random drop chance + if (rand() % 100 > settings::EVENTCRATECHANCE) + return; + // no slot = no award + int slot = ItemManager::findFreeSlot(player); + if (slot == -1) + return; + + const size_t resplen = sizeof(sP_FE2CL_REP_REWARD_ITEM) + sizeof(sItemReward); + assert(resplen < CN_PACKET_BUFFER_SIZE - 8); + // we know it's only one trailing struct, so we can skip full validation + + uint8_t respbuf[resplen]; // not a variable length array, don't worry + sP_FE2CL_REP_REWARD_ITEM* reward = (sP_FE2CL_REP_REWARD_ITEM*)respbuf; + sItemReward* item = (sItemReward*)(respbuf + sizeof(sP_FE2CL_REP_REWARD_ITEM)); + + // don't forget to zero the buffer! + memset(respbuf, 0, resplen); + + // leave everything here as it is + reward->m_iCandy = player->money; + reward->m_iFusionMatter = player->fusionmatter; + reward->m_iBatteryN = player->batteryN; + reward->m_iBatteryW = player->batteryW; + reward->iFatigue = 100; // prevents warning message + reward->iFatigue_Level = 1; + reward->iItemCnt = 1; // remember to update resplen if you change this + + // which crate to drop + int crateId; + switch (settings::EVENTMODE) + { + // knishmas + case 1: crateId = 1187; break; + // halloween + case 2: crateId = 1181; break; + // spring + case 3: crateId = 1126; break; + // what + default: + std::cout << "[WARN] Unknown event Id " << settings::EVENTMODE << std::endl; + return; + } + + item->sItem.iType = 9; + item->sItem.iID = crateId; + item->sItem.iOpt = 1; + item->iSlotNum = slot; + item->eIL = 1; // Inventory Location. 1 means player inventory. + + // update player + player->Inven[slot] = item->sItem; + sock->sendPacket((void*)respbuf, P_FE2CL_REP_REWARD_ITEM, resplen); } int MobManager::hitMob(CNSocket *sock, Mob *mob, int damage) { @@ -233,7 +349,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) { mob->target = nullptr; mob->appearanceData.iConditionBitFlag = 0; mob->killedTime = getTime(); // XXX: maybe introduce a shard-global time for each step? - + // check for the edge case where hitting the mob did not aggro it if (sock != nullptr) { Player* plr = PlayerManager::getPlayer(sock); @@ -242,7 +358,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) { return; if (plr->groupCnt == 1 && plr->iIDGroup == plr->iID) { - giveReward(sock); + giveReward(sock, mob); MissionManager::mobKilled(sock, mob->appearanceData.iNPCType); } else { plr = PlayerManager::getPlayerFromID(plr->iIDGroup); @@ -252,7 +368,7 @@ void MobManager::killMob(CNSocket *sock, Mob *mob) { for (int i = 0; i < plr->groupCnt; i++) { CNSocket* sockTo = PlayerManager::getSockFromID(plr->groupIDs[i]); - giveReward(sockTo); + giveReward(sockTo, mob); MissionManager::mobKilled(sockTo, mob->appearanceData.iNPCType); } } diff --git a/src/MobManager.hpp b/src/MobManager.hpp index 2555175..a6e79eb 100644 --- a/src/MobManager.hpp +++ b/src/MobManager.hpp @@ -25,6 +25,7 @@ struct Mob : public BaseNPC { int spawnX; int spawnY; int spawnZ; + int level; // dead time_t killedTime = 0; @@ -42,6 +43,9 @@ struct Mob : public BaseNPC { time_t nextAttack = 0; int roamX, roamY, roamZ; + //drop + int dropType; + // temporary; until we're sure what's what nlohmann::json data; @@ -53,6 +57,8 @@ struct Mob : public BaseNPC { regenTime = data["m_iRegenTime"]; idleRange = (int)data["m_iIdleRange"] * 2; // TODO: tuning? + dropType = data["m_iDropType"]; + level = data["m_iNpcLevel"]; // XXX: temporarily force respawns for Fusions until we implement instancing //if (regenTime >= 300000000) @@ -84,9 +90,24 @@ struct Mob : public BaseNPC { } }; +struct MobDropChance { + int dropChance; + std::vector cratesRatio; +}; + +struct MobDrop { + std::vector crateIDs; + int dropChanceType; + int taros; + int fm; + int boosts; +}; + namespace MobManager { extern std::map Mobs; extern std::queue RemovalQueue; + extern std::map MobDropChances; + extern std::map MobDrops; extern bool simulateMobs; void init(); @@ -107,7 +128,10 @@ namespace MobManager { void npcAttackPc(Mob *mob, time_t currTime); int hitMob(CNSocket *sock, Mob *mob, int damage); void killMob(CNSocket *sock, Mob *mob); - void giveReward(CNSocket *sock); + void giveReward(CNSocket *sock, Mob *mob); + sItemBase getReward(MobDrop *drop, MobDropChance *chance); + void giveEventReward(CNSocket* sock, Player* player); + std::pair lerp(int, int, int, int, int); std::pair getDamage(int, int, bool, bool, int, int, int); diff --git a/src/TableData.cpp b/src/TableData.cpp index 2844a58..619a3c9 100644 --- a/src/TableData.cpp +++ b/src/TableData.cpp @@ -119,7 +119,7 @@ void TableData::init() { auto item = _item.value(); int typeOverride = getItemType(i); // used for special cases where iEquipLoc doesn't indicate item type ItemManager::ItemData[std::pair(item["m_iItemNumber"], typeOverride != -1 ? typeOverride : (int)item["m_iEquipLoc"])] - = { item["m_iTradeAble"] == 1, item["m_iSellAble"] == 1, item["m_iItemPrice"], item["m_iItemSellPrice"], item["m_iStackNumber"], i > 9 ? 0 : (int)item["m_iMinReqLev"], i > 9 ? 1 : (int)item["m_iRarity"], i > 9 ? 0 : (int)item["m_iPointRat"], i > 9 ? 0 : (int)item["m_iGroupRat"], i > 9 ? 0 : (int)item["m_iDefenseRat"] }; + = { item["m_iTradeAble"] == 1, item["m_iSellAble"] == 1, item["m_iItemPrice"], item["m_iItemSellPrice"], item["m_iStackNumber"], i > 9 ? 0 : (int)item["m_iMinReqLev"], i > 9 ? 1 : (int)item["m_iRarity"], i > 9 ? 0 : (int)item["m_iPointRat"], i > 9 ? 0 : (int)item["m_iGroupRat"], i > 9 ? 0 : (int)item["m_iDefenseRat"], i > 9 ? 0 : (int)item["m_iReqSex"] }; } } @@ -197,6 +197,8 @@ void TableData::init() { std::cerr << "[WARN] Malformed mobs.json file! Reason:" << err.what() << std::endl; } + loadDrops(); + loadPaths(&nextId); // load paths loadGruntwork(&nextId); @@ -293,6 +295,115 @@ void TableData::loadPaths(int* nextId) { } } +/* + * Load drops data from JSON. + * This has to be called after reading xdt because it reffers to ItemData!!! + */ +void TableData::loadDrops() { + try { + std::ifstream inFile(settings::DROPSJSON); + nlohmann::json dropData; + + // read file into json + inFile >> dropData; + + // MobDropChances + nlohmann::json mobDropChances = dropData["MobDropChances"]; + for (nlohmann::json::iterator _dropChance = mobDropChances.begin(); _dropChance != mobDropChances.end(); _dropChance++) { + auto dropChance = _dropChance.value(); + MobDropChance toAdd = {}; + toAdd.dropChance = (int)dropChance["DropChance"]; + for (nlohmann::json::iterator _cratesRatio = dropChance["CratesRatio"].begin(); _cratesRatio != dropChance["CratesRatio"].end(); _cratesRatio++) { + toAdd.cratesRatio.push_back((int)_cratesRatio.value()); + } + MobManager::MobDropChances[(int)dropChance["Type"]] = toAdd; + } + + // MobDrops + nlohmann::json mobDrops = dropData["MobDrops"]; + for (nlohmann::json::iterator _drop = mobDrops.begin(); _drop != mobDrops.end(); _drop++) { + auto drop = _drop.value(); + MobDrop toAdd = {}; + for (nlohmann::json::iterator _crates = drop["CrateIDs"].begin(); _crates != drop["CrateIDs"].end(); _crates++) { + toAdd.crateIDs.push_back((int)_crates.value()); + } + + toAdd.dropChanceType = (int)drop["DropChance"]; + // Check if DropChance exists + if (MobManager::MobDropChances.find(toAdd.dropChanceType) == MobManager::MobDropChances.end()) + { + std::string errorMessage = " MobDropChance not found: " + std::to_string((toAdd.dropChanceType)); + throw (std::exception((errorMessage).c_str())); + } + // Check if number of crates is correct + if (!(MobManager::MobDropChances[(int)drop["DropChance"]].cratesRatio.size() == toAdd.crateIDs.size())) + { + std::string errorMessage = " DropType " + std::to_string((int)drop["DropType"]) + " contains invalid number of crates"; + throw (std::exception((errorMessage).c_str())); + } + + toAdd.taros = (int)drop["Taros"]; + toAdd.fm = (int)drop["FM"]; + toAdd.boosts = (int)drop["Boosts"]; + MobManager::MobDrops[(int)drop["DropType"]] = toAdd; + } + std::cout << "[INFO] Loaded " << MobManager::MobDrops.size() << " Mob Drop Types"<< std::endl; + // Rarity Ratios + nlohmann::json rarities = dropData["RarityRatios"]; + for (nlohmann::json::iterator _rarity = rarities.begin(); _rarity != rarities.end(); _rarity++) { + auto rarity = _rarity.value(); + std::vector toAdd; + for (nlohmann::json::iterator _ratio = rarity["Ratio"].begin(); _ratio != rarity["Ratio"].end(); _ratio++){ + toAdd.push_back((int)_ratio.value()); + } + ItemManager::RarityRatios[(int)rarity["Type"]] = toAdd; + } + // Crates + nlohmann::json crates = dropData["Crates"]; + for (nlohmann::json::iterator _crate = crates.begin(); _crate != crates.end(); _crate++) { + auto crate = _crate.value(); + Crate toAdd; + toAdd.rarityRatioId = (int)crate["RarityRatio"]; + for (nlohmann::json::iterator _itemSet = crate["ItemSets"].begin(); _itemSet != crate["ItemSets"].end(); _itemSet++) { + toAdd.itemSets.push_back((int)_itemSet.value()); + } + ItemManager::Crates[(int)crate["Id"]] = toAdd; + } + // Crate Items + nlohmann::json items = dropData["Items"]; + int itemCount = 0; + for (nlohmann::json::iterator _item = items.begin(); _item != items.end(); _item++) { + auto item = _item.value(); + std::pair itemSetkey = std::make_pair((int)item["ItemSet"], (int)item["Rarity"]); + std::pair itemDataKey = std::make_pair((int)item["Id"], (int)item["Type"]); + if (ItemManager::ItemData.find(itemDataKey) == ItemManager::ItemData.end()) + { + char buff[255]; + sprintf(buff, "Unknown item with Id %d and Type %d", (int)item["Id"], (int)item["Type"]); + throw (std::exception(buff)); + } + std::map, Item>::iterator toAdd = ItemManager::ItemData.find(itemDataKey); + // if item collection doesn't exist, start a new one + if (ItemManager::CrateItems.find(itemSetkey) == ItemManager::CrateItems.end()) { + std::vector, Item>::iterator> vector; + vector.push_back(toAdd); + ItemManager::CrateItems[itemSetkey] = vector; + } + // else add a new element to existing collection + else + ItemManager::CrateItems[itemSetkey].push_back(toAdd); + itemCount++; + } + + std::cout << "[INFO] Loaded " << ItemManager::Crates.size() << " Crates containing " + <