From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Thu, 2 Jul 2020 16:12:10 -0700
Subject: [PATCH] Add PlayerTradeEvent and PlayerPurchaseEvent

Co-authored-by: Alexander <protonull@protonmail.com>

diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
index 6e29b5729b8184d13065c9c4e18f6e450c9d88a6..d323cf157f2a910916baa9ce3f7e5bc81648c47d 100644
--- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
+++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java
@@ -139,11 +139,24 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa
     @Override
     public void overrideXp(int experience) {}
 
+    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+    @Override
+    public void processTrade(MerchantOffer recipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent
+        if (event == null || event.willIncreaseTradeUses()) {
+            recipe.increaseUses();
+        }
+        if (event == null || event.isRewardingExp()) {
+            this.rewardTradeXp(recipe);
+        }
+        this.notifyTrade(recipe);
+    }
+    // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
+
     @Override
     public void notifyTrade(MerchantOffer offer) {
-        offer.increaseUses();
+        // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
         this.ambientSoundTime = -this.getAmbientSoundInterval();
-        this.rewardTradeXp(offer);
+        // this.rewardTradeXp(offer); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
         if (this.tradingPlayer instanceof ServerPlayer) {
             CriteriaTriggers.TRADE.trigger((ServerPlayer) this.tradingPlayer, this, offer.getResult());
         }
diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java
index 4c7e91977fa590abfe7eb3704d8008ed6d4e3ab3..32910f677b0522ac8ec513fa0d00b714b52cfae4 100644
--- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java
+++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java
@@ -768,6 +768,14 @@ public abstract class AbstractContainerMenu {
     public abstract boolean stillValid(Player player);
 
     protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast) {
+        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+        return this.moveItemStackTo(stack, startIndex, endIndex, fromLast, false);
+    }
+    protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean fromLast, boolean isCheck) {
+        if (isCheck) {
+            stack = stack.copy();
+        }
+        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
         boolean flag1 = false;
         int k = startIndex;
 
@@ -791,6 +799,11 @@ public abstract class AbstractContainerMenu {
 
                 slot = (Slot) this.slots.get(k);
                 itemstack1 = slot.getItem();
+                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; clone if only a check
+                if (isCheck) {
+                    itemstack1 = itemstack1.copy();
+                }
+                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
                 if (!itemstack1.isEmpty() && ItemStack.isSameItemSameComponents(stack, itemstack1)) {
                     l = itemstack1.getCount() + stack.getCount();
                     int i1 = slot.getMaxStackSize(itemstack1);
@@ -798,12 +811,16 @@ public abstract class AbstractContainerMenu {
                     if (l <= i1) {
                         stack.setCount(0);
                         itemstack1.setCount(l);
+                        if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         slot.setChanged();
+                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         flag1 = true;
                     } else if (itemstack1.getCount() < i1) {
                         stack.shrink(i1 - itemstack1.getCount());
                         itemstack1.setCount(i1);
+                        if (!isCheck) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         slot.setChanged();
+                        } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                         flag1 = true;
                     }
                 }
@@ -834,10 +851,21 @@ public abstract class AbstractContainerMenu {
 
                 slot = (Slot) this.slots.get(k);
                 itemstack1 = slot.getItem();
+                // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+                if (isCheck) {
+                    itemstack1 = itemstack1.copy();
+                }
+                // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
                 if (itemstack1.isEmpty() && slot.mayPlace(stack)) {
                     l = slot.getMaxStackSize(stack);
+                    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+                    if (isCheck) {
+                        stack.shrink(Math.min(stack.getCount(), l));
+                    } else {
+                    // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
                     slot.setByPlayer(stack.split(Math.min(stack.getCount(), l)));
                     slot.setChanged();
+                    } // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                     flag1 = true;
                     break;
                 }
diff --git a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java
index ecefd4075c097e2118ec23e87baf36465c40f85f..2992e86f5f83431e230162380b33721df785ba91 100644
--- a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java
+++ b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java
@@ -135,12 +135,12 @@ public class MerchantMenu extends AbstractContainerMenu {
 
             itemstack = itemstack1.copy();
             if (slot == 2) {
-                if (!this.moveItemStackTo(itemstack1, 3, 39, true)) {
+                if (!this.moveItemStackTo(itemstack1, 3, 39, true, true)) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                     return ItemStack.EMPTY;
                 }
 
-                slot1.onQuickCraft(itemstack1, itemstack);
-                this.playTradeSound();
+                //  slot1.onQuickCraft(itemstack1, itemstack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved to after the non-check moveItemStackTo call
+                // this.playTradeSound();
             } else if (slot != 0 && slot != 1) {
                 if (slot >= 3 && slot < 30) {
                     if (!this.moveItemStackTo(itemstack1, 30, 39, false)) {
@@ -153,6 +153,7 @@ public class MerchantMenu extends AbstractContainerMenu {
                 return ItemStack.EMPTY;
             }
 
+            if (slot != 2) { // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; moved down for slot 2
             if (itemstack1.isEmpty()) {
                 slot1.setByPlayer(ItemStack.EMPTY);
             } else {
@@ -164,6 +165,21 @@ public class MerchantMenu extends AbstractContainerMenu {
             }
 
             slot1.onTake(player, itemstack1);
+            } // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent; handle slot 2
+            if (slot == 2) { // is merchant result slot
+                slot1.onTake(player, itemstack1);
+                if (itemstack1.isEmpty()) {
+                    slot1.set(ItemStack.EMPTY);
+                    return ItemStack.EMPTY;
+                }
+
+                this.moveItemStackTo(itemstack1, 3, 39, true, false); // This should always succeed because it's checked above
+
+                slot1.onQuickCraft(itemstack1, itemstack);
+                this.playTradeSound();
+                slot1.set(ItemStack.EMPTY); // itemstack1 should ALWAYS be empty
+            }
+            // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
         }
 
         return itemstack;
diff --git a/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java b/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java
index 26227033613a641a9d5a6f29356b19e54753b3f1..c2376538215604210d08acd33e8e5fdc8a35c7d3 100644
--- a/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java
+++ b/src/main/java/net/minecraft/world/inventory/MerchantResultSlot.java
@@ -47,13 +47,32 @@ public class MerchantResultSlot extends Slot {
 
     @Override
     public void onTake(Player player, ItemStack stack) {
-        this.checkTakeAchievements(stack);
+        // this.checkTakeAchievements(stack); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; move to after event is called and not cancelled
         MerchantOffer merchantOffer = this.slots.getActiveOffer();
+        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+        io.papermc.paper.event.player.PlayerPurchaseEvent event = null;
+        if (merchantOffer != null && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
+            if (this.merchant instanceof net.minecraft.world.entity.npc.AbstractVillager abstractVillager) {
+                event = new io.papermc.paper.event.player.PlayerTradeEvent(serverPlayer.getBukkitEntity(), (org.bukkit.entity.AbstractVillager) abstractVillager.getBukkitEntity(), merchantOffer.asBukkit(), true, true);
+            } else if (this.merchant instanceof org.bukkit.craftbukkit.inventory.CraftMerchantCustom.MinecraftMerchant) {
+                event = new io.papermc.paper.event.player.PlayerPurchaseEvent(serverPlayer.getBukkitEntity(), merchantOffer.asBukkit(), false, true);
+            }
+            if (event != null) {
+                if (!event.callEvent()) {
+                    stack.setCount(0);
+                    event.getPlayer().updateInventory();
+                    return;
+                }
+                merchantOffer = org.bukkit.craftbukkit.inventory.CraftMerchantRecipe.fromBukkit(event.getTrade()).toMinecraft();
+            }
+        }
+        this.checkTakeAchievements(stack);
+        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
         if (merchantOffer != null) {
             ItemStack itemStack = this.slots.getItem(0);
             ItemStack itemStack2 = this.slots.getItem(1);
             if (merchantOffer.take(itemStack, itemStack2) || merchantOffer.take(itemStack2, itemStack)) {
-                this.merchant.notifyTrade(merchantOffer);
+                this.merchant.processTrade(merchantOffer, event); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent
                 player.awardStat(Stats.TRADED_WITH_VILLAGER);
                 this.slots.setItem(0, itemStack);
                 this.slots.setItem(1, itemStack2);
diff --git a/src/main/java/net/minecraft/world/item/trading/Merchant.java b/src/main/java/net/minecraft/world/item/trading/Merchant.java
index 5a350948a4735902f5c612592bc9d100445a0c8a..716b30dcd7e63c66736c448dd136c9f74dc7fe43 100644
--- a/src/main/java/net/minecraft/world/item/trading/Merchant.java
+++ b/src/main/java/net/minecraft/world/item/trading/Merchant.java
@@ -20,6 +20,7 @@ public interface Merchant {
 
     void overrideOffers(MerchantOffers offers);
 
+    default void processTrade(MerchantOffer merchantRecipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { this.notifyTrade(merchantRecipe); } // Paper
     void notifyTrade(MerchantOffer offer);
 
     void notifyTradeUpdated(ItemStack stack);
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java
index adf22ce4f0bcd3bd57dc2030c6c92d3df96566e3..e33ddcd967a427abfda9e6692338da4996a81c6c 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMerchantCustom.java
@@ -74,10 +74,25 @@ public class CraftMerchantCustom extends CraftMerchant {
             return this.trades;
         }
 
+        // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+        @Override
+        public void processTrade(MerchantOffer merchantRecipe, @javax.annotation.Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent
+            /** Based on {@link net.minecraft.world.entity.npc.AbstractVillager#processTrade(MerchantOffer, io.papermc.paper.event.player.PlayerPurchaseEvent)} */
+            if (getTradingPlayer() instanceof net.minecraft.server.level.ServerPlayer) {
+                if (event == null || event.willIncreaseTradeUses()) {
+                    merchantRecipe.increaseUses();
+                }
+                if (event == null || event.isRewardingExp()) {
+                    this.tradingPlayer.level().addFreshEntity(new net.minecraft.world.entity.ExperienceOrb(this.tradingPlayer.level(), this.tradingPlayer.getX(), this.tradingPlayer.getY(), this.tradingPlayer.getZ(), merchantRecipe.getXp(), org.bukkit.entity.ExperienceOrb.SpawnReason.VILLAGER_TRADE, this.tradingPlayer, null));
+                }
+            }
+            this.notifyTrade(merchantRecipe);
+        }
+        // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
         @Override
         public void notifyTrade(MerchantOffer offer) {
             // increase recipe's uses
-            offer.increaseUses();
+            // offer.increaseUses(); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent; handled above in processTrade
         }
 
         @Override