diff options
Diffstat (limited to 'runtime-engine/menu-info/src/main/java')
3 files changed, 511 insertions, 0 deletions
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<T extends AbstractContainerMenu, D extends Display> implements MenuInfoContext<T, ServerPlayer, D> { + protected CategoryIdentifier<D> category; + protected T container; + protected MenuInfo<T, D> menuInfo; + private Iterable<SlotAccessor> inputStacks; + private Iterable<SlotAccessor> inventoryStacks; + private ServerPlayer player; + + private InputSlotCrafter(CategoryIdentifier<D> category, T container) { + this.category = category; + this.container = container; + } + + public void setMenuInfo(MenuInfo<T, D> menuInfo) { + this.menuInfo = menuInfo; + } + + public static <T extends AbstractContainerMenu, D extends Display> InputSlotCrafter<T, D> start(CategoryIdentifier<D> category, T menu, ServerPlayer player, CompoundTag display, boolean hasShift) { + InputSlotCrafter<T, D> crafter = new InputSlotCrafter<>(category, menu); + MenuInfo<T, D> 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<Ingredient> ingredients = NonNullList.create(); + for (InputIngredient<ItemStack> 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<SlotAccessor> inputStacks, Iterator<Integer> 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<Ingredient> 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<D> 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<CategoryIdentifier<?>, Map<Class<? extends AbstractContainerMenu>, List<MenuInfoProvider<?, ?>>>> map = Maps.newLinkedHashMap(); + private final Map<Predicate<CategoryIdentifier<?>>, List<MenuInfoProvider<?, ?>>> mapGeneric = Maps.newLinkedHashMap(); + + @Override + public <C extends AbstractContainerMenu, D extends Display> void register(CategoryIdentifier<D> category, Class<C> menuClass, MenuInfoProvider<C, D> menuInfo) { + map.computeIfAbsent(category, id -> Maps.newLinkedHashMap()) + .computeIfAbsent(menuClass, c -> Lists.newArrayList()) + .add(menuInfo); + } + + @Override + public <D extends Display> void registerGeneric(Predicate<CategoryIdentifier<?>> categoryPredicate, MenuInfoProvider<?, D> menuInfo) { + mapGeneric.computeIfAbsent(categoryPredicate, id -> Lists.newArrayList()).add(menuInfo); + } + + @Override + @Nullable + @Environment(EnvType.CLIENT) + public <C extends AbstractContainerMenu, D extends Display> MenuInfo<C, D> getClient(D display, MenuSerializationContext<C, ?, D> context, C menu) { + return getInternal((CategoryIdentifier<D>) display.getCategoryIdentifier(), (Class<C>) menu.getClass(), provider -> provider.provideClient(display, context, menu)); + } + + @Override + @Nullable + public <C extends AbstractContainerMenu, D extends Display> MenuInfo<C, D> get(CategoryIdentifier<D> category, C menu, MenuSerializationContext<C, ?, D> context, CompoundTag tag) { + return getInternal(category, (Class<C>) menu.getClass(), provider -> provider.provide(category, menu, context, tag)); + } + + private <C extends AbstractContainerMenu, D extends Display> MenuInfo<C, D> getInternal(CategoryIdentifier<D> category, Class<C> menuClass, Function<MenuInfoProvider<C, D>, Optional<MenuInfo<C, D>>> function) { + Map<Class<? extends AbstractContainerMenu>, List<MenuInfoProvider<?, ?>>> infoMap = map.get(category); + if (infoMap != null && !infoMap.isEmpty()) { + if (infoMap.containsKey(menuClass)) { + for (MenuInfoProvider<?, ?> provider : infoMap.get(menuClass)) { + Optional<MenuInfo<C, D>> info = function.apply((MenuInfoProvider<C, D>) provider); + if (info.isPresent()) { + return info.get(); + } + } + } + for (Map.Entry<Class<? extends AbstractContainerMenu>, List<MenuInfoProvider<?, ?>>> entry : infoMap.entrySet()) { + if (entry.getKey().isAssignableFrom(menuClass)) { + for (MenuInfoProvider<?, ?> provider : entry.getValue()) { + Optional<MenuInfo<C, D>> info = function.apply((MenuInfoProvider<C, D>) provider); + if (info.isPresent()) { + return info.get(); + } + } + } + } + } + + for (Map.Entry<Predicate<CategoryIdentifier<?>>, List<MenuInfoProvider<?, ?>>> entry : mapGeneric.entrySet()) { + if (entry.getKey().test(category) && !entry.getValue().isEmpty()) { + List<MenuInfoProvider<?, ?>> infoList = entry.getValue(); + if (!infoList.isEmpty()) { + Optional<MenuInfo<C, D>> info = function.apply((MenuInfoProvider<C, D>) 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<AbstractContainerMenu, Player, Display> menuInfoContext = ofContext(menu, display); + MenuInfo<AbstractContainerMenu, Display> 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<InputIngredient<ItemStack>> input = menuInfo.getInputsIndexed(menuInfoContext, false); + List<InputIngredient<ItemStack>> missing = hasItemsIndexed(menuInfoContext, menu, menuInfo, display, input); + if (!missing.isEmpty()) { + IntList missingIndices = new IntArrayList(missing.size()); + for (InputIngredient<ItemStack> ingredient : missing) { + missingIndices.add(ingredient.getIndex()); + } + IntSet missingIndicesSet = new IntLinkedOpenHashSet(missingIndices); + List<List<ItemStack>> 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<AbstractContainerMenu, Player, Display> ofContext(AbstractContainerMenu menu, Display display) { + return new MenuInfoContext<AbstractContainerMenu, Player, Display>() { + @Override + public AbstractContainerMenu getMenu() { + return menu; + } + + @Override + public Player getPlayerEntity() { + return Minecraft.getInstance().player; + } + + @Override + public CategoryIdentifier<Display> getCategoryIdentifier() { + return (CategoryIdentifier<Display>) display.getCategoryIdentifier(); + } + + @Override + public Display getDisplay() { + return display; + } + }; + } + + public IntList hasItems(MenuInfoContext<AbstractContainerMenu, Player, Display> menuInfoContext, AbstractContainerMenu menu, MenuInfo<AbstractContainerMenu, Display> info, Display display, List<List<ItemStack>> inputs) { + List<InputIngredient<ItemStack>> missing = hasItemsIndexed(menuInfoContext, menu, info, display, + CollectionUtils.mapIndexed(inputs, InputIngredient::of)); + IntList ids = new IntArrayList(missing.size()); + for (InputIngredient<ItemStack> ingredient : missing) { + ids.add(ingredient.getIndex()); + } + return ids; + } + + public List<InputIngredient<ItemStack>> hasItemsIndexed(MenuInfoContext<AbstractContainerMenu, Player, Display> menuInfoContext, AbstractContainerMenu menu, MenuInfo<AbstractContainerMenu, Display> info, Display display, List<InputIngredient<ItemStack>> inputs) { + // Create a clone of player's inventory, and count + RecipeFinder recipeFinder = new RecipeFinder(); + info.getRecipeFinderPopulator().populate(menuInfoContext, recipeFinder); + List<InputIngredient<ItemStack>> missing = new ArrayList<>(); + for (InputIngredient<ItemStack> 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; + } +} |
