Display Trades

This commit is contained in:
seb
2026-05-04 02:04:01 +02:00
parent 28aa0cb86b
commit 2ac51f4aff
5 changed files with 219 additions and 1 deletions

View File

@@ -4,6 +4,7 @@ import com.github.sebseb7.autotrade.config.Configs;
import com.github.sebseb7.autotrade.event.InputHandler;
import com.github.sebseb7.autotrade.event.KeybindCallbacks;
import com.github.sebseb7.autotrade.render.TraderHighlightRenderer;
import com.github.sebseb7.autotrade.render.VillagerTradeOverlayRenderer;
import fi.dy.masa.malilib.config.ConfigManager;
import fi.dy.masa.malilib.config.options.ConfigString;
import fi.dy.masa.malilib.event.InputEventHandler;
@@ -17,6 +18,7 @@ public class InitHandler implements IInitializationHandler {
ConfigManager.getInstance().registerConfigHandler(Reference.MOD_ID, new Configs());
TraderHighlightRenderer.register();
VillagerTradeOverlayRenderer.register();
InputHandler handler = new InputHandler();
InputEventHandler.getKeybindManager().registerKeybindProvider(handler);

View File

@@ -58,11 +58,14 @@ public class Configs implements IConfigHandler {
"true: Start the delay after the villager was unloaded; false: Start the delay after the trade has been initiated");
public static final ConfigInteger CONTAINER_CLOSE_DELAY = new ConfigInteger("containerCloseDelay", 0, 0,
30000000, "delay in ticks; to get signal from trapped chest");
public static final ConfigBoolean SHOW_TRADES = new ConfigBoolean("showTrades", true,
"Display villager/wandering-trader trades above their heads (requires trading with them once to cache the offers)");
public static final ImmutableList<IConfigValue> OPTIONS = ImmutableList.of(ENABLED, ITEM_FRAME, GLASS_BLOCK,
SELECTOR_OFFSET, ENABLE_SELL, SELL_ITEM, SELL_LIMIT, ENABLE_BUY, BUY_ITEM, BUY_LIMIT, MAX_INPUT_ITEMS,
INPUT_CONTAINER_X, INPUT_CONTAINER_Y, INPUT_CONTAINER_Z, OUTPUT_CONTAINER_X, OUTPUT_CONTAINER_Y,
OUTPUT_CONTAINER_Z, VOID_TRADING_DELAY, VOID_TRADING_DELAY_AFTER_TELEPORT, CONTAINER_CLOSE_DELAY);
OUTPUT_CONTAINER_Z, VOID_TRADING_DELAY, VOID_TRADING_DELAY_AFTER_TELEPORT, CONTAINER_CLOSE_DELAY,
SHOW_TRADES);
}
public static void loadFromFile() {

View File

@@ -2,6 +2,7 @@ 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 fi.dy.masa.malilib.gui.Message;
import fi.dy.masa.malilib.util.GuiUtils;
@@ -289,6 +290,12 @@ final class AutoTradeClientTick {
state = true;
MerchantMenu menu = screen.getMenu();
MerchantOffers offers = menu.getOffers();
// Cache offers for the in-world trade overlay.
Entity activeEntity = findEntityById(mc, villagerActive);
if (activeEntity != null && offers != null && !offers.isEmpty()) {
VillagerTradeCache.put(activeEntity.getUUID(), offers);
}
for (int i = 0; i < offers.size(); i++) {
MerchantOffer offer = offers.get(i);
int tradesLeft = offer.getMaxUses() - offer.getUses();

View File

@@ -0,0 +1,41 @@
package com.github.sebseb7.autotrade.render;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.world.item.trading.MerchantOffers;
/**
* Client-side cache of villager/wandering-trader trade offers, keyed by entity
* UUID.
*
* <p>Populated by {@code AutoTradeClientTick} when the mod opens a merchant
* screen; consumed by {@link VillagerTradeOverlayRenderer} to draw trade labels
* above each villager's head.
*/
public final class VillagerTradeCache {
private static final Map<UUID, MerchantOffers> CACHE = new ConcurrentHashMap<>();
private VillagerTradeCache() {
}
/** Store (or update) the offers we observed for a given entity. */
public static void put(UUID entityUuid, MerchantOffers offers) {
CACHE.put(entityUuid, offers);
}
/** Retrieve cached offers, or {@code null} if we haven't seen this entity trade yet. */
public static MerchantOffers get(UUID entityUuid) {
return CACHE.get(entityUuid);
}
/** Remove a single entry (e.g. when the entity leaves render distance). */
public static void remove(UUID entityUuid) {
CACHE.remove(entityUuid);
}
/** Drop every cached entry (e.g. on world change). */
public static void clear() {
CACHE.clear();
}
}

View File

@@ -0,0 +1,165 @@
package com.github.sebseb7.autotrade.render;
import com.github.sebseb7.autotrade.config.Configs;
import com.mojang.blaze3d.vertex.PoseStack;
import java.util.ArrayList;
import java.util.List;
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext;
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.npc.villager.Villager;
import net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader;
import net.minecraft.world.item.trading.MerchantOffer;
import net.minecraft.world.item.trading.MerchantOffers;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
/**
* Renders a compact summary of each villager's known trades above their head.
*
* <p>Trade data comes from {@link VillagerTradeCache}, which is populated when
* the mod opens a merchant screen. Villagers whose trades haven't been seen
* yet show nothing.
*/
public final class VillagerTradeOverlayRenderer {
/** Vertical gap between successive trade lines (in world-space blocks). */
private static final float LINE_SPACING = 0.25F;
/** World-space scale of the text (vanilla name-tags use ~0.025). */
private static final float TEXT_SCALE = 0.02F;
/** Text background colour (semi-transparent dark). */
private static final int BG_COLOR = 0x80000000;
/** Normal trade text colour (white). */
private static final int TEXT_COLOR = 0xFFFFFFFF;
/** Depleted trade text colour (grey/red). */
private static final int DEPLETED_COLOR = 0xFFFF6666;
private VillagerTradeOverlayRenderer() {
}
public static void register() {
LevelRenderEvents.AFTER_SOLID_FEATURES.register(VillagerTradeOverlayRenderer::render);
}
private static void render(LevelRenderContext context) {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null || mc.player == null) {
return;
}
if (!Configs.Generic.SHOW_TRADES.getBooleanValue()) {
return;
}
Font font = mc.font;
MultiBufferSource.BufferSource bufferSource = context.bufferSource();
Vec3 camera = mc.gameRenderer.getMainCamera().position();
float tickDelta = mc.getDeltaTracker().getGameTimeDeltaPartialTick(true);
for (Entity entity : mc.level.entitiesForRendering()) {
if (!(entity instanceof Villager) && !(entity instanceof WanderingTrader)) {
continue;
}
// Only render for villagers within a reasonable distance.
if (entity.distanceToSqr(mc.player) > 64.0 * 64.0) {
continue;
}
MerchantOffers offers = VillagerTradeCache.get(entity.getUUID());
if (offers == null || offers.isEmpty()) {
continue;
}
// Build compact trade lines: "CostA [+ CostB] → Result (uses/max)"
List<TradeLineEntry> lines = buildTradeLines(offers);
if (lines.isEmpty()) {
continue;
}
// Interpolated entity position relative to camera.
double x = Mth.lerp(tickDelta, entity.xOld, entity.getX()) - camera.x;
double y = Mth.lerp(tickDelta, entity.yOld, entity.getY()) - camera.y;
double z = Mth.lerp(tickDelta, entity.zOld, entity.getZ()) - camera.z;
// Place the first line above the entity's head (entity height + small gap).
float baseY = entity.getBbHeight() + 0.6F;
PoseStack poseStack = new PoseStack();
poseStack.pushPose();
poseStack.translate(x, y + baseY, z);
// Face the camera (billboard).
poseStack.mulPose(mc.gameRenderer.getMainCamera().rotation());
poseStack.scale(-TEXT_SCALE, -TEXT_SCALE, TEXT_SCALE);
// Draw lines from top (highest index) to bottom (index 0).
for (int i = 0; i < lines.size(); i++) {
TradeLineEntry entry = lines.get(i);
float lineOffsetY = -(lines.size() - 1 - i) * (font.lineHeight + 2);
Matrix4f matrix = poseStack.last().pose();
matrix = new Matrix4f(matrix);
matrix.translate(0, lineOffsetY, 0);
int textWidth = font.width(entry.text);
float textX = -textWidth / 2.0F;
// Background
font.drawInBatch(entry.text, textX, 0, entry.color, false, matrix, bufferSource,
Font.DisplayMode.SEE_THROUGH, BG_COLOR, 0xF000F0);
// Foreground
font.drawInBatch(entry.text, textX, 0, entry.color, false, matrix, bufferSource,
Font.DisplayMode.NORMAL, 0, 0xF000F0);
}
poseStack.popPose();
}
}
private static List<TradeLineEntry> buildTradeLines(MerchantOffers offers) {
List<TradeLineEntry> lines = new ArrayList<>();
for (int i = 0; i < offers.size(); i++) {
MerchantOffer offer = offers.get(i);
StringBuilder sb = new StringBuilder();
// Cost A
if (!offer.getCostA().isEmpty()) {
sb.append(offer.getCostA().getCount()).append("× ")
.append(offer.getCostA().getHoverName().getString());
}
// Cost B (optional)
if (!offer.getCostB().isEmpty()) {
if (sb.length() > 0) {
sb.append(" + ");
}
sb.append(offer.getCostB().getCount()).append("× ")
.append(offer.getCostB().getHoverName().getString());
}
sb.append("");
// Result
sb.append(offer.getResult().getCount()).append("× ")
.append(offer.getResult().getHoverName().getString());
// Remaining uses
int remaining = offer.getMaxUses() - offer.getUses();
sb.append(" (").append(remaining).append("/").append(offer.getMaxUses()).append(")");
boolean depleted = remaining <= 0;
lines.add(new TradeLineEntry(sb.toString(), depleted ? DEPLETED_COLOR : TEXT_COLOR));
}
return lines;
}
private record TradeLineEntry(String text, int color) {
}
}