进度
进度是玩家可以完成的任务式目标,按指定条件授予,并可在完成时执行操作。
在命名空间的 advancement 子目录创建 JSON 即可添加。例如,Mod ID 为 examplemod、名为 example_name 的进度位于 data/examplemod/advancement/example_name.json,其 ID 相对于 advancement 目录,为 examplemod:example_name。名称可自由选择,游戏会自动加载。只有添加新条件或从代码触发条件时才需要 Java(见下文)。
规范
进度 JSON 可包含:
- parent:父进度 ID。循环引用会导致加载失败。可选,省略时为进度树的根进度。
- display:界面显示属性,可选。省略时不可见,但仍能触发。
- criteria:条件名称到所跟踪条件的映射。原版触发器见 CriteriaTriggers,JSON 规范见 Minecraft Wiki。自定义与代码触发方式见下文。
- requirements:列表的列表,内层执行 OR,外层执行 AND,即每个子列表至少满足一项。可选,默认要求全部条件。
- rewards:可选奖励对象,其中所有值也可选。
- 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。