moved settings to merchnat screen

This commit is contained in:
seb
2026-05-14 07:01:23 +02:00
parent a4474870a0
commit 31b1c6b6bb
7 changed files with 223 additions and 26 deletions

View File

@@ -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

View File

@@ -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());

View File

@@ -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);

View File

@@ -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();

View File

@@ -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;

View File

@@ -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}.
*
* <p>{@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();
}

View File

@@ -4,7 +4,8 @@
"package": "com.github.sebseb7.autotrade.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"MultiPlayerGameModeInvoker"
"MultiPlayerGameModeInvoker",
"MerchantMenuAccessor"
],
"injectors": {
"defaultRequire": 1