前言

  1. 本教程是1.19.2版本为例子创建的
  2. 以后的版本肯定会逐渐迭代到1.21的neo-forged系列中去

Advancement

  • 是mojang原生的成就文件(以前的译名就叫成就)

理解Advancement生成的机制

  1. 在代码段内部写进度并添加进服务器进度中
  2. 使用datagen生成进度文件

进度触发器

  1. 触发方式
  2. 实现原理

触发方式

  • 它使用的是trigger进行触发
  • 我们先看看mojang的代码
展开查看代码 net.minecraft.advancements.critereon.RecipeUnlockedTrigger

package net.minecraft.advancements.critereon;

import com.google.gson.JsonObject; import net.minecraft.advancements.critereon.EntityPredicate.Composite; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.GsonHelper; import net.minecraft.world.item.crafting.Recipe;

public class RecipeUnlockedTrigger extends SimpleCriterionTrigger { static final ResourceLocation ID = new ResourceLocation("recipe_unlocked");

public RecipeUnlockedTrigger() {
}

public ResourceLocation getId() {
    return ID;
}

public TriggerInstance createInstance(JsonObject json, EntityPredicate.Composite entityPredicate, DeserializationContext conditionsParser) {
    ResourceLocation resourceLocation = new ResourceLocation(GsonHelper.getAsString(json, "recipe"));
    return new TriggerInstance(entityPredicate, resourceLocation);
}

public void trigger(ServerPlayer player, Recipe<?> recipe) {
    this.trigger(player, (arg2) -> arg2.matches(recipe));
}

public static TriggerInstance unlocked(ResourceLocation recipe) {
    return new TriggerInstance(Composite.ANY, recipe);
}

public static class TriggerInstance extends AbstractCriterionTriggerInstance {
    private final ResourceLocation recipe;

    public TriggerInstance(EntityPredicate.Composite player, ResourceLocation recipe) {
        super(RecipeUnlockedTrigger.ID, player);
        this.recipe = recipe;
    }

    public JsonObject serializeToJson(SerializationContext context) {
        JsonObject jsonObject = super.serializeToJson(context);
        jsonObject.addProperty("recipe", this.recipe.toString());
        return jsonObject;
    }

    public boolean matches(Recipe<?> recipe) {
        return this.recipe.equals(recipe.getId());
    }
}

}

  • 它被分为 SimpleCriterionTrigger 和 AbstractCriterionTriggerInstance 两个类
  • 其中我们需要做的就是
    1. 写自己的trigger 和 matches
    1. 在服务器启动前将触发器注册
    1. 实现触发

接下来我们来实现配方合成时触发成就

  1. 写一个总触发器 CraftingRecipeTrigger类并且创建CraftingRecipeTrigger内部子类 TriggerInstance类
  2. TriggerInstance需要实现序列化和创建对象
  3. 而CraftingRecipeTrigger所需要做的就是解析并反序列化创建实例 createInstance
  4. 在TriggerInstance实现matches判断
  5. 在CraftingRecipeTrigger实现trigger触发器
  6. trigger触发器使用SimpleCriterionTrigger里面的trigger(ServerPlayer, Predicate<? extends AbstractCriterionTriggerInstance>)
  7. 注册实例(forge,fabric,architectury)
import com.google.gson.JsonObject;
import net.minecraft.advancements.critereon.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;

public class CraftingRecipeTrigger extends SimpleCriterionTrigger<CraftingRecipeTrigger.TriggerInstance> {

    static final ResourceLocation ID = new ResourceLocation("tutorial", "crafting_recipe");

    @Override

    protected @NotNull TriggerInstance createInstance(@NotNull JsonObject json, EntityPredicate.@NotNull Composite player, @NotNull DeserializationContext context) {
        return new TriggerInstance(player, ItemPredicate.fromJson(json.get("item")));
    }

    public void trigger(ServerPlayer player, ItemStack item) {
        this.trigger(player, arg -> arg.matches(item));

    }

    // ... 1
    public static class TriggerInstance extends AbstractCriterionTriggerInstance {

        private final ItemPredicate item;

        public TriggerInstance(EntityPredicate.Composite player, ItemPredicate item) {
            super(CraftingRecipeTrigger.ID, player);
            this.item = item;
        }

        @Override
        public @NotNull JsonObject serializeToJson(@NotNull SerializationContext context) {
            JsonObject jsonObject = super.serializeToJson(context);
            jsonObject.add("item", this.item.serializeToJson());
            return jsonObject;
        }

        public boolean matches(ItemStack item) {
            return this.item.matches(item);
        }
    }
}

forge

在主类中实现

import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.fml.common.Mod;

@Mod("tutorial")
public class Tutorial {
    public static final CraftingRecipeTrigger cr = new CraftingRecipeTrigger();

    public Tutorial() {

    }

    @SubscribeEvent
    public static void craftEvents(PlayerEvent.ItemCraftedEvent event) {
        if (event.getEntity() instanceof ServerPlayer serverPlayer) {
            cr.trigger(serverPlayer, event.getCrafting());// 触发
        }
    }
}

Fabric

在主类中实现


import net.fabricmc.api.ModInitializer;

public class Tutorial implements ModInitializer {
    public static final CraftingRecipeTrigger cr = new CraftingRecipeTrigger();

    @Override
    public void onInitialize() {
        
    }
}
import net.minecraft.world.inventory.ResultSlot;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;

@Mixin(ResultSlot.class)
public class MixinCraftingRecipe {
    @Inject(
            method = "checkTakeAchievements",
            at = @At(value = "INVOKE",
                    target = "Lnet/minecraft/world/item/ItemStack;onCraftedBy(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;I)V",
                    shift = At.Shift.AFTER)
    )
    private void craft(ItemStack itemStack, CallbackInfo ci) {
        Tutorial.cr.trigger(serverPlayer, itemStack);
    }
    
}

Architectury

多加载器模式触发的事件

import dev.architectury.event.events.common.PlayerEvent;

public class Tutorial {
    public static final CraftingRecipeTrigger cr = new CraftingRecipeTrigger();

    public static void init() {
        PlayerEvent.CRAFT_ITEM.register((player, constructed, inventory) -> {
            cr.trigger(serverPlayer, constructed);
        });
    }
}

进度数据生成器

  1. 实现方式

Mojang

这个是mojang的原生Advancement DataGen代码


package net.minecraft.data.advancements;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.mojang.logging.LogUtils;

import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;

import net.minecraft.advancements.Advancement;
import net.minecraft.data.*;
import net.minecraft.data.DataGenerator.Target;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.data.ExistingFileHelper;
import org.slf4j.Logger;

public class AdvancementProvider implements DataProvider {
    private static final Logger LOGGER = LogUtils.getLogger();
    private final DataGenerator.PathProvider pathProvider;
    private final List<Consumer<Consumer<Advancement>>> tabs = ImmutableList.of(new TheEndAdvancements(), new HusbandryAdvancements(), new AdventureAdvancements(), new NetherAdvancements(), new StoryAdvancements());
    protected ExistingFileHelper fileHelper;

    /** @deprecated */
    @Deprecated
    public AdvancementProvider(DataGenerator generator) {
        this.pathProvider = generator.createPathProvider(Target.DATA_PACK, "advancements");
    }

    public AdvancementProvider(DataGenerator generatorIn, ExistingFileHelper fileHelperIn) {
        this.pathProvider = generatorIn.createPathProvider(Target.DATA_PACK, "advancements");
        this.fileHelper = fileHelperIn;
    }

    public void run(CachedOutput output) {
        Set<ResourceLocation> set = Sets.newHashSet();
        Consumer<Advancement> consumer = (arg2) -> {
            if (!set.add(arg2.getId())) {
                throw new IllegalStateException("Duplicate advancement " + arg2.getId());
            } else {
                Path path = this.pathProvider.json(arg2.getId());

                try {
                    DataProvider.saveStable(output, arg2.deconstruct().serializeToJson(), path);
                } catch (IOException var6) {
                    LOGGER.error("Couldn't save advancement {}", path, var6);
                }

            }
        };
        this.registerAdvancements(consumer, this.fileHelper);
    }

    protected void registerAdvancements(Consumer<Advancement> consumer, ExistingFileHelper fileHelper) {

        for (Consumer<Consumer<Advancement>> tab : this.tabs) {
            consumer1.accept(consumer);
        }

    }

    public String getName() {
        return "Advancements";
    }
}


Forge

  1. 继承原版进度提供者
  2. 使用access Transformer 改变field tags的私有类型或者使用mixin接口setTags。 我们使用的前者
import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.critereon.EntityPredicate;
import net.minecraft.advancements.critereon.ItemPredicate;
import net.minecraft.data.CachedOutput;
import net.minecraft.data.DataGenerator;
import net.minecraft.data.advancements.AdvancementProvider;
import net.minecraft.world.item.Items;

import java.util.List;
import java.util.function.Consumer;

/** @noinspection ALL*/
public class TutorialProvider extends AdvancementProvider {
    public TutorialProvider(DataGenerator generator) {
        super(generator);
    }

    public List<Consumer<Consumer<Advancement>>> getTags() {
        return List.of(TutorialProvider::register);
    }

    private static void register(Consumer<Advancement> advancementConsumer) {
        Advancement.Builder.advancement()
                .addCriterion("a", new CraftingRecipeTrigger.TriggerInstance(EntityPredicate.Composite.ANY, ItemPredicate.Builder.item().of(Items.STICK)))
                .save(consumer, new ResourceLocation("tutorial", "main/root"));
    }

    @Override
    public void run(CachedOutput output) {
        tags = getTags();
        super.run(output);
    }

    @Override
    public String getName() {
        return "tutorial advancement provider";
    }
}

Fabric

import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
import net.fabricmc.fabric.api.datagen.v1.provider.FabricAdvancementProvider;
import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.critereon.EntityPredicate;
import net.minecraft.advancements.critereon.ItemPredicate;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Items;

import java.util.function.Consumer;

public class TutorialProvider extends FabricAdvancementProvider {
    public TutorialProvider(FabricDataGenerator generator) {
        super(generator);
    }

    @Override
    public void generateAdvancement(Consumer<Advancement> consumer) {
        Advancement.Builder.advancement()
                .addCriterion("a", new CraftingRecipeTrigger.TriggerInstance(EntityPredicate.Composite.ANY, ItemPredicate.Builder.item().of(Items.STICK)))
                .save(consumer, new ResourceLocation("tutorial", "main/root"));
    }
}

Architectury

  1. 写一个forge的dataGen
  2. 分别在fabric forge模块下设定特有的dataen操作

进度存在代码中

  1. 在代码生成一个进度对象
  2. 在服务器启动正式启动前写进服务端的进度中去

Forge

import net.minecraft.advancements.Advancement;
import net.minecraft.advancements.critereon.InventoryChangeTrigger;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.item.Items;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.server.ServerAboutToStartEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;

@Mod("tutorial")
public class Tutorial {

    public static final Advancement root =
            Advancement.Builder.advancement()
                    .addCriterion("key", InventoryChangeTrigger.TriggerInstance.hasItems(Items.STICK))
                    .build(new ResourceLocation("tutorial", "main/root"));

    public Tutorial() {
        MinecraftForge.EVENT_BUS.register(this);
    }


    @SubscribeEvent
    public void aboutServerStart(ServerAboutToStartEvent event) {
        MinecraftServer server = event.getServer();
        server.getAdvancements().getAllAdvancements().add(root);
    }
}

Fabric

import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;

public class Tutorial implements ModInitializer {
    public static final Advancement root =
            Advancement.Builder.advancement()
                    .addCriterion("key", InventoryChangeTrigger.TriggerInstance.hasItems(Items.STICK))
                    .build(new ResourceLocation("tutorial", "main/root"));
    @Override
    public void onInitialize() {
        ServerLifecycleEvents.SERVER_STARTING.register(server -> {
            server.getAdvancements().getAllAdvancements().add(root);
        });
    }
}

Architectury

import dev.architectury.event.events.common.LifecycleEvent;

public class Tutorial {

    public static final Advancement root =
            Advancement.Builder.advancement()
                    .addCriterion("key", InventoryChangeTrigger.TriggerInstance.hasItems(Items.STICK))
                    .build(new ResourceLocation("tutorial", "main/root"));
    
    public static void init() {
        LifecycleEvent.SERVER_BEFORE_START.register(server -> {
            server.getAdvancements().getAllAdvancements().add(root);
        });
    }
}