diff --git a/core/include/vf/Advancement.h b/core/include/vf/Advancement.h new file mode 100644 index 0000000..18b23d7 --- /dev/null +++ b/core/include/vf/Advancement.h @@ -0,0 +1,193 @@ +#pragma once + +#include +#include +#include +#include + +#include "Block.h" +#include "Brewing.h" +#include "DataPack.h" +#include "Dimension.h" +#include "Item.h" +#include "Mob.h" +#include "Villager.h" + +namespace vf { + +// An advancement is addressed the same way as every other loaded resource. +using AdvancementId = ResourceId; + +// One condition inside an advancement, fed by gameplay events. A criterion only +// compares the fields its matchMask names, so "kill any mob" and "kill five +// creepers" are the same structure with different masks and counts. +struct AdvancementCriterion { + enum class Trigger : uint8_t { + ItemObtained, + MobKilled, + DimensionEntered, + PotionBrewed, + VillagerTraded, + BlockBroken, + Count + }; + + static constexpr uint32_t MatchItem = 1u << 0; + static constexpr uint32_t MatchBlock = 1u << 1; + static constexpr uint32_t MatchSpecies = 1u << 2; + static constexpr uint32_t MatchDimension = 1u << 3; + static constexpr uint32_t MatchPotion = 1u << 4; + static constexpr uint32_t MatchProfession = 1u << 5; + static constexpr uint32_t MatchTool = 1u << 6; + + // One thing that happened in the world, as the simulation reports it. + struct Event { + Trigger trigger = Trigger::ItemObtained; + ItemId item = ItemId::None; + Block block = Block::Air; + Species species = Species::Count; + DimensionId dimension = DimensionId::Overworld; + PotionKind potion = PotionKind::Count; + VillagerProfession profession = VillagerProfession::Unemployed; + ItemId tool = ItemId::None; + // How much this one event contributes to a counted criterion. + int amount = 1; + + static Event itemObtained(ItemId item, int amount = 1); + static Event mobKilled(Species species); + static Event dimensionEntered(DimensionId dimension); + static Event potionBrewed(PotionKind potion); + static Event villagerTraded(VillagerProfession profession, ItemId purchased); + static Event blockBroken(Block block, ItemId tool); + }; + + std::string name; + Trigger trigger = Trigger::ItemObtained; + uint32_t matchMask = 0u; + ItemId item = ItemId::None; + Block block = Block::Air; + Species species = Species::Count; + DimensionId dimension = DimensionId::Overworld; + PotionKind potion = PotionKind::Count; + VillagerProfession profession = VillagerProfession::Unemployed; + ItemId tool = ItemId::None; + int requiredCount = 1; + + bool accepts(const Event& event) const; +}; + +struct AdvancementReward { + int experience = 0; + std::vector recipes; + std::vector lootTables; + + bool empty() const { + return experience == 0 && recipes.empty() && lootTables.empty(); + } +}; + +// One node of the tree. An empty parent path marks a root. +// +// requirements is a list of groups: the advancement is granted once every group +// has at least one completed criterion, which covers both "all of them" (one +// group per criterion, or an empty list) and "any of these" (one group holding +// several names). An empty requirements list means every criterion is needed. +struct Advancement { + AdvancementId id; + AdvancementId parent; + std::string title; + std::string description; + // Stays out of sight until the parent is granted. + bool hidden = false; + std::vector criteria; + std::vector> requirements; + AdvancementReward reward; + + bool isRoot() const { return parent.path.empty(); } + const AdvancementCriterion* findCriterion(const std::string& criterionName) const; + bool satisfiedBy(const std::vector& completed) const; + // Names used in requirements that no criterion declares. + std::vector danglingRequirements() const; +}; + +// Every advancement of a world, indexed by id, plus the parent links between +// them. The tree tolerates a missing or looping parent: it reports both rather +// than refusing to hold the node. +class AdvancementTree { +public: + bool add(const Advancement& advancement); + void clear(); + + const Advancement* find(const AdvancementId& id) const; + std::size_t size() const { return nodes.size(); } + std::vector ids() const; + std::vector roots() const; + std::vector childrenOf(const AdvancementId& id) const; + + // 0 for a root, -1 when the chain runs into a cycle or a missing parent. + int depthOf(const AdvancementId& id) const; + bool cycleFrom(const AdvancementId& id) const; + bool hasCycle() const; + // Ids naming a parent that was never loaded. + std::vector missingParents() const; + +private: + std::map nodes; +}; + +// One player's progress against one tree. Time is the caller's logical tick, so +// replaying the same events in the same order always yields the same record. +class AdvancementProgress { +public: + explicit AdvancementProgress(const AdvancementTree& tree) : source(&tree) {} + + // Feeds one event to every criterion that accepts it and returns the + // advancements this event completed, in id order. + std::vector notify(const AdvancementCriterion::Event& event, long long tick); + + bool criterionDone(const AdvancementId& id, const std::string& criterion) const; + int criterionProgress(const AdvancementId& id, const std::string& criterion) const; + // Tick the criterion completed on, -1 while it is still open. + long long criterionTick(const AdvancementId& id, const std::string& criterion) const; + std::vector completedCriteria(const AdvancementId& id) const; + + bool granted(const AdvancementId& id) const; + long long grantedTick(const AdvancementId& id) const; + // Hidden advancements stay invisible until their parent is granted. + bool visible(const AdvancementId& id) const; + + std::vector grantedIds() const; + int grantedCount() const { return static_cast(grants.size()); } + int totalExperience() const { return experience; } + const std::vector& unlockedRecipes() const { return recipes; } + const std::vector& awardedLootTables() const { return lootTables; } + + const AdvancementTree& tree() const { return *source; } + +private: + struct CriterionState { + int progress = 0; + bool done = false; + long long tick = -1; + }; + + const AdvancementTree* source; + std::map> states; + std::map grants; + std::vector recipes; + std::vector lootTables; + int experience = 0; +}; + +const char* advancementTriggerName(AdvancementCriterion::Trigger trigger); +bool parseAdvancementTriggerName(const std::string& text, AdvancementCriterion::Trigger& out); + +bool parseAdvancementJson(const JsonValue& root, const std::string& defaultSpace, Advancement& out, + std::string& problem); + +// Reads every document the loader kept under data//advancements/. A file +// that fails to read costs that advancement and appends one error; the rest of +// the tree still loads. +AdvancementTree loadAdvancements(const DataPackLoader& loader, std::vector& errors); + +} // namespace vf