├── CHANGELOG.md ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── common ├── src │ └── main │ │ ├── resources │ │ ├── pack.mcmeta │ │ ├── inventoryessentials.png │ │ ├── inventoryessentials.mixins.json │ │ └── assets │ │ │ └── inventoryessentials │ │ │ └── lang │ │ │ ├── zh_tw.json │ │ │ └── en_us.json │ │ └── java │ │ └── net │ │ └── blay09 │ │ └── mods │ │ └── inventoryessentials │ │ ├── PlatformBindings.java │ │ ├── data │ │ ├── IgnoredData.java │ │ ├── ModFileJsonCompatLoader.java │ │ └── ConfigJsonCompatLoader.java │ │ ├── mixin │ │ ├── SlotWrapperAccessor.java │ │ ├── AbstractContainerMenuAccessor.java │ │ ├── CreativeModeInventoryScreenAccessor.java │ │ └── AbstractContainerScreenAccessor.java │ │ ├── client │ │ ├── InventoryControls.java │ │ ├── CreativeInventoryControls.java │ │ ├── ServerSupportedInventoryControls.java │ │ ├── InventoryEssentialsClient.java │ │ ├── ModKeyMappings.java │ │ ├── ClientInventorySorting.java │ │ └── ClientOnlyInventoryControls.java │ │ ├── network │ │ ├── ModNetworking.java │ │ ├── HelloMessage.java │ │ ├── SingleTransferMessage.java │ │ ├── BulkTransferAllMessage.java │ │ └── BulkTransferSingleMessage.java │ │ ├── InventoryEssentials.java │ │ ├── ServerInventoryTransfers.java │ │ ├── InventoryEssentialsConfig.java │ │ ├── InventoryUtils.java │ │ └── InventoryEssentialsIgnores.java ├── dependencies.gradle └── build.gradle ├── forge ├── dependencies.gradle ├── src │ └── main │ │ ├── resources │ │ ├── inventoryessentials.forge.mixins.json │ │ └── META-INF │ │ │ └── mods.toml │ │ └── java │ │ └── net │ │ └── blay09 │ │ └── mods │ │ └── inventoryessentials │ │ └── ForgeInventoryEssentials.java └── build.gradle ├── fabric ├── dependencies.gradle ├── src │ └── main │ │ ├── resources │ │ ├── inventoryessentials.fabric.mixins.json │ │ └── fabric.mod.json │ │ └── java │ │ └── net │ │ └── blay09 │ │ └── mods │ │ └── inventoryessentials │ │ └── fabric │ │ ├── client │ │ └── FabricInventoryEssentialsClient.java │ │ └── FabricInventoryEssentials.java └── build.gradle ├── neoforge ├── dependencies.gradle ├── src │ └── main │ │ ├── resources │ │ ├── inventoryessentials.neoforge.mixins.json │ │ └── META-INF │ │ │ └── neoforge.mods.toml │ │ └── java │ │ └── net │ │ └── blay09 │ │ └── mods │ │ └── inventoryessentials │ │ ├── client │ │ └── NeoForgeInventoryEssentialsClient.java │ │ └── NeoForgeInventoryEssentials.java └── build.gradle ├── .gitattributes ├── .gitignore ├── LICENSE ├── .github ├── workflows │ ├── manage-labels.yaml │ ├── label-issues.yaml │ ├── publish-snapshot.yml │ └── publish-release.yml └── advanced-issue-labeler.yml ├── README.md ├── settings.gradle ├── gradle.properties ├── modpage.md ├── gradlew.bat ├── repositories.gradle └── gradlew /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | - Updated to Minecraft 1.21.11 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwelveIterationMods/InventoryEssentials/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /common/src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "description": "${mod_name}", 4 | "pack_format": ${pack_format_number} 5 | } 6 | } -------------------------------------------------------------------------------- /common/dependencies.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(libs.balmCommon) { 3 | changing = libs.versions.balm.get().endsWith("SNAPSHOT") 4 | } 5 | } -------------------------------------------------------------------------------- /common/src/main/resources/inventoryessentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwelveIterationMods/InventoryEssentials/HEAD/common/src/main/resources/inventoryessentials.png -------------------------------------------------------------------------------- /forge/dependencies.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(libs.balmForge) { 3 | changing = libs.versions.balm.get().endsWith("SNAPSHOT") 4 | } 5 | } -------------------------------------------------------------------------------- /fabric/dependencies.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | modImplementation(libs.balmFabric) { 3 | changing = libs.versions.balm.get().endsWith("SNAPSHOT") 4 | } 5 | } -------------------------------------------------------------------------------- /neoforge/dependencies.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation(libs.balmNeoForge) { 3 | changing = libs.versions.balm.get().endsWith("SNAPSHOT") 4 | } 5 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.bat text eol=crlf 3 | *.patch text eol=lf 4 | *.java text eol=lf 5 | *.gradle text eol=crlf 6 | *.png binary 7 | *.gif binary 8 | *.exe binary 9 | *.dll binary 10 | *.jar binary 11 | *.lzma binary 12 | *.zip binary 13 | *.pyd binary 14 | *.cfg text eol=lf 15 | *.jks binary 16 | *.ogg binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .eclipse 5 | .settings 6 | .metadata 7 | .classpath 8 | .project 9 | 10 | # idea 11 | out 12 | *.ipr 13 | *.iws 14 | *.iml 15 | .idea 16 | 17 | # gradle 18 | build 19 | .gradle 20 | 21 | # other 22 | eclipse 23 | run 24 | runs 25 | runserver 26 | logs 27 | 28 | common/src/generated/resources/.cache -------------------------------------------------------------------------------- /forge/src/main/resources/inventoryessentials.forge.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.blay09.mods.inventoryessentials.forge.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | ], 8 | "client": [ 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fabric/src/main/resources/inventoryessentials.fabric.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.blay09.mods.inventoryessentials.fabric.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | ], 8 | "client": [ 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /neoforge/src/main/resources/inventoryessentials.neoforge.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.blay09.mods.inventoryessentials.neoforge.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | ], 8 | "client": [ 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/PlatformBindings.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.minecraft.world.inventory.Slot; 4 | 5 | public abstract class PlatformBindings { 6 | 7 | public static PlatformBindings INSTANCE; 8 | 9 | public abstract boolean isSameInventory(Slot targetSlot, Slot slot); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/data/IgnoredData.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.data; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | public class IgnoredData { 7 | public Set ignoredScreenClasses = new HashSet<>(); 8 | public Set ignoredMenuClasses = new HashSet<>(); 9 | public Set ignoredSlotClasses = new HashSet<>(); 10 | public Set ignoredMenuTypes = new HashSet<>(); 11 | } 12 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/mixin/SlotWrapperAccessor.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.mixin; 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(targets = "net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen$SlotWrapper") 8 | public interface SlotWrapperAccessor { 9 | @Accessor 10 | Slot getTarget(); 11 | } 12 | -------------------------------------------------------------------------------- /common/src/main/resources/inventoryessentials.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "net.blay09.mods.inventoryessentials.mixin", 5 | "refmap": "${mod_id}.refmap.json", 6 | "compatibilityLevel": "JAVA_17", 7 | "mixins": [ 8 | ], 9 | "client": [ 10 | "AbstractContainerScreenAccessor", 11 | "AbstractContainerMenuAccessor", 12 | "CreativeModeInventoryScreenAccessor", 13 | "SlotWrapperAccessor" 14 | ], 15 | "injectors": { 16 | "defaultRequire": 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/mixin/AbstractContainerMenuAccessor.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.mixin; 2 | 3 | import net.minecraft.world.inventory.AbstractContainerMenu; 4 | import net.minecraft.world.inventory.MenuType; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | @Mixin(AbstractContainerMenu.class) 9 | public interface AbstractContainerMenuAccessor { 10 | @Accessor("menuType") 11 | MenuType balm$getMenuType(); 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/mixin/CreativeModeInventoryScreenAccessor.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.mixin; 2 | 3 | import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; 4 | import net.minecraft.world.SimpleContainer; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | @Mixin(CreativeModeInventoryScreen.class) 9 | public interface CreativeModeInventoryScreenAccessor { 10 | @Accessor 11 | SimpleContainer getCONTAINER(); 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All Rights Reserved 2 | 3 | Copyright (c) 2023 BlayTheNinth 4 | 5 | For modpack permissions and other exceptions, see https://mods.twelveiterations.com/permissions 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 8 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 9 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 10 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /fabric/src/main/java/net/blay09/mods/inventoryessentials/fabric/client/FabricInventoryEssentialsClient.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.fabric.client; 2 | 3 | import net.blay09.mods.balm.client.BalmClient; 4 | import net.blay09.mods.balm.fabric.platform.runtime.FabricLoadContext; 5 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 6 | import net.blay09.mods.inventoryessentials.client.InventoryEssentialsClient; 7 | import net.fabricmc.api.ClientModInitializer; 8 | 9 | public class FabricInventoryEssentialsClient implements ClientModInitializer { 10 | 11 | @Override 12 | public void onInitializeClient() { 13 | BalmClient.initializeMod(InventoryEssentials.MOD_ID, FabricLoadContext.INSTANCE, InventoryEssentialsClient::initialize); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /forge/src/main/resources/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="javafml" 2 | loaderVersion="*" 3 | license="${license}" 4 | issueTrackerURL="${issues}" 5 | [[mods]] 6 | modId="${mod_id}" 7 | version="${version}" 8 | displayName="${mod_name}" 9 | displayURL="${homepage}" 10 | logoFile="${mod_id}.png" 11 | credits="BlayTheNinth" 12 | authors="BlayTheNinth" 13 | description='''${description}''' 14 | [[dependencies.${mod_id}]] 15 | modId="forge" 16 | mandatory=true 17 | versionRange="[${forge_version},)" 18 | ordering="NONE" 19 | side="BOTH" 20 | [[dependencies.${mod_id}]] 21 | modId="minecraft" 22 | mandatory=true 23 | versionRange="[${minecraft_version},)" 24 | ordering="NONE" 25 | side="BOTH" 26 | [[dependencies.${mod_id}]] 27 | modId="balm" 28 | mandatory=true 29 | versionRange="[${balm_version},)" 30 | ordering="NONE" 31 | side="BOTH" 32 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/mixin/AbstractContainerScreenAccessor.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.mixin; 2 | 3 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 4 | import net.minecraft.world.inventory.Slot; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | import org.spongepowered.asm.mixin.gen.Invoker; 8 | 9 | @Mixin(AbstractContainerScreen.class) 10 | public interface AbstractContainerScreenAccessor { 11 | 12 | @Accessor 13 | int getLeftPos(); 14 | 15 | @Accessor 16 | int getTopPos(); 17 | 18 | @Accessor 19 | Slot getHoveredSlot(); 20 | 21 | @Invoker 22 | boolean callHasClickedOutside(double x, double y, int left, int top); 23 | 24 | @Accessor 25 | void setIsQuickCrafting(boolean isQuickCrafting); 26 | } 27 | -------------------------------------------------------------------------------- /neoforge/src/main/java/net/blay09/mods/inventoryessentials/client/NeoForgeInventoryEssentialsClient.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.blay09.mods.balm.client.BalmClient; 4 | import net.blay09.mods.balm.neoforge.platform.runtime.NeoForgeLoadContext; 5 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 6 | import net.neoforged.api.distmarker.Dist; 7 | import net.neoforged.bus.api.IEventBus; 8 | import net.neoforged.fml.common.Mod; 9 | 10 | @Mod(value = InventoryEssentials.MOD_ID, dist = Dist.CLIENT) 11 | public class NeoForgeInventoryEssentialsClient { 12 | 13 | public NeoForgeInventoryEssentialsClient(IEventBus modEventBus) { 14 | final var context = new NeoForgeLoadContext(modEventBus); 15 | BalmClient.initializeMod(InventoryEssentials.MOD_ID, context, InventoryEssentialsClient::initialize); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/inventoryessentials/lang/zh_tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "key.categories.inventoryessentials": "Inventory Essentials 物品欄助手", 3 | "key.inventoryessentials.single_transfer": "轉移單個物品", 4 | "key.inventoryessentials.bulk_transfer": "轉移所有同類物品", 5 | "key.inventoryessentials.bulk_transfer_all": "轉移所有物品", 6 | "key.inventoryessentials.bulk_drop": "批量丟棄", 7 | "key.inventoryessentials.screen_bulk_drop": "批量丟棄至畫面外", 8 | "key.inventoryessentials.drag_transfer": "懸停轉移(長按)", 9 | 10 | "inventoryessentials.configuration.title": "Inventory Essentials 物品欄助手", 11 | "inventoryessentials.configuration.forceClientImplementation": "強制使用用戶端實作(僅限開發)", 12 | "inventoryessentials.configuration.forceClientImplementation.tooltip": "即使在已安裝此模組的伺服器上也使用用戶端實作 - 僅用於開發目的。", 13 | "inventoryessentials.configuration.allowBulkTransferAllOnEmptySlot": "允許在空欄位上使用轉移所有物品", 14 | "inventoryessentials.configuration.allowBulkTransferAllOnEmptySlot.tooltip": "是否允許在點選空欄位時,使用「轉移所有物品」移動所有物品?" 15 | } -------------------------------------------------------------------------------- /neoforge/src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="javafml" 2 | loaderVersion="[1,)" 3 | license="${license}" 4 | issueTrackerURL="${issues}" 5 | [[mods]] 6 | modId="${mod_id}" 7 | version="${version}" 8 | displayName="${mod_name}" 9 | displayURL="${homepage}" 10 | displayTest="NONE" 11 | logoFile="${mod_id}.png" 12 | credits="BlayTheNinth" 13 | authors="BlayTheNinth" 14 | description='''${description}''' 15 | [[mixins]] 16 | config = "${mod_id}.mixins.json" 17 | [[mixins]] 18 | config = "${mod_id}.neoforge.mixins.json" 19 | [[dependencies.${mod_id}]] 20 | modId="neoforge" 21 | type="required" 22 | versionRange="[${neoforge_version},)" 23 | ordering="NONE" 24 | side="BOTH" 25 | [[dependencies.${mod_id}]] 26 | modId="minecraft" 27 | type="required" 28 | versionRange="[${minecraft_version},)" 29 | ordering="NONE" 30 | side="BOTH" 31 | [[dependencies.${mod_id}]] 32 | modId="balm" 33 | type="required" 34 | versionRange="[${balm_version},)" 35 | ordering="NONE" 36 | side="BOTH" 37 | -------------------------------------------------------------------------------- /.github/workflows/manage-labels.yaml: -------------------------------------------------------------------------------- 1 | name: manage-labels 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | dry: 6 | description: 'Dry run (no changes, log only)' 7 | required: false 8 | default: true 9 | type: boolean 10 | remove_missing: 11 | description: 'Remove labels not present in the source data' 12 | required: false 13 | default: false 14 | type: boolean 15 | jobs: 16 | manage-labels: 17 | permissions: 18 | contents: read 19 | issues: write 20 | runs-on: ubuntu-latest 21 | name: manage-labels 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: TwelveIterations/manage-labels@main 25 | with: 26 | dry: ${{ inputs.dry }} 27 | remove_missing: ${{ inputs.remove_missing }} 28 | source: https://raw.githubusercontent.com/TwelveIterationMods/.github/refs/heads/main/labels.yaml 29 | env: 30 | GITHUB_TOKEN: ${{ github.token }} 31 | -------------------------------------------------------------------------------- /.github/advanced-issue-labeler.yml: -------------------------------------------------------------------------------- 1 | policy: 2 | - template: [report-a-bug.yml] 3 | section: 4 | - id: [minecraftVersion] 5 | block-list: [other] 6 | label: 7 | - name: Minecraft 1.21.11 8 | keys: ['1.21.11'] 9 | - name: Minecraft 1.21.10 10 | keys: ['1.21.10'] 11 | - name: Minecraft 1.21.8 12 | keys: ['1.21.8'] 13 | - name: Minecraft 1.21.5 14 | keys: ['1.21.5'] 15 | - name: Minecraft 1.21.4 16 | keys: ['1.21.4'] 17 | - name: Minecraft 1.21.1 18 | keys: ['1.21.1 (LTS)'] 19 | - name: Minecraft 1.20.1 20 | keys: ['1.20.1 (LTS)'] 21 | - name: EOL 22 | keys: ['Other (specify below)'] 23 | - id: [modLoader] 24 | label: 25 | - name: NeoForge 26 | keys: ['NeoForge'] 27 | - name: Fabric 28 | keys: ['Fabric'] 29 | - name: 'Forge' 30 | keys: ['Forge'] 31 | -------------------------------------------------------------------------------- /fabric/src/main/java/net/blay09/mods/inventoryessentials/fabric/FabricInventoryEssentials.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.fabric; 2 | 3 | import net.blay09.mods.balm.Balm; 4 | import net.blay09.mods.balm.fabric.platform.runtime.FabricLoadContext; 5 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 6 | import net.blay09.mods.inventoryessentials.PlatformBindings; 7 | import net.fabricmc.api.ModInitializer; 8 | import net.minecraft.world.inventory.Slot; 9 | 10 | public class FabricInventoryEssentials implements ModInitializer { 11 | @Override 12 | public void onInitialize() { 13 | PlatformBindings.INSTANCE = new PlatformBindings() { 14 | @Override 15 | public boolean isSameInventory(Slot targetSlot, Slot slot) { 16 | return slot.container == targetSlot.container; 17 | } 18 | }; 19 | 20 | Balm.initializeMod(InventoryEssentials.MOD_ID, FabricLoadContext.INSTANCE, InventoryEssentials::initialize); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/label-issues.yaml: -------------------------------------------------------------------------------- 1 | name: Label Issues 2 | on: 3 | issues: 4 | types: [ opened ] 5 | jobs: 6 | label-component: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: read 10 | issues: write 11 | strategy: 12 | matrix: 13 | template: [ report-a-bug.yml ] 14 | steps: 15 | - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 16 | 17 | - name: Parse issue form 18 | uses: TwelveIterations/github-issue-parser@main 19 | id: issue-parser 20 | with: 21 | template-path: https://raw.githubusercontent.com/TwelveIterationMods/.github/refs/heads/main/.github/ISSUE_TEMPLATE/${{ matrix.template }} 22 | 23 | - name: Set labels based on component field 24 | uses: redhat-plumbers-in-action/advanced-issue-labeler@d498805e5c7c0658e336948b3363480bcfd68da6 25 | with: 26 | issue-form: ${{ steps.issue-parser.outputs.jsonString }} 27 | template: ${{ matrix.template }} 28 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inventory Essentials 2 | 3 | Minecraft Mod. Basic inventory tweaks designed to supplement inventory sorting mods. 4 | 5 | - [Modpack Permissions](https://mods.twelveiterations.com/permissions) 6 | 7 | #### Downloads 8 | 9 | [![Versions](http://cf.way2muchnoise.eu/versions/368825_latest.svg)](https://www.curseforge.com/minecraft/mc-mods/inventory-essentials) 10 | [![Downloads](http://cf.way2muchnoise.eu/full_368825_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/inventory-essentials) 11 | 12 | ## Contributing 13 | 14 | If you're interested in contributing to the mod, you can check 15 | out [issues labelled as "help wanted"](https://github.com/TwelveIterationMods/InventoryEssentials/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). 16 | 17 | When it comes to new features, it's best to confer with me first to ensure we share the same vision. You can join us on [Discord](https://discord.gg/VAfZ2Nau6j) if you'd like to talk. 18 | 19 | Contributions must be done through pull requests. I will not be able to accept translations, code or other assets through any other channels. 20 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/InventoryControls.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 4 | import net.minecraft.world.inventory.Slot; 5 | import net.minecraft.world.item.ItemStack; 6 | 7 | public interface InventoryControls { 8 | boolean singleTransfer(AbstractContainerScreen screen, Slot clickedSlot); 9 | 10 | boolean bulkTransferByType(AbstractContainerScreen screen, Slot clickedSlot); 11 | 12 | boolean bulkTransferSingle(AbstractContainerScreen screen, Slot clickedSlot); 13 | 14 | boolean bulkTransferAll(AbstractContainerScreen screen, Slot clickedSlot); 15 | 16 | void dragTransfer(AbstractContainerScreen screen, Slot clickedSlot); 17 | 18 | void dragClick(AbstractContainerScreen screen, Slot hoveredSlot, int mouseButton); 19 | 20 | boolean dropByType(AbstractContainerScreen screen, Slot hoverSlot); 21 | 22 | boolean dropByType(AbstractContainerScreen screen, ItemStack itemStack); 23 | 24 | boolean sort(AbstractContainerScreen screen, Slot hoverSlot); 25 | } 26 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/network/ModNetworking.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.network; 2 | 3 | import net.blay09.mods.balm.network.BalmNetworking; 4 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 5 | 6 | public class ModNetworking { 7 | 8 | public static void initialize(BalmNetworking networking) { 9 | networking.defineNetworkVersion(InventoryEssentials.MOD_ID, "1"); 10 | networking.allowClientAndServerOnly(InventoryEssentials.MOD_ID); 11 | 12 | networking.registerClientboundPacket(HelloMessage.TYPE, HelloMessage.class, HelloMessage.STREAM_CODEC, HelloMessage::handle); 13 | 14 | networking.registerServerboundPacket(SingleTransferMessage.TYPE, SingleTransferMessage.class, SingleTransferMessage.STREAM_CODEC, SingleTransferMessage::handle); 15 | networking.registerServerboundPacket(BulkTransferAllMessage.TYPE, BulkTransferAllMessage.class, BulkTransferAllMessage.STREAM_CODEC, BulkTransferAllMessage::handle); 16 | networking.registerServerboundPacket(BulkTransferSingleMessage.TYPE, BulkTransferSingleMessage.class, BulkTransferSingleMessage.STREAM_CODEC, BulkTransferSingleMessage::handle); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/network/HelloMessage.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.network; 2 | 3 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 4 | import net.minecraft.network.RegistryFriendlyByteBuf; 5 | import net.minecraft.network.codec.StreamCodec; 6 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 7 | import net.minecraft.resources.Identifier; 8 | import net.minecraft.world.entity.player.Player; 9 | 10 | public class HelloMessage implements CustomPacketPayload { 11 | 12 | public static final HelloMessage INSTANCE = new HelloMessage(); 13 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, 14 | "hello")); 15 | public static final StreamCodec STREAM_CODEC = StreamCodec.unit(INSTANCE); 16 | 17 | private HelloMessage() { 18 | } 19 | 20 | public static void handle(Player player, HelloMessage message) { 21 | InventoryEssentials.isServerSideInstalled = true; 22 | } 23 | 24 | @Override 25 | public Type type() { 26 | return TYPE; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/InventoryEssentials.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.blay09.mods.balm.Balm; 4 | import net.blay09.mods.balm.core.BalmRegistrars; 5 | import net.blay09.mods.balm.platform.event.callback.ServerPlayerCallback; 6 | import net.blay09.mods.inventoryessentials.data.ConfigJsonCompatLoader; 7 | import net.blay09.mods.inventoryessentials.data.ModFileJsonCompatLoader; 8 | import net.blay09.mods.inventoryessentials.network.HelloMessage; 9 | import net.blay09.mods.inventoryessentials.network.ModNetworking; 10 | 11 | public class InventoryEssentials { 12 | 13 | public static final String MOD_ID = "inventoryessentials"; 14 | public static boolean isServerSideInstalled; 15 | 16 | public static void initialize(BalmRegistrars registrars) { 17 | InventoryEssentialsConfig.initialize(); 18 | ModNetworking.initialize(Balm.networking()); 19 | 20 | ServerPlayerCallback.Join.EVENT.register(player -> Balm.networking().sendTo(player, HelloMessage.INSTANCE)); 21 | 22 | Balm.config().onConfigAvailable(InventoryEssentialsConfig.class, config -> { 23 | ModFileJsonCompatLoader.load(); 24 | ConfigJsonCompatLoader.load(); 25 | }); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /fabric/src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "${mod_id}", 4 | "version": "${version}", 5 | 6 | "name": "${mod_name}", 7 | "description": "${description}", 8 | "authors": [ 9 | "BlayTheNinth" 10 | ], 11 | "contact": { 12 | "homepage": "${homepage}", 13 | "sources": "${sources}", 14 | "issues": "${issues}" 15 | }, 16 | 17 | "license": "${license}", 18 | "icon": "${mod_id}.png", 19 | 20 | "environment": "*", 21 | "entrypoints": { 22 | "main": [ 23 | "net.blay09.mods.inventoryessentials.fabric.FabricInventoryEssentials" 24 | ], 25 | "client": [ 26 | "net.blay09.mods.inventoryessentials.fabric.client.FabricInventoryEssentialsClient" 27 | ] 28 | }, 29 | "mixins": [ 30 | "inventoryessentials.mixins.json", 31 | "inventoryessentials.fabric.mixins.json" 32 | ], 33 | 34 | "depends": { 35 | "fabricloader": ">=${fabric_loader_version}", 36 | "fabric-api": "*", 37 | "balm-fabric": ">=${balm_version}", 38 | "minecraft": ">=${minecraft_version}", 39 | "java": ">=${java_version}" 40 | }, 41 | "suggests": { 42 | }, 43 | "custom": { 44 | "modmenu": { 45 | "links": { 46 | "modmenu.discord": "https://discord.gg/VAfZ2Nau6j" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /neoforge/src/main/java/net/blay09/mods/inventoryessentials/NeoForgeInventoryEssentials.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.blay09.mods.balm.Balm; 4 | import net.blay09.mods.balm.neoforge.platform.runtime.NeoForgeLoadContext; 5 | import net.minecraft.world.inventory.Slot; 6 | import net.neoforged.bus.api.IEventBus; 7 | import net.neoforged.fml.common.Mod; 8 | import net.neoforged.neoforge.items.SlotItemHandler; 9 | 10 | @Mod(InventoryEssentials.MOD_ID) 11 | public class NeoForgeInventoryEssentials { 12 | 13 | public NeoForgeInventoryEssentials(IEventBus modEventBus) { 14 | PlatformBindings.INSTANCE = new PlatformBindings() { 15 | @Override 16 | public boolean isSameInventory(Slot targetSlot, Slot slot) { 17 | if (targetSlot instanceof SlotItemHandler && slot instanceof SlotItemHandler) { 18 | return ((SlotItemHandler) targetSlot).getItemHandler() == ((SlotItemHandler) slot).getItemHandler(); 19 | } 20 | 21 | return slot.isSameInventory(targetSlot); 22 | } 23 | }; 24 | 25 | final var context = new NeoForgeLoadContext(modEventBus); 26 | Balm.initializeMod(InventoryEssentials.MOD_ID, context, InventoryEssentials::initialize); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/data/ModFileJsonCompatLoader.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.data; 2 | 3 | import com.google.gson.Gson; 4 | import net.blay09.mods.balm.Balm; 5 | import net.blay09.mods.inventoryessentials.InventoryEssentialsIgnores; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.IOException; 10 | 11 | public class ModFileJsonCompatLoader { 12 | 13 | private static final Logger logger = LoggerFactory.getLogger(ModFileJsonCompatLoader.class); 14 | private static final Gson gson = new Gson(); 15 | 16 | public static void load() { 17 | Balm.platform().loadedPrimaryModIds().forEach(modId -> Balm.platform().visitModResources(modId, "inventoryessentials/ignores", (resource) -> { 18 | if (resource.extension().equals("json")) { 19 | try (final var reader = resource.bufferedReader()) { 20 | final var ignoredData = gson.fromJson(reader, IgnoredData.class); 21 | if (ignoredData != null) { 22 | InventoryEssentialsIgnores.addIgnoredData(ignoredData); 23 | } 24 | } catch (IOException e) { 25 | logger.error("Failed to load InventoryEssentials file {}", resource.name(), e); 26 | } 27 | } 28 | })); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/ServerInventoryTransfers.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.minecraft.server.level.ServerPlayer; 4 | import net.minecraft.world.inventory.AbstractContainerMenu; 5 | import net.minecraft.world.inventory.ClickType; 6 | import net.minecraft.world.inventory.Slot; 7 | 8 | public class ServerInventoryTransfers { 9 | public static void singleTransfer(ServerPlayer player, AbstractContainerMenu menu, Slot slot) { 10 | if (!slot.mayPickup(player)) { 11 | return; 12 | } 13 | 14 | final var sourceStack = slot.getItem(); 15 | if (sourceStack.getCount() == 1) { 16 | menu.clicked(slot.index, 0, ClickType.QUICK_MOVE, player); 17 | } else if (!sourceStack.isEmpty()) { 18 | final var restStack = sourceStack.copy(); 19 | sourceStack.setCount(1); 20 | 21 | // We specifically set the slot stack as some mods return transient copies in getItem that will not be reflected back to the inventory 22 | slot.set(sourceStack); 23 | 24 | restStack.shrink(1); 25 | menu.clicked(slot.index, 0, ClickType.QUICK_MOVE, player); 26 | if (!slot.hasItem()) { 27 | slot.set(restStack); 28 | } else { 29 | if (!player.addItem(restStack)) { 30 | player.drop(restStack, false); 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | exclusiveContent { 6 | forRepository { 7 | maven { 8 | name = 'Fabric' 9 | url = uri("https://maven.fabricmc.net") 10 | } 11 | } 12 | filter { 13 | includeGroupAndSubgroups("net.fabricmc") 14 | includeGroup("fabric-loom") 15 | } 16 | } 17 | exclusiveContent { 18 | forRepository { 19 | maven { 20 | name = 'Sponge' 21 | url = uri('https://repo.spongepowered.org/repository/maven-public') 22 | } 23 | } 24 | filter { 25 | includeGroupAndSubgroups("org.spongepowered") 26 | } 27 | } 28 | exclusiveContent { 29 | forRepository { 30 | maven { 31 | name = 'Forge' 32 | url = uri("https://maven.minecraftforge.net") 33 | } 34 | } 35 | filter { 36 | includeGroupAndSubgroups("net.minecraftforge") 37 | } 38 | } 39 | } 40 | } 41 | 42 | plugins { 43 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' 44 | } 45 | 46 | includeBuild('build-logic') 47 | include('common', 'fabric', 'neoforge', 'forge') 48 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/data/ConfigJsonCompatLoader.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.data; 2 | 3 | import com.google.gson.Gson; 4 | import net.blay09.mods.balm.Balm; 5 | import net.blay09.mods.inventoryessentials.InventoryEssentialsIgnores; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | 13 | public class ConfigJsonCompatLoader { 14 | 15 | private static final Logger logger = LoggerFactory.getLogger(ConfigJsonCompatLoader.class); 16 | private static final Gson gson = new Gson(); 17 | 18 | public static void load() { 19 | final var configDir = new File(Balm.config().getConfigDir(), "inventoryessentials/ignores"); 20 | if (!configDir.exists() && !configDir.mkdirs()) { 21 | logger.error("Failed to create InventoryEssentials config directory {}", configDir); 22 | return; 23 | } 24 | 25 | final var files = configDir.listFiles(it -> it.getName().endsWith(".json")); 26 | if (files == null) { 27 | return; 28 | } 29 | 30 | for (final var file : files) { 31 | try (final var reader = Files.newBufferedReader(file.toPath())) { 32 | final var ignoredData = gson.fromJson(reader, IgnoredData.class); 33 | if (ignoredData != null) { 34 | InventoryEssentialsIgnores.addIgnoredData(ignoredData); 35 | } 36 | } catch (IOException e) { 37 | logger.error("Failed to load InventoryEssentials file {}", file, e); 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | balm = "21.11.2" 3 | minecraft = "1.21.11" 4 | neoForm = "1.21.11-20251209.172050" 5 | neoForge = "21.11.0-beta" 6 | forge = "61.0.3" 7 | fabricApi = "0.139.4+1.21.11" 8 | fabricLoader = "0.18.1" 9 | 10 | [libraries] 11 | minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } 12 | parchment = { module = "org.parchmentmc.data:parchment-1.21.10", version = "2025.10.12" } 13 | 14 | balmCommon = { module = "net.blay09.mods:balm-common", version.ref = "balm" } 15 | balmNeoForge = { module = "net.blay09.mods:balm-neoforge", version.ref = "balm" } 16 | balmForge = { module = "net.blay09.mods:balm-forge", version.ref = "balm" } 17 | balmFabric = { module = "net.blay09.mods:balm-fabric", version.ref = "balm" } 18 | 19 | neoForm = { module = "net.neoforged:neoform", version.ref = "neoForm" } 20 | neoForge = { module = "net.neoforged:neoforge", version.ref = "neoForge" } 21 | forge = { module = "net.minecraftforge:forge", version.ref = "forge" } 22 | fabricApi = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabricApi" } 23 | fabricLoader = { module = "net.fabricmc:fabric-loader", version.ref = "fabricLoader" } 24 | 25 | mixin = { module = "org.spongepowered:mixin", version = "0.8.7" } 26 | 27 | [plugins] 28 | fabricLoom = { id = "fabric-loom", version = "1.13-SNAPSHOT" } 29 | modDevGradle = { id = "net.neoforged.moddev", version = "2.0.107" } 30 | forgeGradle = { id = "net.minecraftforge.gradle", version = "[6.0.25,6.2)" } 31 | mixin = { id = "org.spongepowered.mixin", version = "0.7-SNAPSHOT" } 32 | curseForgeGradle = { id = "net.darkhax.curseforgegradle", version = "1.1.26" } 33 | modrinthMinotaur = { id = "com.modrinth.minotaur", version = "2.+" } 34 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/CreativeInventoryControls.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.blay09.mods.inventoryessentials.mixin.SlotWrapperAccessor; 4 | import net.minecraft.client.Minecraft; 5 | import net.minecraft.client.player.LocalPlayer; 6 | import net.minecraft.world.entity.player.Inventory; 7 | import net.minecraft.world.inventory.AbstractContainerMenu; 8 | import net.minecraft.world.inventory.ClickType; 9 | import net.minecraft.world.inventory.InventoryMenu; 10 | import net.minecraft.world.inventory.Slot; 11 | 12 | public class CreativeInventoryControls extends ClientOnlyInventoryControls { 13 | @Override 14 | protected boolean isValidTargetSlot(Slot slot) { 15 | return slot.container instanceof Inventory; 16 | } 17 | 18 | @Override 19 | protected void slotClick(AbstractContainerMenu menu, Slot slot, int mouseButton, ClickType clickType) { 20 | if (slot instanceof SlotWrapperAccessor accessor) { 21 | final var player = Minecraft.getInstance().player; 22 | if (player != null) { 23 | slotClick(player.inventoryMenu, accessor.getTarget().index, mouseButton, clickType); 24 | } 25 | } else { 26 | slotClick(menu, slot.index, mouseButton, clickType); 27 | } 28 | } 29 | 30 | @Override 31 | protected void slotClick(AbstractContainerMenu menu, int slotIndex, int mouseButton, ClickType clickType) { 32 | final var player = Minecraft.getInstance().player; 33 | if (player != null) { 34 | menu.clicked(slotIndex, mouseButton, clickType, player); 35 | player.inventoryMenu.broadcastChanges(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/network/SingleTransferMessage.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.network; 2 | 3 | import net.blay09.mods.inventoryessentials.ServerInventoryTransfers; 4 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 5 | import net.minecraft.network.RegistryFriendlyByteBuf; 6 | import net.minecraft.network.codec.ByteBufCodecs; 7 | import net.minecraft.network.codec.StreamCodec; 8 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 9 | import net.minecraft.resources.Identifier; 10 | import net.minecraft.server.level.ServerPlayer; 11 | 12 | public record SingleTransferMessage(int slotNumber) implements CustomPacketPayload { 13 | 14 | public static CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath( 15 | InventoryEssentials.MOD_ID, 16 | "single_transfer")); 17 | 18 | public static final StreamCodec STREAM_CODEC = StreamCodec.composite( 19 | ByteBufCodecs.INT, 20 | SingleTransferMessage::slotNumber, 21 | SingleTransferMessage::new 22 | ); 23 | 24 | public static void handle(ServerPlayer player, SingleTransferMessage message) { 25 | final var menu = player.containerMenu; 26 | if (menu != null && message.slotNumber >= 0 && message.slotNumber < menu.slots.size()) { 27 | final var slot = menu.slots.get(message.slotNumber); 28 | ServerInventoryTransfers.singleTransfer(player, menu, slot); 29 | } 30 | } 31 | 32 | @Override 33 | public Type type() { 34 | return TYPE; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Mod 2 | mod_id = inventoryessentials 3 | mod_name=Inventory Essentials 4 | mod_main=InventoryEssentials 5 | description=Basic inventory tweaks. Lightweight and compatible. 6 | version = 21.11.2 7 | group = net.blay09.mods 8 | homepage=https://mods.twelveiterations.com/mc/inventory-essentials 9 | sources=https://github.com/TwelveIterationMods/InventoryEssentials 10 | issues=https://github.com/TwelveIterationMods/InventoryEssentials/issues 11 | discord = https://discord.gg/VAfZ2Nau6j 12 | license=All Rights Reserved 13 | 14 | # Publishing 15 | curseforge_project_id = 368825 16 | modrinth_project_id = Boon8xwi 17 | maven_releases = https://maven.twelveiterations.com/repository/maven-releases/ 18 | maven_snapshots = https://maven.twelveiterations.com/repository/maven-snapshots/ 19 | 20 | # Minecraft 21 | minecraft_version = 1.21.11-pre1 22 | minecraft_versions = 1.21.11 23 | minecraft_version_range = [1.21.11-beta.1,) 24 | pack_format_number = 64 25 | java_version = 21 26 | 27 | # Balm 28 | balm_version = 21.11.1-SNAPSHOT 29 | balm_version_range = [21.11.0,) 30 | 31 | # Forge 32 | forge_version = 60.0.0 33 | forge_version_range = [60,) 34 | forge_loader_version_range = [60,) 35 | 36 | # NeoForge 37 | neoforge_snapshot_url = 38 | neoforge_version = 21.10.38-beta 39 | neoforge_version_range = [21,) 40 | neoforge_loader_version_range = [1,) 41 | 42 | # Fabric 43 | fabric_version = 0.139.1+1.21.11 44 | fabric_loader_version = 0.17.3 45 | 46 | # Dependencies 47 | mixin_version=0.8.5 48 | modmenu_version=9.0.0 49 | 50 | # Gradle 51 | org.gradle.jvmargs=-Xmx3G 52 | org.gradle.daemon=false 53 | mod_author = BlayTheNinth 54 | credits = BlayTheNinth 55 | kuma_version = 21.11.2 56 | neo_form_version = 1.21.11-pre1-20251119.112005 57 | parchment_minecraft = 1.21.10 58 | parchment_version = 2025.10.12 59 | kuma_version_range = [21.11,21.12) -------------------------------------------------------------------------------- /forge/src/main/java/net/blay09/mods/inventoryessentials/ForgeInventoryEssentials.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.blay09.mods.balm.Balm; 4 | import net.blay09.mods.balm.client.BalmClient; 5 | import net.blay09.mods.balm.forge.platform.runtime.ForgeLoadContext; 6 | import net.blay09.mods.inventoryessentials.client.InventoryEssentialsClient; 7 | import net.minecraft.world.inventory.Slot; 8 | import net.minecraftforge.fml.IExtensionPoint; 9 | import net.minecraftforge.fml.common.Mod; 10 | import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; 11 | import net.minecraftforge.fml.loading.FMLEnvironment; 12 | import net.minecraftforge.items.SlotItemHandler; 13 | 14 | @Mod(InventoryEssentials.MOD_ID) 15 | public class ForgeInventoryEssentials { 16 | 17 | public ForgeInventoryEssentials(FMLJavaModLoadingContext context) { 18 | final var loadContext = new ForgeLoadContext(context.getModBusGroup()); 19 | PlatformBindings.INSTANCE = new PlatformBindings() { 20 | @Override 21 | public boolean isSameInventory(Slot targetSlot, Slot slot) { 22 | if (targetSlot instanceof SlotItemHandler && slot instanceof SlotItemHandler) { 23 | return ((SlotItemHandler) targetSlot).getItemHandler() == ((SlotItemHandler) slot).getItemHandler(); 24 | } 25 | 26 | return slot.isSameInventory(targetSlot); 27 | } 28 | }; 29 | 30 | Balm.initializeMod(InventoryEssentials.MOD_ID, loadContext, InventoryEssentials::initialize); 31 | if (FMLEnvironment.dist.isClient()) { 32 | BalmClient.initializeMod(InventoryEssentials.MOD_ID, loadContext, InventoryEssentialsClient::initialize); 33 | } 34 | 35 | context.registerDisplayTest(IExtensionPoint.DisplayTest.IGNORE_ALL_VERSION); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'multiloader-common' 3 | alias libs.plugins.modDevGradle 4 | } 5 | 6 | neoForge { 7 | neoFormVersion = libs.neoForm.get().version 8 | // Automatically enable AccessTransformers if the file exists 9 | def at = file('src/main/resources/META-INF/accesstransformer.cfg') 10 | if (at.exists()) { 11 | accessTransformers.from(at.absolutePath) 12 | } 13 | parchment.parchmentArtifact = libs.parchment.get().toString() 14 | } 15 | 16 | dependencies { 17 | compileOnly libs.mixin 18 | } 19 | 20 | apply from: rootProject.file('repositories.gradle') 21 | apply from: 'dependencies.gradle' 22 | 23 | configurations { 24 | commonJava { 25 | canBeResolved = false 26 | canBeConsumed = true 27 | } 28 | commonResources { 29 | canBeResolved = false 30 | canBeConsumed = true 31 | } 32 | commonGeneratedResources { 33 | canBeResolved = false 34 | canBeConsumed = true 35 | } 36 | } 37 | 38 | sourceSets { 39 | generated { 40 | resources { 41 | srcDir 'src/generated/resources' 42 | } 43 | } 44 | } 45 | 46 | artifacts { 47 | commonJava sourceSets.main.java.sourceDirectories.singleFile 48 | commonResources sourceSets.main.resources.sourceDirectories.singleFile 49 | commonGeneratedResources sourceSets.generated.resources.sourceDirectories.singleFile 50 | } 51 | 52 | // Implement mcgradleconventions loader attribute 53 | def loaderAttribute = Attribute.of('io.github.mcgradleconventions.loader', String) 54 | ['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> 55 | configurations.named("$variant") { 56 | attributes { 57 | attribute(loaderAttribute, 'common') 58 | } 59 | } 60 | } 61 | sourceSets.configureEach { 62 | [it.compileClasspathConfigurationName, it.runtimeClasspathConfigurationName].each { variant-> 63 | configurations.named("$variant") { 64 | attributes { 65 | attribute(loaderAttribute, 'common') 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /modpage.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Requires Balm 4 | 5 | 6 | 7 | Become a Patron 8 | 9 | 10 | 11 | Follow me on Twitter 12 | 13 | 14 | Join our Discord 15 | 16 |

17 | 18 | ![](https://blay09.net/files/brand/inventoryessentials.png) 19 | 20 | This mod adds a few inventory control enhancements that have initially been introduced in mods like Inventory Tweaks and Mouse Tweaks, but keeps it limited to only the most essential functionality. This means it can be used alongside other mods that add inventory features, such as Quark or Inventory Sorter, without having to worry about clashes in functionality. As no default behaviour is overriden by this mod, it can be used Plug-and-Play without having to worry about configurations first. 21 | 22 | This mod does not add inventory sorting. If you want sorting alongside this mod, I recommend Inventory Sorter. 23 | 24 | ## Features 25 | 26 | - Control-click an item to only quick-move a single item instead of the entire stack 27 | - Shift-control click an item to quick-move all stacks of that item type (e.g. all cobblestone from a chest into your inventory) 28 | - Hold shift and move over items to quick-move them without having to click each individually 29 | - Optional on client and server. Only those who want to use it need to install it, and if the server doesn't have it installed, it will still work for clients who do have it installed. -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/InventoryEssentialsConfig.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.blay09.mods.balm.Balm; 4 | import net.blay09.mods.balm.platform.config.reflection.Comment; 5 | import net.blay09.mods.balm.platform.config.reflection.Config; 6 | 7 | @Config(InventoryEssentials.MOD_ID) 8 | public class InventoryEssentialsConfig { 9 | 10 | @Comment("Use the client implementation even on servers that have the mod installed - only useful for development purposes.") 11 | public boolean forceClientImplementation; 12 | 13 | @Comment("Should space-clicking move all items even if an empty slot was clicked?") 14 | public boolean allowBulkTransferAllOnEmptySlot = false; 15 | 16 | @Comment("Should space-clicking armor in the inventory swap to all matching armor?") 17 | public boolean bulkTransferArmorSets = true; 18 | 19 | @Comment("Should ctrl-clicking only move one item at a time instead of the full stack?") 20 | public boolean enableSingleTransfer = true; 21 | 22 | @Comment("Should shift-ctrl-clicking move all items of the same type at once?") 23 | public boolean enableBulkTransfer = true; 24 | 25 | @Comment("Should control-space-clicking an item move one of each item from that inventory at once?") 26 | public boolean enableBulkTransferSingle = true; 27 | 28 | @Comment("Should space-clicking an item move all items from that inventory at once?") 29 | public boolean enableBulkTransferAll = true; 30 | 31 | @Comment("Should shift-ctrl-drop-clicking drop all items of the same type at once?") 32 | public boolean enableBulkDrop = true; 33 | 34 | @Comment("Should holding shift and moving your mouse over items quick-transfer them without requiring each to be clicked?") 35 | public boolean enableShiftDrag = true; 36 | 37 | @Comment("Should holding click or right-click with a bundle empty or insert hovered slots?") 38 | public boolean enableBundleDrag = true; 39 | 40 | @Comment("Should middle-clicking a slot sort the inventory?") 41 | public boolean enableMiddleClickSort = true; 42 | 43 | public static InventoryEssentialsConfig getActive() { 44 | return Balm.config().getActiveConfig(InventoryEssentialsConfig.class); 45 | } 46 | 47 | public static void initialize() { 48 | Balm.config().registerConfig(InventoryEssentialsConfig.class); 49 | } 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/ServerSupportedInventoryControls.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.blay09.mods.balm.Balm; 4 | import net.blay09.mods.inventoryessentials.InventoryEssentialsConfig; 5 | import net.blay09.mods.inventoryessentials.network.BulkTransferAllMessage; 6 | import net.blay09.mods.inventoryessentials.network.BulkTransferSingleMessage; 7 | import net.blay09.mods.inventoryessentials.network.SingleTransferMessage; 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.world.entity.player.Player; 11 | import net.minecraft.world.inventory.Slot; 12 | 13 | public class ServerSupportedInventoryControls extends ClientOnlyInventoryControls { 14 | 15 | @Override 16 | public boolean singleTransfer(AbstractContainerScreen screen, Slot clickedSlot) { 17 | Player player = Minecraft.getInstance().player; 18 | if (player == null) { 19 | return false; 20 | } 21 | 22 | if (clickedSlot.mayPickup(player)) { 23 | Balm.networking().sendToServer(new SingleTransferMessage(clickedSlot.index)); 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | @Override 31 | public boolean bulkTransferSingle(AbstractContainerScreen screen, Slot clickedSlot) { 32 | Player player = Minecraft.getInstance().player; 33 | if (player == null) { 34 | return false; 35 | } 36 | 37 | if (!clickedSlot.hasItem() && !InventoryEssentialsConfig.getActive ().allowBulkTransferAllOnEmptySlot) { 38 | return false; 39 | } 40 | 41 | if (clickedSlot.mayPickup(player)) { 42 | Balm.networking().sendToServer(new BulkTransferSingleMessage(clickedSlot.index)); 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | @Override 50 | public boolean bulkTransferAll(AbstractContainerScreen screen, Slot clickedSlot) { 51 | Player player = Minecraft.getInstance().player; 52 | if (player == null) { 53 | return false; 54 | } 55 | 56 | if (!clickedSlot.hasItem() && !InventoryEssentialsConfig.getActive ().allowBulkTransferAllOnEmptySlot) { 57 | return false; 58 | } 59 | 60 | if (clickedSlot.mayPickup(player)) { 61 | Balm.networking().sendToServer(new BulkTransferAllMessage(clickedSlot.index)); 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: publish-snapshot 2 | on: 3 | push: 4 | branches: 5 | - '[0-9]+.[0-9]+.[0-9]+' 6 | - '[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | prepare-matrix: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | matrix: ${{ steps.set-matrix.outputs.result }} 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | - name: Preparing matrix 17 | id: set-matrix 18 | uses: actions/github-script@v7 19 | with: 20 | script: | 21 | const fs = require('fs'); 22 | const settingsGradle = fs.readFileSync('settings.gradle', 'utf8'); 23 | const includePattern = /^(?!\s*\/\/)\s*include\s*\(\s*(['"]([^'"]+)['"](?:,\s*['"]([^'"]+)['"])*\s*)\)/gm; 24 | const includes = [...settingsGradle.matchAll(includePattern)] 25 | .flatMap(match => match[0].match(/['"]([^'"]+)['"]/g).map(item => item.replace(/['"]/g, ''))); 26 | const includeFabric = includes.includes('fabric'); 27 | const includeForge = includes.includes('forge'); 28 | const includeNeoForge = includes.includes('neoforge'); 29 | const gradleProperties = fs.readFileSync('gradle.properties', 'utf8'); 30 | const mavenSnapshots = gradleProperties.match(/^(?!#)maven_snapshots\s*=\s*(.+)/m); 31 | return { 32 | loader: ['common', includeFabric ? 'fabric' : false, includeForge ? 'forge' : false, includeNeoForge ? 'neoforge' : false].filter(Boolean), 33 | task: [mavenSnapshots ? 'publish' : 'build'] 34 | }; 35 | publish-snapshot: 36 | runs-on: ubuntu-latest 37 | strategy: 38 | matrix: ${{fromJson(needs.prepare-matrix.outputs.matrix)}} 39 | fail-fast: false 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | - name: Validate gradle wrapper 44 | uses: gradle/actions/wrapper-validation@v5 45 | - name: Setup JDK 46 | uses: actions/setup-java@v4 47 | with: 48 | java-version: 21 49 | distribution: temurin 50 | - name: Make gradle wrapper executable 51 | run: chmod +x ./gradlew 52 | - name: Extracting version from properties 53 | shell: bash 54 | run: echo "version=$(cat gradle.properties | grep -w "\bversion\s*=" | cut -d= -f2)" >> $GITHUB_OUTPUT 55 | id: extract-version 56 | - name: Bumping version 57 | uses: TwelveIterationMods/bump-version@v1 58 | with: 59 | version: ${{ steps.extract-version.outputs.version }} 60 | bump: patch 61 | id: bump-version 62 | - name: Publish 63 | run: ./gradlew :${{ matrix.loader }}:${{ matrix.task }} '-Pversion=${{ steps.bump-version.outputs.version }}-SNAPSHOT' '-PmavenUsername=${{ secrets.MAVEN_USER }}' '-PmavenPassword=${{ secrets.MAVEN_PASSWORD }}' 64 | needs: prepare-matrix 65 | -------------------------------------------------------------------------------- /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 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/inventoryessentials/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "key.category.inventoryessentials.default": "Inventory Essentials", 3 | "key.inventoryessentials.single_transfer": "Transfer one", 4 | "key.inventoryessentials.bulk_transfer": "Transfer all of type", 5 | "key.inventoryessentials.bulk_transfer_all": "Transfer all", 6 | "key.inventoryessentials.bulk_drop": "Bulk Drop", 7 | "key.inventoryessentials.screen_bulk_drop": "Bulk Drop out of Screen", 8 | "key.inventoryessentials.drag_transfer": "Transfer on Hover (Hold)", 9 | "key.inventoryessentials.sort_inventory": "Sort Inventory", 10 | 11 | "inventoryessentials.configuration.title": "Inventory Essentials", 12 | "inventoryessentials.configuration.forceClientImplementation": "Force Client Implementation (dev only)", 13 | "inventoryessentials.configuration.forceClientImplementation.tooltip": "Use the client implementation even on servers that have the mod installed - only useful for development purposes.", 14 | "inventoryessentials.configuration.allowBulkTransferAllOnEmptySlot": "Allow Transfer All on Empty Slot", 15 | "inventoryessentials.configuration.allowBulkTransferAllOnEmptySlot.tooltip": "Should Transfer All move all items even if an empty slot was clicked?", 16 | "inventoryessentials.configuration.bulkTransferArmorSets": "Bulk Transfer Armor Sets", 17 | "inventoryessentials.configuration.bulkTransferArmorSets.tooltip": "Should space-clicking armor in the inventory swap to all matching armor?", 18 | "inventoryessentials.configuration.enableSingleTransfer": "Enable Single Transfer", 19 | "inventoryessentials.configuration.enableSingleTransfer.tooltip": "Should ctrl-clicking only move one item at a time instead of the full stack?", 20 | "inventoryessentials.configuration.enableBulkTransfer": "Enable Bulk Transfer", 21 | "inventoryessentials.configuration.enableBulkTransfer.tooltip": "Should shift-ctrl-clicking move all items of the same type at once?", 22 | "inventoryessentials.configuration.enableBulkTransferSingle": "Enable Bulk Transfer (One of Each)", 23 | "inventoryessentials.configuration.enableBulkTransferSingle.tooltip": "Should control-space-clicking an item move one of each item from that inventory at once?", 24 | "inventoryessentials.configuration.enableBulkTransferAll": "Enable Bulk Transfer (All)", 25 | "inventoryessentials.configuration.enableBulkTransferAll.tooltip": "Should space-clicking an item move all items from that inventory at once?", 26 | "inventoryessentials.configuration.enableBulkDrop": "Enable Bulk Drop", 27 | "inventoryessentials.configuration.enableBulkDrop.tooltip": "Should shift-ctrl-drop-clicking drop all items of the same type at once?", 28 | "inventoryessentials.configuration.enableShiftDrag": "Enable Shift Drag", 29 | "inventoryessentials.configuration.enableShiftDrag.tooltip": "Should holding shift and moving your mouse over items quick-transfer them without requiring each to be clicked?", 30 | "inventoryessentials.configuration.enableBundleDrag": "Enable Bundle Drag", 31 | "inventoryessentials.configuration.enableBundleDrag.tooltip": "Should holding click or right-click with a bundle empty or insert hovered slots?", 32 | "inventoryessentials.configuration.enableMiddleClickSort": "Enable Middle Click Sort", 33 | "inventoryessentials.configuration.enableMiddleClickSort.tooltip": "Should middle-clicking a slot sort the inventory?" 34 | } -------------------------------------------------------------------------------- /repositories.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | maven { 3 | name = 'Twelve Iterations' 4 | url = 'https://maven.twelveiterations.com/repository/maven-public/' 5 | content { 6 | includeGroup 'net.blay09.mods' 7 | } 8 | } 9 | 10 | maven { 11 | name = 'Twelve Iterations Proxy' 12 | url = 'https://maven.twelveiterations.com/repository/maven-proxy/' 13 | content { 14 | includeGroup 'dev.emi' 15 | } 16 | } 17 | 18 | maven { 19 | name = 'CurseMaven' 20 | url = 'https://www.cursemaven.com' 21 | content { 22 | includeGroup 'curse.maven' 23 | } 24 | } 25 | 26 | maven { 27 | name = 'Shedaniel' 28 | url = 'https://maven.shedaniel.me/' 29 | content { 30 | includeGroup "me.shedaniel" 31 | includeGroup "me.shedaniel.cloth" 32 | includeGroup "dev.architectury" 33 | } 34 | } 35 | 36 | maven { 37 | name = 'BlameJared' 38 | url = 'https://maven.blamejared.com' 39 | content { 40 | includeGroup "mezz.jei" 41 | includeGroup "info.journeymap" 42 | includeGroup "mysticdrew" 43 | } 44 | } 45 | 46 | maven { 47 | name = 'Bai' 48 | url = 'https://maven.bai.lol' 49 | content { 50 | includeGroup "lol.bai" 51 | includeGroup "mcp.mobius.waila" 52 | } 53 | } 54 | 55 | maven { 56 | name = 'JitPack' 57 | url = 'https://jitpack.io' 58 | content { 59 | includeGroup "com.github.BlueMap-Minecraft" 60 | includeGroup "com.github.mattidragon" 61 | } 62 | } 63 | 64 | maven { 65 | name = 'MikePrimm' 66 | url = 'https://repo.mikeprimm.com/' 67 | content { 68 | includeGroup "us.dynmap" 69 | } 70 | } 71 | 72 | maven { 73 | name = 'LadySnake' 74 | url = 'https://maven.ladysnake.org/releases' 75 | content { 76 | includeGroup "dev.onyxstudios.cardinal-components-api" 77 | includeGroup "org.ladysnake.cardinal-components-api" 78 | } 79 | } 80 | 81 | maven { 82 | name = 'Siphalor' 83 | url = 'https://maven.siphalor.de/' 84 | content { 85 | includeGroup "de.siphalor" 86 | } 87 | } 88 | 89 | maven { 90 | name = 'Theillusivec4' 91 | url = 'https://maven.theillusivec4.top/' 92 | content { 93 | includeGroup "top.theillusivec4.curios" 94 | } 95 | } 96 | 97 | maven { 98 | name = 'CloudSmith' 99 | url = 'https://dl.cloudsmith.io/public/novamachina-mods/release/maven/' 100 | content { 101 | includeGroup "novamachina.novacore" 102 | includeGroup "novamachina.exnihilosequentia" 103 | } 104 | } 105 | 106 | exclusiveContent { 107 | forRepository { 108 | maven { 109 | name = 'Minecraft' 110 | url = 'https://libraries.minecraft.net/' 111 | } 112 | } 113 | filter { includeGroupAndSubgroups("com.mojang") } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/InventoryUtils.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.minecraft.core.component.DataComponents; 4 | import net.minecraft.world.entity.EquipmentSlot; 5 | import net.minecraft.world.entity.player.Inventory; 6 | import net.minecraft.world.inventory.AbstractContainerMenu; 7 | import net.minecraft.world.inventory.InventoryMenu; 8 | import net.minecraft.world.inventory.Slot; 9 | import net.minecraft.world.item.ItemStack; 10 | 11 | import java.util.Arrays; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | 16 | public class InventoryUtils { 17 | 18 | public static boolean isSameInventory(Slot targetSlot, Slot slot) { 19 | return isSameInventory(targetSlot, slot, false); 20 | } 21 | 22 | public static boolean isSameInventory(Slot targetSlot, Slot slot, boolean treatHotBarAsSeparate) { 23 | boolean isTargetPlayerInventory = targetSlot.container instanceof Inventory; 24 | boolean isTargetHotBar = isTargetPlayerInventory && Inventory.isHotbarSlot(targetSlot.getContainerSlot()); 25 | boolean isPlayerInventory = slot.container instanceof Inventory; 26 | boolean isHotBar = isPlayerInventory && Inventory.isHotbarSlot(slot.getContainerSlot()); 27 | 28 | if (isTargetPlayerInventory && isPlayerInventory && treatHotBarAsSeparate) { 29 | return isHotBar == isTargetHotBar; 30 | } 31 | 32 | return PlatformBindings.INSTANCE.isSameInventory(targetSlot, slot); 33 | } 34 | 35 | public static boolean containerContainsPlayerInventory(AbstractContainerMenu menu) { 36 | for (Slot slot : menu.slots) { 37 | if (slot.container instanceof Inventory && slot.getContainerSlot() >= 9 && slot.getContainerSlot() < 37) { 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | public static Map findMatchingArmorSetSlots(AbstractContainerMenu menu, Slot baseSlot) { 46 | final var result = new HashMap(); 47 | final var equipmentSlots = Arrays.stream(EquipmentSlot.values()).filter(EquipmentSlot::isArmor).toList(); 48 | final var baseItem = baseSlot.getItem(); 49 | final var baseEquippable = baseItem.get(DataComponents.EQUIPPABLE); 50 | if (baseEquippable != null && baseEquippable.slot().isArmor()) { 51 | result.put(baseEquippable.slot(), baseSlot); 52 | } 53 | 54 | for (final var slot : menu.slots) { 55 | if (menu instanceof InventoryMenu && slot.index >= InventoryMenu.ARMOR_SLOT_START && slot.index < InventoryMenu.ARMOR_SLOT_END) { 56 | continue; 57 | } 58 | 59 | final var slotStack = slot.getItem(); 60 | final var slotEquippable = slotStack.get(DataComponents.EQUIPPABLE); 61 | if (slotEquippable != null && slotEquippable.slot().isArmor()) { 62 | if (isMatchingArmorSet(baseItem, slotStack) && !result.containsKey(slotEquippable.slot())) { 63 | result.put(slotEquippable.slot(), slot); 64 | } 65 | } 66 | if (result.size() >= equipmentSlots.size()) { 67 | break; 68 | } 69 | } 70 | 71 | return result; 72 | } 73 | 74 | private static boolean isMatchingArmorSet(ItemStack baseItem, ItemStack otherItem) { 75 | final var baseEquippable = baseItem.get(DataComponents.EQUIPPABLE); 76 | final var otherEquippable = otherItem.get(DataComponents.EQUIPPABLE); 77 | if (baseEquippable != null && otherEquippable != null) { 78 | return Objects.equals(baseEquippable.assetId().orElse(null), otherEquippable.assetId().orElse(null)); 79 | } 80 | 81 | return false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/InventoryEssentialsIgnores.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials; 2 | 3 | import net.blay09.mods.inventoryessentials.data.IgnoredData; 4 | import net.blay09.mods.inventoryessentials.mixin.AbstractContainerMenuAccessor; 5 | import net.blay09.mods.inventoryessentials.mixin.AbstractContainerScreenAccessor; 6 | import net.blay09.mods.inventoryessentials.mixin.CreativeModeInventoryScreenAccessor; 7 | import net.minecraft.client.gui.screens.Screen; 8 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 9 | import net.minecraft.core.registries.BuiltInRegistries; 10 | import net.minecraft.resources.Identifier; 11 | import net.minecraft.world.entity.player.Inventory; 12 | import net.minecraft.world.inventory.ResultSlot; 13 | import net.minecraft.world.inventory.Slot; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | public class InventoryEssentialsIgnores { 20 | 21 | private static final Set ignoredScreenClasses = new HashSet<>(); 22 | private static final Set ignoredMenuClasses = new HashSet<>(); 23 | private static final Set ignored = new HashSet<>(); 24 | private static final Set ignoredMenuTypes = new HashSet<>(); 25 | 26 | public static boolean shouldIgnoreScreen(Screen screen) { 27 | if (!(screen instanceof AbstractContainerScreenAccessor)) { 28 | return true; 29 | } 30 | 31 | if (ignoredScreenClasses.contains(screen.getClass().getName())) { 32 | return true; 33 | } 34 | 35 | final var menu = ((AbstractContainerScreen) screen).getMenu(); 36 | if (ignoredMenuClasses.contains(menu.getClass().getName())) { 37 | return true; 38 | } 39 | 40 | final var menuType = ((AbstractContainerMenuAccessor) menu).balm$getMenuType(); 41 | final var typeId = menuType != null ? BuiltInRegistries.MENU.getKey(menuType) : null; 42 | //noinspection RedundantIfStatement 43 | if (typeId != null && ignoredMenuTypes.contains(typeId)) { 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public static boolean shouldIgnoreSlot(AbstractContainerScreen screen, @Nullable Slot slot) { 51 | if (slot == null) { 52 | return true; 53 | } 54 | 55 | if (ignored.contains(slot.getClass().getName())) { 56 | return true; 57 | } 58 | 59 | // Do not handle drags on crafting result slots 60 | if (slot instanceof ResultSlot) { 61 | return true; 62 | } 63 | 64 | if (screen instanceof CreativeModeInventoryScreenAccessor creativeAccessor) { 65 | return !(slot.container instanceof Inventory) && slot.container == creativeAccessor.getCONTAINER(); 66 | } 67 | 68 | return false; 69 | } 70 | 71 | public static void addIgnoredMenuType(Identifier menuId) { 72 | ignoredMenuTypes.add(menuId); 73 | } 74 | 75 | public static void addIgnoredMenuClass(String menuClass) { 76 | ignoredMenuClasses.add(menuClass); 77 | } 78 | 79 | public static void addIgnoredScreenClass(String screenClass) { 80 | ignoredScreenClasses.add(screenClass); 81 | } 82 | 83 | public static void addIgnoredSlotClass(String slotClass) { 84 | ignored.add(slotClass); 85 | } 86 | 87 | public static void addIgnoredData(IgnoredData ignoredData) { 88 | ignoredData.ignoredMenuClasses.forEach(InventoryEssentialsIgnores::addIgnoredMenuClass); 89 | ignoredData.ignoredMenuTypes.stream().map(Identifier::parse).forEach(InventoryEssentialsIgnores::addIgnoredMenuType); 90 | ignoredData.ignoredScreenClasses.forEach(InventoryEssentialsIgnores::addIgnoredScreenClass); 91 | ignoredData.ignoredSlotClasses.forEach(InventoryEssentialsIgnores::addIgnoredSlotClass); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /neoforge/build.gradle: -------------------------------------------------------------------------------- 1 | import net.darkhax.curseforgegradle.TaskPublishCurseForge 2 | 3 | plugins { 4 | id 'multiloader-loader' 5 | alias libs.plugins.modDevGradle 6 | alias libs.plugins.curseForgeGradle 7 | alias libs.plugins.modrinthMinotaur 8 | } 9 | 10 | neoForge { 11 | version = libs.neoForge.get().version 12 | // Automatically enable neoforge AccessTransformers if the file exists 13 | def at = project(':common').file('src/main/resources/META-INF/accesstransformer.cfg') 14 | if (at.exists()) { 15 | accessTransformers.from(at.absolutePath) 16 | } 17 | 18 | parchment.parchmentArtifact = libs.parchment.get().toString() 19 | 20 | runs { 21 | configureEach { 22 | systemProperty('neoforge.enabledGameTestNamespaces', mod_id) 23 | ideName = "NeoForge ${it.name.capitalize()} (${project.path})" 24 | } 25 | 26 | client { 27 | client() 28 | } 29 | 30 | server { 31 | server() 32 | } 33 | } 34 | 35 | mods { 36 | "${mod_id}" { 37 | sourceSet sourceSets.main 38 | } 39 | } 40 | } 41 | 42 | sourceSets.main.resources { srcDir 'src/generated/resources' } 43 | 44 | apply from: rootProject.file('repositories.gradle') 45 | apply from: 'dependencies.gradle' 46 | 47 | tasks.register('curseforge', TaskPublishCurseForge) { 48 | dependsOn('build') 49 | description = 'Publishes the NeoForge build to CurseForge.' 50 | group = 'publishing' 51 | 52 | apiToken = project.findProperty("curseforge.api_key") ?: System.getenv("CURSEFORGE_TOKEN") ?: "none" 53 | 54 | def projectId = findProperty("curseforge_project_id") 55 | onlyIf { 56 | projectId != null 57 | } 58 | if (projectId) { 59 | def mainFile = upload(findProperty("curseforge_project_id"), jar.archiveFile.get().asFile) 60 | mainFile.changelog = rootProject.file('CHANGELOG.md').text 61 | mainFile.addRequirement("balm") 62 | mainFile.addGameVersion(libs.minecraft.get().version) 63 | mainFile.releaseType = "release" 64 | project.findProperty("curseforge_environments")?.split(",")?.toList()?.each { mainFile.addEnvironment(it) } 65 | mainFile.addModLoader("NeoForge") 66 | } 67 | } 68 | 69 | modrinth { 70 | token = project.findProperty("modrinth.token") ?: System.getenv("MODRINTH_TOKEN") ?: "none" 71 | projectId = findProperty("modrinth_project_id") 72 | versionType = "release" 73 | versionNumber = project.version + "+neoforge-" + libs.minecraft.get().version 74 | uploadFile = jar 75 | changelog = rootProject.file("CHANGELOG.md").text 76 | gameVersions = [libs.minecraft.get().version] 77 | syncBodyFrom = rootProject.file("modpage.md").text 78 | loaders = ['neoforge'] 79 | dependencies { 80 | required.project "balm" 81 | } 82 | } 83 | 84 | def neoForgeSnapshotUrl = findProperty("neoforge_snapshot_url") 85 | if (neoForgeSnapshotUrl != null && !neoForgeSnapshotUrl.isBlank()) { 86 | repositories { 87 | maven { 88 | name = 'Maven for NeoForge Snapshots' 89 | url = neoForgeSnapshotUrl 90 | content { 91 | includeModule('net.neoforged', 'neoforge') 92 | includeModule('net.neoforged', 'testframework') 93 | } 94 | } 95 | } 96 | } 97 | 98 | // Implement mcgradleconventions loader attribute 99 | def loaderAttribute = Attribute.of('io.github.mcgradleconventions.loader', String) 100 | ['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> 101 | configurations.named("$variant") { 102 | attributes { 103 | attribute(loaderAttribute, 'neoforge') 104 | } 105 | } 106 | } 107 | sourceSets.configureEach { 108 | [it.compileClasspathConfigurationName, it.runtimeClasspathConfigurationName, it.getTaskName(null, 'jarJar')].each { variant-> 109 | configurations.named("$variant") { 110 | attributes { 111 | attribute(loaderAttribute, 'neoforge') 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /fabric/build.gradle: -------------------------------------------------------------------------------- 1 | import net.darkhax.curseforgegradle.TaskPublishCurseForge 2 | 3 | plugins { 4 | id 'multiloader-loader' 5 | alias libs.plugins.fabricLoom 6 | alias libs.plugins.curseForgeGradle 7 | alias libs.plugins.modrinthMinotaur 8 | } 9 | 10 | dependencies { 11 | minecraft libs.minecraft 12 | mappings loom.layered() { 13 | officialMojangMappings() 14 | parchment variantOf(libs.parchment) { 15 | artifactType('zip') 16 | } 17 | } 18 | modImplementation libs.fabricLoader 19 | modImplementation libs.fabricApi 20 | } 21 | 22 | apply from: rootProject.file('repositories.gradle') 23 | apply from: 'dependencies.gradle' 24 | 25 | loom { 26 | def aw = project(":common").file("src/main/resources/${mod_id}.accesswidener") 27 | if (aw.exists()) { 28 | accessWidenerPath.set(aw) 29 | } 30 | 31 | mixin { 32 | defaultRefmapName.set("${mod_id}.refmap.json") 33 | } 34 | 35 | runs { 36 | client { 37 | client() 38 | setConfigName("fabric Client") 39 | ideConfigGenerated(true) 40 | runDir("runs/client") 41 | } 42 | server { 43 | server() 44 | setConfigName("fabric Server") 45 | ideConfigGenerated(true) 46 | runDir("runs/server") 47 | } 48 | data { 49 | inherit client 50 | setConfigName("fabric Data") 51 | ideConfigGenerated(true) 52 | runDir("build/datagen") 53 | 54 | vmArg "-Dfabric-api.datagen" 55 | vmArg "-Dfabric-api.datagen.output-dir=${project(":common").file("src/generated/resources")}" 56 | vmArg "-Dfabric-api.datagen.modid=${mod_id}" 57 | } 58 | } 59 | } 60 | 61 | // Implement mcgradleconventions loader attribute 62 | def loaderAttribute = Attribute.of('io.github.mcgradleconventions.loader', String) 63 | ['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements', 'includeInternal', 'modCompileClasspath'].each { variant -> 64 | configurations.named("$variant") { 65 | attributes { 66 | attribute(loaderAttribute, 'fabric') 67 | } 68 | } 69 | } 70 | sourceSets.configureEach { 71 | [it.compileClasspathConfigurationName, it.runtimeClasspathConfigurationName].each { variant-> 72 | configurations.named("$variant") { 73 | attributes { 74 | attribute(loaderAttribute, 'fabric') 75 | } 76 | } 77 | } 78 | } 79 | loom.remapConfigurations.configureEach { 80 | configurations.named(it.name) { 81 | attributes { 82 | attribute(loaderAttribute, 'fabric') 83 | } 84 | } 85 | } 86 | 87 | tasks.register('curseforge', TaskPublishCurseForge) { 88 | dependsOn('build') 89 | description = 'Publishes the Fabric build to CurseForge.' 90 | group = 'publishing' 91 | 92 | apiToken = project.findProperty("curseforge.api_key") ?: System.getenv("CURSEFORGE_TOKEN") ?: "none" 93 | 94 | def projectId = findProperty("curseforge_project_id") 95 | onlyIf { 96 | projectId != null 97 | } 98 | if (projectId) { 99 | def mainFile = upload(findProperty("curseforge_project_id"), remapJar.archiveFile.get().asFile) 100 | mainFile.changelog = rootProject.file('CHANGELOG.md').text 101 | mainFile.addRequirement("fabric-api") 102 | mainFile.addRequirement("balm") 103 | mainFile.addGameVersion(libs.minecraft.get().version) 104 | mainFile.releaseType = "release" 105 | project.findProperty("curseforge_environments")?.split(",")?.toList()?.each { mainFile.addEnvironment(it) } 106 | } 107 | } 108 | 109 | modrinth { 110 | token = project.findProperty("modrinth.token") ?: System.getenv("MODRINTH_TOKEN") ?: "none" 111 | projectId = findProperty("modrinth_project_id") 112 | versionType = "release" 113 | versionNumber = project.version + "+fabric-" + libs.minecraft.get().version 114 | uploadFile = remapJar 115 | changelog = rootProject.file("CHANGELOG.md").text 116 | gameVersions = [libs.minecraft.get().version] 117 | syncBodyFrom = rootProject.file("modpage.md").text 118 | loaders = ['fabric'] 119 | dependencies { 120 | required.project "fabric-api" 121 | required.project "balm" 122 | } 123 | } -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/InventoryEssentialsClient.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.blay09.mods.balm.client.BalmClientRegistrars; 4 | import net.blay09.mods.balm.client.platform.event.callback.ClientLifecycleCallback; 5 | import net.blay09.mods.balm.client.platform.event.callback.ScreenCallback; 6 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 7 | import net.blay09.mods.inventoryessentials.InventoryEssentialsConfig; 8 | import net.blay09.mods.inventoryessentials.InventoryEssentialsIgnores; 9 | import net.blay09.mods.inventoryessentials.mixin.AbstractContainerScreenAccessor; 10 | import net.blay09.mods.inventoryessentials.mixin.CreativeModeInventoryScreenAccessor; 11 | import net.minecraft.client.gui.screens.Screen; 12 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 13 | import net.minecraft.tags.ItemTags; 14 | import net.minecraft.world.inventory.Slot; 15 | 16 | public class InventoryEssentialsClient { 17 | 18 | private static final InventoryControls clientOnlyControls = new ClientOnlyInventoryControls(); 19 | private static final InventoryControls creativeControls = new CreativeInventoryControls(); 20 | private static final InventoryControls serverSupportedControls = new ServerSupportedInventoryControls(); 21 | 22 | private static Slot lastDragHoverSlot; 23 | private static boolean hasDragClicked; 24 | 25 | public static void initialize(BalmClientRegistrars registrars) { 26 | ClientLifecycleCallback.DisconnectedFromServer.EVENT.register(client -> InventoryEssentials.isServerSideInstalled = false); 27 | 28 | ModKeyMappings.initialize(); 29 | 30 | ScreenCallback.MouseDrag.Before.EVENT.register(InventoryEssentialsClient::onMouseDrag); 31 | ScreenCallback.MouseRelease.Before.EVENT.register(InventoryEssentialsClient::onMouseRelease); 32 | } 33 | 34 | public static InventoryControls getInventoryControls(Screen screen) { 35 | if (screen instanceof CreativeModeInventoryScreenAccessor) { 36 | return creativeControls; 37 | } 38 | 39 | return InventoryEssentials.isServerSideInstalled && !InventoryEssentialsConfig.getActive().forceClientImplementation ? serverSupportedControls : clientOnlyControls; 40 | } 41 | 42 | public static boolean onMouseRelease(Screen screen, double mouseX, double mouseY, int button) { 43 | if (screen instanceof AbstractContainerScreen containerScreen) { 44 | Slot hoverSlot = ((AbstractContainerScreenAccessor) containerScreen).getHoveredSlot(); 45 | if (hoverSlot == null || InventoryEssentialsIgnores.shouldIgnoreScreen(containerScreen) || InventoryEssentialsIgnores.shouldIgnoreSlot(containerScreen, hoverSlot)) { 46 | return false; 47 | } 48 | 49 | if (hasDragClicked) { 50 | hasDragClicked = false; 51 | return true; 52 | } 53 | } 54 | return false; 55 | } 56 | 57 | public static boolean onMouseDrag(Screen screen, double mouseX, double mouseY, int button, double horizontalAmount, double verticalAmount) { 58 | if (screen instanceof AbstractContainerScreen containerScreen) { 59 | Slot hoverSlot = ((AbstractContainerScreenAccessor) containerScreen).getHoveredSlot(); 60 | if (hoverSlot == null || InventoryEssentialsIgnores.shouldIgnoreScreen(containerScreen) || InventoryEssentialsIgnores.shouldIgnoreSlot(containerScreen, hoverSlot)) { 61 | return false; 62 | } 63 | 64 | // If shift is held, perform drag transfer 65 | if (ModKeyMappings.keyDragTransfer.isActiveAndDown() && (button == 0 || button == 1)) { 66 | if (hoverSlot.hasItem() && hoverSlot != lastDragHoverSlot) { 67 | InventoryControls controls = getInventoryControls(containerScreen); 68 | if (InventoryEssentialsConfig.getActive().enableShiftDrag) { 69 | controls.dragTransfer(containerScreen, hoverSlot); 70 | } 71 | lastDragHoverSlot = hoverSlot; 72 | } 73 | return false; 74 | } 75 | 76 | // If dragging mouse button while holding a bundle, perform drag clicks 77 | if (InventoryEssentialsConfig.getActive().enableBundleDrag) { 78 | final var carriedStack = containerScreen.getMenu().getCarried(); 79 | if (carriedStack.is(ItemTags.BUNDLES)) { 80 | if (hoverSlot != lastDragHoverSlot) { 81 | if ((button == 0 && hoverSlot.hasItem()) || (button == 1 && !hoverSlot.hasItem())) { 82 | final var controls = getInventoryControls(containerScreen); 83 | controls.dragClick(containerScreen, hoverSlot, button); 84 | hasDragClicked = true; 85 | // Quick-craft causes subsequent clicks to not work right because it never gets reset due to our cancels 86 | ((AbstractContainerScreenAccessor) containerScreen).setIsQuickCrafting(false); 87 | } 88 | lastDragHoverSlot = hoverSlot; 89 | } 90 | return true; 91 | } 92 | } 93 | 94 | lastDragHoverSlot = null; 95 | } else { 96 | lastDragHoverSlot = null; 97 | } 98 | 99 | return false; 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /forge/build.gradle: -------------------------------------------------------------------------------- 1 | import net.darkhax.curseforgegradle.TaskPublishCurseForge 2 | 3 | plugins { 4 | id 'multiloader-loader' 5 | alias libs.plugins.forgeGradle 6 | alias libs.plugins.mixin 7 | alias libs.plugins.curseForgeGradle 8 | alias libs.plugins.modrinthMinotaur 9 | } 10 | 11 | mixin { 12 | config("${mod_id}.mixins.json") 13 | config("${mod_id}.forge.mixins.json") 14 | } 15 | 16 | jar { 17 | manifest { 18 | attributes["MixinConfigs"] = "${mod_id}.mixins.json,${mod_id}.forge.mixins.json" 19 | } 20 | } 21 | 22 | minecraft { 23 | mappings channel: 'official', version: libs.minecraft.get().version 24 | 25 | copyIdeResources = true //Calls processResources when in dev 26 | 27 | reobf = false // Forge 1.20.6+ uses official mappings at runtime, so we shouldn't reobf from official to SRG 28 | 29 | // Automatically enable forge AccessTransformers if the file exists 30 | def at = file('src/main/resources/META-INF/accesstransformer.cfg') 31 | if (at.exists()) { 32 | accessTransformer = at 33 | } 34 | 35 | runs { 36 | client { 37 | workingDirectory file('runs/client') 38 | ideaModule "${rootProject.name}.${project.name}.main" 39 | taskName "Client" 40 | 41 | property 'forge.enabledGameTestNamespaces', mod_id 42 | 43 | mods { 44 | modClientRun { 45 | source sourceSets.main 46 | } 47 | } 48 | } 49 | 50 | server { 51 | workingDirectory file('runs/server') 52 | ideaModule "${rootProject.name}.${project.name}.main" 53 | taskName "Server" 54 | 55 | property 'forge.enabledGameTestNamespaces', mod_id 56 | 57 | mods { 58 | modServerRun { 59 | source sourceSets.main 60 | } 61 | } 62 | } 63 | 64 | data { 65 | workingDirectory file('runs/data') 66 | ideaModule "${rootProject.name}.${project.name}.main" 67 | args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') 68 | taskName "Data" 69 | 70 | mods { 71 | modDataRun { 72 | source sourceSets.main 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | sourceSets.main.resources.srcDir 'src/generated/resources' 80 | 81 | dependencies { 82 | def mcVersion = libs.minecraft.get().version 83 | def forgeVersion = libs.forge.get().version 84 | minecraft "net.minecraftforge:forge:${mcVersion}-${forgeVersion}" 85 | def mixinVersion = libs.mixin.get().version 86 | annotationProcessor "org.spongepowered:mixin:${mixinVersion}:processor" 87 | } 88 | 89 | apply from: rootProject.file('repositories.gradle') 90 | apply from: 'dependencies.gradle' 91 | 92 | publishing { 93 | publications { 94 | mavenJava(MavenPublication) { 95 | fg.component(it) 96 | } 97 | } 98 | } 99 | 100 | tasks.register('curseforge', TaskPublishCurseForge) { 101 | dependsOn('build') 102 | description = 'Publishes the Forge build to CurseForge.' 103 | group = 'publishing' 104 | 105 | apiToken = project.findProperty("curseforge.api_key") ?: System.getenv("CURSEFORGE_TOKEN") ?: "none" 106 | 107 | def projectId = findProperty("curseforge_project_id") 108 | onlyIf { 109 | projectId != null 110 | } 111 | if (projectId) { 112 | def mainFile = upload(findProperty("curseforge_project_id"), jar.archiveFile.get().asFile) 113 | mainFile.changelog = rootProject.file('CHANGELOG.md').text 114 | mainFile.addRequirement("balm") 115 | mainFile.addGameVersion(libs.minecraft.get().version) 116 | mainFile.releaseType = "release" 117 | project.findProperty("curseforge_environments")?.split(",")?.toList()?.each { mainFile.addEnvironment(it) } 118 | } 119 | } 120 | 121 | modrinth { 122 | token = project.findProperty("modrinth.token") ?: System.getenv("MODRINTH_TOKEN") ?: "none" 123 | projectId = findProperty("modrinth_project_id") 124 | versionType = "release" 125 | versionNumber = project.version + "+forge-" + libs.minecraft.get().version 126 | uploadFile = jar 127 | changelog = rootProject.file("CHANGELOG.md").text 128 | gameVersions = [libs.minecraft.get().version] 129 | syncBodyFrom = rootProject.file("modpage.md").text 130 | loaders = ['forge'] 131 | dependencies { 132 | required.project "balm" 133 | } 134 | } 135 | 136 | sourceSets.each { 137 | def dir = layout.buildDirectory.dir("sourcesSets/$it.name") 138 | it.output.resourcesDir = dir 139 | it.java.destinationDirectory = dir 140 | } 141 | 142 | // Implement mcgradleconventions loader attribute 143 | def loaderAttribute = Attribute.of('io.github.mcgradleconventions.loader', String) 144 | ['apiElements', 'runtimeElements', 'sourcesElements', 'javadocElements'].each { variant -> 145 | configurations.named("$variant") { 146 | attributes { 147 | attribute(loaderAttribute, 'forge') 148 | } 149 | } 150 | } 151 | sourceSets.configureEach { 152 | [it.compileClasspathConfigurationName, it.runtimeClasspathConfigurationName].each { variant-> 153 | configurations.named("$variant") { 154 | attributes { 155 | attribute(loaderAttribute, 'forge') 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/ModKeyMappings.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import com.mojang.blaze3d.platform.InputConstants; 4 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 5 | import net.blay09.mods.inventoryessentials.InventoryEssentialsIgnores; 6 | import net.blay09.mods.inventoryessentials.InventoryEssentialsConfig; 7 | import net.blay09.mods.inventoryessentials.mixin.AbstractContainerScreenAccessor; 8 | import net.blay09.mods.kuma.api.*; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.resources.Identifier; 11 | import net.minecraft.world.inventory.Slot; 12 | 13 | import java.util.function.BiFunction; 14 | import java.util.function.Supplier; 15 | 16 | public class ModKeyMappings { 17 | 18 | public static ManagedKeyMapping keySingleTransfer; 19 | public static ManagedKeyMapping keyBulkTransfer; 20 | public static ManagedKeyMapping keyBulkTransferSingle; 21 | public static ManagedKeyMapping keyBulkTransferAll; 22 | public static ManagedKeyMapping keyBulkDrop; 23 | public static ManagedKeyMapping keyScreenBulkDrop; 24 | public static ManagedKeyMapping keyDragTransfer; 25 | public static ManagedKeyMapping keySortInventory; 26 | 27 | public static void initialize() { 28 | keySingleTransfer = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "single_transfer")) 29 | .withDefault(InputBinding.mouse(InputConstants.MOUSE_BUTTON_LEFT, KeyModifiers.of(KeyModifier.CONTROL))) 30 | .handleScreenInput(event -> handleSlotInput(event, () -> InventoryEssentialsConfig.getActive().enableSingleTransfer, 31 | (screen, slot) -> InventoryEssentialsClient.getInventoryControls(screen).singleTransfer(screen, slot))) 32 | .build(); 33 | 34 | keyBulkTransfer = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "bulk_transfer")) 35 | .withDefault(InputBinding.mouse(InputConstants.MOUSE_BUTTON_LEFT, KeyModifiers.of(KeyModifier.SHIFT, KeyModifier.CONTROL))) 36 | .handleScreenInput(event -> handleSlotInput(event, () -> InventoryEssentialsConfig.getActive().enableBulkTransfer, 37 | (screen, slot) -> InventoryEssentialsClient.getInventoryControls(screen).bulkTransferByType(screen, slot))) 38 | .build(); 39 | 40 | keyBulkTransferSingle = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "bulk_transfer_single")) 41 | .withDefault(InputBinding.mouse(InputConstants.MOUSE_BUTTON_RIGHT, KeyModifiers.ofCustom(InputConstants.Type.KEYSYM.getOrCreate(InputConstants.KEY_SPACE)))) 42 | .handleScreenInput(event -> handleSlotInput(event, () -> InventoryEssentialsConfig.getActive().enableBulkTransferSingle, 43 | (screen, slot) -> InventoryEssentialsClient.getInventoryControls(screen).bulkTransferSingle(screen, slot))) 44 | .build(); 45 | 46 | keyBulkTransferAll = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "bulk_transfer_all")) 47 | .withDefault(InputBinding.mouse(InputConstants.MOUSE_BUTTON_LEFT, KeyModifiers.ofCustom(InputConstants.Type.KEYSYM.getOrCreate(InputConstants.KEY_SPACE)))) 48 | .handleScreenInput(event -> handleSlotInput(event, () -> InventoryEssentialsConfig.getActive().enableBulkTransferAll, 49 | (screen, slot) -> InventoryEssentialsClient.getInventoryControls(screen).bulkTransferAll(screen, slot))) 50 | .build(); 51 | 52 | keyBulkDrop = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "bulk_drop")) 53 | .withDefault(InputBinding.key(InputConstants.KEY_Q, KeyModifiers.of(KeyModifier.SHIFT, KeyModifier.CONTROL))) 54 | .handleScreenInput(event -> handleSlotInput(event, () -> InventoryEssentialsConfig.getActive().enableBulkDrop, 55 | (screen, slot) -> InventoryEssentialsClient.getInventoryControls(screen).dropByType(screen, slot))) 56 | .build(); 57 | 58 | keyScreenBulkDrop = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "screen_bulk_drop")) 59 | .withDefault(InputBinding.mouse(InputConstants.MOUSE_BUTTON_LEFT, KeyModifiers.of(KeyModifier.SHIFT))) 60 | .handleScreenInput(event -> { 61 | if (!InventoryEssentialsConfig.getActive().enableBulkDrop) { 62 | return false; 63 | } 64 | 65 | if (InventoryEssentialsIgnores.shouldIgnoreScreen(event.screen())) { 66 | return false; 67 | } 68 | 69 | if (!(event.screen() instanceof AbstractContainerScreen containerScreen)) { 70 | return false; 71 | } 72 | 73 | final var accessor = (AbstractContainerScreenAccessor) containerScreen; 74 | final var clickedOutside = accessor.callHasClickedOutside(event.mouseX(), 75 | event.mouseY(), 76 | accessor.getLeftPos(), 77 | accessor.getTopPos()); 78 | return clickedOutside && InventoryEssentialsClient.getInventoryControls(containerScreen) 79 | .dropByType(containerScreen, containerScreen.getMenu().getCarried()); 80 | }) 81 | .build(); 82 | 83 | keyDragTransfer = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "drag_transfer")) 84 | .withDefault(InputBinding.key(InputConstants.KEY_LSHIFT)) 85 | .withContext(KeyConflictContext.SCREEN) 86 | .forceVirtual() 87 | .build(); 88 | 89 | keySortInventory = Kuma.createKeyMapping(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, "sort_inventory")) 90 | .withDefault(InputBinding.mouse(InputConstants.MOUSE_BUTTON_MIDDLE)) 91 | .handleScreenInput(event -> handleSlotInput(event, () -> InventoryEssentialsConfig.getActive().enableMiddleClickSort, 92 | (screen, slot) -> InventoryEssentialsClient.getInventoryControls(screen).sort(screen, slot))) 93 | .build(); 94 | } 95 | 96 | private static boolean handleSlotInput(ScreenInputEvent event, Supplier predicate, BiFunction, Slot, Boolean> handler) { 97 | if (!predicate.get()) { 98 | return false; 99 | } 100 | 101 | if (InventoryEssentialsIgnores.shouldIgnoreScreen(event.screen())) { 102 | return false; 103 | } 104 | 105 | if (!(event.screen() instanceof AbstractContainerScreen containerScreen)) { 106 | return false; 107 | } 108 | 109 | final var hoverSlot = ((AbstractContainerScreenAccessor) containerScreen).getHoveredSlot(); 110 | if (InventoryEssentialsIgnores.shouldIgnoreSlot(containerScreen, hoverSlot)) { 111 | return false; 112 | } 113 | 114 | return handler.apply(containerScreen, hoverSlot); 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/network/BulkTransferAllMessage.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.network; 2 | 3 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 4 | import net.blay09.mods.inventoryessentials.InventoryEssentialsConfig; 5 | import net.blay09.mods.inventoryessentials.InventoryUtils; 6 | import net.minecraft.core.component.DataComponents; 7 | import net.minecraft.network.RegistryFriendlyByteBuf; 8 | import net.minecraft.network.codec.ByteBufCodecs; 9 | import net.minecraft.network.codec.StreamCodec; 10 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 11 | import net.minecraft.resources.Identifier; 12 | import net.minecraft.server.level.ServerPlayer; 13 | import net.minecraft.world.entity.EquipmentSlot; 14 | import net.minecraft.world.entity.player.Inventory; 15 | import net.minecraft.world.entity.player.Player; 16 | import net.minecraft.world.inventory.AbstractContainerMenu; 17 | import net.minecraft.world.inventory.ClickType; 18 | import net.minecraft.world.inventory.InventoryMenu; 19 | import net.minecraft.world.inventory.Slot; 20 | import net.minecraft.world.item.ItemStack; 21 | 22 | import java.util.*; 23 | 24 | public record BulkTransferAllMessage(int slotNumber) implements CustomPacketPayload { 25 | 26 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, 27 | "bulk_transfer_all")); 28 | 29 | public static final StreamCodec STREAM_CODEC = StreamCodec.composite( 30 | ByteBufCodecs.INT, 31 | BulkTransferAllMessage::slotNumber, 32 | BulkTransferAllMessage::new 33 | ); 34 | 35 | public static void handle(ServerPlayer player, BulkTransferAllMessage message) { 36 | AbstractContainerMenu menu = player.containerMenu; 37 | if (menu != null && message.slotNumber >= 0 && message.slotNumber < menu.slots.size()) { 38 | Slot clickedSlot = menu.slots.get(message.slotNumber); 39 | 40 | boolean isProbablyMovingToPlayerInventory = false; 41 | // If the clicked slot is *not* from the player inventory, 42 | if (!(clickedSlot.container instanceof Inventory)) { 43 | // Search for any slot that belongs to the player inventory area (not hotbar) 44 | isProbablyMovingToPlayerInventory = InventoryUtils.containerContainsPlayerInventory(menu); 45 | } 46 | 47 | final var clickedEquippable = clickedSlot.getItem().get(DataComponents.EQUIPPABLE); 48 | boolean clickedAnArmorItem = clickedEquippable != null && clickedEquippable.slot().isArmor(); 49 | boolean isInsideInventory = menu instanceof InventoryMenu; 50 | 51 | if (isProbablyMovingToPlayerInventory) { 52 | // To avoid O(n²), find empty and non-empty slots beforehand in one loop iteration 53 | Deque emptySlots = new ArrayDeque<>(); 54 | List nonEmptySlots = new ArrayList<>(); 55 | for (Slot slot : menu.slots) { 56 | if (InventoryUtils.isSameInventory(slot, clickedSlot) || !(slot.container instanceof Inventory)) { 57 | continue; 58 | } 59 | 60 | if (slot.hasItem()) { 61 | nonEmptySlots.add(slot); 62 | } else if (!Inventory.isHotbarSlot(slot.getContainerSlot())) { 63 | emptySlots.add(slot); 64 | } 65 | } 66 | 67 | // Now go through each slot that is accessible and belongs to the same inventory as the clicked slot 68 | for (Slot slot : menu.slots) { 69 | if (!slot.mayPickup(player)) { 70 | continue; 71 | } 72 | 73 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 74 | // and bulk-transfer each of them using the prefer-inventory behaviour 75 | bulkTransferPreferInventory(player, menu, emptySlots, nonEmptySlots, slot); 76 | } 77 | } 78 | } else if (clickedAnArmorItem && isInsideInventory) { 79 | if (!InventoryEssentialsConfig.getActive().bulkTransferArmorSets) { 80 | return; 81 | } 82 | 83 | // When clicking an equipped armor, un-equip all 84 | if (clickedSlot.index >= InventoryMenu.ARMOR_SLOT_START && clickedSlot.index < InventoryMenu.ARMOR_SLOT_END) { 85 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 86 | menu.clicked(i, 0, ClickType.QUICK_MOVE, player); 87 | } 88 | return; 89 | } 90 | 91 | // Swap current armor with clicked armor set 92 | final var armorSlots = InventoryUtils.findMatchingArmorSetSlots(menu, clickedSlot); 93 | final var equipmentSlots = List.of(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET); 94 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 95 | final var equipmentSlot = equipmentSlots.get(i - InventoryMenu.ARMOR_SLOT_START); 96 | final var swapSlot = armorSlots.get(equipmentSlot); 97 | if (swapSlot != null) { 98 | menu.clicked(i, 0, ClickType.PICKUP, player); 99 | menu.clicked(swapSlot.index, 0, ClickType.PICKUP, player); 100 | menu.clicked(i, 0, ClickType.PICKUP, player); 101 | } 102 | } 103 | } else { 104 | // Just a normal inventory-to-inventory transfer, simply shift-click the items 105 | for (Slot slot : menu.slots) { 106 | if (!slot.mayPickup(player)) { 107 | continue; 108 | } 109 | 110 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 111 | menu.clicked(slot.index, 0, ClickType.QUICK_MOVE, player); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | 118 | private static boolean bulkTransferPreferInventory(Player player, AbstractContainerMenu menu, Deque emptySlots, List nonEmptySlots, Slot slot) { 119 | ItemStack targetStack = slot.getItem().copy(); 120 | if (targetStack.isEmpty()) { 121 | return false; 122 | } 123 | 124 | menu.clicked(slot.index, 0, ClickType.PICKUP, player); 125 | 126 | for (Slot nonEmptySlot : nonEmptySlots) { 127 | ItemStack stack = nonEmptySlot.getItem(); 128 | if (ItemStack.isSameItemSameComponents(targetStack, stack)) { 129 | boolean hasSpaceLeft = stack.getCount() < Math.min(nonEmptySlot.getMaxStackSize(), nonEmptySlot.getMaxStackSize(stack)); 130 | if (!hasSpaceLeft) { 131 | continue; 132 | } 133 | 134 | menu.clicked(nonEmptySlot.index, 0, ClickType.PICKUP, player); 135 | ItemStack mouseItem = menu.getCarried(); 136 | if (mouseItem.isEmpty()) { 137 | return true; 138 | } 139 | } 140 | } 141 | 142 | for (Iterator iterator = emptySlots.iterator(); iterator.hasNext(); ) { 143 | Slot emptySlot = iterator.next(); 144 | menu.clicked(emptySlot.index, 0, ClickType.PICKUP, player); 145 | if (emptySlot.hasItem()) { 146 | nonEmptySlots.add(emptySlot); 147 | iterator.remove(); 148 | } 149 | 150 | ItemStack mouseItem = menu.getCarried(); 151 | if (mouseItem.isEmpty()) { 152 | return true; 153 | } 154 | } 155 | 156 | ItemStack mouseItem = menu.getCarried(); 157 | if (!mouseItem.isEmpty()) { 158 | menu.clicked(slot.index, 0, ClickType.PICKUP, player); 159 | } 160 | 161 | return false; 162 | } 163 | 164 | @Override 165 | public Type type() { 166 | return TYPE; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/ClientInventorySorting.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.blay09.mods.inventoryessentials.InventoryUtils; 4 | import net.minecraft.client.Minecraft; 5 | import net.minecraft.tags.ItemTags; 6 | import net.minecraft.world.entity.player.Inventory; 7 | import net.minecraft.world.inventory.AbstractContainerMenu; 8 | import net.minecraft.world.inventory.ClickType; 9 | import net.minecraft.world.inventory.ShulkerBoxSlot; 10 | import net.minecraft.world.inventory.Slot; 11 | import net.minecraft.world.item.ItemStack; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | public class ClientInventorySorting { 19 | 20 | private static final Comparator defaultComparator = 21 | Comparator.comparing((ItemStack itemStack) -> itemStack.getHoverName().getString(), String.CASE_INSENSITIVE_ORDER) 22 | .thenComparing(Comparator.comparingInt(ItemStack::getCount).reversed()) 23 | .thenComparing(itemStack -> itemStack.isEnchanted() ? 0 : 1) 24 | .thenComparingInt(ItemStack::getDamageValue) 25 | .thenComparing(itemStack -> Objects.toString(itemStack.getComponents(), "")); 26 | 27 | @FunctionalInterface 28 | public interface SlotClicker { 29 | void click(AbstractContainerMenu menu, Slot slot, int mouseButton, ClickType clickType); 30 | } 31 | 32 | public static boolean sort(AbstractContainerMenu menu, Slot baseSlot, SlotClicker clicker) { 33 | final var player = Minecraft.getInstance().player; 34 | if (player == null) { 35 | return false; 36 | } 37 | 38 | final var slotsToSort = new ArrayList(); 39 | for (final var slot : menu.slots) { 40 | if (isSortableSlot(slot) && InventoryUtils.isSameInventory(baseSlot, slot, true)) { 41 | slotsToSort.add(slot); 42 | } 43 | } 44 | 45 | if (slotsToSort.isEmpty()) { 46 | return false; 47 | } 48 | 49 | // Merge matching stacks first before sorting 50 | consolidateStacks(menu, slotsToSort, clicker); 51 | 52 | // Compute the sorted order 53 | final var goalSorting = slotsToSort.stream() 54 | .map(Slot::getItem) 55 | .map(ItemStack::copy) 56 | .filter(stack -> !stack.isEmpty()) 57 | .sorted(defaultComparator) 58 | .toList(); 59 | 60 | // Swap items to match the new sorting 61 | for (int i = 0; i < goalSorting.size(); i++) { 62 | final var goalStack = goalSorting.get(i); 63 | final var currentStack = slotsToSort.get(i).getItem(); 64 | // If current stack already matches goal stack, skip it 65 | if (ItemStack.isSameItemSameComponents(goalStack, currentStack) 66 | && goalStack.getCount() == currentStack.getCount()) { 67 | continue; 68 | } 69 | 70 | // Find the first stack from here that matches the goal stack 71 | int foundSwapIndex = -1; 72 | for (int j = i + 1; j < slotsToSort.size(); j++) { 73 | final var candidateStack = slotsToSort.get(j).getItem(); 74 | if (ItemStack.isSameItemSameComponents(goalStack, candidateStack) 75 | && goalStack.getCount() == candidateStack.getCount()) { 76 | foundSwapIndex = j; 77 | break; 78 | } 79 | } 80 | 81 | // If we found a slot matching the goal stack, swap the two slots 82 | if (foundSwapIndex != -1) { 83 | swapSlots(menu, slotsToSort, i, foundSwapIndex, clicker); 84 | } 85 | } 86 | 87 | return true; 88 | } 89 | 90 | private static void swapSlots(AbstractContainerMenu menu, List slots, int firstIndex, int secondIndex, SlotClicker clicker) { 91 | if (firstIndex != secondIndex) { 92 | final var firstSlot = slots.get(firstIndex); 93 | final var secondSlot = slots.get(secondIndex); 94 | final var firstStack = firstSlot.getItem(); 95 | final var secondStack = secondSlot.getItem(); 96 | 97 | // If one of the two slots is empty, we just have to do a simple move 98 | if (!firstSlot.hasItem() || !secondSlot.hasItem()) { 99 | final var fromSlot = firstSlot.hasItem() ? firstSlot : secondSlot; 100 | final var toSlot = firstSlot.hasItem() ? secondSlot : firstSlot; 101 | clicker.click(menu, fromSlot, 0, ClickType.PICKUP); 102 | clicker.click(menu, toSlot, 0, ClickType.PICKUP); 103 | return; 104 | } 105 | 106 | // We can't swap with a bundle normally because clicking it would insert the item - try another way 107 | if (firstStack.is(ItemTags.BUNDLES) || secondStack.is(ItemTags.BUNDLES)) { 108 | Slot emptyBufferSlot = null; 109 | for (final var candidate : slots) { 110 | if (!candidate.hasItem()) { 111 | emptyBufferSlot = candidate; 112 | break; 113 | } 114 | } 115 | 116 | // If we found an empty slot to use as a buffer, use it to swap the two slots; otherwise just leave the bundle untouched 117 | if (emptyBufferSlot != null) { 118 | clicker.click(menu, firstSlot, 0, ClickType.PICKUP); 119 | clicker.click(menu, emptyBufferSlot, 0, ClickType.PICKUP); 120 | clicker.click(menu, secondSlot, 0, ClickType.PICKUP); 121 | clicker.click(menu, firstSlot, 0, ClickType.PICKUP); 122 | clicker.click(menu, emptyBufferSlot, 0, ClickType.PICKUP); 123 | clicker.click(menu, secondSlot, 0, ClickType.PICKUP); 124 | } 125 | } else { 126 | clicker.click(menu, firstSlot, 0, ClickType.PICKUP); 127 | clicker.click(menu, secondSlot, 0, ClickType.PICKUP); 128 | clicker.click(menu, firstSlot, 0, ClickType.PICKUP); 129 | } 130 | } 131 | } 132 | 133 | private static void consolidateStacks(AbstractContainerMenu menu, List slots, SlotClicker clicker) { 134 | for (int i = 0; i < slots.size(); i++) { 135 | final var thisSlot = slots.get(i); 136 | if (!thisSlot.hasItem()) { 137 | continue; 138 | } 139 | 140 | final var thisStack = thisSlot.getItem(); 141 | for (int j = i + 1; j < slots.size(); j++) { 142 | final var otherSlot = slots.get(j); 143 | final var otherStack = otherSlot.getItem(); 144 | 145 | // We ignore bundles because clicking them would insert the item into the bundle 146 | if (thisStack.is(ItemTags.BUNDLES) || otherStack.is(ItemTags.BUNDLES)) { 147 | continue; 148 | } 149 | 150 | if (!otherStack.isEmpty() && ItemStack.isSameItemSameComponents(thisStack, otherStack)) { 151 | clicker.click(menu, otherSlot, 0, ClickType.PICKUP); 152 | clicker.click(menu, thisSlot, 0, ClickType.PICKUP); 153 | if (!menu.getCarried().isEmpty()) { 154 | clicker.click(menu, otherSlot, 0, ClickType.PICKUP); 155 | } 156 | 157 | final var newThisStack = thisSlot.getItem(); 158 | final int newThisStackFull = newThisStack.getMaxStackSize(); 159 | if (newThisStack.getCount() >= newThisStackFull) { 160 | break; 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | private static boolean isSortableSlot(Slot slot) { 168 | // Hotbar and armor slots are never sortable 169 | if (slot.container instanceof Inventory) { 170 | final var containerSlot = slot.getContainerSlot(); 171 | if (containerSlot < Inventory.SELECTION_SIZE || containerSlot >= Inventory.INVENTORY_SIZE) { 172 | return false; 173 | } 174 | } 175 | 176 | // We only sort the most standard slots you would find in your inventory or chests 177 | return slot.getClass() == Slot.class 178 | || slot.getClass() == ShulkerBoxSlot.class; 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/network/BulkTransferSingleMessage.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.network; 2 | 3 | import net.blay09.mods.inventoryessentials.InventoryEssentials; 4 | import net.blay09.mods.inventoryessentials.InventoryEssentialsConfig; 5 | import net.blay09.mods.inventoryessentials.InventoryUtils; 6 | import net.blay09.mods.inventoryessentials.ServerInventoryTransfers; 7 | import net.minecraft.core.component.DataComponents; 8 | import net.minecraft.network.FriendlyByteBuf; 9 | import net.minecraft.network.RegistryFriendlyByteBuf; 10 | import net.minecraft.network.codec.ByteBufCodecs; 11 | import net.minecraft.network.codec.StreamCodec; 12 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 13 | import net.minecraft.resources.Identifier; 14 | import net.minecraft.server.level.ServerPlayer; 15 | import net.minecraft.world.entity.EquipmentSlot; 16 | import net.minecraft.world.entity.player.Inventory; 17 | import net.minecraft.world.entity.player.Player; 18 | import net.minecraft.world.inventory.AbstractContainerMenu; 19 | import net.minecraft.world.inventory.ClickType; 20 | import net.minecraft.world.inventory.InventoryMenu; 21 | import net.minecraft.world.inventory.Slot; 22 | import net.minecraft.world.item.ItemStack; 23 | 24 | import java.util.*; 25 | 26 | public record BulkTransferSingleMessage(int slotNumber) implements CustomPacketPayload { 27 | 28 | public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(InventoryEssentials.MOD_ID, 29 | "bulk_transfer_single")); 30 | 31 | public static final StreamCodec STREAM_CODEC = StreamCodec.composite( 32 | ByteBufCodecs.INT, 33 | BulkTransferSingleMessage::slotNumber, 34 | BulkTransferSingleMessage::new 35 | ); 36 | 37 | public static BulkTransferSingleMessage decode(FriendlyByteBuf buf) { 38 | int slotNumber = buf.readByte(); 39 | return new BulkTransferSingleMessage(slotNumber); 40 | } 41 | 42 | public static void encode(FriendlyByteBuf buf, BulkTransferSingleMessage message) { 43 | buf.writeByte(message.slotNumber); 44 | } 45 | 46 | public static void handle(ServerPlayer player, BulkTransferSingleMessage message) { 47 | AbstractContainerMenu menu = player.containerMenu; 48 | if (menu != null && message.slotNumber >= 0 && message.slotNumber < menu.slots.size()) { 49 | Slot clickedSlot = menu.slots.get(message.slotNumber); 50 | 51 | boolean isProbablyMovingToPlayerInventory = false; 52 | // If the clicked slot is *not* from the player inventory, 53 | if (!(clickedSlot.container instanceof Inventory)) { 54 | // Search for any slot that belongs to the player inventory area (not hotbar) 55 | isProbablyMovingToPlayerInventory = InventoryUtils.containerContainsPlayerInventory(menu); 56 | } 57 | 58 | final var clickedEquippable = clickedSlot.getItem().get(DataComponents.EQUIPPABLE); 59 | boolean clickedAnArmorItem = clickedEquippable != null && clickedEquippable.slot().isArmor(); 60 | boolean isInsideInventory = menu instanceof InventoryMenu; 61 | 62 | if (isProbablyMovingToPlayerInventory) { 63 | // To avoid O(n²), find empty and non-empty slots beforehand in one loop iteration 64 | Deque emptySlots = new ArrayDeque<>(); 65 | List nonEmptySlots = new ArrayList<>(); 66 | for (Slot slot : menu.slots) { 67 | if (InventoryUtils.isSameInventory(slot, clickedSlot) || !(slot.container instanceof Inventory)) { 68 | continue; 69 | } 70 | 71 | if (slot.hasItem()) { 72 | nonEmptySlots.add(slot); 73 | } else if (!Inventory.isHotbarSlot(slot.getContainerSlot())) { 74 | emptySlots.add(slot); 75 | } 76 | } 77 | 78 | // Now go through each slot that is accessible and belongs to the same inventory as the clicked slot 79 | for (Slot slot : menu.slots) { 80 | if (!slot.mayPickup(player)) { 81 | continue; 82 | } 83 | 84 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 85 | // and bulk-transfer each of them using the prefer-inventory behaviour 86 | quickTransferSingle(player, menu, emptySlots, nonEmptySlots, slot); 87 | } 88 | } 89 | } else if (clickedAnArmorItem && isInsideInventory) { 90 | if (!InventoryEssentialsConfig.getActive().bulkTransferArmorSets) { 91 | return; 92 | } 93 | 94 | // When clicking an equipped armor, un-equip all 95 | if (clickedSlot.index >= InventoryMenu.ARMOR_SLOT_START && clickedSlot.index < InventoryMenu.ARMOR_SLOT_END) { 96 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 97 | menu.clicked(i, 0, ClickType.QUICK_MOVE, player); 98 | } 99 | return; 100 | } 101 | 102 | // Swap current armor with clicked armor set 103 | final var armorSlots = InventoryUtils.findMatchingArmorSetSlots(menu, clickedSlot); 104 | final var equipmentSlots = List.of(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET); 105 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 106 | final var equipmentSlot = equipmentSlots.get(i - InventoryMenu.ARMOR_SLOT_START); 107 | final var swapSlot = armorSlots.get(equipmentSlot); 108 | if (swapSlot != null) { 109 | menu.clicked(i, 0, ClickType.PICKUP, player); 110 | menu.clicked(swapSlot.index, 0, ClickType.PICKUP, player); 111 | menu.clicked(i, 0, ClickType.PICKUP, player); 112 | } 113 | } 114 | } else { 115 | // Just a normal inventory-to-inventory transfer, simply shift-click the items 116 | for (Slot slot : menu.slots) { 117 | if (!slot.mayPickup(player)) { 118 | continue; 119 | } 120 | 121 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 122 | ServerInventoryTransfers.singleTransfer(player, menu, slot); 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | private static boolean quickTransferSingle(Player player, AbstractContainerMenu menu, Deque emptySlots, List nonEmptySlots, Slot slot) { 130 | final var targetStack = slot.getItem().copy(); 131 | if (targetStack.isEmpty()) { 132 | return false; 133 | } 134 | 135 | menu.clicked(slot.index, 0, ClickType.PICKUP, player); 136 | 137 | for (final var nonEmptySlot : nonEmptySlots) { 138 | final var stack = nonEmptySlot.getItem(); 139 | if (ItemStack.isSameItemSameComponents(targetStack, stack)) { 140 | boolean hasSpaceLeft = stack.getCount() < Math.min(nonEmptySlot.getMaxStackSize(), nonEmptySlot.getMaxStackSize(stack)); 141 | if (!hasSpaceLeft) { 142 | continue; 143 | } 144 | 145 | menu.clicked(nonEmptySlot.index, 1, ClickType.PICKUP, player); 146 | ItemStack mouseItem = menu.getCarried(); 147 | if (mouseItem.getCount() < targetStack.getCount()) { 148 | menu.clicked(slot.index, 0, ClickType.PICKUP, player); 149 | return true; 150 | } 151 | } 152 | } 153 | 154 | for (Iterator iterator = emptySlots.iterator(); iterator.hasNext(); ) { 155 | Slot emptySlot = iterator.next(); 156 | menu.clicked(emptySlot.index, 1, ClickType.PICKUP, player); 157 | if (emptySlot.hasItem()) { 158 | nonEmptySlots.add(emptySlot); 159 | iterator.remove(); 160 | } 161 | 162 | ItemStack mouseItem = menu.getCarried(); 163 | if (mouseItem.getCount() < targetStack.getCount()) { 164 | menu.clicked(slot.index, 0, ClickType.PICKUP, player); 165 | return true; 166 | } 167 | } 168 | 169 | ItemStack mouseItem = menu.getCarried(); 170 | if (!mouseItem.isEmpty()) { 171 | menu.clicked(slot.index, 0, ClickType.PICKUP, player); 172 | } 173 | 174 | return false; 175 | } 176 | 177 | @Override 178 | public Type type() { 179 | return TYPE; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: publish-release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | forge: 6 | description: 'Forge' 7 | required: true 8 | type: boolean 9 | default: true 10 | fabric: 11 | description: 'Fabric' 12 | required: true 13 | type: boolean 14 | default: true 15 | neoforge: 16 | description: 'NeoForge' 17 | required: true 18 | type: boolean 19 | default: true 20 | maven: 21 | description: 'Maven' 22 | required: true 23 | type: boolean 24 | default: true 25 | modrinth: 26 | description: 'Modrinth' 27 | required: true 28 | type: boolean 29 | default: true 30 | curseforge: 31 | description: 'CurseForge' 32 | required: true 33 | type: boolean 34 | default: true 35 | 36 | jobs: 37 | create-release: 38 | runs-on: ubuntu-latest 39 | outputs: 40 | ref: v${{ steps.bump-version.outputs.version }} 41 | version: ${{ steps.bump-version.outputs.version }} 42 | build-matrix: ${{ steps.set-build-matrix.outputs.result }} 43 | publish-matrix: ${{ steps.set-publish-matrix.outputs.result }} 44 | steps: 45 | - name: Checkout repository 46 | uses: actions/checkout@v4 47 | - name: Extracting version from properties 48 | shell: bash 49 | run: echo "version=$(cat gradle.properties | grep -w "\bversion\s*=" | cut -d= -f2)" >> $GITHUB_OUTPUT 50 | id: extract-version 51 | - name: Bumping version 52 | uses: TwelveIterationMods/bump-version@v1 53 | with: 54 | version: ${{ steps.extract-version.outputs.version }} 55 | bump: patch 56 | id: bump-version 57 | - name: Updating version properties 58 | run: | 59 | sed -i "s/^\s*version\s*=.*/version = ${{ steps.bump-version.outputs.version }}/g" gradle.properties 60 | git config user.name "GitHub Actions" 61 | git config user.email "<>" 62 | git commit -am "Set version to ${{ steps.bump-version.outputs.version }}" 63 | git push origin ${BRANCH_NAME} 64 | git tag -a "v${{ steps.bump-version.outputs.version }}" -m "Release ${{ steps.bump-version.outputs.version }}" 65 | git push origin "v${{ steps.bump-version.outputs.version }}" 66 | shell: bash 67 | env: 68 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 69 | - name: Preparing build matrix 70 | id: set-build-matrix 71 | uses: actions/github-script@v7 72 | with: 73 | script: | 74 | const fs = require('fs'); 75 | const settingsGradle = fs.readFileSync('settings.gradle', 'utf8'); 76 | const includePattern = /^(?!\s*\/\/)\s*include\s*\(\s*(['"]([^'"]+)['"](?:,\s*['"]([^'"]+)['"])*\s*)\)/gm; 77 | const includes = [...settingsGradle.matchAll(includePattern)].flatMap(match => match[0].match(/['"]([^'"]+)['"]/g).map(item => item.replace(/['"]/g, ''))); 78 | const includeFabric = includes.includes('fabric') && ${{inputs.fabric}}; 79 | const includeForge = includes.includes('forge') && ${{inputs.forge}}; 80 | const includeNeoForge = includes.includes('neoforge') && ${{inputs.neoforge}}; 81 | return { 82 | loader: [includeFabric ? 'fabric' : false, includeForge ? 'forge' : false, includeNeoForge ? 'neoforge' : false].filter(Boolean), 83 | } 84 | - name: Preparing publish matrix 85 | id: set-publish-matrix 86 | uses: actions/github-script@v7 87 | with: 88 | script: | 89 | const fs = require('fs'); 90 | const settingsGradle = fs.readFileSync('settings.gradle', 'utf8'); 91 | const includePattern = /^(?!\s*\/\/)\s*include\s*\(\s*(['"]([^'"]+)['"](?:,\s*['"]([^'"]+)['"])*\s*)\)/gm; 92 | const includes = [...settingsGradle.matchAll(includePattern)].flatMap(match => match[0].match(/['"]([^'"]+)['"]/g).map(item => item.replace(/['"]/g, ''))); 93 | const includeFabric = includes.includes('fabric') && ${{inputs.fabric}}; 94 | const includeForge = includes.includes('forge') && ${{inputs.forge}}; 95 | const includeNeoForge = includes.includes('neoforge') && ${{inputs.neoforge}}; 96 | const gradleProperties = fs.readFileSync('gradle.properties', 'utf8'); 97 | const curseForgeProjectId = gradleProperties.match(/^(?!#)curseforge_project_id\s*=\s*(.+)/m); 98 | const modrinthProjectId = gradleProperties.match(/^(?!#)modrinth_project_id\s*=\s*(.+)/m); 99 | const mavenReleases = gradleProperties.match(/^(?!#)maven_releases\s*=\s*(.+)/m); 100 | const publishCurseForge = curseForgeProjectId && ${{inputs.curseforge}}; 101 | const publishModrinth = modrinthProjectId && ${{inputs.modrinth}}; 102 | const publishMaven = mavenReleases && ${{inputs.maven}}; 103 | return { 104 | loader: ['common', includeFabric ? 'fabric' : false, includeForge ? 'forge' : false, includeNeoForge ? 'neoforge' : false].filter(Boolean), 105 | site: [publishCurseForge ? 'curseforge' : false, publishModrinth ? 'modrinth' : false, publishMaven ? 'publish' : false].filter(Boolean), 106 | exclude: [ 107 | {loader: 'common', site: 'curseforge'}, 108 | {loader: 'common', site: 'modrinth'} 109 | ] 110 | } 111 | build-common: 112 | runs-on: ubuntu-latest 113 | steps: 114 | - name: Checkout repository 115 | uses: actions/checkout@v4 116 | with: 117 | ref: ${{ needs.create-release.outputs.ref }} 118 | - name: Validate gradle wrapper 119 | uses: gradle/actions/wrapper-validation@v5 120 | - name: Setup JDK 121 | uses: actions/setup-java@v4 122 | with: 123 | java-version: 21 124 | distribution: temurin 125 | cache: 'gradle' 126 | - name: Make gradle wrapper executable 127 | run: chmod +x ./gradlew 128 | - name: Build common artifact 129 | run: ./gradlew :common:build '-Pversion=${{needs.create-release.outputs.version}}' 130 | - name: Upload common artifact 131 | uses: actions/upload-artifact@v4 132 | with: 133 | name: common-artifact 134 | path: common/build 135 | needs: create-release 136 | build-release: 137 | runs-on: ubuntu-latest 138 | strategy: 139 | matrix: ${{fromJson(needs.create-release.outputs.build-matrix)}} 140 | fail-fast: false 141 | steps: 142 | - name: Checkout repository 143 | uses: actions/checkout@v4 144 | with: 145 | ref: ${{ needs.create-release.outputs.ref }} 146 | - name: Validate gradle wrapper 147 | uses: gradle/actions/wrapper-validation@v5 148 | - name: Setup JDK 149 | uses: actions/setup-java@v4 150 | with: 151 | java-version: 21 152 | distribution: temurin 153 | cache: 'gradle' 154 | - name: Make gradle wrapper executable 155 | run: chmod +x ./gradlew 156 | - name: Download common artifact 157 | uses: actions/download-artifact@v4 158 | with: 159 | name: common-artifact 160 | path: common/build 161 | - name: Build ${{ matrix.loader }} artifact 162 | run: ./gradlew :${{ matrix.loader }}:build '-Pversion=${{needs.create-release.outputs.version}}' 163 | - name: Upload ${{ matrix.loader }} artifact 164 | uses: actions/upload-artifact@v4 165 | with: 166 | name: ${{ matrix.loader }}-artifact 167 | path: ${{ matrix.loader }}/build 168 | needs: 169 | - create-release 170 | - build-common 171 | publish-release: 172 | runs-on: ubuntu-latest 173 | strategy: 174 | matrix: ${{fromJson(needs.create-release.outputs.publish-matrix)}} 175 | fail-fast: false 176 | steps: 177 | - name: Checkout repository 178 | uses: actions/checkout@v4 179 | with: 180 | ref: ${{ needs.create-release.outputs.ref }} 181 | - name: Download ${{ matrix.loader }} artifact 182 | uses: actions/download-artifact@v4 183 | with: 184 | name: ${{ matrix.loader }}-artifact 185 | path: ${{ matrix.loader }}/build 186 | - name: Validate gradle wrapper 187 | uses: gradle/actions/wrapper-validation@v5 188 | - name: Setup JDK 189 | uses: actions/setup-java@v4 190 | with: 191 | java-version: 21 192 | distribution: temurin 193 | cache: 'gradle' 194 | - name: Make gradle wrapper executable 195 | run: chmod +x ./gradlew 196 | - name: Publish 197 | run: ./gradlew :${{ matrix.loader }}:${{ matrix.site }} '-Pversion=${{needs.create-release.outputs.version}}' '-PmavenUsername=${{ secrets.MAVEN_USER }}' '-PmavenPassword=${{ secrets.MAVEN_PASSWORD }}' 198 | env: 199 | CURSEFORGE_TOKEN: ${{secrets.CURSEFORGE_TOKEN}} 200 | MODRINTH_TOKEN: ${{secrets.MODRINTH_TOKEN}} 201 | needs: 202 | - create-release 203 | - build-common 204 | - build-release -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 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 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /common/src/main/java/net/blay09/mods/inventoryessentials/client/ClientOnlyInventoryControls.java: -------------------------------------------------------------------------------- 1 | package net.blay09.mods.inventoryessentials.client; 2 | 3 | import net.blay09.mods.inventoryessentials.InventoryEssentialsConfig; 4 | import net.blay09.mods.inventoryessentials.InventoryUtils; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 7 | import net.minecraft.client.multiplayer.MultiPlayerGameMode; 8 | import net.minecraft.core.component.DataComponents; 9 | import net.minecraft.world.entity.EquipmentSlot; 10 | import net.minecraft.world.entity.player.Inventory; 11 | import net.minecraft.world.entity.player.Player; 12 | import net.minecraft.world.inventory.AbstractContainerMenu; 13 | import net.minecraft.world.inventory.ClickType; 14 | import net.minecraft.world.inventory.InventoryMenu; 15 | import net.minecraft.world.inventory.Slot; 16 | import net.minecraft.world.item.ItemStack; 17 | 18 | import java.util.*; 19 | 20 | public class ClientOnlyInventoryControls implements InventoryControls { 21 | 22 | @Override 23 | public boolean singleTransfer(AbstractContainerScreen screen, Slot clickedSlot) { 24 | AbstractContainerMenu menu = screen.getMenu(); 25 | Player player = Minecraft.getInstance().player; 26 | if (player == null) { 27 | return false; 28 | } 29 | 30 | if (!clickedSlot.mayPickup(player)) { 31 | return false; 32 | } 33 | 34 | ItemStack targetStack = clickedSlot.getItem().copy(); 35 | // If clicked stack only has a count of one to begin with, just do a normal shift-click on it 36 | if (targetStack.getCount() == 1) { 37 | slotClick(menu, clickedSlot, 0, ClickType.QUICK_MOVE); 38 | return true; 39 | } 40 | 41 | Slot fallbackSlot = null; 42 | 43 | // Go through all slots in the container 44 | for (Slot slot : menu.slots) { 45 | ItemStack stack = slot.getItem(); 46 | // Skip the clicked slot, skip slots that do not accept the clicked item, skip slots that are of the same inventory (since we're moving between inventories), and skip slots that are already full 47 | if (!isValidTargetSlot(slot) || slot == clickedSlot || !slot.mayPlace(targetStack) || InventoryUtils.isSameInventory(clickedSlot, slot) 48 | || stack.getCount() >= Math.min(slot.getMaxStackSize(), slot.getMaxStackSize(stack))) { 49 | continue; 50 | } 51 | 52 | // Prefer inputting into an existing stack if the items match 53 | if (ItemStack.isSameItemSameComponents(targetStack, stack)) { 54 | slotClick(menu, clickedSlot, 1, ClickType.PICKUP); 55 | slotClick(menu, slot, 1, ClickType.PICKUP); 56 | slotClick(menu, clickedSlot, 0, ClickType.PICKUP); 57 | return true; 58 | } else if (!slot.hasItem() && fallbackSlot == null) { 59 | // Remember the first empty slot and move the item there later in case we didn't find an existing stack 60 | fallbackSlot = slot; 61 | } 62 | } 63 | 64 | // There was no existing stack, so move the item into the first empty slot we found 65 | if (fallbackSlot != null) { 66 | slotClick(menu, clickedSlot, 1, ClickType.PICKUP); 67 | slotClick(menu, fallbackSlot, 1, ClickType.PICKUP); 68 | slotClick(menu, clickedSlot, 0, ClickType.PICKUP); 69 | return true; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | @Override 76 | public boolean bulkTransferByType(AbstractContainerScreen screen, Slot clickedSlot) { 77 | ItemStack clickedStackCopy = clickedSlot.getItem().copy(); 78 | clickedStackCopy.setDamageValue(0); 79 | AbstractContainerMenu menu = screen.getMenu(); 80 | List transferSlots = new ArrayList<>(); 81 | transferSlots.add(clickedSlot); 82 | for (Slot slot : menu.slots) { 83 | if (slot == clickedSlot || !isValidTargetSlot(slot)) { 84 | continue; 85 | } 86 | 87 | if (InventoryUtils.isSameInventory(slot, clickedSlot)) { 88 | ItemStack slotStackCopy = slot.getItem().copy(); 89 | slotStackCopy.setDamageValue(0); 90 | if (ItemStack.isSameItemSameComponents(clickedStackCopy, slotStackCopy)) { 91 | transferSlots.add(slot); 92 | } 93 | } 94 | } 95 | 96 | for (Slot transferSlot : transferSlots) { 97 | slotClick(menu, transferSlot, 0, ClickType.QUICK_MOVE); 98 | } 99 | 100 | return true; 101 | } 102 | 103 | @Override 104 | public boolean bulkTransferSingle(AbstractContainerScreen screen, Slot clickedSlot) { 105 | if (!clickedSlot.hasItem() && !InventoryEssentialsConfig.getActive().allowBulkTransferAllOnEmptySlot) { 106 | return false; 107 | } 108 | 109 | Player player = Minecraft.getInstance().player; 110 | if (player == null) { 111 | return false; 112 | } 113 | 114 | AbstractContainerMenu menu = screen.getMenu(); 115 | 116 | boolean isProbablyMovingToPlayerInventory = false; 117 | // If the clicked slot is *not* from the player inventory, 118 | if (!(clickedSlot.container instanceof Inventory)) { 119 | // Search for any slot that belongs to the player inventory area (not hotbar) 120 | isProbablyMovingToPlayerInventory = InventoryUtils.containerContainsPlayerInventory(menu); 121 | } 122 | 123 | final var clickedEquippable = clickedSlot.getItem().get(DataComponents.EQUIPPABLE); 124 | boolean clickedAnArmorItem = clickedEquippable != null && clickedEquippable.slot().isArmor(); 125 | boolean isInsideInventory = menu instanceof InventoryMenu; 126 | 127 | boolean movedAny = false; 128 | 129 | // If we're probably transferring to the player inventory, use transfer-to-inventory behaviour instead of just shift-clicking the items 130 | if (isProbablyMovingToPlayerInventory) { 131 | // To avoid O(n²), find empty and non-empty slots beforehand in one loop iteration 132 | Deque emptySlots = new ArrayDeque<>(); 133 | List nonEmptySlots = new ArrayList<>(); 134 | for (Slot slot : menu.slots) { 135 | if (InventoryUtils.isSameInventory(slot, clickedSlot) || !(slot.container instanceof Inventory) || !isValidTargetSlot(slot)) { 136 | continue; 137 | } 138 | 139 | if (slot.hasItem()) { 140 | nonEmptySlots.add(slot); 141 | } else if (!Inventory.isHotbarSlot(slot.getContainerSlot())) { 142 | emptySlots.add(slot); 143 | } 144 | } 145 | 146 | // Now go through each slot that is accessible and belongs to the same inventory as the clicked slot 147 | for (Slot slot : menu.slots) { 148 | if (!slot.mayPickup(player)) { 149 | continue; 150 | } 151 | 152 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 153 | // and bulk-transfer each of them using the prefer-inventory behaviour 154 | if (quickTransferSingle(menu, emptySlots, nonEmptySlots, slot)) { 155 | movedAny = true; 156 | } 157 | } 158 | } 159 | } else if (clickedAnArmorItem && isInsideInventory) { 160 | if (!InventoryEssentialsConfig.getActive().bulkTransferArmorSets) { 161 | return false; 162 | } 163 | 164 | // If holding an item in hand already, do nothing 165 | if (!menu.getCarried().isEmpty()) { 166 | return false; 167 | } 168 | 169 | // When clicking an equipped armor, un-equip all 170 | if (clickedSlot.index >= InventoryMenu.ARMOR_SLOT_START && clickedSlot.index < InventoryMenu.ARMOR_SLOT_END) { 171 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 172 | slotClick(menu, i, 0, ClickType.QUICK_MOVE); 173 | } 174 | return true; 175 | } 176 | 177 | // Swap current armor with clicked armor set 178 | final var armorSlots = InventoryUtils.findMatchingArmorSetSlots(menu, clickedSlot); 179 | final var equipmentSlots = List.of(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET); 180 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 181 | final var equipmentSlot = equipmentSlots.get(i - InventoryMenu.ARMOR_SLOT_START); 182 | final var swapSlot = armorSlots.get(equipmentSlot); 183 | if (swapSlot != null) { 184 | slotClick(menu, i, 0, ClickType.PICKUP); 185 | slotClick(menu, swapSlot, 0, ClickType.PICKUP); 186 | slotClick(menu, i, 0, ClickType.PICKUP); 187 | } 188 | } 189 | 190 | movedAny = true; 191 | } else { 192 | // Just a normal inventory-to-inventory transfer, simply shift-click the items 193 | for (Slot slot : menu.slots) { 194 | if (!slot.mayPickup(player) || !isValidTargetSlot(slot)) { 195 | continue; 196 | } 197 | 198 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 199 | singleTransfer(screen, slot); 200 | movedAny = true; 201 | } 202 | } 203 | 204 | } 205 | 206 | return movedAny; 207 | } 208 | 209 | @Override 210 | public boolean bulkTransferAll(AbstractContainerScreen screen, Slot clickedSlot) { 211 | if (!clickedSlot.hasItem() && !InventoryEssentialsConfig.getActive().allowBulkTransferAllOnEmptySlot) { 212 | return false; 213 | } 214 | 215 | final var player = Minecraft.getInstance().player; 216 | if (player == null) { 217 | return false; 218 | } 219 | 220 | final var menu = screen.getMenu(); 221 | 222 | boolean isProbablyMovingToPlayerInventory = false; 223 | // If the clicked slot is *not* from the player inventory, 224 | if (!(clickedSlot.container instanceof Inventory)) { 225 | // Search for any slot that belongs to the player inventory area (not hotbar) 226 | isProbablyMovingToPlayerInventory = InventoryUtils.containerContainsPlayerInventory(menu); 227 | } 228 | 229 | final var clickedEquippable = clickedSlot.getItem().get(DataComponents.EQUIPPABLE); 230 | boolean clickedAnArmorItem = clickedEquippable != null && clickedEquippable.slot().isArmor(); 231 | boolean isInsideInventory = menu instanceof InventoryMenu; 232 | 233 | boolean movedAny = false; 234 | 235 | // If we're probably transferring to the player inventory, use transfer-to-inventory behaviour instead of just shift-clicking the items 236 | if (isProbablyMovingToPlayerInventory) { 237 | // To avoid O(n²), find empty and non-empty slots beforehand in one loop iteration 238 | Deque emptySlots = new ArrayDeque<>(); 239 | List nonEmptySlots = new ArrayList<>(); 240 | for (Slot slot : menu.slots) { 241 | if (InventoryUtils.isSameInventory(slot, clickedSlot) || !(slot.container instanceof Inventory) || !isValidTargetSlot(slot)) { 242 | continue; 243 | } 244 | 245 | if (slot.hasItem()) { 246 | nonEmptySlots.add(slot); 247 | } else if (!Inventory.isHotbarSlot(slot.getContainerSlot())) { 248 | emptySlots.add(slot); 249 | } 250 | } 251 | 252 | // Now go through each slot that is accessible and belongs to the same inventory as the clicked slot 253 | for (Slot slot : menu.slots) { 254 | if (!slot.mayPickup(player)) { 255 | continue; 256 | } 257 | 258 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 259 | // and bulk-transfer each of them using the prefer-inventory behaviour 260 | if (quickTransferStack(menu, emptySlots, nonEmptySlots, slot)) { 261 | movedAny = true; 262 | } 263 | } 264 | } 265 | } else if (clickedAnArmorItem && isInsideInventory) { 266 | if (!InventoryEssentialsConfig.getActive().bulkTransferArmorSets) { 267 | return false; 268 | } 269 | 270 | // If holding an item in hand already, do nothing 271 | if (!menu.getCarried().isEmpty()) { 272 | return false; 273 | } 274 | 275 | // When clicking an equipped armor, un-equip all 276 | if (clickedSlot.index >= InventoryMenu.ARMOR_SLOT_START && clickedSlot.index < InventoryMenu.ARMOR_SLOT_END) { 277 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 278 | slotClick(menu, i, 0, ClickType.QUICK_MOVE); 279 | } 280 | return true; 281 | } 282 | 283 | // Swap current armor with clicked armor set 284 | final var armorSlots = InventoryUtils.findMatchingArmorSetSlots(menu, clickedSlot); 285 | final var equipmentSlots = List.of(EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET); 286 | for (int i = InventoryMenu.ARMOR_SLOT_START; i < InventoryMenu.ARMOR_SLOT_END; i++) { 287 | final var equipmentSlot = equipmentSlots.get(i - InventoryMenu.ARMOR_SLOT_START); 288 | final var swapSlot = armorSlots.get(equipmentSlot); 289 | if (swapSlot != null) { 290 | slotClick(menu, i, 0, ClickType.PICKUP); 291 | slotClick(menu, swapSlot, 0, ClickType.PICKUP); 292 | slotClick(menu, i, 0, ClickType.PICKUP); 293 | } 294 | } 295 | 296 | movedAny = true; 297 | } else { 298 | // Just a normal inventory-to-inventory transfer, simply shift-click the items 299 | for (Slot slot : menu.slots) { 300 | if (!slot.mayPickup(player) || !isValidTargetSlot(slot)) { 301 | continue; 302 | } 303 | 304 | if (InventoryUtils.isSameInventory(slot, clickedSlot, true)) { 305 | slotClick(menu, slot, 0, ClickType.QUICK_MOVE); 306 | movedAny = true; 307 | } 308 | } 309 | 310 | } 311 | 312 | return movedAny; 313 | } 314 | 315 | private boolean quickTransferStack(AbstractContainerMenu menu, Deque emptySlots, List nonEmptySlots, Slot slot) { 316 | ItemStack targetStack = slot.getItem().copy(); 317 | if (targetStack.isEmpty()) { 318 | return false; 319 | } 320 | 321 | slotClick(menu, slot, 0, ClickType.PICKUP); 322 | 323 | for (Slot nonEmptySlot : nonEmptySlots) { 324 | ItemStack stack = nonEmptySlot.getItem(); 325 | if (ItemStack.isSameItemSameComponents(targetStack, stack)) { 326 | boolean hasSpaceLeft = stack.getCount() < Math.min(nonEmptySlot.getMaxStackSize(), nonEmptySlot.getMaxStackSize(stack)); 327 | if (!hasSpaceLeft) { 328 | continue; 329 | } 330 | 331 | slotClick(menu, nonEmptySlot, 0, ClickType.PICKUP); 332 | ItemStack mouseItem = menu.getCarried(); 333 | if (mouseItem.isEmpty()) { 334 | return true; 335 | } 336 | } 337 | } 338 | 339 | for (Iterator iterator = emptySlots.iterator(); iterator.hasNext(); ) { 340 | Slot emptySlot = iterator.next(); 341 | slotClick(menu, emptySlot, 0, ClickType.PICKUP); 342 | if (emptySlot.hasItem()) { 343 | nonEmptySlots.add(emptySlot); 344 | iterator.remove(); 345 | } 346 | 347 | ItemStack mouseItem = menu.getCarried(); 348 | if (mouseItem.isEmpty()) { 349 | return true; 350 | } 351 | } 352 | 353 | ItemStack mouseItem = menu.getCarried(); 354 | if (!mouseItem.isEmpty()) { 355 | slotClick(menu, slot, 0, ClickType.PICKUP); 356 | } 357 | 358 | return false; 359 | } 360 | 361 | private boolean quickTransferSingle(AbstractContainerMenu menu, Deque emptySlots, List nonEmptySlots, Slot slot) { 362 | ItemStack targetStack = slot.getItem().copy(); 363 | if (targetStack.isEmpty()) { 364 | return false; 365 | } 366 | 367 | slotClick(menu, slot, 0, ClickType.PICKUP); 368 | 369 | for (Slot nonEmptySlot : nonEmptySlots) { 370 | ItemStack stack = nonEmptySlot.getItem(); 371 | if (ItemStack.isSameItemSameComponents(targetStack, stack)) { 372 | boolean hasSpaceLeft = stack.getCount() < Math.min(nonEmptySlot.getMaxStackSize(), nonEmptySlot.getMaxStackSize(stack)); 373 | if (!hasSpaceLeft) { 374 | continue; 375 | } 376 | 377 | slotClick(menu, nonEmptySlot, 1, ClickType.PICKUP); 378 | ItemStack mouseItem = menu.getCarried(); 379 | if (mouseItem.getCount() < targetStack.getCount()) { 380 | slotClick(menu, slot, 0, ClickType.PICKUP); 381 | return true; 382 | } 383 | } 384 | } 385 | 386 | for (Iterator iterator = emptySlots.iterator(); iterator.hasNext(); ) { 387 | Slot emptySlot = iterator.next(); 388 | slotClick(menu, emptySlot, 1, ClickType.PICKUP); 389 | if (emptySlot.hasItem()) { 390 | nonEmptySlots.add(emptySlot); 391 | iterator.remove(); 392 | } 393 | 394 | ItemStack mouseItem = menu.getCarried(); 395 | if (mouseItem.getCount() < targetStack.getCount()) { 396 | slotClick(menu, slot, 0, ClickType.PICKUP); 397 | return true; 398 | } 399 | } 400 | 401 | ItemStack mouseItem = menu.getCarried(); 402 | if (!mouseItem.isEmpty()) { 403 | slotClick(menu, slot, 0, ClickType.PICKUP); 404 | } 405 | 406 | return false; 407 | } 408 | 409 | @Override 410 | public void dragTransfer(AbstractContainerScreen screen, Slot clickedSlot) { 411 | slotClick(screen.getMenu(), clickedSlot, 0, ClickType.QUICK_MOVE); 412 | } 413 | 414 | @Override 415 | public void dragClick(AbstractContainerScreen screen, Slot hoveredSlot, int mouseButton) { 416 | slotClick(screen.getMenu(), hoveredSlot, mouseButton, ClickType.PICKUP); 417 | } 418 | 419 | @Override 420 | public boolean sort(AbstractContainerScreen screen, Slot baseSlot) { 421 | final var menu = screen.getMenu(); 422 | return ClientInventorySorting.sort(menu, baseSlot, this::slotClick); 423 | } 424 | 425 | 426 | 427 | protected void slotClick(AbstractContainerMenu menu, Slot slot, int mouseButton, ClickType clickType) { 428 | slotClick(menu, slot.index, mouseButton, clickType); 429 | } 430 | 431 | protected void slotClick(AbstractContainerMenu menu, int slotIndex, int mouseButton, ClickType clickType) { 432 | Player player = Minecraft.getInstance().player; 433 | MultiPlayerGameMode gameMode = Minecraft.getInstance().gameMode; 434 | if (player != null && gameMode != null && (slotIndex >= 0 && slotIndex < menu.slots.size() || slotIndex == -999)) { 435 | gameMode.handleInventoryMouseClick(menu.containerId, slotIndex, mouseButton, clickType, player); 436 | } 437 | } 438 | 439 | @Override 440 | public boolean dropByType(AbstractContainerScreen screen, Slot hoverSlot) { 441 | ItemStack targetStack = hoverSlot.getItem().copy(); 442 | AbstractContainerMenu menu = screen.getMenu(); 443 | List transferSlots = new ArrayList<>(); 444 | transferSlots.add(hoverSlot); 445 | for (Slot slot : menu.slots) { 446 | if (slot == hoverSlot || !isValidTargetSlot(slot)) { 447 | continue; 448 | } 449 | 450 | if (InventoryUtils.isSameInventory(slot, hoverSlot)) { 451 | ItemStack stack = slot.getItem(); 452 | if (ItemStack.isSameItemSameComponents(targetStack, stack)) { 453 | transferSlots.add(slot); 454 | } 455 | } 456 | } 457 | 458 | for (Slot transferSlot : transferSlots) { 459 | slotClick(menu, transferSlot, 1, ClickType.THROW); 460 | } 461 | 462 | return true; 463 | } 464 | 465 | @Override 466 | public boolean dropByType(AbstractContainerScreen screen, ItemStack targetStack) { 467 | if (targetStack.isEmpty()) { 468 | return false; 469 | } 470 | 471 | AbstractContainerMenu menu = screen.getMenu(); 472 | List transferSlots = new ArrayList<>(); 473 | for (Slot slot : menu.slots) { 474 | ItemStack stack = slot.getItem(); 475 | if (ItemStack.isSameItemSameComponents(targetStack, stack) && isValidTargetSlot(slot)) { 476 | transferSlots.add(slot); 477 | } 478 | } 479 | 480 | slotClick(menu, -999, 0, ClickType.PICKUP); 481 | for (Slot transferSlot : transferSlots) { 482 | slotClick(menu, transferSlot, 1, ClickType.THROW); 483 | } 484 | 485 | return true; 486 | } 487 | 488 | protected boolean isValidTargetSlot(Slot slot) { 489 | return true; 490 | } 491 | } 492 | --------------------------------------------------------------------------------