feat: add expandTag and parseRecipeJson to DataPack

This commit is contained in:
dualfroz committed 2026-09-07 12:02:51 +02:00
1 parent 4423e6c2b3
commit 7991f6c660
1 file changed
+158
+158
View File
@@ -316,3 +316,161 @@ void TagRegistry::clear() { tags.clear(); }
bool TagRegistry::has(const ResourceId& id) const { return tags.count(id) > 0u; } 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<ResourceId> TagRegistry::ids() const {
std::vector<ResourceId> 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<ResourceId>& out,
std::vector<ResourceId>& path, std::vector<ResourceId>& 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<ResourceId>& out,
std::string& problem) const {
out.clear();
std::vector<ResourceId> path;
std::vector<ResourceId> expanded;
if (!expandTag(*this, id, out, path, expanded, problem)) {
out.clear();
return false;
}
return true;
}
bool TagRegistry::resolve(const ResourceId& id, std::vector<ResourceId>& out) const {
std::string ignored;
return resolve(id, out, ignored);
}
bool TagRegistry::contains(const ResourceId& id, const ResourceId& member) const {
std::vector<ResourceId> 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<ResourceId> 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<int>(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<std::size_t>(craftGridHeight)) {
problem = "\"pattern\" is taller than the crafting grid";
return false;
}
parsed.height = static_cast<int>(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<std::size_t>(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<std::size_t>(craftGridWidth)) {
problem = "\"pattern\" row width must be 1 to 3 characters";
return false;
}
if (parsed.width < 0) parsed.width = static_cast<int>(cells.size());
if (parsed.width != static_cast<int>(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<std::size_t>(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<std::size_t>(row * parsed.width + column)] = ingredient;
}
}
parsed.allowMirror = root.boolOr("mirror", true);
} else if (kindName == "craftingShapeless") {
parsed.kind = RecipeData::Kind::Shapeless;