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 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 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 sellingEnabled = sgGeneral.add(new BoolSetting.Builder() .name("Enable Selling") .build() ); private final Setting> sellsSetting = sgGeneral.add(new ItemListSetting.Builder() .name("Sells") .description("Items to automatically SELL TO villagers.") .filter((item) -> allSellItems.contains(item)) .build() ); private final Setting buyingEnabled = sgGeneral.add(new BoolSetting.Builder() .name("Enable Buying") .build() ); private final Setting> buysSetting = sgGeneral.add(new ItemListSetting.Builder() .name("Buys") .description("Items to automatically BUY FROM villagers.") .filter((item) -> allBuyItems.contains(item)) .build() ); private final Setting acceptablePricesSetting = sgGeneral.add(new GenericSetting.Builder() .name("acceptable-prices") .description("Configure maximum acceptable price for each item.") .defaultValue(new AcceptablePrices(new HashMap<>())) .build() ); private final Setting interactionRate = sgGeneral.add(new IntSetting.Builder() .name("Interaction Rate") .description("Number of ticks between interactions.") .min(0) .max(20) .build() ); private final Setting autoClose = sgGeneral.add(new BoolSetting.Builder() .name("Auto Close") .description("Close trading screen after any successful trades.") .build() ); private final Setting 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 itemsSold; private ArrayList 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 sells = sellingEnabled.get() ? this.sellsSetting.get() : List.of(); List 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 itemsTraded = countAfter < countBefore ? itemsSold : itemsBought; int count = java.lang.Math.abs(countAfter - countBefore); Optional 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; } } }