MC Fabric 1.20.1 学习笔记 —— 制作一个独立配方的工作台 03 ScreenHandler

118 阅读5分钟

WorkBlockScreenHandler可以视为屏幕的后端逻辑
这个类分为两个主体,一个是物品的输入输出类,一个是GUI屏幕类
我们需要在这里面完成对GUI屏幕的后端逻辑控制,以及输出输出类的后端逻辑控制
代码很长,但大部分内容是fabric约定的固定写法,可以直接CV,当然你也可以将此类封装一次以便更快速的开发 JAVA是这样的

WorkBlockInputInventory以及WorkBlockResultInventory将在以后编写

/**
 * 因为这个GUI是一个合成台的GUI,需要继承AbstractRecipeScreenHandler,
 * 而这个AbstractRecipeScreenHandler类中匹配配方的方法参数是其泛型中的类,故我们在这里使用了RecipeInputInventory作为其泛型
 */
public class WorkBlockScreenHandler extends AbstractRecipeScreenHandler<RecipeInputInventory> {
    public static final String NAME = "work_block_screen_handler";
    public static final Identifier ID = new Identifier(ModInfo.MOD_ID, NAME);

    // 自定义屏幕类型
    public static ScreenHandlerType<WorkBlockScreenHandler> WORK_BLOCK_SCREEN_HANDLER_TYPE;

    // 给屏幕上看的输入,本质上是一个Inventory接口的实现,我们将在这里面进行触发本类中的配方匹配
    private final WorkBlockInputInventory input = new WorkBlockInputInventory(this, 3, 3);

    // 给屏幕上看的输出,是一个RecipeUnlocker的实现,RecipeUnlocker接口用于遵守合成配方是否可用的规范
    private final WorkBlockResultInventory result = new WorkBlockResultInventory();
    // 屏幕上下文句柄
    private final ScreenHandlerContext context;
    // 正在使用的玩家
    private final PlayerEntity player;

    public WorkBlockScreenHandler(int syncId, PlayerInventory playerInventory) {
        // 这个屏幕需要同事显示玩家背包格以及合成台物品格
        this(syncId, playerInventory, new SimpleInventory(9), ScreenHandlerContext.EMPTY);
    }

    public WorkBlockScreenHandler(int syncId, PlayerInventory playerInventory, Inventory inventory, ScreenHandlerContext context) {
        super(WORK_BLOCK_SCREEN_HANDLER_TYPE, syncId);
        this.context = context;
        this.player = playerInventory.player;
        // public方法验参常规写法,检查这个物品格子是否为第二个参数的大小,不相符时会抛出异常
        checkSize(inventory, 9);
        // 触发一下背包被打开时的方法,也可以不写
        inventory.onOpen(playerInventory.player);
        // 产物槽位
        this.addSlot(new WorkBlockOutputSlot(this.result, 0, 124, 35, playerInventory.player, this.input));
        // 耗材槽位
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                // 方块栏的物品从0开始到8结束
                this.addSlot(new Slot(this.input, j + i * 3, 30 + j * 18, 17 + i * 18));
            }
        }
        // 玩家快捷键
        for (int i = 0; i < 9; i++) {
            // 玩家快捷栏从0到8
            this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142));
        }
        // 玩家物品栏
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 9; j++) {
                // 玩家背包从9到35结束
                this.addSlot(new Slot(playerInventory, i * 9 + j + 9, 8 + j * 18, 84 + i * 18));
            }
        }
    }

    // 注册方法,仅供参考
    public static void registerScreenHandlerType() {
        WORK_BLOCK_SCREEN_HANDLER_TYPE =
                Registry.register(Registries.SCREEN_HANDLER, ID, new ScreenHandlerType<>(WorkBlockScreenHandler::new, FeatureFlags.VANILLA_FEATURES));
    }

    // 更新合成配方的结果
    protected static void updateResult(ScreenHandler handler, World world, PlayerEntity player, WorkBlockInputInventory inputInventory, WorkBlockResultInventory resultInventory) {
        ItemStack itemStack2;
        WorkBlockRecipe workBlockRecipe;
        if (world.isClient) {
            return;
        }
        // 因为确认了是在服务端,我们需要使用到serverPlayerEntity中的方法发送数据包resultInventory的方法只支持serverPlayerEntity,因而在这里进行强转
        ServerPlayerEntity serverPlayerEntity = (ServerPlayerEntity) player;
        ItemStack itemStack = ItemStack.EMPTY;
        // 这里去匹配配方
        Optional<WorkBlockRecipe> optional = world.getRecipeManager().getFirstMatch(WorkBlockRecipe.Type.INSTANCE, inputInventory, world);
        // 当我们找到的配方不为空,可以合成,且合 成结果是可以被使用的方块时我们将其...
        if (optional.isPresent() && resultInventory.shouldCraftRecipe(world, serverPlayerEntity, workBlockRecipe = optional.get()) && (itemStack2 = workBlockRecipe.craft(inputInventory, world.getRegistryManager())).isItemEnabled(world.getEnabledFeatures())) {
            itemStack = itemStack2;
        }
        // 接上文注释...我们将其设置到结果中
        resultInventory.setStack(0, itemStack);
        // 这里是固定写法,效仿MC自己的工作台方块写的
        handler.setPreviousTrackedSlot(0, itemStack);
        // 发送数据包给客户端
        serverPlayerEntity.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(handler.syncId, handler.nextRevision(), 0, itemStack));
    }

    @Override
    public void onContentChanged(Inventory inventory) {
        // 这个触发方法会在WorkBlockInputInventory中进行调用,触发更新
        this.context.run((world, pos) -> WorkBlockScreenHandler.updateResult(this, world, this.player, this.input, this.result));
    }

    @Override
    public void populateRecipeFinder(RecipeMatcher finder) {
        // 由fabric调用,是配方相关的搜索器,复制就完事了嗷,每次更新配方fabric都会去调用这个方法
        this.input.provideRecipeInputs(finder);
    }

    @Override
    public void clearCraftingSlots() {
        // 同时清空两个
        this.input.clear();
        this.result.clear();
    }

    @Override
    public boolean matches(Recipe<? super RecipeInputInventory> recipe) {
        // 去匹配配方,也由fabric调用
        return recipe.matches(this.input, this.player.getWorld());
    }

    @Override
    public void onClosed(PlayerEntity player) {
        // 当界面被关闭时,父类方法会将物品归还给玩家。
        super.onClosed(player);
        // 固定写法,没有细看,有兴趣可以自己点进去看看
        this.context.run((world, pos) -> this.dropInventory(player, this.input));
    }

    @Override
    public boolean canUse(PlayerEntity player) {
        // 8格方块以内的玩家才可以使用,你也可以在这里写一些仅一个玩家可以使用之类的逻辑
        return WorkBlockScreenHandler.canUse(this.context, player, WorkBlock.INSTANCE);
    }

    // 快速移动,就是shift点击物品时的效果,主要更改里面那些整数,我这里合成表大小就是9输入+1输出,因而你能看到10这个整数,大于10就是玩家背包和物品栏4*9也就是10~45
    @Override
    public ItemStack quickMove(PlayerEntity player, int slot) {
        ItemStack itemStack = ItemStack.EMPTY;
        Slot slot2 = this.slots.get(slot);
        if (slot2.hasStack()) {
            ItemStack itemStack2 = slot2.getStack();
            itemStack = itemStack2.copy();
            if (slot == 0) {
                this.context.run((world, pos) -> itemStack2.getItem().onCraft(itemStack2, world, player));
                if (!this.insertItem(itemStack2, 10, 46, true)) {
                    return ItemStack.EMPTY;
                }
                slot2.onQuickTransfer(itemStack2, itemStack);
            } else if (slot >= 10 && slot < 46 ? !this.insertItem(itemStack2, 1, 10, false) && (slot < 37 ? !this.insertItem(itemStack2, 37, 46, false) : !this.insertItem(itemStack2, 10, 37, false)) : !this.insertItem(itemStack2, 10, 46, false)) {
                return ItemStack.EMPTY;
            }
            if (itemStack2.isEmpty()) {
                slot2.setStack(ItemStack.EMPTY);
            } else {
                slot2.markDirty();
            }
            if (itemStack2.getCount() == itemStack.getCount()) {
                return ItemStack.EMPTY;
            }
            slot2.onTakeItem(player, itemStack2);
            if (slot == 0) {
                player.dropItem(itemStack2, false);
            }
        }
        return itemStack;
    }
    @Override

    public boolean canInsertIntoSlot(ItemStack stack, Slot slot) {
        // GUI上的结果栏不可插入,其余的栏位按自己的规则,一般是都可以插入,这里的插入不涉及满了不能插这种判断,算是约定
        return slot.inventory != this.result && super.canInsertIntoSlot(stack, slot);
    }

    @Override
    public int getCraftingResultSlotIndex() {
        // 我们将输出栏的槽位视为0,在WorkBlockScreenHandler构造方法中已经设置好了,这里直接返回0即可
        return 0;
    }

    @Override
    public int getCraftingWidth() {
        // fabric要这玩意,咱就写上
        return this.input.getWidth();
    }


    @Override
    public int getCraftingHeight() {
        // fabric要这玩意,咱就写上
        return this.input.getHeight();
    }

    @Override
    public int getCraftingSlotCount() {
        // fabric要这玩意,咱就写上
        return 9;
    }

    @Override
    public RecipeBookCategory getCategory() {
        // fabric要这玩意,咱就写上,这个应该是小绿书的内容,没细看
        return RecipeBookCategory.CRAFTING;
    }

    @Override
    public boolean canInsertIntoSlot(int index) {
        // 抽象配方类需要的一个检查槽位是否可插入的方法,也不涉及满了无法放入的约束
        return index != this.getCraftingResultSlotIndex();
    }

}