diff --git a/gradle.properties b/gradle.properties index d0c1013..ff7ec13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,4 @@ mod_group=com.github.sebseb7.autotrade author=sebseb7 mod_file_name=autotrade-fabric -mod_version=0.0.14 +mod_version=0.0.15 diff --git a/src/main/java/com/github/sebseb7/autotrade/AutoTrade.java b/src/main/java/com/github/sebseb7/autotrade/AutoTrade.java index 8af15b0..2e37d71 100644 --- a/src/main/java/com/github/sebseb7/autotrade/AutoTrade.java +++ b/src/main/java/com/github/sebseb7/autotrade/AutoTrade.java @@ -11,6 +11,13 @@ public class AutoTrade implements ModInitializer { public static int sold = 0; public static int bought = 0; + /** + * Set to {@code true} while the auto-trader is firing its synthetic + * villager interact packets so {@link InitHandler}'s {@code UseEntityCallback} + * can tell automated clicks apart from a real player right-click. + */ + public static boolean autoInteracting = false; + @Override public void onInitialize() { InitializationHandler.getInstance().registerInitializationHandler(new InitHandler()); diff --git a/src/main/java/com/github/sebseb7/autotrade/InitHandler.java b/src/main/java/com/github/sebseb7/autotrade/InitHandler.java index 2395212..7421f26 100644 --- a/src/main/java/com/github/sebseb7/autotrade/InitHandler.java +++ b/src/main/java/com/github/sebseb7/autotrade/InitHandler.java @@ -10,8 +10,20 @@ import fi.dy.masa.malilib.config.ConfigManager; import fi.dy.masa.malilib.config.options.ConfigString; import fi.dy.masa.malilib.event.InputEventHandler; import fi.dy.masa.malilib.event.TickHandler; +import fi.dy.masa.malilib.gui.Message; import fi.dy.masa.malilib.interfaces.IInitializationHandler; import fi.dy.masa.malilib.interfaces.IValueChangeCallback; +import fi.dy.masa.malilib.util.InfoUtils; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; +//? if npcSplit { +import net.minecraft.world.entity.npc.villager.Villager; +import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader; +//?} +//? if npcFlat { +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.WanderingTrader; +//?} +import net.minecraft.world.InteractionResult; public class InitHandler implements IInitializationHandler { @Override @@ -29,6 +41,20 @@ public class InitHandler implements IInitializationHandler { KeybindCallbacks.getInstance().setCallbacks(); + // A real right-click on a villager/wandering trader cancels the global + // auto-trade switch — but skip the synthetic interact packets the mod + // itself emits in AutoTradeClientTick (guarded by AutoTrade.autoInteracting). + UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (!AutoTrade.autoInteracting && player.level().isClientSide() + && (entity instanceof Villager || entity instanceof WanderingTrader) + && Configs.Generic.ENABLED.getBooleanValue()) { + Configs.Generic.ENABLED.setBooleanValue(false); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.toggled_mod_off"); + Configs.saveToFile(); + } + return InteractionResult.PASS; + }); + ValueChangeCallback valueChangeCallback = new ValueChangeCallback(); Configs.Generic.SELL_ITEM.setValueChangeCallback(valueChangeCallback); Configs.Generic.BUY_ITEM.setValueChangeCallback(valueChangeCallback); diff --git a/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java b/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java index 3abc05e..e8db627 100644 --- a/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java +++ b/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeClientTick.java @@ -171,12 +171,17 @@ final class AutoTradeClientTick { mc.player.setXRot(pitch); mc.player.yHeadRot = yaw; EntityHitResult ehr = new EntityHitResult(entity, targetEye); - //? if mc26 { - mc.gameMode.interact(mc.player, entity, ehr, InteractionHand.MAIN_HAND); - //?} else { - mc.gameMode.interactAt(mc.player, entity, ehr, InteractionHand.MAIN_HAND); - mc.gameMode.interact(mc.player, entity, InteractionHand.MAIN_HAND); - //?} + AutoTrade.autoInteracting = true; + try { + //? if mc26 { + mc.gameMode.interact(mc.player, entity, ehr, InteractionHand.MAIN_HAND); + //?} else { + mc.gameMode.interactAt(mc.player, entity, ehr, InteractionHand.MAIN_HAND); + mc.gameMode.interact(mc.player, entity, InteractionHand.MAIN_HAND); + //?} + } finally { + AutoTrade.autoInteracting = false; + } mc.player.swing(InteractionHand.MAIN_HAND); postMerchantInventorySyncTicks = 0; voidDelay = Configs.Generic.VOID_TRADING_DELAY.getIntegerValue(); diff --git a/src/main/java/com/github/sebseb7/autotrade/gui/MerchantScreenButtonInjector.java b/src/main/java/com/github/sebseb7/autotrade/gui/MerchantScreenButtonInjector.java index cdfbcdb..a41b770 100644 --- a/src/main/java/com/github/sebseb7/autotrade/gui/MerchantScreenButtonInjector.java +++ b/src/main/java/com/github/sebseb7/autotrade/gui/MerchantScreenButtonInjector.java @@ -1,6 +1,12 @@ package com.github.sebseb7.autotrade.gui; +import com.github.sebseb7.autotrade.config.Configs; +import com.github.sebseb7.autotrade.mixin.MerchantMenuAccessor; import com.github.sebseb7.autotrade.render.VillagerTradeCache; +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.util.InfoUtils; import java.util.List; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.fabric.api.client.screen.v1.Screens; @@ -18,6 +24,10 @@ import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader; import net.minecraft.world.entity.npc.Villager; import net.minecraft.world.entity.npc.WanderingTrader; //?} +import net.minecraft.world.inventory.MerchantMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.item.trading.MerchantOffers; import net.minecraft.world.phys.AABB; @@ -35,37 +45,165 @@ public final class MerchantScreenButtonInjector { return; } - // Add button during AFTER_INIT so it is properly registered as renderable. - // We position it safely to the right of the merchant GUI. - // The merchant GUI is 276 pixels wide and centered. - Button button = Button - .builder(Component.literal("Select Enchantments"), - btn -> client.setScreen(new EnchantmentSelectionScreen(merchantScreen))) - .bounds(scaledWidth / 2 + 140, scaledHeight / 2 - 83, 120, 20).build(); + // Position buttons safely to the right of the merchant GUI (276 px wide, centered). + int x = scaledWidth / 2 + 140; + int y = scaledHeight / 2 - 83; + int w = 160; + int h = 20; + int gap = 2; + + Button openSettings = Button + .builder(Component.literal("Open Settings"), btn -> GuiBase.openGui(new GuiConfigs())) + .bounds(x, y, w, h).build(); + + Button selectSell = Button + .builder(sellButtonLabel(), btn -> applySelectedTradeAsSell(client, merchantScreen)) + .bounds(x, y + (h + gap), w, h).build(); + + Button selectBuy = Button + .builder(buyButtonLabel(), btn -> applySelectedTradeAsBuy(client, merchantScreen)) + .bounds(x, y + 2 * (h + gap), w, h).build(); + + Button enableAutotrade = Button + .builder(autotradeButtonLabel(), btn -> toggleAutotrade(btn)) + .bounds(x, y + 3 * (h + gap), w, h).build(); + Screen asScreen = merchantScreen; //? if mc26 { - Screens.getWidgets(asScreen).add(button); + Screens.getWidgets(asScreen).add(openSettings); + Screens.getWidgets(asScreen).add(selectSell); + Screens.getWidgets(asScreen).add(selectBuy); + Screens.getWidgets(asScreen).add(enableAutotrade); //?} else { - Screens.getButtons(asScreen).add(button); + Screens.getButtons(asScreen).add(openSettings); + Screens.getButtons(asScreen).add(selectSell); + Screens.getButtons(asScreen).add(selectBuy); + Screens.getButtons(asScreen).add(enableAutotrade); //?} // Offers arrive via a server packet after the screen opens. - // Register a per-screen tick handler to wait for offers and cache them. - final boolean[] handled = {false}; + // Register a per-screen tick handler to wait for offers, cache them, + // and refresh dynamic button state (active/label). + final boolean[] cached = {false}; ScreenEvents.afterTick(merchantScreen).register(s -> { - if (handled[0]) { - return; - } MerchantOffers offers = merchantScreen.getMenu().getOffers(); - if (offers == null || offers.isEmpty()) { - return; // not yet synced + if (!cached[0] && offers != null && !offers.isEmpty()) { + cached[0] = true; + cacheOffersForNearestTrader(client, offers); } - handled[0] = true; - cacheOffersForNearestTrader(client, offers); + MerchantOffer current = currentSelectedOffer(merchantScreen); + selectSell.active = current != null && isSellOffer(current); + selectBuy.active = current != null && isBuyOffer(current); + selectSell.setMessage(sellButtonLabel()); + selectBuy.setMessage(buyButtonLabel()); + enableAutotrade.setMessage(autotradeButtonLabel()); }); } + private static Component autotradeButtonLabel() { + boolean on = Configs.Generic.ENABLED.getBooleanValue(); + return Component.literal(on ? "Disable Autotrade" : "Enable Autotrade"); + } + + private static Component sellButtonLabel() { + String enabled = Configs.Generic.ENABLE_SELL.getBooleanValue() ? "" : " (off)"; + return Component.literal("Sell: " + describeSpec(Configs.Generic.SELL_ITEM.getStringValue()) + enabled); + } + + private static Component buyButtonLabel() { + String enabled = Configs.Generic.ENABLE_BUY.getBooleanValue() ? "" : " (off)"; + return Component.literal("Buy: " + describeSpec(Configs.Generic.BUY_ITEM.getStringValue()) + enabled); + } + + /** + * Compact human-readable form of a {@link com.github.sebseb7.autotrade.util.TradeItemSpec} + * string. Strips the {@code minecraft:} namespace and renders enchant + * suffixes after a {@code +}, e.g. {@code minecraft:enchanted_book#minecraft:mending=1} + * → {@code enchanted_book +mending=1}. + */ + private static String describeSpec(String spec) { + if (spec == null || spec.isEmpty()) { + return "(none)"; + } + int sep = spec.indexOf('#'); + String itemPart = sep < 0 ? spec : spec.substring(0, sep); + String name = stripVanillaNs(itemPart); + if (sep < 0) { + return name; + } + String enchants = spec.substring(sep + 1).replace("minecraft:", ""); + return name + " +" + enchants; + } + + private static String stripVanillaNs(String id) { + return id.startsWith("minecraft:") ? id.substring("minecraft:".length()) : id; + } + + private static void toggleAutotrade(Button btn) { + Configs.Generic.ENABLED.toggleBooleanValue(); + boolean enabled = Configs.Generic.ENABLED.getBooleanValue(); + btn.setMessage(autotradeButtonLabel()); + String msg = enabled ? "autotrade.message.toggled_mod_on" : "autotrade.message.toggled_mod_off"; + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, msg); + Configs.saveToFile(); + } + + private static MerchantOffer currentSelectedOffer(MerchantScreen screen) { + MerchantMenu menu = screen.getMenu(); + MerchantOffers offers = menu.getOffers(); + if (offers == null || offers.isEmpty()) { + return null; + } + int idx = ((MerchantMenuAccessor) (Object) screen).getShopItem(); + if (idx < 0 || idx >= offers.size()) { + return null; + } + return offers.get(idx); + } + + /** A "sell" trade pays the player in emeralds for an item. */ + private static boolean isSellOffer(MerchantOffer offer) { + return offer.getResult().is(Items.EMERALD) && !offer.getCostA().isEmpty() + && !offer.getCostA().is(Items.EMERALD); + } + + /** A "buy" trade gives the player an item in exchange for emeralds. */ + private static boolean isBuyOffer(MerchantOffer offer) { + if (offer.getResult().isEmpty() || offer.getResult().is(Items.EMERALD)) { + return false; + } + return offer.getCostA().is(Items.EMERALD) || offer.getCostB().is(Items.EMERALD); + } + + private static void applySelectedTradeAsSell(Minecraft client, MerchantScreen screen) { + MerchantOffer offer = currentSelectedOffer(screen); + if (offer == null || !isSellOffer(offer)) { + return; + } + ItemStack item = offer.getCostA(); + String spec = TradeItemSpec.encodeFromStack(item); + Configs.Generic.SELL_ITEM.setValueFromString(spec); + Configs.Generic.SELL_LIMIT.setIntegerValue(item.getCount()); + Configs.Generic.ENABLE_SELL.setBooleanValue(true); + Configs.saveToFile(); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.sell_item_set", spec); + } + + private static void applySelectedTradeAsBuy(Minecraft client, MerchantScreen screen) { + MerchantOffer offer = currentSelectedOffer(screen); + if (offer == null || !isBuyOffer(offer)) { + return; + } + ItemStack item = offer.getResult(); + String spec = TradeItemSpec.encodeFromStack(item); + Configs.Generic.BUY_ITEM.setValueFromString(spec); + Configs.Generic.BUY_LIMIT.setIntegerValue(item.getCount()); + Configs.Generic.ENABLE_BUY.setBooleanValue(true); + Configs.saveToFile(); + InfoUtils.showGuiOrInGameMessage(Message.MessageType.INFO, "autotrade.message.buy_item_set", spec); + } + private static void cacheOffersForNearestTrader(Minecraft mc, MerchantOffers offers) { if (mc.player == null || mc.level == null) { return; diff --git a/src/main/java/com/github/sebseb7/autotrade/mixin/MerchantMenuAccessor.java b/src/main/java/com/github/sebseb7/autotrade/mixin/MerchantMenuAccessor.java new file mode 100644 index 0000000..835fc69 --- /dev/null +++ b/src/main/java/com/github/sebseb7/autotrade/mixin/MerchantMenuAccessor.java @@ -0,0 +1,20 @@ +package com.github.sebseb7.autotrade.mixin; + +import net.minecraft.client.gui.screens.inventory.MerchantScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * Exposes the currently-selected trade index from {@link MerchantScreen}. + * + *
{@code MerchantMenu#selectionHint} is server-side hint state and is not + * persisted on the client in newer Minecraft versions, so the only reliable + * client source of truth is the screen's {@code shopItem} field which is + * updated by the trade buttons. + */ +@Mixin(MerchantScreen.class) +public interface MerchantMenuAccessor { + + @Accessor("shopItem") + int getShopItem(); +} diff --git a/src/main/resources/autotrade.mixins.json b/src/main/resources/autotrade.mixins.json index 69598c4..739a508 100644 --- a/src/main/resources/autotrade.mixins.json +++ b/src/main/resources/autotrade.mixins.json @@ -4,7 +4,8 @@ "package": "com.github.sebseb7.autotrade.mixin", "compatibilityLevel": "JAVA_21", "client": [ - "MultiPlayerGameModeInvoker" + "MultiPlayerGameModeInvoker", + "MerchantMenuAccessor" ], "injectors": { "defaultRequire": 1