1. 自定义进度触发条件
进度触发条件(Criterion Trigger)代表了解锁进度的某一个具体条件。一个进度的解锁可能会需要满足一个触发条件,或是多个触发条件的组合。虽然原版提供了相当丰富的触发条件,但我们仍然会不可避免地遇到需要自定义触发条件的情况。不过,写一个新的触发条件其实并不简单。一个完整的 ICriterionTrigger<? extends ICriterionInstance>
的实现大约长这样:
import net.minecraft.advancements.ICriterionTrigger;
import net.minecraft.advancements.PlayerAdvancements;
import net.minecraft.advancements.critereon.AbstractCriterionInstance;
public final class CustomTrigger implements ICriterionTrigger<CustomTrigger.Instance> {
// 用作该 trigger 的唯一 ID
private static final ResourceLocation ID = new ResourceLocation("my_mod", "custom");
// 集中管理所有进度用到的该触发条件的具体实例
private final Map<PlayerAdvancements, Set<Listener<CustomTrigger.Instance>>> listeners = new HashMap<>();
/*
* 该 Criterion 唯一 ID 的访问器
*/
@Nonnull
@Override
public ResourceLocation getId() {
return ID;
}
/*
* 加入新的监听器
*/
@Override
public void addListener(PlayerAdvancements playerAdvancements, Listener<CustomTrigger.Instance> listener) {
this.listeners.computeIfAbsent(playerAdvancements, advancements -> new HashSet<>()).add(listener);
}
/*
* 清除不用的监听器,避免占用资源。由 Minecraft 内部统一管理。
*/
@Override
public void removeListener(PlayerAdvancements playerAdvancements, Listener<CustomTrigger.Instance> listener) {
Set<Listener<CustomTrigger.Instance>> listeners = this.listeners.get(playerAdvancements);
if (listeners != null) {
listeners.remove(listener);
if (listeners.isEmpty()) {
this.listeners.remove(playerAdvancements);
}
}
}
/*
* 直接取消对某个玩家进度的跟踪
*/
@Override
public void removeAllListeners(PlayerAdvancements playerAdvancements) {
this.listeners.remove(playerAdvancements);
}
/*
* 抽象工厂方法,实际上是反序列化器
*/
@Override
public CustomTrigger.Instance deserializeInstance(JsonObject json, JsonDeserializationContext context) {
return new Instance(...);
}
/*
* 该触发器的入口,需要在合适的时间与位置调用
*/
public void trigger(EntityPlayerMP player, Context context) {
PlayerAdvancements advancements = player.getAdvancements()
HashSet<Listener<Instance>> listeners = this.listeners.get(advancements);
if (listeners != null) {
List<Listener<Instance>> triggered = new ArrayList<>();
for (Listener<CustomTrigger.Instance> listener : listeners) {
if (listener.getCriterionInstance().test(context)) {
triggered.add(listener);
}
}
for (Listener<CustomTrigger.Instance> listener : triggered) {
listener.grantCriterion(advancements);
}
}
}
/*
* 抽象工厂方法生产出的触发器实例。不能为 private,否则 outer class 类声明中接口的
* 泛型参数看不到这个类型。
* 此外,ICriterionTrigger 的类型参数的边界实际上是 extends ICriterionInstance,
* 这里使用 AbstractCriterionInstance 是为了复用一些 boiler plate code(toString
* 啦这样的)。
*/
static final class Instance extends AbstractCriterionInstance {
Instance(...) {
// 进行必要的初始化操作
// AbstractCriterionInstance 的构造器要求传入的是这个进度唯一的
// 识别 ID,即 ICriterionTrigger.getId() 的返回值。
super(CustomTrigger.ID)
}
/*
* 每一个触发器实例都有其独立的判据。
*/
boolean test(Context context) {
return false;
}
}
}
天知道 Dinnerbone 写出这个东西的时候在想什么。
简单来说,一个新的 Criterion 分成这两个部分:
ICriterionTrigger<? extends AbstractCriterionInstance>
的实现。每一个它的实现的实例代表了一大类具体的条件,比如说,对于minecraft:consume_item
,你可以通过指定不同的物品来获得不同的条件,但这些不同的条件都属于minecraft:consume_item
这个类型。换言之,这是享元模式(Flyweight pattern)中的共用对象(Flyweight),主要负责管理所有具体的触发条件实例,同时兼职抽象工厂(见 2.)。ICriterionInstance
的实现。它代表了具体的一个条件,即便识别 ID 和别的条件一致,其具体判断细节也未必相同。它的实例由一个抽象工厂产出,而这个抽象工厂正是由对应的ICriterionTrigger
兼职的(即deserializeInstance
方法)。
而使用这个新的 Criterion 的方式是这样的:
- 首先注册
ICriterionTrigger
的实例。通过原版的CriteriaTriggers.register
完成注册。这个方法实际上是被 Forge patch 过的,在 patch 之前它不能正确处理 ID 中的 namespace 不是minecraft
的 criterion。这个方法返回被注册的 criterion,所以你可以写:CustomTrigger TRIGGER = CriteriaTriggers.register(new CustomTrigger());
- 在适当的时候调用
TRIGGER.trigger(player, ...)
,其中player
是玩家实体,...
代表你需要的上下文信息。玩家实体的作用是提供PlayerAdvancements
对象。上文中的示例代码使用了Context
——这是一个假想的 parameter object,在这里充当上下文的容器。 TRIGGER.trigger(player, ...)
查表获得对应的Set<Listener<? extends ICriterionInstance>>
,若存在则遍历所有已知的ICriterionInstance
,并根据上下文过滤出所有满足触发条件的Listener<? extends ICriterionInstance>
。然后对所有满足条件的Listener<? extends ICriterionInstance>
对象调用grantCriterion(PlayerAdvancements)
方法。上文中示例代码之所以使用了单独的List
是因为 Minecraft 会在grantCriterion
被调用时清理不再使用的ICriterionInstance
,若处理不当,极易导致ConcurrentModificationException
出现。
1.1. 真实案例:右击方块时触发的 criterion
【施工中】