进度
进度(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
:进度的框架类型。接受challenge
、goal
和task
。可选,默认为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>
。如果子类是带有此类型参数的记录(如下所示),则自动生成的方法就足够了。
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player/*, 其他参数在这里*/)
implements SimpleCriterionTrigger.SimpleInstance {}
通常,触发器实例具有静态辅助方法,这些方法从实例的参数构造完整对象。这允许在数据生成期间轻松创建这些实例,但它们是可选的。
// 在此示例中,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
。
// 假设我们有一个额外的 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
。重用我们上面的示例触发器实例,我们的触发器可能如下所示:
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
注册表:
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/*, 其他参数在这里*/)
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
的记录的早期示例,编解码器可能是:
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
中找到。
// 在执行操作的代码片段中
// 再次,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.AdvancementProvider
和net.neoforged.neoforge.common.data.AdvancementProvider
。NeoForge 类是对 Minecraft 提供的类的改进,应始终优先使用。以下文档始终假设使用 NeoForge 类。
首先,创建AdvancementProvider
的子类:
public class MyAdvancementProvider extends AdvancementProvider {
// 参数可以从 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) {
// 将我们的生成器的实例添加到列表参数中。这可以根据需要多次完成。
// 拥有多个生成器纯粹是为了组织,所有功能都可以通过单个生成器实现。
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
:
// 所有方法都遵循构建器模式,意味着可以并且鼓励链式调用。
// 为了更好的可读性,此处不会进行链式调用。
// 使用静态方法 #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
中:
@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)
);
}