diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/MerchantScreenHandlerAccessor.java b/src/main/java/meteordevelopment/meteorclient/mixin/MerchantScreenHandlerAccessor.java new file mode 100644 index 0000000000..61c14c966a --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/mixin/MerchantScreenHandlerAccessor.java @@ -0,0 +1,19 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.mixin; + +import net.minecraft.screen.MerchantScreenHandler; +import net.minecraft.village.Merchant; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(MerchantScreenHandler.class) +public interface MerchantScreenHandlerAccessor { + + @Accessor + Merchant getMerchant(); + +} diff --git a/src/main/java/meteordevelopment/meteorclient/mixin/MerchantScreenMixin.java b/src/main/java/meteordevelopment/meteorclient/mixin/MerchantScreenMixin.java new file mode 100644 index 0000000000..96f8b00e75 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/mixin/MerchantScreenMixin.java @@ -0,0 +1,82 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.mixin; + +import meteordevelopment.meteorclient.systems.modules.Modules; +import meteordevelopment.meteorclient.systems.modules.world.QuickTrade; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.ingame.MerchantScreen; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.MerchantScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.village.TradeOffer; +import net.minecraft.village.TradeOfferList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MerchantScreen.class) +public abstract class MerchantScreenMixin extends HandledScreen { + @Shadow + private int selectedIndex; + + public MerchantScreenMixin(MerchantScreenHandler handler, PlayerInventory inventory, Text title) { + super(handler, inventory, title); + } + + @Inject( + method = "render", + at = @At("TAIL") + ) + private void render(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + + if (client == null) { + return; + } + + QuickTrade module = Modules.get().get(QuickTrade.class); + + + if (!module.isActive()) { + return; + } + + if (!module.modifier.get().isPressed()) { + return; + } + + context.drawCenteredTextWithShadow(client.textRenderer, "Select a trade on the left to quick-trade", width / 2, height / 2 + 100, 0xFFFFFFFF); + } + + @Inject( + method = "syncRecipeIndex", + at = @At("TAIL") + ) + private void syncRecipeIndex(CallbackInfo ci) { + // Called when a new trade is selected, server is notified at end of call + + QuickTrade module = Modules.get().get(QuickTrade.class); + + if (!module.isActive()) { + return; + } + + if (!module.modifier.get().isPressed()) { + return; + } + + TradeOfferList tradeOfferList = this.handler.getRecipes(); + if (tradeOfferList.size() < selectedIndex) return; + + TradeOffer selectedOffer = tradeOfferList.get(selectedIndex); + MerchantScreenHandler handler = getScreenHandler(); + module.trade(selectedOffer, handler, this.selectedIndex); + } +} + diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java index 8fadc77779..d0edba7036 100644 --- a/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/Modules.java @@ -565,6 +565,7 @@ private void initWorld() { add(new SpawnProofer()); add(new Timer()); add(new VeinMiner()); + add(new QuickTrade()); if (BaritoneUtils.IS_AVAILABLE) { add(new Excavator()); diff --git a/src/main/java/meteordevelopment/meteorclient/systems/modules/world/QuickTrade.java b/src/main/java/meteordevelopment/meteorclient/systems/modules/world/QuickTrade.java new file mode 100644 index 0000000000..e77a55b4d9 --- /dev/null +++ b/src/main/java/meteordevelopment/meteorclient/systems/modules/world/QuickTrade.java @@ -0,0 +1,150 @@ +/* + * This file is part of the Meteor Client distribution (https://github.com/MeteorDevelopment/meteor-client). + * Copyright (c) Meteor Development. + */ + +package meteordevelopment.meteorclient.systems.modules.world; + +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.settings.BoolSetting; +import meteordevelopment.meteorclient.settings.KeybindSetting; +import meteordevelopment.meteorclient.settings.Setting; +import meteordevelopment.meteorclient.settings.SettingGroup; +import meteordevelopment.meteorclient.systems.modules.Categories; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.misc.Keybind; +import meteordevelopment.orbit.EventHandler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.c2s.play.SelectMerchantTradeC2SPacket; +import net.minecraft.screen.MerchantScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.village.TradeOffer; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Supplier; + +public class QuickTrade extends Module { + private final List> tasks = Collections.synchronizedList(new LinkedList<>()); + private final SettingGroup sgGeneral = settings.getDefaultGroup(); + + public final Setting modifier = sgGeneral.add(new KeybindSetting.Builder() + .name("Activation key") + .description("Key to press to perform trade until exhausted.") + .defaultValue(Keybind.none()) + .build() + ); + + public final Setting drop = sgGeneral.add(new BoolSetting.Builder() + .name("Drop if full") + .description("Should we drop items on the floor if we run out of inventory space?") + .defaultValue(true) + .build() + ); + + public QuickTrade() { + super(Categories.World, "quick-trade", "Quickly perform trades with villagers."); + } + + @EventHandler + private void onTick(TickEvent.Pre event) { + synchronized (tasks) { + tasks.removeIf(task -> !task.get()); + } + } + + private void runUntilFalse(Supplier task) { + tasks.add(task); + } + + public void trade(TradeOffer selectedOffer, MerchantScreenHandler handler, int selectedIndex) { + final MinecraftClient client = MinecraftClient.getInstance(); + + Slot inSlot0 = handler.getSlot(0); + Slot inSlot1 = handler.getSlot(1); + Slot outputSlot = handler.getSlot(2); + + runUntilFalse(() -> { + + if (client.interactionManager == null) { + error("Client interaction manager is null!"); + return false; + } + + if (client.player == null) { + error("Player is null, stopping trading."); + return false; + } + + if (client.player.currentScreenHandler != handler) { + error("Screen is closed, stopping trading."); + return false; + } + + if (client.getNetworkHandler() == null) { + error("Network handler is null, stopping trading."); + return false; + } + + // If there is an item in the trade slot(s) already, then shift click or drop them + if (!inSlot0.getStack().isEmpty()) { + client.interactionManager.clickSlot(handler.syncId, inSlot0.id, 1, SlotActionType.QUICK_MOVE, client.player); + + if (drop.get()) { + client.interactionManager.clickSlot(handler.syncId, inSlot0.id, 1, SlotActionType.THROW, client.player); + } else if (!inSlot0.getStack().isEmpty()) { + // If still not empty then we should stop trading + return false; + } + } + + if (!inSlot1.getStack().isEmpty()) { + client.interactionManager.clickSlot(handler.syncId, inSlot1.id, 1, SlotActionType.QUICK_MOVE, client.player); + + if (drop.get()) { + client.interactionManager.clickSlot(handler.syncId, inSlot1.id, 1, SlotActionType.THROW, client.player); + } else if (!inSlot1.getStack().isEmpty()) { + return false; + } + } + + // Refresh items + handler.setRecipeIndex(selectedIndex); + handler.switchTo(selectedIndex); + + client.getNetworkHandler().sendPacket(new SelectMerchantTradeC2SPacket(selectedIndex)); + + // Out of materials OR trade is out of stock + + // todo auto-convert emerald blocks if we're out of them + // todo auto-trade without clicking a trade (preset trades?) + boolean shouldStopTrading = !selectedOffer.matchesBuyItems( + handler.slots.get(0).getStack(), + handler.slots.get(1).getStack()) + || selectedOffer.isDisabled(); + + if (shouldStopTrading) { + return false; + } + + if (hasSpace(client.player.getInventory(), selectedOffer.getSellItem())) { + client.interactionManager.clickSlot(handler.syncId, outputSlot.id, 0, SlotActionType.QUICK_MOVE, client.player); + } else if (drop.get()) { + client.interactionManager.clickSlot(handler.syncId, outputSlot.id, 0, SlotActionType.THROW, client.player); + } else { + // Out of inventory space and drop is not enabled - stop trading + return false; + } + + return true; + }); + } + + public boolean hasSpace(PlayerInventory inv, ItemStack outStack) { + return outStack.isEmpty() || inv.getEmptySlot() >= 0 || inv.getOccupiedSlotWithRoomForStack(outStack) >= 0; + } +} diff --git a/src/main/resources/meteor-client.mixins.json b/src/main/resources/meteor-client.mixins.json index 05fd2e9657..efa4605987 100644 --- a/src/main/resources/meteor-client.mixins.json +++ b/src/main/resources/meteor-client.mixins.json @@ -126,6 +126,8 @@ "LivingEntityRendererMixin", "MapRendererMixin", "MapTextureManagerAccessor", + "MerchantScreenMixin", + "MerchantScreenHandlerAccessor", "MessageHandlerMixin", "MinecraftClientAccessor", "MinecraftClientMixin",