Display Trades
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user