From c2493cb50ac50d324195420ea578dc5c1a201fbc Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 13:20:04 +0200 Subject: [PATCH] feat: add parseLootTableJson and parseTagJson to DataPack --- core/src/DataPack.cpp | 160 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/core/src/DataPack.cpp b/core/src/DataPack.cpp index 9c3cf91..fe10d71 100644 --- a/core/src/DataPack.cpp +++ b/core/src/DataPack.cpp @@ -474,3 +474,163 @@ bool parseRecipeJson(const JsonValue& root, const std::string& defaultSpace, Rec parsed.allowMirror = root.boolOr("mirror", true); } else if (kindName == "craftingShapeless") { parsed.kind = RecipeData::Kind::Shapeless; + const JsonValue* ingredients = root.find("ingredients"); + if (ingredients == nullptr || !ingredients->isArray() || ingredients->elements().empty()) { + problem = "\"ingredients\" must be a non empty array"; + return false; + } + if (ingredients->elements().size() > static_cast(craftGridCells)) { + problem = "\"ingredients\" holds more than the grid can take"; + return false; + } + for (const JsonValue& element : ingredients->elements()) { + if (!element.isString()) { + problem = "\"ingredients\" holds a non string entry"; + return false; + } + ResourceId id; + ItemId ingredient = ItemId::None; + if (!ResourceId::parse(element.stringValue(), defaultSpace, id) || + !resolveItemId(id, ingredient)) { + problem = "no such item: " + element.stringValue(); + return false; + } + parsed.ingredients.push_back(ingredient); + } + } else if (kindName == "smelting") { + parsed.kind = RecipeData::Kind::Smelting; + if (!readItemField(root, "ingredient", defaultSpace, parsed.smeltInput, problem)) + return false; + parsed.smeltSeconds = static_cast(root.numberOr("seconds", 10.0)); + parsed.experience = static_cast(root.numberOr("experience", 0.0)); + if (parsed.smeltSeconds <= 0.0f) { + problem = "\"seconds\" must be above zero"; + return false; + } + if (parsed.experience < 0.0f) { + problem = "\"experience\" cannot be negative"; + return false; + } + } else { + problem = "unknown recipe type: " + kindName; + return false; + } + out = parsed; + return true; +} + +bool parseLootTableJson(const JsonValue& root, const std::string& defaultSpace, LootTable& out, + std::string& problem) { + if (!root.isObject()) { + problem = "a loot table must be an object"; + return false; + } + const JsonValue* pools = root.find("pools"); + if (pools == nullptr || !pools->isArray() || pools->elements().empty()) { + problem = "\"pools\" must be a non empty array"; + return false; + } + LootTable parsed; + for (const JsonValue& poolJson : pools->elements()) { + if (!poolJson.isObject()) { + problem = "every pool must be an object"; + return false; + } + LootPool pool; + if (!readCountRange(poolJson, "rolls", pool.minRolls, pool.maxRolls, problem)) return false; + pool.bonusRollsPerLooting = static_cast(poolJson.intOr("bonusRollsPerLooting", 0)); + if (pool.bonusRollsPerLooting < 0) { + problem = "\"bonusRollsPerLooting\" cannot be negative"; + return false; + } + const JsonValue* entries = poolJson.find("entries"); + if (entries == nullptr || !entries->isArray() || entries->elements().empty()) { + problem = "every pool needs a non empty \"entries\" array"; + return false; + } + for (const JsonValue& entryJson : entries->elements()) { + if (!entryJson.isObject()) { + problem = "every loot entry must be an object"; + return false; + } + LootEntry entry; + if (!readItemField(entryJson, "item", defaultSpace, entry.item, problem)) return false; + entry.weight = static_cast(entryJson.intOr("weight", 1)); + if (entry.weight < 1) { + problem = "\"weight\" must be at least 1"; + return false; + } + if (!readCountRange(entryJson, "count", entry.minCount, entry.maxCount, problem)) + return false; + // An authored range is meant to be rolled; the roller only does that + // when the CountRange function is on the entry. + if (entry.maxCount > entry.minCount) entry.functions |= LootEntry::CountRange; + entry.chance = static_cast(entryJson.numberOr("chance", 1.0)); + entry.chancePerLevel = static_cast(entryJson.numberOr("chancePerLevel", 0.0)); + if (entry.chance < 0.0f || entry.chance > 1.0f) { + problem = "\"chance\" must sit between 0 and 1"; + return false; + } + if (!readFlagList(entryJson, "conditions", lootConditionFlags, + sizeof(lootConditionFlags) / sizeof(lootConditionFlags[0]), + entry.conditions, problem)) + return false; + if (!readFlagList(entryJson, "functions", lootFunctionFlags, + sizeof(lootFunctionFlags) / sizeof(lootFunctionFlags[0]), + entry.functions, problem)) + return false; + pool.entries.push_back(entry); + } + parsed.pools.push_back(pool); + } + out = parsed; + return true; +} + +bool parseTagJson(const JsonValue& root, const std::string& defaultSpace, BlockTag& out, + std::string& problem) { + if (!root.isObject()) { + problem = "a tag must be an object"; + return false; + } + const JsonValue* values = root.find("values"); + if (values == nullptr || !values->isArray()) { + problem = "\"values\" must be an array"; + return false; + } + BlockTag parsed; + parsed.replace = root.boolOr("replace", false); + for (const JsonValue& element : values->elements()) { + if (!element.isString()) { + problem = "\"values\" holds a non string entry"; + return false; + } + BlockTag::Entry entry; + std::string text = element.stringValue(); + if (!text.empty() && text[0] == '#') { + entry.reference = true; + text = text.substr(1); + } + if (!ResourceId::parse(text, defaultSpace, entry.id)) { + problem = "not a valid resource id: " + element.stringValue(); + return false; + } + parsed.entries.push_back(entry); + } + out = parsed; + return true; +} + +void DataPackLoader::addPack(DataPack pack) { loaded.push_back(std::move(pack)); } + +void DataPackLoader::clear() { + loaded.clear(); + problems.clear(); + order.clear(); + folders.clear(); + recipeSet.clear(); + lootSet.clear(); + blockTagSet.clear(); + itemTagSet.clear(); +} +