moved settings to merchnat screen
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"package": "com.github.sebseb7.autotrade.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"MultiPlayerGameModeInvoker"
|
||||
"MultiPlayerGameModeInvoker",
|
||||
"MerchantMenuAccessor"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
Reference in New Issue
Block a user