Skip to content

Commit a4246db

Browse files
committed
feat: backport merchant features to 1.21.1
1 parent 8fff063 commit a4246db

23 files changed

Lines changed: 1075 additions & 3 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,11 @@ runs/
123123
/node_modules/
124124

125125
.agents/
126+
.omc/
126127
.settings/
127128
.project
128129
.classpath
129130
skills-lock.json
130131

131132
AGENTS.md
133+
CLAUDE.md
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.euphony.better_client.client.events;
2+
3+
import com.euphony.better_client.config.BetterClientConfig;
4+
import com.euphony.better_client.utils.MerchantInfo;
5+
import com.euphony.better_client.utils.TradingUtils;
6+
import net.minecraft.client.Minecraft;
7+
import net.minecraft.client.multiplayer.ClientLevel;
8+
import net.minecraft.client.multiplayer.ClientPacketListener;
9+
import net.minecraft.client.player.LocalPlayer;
10+
import net.minecraft.network.protocol.game.ServerboundInteractPacket;
11+
import net.minecraft.world.InteractionHand;
12+
import net.minecraft.world.entity.Entity;
13+
14+
/**
15+
* Client-tick driven Trading HUD controller.
16+
* <p>
17+
* While no screen is open, probes the crosshair target each tick; on a new tradable
18+
* merchant we silently send a {@link ServerboundInteractPacket} so the server replies
19+
* with offers. The reply is intercepted by {@code ClientPacketListenerMixin} and stored
20+
* in {@link MerchantInfo#INSTANCE} without actually opening the merchant UI.
21+
*/
22+
public final class TradingHudEvent {
23+
24+
/** Cooldown between repeat interaction requests, in client ticks. */
25+
private static final int REQUEST_COOLDOWN_TICKS = 5;
26+
27+
private static boolean windowOpen = false;
28+
private static int interactionCooldown = 0;
29+
30+
private TradingHudEvent() {}
31+
32+
/**
33+
* Called once per client tick. Detects the merchant under the crosshair and triggers
34+
* a silent interaction when the target changes.
35+
*/
36+
public static void clientLevelPost(ClientLevel level) {
37+
BetterClientConfig config = BetterClientConfig.HANDLER.instance();
38+
if (!config.enableTradingHud || level == null) {
39+
return;
40+
}
41+
42+
if (interactionCooldown > 0) {
43+
interactionCooldown--;
44+
}
45+
46+
Minecraft minecraft = Minecraft.getInstance();
47+
LocalPlayer player = minecraft.player;
48+
49+
// Any active screen (including the trade screen itself) blocks probing.
50+
if (minecraft.screen != null || player == null) {
51+
return;
52+
}
53+
54+
Entity tradableEntity = TradingUtils.getCrosshairTradableEntity(minecraft, windowOpen);
55+
handleTradableEntity(tradableEntity, player);
56+
}
57+
58+
private static void handleTradableEntity(Entity entity, LocalPlayer player) {
59+
MerchantInfo merchantInfo = MerchantInfo.INSTANCE;
60+
61+
if (entity == null) {
62+
merchantInfo.setLastEntityId(null);
63+
return;
64+
}
65+
66+
// Same merchant we already have offers for — nothing to do.
67+
if (merchantInfo.isSameEntity(entity.getId())) {
68+
return;
69+
}
70+
71+
if (interactionCooldown > 0) {
72+
return;
73+
}
74+
75+
merchantInfo.reset();
76+
merchantInfo.setLastEntityId(entity.getId());
77+
interactionCooldown = REQUEST_COOLDOWN_TICKS;
78+
sendInteractionPacket(entity, player);
79+
}
80+
81+
private static void sendInteractionPacket(Entity entity, LocalPlayer player) {
82+
ClientPacketListener connection = player.connection;
83+
if (connection == null) {
84+
return;
85+
}
86+
ServerboundInteractPacket packet = ServerboundInteractPacket.createInteractionPacket(
87+
entity, player.isShiftKeyDown(), InteractionHand.MAIN_HAND);
88+
connection.send(packet);
89+
}
90+
91+
public static boolean isWindowOpen() {
92+
return windowOpen;
93+
}
94+
95+
public static void setWindowOpen(boolean open) {
96+
windowOpen = open;
97+
}
98+
99+
/**
100+
* Consumed by {@code ClientPacketListenerMixin} (Worker D) to decide whether to
101+
* suppress an incoming {@link net.minecraft.network.protocol.game.ClientboundOpenScreenPacket}
102+
* for a merchant the player did not actually choose to open.
103+
*/
104+
public static boolean shouldSkipOpenMerchantPacket() {
105+
return !windowOpen;
106+
}
107+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.euphony.better_client.client.renderer;
2+
3+
import com.euphony.better_client.config.BetterClientConfig;
4+
import com.euphony.better_client.config.option.TradingHudPos;
5+
import com.euphony.better_client.utils.MerchantInfo;
6+
import net.minecraft.client.DeltaTracker;
7+
import net.minecraft.client.Minecraft;
8+
import net.minecraft.client.gui.Font;
9+
import net.minecraft.client.gui.GuiGraphics;
10+
import net.minecraft.resources.ResourceLocation;
11+
import net.minecraft.util.CommonColors;
12+
import net.minecraft.world.entity.player.Player;
13+
import net.minecraft.world.item.ItemStack;
14+
import net.minecraft.world.item.trading.MerchantOffer;
15+
import net.minecraft.world.item.trading.MerchantOffers;
16+
17+
import java.util.List;
18+
19+
/**
20+
* Trading HUD renderer — draws up to 7 merchant offers on-screen while a tradable
21+
* villager / wandering trader is under the crosshair.
22+
*/
23+
public final class TradingHudRenderer {
24+
25+
private static final int MAX_VISIBLE_OFFERS = 7;
26+
private static final int HUD_WIDTH = 90;
27+
private static final int OFFER_HEIGHT = 20;
28+
private static final int EDGE_MARGIN = 5;
29+
private static final int REAL_COST_EXTRA_SPACE = 15;
30+
31+
private static final ResourceLocation TRADE_ARROW_OUT_OF_STOCK_SPRITE =
32+
ResourceLocation.withDefaultNamespace("container/villager/trade_arrow_out_of_stock");
33+
private static final ResourceLocation TRADE_ARROW_SPRITE =
34+
ResourceLocation.withDefaultNamespace("container/villager/trade_arrow");
35+
private static final ResourceLocation DISCOUNT_STRIKETHROUGH_SPRITE =
36+
ResourceLocation.withDefaultNamespace("container/villager/discount_strikethrough");
37+
38+
private TradingHudRenderer() {}
39+
40+
public static void renderHud(GuiGraphics graphics, DeltaTracker deltaTracker) {
41+
BetterClientConfig config = BetterClientConfig.HANDLER.instance();
42+
if (!config.enableTradingHud) return;
43+
44+
MerchantInfo merchantInfo = MerchantInfo.INSTANCE;
45+
if (merchantInfo.getLastEntityId().isEmpty()) return;
46+
47+
MerchantOffers offers = merchantInfo.getOffers();
48+
if (offers.isEmpty()) return;
49+
50+
Minecraft minecraft = Minecraft.getInstance();
51+
Player player = minecraft.player;
52+
if (player == null) return;
53+
54+
Font font = minecraft.font;
55+
List<String> enchantmentTexts = merchantInfo.getOfferEnchantmentTexts();
56+
57+
int screenWidth = minecraft.getWindow().getGuiScaledWidth();
58+
int screenHeight = minecraft.getWindow().getGuiScaledHeight();
59+
60+
int visibleOffers = Math.min(offers.size(), MAX_VISIBLE_OFFERS);
61+
int hudHeight = visibleOffers * OFFER_HEIGHT;
62+
63+
int i;
64+
int k;
65+
TradingHudPos pos = config.tradingHudPos;
66+
switch (pos) {
67+
case TOP_LEFT -> {
68+
i = 0;
69+
k = EDGE_MARGIN;
70+
}
71+
case TOP_RIGHT -> {
72+
i = screenWidth - HUD_WIDTH;
73+
k = EDGE_MARGIN;
74+
}
75+
case BOTTOM_LEFT -> {
76+
i = 0;
77+
k = screenHeight - hudHeight - EDGE_MARGIN;
78+
}
79+
case BOTTOM_RIGHT -> {
80+
i = screenWidth - HUD_WIDTH;
81+
k = screenHeight - hudHeight - EDGE_MARGIN;
82+
}
83+
default -> {
84+
i = config.tradingHudXOffset;
85+
k = EDGE_MARGIN + config.tradingHudYOffset;
86+
}
87+
}
88+
89+
int costAStartX = i + EDGE_MARGIN + EDGE_MARGIN;
90+
int extraSpace = config.renderRealCostDirectly ? 0 : REAL_COST_EXTRA_SPACE;
91+
92+
int rendered = 0;
93+
for (MerchantOffer offer : offers) {
94+
if (rendered >= MAX_VISIBLE_OFFERS) {
95+
break;
96+
}
97+
98+
ItemStack baseCostA = offer.getBaseCostA();
99+
ItemStack costA = offer.getCostA();
100+
ItemStack costB = offer.getCostB();
101+
ItemStack result = offer.getResult();
102+
103+
int rowY = k + 2;
104+
105+
renderAndDecorateCostA(graphics, font, costA, baseCostA, costAStartX, rowY, config.renderRealCostDirectly);
106+
107+
if (!costB.isEmpty()) {
108+
int costBX = i + EDGE_MARGIN + 25 + extraSpace;
109+
graphics.renderFakeItem(costB, costBX, rowY);
110+
graphics.renderItemDecorations(font, costB, costBX, rowY);
111+
}
112+
113+
renderTradeArrow(graphics, offer, i, rowY);
114+
115+
int resultX = i + EDGE_MARGIN + 58 + extraSpace;
116+
graphics.renderFakeItem(result, resultX, rowY);
117+
graphics.renderItemDecorations(font, result, resultX, rowY);
118+
119+
if (rendered < enchantmentTexts.size()) {
120+
String enchantmentText = enchantmentTexts.get(rendered);
121+
if (!enchantmentText.isEmpty()) {
122+
graphics.drawString(font, enchantmentText, i + 85 + extraSpace, rowY + 3, CommonColors.WHITE);
123+
}
124+
}
125+
126+
k += OFFER_HEIGHT;
127+
rendered++;
128+
}
129+
}
130+
131+
private static void renderTradeArrow(GuiGraphics graphics, MerchantOffer offer, int posX, int posY) {
132+
int extraSpace = BetterClientConfig.HANDLER.instance().renderRealCostDirectly ? 0 : REAL_COST_EXTRA_SPACE;
133+
ResourceLocation sprite = offer.isOutOfStock() ? TRADE_ARROW_OUT_OF_STOCK_SPRITE : TRADE_ARROW_SPRITE;
134+
graphics.blitSprite(sprite, posX + EDGE_MARGIN + 25 + 20 + extraSpace, posY + 3, 10, 9);
135+
}
136+
137+
private static void renderAndDecorateCostA(
138+
GuiGraphics graphics,
139+
Font font,
140+
ItemStack realCost,
141+
ItemStack baseCost,
142+
int x,
143+
int y,
144+
boolean renderRealCostDirectly) {
145+
graphics.renderFakeItem(realCost, x, y);
146+
if (baseCost.getCount() == realCost.getCount() || renderRealCostDirectly) {
147+
graphics.renderItemDecorations(font, realCost, x, y);
148+
} else {
149+
graphics.renderItemDecorations(font, baseCost, x, y, baseCost.getCount() == 1 ? "1" : null);
150+
graphics.renderItemDecorations(font, realCost, x + 14, y, realCost.getCount() == 1 ? "1" : null);
151+
graphics.blitSprite(DISCOUNT_STRIKETHROUGH_SPRITE, x + 7, y + 12, 9, 2);
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)