diff --git a/core/src/DataPack.cpp b/core/src/DataPack.cpp index 25bf783..9c3cf91 100644 --- a/core/src/DataPack.cpp +++ b/core/src/DataPack.cpp @@ -316,3 +316,161 @@ void TagRegistry::clear() { tags.clear(); } bool TagRegistry::has(const ResourceId& id) const { return tags.count(id) > 0u; } +const BlockTag* TagRegistry::find(const ResourceId& id) const { + auto found = tags.find(id); + return found == tags.end() ? nullptr : &found->second; +} + +std::vector TagRegistry::ids() const { + std::vector out; + out.reserve(tags.size()); + for (const auto& entry : tags) out.push_back(entry.first); + return out; +} + +namespace { + +bool expandTag(const TagRegistry& registry, const ResourceId& id, std::vector& out, + std::vector& path, std::vector& expanded, + std::string& problem) { + for (const ResourceId& onPath : path) { + if (onPath == id) { + problem = "tag cycle through " + id.text(); + return false; + } + } + for (const ResourceId& done : expanded) + if (done == id) return true; + + const BlockTag* tag = registry.find(id); + if (tag == nullptr) { + problem = "unknown tag " + id.text(); + return false; + } + path.push_back(id); + for (const BlockTag::Entry& entry : tag->entries) { + if (entry.reference) { + if (!expandTag(registry, entry.id, out, path, expanded, problem)) return false; + continue; + } + bool already = false; + for (const ResourceId& present : out) + if (present == entry.id) already = true; + if (!already) out.push_back(entry.id); + } + path.pop_back(); + expanded.push_back(id); + return true; +} + +} // namespace + +bool TagRegistry::resolve(const ResourceId& id, std::vector& out, + std::string& problem) const { + out.clear(); + std::vector path; + std::vector expanded; + if (!expandTag(*this, id, out, path, expanded, problem)) { + out.clear(); + return false; + } + return true; +} + +bool TagRegistry::resolve(const ResourceId& id, std::vector& out) const { + std::string ignored; + return resolve(id, out, ignored); +} + +bool TagRegistry::contains(const ResourceId& id, const ResourceId& member) const { + std::vector members; + if (!resolve(id, members)) return false; + for (const ResourceId& present : members) + if (present == member) return true; + return false; +} + +bool TagRegistry::hasCycle(const ResourceId& id) const { + std::vector members; + std::string problem; + if (resolve(id, members, problem)) return false; + return problem.compare(0, 10, "tag cycle ") == 0; +} + +bool parseRecipeJson(const JsonValue& root, const std::string& defaultSpace, RecipeData& out, + std::string& problem) { + if (!root.isObject()) { + problem = "a recipe must be an object"; + return false; + } + const JsonValue* type = root.find("type"); + if (type == nullptr || !type->isString()) { + problem = "\"type\" must be one of craftingShaped, craftingShapeless, smelting"; + return false; + } + const JsonValue* result = root.find("result"); + if (result == nullptr || !result->isObject()) { + problem = "\"result\" must be an object"; + return false; + } + RecipeData parsed; + if (!readItemField(*result, "item", defaultSpace, parsed.resultItem, problem)) return false; + parsed.resultCount = static_cast(result->intOr("count", 1)); + if (parsed.resultCount < 1 || parsed.resultCount > maxStackSize(parsed.resultItem)) { + problem = "result count out of range for " + result->stringOr("item", ""); + return false; + } + + const std::string& kindName = type->stringValue(); + if (kindName == "craftingShaped") { + parsed.kind = RecipeData::Kind::Shaped; + const JsonValue* pattern = root.find("pattern"); + const JsonValue* key = root.find("key"); + if (pattern == nullptr || !pattern->isArray() || pattern->elements().empty()) { + problem = "\"pattern\" must be a non empty array of rows"; + return false; + } + if (key == nullptr || !key->isObject()) { + problem = "\"key\" must be an object mapping pattern characters to items"; + return false; + } + if (pattern->elements().size() > static_cast(craftGridHeight)) { + problem = "\"pattern\" is taller than the crafting grid"; + return false; + } + parsed.height = static_cast(pattern->elements().size()); + parsed.width = -1; + parsed.pattern.fill(ItemId::None); + for (int row = 0; row < parsed.height; ++row) { + const JsonValue& line = pattern->elements()[static_cast(row)]; + if (!line.isString()) { + problem = "\"pattern\" rows must be strings"; + return false; + } + const std::string& cells = line.stringValue(); + if (cells.empty() || cells.size() > static_cast(craftGridWidth)) { + problem = "\"pattern\" row width must be 1 to 3 characters"; + return false; + } + if (parsed.width < 0) parsed.width = static_cast(cells.size()); + if (parsed.width != static_cast(cells.size())) { + problem = "\"pattern\" rows are not all the same width"; + return false; + } + for (int column = 0; column < parsed.width; ++column) { + char symbol = cells[static_cast(column)]; + if (symbol == ' ') continue; + ItemId ingredient = ItemId::None; + if (!readItemField(*key, std::string(1, symbol), defaultSpace, ingredient, + problem)) { + problem = "pattern character '" + std::string(1, symbol) + "': " + problem; + return false; + } + // Packed by the recipe's own width, which is how matchesShaped + // in Crafting.cpp indexes a pattern. + parsed.pattern[static_cast(row * parsed.width + column)] = ingredient; + } + } + parsed.allowMirror = root.boolOr("mirror", true); + } else if (kindName == "craftingShapeless") { + parsed.kind = RecipeData::Kind::Shapeless;