├── LICENSE ├── gradlew ├── .gitattributes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ ├── assets │ │ └── benefit │ │ │ ├── LeftyDupes.png │ │ │ └── lang │ │ │ └── en_us.json │ ├── benefit.mixins.json │ └── fabric.mod.json │ └── java │ └── org │ └── benefit │ ├── Variables.java │ ├── mixin │ ├── SplashTextResourceSupplierMixin.java │ ├── screen │ │ ├── VideoOptionsScreenMixin.java │ │ ├── ChatScreenMixin.java │ │ ├── BookScreenMixin.java │ │ ├── AbstractSignEditScreenMixin.java │ │ └── HandledScreenMixin.java │ ├── ClientConnectionMixin.java │ └── sodium │ │ └── SodiumGameOptionPagesMixin.java │ ├── LayoutMode.java │ ├── LayoutPos.java │ ├── Config.java │ └── Benefit.java ├── settings.gradle ├── .gitignore ├── gradle.properties ├── .github └── workflows │ ├── gradle_build.yml │ └── gradle-publish.yml ├── README.md └── gradlew.bat /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Lefty Dupes 2023 2 | 3 | All rights reserved. 4 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bruh1273/Lefty-Benefit-Latest/HEAD/gradlew -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bruh1273/Lefty-Benefit-Latest/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/assets/benefit/LeftyDupes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bruh1273/Lefty-Benefit-Latest/HEAD/src/main/resources/assets/benefit/LeftyDupes.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle Tweaks 2 | org.gradle.jvmargs = -Xmx2G 3 | 4 | # Project Properties 5 | minecraft_version = 1.21.1 6 | yarn_mappings = 1.21.1+build.3 7 | loader_version = 0.16.2 8 | 9 | mod_version = 1.3.7-1.21.1 10 | maven_group = org.benefit 11 | archives_base_name = benefit 12 | 13 | # Dependencies 14 | fabric_version = 0.102.1+1.21.1 15 | sodium_version = mc1.21-0.5.11 -------------------------------------------------------------------------------- /src/main/resources/benefit.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "org.benefit.mixin", 5 | "compatibilityLevel": "JAVA_21", 6 | "client": [ 7 | "ClientConnectionMixin", 8 | "SplashTextResourceSupplierMixin", 9 | "screen.AbstractSignEditScreenMixin", 10 | "screen.BookScreenMixin", 11 | "screen.ChatScreenMixin", 12 | "screen.HandledScreenMixin", 13 | "screen.VideoOptionsScreenMixin", 14 | "sodium.SodiumGameOptionPagesMixin" 15 | ], 16 | "injectors": { 17 | "defaultRequire": 1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/benefit/Variables.java: -------------------------------------------------------------------------------- 1 | package org.benefit; 2 | 3 | import net.minecraft.client.gui.screen.Screen; 4 | import net.minecraft.network.packet.Packet; 5 | import net.minecraft.screen.ScreenHandler; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * Variables used across the mod 12 | */ 13 | public class Variables { 14 | public static boolean delayUIPackets = false; 15 | public static final List> delayedPackets = new ArrayList<>(); 16 | public static Screen storedScreen = null; 17 | public static ScreenHandler storedScreenHandler = null; 18 | public static String lastCommand = ""; 19 | } -------------------------------------------------------------------------------- /src/main/resources/assets/benefit/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "benefit.key.restoreScreen": "Restore Screen", 3 | "benefit.format": "Benefit Layout", 4 | "benefit.format.default": "Default", 5 | "benefit.format.topright": "Top Right", 6 | "benefit.format.bottomleft": "Bottom Left", 7 | "benefit.format.bottomright": "Bottom Right", 8 | "benefit.format.disabled": "Disabled", 9 | "benefit.format.tooltip": "Flip through this option to change the layout of Lefty's Benefit's GUI buttons.", 10 | "benefit.overlay": "Slot Overlay", 11 | "benefit.overlay.tooltip": "Disabling this option will remove the slot overlay from your screen. Enabling it again will bring the overlay back.", 12 | "benefit.json": "Copy Json Name", 13 | "benefit.json.tooltip": "With this enabled, pressing \"Get Name\" will copy the container's JSON opposed to it's default text." 14 | } -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "leftybenefit", 4 | "version": "${version}", 5 | "name": "Lefty's Benefit", 6 | "description": "Super useful addons for duping/hunting.", 7 | "authors": [ 8 | "Lefty Dupes" 9 | ], 10 | "contact": { 11 | "discord": "https://discord.gg/lefty", 12 | "sources": "https://github.com/bruh1273/Lefty-Benefit-Latest", 13 | "issues": "https://github.com/bruh1273/Lefty-Benefit-Latest/issues" 14 | }, 15 | "license": "ARR", 16 | "icon": "assets/benefit/LeftyDupes.png", 17 | "environment": "client", 18 | "entrypoints": { 19 | "client": [ 20 | "org.benefit.Benefit" 21 | ] 22 | }, 23 | "mixins": [ 24 | "benefit.mixins.json" 25 | ], 26 | "depends": { 27 | "fabricloader": ">=0.16.0", 28 | "minecraft": "1.21.1", 29 | "java": ">=21" 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/SplashTextResourceSupplierMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin; 2 | 3 | import net.minecraft.client.resource.SplashTextResourceSupplier; 4 | import net.minecraft.resource.ResourceManager; 5 | import net.minecraft.util.profiler.Profiler; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 10 | 11 | import java.util.List; 12 | 13 | @Mixin(SplashTextResourceSupplier.class) 14 | public abstract class SplashTextResourceSupplierMixin { 15 | @Inject(at = @At(value = "INVOKE", target = "Ljava/util/List;addAll(Ljava/util/Collection;)Z"), method = "apply(Ljava/util/List;Lnet/minecraft/resource/ResourceManager;Lnet/minecraft/util/profiler/Profiler;)V") 16 | private void injectEasterEgg(List list, ResourceManager resourceManager, Profiler profiler, CallbackInfo ci) { 17 | list.add("§aLefty Dupes Was Here"); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/screen/VideoOptionsScreenMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin.screen; 2 | 3 | import net.minecraft.client.gui.screen.option.VideoOptionsScreen; 4 | import net.minecraft.client.option.GameOptions; 5 | import net.minecraft.client.option.SimpleOption; 6 | import org.apache.commons.lang3.ArrayUtils; 7 | import org.benefit.Benefit; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 12 | 13 | @Mixin(value = VideoOptionsScreen.class, priority = 1337) 14 | public abstract class VideoOptionsScreenMixin { 15 | 16 | @Inject(at = @At("RETURN"), method = "getOptions", cancellable = true) 17 | private static void getOptions(GameOptions gameOptions, CallbackInfoReturnable[]> cir) { 18 | SimpleOption[] values = cir.getReturnValue(); 19 | values = ArrayUtils.insert(9, values, Benefit.format); 20 | values = ArrayUtils.insert(10, values, Benefit.overlay); 21 | cir.setReturnValue(ArrayUtils.insert(11, values, Benefit.copyJson)); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/ClientConnectionMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin; 2 | 3 | import net.minecraft.network.*; 4 | import net.minecraft.network.packet.*; 5 | import net.minecraft.network.packet.c2s.play.*; 6 | import org.spongepowered.asm.mixin.*; 7 | import org.spongepowered.asm.mixin.injection.*; 8 | import org.spongepowered.asm.mixin.injection.callback.*; 9 | import org.benefit.Variables; 10 | 11 | @Mixin(ClientConnection.class) 12 | public abstract class ClientConnectionMixin { 13 | @Inject(at = @At("HEAD"), method = "sendImmediately", cancellable = true) 14 | private void sendImmediately(Packet packet, PacketCallbacks callbacks, boolean flush, CallbackInfo ci) { 15 | // Store the delayed packets 16 | if (Variables.delayUIPackets && (packet instanceof ClickSlotC2SPacket 17 | || packet instanceof ButtonClickC2SPacket 18 | || packet instanceof CloseHandledScreenC2SPacket 19 | || packet instanceof UpdateSignC2SPacket 20 | || packet instanceof RenameItemC2SPacket)) { 21 | Variables.delayedPackets.add(packet); 22 | ci.cancel(); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/screen/ChatScreenMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin.screen; 2 | 3 | import net.minecraft.client.gui.DrawContext; 4 | import net.minecraft.client.gui.screen.ChatScreen; 5 | import net.minecraft.client.gui.screen.Screen; 6 | import net.minecraft.client.gui.widget.TextFieldWidget; 7 | import net.minecraft.text.Text; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | @Mixin(ChatScreen.class) 15 | public abstract class ChatScreenMixin extends Screen { 16 | 17 | @Shadow protected TextFieldWidget chatField; 18 | 19 | protected ChatScreenMixin(Text title) { 20 | super(title); 21 | } 22 | 23 | @Inject(at = @At("TAIL"), method = "render") 24 | private void drawChars(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { 25 | assert client != null; 26 | 27 | int length = chatField.getText().length(); 28 | // Display the amount of characters in the chat box. 29 | context.drawText(client.textRenderer, Integer.toString(length), 4, client.getWindow().getScaledHeight() - 36, 30 | length > 256 ? 0xFFC38C8C : 0xFF7F7F7F, 31 | false); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /.github/workflows/gradle_build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | # Use these Java versions 14 | java: [ 15 | 21, # Current Java LTS 16 | ] 17 | # and run on both Linux and Windows 18 | os: [ubuntu-22.04, windows-2022] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: checkout repository 22 | uses: actions/checkout@v4 23 | - name: validate gradle wrapper 24 | uses: gradle/wrapper-validation-action@v1 25 | - name: setup jdk ${{ matrix.java }} 26 | uses: actions/setup-java@v4 27 | with: 28 | java-version: ${{ matrix.java }} 29 | distribution: 'microsoft' 30 | - name: make gradle wrapper executable 31 | if: ${{ runner.os != 'Windows' }} 32 | run: chmod +x ./gradlew 33 | - name: build 34 | run: ./gradlew build 35 | - name: capture build artifacts 36 | if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: Artifacts 40 | path: build/libs/ 41 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 6 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle 7 | 8 | name: Gradle Package 9 | 10 | on: 11 | release: 12 | types: [created] 13 | 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | packages: write 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up JDK 11 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '21' 28 | distribution: 'temurin' 29 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 30 | settings-path: ${{ github.workspace }} # location for the settings.xml file 31 | 32 | - name: Build with Gradle 33 | uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 34 | with: 35 | arguments: build 36 | 37 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in 38 | # the publishing section of your build.gradle 39 | - name: Publish to GitHub Packages 40 | uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0 41 | with: 42 | arguments: publish 43 | env: 44 | USERNAME: ${{ github.actor }} 45 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /src/main/java/org/benefit/LayoutMode.java: -------------------------------------------------------------------------------- 1 | package org.benefit; 2 | 3 | import net.minecraft.text.Text; 4 | import net.minecraft.util.TranslatableOption; 5 | import net.minecraft.util.function.ValueLists; 6 | 7 | import java.util.function.IntFunction; 8 | 9 | public enum LayoutMode implements TranslatableOption { 10 | TOP_LEFT(0, "benefit.format.default"), 11 | TOP_RIGHT(1, "benefit.format.topright"), 12 | BOTTOM_LEFT(2, "benefit.format.bottomleft"), 13 | BOTTOM_RIGHT(3, "benefit.format.bottomright"), 14 | NONE(4, "benefit.format.disabled"); 15 | 16 | private static final IntFunction BY_ID = ValueLists.createIdToValueFunction(LayoutMode::getId, values(), ValueLists.OutOfBoundsHandling.WRAP); 17 | private final int id; 18 | private final String translationKey; 19 | 20 | LayoutMode(int id, String translationKey) { 21 | this.id = id; 22 | this.translationKey = translationKey; 23 | } 24 | 25 | @Override 26 | public int getId() { 27 | return this.id; 28 | } 29 | 30 | @Override 31 | public String getTranslationKey() { 32 | return this.translationKey; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return switch(this) { 38 | case TOP_LEFT -> "Default"; 39 | case TOP_RIGHT -> "Top Right"; 40 | case BOTTOM_LEFT -> "Bottom Left"; 41 | case BOTTOM_RIGHT -> "Bottom Right"; 42 | case NONE -> "Disabled"; 43 | }; 44 | } 45 | 46 | public static final Text[] translationKeys = { 47 | Text.translatable(TOP_LEFT.getTranslationKey()), 48 | Text.translatable(TOP_RIGHT.getTranslationKey()), 49 | Text.translatable(BOTTOM_LEFT.getTranslationKey()), 50 | Text.translatable(BOTTOM_RIGHT.getTranslationKey()), 51 | Text.translatable(NONE.getTranslationKey()) 52 | }; 53 | 54 | public static LayoutMode byId(int id) { 55 | return BY_ID.apply(id); 56 | } 57 | 58 | 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lefty's Benefit. https://discord.gg/lefty 2 | --- 3 | - DISCLAIMER NOT MY ORIGINAL MOD. ORIGINAL MOD CREDIT GOES TO Ui Utils by Coderx_Gamer (https://github.com/Coderx-Gamer/ui-utils) and konoashi#9999 on discord 4 | - Shoutouts to Crosby.dev on Discord for some great improvements. 5 | - Here's how you use the mods, Fabric API is required for this mod to work. 6 | --- 7 | 8 | - "Soft Close" closes your current gui without sending a CloseHandledScreen Packet to the server, a TON of small dupes use this, because the gui cannot keep track of the correct one you're in. 9 | 10 | - "De-sync" closes your current menu server-side and keeps it open client-side, this is totally useless in practice. 11 | 12 | - "Send packets true/false" tells the client whether it should send any ClickSlot and ButtonClick Packets. (This means when send packets is active, nothing that you do in a GUI will ever be sent to the server.) 13 | 14 | - "Delay packets true/false" when turned on it will store all ClickSlot and ButtonClick Packets into the memory, and will not send them until you turn it back on. (This is effectively "blink" for clickslots, and it is used a TON for dupes. See here : https://www.youtube.com/watch?v=L_-jZ_dJi_w )(You will also notice that it will say in chat "You have sent 'x' delayed packets." this is simply to give you a better understanding of what is happening.) 15 | 16 | - "Save UI" saves your current gui to the memory, and can be restored be pressing 'V'. This is configurable in the Minecraft Keybinds options. The gui will only restore a real gui if you haven't closed it, or opened a new one. (It's main function is to allow you to do things, like place blocks down while still being in the GUI.) 17 | 18 | - "Leave & Send Packets" only works when Delay packets is turned on. What it does, is immediately send all delayed packets, and disconnect you from the server .(can create potential race conditions on non-vanilla servers). 19 | 20 | - "Sync Id" Is used by the game to keep track of which gui your player is currently in. 21 | 22 | - "Revision" is something the server uses to keep track of what / how many times you've clicked on something in a gui. 23 | 24 | - "GUI Chat" will let you send any chat message, or command while still existing in the gui. (I suggest copy pasting the command beforehand cause pressing E will close your gui rather then type it.) 25 | 26 | - "Get Name" will automatically print the UI's name in chat, and copy it to your clipboard. 27 | -------------------------------------------------------------------------------- /src/main/java/org/benefit/LayoutPos.java: -------------------------------------------------------------------------------- 1 | package org.benefit; 2 | 3 | import net.minecraft.client.util.Window; 4 | import net.minecraft.text.Text; 5 | 6 | public class LayoutPos { 7 | private static final Window win = Benefit.mc.getWindow(); 8 | public static int baseY() { 9 | return switch(Benefit.config.getLayoutMode()) { 10 | case TOP_LEFT, TOP_RIGHT -> 190; 11 | case BOTTOM_LEFT, BOTTOM_RIGHT -> win.getScaledHeight() + 6; 12 | case NONE -> 9999; 13 | }; 14 | } 15 | public static int signBaseY() { 16 | return switch(Benefit.config.getLayoutMode()) { 17 | case TOP_LEFT, TOP_RIGHT -> 54; 18 | case BOTTOM_LEFT, BOTTOM_RIGHT -> win.getScaledHeight() - 28; 19 | case NONE -> 9999; 20 | }; 21 | } 22 | public static int xValue(int width) { 23 | return switch(Benefit.config.getLayoutMode()) { 24 | case TOP_LEFT, BOTTOM_LEFT -> 4; 25 | case TOP_RIGHT, BOTTOM_RIGHT -> win.getScaledWidth() - width - 4; 26 | case NONE -> 9999; 27 | }; 28 | } 29 | public static int sendChatYPos() { 30 | return switch(Benefit.config.getLayoutMode()) { 31 | case TOP_LEFT, TOP_RIGHT -> 220; 32 | case BOTTOM_LEFT, BOTTOM_RIGHT -> win.getScaledHeight() - 204; 33 | case NONE -> 9999; 34 | }; 35 | } 36 | public static int getNameYPos() { 37 | return switch(Benefit.config.getLayoutMode()) { 38 | case TOP_LEFT, TOP_RIGHT -> 190; 39 | case BOTTOM_LEFT, BOTTOM_RIGHT -> win.getScaledHeight() - 174; 40 | case NONE -> 9999; 41 | }; 42 | } 43 | public static int windowIdY(boolean syncId) { 44 | return switch(Benefit.config.getLayoutMode()) { 45 | case TOP_LEFT, TOP_RIGHT -> syncId ? 5 : 20; 46 | case BOTTOM_LEFT, BOTTOM_RIGHT -> LayoutPos.baseY() - (syncId ? 79 : 89); 47 | case NONE -> 9999; 48 | }; 49 | } 50 | public static int windowIdX(Text text) { 51 | final int topRight = Benefit.mc.getWindow().getScaledWidth() - Benefit.mc.textRenderer.getWidth(text) - 4, 52 | bottomRight = (Benefit.mc.getWindow().getScaledWidth() - 82) - (Benefit.mc.textRenderer.getWidth(text) + 4); 53 | return switch(Benefit.config.getLayoutMode()) { 54 | case TOP_LEFT -> 4; 55 | case TOP_RIGHT -> topRight; 56 | case BOTTOM_LEFT -> 88; 57 | case BOTTOM_RIGHT -> bottomRight; 58 | case NONE -> 9999; 59 | }; 60 | } 61 | public static int signPosY() { 62 | return switch(Benefit.config.getLayoutMode()) { 63 | case TOP_LEFT, TOP_RIGHT -> 78; 64 | case BOTTOM_LEFT, BOTTOM_RIGHT -> LayoutPos.signBaseY() - 64; 65 | case NONE -> 9999; 66 | }; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/Config.java: -------------------------------------------------------------------------------- 1 | package org.benefit; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import com.google.gson.JsonParseException; 6 | import com.google.gson.JsonParser; 7 | 8 | import java.io.File; 9 | import java.io.FileReader; 10 | import java.io.FileWriter; 11 | import java.io.IOException; 12 | 13 | import static org.benefit.LayoutMode.TOP_LEFT; 14 | 15 | // Don't reference this class for json troubleshooting, it has zero robustness, and only accomplishes what absolutely has to be done. 16 | public class Config { 17 | private final String FILENAME = "config/Benefit.json"; 18 | private int theMode = TOP_LEFT.getId(); 19 | private boolean overlayValue = true; 20 | private boolean copyJson = false; 21 | 22 | public Config() { 23 | try { 24 | FileReader reader = new FileReader(FILENAME); 25 | JsonElement rootElement = JsonParser.parseReader(reader); 26 | if (!rootElement.isJsonObject()) throw new JsonParseException("Invalid Json Element Provided!"); 27 | JsonObject asObject = rootElement.getAsJsonObject(); 28 | theMode = asObject.get("Layout").getAsInt(); 29 | overlayValue = asObject.get("Slot Overlay").getAsBoolean(); 30 | copyJson = asObject.get("Json Name").getAsBoolean(); 31 | } catch (Exception e) { 32 | File file = new File(FILENAME); 33 | if (!file.exists()) createConfig(); 34 | } 35 | } 36 | 37 | private void createConfig() { 38 | try (FileWriter fileWriter = new FileWriter(FILENAME)) { 39 | fileWriter.write("{\"Layout\": 1, \"Slot Overlay\": true, \"Json Name\": false}"); 40 | } catch (IOException e) { 41 | //noinspection CallToPrintStackTrace 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | public void save() { 47 | try (FileWriter fileWriter = new FileWriter(FILENAME)) { 48 | fileWriter.write(String.format("{\"Layout\": %s, \"Slot Overlay\": %s, \"Json Name\": %s}", theMode, overlayValue, copyJson)); 49 | } catch (IOException e) { 50 | //noinspection CallToPrintStackTrace 51 | e.printStackTrace(); 52 | } 53 | } 54 | 55 | public LayoutMode getLayoutMode() { 56 | return LayoutMode.byId(this.theMode); 57 | } 58 | 59 | public void setLayout(LayoutMode mode) { 60 | this.theMode = mode.getId(); 61 | } 62 | 63 | public boolean getOverlayValue() { 64 | return this.overlayValue; 65 | } 66 | 67 | public void setOverlayValue(boolean overlayValue) { 68 | this.overlayValue = overlayValue; 69 | } 70 | 71 | public boolean shouldCopyJson() { 72 | return this.copyJson; 73 | } 74 | 75 | public void setCopyJson(boolean copyJson) { 76 | this.copyJson = copyJson; 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/sodium/SodiumGameOptionPagesMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin.sodium; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import me.jellysquid.mods.sodium.client.gui.SodiumGameOptionPages; 5 | import me.jellysquid.mods.sodium.client.gui.options.Option; 6 | import me.jellysquid.mods.sodium.client.gui.options.OptionGroup; 7 | import me.jellysquid.mods.sodium.client.gui.options.OptionImpl; 8 | import me.jellysquid.mods.sodium.client.gui.options.OptionPage; 9 | import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl; 10 | import me.jellysquid.mods.sodium.client.gui.options.control.TickBoxControl; 11 | import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; 12 | import net.minecraft.text.Text; 13 | import org.benefit.Benefit; 14 | import org.benefit.LayoutMode; 15 | import org.spongepowered.asm.mixin.Final; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.Shadow; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | @Mixin(value = SodiumGameOptionPages.class, remap = false) 26 | public abstract class SodiumGameOptionPagesMixin { 27 | 28 | @Shadow @Final private static MinecraftOptionsStorage vanillaOpts; 29 | 30 | @Inject(at = @At("TAIL"), method = "advanced", cancellable = true) 31 | private static void addBenefitLayout(CallbackInfoReturnable cir) { 32 | List groups = new ArrayList<>(cir.getReturnValue().getGroups()); 33 | List> options = new ArrayList<>(cir.getReturnValue().getOptions()); 34 | 35 | options.add(2, OptionImpl.createBuilder(Boolean.TYPE, vanillaOpts) 36 | .setName(Text.translatable("benefit.json")) 37 | .setTooltip(Text.translatable("benefit.json.tooltip")) 38 | .setBinding((option, value) -> Benefit.copyJson.setValue(value), option -> Benefit.copyJson.getValue()) 39 | .setControl(TickBoxControl::new) 40 | .build()); 41 | 42 | options.add(2, OptionImpl.createBuilder(LayoutMode.class, vanillaOpts) 43 | .setName(Text.translatable("benefit.format")) 44 | .setTooltip(Text.translatable("benefit.format.tooltip")) 45 | .setBinding((option, value) -> Benefit.format.setValue(value), option -> Benefit.format.getValue()) 46 | .setControl(control -> new CyclingControl<>(control, LayoutMode.class, LayoutMode.translationKeys)) 47 | .build()); 48 | 49 | options.add(2, OptionImpl.createBuilder(Boolean.TYPE, vanillaOpts) 50 | .setName(Text.translatable("benefit.overlay")) 51 | .setTooltip(Text.translatable("benefit.overlay.tooltip")) 52 | .setBinding((option, value) -> Benefit.overlay.setValue(value), option -> Benefit.overlay.getValue()) 53 | .setControl(TickBoxControl::new) 54 | .build()); 55 | 56 | OptionGroup.Builder builder = OptionGroup.createBuilder(); 57 | options.forEach(builder::add); 58 | groups.set(1, builder.build()); 59 | 60 | cir.setReturnValue(new OptionPage(Text.translatable("sodium.options.pages.advanced"), ImmutableList.copyOf(groups))); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/screen/BookScreenMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin.screen; 2 | 3 | import net.minecraft.client.gui.DrawContext; 4 | import net.minecraft.client.gui.screen.Screen; 5 | import net.minecraft.client.gui.screen.ingame.BookScreen; 6 | import net.minecraft.client.gui.widget.ButtonWidget; 7 | import net.minecraft.network.packet.Packet; 8 | import net.minecraft.text.Text; 9 | import net.minecraft.util.Formatting; 10 | import org.benefit.Benefit; 11 | import org.benefit.LayoutPos; 12 | import org.benefit.Variables; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | import java.util.Objects; 19 | 20 | import static org.benefit.Benefit.mc; 21 | import static org.benefit.Benefit.restoreScreenBind; 22 | 23 | 24 | @Mixin(BookScreen.class) 25 | public abstract class BookScreenMixin extends Screen { 26 | protected BookScreenMixin(Text title) { 27 | super(title); 28 | } 29 | 30 | @Inject(at = @At("TAIL"), method = "init") 31 | public void init(CallbackInfo ci) { 32 | assert mc.player != null; 33 | 34 | String boldGray = Formatting.BOLD.toString() + Formatting.GRAY, boldGreen = Formatting.BOLD.toString() + Formatting.GREEN; 35 | 36 | // Delay Packets 37 | addDrawableChild(ButtonWidget.builder(Text.of("Delay packets: " + Variables.delayUIPackets), button -> { 38 | Variables.delayUIPackets = !Variables.delayUIPackets; 39 | 40 | button.setMessage(Text.of("Delay packets: " + Variables.delayUIPackets)); 41 | 42 | if (!Variables.delayUIPackets && !Variables.delayedPackets.isEmpty()) { 43 | for (final Packet packet : Variables.delayedPackets) { 44 | mc.player.networkHandler.sendPacket(packet); 45 | } 46 | int delayedPacketsCount = Variables.delayedPackets.size(); 47 | mc.player.sendMessage(Text.of(boldGray + "Successfully sent " + boldGreen + delayedPacketsCount + Formatting.GRAY + " delayed packets.")); 48 | Variables.delayedPackets.clear(); 49 | } 50 | }).width(120).position(LayoutPos.xValue(120), LayoutPos.baseY() - 120).build()); 51 | 52 | // Soft Close 53 | addDrawableChild(ButtonWidget.builder(Text.of("Soft Close"), button -> mc.setScreen(null)) 54 | .width(80).position(LayoutPos.xValue(80), LayoutPos.baseY() - 150).build()); 55 | 56 | // Save UI 57 | addDrawableChild(ButtonWidget.builder(Text.of("Save UI"), button -> { 58 | Variables.storedScreen = mc.currentScreen; 59 | Variables.storedScreenHandler = mc.player.currentScreenHandler; 60 | mc.setScreen(null); 61 | mc.player.sendMessage(Text.literal("Screen§a successfully§r saved! Press §a" + restoreScreenBind.getString() + " §rto restore it!")); 62 | }).width(80).position(LayoutPos.xValue(80), LayoutPos.baseY() - 90).build()); 63 | 64 | // Leave & send packets 65 | addDrawableChild(ButtonWidget.builder(Text.of("Leave & send packets"), button -> { 66 | if (!Variables.delayedPackets.isEmpty()) { 67 | 68 | Variables.delayUIPackets = false; 69 | 70 | for(final Packet packet : Variables.delayedPackets) { 71 | mc.player.networkHandler.sendPacket(packet); 72 | } 73 | 74 | int delayedPacketsAmount = Variables.delayedPackets.size(); 75 | // Disconnect player 76 | Objects.requireNonNull(mc.getNetworkHandler()).getConnection().disconnect( 77 | Text.of(boldGray + "Disconnected, " + boldGreen + delayedPacketsAmount + boldGray + " packets successfully sent.")); 78 | Variables.delayedPackets.clear(); 79 | } 80 | }).width(140).position(LayoutPos.xValue(140), LayoutPos.baseY() - 60).build()); 81 | } 82 | 83 | @Inject(at = @At("RETURN"), method = "render") 84 | public void render(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { 85 | assert client != null; 86 | 87 | // Sync ID and Revision 88 | Benefit.renderTexts(context, client.textRenderer, client); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/screen/AbstractSignEditScreenMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin.screen; 2 | 3 | import net.minecraft.block.entity.SignBlockEntity; 4 | import net.minecraft.client.gui.DrawContext; 5 | import net.minecraft.client.gui.screen.Screen; 6 | import net.minecraft.client.gui.screen.ingame.AbstractSignEditScreen; 7 | import net.minecraft.client.gui.tooltip.Tooltip; 8 | import net.minecraft.client.gui.widget.ButtonWidget; 9 | import net.minecraft.network.packet.Packet; 10 | import net.minecraft.text.Text; 11 | import net.minecraft.util.math.BlockPos; 12 | import org.benefit.*; 13 | import org.spongepowered.asm.mixin.Final; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.Shadow; 16 | import org.spongepowered.asm.mixin.Unique; 17 | import org.spongepowered.asm.mixin.injection.At; 18 | import org.spongepowered.asm.mixin.injection.Inject; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 20 | 21 | @Mixin(AbstractSignEditScreen.class) 22 | public abstract class AbstractSignEditScreenMixin extends Screen { 23 | protected AbstractSignEditScreenMixin(Text title) { 24 | super(title); 25 | } 26 | 27 | @Shadow @Final private SignBlockEntity blockEntity; 28 | 29 | @Inject(at = @At("TAIL"), method = "init") 30 | private void init(CallbackInfo ci) { 31 | if(Benefit.config.getLayoutMode() == LayoutMode.NONE) return; 32 | drawButtons(); 33 | } 34 | 35 | @Unique 36 | private void drawButtons() { 37 | if(client == null || client.player == null || client.player.networkHandler == null) return; 38 | final int x = LayoutPos.xValue(80); 39 | // Delay Packets 40 | addDrawableChild(ButtonWidget.builder(Text.literal("Delay packets: " + Variables.delayUIPackets), button -> { 41 | Variables.delayUIPackets = !Variables.delayUIPackets; 42 | button.setMessage(Text.literal("Delay packets: " + Variables.delayUIPackets)); 43 | if (!Variables.delayUIPackets && !Variables.delayedPackets.isEmpty()) { 44 | for(final Packet packet : Variables.delayedPackets) { 45 | client.player.networkHandler.sendPacket(packet); 46 | } 47 | final String msg = String.format("§7Successfully sent §a%s §7delayed packets.", Variables.delayedPackets.size()); 48 | client.player.sendMessage(Text.literal(msg)); 49 | Variables.delayedPackets.clear(); 50 | } 51 | }).width(120).position(LayoutPos.xValue(120), LayoutPos.signBaseY()).build()); 52 | 53 | // Soft Close 54 | addDrawableChild(ButtonWidget.builder(Text.literal("Soft Close"), button -> client.setScreen(null)) 55 | .width(80).position(x, LayoutPos.signBaseY() - 50).build()); 56 | 57 | // Save UI 58 | addDrawableChild(ButtonWidget.builder(Text.literal("Save UI"), button -> { 59 | Variables.storedScreen = client.currentScreen; 60 | Variables.storedScreenHandler = client.player.currentScreenHandler; 61 | client.setScreen(null); 62 | client.player.sendMessage(Text.literal("Screen§a successfully§r saved! Press §a" + Benefit.restoreScreenBind.getString() + " §rto restore it!")); 63 | }).tooltip(Tooltip.of(Text.literal("Delay packets has to be enabled in order to Save UI without updating the sign."))) 64 | .width(80) 65 | .position(x, LayoutPos.signBaseY() - 26) 66 | .build()); 67 | } 68 | 69 | @Inject(at = @At("TAIL"), method = "render") 70 | private void render(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { 71 | if(Benefit.config.getLayoutMode() == LayoutMode.NONE) return; 72 | drawText(context); 73 | } 74 | 75 | @Unique 76 | private void drawText(DrawContext context) { 77 | // We can actually get the position of the sign we're in which is neat. 78 | final BlockPos pos = blockEntity.getPos() != null ? blockEntity.getPos() : BlockPos.ORIGIN; 79 | final Text signPos = Text.literal("Sign Pos: " + (pos.equals(BlockPos.ORIGIN) ? "INVALID" : pos.toShortString())); 80 | final int width = textRenderer.getWidth(signPos); 81 | context.drawText(textRenderer, signPos, 82 | LayoutPos.xValue(width), LayoutPos.signPosY(), 83 | -1, false); 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/Benefit.java: -------------------------------------------------------------------------------- 1 | package org.benefit; 2 | 3 | import com.mojang.logging.LogUtils; 4 | import com.mojang.serialization.Codec; 5 | import net.fabricmc.api.ClientModInitializer; 6 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; 7 | import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 8 | import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; 9 | import net.minecraft.client.MinecraftClient; 10 | import net.minecraft.client.font.TextRenderer; 11 | import net.minecraft.client.gui.*; 12 | import net.minecraft.client.option.KeyBinding; 13 | import net.minecraft.client.option.SimpleOption; 14 | import net.minecraft.client.util.InputUtil; 15 | import net.minecraft.screen.slot.Slot; 16 | import net.minecraft.text.Text; 17 | import org.lwjgl.glfw.GLFW; 18 | import org.slf4j.Logger; 19 | 20 | import java.util.Arrays; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.Stream; 23 | 24 | public class Benefit implements ClientModInitializer { 25 | public static final MinecraftClient mc = MinecraftClient.getInstance(); 26 | public static final Config config = new Config(); 27 | public static Text restoreScreenBind; 28 | public static int txtColor = 0xFF828282; 29 | private static final Logger LOGGER = LogUtils.getLogger(); 30 | 31 | @Override 32 | public void onInitializeClient() { 33 | // Console text on game close 34 | ClientLifecycleEvents.CLIENT_STOPPING.register(this::onShutdownClient); 35 | 36 | // Console text on game open 37 | LOGGER.info("\033[38;2;50;205;50mLefty Benefit Successfully Initialized!\033[0m"); 38 | 39 | // Register keybinding for restoring the saved screen 40 | KeyBinding restoreScreenKey = KeyBindingHelper.registerKeyBinding(new KeyBinding("benefit.key.restoreScreen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_V, "Benefit")); 41 | 42 | // When the keybind is pressed 43 | ClientTickEvents.END_CLIENT_TICK.register(client -> { 44 | assert client.player != null; 45 | restoreScreenBind = restoreScreenKey.getBoundKeyLocalizedText(); 46 | 47 | if (restoreScreenKey.wasPressed() && Variables.storedScreen != null && Variables.storedScreenHandler != null) { 48 | client.setScreen(Variables.storedScreen); 49 | client.player.currentScreenHandler = Variables.storedScreenHandler; 50 | } 51 | }); 52 | } 53 | 54 | public void onShutdownClient(MinecraftClient ignoredClient) { 55 | LOGGER.info("\033[38;2;50;205;50mShutting Down Lefty's Benefit...\033[0m"); 56 | LOGGER.info("\033[38;2;50;205;50mLefty's Benefit Successfully Shut Down!\033[0m"); 57 | } 58 | 59 | // Slot Overlay 60 | public static void addText(DrawContext context, TextRenderer textRenderer, MinecraftClient client, int x, int y) { 61 | if(client.player == null) return; 62 | for(final Slot slot : client.player.currentScreenHandler.slots) { 63 | final Text id = Text.literal(Integer.toString(slot.id)); 64 | context.drawText(textRenderer, id, 65 | slot.x + x + 16 / 2 - textRenderer.getWidth(id) / 2, 66 | slot.y + y + 16 / 2 - textRenderer.fontHeight / 2, 67 | txtColor, false); 68 | } 69 | } 70 | 71 | // Sync ID & Revision 72 | public static void renderTexts(DrawContext context, TextRenderer textRenderer, MinecraftClient client) { 73 | if(client.player == null) return; 74 | final Text syncId = Text.literal("Sync Id: " + client.player.currentScreenHandler.syncId), 75 | revision = Text.literal("Revision: " + client.player.currentScreenHandler.getRevision()); 76 | context.drawText(textRenderer, syncId, LayoutPos.windowIdX(syncId), LayoutPos.windowIdY(true), -1, false); 77 | context.drawText(textRenderer, revision, LayoutPos.windowIdX(revision), LayoutPos.windowIdY(false), -1, false); 78 | } 79 | 80 | // Layout Mode Option 81 | public static final SimpleOption format = new SimpleOption<>("benefit.format", SimpleOption.constantTooltip(Text.translatable("benefit.format.tooltip")), (optionText, value) -> 82 | Text.translatable(value.getTranslationKey()), 83 | new SimpleOption.AlternateValuesSupportingCyclingCallbacks<>( 84 | Arrays.asList(LayoutMode.values()), 85 | Stream.of(LayoutMode.values()).collect(Collectors.toList()), 86 | mc::isRunning, 87 | SimpleOption::setValue, 88 | Codec.INT.xmap(LayoutMode::byId, LayoutMode::getId)), 89 | config.getLayoutMode(), value -> { 90 | config.setLayout(value); 91 | config.save(); 92 | }); 93 | 94 | // Slot Overlay Option 95 | public static final SimpleOption overlay = SimpleOption.ofBoolean("benefit.overlay", SimpleOption.constantTooltip(Text.translatable("benefit.overlay.tooltip")), config.getOverlayValue(), value -> { 96 | config.setOverlayValue(value); 97 | config.save(); 98 | }); 99 | 100 | // Copy Json Option 101 | public static final SimpleOption copyJson = SimpleOption.ofBoolean("benefit.json", SimpleOption.constantTooltip(Text.translatable("benefit.json.tooltip")), config.shouldCopyJson(), value -> { 102 | config.setCopyJson(value); 103 | config.save(); 104 | }); 105 | 106 | 107 | } -------------------------------------------------------------------------------- /src/main/java/org/benefit/mixin/screen/HandledScreenMixin.java: -------------------------------------------------------------------------------- 1 | package org.benefit.mixin.screen; 2 | 3 | import com.google.gson.JsonParseException; 4 | import com.mojang.serialization.JsonOps; 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; 6 | import net.minecraft.client.gui.DrawContext; 7 | import net.minecraft.client.gui.screen.Screen; 8 | import net.minecraft.client.gui.screen.ingame.*; 9 | import net.minecraft.client.gui.widget.ButtonWidget; 10 | import net.minecraft.client.gui.widget.TextFieldWidget; 11 | import net.minecraft.item.ItemStack; 12 | import net.minecraft.item.Items; 13 | import net.minecraft.network.packet.Packet; 14 | import net.minecraft.network.packet.c2s.play.BookUpdateC2SPacket; 15 | import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket; 16 | import net.minecraft.screen.slot.SlotActionType; 17 | import net.minecraft.text.Text; 18 | import net.minecraft.text.TextCodecs; 19 | import net.minecraft.util.Formatting; 20 | import org.benefit.Benefit; 21 | import org.benefit.LayoutMode; 22 | import org.benefit.LayoutPos; 23 | import org.benefit.Variables; 24 | import org.lwjgl.glfw.GLFW; 25 | import org.spongepowered.asm.mixin.Mixin; 26 | import org.spongepowered.asm.mixin.Shadow; 27 | import org.spongepowered.asm.mixin.Unique; 28 | import org.spongepowered.asm.mixin.injection.At; 29 | import org.spongepowered.asm.mixin.injection.Inject; 30 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 31 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 32 | 33 | import java.util.List; 34 | import java.util.Objects; 35 | import java.util.Optional; 36 | 37 | import static org.benefit.Benefit.mc; 38 | import static org.benefit.Benefit.restoreScreenBind; 39 | 40 | 41 | @Mixin(HandledScreen.class) 42 | public abstract class HandledScreenMixin extends Screen { 43 | @Shadow protected int x, y; 44 | @Unique private TextFieldWidget textBox; 45 | 46 | protected HandledScreenMixin(Text title) { 47 | super(title); 48 | } 49 | 50 | // The main method 51 | @Inject(at = @At("TAIL"), method = "init") 52 | private void init(CallbackInfo ci) { 53 | assert mc.player != null; 54 | 55 | String boldGray = Formatting.BOLD.toString() + Formatting.GRAY, boldGreen = Formatting.BOLD.toString() + Formatting.GREEN; 56 | 57 | // Delay packets 58 | addDrawableChild(ButtonWidget.builder(Text.of("Delay packets: " + Variables.delayUIPackets), button -> { 59 | Variables.delayUIPackets = !Variables.delayUIPackets; 60 | 61 | button.setMessage(Text.of("Delay packets: " + Variables.delayUIPackets)); 62 | 63 | if (!Variables.delayUIPackets && !Variables.delayedPackets.isEmpty()) { 64 | for (final Packet packet : Variables.delayedPackets) { 65 | mc.player.networkHandler.sendPacket(packet); 66 | } 67 | int delayedPacketsCount = Variables.delayedPackets.size(); 68 | mc.player.sendMessage(Text.of(boldGray + "Successfully sent " + boldGreen + delayedPacketsCount + Formatting.GRAY + " delayed packets.")); 69 | Variables.delayedPackets.clear(); 70 | } 71 | }).width(120).position(LayoutPos.xValue(120), LayoutPos.baseY() - 120).build()); 72 | 73 | // Soft Close 74 | addDrawableChild(ButtonWidget.builder(Text.of("Soft Close"), button -> mc.setScreen(null)) 75 | .width(80).position(LayoutPos.xValue(80), LayoutPos.baseY() - 150).build()); 76 | 77 | // Save UI 78 | addDrawableChild(ButtonWidget.builder(Text.of("Save UI"), button -> { 79 | Variables.storedScreen = mc.currentScreen; 80 | Variables.storedScreenHandler = mc.player.currentScreenHandler; 81 | mc.setScreen(null); 82 | mc.player.sendMessage(Text.literal("Screen §asuccessfully §rsaved! Press §a" + restoreScreenBind.getString() + " §rto restore it!")); 83 | }).width(80).position(LayoutPos.xValue(80), LayoutPos.baseY() - 90).build()); 84 | 85 | // Leave & send packets 86 | addDrawableChild(ButtonWidget.builder(Text.of("Leave & send packets"), button -> { 87 | if (!Variables.delayedPackets.isEmpty()) { 88 | 89 | Variables.delayUIPackets = false; 90 | 91 | for(final Packet packet : Variables.delayedPackets) { 92 | mc.player.networkHandler.sendPacket(packet); 93 | } 94 | int delayedPacketsAmount = Variables.delayedPackets.size(); 95 | // Disconnect player 96 | Objects.requireNonNull(mc.getNetworkHandler()).getConnection().disconnect( 97 | Text.of(boldGray + "Disconnected, " + boldGreen + delayedPacketsAmount + boldGray + " packets successfully sent.")); 98 | Variables.delayedPackets.clear(); 99 | } 100 | }).width(140).position(LayoutPos.xValue(140), LayoutPos.baseY() - 60).build()); 101 | 102 | // Get Name 103 | if(inContainer()) addDrawableChild(ButtonWidget.builder(Text.of("Get Name"), button -> { 104 | boolean json = Benefit.config.shouldCopyJson(); 105 | System.out.println(json); 106 | // We can use codecs to convert anything into a DynamicOp, default ones are NBT & JSON. 107 | String dfuParsed = TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, title).getOrThrow(JsonParseException::new).toString(); 108 | // Dispatch container's name to player in chat 109 | mc.player.sendMessage(Text.literal(json ? "Container JSON: " : "Container Name: ").append(json ? Text.of(dfuParsed) : title)); 110 | // Automatically copy the title to clipboard when called 111 | mc.keyboard.setClipboard(json ? dfuParsed : title.getString()); 112 | }).dimensions(LayoutPos.xValue(80), LayoutPos.getNameYPos(), 80, 20).build()); 113 | 114 | // Paper Dupe (1.20.6 - 1.21.1) 115 | addDrawableChild(ButtonWidget.builder(Text.of("Paper Dupe"), button -> { 116 | if(!(mc.player.getInventory().getMainHandStack().getItem() == Items.WRITABLE_BOOK)) { 117 | mc.player.sendMessage(Text.of("Please hold a writable book!")); 118 | return; 119 | } 120 | for(int i = 9; i < 44; i++) { 121 | if(36 + mc.player.getInventory().selectedSlot == i) continue; 122 | mc.player.networkHandler.sendPacket(new ClickSlotC2SPacket( 123 | mc.player.currentScreenHandler.syncId, 124 | mc.player.currentScreenHandler.getRevision(), 125 | i, 126 | 1, 127 | SlotActionType.THROW, 128 | ItemStack.EMPTY, 129 | Int2ObjectMaps.emptyMap() 130 | )); 131 | } 132 | mc.player.networkHandler.sendPacket(new BookUpdateC2SPacket( 133 | mc.player.getInventory().selectedSlot, List.of("discord.gg/lefty"), Optional.of("Lefty Dupes On Fucking TOP discord.gg/lefty" 134 | ))); 135 | }).dimensions(LayoutPos.xValue(80), LayoutPos.baseY() - 30, 80, 20).build()); 136 | 137 | // Create input text box 138 | textBox = new TextFieldWidget(mc.textRenderer, LayoutPos.xValue(100), LayoutPos.sendChatYPos(), 100, 20, Text.of("Send Chat")); 139 | textBox.setText(Variables.lastCommand); 140 | textBox.setMaxLength(65535); 141 | } 142 | 143 | @Inject(at = @At("RETURN"), method = "render") 144 | private void render(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { 145 | assert client != null; 146 | 147 | // Slot Overlay 148 | if(Benefit.config.getOverlayValue()) { 149 | Benefit.addText(context, client.textRenderer, client, this.x, this.y); 150 | } 151 | 152 | // Sync ID and Revision 153 | Benefit.renderTexts(context, client.textRenderer, client); 154 | 155 | // Technical Handling 156 | if(inContainer()) textBox.render(context, mouseX, mouseY, delta); 157 | if(!textBox.isFocused() && textBox.getText().isBlank()) textBox.setSuggestion("Send Chat..."); 158 | if(textBox.isFocused()) textBox.setSuggestion(""); 159 | } 160 | 161 | @Override 162 | public boolean keyReleased(int keyCode, int scanCode, int modifiers) { 163 | // Release any alt key and the color of the slot overlay will go away. 164 | final int color = 0xFF828282; 165 | if(keyCode == GLFW.GLFW_KEY_LEFT_ALT || keyCode == GLFW.GLFW_KEY_RIGHT_ALT && Benefit.txtColor != color) { 166 | Benefit.txtColor = color; 167 | } 168 | return super.keyReleased(keyCode, scanCode, modifiers); 169 | } 170 | 171 | @Override 172 | public boolean charTyped(char chr, int keyCode) { 173 | return textBox.charTyped(chr, keyCode) || super.charTyped(chr, keyCode); 174 | } 175 | 176 | @Inject(at = @At("HEAD"), method = "keyPressed", cancellable = true) 177 | private void keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable cir) { 178 | assert client != null; 179 | if(keyCode == GLFW.GLFW_KEY_LEFT_ALT || keyCode == GLFW.GLFW_KEY_RIGHT_ALT && Benefit.txtColor != -1) { 180 | Benefit.txtColor = -1; 181 | } 182 | 183 | if (textBox.isFocused()) { 184 | if(keyCode == GLFW.GLFW_KEY_ENTER) sendChat(); 185 | if(client.options.inventoryKey.matchesKey(keyCode, scanCode)) cir.setReturnValue(false); 186 | textBox.keyPressed(keyCode, scanCode, modifiers); 187 | } 188 | } 189 | 190 | @Inject(at = @At("HEAD"), method = "mouseClicked") 191 | private void mouseClick(double mX, double mY, int b, CallbackInfoReturnable cir) { 192 | textBox.onClick(mX, mY); 193 | if(textBox.mouseClicked(mX, mY, b)) textBox.setFocused(true); 194 | if(!textBox.mouseClicked(mX, mY, b)) textBox.setFocused(false); 195 | } 196 | 197 | @Unique 198 | private void sendChat() { 199 | assert mc.player != null; 200 | if(Benefit.config.getLayoutMode() != LayoutMode.NONE) { 201 | // Send message 202 | String s = textBox.getText(); 203 | if (s.startsWith("/")) mc.player.networkHandler.sendChatCommand(s.substring(1)); 204 | else mc.player.networkHandler.sendChatMessage(s); 205 | 206 | // Reset state 207 | textBox.setText(""); 208 | Variables.lastCommand = ""; 209 | } 210 | } 211 | 212 | @Unique 213 | private boolean inContainer() { 214 | return mc.currentScreen instanceof Generic3x3ContainerScreen 215 | || mc.currentScreen instanceof GenericContainerScreen 216 | || mc.currentScreen instanceof ShulkerBoxScreen 217 | || mc.currentScreen instanceof HopperScreen; 218 | } 219 | 220 | 221 | } --------------------------------------------------------------------------------