From 7f0afdc5053e05386f2728549fcb8ce457e6e1a9 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 26 Apr 2026 18:47:07 +0200 Subject: [PATCH] Release 0.0.13: buy/sell strings can include an optional #enchant=level suffix (encoded from the held item or item frames). --- gradle.properties | 2 +- .../sebseb7/autotrade/config/Configs.java | 4 +- .../autotrade/event/AutoTradeClientTick.java | 296 +++++++++++++ .../autotrade/event/ContainerIoHelper.java | 70 +++ .../autotrade/event/HotkeyActions.java | 72 ++++ .../autotrade/event/KeybindCallbacks.java | 398 +----------------- .../sebseb7/autotrade/util/TradeItemSpec.java | 116 +++++ 7 files changed, 560 insertions(+), 398 deletions(-) create mode 100644 src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java create mode 100644 src/main/java/com/github/sebseb7/autotrade/event/ContainerIoHelper.java create mode 100644 src/main/java/com/github/sebseb7/autotrade/event/HotkeyActions.java create mode 100644 src/main/java/com/github/sebseb7/autotrade/util/TradeItemSpec.java diff --git a/gradle.properties b/gradle.properties index 3e2de67..c6079fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ mod_name = AutoTrade author = sebseb7 mod_file_name = autotrade-fabric -mod_version = 0.0.12 +mod_version = 0.0.13 malilib_version = 0.28.2 minecraft_version_min = 26.1.2 diff --git a/src/main/java/com/github/sebseb7/autotrade/config/Configs.java b/src/main/java/com/github/sebseb7/autotrade/config/Configs.java index 6a21462..bd92d50 100644 --- a/src/main/java/com/github/sebseb7/autotrade/config/Configs.java +++ b/src/main/java/com/github/sebseb7/autotrade/config/Configs.java @@ -28,13 +28,13 @@ public class Configs implements IConfigHandler { public static final ConfigBoolean ENABLE_SELL = new ConfigBoolean("enableSell", false, "Enable selling (if disabled emeralds are taken from the input container)"); public static final ConfigString SELL_ITEM = new ConfigString("sellItem", "minecraft:gold_ingot", - "The item to sell for emerald."); + "The item to sell for emerald. Optional suffix #enc1=lv&enc2=lv (enchantment registry ids) matches exact enchantments, e.g. enchanted books."); public static final ConfigInteger SELL_LIMIT = new ConfigInteger("sellLimit", 64, 1, 64, "max price to sell for"); public static final ConfigBoolean ENABLE_BUY = new ConfigBoolean("enableBuy", false, "Enable buying (if disabled emeralds are placed in the output container)"); public static final ConfigString BUY_ITEM = new ConfigString("buyItem", "minecraft:redstone", - "The item to buy using emerald."); + "The item to buy using emerald. Optional suffix #enc1=lv&enc2=lv matches exact enchantments (use set-buy hotkey with the book in hand)."); public static final ConfigInteger BUY_LIMIT = new ConfigInteger("buyLimit", 64, 1, 64, "max price to buy for"); public static final ConfigInteger MAX_INPUT_ITEMS = new ConfigInteger("maxInputStacks", 9, 1, 35, "stacks to take from input container (or emerald container in buy-only mode)"); diff --git a/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java b/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java new file mode 100644 index 0000000..782cf0e --- /dev/null +++ b/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java @@ -0,0 +1,296 @@ +package com.github.sebseb7.autotrade.event; + +import com.github.sebseb7.autotrade.AutoTrade; +import com.github.sebseb7.autotrade.config.Configs; +import com.github.sebseb7.autotrade.util.TradeItemSpec; +import fi.dy.masa.malilib.gui.Message; +import fi.dy.masa.malilib.util.GuiUtils; +import fi.dy.masa.malilib.util.InfoUtils; +import java.util.List; +import java.util.Vector; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.ContainerScreen; +import net.minecraft.client.gui.screens.inventory.MerchantScreen; +import net.minecraft.client.gui.screens.inventory.ShulkerBoxScreen; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.protocol.game.ServerboundSelectTradePacket; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.npc.villager.Villager; +import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.MerchantMenu; +import net.minecraft.world.inventory.ShulkerBoxMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.Vec3; + +final class AutoTradeClientTick { + private final Vector villagersInRange = new Vector<>(); + private int villagerActive = 0; + + private boolean state = false; + private boolean inputInRange = false; + private boolean inputOpened = false; + private boolean outputInRange = false; + private boolean outputOpened = false; + private int tickCount = 0; + private int voidDelay = 0; + private int containerDelay = 0; + + void tick(Minecraft mc) { + if (voidDelay > 0) { + if (Configs.Generic.VOID_TRADING_DELAY_AFTER_TELEPORT.getBooleanValue()) { + boolean found = false; + for (Entity entity : mc.level.entitiesForRendering()) { + if (entity.getId() == villagerActive) { + found = true; + } + } + if (!found) { + voidDelay--; + } + } else { + voidDelay--; + } + return; + } + if (containerDelay > 0) { + containerDelay--; + } + if (!Configs.Generic.ENABLED.getBooleanValue() || mc.player == null) { + return; + } + Inventory plInv = mc.player.getInventory(); + if (Configs.Generic.GLASS_BLOCK.getBooleanValue()) { + tickGlassBlockSelection(mc); + } + if (Configs.Generic.ITEM_FRAME.getBooleanValue()) { + tickItemFrameSelection(mc); + } + if (GuiUtils.getCurrentScreen() instanceof MerchantScreen screen) { + tickMerchantScreen(mc, screen); + inputInRange = false; + outputInRange = false; + return; + } + if (GuiUtils.getCurrentScreen() instanceof ShulkerBoxScreen sbs) { + ShulkerBoxMenu m = sbs.getMenu(); + if ((containerDelay == 0) && inputOpened) { + inputOpened = false; + ContainerIoHelper.processInput(m, plInv); + sbs.onClose(); + } + if ((containerDelay == 0) && outputOpened) { + outputOpened = false; + ContainerIoHelper.processOutput(m, plInv); + sbs.onClose(); + } + } else if (GuiUtils.getCurrentScreen() instanceof ContainerScreen cs) { + AbstractContainerMenu m = cs.getMenu(); + if ((containerDelay == 0) && inputOpened) { + inputOpened = false; + ContainerIoHelper.processInput(m, plInv); + cs.onClose(); + } + if ((containerDelay == 0) && outputOpened) { + outputOpened = false; + ContainerIoHelper.processOutput(m, plInv); + cs.onClose(); + } + } + boolean found = false; + Vector newVillagersInRange = new Vector<>(villagersInRange); + for (Entity entity : mc.level.entitiesForRendering()) { + if (entity instanceof Villager || entity instanceof WanderingTrader) { + if (entity.distanceToSqr(mc.player) < (2.5f * 2.5f)) { + if (!found) { + if (!newVillagersInRange.contains(entity)) { + found = true; + newVillagersInRange.add(entity); + EntityHitResult ehr = new EntityHitResult(entity, entity.position()); + mc.gameMode.interact(mc.player, entity, ehr, InteractionHand.MAIN_HAND); + voidDelay = Configs.Generic.VOID_TRADING_DELAY.getIntegerValue(); + villagerActive = entity.getId(); + state = false; + break; + } + } + } + } + } + for (Entity entity : villagersInRange) { + if (entity.distanceToSqr(mc.player) >= 16.0D) { + newVillagersInRange.remove(entity); + } + } + villagersInRange.clear(); + villagersInRange.addAll(newVillagersInRange); + if (found) { + return; + } + BlockPos input = new BlockPos(Configs.Generic.INPUT_CONTAINER_X.getIntegerValue(), + Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue(), + Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue()); + BlockPos output = new BlockPos(Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue(), + Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue(), + Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue()); + Vec3 ic = input.getCenter(); + Vec3 oc = output.getCenter(); + if ((mc.player.distanceToSqr(ic) < 16.0D) && (inputInRange == false)) { + inputInRange = true; + mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, + new BlockHitResult(ic, Direction.UP, input, false)); + containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue(); + inputOpened = true; + return; + } + if ((mc.player.distanceToSqr(oc) < 16.0D) && (outputInRange == false)) { + outputInRange = true; + mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, + new BlockHitResult(oc, Direction.UP, output, false)); + containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue(); + outputOpened = true; + return; + } + if (mc.player.distanceToSqr(ic) > 25.0D) { + inputOpened = false; + inputInRange = false; + } + if (mc.player.distanceToSqr(oc) > 25.0D) { + outputOpened = false; + outputInRange = false; + } + tickCount++; + if (tickCount > 200) { + tickCount = 0; + villagersInRange.clear(); + inputInRange = false; + outputInRange = false; + var cur = GuiUtils.getCurrentScreen(); + if (cur != null) { + if (cur instanceof MerchantScreen || cur instanceof ShulkerBoxScreen + || cur instanceof ContainerScreen) { + cur.onClose(); + } + } + } + } + + private void tickGlassBlockSelection(Minecraft mc) { + int playerX = (int) mc.player.getX(); + int playerZ = (int) mc.player.getZ(); + int playerY = (int) mc.player.getY(); + int selectorOffset = Configs.Generic.SELECTOR_OFFSET.getIntegerValue(); + int absSelectorOffset = Math.abs(selectorOffset); + for (int x = playerX - (absSelectorOffset + 3); x < playerX + (absSelectorOffset + 3); x += 1) { + for (int z = playerZ - (absSelectorOffset + 3); z < playerZ + (absSelectorOffset + 3); z += 1) { + for (int y = playerY - (absSelectorOffset + 3); y < playerY + (absSelectorOffset + 3); y += 1) { + BlockPos pos = new BlockPos(x, y, z); + if (mc.level.getBlockState(pos).getBlock() == Blocks.RED_STAINED_GLASS) { + if ((x != Configs.Generic.INPUT_CONTAINER_X.getIntegerValue()) + || ((y - selectorOffset) != Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue()) + || (z != Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue())) { + Configs.Generic.INPUT_CONTAINER_X.setIntegerValue(x); + Configs.Generic.INPUT_CONTAINER_Y.setIntegerValue(y - selectorOffset); + Configs.Generic.INPUT_CONTAINER_Z.setIntegerValue(z); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, + "autotrade.message.input_container_set", x, y - selectorOffset, z); + } + break; + } + if (mc.level.getBlockState(pos).getBlock() == Blocks.BLUE_STAINED_GLASS) { + if ((x != Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue()) + || ((y - selectorOffset) != Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue()) + || (z != Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue())) { + Configs.Generic.OUTPUT_CONTAINER_X.setIntegerValue(x); + Configs.Generic.OUTPUT_CONTAINER_Y.setIntegerValue(y - selectorOffset); + Configs.Generic.OUTPUT_CONTAINER_Z.setIntegerValue(z); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, + "autotrade.message.output_container_set", x, y - selectorOffset, z); + } + break; + } + } + } + } + } + + private void tickItemFrameSelection(Minecraft mc) { + Vec3 pm = new Vec3(mc.player.getX(), mc.player.getY(), mc.player.getZ()); + AABB box = new AABB(pm.subtract(3, 3, 3), pm.add(3, 3, 3)); + @SuppressWarnings("unchecked") + List frames = (List) (List) mc.level.getEntities((Entity) null, box, + e -> e instanceof ItemFrame && e.isAlive()); + for (ItemFrame entity : frames) { + ItemStack stack = entity.getItem(); + String customName = stack.getHoverName().getString(); + if ("sell".equalsIgnoreCase(customName) || "\"sell\"".equals(customName)) { + String sellItem = TradeItemSpec.encodeFromStack(stack); + if (!Configs.Generic.SELL_ITEM.getStringValue().equals(sellItem)) { + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set", + sellItem); + Configs.Generic.SELL_ITEM.setValueFromString(sellItem); + break; + } + } + if ("buy".equalsIgnoreCase(customName) || "\"buy\"".equals(customName)) { + String buyItem = TradeItemSpec.encodeFromStack(stack); + if (!Configs.Generic.BUY_ITEM.getStringValue().equals(buyItem)) { + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set", + buyItem); + Configs.Generic.BUY_ITEM.setValueFromString(buyItem); + break; + } + } + } + } + + private void tickMerchantScreen(Minecraft mc, MerchantScreen screen) { + if (!state) { + String sellItemStr = Configs.Generic.SELL_ITEM.getStringValue(); + String buyItemStr = Configs.Generic.BUY_ITEM.getStringValue(); + state = true; + MerchantMenu menu = screen.getMenu(); + MerchantOffers offers = menu.getOffers(); + for (int i = 0; i < offers.size(); i++) { + MerchantOffer offer = offers.get(i); + if (TradeItemSpec.matches(offer.getResult(), buyItemStr) && Configs.Generic.ENABLE_BUY.getBooleanValue() + && offer.getResult().getCount() <= Configs.Generic.BUY_LIMIT.getIntegerValue()) { + Slot slot = menu.getSlot(2); + menu.setSelectionHint(i); + mc.player.connection.send(new ServerboundSelectTradePacket(i)); + AutoTrade.bought += offer.getMaxUses(); + try { + ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index); + } catch (Exception e) { + System.out.println("err " + e); + } + } + if (TradeItemSpec.matches(offer.getCostA(), sellItemStr) + && Configs.Generic.ENABLE_SELL.getBooleanValue() + && offer.getCostA().getCount() <= Configs.Generic.SELL_LIMIT.getIntegerValue()) { + Slot slot = menu.getSlot(2); + menu.setSelectionHint(i); + AutoTrade.sold += offer.getMaxUses(); + mc.player.connection.send(new ServerboundSelectTradePacket(i)); + try { + ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index); + } catch (Exception e) { + System.out.println("err " + e); + } + } + } + } + screen.onClose(); + } +} diff --git a/src/main/java/com/github/sebseb7/autotrade/event/ContainerIoHelper.java b/src/main/java/com/github/sebseb7/autotrade/event/ContainerIoHelper.java new file mode 100644 index 0000000..0149300 --- /dev/null +++ b/src/main/java/com/github/sebseb7/autotrade/event/ContainerIoHelper.java @@ -0,0 +1,70 @@ +package com.github.sebseb7.autotrade.event; + +import com.github.sebseb7.autotrade.config.Configs; +import com.github.sebseb7.autotrade.util.TradeItemSpec; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.inventory.Slot; + +final class ContainerIoHelper { + private ContainerIoHelper() { + } + + static void quickMoveResultSlot(Minecraft mc, AbstractContainerMenu menu, int slotIndex) { + Slot slot = menu.getSlot(slotIndex); + mc.gameMode.handleContainerInput(menu.containerId, slot.index, 0, ContainerInput.QUICK_MOVE, mc.player); + } + + static void processOutput(AbstractContainerMenu menu, Inventory playerInv) { + String itemToPlace = "minecraft:emerald"; + if (Configs.Generic.ENABLE_BUY.getBooleanValue()) { + itemToPlace = Configs.Generic.BUY_ITEM.getStringValue(); + } + Minecraft mc = Minecraft.getInstance(); + for (int i = 0; i < menu.slots.size(); i++) { + Slot s = menu.getSlot(i); + if (s.container == playerInv) { + if (TradeItemSpec.matches(s.getItem(), itemToPlace)) { + try { + quickMoveResultSlot(mc, menu, i); + } catch (Exception e) { + System.out.println("err " + e); + } + } + } + } + } + + static void processInput(AbstractContainerMenu menu, Inventory playerInv) { + String itemToTake = "minecraft:emerald"; + if (Configs.Generic.ENABLE_SELL.getBooleanValue()) { + itemToTake = Configs.Generic.SELL_ITEM.getStringValue(); + } + int inputCount = 0; + for (int i = 0; i < menu.slots.size(); i++) { + Slot s = menu.getSlot(i); + if (s.container == playerInv && TradeItemSpec.matches(s.getItem(), itemToTake)) { + inputCount += s.getItem().getCount(); + } + } + Minecraft mc = Minecraft.getInstance(); + for (int i = 0; i < menu.slots.size(); i++) { + Slot s = menu.getSlot(i); + if (s.container == playerInv) { + continue; + } + if (TradeItemSpec.matches(s.getItem(), itemToTake)) { + if (inputCount < (Configs.Generic.MAX_INPUT_ITEMS.getIntegerValue() * 64)) { + inputCount += s.getItem().getCount(); + try { + quickMoveResultSlot(mc, menu, i); + } catch (Exception e) { + System.out.println("err " + e); + } + } + } + } + } +} diff --git a/src/main/java/com/github/sebseb7/autotrade/event/HotkeyActions.java b/src/main/java/com/github/sebseb7/autotrade/event/HotkeyActions.java new file mode 100644 index 0000000..4a74fe2 --- /dev/null +++ b/src/main/java/com/github/sebseb7/autotrade/event/HotkeyActions.java @@ -0,0 +1,72 @@ +package com.github.sebseb7.autotrade.event; + +import com.github.sebseb7.autotrade.AutoTrade; +import com.github.sebseb7.autotrade.config.Configs; +import com.github.sebseb7.autotrade.config.Hotkeys; +import com.github.sebseb7.autotrade.gui.GuiConfigs; +import com.github.sebseb7.autotrade.util.TradeItemSpec; +import fi.dy.masa.malilib.gui.GuiBase; +import fi.dy.masa.malilib.gui.Message; +import fi.dy.masa.malilib.hotkeys.IKeybind; +import fi.dy.masa.malilib.util.InfoUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; + +final class HotkeyActions { + private HotkeyActions() { + } + + static boolean handle(Minecraft mc, IKeybind key) { + if (mc.player == null || mc.level == null) { + return false; + } + if (key == Hotkeys.TOGGLE_KEY.getKeybind()) { + Configs.Generic.ENABLED.toggleBooleanValue(); + boolean enabled = Configs.Generic.ENABLED.getBooleanValue(); + String msg = enabled ? "autotrade.message.toggled_mod_on" : "autotrade.message.toggled_mod_off"; + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, msg); + if (enabled) { + AutoTrade.sold = 0; + AutoTrade.bought = 0; + AutoTrade.sessionStart = System.currentTimeMillis() / 1000L; + } + } else if (key == Hotkeys.OPEN_GUI_SETTINGS.getKeybind()) { + GuiBase.openGui(new GuiConfigs()); + return true; + } else if (key == Hotkeys.SET_INPUT_KEY.getKeybind()) { + HitResult result = mc.player.pick(20.0D, 0.0F, false); + if (result.getType() == HitResult.Type.BLOCK) { + BlockHitResult blockHit = (BlockHitResult) result; + BlockPos p = blockHit.getBlockPos(); + Configs.Generic.INPUT_CONTAINER_X.setIntegerValue(p.getX()); + Configs.Generic.INPUT_CONTAINER_Y.setIntegerValue(p.getY()); + Configs.Generic.INPUT_CONTAINER_Z.setIntegerValue(p.getZ()); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.input_container_set", + p.getX(), p.getY(), p.getZ()); + } + } else if (key == Hotkeys.SET_OUTPUT_KEY.getKeybind()) { + HitResult result = mc.player.pick(20.0D, 0.0F, false); + if (result.getType() == HitResult.Type.BLOCK) { + BlockHitResult blockHit = (BlockHitResult) result; + BlockPos p = blockHit.getBlockPos(); + Configs.Generic.OUTPUT_CONTAINER_X.setIntegerValue(p.getX()); + Configs.Generic.OUTPUT_CONTAINER_Y.setIntegerValue(p.getY()); + Configs.Generic.OUTPUT_CONTAINER_Z.setIntegerValue(p.getZ()); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.output_container_set", + p.getX(), p.getY(), p.getZ()); + } + } else if (key == Hotkeys.SET_BUY_KEY.getKeybind()) { + String buyItem = TradeItemSpec.encodeFromStack(mc.player.getItemInHand(InteractionHand.MAIN_HAND)); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set", buyItem); + Configs.Generic.BUY_ITEM.setValueFromString(buyItem); + } else if (key == Hotkeys.SET_SELL_KEY.getKeybind()) { + String sellItem = TradeItemSpec.encodeFromStack(mc.player.getItemInHand(InteractionHand.MAIN_HAND)); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set", sellItem); + Configs.Generic.SELL_ITEM.setValueFromString(sellItem); + } + return false; + } +} diff --git a/src/main/java/com/github/sebseb7/autotrade/event/KeybindCallbacks.java b/src/main/java/com/github/sebseb7/autotrade/event/KeybindCallbacks.java index 7506bf2..501a8fb 100644 --- a/src/main/java/com/github/sebseb7/autotrade/event/KeybindCallbacks.java +++ b/src/main/java/com/github/sebseb7/autotrade/event/KeybindCallbacks.java @@ -1,64 +1,18 @@ package com.github.sebseb7.autotrade.event; -import com.github.sebseb7.autotrade.AutoTrade; import com.github.sebseb7.autotrade.config.Configs; import com.github.sebseb7.autotrade.config.Hotkeys; -import com.github.sebseb7.autotrade.gui.GuiConfigs; import fi.dy.masa.malilib.config.options.ConfigHotkey; -import fi.dy.masa.malilib.gui.GuiBase; -import fi.dy.masa.malilib.gui.Message; import fi.dy.masa.malilib.hotkeys.IHotkeyCallback; import fi.dy.masa.malilib.hotkeys.IKeybind; import fi.dy.masa.malilib.hotkeys.KeyAction; import fi.dy.masa.malilib.interfaces.IClientTickHandler; -import fi.dy.masa.malilib.util.GuiUtils; -import fi.dy.masa.malilib.util.InfoUtils; -import java.util.HashMap; -import java.util.List; -import java.util.Vector; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.inventory.ContainerScreen; -import net.minecraft.client.gui.screens.inventory.MerchantScreen; -import net.minecraft.client.gui.screens.inventory.ShulkerBoxScreen; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.network.protocol.game.ServerboundSelectTradePacket; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.decoration.ItemFrame; -import net.minecraft.world.entity.npc.villager.Villager; -import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.ContainerInput; -import net.minecraft.world.inventory.MerchantMenu; -import net.minecraft.world.inventory.ShulkerBoxMenu; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.trading.MerchantOffer; -import net.minecraft.world.item.trading.MerchantOffers; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.EntityHitResult; -import net.minecraft.world.phys.HitResult; -import net.minecraft.world.phys.Vec3; public class KeybindCallbacks implements IHotkeyCallback, IClientTickHandler { private static final KeybindCallbacks INSTANCE = new KeybindCallbacks(); - private Vector villagersInRange = new Vector<>(); - private int villagerActive = 0; - - private boolean state = false; - private boolean inputInRange = false; - private boolean inputOpened = false; - private boolean outputInRange = false; - private boolean outputOpened = false; - private int tickCount = 0; - private int voidDelay = 0; - private int containerDelay = 0; + private final AutoTradeClientTick clientTick = new AutoTradeClientTick(); public static KeybindCallbacks getInstance() { return INSTANCE; @@ -79,357 +33,11 @@ public class KeybindCallbacks implements IHotkeyCallback, IClientTickHandler { @Override public boolean onKeyAction(KeyAction action, IKeybind key) { - return this.onKeyActionImpl(action, key); - } - - private static String id(net.minecraft.world.item.Item item) { - return BuiltInRegistries.ITEM.getKey(item).toString(); - } - - private void quickMoveResultSlot(Minecraft mc, AbstractContainerMenu menu, int slotIndex) { - Slot slot = menu.getSlot(slotIndex); - mc.gameMode.handleContainerInput(menu.containerId, slot.index, 0, ContainerInput.QUICK_MOVE, mc.player); - } - - private void processOutput(AbstractContainerMenu menu, Inventory playerInv) { - outputOpened = false; - String itemToPlace = "minecraft:emerald"; - if (Configs.Generic.ENABLE_BUY.getBooleanValue()) { - itemToPlace = Configs.Generic.BUY_ITEM.getStringValue(); - } - Minecraft mc = Minecraft.getInstance(); - for (int i = 0; i < menu.slots.size(); i++) { - Slot s = menu.getSlot(i); - if (s.container == playerInv) { - if (id(s.getItem().getItem()).equals(itemToPlace)) { - try { - quickMoveResultSlot(mc, menu, i); - } catch (Exception e) { - System.out.println("err " + e); - } - } - } - } - } - - private void processInput(AbstractContainerMenu menu, Inventory playerInv) { - inputOpened = false; - HashMap inventory = new HashMap<>(); - for (int i = 0; i < menu.slots.size(); i++) { - Slot s = menu.getSlot(i); - if (s.container == playerInv) { - String k = id(s.getItem().getItem()); - inventory.put(k, s.getItem().getCount() + inventory.getOrDefault(k, 0)); - } - } - String itemToTake = "minecraft:emerald"; - if (Configs.Generic.ENABLE_SELL.getBooleanValue()) { - itemToTake = Configs.Generic.SELL_ITEM.getStringValue(); - } - int inputCount = inventory.getOrDefault(itemToTake, 0); - Minecraft mc = Minecraft.getInstance(); - for (int i = 0; i < menu.slots.size(); i++) { - Slot s = menu.getSlot(i); - if (s.container == playerInv) { - continue; - } - if (id(s.getItem().getItem()).equals(itemToTake)) { - if (inputCount < (Configs.Generic.MAX_INPUT_ITEMS.getIntegerValue() * 64)) { - inputCount += s.getItem().getCount(); - try { - quickMoveResultSlot(mc, menu, i); - } catch (Exception e) { - System.out.println("err " + e); - } - } - } - } - } - - private boolean onKeyActionImpl(KeyAction action, IKeybind key) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || mc.level == null) { - return false; - } - if (key == Hotkeys.TOGGLE_KEY.getKeybind()) { - Configs.Generic.ENABLED.toggleBooleanValue(); - String msg = this.functionalityEnabled() - ? "autotrade.message.toggled_mod_on" - : "autotrade.message.toggled_mod_off"; - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, msg); - if (this.functionalityEnabled()) { - AutoTrade.sold = 0; - AutoTrade.bought = 0; - AutoTrade.sessionStart = System.currentTimeMillis() / 1000L; - } - } else if (key == Hotkeys.OPEN_GUI_SETTINGS.getKeybind()) { - GuiBase.openGui(new GuiConfigs()); - return true; - } else if (key == Hotkeys.SET_INPUT_KEY.getKeybind()) { - HitResult result = mc.player.pick(20.0D, 0.0F, false); - if (result.getType() == HitResult.Type.BLOCK) { - BlockHitResult blockHit = (BlockHitResult) result; - BlockPos p = blockHit.getBlockPos(); - Configs.Generic.INPUT_CONTAINER_X.setIntegerValue(p.getX()); - Configs.Generic.INPUT_CONTAINER_Y.setIntegerValue(p.getY()); - Configs.Generic.INPUT_CONTAINER_Z.setIntegerValue(p.getZ()); - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.input_container_set", - p.getX(), p.getY(), p.getZ()); - } - } else if (key == Hotkeys.SET_OUTPUT_KEY.getKeybind()) { - HitResult result = mc.player.pick(20.0D, 0.0F, false); - if (result.getType() == HitResult.Type.BLOCK) { - BlockHitResult blockHit = (BlockHitResult) result; - BlockPos p = blockHit.getBlockPos(); - Configs.Generic.OUTPUT_CONTAINER_X.setIntegerValue(p.getX()); - Configs.Generic.OUTPUT_CONTAINER_Y.setIntegerValue(p.getY()); - Configs.Generic.OUTPUT_CONTAINER_Z.setIntegerValue(p.getZ()); - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.output_container_set", - p.getX(), p.getY(), p.getZ()); - } - } else if (key == Hotkeys.SET_BUY_KEY.getKeybind()) { - String buyItem = id(mc.player.getItemInHand(InteractionHand.MAIN_HAND).getItem()); - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set", buyItem); - Configs.Generic.BUY_ITEM.setValueFromString(buyItem); - } else if (key == Hotkeys.SET_SELL_KEY.getKeybind()) { - String sellItem = id(mc.player.getItemInHand(InteractionHand.MAIN_HAND).getItem()); - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set", sellItem); - Configs.Generic.SELL_ITEM.setValueFromString(sellItem); - } - return false; + return HotkeyActions.handle(Minecraft.getInstance(), key); } @Override public void onClientTick(Minecraft mc) { - if (voidDelay > 0) { - if (Configs.Generic.VOID_TRADING_DELAY_AFTER_TELEPORT.getBooleanValue()) { - boolean found = false; - for (Entity entity : mc.level.entitiesForRendering()) { - if (entity.getId() == villagerActive) { - found = true; - } - } - if (!found) { - voidDelay--; - } - } else { - voidDelay--; - } - return; - } - if (containerDelay > 0) { - containerDelay--; - } - if (this.functionalityEnabled() == false || mc.player == null) { - return; - } - Inventory plInv = mc.player.getInventory(); - if (Configs.Generic.GLASS_BLOCK.getBooleanValue()) { - int playerX = (int) mc.player.getX(); - int playerZ = (int) mc.player.getZ(); - int playerY = (int) mc.player.getY(); - int selectorOffset = Configs.Generic.SELECTOR_OFFSET.getIntegerValue(); - int absSelectorOffset = Math.abs(selectorOffset); - for (int x = playerX - (absSelectorOffset + 3); x < playerX + (absSelectorOffset + 3); x += 1) { - for (int z = playerZ - (absSelectorOffset + 3); z < playerZ + (absSelectorOffset + 3); z += 1) { - for (int y = playerY - (absSelectorOffset + 3); y < playerY + (absSelectorOffset + 3); y += 1) { - BlockPos pos = new BlockPos(x, y, z); - if (mc.level.getBlockState(pos).getBlock() == Blocks.RED_STAINED_GLASS) { - if ((x != Configs.Generic.INPUT_CONTAINER_X.getIntegerValue()) - || ((y - selectorOffset) != Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue()) - || (z != Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue())) { - Configs.Generic.INPUT_CONTAINER_X.setIntegerValue(x); - Configs.Generic.INPUT_CONTAINER_Y.setIntegerValue(y - selectorOffset); - Configs.Generic.INPUT_CONTAINER_Z.setIntegerValue(z); - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, - "autotrade.message.input_container_set", x, y - selectorOffset, z); - } - break; - } - if (mc.level.getBlockState(pos).getBlock() == Blocks.BLUE_STAINED_GLASS) { - if ((x != Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue()) - || ((y - selectorOffset) != Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue()) - || (z != Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue())) { - Configs.Generic.OUTPUT_CONTAINER_X.setIntegerValue(x); - Configs.Generic.OUTPUT_CONTAINER_Y.setIntegerValue(y - selectorOffset); - Configs.Generic.OUTPUT_CONTAINER_Z.setIntegerValue(z); - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, - "autotrade.message.output_container_set", x, y - selectorOffset, z); - } - break; - } - } - } - } - } - if (Configs.Generic.ITEM_FRAME.getBooleanValue()) { - Vec3 pm = new Vec3(mc.player.getX(), mc.player.getY(), mc.player.getZ()); - AABB box = new AABB(pm.subtract(3, 3, 3), pm.add(3, 3, 3)); - @SuppressWarnings("unchecked") - List frames = (List) (List) mc.level.getEntities((Entity) null, box, - e -> e instanceof ItemFrame && e.isAlive()); - for (ItemFrame entity : frames) { - ItemStack stack = entity.getItem(); - String customName = stack.getHoverName().getString(); - if ("sell".equalsIgnoreCase(customName) || "\"sell\"".equals(customName)) { - String sellItem = id(stack.getItem()); - if (!Configs.Generic.SELL_ITEM.getStringValue().equals(sellItem)) { - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set", - sellItem); - Configs.Generic.SELL_ITEM.setValueFromString(sellItem); - break; - } - } - if ("buy".equalsIgnoreCase(customName) || "\"buy\"".equals(customName)) { - String buyItem = id(stack.getItem()); - if (!Configs.Generic.BUY_ITEM.getStringValue().equals(buyItem)) { - InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set", - buyItem); - Configs.Generic.BUY_ITEM.setValueFromString(buyItem); - break; - } - } - } - } - if (GuiUtils.getCurrentScreen() instanceof MerchantScreen screen) { - if (!state) { - String sellItemStr = Configs.Generic.SELL_ITEM.getStringValue(); - String buyItemStr = Configs.Generic.BUY_ITEM.getStringValue(); - state = true; - MerchantMenu menu = screen.getMenu(); - MerchantOffers offers = menu.getOffers(); - for (int i = 0; i < offers.size(); i++) { - MerchantOffer offer = offers.get(i); - String costA = id(offer.getCostA().getItem()); - String resultI = id(offer.getResult().getItem()); - // buying from villager: configured buy item matches the trade result - if (resultI.equals(buyItemStr) && Configs.Generic.ENABLE_BUY.getBooleanValue() - && offer.getResult().getCount() <= Configs.Generic.BUY_LIMIT.getIntegerValue()) { - Slot slot = menu.getSlot(2); - menu.setSelectionHint(i); - mc.player.connection.send(new ServerboundSelectTradePacket(i)); - AutoTrade.bought += offer.getMaxUses(); - try { - quickMoveResultSlot(mc, menu, slot.index); - } catch (Exception e) { - System.out.println("err " + e); - } - } - // "sell" to villager: cost matches configured sell list - if (costA.equals(sellItemStr) && Configs.Generic.ENABLE_SELL.getBooleanValue() - && offer.getCostA().getCount() <= Configs.Generic.SELL_LIMIT.getIntegerValue()) { - Slot slot = menu.getSlot(2); - menu.setSelectionHint(i); - AutoTrade.sold += offer.getMaxUses(); - mc.player.connection.send(new ServerboundSelectTradePacket(i)); - try { - quickMoveResultSlot(mc, menu, slot.index); - } catch (Exception e) { - System.out.println("err " + e); - } - } - } - } - screen.onClose(); - inputInRange = false; - outputInRange = false; - return; - } - if (GuiUtils.getCurrentScreen() instanceof ShulkerBoxScreen sbs) { - ShulkerBoxMenu m = sbs.getMenu(); - if ((containerDelay == 0) && inputOpened) { - processInput(m, plInv); - sbs.onClose(); - } - if ((containerDelay == 0) && outputOpened) { - processOutput(m, plInv); - sbs.onClose(); - } - } else if (GuiUtils.getCurrentScreen() instanceof ContainerScreen cs) { - AbstractContainerMenu m = cs.getMenu(); - if ((containerDelay == 0) && inputOpened) { - processInput(m, plInv); - cs.onClose(); - } - if ((containerDelay == 0) && outputOpened) { - processOutput(m, plInv); - cs.onClose(); - } - } - boolean found = false; - Vector newVillagersInRange = new Vector<>(villagersInRange); - for (Entity entity : mc.level.entitiesForRendering()) { - if (entity instanceof Villager || entity instanceof WanderingTrader) { - if (entity.distanceToSqr(mc.player) < (2.5f * 2.5f)) { - if (!found) { - if (!newVillagersInRange.contains(entity)) { - found = true; - newVillagersInRange.add(entity); - EntityHitResult ehr = new EntityHitResult(entity, entity.position()); - mc.gameMode.interact(mc.player, entity, ehr, InteractionHand.MAIN_HAND); - voidDelay = Configs.Generic.VOID_TRADING_DELAY.getIntegerValue(); - villagerActive = entity.getId(); - state = false; - break; - } - } - } - } - } - for (Entity entity : villagersInRange) { - if (entity.distanceToSqr(mc.player) >= 16.0D) { - newVillagersInRange.remove(entity); - } - } - villagersInRange = newVillagersInRange; - if (found) { - return; - } - BlockPos input = new BlockPos(Configs.Generic.INPUT_CONTAINER_X.getIntegerValue(), - Configs.Generic.INPUT_CONTAINER_Y.getIntegerValue(), - Configs.Generic.INPUT_CONTAINER_Z.getIntegerValue()); - BlockPos output = new BlockPos(Configs.Generic.OUTPUT_CONTAINER_X.getIntegerValue(), - Configs.Generic.OUTPUT_CONTAINER_Y.getIntegerValue(), - Configs.Generic.OUTPUT_CONTAINER_Z.getIntegerValue()); - Vec3 ic = input.getCenter(); - Vec3 oc = output.getCenter(); - if ((mc.player.distanceToSqr(ic) < 16.0D) && (inputInRange == false)) { - inputInRange = true; - mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, - new BlockHitResult(ic, Direction.UP, input, false)); - containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue(); - inputOpened = true; - return; - } - if ((mc.player.distanceToSqr(oc) < 16.0D) && (outputInRange == false)) { - outputInRange = true; - mc.gameMode.useItemOn(mc.player, InteractionHand.MAIN_HAND, - new BlockHitResult(oc, Direction.UP, output, false)); - containerDelay = Configs.Generic.CONTAINER_CLOSE_DELAY.getIntegerValue(); - outputOpened = true; - return; - } - if (mc.player.distanceToSqr(ic) > 25.0D) { - inputOpened = false; - inputInRange = false; - } - if (mc.player.distanceToSqr(oc) > 25.0D) { - outputOpened = false; - outputInRange = false; - } - tickCount++; - if (tickCount > 200) { - tickCount = 0; - villagersInRange = new Vector<>(); - inputInRange = false; - outputInRange = false; - var cur = GuiUtils.getCurrentScreen(); - if (cur != null) { - if (cur instanceof MerchantScreen || cur instanceof ShulkerBoxScreen - || cur instanceof ContainerScreen) { - cur.onClose(); - } - } - } + clientTick.tick(mc); } } diff --git a/src/main/java/com/github/sebseb7/autotrade/util/TradeItemSpec.java b/src/main/java/com/github/sebseb7/autotrade/util/TradeItemSpec.java new file mode 100644 index 0000000..75e366e --- /dev/null +++ b/src/main/java/com/github/sebseb7/autotrade/util/TradeItemSpec.java @@ -0,0 +1,116 @@ +package com.github.sebseb7.autotrade.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.ItemEnchantments; + +/** + * Buy/sell config strings: {@code namespace:item_id} matches any stack of that + * item. For enchanted books (and other enchanted items), holding the item when + * binding the hotkey stores + * {@code minecraft:enchanted_book#minecraft:sharpness=4&minecraft:unbreaking=3} + * so only that exact enchantment set is matched. + */ +public final class TradeItemSpec { + private static final char SPEC_SEP = '#'; + + private TradeItemSpec() { + } + + public static String encodeFromStack(ItemStack stack) { + String base = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(); + ItemEnchantments enchants = enchantmentsForSpec(stack); + if (enchants == null || enchants.isEmpty()) { + return base; + } + List parts = new ArrayList<>(); + for (var e : enchants.entrySet()) { + String name = e.getKey().getRegisteredName(); + parts.add(name + "=" + e.getIntValue()); + } + Collections.sort(parts); + StringBuilder sb = new StringBuilder(base); + sb.append(SPEC_SEP); + for (int i = 0; i < parts.size(); i++) { + if (i > 0) { + sb.append('&'); + } + sb.append(parts.get(i)); + } + return sb.toString(); + } + + public static boolean matches(ItemStack stack, String spec) { + if (stack.isEmpty()) { + return false; + } + int sep = spec.indexOf(SPEC_SEP); + String itemPart = sep < 0 ? spec : spec.substring(0, sep); + if (!BuiltInRegistries.ITEM.getKey(stack.getItem()).toString().equals(itemPart)) { + return false; + } + if (sep < 0) { + return true; + } + Map expected = parseEnchantSection(spec.substring(sep + 1)); + if (expected == null) { + return false; + } + ItemEnchantments actual = enchantmentsForSpec(stack); + if (actual == null || actual.isEmpty()) { + return expected.isEmpty(); + } + if (actual.size() != expected.size()) { + return false; + } + for (var e : actual.entrySet()) { + String name = e.getKey().getRegisteredName(); + int level = e.getIntValue(); + Integer want = expected.get(name); + if (want == null || want != level) { + return false; + } + } + return true; + } + + private static ItemEnchantments enchantmentsForSpec(ItemStack stack) { + ItemEnchantments stored = stack.getOrDefault(DataComponents.STORED_ENCHANTMENTS, ItemEnchantments.EMPTY); + if (!stored.isEmpty()) { + return stored; + } + return stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); + } + + private static Map parseEnchantSection(String section) { + if (section.isEmpty()) { + return Map.of(); + } + Map out = new HashMap<>(); + for (String piece : section.split("&")) { + if (piece.isEmpty()) { + return null; + } + int eq = piece.lastIndexOf('='); + if (eq <= 0 || eq == piece.length() - 1) { + return null; + } + String enchantId = piece.substring(0, eq); + String levelStr = piece.substring(eq + 1); + int level; + try { + level = Integer.parseInt(levelStr); + } catch (NumberFormatException e) { + return null; + } + out.put(enchantId, level); + } + return out; + } +}