Files
autotrade/src/main/java/com/github/sebseb7/autotrade/event/AutoTradeMerchantScreenTick.java
2026-05-15 07:49:27 +02:00

171 lines
6.7 KiB
Java

package com.github.sebseb7.autotrade.event;
import com.github.sebseb7.autotrade.AutoTrade;
import com.github.sebseb7.autotrade.config.Configs;
import com.github.sebseb7.autotrade.render.VillagerTradeCache;
import com.github.sebseb7.autotrade.util.TradeItemSpec;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.inventory.MerchantScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.inventory.MerchantMenu;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.trading.MerchantOffers;
/**
* Villager GUI: one trade attempt per {@link #tick}, matching {@link MerchantOffers}
* recipe stacks, then deferring a normal pickup (not shift-click) from the result slot when
* it is an enchanted book (shift-click would chain other book trades); other results still use shift-click.
*/
final class AutoTradeMerchantScreenTick {
/** First tick after a trade decrements without acting; remaining ticks wait on the server. */
private static final int RESULT_QUICK_MOVE_DELAY_TICKS = 3;
private final AutoTradeTickState state;
AutoTradeMerchantScreenTick(AutoTradeTickState state) {
this.state = state;
}
void tickDeferredResultQuickMove(Minecraft mc) {
if (state.merchantResultQuickMoveDelayTicks <= 0) {
return;
}
state.merchantResultQuickMoveDelayTicks--;
if (state.merchantResultQuickMoveDelayTicks > 0) {
return;
}
if (!(mc.screen instanceof MerchantScreen screen)) {
AutoTrade.logger.warn("[AutoTrade merchant] defer execute aborted: current screen is not MerchantScreen");
state.clearMerchantQuickMoveDefer();
return;
}
MerchantMenu menu = screen.getMenu();
MerchantOffers offers = menu.getOffers();
int idx = state.merchantResultQuickMoveOfferIndex;
if (offers == null || idx < 0 || idx >= offers.size()) {
AutoTrade.logger.warn(
"[AutoTrade merchant] defer execute aborted: bad offer index idx={} offersSize={}",
idx,
offers == null ? -1 : offers.size());
state.clearMerchantQuickMoveDefer();
return;
}
menu.setSelectionHint(idx);
menu.tryMoveItems(idx);
var slot = menu.getSlot(2);
ItemStack slot2 = slot.getItem();
String buySpec = Configs.Generic.BUY_ITEM.getStringValue();
try {
if (state.merchantResultQuickMoveIsBuy) {
boolean match = !slot2.isEmpty() && TradeItemSpec.matches(slot2, buySpec);
if (match) {
ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index);
} else {
AutoTrade.logger.warn("[AutoTrade merchant] defer quickMove skipped: slot2 did not match buy spec");
}
} else {
if (!slot2.isEmpty()) {
ContainerIoHelper.quickMoveResultSlot(mc, menu, slot.index);
} else {
AutoTrade.logger.warn("[AutoTrade merchant] defer quickMove skipped: result slot empty");
}
}
} catch (Exception e) {
AutoTrade.logger.warn("[AutoTrade merchant] defer quickMove exception", e);
}
state.clearMerchantQuickMoveDefer();
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
}
void tick(Minecraft mc, MerchantScreen screen) {
MerchantMenu menu = screen.getMenu();
MerchantOffers offers = menu.getOffers();
int villagerActive = state.getVillagerActive();
cacheTraderOffers(mc, villagerActive, offers);
if (state.merchantResultQuickMoveDelayTicks > 0) {
return;
}
if (tryExecuteOneMerchantTrade(mc, menu, offers)) {
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
return;
}
finishMerchantSession(mc, screen);
}
private static void cacheTraderOffers(Minecraft mc, int villagerActive, MerchantOffers offers) {
Entity activeEntity = TraderInteractor.findEntityById(mc, villagerActive);
if (activeEntity != null && offers != null && !offers.isEmpty()) {
VillagerTradeCache.put(activeEntity.getUUID(), offers);
}
}
private boolean tryExecuteOneMerchantTrade(Minecraft mc, MerchantMenu menu, MerchantOffers offers) {
if (offers == null || offers.isEmpty()) {
return false;
}
String sellItemStr = Configs.Generic.SELL_ITEM.getStringValue();
String buyItemStr = Configs.Generic.BUY_ITEM.getStringValue();
boolean buyOn = Configs.Generic.ENABLE_BUY.getBooleanValue();
boolean sellOn = Configs.Generic.ENABLE_SELL.getBooleanValue();
int buyLimit = Configs.Generic.BUY_LIMIT.getIntegerValue();
int sellLimit = Configs.Generic.SELL_LIMIT.getIntegerValue();
for (int i = 0; i < offers.size(); i++) {
var offer = offers.get(i);
int tradesLeft = offer.getMaxUses() - offer.getUses();
boolean buyRecipe = buyOn && TradeItemSpec.matches(offer.getResult(), buyItemStr);
boolean buyCountOk = offer.getResult().getCount() <= buyLimit;
boolean costOk = TradeFormatHelper.playerHasMerchantCosts(mc.player, offer);
boolean sellRecipe = sellOn && TradeItemSpec.matches(offer.getCostA(), sellItemStr);
boolean sellCountOk = offer.getCostA().getCount() <= sellLimit;
if (buyOn && buyRecipe && buyCountOk) {
if (tradesLeft > 0 && costOk) {
menu.setSelectionHint(i);
mc.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSelectTradePacket(i));
AutoTrade.bought += offer.getMaxUses();
TradeFormatHelper.showTradeNotice(mc, "autotrade.message.trade_bought",
Component.literal(TradeFormatHelper.formatItemCountNameForTrades(offer.getResult(), tradesLeft)),
Component.literal(TradeFormatHelper.formatOfferPriceForTrades(offer, tradesLeft)));
state.merchantResultQuickMoveDelayTicks = RESULT_QUICK_MOVE_DELAY_TICKS;
state.merchantResultQuickMoveOfferIndex = i;
state.merchantResultQuickMoveIsBuy = true;
return true;
}
}
if (sellOn && sellRecipe && sellCountOk) {
if (tradesLeft > 0 && costOk) {
menu.setSelectionHint(i);
mc.player.connection.send(new net.minecraft.network.protocol.game.ServerboundSelectTradePacket(i));
AutoTrade.sold += offer.getMaxUses();
TradeFormatHelper.showTradeNotice(mc, "autotrade.message.trade_sold",
Component.literal(TradeFormatHelper.formatItemCountNameForTrades(offer.getCostA(), tradesLeft)
+ TradeFormatHelper.formatOptionalSecondCostForTrades(offer, tradesLeft)),
Component.literal(TradeFormatHelper.formatItemCountNameForTrades(offer.getResult(), tradesLeft)));
state.merchantResultQuickMoveDelayTicks = RESULT_QUICK_MOVE_DELAY_TICKS;
state.merchantResultQuickMoveOfferIndex = i;
state.merchantResultQuickMoveIsBuy = false;
return true;
}
}
}
return false;
}
private void finishMerchantSession(Minecraft mc, MerchantScreen screen) {
state.clearMerchantQuickMoveDefer();
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
screen.onClose();
ContainerIoHelper.syncPlayerInventoryAfterMerchant(mc);
state.postMerchantInventorySyncTicks = 15;
state.startTraderGlow(mc, state.getVillagerActive());
}
}