From 083ddabb85d0acab1127f2c70765ef76ad0631fb Mon Sep 17 00:00:00 2001
From: Chris Xiong <chirs241097@gmail.com>
Date: Sun, 21 Apr 2024 11:30:03 -0400
Subject: 1.20.4 + AutoTrade.

---
 src/main/java/org/chrisoft/trashyaddon/Addon.java  |   4 +-
 .../trashyaddon/mixin/MerchantScreenAccessor.java  |  14 +
 .../chrisoft/trashyaddon/modules/AutoTrade.java    | 330 +++++++++++++++++++++
 3 files changed, 347 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/org/chrisoft/trashyaddon/mixin/MerchantScreenAccessor.java
 create mode 100644 src/main/java/org/chrisoft/trashyaddon/modules/AutoTrade.java

(limited to 'src/main/java/org')

diff --git a/src/main/java/org/chrisoft/trashyaddon/Addon.java b/src/main/java/org/chrisoft/trashyaddon/Addon.java
index a956b6c..eb197ff 100644
--- a/src/main/java/org/chrisoft/trashyaddon/Addon.java
+++ b/src/main/java/org/chrisoft/trashyaddon/Addon.java
@@ -1,9 +1,11 @@
 package org.chrisoft.trashyaddon;
 
 import org.chrisoft.trashyaddon.commands.*;
+import org.chrisoft.trashyaddon.modules.*;
 import com.mojang.logging.LogUtils;
 import meteordevelopment.meteorclient.addons.MeteorAddon;
 import meteordevelopment.meteorclient.commands.Commands;
+import meteordevelopment.meteorclient.systems.modules.Modules;
 import org.slf4j.Logger;
 
 public class Addon extends MeteorAddon {
@@ -16,7 +18,7 @@ public class Addon extends MeteorAddon {
         LOG.info("Initializing Meteor Trash Addons");
 
         // Modules
-        //Modules.get().add(new ModuleExample());
+        Modules.get().add(new AutoTrade());
 
         // Commands
         Commands.add(new EntityDataCommand());
diff --git a/src/main/java/org/chrisoft/trashyaddon/mixin/MerchantScreenAccessor.java b/src/main/java/org/chrisoft/trashyaddon/mixin/MerchantScreenAccessor.java
new file mode 100644
index 0000000..8050ad3
--- /dev/null
+++ b/src/main/java/org/chrisoft/trashyaddon/mixin/MerchantScreenAccessor.java
@@ -0,0 +1,14 @@
+package org.chrisoft.trashyaddon.mixin;
+
+import net.minecraft.client.gui.screen.ingame.MerchantScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin(MerchantScreen.class)
+public interface MerchantScreenAccessor {
+    @Accessor("selectedIndex")
+    public void setSelectedIndex(int selectedIndex);
+    @Invoker("syncRecipeIndex")
+    public void invokeSyncRecipeIndex();
+}
diff --git a/src/main/java/org/chrisoft/trashyaddon/modules/AutoTrade.java b/src/main/java/org/chrisoft/trashyaddon/modules/AutoTrade.java
new file mode 100644
index 0000000..5cfacdc
--- /dev/null
+++ b/src/main/java/org/chrisoft/trashyaddon/modules/AutoTrade.java
@@ -0,0 +1,330 @@
+package org.chrisoft.trashyaddon.modules;
+
+import meteordevelopment.meteorclient.events.game.OpenScreenEvent;
+import meteordevelopment.meteorclient.events.packets.PacketEvent;
+import meteordevelopment.meteorclient.events.world.TickEvent;
+import meteordevelopment.meteorclient.settings.*;
+import meteordevelopment.meteorclient.systems.modules.Categories;
+import meteordevelopment.meteorclient.systems.modules.Module;
+import meteordevelopment.meteorclient.utils.player.FindItemResult;
+import meteordevelopment.meteorclient.utils.player.InvUtils;
+import meteordevelopment.orbit.EventHandler;
+import net.minecraft.client.gui.screen.ingame.MerchantScreen;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket;
+import net.minecraft.network.packet.s2c.play.SetTradeOffersS2CPacket;
+import net.minecraft.village.TradeOffer;
+import net.minecraft.village.TradeOfferList;
+import org.chrisoft.trashyaddon.mixin.MerchantScreenAccessor;
+
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.List;
+import java.util.Optional;
+
+public class AutoTrade extends Module {
+    private final SettingGroup sgGeneral = settings.getDefaultGroup();
+    private static final List<Item> allSellItems = List.of(
+        Items.COAL,
+        Items.CHICKEN,
+        Items.PORKCHOP,
+        Items.RABBIT,
+        Items.MUTTON,
+        Items.BEEF,
+        Items.DRIED_KELP_BLOCK,
+        Items.SWEET_BERRIES,
+        Items.IRON_INGOT,
+        Items.DIAMOND,
+        Items.PAPER,
+        Items.GLASS_PANE,
+        Items.ROTTEN_FLESH,
+        Items.GOLD_INGOT,
+        Items.RABBIT_FOOT,
+        Items.SCUTE,
+        Items.GLASS_BOTTLE,
+        Items.NETHER_WART,
+        Items.WHEAT,
+        Items.POTATO,
+        Items.CARROT,
+        Items.BEETROOT,
+        Items.PUMPKIN,
+        Items.MELON,
+        Items.STRING,
+        Items.COD,
+        Items.SALMON,
+        Items.TROPICAL_FISH,
+        Items.PUFFERFISH,
+        Items.STICK,
+        Items.FLINT,
+        Items.FEATHER,
+        Items.TRIPWIRE_HOOK,
+        Items.LEATHER,
+        Items.RABBIT_HIDE,
+        Items.BOOK,
+        Items.INK_SAC,
+        Items.CLAY_BALL,
+        Items.STONE,
+        Items.GRANITE,
+        Items.ANDESITE,
+        Items.DIORITE,
+        Items.QUARTZ
+    );
+
+    private static final List<Item> allBuyItems = List.of(
+        Items.BELL,
+        Items.COOKED_PORKCHOP,
+        Items.COOKED_CHICKEN,
+        Items.MAP,
+        Items.ITEM_FRAME,
+        Items.REDSTONE,
+        Items.LAPIS_LAZULI,
+        Items.GLOWSTONE,
+        Items.ENDER_PEARL,
+        Items.EXPERIENCE_BOTTLE,
+        Items.BREAD,
+        Items.PUMPKIN_PIE,
+        Items.APPLE,
+        Items.COOKIE,
+        Items.GOLDEN_CARROT,
+        Items.GLISTERING_MELON_SLICE,
+        Items.CAMPFIRE,
+        Items.ARROW,
+        Items.BOOKSHELF,
+        Items.LANTERN,
+        Items.GLASS,
+        Items.CLOCK,
+        Items.COMPASS,
+        Items.NAME_TAG,
+        Items.BRICK,
+        Items.CHISELED_STONE_BRICKS,
+        Items.DRIPSTONE_BLOCK,
+        Items.POLISHED_ANDESITE,
+        Items.POLISHED_DIORITE,
+        Items.POLISHED_GRANITE,
+        Items.QUARTZ_PILLAR,
+        Items.QUARTZ_BLOCK,
+        Items.PAINTING
+    );
+
+    private final Setting<List<Item>> sellsSetting = sgGeneral.add(new ItemListSetting.Builder()
+        .name("sells")
+        .description("Items to automatically SELL TO villagers.")
+        .filter((item) -> allSellItems.contains(item))
+        .build()
+    );
+    private final Setting<List<Item>> buysSetting = sgGeneral.add(new ItemListSetting.Builder()
+        .name("buys")
+        .description("Items to automatically BUY FROM villagers.")
+        .filter((item) -> allBuyItems.contains(item))
+        .build()
+    );
+    private final Setting<Integer> interactionRate = sgGeneral.add(new IntSetting.Builder()
+        .name("Interaction Rate")
+        .description("Number of ticks between interactions.")
+        .min(0)
+        .max(10)
+        .build()
+    );
+    private final Setting<Boolean> autoClose = sgGeneral.add(new BoolSetting.Builder()
+        .name("Auto Close")
+        .description("Close trading screen after any successful trades.")
+        .build()
+    );
+    private final Setting<Boolean> logSummary = sgGeneral.add(new BoolSetting.Builder()
+        .name("Log Summary")
+        .description("Give a summary of what has been traded once it finishes trading.")
+        .build()
+    );
+
+    private TradeOfferList offers;
+    private MerchantScreen screen;
+    private int currentOffer;
+    private int ticksRemaining;
+    private int countBefore;
+    private int countAfter;
+
+    private enum WaitState {
+        None,
+        WaitingForOutput,
+        WaitingForInventoryUpdate,
+        WaitingForFinalize
+    };
+    private WaitState waitState;
+    private ArrayList<ItemStack> itemsSold;
+    private ArrayList<ItemStack> itemsBought;
+
+
+    public AutoTrade() {
+        super(Categories.World, "Auto Trade", "Help prevent getting arthritis from excessively trading with villagers.");
+        itemsSold = new ArrayList<>();
+        itemsBought = new ArrayList<>();
+        waitState = WaitState.None;
+    }
+
+    /*
+    @Override
+    public String getInfoString() {
+        return waitState.toString() + " " + ticksRemaining;
+    }
+     */
+
+    private boolean isOfferEligible(TradeOffer o) {
+        // I don't give a SHIT to trades that use the second slot
+        // well actually I do, but I don't need THAT many enchanted books...
+        if (!o.getSecondBuyItem().getItem().equals(Items.AIR))
+            return false;
+        if (o.isDisabled())
+            return false;
+        ItemStack mbuy = o.getAdjustedFirstBuyItem();
+        ItemStack msell = o.getSellItem();
+        List<Item> sells = this.sellsSetting.get();
+        List<Item> buys = this.buysSetting.get();
+        if (!sells.contains(mbuy.getItem()) && !buys.contains(msell.getItem()))
+            return false;
+        FindItemResult rs = InvUtils.find((stack) -> stack.getItem().equals(mbuy.getItem()) && stack.getCount() >= mbuy.getCount());
+        if (!rs.found())
+            return false;
+        return true;
+    }
+
+    private void selectTrade() {
+        while (currentOffer < offers.size() && !isOfferEligible(offers.get(currentOffer)))
+            ++currentOffer;
+        if (currentOffer >= offers.size()) {
+            ticksRemaining = interactionRate.get();
+            waitState = WaitState.WaitingForFinalize;
+            return;
+        }
+        waitState = WaitState.WaitingForOutput;
+        ticksRemaining = -0xdead;
+        TradeOffer o = offers.get(currentOffer);
+        Item tradedItem = o.getSellItem().getItem().equals(Items.EMERALD) ?
+            o.getAdjustedFirstBuyItem().getItem() : o.getSellItem().getItem();
+        countBefore = screen.getScreenHandler().slots.stream()
+            .map(slot -> slot.getStack())
+            .filter(s -> s.getItem().equals(tradedItem))
+            .map(s -> s.getCount())
+            .reduce(0, Integer::sum);
+        ((MerchantScreenAccessor)screen).setSelectedIndex(currentOffer);
+        ((MerchantScreenAccessor)screen).invokeSyncRecipeIndex();
+    }
+
+    private void finalizeCurrentTrade() {
+        TradeOffer o = offers.get(currentOffer);
+        Item tradedItem = o.getSellItem().getItem().equals(Items.EMERALD) ?
+            o.getAdjustedFirstBuyItem().getItem() : o.getSellItem().getItem();
+        countAfter = screen.getScreenHandler().slots.stream()
+            .map(slot -> slot.getStack())
+            .filter(s -> s.getItem().equals(tradedItem))
+            .map(s -> s.getCount())
+            .reduce(0, Integer::sum);
+        ArrayList<ItemStack> itemsTraded = countAfter < countBefore ? itemsSold : itemsBought;
+        int count = java.lang.Math.abs(countAfter - countBefore);
+        Optional<ItemStack> t = itemsTraded.stream().filter(s -> s.getItem().equals(tradedItem)).findFirst();
+        if (t.isPresent()) {
+            t.get().setCount(t.get().getCount() + count);
+        }
+        else {
+            ItemStack s = new ItemStack(tradedItem, count);
+            itemsTraded.add(s);
+        }
+    }
+
+    private void endTrading() {
+        if (logSummary.get()) {
+            if (!itemsSold.isEmpty()) {
+                Formatter f = new Formatter();
+                f.format("Item(s) sold:");
+                boolean first = true;
+                for (ItemStack i : itemsSold) {
+                    f.format("%s%s*%d", first ? " " : ", ", i.getName().getString(), i.getCount());
+                    first = false;
+                }
+                info(f.toString());
+            }
+            if (!itemsBought.isEmpty()) {
+                Formatter f = new Formatter();
+                f.format("Item(s) bought:");
+                boolean first = true;
+                for (ItemStack i : itemsBought) {
+                    f.format("%s%s*%d", first ? " " : ", ", i.getName().getString(), i.getCount());
+                    first = false;
+                }
+                info(f.toString());
+            }
+        }
+        if (autoClose.get() && (!itemsBought.isEmpty() || !itemsSold.isEmpty()))
+            screen.close();
+        screen = null;
+    }
+    @EventHandler
+    private void onTick(TickEvent.Pre event) {
+        if (ticksRemaining > 0) {
+            --ticksRemaining;
+            return;
+        }
+        if (offers != null && screen != null) {
+            if (waitState == WaitState.WaitingForFinalize) {
+                endTrading();
+                return;
+            }
+            if (waitState != WaitState.None && currentOffer < offers.size() && offers.get(currentOffer).isDisabled()) {
+                finalizeCurrentTrade();
+                waitState = WaitState.None;
+                ticksRemaining = interactionRate.get();
+                return;
+            }
+            if (ticksRemaining == -0xdead)
+                return;
+            switch (waitState) {
+                case None: selectTrade(); break;
+                case WaitingForOutput:
+                    InvUtils.shiftClick().slotId(2);
+                    waitState = WaitState.WaitingForInventoryUpdate;
+                    ticksRemaining = -0xdead;
+                break;
+                case WaitingForInventoryUpdate:
+                    finalizeCurrentTrade();
+                    waitState = WaitState.None;
+                    ticksRemaining = interactionRate.get();
+                break;
+            }
+        }
+    }
+
+    @EventHandler
+    private void onReceivePacket(PacketEvent.Receive e) {
+        if (e.packet instanceof SetTradeOffersS2CPacket p) {
+            this.offers = p.getOffers();
+            currentOffer = 0;
+            waitState = WaitState.None;
+            ticksRemaining = 0;
+            itemsSold.clear();
+            itemsBought.clear();
+        } else if (e.packet instanceof ScreenHandlerSlotUpdateS2CPacket p) {
+            if (screen == null || p.getSyncId() != screen.getScreenHandler().syncId || p.getSlot() != 2)
+                return;
+            if (waitState == WaitState.WaitingForOutput) {
+                ticksRemaining = interactionRate.get();
+            }
+            if (waitState == WaitState.WaitingForInventoryUpdate) {
+                if (!p.getStack().isEmpty())
+                    waitState = WaitState.WaitingForOutput;
+                ticksRemaining = interactionRate.get();
+            }
+        }
+    }
+
+    @EventHandler
+    private void onOpenScreen(OpenScreenEvent e) {
+        if (e.screen == null) {
+            this.screen = null;
+            this.offers = null;
+        }
+        else if (e.screen instanceof MerchantScreen s) {
+            this.screen = s;
+        }
+    }
+}
-- 
cgit v1.2.3