#include "StarItemDatabase.hpp" #include "StarCodexDatabase.hpp" #include "StarJsonExtra.hpp" #include "StarRoot.hpp" #include "StarAssets.hpp" #include "StarCasting.hpp" #include "StarCurrency.hpp" #include "StarConsumableItem.hpp" #include "StarBlueprintItem.hpp" #include "StarCodexItem.hpp" #include "StarLiquidItem.hpp" #include "StarMaterialItem.hpp" #include "StarObjectItem.hpp" #include "StarItemDrop.hpp" #include "StarInspectionTool.hpp" #include "StarInstrumentItem.hpp" #include "StarThrownItem.hpp" #include "StarUnlockItem.hpp" #include "StarActiveItem.hpp" #include "StarAugmentItem.hpp" #include "StarTools.hpp" #include "StarArmors.hpp" #include "StarObjectDatabase.hpp" #include "StarRootLuaBindings.hpp" #include "StarItemLuaBindings.hpp" #include "StarConfigLuaBindings.hpp" #include "StarUtilityLuaBindings.hpp" namespace Star { EnumMap ItemTypeNames{ {ItemType::Generic, "generic"}, {ItemType::LiquidItem, "liquid"}, {ItemType::MaterialItem, "material"}, {ItemType::ObjectItem, "object"}, {ItemType::CurrencyItem, "currency"}, {ItemType::MiningTool, "miningtool"}, {ItemType::Flashlight, "flashlight"}, {ItemType::WireTool, "wiretool"}, {ItemType::BeamMiningTool, "beamminingtool"}, {ItemType::HarvestingTool, "harvestingtool"}, {ItemType::TillingTool, "tillingtool"}, {ItemType::PaintingBeamTool, "paintingbeamtool"}, {ItemType::HeadArmor, "headarmor"}, {ItemType::ChestArmor, "chestarmor"}, {ItemType::LegsArmor, "legsarmor"}, {ItemType::BackArmor, "backarmor"}, {ItemType::Consumable, "consumable"}, {ItemType::Blueprint, "blueprint"}, {ItemType::Codex, "codex"}, {ItemType::InspectionTool, "inspectiontool"}, {ItemType::InstrumentItem, "instrument"}, {ItemType::ThrownItem, "thrownitem"}, {ItemType::UnlockItem, "unlockitem"}, {ItemType::ActiveItem, "activeitem"}, {ItemType::AugmentItem, "augmentitem"} }; uint64_t ItemDatabase::getCountOfItem(List const& bag, ItemDescriptor const& item, bool exactMatch) { auto normalizedBag = normalizeBag(bag); return getCountOfItem(normalizedBag, item, exactMatch); } uint64_t ItemDatabase::getCountOfItem(HashMap const& bag, ItemDescriptor const& item, bool exactMatch) { ItemDescriptor matchItem = exactMatch ? item.singular() : ItemDescriptor(item.name(), 1); if (!bag.contains(matchItem)) { return 0; } else { return bag.get(matchItem); } } HashMap ItemDatabase::normalizeBag(List const& bag) { HashMap normalizedBag; for (auto const& item : bag) { if (!item) continue; normalizedBag[ItemDescriptor(item->name(), 1)] += item->count(); if (!item->parameters().toObject().empty()) normalizedBag[ItemDescriptor(item->name(), 1, item->parameters())] += item->count(); } return normalizedBag; } HashSet ItemDatabase::recipesFromSubset(HashMap const& normalizedBag, StringMap const& availableCurrencies, HashSet const& subset) { HashSet res; for (auto const& recipe : subset) { // add this recipe if we can make it. if (canMakeRecipe(recipe, normalizedBag, availableCurrencies)) res.add(recipe); } return res; } HashSet ItemDatabase::recipesFromSubset(HashMap const& normalizedBag, StringMap const& availableCurrencies, HashSet const& subset, StringSet const& allowedTypes) { HashSet res; for (auto const& recipe : subset) { // is it the right kind of recipe for this check ? if (recipe.groups.hasIntersection(allowedTypes) || allowedTypes.empty() || recipe.groups.empty()) { // do we have the ingredients to make it. if (canMakeRecipe(recipe, normalizedBag, availableCurrencies)) { res.add(recipe); } } } return res; } String ItemDatabase::guiFilterString(ItemPtr const& item) { return (item->name() + item->friendlyName() + item->description()).toLower().splitAny(" ,.?*\\+/|\t").join(""); } bool ItemDatabase::canMakeRecipe(ItemRecipe const& recipe, HashMap const& availableIngredients, StringMap const& availableCurrencies) { for (auto const& p : recipe.currencyInputs) { if (availableCurrencies.value(p.first, 0) < p.second) return false; } for (auto const& input : recipe.inputs) { ItemDescriptor matchInput = recipe.matchInputParameters ? input.singular() : ItemDescriptor(input.name(), 1); if (availableIngredients.value(matchInput) < input.count()) return false; } return true; } ItemDatabase::ItemDatabase() : m_luaRoot(make_shared()) { scanItems(); addObjectItems(); addCodexes(); scanRecipes(); addBlueprints(); } void ItemDatabase::cleanup() { { MutexLocker locker(m_cacheMutex); m_itemCache.cleanup([](ItemCacheEntry const&, ItemPtr const& item) { return !item.unique(); }); } } ItemPtr ItemDatabase::diskLoad(Json const& diskStore) const { if (diskStore) { return item(ItemDescriptor::loadStore(diskStore)); } else { return {}; } } ItemPtr ItemDatabase::fromJson(Json const& spec) const { return item(ItemDescriptor(spec)); } Json ItemDatabase::diskStore(ItemConstPtr const& itemPtr) const { if (itemPtr) return itemPtr->descriptor().diskStore(); else return Json(); } Json ItemDatabase::toJson(ItemConstPtr const& itemPtr) const { if (itemPtr) return itemPtr->descriptor().toJson(); else return Json(); } bool ItemDatabase::hasItem(String const& itemName) const { return m_items.contains(itemName); } ItemType ItemDatabase::itemType(String const& itemName) const { return itemData(itemName).type; } String ItemDatabase::itemFriendlyName(String const& itemName) const { return itemData(itemName).friendlyName; } StringSet ItemDatabase::itemTags(String const& itemName) const { return itemData(itemName).itemTags; } ItemDatabase::ItemConfig ItemDatabase::itemConfig(String const& itemName, Json parameters, Maybe level, Maybe seed) const { auto const& data = itemData(itemName); ItemConfig itemConfig; if (data.assetsConfig) itemConfig.config = Root::singleton().assets()->json(*data.assetsConfig); itemConfig.directory = data.directory; itemConfig.config = jsonMerge(itemConfig.config, data.customConfig); itemConfig.parameters = parameters; if (auto builder = itemConfig.config.optString("builder")) { RecursiveMutexLocker locker(m_luaMutex); auto context = m_luaRoot->createContext(*builder); context.setCallbacks("root", LuaBindings::makeRootCallbacks()); context.setCallbacks("sb", LuaBindings::makeUtilityCallbacks()); luaTie(itemConfig.config, itemConfig.parameters) = context.invokePath>( "build", itemConfig.directory, itemConfig.config, itemConfig.parameters, level, seed); } return itemConfig; } ItemPtr ItemDatabase::itemShared(ItemDescriptor descriptor, Maybe level, Maybe seed) const { if (!descriptor) return {}; ItemCacheEntry entry{ descriptor, level, seed }; MutexLocker locker(m_cacheMutex); if (ItemPtr* cached = m_itemCache.ptr(entry)) return *cached; else { locker.unlock(); ItemPtr item = tryCreateItem(descriptor, level, seed); get<2>(entry) = item->parameters().optUInt("seed"); // Seed could've been changed by the buildscript locker.lock(); return m_itemCache.get(entry, [&](ItemCacheEntry const&) -> ItemPtr { return move(item); }); } } ItemPtr ItemDatabase::item(ItemDescriptor descriptor, Maybe level, Maybe seed) const { if (!descriptor) return {}; else return tryCreateItem(descriptor, level, seed); } bool ItemDatabase::hasRecipeToMake(ItemDescriptor const& item) const { auto si = item.singular(); for (auto const& recipe : m_recipes) if (recipe.output.singular() == si) return true; return false; } bool ItemDatabase::hasRecipeToMake(ItemDescriptor const& item, StringSet const& allowedTypes) const { auto si = item.singular(); for (auto const& recipe : m_recipes) if (recipe.output.singular() == si) for (auto allowedType : allowedTypes) if (recipe.groups.contains(allowedType)) return true; return false; } HashSet ItemDatabase::recipesForOutputItem(String itemName) const { HashSet result; for (auto const& recipe : m_recipes) if (recipe.output.name() == itemName) result.add(recipe); return result; } HashSet ItemDatabase::recipesFromBagContents(List const& bag, StringMap const& availableCurrencies) const { auto normalizedBag = normalizeBag(bag); return recipesFromBagContents(normalizedBag, availableCurrencies); } HashSet ItemDatabase::recipesFromBagContents(HashMap const& bag, StringMap const& availableCurrencies) const { return recipesFromSubset(bag, availableCurrencies, m_recipes); } HashSet ItemDatabase::recipesFromBagContents(List const& bag, StringMap const& availableCurrencies, StringSet const& allowedTypes) const { auto normalizedBag = normalizeBag(bag); return recipesFromBagContents(normalizedBag, availableCurrencies, allowedTypes); } HashSet ItemDatabase::recipesFromBagContents(HashMap const& bag, StringMap const& availableCurrencies, StringSet const& allowedTypes) const { return recipesFromSubset(bag, availableCurrencies, m_recipes, allowedTypes); } uint64_t ItemDatabase::maxCraftableInBag(List const& bag, StringMap const& availableCurrencies, ItemRecipe const& recipe) const { auto normalizedBag = normalizeBag(bag); return maxCraftableInBag(normalizedBag, availableCurrencies, recipe); } uint64_t ItemDatabase::maxCraftableInBag(HashMap const& bag, StringMap const& availableCurrencies, ItemRecipe const& recipe) const { uint64_t res = highest(); for (auto const& p : recipe.currencyInputs) { uint64_t available = availableCurrencies.value(p.first, 0); if (available == 0) return 0; else if (p.second > 0) res = min(available / p.second, res); } for (auto const& input : recipe.inputs) { if (!bag.contains(input.singular())) return 0; else if (input.count() > 0) res = min(bag.get(input.singular()) / input.count(), res); } return res; } ItemRecipe ItemDatabase::getPreciseRecipeForMaterials(String const& group, List const& bag, StringMap const& availableCurrencies) const { // picks the recipe that: // * can be crafted (duh) // * uses all the input material types // * uses the most materials (if recipes exist with the same input materials) auto options = recipesFromBagContents(bag, availableCurrencies); ItemRecipe result; int ingredientsCount = 0; for (auto const& recipe : options) { if (!recipe.groups.contains(group)) continue; bool usesAllItemTypes = true; for (auto const& item : bag) { bool match = false; for (auto const& input : recipe.inputs) if (item->matches(input, recipe.matchInputParameters)) match = true; if (!match) usesAllItemTypes = false; } if (!usesAllItemTypes) continue; int count = 0; for (auto const& input : recipe.inputs) count += input.count(); if (count > ingredientsCount) result = recipe; } return result; } ItemRecipe ItemDatabase::parseRecipe(Json const& config) const { ItemRecipe res; try { res.currencyInputs = jsonToMapV>(config.get("currencyInputs", JsonObject()), mem_fn(&Json::toUInt)); // parse currency items into currency inputs for (auto input : config.getArray("input")) { auto id = ItemDescriptor(input); if (itemType(id.name()) == ItemType::CurrencyItem) { auto currencyItem = as(itemShared(id)); res.currencyInputs[currencyItem->currencyType()] += currencyItem->totalValue(); } else { res.inputs.push_back(id); } } res.output = ItemDescriptor(config.get("output")); res.duration = config.getFloat("duration", Root::singleton().assets()->json("/items/defaultParameters.config:defaultCraftDuration").toFloat()); res.groups = StringSet::from(jsonToStringList(config.get("groups", JsonArray()))); if (auto item = ItemDatabase::itemShared(res.output)) { res.outputRarity = item->rarity(); res.guiFilterString = guiFilterString(item); } res.collectables = jsonToMapV>(config.get("collectables", JsonObject()), mem_fn(&Json::toString)); res.matchInputParameters = config.getBool("matchInputParameters", false); } catch (JsonException const& e) { throw RecipeException(strf("Recipe missing required ingredient: {}", outputException(e, false))); } return res; } HashSet ItemDatabase::allRecipes() const { return m_recipes; } HashSet ItemDatabase::allRecipes(StringSet const& types) const { HashSet res; for (auto const& i : m_recipes) { if (i.groups.hasIntersection(types)) res.add(i); } return res; } ItemPtr ItemDatabase::applyAugment(ItemPtr const item, AugmentItem* augment) const { if (item) { RecursiveMutexLocker locker(m_luaMutex); LuaBaseComponent script; script.setLuaRoot(m_luaRoot); script.setScripts(augment->augmentScripts()); script.addCallbacks("item", LuaBindings::makeItemCallbacks(augment)); script.addCallbacks("config", LuaBindings::makeConfigCallbacks(bind(&Item::instanceValue, augment, _1, _2))); script.init(); auto luaResult = script.invoke>>("apply", item->descriptor().toJson()); script.uninit(); locker.unlock(); if (luaResult) { if (!get<0>(*luaResult).isNull()) { augment->take(get<1>(*luaResult).value(1)); return ItemDatabase::item(ItemDescriptor(get<0>(*luaResult))); } } } return item; } bool ItemDatabase::ageItem(ItemPtr& item, double aging) const { if (!item) return false; auto const& itemData = ItemDatabase::itemData(item->name()); if (itemData.agingScripts.empty()) return false; ItemDescriptor original = item->descriptor(); RecursiveMutexLocker locker(m_luaMutex); LuaBaseComponent script; script.setLuaRoot(m_luaRoot); script.setScripts(itemData.agingScripts); script.init(); auto aged = script.invoke("ageItem", original.toJson(), aging).apply(construct()); script.uninit(); locker.unlock(); if (aged && *aged != original) { item = ItemDatabase::item(*aged); return true; } return false; } List ItemDatabase::allItems() const { return m_items.keys(); } ItemPtr ItemDatabase::createItem(ItemType type, ItemConfig const& config) { if (type == ItemType::Generic) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::LiquidItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::MaterialItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::ObjectItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::CurrencyItem) { return make_shared(config.config, config.directory); } else if (type == ItemType::MiningTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::Flashlight) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::WireTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::BeamMiningTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::PaintingBeamTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::TillingTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::HarvestingTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::HeadArmor) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::ChestArmor) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::LegsArmor) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::BackArmor) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::Consumable) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::Blueprint) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::Codex) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::InspectionTool) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::InstrumentItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::ThrownItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::UnlockItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::ActiveItem) { return make_shared(config.config, config.directory, config.parameters); } else if (type == ItemType::AugmentItem) { return make_shared(config.config, config.directory, config.parameters); } else { throw ItemException(strf("Unknown item type {}", (int)type)); } } ItemPtr ItemDatabase::tryCreateItem(ItemDescriptor const& descriptor, Maybe level, Maybe seed) const { ItemPtr result; try { result = createItem(m_items.get(descriptor.name()).type, itemConfig(descriptor.name(), descriptor.parameters(), level, seed)); } catch (std::exception const& e) { Logger::error("Could not instantiate item '{}'. {}", descriptor, outputException(e, false)); result = createItem(m_items.get("perfectlygenericitem").type, itemConfig("perfectlygenericitem", JsonObject(), {}, {})); } result->setCount(descriptor.count()); return result; } ItemDatabase::ItemData const& ItemDatabase::itemData(String const& name) const { if (auto p = m_items.ptr(name)) return *p; throw ItemException::format("No such item '{}'", name); } ItemRecipe ItemDatabase::makeRecipe(List inputs, ItemDescriptor output, float duration, StringSet groups) const { ItemRecipe res; res.inputs = move(inputs); res.output = move(output); res.duration = duration; res.groups = move(groups); if (auto item = ItemDatabase::itemShared(res.output)) { res.outputRarity = item->rarity(); res.guiFilterString = guiFilterString(item); } return res; } void ItemDatabase::addItemSet(ItemType type, String const& extension) { auto assets = Root::singleton().assets(); for (auto file : assets->scanExtension(extension)) { ItemData data; try { auto config = assets->json(file); data.type = type; data.assetsConfig = file; data.name = config.get("itemName").toString(); data.friendlyName = config.getString("shortdescription", {}); data.itemTags = config.opt("itemTags").apply(jsonToStringSet).value(); data.agingScripts = config.opt("itemAgingScripts").apply(jsonToStringList).value(); data.directory = AssetPath::directory(file); data.agingScripts = data.agingScripts.transformed(bind(&AssetPath::relativeTo, data.directory, _1)); } catch (std::exception const& e) { throw ItemException(strf("Could not load item asset {}", file), e); } if (m_items.contains(data.name)) throw ItemException(strf("Duplicate item name '{}' found", data.name)); m_items[data.name] = data; } } void ItemDatabase::addObjectDropItem(String const& objectPath, Json const& objectConfig) { auto assets = Root::singleton().assets(); ItemData data; data.type = ItemType::ObjectItem; data.name = objectConfig.get("objectName").toString(); data.friendlyName = objectConfig.getString("shortdescription", {}); data.itemTags = objectConfig.opt("itemTags").apply(jsonToStringSet).value(); data.agingScripts = objectConfig.opt("itemAgingScripts").apply(jsonToStringList).value(); data.directory = AssetPath::directory(objectPath); JsonObject customConfig = objectConfig.toObject(); if (!customConfig.contains("inventoryIcon")) { customConfig["inventoryIcon"] = assets->json("/objects/defaultParameters.config:missingIcon"); Logger::warn(strf("Missing inventoryIcon for {}, using default", data.name).c_str()); } customConfig["itemName"] = data.name; if (!customConfig.contains("tooltipKind")) customConfig["tooltipKind"] = "object"; if (!customConfig.contains("printable")) customConfig["printable"] = customConfig.contains("price"); // Don't inherit object scripts. this is kind of a crappy solution to prevent // ObjectItems (which are firable and therefore scripted) from trying to // execute scripts intended for objects customConfig.remove("scripts"); data.customConfig = move(customConfig); if (m_items.contains(data.name)) throw ItemException(strf("Object drop '{}' shares name with existing item", data.name)); m_items[data.name] = move(data); } void ItemDatabase::scanItems() { auto assets = Root::singleton().assets(); List> itemSets; auto scanItemType = [&itemSets, assets](ItemType type, String const& extension) { itemSets.append(make_pair(type, extension)); assets->queueJsons(assets->scanExtension(extension)); }; scanItemType(ItemType::Generic, "item"); scanItemType(ItemType::LiquidItem, "liqitem"); scanItemType(ItemType::MaterialItem, "matitem"); scanItemType(ItemType::MiningTool, "miningtool"); scanItemType(ItemType::Flashlight, "flashlight"); scanItemType(ItemType::WireTool, "wiretool"); scanItemType(ItemType::BeamMiningTool, "beamaxe"); scanItemType(ItemType::TillingTool, "tillingtool"); scanItemType(ItemType::PaintingBeamTool, "painttool"); scanItemType(ItemType::HarvestingTool, "harvestingtool"); scanItemType(ItemType::HeadArmor, "head"); scanItemType(ItemType::ChestArmor, "chest"); scanItemType(ItemType::LegsArmor, "legs"); scanItemType(ItemType::BackArmor, "back"); scanItemType(ItemType::CurrencyItem, "currency"); scanItemType(ItemType::Consumable, "consumable"); scanItemType(ItemType::Blueprint, "blueprint"); scanItemType(ItemType::InspectionTool, "inspectiontool"); scanItemType(ItemType::InstrumentItem, "instrument"); scanItemType(ItemType::ThrownItem, "thrownitem"); scanItemType(ItemType::UnlockItem, "unlock"); scanItemType(ItemType::ActiveItem, "activeitem"); scanItemType(ItemType::AugmentItem, "augment"); for (auto const& itemset : itemSets) addItemSet(itemset.first, itemset.second); } void ItemDatabase::addObjectItems() { auto objectDatabase = Root::singleton().objectDatabase(); for (auto const& objectName : objectDatabase->allObjects()) { auto objectConfig = objectDatabase->getConfig(objectName); if (objectConfig->hasObjectItem) addObjectDropItem(objectConfig->path, objectConfig->config); } } void ItemDatabase::scanRecipes() { auto assets = Root::singleton().assets(); auto files = assets->scanExtension("recipe"); assets->queueJsons(files); for (auto file : files) { try { m_recipes.add(parseRecipe(assets->json(file))); } catch (std::exception const& e) { Logger::error("Could not load recipe {}: {}", file, outputException(e, false)); } } } void ItemDatabase::addBlueprints() { auto assets = Root::singleton().assets(); for (auto const& recipe : m_recipes) { auto baseDesc = recipe.output; auto baseItem = itemShared(baseDesc); String blueprintName = strf("{}-recipe", baseItem->name()); if (m_items.contains(blueprintName)) continue; try { ItemData blueprintData; blueprintData.type = ItemType::Blueprint; JsonObject configInfo; configInfo["recipe"] = baseDesc.singular().toJson(); String description = assets->json("/blueprint.config:description").toString(); description = description.replace("", baseItem->friendlyName()); configInfo["description"] = Json(description); String shortDesc = assets->json("/blueprint.config:shortdescription").toString(); shortDesc = shortDesc.replace("", baseItem->friendlyName()); configInfo["shortdescription"] = Json(shortDesc); configInfo["category"] = assets->json("/blueprint.config:category").toString(); blueprintData.name = blueprintName; blueprintData.friendlyName = shortDesc; configInfo["itemName"] = blueprintData.name; if (baseItem->instanceValue("inventoryIcon", false)) configInfo["inventoryIcon"] = baseItem->instanceValue("inventoryIcon"); configInfo["rarity"] = RarityNames.getRight(baseItem->rarity()); configInfo["price"] = baseItem->price(); blueprintData.customConfig = move(configInfo); blueprintData.directory = itemData(baseDesc.name()).directory; m_items[blueprintData.name] = blueprintData; } catch (std::exception const& e) { Logger::error("Could not create blueprint item from recipe: {}", outputException(e, false)); } } } void ItemDatabase::addCodexes() { auto assets = Root::singleton().assets(); auto codexConfig = assets->json("/codex.config"); auto codexDatabase = Root::singleton().codexDatabase(); for (auto const& codexPair : codexDatabase->codexes()) { String codexItemName = strf("{}-codex", codexPair.second->id()); if (m_items.contains(codexItemName)) { Logger::warn("Couldn't create codex item {} because an item with that name is already defined", codexItemName); continue; } try { ItemData codexItemData; codexItemData.type = ItemType::Codex; codexItemData.name = codexItemName; codexItemData.friendlyName = codexPair.second->title(); codexItemData.directory = codexPair.second->directory(); auto customConfig = jsonMerge(codexConfig.get("defaultItemConfig"), codexPair.second->itemConfig()).toObject(); customConfig["itemName"] = codexItemName; customConfig["codexId"] = codexPair.second->id(); customConfig["shortdescription"] = codexPair.second->title(); customConfig["description"] = codexPair.second->description(); customConfig["codexIcon"] = codexPair.second->icon(); codexItemData.customConfig = customConfig; m_items[codexItemName] = codexItemData; } catch (std::exception const& e) { Logger::error("Could not create item for codex {}: {}", codexPair.second->id(), outputException(e, false)); } } } }