aboutsummaryrefslogblamecommitdiff
path: root/src/main/java/org/chrisoft/trashyaddon/modules/AutoTrade.java
blob: a062b380c0fdc2783c0cc9fe34827aae9a1644b7 (plain) (tree)




















                                                                              
                   


                                                                      
                                                          














                               
                           




























                            
                                                         


































                                     



                                                                                           
                                                                                                
                      



                                                                 



                                                                                          
                                                                                               
                     



                                                                  





                                                                                                                                   



                                                                                           
                








                                                                                       
                                                                                        





































                                                                                                                         
                                                                                              


                           
                                                      
                                          

                                                                                      





                                             
                         



                                                                                            
                                                                                                                                    


                                                                                                                           















                                                                                          
                                                                               











                                                                           
                                                                               



























                                                                                                             
                          









                                                                                                   
                          
















                                                                                
                                                                                                                        
                                                                                                                                                 

                                                                                                                 
























































                                                                                                        
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.*;

public class AutoTrade extends Module {
    private final SettingGroup sgGeneral = settings.getDefaultGroup();
    public 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.TURTLE_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
    );

    public 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<Boolean> sellingEnabled = sgGeneral.add(new BoolSetting.Builder()
        .name("Enable Selling")
        .build()
    );
    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<Boolean> buyingEnabled = sgGeneral.add(new BoolSetting.Builder()
        .name("Enable Buying")
        .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<AcceptablePrices> acceptablePricesSetting = sgGeneral.add(new GenericSetting.Builder<AcceptablePrices>()
        .name("acceptable-prices")
        .description("Configure maximum acceptable price for each item.")
        .defaultValue(new AcceptablePrices(new HashMap<>()))
        .build()
    );
    private final Setting<Integer> interactionRate = sgGeneral.add(new IntSetting.Builder()
        .name("Interaction Rate")
        .description("Number of ticks between interactions.")
        .min(0)
        .max(20)
        .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 trading is complete.")
        .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().isPresent() && !o.getSecondBuyItem().get().equals(Items.AIR))
            return false;
        if (o.isDisabled())
            return false;
        ItemStack mbuy = o.getDisplayedFirstBuyItem();
        ItemStack msell = o.getSellItem();
        List<Item> sells = sellingEnabled.get() ? this.sellsSetting.get() : List.of();
        List<Item> buys = buyingEnabled.get() ? this.buysSetting.get() : List.of();
        Item interestedItem = null;
        if (sells.contains(mbuy.getItem()))
            interestedItem = mbuy.getItem();
        if (buys.contains(msell.getItem()))
            interestedItem = msell.getItem();
        if (interestedItem == null)
            return false;
        Integer maxPrice = acceptablePricesSetting.get().getMaxPriceForItem(interestedItem);
        if (maxPrice != null && mbuy.getCount() > maxPrice) {
            return false;
        }
        FindItemResult rs = InvUtils.find((stack) -> stack.getItem().equals(mbuy.getItem()) && stack.getCount() >= mbuy.getCount());
        ItemStack s0is = screen.getScreenHandler().slots.get(0).getStack();
        boolean remainingFirstSlotSufficient = s0is.getItem().equals(mbuy.getItem()) && s0is.getCount() >= mbuy.getCount();
        if (!rs.found() && !remainingFirstSlotSufficient)
            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.getDisplayedFirstBuyItem().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.getDisplayedFirstBuyItem().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());
                f.close();
            }
            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());
                f.close();
            }
        }
        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() ||
                screen.getScreenHandler().slots.get(0).getStack().getCount() < offers.get(currentOffer).getDisplayedFirstBuyItem().getCount())) {
                // the WaitingForInventoryUpdate state is mostly useless because ScreenHandlerSlotUpdateS2CPacket
                // isn't sent after the shift-click...
                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;
        }
    }
}