From 8c13c015031a0de865d2e767cd8e879754f803e2 Mon Sep 17 00:00:00 2001 From: shedaniel Date: Fri, 5 Aug 2022 01:30:08 +0800 Subject: More work --- .../rei/impl/common/transfer/InputSlotCrafter.java | 198 +++++++++++++++++++++ .../impl/common/transfer/MenuInfoRegistryImpl.java | 128 +++++++++++++ .../autocrafting/DefaultCategoryHandler.java | 185 +++++++++++++++++++ 3 files changed, 511 insertions(+) create mode 100644 runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/InputSlotCrafter.java create mode 100644 runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/MenuInfoRegistryImpl.java create mode 100644 runtime-engine/menu-info/src/main/java/me/shedaniel/rei/plugin/autocrafting/DefaultCategoryHandler.java (limited to 'runtime-engine/menu-info/src/main/java') diff --git a/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/InputSlotCrafter.java b/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/InputSlotCrafter.java new file mode 100644 index 000000000..e8a252389 --- /dev/null +++ b/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/InputSlotCrafter.java @@ -0,0 +1,198 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.shedaniel.rei.impl.common.transfer; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import me.shedaniel.rei.api.common.category.CategoryIdentifier; +import me.shedaniel.rei.api.common.display.Display; +import me.shedaniel.rei.api.common.entry.InputIngredient; +import me.shedaniel.rei.api.common.transfer.RecipeFinder; +import me.shedaniel.rei.api.common.transfer.info.MenuInfo; +import me.shedaniel.rei.api.common.transfer.info.MenuInfoContext; +import me.shedaniel.rei.api.common.transfer.info.MenuInfoRegistry; +import me.shedaniel.rei.api.common.transfer.info.stack.SlotAccessor; +import me.shedaniel.rei.api.common.util.CollectionUtils; +import net.minecraft.core.NonNullList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.Objects; + +public class InputSlotCrafter implements MenuInfoContext { + protected CategoryIdentifier category; + protected T container; + protected MenuInfo menuInfo; + private Iterable inputStacks; + private Iterable inventoryStacks; + private ServerPlayer player; + + private InputSlotCrafter(CategoryIdentifier category, T container) { + this.category = category; + this.container = container; + } + + public void setMenuInfo(MenuInfo menuInfo) { + this.menuInfo = menuInfo; + } + + public static InputSlotCrafter start(CategoryIdentifier category, T menu, ServerPlayer player, CompoundTag display, boolean hasShift) { + InputSlotCrafter crafter = new InputSlotCrafter<>(category, menu); + MenuInfo menuInfo = Objects.requireNonNull(MenuInfoRegistry.getInstance().get(category, menu, crafter, display), "Container Info does not exist on the server!"); + crafter.setMenuInfo(menuInfo); + crafter.fillInputSlots(player, hasShift); + return crafter; + } + + private void fillInputSlots(ServerPlayer player, boolean hasShift) { + this.player = player; + this.inventoryStacks = this.menuInfo.getInventorySlots(this); + this.inputStacks = this.menuInfo.getInputSlots(this); + + // Return the already placed items on the grid + this.cleanInputs(); + + RecipeFinder recipeFinder = new RecipeFinder(); + this.menuInfo.getRecipeFinderPopulator().populate(this, recipeFinder); + NonNullList ingredients = NonNullList.create(); + for (InputIngredient itemStacks : this.menuInfo.getInputsIndexed(this, true)) { + ingredients.add(CollectionUtils.toIngredient(itemStacks.get())); + } + + if (recipeFinder.findRecipe(ingredients, null)) { + this.fillInputSlots(recipeFinder, ingredients, hasShift); + } else { + this.cleanInputs(); + this.menuInfo.markDirty(this); + throw new NotEnoughMaterialsException(); + } + + this.menuInfo.markDirty(this); + } + + public void alignRecipeToGrid(Iterable inputStacks, Iterator recipeItemIds, int craftsAmount) { + for (SlotAccessor inputStack : inputStacks) { + if (!recipeItemIds.hasNext()) { + return; + } + + this.acceptAlignedInput(recipeItemIds.next(), inputStack, craftsAmount); + } + } + + public void acceptAlignedInput(Integer recipeItemId, SlotAccessor inputStack, int craftsAmount) { + ItemStack toBeTakenStack = RecipeFinder.getStackFromId(recipeItemId); + if (!toBeTakenStack.isEmpty()) { + for (int i = 0; i < craftsAmount; ++i) { + this.fillInputSlot(inputStack, toBeTakenStack); + } + } + } + + protected void fillInputSlot(SlotAccessor slot, ItemStack toBeTakenStack) { + SlotAccessor takenSlot = this.takeInventoryStack(toBeTakenStack); + if (takenSlot != null) { + ItemStack takenStack = takenSlot.getItemStack().copy(); + if (!takenStack.isEmpty()) { + if (takenStack.getCount() > 1) { + takenSlot.takeStack(1); + } else { + takenSlot.setItemStack(ItemStack.EMPTY); + } + + takenStack.setCount(1); + if (slot.getItemStack().isEmpty()) { + slot.setItemStack(takenStack); + } else { + slot.getItemStack().grow(1); + } + } + } + } + + protected void fillInputSlots(RecipeFinder recipeFinder, NonNullList ingredients, boolean hasShift) { + int recipeCrafts = recipeFinder.countRecipeCrafts(ingredients, null); + int amountToFill = hasShift ? recipeCrafts : 1; + IntList recipeItemIds = new IntArrayList(); + if (recipeFinder.findRecipe(ingredients, recipeItemIds, amountToFill)) { + int finalCraftsAmount = amountToFill; + + for (int itemId : recipeItemIds) { + finalCraftsAmount = Math.min(finalCraftsAmount, RecipeFinder.getStackFromId(itemId).getMaxStackSize()); + } + + if (recipeFinder.findRecipe(ingredients, recipeItemIds, finalCraftsAmount)) { + this.cleanInputs(); + this.alignRecipeToGrid(inputStacks, recipeItemIds.iterator(), finalCraftsAmount); + } + } + } + + protected void cleanInputs() { + this.menuInfo.getInputCleanHandler().clean(this); + } + + @Nullable + public SlotAccessor takeInventoryStack(ItemStack itemStack) { + for (SlotAccessor inventoryStack : inventoryStacks) { + ItemStack itemStack1 = inventoryStack.getItemStack(); + if (!itemStack1.isEmpty() && areItemsEqual(itemStack, itemStack1) && !itemStack1.isDamaged() && !itemStack1.isEnchanted() && !itemStack1.hasCustomHoverName()) { + return inventoryStack; + } + } + + return null; + } + + private static boolean areItemsEqual(ItemStack stack1, ItemStack stack2) { + return stack1.getItem() == stack2.getItem() && ItemStack.tagMatches(stack1, stack2); + } + + @Override + public T getMenu() { + return container; + } + + @Override + public ServerPlayer getPlayerEntity() { + return player; + } + + @Override + public D getDisplay() { + return menuInfo.getDisplay(); + } + + @Override + public CategoryIdentifier getCategoryIdentifier() { + return category; + } + + public static class NotEnoughMaterialsException extends RuntimeException {} +} diff --git a/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/MenuInfoRegistryImpl.java b/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/MenuInfoRegistryImpl.java new file mode 100644 index 000000000..dfbf67866 --- /dev/null +++ b/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/impl/common/transfer/MenuInfoRegistryImpl.java @@ -0,0 +1,128 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.shedaniel.rei.impl.common.transfer; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import me.shedaniel.rei.api.common.category.CategoryIdentifier; +import me.shedaniel.rei.api.common.display.Display; +import me.shedaniel.rei.api.common.plugins.REIServerPlugin; +import me.shedaniel.rei.api.common.transfer.info.MenuInfo; +import me.shedaniel.rei.api.common.transfer.info.MenuInfoProvider; +import me.shedaniel.rei.api.common.transfer.info.MenuInfoRegistry; +import me.shedaniel.rei.api.common.transfer.info.MenuSerializationContext; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.inventory.AbstractContainerMenu; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +public class MenuInfoRegistryImpl implements MenuInfoRegistry { + private final Map, Map, List>>> map = Maps.newLinkedHashMap(); + private final Map>, List>> mapGeneric = Maps.newLinkedHashMap(); + + @Override + public void register(CategoryIdentifier category, Class menuClass, MenuInfoProvider menuInfo) { + map.computeIfAbsent(category, id -> Maps.newLinkedHashMap()) + .computeIfAbsent(menuClass, c -> Lists.newArrayList()) + .add(menuInfo); + } + + @Override + public void registerGeneric(Predicate> categoryPredicate, MenuInfoProvider menuInfo) { + mapGeneric.computeIfAbsent(categoryPredicate, id -> Lists.newArrayList()).add(menuInfo); + } + + @Override + @Nullable + @Environment(EnvType.CLIENT) + public MenuInfo getClient(D display, MenuSerializationContext context, C menu) { + return getInternal((CategoryIdentifier) display.getCategoryIdentifier(), (Class) menu.getClass(), provider -> provider.provideClient(display, context, menu)); + } + + @Override + @Nullable + public MenuInfo get(CategoryIdentifier category, C menu, MenuSerializationContext context, CompoundTag tag) { + return getInternal(category, (Class) menu.getClass(), provider -> provider.provide(category, menu, context, tag)); + } + + private MenuInfo getInternal(CategoryIdentifier category, Class menuClass, Function, Optional>> function) { + Map, List>> infoMap = map.get(category); + if (infoMap != null && !infoMap.isEmpty()) { + if (infoMap.containsKey(menuClass)) { + for (MenuInfoProvider provider : infoMap.get(menuClass)) { + Optional> info = function.apply((MenuInfoProvider) provider); + if (info.isPresent()) { + return info.get(); + } + } + } + for (Map.Entry, List>> entry : infoMap.entrySet()) { + if (entry.getKey().isAssignableFrom(menuClass)) { + for (MenuInfoProvider provider : entry.getValue()) { + Optional> info = function.apply((MenuInfoProvider) provider); + if (info.isPresent()) { + return info.get(); + } + } + } + } + } + + for (Map.Entry>, List>> entry : mapGeneric.entrySet()) { + if (entry.getKey().test(category) && !entry.getValue().isEmpty()) { + List> infoList = entry.getValue(); + if (!infoList.isEmpty()) { + Optional> info = function.apply((MenuInfoProvider) infoList.get(0)); + if (info.isPresent()) { + return info.get(); + } + } + } + } + return null; + } + + @Override + public int infoSize() { + return map.size() + mapGeneric.size(); + } + + @Override + public void startReload() { + map.clear(); + mapGeneric.clear(); + } + + @Override + public void acceptPlugin(REIServerPlugin plugin) { + plugin.registerMenuInfo(this); + } +} diff --git a/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/plugin/autocrafting/DefaultCategoryHandler.java b/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/plugin/autocrafting/DefaultCategoryHandler.java new file mode 100644 index 000000000..3c1f69b29 --- /dev/null +++ b/runtime-engine/menu-info/src/main/java/me/shedaniel/rei/plugin/autocrafting/DefaultCategoryHandler.java @@ -0,0 +1,185 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.shedaniel.rei.plugin.autocrafting; + +import dev.architectury.networking.NetworkManager; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntSet; +import me.shedaniel.rei.RoughlyEnoughItemsNetwork; +import me.shedaniel.rei.api.client.ClientHelper; +import me.shedaniel.rei.api.client.registry.transfer.TransferHandler; +import me.shedaniel.rei.api.common.category.CategoryIdentifier; +import me.shedaniel.rei.api.common.display.Display; +import me.shedaniel.rei.api.common.entry.InputIngredient; +import me.shedaniel.rei.api.common.transfer.RecipeFinder; +import me.shedaniel.rei.api.common.transfer.info.MenuInfo; +import me.shedaniel.rei.api.common.transfer.info.MenuInfoContext; +import me.shedaniel.rei.api.common.transfer.info.MenuInfoRegistry; +import me.shedaniel.rei.api.common.transfer.info.MenuTransferException; +import me.shedaniel.rei.api.common.util.CollectionUtils; +import me.shedaniel.rei.api.common.util.EntryIngredients; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.recipebook.RecipeUpdateListener; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +@Environment(EnvType.CLIENT) +public class DefaultCategoryHandler implements TransferHandler { + @Override + public Result handle(Context context) { + Display display = context.getDisplay(); + AbstractContainerScreen containerScreen = context.getContainerScreen(); + if (containerScreen == null) { + return Result.createNotApplicable(); + } + AbstractContainerMenu menu = context.getMenu(); + MenuInfoContext menuInfoContext = ofContext(menu, display); + MenuInfo menuInfo = MenuInfoRegistry.getInstance().getClient(display, menuInfoContext, menu); + if (menuInfo == null) { + return Result.createNotApplicable(); + } + try { + menuInfo.validate(menuInfoContext); + } catch (MenuTransferException e) { + if (e.isApplicable()) { + return Result.createFailed(e.getError()); + } else { + return Result.createNotApplicable(); + } + } + List> input = menuInfo.getInputsIndexed(menuInfoContext, false); + List> missing = hasItemsIndexed(menuInfoContext, menu, menuInfo, display, input); + if (!missing.isEmpty()) { + IntList missingIndices = new IntArrayList(missing.size()); + for (InputIngredient ingredient : missing) { + missingIndices.add(ingredient.getIndex()); + } + IntSet missingIndicesSet = new IntLinkedOpenHashSet(missingIndices); + List> oldInputs = CollectionUtils.map(input, InputIngredient::get); + return Result.createFailed(new TranslatableComponent("error.rei.not.enough.materials")) + .renderer((matrices, mouseX, mouseY, delta, widgets, bounds, d) -> { + menuInfo.renderMissingInput(menuInfoContext, oldInputs, missingIndices, matrices, mouseX, mouseY, delta, widgets, bounds); + menuInfo.renderMissingInput(menuInfoContext, input, missing, missingIndicesSet, matrices, mouseX, mouseY, delta, widgets, bounds); + }) + .tooltipMissing(CollectionUtils.map(missing, ingredient -> EntryIngredients.ofItemStacks(ingredient.get()))); + } + if (!ClientHelper.getInstance().canUseMovePackets()) { + return Result.createFailed(new TranslatableComponent("error.rei.not.on.server")); + } + if (!context.isActuallyCrafting()) { + return Result.createSuccessful(); + } + + context.getMinecraft().setScreen(containerScreen); + if (containerScreen instanceof RecipeUpdateListener listener) { + listener.getRecipeBookComponent().ghostRecipe.clear(); + } + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeResourceLocation(display.getCategoryIdentifier().getIdentifier()); + buf.writeBoolean(context.isStackedCrafting()); + + buf.writeNbt(menuInfo.save(menuInfoContext, display)); + NetworkManager.sendToServer(RoughlyEnoughItemsNetwork.MOVE_ITEMS_PACKET, buf); + return Result.createSuccessful(); + } + + @Override + public double getPriority() { + return -10; + } + + private static MenuInfoContext ofContext(AbstractContainerMenu menu, Display display) { + return new MenuInfoContext() { + @Override + public AbstractContainerMenu getMenu() { + return menu; + } + + @Override + public Player getPlayerEntity() { + return Minecraft.getInstance().player; + } + + @Override + public CategoryIdentifier getCategoryIdentifier() { + return (CategoryIdentifier) display.getCategoryIdentifier(); + } + + @Override + public Display getDisplay() { + return display; + } + }; + } + + public IntList hasItems(MenuInfoContext menuInfoContext, AbstractContainerMenu menu, MenuInfo info, Display display, List> inputs) { + List> missing = hasItemsIndexed(menuInfoContext, menu, info, display, + CollectionUtils.mapIndexed(inputs, InputIngredient::of)); + IntList ids = new IntArrayList(missing.size()); + for (InputIngredient ingredient : missing) { + ids.add(ingredient.getIndex()); + } + return ids; + } + + public List> hasItemsIndexed(MenuInfoContext menuInfoContext, AbstractContainerMenu menu, MenuInfo info, Display display, List> inputs) { + // Create a clone of player's inventory, and count + RecipeFinder recipeFinder = new RecipeFinder(); + info.getRecipeFinderPopulator().populate(menuInfoContext, recipeFinder); + List> missing = new ArrayList<>(); + for (InputIngredient possibleStacks : inputs) { + boolean done = possibleStacks.get().isEmpty(); + for (ItemStack possibleStack : possibleStacks.get()) { + if (!done) { + int invRequiredCount = possibleStack.getCount(); + int key = RecipeFinder.getItemId(possibleStack); + while (invRequiredCount > 0 && recipeFinder.contains(key)) { + invRequiredCount--; + recipeFinder.take(key, 1); + } + if (invRequiredCount <= 0) { + done = true; + break; + } + } + } + if (!done) { + missing.add(possibleStacks); + } + } + return missing; + } +} -- cgit