From c87eafd189a8264063a2ff9a9924ec0782e07f03 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 15:20:10 +0200 Subject: [PATCH] feat(vf): add ResourceId and DataPackMeta --- core/include/vf/DataPack.h | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 core/include/vf/DataPack.h diff --git a/core/include/vf/DataPack.h b/core/include/vf/DataPack.h new file mode 100644 index 0000000..23303f1 --- /dev/null +++ b/core/include/vf/DataPack.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Crafting.h" +#include "Item.h" +#include "Json.h" +#include "Loot.h" + +namespace vf { + +// A namespaced identifier, "voxelforge:iron_ingot". Both halves are lowercase +// by construction: the space accepts [a-z0-9_.-] and the path additionally +// accepts '/', so one namespace can hold folders of resources. +struct ResourceId { + std::string space; + std::string path; + + ResourceId() = default; + ResourceId(std::string space, std::string path); + + // Accepts "space:path" or a bare "path", which takes defaultSpace. + static bool parse(const std::string& text, const std::string& defaultSpace, ResourceId& out); + static bool validSpace(const std::string& text); + static bool validPath(const std::string& text); + + bool valid() const { return validSpace(space) && validPath(path); } + bool empty() const { return space.empty() && path.empty(); } + std::string text() const { return space + ":" + path; } + + bool operator==(const ResourceId& other) const { + return space == other.space && path == other.path; + } + bool operator!=(const ResourceId& other) const { return !(*this == other); } + bool operator<(const ResourceId& other) const { + return space == other.space ? path < other.path : space < other.space; + } +}; + +// Maps a resource id onto the palettes the engine already has. The path is +// matched case insensitively with '_' and '-' removed, so "iron_ingot" finds +// ItemId::IronIngot through the shared name table in Command.cpp. +bool resolveItemId(const ResourceId& id, ItemId& out); +bool resolveBlockId(const ResourceId& id, Block& out); + +struct DataPackMeta { + std::string name; + std::string description; + int formatVersion = 1; + // Higher priority is applied later, so it overwrites what came before. + int priority = 0; + std::string defaultSpace = "voxelforge"; + + static bool fromJson(const JsonValue& root, DataPackMeta& out, std::string& problem); +}; + +// One thing that went wrong while loading, always naming the file it came from. +// A pack keeps loading past its own errors: one broken recipe costs that recipe +// and nothing else. +struct DataPackError { + std::string pack; + std::string file; + std::string reason; + int line = 0; + int column = 0; + + std::string text() const; +}; + +// A pack as a directory tree held in memory: a map from a slash separated path +// to the file's bytes. Nothing here ever touches a real filesystem. +// +// Layout, all below data//: +// recipes/.json shaped, shapeless or smelting +// lootTables/.json pools of entries, conditions and functions +// tags/blocks/.json block tag, entries may reference other tags +// tags/items/.json item tag +// advancements/.json read by Advancement.h +// plus an optional pack.json at the root carrying the metadata. +class DataPack { +public: + DataPack() = default; + explicit DataPack(DataPackMeta meta) : packMeta(std::move(meta)) {} + + const DataPackMeta& meta() const { return packMeta; } + DataPackMeta& meta() { return packMeta; } + + void addFile(const std::string& path, std::string contents); + bool removeFile(const std::string& path); + bool hasFile(const std::string& path) const; + const std::string* fileContents(const std::string& path) const; + + // Sorted, so loading order never depends on hashing. + std::vector filesUnder(const std::string& prefix) const; + std::vector allFiles() const; + std::size_t fileCount() const { return files.size(); } + +private: + DataPackMeta packMeta; + std::map files; +}; + +// A recipe read from data, in the shape the existing CraftingRegistry takes. +// The conversions below hand back the very structs Crafting.h declares, so a +// loaded recipe is matched by the same craftMatch as a built in one. +struct RecipeData { + enum class Kind : uint8_t { Shaped, Shapeless, Smelting }; + + ResourceId id; + Kind kind = Kind::Shaped; + + int width = 0; + int height = 0; + std::array pattern{}; + bool allowMirror = true; + + std::vector ingredients; + + ItemId smeltInput = ItemId::None; + float smeltSeconds = 10.0f; + float experience = 0.0f; + + ItemId resultItem = ItemId::None; + int resultCount = 1; + + ShapedRecipe toShaped() const; + ShapelessRecipe toShapeless() const; + SmeltingRecipe toSmelting() const; +}; + +// A tag: a named set of resource ids that may itself name other tags. The same +// type carries block tags and item tags; which palette an entry resolves +// against is decided by the registry it was loaded into. +struct BlockTag { + struct Entry { + ResourceId id; + // Written as "#space:path" in data: pull in every member of that tag. + bool reference = false; + + bool operator==(const Entry& other) const { + return id == other.id && reference == other.reference; + } + }; + + ResourceId id; + // false appends to whatever an earlier pack defined, true wipes it first. + bool replace = false; + std::vector entries; + + bool contains(const ResourceId& member) const; +}; +