stable build - trading works end to end with the /trade debug init with the full trade workflow in place.

This commit is contained in:
trunksbomb
2026-03-24 20:02:17 -04:00
parent 456913cea0
commit ce3e83b928
6 changed files with 176 additions and 61 deletions

View File

@@ -6,6 +6,8 @@ import com.trunksbomb.trade.mod.trade.DebugControlAction;
import com.trunksbomb.trade.mod.trade.TradeAction;
import com.trunksbomb.trade.mod.trade.TradeStage;
import com.trunksbomb.trade.mod.trade.TradeView;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
@@ -30,6 +32,8 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
private static final int OFFER_START_Y = 18;
private static final int INVENTORY_START_X = 62;
private static final int INVENTORY_START_Y = 139;
private static final int BANNER_X = 58;
private static final int BANNER_Y = 6;
private static final int CENTER_COLUMN_X = 115;
private static final int STATUS_LABEL_Y = 54;
private static final int ACCEPT_BUTTON_Y = 74;
@@ -40,10 +44,7 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
private static final int DEBUG_BUTTON_WIDTH = 86;
private static final int DEBUG_BUTTON_HEIGHT = 20;
private static final int DEBUG_BUTTON_GAP = 4;
private static final int CONFIRM_PANEL_X = 89;
private static final int CONFIRM_PANEL_Y = 77;
private static final int CONFIRM_PANEL_WIDTH = 106;
private static final int CONFIRM_PANEL_HEIGHT = 64;
private ContextMenu contextMenu;
public TradeScreen(TradeMenu menu, Inventory inventory, Component title) {
super(menu, inventory, title);
@@ -64,13 +65,13 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
@Override
protected void init() {
super.init();
titleLabelX = LEFT_OFFER_START_X;
titleLabelY = 6;
titleLabelX = BANNER_X;
titleLabelY = BANNER_Y;
addRenderableWidget(Button.builder(Component.literal("Accept"), button -> sendAction(TradeAction.ACCEPT, -1, false))
addRenderableWidget(Button.builder(Component.literal("Accept"), button -> sendAction(TradeAction.ACCEPT, -1, 1))
.bounds(leftPos + CENTER_COLUMN_X + 3, topPos + ACCEPT_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT)
.build());
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> sendAction(TradeAction.DECLINE, -1, false))
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> sendAction(TradeAction.DECLINE, -1, 1))
.bounds(leftPos + CENTER_COLUMN_X + 3, topPos + CANCEL_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT)
.build());
@@ -103,7 +104,7 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
@Override
public void onClose() {
sendAction(TradeAction.CLOSE, -1, false);
sendAction(TradeAction.CLOSE, -1, 1);
if (minecraft != null) {
minecraft.setScreen(null);
}
@@ -111,12 +112,23 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (contextMenu != null) {
if (handleContextMenuClick(mouseX, mouseY)) {
return true;
}
contextMenu = null;
}
if (menu.view().stage() == TradeStage.CONFIRMING) {
return super.mouseClicked(mouseX, mouseY, button);
}
if (hoveredSlot instanceof SelfOfferSlot selfOfferSlot) {
sendAction(TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), button == 1);
if (button == 1) {
openContextMenu(mouseX, mouseY, TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), hoveredSlot.getItem(), "Remove");
} else {
sendAction(TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), 1);
}
return true;
}
@@ -125,7 +137,11 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
}
if (hoveredSlot != null && hoveredSlot.container == menu.playerInventory()) {
sendAction(TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), button == 1);
if (button == 1) {
openContextMenu(mouseX, mouseY, TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), hoveredSlot.getItem(), "Trade");
} else {
sendAction(TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), 1);
}
return true;
}
@@ -135,37 +151,34 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
@Override
protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) {
guiGraphics.blit(TEXTURE, leftPos, topPos, 0, 0, imageWidth, imageHeight, imageWidth, imageHeight);
if (menu.view().stage() == TradeStage.CONFIRMING) {
renderConfirmationOverlay(guiGraphics);
}
}
@Override
protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) {
guiGraphics.drawString(font, menu.view().selfName(), titleLabelX, titleLabelY, 0x404040, false);
guiGraphics.drawString(font, menu.view().otherName(), RIGHT_OFFER_START_X, 6, 0x404040, false);
guiGraphics.drawString(font, "Trading with " + menu.view().otherName(), titleLabelX, titleLabelY, 0x404040, false);
guiGraphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
if (menu.view().otherAccepted()) {
if (menu.view().stage() == TradeStage.CONFIRMING) {
drawScaledCenteredColumnText(guiGraphics, "Are you", STATUS_LABEL_Y, 0xB02020, 0.7F);
drawScaledCenteredColumnText(guiGraphics, "sure you want", STATUS_LABEL_Y + 7, 0xB02020, 0.7F);
drawScaledCenteredColumnText(guiGraphics, "to trade?", STATUS_LABEL_Y + 14, 0xB02020, 0.7F);
} else if (menu.view().otherAccepted()) {
drawScaledCenteredColumnText(guiGraphics, "Other player", STATUS_LABEL_Y, 0x2E7D32, 0.7F);
drawScaledCenteredColumnText(guiGraphics, "has accepted", STATUS_LABEL_Y + 7, 0x2E7D32, 0.7F);
}
if (menu.view().stage() == TradeStage.CONFIRMING) {
drawCenteredColumnText(guiGraphics, "FINAL", 16, 0x7A4F00);
drawCenteredColumnText(guiGraphics, "CONFIRM", 28, 0x7A4F00);
}
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
super.render(guiGraphics, mouseX, mouseY, partialTick);
renderContextMenu(guiGraphics, mouseX, mouseY);
renderTooltip(guiGraphics, mouseX, mouseY);
}
private void sendAction(TradeAction action, int slot, boolean secondary) {
private void sendAction(TradeAction action, int slot, int amount) {
if (minecraft == null || minecraft.getConnection() == null) {
return;
}
PacketDistributor.sendToServer(new TradeActionPayload(menu.view().sessionId(), action, slot, secondary));
PacketDistributor.sendToServer(new TradeActionPayload(menu.view().sessionId(), action, slot, amount));
}
private void sendDebug(DebugControlAction action) {
@@ -175,17 +188,6 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
PacketDistributor.sendToServer(new DebugTradeControlPayload(menu.view().sessionId(), action, ""));
}
private void renderConfirmationOverlay(GuiGraphics guiGraphics) {
int panelLeft = leftPos + CONFIRM_PANEL_X;
int panelTop = topPos + CONFIRM_PANEL_Y;
guiGraphics.fill(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight, 0x22000000);
guiGraphics.fill(panelLeft, panelTop, panelLeft + CONFIRM_PANEL_WIDTH, panelTop + CONFIRM_PANEL_HEIGHT, 0xDDF7D48C);
guiGraphics.fill(panelLeft + 1, panelTop + 1, panelLeft + CONFIRM_PANEL_WIDTH - 1, panelTop + CONFIRM_PANEL_HEIGHT - 1, 0xAAFFF4D6);
guiGraphics.drawString(font, Component.literal("Confirm Trade"), panelLeft + 11, panelTop + 9, 0x5B3A00, false);
guiGraphics.drawString(font, Component.literal("Review items"), panelLeft + 18, panelTop + 24, 0x5B3A00, false);
guiGraphics.drawString(font, Component.literal("No more changes"), panelLeft + 9, panelTop + 36, 0x5B3A00, false);
}
private void drawCenteredColumnText(GuiGraphics guiGraphics, String text, int y, int color) {
int textX = CENTER_COLUMN_X + (ACTION_BUTTON_WIDTH / 2) - (font.width(text) / 2);
guiGraphics.drawString(font, text, textX, y, color, false);
@@ -202,6 +204,115 @@ public class TradeScreen extends AbstractContainerScreen<TradeScreen.TradeMenu>
pose.popPose();
}
private void openContextMenu(double mouseX, double mouseY, TradeAction action, int slot, ItemStack stack, String verb) {
if (stack.isEmpty()) {
return;
}
List<ContextOption> options = new ArrayList<>();
for (int amount : contextAmounts(stack.getMaxStackSize())) {
options.add(new ContextOption(verb + " " + amount, amount));
}
options.add(new ContextOption(verb + " X", -1));
contextMenu = new ContextMenu(action, slot, options, (int) mouseX, (int) mouseY);
}
private boolean handleContextMenuClick(double mouseX, double mouseY) {
if (contextMenu == null) {
return false;
}
int menuX = contextMenuX();
int menuY = contextMenuY();
int menuWidth = contextMenuWidth();
int optionHeight = 12;
int menuHeight = contextMenu.options().size() * optionHeight + 4;
if (mouseX < menuX || mouseX >= menuX + menuWidth || mouseY < menuY || mouseY >= menuY + menuHeight) {
return false;
}
int index = ((int) mouseY - menuY - 2) / optionHeight;
if (index < 0 || index >= contextMenu.options().size()) {
return true;
}
ContextOption option = contextMenu.options().get(index);
if (option.amount() < 0) {
if (minecraft != null && minecraft.player != null) {
minecraft.player.displayClientMessage(Component.literal(option.label() + " is not implemented yet."), true);
}
} else {
sendAction(contextMenu.action(), contextMenu.slot(), option.amount());
}
contextMenu = null;
return true;
}
private void renderContextMenu(GuiGraphics guiGraphics, int mouseX, int mouseY) {
if (contextMenu == null) {
return;
}
var pose = guiGraphics.pose();
pose.pushPose();
pose.translate(0.0F, 0.0F, 400.0F);
int menuX = contextMenuX();
int menuY = contextMenuY();
int menuWidth = contextMenuWidth();
int optionHeight = 12;
int menuHeight = contextMenu.options().size() * optionHeight + 4;
guiGraphics.fill(menuX, menuY, menuX + menuWidth, menuY + menuHeight, 0xF0101010);
guiGraphics.fill(menuX + 1, menuY + 1, menuX + menuWidth - 1, menuY + menuHeight - 1, 0xFF2B2B2B);
for (int i = 0; i < contextMenu.options().size(); i++) {
int optionY = menuY + 2 + i * optionHeight;
boolean hovered = mouseX >= menuX + 2 && mouseX < menuX + menuWidth - 2 && mouseY >= optionY && mouseY < optionY + optionHeight;
if (hovered) {
guiGraphics.fill(menuX + 2, optionY, menuX + menuWidth - 2, optionY + optionHeight, 0xFF5B6EE1);
}
guiGraphics.drawString(font, contextMenu.options().get(i).label(), menuX + 6, optionY + 2, 0xFFFFFF, false);
}
pose.popPose();
}
private int contextMenuX() {
int width = contextMenuWidth();
return Math.min(contextMenu.mouseX(), this.width - width - 4);
}
private int contextMenuY() {
int height = contextMenu.options().size() * 12 + 4;
return Math.min(contextMenu.mouseY(), this.height - height - 4);
}
private int contextMenuWidth() {
int width = 0;
for (ContextOption option : contextMenu.options()) {
width = Math.max(width, font.width(option.label()));
}
return width + 12;
}
private static int[] contextAmounts(int maxStackSize) {
if (maxStackSize == 1) {
return new int[] {1};
}
if (maxStackSize == 16) {
return new int[] {1, 2, 4, 8, 16};
}
if (maxStackSize == 32) {
return new int[] {1, 4, 8, 16, 32};
}
return new int[] {1, 8, 16, 32, 64};
}
private record ContextMenu(TradeAction action, int slot, List<ContextOption> options, int mouseX, int mouseY) {}
private record ContextOption(String label, int amount) {}
public static class TradeMenu extends AbstractContainerMenu {
private static final int OFFER_COLUMNS = 6;
private static final int OFFER_ROWS = 6;

View File

@@ -8,7 +8,7 @@ import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, boolean secondary) implements CustomPacketPayload {
public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, int amount) implements CustomPacketPayload {
public static final Type<TradeActionPayload> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TradeMod.MODID, "trade_action"));
public static final StreamCodec<RegistryFriendlyByteBuf, TradeActionPayload> STREAM_CODEC = new StreamCodec<>() {
@Override
@@ -16,12 +16,12 @@ public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, b
buf.writeUUID(value.sessionId);
buf.writeEnum(value.action);
buf.writeVarInt(value.slot);
buf.writeBoolean(value.secondary);
buf.writeVarInt(value.amount);
}
@Override
public TradeActionPayload decode(RegistryFriendlyByteBuf buf) {
return new TradeActionPayload(buf.readUUID(), buf.readEnum(TradeAction.class), buf.readVarInt(), buf.readBoolean());
return new TradeActionPayload(buf.readUUID(), buf.readEnum(TradeAction.class), buf.readVarInt(), buf.readVarInt());
}
};

View File

@@ -61,7 +61,7 @@ public final class TradeNetworking {
private static void handleTradeActionServer(TradeActionPayload payload, IPayloadContext context) {
if (context.player() instanceof ServerPlayer player) {
TradeManager.get(player.server).handleAction(player, payload.sessionId(), payload.action(), payload.slot(), payload.secondary());
TradeManager.get(player.server).handleAction(player, payload.sessionId(), payload.action(), payload.slot(), payload.amount());
}
}

View File

@@ -48,7 +48,7 @@ public class DebugTradeSession {
PacketDistributor.sendToPlayer(player, new TradeClosePayload(id, reason));
}
public boolean addFromInventory(int inventorySlot, boolean singleItem) {
public boolean addFromInventory(int inventorySlot, int amount) {
if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) {
return false;
}
@@ -59,12 +59,13 @@ public class DebugTradeSession {
return false;
}
ItemStack moving = sourceStack.copyWithCount(singleItem ? 1 : sourceStack.getCount());
int moveAmount = Math.max(1, Math.min(amount, sourceStack.getCount()));
ItemStack moving = sourceStack.copyWithCount(moveAmount);
if (!insertStack(selfOffer, moving)) {
return false;
}
sourceStack.shrink(singleItem ? 1 : sourceStack.getCount());
sourceStack.shrink(moveAmount);
if (sourceStack.isEmpty()) {
inventory.setItem(inventorySlot, ItemStack.EMPTY);
}
@@ -73,7 +74,7 @@ public class DebugTradeSession {
return true;
}
public boolean removeFromOffer(int offerSlot, boolean singleItem) {
public boolean removeFromOffer(int offerSlot, int amount) {
if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) {
return false;
}
@@ -84,7 +85,8 @@ public class DebugTradeSession {
}
List<ItemStack> workingInventory = inventorySnapshot();
ItemStack returned = offered.copyWithCount(singleItem ? 1 : offered.getCount());
int moveAmount = Math.max(1, Math.min(amount, offered.getCount()));
ItemStack returned = offered.copyWithCount(moveAmount);
if (!insertStack(workingInventory, returned)) {
return false;
}
@@ -95,9 +97,9 @@ public class DebugTradeSession {
}
inventory.setChanged();
if (singleItem && offered.getCount() > 1) {
if (moveAmount < offered.getCount()) {
ItemStack updated = offered.copy();
updated.shrink(1);
updated.shrink(moveAmount);
selfOffer.set(offerSlot, updated);
} else {
selfOffer.set(offerSlot, ItemStack.EMPTY);

View File

@@ -37,24 +37,24 @@ public class TradeManager {
return true;
}
public void handleAction(ServerPlayer player, UUID sessionId, TradeAction action, int slot, boolean secondary) {
public void handleAction(ServerPlayer player, UUID sessionId, TradeAction action, int slot, int amount) {
TradeSession session = sessionsById.get(sessionId);
if (session != null && session.involves(player)) {
handleRealAction(player, session, action, slot, secondary);
handleRealAction(player, session, action, slot, amount);
return;
}
DebugTradeSession debugSession = debugSessionsById.get(sessionId);
if (debugSession != null && debugSession.player() == player) {
handleDebugAction(debugSession, action, slot, secondary);
handleDebugAction(debugSession, action, slot, amount);
return;
}
}
private void handleRealAction(ServerPlayer player, TradeSession session, TradeAction action, int slot, boolean secondary) {
private void handleRealAction(ServerPlayer player, TradeSession session, TradeAction action, int slot, int amount) {
boolean changed = switch (action) {
case ADD_ITEM -> session.addFromInventory(player, slot, secondary);
case REMOVE_ITEM -> session.removeFromOffer(player, slot, secondary);
case ADD_ITEM -> session.addFromInventory(player, slot, amount);
case REMOVE_ITEM -> session.removeFromOffer(player, slot, amount);
case ACCEPT -> {
session.accept(player);
yield true;
@@ -87,10 +87,10 @@ public class TradeManager {
}
}
private void handleDebugAction(DebugTradeSession session, TradeAction action, int slot, boolean secondary) {
private void handleDebugAction(DebugTradeSession session, TradeAction action, int slot, int amount) {
boolean changed = switch (action) {
case ADD_ITEM -> session.addFromInventory(slot, secondary);
case REMOVE_ITEM -> session.removeFromOffer(slot, secondary);
case ADD_ITEM -> session.addFromInventory(slot, amount);
case REMOVE_ITEM -> session.removeFromOffer(slot, amount);
case ACCEPT -> {
session.acceptSelf();
yield true;

View File

@@ -59,7 +59,7 @@ public class TradeSession {
PacketDistributor.sendToPlayer(second, new TradeClosePayload(id, reason));
}
public boolean addFromInventory(ServerPlayer player, int inventorySlot, boolean singleItem) {
public boolean addFromInventory(ServerPlayer player, int inventorySlot, int amount) {
if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) {
return false;
}
@@ -71,12 +71,13 @@ public class TradeSession {
return false;
}
ItemStack moving = sourceStack.copyWithCount(singleItem ? 1 : sourceStack.getCount());
int moveAmount = Math.max(1, Math.min(amount, sourceStack.getCount()));
ItemStack moving = sourceStack.copyWithCount(moveAmount);
if (!insertStack(offer, moving)) {
return false;
}
sourceStack.shrink(singleItem ? 1 : sourceStack.getCount());
sourceStack.shrink(moveAmount);
if (sourceStack.isEmpty()) {
inventory.setItem(inventorySlot, ItemStack.EMPTY);
}
@@ -85,7 +86,7 @@ public class TradeSession {
return true;
}
public boolean removeFromOffer(ServerPlayer player, int offerSlot, boolean singleItem) {
public boolean removeFromOffer(ServerPlayer player, int offerSlot, int amount) {
if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) {
return false;
}
@@ -98,15 +99,16 @@ public class TradeSession {
Inventory inventory = player.getInventory();
List<ItemStack> workingInventory = inventorySnapshot(player);
ItemStack returned = offered.copyWithCount(singleItem ? 1 : offered.getCount());
int moveAmount = Math.max(1, Math.min(amount, offered.getCount()));
ItemStack returned = offered.copyWithCount(moveAmount);
if (!insertStack(workingInventory, returned)) {
return false;
}
applyInventory(player, workingInventory);
if (singleItem && offered.getCount() > 1) {
if (moveAmount < offered.getCount()) {
ItemStack updated = offered.copy();
updated.shrink(1);
updated.shrink(moveAmount);
offer.set(offerSlot, updated);
} else {
offer.set(offerSlot, ItemStack.EMPTY);