From 2dfdfb19d1aa7fd084cb7b9250ad35868fd1c8e4 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 18:15:56 -0500 Subject: [PATCH] Client-side trade validation and stock update Perform more complete client-side checks before predicting currency deductions in GuiNPCTrader.handleMouseClick. The method now verifies that the item can be given to the player (canGivePlayerClient), that the slot is enabled, and that stock is available, mirroring server-side checks. When a client-side purchase is accepted, availableStock is decremented if applicable. Added a private canGivePlayerClient(ItemStack) helper to check cursor-stack compatibility and max stack size. These changes prevent the UI from showing incorrect balances for trades the server would reject. --- .../npcs/client/gui/player/GuiNPCTrader.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index 55f88ac8c..49edf72b6 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -337,18 +337,45 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { protected void handleMouseClick(Slot slot, int slotId, int mouseButton, int modifier) { // Client-side balance prediction: immediately subtract currency cost on click // so the displayed balance updates instantly without waiting for server response. + // Must mirror ALL server-side checks in ContainerNPCTrader.slotClick() to avoid + // predicting a deduction for trades the server will reject. if (slotId >= 0 && slotId < 18 && mouseButton == 0) { long cost = currencyCost[slotId]; if (cost > 0 && playerBalance >= cost) { ItemStack sold = role.inventorySold.items.get(slotId); - if (sold != null && container.canBuy(slotId, player)) { + if (sold != null + && canGivePlayerClient(sold) + && container.isSlotEnabled(slotId, player) + && (!stockEnabled || availableStock[slotId] > 0) + && container.canBuy(slotId, player)) { playerBalance -= cost; + if (stockEnabled && availableStock[slotId] < Integer.MAX_VALUE) { + availableStock[slotId]--; + } } } } super.handleMouseClick(slot, slotId, mouseButton, modifier); } + /** + * Client-side mirror of ContainerNPCTrader.canGivePlayer. + * Checks if the item can be added to the player's cursor stack. + */ + private boolean canGivePlayerClient(ItemStack item) { + ItemStack held = player.inventory.getItemStack(); + if (held == null) { + return true; + } + if (NoppesUtilPlayer.compareItems(held, item, false, false)) { + int k1 = item.stackSize; + if (k1 > 0 && k1 + held.stackSize <= held.getMaxStackSize()) { + return true; + } + } + return false; + } + @Override public void save() { }