进度

进度

进度是玩家可以完成的任务式目标,按指定条件授予,并可在完成时执行操作。

在命名空间的 advancement 子目录创建 JSON 即可添加。例如,Mod ID 为 examplemod、名为 example_name 的进度位于 data/examplemod/advancement/example_name.json,其 ID 相对于 advancement 目录,为 examplemod:example_name。名称可自由选择,游戏会自动加载。只有添加新条件或从代码触发条件时才需要 Java(见下文)。

规范

进度 JSON 可包含:

  • parent:父进度 ID。循环引用会导致加载失败。可选,省略时为进度树的根进度。
  • display:界面显示属性,可选。省略时不可见,但仍能触发。
    • icon:物品堆 JSON。
    • text:标题文本组件。
    • description:描述文本组件。
    • frame:challenge、goal 或 task,默认 task。
    • background:树背景纹理,必须包含 textures/ 前缀;默认缺失纹理,仅对根进度有效。
    • show_toast:完成时是否在右上角提示,默认 true。
    • announce_to_chat:是否在聊天中公告,默认 true。
    • hidden:完成前是否隐藏此进度及子进度,默认 false。不隐藏根进度本身,但仍隐藏其子进度。
  • criteria:条件名称到所跟踪条件的映射。原版触发器见 CriteriaTriggers,JSON 规范见 Minecraft Wiki。自定义与代码触发方式见下文。
  • requirements:列表的列表,内层执行 OR,外层执行 AND,即每个子列表至少满足一项。可选,默认要求全部条件。
  • rewards:可选奖励对象,其中所有值也可选。
    • experience:经验数量。
    • recipes:解锁的配方 ID 列表。
    • loot:抽取并给予玩家的战利品表列表。
    • function:运行的函数。要运行多个函数,可用一个包装函数依次调用。
  • sends_telemetry_event:是否在完成时收集遥测,仅 minecraft 命名空间有效,默认 false。
  • neoforge:conditions:NeoForge 添加,可选加载条件列表,全部通过才加载。

进度树

可用目录对进度文件分组,形成多个进度标签页。每个标签页可根据根进度数量包含一棵或多棵树。空标签页会自动隐藏。

建议 原版每个标签页仅有一个根进度,并始终命名为 root,建议遵循此惯例。

条件触发器

解锁进度需要满足条件。条件由触发器跟踪,在相关行为发生时由代码调用,例如 player_killed_entity。加载进度时,条件会被读取并作为监听器添加到触发器。触发时,关联的全部进度重新检查是否完成,完成后移除监听器。

自定义触发器有两部分:通过 #trigger 激活的触发器,以及规定授予条件的实例。前者继承 SimpleCriterionTrigger<T>,后者实现 SimpleCriterionTrigger.SimpleInstance,泛型 T 为实例类型。

SimpleCriterionTrigger.SimpleInstance

实例代表 criteria 中的单个条件,保存条件定义并判断输入是否符合。条件通常通过构造函数传入。接口只要求 #player 方法,返回玩家必须满足的 Optional<ContextAwarePredicate>。如果 record 有相同类型的 player 参数,自动生成的方法即可满足:

public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, other parameters here*/)
        implements SimpleCriterionTrigger.SimpleInstance {}

实例通常提供可选的静态辅助方法,用实例参数构建完整的 Criterion<T>,方便数据生成:

// In this example, EXAMPLE_TRIGGER is a DeferredHolder<CriterionTrigger<?>, ExampleTrigger>.
// See below for how to register triggers.
public static Criterion<ExampleTriggerInstance> instance(ContextAwarePredicate player, ItemPredicate item) {
    return EXAMPLE_TRIGGER.get().createCriterion(new ExampleTriggerInstance(Optional.of(player), item));
}

最后添加接收当前数据状态并判断条件的方法,通常称为 #matches。玩家条件已由 SimpleCriterionTrigger#trigger(ServerPlayer, Predicate) 检查:

// Let's assume we have an additional ItemPredicate parameter. This can be whatever you need.
// For example, this could also be a Predicate<LivingEntity>.
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player, ItemPredicate predicate)
        implements SimpleCriterionTrigger.SimpleInstance {
    // This method is unique for each instance and is as such not overridden.
    // The parameter may be whatever you need to properly match, for example, this could also be a LivingEntity.
    // If you need no context other than the player, this may also take no parameters at all.
    public boolean matches(ItemStack stack) {
        // Since ItemPredicate matches a stack, we use a stack as the input here.
        return this.predicate.test(stack);
    }
}

SimpleCriterionTrigger

SimpleCriterionTrigger<T> 负责提供检查实例、成功时运行监听器的方法,以及指定序列化实例 T 的 codec。

先添加接收所需输入并调用 SimpleCriterionTrigger#trigger 的方法,通常也称 #trigger:

public class ExampleCriterionTrigger extends SimpleCriterionTrigger<ExampleTriggerInstance> {
    // This method is unique for each trigger and is as such not a method to override
    public void trigger(ServerPlayer player, ItemStack stack) {
        this.trigger(player,
                // The condition checker method within the SimpleCriterionTrigger.SimpleInstance subclass
                triggerInstance -> triggerInstance.matches(stack)
        );
    }
}

触发器必须注册到 Registries.TRIGGER_TYPE 注册表:

public static final DeferredRegister<CriterionTrigger<?>> TRIGGER_TYPES =
        DeferredRegister.create(Registries.TRIGGER_TYPE, ExampleMod.MOD_ID);

public static final Supplier<ExampleCriterionTrigger> EXAMPLE_TRIGGER =
        TRIGGER_TYPES.register("example", ExampleCriterionTrigger::new);

然后重写 #codec,提供实例的编解码器,通常在实例实现中定义为常量:

public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, other parameters here*/)
        implements SimpleCriterionTrigger.SimpleInstance {
    public static final Codec<ExampleTriggerInstance> CODEC = ...;

    // ...
}

public class ExampleTrigger extends SimpleCriterionTrigger<ExampleTriggerInstance> {
    @Override
    public Codec<ExampleTriggerInstance> codec() {
        return ExampleTriggerInstance.CODEC;
    }

    // ...
}

对前述 ContextAwarePredicate 与 ItemPredicate 的 record,可使用:

public static final Codec<ExampleTriggerInstace> CODEC = RecordCodecBuilder.create(instance -> instance.group(
        EntityPredicate.ADVANCEMENT_CODEC.optionalFieldOf("player").forGetter(ExampleTriggerInstance::player),
        ItemPredicate.CODEC.fieldOf("item").forGetter(ExampleTriggerInstance::item)
).apply(instance, ExampleTriggerInstance::new));

调用触发器

每当目标行为发生,就应调用自定义子类的 #trigger。也可调用 CriteriaTriggers 中的原版触发器。

// In some piece of code where the action is being performed
// Again, EXAMPLE_TRIGGER is a supplier for the registered instance of the custom criterion trigger
public void performExampleAction(ServerPlayer player, additionalContextParametersHere) {
    // Run code to perform action here
    EXAMPLE_TRIGGER.get().trigger(player, additionalContextParametersHere);
}

数据生成

通过 AdvancementProvider 可生成进度。它接受 AdvancementGenerator 列表,由这些生成器使用 Advancement.Builder 实际构建。

警告 Minecraft 和 NeoForge 都有 AdvancementProvider,分别位于 net.minecraft.data.advancements 与 net.neoforged.neoforge.common.data。NeoForge 版是改进实现,应始终优先使用。以下均指 NeoForge 版。

先创建子类:

public class MyAdvancementProvider extends AdvancementProvider {
    // Parameters can be obtained from GatherDataEvent.
    public MyAdvancementProvider(PackOutput output,
            CompletableFuture<HolderLookup.Provider> lookupProvider, ExistingFileHelper existingFileHelper) {
        super(output, lookupProvider, existingFileHelper, List.of());
    }
}

再将一个或多个生成器作为静态内部类添加,并把各实例放入构造函数原本为空的列表:

public class MyAdvancementProvider extends AdvancementProvider {
    public MyAdvancementProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider, ExistingFileHelper existingFileHelper) {
        // Add an instance of our generator to the list parameter. This can be done as many times as you want.
        // Having multiple generators is purely for organization, all functionality can be achieved with a single generator.
        super(output, lookupProvider, existingFileHelper, List.of(new MyAdvancementGenerator()));
    }

    private static final class MyAdvancementGenerator implements AdvancementProvider.AdvancementGenerator {
        @Override
        public void generate(HolderLookup.Provider registries, Consumer<AdvancementHolder> saver, ExistingFileHelper existingFileHelper) {
            // Generate your advancements here.
        }
    }
}

使用 Advancement.Builder 生成进度:

// All methods follow the builder pattern, meaning that chaining is possible and encouraged.
// For better readability of the explanations, chaining will not be done here.

// Create an advancement builder using the static #advancement() method.
// Using #advancement() automatically enables telemetry events. If you do not want this,
// #recipeAdvancement() can be used instead, there are no other functional differences.
Advancement.Builder builder = Advancement.Builder.advancement();

// Sets the parent of the advancement. You can use another advancement you have already generated,
// or create a placeholder advancement using the static AdvancementSubProvider#createPlaceholder method.
builder.parent(AdvancementSubProvider.createPlaceholder("minecraft:story/root"));

// Sets the display properties of the advancement. This can either be a DisplayInfo object,
// or pass in the values directly. If values are passed in directly, a DisplayInfo object will be created for you.
builder.display(
        // The advancement icon. Can be an ItemStack or an ItemLike.
        new ItemStack(Items.GRASS_BLOCK),
        // The advancement title and description. Don't forget to add translations for these!
        Component.translatable("advancements.examplemod.example_advancement.title"),
        Component.translatable("advancements.examplemod.example_advancement.description"),
        // The background texture. Use null if you don't want a background texture (for non-root advancements).
        null,
        // The frame type. Valid values are AdvancementType.TASK, CHALLENGE, or GOAL.
        AdvancementType.GOAL,
        // Whether to show the advancement toast or not.
        true,
        // Whether to announce the advancement into chat or not.
        true,
        // Whether the advancement should be hidden or not.
        false
);

// An advancement reward builder. Can be created with any of the four reward types, and further rewards
// can be added using the methods prefixed with add. This can also be built beforehand,
// and the resulting AdvancementRewards can then be reused across multiple advancement builders.
builder.rewards(
    // Alternatively, use addExperience() to add to an existing builder.
    AdvancementRewards.Builder.experience(100)
    // Alternatively, use loot() to create a new builder.
    .addLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.fromNamespaceAndPath("minecraft", "chests/igloo")))
    // Alternatively, use recipe() to create a new builder.
    .addRecipe(ResourceLocation.fromNamespaceAndPath("minecraft", "iron_ingot"))
    // Alternatively, use function() to create a new builder.
    .runs(ResourceLocation.fromNamespaceAndPath("examplemod", "example_function"))
);

// Adds a criterion with the given name to the advancement. Use the corresponding trigger instance's static method.
builder.addCriterion("pickup_dirt", InventoryChangeTrigger.TriggerInstance.hasItems(Items.DIRT));

// Adds a requirements handler. Minecraft natively provides allOf() and anyOf(), more complex requirements
// must be implemented manually. Only has an effect with two or more criteria.
builder.requirements(AdvancementRequirements.allOf(List.of("pickup_dirt")));

// Save the advancement to disk, using the given resource location. This returns an AdvancementHolder,
// which may be stored in a variable and used as a parent by other advancement builders.
builder.save(saver, ResourceLocation.fromNamespaceAndPath("examplemod", "example_advancement"), existingFileHelper);

最后在 GatherDataEvent 中添加提供器:

@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent event) {
    DataGenerator generator = event.getGenerator();
    PackOutput output = generator.getPackOutput();
    CompletableFuture<HolderLookup.Provider> lookupProvider = event.getLookupProvider();
    ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

    // other providers here
    generator.addProvider(
            event.includeServer(),
            new MyAdvancementProvider(output, lookupProvider, existingFileHelper)
    );
}

来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交 4a680cdd4d15。