Skip to content

进度

字数
3246 字
阅读时间
14 分钟

进度(Advancements)

进度是类似任务的目标,玩家可以完成这些目标。进度根据进度条件授予,并在完成时运行行为。

可以通过在命名空间的子文件夹中创建 JSON 文件来添加新的进度。例如,如果我们想为模组 ID 为examplemod的模组添加一个名为example_name的进度,它将位于data/examplemod/advancement/example_name.json。进度的 ID 将相对于advancement目录,因此对于我们的示例,它将是examplemod:example_name。可以选择任何名称,进度将自动被游戏识别。只有在你想添加新条件或从代码中触发特定条件时,才需要 Java 代码(见下文)。


规范

进度 JSON 文件可能包含以下条目:

  • parent:此进度的父进度 ID。循环引用将被检测并导致加载失败。可选;如果不存在,此进度将被视为根进度。根进度是没有设置父进度的进度,它们将成为其进度树的根。
  • display:包含用于在进度 GUI 中显示进度的多个属性的对象。可选;如果不存在,此进度将不可见,但仍可触发。
    • icon:物品堆叠的 JSON 表示。
    • text:用作进度标题的文本组件。
    • description:用作进度描述的文本组件。
    • frame:进度的框架类型。接受challengegoaltask。可选,默认为task
    • background:用于树背景的纹理。这不相对于textures目录,即必须包含textures/文件夹前缀。可选,默认为缺失纹理。仅在根进度上有效。
    • show_toast:是否在完成时在右上角显示提示。可选,默认为true
    • announce_to_chat:是否在聊天中宣布进度完成。可选,默认为true
    • hidden:是否在完成前隐藏此进度及其所有子进度。对根进度本身没有影响,但仍会隐藏其所有子进度。可选,默认为false
  • criteria:此进度应跟踪的条件映射。每个条件由其映射键标识。Minecraft 添加的条件触发器列表可以在CriteriaTriggers类中找到,JSON 规范可以在 Minecraft Wiki 上找到。有关实现你自己的条件或从代码中触发条件,请参阅下文。
  • requirements:确定需要哪些条件的列表。这是一个 OR 列表的列表,它们被 AND 在一起,换句话说,每个子列表必须至少有一个匹配的条件。可选,默认为所有条件都是必需的。
  • rewards:表示完成此进度时授予的奖励的对象。可选,对象的所有值也是可选的。
    • experience:授予玩家的经验值。
    • recipes:要解锁的配方 ID 列表。
    • loot:要滚动并给予玩家的战利品表列表。
    • function:要运行的函数。如果你想运行多个函数,请创建一个运行所有其他函数的包装函数。
    • sends_telemetry_event:确定在完成此进度时是否应收集遥测数据。仅在minecraft命名空间中有效。可选,默认为false
  • neoforge:conditions:NeoForge 添加的。必须通过的条件列表才能加载进度。可选。

进度树

进度文件可以分组在目录中,这告诉游戏创建多个进度选项卡。一个进度选项卡可能包含一个或多个进度树,具体取决于根进度的数量。空的进度选项卡将自动隐藏。

提示:Minecraft 每个选项卡只有一个根进度,并且总是将根进度称为root。建议遵循此做法。


条件触发器(Criteria Triggers)

要解锁进度,必须满足指定的条件。条件通过触发器跟踪,当相关操作发生时(例如,当玩家杀死指定实体时,player_killed_entity触发器执行),触发器从代码中执行。每当进度加载到游戏中时,定义的条件都会被读取并作为侦听器添加到触发器中。当触发器执行时,所有具有相应条件侦听器的进度都会重新检查是否完成。如果进度完成,侦听器将被移除。

自定义条件触发器由两部分组成:触发器,通过调用#trigger在代码中激活;以及定义触发器应授予条件的条件的实例。触发器扩展SimpleCriterionTrigger<T>,而实例实现SimpleCriterionTrigger.SimpleInstance。泛型值T表示触发器实例类型。

SimpleCriterionTrigger.SimpleInstance

SimpleCriterionTrigger.SimpleInstance表示在criteria对象中定义的单个条件。触发器实例负责保存定义的条件,并返回输入是否匹配条件。

条件通常通过构造函数传递。SimpleCriterionTrigger.SimpleInstance接口只需要一个方法#player,它返回玩家必须满足的条件作为Optional<ContextAwarePredicate>。如果子类是带有此类型参数的记录(如下所示),则自动生成的方法就足够了。

java
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, 其他参数在这里*/)
        implements SimpleCriterionTrigger.SimpleInstance {}

通常,触发器实例具有静态辅助方法,这些方法从实例的参数构造完整对象。这允许在数据生成期间轻松创建这些实例,但它们是可选的。

java
// 在此示例中,EXAMPLE_TRIGGER 是 DeferredHolder<CriterionTrigger<?>, ExampleTrigger>。
// 有关如何注册触发器,请参阅下文。
public static Criterion<ExampleTriggerInstance> instance(ContextAwarePredicate player, ItemPredicate item) {
    return EXAMPLE_TRIGGER.get().createCriterion(new ExampleTriggerInstance(Optional.of(player), item));
}

最后,应添加一个方法,该方法接受当前数据状态并返回用户是否满足必要条件。玩家的条件已经通过SimpleCriterionTrigger#trigger(ServerPlayer, Predicate)检查。大多数触发器实例将此方法称为#matches

java
// 假设我们有一个额外的 ItemPredicate 参数。这可以是任何你需要的。
// 例如,这也可能是 Predicate<LivingEntity>。
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player, ItemPredicate predicate)
        implements SimpleCriterionTrigger.SimpleInstance {
    // 此方法对于每个实例都是唯一的,因此不需要重写。
    // 参数可以是任何你需要的上下文,例如,这也可能是 LivingEntity。
    // 如果你不需要除玩家之外的上下文,这也可能根本不带参数。
    public boolean matches(ItemStack stack) {
        // 由于 ItemPredicate 匹配堆叠,我们在这里使用堆叠作为输入。
        return this.predicate.test(stack);
    }
}

SimpleCriterionTrigger

SimpleCriterionTrigger的实现有两个目的:提供检查触发器实例并在成功时运行附加侦听器的方法,以及指定序列化触发器实例的编解码器(T)。

首先,我们想添加一个方法,该方法接受我们需要的输入并调用SimpleCriterionTrigger#trigger以正确处理所有侦听器。大多数触发器实例也将此方法称为#trigger。重用我们上面的示例触发器实例,我们的触发器可能如下所示:

java
public class ExampleCriterionTrigger extends SimpleCriterionTrigger<ExampleTriggerInstance> {
    // 此方法对于每个触发器都是唯一的,因此不是要重写的方法
    public void trigger(ServerPlayer player, ItemStack stack) {
        this.trigger(player,
                // SimpleCriterionTrigger.SimpleInstance 子类中的条件检查方法
                triggerInstance -> triggerInstance.matches(stack)
        );
    }
}

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

java
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定义编解码器以序列化和反序列化触发器实例。此编解码器通常在实例实现中创建为常量。

java
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, 其他参数在这里*/)
        implements SimpleCriterionTrigger.SimpleInstance {
    public static final Codec<ExampleTriggerInstance> CODEC = ...;

    // ...
}

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

    // ...
}

对于带有ContextAwarePredicateItemPredicate的记录的早期示例,编解码器可能是:

java
public static final Codec<ExampleTriggerInstance> 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));

调用条件触发器

每当执行被检查的操作时,应调用由我们的SimpleCriterionTrigger子类定义的方法。当然,你也可以调用原版触发器,它们可以在CriteriaTriggers中找到。

java
// 在执行操作的代码片段中
// 再次,EXAMPLE_TRIGGER 是注册的自定义条件触发器的实例的供应商
public void performExampleAction(ServerPlayer player, additionalContextParametersHere) {
    // 在此处运行执行操作的代码
    EXAMPLE_TRIGGER.get().trigger(player, additionalContextParametersHere);
}

数据生成

进度可以使用AdvancementProvider进行数据生成。AdvancementProvider接受一个AdvancementGenerator列表,这些生成器实际上使用Advancement.Builder生成进度。

警告:Minecraft 和 NeoForge 都提供了一个名为AdvancementProvider的类,分别位于net.minecraft.data.advancements.AdvancementProvidernet.neoforged.neoforge.common.data.AdvancementProvider。NeoForge 类是对 Minecraft 提供的类的改进,应始终优先使用。以下文档始终假设使用 NeoForge 类。

首先,创建AdvancementProvider的子类:

java
public class MyAdvancementProvider extends AdvancementProvider {
    // 参数可以从 GatherDataEvent 获取。
    public MyAdvancementProvider(PackOutput output,
            CompletableFuture<HolderLookup.Provider> lookupProvider, ExistingFileHelper existingFileHelper) {
        super(output, lookupProvider, existingFileHelper, List.of());
    }
}

现在,下一步是用我们的生成器填充列表。为此,我们添加一个或多个生成器作为静态类,然后将每个生成器的实例添加到构造函数参数中当前为空的列表中。

java
public class MyAdvancementProvider extends AdvancementProvider {
    public MyAdvancementProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider, ExistingFileHelper existingFileHelper) {
        // 将我们的生成器的实例添加到列表参数中。这可以根据需要多次完成。
        // 拥有多个生成器纯粹是为了组织,所有功能都可以通过单个生成器实现。
        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) {
            // 在此处生成你的进度。
        }
    }
}

要生成进度,你需要使用Advancement.Builder

java
// 所有方法都遵循构建器模式,意味着可以并且鼓励链式调用。
// 为了更好的可读性,此处不会进行链式调用。

// 使用静态方法 #advancement() 创建进度构建器。
// 使用 #advancement() 会自动启用遥测事件。如果你不想要这个,
// 可以使用 #recipeAdvancement(),没有其他功能差异。
Advancement.Builder builder = Advancement.Builder.advancement();

// 设置进度的父进度。你可以使用你已经生成的另一个进度,
// 或者使用静态方法 AdvancementSubProvider#createPlaceholder 创建一个占位符进度。
builder.parent(AdvancementSubProvider.createPlaceholder("minecraft:story/root"));

// 设置进度的显示属性。这可以是 DisplayInfo 对象,
// 或者直接传入值。如果直接传入值,将为你创建 DisplayInfo 对象。
builder.display(
        // 进度图标。可以是 ItemStack 或 ItemLike。
        new ItemStack(Items.GRASS_BLOCK),
        // 进度标题和描述。别忘了为这些添加翻译!
        Component.translatable("advancements.examplemod.example_advancement.title"),
        Component.translatable("advancements.examplemod.example_advancement.description"),
        // 背景纹理。如果你不想要背景纹理(对于非根进度),请使用 null。
        null,
        // 框架类型。有效值为 AdvancementType.TASK、CHALLENGE 或 GOAL。
        AdvancementType.GOAL,
        // 是否显示进度提示。
        true,
        // 是否在聊天中宣布进度。
        true,
        // 进度是否应隐藏。
        false
);

// 进度奖励构建器。可以使用四种奖励类型中的任何一种创建,并且可以使用前缀为 add 的方法添加更多奖励。
// 这也可以提前构建,然后生成的 AdvancementRewards 可以在多个进度构建器中重复使用。
builder.rewards(
    // 或者,使用 addExperience() 添加到现有构建器。
    AdvancementRewards.Builder.experience(100)
    // 或者,使用 loot() 创建新构建器。
    .addLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.fromNamespaceAndPath("minecraft", "chests/igloo")))
    // 或者,使用 recipe() 创建新构建器。
    .addRecipe(ResourceLocation.fromNamespaceAndPath("minecraft", "iron_ingot"))
    // 或者,使用 function() 创建新构建器。
    .runs(ResourceLocation.fromNamespaceAndPath("examplemod", "example_function"))
);

// 使用给定名称将条件添加到进度。使用相应触发器实例的静态方法。
builder.addCriterion("pickup_dirt", InventoryChangeTrigger.TriggerInstance.hasItems(Items.DIRT));

// 添加需求处理程序。Minecraft 原生提供 allOf() 和 anyOf(),更复杂的需求必须手动实现。
// 仅在有两个或更多条件时有效。
builder.requirements(AdvancementRequirements.allOf(List.of("pickup_dirt")));

// 使用给定的资源位置将进度保存到磁盘。这将返回一个 AdvancementHolder,
// 可以存储在变量中并用作其他进度构建器的父进度。
builder.save(saver, ResourceLocation.fromNamespaceAndPath("examplemod", "example_advancement"), existingFileHelper);

当然,别忘了将你的提供者添加到GatherDataEvent中:

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

    // 其他提供者在这里
    generator.addProvider(
            event.includeServer(),
            new MyAdvancementProvider(output, lookupProvider, existingFileHelper)
    );
}

贡献者

The avatar of contributor named as 小飘 小飘

文件历史