├── jitpack.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── src └── main │ ├── resources │ ├── assets │ │ └── itemscroller │ │ │ ├── icon.png │ │ │ └── textures │ │ │ ├── gui │ │ │ └── gui_widgets.png │ │ │ └── xcf │ │ │ └── gui_widgets.xcf │ ├── itemscroller.accesswidener │ ├── mixins.itemscroller.json │ └── fabric.mod.json │ └── java │ └── fi │ └── dy │ └── masa │ └── itemscroller │ ├── villager │ ├── FavoriteData.java │ ├── IMerchantScreenHandler.java │ ├── VillagerData.java │ ├── TradeType.java │ ├── VillagerUtils.java │ └── VillagerDataStorage.java │ ├── util │ ├── MoveType.java │ ├── MoveAmount.java │ ├── MoveAction.java │ ├── ClickPacketBuffer.java │ ├── ItemType.java │ ├── AccessorUtils.java │ ├── SortingMethod.java │ ├── SortingCategory.java │ └── InputUtils.java │ ├── mixin │ ├── item │ │ ├── IMixinSlot.java │ │ └── MixinItemStack.java │ ├── screen │ │ ├── IMixinMerchantScreen.java │ │ ├── IMixinRecipeBookScreen.java │ │ ├── IMixinAbstractCraftingScreenHandler.java │ │ ├── MixinHandledScreen.java │ │ ├── IMixinScreenWithHandler.java │ │ ├── MixinScreen.java │ │ ├── MixinMerchantScreenHandler.java │ │ ├── MixinCraftingScreenHandler.java │ │ └── MixinMerchantScreen.java │ ├── recipe │ │ ├── IMixinCraftingResultSlot.java │ │ ├── IMixinRecipeBookWidget.java │ │ ├── IMixinClientRecipeBook.java │ │ └── MixinClientRecipeBook.java │ ├── MixinStatusEffectsDisplay.java │ └── network │ │ ├── MixinClientPlayerInteractionManager.java │ │ └── MixinClientPlayNetworkHandler.java │ ├── compat │ └── modmenu │ │ └── ModMenuImpl.java │ ├── Reference.java │ ├── ItemScroller.java │ ├── InitHandler.java │ ├── event │ ├── WorldLoadListener.java │ ├── InputHandler.java │ ├── RenderEventHandler.java │ └── KeybindCallbacks.java │ ├── gui │ ├── ItemScrollerIcons.java │ └── GuiConfigs.java │ ├── recipes │ ├── CraftingHandler.java │ └── RecipeStorage.java │ └── config │ ├── Hotkeys.java │ └── Configs.java ├── settings.gradle ├── .gitattributes ├── gradle.properties ├── README.md ├── .github └── workflows │ └── build.yml ├── gradlew.bat ├── LICENSE.txt └── gradlew /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sdk install java 21.0.9-tem 3 | - sdk use java 21.0.9-tem 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakura-ryoko/itemscroller/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .settings 3 | bin/ 4 | build/ 5 | eclipse/ 6 | .classpath 7 | .project 8 | build.number 9 | libs/ 10 | .idea/ 11 | run/ 12 | -------------------------------------------------------------------------------- /src/main/resources/assets/itemscroller/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakura-ryoko/itemscroller/HEAD/src/main/resources/assets/itemscroller/icon.png -------------------------------------------------------------------------------- /src/main/resources/itemscroller.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | accessible field net/minecraft/world/inventory/AbstractContainerMenu lastSlots Lnet/minecraft/core/NonNullList; -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/assets/itemscroller/textures/gui/gui_widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakura-ryoko/itemscroller/HEAD/src/main/resources/assets/itemscroller/textures/gui/gui_widgets.png -------------------------------------------------------------------------------- /src/main/resources/assets/itemscroller/textures/xcf/gui_widgets.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sakura-ryoko/itemscroller/HEAD/src/main/resources/assets/itemscroller/textures/xcf/gui_widgets.xcf -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/villager/FavoriteData.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.villager; 2 | 3 | import it.unimi.dsi.fastutil.ints.IntArrayList; 4 | 5 | public record FavoriteData(IntArrayList favorites, boolean isGlobal) 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/MoveType.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | public enum MoveType 4 | { 5 | NONE, 6 | MOVE_TO_OTHER, 7 | MOVE_TO_THIS, 8 | MOVE_UP, 9 | MOVE_DOWN, 10 | DROP 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/MoveAmount.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | public enum MoveAmount 4 | { 5 | NONE, 6 | MOVE_ONE, 7 | LEAVE_ONE, 8 | FULL_STACKS, 9 | ALL_MATCHING, 10 | EVERYTHING 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/villager/IMerchantScreenHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.villager; 2 | 3 | import net.minecraft.world.item.trading.MerchantOffers; 4 | 5 | public interface IMerchantScreenHandler 6 | { 7 | MerchantOffers itemscroller$getOriginalList(); 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/item/IMixinSlot.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.item; 2 | 3 | import net.minecraft.world.inventory.Slot; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Accessor; 6 | 7 | @Mixin(Slot.class) 8 | public interface IMixinSlot 9 | { 10 | @Accessor("slot") 11 | int itemscroller_getSlotIndex(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/IMixinMerchantScreen.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import net.minecraft.client.gui.screens.inventory.MerchantScreen; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Accessor; 6 | 7 | @Mixin(MerchantScreen.class) 8 | public interface IMixinMerchantScreen 9 | { 10 | @Accessor("shopItem") 11 | int itemscroller_getSelectedMerchantRecipe(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/recipe/IMixinCraftingResultSlot.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.recipe; 2 | 3 | import net.minecraft.world.inventory.CraftingContainer; 4 | import net.minecraft.world.inventory.ResultSlot; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | @Mixin(ResultSlot.class) 9 | public interface IMixinCraftingResultSlot 10 | { 11 | @Accessor("craftSlots") 12 | CraftingContainer itemscroller_getCraftingInventory(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/recipe/IMixinRecipeBookWidget.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.recipe; 2 | 3 | import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; 4 | import net.minecraft.world.item.crafting.display.RecipeDisplayId; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | @Mixin(RecipeBookComponent.class) 9 | public interface IMixinRecipeBookWidget 10 | { 11 | @Accessor("lastRecipe") 12 | RecipeDisplayId itemscroller_getSelectedRecipe(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/IMixinRecipeBookScreen.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; 4 | import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | @Mixin(AbstractRecipeBookScreen.class) 9 | public interface IMixinRecipeBookScreen 10 | { 11 | @Accessor("recipeBookComponent") 12 | RecipeBookComponent itemscroller_getRecipeBookWidget(); 13 | } 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # text stuff 2 | * text=auto 3 | *.bat text eol=crlf 4 | *.groovy text eol=lf 5 | *.java text eol=crlf 6 | *.md text 7 | *.properties text eol=lf 8 | *.scala text eol=lf 9 | *.sh text eol=lf 10 | .gitattributes text eol=lf 11 | .gitignore text eol=lf 12 | build.gradle text eol=lf 13 | gradlew text eol=lf 14 | gradle/wrapper/gradle-wrapper.properties text eol=crlf 15 | COPYING.txt text eol=lf 16 | COPYING.LESSER.txt text eol=lf 17 | README.md text eol=lf 18 | 19 | #binary 20 | *.dat binary 21 | *.bin binary 22 | *.png binary 23 | *.exe binary 24 | *.dll binary 25 | *.zip binary 26 | *.jar binary 27 | *.7z binary 28 | *.db binary 29 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/compat/modmenu/ModMenuImpl.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.compat.modmenu; 2 | 3 | import com.terraformersmc.modmenu.api.ConfigScreenFactory; 4 | import com.terraformersmc.modmenu.api.ModMenuApi; 5 | import fi.dy.masa.itemscroller.gui.GuiConfigs; 6 | 7 | public class ModMenuImpl implements ModMenuApi 8 | { 9 | @Override 10 | public ConfigScreenFactory getModConfigScreenFactory() 11 | { 12 | return (screen) -> { 13 | GuiConfigs gui = new GuiConfigs(); 14 | gui.setParent(screen); 15 | return gui; 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/recipe/IMixinClientRecipeBook.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.recipe; 2 | 3 | import java.util.Map; 4 | import net.minecraft.client.ClientRecipeBook; 5 | import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; 6 | import net.minecraft.world.item.crafting.display.RecipeDisplayId; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.gen.Accessor; 9 | 10 | @Mixin(ClientRecipeBook.class) 11 | public interface IMixinClientRecipeBook 12 | { 13 | @Accessor("known") 14 | Map itemscroller_getRecipeMap(); 15 | } 16 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs = -Xmx3G 2 | org.gradle.daemon = false 3 | org.gradle.cache.cleanup = false 4 | 5 | group = fi.dy.masa 6 | mod_id = itemscroller 7 | mod_name = Item Scroller 8 | author = masa 9 | mod_file_name = itemscroller-fabric 10 | 11 | # Current mod version 12 | mod_version = 0.30.1 13 | 14 | # Required malilib version 15 | malilib_version = 1.21.11-0.27.2 16 | 17 | # Minecraft, Fabric Loader and API and mappings versions 18 | minecraft_version_out = 1.21.11 19 | minecraft_version = 1.21.11 20 | mappings_version = 1.21.11+build.1 21 | 22 | fabric_loader_version = 0.18.2 23 | mod_menu_version = 17.0.0-alpha.1 24 | # fabric_api_version = 0.139.5+1.21.11 25 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/Reference.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller; 2 | 3 | import net.minecraft.SharedConstants; 4 | import fi.dy.masa.malilib.util.StringUtils; 5 | 6 | public class Reference 7 | { 8 | public static final String MOD_ID = "itemscroller"; 9 | public static final String MOD_NAME = "Item Scroller"; 10 | public static final String MOD_VERSION = StringUtils.getModVersionString(MOD_ID); 11 | public static final String MC_VERSION = SharedConstants.getCurrentVersion().id(); 12 | public static final String MOD_TYPE = "fabric"; 13 | public static final String MOD_STRING = MOD_ID + "-" + MOD_TYPE + "-" + MC_VERSION + "-" + MOD_VERSION; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/IMixinAbstractCraftingScreenHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import net.minecraft.world.inventory.AbstractCraftingMenu; 4 | import net.minecraft.world.inventory.CraftingContainer; 5 | import net.minecraft.world.inventory.ResultContainer; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.gen.Accessor; 8 | 9 | @Mixin(AbstractCraftingMenu.class) 10 | public interface IMixinAbstractCraftingScreenHandler 11 | { 12 | @Accessor("craftSlots") 13 | CraftingContainer itemscroller_getCraftingInventory(); 14 | 15 | @Accessor("resultSlots") 16 | ResultContainer itemscroller_getCraftingResultInventory(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/recipe/MixinClientRecipeBook.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.recipe; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.injection.At; 5 | import org.spongepowered.asm.mixin.injection.Inject; 6 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 7 | 8 | import fi.dy.masa.itemscroller.recipes.RecipeStorage; 9 | import net.minecraft.client.ClientRecipeBook; 10 | import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; 11 | 12 | @Mixin(ClientRecipeBook.class) 13 | public class MixinClientRecipeBook 14 | { 15 | @Inject(method = "add", at = @At("RETURN")) 16 | private void itemscroller_addToRecipeBook(RecipeDisplayEntry entry, CallbackInfo ci) 17 | { 18 | RecipeStorage.getInstance().onAddToRecipeBook(entry); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/MoveAction.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | public enum MoveAction 4 | { 5 | NONE, 6 | SCROLL_TO_OTHER_MOVE_ONE, 7 | SCROLL_TO_OTHER_STACKS, 8 | SCROLL_TO_OTHER_MATCHING, 9 | SCROLL_TO_OTHER_EVERYTHING, 10 | SCROLL_TO_THIS_MOVE_ONE, 11 | SCROLL_TO_THIS_STACKS, 12 | SCROLL_TO_THIS_MATCHING, 13 | MOVE_TO_OTHER_MOVE_ONE, 14 | MOVE_TO_OTHER_LEAVE_ONE, 15 | MOVE_TO_OTHER_STACKS, 16 | MOVE_TO_OTHER_MATCHING, 17 | MOVE_TO_OTHER_EVERYTHING, 18 | MOVE_UP_MOVE_ONE, 19 | MOVE_UP_LEAVE_ONE, 20 | MOVE_UP_STACKS, 21 | MOVE_UP_MATCHING, 22 | MOVE_DOWN_MOVE_ONE, 23 | MOVE_DOWN_LEAVE_ONE, 24 | MOVE_DOWN_STACKS, 25 | MOVE_DOWN_MATCHING, 26 | DROP_ONE, 27 | DROP_LEAVE_ONE, 28 | DROP_STACKS, 29 | DROP_ALL_MATCHING 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/ItemScroller.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller; 2 | 3 | import net.fabricmc.api.ModInitializer; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | import fi.dy.masa.malilib.event.InitializationHandler; 7 | import fi.dy.masa.itemscroller.config.Configs; 8 | 9 | public class ItemScroller implements ModInitializer 10 | { 11 | public static final Logger LOGGER = LogManager.getLogger(Reference.MOD_ID); 12 | 13 | @Override 14 | public void onInitialize() 15 | { 16 | InitializationHandler.getInstance().registerInitializationHandler(new InitHandler()); 17 | } 18 | 19 | public static void debugLog(String key, Object... args) 20 | { 21 | if (Configs.Generic.DEBUG_MESSAGES.getBooleanValue()) 22 | { 23 | LOGGER.info(key, args); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/sakura-ryoko/itemscroller.svg)](https://jitpack.io/#sakura-ryoko/itemscroller) 2 | 3 | Item Scroller 4 | ============== 5 | Item Scroller is a Minecraft mod that adds various convenience features for moving items 6 | inside inventory GUIs. Examples are scrolling the mouse wheel over slots with items in them 7 | or Shift/Ctrl + click + dragging over slots to move items from them in various ways etc. 8 | 9 | Item scrolling is basically what the old NEI mod did and Mouse Tweaks also does. 10 | This mod has some different drag features compared to Mouse Tweaks, and also some special 11 | villager trading related helper features as well as crafting helper features. 12 | 13 | For more information and downloads of the already compiled builds, 14 | see https://www.curseforge.com/minecraft/mc-mods/item-scroller 15 | 16 | Compiling 17 | ========= 18 | * Clone the repository 19 | * Open a command prompt/terminal to the repository directory 20 | * run 'gradlew build' 21 | * The built jar file will be in build/libs/ 22 | -------------------------------------------------------------------------------- /src/main/resources/mixins.itemscroller.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "fi.dy.masa.itemscroller.mixin", 4 | "compatibilityLevel": "JAVA_21", 5 | "minVersion": "0.8", 6 | "mixins": [ 7 | "MixinStatusEffectsDisplay", 8 | "item.IMixinSlot", 9 | "item.MixinItemStack", 10 | "network.MixinClientPlayerInteractionManager", 11 | "network.MixinClientPlayNetworkHandler", 12 | "recipe.IMixinClientRecipeBook", 13 | "recipe.IMixinCraftingResultSlot", 14 | "recipe.IMixinRecipeBookWidget", 15 | "recipe.MixinClientRecipeBook", 16 | "screen.IMixinAbstractCraftingScreenHandler", 17 | "screen.IMixinMerchantScreen", 18 | "screen.IMixinRecipeBookScreen", 19 | "screen.IMixinScreenWithHandler", 20 | "screen.MixinCraftingScreenHandler", 21 | "screen.MixinHandledScreen", 22 | "screen.MixinMerchantScreen", 23 | "screen.MixinMerchantScreenHandler", 24 | "screen.MixinScreen" 25 | ], 26 | "injectors": { 27 | "defaultRequire": 1 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/MixinHandledScreen.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import java.util.List; 4 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 5 | import net.minecraft.network.chat.Component; 6 | import net.minecraft.world.item.BundleItem; 7 | import net.minecraft.world.item.ItemStack; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 12 | 13 | import fi.dy.masa.itemscroller.util.InventoryUtils; 14 | 15 | @Mixin(AbstractContainerScreen.class) 16 | public class MixinHandledScreen 17 | { 18 | @Inject(method = "getTooltipFromContainerItem(Lnet/minecraft/world/item/ItemStack;)Ljava/util/List;", at = @At("HEAD")) 19 | private void itemscroller_ignore_bundleTooltipsForScrolling(ItemStack stack, CallbackInfoReturnable> cir) 20 | { 21 | InventoryUtils.setIgnoreScrollingInsideOfBundles(stack.getItem() instanceof BundleItem); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/IMixinScreenWithHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import net.minecraft.world.inventory.Slot; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Accessor; 6 | import org.spongepowered.asm.mixin.gen.Invoker; 7 | 8 | @Mixin(net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.class) 9 | public interface IMixinScreenWithHandler 10 | { 11 | @Invoker("getHoveredSlot") 12 | Slot itemscroller_getSlotAtPositionInvoker(double x, double y); 13 | 14 | @Invoker("slotClicked") 15 | void itemscroller_handleMouseClickInvoker(Slot slotIn, int slotId, int mouseButton, net.minecraft.world.inventory.ClickType type); 16 | 17 | @Accessor("hoveredSlot") 18 | Slot itemscroller_getHoveredSlot(); 19 | 20 | @Accessor("leftPos") 21 | int itemscroller_getGuiLeft(); 22 | 23 | @Accessor("topPos") 24 | int itemscroller_getGuiTop(); 25 | 26 | @Accessor("imageWidth") 27 | int itemscroller_getBackgroundWidth(); 28 | 29 | @Accessor("imageHeight") 30 | int itemscroller_getBackgroundHeight(); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/MixinStatusEffectsDisplay.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin; 2 | 3 | import java.util.Collection; 4 | import net.minecraft.client.gui.GuiGraphics; 5 | import net.minecraft.client.gui.screens.inventory.EffectsInInventory; 6 | import net.minecraft.world.effect.MobEffectInstance; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import fi.dy.masa.itemscroller.util.InputUtils; 12 | 13 | @Mixin(EffectsInInventory.class) 14 | public abstract class MixinStatusEffectsDisplay 15 | { 16 | @Inject(method = "renderEffects(Lnet/minecraft/client/gui/GuiGraphics;Ljava/util/Collection;IIIII)V", 17 | at = @At("HEAD"), cancellable = true) 18 | private void itemscroller_preventPotionEffectRendering(GuiGraphics context, Collection effects, int x, int height, int mouseX, int mouseY, int width, CallbackInfo ci) 19 | { 20 | if (InputUtils.isRecipeViewOpen()) 21 | { 22 | ci.cancel(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "itemscroller", 4 | "name": "Item Scroller", 5 | "version": "${mod_version}", 6 | 7 | "description": "Move items in inventory GUIs by scrolling the mouse wheel or dragging over slots", 8 | "authors": [ 9 | "masa" 10 | ], 11 | "contact": { 12 | "homepage": "https://www.curseforge.com/minecraft/mc-mods/item-scroller", 13 | "issues": "https://github.com/maruohon/itemscroller/issues", 14 | "sources": "https://github.com/maruohon/itemscroller", 15 | "twitter": "https://twitter.com/maruohon", 16 | "discord": "https://discordapp.com/channels/211786369951989762/453662800460644354/" 17 | }, 18 | 19 | "license": "LGPLv3", 20 | "icon": "assets/itemscroller/icon.png", 21 | "environment": "client", 22 | "entrypoints": { 23 | "main": [ 24 | "fi.dy.masa.itemscroller.ItemScroller" 25 | ], 26 | "modmenu": [ 27 | "fi.dy.masa.itemscroller.compat.modmenu.ModMenuImpl" 28 | ] 29 | }, 30 | 31 | "mixins": [ 32 | "mixins.itemscroller.json" 33 | ], 34 | "accessWidener": "itemscroller.accesswidener", 35 | 36 | "depends": { 37 | "minecraft": "1.21.11", 38 | "malilib": ">=0.27.2- <0.28.0-" 39 | }, 40 | "breaks": { 41 | "malilib": "<0.27.2-" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [ pull_request, push ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | # Use these Java versions 14 | java: [ 21 ] 15 | distro: [ temurin ] 16 | # and run on both Linux and Windows 17 | os: [ ubuntu-latest ] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: checkout repository 21 | uses: actions/checkout@v4 22 | - name: validate gradle wrapper 23 | uses: gradle/actions/wrapper-validation@v4 24 | - name: setup jdk ${{ matrix.java }} 25 | uses: actions/setup-java@v4 26 | with: 27 | distribution: ${{ matrix.distro }} 28 | java-version: ${{ matrix.java }} 29 | - name: make gradle wrapper executable 30 | if: ${{ runner.os != 'Windows' }} 31 | run: chmod +x ./gradlew 32 | - name: build 33 | run: ./gradlew build 34 | - name: capture build artifacts 35 | if: ${{ runner.os == 'Linux' }} 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: Artifacts 39 | path: build/libs/ 40 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/MixinScreen.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import org.spongepowered.asm.mixin.Final; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | 12 | import fi.dy.masa.itemscroller.event.RenderEventHandler; 13 | import fi.dy.masa.malilib.render.GuiContext; 14 | import net.minecraft.client.Minecraft; 15 | import net.minecraft.client.gui.GuiGraphics; 16 | import net.minecraft.client.gui.screens.Screen; 17 | 18 | @Mixin(Screen.class) 19 | public abstract class MixinScreen 20 | { 21 | /* 22 | @Inject(method = "renderWithTooltip", 23 | at = @At(value = "INVOKE", 24 | target = "Lnet/minecraft/client/gui/screen/Screen;render(Lnet/minecraft/client/gui/DrawContext;IIF)V", 25 | shift = At.Shift.AFTER)) 26 | private void itemscroller_inDrawScreenPre(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) 27 | { 28 | RenderEventHandler.instance().onDrawCraftingScreenBackground(MinecraftClient.getInstance(), context, mouseX, mouseY); 29 | } 30 | */ 31 | 32 | @Final @Shadow @Nullable protected Minecraft minecraft; 33 | 34 | @Inject(method = "renderWithTooltipAndSubtitles", at = @At(value = "TAIL")) 35 | private void itemscroller_onDrawScreenPost(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) 36 | { 37 | RenderEventHandler.instance().onDrawScreenPost(GuiContext.fromGuiGraphics(context), this.minecraft, mouseX, mouseY); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/InitHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller; 2 | 3 | import fi.dy.masa.malilib.config.ConfigManager; 4 | import fi.dy.masa.malilib.event.InputEventHandler; 5 | import fi.dy.masa.malilib.event.TickHandler; 6 | import fi.dy.masa.malilib.event.WorldLoadHandler; 7 | import fi.dy.masa.malilib.interfaces.IInitializationHandler; 8 | import fi.dy.masa.malilib.registry.Registry; 9 | import fi.dy.masa.malilib.util.data.ModInfo; 10 | import fi.dy.masa.itemscroller.config.Configs; 11 | import fi.dy.masa.itemscroller.event.InputHandler; 12 | import fi.dy.masa.itemscroller.event.KeybindCallbacks; 13 | import fi.dy.masa.itemscroller.event.WorldLoadListener; 14 | import fi.dy.masa.itemscroller.gui.GuiConfigs; 15 | 16 | public class InitHandler implements IInitializationHandler 17 | { 18 | @Override 19 | public void registerModHandlers() 20 | { 21 | ConfigManager.getInstance().registerConfigHandler(Reference.MOD_ID, new Configs()); 22 | Registry.CONFIG_SCREEN.registerConfigScreenFactory( 23 | new ModInfo(Reference.MOD_ID, Reference.MOD_NAME, GuiConfigs::new) 24 | ); 25 | 26 | InputHandler handler = new InputHandler(); 27 | InputEventHandler.getKeybindManager().registerKeybindProvider(handler); 28 | InputEventHandler.getInputManager().registerKeyboardInputHandler(handler); 29 | InputEventHandler.getInputManager().registerMouseInputHandler(handler); 30 | 31 | WorldLoadListener listener = new WorldLoadListener(); 32 | WorldLoadHandler.getInstance().registerWorldLoadPreHandler(listener); 33 | WorldLoadHandler.getInstance().registerWorldLoadPostHandler(listener); 34 | 35 | TickHandler.getInstance().registerClientTickHandler(KeybindCallbacks.getInstance()); 36 | 37 | KeybindCallbacks.getInstance().setCallbacks(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/item/MixinItemStack.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.item; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.injection.At; 5 | import org.spongepowered.asm.mixin.injection.Inject; 6 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 7 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 8 | 9 | import fi.dy.masa.itemscroller.config.Configs; 10 | import fi.dy.masa.itemscroller.util.InventoryUtils; 11 | import net.minecraft.client.Minecraft; 12 | import net.minecraft.world.item.ItemStack; 13 | 14 | @Mixin(ItemStack.class) 15 | public abstract class MixinItemStack 16 | { 17 | @Inject(method = "limitSize", at = @At("HEAD"), cancellable = true) 18 | private void dontCap(int maxCount, CallbackInfo ci) 19 | { 20 | // Client-side fx for empty shulker box stacking 21 | if (Minecraft.getInstance().isSameThread() && 22 | Configs.Generic.MOD_MAIN_TOGGLE.getBooleanValue() && 23 | Configs.Generic.SORT_INVENTORY_TOGGLE.getBooleanValue() && 24 | Configs.Generic.SORT_ASSUME_EMPTY_BOX_STACKS.getBooleanValue()) 25 | { 26 | ci.cancel(); 27 | } 28 | } 29 | 30 | @Inject(method = "getMaxStackSize", at = @At("HEAD"), cancellable = true) 31 | private void getMaxCount(CallbackInfoReturnable cir) 32 | { 33 | //System.out.printf("getMaxCount(): this item [%s] // Default Component [%d]\n", this.toString(), this.getComponents().getOrDefault(DataComponentTypes.MAX_STACK_SIZE, 1)); 34 | 35 | // Client-side fx for empty shulker box stacking 36 | if (Minecraft.getInstance().isSameThread() && 37 | Configs.Generic.MOD_MAIN_TOGGLE.getBooleanValue() && 38 | Configs.Generic.SORT_INVENTORY_TOGGLE.getBooleanValue() && 39 | Configs.Generic.SORT_ASSUME_EMPTY_BOX_STACKS.getBooleanValue() && 40 | InventoryUtils.assumeEmptyShulkerStacking) 41 | { 42 | cir.setReturnValue(InventoryUtils.stackMaxSize((ItemStack) (Object) this, true)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/network/MixinClientPlayerInteractionManager.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.network; 2 | 3 | import fi.dy.masa.itemscroller.util.ClickPacketBuffer; 4 | import net.minecraft.client.multiplayer.ClientPacketListener; 5 | import net.minecraft.client.multiplayer.MultiPlayerGameMode; 6 | import net.minecraft.network.protocol.Packet; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.Redirect; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | 13 | @Mixin(MultiPlayerGameMode.class) 14 | public class MixinClientPlayerInteractionManager 15 | { 16 | @Inject(method = "handleInventoryMouseClick", at = @At("HEAD"), cancellable = true) 17 | private void cancelWindowClicksWhileReplayingBufferedPackets(CallbackInfo ci) 18 | { 19 | if (ClickPacketBuffer.shouldCancelWindowClicks()) 20 | { 21 | ci.cancel(); 22 | } 23 | } 24 | 25 | @Redirect(method = "handleInventoryMouseClick", 26 | at = @At(value = "INVOKE", 27 | target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;send(Lnet/minecraft/network/protocol/Packet;)V")) 28 | private void bufferClickPacketsAndCancel(ClientPacketListener netHandler, Packet packet) 29 | { 30 | /* 31 | if (packet instanceof ClickSlotC2SPacket clickPacket) 32 | { 33 | MinecraftClient mc = MinecraftClient.getInstance(); 34 | System.out.printf("clickPacket: type: %s button: %d, slot: %d, (after) cursor item: %s\n", clickPacket.getActionType(), clickPacket.getButton(), clickPacket.getSlot(), clickPacket.getStack()); 35 | clickPacket.getModifiedStacks().forEach((integer, stack) -> System.out.printf("%d = %s, ", integer, stack)); 36 | System.out.println(); 37 | } 38 | */ 39 | if (ClickPacketBuffer.shouldBufferClickPackets()) 40 | { 41 | ClickPacketBuffer.bufferPacket(packet); 42 | return; 43 | } 44 | 45 | netHandler.send(packet); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/ClickPacketBuffer.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | import java.util.ArrayDeque; 4 | import java.util.Queue; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.network.protocol.Packet; 7 | 8 | public class ClickPacketBuffer 9 | { 10 | private static final Queue> BUFFER = new ArrayDeque<>(2048); 11 | private static boolean shouldBufferPackets; 12 | private static boolean hasBufferedPackets; 13 | 14 | public static void reset() 15 | { 16 | shouldBufferPackets = false; 17 | hasBufferedPackets = false; 18 | BUFFER.clear(); 19 | } 20 | 21 | public static int getBufferedActionsCount() 22 | { 23 | return BUFFER.size(); 24 | } 25 | 26 | public static boolean shouldBufferClickPackets() 27 | { 28 | return shouldBufferPackets; 29 | } 30 | 31 | public static boolean shouldCancelWindowClicks() 32 | { 33 | // Don't cancel the clicks on the client if we have some Item Scroller actions in progress 34 | return shouldBufferPackets == false && BUFFER.isEmpty() == false; 35 | } 36 | 37 | public static void setShouldBufferClickPackets(boolean shouldBuffer) 38 | { 39 | shouldBufferPackets = shouldBuffer; 40 | } 41 | 42 | public static void bufferPacket(Packet packet) 43 | { 44 | BUFFER.offer(packet); 45 | hasBufferedPackets = true; 46 | } 47 | 48 | public static void sendBufferedPackets(int maxCount) 49 | { 50 | Minecraft mc = Minecraft.getInstance(); 51 | 52 | if (hasBufferedPackets) 53 | { 54 | if (mc.screen == null) 55 | { 56 | reset(); 57 | } 58 | else if (mc.player != null) 59 | { 60 | maxCount = Math.min(maxCount, BUFFER.size()); 61 | 62 | for (int i = 0; i < maxCount; ++i) 63 | { 64 | Packet packet = BUFFER.poll(); 65 | 66 | if (packet != null) 67 | { 68 | mc.player.connection.send(packet); 69 | } 70 | } 71 | 72 | hasBufferedPackets = BUFFER.isEmpty() == false; 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/MixinMerchantScreenHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | import org.spongepowered.asm.mixin.Final; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.Shadow; 7 | import org.spongepowered.asm.mixin.Unique; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 12 | import fi.dy.masa.itemscroller.config.Configs; 13 | import fi.dy.masa.itemscroller.villager.IMerchantScreenHandler; 14 | import fi.dy.masa.itemscroller.villager.VillagerUtils; 15 | import net.minecraft.world.inventory.AbstractContainerMenu; 16 | import net.minecraft.world.inventory.MenuType; 17 | import net.minecraft.world.inventory.MerchantMenu; 18 | import net.minecraft.world.item.trading.Merchant; 19 | import net.minecraft.world.item.trading.MerchantOffers; 20 | 21 | @Mixin(MerchantMenu.class) 22 | public abstract class MixinMerchantScreenHandler extends AbstractContainerMenu implements IMerchantScreenHandler 23 | { 24 | @Shadow @Final private Merchant trader; 25 | @Unique @Nullable private MerchantOffers customList; 26 | 27 | protected MixinMerchantScreenHandler(@Nullable MenuType type, int syncId) 28 | { 29 | super(type, syncId); 30 | } 31 | 32 | @Inject(method = "getOffers", at = @At("HEAD"), cancellable = true) 33 | private void replaceTradeList(CallbackInfoReturnable cir) 34 | { 35 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue() && this.customList != null) 36 | { 37 | cir.setReturnValue(this.customList); 38 | } 39 | } 40 | 41 | @Inject(method = "setOffers", at = @At("HEAD")) 42 | private void onTradeListSet(MerchantOffers offers, CallbackInfo ci) 43 | { 44 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue()) 45 | { 46 | this.customList = VillagerUtils.buildCustomTradeList(offers); 47 | } 48 | } 49 | 50 | @Override 51 | public MerchantOffers itemscroller$getOriginalList() 52 | { 53 | return this.trader.getOffers(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/ItemType.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | import javax.annotation.Nonnull; 4 | import net.minecraft.world.item.ItemStack; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import it.unimi.dsi.fastutil.ints.IntArrayList; 8 | 9 | /** 10 | * Wrapper class for ItemStack, which implements equals() 11 | * for the item, damage and NBT, but not stackSize. 12 | */ 13 | public record ItemType(ItemStack stack) 14 | { 15 | public ItemType(@Nonnull ItemStack stack) 16 | { 17 | this.stack = stack.isEmpty() ? InventoryUtils.EMPTY_STACK : InventoryUtils.copyStack(stack, false); 18 | } 19 | 20 | @Override 21 | public int hashCode() 22 | { 23 | final int prime = 31; 24 | int result = 1; 25 | //result = prime * result + ((stack == null) ? 0 : stack.hashCode()); 26 | result = prime * result + this.stack.getItem().hashCode(); 27 | result = prime * result + (this.stack.getComponents() != null ? this.stack.getComponents().hashCode() : 0); 28 | return result; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object obj) 33 | { 34 | if (this == obj) 35 | return true; 36 | if (obj == null) 37 | return false; 38 | if (this.getClass() != obj.getClass()) 39 | return false; 40 | 41 | ItemType other = (ItemType) obj; 42 | 43 | return ItemStack.isSameItemSameComponents(this.stack, other.stack); 44 | } 45 | 46 | /** 47 | * Returns a map that has a list of the indices for each different item in the input list 48 | * 49 | * @param stacks () 50 | * @return () 51 | */ 52 | public static Map getSlotsPerItem(ItemStack[] stacks) 53 | { 54 | Map mapSlots = new HashMap<>(); 55 | 56 | for (int i = 0; i < stacks.length; i++) 57 | { 58 | ItemStack stack = stacks[i]; 59 | 60 | if (InventoryUtils.isStackEmpty(stack) == false) 61 | { 62 | ItemType item = new ItemType(stack); 63 | IntArrayList slots = mapSlots.computeIfAbsent(item, k -> new IntArrayList()); 64 | 65 | slots.add(i); 66 | } 67 | } 68 | 69 | return mapSlots; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/AccessorUtils.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | import javax.annotation.Nullable; 4 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 5 | import net.minecraft.client.gui.screens.inventory.MerchantScreen; 6 | import net.minecraft.world.inventory.ClickType; 7 | import net.minecraft.world.inventory.Slot; 8 | import fi.dy.masa.itemscroller.mixin.screen.IMixinMerchantScreen; 9 | import fi.dy.masa.itemscroller.mixin.screen.IMixinScreenWithHandler; 10 | import fi.dy.masa.itemscroller.mixin.item.IMixinSlot; 11 | 12 | public class AccessorUtils 13 | { 14 | @Nullable 15 | public static Slot getSlotUnderMouse(AbstractContainerScreen gui) 16 | { 17 | return ((IMixinScreenWithHandler) gui).itemscroller_getHoveredSlot(); 18 | } 19 | 20 | @Nullable 21 | public static Slot getSlotAtPosition(AbstractContainerScreen gui, double x, double y) 22 | { 23 | return ((IMixinScreenWithHandler) gui).itemscroller_getSlotAtPositionInvoker(x, y); 24 | } 25 | 26 | public static void handleMouseClick(AbstractContainerScreen gui, Slot slotIn, int slotId, int mouseButton, ClickType type) 27 | { 28 | ((IMixinScreenWithHandler) gui).itemscroller_handleMouseClickInvoker(slotIn, slotId, mouseButton, type); 29 | } 30 | 31 | public static int getGuiLeft(AbstractContainerScreen gui) 32 | { 33 | return ((IMixinScreenWithHandler) gui).itemscroller_getGuiLeft(); 34 | } 35 | 36 | public static int getGuiTop(AbstractContainerScreen gui) 37 | { 38 | return ((IMixinScreenWithHandler) gui).itemscroller_getGuiTop(); 39 | } 40 | 41 | public static int getGuiXSize(AbstractContainerScreen gui) 42 | { 43 | return ((IMixinScreenWithHandler) gui).itemscroller_getBackgroundWidth(); 44 | } 45 | 46 | public static int getGuiYSize(AbstractContainerScreen gui) 47 | { 48 | return ((IMixinScreenWithHandler) gui).itemscroller_getBackgroundHeight(); 49 | } 50 | 51 | public static int getSelectedMerchantRecipe(MerchantScreen gui) 52 | { 53 | return ((IMixinMerchantScreen) gui).itemscroller_getSelectedMerchantRecipe(); 54 | } 55 | 56 | public static int getSlotIndex(Slot slot) 57 | { 58 | return ((IMixinSlot) slot).itemscroller_getSlotIndex(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/event/WorldLoadListener.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.event; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.multiplayer.ClientLevel; 7 | import net.minecraft.core.RegistryAccess; 8 | import fi.dy.masa.malilib.interfaces.IWorldLoadListener; 9 | import fi.dy.masa.itemscroller.config.Configs; 10 | import fi.dy.masa.itemscroller.recipes.RecipeStorage; 11 | import fi.dy.masa.itemscroller.util.ClickPacketBuffer; 12 | import fi.dy.masa.itemscroller.villager.VillagerDataStorage; 13 | 14 | public class WorldLoadListener implements IWorldLoadListener 15 | { 16 | @Override 17 | public void onWorldLoadPre(@Nullable ClientLevel worldBefore, @Nullable ClientLevel worldAfter, Minecraft mc) 18 | { 19 | // Quitting to main menu, save the settings before the integrated server gets shut down 20 | if (worldBefore != null && worldAfter == null) 21 | { 22 | this.writeData(worldBefore.registryAccess()); 23 | VillagerDataStorage.getInstance().writeToDisk(); 24 | } 25 | } 26 | 27 | @Override 28 | public void onWorldLoadPost(@Nullable ClientLevel worldBefore, @Nullable ClientLevel worldAfter, Minecraft mc) 29 | { 30 | RecipeStorage.getInstance().reset(worldAfter == null); 31 | 32 | // Logging in to a world, load the data 33 | if (worldBefore == null && worldAfter != null) 34 | { 35 | this.readStoredData(worldAfter.registryAccess()); 36 | VillagerDataStorage.getInstance().readFromDisk(); 37 | } 38 | 39 | // Logging out 40 | if (worldAfter == null) 41 | { 42 | ClickPacketBuffer.reset(); 43 | } 44 | } 45 | 46 | private void writeData(@Nonnull RegistryAccess registryManager) 47 | { 48 | if (Configs.Generic.SCROLL_CRAFT_STORE_RECIPES_TO_FILE.getBooleanValue()) 49 | { 50 | RecipeStorage.getInstance().writeToDisk(registryManager); 51 | } 52 | } 53 | 54 | private void readStoredData(@Nonnull RegistryAccess registryManager) 55 | { 56 | if (Configs.Generic.SCROLL_CRAFT_STORE_RECIPES_TO_FILE.getBooleanValue()) 57 | { 58 | RecipeStorage.getInstance().readFromDisk(registryManager); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/MixinCraftingScreenHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import org.spongepowered.asm.mixin.Final; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.Shadow; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | import fi.dy.masa.itemscroller.config.Configs; 11 | import fi.dy.masa.itemscroller.util.InventoryUtils; 12 | import net.minecraft.client.Minecraft; 13 | import net.minecraft.server.level.ServerLevel; 14 | import net.minecraft.world.entity.player.Player; 15 | import net.minecraft.world.inventory.AbstractContainerMenu; 16 | import net.minecraft.world.inventory.CraftingContainer; 17 | import net.minecraft.world.inventory.CraftingMenu; 18 | import net.minecraft.world.inventory.ResultContainer; 19 | import net.minecraft.world.item.crafting.CraftingRecipe; 20 | import net.minecraft.world.item.crafting.RecipeHolder; 21 | 22 | @Mixin(CraftingMenu.class) 23 | public abstract class MixinCraftingScreenHandler 24 | { 25 | @Shadow @Final private Player player; 26 | 27 | @Inject(method = "slotsChanged", at = @At("RETURN")) 28 | private void onSlotChangedCraftingGrid(net.minecraft.world.Container inventory, CallbackInfo ci) 29 | { 30 | if (Minecraft.getInstance().isSameThread() && 31 | Configs.Generic.MOD_MAIN_TOGGLE.getBooleanValue()) 32 | { 33 | InventoryUtils.onSlotChangedCraftingGrid(this.player, 34 | ((IMixinAbstractCraftingScreenHandler) this).itemscroller_getCraftingInventory(), 35 | ((IMixinAbstractCraftingScreenHandler) this).itemscroller_getCraftingResultInventory()); 36 | } 37 | } 38 | 39 | @Inject(method = "slotChangedCraftingGrid", at = @At("RETURN")) 40 | private static void onUpdateResult( 41 | AbstractContainerMenu handler, ServerLevel serverWorld, Player player, CraftingContainer craftingInventory, ResultContainer resultInventory, RecipeHolder recipe, CallbackInfo ci) 42 | { 43 | if (Minecraft.getInstance().isSameThread() && 44 | Configs.Generic.MOD_MAIN_TOGGLE.getBooleanValue()) 45 | { 46 | InventoryUtils.onSlotChangedCraftingGrid(player, craftingInventory, resultInventory); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/gui/ItemScrollerIcons.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.gui; 2 | 3 | import fi.dy.masa.malilib.gui.interfaces.IGuiIcon; 4 | import fi.dy.masa.malilib.render.GuiContext; 5 | import fi.dy.masa.malilib.render.RenderUtils; 6 | import net.minecraft.resources.Identifier; 7 | import fi.dy.masa.itemscroller.Reference; 8 | 9 | public enum ItemScrollerIcons implements IGuiIcon 10 | { 11 | TRADE_ARROW_AVAILABLE (112, 0, 10, 9), 12 | TRADE_ARROW_LOCKED (112, 9, 10, 9), 13 | SCROLL_BAR_6 (106, 0, 6, 167), 14 | STAR_5_YELLOW (112, 18, 5, 5), 15 | STAR_5_PURPLE (117, 18, 5, 5); 16 | 17 | public static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(Reference.MOD_ID, "textures/gui/gui_widgets.png"); 18 | 19 | private final int u; 20 | private final int v; 21 | private final int w; 22 | private final int h; 23 | private final int hoverOffU; 24 | private final int hoverOffV; 25 | 26 | ItemScrollerIcons(int u, int v, int w, int h) 27 | { 28 | this(u, v, w, h, w, 0); 29 | } 30 | 31 | ItemScrollerIcons(int u, int v, int w, int h, int hoverOffU, int hoverOffV) 32 | { 33 | this.u = u; 34 | this.v = v; 35 | this.w = w; 36 | this.h = h; 37 | this.hoverOffU = hoverOffU; 38 | this.hoverOffV = hoverOffV; 39 | } 40 | 41 | @Override 42 | public int getWidth() 43 | { 44 | return this.w; 45 | } 46 | 47 | @Override 48 | public int getHeight() 49 | { 50 | return this.h; 51 | } 52 | 53 | @Override 54 | public int getU() 55 | { 56 | return this.u; 57 | } 58 | 59 | @Override 60 | public int getV() 61 | { 62 | return this.v; 63 | } 64 | 65 | @Override 66 | public void renderAt(GuiContext ctx, int x, int y, float zLevel, boolean enabled, boolean selected) 67 | { 68 | int u = this.u; 69 | int v = this.v; 70 | 71 | if (enabled) 72 | { 73 | u += this.hoverOffU; 74 | v += this.hoverOffV; 75 | } 76 | 77 | if (selected) 78 | { 79 | u += this.hoverOffU; 80 | v += this.hoverOffV; 81 | } 82 | 83 | RenderUtils.drawTexturedRect(ctx, this.getTexture(), x, y, u, v, this.w, this.h, zLevel); 84 | } 85 | 86 | @Override 87 | public Identifier getTexture() 88 | { 89 | return TEXTURE; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/villager/VillagerData.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.villager; 2 | 3 | import java.util.UUID; 4 | import javax.annotation.Nullable; 5 | import it.unimi.dsi.fastutil.ints.IntArrayList; 6 | 7 | import fi.dy.masa.malilib.util.data.Constants; 8 | import fi.dy.masa.malilib.util.data.tag.CompoundData; 9 | import fi.dy.masa.malilib.util.data.tag.IntData; 10 | import fi.dy.masa.malilib.util.data.tag.ListData; 11 | 12 | public class VillagerData 13 | { 14 | private final UUID uuid; 15 | private final IntArrayList favorites = new IntArrayList(); 16 | private int tradeListPosition; 17 | 18 | VillagerData(UUID uuid) 19 | { 20 | this.uuid = uuid; 21 | } 22 | 23 | public UUID getUUID() 24 | { 25 | return this.uuid; 26 | } 27 | 28 | public int getTradeListPosition() 29 | { 30 | return this.tradeListPosition; 31 | } 32 | 33 | void setTradeListPosition(int position) 34 | { 35 | this.tradeListPosition = position; 36 | } 37 | 38 | void toggleFavorite(int tradeIndex) 39 | { 40 | if (this.favorites.contains(tradeIndex)) 41 | { 42 | this.favorites.rem(tradeIndex); 43 | } 44 | else 45 | { 46 | this.favorites.add(tradeIndex); 47 | } 48 | } 49 | 50 | IntArrayList getFavorites() 51 | { 52 | return this.favorites; 53 | } 54 | 55 | public CompoundData toNBT() 56 | { 57 | CompoundData data = new CompoundData(); 58 | 59 | data.putLong("UUIDM", this.uuid.getMostSignificantBits()); 60 | data.putLong("UUIDL", this.uuid.getLeastSignificantBits()); 61 | data.putInt("ListPosition", this.tradeListPosition); 62 | 63 | ListData tagList = new ListData(); 64 | 65 | for (Integer val : this.favorites) 66 | { 67 | tagList.add(new IntData(val)); 68 | } 69 | 70 | data.put("Favorites", tagList); 71 | 72 | return data; 73 | } 74 | 75 | @Nullable 76 | public static VillagerData fromNBT(CompoundData tag) 77 | { 78 | if (tag.contains("UUIDM", Constants.NBT.TAG_LONG) && tag.contains("UUIDL", Constants.NBT.TAG_LONG)) 79 | { 80 | VillagerData data = new VillagerData(new UUID(tag.getLong("UUIDM"), tag.getLong("UUIDL"))); 81 | ListData tagList = tag.getList("Favorites"); 82 | final int count = tagList.size(); 83 | 84 | data.favorites.clear(); 85 | data.tradeListPosition = tag.getInt("ListPosition"); 86 | 87 | for (int i = 0; i < count; ++i) 88 | { 89 | data.favorites.add(tagList.getIntAt(i)); 90 | } 91 | 92 | return data; 93 | } 94 | 95 | return null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/network/MixinClientPlayNetworkHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.network; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.injection.At; 5 | import org.spongepowered.asm.mixin.injection.Inject; 6 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 7 | 8 | import fi.dy.masa.itemscroller.util.InventoryUtils; 9 | import net.minecraft.client.multiplayer.ClientPacketListener; 10 | import net.minecraft.network.protocol.game.ClientboundAwardStatsPacket; 11 | import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; 12 | import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; 13 | 14 | @Mixin(ClientPacketListener.class) 15 | public class MixinClientPlayNetworkHandler 16 | { 17 | @Inject(method = "handleAwardStats", at = @At("RETURN"), cancellable = true) 18 | private void onPong(ClientboundAwardStatsPacket packet, CallbackInfo ci) 19 | { 20 | if (InventoryUtils.onPong(packet)) 21 | { 22 | ci.cancel(); 23 | } 24 | } 25 | 26 | // @Inject(method = "onScreenHandlerSlotUpdate", at = @At("RETURN")) 27 | // private void onScreenHandlerSlotUpdate(ScreenHandlerSlotUpdateS2CPacket packet, CallbackInfo ci) 28 | // { 29 | // KeybindCallbacks.getInstance().onPacket(packet); 30 | // } 31 | 32 | @Inject( 33 | method = "handleContainerContent", 34 | at = @At( 35 | value = "INVOKE", 36 | target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/network/PacketProcessor;)V", 37 | shift = At.Shift.AFTER 38 | ), 39 | cancellable = true) 40 | private void onInventory(ClientboundContainerSetContentPacket packet, CallbackInfo ci) 41 | { 42 | if (InventoryUtils.bufferInvUpdates) 43 | { 44 | InventoryUtils.invUpdatesBuffer.add(packet); 45 | ci.cancel(); 46 | } 47 | } 48 | 49 | @Inject( 50 | method = "handleContainerSetSlot", 51 | at = @At( 52 | value = "INVOKE", 53 | target = "Lnet/minecraft/network/protocol/PacketUtils;ensureRunningOnSameThread(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;Lnet/minecraft/network/PacketProcessor;)V", 54 | shift = At.Shift.AFTER 55 | ), 56 | cancellable = true 57 | ) 58 | private void onScreenHandlerSlotUpdateInvokeMainThread(ClientboundContainerSetSlotPacket packet, CallbackInfo ci) 59 | { 60 | if (InventoryUtils.bufferInvUpdates) 61 | { 62 | InventoryUtils.invUpdatesBuffer.add(packet); 63 | ci.cancel(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/SortingMethod.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | import javax.annotation.Nonnull; 4 | import net.minecraft.util.StringRepresentable; 5 | import com.google.common.collect.ImmutableList; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import fi.dy.masa.malilib.config.IConfigOptionListEntry; 9 | import fi.dy.masa.malilib.util.StringUtils; 10 | import fi.dy.masa.itemscroller.Reference; 11 | 12 | public enum SortingMethod implements IConfigOptionListEntry, StringRepresentable 13 | { 14 | CATEGORY_NAME ("category_name", "category_name"), 15 | CATEGORY_COUNT ("category_count", "category_count"), 16 | CATEGORY_RARITY ("category_rarity", "category_rarity"), 17 | CATEGORY_RAWID ("category_rawid", "category_rawid"), 18 | ITEM_NAME ("item_name", "item_name"), 19 | ITEM_COUNT ("item_count", "item_count"), 20 | ITEM_RARITY ("item_rarity", "item_rarity"), 21 | ITEM_RAWID ("item_rawid", "item_rawid"); 22 | 23 | public static final StringRepresentable.EnumCodec CODEC = StringRepresentable.fromEnum(SortingMethod::values); 24 | public static final ImmutableList<@NotNull SortingMethod> VALUES = ImmutableList.copyOf(values()); 25 | 26 | private final String configString; 27 | private final String translationKey; 28 | 29 | SortingMethod(String configString, String translationKey) 30 | { 31 | this.configString = configString; 32 | this.translationKey = Reference.MOD_ID+".gui.label.sorting_method."+translationKey; 33 | } 34 | 35 | @Override 36 | public @Nonnull String getSerializedName() 37 | { 38 | return this.configString; 39 | } 40 | 41 | @Override 42 | public String getStringValue() 43 | { 44 | return this.configString; 45 | } 46 | 47 | @Override 48 | public String getDisplayName() 49 | { 50 | return StringUtils.getTranslatedOrFallback(this.translationKey, this.configString); 51 | } 52 | 53 | @Override 54 | public IConfigOptionListEntry cycle(boolean forward) 55 | { 56 | int id = this.ordinal(); 57 | 58 | if (forward) 59 | { 60 | if (++id >= values().length) 61 | { 62 | id = 0; 63 | } 64 | } 65 | else 66 | { 67 | if (--id < 0) 68 | { 69 | id = values().length - 1; 70 | } 71 | } 72 | 73 | return values()[id % values().length]; 74 | } 75 | 76 | @Override 77 | public SortingMethod fromString(String value) 78 | { 79 | return fromStringStatic(value); 80 | } 81 | 82 | public static SortingMethod fromStringStatic(String name) 83 | { 84 | for (SortingMethod val : VALUES) 85 | { 86 | if (val.configString.equalsIgnoreCase(name)) 87 | { 88 | return val; 89 | } 90 | } 91 | 92 | return SortingMethod.CATEGORY_NAME; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/gui/GuiConfigs.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.gui; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import com.google.common.collect.ImmutableList; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import fi.dy.masa.malilib.config.IConfigBase; 9 | import fi.dy.masa.malilib.gui.GuiConfigsBase; 10 | import fi.dy.masa.malilib.gui.button.ButtonBase; 11 | import fi.dy.masa.malilib.gui.button.ButtonGeneric; 12 | import fi.dy.masa.malilib.gui.button.IButtonActionListener; 13 | import fi.dy.masa.malilib.util.StringUtils; 14 | import fi.dy.masa.itemscroller.Reference; 15 | import fi.dy.masa.itemscroller.config.Configs; 16 | import fi.dy.masa.itemscroller.config.Hotkeys; 17 | 18 | public class GuiConfigs extends GuiConfigsBase 19 | { 20 | private static ConfigGuiTab tab = ConfigGuiTab.GENERIC; 21 | 22 | public GuiConfigs() 23 | { 24 | super(10, 50, Reference.MOD_ID, null, "itemscroller.gui.title.configs", String.format("%s", Reference.MOD_VERSION)); 25 | } 26 | 27 | @Override 28 | public void initGui() 29 | { 30 | super.initGui(); 31 | this.clearOptions(); 32 | 33 | int x = 10; 34 | int y = 26; 35 | 36 | for (ConfigGuiTab tab : ConfigGuiTab.VALUES) 37 | { 38 | x += this.createButton(x, y, -1, tab); 39 | } 40 | } 41 | 42 | private int createButton(int x, int y, int width, ConfigGuiTab tab) 43 | { 44 | ButtonGeneric button = new ButtonGeneric(x, y, width, 20, tab.getDisplayName()); 45 | button.setEnabled(GuiConfigs.tab != tab); 46 | this.addButton(button, new ButtonListener(tab, this)); 47 | 48 | return button.getWidth() + 2; 49 | } 50 | 51 | @Override 52 | protected int getConfigWidth() 53 | { 54 | ConfigGuiTab tab = GuiConfigs.tab; 55 | 56 | if (tab == ConfigGuiTab.GENERIC || tab == ConfigGuiTab.TOGGLES) 57 | { 58 | return 100; 59 | } 60 | 61 | return super.getConfigWidth(); 62 | } 63 | 64 | @Override 65 | public List getConfigs() 66 | { 67 | List configs; 68 | ConfigGuiTab tab = GuiConfigs.tab; 69 | 70 | if (tab == ConfigGuiTab.GENERIC) 71 | { 72 | configs = Configs.Generic.OPTIONS; 73 | } 74 | else if (tab == ConfigGuiTab.TOGGLES) 75 | { 76 | configs = Configs.Toggles.OPTIONS; 77 | } 78 | else if (tab == ConfigGuiTab.HOTKEYS) 79 | { 80 | configs = Hotkeys.HOTKEY_LIST; 81 | } 82 | else 83 | { 84 | return Collections.emptyList(); 85 | } 86 | 87 | return ConfigOptionWrapper.createFor(configs); 88 | } 89 | 90 | private record ButtonListener(ConfigGuiTab tab, GuiConfigs parent) implements IButtonActionListener 91 | { 92 | @Override 93 | public void actionPerformedWithButton(ButtonBase button, int mouseButton) 94 | { 95 | GuiConfigs.tab = this.tab; 96 | 97 | this.parent.reCreateListWidget(); // apply the new config width 98 | this.parent.getListWidget().resetScrollbarPosition(); 99 | this.parent.initGui(); 100 | } 101 | } 102 | 103 | public enum ConfigGuiTab 104 | { 105 | GENERIC ("itemscroller.gui.button.config_gui.generic"), 106 | TOGGLES ("itemscroller.gui.button.config_gui.toggles"), 107 | HOTKEYS ("itemscroller.gui.button.config_gui.hotkeys"); 108 | 109 | private final String translationKey; 110 | 111 | public static final ImmutableList<@NotNull ConfigGuiTab> VALUES = ImmutableList.copyOf(values()); 112 | 113 | ConfigGuiTab(String translationKey) 114 | { 115 | this.translationKey = translationKey; 116 | } 117 | 118 | public String getDisplayName() 119 | { 120 | return StringUtils.translate(this.translationKey); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/villager/TradeType.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.villager; 2 | 3 | import java.util.Optional; 4 | import javax.annotation.Nullable; 5 | import net.minecraft.core.Holder; 6 | import net.minecraft.core.registries.BuiltInRegistries; 7 | import net.minecraft.resources.Identifier; 8 | import net.minecraft.world.item.Item; 9 | import net.minecraft.world.item.ItemStack; 10 | import net.minecraft.world.item.Items; 11 | import net.minecraft.world.item.trading.MerchantOffer; 12 | import fi.dy.masa.malilib.util.data.tag.CompoundData; 13 | 14 | public class TradeType 15 | { 16 | public final Item buyItem1; 17 | public final Item buyItem2; 18 | public final Item sellItem; 19 | 20 | public TradeType(Item buyItem1, Item buyItem2, Item sellItem) 21 | { 22 | this.buyItem1 = buyItem1; 23 | this.buyItem2 = buyItem2; 24 | this.sellItem = sellItem; 25 | } 26 | 27 | public boolean matchesTrade(MerchantOffer trade) 28 | { 29 | ItemStack stackBuyItem1 = trade.getBaseCostA(); 30 | ItemStack stackBuyItem2 = trade.getCostB(); 31 | ItemStack stackSellItem = trade.getResult(); 32 | Item buyItem1 = stackBuyItem1.getItem(); 33 | Item buyItem2 = stackBuyItem2.getItem(); 34 | Item sellItem = stackSellItem.getItem(); 35 | 36 | return this.buyItem1 == buyItem1 && this.buyItem2 == buyItem2 && this.sellItem == sellItem; 37 | } 38 | 39 | public CompoundData toTag() 40 | { 41 | CompoundData tag = new CompoundData(); 42 | 43 | tag.putString("Buy1", getNameForItem(this.buyItem1)); 44 | tag.putString("Buy2", getNameForItem(this.buyItem2)); 45 | tag.putString("Sell", getNameForItem(this.sellItem)); 46 | 47 | return tag; 48 | } 49 | 50 | @Nullable 51 | public static TradeType fromTag(CompoundData tag) 52 | { 53 | Item buy1 = getItemForName(tag.getString("Buy1")); 54 | Item buy2 = getItemForName(tag.getString("Buy2")); 55 | Item sell = getItemForName(tag.getString("Sell")); 56 | 57 | if (buy1 != Items.AIR || buy2 != Items.AIR || sell != Items.AIR) 58 | { 59 | return new TradeType(buy1, buy2, sell); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | public static Item getItemForName(String name) 66 | { 67 | try 68 | { 69 | Identifier id = Identifier.tryParse(name); 70 | 71 | if (id != null) 72 | { 73 | Optional> opt = BuiltInRegistries.ITEM.get(id); 74 | return opt.map(Holder.Reference::value).orElse(Items.AIR); 75 | } 76 | } 77 | catch (Exception ignored) { } 78 | 79 | return Items.AIR; 80 | } 81 | 82 | public static String getNameForItem(Item item) 83 | { 84 | try 85 | { 86 | return BuiltInRegistries.ITEM.getKey(item).toString(); 87 | } 88 | catch (Exception e) 89 | { 90 | return "?"; 91 | } 92 | } 93 | 94 | @Override 95 | public boolean equals(Object o) 96 | { 97 | if (this == o) { return true; } 98 | if (o == null || getClass() != o.getClass()) { return false; } 99 | 100 | TradeType tradeType = (TradeType) o; 101 | 102 | if (!buyItem1.equals(tradeType.buyItem1)) { return false; } 103 | if (!buyItem2.equals(tradeType.buyItem2)) { return false; } 104 | return sellItem.equals(tradeType.sellItem); 105 | } 106 | 107 | @Override 108 | public int hashCode() 109 | { 110 | int result = buyItem1.hashCode(); 111 | result = 31 * result + buyItem2.hashCode(); 112 | result = 31 * result + sellItem.hashCode(); 113 | return result; 114 | } 115 | 116 | public static TradeType of(MerchantOffer trade) 117 | { 118 | ItemStack stackBuyItem1 = trade.getBaseCostA(); 119 | ItemStack stackBuyItem2 = trade.getCostB(); 120 | ItemStack stackSellItem = trade.getResult(); 121 | Item buyItem1 = stackBuyItem1.getItem(); 122 | Item buyItem2 = stackBuyItem2.getItem(); 123 | Item sellItem = stackSellItem.getItem(); 124 | 125 | return new TradeType(buyItem1, buyItem2, sellItem); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/villager/VillagerUtils.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.villager; 2 | 3 | import java.util.Collection; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import net.minecraft.client.Minecraft; 7 | import net.minecraft.client.gui.screens.Screen; 8 | import net.minecraft.client.gui.screens.inventory.MerchantScreen; 9 | import net.minecraft.network.protocol.game.ServerboundSelectTradePacket; 10 | import net.minecraft.world.inventory.MerchantMenu; 11 | import net.minecraft.world.item.trading.MerchantOffer; 12 | import net.minecraft.world.item.trading.MerchantOffers; 13 | import it.unimi.dsi.fastutil.ints.IntArrayList; 14 | import fi.dy.masa.malilib.util.GuiUtils; 15 | 16 | public class VillagerUtils 17 | { 18 | public static boolean switchToTradeByVisibleIndex(int visibleIndex) 19 | { 20 | Screen screen = GuiUtils.getCurrentScreen(); 21 | 22 | if (screen instanceof MerchantScreen merchantScreen) 23 | { 24 | MerchantMenu handler = merchantScreen.getMenu(); 25 | 26 | int realIndex = getRealTradeIndexFor(visibleIndex, handler); 27 | 28 | if (realIndex >= 0) 29 | { 30 | // Use the real (server-side) index 31 | handler.setSelectionHint(realIndex); 32 | 33 | // Use the "visible index", since this will access the custom list 34 | handler.tryMoveItems(visibleIndex); 35 | 36 | // Use the real (server-side) index 37 | Minecraft.getInstance().getConnection().send(new ServerboundSelectTradePacket(realIndex)); 38 | 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | public static int getRealTradeIndexFor(int visibleIndex, MerchantMenu handler) 47 | { 48 | if (handler instanceof IMerchantScreenHandler) 49 | { 50 | MerchantOffers originalList = ((IMerchantScreenHandler) handler).itemscroller$getOriginalList(); 51 | MerchantOffers customList = handler.getOffers(); 52 | 53 | if (originalList != null && customList != null && 54 | visibleIndex >= 0 && visibleIndex < customList.size()) 55 | { 56 | MerchantOffer trade = customList.get(visibleIndex); 57 | 58 | if (trade != null) 59 | { 60 | int realIndex = originalList.indexOf(trade); 61 | 62 | if (realIndex >= 0 && realIndex < originalList.size()) 63 | { 64 | return realIndex; 65 | } 66 | } 67 | } 68 | } 69 | 70 | return -1; 71 | } 72 | 73 | public static MerchantOffers buildCustomTradeList(MerchantOffers originalList) 74 | { 75 | FavoriteData data = VillagerDataStorage.getInstance().getFavoritesForCurrentVillager(originalList); 76 | IntArrayList favorites = data.favorites(); 77 | 78 | //System.out.printf("build - fav: %s (%s), or: %d\n", favorites, data.isGlobal, originalList.size()); 79 | 80 | // Some favorites defined 81 | if (favorites.isEmpty() == false) 82 | { 83 | MerchantOffers list = new MerchantOffers(); 84 | int originalListSize = originalList.size(); 85 | 86 | // First pick all the favorited recipes, in the order they are in the favorites list 87 | for (int index : favorites) 88 | { 89 | if (index >= 0 && index < originalListSize) 90 | { 91 | list.add(originalList.get(index)); 92 | } 93 | } 94 | 95 | // Then add the rest of the recipes in their original order 96 | for (int i = 0; i < originalListSize; ++i) 97 | { 98 | if (favorites.contains(i) == false) 99 | { 100 | list.add(originalList.get(i)); 101 | } 102 | } 103 | 104 | return list; 105 | } 106 | 107 | return originalList; 108 | } 109 | 110 | public static IntArrayList getGlobalFavoritesFor(MerchantOffers originalTrades, Collection globalFavorites) 111 | { 112 | IntArrayList favorites = new IntArrayList(); 113 | Map trades = new HashMap<>(); 114 | final int size = originalTrades.size(); 115 | 116 | // Build a map from the trade types to the indices in the current villager's trade list 117 | for (int i = 0; i < size; ++i) 118 | { 119 | MerchantOffer trade = originalTrades.get(i); 120 | trades.put(TradeType.of(trade), i); 121 | } 122 | 123 | // Pick the trade list indices that are in the global favorites, in the order that they were global favorited 124 | for (TradeType type : globalFavorites) 125 | { 126 | Integer index = trades.get(type); 127 | 128 | if (index != null) 129 | { 130 | favorites.add(index.intValue()); 131 | } 132 | } 133 | 134 | /* This is a version that is not sorted based on the order of the global favorites 135 | for (int i = 0; i < size; ++i) 136 | { 137 | TradeType type = TradeType.of(originalTrades.get(i)); 138 | 139 | if (globalFavorites.contains(type)) 140 | { 141 | favorites.add(i); 142 | } 143 | } 144 | */ 145 | //System.out.printf("getGlobalFavoritesFor - list: %s - or: %d | global: %s\n", favorites, originalTrades.size(), globalFavorites); 146 | 147 | return favorites; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/SortingCategory.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | import java.util.Collection; 4 | import java.util.Iterator; 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | import net.minecraft.client.Minecraft; 8 | import net.minecraft.core.registries.BuiltInRegistries; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.util.StringRepresentable; 11 | import net.minecraft.world.item.CreativeModeTab; 12 | import net.minecraft.world.item.ItemStack; 13 | import com.google.common.collect.ImmutableList; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import fi.dy.masa.malilib.config.IConfigLockedListEntry; 17 | import fi.dy.masa.malilib.config.IConfigLockedListType; 18 | import fi.dy.masa.malilib.util.StringUtils; 19 | import fi.dy.masa.itemscroller.Reference; 20 | 21 | public class SortingCategory implements IConfigLockedListType 22 | { 23 | public static final SortingCategory INSTANCE = new SortingCategory(); 24 | public ImmutableList<@NotNull Entry> VALUES = ImmutableList.copyOf(Entry.values()); 25 | //public static final Codec CODEC = Entry.CODEC.listOf().xmap(getDefault); 26 | 27 | @Nullable 28 | public CreativeModeTab.ItemDisplayParameters buildDisplayContext(Minecraft mc) 29 | { 30 | if (mc.level == null) 31 | { 32 | return null; 33 | } 34 | 35 | CreativeModeTab.ItemDisplayParameters ctx = new CreativeModeTab.ItemDisplayParameters(mc.level.enabledFeatures(), true, mc.level.registryAccess()); 36 | 37 | BuiltInRegistries.CREATIVE_MODE_TAB.stream().filter((group) -> 38 | group.getType() == CreativeModeTab.Type.CATEGORY).forEach((group) -> 39 | group.buildContents(ctx)); 40 | 41 | return ctx; 42 | } 43 | 44 | public Entry fromItemStack(ItemStack stack) 45 | { 46 | for (int i = 0; i < BuiltInRegistries.CREATIVE_MODE_TAB.size(); i++) 47 | { 48 | CreativeModeTab itemGroup = BuiltInRegistries.CREATIVE_MODE_TAB.byId(i); 49 | 50 | if (itemGroup != null && itemGroup.getType().equals(CreativeModeTab.Type.CATEGORY)) 51 | { 52 | Collection stacks; 53 | Iterator iter; 54 | 55 | if (itemGroup.hasAnyItems()) 56 | { 57 | stacks = itemGroup.getDisplayItems(); 58 | iter = stacks.iterator(); 59 | 60 | while (iter.hasNext()) 61 | { 62 | if (ItemStack.isSameItem(iter.next(), stack)) 63 | { 64 | return fromItemGroup(itemGroup); 65 | } 66 | } 67 | 68 | } 69 | 70 | stacks = itemGroup.getSearchTabDisplayItems(); 71 | iter = stacks.iterator(); 72 | 73 | while (iter.hasNext()) 74 | { 75 | if (ItemStack.isSameItem(iter.next(), stack)) 76 | { 77 | return fromItemGroup(itemGroup); 78 | } 79 | } 80 | 81 | } 82 | } 83 | 84 | return Entry.OTHER; 85 | } 86 | 87 | @Nullable 88 | public Entry fromItemGroup(CreativeModeTab group) 89 | { 90 | Identifier id = BuiltInRegistries.CREATIVE_MODE_TAB.getKey(group); 91 | 92 | if (id != null) 93 | { 94 | return Entry.fromString(id.getPath()); 95 | } 96 | 97 | return Entry.OTHER; 98 | } 99 | 100 | @Override 101 | public ImmutableList<@NotNull IConfigLockedListEntry> getDefaultEntries() 102 | { 103 | ImmutableList.Builder<@NotNull IConfigLockedListEntry> list = ImmutableList.builder(); 104 | 105 | VALUES.forEach((list::add)); 106 | 107 | return list.build(); 108 | } 109 | 110 | @Override 111 | @Nullable 112 | public IConfigLockedListEntry fromString(String string) 113 | { 114 | return Entry.fromString(string); 115 | } 116 | 117 | public enum Entry implements IConfigLockedListEntry, StringRepresentable 118 | { 119 | BUILDING_BLOCKS ("building_blocks", "building_blocks"), 120 | COLORED_BLOCKS ("colored_blocks", "colored_blocks"), 121 | NATURAL ("natural_blocks", "natural_blocks"), 122 | FUNCTIONAL ("functional_blocks", "functional_blocks"), 123 | REDSTONE ("redstone_blocks", "redstone_blocks"), 124 | TOOLS ("tools_and_utilities", "tools_and_utilities"), 125 | COMBAT ("combat", "combat"), 126 | FOOD_AND_DRINK ("food_and_drinks", "food_and_drinks"), 127 | INGREDIENTS ("ingredients", "ingredients"), 128 | SPAWN_EGGS ("spawn_eggs", "spawn_eggs"), 129 | OPERATOR ("op_blocks", "op_blocks"), 130 | OTHER ("other", "other"); 131 | 132 | public static final StringRepresentable.EnumCodec CODEC = StringRepresentable.fromEnum(Entry::values); 133 | public static final ImmutableList<@NotNull Entry> VALUES = ImmutableList.copyOf(values()); 134 | 135 | private final String configKey; 136 | private final String translationKey; 137 | 138 | Entry(String configKey, String translationKey) 139 | { 140 | this.configKey = configKey; 141 | this.translationKey = Reference.MOD_ID+".gui.label.sorting_category."+translationKey; 142 | } 143 | 144 | @Override 145 | public @Nonnull String getSerializedName() 146 | { 147 | return this.configKey; 148 | } 149 | 150 | @Override 151 | public String getStringValue() 152 | { 153 | return this.configKey; 154 | } 155 | 156 | @Override 157 | public String getDisplayName() 158 | { 159 | return StringUtils.getTranslatedOrFallback(this.translationKey, this.configKey); 160 | } 161 | 162 | @Nullable 163 | public static Entry fromString(String key) 164 | { 165 | for (Entry entry : values()) 166 | { 167 | if (entry.configKey.equalsIgnoreCase(key)) 168 | { 169 | return entry; 170 | } 171 | else if (entry.translationKey.equalsIgnoreCase(key)) 172 | { 173 | return entry; 174 | } 175 | else if (StringUtils.hasTranslation(entry.translationKey) && StringUtils.translate(entry.translationKey).equalsIgnoreCase(key)) 176 | { 177 | return entry; 178 | } 179 | } 180 | 181 | return null; 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/recipes/CraftingHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.recipes; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Map; 6 | import java.util.Set; 7 | import javax.annotation.Nullable; 8 | import net.minecraft.client.gui.screens.Screen; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.world.inventory.AbstractContainerMenu; 11 | import net.minecraft.world.inventory.Slot; 12 | import fi.dy.masa.itemscroller.ItemScroller; 13 | 14 | public class CraftingHandler 15 | { 16 | private static final Map CRAFTING_GRID_SLOTS = new HashMap<>(); 17 | private static final Set>> CRAFTING_GUIS = new HashSet<>(); 18 | 19 | public static void clearDefinitions() 20 | { 21 | CRAFTING_GRID_SLOTS.clear(); 22 | CRAFTING_GUIS.clear(); 23 | } 24 | 25 | @SuppressWarnings("unchecked") 26 | public static boolean addCraftingGridDefinition(String guiClassName, String slotClassName, int outputSlot, SlotRange range) 27 | { 28 | try 29 | { 30 | Class> guiClass = (Class>) Class.forName(guiClassName); 31 | Class slotClass = (Class) Class.forName(slotClassName); 32 | 33 | CRAFTING_GRID_SLOTS.put(new CraftingOutputSlot(guiClass, slotClass, outputSlot), range); 34 | CRAFTING_GUIS.add(guiClass); 35 | 36 | return true; 37 | } 38 | catch (Exception e) 39 | { 40 | ItemScroller.LOGGER.warn("addCraftingGridDefinition(): Failed to find classes for grid definition: gui: '{}', slot: '{}', outputSlot: {}", 41 | guiClassName, slotClassName, outputSlot); 42 | } 43 | 44 | return false; 45 | } 46 | 47 | public static boolean isCraftingGui(Screen gui) 48 | { 49 | return (gui instanceof AbstractContainerScreen) && CRAFTING_GUIS.contains(((AbstractContainerScreen) gui).getClass()); 50 | } 51 | 52 | /** 53 | * Gets the crafting grid SlotRange associated with the given slot in the given gui, if any. 54 | * 55 | * @param gui () 56 | * @param slot () 57 | * @return the SlotRange of the crafting grid, or null, if the given slot is not a crafting output slot 58 | */ 59 | @Nullable 60 | public static SlotRange getCraftingGridSlots(AbstractContainerScreen gui, Slot slot) 61 | { 62 | return CRAFTING_GRID_SLOTS.get(CraftingOutputSlot.from(gui, slot)); 63 | } 64 | 65 | @Nullable 66 | public static Slot getFirstCraftingOutputSlotForGui(AbstractContainerScreen gui) 67 | { 68 | if (CRAFTING_GUIS.contains(gui.getClass())) 69 | { 70 | for (Slot slot : gui.getMenu().slots) 71 | { 72 | if (getCraftingGridSlots(gui, slot) != null) 73 | { 74 | return slot; 75 | } 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | 82 | public static class CraftingOutputSlot 83 | { 84 | private final Class> guiClass; 85 | private final Class slotClass; 86 | private final int outputSlot; 87 | 88 | private CraftingOutputSlot(Class> guiClass, Class slotClass, int outputSlot) 89 | { 90 | this.guiClass = guiClass; 91 | this.slotClass = slotClass; 92 | this.outputSlot = outputSlot; 93 | } 94 | 95 | @SuppressWarnings("unchecked") 96 | public static CraftingOutputSlot from(AbstractContainerScreen gui, Slot slot) 97 | { 98 | return new CraftingOutputSlot((Class>) gui.getClass(), slot.getClass(), slot.index); 99 | } 100 | 101 | public Class> getGuiClass() 102 | { 103 | return this.guiClass; 104 | } 105 | 106 | public Class getSlotClass() 107 | { 108 | return this.slotClass; 109 | } 110 | 111 | public int getSlotNumber() 112 | { 113 | return this.outputSlot; 114 | } 115 | 116 | public boolean matches(AbstractContainerScreen gui, Slot slot, int outputSlot) 117 | { 118 | return outputSlot == this.outputSlot && gui.getClass() == this.guiClass && slot.getClass() == this.slotClass; 119 | } 120 | 121 | @Override 122 | public int hashCode() 123 | { 124 | final int prime = 31; 125 | int result = 1; 126 | result = prime * result + ((guiClass == null) ? 0 : guiClass.hashCode()); 127 | result = prime * result + outputSlot; 128 | result = prime * result + ((slotClass == null) ? 0 : slotClass.hashCode()); 129 | return result; 130 | } 131 | 132 | @Override 133 | public boolean equals(Object obj) 134 | { 135 | if (this == obj) 136 | { 137 | return true; 138 | } 139 | if (obj == null) 140 | { 141 | return false; 142 | } 143 | if (getClass() != obj.getClass()) 144 | { 145 | return false; 146 | } 147 | CraftingOutputSlot other = (CraftingOutputSlot) obj; 148 | if (guiClass == null) 149 | { 150 | if (other.guiClass != null) 151 | { 152 | return false; 153 | } 154 | } 155 | else if (guiClass != other.guiClass) 156 | { 157 | return false; 158 | } 159 | if (outputSlot != other.outputSlot) 160 | { 161 | return false; 162 | } 163 | if (slotClass == null) 164 | { 165 | return other.slotClass == null; 166 | } 167 | else return slotClass == other.slotClass; 168 | } 169 | 170 | } 171 | 172 | public static class SlotRange 173 | { 174 | private final int first; 175 | private final int last; 176 | 177 | public SlotRange(int start, int numSlots) 178 | { 179 | this.first = start; 180 | this.last = start + numSlots - 1; 181 | } 182 | 183 | public int getFirst() 184 | { 185 | return this.first; 186 | } 187 | 188 | public int getLast() 189 | { 190 | return this.last; 191 | } 192 | 193 | public int getSlotCount() 194 | { 195 | return this.last - this.first + 1; 196 | } 197 | 198 | public boolean contains(int slot) 199 | { 200 | return slot >= this.first && slot <= this.last; 201 | } 202 | 203 | @Override 204 | public String toString() 205 | { 206 | return String.format("SlotRange: {first: %d, last: %d}", this.first, this.last); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/config/Hotkeys.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.config; 2 | 3 | import java.util.List; 4 | import com.google.common.collect.ImmutableList; 5 | import fi.dy.masa.malilib.config.options.ConfigHotkey; 6 | import fi.dy.masa.malilib.hotkeys.KeyAction; 7 | import fi.dy.masa.malilib.hotkeys.KeybindSettings; 8 | import fi.dy.masa.itemscroller.Reference; 9 | 10 | public class Hotkeys 11 | { 12 | private static final KeybindSettings GUI_RELAXED = KeybindSettings.create(KeybindSettings.Context.GUI, KeyAction.PRESS, true, false, false, false); 13 | private static final KeybindSettings GUI_RELAXED_CANCEL = KeybindSettings.create(KeybindSettings.Context.GUI, KeyAction.PRESS, true, false, false, true); 14 | private static final KeybindSettings GUI_NO_ORDER = KeybindSettings.create(KeybindSettings.Context.GUI, KeyAction.PRESS, false, false, false, true); 15 | 16 | private static final String HOTKEYS_KEY = Reference.MOD_ID+".config.hotkeys"; 17 | 18 | public static final ConfigHotkey OPEN_CONFIG_GUI = new ConfigHotkey("openConfigGui", "I,C").apply(HOTKEYS_KEY); 19 | 20 | public static final ConfigHotkey CRAFT_EVERYTHING = new ConfigHotkey("craftEverything", "LEFT_CONTROL,C", GUI_NO_ORDER).apply(HOTKEYS_KEY); 21 | public static final ConfigHotkey DROP_ALL_MATCHING = new ConfigHotkey("dropAllMatching", "LEFT_CONTROL,LEFT_SHIFT,Q", GUI_NO_ORDER).apply(HOTKEYS_KEY); 22 | public static final ConfigHotkey MASS_CRAFT = new ConfigHotkey("massCraft", "LEFT_CONTROL,LEFT_ALT,C", GUI_NO_ORDER).apply(HOTKEYS_KEY); 23 | public static final ConfigHotkey MASS_CRAFT_TOGGLE = new ConfigHotkey("massCraftToggle", "").apply(HOTKEYS_KEY); 24 | public static final ConfigHotkey MOVE_CRAFT_RESULTS = new ConfigHotkey("moveCraftResults", "LEFT_CONTROL,M", GUI_NO_ORDER).apply(HOTKEYS_KEY); 25 | public static final ConfigHotkey RECIPE_VIEW = new ConfigHotkey("recipeView", "A", GUI_RELAXED).apply(HOTKEYS_KEY); 26 | public static final ConfigHotkey SLOT_DEBUG = new ConfigHotkey("slotDebug", "LEFT_CONTROL,LEFT_ALT,LEFT_SHIFT,I", GUI_NO_ORDER).apply(HOTKEYS_KEY); 27 | public static final ConfigHotkey STORE_RECIPE = new ConfigHotkey("storeRecipe", "BUTTON_3", GUI_RELAXED_CANCEL).apply(HOTKEYS_KEY); 28 | public static final ConfigHotkey THROW_CRAFT_RESULTS = new ConfigHotkey("throwCraftResults", "LEFT_CONTROL,T", GUI_NO_ORDER).apply(HOTKEYS_KEY); 29 | public static final ConfigHotkey TOGGLE_MOD_ON_OFF = new ConfigHotkey("toggleModOnOff", "", KeybindSettings.GUI).apply(HOTKEYS_KEY); 30 | public static final ConfigHotkey VILLAGER_TRADE_FAVORITES = new ConfigHotkey("villagerTradeFavorites","", KeybindSettings.GUI).apply(HOTKEYS_KEY); 31 | 32 | public static final ConfigHotkey KEY_DRAG_DROP_LEAVE_ONE = new ConfigHotkey("keyDragDropLeaveOne", "LEFT_SHIFT,Q,BUTTON_2", GUI_NO_ORDER).apply(HOTKEYS_KEY); 33 | public static final ConfigHotkey KEY_DRAG_DROP_SINGLE = new ConfigHotkey("keyDragDropSingle", "Q,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 34 | public static final ConfigHotkey KEY_DRAG_DROP_STACKS = new ConfigHotkey("keyDragDropStacks", "LEFT_SHIFT,Q,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 35 | 36 | public static final ConfigHotkey KEY_DRAG_LEAVE_ONE = new ConfigHotkey("keyDragMoveLeaveOne", "LEFT_SHIFT,BUTTON_2", GUI_NO_ORDER).apply(HOTKEYS_KEY); 37 | public static final ConfigHotkey KEY_DRAG_MATCHING = new ConfigHotkey("keyDragMoveMatching", "LEFT_ALT,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 38 | public static final ConfigHotkey KEY_DRAG_MOVE_ONE = new ConfigHotkey("keyDragMoveOne", "LEFT_CONTROL,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 39 | public static final ConfigHotkey KEY_DRAG_FULL_STACKS = new ConfigHotkey("keyDragMoveStacks", "LEFT_SHIFT,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 40 | 41 | public static final ConfigHotkey KEY_MOVE_EVERYTHING = new ConfigHotkey("keyMoveEverything", "LEFT_ALT,LEFT_SHIFT,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 42 | 43 | public static final ConfigHotkey KEY_WS_MOVE_DOWN_LEAVE_ONE = new ConfigHotkey("wsMoveDownLeaveOne", "S,BUTTON_2", GUI_NO_ORDER).apply(HOTKEYS_KEY); 44 | public static final ConfigHotkey KEY_WS_MOVE_DOWN_MATCHING = new ConfigHotkey("wsMoveDownMatching", "LEFT_ALT,S,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 45 | public static final ConfigHotkey KEY_WS_MOVE_DOWN_SINGLE = new ConfigHotkey("wsMoveDownSingle", "S,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 46 | public static final ConfigHotkey KEY_WS_MOVE_DOWN_STACKS = new ConfigHotkey("wsMoveDownStacks", "LEFT_SHIFT,S,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 47 | public static final ConfigHotkey KEY_WS_MOVE_UP_LEAVE_ONE = new ConfigHotkey("wsMoveUpLeaveOne", "W,BUTTON_2", GUI_NO_ORDER).apply(HOTKEYS_KEY); 48 | public static final ConfigHotkey KEY_WS_MOVE_UP_MATCHING = new ConfigHotkey("wsMoveUpMatching", "LEFT_ALT,W,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 49 | public static final ConfigHotkey KEY_WS_MOVE_UP_SINGLE = new ConfigHotkey("wsMoveUpSingle", "W,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 50 | public static final ConfigHotkey KEY_WS_MOVE_UP_STACKS = new ConfigHotkey("wsMoveUpStacks", "LEFT_SHIFT,W,BUTTON_1", GUI_NO_ORDER).apply(HOTKEYS_KEY); 51 | 52 | public static final ConfigHotkey MODIFIER_MOVE_EVERYTHING = new ConfigHotkey("modifierMoveEverything", "LEFT_ALT,LEFT_SHIFT", GUI_NO_ORDER).apply(HOTKEYS_KEY); 53 | public static final ConfigHotkey MODIFIER_MOVE_MATCHING = new ConfigHotkey("modifierMoveMatching", "LEFT_ALT", GUI_NO_ORDER).apply(HOTKEYS_KEY); 54 | public static final ConfigHotkey MODIFIER_MOVE_STACK = new ConfigHotkey("modifierMoveStack", "LEFT_SHIFT", GUI_NO_ORDER).apply(HOTKEYS_KEY); 55 | public static final ConfigHotkey MODIFIER_TOGGLE_VILLAGER_GLOBAL_FAVORITE = new ConfigHotkey("modifierToggleVillagerGlobalFavorite", "LEFT_SHIFT", GUI_RELAXED).apply(HOTKEYS_KEY); 56 | 57 | public static final ConfigHotkey SORT_INVENTORY = new ConfigHotkey("sortInventory", "R", GUI_NO_ORDER).apply(HOTKEYS_KEY); 58 | 59 | public static final List HOTKEY_LIST = ImmutableList.of( 60 | OPEN_CONFIG_GUI, 61 | TOGGLE_MOD_ON_OFF, 62 | 63 | CRAFT_EVERYTHING, 64 | DROP_ALL_MATCHING, 65 | MASS_CRAFT, 66 | MASS_CRAFT_TOGGLE, 67 | MOVE_CRAFT_RESULTS, 68 | RECIPE_VIEW, 69 | SLOT_DEBUG, 70 | STORE_RECIPE, 71 | THROW_CRAFT_RESULTS, 72 | VILLAGER_TRADE_FAVORITES, 73 | 74 | MODIFIER_MOVE_EVERYTHING, 75 | MODIFIER_MOVE_MATCHING, 76 | MODIFIER_MOVE_STACK, 77 | MODIFIER_TOGGLE_VILLAGER_GLOBAL_FAVORITE, 78 | 79 | KEY_DRAG_FULL_STACKS, 80 | KEY_DRAG_LEAVE_ONE, 81 | KEY_DRAG_MATCHING, 82 | KEY_DRAG_MOVE_ONE, 83 | 84 | KEY_DRAG_DROP_LEAVE_ONE, 85 | KEY_DRAG_DROP_SINGLE, 86 | KEY_DRAG_DROP_STACKS, 87 | 88 | KEY_MOVE_EVERYTHING, 89 | 90 | KEY_WS_MOVE_DOWN_LEAVE_ONE, 91 | KEY_WS_MOVE_DOWN_MATCHING, 92 | KEY_WS_MOVE_DOWN_SINGLE, 93 | KEY_WS_MOVE_DOWN_STACKS, 94 | KEY_WS_MOVE_UP_LEAVE_ONE, 95 | KEY_WS_MOVE_UP_MATCHING, 96 | KEY_WS_MOVE_UP_SINGLE, 97 | KEY_WS_MOVE_UP_STACKS, 98 | 99 | SORT_INVENTORY 100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/util/InputUtils.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.util; 2 | 3 | import javax.annotation.Nullable; 4 | import net.minecraft.client.Minecraft; 5 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 6 | import net.minecraft.client.input.KeyEvent; 7 | import net.minecraft.client.input.MouseButtonEvent; 8 | import fi.dy.masa.malilib.hotkeys.IKeybind; 9 | import fi.dy.masa.malilib.hotkeys.KeybindMulti; 10 | import fi.dy.masa.malilib.util.GuiUtils; 11 | import fi.dy.masa.itemscroller.config.Hotkeys; 12 | import fi.dy.masa.itemscroller.event.KeybindCallbacks; 13 | import fi.dy.masa.itemscroller.recipes.CraftingHandler; 14 | 15 | public class InputUtils 16 | { 17 | public static boolean isRecipeViewOpen() 18 | { 19 | return GuiUtils.getCurrentScreen() != null && 20 | KeybindCallbacks.getInstance().functionalityEnabled() && 21 | Hotkeys.RECIPE_VIEW.getKeybind().isKeybindHeld() && 22 | CraftingHandler.isCraftingGui(GuiUtils.getCurrentScreen()); 23 | } 24 | 25 | public static boolean canShiftDropItems(AbstractContainerScreen gui, Minecraft mc, int mouseX, int mouseY) 26 | { 27 | if (InventoryUtils.isStackEmpty(gui.getMenu().getCarried()) == false) 28 | { 29 | int left = AccessorUtils.getGuiLeft(gui); 30 | int top = AccessorUtils.getGuiTop(gui); 31 | int xSize = AccessorUtils.getGuiXSize(gui); 32 | int ySize = AccessorUtils.getGuiYSize(gui); 33 | boolean isOutsideGui = mouseX < left || mouseY < top || mouseX >= left + xSize || mouseY >= top + ySize; 34 | 35 | return isOutsideGui && AccessorUtils.getSlotAtPosition(gui, mouseX - left, mouseY - top) == null; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public static MoveAction getDragMoveAction(IKeybind key) 42 | { 43 | if (key == Hotkeys.KEY_DRAG_FULL_STACKS.getKeybind()) { return MoveAction.MOVE_TO_OTHER_STACKS; } 44 | else if (key == Hotkeys.KEY_DRAG_LEAVE_ONE.getKeybind()) { return MoveAction.MOVE_TO_OTHER_LEAVE_ONE; } 45 | else if (key == Hotkeys.KEY_DRAG_MOVE_ONE.getKeybind()) { return MoveAction.MOVE_TO_OTHER_MOVE_ONE; } 46 | else if (key == Hotkeys.KEY_DRAG_MATCHING.getKeybind()) { return MoveAction.MOVE_TO_OTHER_MATCHING; } 47 | 48 | else if (key == Hotkeys.KEY_DRAG_DROP_STACKS.getKeybind()) { return MoveAction.DROP_STACKS; } 49 | else if (key == Hotkeys.KEY_DRAG_DROP_LEAVE_ONE.getKeybind()) { return MoveAction.DROP_LEAVE_ONE; } 50 | else if (key == Hotkeys.KEY_DRAG_DROP_SINGLE.getKeybind()) { return MoveAction.DROP_ONE; } 51 | 52 | else if (key == Hotkeys.KEY_WS_MOVE_UP_STACKS.getKeybind()) { return MoveAction.MOVE_UP_STACKS; } 53 | else if (key == Hotkeys.KEY_WS_MOVE_UP_MATCHING.getKeybind()) { return MoveAction.MOVE_UP_MATCHING; } 54 | else if (key == Hotkeys.KEY_WS_MOVE_UP_LEAVE_ONE.getKeybind()) { return MoveAction.MOVE_UP_LEAVE_ONE; } 55 | else if (key == Hotkeys.KEY_WS_MOVE_UP_SINGLE.getKeybind()) { return MoveAction.MOVE_UP_MOVE_ONE; } 56 | else if (key == Hotkeys.KEY_WS_MOVE_DOWN_STACKS.getKeybind()) { return MoveAction.MOVE_DOWN_STACKS; } 57 | else if (key == Hotkeys.KEY_WS_MOVE_DOWN_MATCHING.getKeybind()) { return MoveAction.MOVE_DOWN_MATCHING; } 58 | else if (key == Hotkeys.KEY_WS_MOVE_DOWN_LEAVE_ONE.getKeybind()){ return MoveAction.MOVE_DOWN_LEAVE_ONE; } 59 | else if (key == Hotkeys.KEY_WS_MOVE_DOWN_SINGLE.getKeybind()) { return MoveAction.MOVE_DOWN_MOVE_ONE; } 60 | 61 | return MoveAction.NONE; 62 | } 63 | 64 | public static boolean isActionKeyActive(MoveAction action) 65 | { 66 | switch (action) 67 | { 68 | case MOVE_TO_OTHER_STACKS: return Hotkeys.KEY_DRAG_FULL_STACKS.getKeybind().isKeybindHeld(); 69 | case MOVE_TO_OTHER_LEAVE_ONE: return Hotkeys.KEY_DRAG_LEAVE_ONE.getKeybind().isKeybindHeld(); 70 | case MOVE_TO_OTHER_MOVE_ONE: return Hotkeys.KEY_DRAG_MOVE_ONE.getKeybind().isKeybindHeld(); 71 | case MOVE_TO_OTHER_MATCHING: return Hotkeys.KEY_DRAG_MATCHING.getKeybind().isKeybindHeld(); 72 | case MOVE_TO_OTHER_EVERYTHING: return Hotkeys.KEY_MOVE_EVERYTHING.getKeybind().isKeybindHeld(); 73 | case DROP_STACKS: return Hotkeys.KEY_DRAG_DROP_STACKS.getKeybind().isKeybindHeld(); 74 | case DROP_LEAVE_ONE: return Hotkeys.KEY_DRAG_DROP_LEAVE_ONE.getKeybind().isKeybindHeld(); 75 | case DROP_ONE: return Hotkeys.KEY_DRAG_DROP_SINGLE.getKeybind().isKeybindHeld(); 76 | case MOVE_UP_STACKS: return Hotkeys.KEY_WS_MOVE_UP_STACKS.getKeybind().isKeybindHeld(); 77 | case MOVE_UP_MATCHING: return Hotkeys.KEY_WS_MOVE_UP_MATCHING.getKeybind().isKeybindHeld(); 78 | case MOVE_UP_LEAVE_ONE: return Hotkeys.KEY_WS_MOVE_UP_LEAVE_ONE.getKeybind().isKeybindHeld(); 79 | case MOVE_UP_MOVE_ONE: return Hotkeys.KEY_WS_MOVE_UP_SINGLE.getKeybind().isKeybindHeld(); 80 | case MOVE_DOWN_STACKS: return Hotkeys.KEY_WS_MOVE_DOWN_STACKS.getKeybind().isKeybindHeld(); 81 | case MOVE_DOWN_MATCHING: return Hotkeys.KEY_WS_MOVE_DOWN_MATCHING.getKeybind().isKeybindHeld(); 82 | case MOVE_DOWN_LEAVE_ONE: return Hotkeys.KEY_WS_MOVE_DOWN_LEAVE_ONE.getKeybind().isKeybindHeld(); 83 | case MOVE_DOWN_MOVE_ONE: return Hotkeys.KEY_WS_MOVE_DOWN_SINGLE.getKeybind().isKeybindHeld(); 84 | default: 85 | } 86 | 87 | return false; 88 | } 89 | 90 | public static MoveAmount getMoveAmount(MoveAction action) 91 | { 92 | switch (action) 93 | { 94 | case SCROLL_TO_OTHER_MOVE_ONE: 95 | case MOVE_TO_OTHER_MOVE_ONE: 96 | case DROP_ONE: 97 | case MOVE_DOWN_MOVE_ONE: 98 | case MOVE_UP_MOVE_ONE: 99 | return MoveAmount.MOVE_ONE; 100 | 101 | case MOVE_TO_OTHER_LEAVE_ONE: 102 | case DROP_LEAVE_ONE: 103 | case MOVE_DOWN_LEAVE_ONE: 104 | case MOVE_UP_LEAVE_ONE: 105 | return MoveAmount.LEAVE_ONE; 106 | 107 | case SCROLL_TO_OTHER_STACKS: 108 | case MOVE_TO_OTHER_STACKS: 109 | case DROP_STACKS: 110 | case MOVE_DOWN_STACKS: 111 | case MOVE_UP_STACKS: 112 | return MoveAmount.FULL_STACKS; 113 | 114 | case SCROLL_TO_OTHER_MATCHING: 115 | case MOVE_TO_OTHER_MATCHING: 116 | case DROP_ALL_MATCHING: 117 | case MOVE_UP_MATCHING: 118 | case MOVE_DOWN_MATCHING: 119 | return MoveAmount.ALL_MATCHING; 120 | 121 | case MOVE_TO_OTHER_EVERYTHING: 122 | case SCROLL_TO_OTHER_EVERYTHING: 123 | return MoveAmount.EVERYTHING; 124 | 125 | default: 126 | } 127 | 128 | return MoveAmount.NONE; 129 | } 130 | 131 | public static boolean isAttack(int keyCode, Minecraft mc) 132 | { 133 | return keyCode == KeybindMulti.getKeyCode(mc.options.keyAttack); 134 | } 135 | 136 | public static boolean isUse(int keyCode, Minecraft mc) 137 | { 138 | return keyCode == KeybindMulti.getKeyCode(mc.options.keyUse); 139 | } 140 | 141 | public static boolean isPickBlock(int keyCode, Minecraft mc) 142 | { 143 | return keyCode == KeybindMulti.getKeyCode(mc.options.keyPickItem); 144 | } 145 | 146 | public static boolean isAttack(@Nullable MouseButtonEvent click, @Nullable KeyEvent input, Minecraft mc) 147 | { 148 | if (click != null && mc.options.keyAttack.matchesMouse(click)) 149 | { 150 | return true; 151 | } 152 | else return input != null && mc.options.keyAttack.matches(input); 153 | } 154 | 155 | public static boolean isUse(@Nullable MouseButtonEvent click, @Nullable KeyEvent input, Minecraft mc) 156 | { 157 | if (click != null && mc.options.keyUse.matchesMouse(click)) 158 | { 159 | return true; 160 | } 161 | else return input != null && mc.options.keyUse.matches(input); 162 | } 163 | 164 | public static boolean isPickBlock(@Nullable MouseButtonEvent click, @Nullable KeyEvent input, Minecraft mc) 165 | { 166 | if (click != null && mc.options.keyPickItem.matchesMouse(click)) 167 | { 168 | return true; 169 | } 170 | else return input != null && mc.options.keyPickItem.matches(input); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/mixin/screen/MixinMerchantScreen.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.mixin.screen; 2 | 3 | import javax.annotation.Nullable; 4 | import net.minecraft.client.gui.GuiGraphics; 5 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 6 | import net.minecraft.client.gui.screens.inventory.MerchantScreen; 7 | import net.minecraft.client.input.MouseButtonEvent; 8 | import net.minecraft.network.chat.Component; 9 | import net.minecraft.world.entity.player.Inventory; 10 | import net.minecraft.world.inventory.MerchantMenu; 11 | import net.minecraft.world.item.trading.MerchantOffer; 12 | import org.objectweb.asm.Opcodes; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Shadow; 15 | import org.spongepowered.asm.mixin.Unique; 16 | import org.spongepowered.asm.mixin.injection.At; 17 | import org.spongepowered.asm.mixin.injection.Inject; 18 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 20 | 21 | import fi.dy.masa.malilib.gui.interfaces.IGuiIcon; 22 | import fi.dy.masa.malilib.render.GuiContext; 23 | import fi.dy.masa.itemscroller.config.Configs; 24 | import fi.dy.masa.itemscroller.config.Hotkeys; 25 | import fi.dy.masa.itemscroller.gui.ItemScrollerIcons; 26 | import fi.dy.masa.itemscroller.util.InventoryUtils; 27 | import fi.dy.masa.itemscroller.villager.*; 28 | 29 | @Mixin(MerchantScreen.class) 30 | public abstract class MixinMerchantScreen extends AbstractContainerScreen 31 | { 32 | @Unique @Nullable private FavoriteData favoriteData; 33 | @Shadow private int shopItem; 34 | @Shadow int scrollOff; 35 | @Unique private int indexStartOffsetLast = -1; 36 | @Shadow protected abstract boolean canScroll(int listSize); 37 | 38 | private MixinMerchantScreen(MerchantMenu handler, Inventory inventory, Component title) 39 | { 40 | super(handler, inventory, title); 41 | } 42 | 43 | @Inject( 44 | method = "renderContents", 45 | at = @At(value = "INVOKE", 46 | target = "Lnet/minecraft/client/gui/screens/inventory/MerchantScreen;renderScroller(Lnet/minecraft/client/gui/GuiGraphics;IIIILnet/minecraft/world/item/trading/MerchantOffers;)V") 47 | ) 48 | private void fixRenderScrollBar(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) 49 | { 50 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue() && 51 | Configs.Generic.VILLAGER_TRADE_LIST_REMEMBER_SCROLL.getBooleanValue()) 52 | { 53 | VillagerData data = VillagerDataStorage.getInstance().getDataForLastInteractionTarget(); 54 | int listSize = this.menu.getOffers().size(); 55 | 56 | if (data != null && this.canScroll(listSize)) 57 | { 58 | this.scrollOff = this.getClampedIndex(data.getTradeListPosition()); 59 | } 60 | } 61 | } 62 | 63 | @Inject(method = "mouseScrolled", at = @At("RETURN")) 64 | private void onMouseScrollPost(double mouseX, double mouseY, double horizontalAmount, double verticalAmount, CallbackInfoReturnable cir) 65 | { 66 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue() && 67 | Configs.Generic.VILLAGER_TRADE_LIST_REMEMBER_SCROLL.getBooleanValue() && 68 | this.indexStartOffsetLast != this.scrollOff) 69 | { 70 | int index = this.getClampedIndex(this.scrollOff); 71 | VillagerDataStorage.getInstance().setTradeListPosition(index); 72 | this.indexStartOffsetLast = index; 73 | } 74 | } 75 | 76 | @Inject(method = "mouseDragged", at = @At("RETURN")) 77 | private void onMouseDragPost(MouseButtonEvent click, double offsetX, double offsetY, CallbackInfoReturnable cir) 78 | { 79 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue() && 80 | Configs.Generic.VILLAGER_TRADE_LIST_REMEMBER_SCROLL.getBooleanValue() && 81 | this.indexStartOffsetLast != this.scrollOff) 82 | { 83 | int index = this.getClampedIndex(this.scrollOff); 84 | VillagerDataStorage.getInstance().setTradeListPosition(index); 85 | this.indexStartOffsetLast = index; 86 | } 87 | } 88 | 89 | @Inject(method = "mouseClicked", at = @At("RETURN"), cancellable = true) 90 | private void onMouseClicked(MouseButtonEvent click, boolean doubled, CallbackInfoReturnable cir) 91 | { 92 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue()) 93 | { 94 | int visibleIndex = this.getHoveredTradeButtonIndex(click.x(), click.y()); 95 | int realIndex = VillagerUtils.getRealTradeIndexFor(visibleIndex, this.menu); 96 | 97 | if (realIndex >= 0) 98 | { 99 | // right click, trade everything with this trade 100 | if (click.input() == 1) 101 | { 102 | InventoryUtils.villagerTradeEverythingPossibleWithTrade(visibleIndex); 103 | cir.setReturnValue(true); 104 | } 105 | // Middle click, toggle trade favorite 106 | else if (click.input() == 2) 107 | { 108 | if (Hotkeys.MODIFIER_TOGGLE_VILLAGER_GLOBAL_FAVORITE.getKeybind().isKeybindHeld()) 109 | { 110 | MerchantOffer trade = this.menu.getOffers().get(visibleIndex); 111 | VillagerDataStorage.getInstance().toggleGlobalFavorite(trade); 112 | } 113 | else 114 | { 115 | VillagerDataStorage.getInstance().toggleFavorite(realIndex); 116 | } 117 | 118 | this.favoriteData = null; // Force a re-build of the list 119 | 120 | // Rebuild the custom list based on the new favorites (See the Mixin for MerchantScreenHandler#setOffers()) 121 | this.menu.setOffers(((IMerchantScreenHandler) this.menu).itemscroller$getOriginalList()); 122 | 123 | cir.setReturnValue(true); 124 | } 125 | } 126 | } 127 | } 128 | 129 | @Inject(method = "postButtonClick", at = @At("HEAD"), cancellable = true) 130 | private void fixRecipeIndex(CallbackInfo ci) 131 | { 132 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue() && 133 | this.getMenu() instanceof IMerchantScreenHandler) 134 | { 135 | if (VillagerUtils.switchToTradeByVisibleIndex(this.shopItem)) 136 | { 137 | ci.cancel(); 138 | } 139 | } 140 | } 141 | 142 | @Inject(method = "renderContents", at = @At(value = "FIELD", 143 | target = "Lnet/minecraft/client/gui/screens/inventory/MerchantScreen;tradeOfferButtons:[Lnet/minecraft/client/gui/screens/inventory/MerchantScreen$TradeOfferButton;", 144 | opcode = Opcodes.GETFIELD)) 145 | private void renderFavoriteMarker(GuiGraphics context, int mouseX, int mouseY, float delta, CallbackInfo ci) 146 | { 147 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue()) 148 | { 149 | FavoriteData favoriteData = this.favoriteData; 150 | 151 | if (favoriteData == null) 152 | { 153 | favoriteData = VillagerDataStorage.getInstance().getFavoritesForCurrentVillager(this.menu); 154 | this.favoriteData = favoriteData; 155 | } 156 | 157 | int numFavorites = favoriteData.favorites().size(); 158 | 159 | if (numFavorites > 0 && this.scrollOff < numFavorites) 160 | { 161 | int screenX = (this.width - this.imageWidth) / 2; 162 | int screenY = (this.height - this.imageHeight) / 2; 163 | int buttonsStartX = screenX + 5; 164 | int buttonsStartY = screenY + 16 + 2; 165 | int x = buttonsStartX + 89 - 8; 166 | int y = buttonsStartY + 2; 167 | float z = 300; 168 | IGuiIcon icon = favoriteData.isGlobal() ? ItemScrollerIcons.STAR_5_PURPLE : ItemScrollerIcons.STAR_5_YELLOW; 169 | 170 | for (int i = 0; i < (numFavorites - this.scrollOff); ++i) 171 | { 172 | //RenderUtils.bindTexture(icon.getTexture()); 173 | icon.renderAt(GuiContext.fromGuiGraphics(context), x, y, z, false, false); 174 | y += 20; 175 | } 176 | } 177 | } 178 | } 179 | 180 | @Unique 181 | private int getClampedIndex(int index) 182 | { 183 | int listSize = this.menu.getOffers().size(); 184 | return Math.max(0, Math.min(index, listSize - 7)); 185 | } 186 | 187 | @Unique 188 | private int getHoveredTradeButtonIndex(double mouseX, double mouseY) 189 | { 190 | int screenX = (this.width - this.imageWidth) / 2; 191 | int screenY = (this.height - this.imageHeight) / 2; 192 | int buttonsStartX = screenX + 5; 193 | int buttonsStartY = screenY + 16 + 2; 194 | int buttonWidth = 89; 195 | int buttonHeight = 20; 196 | 197 | if (mouseX >= buttonsStartX && mouseX <= buttonsStartX + buttonWidth && 198 | mouseY >= buttonsStartY && mouseY <= buttonsStartY + 7 * buttonHeight) 199 | { 200 | return this.scrollOff + (((int) mouseY - buttonsStartY) / buttonHeight); 201 | } 202 | 203 | return -1; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/villager/VillagerDataStorage.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.villager; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.util.*; 6 | import javax.annotation.Nullable; 7 | import net.minecraft.world.inventory.MerchantMenu; 8 | import net.minecraft.world.item.trading.MerchantOffer; 9 | import net.minecraft.world.item.trading.MerchantOffers; 10 | import it.unimi.dsi.fastutil.ints.IntArrayList; 11 | import fi.dy.masa.malilib.util.FileUtils; 12 | import fi.dy.masa.malilib.util.StringUtils; 13 | import fi.dy.masa.malilib.util.data.Constants; 14 | import fi.dy.masa.malilib.util.data.tag.CompoundData; 15 | import fi.dy.masa.malilib.util.data.tag.ListData; 16 | import fi.dy.masa.malilib.util.data.tag.util.DataFileUtils; 17 | import fi.dy.masa.itemscroller.ItemScroller; 18 | import fi.dy.masa.itemscroller.Reference; 19 | import fi.dy.masa.itemscroller.config.Configs; 20 | 21 | public class VillagerDataStorage 22 | { 23 | private static final VillagerDataStorage INSTANCE = new VillagerDataStorage(); 24 | 25 | private final Map data = new HashMap<>(); 26 | private final List globalFavorites = new ArrayList<>(); 27 | private UUID lastInteractedUUID; 28 | private boolean dirty; 29 | 30 | public static VillagerDataStorage getInstance() 31 | { 32 | return INSTANCE; 33 | } 34 | 35 | public void setLastInteractedUUID(UUID uuid) 36 | { 37 | this.lastInteractedUUID = uuid; 38 | } 39 | 40 | @Nullable 41 | public VillagerData getDataForLastInteractionTarget() 42 | { 43 | return this.getDataFor(this.lastInteractedUUID, true); 44 | } 45 | 46 | public VillagerData getDataFor(@Nullable UUID uuid, boolean create) 47 | { 48 | VillagerData data = uuid != null ? this.data.get(uuid) : null; 49 | 50 | if (data == null && uuid != null && create) 51 | { 52 | this.setLastInteractedUUID(uuid); 53 | data = new VillagerData(uuid); 54 | this.data.put(uuid, data); 55 | this.dirty = true; 56 | } 57 | 58 | return data; 59 | } 60 | 61 | public void setTradeListPosition(int position) 62 | { 63 | VillagerData data = this.getDataFor(this.lastInteractedUUID, true); 64 | 65 | if (data != null) 66 | { 67 | data.setTradeListPosition(position); 68 | this.dirty = true; 69 | } 70 | } 71 | 72 | public void toggleFavorite(int tradeIndex) 73 | { 74 | VillagerData data = this.getDataFor(this.lastInteractedUUID, true); 75 | 76 | if (data != null) 77 | { 78 | data.toggleFavorite(tradeIndex); 79 | this.dirty = true; 80 | } 81 | } 82 | 83 | public void toggleGlobalFavorite(MerchantOffer trade) 84 | { 85 | TradeType type = TradeType.of(trade); 86 | 87 | if (this.globalFavorites.contains(type)) 88 | { 89 | this.globalFavorites.remove(type); 90 | } 91 | else 92 | { 93 | this.globalFavorites.add(type); 94 | } 95 | 96 | this.dirty = true; 97 | } 98 | 99 | public FavoriteData getFavoritesForCurrentVillager(MerchantMenu handler) 100 | { 101 | return this.getFavoritesForCurrentVillager(((IMerchantScreenHandler) handler).itemscroller$getOriginalList()); 102 | } 103 | 104 | public FavoriteData getFavoritesForCurrentVillager(MerchantOffers originalTrades) 105 | { 106 | VillagerData data = this.getDataFor(this.lastInteractedUUID, false); 107 | IntArrayList favorites = data != null ? data.getFavorites() : null; 108 | 109 | if (favorites != null && favorites.isEmpty() == false) 110 | { 111 | return new FavoriteData(favorites, false); 112 | } 113 | 114 | if (Configs.Generic.VILLAGER_TRADE_USE_GLOBAL_FAVORITES.getBooleanValue() && this.lastInteractedUUID != null) 115 | { 116 | return new FavoriteData(VillagerUtils.getGlobalFavoritesFor(originalTrades, this.globalFavorites), true); 117 | } 118 | 119 | return new FavoriteData(IntArrayList.of(), favorites == null); 120 | } 121 | 122 | private void readFromNBT(CompoundData tags) 123 | { 124 | if (tags == null || tags.contains("VillagerData", Constants.NBT.TAG_LIST) == false) 125 | { 126 | return; 127 | } 128 | 129 | ListData tagList = tags.getList("VillagerData"); 130 | int count = tagList.size(); 131 | 132 | for (int i = 0; i < count; i++) 133 | { 134 | CompoundData tag = tagList.getCompoundAt(i); 135 | VillagerData data = VillagerData.fromNBT(tag); 136 | 137 | if (data != null) 138 | { 139 | this.data.put(data.getUUID(), data); 140 | } 141 | } 142 | 143 | tagList = tags.getList("GlobalFavorites"); 144 | count = tagList.size(); 145 | 146 | for (int i = 0; i < count; i++) 147 | { 148 | CompoundData tag = tagList.getCompoundAt(i); 149 | TradeType type = TradeType.fromTag(tag); 150 | 151 | if (type != null) 152 | { 153 | this.globalFavorites.add(type); 154 | } 155 | } 156 | } 157 | 158 | private CompoundData writeToNBT() 159 | { 160 | CompoundData tags = new CompoundData(); 161 | ListData favoriteListData = new ListData(); 162 | ListData globalFavoriteData = new ListData(); 163 | 164 | for (VillagerData data : this.data.values()) 165 | { 166 | favoriteListData.add(data.toNBT()); 167 | } 168 | 169 | for (TradeType type : this.globalFavorites) 170 | { 171 | globalFavoriteData.add(type.toTag()); 172 | } 173 | 174 | tags.put("VillagerData", favoriteListData); 175 | tags.put("GlobalFavorites", globalFavoriteData); 176 | 177 | this.dirty = false; 178 | 179 | return tags; 180 | } 181 | 182 | private String getFileName() 183 | { 184 | String worldName = StringUtils.getWorldOrServerName(); 185 | 186 | if (worldName != null) 187 | { 188 | return "villager_data_" + worldName + ".nbt"; 189 | } 190 | 191 | return "villager_data.nbt"; 192 | } 193 | 194 | private Path getSaveDirPath() 195 | { 196 | return FileUtils.getMinecraftDirectoryAsPath().resolve(Reference.MOD_ID); 197 | } 198 | 199 | public void readFromDisk() 200 | { 201 | this.data.clear(); 202 | this.globalFavorites.clear(); 203 | 204 | try 205 | { 206 | Path saveDir = this.getSaveDirPath(); 207 | 208 | if (Files.isDirectory(saveDir)) 209 | { 210 | Path file = saveDir.resolve(this.getFileName()); 211 | 212 | if (Files.exists(file)) 213 | { 214 | // NbtCompound nbtIn = NbtUtils.readNbtFromFileAsPath(file, NbtSizeTracker.ofUnlimitedBytes()); 215 | CompoundData data = DataFileUtils.readCompoundDataFromNbtFile(file); 216 | 217 | if (data != null && !data.isEmpty()) 218 | { 219 | this.readFromNBT(data); 220 | //ItemScroller.debugLog("readFromDisk(): Successfully loaded villager's from file '{}'", file.toAbsolutePath()); 221 | } 222 | else 223 | { 224 | ItemScroller.LOGGER.warn("readFromDisk(): Error reading villager data from file '{}'", file.toAbsolutePath()); 225 | } 226 | } 227 | // File does not exist 228 | } 229 | else 230 | { 231 | ItemScroller.LOGGER.warn("readFromDisk(): Error reading villager data from dir '{}'", saveDir.toAbsolutePath()); 232 | } 233 | } 234 | catch (Exception e) 235 | { 236 | ItemScroller.LOGGER.warn("Failed to read villager data from file", e); 237 | } 238 | } 239 | 240 | public void writeToDisk() 241 | { 242 | if (this.dirty) 243 | { 244 | try 245 | { 246 | Path saveDir = this.getSaveDirPath(); 247 | 248 | if (!Files.exists(saveDir)) 249 | { 250 | FileUtils.createDirectoriesIfMissing(saveDir); 251 | //ItemScroller.debugLog("writeToDisk(): Creating directory '{}'.", saveDir.toAbsolutePath()); 252 | } 253 | 254 | if (Files.isDirectory(saveDir)) 255 | { 256 | Path fileTmp = saveDir.resolve(this.getFileName() + ".tmp"); 257 | Path fileReal = saveDir.resolve(this.getFileName()); 258 | 259 | // NbtUtils.writeCompressed(this.writeToNBT(), fileTmp); 260 | CompoundData data = this.writeToNBT(); 261 | DataFileUtils.writeCompoundDataToCompressedNbtFile(fileTmp, data); 262 | 263 | if (Files.exists(fileReal)) 264 | { 265 | Files.delete(fileReal); 266 | } 267 | 268 | Files.move(fileTmp, fileReal); 269 | 270 | //ItemScroller.debugLog("writeToDisk(): Successfully saved recipes file '{}'", fileReal.toAbsolutePath()); 271 | this.dirty = false; 272 | } 273 | } 274 | catch (Exception e) 275 | { 276 | ItemScroller.LOGGER.warn("Failed to write villager data to file!", e); 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/event/InputHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.event; 2 | 3 | import org.lwjgl.glfw.GLFW; 4 | import fi.dy.masa.malilib.gui.GuiBase; 5 | import fi.dy.masa.malilib.hotkeys.*; 6 | import fi.dy.masa.malilib.util.GuiUtils; 7 | import fi.dy.masa.malilib.util.KeyCodes; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; 11 | import net.minecraft.client.input.KeyEvent; 12 | import net.minecraft.client.input.MouseButtonEvent; 13 | import net.minecraft.util.Mth; 14 | import net.minecraft.world.entity.npc.villager.AbstractVillager; 15 | import net.minecraft.world.inventory.Slot; 16 | import net.minecraft.world.phys.EntityHitResult; 17 | import net.minecraft.world.phys.HitResult; 18 | import fi.dy.masa.itemscroller.Reference; 19 | import fi.dy.masa.itemscroller.config.Configs; 20 | import fi.dy.masa.itemscroller.config.Hotkeys; 21 | import fi.dy.masa.itemscroller.recipes.RecipeStorage; 22 | import fi.dy.masa.itemscroller.util.*; 23 | import fi.dy.masa.itemscroller.villager.VillagerDataStorage; 24 | 25 | public class InputHandler implements IKeybindProvider, IKeyboardInputHandler, IMouseInputHandler 26 | { 27 | private final KeybindCallbacks callbacks; 28 | 29 | public InputHandler() 30 | { 31 | this.callbacks = KeybindCallbacks.getInstance(); 32 | } 33 | 34 | @Override 35 | public void addKeysToMap(IKeybindManager manager) 36 | { 37 | for (IHotkey hotkey : Hotkeys.HOTKEY_LIST) 38 | { 39 | manager.addKeybindToMap(hotkey.getKeybind()); 40 | } 41 | } 42 | 43 | @Override 44 | public void addHotkeys(IKeybindManager manager) 45 | { 46 | manager.addHotkeysForCategory(Reference.MOD_NAME, "itemscroller.hotkeys.category.hotkeys", Hotkeys.HOTKEY_LIST); 47 | } 48 | 49 | @Override 50 | public boolean onKeyInput(KeyEvent input, boolean eventKeyState) 51 | { 52 | if (InputUtils.isRecipeViewOpen() && eventKeyState) 53 | { 54 | int index = -1; 55 | RecipeStorage recipes = RecipeStorage.getInstance(); 56 | int oldIndex = recipes.getSelection(); 57 | int recipesPerPage = recipes.getRecipeCountPerPage(); 58 | // int recipeIndexChange = GuiBase.isShiftDown() ? recipesPerPage : recipesPerPage / 2; 59 | int recipeIndexChange = (input.hasShiftDown() || GuiBase.isShiftDown()) ? recipesPerPage : recipesPerPage / 2; 60 | 61 | if (input.key() >= KeyCodes.KEY_1 && input.key() <= KeyCodes.KEY_9) 62 | { 63 | index = Mth.clamp(input.key() - GLFW.GLFW_KEY_1, 0, 8); 64 | } 65 | else if (input.key() == KeyCodes.KEY_UP && oldIndex > 0) 66 | { 67 | index = oldIndex - 1; 68 | } 69 | else if (input.key() == KeyCodes.KEY_DOWN && oldIndex < (recipes.getTotalRecipeCount() - 1)) 70 | { 71 | index = oldIndex + 1; 72 | } 73 | else if (input.key() == KeyCodes.KEY_LEFT && oldIndex >= recipeIndexChange) 74 | { 75 | index = oldIndex - recipeIndexChange; 76 | } 77 | else if (input.key() == KeyCodes.KEY_RIGHT && oldIndex < (recipes.getTotalRecipeCount() - recipeIndexChange)) 78 | { 79 | index = oldIndex + recipeIndexChange; 80 | } 81 | 82 | if (index >= 0) 83 | { 84 | recipes.changeSelectedRecipe(index); 85 | return true; 86 | } 87 | } 88 | 89 | return this.handleInput(input.key(), eventKeyState, 0); 90 | } 91 | 92 | @Override 93 | public boolean onMouseScroll(double mouseX, double mouseY, double amount) 94 | { 95 | // return this.handleInput(null, null, KeyCodes.KEY_NONE, false, amount); 96 | return this.handleInput(KeyCodes.KEY_NONE, false, amount); 97 | } 98 | 99 | @Override 100 | public boolean onMouseClick(MouseButtonEvent click, boolean eventButtonState) 101 | { 102 | // return this.handleInput(click,null, click.getKeycode() - 100, eventButtonState, 0); 103 | // return this.handleInput(new Click(click.x(), click.y(), new MouseInput(click.getKeycode() - 100, click.modifiers())), null, eventButtonState, 0); 104 | return this.handleInput(click.input() - 100, eventButtonState, 0); 105 | } 106 | 107 | private boolean handleInput(int keyCode, boolean keyState, double dWheel) 108 | { 109 | Minecraft mc = Minecraft.getInstance(); 110 | 111 | if (mc.player == null) 112 | { 113 | return false; 114 | } 115 | 116 | if (Configs.Generic.RATE_LIMIT_CLICK_PACKETS.getBooleanValue() && 117 | this.callbacks.functionalityEnabled()) 118 | { 119 | ClickPacketBuffer.setShouldBufferClickPackets(true); 120 | } 121 | 122 | boolean cancel = this.handleInputImpl(keyCode, keyState, dWheel, mc); 123 | 124 | ClickPacketBuffer.setShouldBufferClickPackets(false); 125 | 126 | return cancel; 127 | } 128 | 129 | private boolean handleInputImpl(int keyCode, boolean keyState, double dWheel, Minecraft mc) 130 | { 131 | MoveAction action = InventoryUtils.getActiveMoveAction(); 132 | 133 | if (action != MoveAction.NONE && InputUtils.isActionKeyActive(action) == false) 134 | { 135 | InventoryUtils.stopDragging(); 136 | } 137 | 138 | boolean cancel = false; 139 | 140 | if (this.callbacks.functionalityEnabled() && mc.player != null) 141 | { 142 | final boolean isAttack = InputUtils.isAttack(keyCode, mc); 143 | final boolean isUse = InputUtils.isUse(keyCode, mc); 144 | final boolean isPickBlock = InputUtils.isPickBlock(keyCode, mc); 145 | final boolean isAttackUseOrPick = isAttack || isUse || isPickBlock; 146 | final int mouseX = fi.dy.masa.malilib.util.InputUtils.getMouseX(); 147 | final int mouseY = fi.dy.masa.malilib.util.InputUtils.getMouseY(); 148 | 149 | if (Configs.Toggles.VILLAGER_TRADE_FEATURES.getBooleanValue()) 150 | { 151 | VillagerDataStorage storage = VillagerDataStorage.getInstance(); 152 | 153 | if (mc.screen == null && mc.hitResult != null && 154 | mc.hitResult.getType() == HitResult.Type.ENTITY && 155 | ((EntityHitResult) mc.hitResult).getEntity() instanceof AbstractVillager) 156 | { 157 | storage.setLastInteractedUUID(((EntityHitResult) mc.hitResult).getEntity().getUUID()); 158 | } 159 | } 160 | 161 | if (mc.screen instanceof AbstractContainerScreen gui && 162 | (mc.screen instanceof CreativeModeInventoryScreen) == false && 163 | Configs.GUI_BLACKLIST.contains(mc.screen.getClass().getName()) == false) 164 | { 165 | RecipeStorage recipes = RecipeStorage.getInstance(); 166 | 167 | if (dWheel != 0) 168 | { 169 | // When scrolling while the recipe view is open, change the selection instead of moving items 170 | if (InputUtils.isRecipeViewOpen()) 171 | { 172 | recipes.scrollSelection(dWheel < 0); 173 | cancel = true; 174 | } 175 | else if (!InventoryUtils.ignoreScrollingInsideOfBundles) 176 | { 177 | cancel = InventoryUtils.tryMoveItems(gui, recipes, dWheel > 0); 178 | } 179 | } 180 | else 181 | { 182 | Slot slot = AccessorUtils.getSlotUnderMouse(gui); 183 | final boolean isShiftDown = GuiBase.isShiftDown(); 184 | 185 | if (keyState && isAttackUseOrPick) 186 | { 187 | int hoveredRecipeId = RenderEventHandler.instance().getHoveredRecipeId(mouseX, mouseY, recipes, gui); 188 | 189 | // Hovering over an item in the recipe view 190 | if (hoveredRecipeId >= 0) 191 | { 192 | InventoryUtils.handleRecipeClick(gui, mc, recipes, hoveredRecipeId, isAttack, isUse, isPickBlock, isShiftDown); 193 | return true; 194 | } 195 | // Pick-blocking over a crafting output slot with the recipe view open, store the recipe 196 | else if (isPickBlock && InputUtils.isRecipeViewOpen() && InventoryUtils.isCraftingSlot(gui, slot)) 197 | { 198 | //System.out.print("handleInputImpl()\n"); 199 | recipes.storeCraftingRecipeToCurrentSelection(slot, gui, true, false, mc); 200 | cancel = true; 201 | } 202 | } 203 | 204 | InventoryUtils.checkForItemPickup(gui); 205 | 206 | if (keyState && (isAttack || isUse)) 207 | { 208 | InventoryUtils.storeSourceSlotCandidate(slot, gui); 209 | } 210 | 211 | if (Configs.Toggles.RIGHT_CLICK_CRAFT_STACK.getBooleanValue() && 212 | isUse && keyState && 213 | InventoryUtils.isCraftingSlot(gui, slot)) 214 | { 215 | InventoryUtils.rightClickCraftOneStack(gui); 216 | } 217 | else if (Configs.Toggles.SHIFT_PLACE_ITEMS.getBooleanValue() && 218 | isAttack && isShiftDown && 219 | InventoryUtils.canShiftPlaceItems(gui) && slot != null) 220 | { 221 | cancel |= InventoryUtils.shiftPlaceItems(slot, gui); 222 | } 223 | else if (Configs.Toggles.SHIFT_DROP_ITEMS.getBooleanValue() && 224 | isAttack && isShiftDown && 225 | InputUtils.canShiftDropItems(gui, mc, mouseX, mouseY)) 226 | { 227 | cancel |= InventoryUtils.shiftDropItems(gui); 228 | } 229 | } 230 | } 231 | } 232 | 233 | return cancel; 234 | } 235 | 236 | @Override 237 | public void onMouseMove(double mouseX, double mouseY) 238 | { 239 | Minecraft mc = Minecraft.getInstance(); 240 | if (mc.player == null) return; 241 | 242 | if (this.callbacks.functionalityEnabled() && 243 | mc.player != null && 244 | GuiUtils.getCurrentScreen() instanceof AbstractContainerScreen screen && 245 | Configs.GUI_BLACKLIST.contains(screen.getClass().getName()) == false) 246 | { 247 | this.handleDragging(screen, mc, (int) mouseX, (int) mouseY, false); 248 | } 249 | } 250 | 251 | private boolean handleDragging(AbstractContainerScreen gui, Minecraft mc, int mouseX, int mouseY, boolean isClick) 252 | { 253 | MoveAction action = InventoryUtils.getActiveMoveAction(); 254 | boolean cancel = false; 255 | 256 | if (Configs.Generic.RATE_LIMIT_CLICK_PACKETS.getBooleanValue()) 257 | { 258 | ClickPacketBuffer.setShouldBufferClickPackets(true); 259 | } 260 | 261 | if (InputUtils.isActionKeyActive(action)) 262 | { 263 | cancel = InventoryUtils.dragMoveItems(gui, action, mouseX, mouseY, false); 264 | } 265 | else if (action != MoveAction.NONE) 266 | { 267 | InventoryUtils.stopDragging(); 268 | } 269 | 270 | ClickPacketBuffer.setShouldBufferClickPackets(false); 271 | 272 | return cancel; 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/event/RenderEventHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.event; 2 | 3 | import fi.dy.masa.malilib.render.GuiContext; 4 | import fi.dy.masa.malilib.render.InventoryOverlay; 5 | import fi.dy.masa.malilib.render.RenderUtils; 6 | import fi.dy.masa.malilib.util.GuiUtils; 7 | import fi.dy.masa.malilib.util.StringUtils; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.gui.Font; 10 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 11 | import net.minecraft.world.item.ItemStack; 12 | import fi.dy.masa.itemscroller.config.Configs; 13 | import fi.dy.masa.itemscroller.recipes.RecipePattern; 14 | import fi.dy.masa.itemscroller.recipes.RecipeStorage; 15 | import fi.dy.masa.itemscroller.util.AccessorUtils; 16 | import fi.dy.masa.itemscroller.util.ClickPacketBuffer; 17 | import fi.dy.masa.itemscroller.util.InputUtils; 18 | import fi.dy.masa.itemscroller.util.InventoryUtils; 19 | 20 | public class RenderEventHandler 21 | { 22 | private static final RenderEventHandler INSTANCE = new RenderEventHandler(); 23 | 24 | private final Minecraft mc = Minecraft.getInstance(); 25 | private int recipeListX; 26 | private int recipeListY; 27 | private int recipesPerColumn; 28 | private int columnWidth; 29 | private int columns; 30 | private int numberTextWidth; 31 | private int gapColumn; 32 | private int entryHeight; 33 | private double scale; 34 | 35 | public static RenderEventHandler instance() 36 | { 37 | return INSTANCE; 38 | } 39 | 40 | public void renderRecipeView(GuiContext ctx, Minecraft mc, int mouseX, int mouseY) 41 | { 42 | if (GuiUtils.getCurrentScreen() instanceof AbstractContainerScreen gui && 43 | InputUtils.isRecipeViewOpen()) 44 | { 45 | RecipeStorage recipes = RecipeStorage.getInstance(); 46 | final int first = recipes.getFirstVisibleRecipeId(); 47 | final int countPerPage = recipes.getRecipeCountPerPage(); 48 | final int lastOnPage = first + countPerPage - 1; 49 | 50 | this.calculateRecipePositions(gui); 51 | 52 | ctx.pose().pushMatrix(); 53 | ctx.pose().translate(this.recipeListX, this.recipeListY); 54 | ctx.pose().scale((float) this.scale, (float) this.scale); 55 | 56 | String str = StringUtils.translate("itemscroller.gui.label.recipe_page", (first / countPerPage) + 1, recipes.getTotalRecipeCount() / countPerPage); 57 | 58 | ctx.drawString(mc.font, str, 16, -12, 0xC0C0C0C0, false); 59 | 60 | for (int i = 0, recipeId = first; recipeId <= lastOnPage; ++i, ++recipeId) 61 | { 62 | ItemStack stack = recipes.getRecipe(recipeId).getResult(); 63 | boolean selected = recipeId == recipes.getSelection(); 64 | int row = i % this.recipesPerColumn; 65 | int column = i / this.recipesPerColumn; 66 | 67 | this.renderStoredRecipeStack(ctx, stack, recipeId, row, column, gui, selected); 68 | } 69 | 70 | if (Configs.Generic.CRAFTING_RENDER_RECIPE_ITEMS.getBooleanValue()) 71 | { 72 | final int recipeId = this.getHoveredRecipeId(mouseX, mouseY, recipes, gui); 73 | RecipePattern recipe = recipeId >= 0 ? recipes.getRecipe(recipeId) : recipes.getSelectedRecipe(); 74 | 75 | this.renderRecipeItems(ctx, recipe, recipes.getRecipeCountPerPage(), gui); 76 | } 77 | 78 | ctx.pose().popMatrix(); 79 | } 80 | } 81 | 82 | public void onDrawScreenPost(GuiContext ctx, Minecraft mc, int mouseX, int mouseY) 83 | { 84 | this.renderRecipeView(ctx, mc, mouseX, mouseY); 85 | 86 | if (GuiUtils.getCurrentScreen() instanceof AbstractContainerScreen gui) 87 | { 88 | int bufferedCount = ClickPacketBuffer.getBufferedActionsCount(); 89 | 90 | if (bufferedCount > 0) 91 | { 92 | ctx.drawString(mc.font, "Buffered slot clicks: " + bufferedCount, 10, 10, 0xFFD0D0D0, false); 93 | } 94 | 95 | if (InputUtils.isRecipeViewOpen()) 96 | { 97 | RecipeStorage recipes = RecipeStorage.getInstance(); 98 | final int recipeId = this.getHoveredRecipeId(mouseX, mouseY, recipes, gui); 99 | 100 | ctx.pose().pushMatrix(); 101 | ctx.pose().translate(0, 0); // z = 300.f 102 | 103 | if (recipeId >= 0) 104 | { 105 | RecipePattern recipe = recipes.getRecipe(recipeId); 106 | this.renderHoverTooltip(ctx, mouseX, mouseY, recipe, gui); 107 | } 108 | else if (Configs.Generic.CRAFTING_RENDER_RECIPE_ITEMS.getBooleanValue()) 109 | { 110 | RecipePattern recipe = recipes.getSelectedRecipe(); 111 | ItemStack stack = this.getHoveredRecipeIngredient(mouseX, mouseY, recipe, recipes.getRecipeCountPerPage(), gui); 112 | 113 | if (!InventoryUtils.isStackEmpty(stack)) 114 | { 115 | InventoryOverlay.renderStackToolTip(ctx, (int) mouseX, (int) mouseY, stack); 116 | } 117 | } 118 | 119 | ctx.pose().popMatrix(); 120 | } 121 | } 122 | } 123 | 124 | private void calculateRecipePositions(AbstractContainerScreen gui) 125 | { 126 | RecipeStorage recipes = RecipeStorage.getInstance(); 127 | final int gapHorizontal = 2; 128 | final int gapVertical = 2; 129 | final int stackBaseHeight = 16; 130 | 131 | this.recipesPerColumn = 9; 132 | this.columns = (int) Math.ceil((double) recipes.getRecipeCountPerPage() / (double) this.recipesPerColumn); 133 | this.numberTextWidth = 12; 134 | this.gapColumn = 4; 135 | 136 | int usableHeight = GuiUtils.getScaledWindowHeight(); 137 | int usableWidth = AccessorUtils.getGuiLeft(gui); 138 | // Scale the maximum stack size by taking into account the relative gap size 139 | double gapScaleVertical = (1D - (double) gapVertical / (double) (stackBaseHeight + gapVertical)); 140 | // the +1.2 is for the gap and page text height on the top and bottom 141 | int maxStackDimensionsVertical = (int) ((usableHeight / ((double) this.recipesPerColumn + 1.2)) * gapScaleVertical); 142 | // assume a maximum of 3x3 recipe size for now... thus columns + 3 stacks rendered horizontally 143 | double gapScaleHorizontal = (1D - (double) gapHorizontal / (double) (stackBaseHeight + gapHorizontal)); 144 | int maxStackDimensionsHorizontal = (int) (((usableWidth - (this.columns * (this.numberTextWidth + this.gapColumn))) / (this.columns + 3 + 0.8)) * gapScaleHorizontal); 145 | int stackDimensions = (int) Math.min(maxStackDimensionsVertical, maxStackDimensionsHorizontal); 146 | 147 | this.scale = (double) stackDimensions / (double) stackBaseHeight; 148 | this.entryHeight = stackBaseHeight + gapVertical; 149 | this.recipeListX = usableWidth - (int) ((this.columns * (stackBaseHeight + this.numberTextWidth + this.gapColumn) + gapHorizontal) * this.scale); 150 | this.recipeListY = (int) (this.entryHeight * this.scale); 151 | this.columnWidth = stackBaseHeight + this.numberTextWidth + this.gapColumn; 152 | } 153 | 154 | private void renderHoverTooltip(GuiContext ctx, double mouseX, double mouseY, RecipePattern recipe, 155 | AbstractContainerScreen gui) 156 | { 157 | ItemStack stack = recipe.getResult(); 158 | 159 | if (!InventoryUtils.isStackEmpty(stack)) 160 | { 161 | InventoryOverlay.renderStackToolTip(ctx, (int) mouseX, (int) mouseY, stack); 162 | } 163 | } 164 | 165 | public int getHoveredRecipeId(int mouseX, int mouseY, RecipeStorage recipes, AbstractContainerScreen gui) 166 | { 167 | if (InputUtils.isRecipeViewOpen()) 168 | { 169 | this.calculateRecipePositions(gui); 170 | final int stackDimensions = (int) (16 * this.scale); 171 | 172 | for (int column = 0; column < this.columns; ++column) 173 | { 174 | int startX = this.recipeListX + (int) ((column * this.columnWidth + this.gapColumn + this.numberTextWidth) * this.scale); 175 | 176 | if (mouseX >= startX && mouseX <= startX + stackDimensions) 177 | { 178 | for (int row = 0; row < this.recipesPerColumn; ++row) 179 | { 180 | int startY = this.recipeListY + (int) (row * this.entryHeight * this.scale); 181 | 182 | if (mouseY >= startY && mouseY <= startY + stackDimensions) 183 | { 184 | return recipes.getFirstVisibleRecipeId() + column * this.recipesPerColumn + row; 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | return -1; 192 | } 193 | 194 | private void renderStoredRecipeStack(GuiContext ctx, ItemStack stack, int recipeId, int row, int column, 195 | AbstractContainerScreen gui, boolean selected) 196 | { 197 | final Font font = this.mc.font; 198 | final String indexStr = String.valueOf(recipeId + 1); 199 | 200 | int x = column * this.columnWidth + this.gapColumn + this.numberTextWidth; 201 | int y = row * this.entryHeight; 202 | this.renderStackAt(ctx, stack, x, y, selected); 203 | 204 | float scale = 0.75F; 205 | x = x - (int) (font.width(indexStr) * scale) - 2; 206 | y = row * this.entryHeight + this.entryHeight / 2 - font.lineHeight / 2; 207 | 208 | ctx.pose().pushMatrix(); 209 | ctx.pose().translate(x, y); 210 | ctx.pose().scale(scale, scale); 211 | 212 | ctx.drawString(font, indexStr, 0, 0, 0xFFC0C0C0, false); 213 | 214 | ctx.pose().popMatrix(); 215 | } 216 | 217 | private void renderRecipeItems(GuiContext ctx, 218 | RecipePattern recipe, int recipeCountPerPage, 219 | AbstractContainerScreen gui) 220 | { 221 | ItemStack[] items = recipe.getRecipeItems(); 222 | final int recipeDimensions = (int) Math.ceil(Math.sqrt(Math.min(recipe.getRecipeLength(), 9))); 223 | int x = -3 * 17 + 2; 224 | int y = 3 * this.entryHeight; 225 | 226 | for (int i = 0, row = 0; row < recipeDimensions; row++) 227 | { 228 | for (int col = 0; col < recipeDimensions; col++, i++) 229 | { 230 | //int xOff = col * 17; 231 | //int yOff = row * 17; 232 | int xOff = col > 0 ? col * 17 : 0; 233 | int yOff = row > 0 ? row * 17 : 0; 234 | 235 | this.renderStackAt(ctx, items[i], x + xOff, y + yOff, false); 236 | } 237 | } 238 | } 239 | 240 | private ItemStack getHoveredRecipeIngredient(int mouseX, int mouseY, 241 | RecipePattern recipe, int recipeCountPerPage, 242 | AbstractContainerScreen gui) 243 | { 244 | final int recipeDimensions = (int) Math.ceil(Math.sqrt(Math.min(recipe.getRecipeLength(), 9))); 245 | int scaledStackDimensions = (int) (16 * this.scale); 246 | int scaledGridEntry = (int) (17 * this.scale); 247 | int x = this.recipeListX - (int) ((3 * 17 - 2) * this.scale); 248 | int y = this.recipeListY + (int) (3 * this.entryHeight * this.scale); 249 | 250 | if (mouseX >= x && mouseX <= x + recipeDimensions * scaledGridEntry && 251 | mouseY >= y && mouseY <= y + recipeDimensions * scaledGridEntry) 252 | { 253 | for (int i = 0, row = 0; row < recipeDimensions; row++) 254 | { 255 | for (int col = 0; col < recipeDimensions; col++, i++) 256 | { 257 | int xOff = col * scaledGridEntry; 258 | int yOff = row * scaledGridEntry; 259 | int xStart = x + xOff; 260 | int yStart = y + yOff; 261 | 262 | if (mouseX >= xStart && mouseX < xStart + scaledStackDimensions && 263 | mouseY >= yStart && mouseY < yStart + scaledStackDimensions) 264 | { 265 | return recipe.getRecipeItems()[i]; 266 | } 267 | } 268 | } 269 | } 270 | 271 | return ItemStack.EMPTY; 272 | } 273 | 274 | private void renderStackAt(GuiContext ctx, ItemStack stack, int x, int y, boolean border) 275 | { 276 | final int w = 16; 277 | // int xAdj = (int) ((x) * this.scale) + this.recipeListX; 278 | // int yAdj = (int) ((y) * this.scale) + this.recipeListY; 279 | // int wAdj = (int) ((w) * this.scale); 280 | 281 | if (border) 282 | { 283 | // Draw a light/white border around the stack 284 | RenderUtils.drawOutline(ctx, x - 1, y - 1, w + 2, w + 2, 0xFFFFFFFF); 285 | } 286 | 287 | // light background for the item 288 | RenderUtils.drawRect(ctx, x, y, w, w, 0x20FFFFFF); 289 | 290 | if (!InventoryUtils.isStackEmpty(stack)) 291 | { 292 | stack = stack.copy(); 293 | InventoryUtils.setStackSize(stack, 1); 294 | 295 | ctx.pose().pushMatrix(); 296 | ctx.pose().translate(0, 0); // z = 100.f 297 | ctx.renderItem(stack, x, y); 298 | ctx.pose().popMatrix(); 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/recipes/RecipeStorage.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.recipes; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import javax.annotation.Nonnull; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.core.RegistryAccess; 11 | import net.minecraft.world.inventory.Slot; 12 | import net.minecraft.world.item.crafting.display.RecipeDisplayEntry; 13 | import net.minecraft.world.item.crafting.display.RecipeDisplayId; 14 | import fi.dy.masa.malilib.util.FileUtils; 15 | import fi.dy.masa.malilib.util.StringUtils; 16 | import fi.dy.masa.malilib.util.data.Constants; 17 | import fi.dy.masa.malilib.util.data.tag.CompoundData; 18 | import fi.dy.masa.malilib.util.data.tag.ListData; 19 | import fi.dy.masa.malilib.util.data.tag.util.DataFileUtils; 20 | import fi.dy.masa.malilib.util.game.RecipeBookUtils; 21 | import fi.dy.masa.itemscroller.ItemScroller; 22 | import fi.dy.masa.itemscroller.Reference; 23 | import fi.dy.masa.itemscroller.config.Configs; 24 | 25 | public class RecipeStorage 26 | { 27 | private static final int MAX_PAGES = 8; // 8 Pages of 18 = 144 total slots 28 | private static final int MAX_RECIPES = 18; // 8 Pages of 18 = 144 total slots 29 | private static final RecipeStorage INSTANCE = new RecipeStorage(MAX_RECIPES * MAX_PAGES); 30 | private final RecipePattern[] recipes; 31 | private int selected; 32 | private boolean dirty; 33 | 34 | public static RecipeStorage getInstance() 35 | { 36 | return INSTANCE; 37 | } 38 | 39 | public RecipeStorage(int recipeCount) 40 | { 41 | this.recipes = new RecipePattern[recipeCount]; 42 | this.initRecipes(); 43 | } 44 | 45 | public void reset(boolean isLogout) 46 | { 47 | if (isLogout) 48 | { 49 | this.clearRecipes(); 50 | } 51 | } 52 | 53 | private void initRecipes() 54 | { 55 | for (int i = 0; i < this.recipes.length; i++) 56 | { 57 | this.recipes[i] = new RecipePattern(); 58 | } 59 | } 60 | 61 | private void clearRecipes() 62 | { 63 | for (int i = 0; i < this.recipes.length; i++) 64 | { 65 | this.clearRecipe(i); 66 | } 67 | } 68 | 69 | public int getSelection() 70 | { 71 | return this.selected; 72 | } 73 | 74 | public void changeSelectedRecipe(int index) 75 | { 76 | if (index >= 0 && index < this.recipes.length) 77 | { 78 | this.selected = index; 79 | this.dirty = true; 80 | } 81 | } 82 | 83 | public void scrollSelection(boolean forward) 84 | { 85 | this.changeSelectedRecipe(this.selected + (forward ? 1 : -1)); 86 | } 87 | 88 | public int getFirstVisibleRecipeId() 89 | { 90 | return this.getCurrentRecipePage() * this.getRecipeCountPerPage(); 91 | } 92 | 93 | public int getTotalRecipeCount() 94 | { 95 | return this.recipes.length; 96 | } 97 | 98 | public int getRecipeCountPerPage() 99 | { 100 | return MAX_RECIPES; 101 | } 102 | 103 | public int getCurrentRecipePage() 104 | { 105 | return this.getSelection() / this.getRecipeCountPerPage(); 106 | } 107 | 108 | /** 109 | * Returns the recipe for the given index. 110 | * If the index is invalid, then the first recipe is returned, instead of null. 111 | */ 112 | @Nonnull 113 | public RecipePattern getRecipe(int index) 114 | { 115 | if (index >= 0 && index < this.recipes.length) 116 | { 117 | return this.recipes[index]; 118 | } 119 | 120 | return this.recipes[0]; 121 | } 122 | 123 | @Nonnull 124 | public RecipePattern getSelectedRecipe() 125 | { 126 | return this.getRecipe(this.getSelection()); 127 | } 128 | 129 | public void storeCraftingRecipeToCurrentSelection(Slot slot, AbstractContainerScreen gui, boolean clearIfEmpty, boolean fromKeybind, Minecraft mc) 130 | { 131 | this.storeCraftingRecipe(this.getSelection(), slot, gui, clearIfEmpty, fromKeybind, mc); 132 | } 133 | 134 | public void storeCraftingRecipe(int index, Slot slot, AbstractContainerScreen gui, boolean clearIfEmpty, boolean fromKeybind, Minecraft mc) 135 | { 136 | this.getRecipe(index).storeCraftingRecipe(slot, gui, clearIfEmpty, fromKeybind, mc); 137 | this.dirty = true; 138 | } 139 | 140 | public void clearRecipe(int index) 141 | { 142 | this.getRecipe(index).clearRecipe(); 143 | this.dirty = true; 144 | } 145 | 146 | public void onAddToRecipeBook(RecipeDisplayEntry entry) 147 | { 148 | Minecraft mc = Minecraft.getInstance(); 149 | 150 | // DEBUG 151 | // RecipeBookUtils.toggleDebugLog(true); 152 | // RecipeBookUtils.toggleAnsiColorLog(true); 153 | 154 | for (RecipePattern recipe : this.recipes) 155 | { 156 | if (!recipe.isEmpty()) 157 | { 158 | List types; 159 | 160 | if (recipe.getRecipeType() != null) 161 | { 162 | types = List.of(recipe.getRecipeType()); 163 | } 164 | else 165 | { 166 | types = List.of(RecipeBookUtils.Type.SHAPED, RecipeBookUtils.Type.SHAPELESS); 167 | } 168 | 169 | if (RecipeBookUtils.matchClientRecipeBookEntry(recipe.getResult(), Arrays.asList(recipe.getRecipeItems()), entry, types, mc)) 170 | // if (recipe.matchClientRecipeBookEntry(entry, mc)) 171 | { 172 | ItemScroller.debugLog("onAddToRecipeBook(): Positive Match for result stack: [{}] networkId [{}]", recipe.getResult().toString(), entry.id().index()); 173 | recipe.storeNetworkRecipeId(entry.id()); 174 | recipe.storeRecipeCategory(entry.category()); 175 | recipe.storeRecipeDisplayEntry(entry); 176 | recipe.storeRecipeType(RecipeBookUtils.Type.fromRecipeDisplay(entry.display())); 177 | break; 178 | } 179 | } 180 | } 181 | } 182 | 183 | private void readFromNBT(CompoundData data, @Nonnull RegistryAccess registryManager) 184 | { 185 | if (data == null || data.contains("Recipes", Constants.NBT.TAG_LIST) == false) 186 | { 187 | return; 188 | } 189 | 190 | for (int i = 0; i < this.recipes.length; i++) 191 | { 192 | this.recipes[i].clearRecipe(); 193 | } 194 | 195 | ListData tagList = data.getList("Recipes"); 196 | int count = tagList.size(); 197 | 198 | for (int i = 0; i < count; i++) 199 | { 200 | CompoundData tag = tagList.getCompoundAt(i); 201 | 202 | int index = tag.getByte("RecipeIndex"); 203 | 204 | if (index >= 0 && index < this.recipes.length) 205 | { 206 | this.recipes[index].readFromData(tag, registryManager); 207 | 208 | if (tag.contains("RecipeCategory", Constants.NBT.TAG_STRING)) 209 | { 210 | this.recipes[index].storeRecipeCategory(RecipeBookUtils.getRecipeCategoryFromId(tag.getString("RecipeCategory"))); 211 | } 212 | if (tag.contains("LastNetworkId", Constants.NBT.TAG_INT)) 213 | { 214 | this.recipes[index].storeNetworkRecipeId(new RecipeDisplayId(tag.getInt("LastNetworkId"))); 215 | } 216 | if (tag.contains("RecipeType", Constants.NBT.TAG_STRING)) 217 | { 218 | String recipeType = tag.getString("RecipeType"); 219 | 220 | if (!recipeType.isEmpty()) 221 | { 222 | for (RecipeBookUtils.Type type : RecipeBookUtils.Type.values()) 223 | { 224 | if (type.name().equalsIgnoreCase(recipeType)) 225 | { 226 | this.recipes[index].storeRecipeType(type); 227 | } 228 | } 229 | } 230 | 231 | } 232 | } 233 | } 234 | 235 | this.changeSelectedRecipe(data.getByte("Selected")); 236 | } 237 | 238 | private CompoundData writeToNBT(@Nonnull RegistryAccess registry) 239 | { 240 | ListData tagRecipes = new ListData(); 241 | CompoundData data = new CompoundData(); 242 | 243 | for (int i = 0; i < this.recipes.length; i++) 244 | { 245 | if (this.recipes[i].isValid()) 246 | { 247 | RecipePattern entry = this.recipes[i]; 248 | CompoundData tag = entry.writeToData(registry); 249 | tag.putByte("RecipeIndex", (byte) i); 250 | 251 | if (entry.getRecipeCategory() != null) 252 | { 253 | String id = RecipeBookUtils.getRecipeCategoryId(entry.getRecipeCategory()); 254 | 255 | if (!id.isEmpty()) 256 | { 257 | tag.putString("RecipeCategory", id); 258 | } 259 | } 260 | if (entry.getNetworkRecipeId() != null) 261 | { 262 | tag.putInt("LastNetworkId", entry.getNetworkRecipeId().index()); 263 | } 264 | if (entry.getRecipeType() != null) 265 | { 266 | tag.putString("RecipeType", entry.getRecipeType().name().toLowerCase()); 267 | } 268 | 269 | tagRecipes.add(tag); 270 | } 271 | } 272 | 273 | data.put("Recipes", tagRecipes); 274 | data.putByte("Selected", (byte) this.selected); 275 | 276 | return data; 277 | } 278 | 279 | private String getFileName() 280 | { 281 | if (Configs.Generic.SCROLL_CRAFT_RECIPE_FILE_GLOBAL.getBooleanValue() == false) 282 | { 283 | String worldName = StringUtils.getWorldOrServerName(); 284 | 285 | if (worldName != null) 286 | { 287 | return "recipes_" + worldName + ".nbt"; 288 | } 289 | else 290 | { 291 | return "recipes_unknown.nbt"; 292 | } 293 | } 294 | 295 | return "recipes.nbt"; 296 | } 297 | 298 | private Path getSaveDirAsPath() 299 | { 300 | return FileUtils.getMinecraftDirectoryAsPath().resolve(Reference.MOD_ID); 301 | } 302 | 303 | public void readFromDisk(@Nonnull RegistryAccess registry) 304 | { 305 | try 306 | { 307 | Path saveDir = this.getSaveDirAsPath(); 308 | 309 | if (Files.isDirectory(saveDir)) 310 | { 311 | Path file = saveDir.resolve(this.getFileName()); 312 | 313 | if (Files.exists(file)) 314 | { 315 | CompoundData nbtIn = DataFileUtils.readCompoundDataFromNbtFile(file); 316 | 317 | if (nbtIn != null && !nbtIn.isEmpty()) 318 | { 319 | this.initRecipes(); 320 | this.readFromNBT(nbtIn, registry); 321 | 322 | //ItemScroller.debugLog("readFromDisk(): Successfully loaded recipe's from file '{}'", file.toAbsolutePath()); 323 | } 324 | else 325 | { 326 | ItemScroller.LOGGER.warn("readFromDisk(): Error reading recipes from file '{}'", file.toAbsolutePath()); 327 | } 328 | } 329 | // File does not exist 330 | } 331 | else 332 | { 333 | ItemScroller.LOGGER.warn("readFromDisk(): Error reading recipes saveDir '{}'", saveDir.toAbsolutePath()); 334 | } 335 | } 336 | catch (Exception e) 337 | { 338 | ItemScroller.LOGGER.warn("readFromDisk(): Failed to read recipes from file", e); 339 | } 340 | } 341 | 342 | public void writeToDisk(@Nonnull RegistryAccess registry) 343 | { 344 | if (this.dirty) 345 | { 346 | try 347 | { 348 | Path saveDir = this.getSaveDirAsPath(); 349 | 350 | if (!Files.exists(saveDir)) 351 | { 352 | FileUtils.createDirectoriesIfMissing(saveDir); 353 | //ItemScroller.debugLog("writeToDisk(): Creating directory '{}'.", saveDir.toAbsolutePath()); 354 | } 355 | 356 | if (Files.isDirectory(saveDir)) 357 | { 358 | Path fileTmp = saveDir.resolve(this.getFileName() + ".tmp"); 359 | Path fileReal = saveDir.resolve(this.getFileName()); 360 | 361 | // NbtUtils.writeCompressed(this.writeToNBT(registry), fileTmp); 362 | CompoundData data = this.writeToNBT(registry); 363 | DataFileUtils.writeCompoundDataToCompressedNbtFile(fileTmp, data); 364 | 365 | if (Files.exists(fileReal)) 366 | { 367 | Files.delete(fileReal); 368 | } 369 | 370 | Files.move(fileTmp, fileReal); 371 | 372 | //ItemScroller.debugLog("writeToDisk(): Successfully saved recipes file '{}'", fileReal.toAbsolutePath()); 373 | this.dirty = false; 374 | } 375 | } 376 | catch (Exception e) 377 | { 378 | ItemScroller.LOGGER.warn("writeToDisk(): Failed to write recipes to file!", e); 379 | } 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/config/Configs.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.config; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | import net.minecraft.client.gui.screens.inventory.CraftingScreen; 8 | import net.minecraft.client.gui.screens.inventory.InventoryScreen; 9 | import net.minecraft.world.inventory.ResultSlot; 10 | import com.google.common.collect.ImmutableList; 11 | import com.google.gson.JsonArray; 12 | import com.google.gson.JsonElement; 13 | import com.google.gson.JsonObject; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import fi.dy.masa.malilib.config.ConfigUtils; 17 | import fi.dy.masa.malilib.config.IConfigBase; 18 | import fi.dy.masa.malilib.config.IConfigHandler; 19 | import fi.dy.masa.malilib.config.IConfigValue; 20 | import fi.dy.masa.malilib.config.options.*; 21 | import fi.dy.masa.malilib.util.FileUtils; 22 | import fi.dy.masa.malilib.util.JsonUtils; 23 | import fi.dy.masa.itemscroller.ItemScroller; 24 | import fi.dy.masa.itemscroller.Reference; 25 | import fi.dy.masa.itemscroller.recipes.CraftingHandler; 26 | import fi.dy.masa.itemscroller.recipes.CraftingHandler.SlotRange; 27 | import fi.dy.masa.itemscroller.util.SortingCategory; 28 | import fi.dy.masa.itemscroller.util.SortingMethod; 29 | 30 | public class Configs implements IConfigHandler 31 | { 32 | private static final String CONFIG_FILE_NAME = Reference.MOD_ID + ".json"; 33 | 34 | private static final ImmutableList<@NotNull String> DEFAULT_TOP_SORTING = ImmutableList.of("minecraft:diamond_sword", "minecraft:diamond_pickaxe", "minecraft:diamond_axe", "minecraft:diamond_shovel", "minecraft:diamond_hoe", "minecraft:netherite_sword", "minecraft:netherite_pickaxe", "minecraft:netherite_axe", "minecraft:netherite_shovel", "minecraft:netherite_hoe"); 35 | private static final ImmutableList<@NotNull String> DEFAULT_BOTTOM_SORTING = ImmutableList.of(); 36 | 37 | private static final String GENERIC_KEY = Reference.MOD_ID+".config.generic"; 38 | public static class Generic 39 | { 40 | public static final ConfigBoolean CARPET_CTRL_Q_CRAFTING = new ConfigBoolean("carpetCtrlQCraftingEnabledOnServer", false).apply(GENERIC_KEY); 41 | public static final ConfigBoolean CLIENT_CRAFTING_FIX = new ConfigBoolean("clientCraftingFixOn1.12", true).apply(GENERIC_KEY); 42 | public static final ConfigBoolean CRAFTING_RENDER_RECIPE_ITEMS = new ConfigBoolean("craftingRenderRecipeItems", true).apply(GENERIC_KEY); 43 | public static final ConfigBoolean DEBUG_MESSAGES = new ConfigBoolean("debugMessages", false).apply(GENERIC_KEY); 44 | public static final ConfigBoolean MOD_MAIN_TOGGLE = new ConfigBoolean("modMainToggle", true).apply(GENERIC_KEY); 45 | public static final ConfigBoolean MASS_CRAFT_INHIBIT_MID_UPDATES = new ConfigBoolean("massCraftInhibitMidUpdates", true).apply(GENERIC_KEY); 46 | public static final ConfigInteger MASS_CRAFT_INTERVAL = new ConfigInteger("massCraftInterval", 2, 1, 60).apply(GENERIC_KEY); 47 | public static final ConfigInteger MASS_CRAFT_ITERATIONS = new ConfigInteger("massCraftIterations", 36, 1, 256).apply(GENERIC_KEY); 48 | public static final ConfigBoolean MASS_CRAFT_SWAPS = new ConfigBoolean("massCraftSwapsOnly", false).apply(GENERIC_KEY); 49 | public static final ConfigBoolean MASS_CRAFT_RECIPE_BOOK = new ConfigBoolean("massCraftUseRecipeBook", true).apply(GENERIC_KEY); 50 | public static final ConfigBoolean MASS_CRAFT_HOLD = new ConfigBoolean("massCraftHold", false).apply(GENERIC_KEY); 51 | public static final ConfigInteger PACKET_RATE_LIMIT = new ConfigInteger("packetRateLimit", 4, 1, 1024).apply(GENERIC_KEY); 52 | public static final ConfigBoolean SCROLL_CRAFT_STORE_RECIPES_TO_FILE = new ConfigBoolean("craftingRecipesSaveToFile", true).apply(GENERIC_KEY); 53 | public static final ConfigBoolean SCROLL_CRAFT_RECIPE_FILE_GLOBAL = new ConfigBoolean("craftingRecipesSaveFileIsGlobal", false).apply(GENERIC_KEY); 54 | public static final ConfigBoolean RATE_LIMIT_CLICK_PACKETS = new ConfigBoolean("rateLimitClickPackets", false).apply(GENERIC_KEY); 55 | public static final ConfigBoolean REVERSE_SCROLL_DIRECTION_SINGLE = new ConfigBoolean("reverseScrollDirectionSingle", false).apply(GENERIC_KEY); 56 | public static final ConfigBoolean REVERSE_SCROLL_DIRECTION_STACKS = new ConfigBoolean("reverseScrollDirectionStacks", false).apply(GENERIC_KEY); 57 | public static final ConfigBoolean USE_RECIPE_CACHING = new ConfigBoolean("useRecipeCaching", true).apply(GENERIC_KEY); 58 | public static final ConfigBoolean SLOT_POSITION_AWARE_SCROLL_DIRECTION = new ConfigBoolean("useSlotPositionAwareScrollDirection", false).apply(GENERIC_KEY); 59 | public static final ConfigBoolean VILLAGER_TRADE_USE_GLOBAL_FAVORITES = new ConfigBoolean("villagerTradeUseGlobalFavorites", true).apply(GENERIC_KEY); 60 | public static final ConfigBoolean VILLAGER_TRADE_LIST_REMEMBER_SCROLL = new ConfigBoolean("villagerTradeListRememberScrollPosition", true).apply(GENERIC_KEY); 61 | 62 | public static final ConfigBoolean SORT_INVENTORY_TOGGLE = new ConfigBoolean("sortInventoryToggle", false).apply(GENERIC_KEY); 63 | public static final ConfigBoolean SORT_ASSUME_EMPTY_BOX_STACKS = new ConfigBoolean("sortAssumeEmptyBoxStacks", false).apply(GENERIC_KEY); 64 | public static final ConfigBoolean SORT_SHULKER_BOXES_AT_END = new ConfigBoolean("sortShulkerBoxesAtEnd", true).apply(GENERIC_KEY); 65 | public static final ConfigBoolean SORT_SHULKER_BOXES_INVERTED = new ConfigBoolean("sortShulkerBoxesInverted", false).apply(GENERIC_KEY); 66 | public static final ConfigBoolean SORT_BUNDLES_AT_END = new ConfigBoolean("sortBundlesAtEnd", true).apply(GENERIC_KEY); 67 | public static final ConfigBoolean SORT_BUNDLES_INVERTED = new ConfigBoolean("sortBundlesInverted", false).apply(GENERIC_KEY); 68 | public static final ConfigStringList SORT_TOP_PRIORITY_INVENTORY = new ConfigStringList("sortTopPriorityInventory", DEFAULT_TOP_SORTING).apply(GENERIC_KEY); 69 | public static final ConfigStringList SORT_BOTTOM_PRIORITY_INVENTORY = new ConfigStringList("sortBottomPriorityInventory", DEFAULT_BOTTOM_SORTING).apply(GENERIC_KEY); 70 | public static final ConfigOptionList SORT_METHOD_DEFAULT = new ConfigOptionList("sortMethodDefault", SortingMethod.CATEGORY_NAME).apply(GENERIC_KEY); 71 | public static final ConfigLockedList SORT_CATEGORY_ORDER = new ConfigLockedList("sortCategoryOrder", SortingCategory.INSTANCE).apply(GENERIC_KEY); 72 | 73 | public static final ImmutableList<@NotNull IConfigBase> OPTIONS = ImmutableList.of( 74 | CARPET_CTRL_Q_CRAFTING, 75 | CLIENT_CRAFTING_FIX, 76 | CRAFTING_RENDER_RECIPE_ITEMS, 77 | DEBUG_MESSAGES, 78 | MASS_CRAFT_INHIBIT_MID_UPDATES, 79 | MASS_CRAFT_INTERVAL, 80 | MASS_CRAFT_ITERATIONS, 81 | MASS_CRAFT_SWAPS, 82 | MASS_CRAFT_RECIPE_BOOK, 83 | MASS_CRAFT_HOLD, 84 | MOD_MAIN_TOGGLE, 85 | PACKET_RATE_LIMIT, 86 | RATE_LIMIT_CLICK_PACKETS, 87 | SCROLL_CRAFT_STORE_RECIPES_TO_FILE, 88 | SCROLL_CRAFT_RECIPE_FILE_GLOBAL, 89 | REVERSE_SCROLL_DIRECTION_SINGLE, 90 | REVERSE_SCROLL_DIRECTION_STACKS, 91 | SLOT_POSITION_AWARE_SCROLL_DIRECTION, 92 | USE_RECIPE_CACHING, 93 | VILLAGER_TRADE_USE_GLOBAL_FAVORITES, 94 | VILLAGER_TRADE_LIST_REMEMBER_SCROLL, 95 | 96 | SORT_INVENTORY_TOGGLE, 97 | SORT_ASSUME_EMPTY_BOX_STACKS, 98 | SORT_SHULKER_BOXES_AT_END, 99 | SORT_SHULKER_BOXES_INVERTED, 100 | SORT_BUNDLES_AT_END, 101 | SORT_BUNDLES_INVERTED, 102 | SORT_TOP_PRIORITY_INVENTORY, 103 | SORT_BOTTOM_PRIORITY_INVENTORY, 104 | SORT_METHOD_DEFAULT, 105 | SORT_CATEGORY_ORDER 106 | ); 107 | } 108 | 109 | private static final String TOGGLES_KEY = Reference.MOD_ID+".config.toggles"; 110 | public static class Toggles 111 | { 112 | public static final ConfigBoolean CRAFTING_FEATURES = new ConfigBoolean("enableCraftingFeatures", true).apply(TOGGLES_KEY); 113 | public static final ConfigBoolean DROP_MATCHING = new ConfigBoolean("enableDropkeyDropMatching", true).apply(TOGGLES_KEY); 114 | public static final ConfigBoolean RIGHT_CLICK_CRAFT_STACK = new ConfigBoolean("enableRightClickCraftingOneStack", true).apply(TOGGLES_KEY); 115 | public static final ConfigBoolean SCROLL_EVERYTHING = new ConfigBoolean("enableScrollingEverything", true).apply(TOGGLES_KEY); 116 | public static final ConfigBoolean SCROLL_MATCHING = new ConfigBoolean("enableScrollingMatchingStacks", true).apply(TOGGLES_KEY); 117 | public static final ConfigBoolean SCROLL_SINGLE = new ConfigBoolean("enableScrollingSingle", true).apply(TOGGLES_KEY); 118 | public static final ConfigBoolean SCROLL_STACKS = new ConfigBoolean("enableScrollingStacks", true).apply(TOGGLES_KEY); 119 | public static final ConfigBoolean SCROLL_STACKS_FALLBACK = new ConfigBoolean("enableScrollingStacksFallback", true).apply(TOGGLES_KEY); 120 | public static final ConfigBoolean SCROLL_VILLAGER = new ConfigBoolean("enableScrollingVillager", true).apply(TOGGLES_KEY); 121 | public static final ConfigBoolean SHIFT_DROP_ITEMS = new ConfigBoolean("enableShiftDropItems", true).apply(TOGGLES_KEY); 122 | public static final ConfigBoolean SHIFT_PLACE_ITEMS = new ConfigBoolean("enableShiftPlaceItems", true).apply(TOGGLES_KEY); 123 | public static final ConfigBoolean VILLAGER_TRADE_FEATURES = new ConfigBoolean("enableVillagerTradeFeatures", true).apply(TOGGLES_KEY); 124 | 125 | public static final ImmutableList<@NotNull IConfigValue> OPTIONS = ImmutableList.of( 126 | CRAFTING_FEATURES, 127 | DROP_MATCHING, 128 | RIGHT_CLICK_CRAFT_STACK, 129 | SCROLL_EVERYTHING, 130 | SCROLL_MATCHING, 131 | SCROLL_SINGLE, 132 | SCROLL_STACKS, 133 | SCROLL_STACKS_FALLBACK, 134 | SCROLL_VILLAGER, 135 | SHIFT_DROP_ITEMS, 136 | SHIFT_PLACE_ITEMS, 137 | VILLAGER_TRADE_FEATURES 138 | ); 139 | } 140 | 141 | public static final Set GUI_BLACKLIST = new HashSet<>(); 142 | public static final Set SLOT_BLACKLIST = new HashSet<>(); 143 | 144 | public static void loadFromFile() 145 | { 146 | Path configFile = FileUtils.getConfigDirectoryAsPath().resolve(CONFIG_FILE_NAME); 147 | 148 | if (Files.exists(configFile) && Files.isReadable(configFile)) 149 | { 150 | JsonElement element = JsonUtils.parseJsonFileAsPath(configFile); 151 | 152 | if (element != null && element.isJsonObject()) 153 | { 154 | JsonObject root = element.getAsJsonObject(); 155 | 156 | ConfigUtils.readConfigBase(root, "Generic", Generic.OPTIONS); 157 | ConfigUtils.readConfigBase(root, "Hotkeys", Hotkeys.HOTKEY_LIST); 158 | ConfigUtils.readConfigBase(root, "Toggles", Toggles.OPTIONS); 159 | 160 | getStrings(root, GUI_BLACKLIST, "guiBlacklist"); 161 | getStrings(root, SLOT_BLACKLIST, "slotBlacklist"); 162 | 163 | //ItemScroller.debugLog("loadFromFile(): Successfully loaded config file '{}'.", configFile.toAbsolutePath()); 164 | } 165 | } 166 | else 167 | { 168 | ItemScroller.LOGGER.error("loadFromFile(): Failed to load config file '{}'.", configFile.toAbsolutePath()); 169 | } 170 | 171 | CraftingHandler.clearDefinitions(); 172 | 173 | // "net.minecraft.client.gui.inventory.GuiCrafting,net.minecraft.inventory.SlotCrafting,0,1-9", // vanilla Crafting Table 174 | CraftingHandler.addCraftingGridDefinition(CraftingScreen.class.getName(), ResultSlot.class.getName(), 0, new SlotRange(1, 9)); 175 | //"net.minecraft.client.gui.inventory.PlayerInventoryScreen,net.minecraft.inventory.SlotCrafting,0,1-4", // vanilla player inventory crafting grid 176 | CraftingHandler.addCraftingGridDefinition(InventoryScreen.class.getName(), ResultSlot.class.getName(), 0, new SlotRange(1, 4)); 177 | // CraftingHandler.addCraftingGridDefinition(StonecutterScreen.class.getName(), Slot.class.getName(), 0, new SlotRange(1, 1)); 178 | // TODO FIXME -- Stonecutter Screen (Slot numbering, etc) --> Doesn't work the same as the crafting grid 179 | } 180 | 181 | public static void saveToFile() 182 | { 183 | Path dir = FileUtils.getConfigDirectoryAsPath(); 184 | 185 | if (!Files.exists(dir)) 186 | { 187 | FileUtils.createDirectoriesIfMissing(dir); 188 | //ItemScroller.debugLog("saveToFile(): Creating directory '{}'.", dir.toAbsolutePath()); 189 | } 190 | 191 | if (Files.isDirectory(dir)) 192 | { 193 | JsonObject root = new JsonObject(); 194 | 195 | ConfigUtils.writeConfigBase(root, "Generic", Generic.OPTIONS); 196 | ConfigUtils.writeConfigBase(root, "Hotkeys", Hotkeys.HOTKEY_LIST); 197 | ConfigUtils.writeConfigBase(root, "Toggles", Toggles.OPTIONS); 198 | 199 | writeStrings(root, GUI_BLACKLIST, "guiBlacklist"); 200 | writeStrings(root, SLOT_BLACKLIST, "slotBlacklist"); 201 | 202 | JsonUtils.writeJsonToFileAsPath(root, dir.resolve(CONFIG_FILE_NAME)); 203 | } 204 | else 205 | { 206 | ItemScroller.LOGGER.error("saveToFile(): Config Folder '{}' does not exist!", dir.toAbsolutePath()); 207 | } 208 | } 209 | 210 | @Override 211 | public void load() 212 | { 213 | loadFromFile(); 214 | } 215 | 216 | @Override 217 | public void save() 218 | { 219 | saveToFile(); 220 | } 221 | 222 | private static void getStrings(JsonObject obj, Set outputSet, String arrayName) 223 | { 224 | outputSet.clear(); 225 | 226 | if (JsonUtils.hasArray(obj, arrayName)) 227 | { 228 | JsonArray arr = obj.getAsJsonArray(arrayName); 229 | final int size = arr.size(); 230 | 231 | for (int i = 0; i < size; i++) 232 | { 233 | outputSet.add(arr.get(i).getAsString()); 234 | } 235 | } 236 | } 237 | 238 | private static void writeStrings(JsonObject obj, Set inputSet, String arrayName) 239 | { 240 | if (inputSet.isEmpty() == false) 241 | { 242 | JsonArray arr = new JsonArray(); 243 | 244 | for (String str : inputSet) 245 | { 246 | arr.add(str); 247 | } 248 | 249 | obj.add(arrayName, arr); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/itemscroller/event/KeybindCallbacks.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.itemscroller.event; 2 | 3 | import fi.dy.masa.malilib.config.options.ConfigHotkey; 4 | import fi.dy.masa.malilib.gui.GuiBase; 5 | import fi.dy.masa.malilib.gui.Message; 6 | import fi.dy.masa.malilib.hotkeys.IHotkeyCallback; 7 | import fi.dy.masa.malilib.hotkeys.IKeybind; 8 | import fi.dy.masa.malilib.hotkeys.KeyAction; 9 | import fi.dy.masa.malilib.hotkeys.KeyCallbackToggleBooleanConfigWithMessage; 10 | import fi.dy.masa.malilib.interfaces.IClientTickHandler; 11 | import fi.dy.masa.malilib.util.GuiUtils; 12 | import fi.dy.masa.malilib.util.InfoUtils; 13 | import net.minecraft.client.Minecraft; 14 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 15 | import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; 16 | import net.minecraft.world.inventory.CraftingContainer; 17 | import net.minecraft.world.inventory.Slot; 18 | import net.minecraft.world.item.ItemStack; 19 | import fi.dy.masa.itemscroller.ItemScroller; 20 | import fi.dy.masa.itemscroller.config.Configs; 21 | import fi.dy.masa.itemscroller.config.Hotkeys; 22 | import fi.dy.masa.itemscroller.gui.GuiConfigs; 23 | import fi.dy.masa.itemscroller.mixin.recipe.IMixinCraftingResultSlot; 24 | import fi.dy.masa.itemscroller.recipes.CraftingHandler; 25 | import fi.dy.masa.itemscroller.recipes.RecipePattern; 26 | import fi.dy.masa.itemscroller.recipes.RecipeStorage; 27 | import fi.dy.masa.itemscroller.util.*; 28 | 29 | public class KeybindCallbacks implements IHotkeyCallback, IClientTickHandler 30 | { 31 | private static final KeybindCallbacks INSTANCE = new KeybindCallbacks(); 32 | protected int massCraftTicker; 33 | private boolean recipeBookClicks = false; 34 | public static KeybindCallbacks getInstance() 35 | { 36 | return INSTANCE; 37 | } 38 | 39 | private KeybindCallbacks() 40 | { 41 | } 42 | 43 | public void setCallbacks() 44 | { 45 | for (ConfigHotkey hotkey : Hotkeys.HOTKEY_LIST) 46 | { 47 | hotkey.getKeybind().setCallback(this); 48 | } 49 | 50 | Hotkeys.MASS_CRAFT_TOGGLE.getKeybind().setCallback(new KeyCallbackToggleBooleanConfigWithMessage(Configs.Generic.MASS_CRAFT_HOLD)); 51 | } 52 | 53 | public boolean functionalityEnabled() 54 | { 55 | return Configs.Generic.MOD_MAIN_TOGGLE.getBooleanValue(); 56 | } 57 | 58 | @Override 59 | public boolean onKeyAction(KeyAction action, IKeybind key) 60 | { 61 | if (Configs.Generic.RATE_LIMIT_CLICK_PACKETS.getBooleanValue()) 62 | { 63 | ClickPacketBuffer.setShouldBufferClickPackets(true); 64 | } 65 | 66 | boolean cancel = this.onKeyActionImpl(action, key); 67 | 68 | ClickPacketBuffer.setShouldBufferClickPackets(false); 69 | 70 | return cancel; 71 | } 72 | 73 | private boolean onKeyActionImpl(KeyAction action, IKeybind key) 74 | { 75 | Minecraft mc = Minecraft.getInstance(); 76 | 77 | if (mc.player == null || mc.level == null) 78 | { 79 | return false; 80 | } 81 | 82 | if (key == Hotkeys.TOGGLE_MOD_ON_OFF.getKeybind()) 83 | { 84 | Configs.Generic.MOD_MAIN_TOGGLE.toggleBooleanValue(); 85 | String msg = this.functionalityEnabled() ? "itemscroller.message.toggled_mod_on" : "itemscroller.message.toggled_mod_off"; 86 | InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, msg); 87 | return true; 88 | } 89 | else if (key == Hotkeys.OPEN_CONFIG_GUI.getKeybind()) 90 | { 91 | GuiBase.openGui(new GuiConfigs()); 92 | return true; 93 | } 94 | 95 | if (this.functionalityEnabled() == false || 96 | (GuiUtils.getCurrentScreen() instanceof AbstractContainerScreen) == false || 97 | Configs.GUI_BLACKLIST.contains(GuiUtils.getCurrentScreen().getClass().getName())) 98 | { 99 | return false; 100 | } 101 | 102 | AbstractContainerScreen gui = (AbstractContainerScreen) GuiUtils.getCurrentScreen(); 103 | Slot slot = AccessorUtils.getSlotUnderMouse(gui); 104 | RecipeStorage recipes = RecipeStorage.getInstance(); 105 | MoveAction moveAction = InputUtils.getDragMoveAction(key); 106 | 107 | if (slot != null) 108 | { 109 | if (moveAction != MoveAction.NONE) 110 | { 111 | final int mouseX = fi.dy.masa.malilib.util.InputUtils.getMouseX(); 112 | final int mouseY = fi.dy.masa.malilib.util.InputUtils.getMouseY(); 113 | return InventoryUtils.dragMoveItems(gui, moveAction, mouseX, mouseY, true); 114 | } 115 | else if (key == Hotkeys.KEY_MOVE_EVERYTHING.getKeybind()) 116 | { 117 | InventoryUtils.tryMoveStacks(slot, gui, false, true, false); 118 | return true; 119 | } 120 | else if (key == Hotkeys.DROP_ALL_MATCHING.getKeybind()) 121 | { 122 | if (Configs.Toggles.DROP_MATCHING.getBooleanValue() && 123 | Configs.GUI_BLACKLIST.contains(gui.getClass().getName()) == false && 124 | slot.hasItem()) 125 | { 126 | InventoryUtils.dropStacks(gui, slot.getItem(), slot, true); 127 | return true; 128 | } 129 | } 130 | } 131 | 132 | if (key == Hotkeys.CRAFT_EVERYTHING.getKeybind()) 133 | { 134 | InventoryUtils.craftEverythingPossibleWithCurrentRecipe(recipes.getSelectedRecipe(), gui); 135 | return true; 136 | } 137 | else if (key == Hotkeys.THROW_CRAFT_RESULTS.getKeybind()) 138 | { 139 | InventoryUtils.throwAllCraftingResultsToGround(recipes.getSelectedRecipe(), gui); 140 | return true; 141 | } 142 | else if (key == Hotkeys.MOVE_CRAFT_RESULTS.getKeybind()) 143 | { 144 | InventoryUtils.moveAllCraftingResultsToOtherInventory(recipes.getSelectedRecipe(), gui); 145 | return true; 146 | } 147 | else if (key == Hotkeys.STORE_RECIPE.getKeybind()) 148 | { 149 | if (InputUtils.isRecipeViewOpen() && InventoryUtils.isCraftingSlot(gui, slot)) 150 | { 151 | recipes.storeCraftingRecipeToCurrentSelection(slot, gui, true, true, mc); 152 | return true; 153 | } 154 | } 155 | else if (key == Hotkeys.VILLAGER_TRADE_FAVORITES.getKeybind()) 156 | { 157 | return InventoryUtils.villagerTradeEverythingPossibleWithAllFavoritedTrades(); 158 | } 159 | else if (key == Hotkeys.SLOT_DEBUG.getKeybind()) 160 | { 161 | if (slot != null) 162 | { 163 | InventoryUtils.debugPrintSlotInfo(gui, slot); 164 | } 165 | else 166 | { 167 | ItemScroller.LOGGER.info("GUI class: {}", gui.getClass().getName()); 168 | } 169 | 170 | return true; 171 | } 172 | else if (key == Hotkeys.SORT_INVENTORY.getKeybind()) 173 | { 174 | if (Configs.Generic.SORT_INVENTORY_TOGGLE.getBooleanValue()) 175 | { 176 | InventoryUtils.sortInventory(gui); 177 | return true; 178 | } 179 | } 180 | 181 | return false; 182 | } 183 | 184 | @Override 185 | public void onClientTick(Minecraft mc) 186 | { 187 | if (InventoryUtils.dontUpdateRecipeBook > 0) 188 | { 189 | --InventoryUtils.dontUpdateRecipeBook; 190 | } 191 | 192 | if (this.functionalityEnabled() == false || 193 | mc.gameMode == null || mc.player == null || mc.level == null) 194 | { 195 | return; 196 | } 197 | 198 | ClickPacketBuffer.sendBufferedPackets(Configs.Generic.PACKET_RATE_LIMIT.getIntegerValue()); 199 | 200 | if (ClickPacketBuffer.shouldCancelWindowClicks()) 201 | { 202 | return; 203 | } 204 | 205 | if (GuiUtils.getCurrentScreen() instanceof AbstractContainerScreen gui && 206 | (GuiUtils.getCurrentScreen() instanceof CreativeModeInventoryScreen) == false && 207 | Configs.GUI_BLACKLIST.contains(GuiUtils.getCurrentScreen().getClass().getName()) == false && 208 | (Hotkeys.MASS_CRAFT.getKeybind().isKeybindHeld() || Configs.Generic.MASS_CRAFT_HOLD.getBooleanValue())) 209 | { 210 | if (++this.massCraftTicker < Configs.Generic.MASS_CRAFT_INTERVAL.getIntegerValue()) 211 | { 212 | return; 213 | } 214 | 215 | InventoryUtils.bufferInvUpdates = true; 216 | Slot outputSlot = CraftingHandler.getFirstCraftingOutputSlotForGui(gui); 217 | 218 | if (outputSlot != null) 219 | { 220 | if (Configs.Generic.RATE_LIMIT_CLICK_PACKETS.getBooleanValue()) 221 | { 222 | ClickPacketBuffer.setShouldBufferClickPackets(true); 223 | } 224 | 225 | RecipePattern recipe = RecipeStorage.getInstance().getSelectedRecipe(); 226 | 227 | int limit = Configs.Generic.MASS_CRAFT_ITERATIONS.getIntegerValue(); 228 | 229 | if (Configs.Generic.MASS_CRAFT_RECIPE_BOOK.getBooleanValue() && recipe.getNetworkRecipeId() != null) 230 | { 231 | InventoryUtils.dontUpdateRecipeBook = 2; 232 | 233 | for (int i = 0; i < limit; ++i) 234 | { 235 | // todo 236 | //InventoryUtils.setInhibitCraftingOutputUpdate(true); 237 | 238 | CraftingContainer craftingInv = ((IMixinCraftingResultSlot) outputSlot).itemscroller_getCraftingInventory(); 239 | if (recipe.getVanillaRecipe() != null && !recipe.getVanillaRecipe().matches(craftingInv.asCraftInput(), mc.level)) 240 | { 241 | CraftingHandler.SlotRange range = CraftingHandler.getCraftingGridSlots(gui, outputSlot); 242 | final int invSlots = gui.getMenu().slots.size(); 243 | final int rangeSlots = range.getSlotCount(); 244 | 245 | for (int j = 0, slotNum = range.getFirst(); j < rangeSlots && slotNum < invSlots; j++, slotNum++) 246 | { 247 | InventoryUtils.shiftClickSlot(gui, slotNum); 248 | 249 | Slot slotTmp = gui.getMenu().getSlot(slotNum); 250 | ItemStack stack = slotTmp.getItem(); 251 | if (!stack.isEmpty()) 252 | { 253 | InventoryUtils.dropStack(gui, slotNum); 254 | } 255 | } 256 | } 257 | 258 | mc.gameMode.handlePlaceRecipe(gui.getMenu().containerId, recipe.getNetworkRecipeId(), true); 259 | // InventoryUtils.setInhibitCraftingOutputUpdate(false); 260 | // InventoryUtils.updateCraftingOutputSlot(outputSlot); 261 | 262 | craftingInv = ((IMixinCraftingResultSlot) outputSlot).itemscroller_getCraftingInventory(); 263 | 264 | if (recipe.getVanillaRecipe() != null && 265 | recipe.getVanillaRecipe().matches(craftingInv.asCraftInput(), mc.level)) 266 | { 267 | break; 268 | } 269 | 270 | InventoryUtils.shiftClickSlot(gui, outputSlot.index); 271 | InventoryUtils.dropStack(gui, outputSlot.index); 272 | recipeBookClicks = true; 273 | } 274 | 275 | InventoryUtils.tryClearCursor(gui); 276 | InventoryUtils.throwAllCraftingResultsToGround(recipe, gui); 277 | } 278 | else if (Configs.Generic.MASS_CRAFT_SWAPS.getBooleanValue()) 279 | { 280 | for (int i = 0; i < limit; ++i) 281 | { 282 | InventoryUtils.tryClearCursor(gui); 283 | InventoryUtils.setInhibitCraftingOutputUpdate(true); 284 | InventoryUtils.throwAllCraftingResultsToGround(recipe, gui); 285 | InventoryUtils.throwAllNonRecipeItemsToGround(recipe, gui); 286 | CraftingContainer inv = ((IMixinCraftingResultSlot) (outputSlot)).itemscroller_getCraftingInventory(); 287 | //System.out.println("Before:"); 288 | //debugPrintInv(inv); 289 | try 290 | { 291 | Thread.sleep(0); 292 | } 293 | catch (InterruptedException ignored) { } 294 | 295 | InventoryUtils.setCraftingGridContentsUsingSwaps(gui, mc.player.getInventory(), recipe, outputSlot); 296 | //System.out.println("After:"); 297 | //debugPrintInv(inv); 298 | InventoryUtils.setInhibitCraftingOutputUpdate(false); 299 | InventoryUtils.updateCraftingOutputSlot(outputSlot); 300 | 301 | //System.out.printf("Output slot: %s\n", outputSlot.getStack()); 302 | 303 | if (InventoryUtils.areStacksEqual(outputSlot.getItem(), recipe.getResult()) == false) 304 | { 305 | break; 306 | } 307 | 308 | InventoryUtils.shiftClickSlot(gui, outputSlot.index); 309 | //System.out.println("Shift clicked"); 310 | //debugPrintInv(inv); 311 | } 312 | } 313 | else 314 | { 315 | int failsafe = 0; 316 | 317 | while (++failsafe < limit) 318 | { 319 | InventoryUtils.tryClearCursor(gui); 320 | InventoryUtils.setInhibitCraftingOutputUpdate(true); 321 | InventoryUtils.throwAllCraftingResultsToGround(recipe, gui); 322 | InventoryUtils.throwAllNonRecipeItemsToGround(recipe, gui); 323 | InventoryUtils.tryMoveItemsToFirstCraftingGrid(recipe, gui, true); 324 | InventoryUtils.setInhibitCraftingOutputUpdate(false); 325 | InventoryUtils.updateCraftingOutputSlot(outputSlot); 326 | 327 | if (InventoryUtils.areStacksEqual(outputSlot.getItem(), recipe.getResult()) == false) 328 | { 329 | break; 330 | } 331 | 332 | if (Configs.Generic.CARPET_CTRL_Q_CRAFTING.getBooleanValue()) 333 | { 334 | InventoryUtils.dropStack(gui, outputSlot.index); 335 | } 336 | else 337 | { 338 | InventoryUtils.dropStacksWhileHasItem(gui, outputSlot.index, recipe.getResult()); 339 | } 340 | } 341 | } 342 | 343 | ClickPacketBuffer.setShouldBufferClickPackets(false); 344 | } 345 | 346 | this.massCraftTicker = 0; 347 | InventoryUtils.bufferInvUpdates = false; 348 | InventoryUtils.invUpdatesBuffer.removeIf(packet -> 349 | { 350 | packet.handle(mc.player.connection); 351 | return true; 352 | }); 353 | } 354 | } 355 | 356 | // public void onPacket(ScreenHandlerSlotUpdateS2CPacket packet) 357 | // { 358 | //// var mc = MinecraftClient.getInstance(); 359 | // } 360 | } 361 | --------------------------------------------------------------------------------