--- a/net/minecraft/world/entity/npc/AbstractVillager.java
+++ b/net/minecraft/world/entity/npc/AbstractVillager.java
@@ -42,7 +_,7 @@
     private Player tradingPlayer;
     @Nullable
     protected MerchantOffers offers;
-    private final SimpleContainer inventory = new SimpleContainer(8);
+    private final SimpleContainer inventory = new SimpleContainer(8, (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity()); // CraftBukkit - add argument
 
     public AbstractVillager(EntityType<? extends AbstractVillager> entityType, Level level) {
         super(entityType, level);
@@ -95,6 +_,20 @@
         return this.tradingPlayer != null;
     }
 
+    // CraftBukkit start
+    @Override
+    public org.bukkit.craftbukkit.inventory.CraftMerchant getCraftMerchant() {
+        return (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity();
+    }
+    // CraftBukkit end
+
+    // Paper start - Villager#resetOffers
+    public void resetOffers() {
+        this.offers = new MerchantOffers();
+        this.updateTrades();
+    }
+    // Paper end - Villager#resetOffers
+
     @Override
     public MerchantOffers getOffers() {
         if (this.level().isClientSide()) {
@@ -117,11 +_,24 @@
     public void overrideXp(int xp) {
     }
 
+    // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent
+    @Override
+    public void processTrade(MerchantOffer offer, @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()) {
+            offer.increaseUses();
+        }
+        if (event == null || event.isRewardingExp()) {
+            this.rewardTradeXp(offer);
+        }
+        this.notifyTrade(offer);
+    }
+    // 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());
         }
@@ -226,7 +_,20 @@
         while (i < maxNumbers && !list.isEmpty()) {
             MerchantOffer offer = list.remove(this.random.nextInt(list.size())).getOffer(this, this.random);
             if (offer != null) {
-                givenMerchantOffers.add(offer);
+                // CraftBukkit start
+                org.bukkit.event.entity.VillagerAcquireTradeEvent event = new org.bukkit.event.entity.VillagerAcquireTradeEvent((org.bukkit.entity.AbstractVillager) this.getBukkitEntity(), offer.asBukkit());
+                // Suppress during worldgen
+                if (this.valid) {
+                    event.callEvent();
+                }
+                if (!event.isCancelled()) {
+                    // Paper start - Fix crash from invalid ingredient list
+                    final org.bukkit.craftbukkit.inventory.CraftMerchantRecipe craftMerchantRecipe = org.bukkit.craftbukkit.inventory.CraftMerchantRecipe.fromBukkit(event.getRecipe());
+                    if (craftMerchantRecipe.getIngredients().isEmpty()) return;
+                    givenMerchantOffers.add(craftMerchantRecipe.toMinecraft());
+                    // Paper end - Fix crash from invalid ingredient list
+                }
+                // CraftBukkit end
                 i++;
             }
         }
